openworkgraph 0.2.12
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 +64 -0
- package/dist/api-image-agent.d.ts +32 -0
- package/dist/api-image-agent.js +132 -0
- package/dist/api-image-output.d.ts +8 -0
- package/dist/api-image-output.js +41 -0
- package/dist/api.d.ts +35 -0
- package/dist/api.js +153 -0
- package/dist/auth.d.ts +58 -0
- package/dist/auth.js +273 -0
- package/dist/backend/adapter.d.ts +35 -0
- package/dist/backend/adapter.js +439 -0
- package/dist/backend/capabilities.d.ts +23 -0
- package/dist/backend/capabilities.js +75 -0
- package/dist/backend/diagnostics.d.ts +9 -0
- package/dist/backend/diagnostics.js +80 -0
- package/dist/backend/index.d.ts +6 -0
- package/dist/backend/index.js +7 -0
- package/dist/backend/models.d.ts +15 -0
- package/dist/backend/models.js +43 -0
- package/dist/backend/paths.d.ts +6 -0
- package/dist/backend/paths.js +69 -0
- package/dist/backend/stdio.d.ts +66 -0
- package/dist/backend/stdio.js +172 -0
- package/dist/backend/types.d.ts +90 -0
- package/dist/backend/types.js +11 -0
- package/dist/backup-api.d.ts +10 -0
- package/dist/backup-api.js +40 -0
- package/dist/blob-store.d.ts +40 -0
- package/dist/blob-store.js +218 -0
- package/dist/cli-help.d.ts +1 -0
- package/dist/cli-help.js +63 -0
- package/dist/cli-output.d.ts +4 -0
- package/dist/cli-output.js +130 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +284 -0
- package/dist/client-pairing.d.ts +8 -0
- package/dist/client-pairing.js +30 -0
- package/dist/directories.d.ts +14 -0
- package/dist/directories.js +34 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +7 -0
- package/dist/events.d.ts +22 -0
- package/dist/events.js +155 -0
- package/dist/execution-chains.d.ts +24 -0
- package/dist/execution-chains.js +231 -0
- package/dist/execution-settings.d.ts +10 -0
- package/dist/execution-settings.js +38 -0
- package/dist/graph-document-history.d.ts +43 -0
- package/dist/graph-document-history.js +275 -0
- package/dist/graph-files.d.ts +14 -0
- package/dist/graph-files.js +59 -0
- package/dist/graph-transfer.d.ts +53 -0
- package/dist/graph-transfer.js +501 -0
- package/dist/graphs.d.ts +68 -0
- package/dist/graphs.js +771 -0
- package/dist/history.d.ts +20 -0
- package/dist/history.js +61 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +42 -0
- package/dist/image-credentials.d.ts +11 -0
- package/dist/image-credentials.js +130 -0
- package/dist/image-providers.d.ts +51 -0
- package/dist/image-providers.js +271 -0
- package/dist/input-materialization.d.ts +17 -0
- package/dist/input-materialization.js +95 -0
- package/dist/inputs.d.ts +74 -0
- package/dist/inputs.js +301 -0
- package/dist/local-control.d.ts +13 -0
- package/dist/local-control.js +37 -0
- package/dist/network.d.ts +11 -0
- package/dist/network.js +48 -0
- package/dist/openai-image-driver.d.ts +42 -0
- package/dist/openai-image-driver.js +368 -0
- package/dist/operations/backups.d.ts +40 -0
- package/dist/operations/backups.js +391 -0
- package/dist/operations/capacity.d.ts +7 -0
- package/dist/operations/capacity.js +35 -0
- package/dist/operations/cli-context.d.ts +7 -0
- package/dist/operations/cli-context.js +58 -0
- package/dist/operations/daemon.d.ts +36 -0
- package/dist/operations/daemon.js +159 -0
- package/dist/operations/files.d.ts +12 -0
- package/dist/operations/files.js +84 -0
- package/dist/operations/lifecycle.d.ts +28 -0
- package/dist/operations/lifecycle.js +155 -0
- package/dist/operations/local-admin.d.ts +6 -0
- package/dist/operations/local-admin.js +38 -0
- package/dist/operations/logs.d.ts +16 -0
- package/dist/operations/logs.js +37 -0
- package/dist/operations/plugins.d.ts +10 -0
- package/dist/operations/plugins.js +11 -0
- package/dist/operations/snapshot-worker.d.ts +1 -0
- package/dist/operations/snapshot-worker.js +18 -0
- package/dist/operations/snapshot.d.ts +1 -0
- package/dist/operations/snapshot.js +14 -0
- package/dist/operations/update.d.ts +26 -0
- package/dist/operations/update.js +216 -0
- package/dist/persistence/asset-business-schema.d.ts +2 -0
- package/dist/persistence/asset-business-schema.js +36 -0
- package/dist/persistence/database.d.ts +7 -0
- package/dist/persistence/database.js +91 -0
- package/dist/persistence/plugin-schema.d.ts +9 -0
- package/dist/persistence/plugin-schema.js +38 -0
- package/dist/persistence/repositories.d.ts +20 -0
- package/dist/persistence/repositories.js +106 -0
- package/dist/persistence/resource-schema.d.ts +2 -0
- package/dist/persistence/resource-schema.js +44 -0
- package/dist/persistence/schema.d.ts +6 -0
- package/dist/persistence/schema.js +236 -0
- package/dist/persistence/workflow-schema.d.ts +2 -0
- package/dist/persistence/workflow-schema.js +19 -0
- package/dist/plugin-api.d.ts +10 -0
- package/dist/plugin-api.js +40 -0
- package/dist/plugin-management.d.ts +36 -0
- package/dist/plugin-management.js +112 -0
- package/dist/plugins.d.ts +55 -0
- package/dist/plugins.js +239 -0
- package/dist/project-discovery.d.ts +3 -0
- package/dist/project-discovery.js +57 -0
- package/dist/project-files-api.d.ts +16 -0
- package/dist/project-files-api.js +243 -0
- package/dist/project-files.d.ts +97 -0
- package/dist/project-files.js +557 -0
- package/dist/project-lock.d.ts +4 -0
- package/dist/project-lock.js +35 -0
- package/dist/project-probe-worker.d.ts +1 -0
- package/dist/project-probe-worker.js +17 -0
- package/dist/project-probe.d.ts +22 -0
- package/dist/project-probe.js +67 -0
- package/dist/projects.d.ts +29 -0
- package/dist/projects.js +219 -0
- package/dist/publication.d.ts +31 -0
- package/dist/publication.js +642 -0
- package/dist/resource-api.d.ts +15 -0
- package/dist/resource-api.js +256 -0
- package/dist/resources-orphans.d.ts +41 -0
- package/dist/resources-orphans.js +235 -0
- package/dist/resources.d.ts +246 -0
- package/dist/resources.js +714 -0
- package/dist/run-api.d.ts +10 -0
- package/dist/run-api.js +308 -0
- package/dist/run-context.d.ts +54 -0
- package/dist/run-context.js +137 -0
- package/dist/runs.d.ts +82 -0
- package/dist/runs.js +506 -0
- package/dist/runtime-backend.d.ts +24 -0
- package/dist/runtime-backend.js +214 -0
- package/dist/runtime.d.ts +97 -0
- package/dist/runtime.js +553 -0
- package/dist/scheduler.d.ts +85 -0
- package/dist/scheduler.js +344 -0
- package/dist/service.d.ts +29 -0
- package/dist/service.js +515 -0
- package/dist/skills-api.d.ts +18 -0
- package/dist/skills-api.js +120 -0
- package/dist/storage-api.d.ts +21 -0
- package/dist/storage-api.js +92 -0
- package/dist/transfer-api.d.ts +10 -0
- package/dist/transfer-api.js +60 -0
- package/dist/vendor/protocol/contracts.typecheck.d.ts +1 -0
- package/dist/vendor/protocol/contracts.typecheck.js +35 -0
- package/dist/vendor/protocol/execution-chain.d.ts +6 -0
- package/dist/vendor/protocol/execution-chain.js +26 -0
- package/dist/vendor/protocol/graph-binary.d.ts +15 -0
- package/dist/vendor/protocol/graph-binary.js +121 -0
- package/dist/vendor/protocol/graph-title.d.ts +2 -0
- package/dist/vendor/protocol/graph-title.js +23 -0
- package/dist/vendor/protocol/index.d.ts +617 -0
- package/dist/vendor/protocol/index.js +20 -0
- package/dist/vendor/protocol/pairing.d.ts +29 -0
- package/dist/vendor/protocol/pairing.js +71 -0
- package/dist/vendor/protocol/preview.d.ts +2 -0
- package/dist/vendor/protocol/preview.js +9 -0
- package/dist/vendor/protocol/resource-mime.d.ts +2 -0
- package/dist/vendor/protocol/resource-mime.js +35 -0
- package/dist/video-thumbnail.d.ts +2 -0
- package/dist/video-thumbnail.js +34 -0
- package/package.json +9 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { BackendError } from './types.js';
|
|
2
|
+
import { MINIMUM_CODEX_VERSION } from './capabilities.js';
|
|
3
|
+
const stages = { initialize: '连接握手', 'model/list': '读取模型列表', 'thread/start': '启动任务会话', 'turn/start': '启动任务' };
|
|
4
|
+
/** Explain observed failures without forwarding stderr, backend messages, accounts or paths. */
|
|
5
|
+
export function describeCodexFailure(error, rpc, fallbackStage = 'initialize') {
|
|
6
|
+
const diagnostic = error instanceof BackendError ? error.diagnostic : undefined;
|
|
7
|
+
const method = diagnostic?.method;
|
|
8
|
+
const stage = method && Object.hasOwn(stages, method) ? method : fallbackStage;
|
|
9
|
+
const details = { source: 'codex', stage };
|
|
10
|
+
let reason = diagnostic?.reason;
|
|
11
|
+
let explanation;
|
|
12
|
+
let retryable = false;
|
|
13
|
+
const failure = rpc?.failure;
|
|
14
|
+
if (reason === 'unsupported_version') {
|
|
15
|
+
explanation = `Codex CLI ${diagnostic?.version ?? ''} 不支持任务执行所需协议,请升级到 ${MINIMUM_CODEX_VERSION} 或更高稳定版本。`;
|
|
16
|
+
details.minimumVersion = MINIMUM_CODEX_VERSION;
|
|
17
|
+
if (diagnostic?.version)
|
|
18
|
+
details.version = diagnostic.version;
|
|
19
|
+
}
|
|
20
|
+
else if (reason === 'invalid_handshake')
|
|
21
|
+
explanation = 'Codex 返回的连接握手数据无效,请检查 CLI 版本兼容性。';
|
|
22
|
+
else if (reason === 'invalid_catalog')
|
|
23
|
+
explanation = 'Codex 返回的模型目录、推理强度或分页数据格式无效,请检查 CLI 版本兼容性。';
|
|
24
|
+
else if (error instanceof BackendError && error.code === 'MODEL_UNAVAILABLE') {
|
|
25
|
+
reason = 'model_unavailable';
|
|
26
|
+
explanation = '所选模型或推理强度在当前 Codex 目录中不可用,请重新选择。';
|
|
27
|
+
}
|
|
28
|
+
else if (error instanceof BackendError && error.code === 'TIMEOUT') {
|
|
29
|
+
reason = 'rpc_timeout';
|
|
30
|
+
retryable = true;
|
|
31
|
+
explanation = `Codex 在 ${diagnostic?.timeoutMs ?? 15000} 毫秒内未响应。`;
|
|
32
|
+
if (diagnostic?.timeoutMs !== undefined)
|
|
33
|
+
details.timeoutMs = diagnostic.timeoutMs;
|
|
34
|
+
}
|
|
35
|
+
else if (failure?.kind === 'spawn_error') {
|
|
36
|
+
reason = failure.spawnCode === 'ENOENT' ? 'executable_not_found' : ['EACCES', 'EPERM'].includes(failure.spawnCode ?? '') ? 'execution_denied' : 'spawn_error';
|
|
37
|
+
explanation = reason === 'executable_not_found' ? '找不到 Codex 可执行程序,请在工作空间设备上安装 Codex CLI,并确认启动工作空间的进程能找到该命令。'
|
|
38
|
+
: reason === 'execution_denied' ? '操作系统拒绝执行 Codex,请检查工作空间账户的执行权限。'
|
|
39
|
+
: 'Codex 进程无法启动,请检查可执行程序及工作空间目录是否可用。';
|
|
40
|
+
if (failure.spawnCode)
|
|
41
|
+
details.spawnCode = failure.spawnCode;
|
|
42
|
+
}
|
|
43
|
+
else if (failure?.kind === 'process_exit') {
|
|
44
|
+
reason = 'process_exit';
|
|
45
|
+
retryable = true;
|
|
46
|
+
explanation = 'Codex 进程在请求完成前退出' + (failure.exitCode != null ? `,退出码 ${failure.exitCode}` : failure.exitSignal ? `,信号 ${failure.exitSignal}` : '') + '。';
|
|
47
|
+
if (failure.exitCode !== undefined)
|
|
48
|
+
details.exitCode = failure.exitCode;
|
|
49
|
+
if (failure.exitSignal !== undefined)
|
|
50
|
+
details.exitSignal = failure.exitSignal;
|
|
51
|
+
}
|
|
52
|
+
else if (failure?.kind === 'invalid_frame' || failure?.kind === 'frame_too_large') {
|
|
53
|
+
reason = failure.kind;
|
|
54
|
+
explanation = reason === 'invalid_frame' ? 'Codex 输出了无效的 JSON-RPC 消息。' : 'Codex 输出消息超过工作空间支持的大小限制。';
|
|
55
|
+
}
|
|
56
|
+
else if (diagnostic?.rpcCode !== undefined) {
|
|
57
|
+
const code = diagnostic.rpcCode;
|
|
58
|
+
details.rpcCode = code;
|
|
59
|
+
reason = code === -32601 ? 'unsupported_method' : code === -32602 ? 'invalid_arguments' : 'request_rejected';
|
|
60
|
+
explanation = code === -32601 ? `Codex 不支持此协议请求(${code}),请检查 CLI 版本兼容性。`
|
|
61
|
+
: code === -32602 ? `Codex 拒绝了请求参数(${code}),请检查 CLI 版本兼容性。`
|
|
62
|
+
: `Codex 拒绝了请求,协议错误码 ${code}。`;
|
|
63
|
+
}
|
|
64
|
+
else if (error instanceof BackendError && error.code === 'PROTOCOL') {
|
|
65
|
+
reason = 'invalid_response';
|
|
66
|
+
explanation = 'Codex 返回的协议响应无效。';
|
|
67
|
+
}
|
|
68
|
+
else if (failure?.kind === 'stdin_error' || failure?.kind === 'host_close' || error instanceof BackendError && error.code === 'UNAVAILABLE') {
|
|
69
|
+
reason = failure?.kind ?? 'disconnected';
|
|
70
|
+
retryable = true;
|
|
71
|
+
explanation = 'Codex 连接在请求完成前关闭。';
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
reason = 'unknown_error';
|
|
75
|
+
explanation = '发生未分类的连接错误,请检查工作空间设备上的 Codex 状态。';
|
|
76
|
+
}
|
|
77
|
+
details.reason = reason;
|
|
78
|
+
return { message: `Codex ${stages[stage]}失败:${explanation}`, details, retryable };
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=diagnostics.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './models.js';
|
|
3
|
+
export * from './capabilities.js';
|
|
4
|
+
export * from './adapter.js';
|
|
5
|
+
export { readRunOutput, validateRunPaths } from './paths.js';
|
|
6
|
+
export { StdioRpc } from './stdio.js';
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import type { ModelDefaults, ModelOption, ModelSelection } from "../vendor/protocol/index.js";
|
|
3
|
+
export interface BackendModel extends ModelOption {
|
|
4
|
+
defaultReasoningEffort: string | null;
|
|
5
|
+
}
|
|
6
|
+
export declare function freezeModelSelection(selection: ModelSelection | null, available: readonly BackendModel[]): Readonly<ModelSelection>;
|
|
7
|
+
/** Synchronous CAS only. Resolve/probe catalog before entering a scheduler transaction. */
|
|
8
|
+
export declare class ModelDefaultsStore {
|
|
9
|
+
private readonly db;
|
|
10
|
+
constructor(db: DatabaseSync);
|
|
11
|
+
private read;
|
|
12
|
+
get(available?: readonly BackendModel[]): ModelDefaults;
|
|
13
|
+
set(selection: ModelSelection, expectedRevision: number, available: readonly BackendModel[]): ModelDefaults;
|
|
14
|
+
freeze(override: ModelSelection | undefined, available: readonly BackendModel[]): Readonly<ModelSelection>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { BackendError } from './types.js';
|
|
2
|
+
export function freezeModelSelection(selection, available) {
|
|
3
|
+
const model = selection && available.find(option => option.id === selection.model);
|
|
4
|
+
if (!model)
|
|
5
|
+
throw new BackendError('MODEL_UNAVAILABLE', 'Select an available model explicitly; automatic fallback is disabled');
|
|
6
|
+
const effort = selection.reasoningEffort ?? model.defaultReasoningEffort;
|
|
7
|
+
if (effort !== null && !model.reasoningEfforts.includes(effort))
|
|
8
|
+
throw new BackendError('MODEL_UNAVAILABLE', 'Selected reasoning effort is unavailable');
|
|
9
|
+
return Object.freeze({ model: model.id, reasoningEffort: effort });
|
|
10
|
+
}
|
|
11
|
+
/** Synchronous CAS only. Resolve/probe catalog before entering a scheduler transaction. */
|
|
12
|
+
export class ModelDefaultsStore {
|
|
13
|
+
db;
|
|
14
|
+
constructor(db) {
|
|
15
|
+
this.db = db;
|
|
16
|
+
}
|
|
17
|
+
read() {
|
|
18
|
+
const row = this.db.prepare("SELECT value FROM settings WHERE key='modelDefaults'").get();
|
|
19
|
+
if (!row || typeof row['value'] !== 'string')
|
|
20
|
+
throw new BackendError('UNAVAILABLE', 'Missing modelDefaults setting');
|
|
21
|
+
const raw = row['value'];
|
|
22
|
+
const value = JSON.parse(raw);
|
|
23
|
+
if (value === null)
|
|
24
|
+
return { raw, revision: 0, selection: null };
|
|
25
|
+
if (!Number.isSafeInteger(value.revision) || value.revision < 0 || (value.selection !== null && (typeof value.selection?.model !== 'string' || !(value.selection.reasoningEffort === null || typeof value.selection.reasoningEffort === 'string'))))
|
|
26
|
+
throw new BackendError('PROTOCOL', 'Invalid persisted model defaults');
|
|
27
|
+
return { raw, revision: value.revision, selection: value.selection };
|
|
28
|
+
}
|
|
29
|
+
get(available = []) { const { revision, selection } = this.read(); return { revision, selection, available: available.map(({ id, reasoningEfforts, defaultReasoningEffort }) => ({ id, reasoningEfforts: [...reasoningEfforts], defaultReasoningEffort })) }; }
|
|
30
|
+
set(selection, expectedRevision, available) {
|
|
31
|
+
const frozen = freezeModelSelection(selection, available);
|
|
32
|
+
const current = this.read();
|
|
33
|
+
if (!Number.isSafeInteger(expectedRevision) || current.revision !== expectedRevision || expectedRevision >= Number.MAX_SAFE_INTEGER)
|
|
34
|
+
throw new BackendError('CONFLICT', 'Model defaults revision changed');
|
|
35
|
+
const next = { revision: expectedRevision + 1, selection: frozen };
|
|
36
|
+
const result = this.db.prepare("UPDATE settings SET value=? WHERE key='modelDefaults' AND value=?").run(JSON.stringify(next), current.raw);
|
|
37
|
+
if (result.changes !== 1)
|
|
38
|
+
throw new BackendError('CONFLICT', 'Model defaults revision changed');
|
|
39
|
+
return { ...next, available: available.map(({ id, reasoningEfforts, defaultReasoningEffort }) => ({ id, reasoningEfforts: [...reasoningEfforts], defaultReasoningEffort })) };
|
|
40
|
+
}
|
|
41
|
+
freeze(override, available) { return freezeModelSelection(override ?? this.read().selection, available); }
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=models.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type BackendRunContext } from './types.js';
|
|
2
|
+
export declare function within(root: string, path: string): boolean;
|
|
3
|
+
/** Runs outside DB transactions. No staging/rollback/Git operations here. */
|
|
4
|
+
export declare function validateRunPaths(context: BackendRunContext): Promise<void>;
|
|
5
|
+
/** Artifact paths remain untrusted even when a turn reports success. No absolute/escaping/symlink paths. */
|
|
6
|
+
export declare function readRunOutput(outputRoot: string, name: string, maxBytes?: number): Promise<Buffer>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { lstat, realpath, readdir, open } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { isAbsolute, relative, resolve, sep, join } from 'node:path';
|
|
4
|
+
import { BackendError } from './types.js';
|
|
5
|
+
export function within(root, path) { const rel = relative(root, path); return rel === '' || (rel !== '..' && !rel.startsWith('..' + sep) && !isAbsolute(rel)); }
|
|
6
|
+
async function directory(path) {
|
|
7
|
+
if (!isAbsolute(path))
|
|
8
|
+
throw new BackendError('INVALID_INPUT', 'Backend paths must be absolute');
|
|
9
|
+
const stat = await lstat(path);
|
|
10
|
+
const canonical = await realpath(path);
|
|
11
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || canonical !== resolve(path))
|
|
12
|
+
throw new BackendError('INVALID_INPUT', 'Use canonical non-symlink directories');
|
|
13
|
+
return canonical;
|
|
14
|
+
}
|
|
15
|
+
/** Runs outside DB transactions. No staging/rollback/Git operations here. */
|
|
16
|
+
export async function validateRunPaths(context) {
|
|
17
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(context.runId))
|
|
18
|
+
throw new BackendError('INVALID_INPUT', 'Invalid Run id');
|
|
19
|
+
const [project, input, output, service] = await Promise.all([context.projectPath, context.inputPath, context.outputPath, context.serviceRoot].map(directory));
|
|
20
|
+
if (!project || !input || !output || !service)
|
|
21
|
+
throw new BackendError('INVALID_INPUT', 'Missing Run paths');
|
|
22
|
+
if (within(project, service) || within(service, project))
|
|
23
|
+
throw new BackendError('INVALID_INPUT', 'Project and service data directories must not overlap');
|
|
24
|
+
const expected = join(service, 'runs', context.runId);
|
|
25
|
+
if (input !== join(expected, 'input') || output !== join(expected, 'output'))
|
|
26
|
+
throw new BackendError('INVALID_INPUT', 'Input/output must belong to exactly this Run');
|
|
27
|
+
// Walking ancestors detects a symlink in runs/<id>, too.
|
|
28
|
+
await directory(join(service, 'runs'));
|
|
29
|
+
await directory(expected);
|
|
30
|
+
if ((await readdir(output)).length)
|
|
31
|
+
throw new BackendError('INVALID_INPUT', 'A new Run requires an empty output directory');
|
|
32
|
+
}
|
|
33
|
+
/** Artifact paths remain untrusted even when a turn reports success. No absolute/escaping/symlink paths. */
|
|
34
|
+
export async function readRunOutput(outputRoot, name, maxBytes = 8 * 1024 * 1024) {
|
|
35
|
+
if (!name || isAbsolute(name) || name.includes('\0') || name.split(/[\/\\]/).some(part => part === '..' || part === '.'))
|
|
36
|
+
throw new BackendError('INVALID_INPUT', 'Invalid output path');
|
|
37
|
+
const root = await directory(outputRoot);
|
|
38
|
+
const path = resolve(root, name);
|
|
39
|
+
if (!within(root, path) || path === root)
|
|
40
|
+
throw new BackendError('INVALID_INPUT', 'Output escapes Run directory');
|
|
41
|
+
let parent = root;
|
|
42
|
+
const parts = relative(root, path).split(sep);
|
|
43
|
+
for (const component of parts.slice(0, -1)) {
|
|
44
|
+
parent = join(parent, component);
|
|
45
|
+
await directory(parent);
|
|
46
|
+
}
|
|
47
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
48
|
+
try {
|
|
49
|
+
const before = await handle.stat();
|
|
50
|
+
if (!before.isFile() || before.nlink !== 1 || before.size > maxBytes || before.size < 0)
|
|
51
|
+
throw new BackendError('INVALID_INPUT', 'Output must be a bounded regular non-hardlinked file');
|
|
52
|
+
const buffer = Buffer.alloc(before.size + 1);
|
|
53
|
+
let total = 0;
|
|
54
|
+
while (total < buffer.length) {
|
|
55
|
+
const { bytesRead } = await handle.read(buffer, total, buffer.length - total, total);
|
|
56
|
+
if (!bytesRead)
|
|
57
|
+
break;
|
|
58
|
+
total += bytesRead;
|
|
59
|
+
}
|
|
60
|
+
const after = await handle.stat();
|
|
61
|
+
if (total !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || await realpath(path) !== path)
|
|
62
|
+
throw new BackendError('INVALID_INPUT', 'Output changed during collection');
|
|
63
|
+
return buffer.subarray(0, total);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
await handle.close();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export type RpcId = string | number;
|
|
2
|
+
export type RpcMessage = {
|
|
3
|
+
id?: RpcId;
|
|
4
|
+
method?: string;
|
|
5
|
+
params?: any;
|
|
6
|
+
result?: any;
|
|
7
|
+
error?: {
|
|
8
|
+
code: number;
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export interface StdioOptions {
|
|
13
|
+
executable?: string;
|
|
14
|
+
args?: string[];
|
|
15
|
+
cwd: string;
|
|
16
|
+
env?: NodeJS.ProcessEnv;
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
initializeTimeoutMs?: number;
|
|
19
|
+
maxFrameBytes?: number;
|
|
20
|
+
}
|
|
21
|
+
/** Bounded diagnostic categories only; never include stderr, frames, commands or paths. */
|
|
22
|
+
export interface TransportFailure {
|
|
23
|
+
kind: "process_exit" | "spawn_error" | "stdin_error" | "frame_too_large" | "invalid_frame" | "rpc_timeout" | "host_close";
|
|
24
|
+
spawnCode?: 'ENOENT' | 'EACCES' | 'EPERM' | 'ENOEXEC';
|
|
25
|
+
exitCode?: number | null;
|
|
26
|
+
exitSignal?: string | null;
|
|
27
|
+
observedFrameBytes?: number;
|
|
28
|
+
}
|
|
29
|
+
/** npm's Windows .cmd shim cannot be spawned with shell:false. Use its JS entry. */
|
|
30
|
+
export declare function codexCommand(options: StdioOptions, platform?: NodeJS.Platform): {
|
|
31
|
+
executable: string;
|
|
32
|
+
args: string[];
|
|
33
|
+
};
|
|
34
|
+
/** Private wire transport: never forward raw backend messages/errors/stderr to HTTP. */
|
|
35
|
+
export declare class StdioRpc {
|
|
36
|
+
private readonly options;
|
|
37
|
+
private child;
|
|
38
|
+
private nextId;
|
|
39
|
+
private buffer;
|
|
40
|
+
private bufferBytes;
|
|
41
|
+
private decoder;
|
|
42
|
+
private stopped;
|
|
43
|
+
private exited;
|
|
44
|
+
private failureKind;
|
|
45
|
+
private exitCode;
|
|
46
|
+
private exitSignal;
|
|
47
|
+
private observedFrameBytes;
|
|
48
|
+
private spawnCode;
|
|
49
|
+
private pending;
|
|
50
|
+
onNotification: (method: string, params: any) => void;
|
|
51
|
+
onRequest: (id: RpcId, method: string, params: any) => void;
|
|
52
|
+
onDisconnect: () => void;
|
|
53
|
+
get isClosed(): boolean;
|
|
54
|
+
/** Exit is stronger evidence than a broken stdio pipe: this child can no longer run a turn. */
|
|
55
|
+
get isExited(): boolean;
|
|
56
|
+
get failure(): TransportFailure | null;
|
|
57
|
+
constructor(options: StdioOptions);
|
|
58
|
+
private receive;
|
|
59
|
+
private write;
|
|
60
|
+
request(method: string, params: unknown): Promise<any>;
|
|
61
|
+
notify(method: string): void;
|
|
62
|
+
reply(id: RpcId, result: unknown): void;
|
|
63
|
+
reject(id: RpcId): void;
|
|
64
|
+
private fail;
|
|
65
|
+
close(): void;
|
|
66
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
5
|
+
import { BackendError } from './types.js';
|
|
6
|
+
const DEFAULT_MAX_FRAME_BYTES = 32 * 1024 * 1024;
|
|
7
|
+
/** npm's Windows .cmd shim cannot be spawned with shell:false. Use its JS entry. */
|
|
8
|
+
export function codexCommand(options, platform = process.platform) {
|
|
9
|
+
const executable = options.executable ?? 'codex';
|
|
10
|
+
const args = options.args ?? ['app-server', '--stdio'];
|
|
11
|
+
if (platform !== 'win32')
|
|
12
|
+
return { executable, args };
|
|
13
|
+
const env = options.env ?? process.env;
|
|
14
|
+
const path = Object.entries(env).find(([key]) => key.toLowerCase() === 'path')?.[1] ?? '';
|
|
15
|
+
const directories = executable === 'codex' ? path.split(';').filter(Boolean).map(p => p.replace(/^"|"$/g, '')) : /(?:^|[\\/])codex[.](?:cmd|ps1)$/i.test(executable) && isAbsolute(executable) ? [dirname(executable)] : [];
|
|
16
|
+
// A running Runtime retains its original PATH when npm installs Codex later.
|
|
17
|
+
// npm's default per-user bin directory remains discoverable through APPDATA.
|
|
18
|
+
if (executable === 'codex' && env.APPDATA)
|
|
19
|
+
directories.push(join(env.APPDATA, 'npm'));
|
|
20
|
+
for (const directory of directories) {
|
|
21
|
+
const native = join(directory, 'codex.exe');
|
|
22
|
+
if (existsSync(native))
|
|
23
|
+
return { executable: native, args };
|
|
24
|
+
const entry = join(directory, 'node_modules', '@openai', 'codex', 'bin', 'codex.js');
|
|
25
|
+
if (existsSync(entry))
|
|
26
|
+
return { executable: process.execPath, args: [entry, ...args] };
|
|
27
|
+
}
|
|
28
|
+
return { executable, args };
|
|
29
|
+
}
|
|
30
|
+
/** Private wire transport: never forward raw backend messages/errors/stderr to HTTP. */
|
|
31
|
+
export class StdioRpc {
|
|
32
|
+
options;
|
|
33
|
+
child;
|
|
34
|
+
nextId = 0;
|
|
35
|
+
buffer = '';
|
|
36
|
+
bufferBytes = 0;
|
|
37
|
+
decoder = new StringDecoder('utf8');
|
|
38
|
+
stopped = false;
|
|
39
|
+
exited = false;
|
|
40
|
+
failureKind = null;
|
|
41
|
+
exitCode = null;
|
|
42
|
+
exitSignal = null;
|
|
43
|
+
observedFrameBytes;
|
|
44
|
+
spawnCode;
|
|
45
|
+
pending = new Map();
|
|
46
|
+
onNotification = () => { };
|
|
47
|
+
onRequest = id => this.reject(id);
|
|
48
|
+
onDisconnect = () => { };
|
|
49
|
+
get isClosed() { return this.stopped; }
|
|
50
|
+
/** Exit is stronger evidence than a broken stdio pipe: this child can no longer run a turn. */
|
|
51
|
+
get isExited() { return this.exited; }
|
|
52
|
+
get failure() { return this.failureKind ? { kind: this.failureKind, ...(this.spawnCode ? { spawnCode: this.spawnCode } : {}), ...(this.exited ? { exitCode: this.exitCode, exitSignal: this.exitSignal } : {}), ...(this.observedFrameBytes !== undefined ? { observedFrameBytes: this.observedFrameBytes } : {}) } : null; }
|
|
53
|
+
constructor(options) {
|
|
54
|
+
this.options = options;
|
|
55
|
+
const command = codexCommand(options);
|
|
56
|
+
this.child = spawn(command.executable, command.args, { cwd: options.cwd, env: options.env ?? process.env, stdio: ['pipe', 'pipe', 'pipe'], shell: false, windowsHide: true });
|
|
57
|
+
this.child.stderr.resume(); // Drain, but never persist credentials/backend diagnostics.
|
|
58
|
+
this.child.on('error', (error) => {
|
|
59
|
+
if (['ENOENT', 'EACCES', 'EPERM', 'ENOEXEC'].includes(error.code ?? ''))
|
|
60
|
+
this.spawnCode = error.code;
|
|
61
|
+
this.fail('spawn_error');
|
|
62
|
+
});
|
|
63
|
+
this.child.on('exit', (code, signal) => { this.exited = true; this.exitCode = code; this.exitSignal = signal; this.fail('process_exit'); });
|
|
64
|
+
this.child.stdin.on('error', () => this.fail('stdin_error'));
|
|
65
|
+
this.child.stdout.on('data', (chunk) => {
|
|
66
|
+
const decoded = this.decoder.write(chunk);
|
|
67
|
+
this.buffer += decoded;
|
|
68
|
+
this.bufferBytes += Buffer.byteLength(decoded);
|
|
69
|
+
const max = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES;
|
|
70
|
+
let end;
|
|
71
|
+
while ((end = this.buffer.indexOf('\n')) >= 0) {
|
|
72
|
+
const line = this.buffer.slice(0, end);
|
|
73
|
+
this.buffer = this.buffer.slice(end + 1);
|
|
74
|
+
this.bufferBytes -= Buffer.byteLength(line) + 1;
|
|
75
|
+
if (Buffer.byteLength(line) > max) {
|
|
76
|
+
this.observedFrameBytes = Buffer.byteLength(line);
|
|
77
|
+
this.fail('frame_too_large');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (!line.trim())
|
|
81
|
+
continue;
|
|
82
|
+
try {
|
|
83
|
+
const message = JSON.parse(line);
|
|
84
|
+
this.receive(message);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
this.fail('invalid_frame');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (this.bufferBytes > max) {
|
|
92
|
+
this.observedFrameBytes = this.bufferBytes;
|
|
93
|
+
this.fail('frame_too_large');
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
receive(m) {
|
|
98
|
+
if (!m || typeof m !== 'object' || Array.isArray(m))
|
|
99
|
+
throw new Error('Invalid frame');
|
|
100
|
+
if (m.id !== undefined && typeof m.id !== 'string' && typeof m.id !== 'number')
|
|
101
|
+
throw new Error('Invalid id');
|
|
102
|
+
if (typeof m.method === 'string') {
|
|
103
|
+
if (m.id !== undefined)
|
|
104
|
+
this.onRequest(m.id, m.method, m.params);
|
|
105
|
+
else
|
|
106
|
+
this.onNotification(m.method, m.params);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (m.id === undefined)
|
|
110
|
+
throw new Error('Missing id');
|
|
111
|
+
const pending = this.pending.get(m.id);
|
|
112
|
+
if (!pending)
|
|
113
|
+
return;
|
|
114
|
+
this.pending.delete(m.id);
|
|
115
|
+
clearTimeout(pending.timer);
|
|
116
|
+
if (m.error)
|
|
117
|
+
pending.reject(new BackendError('PROTOCOL', 'Backend rejected request', { method: pending.method, ...(Number.isSafeInteger(m.error.code) ? { rpcCode: m.error.code } : {}) }));
|
|
118
|
+
else if ('result' in m)
|
|
119
|
+
pending.resolve(m.result);
|
|
120
|
+
else
|
|
121
|
+
pending.reject(new BackendError('PROTOCOL', 'Missing backend result', { method: pending.method }));
|
|
122
|
+
}
|
|
123
|
+
write(message) {
|
|
124
|
+
if (this.stopped)
|
|
125
|
+
throw new BackendError('UNAVAILABLE', 'Backend connection closed');
|
|
126
|
+
const line = JSON.stringify(message) + '\n';
|
|
127
|
+
if (Buffer.byteLength(line) > (this.options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES) || this.child.stdin.writableLength > 2 * 1024 * 1024)
|
|
128
|
+
throw new BackendError('PROTOCOL', 'Backend message limit exceeded');
|
|
129
|
+
this.child.stdin.write(line);
|
|
130
|
+
}
|
|
131
|
+
request(method, params) {
|
|
132
|
+
if (this.pending.size >= 128)
|
|
133
|
+
return Promise.reject(new BackendError('PROTOCOL', 'Too many backend requests'));
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const id = ++this.nextId;
|
|
136
|
+
const timeoutMs = method === 'initialize' ? this.options.initializeTimeoutMs ?? this.options.timeoutMs ?? 60000 : this.options.timeoutMs ?? 15000;
|
|
137
|
+
const timer = setTimeout(() => { this.pending.delete(id); reject(new BackendError('TIMEOUT', 'Backend request timed out; outcome unknown', { method, timeoutMs })); this.fail('rpc_timeout'); }, timeoutMs);
|
|
138
|
+
this.pending.set(id, { resolve, reject, timer, method });
|
|
139
|
+
try {
|
|
140
|
+
this.write({ id, method, params });
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
this.pending.delete(id);
|
|
145
|
+
reject(error);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
notify(method) { this.write({ method }); }
|
|
150
|
+
reply(id, result) { this.write({ id, result }); }
|
|
151
|
+
reject(id) { this.write({ id, error: { code: -32601, message: 'Unsupported backend request' } }); }
|
|
152
|
+
fail(kind) {
|
|
153
|
+
if (!this.failureKind)
|
|
154
|
+
this.failureKind = kind;
|
|
155
|
+
if (this.stopped)
|
|
156
|
+
return;
|
|
157
|
+
this.stopped = true;
|
|
158
|
+
for (const p of this.pending.values()) {
|
|
159
|
+
clearTimeout(p.timer);
|
|
160
|
+
p.reject(new BackendError('UNAVAILABLE', 'Backend disconnected; outcome unknown', { method: p.method }));
|
|
161
|
+
}
|
|
162
|
+
this.pending.clear();
|
|
163
|
+
this.child.kill('SIGTERM');
|
|
164
|
+
const child = this.child;
|
|
165
|
+
const killTimer = setTimeout(() => { if (child.exitCode === null && child.signalCode === null)
|
|
166
|
+
child.kill('SIGKILL'); }, 1000);
|
|
167
|
+
killTimer.unref();
|
|
168
|
+
this.onDisconnect();
|
|
169
|
+
}
|
|
170
|
+
close() { this.fail("host_close"); }
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=stdio.js.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ImageGenerationOptions, ModelSelection, ProjectFileInput, ResourceEnvelope, SandboxMode } from "../vendor/protocol/index.js";
|
|
2
|
+
import type { TransportFailure } from './stdio.js';
|
|
3
|
+
import type { RunContextRequest, RunLineage } from '../run-context.js';
|
|
4
|
+
export type BackendState = 'starting' | 'running' | 'waiting_answer' | 'waiting_approval' | 'cancelling' | 'completed' | 'failed' | 'cancelled' | 'interrupted' | 'unknown';
|
|
5
|
+
export interface BackendProgress {
|
|
6
|
+
itemId: string;
|
|
7
|
+
phase: 'commentary' | 'final_answer';
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
export interface RuntimeSnapshot {
|
|
11
|
+
runId: string;
|
|
12
|
+
threadId: string | null;
|
|
13
|
+
turnId: string | null;
|
|
14
|
+
state: BackendState;
|
|
15
|
+
observedAt: string;
|
|
16
|
+
model: ModelSelection;
|
|
17
|
+
reason: string | null;
|
|
18
|
+
answer: string;
|
|
19
|
+
progress?: BackendProgress;
|
|
20
|
+
transportFailure?: TransportFailure;
|
|
21
|
+
}
|
|
22
|
+
export interface BackendQuestion {
|
|
23
|
+
id: string;
|
|
24
|
+
text: string;
|
|
25
|
+
options: string[];
|
|
26
|
+
}
|
|
27
|
+
export interface BackendInteraction {
|
|
28
|
+
id: string;
|
|
29
|
+
kind: 'question' | 'approval';
|
|
30
|
+
questions: BackendQuestion[];
|
|
31
|
+
reason: string;
|
|
32
|
+
command: string | null;
|
|
33
|
+
canApprove: boolean;
|
|
34
|
+
}
|
|
35
|
+
export type BackendReply = {
|
|
36
|
+
kind: 'question';
|
|
37
|
+
answers: Record<string, string[]>;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'approval';
|
|
40
|
+
decision: 'accept' | 'decline';
|
|
41
|
+
};
|
|
42
|
+
/** Trusted service-resolved paths, never directly populated from a browser payload. */
|
|
43
|
+
export interface BackendRunContext {
|
|
44
|
+
runId: string;
|
|
45
|
+
kind: 'execution' | 'text_generation' | 'image_generation';
|
|
46
|
+
projectPath: string;
|
|
47
|
+
inputPath: string;
|
|
48
|
+
outputPath: string;
|
|
49
|
+
serviceRoot: string;
|
|
50
|
+
prompt: string;
|
|
51
|
+
model: ModelSelection;
|
|
52
|
+
imageOptions?: ImageGenerationOptions;
|
|
53
|
+
sandboxMode?: SandboxMode;
|
|
54
|
+
resources?: ResourceEnvelope[];
|
|
55
|
+
projectFiles?: ProjectFileInput[];
|
|
56
|
+
files?: {
|
|
57
|
+
resourceId: string;
|
|
58
|
+
version: number;
|
|
59
|
+
path: string;
|
|
60
|
+
sha256: string;
|
|
61
|
+
}[];
|
|
62
|
+
upstreamContext?: RunLineage;
|
|
63
|
+
}
|
|
64
|
+
export interface BackendCallbacks {
|
|
65
|
+
onSnapshot(snapshot: RuntimeSnapshot): void | Promise<void>;
|
|
66
|
+
onInteraction(interaction: BackendInteraction): void | Promise<void>;
|
|
67
|
+
/** Must authorize against this Run's project; no global graph/database endpoint. */
|
|
68
|
+
queryHistory?(query: string): Promise<string>;
|
|
69
|
+
/** Exact, paginated read; project scope is derived from the calling Run. */
|
|
70
|
+
readRunContext?(request: RunContextRequest): Promise<string>;
|
|
71
|
+
}
|
|
72
|
+
export interface BackendAdapter {
|
|
73
|
+
start(context: BackendRunContext, callbacks: BackendCallbacks): Promise<RuntimeSnapshot>;
|
|
74
|
+
cancel(runId: string): Promise<RuntimeSnapshot>;
|
|
75
|
+
respond(runId: string, interactionId: string, reply: BackendReply): Promise<RuntimeSnapshot>;
|
|
76
|
+
reconcile(runId: string): Promise<RuntimeSnapshot>;
|
|
77
|
+
}
|
|
78
|
+
/** Only bounded metadata; raw backend messages may contain credentials and paths. */
|
|
79
|
+
export interface BackendDiagnostic {
|
|
80
|
+
reason?: 'unsupported_version' | 'invalid_handshake' | 'invalid_catalog';
|
|
81
|
+
method?: string;
|
|
82
|
+
timeoutMs?: number;
|
|
83
|
+
rpcCode?: number;
|
|
84
|
+
version?: string;
|
|
85
|
+
}
|
|
86
|
+
export declare class BackendError extends Error {
|
|
87
|
+
readonly code: 'UNAVAILABLE' | 'PROTOCOL' | 'TIMEOUT' | 'CONFLICT' | 'INVALID_INPUT' | 'MODEL_UNAVAILABLE' | 'ISOLATION_UNVERIFIED';
|
|
88
|
+
readonly diagnostic?: BackendDiagnostic | undefined;
|
|
89
|
+
constructor(code: 'UNAVAILABLE' | 'PROTOCOL' | 'TIMEOUT' | 'CONFLICT' | 'INVALID_INPUT' | 'MODEL_UNAVAILABLE' | 'ISOLATION_UNVERIFIED', message: string, diagnostic?: BackendDiagnostic | undefined);
|
|
90
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
import type { ApiResult } from './api.js';
|
|
3
|
+
import { Auth } from './auth.js';
|
|
4
|
+
import { Backups } from './operations/backups.js';
|
|
5
|
+
export declare class BackupApi {
|
|
6
|
+
readonly backups: Backups;
|
|
7
|
+
readonly auth: Auth;
|
|
8
|
+
constructor(backups: Backups, auth: Auth);
|
|
9
|
+
handle(request: IncomingMessage, path: string, token: string, origin: string): Promise<ApiResult>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Auth } from './auth.js';
|
|
2
|
+
import { Backups } from './operations/backups.js';
|
|
3
|
+
import { readJson } from './http.js';
|
|
4
|
+
import { ServiceError } from './errors.js';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
export class BackupApi {
|
|
7
|
+
backups;
|
|
8
|
+
auth;
|
|
9
|
+
constructor(backups, auth) {
|
|
10
|
+
this.backups = backups;
|
|
11
|
+
this.auth = auth;
|
|
12
|
+
}
|
|
13
|
+
async handle(request, path, token, origin) {
|
|
14
|
+
if (path === '/v1/restore-guidance' && request.method === 'GET')
|
|
15
|
+
return this.auth.withSession(token, origin, () => ({ handled: true, body: { localOnly: true, command: 'openworkgraph restore apply <backup-path> --sha256 <verified-sha256> --data-dir <service-data-dir>' } }));
|
|
16
|
+
const match = /^[/]v1[/]backups(?:[/]([a-f0-9]{64})[/](download))?$/.exec(path);
|
|
17
|
+
if (!match)
|
|
18
|
+
return { handled: false };
|
|
19
|
+
const principal = this.auth.withSession(token, origin, s => s.id);
|
|
20
|
+
if (request.method === 'GET') {
|
|
21
|
+
if (match[1]) {
|
|
22
|
+
const value = await this.backups.readDownload(match[1]);
|
|
23
|
+
return this.auth.withSession(token, origin, () => ({ handled: true, binary: value.body, headers: { 'Content-Type': value.contentType, 'Content-Length': String(value.body.length), 'Content-Disposition': 'attachment; filename="' + value.filename + '"' } }));
|
|
24
|
+
}
|
|
25
|
+
const value = await this.backups.list();
|
|
26
|
+
return this.auth.withSession(token, origin, () => ({ handled: true, body: value }));
|
|
27
|
+
}
|
|
28
|
+
if (request.method === 'POST' && !match[1]) {
|
|
29
|
+
const body = await readJson(request);
|
|
30
|
+
if (Object.keys(body).length !== 1 || typeof body.idempotencyKey !== 'string' || !body.idempotencyKey || body.idempotencyKey.length > 256)
|
|
31
|
+
throw new ServiceError('INVALID_REQUEST', '备份需要幂等键。');
|
|
32
|
+
this.auth.withSession(token, origin, () => undefined);
|
|
33
|
+
const key = createHash('sha256').update(principal + ':' + body.idempotencyKey).digest('hex');
|
|
34
|
+
const value = await this.backups.create(key);
|
|
35
|
+
return this.auth.withSession(token, origin, () => ({ handled: true, body: value }));
|
|
36
|
+
}
|
|
37
|
+
throw new ServiceError('NOT_FOUND', '备份接口不存在。');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=backup-api.js.map
|