Skip to content

Commit 32caec1

Browse files
authored
fix(cloudflare): harden mirror routing and dispatch (#16)
1 parent d9b1b13 commit 32caec1

4 files changed

Lines changed: 81 additions & 38 deletions

File tree

cloudflare/download-router/src/index.js

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default {
55
async fetch(request, env) {
66
const url = new URL(request.url);
77

8-
if (!url.pathname.startsWith("/latest/")) {
8+
if (!isAllowedLatestPath(url.pathname)) {
99
return new Response("Not found", { status: 404 });
1010
}
1111

@@ -18,35 +18,65 @@ export default {
1818
);
1919

2020
if (secondaryCountryCodes.has(country.toUpperCase()) && hasSecondaryS3Config(env)) {
21-
const objectKey = objectKeyForPath(url.pathname, env.SECONDARY_S3_PREFIX || "");
22-
const downloadMetadata = downloadMetadataForPath(url.pathname);
23-
const signedUrl = await presignS3GetUrl({
24-
endpoint: env.SECONDARY_S3_ENDPOINT,
25-
bucket: env.SECONDARY_S3_BUCKET,
26-
key: objectKey,
27-
region: env.SECONDARY_S3_REGION || "auto",
28-
accessKeyId: env.SECONDARY_S3_ACCESS_KEY_ID,
29-
secretAccessKey: env.SECONDARY_S3_SECRET_ACCESS_KEY,
30-
expiresInSeconds: ttlSeconds(env.SECONDARY_S3_SIGNED_URL_TTL_SECONDS),
31-
responseHeaders: downloadMetadata
32-
? {
33-
"response-content-disposition": `attachment; filename="${downloadMetadata.filename}"`,
34-
"response-content-type": downloadMetadata.contentType,
35-
}
36-
: {},
37-
});
38-
39-
return redirect(signedUrl);
21+
try {
22+
const objectKey = objectKeyForPath(url.pathname, env.SECONDARY_S3_PREFIX || "");
23+
const downloadMetadata = downloadMetadataForPath(url.pathname);
24+
const signedUrl = await presignS3GetUrl({
25+
endpoint: env.SECONDARY_S3_ENDPOINT,
26+
bucket: env.SECONDARY_S3_BUCKET,
27+
key: objectKey,
28+
region: env.SECONDARY_S3_REGION || "auto",
29+
accessKeyId: env.SECONDARY_S3_ACCESS_KEY_ID,
30+
secretAccessKey: env.SECONDARY_S3_SECRET_ACCESS_KEY,
31+
expiresInSeconds: ttlSeconds(env.SECONDARY_S3_SIGNED_URL_TTL_SECONDS),
32+
responseHeaders: downloadMetadata
33+
? {
34+
"response-content-disposition": `attachment; filename="${downloadMetadata.filename}"`,
35+
"response-content-type": downloadMetadata.contentType,
36+
}
37+
: {},
38+
});
39+
40+
return redirect(signedUrl);
41+
} catch (error) {
42+
console.error(
43+
JSON.stringify({
44+
event: "secondary_s3_presign_failed",
45+
path: url.pathname,
46+
country,
47+
error: error instanceof Error ? error.message : String(error),
48+
}),
49+
);
50+
}
4051
}
4152

4253
if (!env.GLOBAL_MIRROR_BASE_URL) {
4354
return new Response("Missing GLOBAL_MIRROR_BASE_URL", { status: 500 });
4455
}
4556

46-
return redirect(withPathAndSearch(env.GLOBAL_MIRROR_BASE_URL, url.pathname, url.search).toString());
57+
return redirect(withPathAndSearch(env.GLOBAL_MIRROR_BASE_URL, url.pathname, url.search).toString(), {
58+
"X-Mirror-Fallback": "global",
59+
});
4760
},
4861
};
4962

63+
function isAllowedLatestPath(pathname) {
64+
const aliases = new Set([
65+
"/latest/win",
66+
"/latest/mac-arm64",
67+
"/latest/mac-intel",
68+
"/latest/checksums",
69+
"/latest/manifest",
70+
"/latest/appcast.xml",
71+
"/latest/appcast-x64.xml",
72+
]);
73+
if (aliases.has(pathname)) {
74+
return true;
75+
}
76+
77+
return /^\/latest\/mac\/(arm64|intel)\/[^/]+\.(zip|delta)$/.test(pathname);
78+
}
79+
5080
function hasSecondaryS3Config(env) {
5181
return Boolean(
5282
env.SECONDARY_S3_ENDPOINT &&
@@ -64,12 +94,13 @@ function ttlSeconds(value) {
6494
return Math.min(Math.max(parsed, 1), 604800);
6595
}
6696

67-
function redirect(location) {
97+
function redirect(location, extraHeaders = {}) {
6898
return new Response(null, {
6999
status: 302,
70100
headers: {
71101
Location: location,
72102
"Cache-Control": "private, no-store",
103+
...extraHeaders,
73104
},
74105
});
75106
}
@@ -96,7 +127,7 @@ function downloadMetadataForPath(pathname) {
96127
contentType: "application/x-apple-diskimage",
97128
},
98129
"mac-intel": {
99-
filename: "Codex-mac-intel.dmg",
130+
filename: "Codex-mac-x64.dmg",
100131
contentType: "application/x-apple-diskimage",
101132
},
102133
win: {

cloudflare/github-dispatcher/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# codex-app-mirror Cloudflare dispatcher
22

33
This Worker uses Cloudflare Cron Triggers as the primary 15-minute scheduler for
4-
`codex-app-mirror`. It does not mirror files itself. It only calls the GitHub
5-
Actions `workflow_dispatch` API for `.github/workflows/mirror.yml`.
4+
`codex-app-mirror` and `agents-cli-mirror`. It does not mirror files itself. It
5+
only calls the GitHub Actions `workflow_dispatch` API for the configured mirror
6+
workflows.
67

78
GitHub Actions `schedule` remains in the repository as a low-frequency fallback.
89

@@ -16,7 +17,7 @@ GitHub Actions release pipeline unchanged.
1617

1718
Create a fine-grained personal access token:
1819

19-
- Repository access: `Wangnov/codex-app-mirror` only
20+
- Repository access: `Wangnov/codex-app-mirror` and `Wangnov/agents-cli-mirror`
2021
- Repository permissions: `Actions` -> `Read and write`
2122
- Expiration: 90 or 180 days recommended
2223

cloudflare/github-dispatcher/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"scripts": {
66
"dev": "wrangler dev --test-scheduled",
77
"deploy": "wrangler deploy",
8-
"check": "wrangler check"
8+
"check": "node --check src/index.js && wrangler deploy --dry-run"
99
},
1010
"devDependencies": {
1111
"wrangler": "^4.0.0"

cloudflare/github-dispatcher/src/index.js

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,28 @@ async function dispatchWorkflow(controller, env) {
2121
const forceRelease = env.GITHUB_FORCE_RELEASE || "false";
2222

2323
const targets = [
24-
{ owner, repo, workflow, ref },
24+
{
25+
owner,
26+
repo,
27+
workflow,
28+
ref,
29+
inputs: {
30+
force_release: forceRelease,
31+
},
32+
required: true,
33+
},
2534
{
2635
owner: "Wangnov",
2736
repo: "agents-cli-mirror",
2837
workflow: "mirror.yml",
2938
ref: "main",
39+
inputs: {},
40+
required: true,
3041
},
3142
];
3243

3344
const settled = await Promise.allSettled(
34-
targets.map((target) => dispatchWorkflowTarget(controller, env, target, forceRelease)),
45+
targets.map((target) => dispatchWorkflowTarget(controller, env, target)),
3546
);
3647

3748
const results = settled.map((entry, index) => {
@@ -44,7 +55,6 @@ async function dispatchWorkflow(controller, env) {
4455
event: "github_workflow_dispatch",
4556
cron: controller.cron,
4657
...targets[index],
47-
force_release: forceRelease,
4858
ok: false,
4959
error: entry.reason.message,
5060
at: new Date().toISOString(),
@@ -54,19 +64,21 @@ async function dispatchWorkflow(controller, env) {
5464

5565
const succeeded = results.filter((result) => result.ok).length;
5666
const failed = results.length - succeeded;
67+
const failedRequired = results.filter((result) => result.required && !result.ok).length;
5768
const result = {
5869
event: "github_workflow_dispatch_batch",
5970
cron: controller.cron,
60-
ok: succeeded > 0,
71+
ok: failedRequired === 0,
6172
succeeded,
6273
failed,
74+
failed_required: failedRequired,
6375
targets: results,
6476
at: new Date().toISOString(),
6577
};
6678

6779
if (!result.ok) {
6880
console.error(JSON.stringify(result));
69-
throw new Error("All GitHub workflow dispatches failed.");
81+
throw new Error("One or more required GitHub workflow dispatches failed.");
7082
}
7183

7284
if (failed > 0) {
@@ -76,8 +88,8 @@ async function dispatchWorkflow(controller, env) {
7688
return result;
7789
}
7890

79-
async function dispatchWorkflowTarget(controller, env, target, forceRelease) {
80-
const { owner, repo, workflow, ref } = target;
91+
async function dispatchWorkflowTarget(controller, env, target) {
92+
const { owner, repo, workflow, ref, inputs = {}, required = true } = target;
8193
const url = new URL(
8294
`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/actions/workflows/${encodeURIComponent(workflow)}/dispatches`,
8395
);
@@ -93,9 +105,7 @@ async function dispatchWorkflowTarget(controller, env, target, forceRelease) {
93105
},
94106
body: JSON.stringify({
95107
ref,
96-
inputs: {
97-
force_release: forceRelease,
98-
},
108+
inputs,
99109
}),
100110
});
101111

@@ -106,7 +116,8 @@ async function dispatchWorkflowTarget(controller, env, target, forceRelease) {
106116
repo,
107117
workflow,
108118
ref,
109-
force_release: forceRelease,
119+
inputs,
120+
required,
110121
status: response.status,
111122
ok: response.ok,
112123
at: new Date().toISOString(),

0 commit comments

Comments
 (0)