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,136 @@
1
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export function createShareStore(config) {
4
+ if (config.kind === "file") {
5
+ return createFileShareStore(config.filePath);
6
+ }
7
+ throw new Error(`Unsupported share store: ${config.kind || "unknown"}`);
8
+ }
9
+ export function createFileShareStore(filePath) {
10
+ let queue = Promise.resolve();
11
+ function enqueue(operation) {
12
+ const run = queue.then(operation, operation);
13
+ queue = run.catch(() => undefined);
14
+ return run;
15
+ }
16
+ return {
17
+ putShare(record) {
18
+ return enqueue(async () => {
19
+ const existing = await readShares(filePath);
20
+ await writeShares(filePath, existing.filter((share) => share.id !== record.id).concat(record));
21
+ });
22
+ },
23
+ async getShare(id) {
24
+ const record = (await readShares(filePath)).find((share) => share.id === id);
25
+ return isExpired(record) ? undefined : record;
26
+ },
27
+ async listShares() {
28
+ return (await readShares(filePath))
29
+ .filter((share) => !isExpired(share))
30
+ .sort(compareSharesNewestFirst);
31
+ },
32
+ deleteShare(id) {
33
+ return enqueue(async () => {
34
+ const existing = await readShares(filePath);
35
+ const next = existing.filter((share) => share.id !== id);
36
+ if (next.length === existing.length) {
37
+ return false;
38
+ }
39
+ await writeShares(filePath, next);
40
+ return true;
41
+ });
42
+ },
43
+ async countShares() {
44
+ return (await readShares(filePath)).filter((share) => !isExpired(share)).length;
45
+ },
46
+ };
47
+ }
48
+ function compareSharesNewestFirst(left, right) {
49
+ const leftTime = new Date(left.updatedAt || left.createdAt || "").getTime();
50
+ const rightTime = new Date(right.updatedAt || right.createdAt || "").getTime();
51
+ const delta = (Number.isFinite(rightTime) ? rightTime : 0) - (Number.isFinite(leftTime) ? leftTime : 0);
52
+ return delta || String(right.id).localeCompare(String(left.id));
53
+ }
54
+ async function readShares(filePath) {
55
+ try {
56
+ const parsed = JSON.parse(await readFile(filePath, "utf8"));
57
+ const entries = Array.isArray(parsed)
58
+ ? parsed
59
+ : parsed && typeof parsed === "object" && Array.isArray(parsed.entries)
60
+ ? parsed.entries
61
+ : [];
62
+ return entries.flatMap((entry) => normalizeShareRecord(entry) ?? []);
63
+ }
64
+ catch (error) {
65
+ if (error?.code === "ENOENT") {
66
+ return [];
67
+ }
68
+ throw error;
69
+ }
70
+ }
71
+ async function writeShares(filePath, records) {
72
+ const dir = path.dirname(filePath);
73
+ const tempFile = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
74
+ await mkdir(dir, { recursive: true });
75
+ try {
76
+ await writeFile(tempFile, `${JSON.stringify({ schemaVersion: 1, updatedAt: new Date().toISOString(), entries: records }, null, 2)}\n`, "utf8");
77
+ await rename(tempFile, filePath);
78
+ }
79
+ catch (error) {
80
+ await rm(tempFile, { force: true }).catch(() => undefined);
81
+ throw error;
82
+ }
83
+ }
84
+ function normalizeShareRecord(value) {
85
+ if (!value || typeof value !== "object") {
86
+ return undefined;
87
+ }
88
+ const record = value;
89
+ const id = sanitizeText(record.id, 120);
90
+ if (!id) {
91
+ return undefined;
92
+ }
93
+ return {
94
+ id,
95
+ title: sanitizeText(record.title, 240) || id,
96
+ engine: sanitizeText(record.engine, 80) || "codex",
97
+ engineLabel: sanitizeText(record.engineLabel, 80) || "Codex",
98
+ sourceRef: sanitizeText(record.sourceRef, 240) || undefined,
99
+ createdAt: normalizeDate(record.createdAt) || new Date().toISOString(),
100
+ updatedAt: normalizeDate(record.updatedAt) || new Date().toISOString(),
101
+ expiresAt: normalizeDate(record.expiresAt) || undefined,
102
+ redacted: record.redacted !== false,
103
+ turnCount: Number.isFinite(Number(record.turnCount)) ? Number(record.turnCount) : 0,
104
+ owner: normalizeShareOwner(record.owner),
105
+ snapshot: record.snapshot,
106
+ };
107
+ }
108
+ function normalizeShareOwner(value) {
109
+ if (!value || typeof value !== "object") {
110
+ return undefined;
111
+ }
112
+ const owner = value;
113
+ const id = sanitizeText(owner.id, 80);
114
+ const login = sanitizeText(owner.login, 80);
115
+ if (!id || !login) {
116
+ return undefined;
117
+ }
118
+ return {
119
+ id,
120
+ login,
121
+ avatarUrl: sanitizeText(owner.avatarUrl, 400) || undefined,
122
+ profileUrl: sanitizeText(owner.profileUrl, 400) || undefined,
123
+ };
124
+ }
125
+ function normalizeDate(value) {
126
+ const date = typeof value === "string" ? new Date(value) : undefined;
127
+ return date && Number.isFinite(date.getTime()) ? date.toISOString() : "";
128
+ }
129
+ function isExpired(record) {
130
+ return Boolean(record?.expiresAt && new Date(record.expiresAt).getTime() <= Date.now());
131
+ }
132
+ function sanitizeText(value, maxLength) {
133
+ return typeof value === "string"
134
+ ? value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim().slice(0, maxLength)
135
+ : "";
136
+ }
@@ -0,0 +1,126 @@
1
+ import sanitizeHtml from "sanitize-html";
2
+ const APP_DIRECTIVE_PATTERN = /^[ \t]*::(?:git-(?:stage|commit|push|create-branch|create-pr)|archive|code-comment)\{[^\n]*\}[ \t]*$/gm;
3
+ const APP_DIRECTIVE_HTML_PATTERN = /<p>\s*::(?:git-(?:stage|commit|push|create-branch|create-pr)|archive|code-comment)\{[\s\S]*?\}\s*<\/p>/g;
4
+ export function stripAppDirectives(value) {
5
+ return String(value ?? "")
6
+ .replace(/\r\n/g, "\n")
7
+ .replace(APP_DIRECTIVE_PATTERN, "")
8
+ .replace(/[ \t]+\n/g, "\n")
9
+ .replace(/\n{3,}/g, "\n\n")
10
+ .trim();
11
+ }
12
+ export function stripAppDirectiveHtml(value) {
13
+ return String(value ?? "").replace(APP_DIRECTIVE_HTML_PATTERN, "").trim();
14
+ }
15
+ export function sanitizeRenderedHtml(value) {
16
+ return sanitizeHtml(stripAppDirectiveHtml(value), {
17
+ allowedTags: [
18
+ "a",
19
+ "b",
20
+ "blockquote",
21
+ "br",
22
+ "code",
23
+ "del",
24
+ "details",
25
+ "div",
26
+ "em",
27
+ "figcaption",
28
+ "figure",
29
+ "h1",
30
+ "h2",
31
+ "h3",
32
+ "h4",
33
+ "h5",
34
+ "h6",
35
+ "hr",
36
+ "li",
37
+ "ol",
38
+ "p",
39
+ "pre",
40
+ "section",
41
+ "span",
42
+ "strong",
43
+ "summary",
44
+ "table",
45
+ "tbody",
46
+ "td",
47
+ "th",
48
+ "thead",
49
+ "tr",
50
+ "ul",
51
+ ],
52
+ allowedAttributes: {
53
+ a: ["href", "name", "rel", "target", "title"],
54
+ blockquote: ["class"],
55
+ code: ["class"],
56
+ details: ["class", "open"],
57
+ div: ["class"],
58
+ figure: ["class"],
59
+ pre: ["class", "data-language"],
60
+ section: ["class"],
61
+ span: ["class"],
62
+ summary: ["class"],
63
+ table: ["class"],
64
+ },
65
+ allowedClasses: {
66
+ blockquote: ["contains-task-list"],
67
+ code: ["hljs", /^language-[A-Za-z0-9_-]+$/],
68
+ details: ["tool-details"],
69
+ div: [
70
+ "attachment-grid",
71
+ "body",
72
+ "empty",
73
+ "image-unavailable",
74
+ "message-card",
75
+ "process-body",
76
+ ],
77
+ figure: ["image-attachment", "image-unavailable"],
78
+ pre: ["hljs"],
79
+ section: [/^process-entry$/, /^process-(?:tool|user|assistant)$/],
80
+ span: [/^hljs-[A-Za-z0-9_-]+$/],
81
+ summary: ["process-summary"],
82
+ table: ["table"],
83
+ },
84
+ allowedSchemes: ["http", "https", "mailto"],
85
+ allowProtocolRelative: false,
86
+ transformTags: {
87
+ a: (_tagName, attribs) => ({
88
+ tagName: "a",
89
+ attribs: {
90
+ ...attribs,
91
+ rel: mergeLinkRel(attribs.rel),
92
+ target: "_blank",
93
+ },
94
+ }),
95
+ },
96
+ }).trim();
97
+ }
98
+ export function sanitizeSnapshotHtml(snapshot) {
99
+ const turns = Array.isArray(snapshot?.turns) ? snapshot.turns : [];
100
+ for (const turn of turns) {
101
+ if (!turn || typeof turn !== "object") {
102
+ continue;
103
+ }
104
+ if (typeof turn.text === "string") {
105
+ turn.text = stripAppDirectives(turn.text);
106
+ }
107
+ if (typeof turn.html === "string") {
108
+ turn.html = sanitizeRenderedHtml(turn.html);
109
+ }
110
+ }
111
+ return snapshot;
112
+ }
113
+ export function escapeHtml(value) {
114
+ return String(value ?? "")
115
+ .replace(/&/g, "&amp;")
116
+ .replace(/</g, "&lt;")
117
+ .replace(/>/g, "&gt;")
118
+ .replace(/"/g, "&quot;")
119
+ .replace(/'/g, "&#39;");
120
+ }
121
+ export function mergeLinkRel(value) {
122
+ const rel = new Set(String(value || "").split(/\s+/).filter(Boolean));
123
+ rel.add("noopener");
124
+ rel.add("noreferrer");
125
+ return Array.from(rel).join(" ");
126
+ }
@@ -0,0 +1 @@
1
+ export * from "../renderers/transcript.js";
@@ -0,0 +1,2 @@
1
+ export { detectRisks, redactText } from "../core/privacy.js";
2
+ export { listSessions, loadSnapshot } from "./local-history.mjs";