working expo updates on iOS (broken on Android for some reason)
diff --git a/package-lock.json b/package-lock.json
index 402b3e2..630a315 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@sanity/client": "^6.6.0",
"domutils": "^3.1.0",
"htmlparser2": "^9.0.0",
+ "mime": "^4.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
@@ -32,6 +33,18 @@
"mime": "^3.0.0"
}
},
+ "node_modules/@cloudflare/kv-asset-handler/node_modules/mime": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+ "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
+ "dev": true,
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/@cloudflare/workerd-darwin-64": {
"version": "1.20231010.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20231010.0.tgz",
@@ -1126,15 +1139,17 @@
}
},
"node_modules/mime": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
- "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
- "dev": true,
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.1.tgz",
+ "integrity": "sha512-5lZ5tyrIfliMXzFtkYyekWbtRXObT9OWa8IwQ5uxTBDHucNNwniRqo0yInflj+iYi5CBa6qxadGzGarDfuEOxA==",
+ "funding": [
+ "https://github.com/sponsors/broofa"
+ ],
"bin": {
- "mime": "cli.js"
+ "mime": "bin/cli.js"
},
"engines": {
- "node": ">=10.0.0"
+ "node": ">=16"
}
},
"node_modules/mimic-response": {
@@ -1625,6 +1640,14 @@
"dev": true,
"requires": {
"mime": "^3.0.0"
+ },
+ "dependencies": {
+ "mime": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+ "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
+ "dev": true
+ }
}
},
"@cloudflare/workerd-darwin-64": {
@@ -2302,10 +2325,9 @@
}
},
"mime": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
- "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
- "dev": true
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.1.tgz",
+ "integrity": "sha512-5lZ5tyrIfliMXzFtkYyekWbtRXObT9OWa8IwQ5uxTBDHucNNwniRqo0yInflj+iYi5CBa6qxadGzGarDfuEOxA=="
},
"mimic-response": {
"version": "3.1.0",
diff --git a/package.json b/package.json
index c3c394f..fc13bec 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"@sanity/client": "^6.6.0",
"domutils": "^3.1.0",
"htmlparser2": "^9.0.0",
+ "mime": "^4.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
diff --git a/src/deployments/expo.ts b/src/deployments/expo.ts
index 82df7b9..0f83d0f 100644
--- a/src/deployments/expo.ts
+++ b/src/deployments/expo.ts
@@ -1,19 +1,30 @@
// Aero
// Copyright (c) jottocraft 2023
+import mime from "mime";
+import { createClient } from "@sanity/client";
+
import { isSafe } from "../util/isSafe";
import { serveR2 } from "../util/r2";
export function expoRouter(request: Request, env: Env, ctx: AeroCtx, product: string, pathname: string): Promise<Response> {
- const [, resource, key] = pathname.match(/\/(.+?)(?:$|\/(.*?)(?:$|\/))/) ?? [];
+ const [, resource, filename] = pathname.match(/\/(.+?)(?:$|\/(.*?)(?:$|\/))/) ?? [];
switch (resource) {
case "protocol1": return serveExpoProtocol1(request, env, ctx, product, pathname);
- case "assets": return serveExpoAsset(request, env, ctx, product, key);
+ case "assets": return serveExpoAsset(request, env, ctx, product, filename);
default: return serveExpoManifest(request, env, ctx, product, resource);
}
}
+const EXPO_CHANNEL_NAMES = {
+ staging: 4,
+ techpreview: 3,
+ alpha: 2,
+ beta: 1,
+ production: 0
+};
+
async function serveExpoProtocol1(request: Request, env: Env, ctx: AeroCtx, product: string, pathname: string) {
const url = new URL(request.url);
@@ -21,15 +32,37 @@
const runtime = request.headers.get("Expo-Runtime-Version") ?? url.searchParams.get("runtime-version");
const channel = request.headers.get("Expo-Channel-Name") ?? url.searchParams.get("channel-name");
- return new Response(null, { status: 501 });
+ if ((platform !== "android" && platform !== "ios") || !runtime || !isSafe(runtime) || !channel || !Object.keys(EXPO_CHANNEL_NAMES).includes(channel)) return env.HTTP_STATUS_PAGES.fetch("https://http-status-pages.jottocraft.com/?status=400");
+
+ const client = createClient({
+ projectId: "xk715cpi",
+ dataset: "production",
+ useCdn: true,
+ apiVersion: "2023-09-20",
+ token: env.SANITY_PRODUCTS_TOKEN,
+ perspective: "published"
+ });
+
+ const manifest = await client.fetch<string | null>(`*[_type == "release" && channel <= $channel && count(artifacts[_type == "expo" && platform == $platform && runtime == $runtime]) == 1] {
+ "manifest": select(artifacts[_type == "expo" && platform == $platform && runtime == $runtime])[0].manifest,
+ version
+ } | order(version desc)[0].manifest`, {
+ platform,
+ runtime,
+ channel: EXPO_CHANNEL_NAMES[channel as keyof typeof EXPO_CHANNEL_NAMES]
+ });
+
+ if (!manifest) return env.HTTP_STATUS_PAGES.fetch("https://http-status-pages.jottocraft.com/?status=404");
+
+ return serveExpoManifest(request, env, ctx, product, manifest, channel);
}
-async function serveExpoAsset(request: Request, env: Env, ctx: AeroCtx, product: string, key: string) {
- if (!isSafe(key)) return env.HTTP_STATUS_PAGES.fetch("https://http-status-pages.jottocraft.com/?status=400");
- return serveR2(request, env, `${product}/expo/${key}`);
+async function serveExpoAsset(request: Request, env: Env, ctx: AeroCtx, product: string, filename: string) {
+ if (!isSafe(filename)) return env.HTTP_STATUS_PAGES.fetch("https://http-status-pages.jottocraft.com/?status=400");
+ return serveR2(request, env, ctx, `${product}/expo/${filename}`, "public, max-age=31536000, immutable", mime.getType(filename.slice(filename.indexOf(".") + 1)) ?? "");
}
-async function serveExpoManifest(request: Request, env: Env, ctx: AeroCtx, product: string, manifestID: string) {
+async function serveExpoManifest(request: Request, env: Env, ctx: AeroCtx, product: string, manifestID: string, channel?: string) {
if (!isSafe(manifestID)) return env.HTTP_STATUS_PAGES.fetch("https://http-status-pages.jottocraft.com/?status=400");
const object = await env.ARTIFACTS.get(`${product}/expo/${manifestID}.json`);
@@ -37,12 +70,12 @@
const buildManifest = await object.json<any>();
// Add URLs to the manifest
- buildManifest.launchAsset.url = `https://aero.jottocraft.com/${product}/expo/assets/${buildManifest.launchAsset.key}`;
- buildManifest.assets.forEach((asset: any) => asset.url = `https://aero.jottocraft.com/${product}/expo/assets/${asset.key}`)
+ buildManifest.launchAsset.url = `https://aero.jottocraft.com/${product}/expo/assets/${buildManifest.launchAsset.key}.js`;
+ buildManifest.assets.forEach((asset: any) => asset.url = `https://aero.jottocraft.com/${product}/expo/assets/${asset.key}${asset.fileExtension}`);
const boundary = "jottocraft-Aero-Boundary-" + crypto.randomUUID();
- let multipartBody = [""];
+ let multipartBody = [];
multipartBody.push("--" + boundary);
multipartBody.push("Content-Disposition: inline; name=\"manifest\"");
multipartBody.push("Content-Type: application/json\n");
@@ -50,13 +83,19 @@
multipartBody.push(JSON.stringify(buildManifest));
multipartBody.push("--" + boundary + "--");
+ multipartBody.push("");
+
+ const headers = new Headers({
+ "Content-Type": "multipart/mixed; boundary=" + boundary,
+ "Cache-Control": "cache-control: private, max-age=0",
+ "Expo-Protocol-Version": "1",
+ "Expo-SFV-Version": "0"
+ });
+
+ if (channel) headers.set("Expo-Manifest-Filters", `channel="${channel}"`);
return new Response(multipartBody.join("\r\n"), {
status: 200,
- headers: {
- "Content-Type": "multipart/mixed; boundary=" + boundary,
- "Expo-Protocol-Version": "1",
- "Expo-SFV-Version": "0"
- }
+ headers
})
}
\ No newline at end of file
diff --git a/src/deployments/msix.tsx b/src/deployments/msix.tsx
index 36d7564..ce59c01 100644
--- a/src/deployments/msix.tsx
+++ b/src/deployments/msix.tsx
@@ -131,7 +131,7 @@
const deployment = release.deployments.find(deployment => deployment._type === "msix");
if (!deployment || (deployment._type !== "msix")) return env.HTTP_STATUS_PAGES.fetch("https://http-status-pages.jottocraft.com/?status=404");
- return serveR2(reqData.request, reqData.env, `${release.product.identity}/msix/${deployment.folder}/${release.product.identity}.msixbundle`);
+ return serveR2(reqData.request, reqData.env, reqData.ctx, `${release.product.identity}/msix/${deployment.folder}/${release.product.identity}.msixbundle`);
}
export async function serveDependency(reqData: MSIXRequestData) {
@@ -142,7 +142,7 @@
const deployment = release.deployments.find(deployment => deployment._type === "msix");
if (!deployment || (deployment._type !== "msix")) return env.HTTP_STATUS_PAGES.fetch("https://http-status-pages.jottocraft.com/?status=404");
- return serveR2(reqData.request, reqData.env, `${release.product.identity}/msix/${deployment.folder}/Dependencies/${arg}`);
+ return serveR2(reqData.request, reqData.env, reqData.ctx, `${release.product.identity}/msix/${deployment.folder}/Dependencies/${arg}`);
}
export async function serveMsAppInstaller(reqData: MSIXRequestData) {
diff --git a/src/index.ts b/src/index.ts
index 7d1999b..a113d28 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -27,9 +27,13 @@
}
})();
- const modifiable = new Response(response.body, response);
- if (headersPatch.current) applyModifications(modifiable.headers, headersPatch.current);
+ return response;
- return modifiable;
+ //const modifiable = new Response(response.body, response);
+ //if (headersPatch.current) applyModifications(modifiable.headers, headersPatch.current);
+//
+ //for (const [h, v] of modifiable.headers.entries()) console.log(h + ": " + v);
+//
+ //return modifiable;
}
};
diff --git a/src/util/r2.ts b/src/util/r2.ts
index ba4d432..a7ed19e 100644
--- a/src/util/r2.ts
+++ b/src/util/r2.ts
@@ -5,7 +5,7 @@
// hits R2 HARD with Range-d requests when installing (I'm talking 300+ REQUESTS PER MINUTE from a single client...)
// MAKE SURE RANGED REQUESTS WORK AND ARE EFFICIENT!!! OTHERWISE INFRA WILL NOT BE HAPPY!!!
-export async function serveR2(request: Request, env: Env, key: string) {
+export async function serveR2(request: Request, env: Env, ctx: ExecutionContext, key: string, cacheControl?: string, contentType?: string) {
if (request.method === "HEAD") {
const object = await env.ARTIFACTS.head(key);
if (object === null) return env.HTTP_STATUS_PAGES.fetch("https://http-status-pages.jottocraft.com/?status=404");
@@ -13,8 +13,10 @@
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set("Etag", object.httpEtag);
- headers.set("Content-Type", "application/octet-stream");
+ headers.set("Content-Type", contentType ?? "application/octet-stream");
+ headers.set("Cache-Control", (cacheControl ? cacheControl + ", " : "") + "no-transform");
headers.set("Accept-Ranges", "bytes");
+ headers.set("Content-Length", String(object.size));
return new Response(null, {
status: 200,
@@ -22,6 +24,8 @@
});
}
+ const hasRequestedRange = request.headers.get("Range") !== null;
+
const object = await env.ARTIFACTS.get(key, {
range: request.headers,
onlyIf: request.headers
@@ -32,18 +36,20 @@
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set("ETag", object.httpEtag);
- if (object.range) {
+ headers.set("Content-Type", contentType ?? "application/octet-stream");
+ headers.set("Cache-Control", (cacheControl ? cacheControl + ", " : "") + "no-transform");
+ headers.set("Accept-Ranges", "bytes");
+
+ if (hasRequestedRange && object.range) {
if (("offset" in object.range) || ("length" in object.range)) {
headers.set("Content-Range", `bytes ${object.range.offset ?? 0}-${object.range.length !== undefined ? (object.range.offset ?? 0) + (object.range.length - 1) : object.size - 1}/${object.size}`);
} else if ("suffix" in object.range) {
headers.set("Content-Range", `bytes ${object.size - object.range.suffix}-${object.size - 1}/${object.size}`);
}
}
- headers.set("Content-Type", "application/octet-stream");
- headers.set("Accept-Ranges", "bytes");
- return new Response(object.body ?? null, {
- status: object.body ? (request.headers.get("Range") !== null ? 206 : 200) : 304,
+ return new Response(object.body ? await object.arrayBuffer() : null, {
+ status: object.body ? (hasRequestedRange ? 206 : 200) : 304,
headers
});
}
\ No newline at end of file