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.
- package/README.md +101 -6
- package/bin/codex-snapshot.mjs +1 -6326
- package/deploy/aliyun/README.md +311 -0
- package/deploy/aliyun/backup-share-data.sh +109 -0
- package/deploy/aliyun/check-ecs-status.sh +149 -0
- package/deploy/aliyun/codex-snapshot-share.env.example +29 -0
- package/deploy/aliyun/codex-snapshot-share.service +26 -0
- package/deploy/aliyun/configure-github-pages-api.sh +141 -0
- package/deploy/aliyun/configure-local-publisher.sh +197 -0
- package/deploy/aliyun/deploy-to-ecs.sh +669 -0
- package/deploy/aliyun/deploy.env.example +52 -0
- package/deploy/aliyun/doctor.mjs +398 -0
- package/deploy/aliyun/install-share-api.sh +252 -0
- package/deploy/aliyun/install-system-deps.sh +84 -0
- package/deploy/aliyun/nginx-codex-snapshots.bootstrap.conf +34 -0
- package/deploy/aliyun/nginx-codex-snapshots.conf +52 -0
- package/deploy/aliyun/preflight.mjs +321 -0
- package/deploy/aliyun/restore-share-data.sh +141 -0
- package/deploy/aliyun/verify-public-share.mjs +404 -0
- package/dist/cli/codex-snapshot.mjs +2654 -0
- package/dist/core/privacy.js +81 -0
- package/dist/core/snapshot.js +1 -0
- package/dist/renderers/markdown.mjs +81 -0
- package/dist/renderers/transcript.js +195 -0
- package/dist/server/http.js +10 -0
- package/dist/server/local-security.js +66 -0
- package/dist/server/local-viewer-app.mjs +1670 -0
- package/dist/server/local-viewer.mjs +210 -0
- package/dist/server/share-api.mjs +1149 -0
- package/dist/server/share-store.js +136 -0
- package/dist/shared/sanitize.js +126 -0
- package/dist/shared/transcript.js +1 -0
- package/dist/sources/index.mjs +2 -0
- package/dist/sources/local-history.mjs +2221 -0
- package/package.json +42 -14
- package/scripts/build-site.mjs +71 -0
- package/scripts/launch-agent.mjs +19 -227
- package/scripts/serve-site.mjs +2 -2
- package/scripts/test-aliyun-deploy-config.sh +230 -0
- package/scripts/test-share-api.mjs +967 -0
- package/scripts/test-site-config.mjs +100 -0
- package/scripts/test-static-site.mjs +403 -0
- package/scripts/write-site-config.mjs +161 -0
- package/server/share-api.mjs +1 -771
- package/site/assets/config.js +3 -0
- package/site/assets/share.js +43 -106
- package/site/assets/site.css +3 -605
- package/site/assets/site.js +15 -92
- package/site/favicon.svg +7 -0
- package/site/index.html +3 -83
- package/site/share/index.html +3 -8
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
export function detectRisks(text) {
|
|
3
|
+
const checks = [
|
|
4
|
+
{ id: "private-key", label: "Private key block", severity: "high", count: 0, pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g },
|
|
5
|
+
{ id: "jwt", label: "JWT-like token", severity: "high", count: 0, pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
|
|
6
|
+
{ id: "api-key", label: "API key or secret assignment", severity: "high", count: 0, pattern: /\b(api[_-]?key|secret|access[_-]?token|auth[_-]?token|refresh[_-]?token|password|passwd|cookie|authorization)\b\s*[:=]\s*["']?[^"'\s`]{8,}/gi },
|
|
7
|
+
{ id: "bearer", label: "Bearer token", severity: "high", count: 0, pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/g },
|
|
8
|
+
{ id: "openai-key", label: "OpenAI-style API key", severity: "high", count: 0, pattern: /\bsk-[A-Za-z0-9_-]{20,}\b/g },
|
|
9
|
+
{ id: "aws-key", label: "AWS access key", severity: "high", count: 0, pattern: /\b(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}\b/g },
|
|
10
|
+
{ id: "home-path", label: "Local home path", severity: "medium", count: 0, pattern: new RegExp(escapeRegExp(os.homedir()), "g") },
|
|
11
|
+
{ id: "internal-domain", label: "Internal-looking domain", severity: "medium", count: 0, pattern: /\b[A-Za-z0-9.-]+\.(bytedance|byteintl|corp|internal|local)\b/gi },
|
|
12
|
+
{ id: "env-file", label: "Environment file mention", severity: "medium", count: 0, pattern: /(^|[\/\s])\.env([.\w-]*)?\b/g },
|
|
13
|
+
];
|
|
14
|
+
const risks = [];
|
|
15
|
+
for (const check of checks) {
|
|
16
|
+
const matches = text.match(check.pattern);
|
|
17
|
+
if (matches?.length) {
|
|
18
|
+
risks.push({
|
|
19
|
+
id: check.id,
|
|
20
|
+
label: check.label,
|
|
21
|
+
severity: check.severity,
|
|
22
|
+
count: matches.length,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return risks;
|
|
27
|
+
}
|
|
28
|
+
export function addRisks(risks, text, turn) {
|
|
29
|
+
for (const risk of detectRisks(text)) {
|
|
30
|
+
addRiskEntry(risks, risk, turn);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function addImageRisk(risks, count, turn) {
|
|
34
|
+
if (!count) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
addRiskEntry(risks, {
|
|
38
|
+
id: "image-attachment",
|
|
39
|
+
label: "Image attachment",
|
|
40
|
+
severity: "medium",
|
|
41
|
+
count,
|
|
42
|
+
}, turn);
|
|
43
|
+
}
|
|
44
|
+
export function severityRank(severity) {
|
|
45
|
+
if (severity === "high") {
|
|
46
|
+
return 3;
|
|
47
|
+
}
|
|
48
|
+
if (severity === "medium") {
|
|
49
|
+
return 2;
|
|
50
|
+
}
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
export function redactText(text) {
|
|
54
|
+
let output = text;
|
|
55
|
+
output = output.replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED_PRIVATE_KEY]");
|
|
56
|
+
output = output.replace(/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, "[REDACTED_JWT]");
|
|
57
|
+
output = output.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/g, "Bearer [REDACTED]");
|
|
58
|
+
output = output.replace(/\bsk-[A-Za-z0-9_-]{20,}\b/g, "sk-[REDACTED]");
|
|
59
|
+
output = output.replace(/\b(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}\b/g, "[REDACTED_AWS_KEY]");
|
|
60
|
+
output = output.replace(/\b(api[_-]?key|secret|access[_-]?token|auth[_-]?token|refresh[_-]?token|password|passwd|cookie|authorization)\b(\s*[:=]\s*)["']?[^"'\s`]{8,}/gi, "$1$2[REDACTED]");
|
|
61
|
+
output = output.replace(new RegExp(escapeRegExp(os.homedir()), "g"), "~");
|
|
62
|
+
return output;
|
|
63
|
+
}
|
|
64
|
+
function addRiskEntry(risks, risk, turn) {
|
|
65
|
+
const key = risk.id;
|
|
66
|
+
const current = risks.get(key) || {
|
|
67
|
+
id: risk.id,
|
|
68
|
+
label: risk.label,
|
|
69
|
+
severity: risk.severity,
|
|
70
|
+
count: 0,
|
|
71
|
+
turns: [],
|
|
72
|
+
};
|
|
73
|
+
current.count += risk.count;
|
|
74
|
+
if (!current.turns.includes(turn)) {
|
|
75
|
+
current.turns.push(turn);
|
|
76
|
+
}
|
|
77
|
+
risks.set(key, current);
|
|
78
|
+
}
|
|
79
|
+
function escapeRegExp(value) {
|
|
80
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import hljs from "highlight.js";
|
|
3
|
+
import markdownit from "markdown-it";
|
|
4
|
+
import { mergeLinkRel, sanitizeRenderedHtml, stripAppDirectives } from "../shared/sanitize.js";
|
|
5
|
+
const MARKDOWN_LANGUAGE_ALIASES = new Map([
|
|
6
|
+
["plain", "plaintext"],
|
|
7
|
+
["plaintext", "plaintext"],
|
|
8
|
+
["text", "plaintext"],
|
|
9
|
+
["js", "javascript"],
|
|
10
|
+
["jsx", "javascript"],
|
|
11
|
+
["ts", "typescript"],
|
|
12
|
+
["tsx", "typescript"],
|
|
13
|
+
["yml", "yaml"],
|
|
14
|
+
]);
|
|
15
|
+
const markdownRenderer = markdownit({
|
|
16
|
+
breaks: true,
|
|
17
|
+
html: false,
|
|
18
|
+
linkify: true,
|
|
19
|
+
typographer: false,
|
|
20
|
+
highlight: renderHighlightedCode,
|
|
21
|
+
});
|
|
22
|
+
configureMarkdownLinks(markdownRenderer);
|
|
23
|
+
export function renderMarkdownHtml(text) {
|
|
24
|
+
return sanitizeRenderedHtml(markdownRenderer.render(stripAppDirectives(text)).trim());
|
|
25
|
+
}
|
|
26
|
+
function configureMarkdownLinks(renderer) {
|
|
27
|
+
const defaultRender = renderer.renderer.rules.link_open || ((tokens, index, options, env, self) => {
|
|
28
|
+
return self.renderToken(tokens, index, options);
|
|
29
|
+
});
|
|
30
|
+
renderer.renderer.rules.link_open = (tokens, index, options, env, self) => {
|
|
31
|
+
const token = tokens[index];
|
|
32
|
+
setMarkdownTokenAttr(token, "target", "_blank");
|
|
33
|
+
setMarkdownTokenAttr(token, "rel", mergeLinkRel(token.attrGet("rel")));
|
|
34
|
+
return defaultRender(tokens, index, options, env, self);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function setMarkdownTokenAttr(token, name, value) {
|
|
38
|
+
const index = token.attrIndex(name);
|
|
39
|
+
if (index < 0) {
|
|
40
|
+
token.attrPush([name, value]);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
token.attrs[index][1] = value;
|
|
44
|
+
}
|
|
45
|
+
function renderHighlightedCode(source, rawLanguage) {
|
|
46
|
+
const language = normalizeMarkdownLanguage(rawLanguage);
|
|
47
|
+
const displayLanguage = language || normalizeMarkdownLanguageLabel(rawLanguage) || "text";
|
|
48
|
+
const code = String(source || "");
|
|
49
|
+
let html = "";
|
|
50
|
+
if (language && hljs.getLanguage(language)) {
|
|
51
|
+
html = hljs.highlight(code, { language, ignoreIllegals: true }).value;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
html = escapeHtml(code);
|
|
55
|
+
}
|
|
56
|
+
const className = language ? ` class="hljs language-${escapeHtml(language)}"` : " class=\"hljs\"";
|
|
57
|
+
return `<pre data-language="${escapeHtml(displayLanguage)}"><code${className}>${html}</code></pre>`;
|
|
58
|
+
}
|
|
59
|
+
function normalizeMarkdownLanguage(rawLanguage) {
|
|
60
|
+
const language = normalizeMarkdownLanguageLabel(rawLanguage);
|
|
61
|
+
if (!language) {
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
const mapped = MARKDOWN_LANGUAGE_ALIASES.get(language) || language;
|
|
65
|
+
return hljs.getLanguage(mapped) ? mapped : "";
|
|
66
|
+
}
|
|
67
|
+
function normalizeMarkdownLanguageLabel(rawLanguage) {
|
|
68
|
+
return String(rawLanguage || "")
|
|
69
|
+
.trim()
|
|
70
|
+
.split(/\s+/)[0]
|
|
71
|
+
.replace(/[^A-Za-z0-9_+-]/g, "")
|
|
72
|
+
.toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
function escapeHtml(value) {
|
|
75
|
+
return String(value ?? "")
|
|
76
|
+
.replace(/&/g, "&")
|
|
77
|
+
.replace(/</g, "<")
|
|
78
|
+
.replace(/>/g, ">")
|
|
79
|
+
.replace(/"/g, """)
|
|
80
|
+
.replace(/'/g, "'");
|
|
81
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { escapeHtml, sanitizeRenderedHtml, stripAppDirectives } from "../shared/sanitize.js";
|
|
2
|
+
export function buildTranscriptItems(turns) {
|
|
3
|
+
const items = [];
|
|
4
|
+
let index = 0;
|
|
5
|
+
let previousUserTurn = null;
|
|
6
|
+
while (index < turns.length) {
|
|
7
|
+
const turn = turns[index];
|
|
8
|
+
if (isUserMessageTurn(turn)) {
|
|
9
|
+
items.push({ kind: "turn", turn });
|
|
10
|
+
previousUserTurn = turn;
|
|
11
|
+
index += 1;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const segment = [];
|
|
15
|
+
while (index < turns.length && !isUserMessageTurn(turns[index])) {
|
|
16
|
+
segment.push(turns[index]);
|
|
17
|
+
index += 1;
|
|
18
|
+
}
|
|
19
|
+
const finalIndex = segment.map(isAssistantMessageTurn).lastIndexOf(true);
|
|
20
|
+
if (finalIndex === -1) {
|
|
21
|
+
if (segment.length) {
|
|
22
|
+
items.push({
|
|
23
|
+
kind: "process",
|
|
24
|
+
turns: segment,
|
|
25
|
+
durationTurns: buildProcessDurationTurns(previousUserTurn, segment),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (finalIndex === segment.length - 1) {
|
|
31
|
+
const processTurns = segment.slice(0, finalIndex);
|
|
32
|
+
const finalTurn = segment[finalIndex];
|
|
33
|
+
if (processTurns.length) {
|
|
34
|
+
items.push({
|
|
35
|
+
kind: "process",
|
|
36
|
+
turns: processTurns,
|
|
37
|
+
durationTurns: buildProcessDurationTurns(previousUserTurn, processTurns.concat(finalTurn)),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
items.push({ kind: "turn", turn: finalTurn });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
items.push({
|
|
44
|
+
kind: "process",
|
|
45
|
+
turns: segment,
|
|
46
|
+
durationTurns: buildProcessDurationTurns(previousUserTurn, segment),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return items;
|
|
50
|
+
}
|
|
51
|
+
export function processLabel(turns, label = "已处理") {
|
|
52
|
+
const duration = processDurationLabel(turns);
|
|
53
|
+
return duration ? `${label} ${duration}` : label;
|
|
54
|
+
}
|
|
55
|
+
export function processDurationLabel(turns) {
|
|
56
|
+
const times = turns.map((turn) => new Date(turn.timestamp || "").getTime()).filter(Number.isFinite);
|
|
57
|
+
if (times.length < 2) {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
const seconds = Math.max(1, Math.round((Math.max(...times) - Math.min(...times)) / 1000));
|
|
61
|
+
if (seconds < 60) {
|
|
62
|
+
return `${seconds}s`;
|
|
63
|
+
}
|
|
64
|
+
const minutes = Math.floor(seconds / 60);
|
|
65
|
+
const rest = seconds % 60;
|
|
66
|
+
if (minutes < 60) {
|
|
67
|
+
return rest ? `${minutes}m ${rest}s` : `${minutes}m`;
|
|
68
|
+
}
|
|
69
|
+
const hours = Math.floor(minutes / 60);
|
|
70
|
+
const minuteRest = minutes % 60;
|
|
71
|
+
return minuteRest ? `${hours}h ${minuteRest}m` : `${hours}h`;
|
|
72
|
+
}
|
|
73
|
+
export function renderTranscriptHtml(turns, emptyHtmlOrOptions = "<div class='empty'>没有可分享的对话记录。</div>", options = {}) {
|
|
74
|
+
const renderOptions = normalizeTranscriptRenderOptions(emptyHtmlOrOptions, options);
|
|
75
|
+
const html = buildTranscriptItems(turns).map((item, index) => renderTranscriptItemHtml(item, index, renderOptions)).join("");
|
|
76
|
+
return html || renderOptions.emptyHtml;
|
|
77
|
+
}
|
|
78
|
+
function normalizeTranscriptRenderOptions(emptyHtmlOrOptions, options) {
|
|
79
|
+
const base = typeof emptyHtmlOrOptions === "string" ? { ...options, emptyHtml: emptyHtmlOrOptions } : emptyHtmlOrOptions;
|
|
80
|
+
return {
|
|
81
|
+
bodyWrapper: base.bodyWrapper !== false,
|
|
82
|
+
emptyHtml: base.emptyHtml || "<div class='empty'>没有可分享的对话记录。</div>",
|
|
83
|
+
bodyClassName: base.bodyClassName || "body",
|
|
84
|
+
contentClassName: base.contentClassName || "",
|
|
85
|
+
includeProcessMessageMeta: Boolean(base.includeProcessMessageMeta),
|
|
86
|
+
includeTopLevelToolMeta: Boolean(base.includeTopLevelToolMeta),
|
|
87
|
+
roleClassMode: base.roleClassMode || "space",
|
|
88
|
+
labels: {
|
|
89
|
+
assistant: base.labels?.assistant || "Assistant",
|
|
90
|
+
message: base.labels?.message || "Message",
|
|
91
|
+
processed: base.labels?.processed || "已处理",
|
|
92
|
+
tool: base.labels?.tool || "工具",
|
|
93
|
+
user: base.labels?.user || "User",
|
|
94
|
+
imageUnavailable: base.labels?.imageUnavailable || "图片暂不可用",
|
|
95
|
+
imageAltPrefix: base.labels?.imageAltPrefix || "图片附件",
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function buildProcessDurationTurns(startTurn, turns) {
|
|
100
|
+
return [startTurn, ...turns].filter(Boolean);
|
|
101
|
+
}
|
|
102
|
+
function isUserMessageTurn(turn) {
|
|
103
|
+
return Boolean(turn && turn.kind !== "tool" && turn.role === "user");
|
|
104
|
+
}
|
|
105
|
+
function isAssistantMessageTurn(turn) {
|
|
106
|
+
return Boolean(turn && turn.kind !== "tool" && turn.role === "assistant");
|
|
107
|
+
}
|
|
108
|
+
function renderTranscriptItemHtml(item, index, options) {
|
|
109
|
+
if (item.kind === "process") {
|
|
110
|
+
return renderProcessGroupHtml(item, index, options);
|
|
111
|
+
}
|
|
112
|
+
return renderTurnHtml(item.turn, options);
|
|
113
|
+
}
|
|
114
|
+
function renderTurnHtml(turn, options) {
|
|
115
|
+
const role = turnRole(turn);
|
|
116
|
+
const meta = options.includeTopLevelToolMeta && role === "tool"
|
|
117
|
+
? `<div class="turn-meta">${escapeHtml(turnLabel(role, turn, options))}</div>`
|
|
118
|
+
: "";
|
|
119
|
+
return `<article class="${escapeHtml(turnClassName(role, options))}"><div class="message-card">${meta}${renderBodyContainerHtml(turn, options)}</div></article>`;
|
|
120
|
+
}
|
|
121
|
+
function renderProcessGroupHtml(item, index, options) {
|
|
122
|
+
const turns = item.turns || [];
|
|
123
|
+
if (!turns.length) {
|
|
124
|
+
return "";
|
|
125
|
+
}
|
|
126
|
+
return `<article class="${escapeHtml(processClassName(options))}"><details class="process-details" data-process-index="${escapeHtml(index)}"><summary class="process-summary"><span>${escapeHtml(processLabel(item.durationTurns || turns, options.labels.processed))}</span></summary><div class="process-body">${turns.map((turn) => renderProcessEntryHtml(turn, options)).join("")}</div></details></article>`;
|
|
127
|
+
}
|
|
128
|
+
function renderProcessEntryHtml(turn, options) {
|
|
129
|
+
const role = turnRole(turn);
|
|
130
|
+
const meta = options.includeProcessMessageMeta && role !== "tool"
|
|
131
|
+
? `<div class="turn-meta">${escapeHtml(turnLabel(role, turn, options))}</div>`
|
|
132
|
+
: "";
|
|
133
|
+
return `<section class="process-entry process-${escapeHtml(role)}">${meta}${renderBodyContainerHtml(turn, options)}</section>`;
|
|
134
|
+
}
|
|
135
|
+
function turnRole(turn) {
|
|
136
|
+
if (turn.kind === "tool") {
|
|
137
|
+
return "tool";
|
|
138
|
+
}
|
|
139
|
+
return turn.role === "user" ? "user" : "assistant";
|
|
140
|
+
}
|
|
141
|
+
function renderTurnBodyHtml(turn, options) {
|
|
142
|
+
if (turn.kind === "tool") {
|
|
143
|
+
return `<details class="tool-details"><summary>${escapeHtml(options.labels.tool)}${turn.name ? ` / ${escapeHtml(turn.name)}` : ""}</summary><pre>${escapeHtml(turn.text || "")}</pre></details>`;
|
|
144
|
+
}
|
|
145
|
+
const content = `${sanitizeRenderedHtml(turn.html || "") || renderPlainTextHtml(turn.text)}${renderImagesHtml(turn.images || [], options)}`;
|
|
146
|
+
return options.contentClassName ? `<div class="${escapeHtml(options.contentClassName)}">${content}</div>` : content;
|
|
147
|
+
}
|
|
148
|
+
function renderBodyContainerHtml(turn, options) {
|
|
149
|
+
const body = renderTurnBodyHtml(turn, options);
|
|
150
|
+
return options.bodyWrapper ? `<div class="${escapeHtml(options.bodyClassName)}">${body}</div>` : body;
|
|
151
|
+
}
|
|
152
|
+
function turnClassName(role, options) {
|
|
153
|
+
return options.roleClassMode === "prefixed" ? `turn turn-${role}` : `turn ${role}`;
|
|
154
|
+
}
|
|
155
|
+
function processClassName(options) {
|
|
156
|
+
return options.roleClassMode === "prefixed" ? "turn turn-process" : "turn process";
|
|
157
|
+
}
|
|
158
|
+
function turnLabel(role, turn, options) {
|
|
159
|
+
if (role === "tool") {
|
|
160
|
+
return `${options.labels.tool}${turn.name ? ` / ${turn.name}` : ""}`;
|
|
161
|
+
}
|
|
162
|
+
if (role === "user") {
|
|
163
|
+
return options.labels.user;
|
|
164
|
+
}
|
|
165
|
+
if (role === "assistant") {
|
|
166
|
+
return options.labels.assistant;
|
|
167
|
+
}
|
|
168
|
+
return role || options.labels.message;
|
|
169
|
+
}
|
|
170
|
+
function renderPlainTextHtml(value) {
|
|
171
|
+
const visibleText = stripAppDirectives(value);
|
|
172
|
+
if (!visibleText) {
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
return visibleText
|
|
176
|
+
.split(/\n{2,}/)
|
|
177
|
+
.map((block) => `<p>${escapeHtml(block).replace(/\n/g, "<br>")}</p>`)
|
|
178
|
+
.join("");
|
|
179
|
+
}
|
|
180
|
+
function renderImagesHtml(images, options) {
|
|
181
|
+
if (!Array.isArray(images) || !images.length) {
|
|
182
|
+
return "";
|
|
183
|
+
}
|
|
184
|
+
return `<div class="attachment-grid">${images
|
|
185
|
+
.map((image, index) => {
|
|
186
|
+
if (!image.src) {
|
|
187
|
+
return `<figure class="image-attachment image-unavailable"><div>${escapeHtml(image.unavailableReason || options.labels.imageUnavailable)}</div></figure>`;
|
|
188
|
+
}
|
|
189
|
+
return `<figure class="image-attachment"><img src="${escapeHtml(image.src)}" alt="${escapeHtml(image.alt || `${options.labels.imageAltPrefix} ${index + 1}`)}" decoding="async"></figure>`;
|
|
190
|
+
})
|
|
191
|
+
.join("")}</div>`;
|
|
192
|
+
}
|
|
193
|
+
export function sanitizeTranscriptHtml(value) {
|
|
194
|
+
return sanitizeRenderedHtml(value);
|
|
195
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function sendJson(response, data, status = 200) {
|
|
2
|
+
send(response, status, "application/json; charset=utf-8", JSON.stringify(data, null, 2));
|
|
3
|
+
}
|
|
4
|
+
export function send(response, status, contentType, body) {
|
|
5
|
+
response.writeHead(status, {
|
|
6
|
+
"content-type": contentType,
|
|
7
|
+
"cache-control": "no-store",
|
|
8
|
+
});
|
|
9
|
+
response.end(body);
|
|
10
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { sendJson } from "./http.js";
|
|
3
|
+
export const MUTATION_CSRF_HEADER = "x-codex-snapshot-csrf";
|
|
4
|
+
export function createMutationCsrfToken() {
|
|
5
|
+
return randomBytes(32).toString("base64url");
|
|
6
|
+
}
|
|
7
|
+
export function setSnapshotServerCorsHeaders(request, response) {
|
|
8
|
+
const origin = request.headers.origin || "";
|
|
9
|
+
if (!origin) {
|
|
10
|
+
response.setHeader("access-control-allow-origin", "*");
|
|
11
|
+
}
|
|
12
|
+
else if (isAllowedSnapshotOrigin(origin)) {
|
|
13
|
+
response.setHeader("access-control-allow-origin", origin);
|
|
14
|
+
response.setHeader("vary", "Origin");
|
|
15
|
+
}
|
|
16
|
+
response.setHeader("access-control-allow-methods", "GET,POST,OPTIONS");
|
|
17
|
+
response.setHeader("access-control-allow-headers", `content-type,${MUTATION_CSRF_HEADER}`);
|
|
18
|
+
response.setHeader("access-control-max-age", "86400");
|
|
19
|
+
}
|
|
20
|
+
export function isAllowedSnapshotServerRequest(request) {
|
|
21
|
+
const origin = request.headers.origin || "";
|
|
22
|
+
return !origin || isAllowedSnapshotOrigin(origin);
|
|
23
|
+
}
|
|
24
|
+
export function isAllowedSnapshotOrigin(origin) {
|
|
25
|
+
const text = Array.isArray(origin) ? origin[0] || "" : origin;
|
|
26
|
+
const configuredOrigins = String(process.env.SNAPSHOT_VIEWER_ALLOWED_ORIGINS || "")
|
|
27
|
+
.split(",")
|
|
28
|
+
.map((value) => value.trim())
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
const allowedOrigins = new Set([
|
|
31
|
+
"http://127.0.0.1:3000",
|
|
32
|
+
"http://localhost:3000",
|
|
33
|
+
...configuredOrigins,
|
|
34
|
+
]);
|
|
35
|
+
if (allowedOrigins.has(text)) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const url = new URL(text);
|
|
40
|
+
return url.protocol === "http:" && ["127.0.0.1", "localhost", "::1"].includes(url.hostname);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function allowMutationRequest(request, response, csrfToken) {
|
|
47
|
+
if (request.method !== "POST") {
|
|
48
|
+
sendJson(response, { error: "method not allowed" }, 405);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const origin = String(request.headers.origin || "");
|
|
52
|
+
if (!origin) {
|
|
53
|
+
sendJson(response, { error: "origin is required for local mutation requests" }, 403);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
if (!isAllowedSnapshotOrigin(origin)) {
|
|
57
|
+
sendJson(response, { error: "origin is not allowed to mutate this local snapshot server" }, 403);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const token = String(request.headers[MUTATION_CSRF_HEADER] || "");
|
|
61
|
+
if (!token || token !== csrfToken) {
|
|
62
|
+
sendJson(response, { error: "invalid csrf token" }, 403);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|