nandi/frqpublic Fork 0
015500d33df581fb4cdcb21c053865b3dfa47e91
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

serve.py · 119 lines · 5.5 KBPython Blame HistoryRaw
A third target, and the seam that was already waiting for it f54ca45 nandi 2d ago1"""The built web app, served from the volume the container built it into.
2
3Not part of `container.py`, and not a key in `container.toml`, because it is
4the other half of a split the build already makes: the Sandbox compiles into
5`/devshell/frq-flutter-web` and exits, and what it leaves behind is a
6directory of static files that outlives it. A Function mounting the same
7volume can hand those out without rebuilding anything, and can scale to zero
8between readers -- which a Sandbox holding a tunnel open cannot.
9
10 modal serve .modal/flutter-web/serve.py # while editing, auto-reloads
11 modal deploy .modal/flutter-web/serve.py # a URL that stays
12
13Deliberately NOT built on the container's own image. That image carries the
14repo and a few gigabytes of devShell closure, all of it for a compile that
15has already happened somewhere else; the bytes this serves come off the
16volume, so the image needs a python and nothing more. Cold starts are the
17difference.
18"""
19
20import json
21import subprocess
22
23import modal
24
25# The same volume the container writes to, named the same way. `from_name` is
26# lazy, so naming it here costs nothing until a container actually mounts it.
27DEVSHELL = modal.Volume.from_name("devshell", create_if_missing=True)
28
29# Where `just flutter-web` leaves its output, under this devShell's own
30# directory on the shared volume -- the same path container.toml builds in.
31WEB_ROOT = "/devshell/frq-flutter-web/flutter/build/web"
32
33PORT = 8080
34
35# This app's own origin, and the one thing here that is not derivable: an
36# OAuth client_id in AT Protocol *is* the URL its metadata is served from, so
37# the document has to name the origin it will be fetched from. Modal's
38# deployed URL is stable for a given app name, which is what makes that safe
39# to write down.
40ORIGIN = "https://codegod100--frq-flutter-web-serve-web.modal.run"
41
42# The OAuth client identity, served at `/client-metadata.json`.
43#
44# This is what makes Bluesky sign-in possible from this origin at all. freeq's
45# auth broker will only redirect to hosts on its own allowlist, and this one is
46# not among them -- but a client that runs the OAuth flow *itself* is not asking
47# the broker for anything. An AT Protocol authorization server fetches this
48# document from the client_id URL and takes it as the authority on where a code
49# may be sent, so the allowlist that matters is the `redirect_uris` below, which
50# we publish.
51#
52# A public client: `token_endpoint_auth_method: none` and no secret, because a
53# page in a browser can keep none. What stands in for one is DPoP -- every token
54# is bound to a key the client proves it holds, which is also exactly what
55# freeq's SASL `pds-oauth` method verifies.
56CLIENT_METADATA = {
57 "client_id": f"{ORIGIN}/client-metadata.json",
58 "client_name": "frq",
59 "client_uri": f"{ORIGIN}/",
60 "redirect_uris": [f"{ORIGIN}/"],
61 "grant_types": ["authorization_code", "refresh_token"],
62 "response_types": ["code"],
63 # `atproto` is the identity scope freeq needs; `transition:generic` is what
64 # a PDS still wants for ordinary reads and writes.
65 "scope": "atproto transition:generic",
66 "token_endpoint_auth_method": "none",
67 "application_type": "web",
68 "dpop_bound_access_tokens": True,
69}
70
71app = modal.App("frq-flutter-web-serve")
72
73image = modal.Image.debian_slim(python_version="3.12")
74
75
76@app.function(
77 image=image,
78 volumes={"/devshell": DEVSHELL},
79 # Nothing here holds state between requests, and a reader who wanders off
80 # should stop costing anything -- so let it go to zero quickly rather than
81 # keeping a container warm for a static directory.
82 scaledown_window=60,
83)
84@modal.web_server(PORT, startup_timeout=30)
85def web():
86 # A Volume mount is a snapshot taken when the container starts, so a
87 # container that outlives a build would keep serving the old one. Reload
88 # first and the newest committed build is what gets served -- which is the
89 # whole point of the split: rebuild in the Sandbox, and the next cold
90 # start here picks it up with nothing redeployed.
91 DEVSHELL.reload()
92
93 # The client metadata is written beside the bundle rather than served by a
94 # route of its own: `http.server` has no routing table, and one file on
95 # disk is less machinery than a handler subclass. Written at start-up and
96 # not baked into the build, because it names the deployed origin, which is
97 # a property of this Function and not of the ClojureDart.
98 #
99 # The volume is shared and durable, so this also survives for the next
100 # cold start; rewriting it every time is what keeps ORIGIN and the file in
101 # step when one of them changes.
102 with open(f"{WEB_ROOT}/client-metadata.json", "w") as f:
103 json.dump(CLIENT_METADATA, f, indent=2)
104 DEVSHELL.commit()
105
106 # Popen and not run: `@modal.web_server` expects the body to *start* a
107 # server and return, so Modal can begin proxying. Blocking here would time
108 # out at startup_timeout with nothing ever listening.
109 #
110 # `python -m http.server` and not something with a routing table: Flutter
111 # writes a service worker and an index.html and asks for its own assets by
112 # exact path, so plain static serving is all the app wants on first load.
113 # What it does not give is a fallback for a deep link -- Flutter's default
114 # URL strategy is real paths, so /chats reaches the server rather than the
115 # router and gets a 404. That wants a real fallback-to-index server, and
116 # is worth having only once there is a router out there to reach.
117 subprocess.Popen(
118 ["python", "-m", "http.server", str(PORT), "--directory", WEB_ROOT],
119 )