2222
2323_start_time = time .monotonic ()
2424
25- # --- Hardcoded OTA manifest scaffolding (cut 1: validate protocol + transport) ---
26- # These point at a real staging bundle staged with ota-stage.mjs and uploaded to
27- # the CDN. They are filled in after that export; until then the client accepts the
28- # manifest framing but the bundle download 404s (which still proves the endpoint).
29- # Per-user selection, signing, and the release registry come later.
30- _OTA_BASE_URL = "https://couchers-dev-assets.s3.amazonaws.com/ota/prod-test"
31-
32- # Per-platform hardcoded update. `id` must differ from the build's embedded update
33- # id or the client no-ops; `runtimeVersion` is echoed from the request so the
34- # fingerprint always matches during validation.
35- _OTA_BUNDLES : dict [str , dict [str , Any ]] = {
36- "ios" : {
37- "id" : "00000000-0000-0000-0000-000000000000" ,
38- "launch_asset" : {"key" : "PLACEHOLDER" , "url" : f"{ _OTA_BASE_URL } /ios/bundle.hbc" },
39- "assets" : [],
40- },
41- "android" : {
42- "id" : "00000000-0000-0000-0000-000000000000" ,
43- "launch_asset" : {"key" : "PLACEHOLDER" , "url" : f"{ _OTA_BASE_URL } /android/bundle.hbc" },
44- "assets" : [],
45- },
46- }
47-
48- # Boundary baked into both the body and the content-type, matching ota-stage.mjs.
4925_OTA_BOUNDARY = "COUCHERS_OTA_BOUNDARY"
5026
5127
5228def _ota_multipart_body (field_name : str , content : dict [str , Any ]) -> bytes :
53- # Protocol-v1 multipart/mixed: the `manifest` (or `directive`) part + an
54- # `extensions` part, with the exact CRLF framing the dev client verified
55- # on-device (ota-serve.mjs). field_name is "manifest" for an update or
56- # "directive" for a noUpdateAvailable/rollBackToEmbedded directive.
29+ # Expo Updates protocol v1 multipart/mixed framing. field_name is "manifest" for
30+ # an update or "directive" for a noUpdateAvailable/rollBackToEmbedded directive.
5731 def part (name : str , body : str , content_type : str ) -> str :
5832 return (
5933 f"--{ _OTA_BOUNDARY } \r \n "
@@ -149,17 +123,15 @@ def header(name: str) -> str:
149123 value = context .headers .get (name , "" )
150124 return value .decode () if isinstance (value , bytes ) else value
151125
152- # The Expo Updates client requires these response headers or it rejects the
153- # manifest before fetching. Envoy forwards initial metadata as HTTP response
154- # headers (same path as set-cookie).
126+ # Expo rejects the manifest without these; Envoy forwards them as HTTP response headers.
155127 context .set_response_headers ([("expo-protocol-version" , "1" ), ("expo-sfv-version" , "0" )])
156128
157129 platform = header ("expo-platform" ) or "ios"
158130 runtime_version = header ("expo-runtime-version" )
159131
160- bundle = _OTA_BUNDLES .get (platform )
132+ bundles : dict [str , Any ] = context .get_object_value ("native_ota_bundles" , {})
133+ bundle = bundles .get (platform )
161134 if bundle is None or runtime_version == "" :
162- # Unknown platform or no fingerprint: tell the client to keep its bundle.
163135 directive = {"type" : "noUpdateAvailable" }
164136 return httpbody_pb2 .HttpBody (
165137 content_type = f"multipart/mixed; boundary={ _OTA_BOUNDARY } " ,
@@ -175,7 +147,7 @@ def header(name: str) -> str:
175147 "contentType" : "application/javascript" ,
176148 "url" : bundle ["launch_asset" ]["url" ],
177149 },
178- "assets" : bundle [ "assets" ] ,
150+ "assets" : bundle . get ( "assets" , []) ,
179151 "metadata" : {},
180152 "extra" : {"expoClient" : {}},
181153 }
0 commit comments