codex-snapshots 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +101 -6
  2. package/bin/codex-snapshot.mjs +1 -6326
  3. package/deploy/aliyun/README.md +311 -0
  4. package/deploy/aliyun/backup-share-data.sh +109 -0
  5. package/deploy/aliyun/check-ecs-status.sh +149 -0
  6. package/deploy/aliyun/codex-snapshot-share.env.example +29 -0
  7. package/deploy/aliyun/codex-snapshot-share.service +26 -0
  8. package/deploy/aliyun/configure-github-pages-api.sh +141 -0
  9. package/deploy/aliyun/configure-local-publisher.sh +197 -0
  10. package/deploy/aliyun/deploy-to-ecs.sh +669 -0
  11. package/deploy/aliyun/deploy.env.example +52 -0
  12. package/deploy/aliyun/doctor.mjs +398 -0
  13. package/deploy/aliyun/install-share-api.sh +252 -0
  14. package/deploy/aliyun/install-system-deps.sh +84 -0
  15. package/deploy/aliyun/nginx-codex-snapshots.bootstrap.conf +34 -0
  16. package/deploy/aliyun/nginx-codex-snapshots.conf +52 -0
  17. package/deploy/aliyun/preflight.mjs +321 -0
  18. package/deploy/aliyun/restore-share-data.sh +141 -0
  19. package/deploy/aliyun/verify-public-share.mjs +404 -0
  20. package/dist/cli/codex-snapshot.mjs +2654 -0
  21. package/dist/core/privacy.js +81 -0
  22. package/dist/core/snapshot.js +1 -0
  23. package/dist/renderers/markdown.mjs +81 -0
  24. package/dist/renderers/transcript.js +195 -0
  25. package/dist/server/http.js +10 -0
  26. package/dist/server/local-security.js +66 -0
  27. package/dist/server/local-viewer-app.mjs +1670 -0
  28. package/dist/server/local-viewer.mjs +210 -0
  29. package/dist/server/share-api.mjs +1149 -0
  30. package/dist/server/share-store.js +136 -0
  31. package/dist/shared/sanitize.js +126 -0
  32. package/dist/shared/transcript.js +1 -0
  33. package/dist/sources/index.mjs +2 -0
  34. package/dist/sources/local-history.mjs +2221 -0
  35. package/package.json +42 -14
  36. package/scripts/build-site.mjs +71 -0
  37. package/scripts/launch-agent.mjs +19 -227
  38. package/scripts/serve-site.mjs +2 -2
  39. package/scripts/test-aliyun-deploy-config.sh +230 -0
  40. package/scripts/test-share-api.mjs +967 -0
  41. package/scripts/test-site-config.mjs +100 -0
  42. package/scripts/test-static-site.mjs +403 -0
  43. package/scripts/write-site-config.mjs +161 -0
  44. package/server/share-api.mjs +1 -771
  45. package/site/assets/config.js +3 -0
  46. package/site/assets/share.js +43 -106
  47. package/site/assets/site.css +3 -605
  48. package/site/assets/site.js +15 -92
  49. package/site/favicon.svg +7 -0
  50. package/site/index.html +3 -83
  51. package/site/share/index.html +3 -8
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "codex-snapshots-site-config-test-"));
11
+
12
+ try {
13
+ await assertWritesEmptyConfigWhenUnset();
14
+ await assertWritesNormalizedPublicApi();
15
+ await assertRejectsUnsafePublicApis();
16
+ await assertAllowLocalForLocalDevelopment();
17
+
18
+ console.log("✓ site config generation checks passed");
19
+ } finally {
20
+ await rm(tempDir, { recursive: true, force: true });
21
+ }
22
+
23
+ async function assertWritesEmptyConfigWhenUnset() {
24
+ const output = path.join(tempDir, "empty-config.js");
25
+ await runConfig(["--output", output], {});
26
+ const text = await readFile(output, "utf8");
27
+ assert(text.includes('"apiUrl": ""'), "empty config should write an empty apiUrl");
28
+ }
29
+
30
+ async function assertWritesNormalizedPublicApi() {
31
+ const output = path.join(tempDir, "public-config.js");
32
+ await runConfig(["--output", output, "--api-url", "https://snapshots.mycompany.dev///"], {});
33
+ const text = await readFile(output, "utf8");
34
+ assert(text.includes('"apiUrl": "https://snapshots.mycompany.dev"'), "public config should normalize trailing slashes");
35
+ }
36
+
37
+ async function assertRejectsUnsafePublicApis() {
38
+ const unsafeUrls = [
39
+ "http://127.0.0.1:8787",
40
+ "http://localhost:8787",
41
+ "https://snapshots.example.com",
42
+ "https://192.168.1.5",
43
+ "ftp://snapshots.mycompany.dev",
44
+ ];
45
+
46
+ for (const unsafeUrl of unsafeUrls) {
47
+ const output = path.join(tempDir, `unsafe-${unsafeUrls.indexOf(unsafeUrl)}.js`);
48
+ const result = await runConfig(["--output", output, "--api-url", unsafeUrl], {}, { expectFailure: true });
49
+ assert(result.stderr.includes("CODEX_SNAPSHOTS_PUBLIC_API_URL"), `unsafe URL should fail with config error: ${unsafeUrl}`);
50
+ }
51
+ }
52
+
53
+ async function assertAllowLocalForLocalDevelopment() {
54
+ const output = path.join(tempDir, "local-config.js");
55
+ await runConfig(["--output", output, "--api-url", "http://127.0.0.1:8787", "--allow-local"], {});
56
+ const text = await readFile(output, "utf8");
57
+ assert(text.includes('"apiUrl": "http://127.0.0.1:8787"'), "allow-local should permit local API config");
58
+ }
59
+
60
+ async function runConfig(args, env, { expectFailure = false } = {}) {
61
+ return await new Promise((resolve, reject) => {
62
+ const child = spawn(process.execPath, ["scripts/write-site-config.mjs", ...args], {
63
+ cwd: ROOT_DIR,
64
+ env: {
65
+ ...process.env,
66
+ CODEX_SNAPSHOTS_PUBLIC_API_URL: "",
67
+ SNAPSHOT_SHARE_PUBLIC_API_URL: "",
68
+ SNAPSHOT_SHARE_API_URL: "",
69
+ ...env,
70
+ },
71
+ stdio: ["ignore", "pipe", "pipe"],
72
+ });
73
+ let stdout = "";
74
+ let stderr = "";
75
+
76
+ child.stdout.setEncoding("utf8");
77
+ child.stderr.setEncoding("utf8");
78
+ child.stdout.on("data", (chunk) => {
79
+ stdout += chunk;
80
+ });
81
+ child.stderr.on("data", (chunk) => {
82
+ stderr += chunk;
83
+ });
84
+ child.once("error", reject);
85
+ child.once("exit", (code) => {
86
+ const result = { code, stdout, stderr };
87
+ if (expectFailure ? code !== 0 : code === 0) {
88
+ resolve(result);
89
+ return;
90
+ }
91
+ reject(new Error(`write-site-config ${args.join(" ")} exited ${code}\n${stdout}\n${stderr}`));
92
+ });
93
+ });
94
+ }
95
+
96
+ function assert(condition, message) {
97
+ if (!condition) {
98
+ throw new Error(message);
99
+ }
100
+ }
@@ -0,0 +1,403 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { JSDOM } from "jsdom";
7
+
8
+ const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
+ const PUBLIC_API_URL = "https://snapshots.example.test";
10
+
11
+ async function testHomepagePublicSessions() {
12
+ const requests = [];
13
+ const { document } = await runStaticPage("site/index.html", {
14
+ locationHref: "https://ffffhx.github.io/codex-snapshots/",
15
+ config: { apiUrl: PUBLIC_API_URL },
16
+ fetch: async (url, options = {}) => {
17
+ requests.push(String(url));
18
+ if (String(url) === "http://127.0.0.1:4321/") {
19
+ return jsonResponse({ ok: true });
20
+ }
21
+ if (String(url).startsWith(`${PUBLIC_API_URL}/api/auth/me`)) {
22
+ assert(options.credentials === "include", "homepage auth check should include credentials");
23
+ return jsonResponse({ configured: true, user: null, loginUrl: `${PUBLIC_API_URL}/api/auth/github/start` });
24
+ }
25
+ if (String(url) === `${PUBLIC_API_URL}/api/snapshots/health`) {
26
+ return jsonResponse({ ok: true, shares: 1 });
27
+ }
28
+ if (String(url) === `${PUBLIC_API_URL}/api/snapshots?limit=12`) {
29
+ return jsonResponse({
30
+ schemaVersion: 1,
31
+ shares: [
32
+ {
33
+ id: "snap_publicsession123456",
34
+ title: "Public Session from Aliyun",
35
+ engine: "codex",
36
+ engineLabel: "Codex",
37
+ createdAt: "2026-05-28T00:00:00.000Z",
38
+ redacted: true,
39
+ turnCount: 2,
40
+ url: "https://ffffhx.github.io/codex-snapshots/share/?id=snap_publicsession123456",
41
+ },
42
+ ],
43
+ count: 1,
44
+ total: 1,
45
+ limit: 12,
46
+ offset: 0,
47
+ });
48
+ }
49
+ throw new Error(`unexpected fetch: ${url}`);
50
+ },
51
+ });
52
+
53
+ await waitFor(() => document.querySelector(".public-session-card"));
54
+
55
+ const card = document.querySelector(".public-session-card");
56
+ const link = card?.querySelector(".public-session-link");
57
+ const title = card?.querySelector("h3");
58
+
59
+ assert(requests.includes(`${PUBLIC_API_URL}/api/snapshots?limit=12`), "homepage should fetch the configured public list API");
60
+ assert(!document.getElementById("share-form"), "homepage should not render the manual share opener form");
61
+ assert(
62
+ sectionOrder(document).indexOf("status-grid") > sectionOrder(document).indexOf("commands"),
63
+ "homepage should show the local viewer opener after install commands",
64
+ );
65
+ assert(card, "homepage should render a public session card");
66
+ assert(title?.textContent === "Public Session from Aliyun", "homepage should render public session title");
67
+ assert(link?.href.includes("/share/index.html?"), `homepage card should link to the static share page: ${link?.href}`);
68
+ assert(link?.href.includes(`api=${encodeURIComponent(PUBLIC_API_URL)}`), `homepage card should preserve the public API URL: ${link?.href}`);
69
+ assert(link?.target === "_blank", "homepage card should open in a new tab");
70
+ assert(link?.rel === "noopener noreferrer", "homepage card should protect the opener");
71
+ assert(!card?.querySelector(".public-session-delete"), "anonymous users should not see a delete action");
72
+ }
73
+
74
+ async function testHomepageDeletesOwnPublicSessionWithGithubLogin() {
75
+ const requests = [];
76
+ let deleted = false;
77
+ const { document, window } = await runStaticPage("site/index.html", {
78
+ locationHref: "https://ffffhx.github.io/codex-snapshots/",
79
+ config: { apiUrl: PUBLIC_API_URL },
80
+ confirm: () => true,
81
+ fetch: async (url, options = {}) => {
82
+ requests.push({
83
+ url: String(url),
84
+ method: String(options.method || "GET").toUpperCase(),
85
+ authorization: options.headers?.authorization || "",
86
+ credentials: options.credentials || "",
87
+ });
88
+ if (String(url) === "http://127.0.0.1:4321/") {
89
+ return jsonResponse({ ok: true });
90
+ }
91
+ if (String(url).startsWith(`${PUBLIC_API_URL}/api/auth/me`)) {
92
+ return jsonResponse({
93
+ configured: true,
94
+ user: {
95
+ id: "42",
96
+ login: "alice",
97
+ avatarUrl: "",
98
+ profileUrl: "https://github.com/alice",
99
+ isOwner: false,
100
+ },
101
+ loginUrl: `${PUBLIC_API_URL}/api/auth/github/start`,
102
+ });
103
+ }
104
+ if (String(url) === `${PUBLIC_API_URL}/api/snapshots/health`) {
105
+ return jsonResponse({ ok: true, shares: deleted ? 0 : 1 });
106
+ }
107
+ if (String(url) === `${PUBLIC_API_URL}/api/snapshots?limit=12`) {
108
+ return jsonResponse({
109
+ schemaVersion: 1,
110
+ shares: deleted
111
+ ? []
112
+ : [
113
+ {
114
+ id: "snap_publicsession123456",
115
+ title: "Public Session from Aliyun",
116
+ engine: "codex",
117
+ engineLabel: "Codex",
118
+ createdAt: "2026-05-28T00:00:00.000Z",
119
+ redacted: true,
120
+ turnCount: 2,
121
+ owner: {
122
+ id: "42",
123
+ login: "alice",
124
+ },
125
+ },
126
+ ],
127
+ count: deleted ? 0 : 1,
128
+ total: deleted ? 0 : 1,
129
+ limit: 12,
130
+ offset: 0,
131
+ });
132
+ }
133
+ if (String(url) === `${PUBLIC_API_URL}/api/snapshots/snap_publicsession123456` && options.method === "DELETE") {
134
+ assert(!options.headers?.authorization, "delete should not send the old shared bearer token");
135
+ assert(options.credentials === "include", "delete should send the GitHub session cookie");
136
+ deleted = true;
137
+ return jsonResponse({ ok: true, deleted: true, id: "snap_publicsession123456" });
138
+ }
139
+ throw new Error(`unexpected fetch: ${url}`);
140
+ },
141
+ });
142
+
143
+ await waitFor(() => document.querySelector(".public-session-card"));
144
+ await flushPromises(window);
145
+
146
+ document.querySelector(".public-session-delete")?.dispatchEvent(
147
+ new window.MouseEvent("click", {
148
+ bubbles: true,
149
+ cancelable: true,
150
+ }),
151
+ );
152
+
153
+ await waitFor(() => !document.querySelector(".public-session-card"));
154
+
155
+ assert(deleted, "homepage should call the delete endpoint");
156
+ assert(
157
+ requests.some((request) => request.method === "DELETE" && request.url.endsWith("/api/snapshots/snap_publicsession123456")),
158
+ "homepage should issue a DELETE request for the selected share",
159
+ );
160
+ assert(document.querySelector(".public-session-status")?.textContent === "已删除分享快照。", "homepage should show delete success");
161
+ assert(document.getElementById("public-sessions")?.textContent === "暂无公开 Session。", "homepage should remove the deleted share from the list");
162
+ }
163
+
164
+ async function testPublicHomepageWithoutConfiguredApiDoesNotFetchLoopback() {
165
+ const requests = [];
166
+ const { document } = await runStaticPage("site/index.html", {
167
+ locationHref: "https://ffffhx.github.io/codex-snapshots/",
168
+ config: {},
169
+ storage: {
170
+ "codex-snapshots.api": "http://127.0.0.1:8787",
171
+ },
172
+ fetch: async (url) => {
173
+ requests.push(String(url));
174
+ if (String(url) === "http://127.0.0.1:4321/") {
175
+ return jsonResponse({ ok: true });
176
+ }
177
+ throw new Error(`unexpected fetch: ${url}`);
178
+ },
179
+ });
180
+
181
+ await waitFor(() => document.getElementById("public-sessions")?.textContent === "公开分享 API 尚未配置。");
182
+
183
+ assert(!document.getElementById("share-form"), "public homepage should not render the manual share opener form");
184
+ assert(
185
+ document.getElementById("public-sessions")?.textContent === "公开分享 API 尚未配置。",
186
+ "public homepage should explain that the public API is not configured",
187
+ );
188
+ assert(
189
+ !requests.some((request) => request.includes("127.0.0.1:8787")),
190
+ "public homepage should not fetch the loopback share API when config is missing",
191
+ );
192
+ }
193
+
194
+ async function testLocalHomepageDefaultsToLocalApi() {
195
+ const requests = [];
196
+ const { document } = await runStaticPage("site/index.html", {
197
+ locationHref: "http://127.0.0.1:4322/",
198
+ config: {},
199
+ fetch: async (url) => {
200
+ requests.push(String(url));
201
+ if (String(url) === "http://127.0.0.1:4321/") {
202
+ return jsonResponse({ ok: true });
203
+ }
204
+ if (String(url).startsWith("http://127.0.0.1:8787/api/auth/me")) {
205
+ return jsonResponse({ configured: false, user: null, loginUrl: null });
206
+ }
207
+ if (String(url) === "http://127.0.0.1:8787/api/snapshots/health") {
208
+ return jsonResponse({ ok: true, shares: 0 });
209
+ }
210
+ if (String(url) === "http://127.0.0.1:8787/api/snapshots?limit=12") {
211
+ return jsonResponse({ schemaVersion: 1, shares: [], count: 0, total: 0, limit: 12, offset: 0 });
212
+ }
213
+ throw new Error(`unexpected fetch: ${url}`);
214
+ },
215
+ });
216
+
217
+ await waitFor(() => document.getElementById("public-sessions")?.textContent === "暂无公开 Session。");
218
+
219
+ assert(!document.getElementById("share-form"), "local homepage should not render the manual share opener form");
220
+ assert(requests.includes("http://127.0.0.1:8787/api/snapshots?limit=12"), "local homepage should load local public sessions");
221
+ }
222
+
223
+ async function testSharePageLoadsFromConfiguredApi() {
224
+ const requests = [];
225
+ const { document } = await runStaticPage("site/share/index.html", {
226
+ locationHref: "https://ffffhx.github.io/codex-snapshots/share/?id=snap_publicsession123456",
227
+ config: { apiUrl: PUBLIC_API_URL },
228
+ fetch: async (url) => {
229
+ requests.push(String(url));
230
+ if (String(url) === `${PUBLIC_API_URL}/api/snapshots/snap_publicsession123456`) {
231
+ return jsonResponse({
232
+ schemaVersion: 1,
233
+ share: {
234
+ id: "snap_publicsession123456",
235
+ title: "Public Session from Aliyun",
236
+ engineLabel: "Codex",
237
+ redacted: true,
238
+ turnCount: 2,
239
+ },
240
+ snapshot: {
241
+ title: "Public Session from Aliyun",
242
+ engineLabel: "Codex",
243
+ goalObjective: "Explain how the public share page loads data.",
244
+ redacted: true,
245
+ turns: [
246
+ {
247
+ role: "user",
248
+ text: "Can everyone view this session?",
249
+ },
250
+ {
251
+ role: "assistant",
252
+ text: "Yes, the static share page loaded it from the public API.",
253
+ images: [
254
+ {
255
+ src: "data:image/png;base64,iVBORw0KGgo=",
256
+ mimeType: "image/png",
257
+ size: "148 KB",
258
+ alt: "Screenshot",
259
+ },
260
+ ],
261
+ },
262
+ ],
263
+ },
264
+ });
265
+ }
266
+ throw new Error(`unexpected fetch: ${url}`);
267
+ },
268
+ });
269
+
270
+ await waitFor(() => document.getElementById("share-title")?.textContent === "Public Session from Aliyun");
271
+
272
+ assert(
273
+ requests.includes(`${PUBLIC_API_URL}/api/snapshots/snap_publicsession123456`),
274
+ "share page should fetch snapshot detail from the configured public API",
275
+ );
276
+ assert(document.getElementById("share-title")?.textContent === "Public Session from Aliyun", "share page should render the public title");
277
+ assert(document.getElementById("share-meta")?.textContent.includes(PUBLIC_API_URL), "share page metadata should show the API URL");
278
+ assert(
279
+ document.querySelector(".share-goal")?.textContent.includes("Explain how the public share page loads data."),
280
+ "share page should render the snapshot goal metadata",
281
+ );
282
+ assert(
283
+ document.getElementById("share-content")?.innerHTML.includes("static share page loaded it from the public API"),
284
+ "share page should render transcript content",
285
+ );
286
+ assert(
287
+ !document.getElementById("share-content")?.innerHTML.includes("image/png /") &&
288
+ !document.getElementById("share-content")?.innerHTML.includes("148 KB") &&
289
+ !document.getElementById("share-content")?.innerHTML.includes("figcaption"),
290
+ "share page should not render image mime type or size captions",
291
+ );
292
+ }
293
+
294
+ async function testPublicSharePageWithoutConfiguredApiDoesNotFetchLoopback() {
295
+ const requests = [];
296
+ const { document } = await runStaticPage("site/share/index.html", {
297
+ locationHref: "https://ffffhx.github.io/codex-snapshots/share/?id=snap_publicsession123456",
298
+ config: {},
299
+ storage: {
300
+ "codex-snapshots.api": "http://127.0.0.1:8787",
301
+ },
302
+ fetch: async (url) => {
303
+ requests.push(String(url));
304
+ throw new Error(`unexpected fetch: ${url}`);
305
+ },
306
+ });
307
+
308
+ await waitFor(() => document.getElementById("share-title")?.textContent === "缺少分享 API");
309
+
310
+ assert(document.getElementById("share-title")?.textContent === "缺少分享 API", "share page should show missing API state");
311
+ assert(document.getElementById("share-meta")?.textContent === "公开站点需要配置分享 API。", "share page should explain missing public API");
312
+ assert(requests.length === 0, "share page should not fetch loopback API when public config is missing");
313
+ }
314
+
315
+ async function runStaticPage(relativeHtmlPath, { locationHref, config, fetch, storage = {}, open = () => null, confirm = () => false }) {
316
+ const html = await readFile(path.join(ROOT_DIR, relativeHtmlPath), "utf8");
317
+ const dom = new JSDOM(html, {
318
+ pretendToBeVisual: true,
319
+ runScripts: "outside-only",
320
+ url: locationHref,
321
+ });
322
+ const { window } = dom;
323
+
324
+ window.CODEX_SNAPSHOTS_CONFIG = config;
325
+ window.fetch = fetch;
326
+ window.open = open;
327
+ window.confirm = confirm;
328
+ Object.defineProperty(window.navigator, "clipboard", {
329
+ configurable: true,
330
+ value: {
331
+ writeText: async () => undefined,
332
+ },
333
+ });
334
+ if (typeof window.AbortSignal.timeout !== "function") {
335
+ Object.defineProperty(window.AbortSignal, "timeout", {
336
+ configurable: true,
337
+ value: AbortSignal.timeout?.bind(AbortSignal) || (() => undefined),
338
+ });
339
+ }
340
+
341
+ for (const [key, value] of Object.entries(storage)) {
342
+ window.localStorage.setItem(key, value);
343
+ }
344
+
345
+ const scriptPath = relativeHtmlPath.includes("/share/") ? "site/assets/share.js" : "site/assets/site.js";
346
+ const code = await readFile(path.join(ROOT_DIR, scriptPath), "utf8");
347
+ window.eval(code);
348
+ await flushPromises(window);
349
+
350
+ return {
351
+ document: window.document,
352
+ window,
353
+ };
354
+ }
355
+
356
+ function sectionOrder(document) {
357
+ return Array.from(document.querySelectorAll("main.shell > section")).map((section) => String(section.className || ""));
358
+ }
359
+
360
+ function jsonResponse(payload, ok = true, status = ok ? 200 : 500) {
361
+ return {
362
+ ok,
363
+ status,
364
+ async json() {
365
+ return payload;
366
+ },
367
+ async text() {
368
+ return JSON.stringify(payload);
369
+ },
370
+ };
371
+ }
372
+
373
+ async function waitFor(predicate, { timeoutMs = 1000 } = {}) {
374
+ const start = Date.now();
375
+ while (Date.now() - start < timeoutMs) {
376
+ if (predicate()) {
377
+ return;
378
+ }
379
+ await new Promise((resolve) => setTimeout(resolve, 10));
380
+ }
381
+ assert(predicate(), "timed out waiting for expected DOM state");
382
+ }
383
+
384
+ async function flushPromises(window) {
385
+ for (let index = 0; index < 10; index += 1) {
386
+ await new Promise((resolve) => window.setTimeout(resolve, 0));
387
+ }
388
+ }
389
+
390
+ function assert(condition, message) {
391
+ if (!condition) {
392
+ throw new Error(message);
393
+ }
394
+ }
395
+
396
+ await testHomepagePublicSessions();
397
+ await testHomepageDeletesOwnPublicSessionWithGithubLogin();
398
+ await testPublicHomepageWithoutConfiguredApiDoesNotFetchLoopback();
399
+ await testLocalHomepageDefaultsToLocalApi();
400
+ await testSharePageLoadsFromConfiguredApi();
401
+ await testPublicSharePageWithoutConfiguredApiDoesNotFetchLoopback();
402
+
403
+ console.log("✓ static site public share checks passed");
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { mkdir, writeFile } from "node:fs/promises";
4
+ import net from "node:net";
5
+ import path from "node:path";
6
+
7
+ const parsed = parseArgs(process.argv.slice(2));
8
+
9
+ if (parsed.help) {
10
+ printHelp();
11
+ process.exit(0);
12
+ }
13
+
14
+ main().catch((error) => {
15
+ console.error(error instanceof Error ? error.message : String(error));
16
+ process.exitCode = 1;
17
+ });
18
+
19
+ async function main() {
20
+ const apiUrl = normalizePublicApiUrl(
21
+ parsed.options.apiUrl ||
22
+ process.env.CODEX_SNAPSHOTS_PUBLIC_API_URL ||
23
+ process.env.SNAPSHOT_SHARE_PUBLIC_API_URL ||
24
+ process.env.SNAPSHOT_SHARE_API_URL ||
25
+ "",
26
+ { allowLocal: parsed.options.allowLocal }
27
+ );
28
+ const outputPath = path.resolve(parsed.options.output || "site/assets/config.js");
29
+
30
+ await mkdir(path.dirname(outputPath), { recursive: true });
31
+ await writeFile(
32
+ outputPath,
33
+ `window.CODEX_SNAPSHOTS_CONFIG = ${inlineJson({ apiUrl })};\n`,
34
+ "utf8"
35
+ );
36
+
37
+ console.log(`Wrote ${outputPath}`);
38
+ console.log(`Public share API: ${apiUrl || "(not configured)"}`);
39
+ }
40
+
41
+ function normalizePublicApiUrl(value, { allowLocal }) {
42
+ const text = String(value || "").trim().replace(/\/+$/, "");
43
+ if (!text) {
44
+ return "";
45
+ }
46
+
47
+ let url;
48
+ try {
49
+ url = new URL(text);
50
+ } catch {
51
+ throw new Error(`CODEX_SNAPSHOTS_PUBLIC_API_URL must be a valid URL: ${text}`);
52
+ }
53
+
54
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
55
+ throw new Error(`CODEX_SNAPSHOTS_PUBLIC_API_URL must start with http:// or https://: ${text}`);
56
+ }
57
+
58
+ if (!allowLocal && !isPublicHost(url.hostname)) {
59
+ throw new Error(`CODEX_SNAPSHOTS_PUBLIC_API_URL must be a public API host, got ${text}`);
60
+ }
61
+
62
+ return url.toString().replace(/\/+$/, "");
63
+ }
64
+
65
+ function isPublicHost(hostname) {
66
+ const host = String(hostname || "").toLowerCase();
67
+ if (!host || host === "localhost" || host.endsWith(".localhost") || host === "::1" || host === "[::1]") {
68
+ return false;
69
+ }
70
+ if (host === "example.com" || host.endsWith(".example.com") || host === "snapshots.example.com") {
71
+ return false;
72
+ }
73
+
74
+ const ipVersion = net.isIP(host);
75
+ if (ipVersion === 4) {
76
+ return !isPrivateIpv4(host);
77
+ }
78
+ if (ipVersion === 6) {
79
+ return !isPrivateIpv6(host);
80
+ }
81
+
82
+ return true;
83
+ }
84
+
85
+ function isPrivateIpv4(host) {
86
+ const parts = host.split(".").map((part) => Number(part));
87
+ const [a, b] = parts;
88
+
89
+ return (
90
+ a === 0 ||
91
+ a === 10 ||
92
+ a === 127 ||
93
+ (a === 169 && b === 254) ||
94
+ (a === 172 && b >= 16 && b <= 31) ||
95
+ (a === 192 && b === 168)
96
+ );
97
+ }
98
+
99
+ function isPrivateIpv6(host) {
100
+ const normalized = host.replace(/^\[|\]$/g, "").toLowerCase();
101
+ return normalized === "::1" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe80:");
102
+ }
103
+
104
+ function inlineJson(value) {
105
+ return JSON.stringify(value, null, 2).replace(/[<>&\u2028\u2029]/g, (char) => {
106
+ if (char === "<") return "\\u003c";
107
+ if (char === ">") return "\\u003e";
108
+ if (char === "&") return "\\u0026";
109
+ if (char === "\u2028") return "\\u2028";
110
+ return "\\u2029";
111
+ });
112
+ }
113
+
114
+ function parseArgs(args) {
115
+ const options = {
116
+ allowLocal: false,
117
+ apiUrl: "",
118
+ output: "",
119
+ };
120
+ let help = false;
121
+
122
+ for (let index = 0; index < args.length; index += 1) {
123
+ const arg = args[index];
124
+ if (arg === "-h" || arg === "--help") {
125
+ help = true;
126
+ continue;
127
+ }
128
+ if (arg === "--allow-local") {
129
+ options.allowLocal = true;
130
+ continue;
131
+ }
132
+ if (arg === "--api-url") {
133
+ options.apiUrl = String(args[++index] || "");
134
+ continue;
135
+ }
136
+ if (arg === "--output") {
137
+ options.output = String(args[++index] || "");
138
+ continue;
139
+ }
140
+ throw new Error(`unknown option: ${arg}`);
141
+ }
142
+
143
+ return { help, options };
144
+ }
145
+
146
+ function printHelp() {
147
+ console.log(`write-site-config
148
+
149
+ Usage:
150
+ node scripts/write-site-config.mjs --api-url https://snapshots.example.com --output site/assets/config.js
151
+
152
+ Environment:
153
+ CODEX_SNAPSHOTS_PUBLIC_API_URL Public share API URL used by GitHub Pages.
154
+
155
+ Options:
156
+ --api-url URL Public share API URL. Defaults to CODEX_SNAPSHOTS_PUBLIC_API_URL.
157
+ --output FILE Config file to write. Defaults to site/assets/config.js.
158
+ --allow-local Allow localhost/private API URLs for local development only.
159
+ -h, --help Show help.
160
+ `);
161
+ }