@hraness/sponge 0.1.0 → 0.2.0
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 +163 -17
- package/bin/sponge.js +9 -2
- package/dist/contracts/index.d.ts +164 -0
- package/dist/contracts/index.js +6 -0
- package/dist/contracts/validate.d.ts +14 -0
- package/dist/contracts/validate.js +187 -0
- package/dist/migrations/001.d.ts +3 -0
- package/dist/migrations/001.js +33 -0
- package/dist/migrations/002.d.ts +3 -0
- package/dist/migrations/002.js +6 -0
- package/dist/migrations/003.d.ts +3 -0
- package/dist/migrations/003.js +10 -0
- package/dist/migrations/004.d.ts +3 -0
- package/dist/migrations/004.js +9 -0
- package/dist/src/acquisition/capture.d.ts +29 -0
- package/dist/src/acquisition/capture.js +160 -0
- package/dist/src/acquisition/cleanup.d.ts +6 -0
- package/dist/src/acquisition/cleanup.js +20 -0
- package/dist/src/acquisition/errors.d.ts +3 -0
- package/dist/src/acquisition/errors.js +20 -0
- package/dist/src/acquisition/files.d.ts +4 -0
- package/dist/src/acquisition/files.js +49 -0
- package/dist/src/acquisition/index.d.ts +6 -0
- package/dist/src/acquisition/index.js +4 -0
- package/dist/src/acquisition/lock.d.ts +1 -0
- package/dist/src/acquisition/lock.js +2 -0
- package/dist/src/acquisition/normalize.d.ts +11 -0
- package/dist/src/acquisition/normalize.js +62 -0
- package/dist/src/acquisition/pins.d.ts +3 -0
- package/dist/src/acquisition/pins.js +9 -0
- package/dist/src/acquisition/process.d.ts +16 -0
- package/dist/src/acquisition/process.js +63 -0
- package/dist/src/acquisition/runtime.d.ts +53 -0
- package/dist/src/acquisition/runtime.js +216 -0
- package/dist/src/acquisition/verify-helper.d.ts +1 -0
- package/dist/src/acquisition/verify-helper.js +19 -0
- package/dist/src/app/article.d.ts +23 -0
- package/dist/src/app/article.js +82 -0
- package/dist/src/app/bundle-files.d.ts +8 -0
- package/dist/src/app/bundle-files.js +66 -0
- package/dist/src/app/markdown.d.ts +6 -0
- package/dist/src/app/markdown.js +90 -0
- package/dist/src/app/source.d.ts +15 -0
- package/dist/src/app/source.js +40 -0
- package/dist/src/cli/arguments.d.ts +9 -0
- package/dist/src/cli/arguments.js +106 -0
- package/dist/src/cli/config.d.ts +8 -0
- package/dist/src/cli/config.js +45 -0
- package/dist/src/cli/import.d.ts +4 -0
- package/dist/src/cli/import.js +3 -0
- package/dist/src/cli/indexing.d.ts +7 -0
- package/dist/src/cli/indexing.js +21 -0
- package/dist/src/cli/main.d.ts +1 -0
- package/dist/src/cli/main.js +333 -0
- package/dist/src/cli/portability.d.ts +9 -0
- package/dist/src/cli/portability.js +39 -0
- package/dist/src/cli/queue.d.ts +2 -0
- package/dist/src/cli/queue.js +23 -0
- package/dist/src/cli/terminal.d.ts +2 -0
- package/dist/src/cli/terminal.js +4 -0
- package/dist/src/cli/worker.d.ts +16 -0
- package/dist/src/cli/worker.js +62 -0
- package/dist/src/importers/apply.d.ts +29 -0
- package/dist/src/importers/apply.js +85 -0
- package/dist/src/importers/custody.d.ts +6 -0
- package/dist/src/importers/custody.js +35 -0
- package/dist/src/importers/inspect.d.ts +21 -0
- package/dist/src/importers/inspect.js +97 -0
- package/dist/src/importers/manifest.d.ts +22 -0
- package/dist/src/importers/manifest.js +43 -0
- package/dist/src/importers/verify-helper.d.ts +1 -0
- package/dist/src/importers/verify-helper.js +27 -0
- package/dist/src/indexing/image.d.ts +10 -0
- package/dist/src/indexing/image.js +34 -0
- package/dist/src/indexing/index.d.ts +53 -0
- package/dist/src/indexing/index.js +123 -0
- package/dist/src/indexing/jobs.d.ts +14 -0
- package/dist/src/indexing/jobs.js +49 -0
- package/dist/src/indexing/runner.d.ts +23 -0
- package/dist/src/indexing/runner.js +114 -0
- package/dist/src/inference/asr/bridge.d.ts +1 -0
- package/dist/src/inference/asr/bridge.js +16 -0
- package/dist/src/inference/asr/protocol.d.ts +23 -0
- package/dist/src/inference/asr/protocol.js +60 -0
- package/dist/src/inference/asr/runtime.d.ts +29 -0
- package/dist/src/inference/asr/runtime.js +103 -0
- package/dist/src/inference/asr/windows.d.ts +9 -0
- package/dist/src/inference/asr/windows.js +41 -0
- package/dist/src/inference/embedding/index.d.ts +47 -0
- package/dist/src/inference/embedding/index.js +122 -0
- package/dist/src/media/capture-helper.d.ts +1 -0
- package/dist/src/media/capture-helper.js +16 -0
- package/dist/src/media/capture.d.ts +32 -0
- package/dist/src/media/capture.js +193 -0
- package/dist/src/media/handler.d.ts +22 -0
- package/dist/src/media/handler.js +82 -0
- package/dist/src/media/transcribe.d.ts +29 -0
- package/dist/src/media/transcribe.js +93 -0
- package/dist/src/metadata/article.d.ts +5 -0
- package/dist/src/metadata/article.js +51 -0
- package/dist/src/models/artifacts.d.ts +36 -0
- package/dist/src/models/artifacts.js +274 -0
- package/dist/src/models/catalog.d.ts +35 -0
- package/dist/src/models/catalog.js +71 -0
- package/dist/src/models/download.d.ts +9 -0
- package/dist/src/models/download.js +147 -0
- package/dist/src/models/runtime.d.ts +24 -0
- package/dist/src/models/runtime.js +37 -0
- package/dist/src/pdf/capture-helper.d.ts +1 -0
- package/dist/src/pdf/capture-helper.js +12 -0
- package/dist/src/pdf/capture.d.ts +29 -0
- package/dist/src/pdf/capture.js +156 -0
- package/dist/src/pdf/errors.d.ts +2 -0
- package/dist/src/pdf/errors.js +13 -0
- package/dist/src/pdf/handler.d.ts +24 -0
- package/dist/src/pdf/handler.js +84 -0
- package/dist/src/pdf/index.d.ts +4 -0
- package/dist/src/pdf/index.js +4 -0
- package/dist/src/pdf/input.d.ts +2 -0
- package/dist/src/pdf/input.js +46 -0
- package/dist/src/pdf/normalize.d.ts +10 -0
- package/dist/src/pdf/normalize.js +64 -0
- package/dist/src/pdf/source-identity.d.ts +4 -0
- package/dist/src/pdf/source-identity.js +12 -0
- package/dist/src/pdf/source.d.ts +6 -0
- package/dist/src/pdf/source.js +22 -0
- package/dist/src/pdf/verify-helper.d.ts +1 -0
- package/dist/src/pdf/verify-helper.js +17 -0
- package/dist/src/portability/backup.d.ts +23 -0
- package/dist/src/portability/backup.js +138 -0
- package/dist/src/portability/database.d.ts +14 -0
- package/dist/src/portability/database.js +78 -0
- package/dist/src/portability/export.d.ts +8 -0
- package/dist/src/portability/export.js +36 -0
- package/dist/src/portability/files.d.ts +33 -0
- package/dist/src/portability/files.js +78 -0
- package/dist/src/portability/gc.d.ts +21 -0
- package/dist/src/portability/gc.js +66 -0
- package/dist/src/search/extension.d.ts +3 -0
- package/dist/src/search/extension.js +17 -0
- package/dist/src/search/hybrid.d.ts +39 -0
- package/dist/src/search/hybrid.js +118 -0
- package/dist/src/search/query.d.ts +12 -0
- package/dist/src/search/query.js +47 -0
- package/dist/src/search/vector.d.ts +21 -0
- package/dist/src/search/vector.js +21 -0
- package/dist/src/service/launch-agent.d.ts +47 -0
- package/dist/src/service/launch-agent.js +179 -0
- package/dist/src/service/lifetime.d.ts +7 -0
- package/dist/src/service/lifetime.js +73 -0
- package/dist/src/service/loop.d.ts +21 -0
- package/dist/src/service/loop.js +94 -0
- package/dist/src/store/files.d.ts +12 -0
- package/dist/src/store/files.js +101 -0
- package/dist/src/store/index.d.ts +118 -0
- package/dist/src/store/index.js +727 -0
- package/dist/src/store/types.d.ts +90 -0
- package/dist/src/store/types.js +8 -0
- package/dist/src/worker/basic.d.ts +10 -0
- package/dist/src/worker/basic.js +27 -0
- package/dist/src/worker/custody.d.ts +13 -0
- package/dist/src/worker/custody.js +185 -0
- package/dist/src/worker/engine.d.ts +28 -0
- package/dist/src/worker/engine.js +78 -0
- package/dist/src/worker/identity.d.ts +7 -0
- package/dist/src/worker/identity.js +27 -0
- package/dist/src/worker/journal.d.ts +5 -0
- package/dist/src/worker/journal.js +30 -0
- package/dist/src/worker/supervisor-child.d.ts +1 -0
- package/dist/src/worker/supervisor-child.js +33 -0
- package/dist/src/worker/supervisor.d.ts +3 -0
- package/dist/src/worker/supervisor.js +47 -0
- package/native/runtime-manifest.json +40 -0
- package/package.json +36 -6
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Additive retry evidence; schema 1 and its checksum remain immutable. */
|
|
2
|
+
export const version = 2;
|
|
3
|
+
export const sql = `
|
|
4
|
+
ALTER TABLE jobs ADD COLUMN retryable INTEGER NOT NULL DEFAULT 0 CHECK(retryable IN (0,1));
|
|
5
|
+
ALTER TABLE jobs ADD COLUMN retry_after_ms INTEGER CHECK(retry_after_ms>=0 AND retry_after_ms<=86400000);
|
|
6
|
+
`;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Optional provenance preserves legacy rows and earlier migration checksums. */
|
|
2
|
+
export declare const version = 3;
|
|
3
|
+
export declare const sql = "\nALTER TABLE metadata_claims ADD COLUMN observed_json TEXT;\nALTER TABLE metadata_claims ADD COLUMN locator TEXT;\nALTER TABLE metadata_claims ADD COLUMN selected INTEGER CHECK(selected IN (0,1));\nALTER TABLE metadata_claims ADD COLUMN extraction_version TEXT;\nALTER TABLE metadata_claims ADD COLUMN date_precision TEXT CHECK(date_precision IN ('date','datetime','invalid'));\nALTER TABLE revisions ADD COLUMN metadata_warnings_json TEXT NOT NULL DEFAULT '[]';\n";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Optional provenance preserves legacy rows and earlier migration checksums. */
|
|
2
|
+
export const version = 3;
|
|
3
|
+
export const sql = `
|
|
4
|
+
ALTER TABLE metadata_claims ADD COLUMN observed_json TEXT;
|
|
5
|
+
ALTER TABLE metadata_claims ADD COLUMN locator TEXT;
|
|
6
|
+
ALTER TABLE metadata_claims ADD COLUMN selected INTEGER CHECK(selected IN (0,1));
|
|
7
|
+
ALTER TABLE metadata_claims ADD COLUMN extraction_version TEXT;
|
|
8
|
+
ALTER TABLE metadata_claims ADD COLUMN date_precision TEXT CHECK(date_precision IN ('date','datetime','invalid'));
|
|
9
|
+
ALTER TABLE revisions ADD COLUMN metadata_warnings_json TEXT NOT NULL DEFAULT '[]';
|
|
10
|
+
`;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Additive library-level work; capture jobs retain their existing item foreign key. */
|
|
2
|
+
export declare const version = 4;
|
|
3
|
+
export declare const sql = "\nALTER TABLE vectors ADD COLUMN input_hash TEXT CHECK(input_hash IS NULL OR length(input_hash)=64);\nCREATE TABLE index_jobs(id TEXT PRIMARY KEY,generation_id TEXT NOT NULL UNIQUE REFERENCES index_generations(id),state TEXT NOT NULL CHECK(state IN ('queued','running','failed','cancelled','succeeded','cleanup-pending')),cancel_requested INTEGER NOT NULL DEFAULT 0 CHECK(cancel_requested IN (0,1)),attempt_count INTEGER NOT NULL DEFAULT 0,reason TEXT,created_at TEXT NOT NULL) STRICT;\nCREATE INDEX index_jobs_ready ON index_jobs(state,created_at);\nCREATE TABLE index_attempts(id TEXT PRIMARY KEY,job_id TEXT NOT NULL REFERENCES index_jobs(id),owner_incarnation TEXT NOT NULL,fence INTEGER NOT NULL,stage TEXT NOT NULL,cleanup TEXT NOT NULL CHECK(cleanup IN ('pending','settled')),created_at TEXT NOT NULL,finished_at TEXT) STRICT;\nCREATE INDEX index_attempts_job ON index_attempts(job_id);\n";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Additive library-level work; capture jobs retain their existing item foreign key. */
|
|
2
|
+
export const version = 4;
|
|
3
|
+
export const sql = `
|
|
4
|
+
ALTER TABLE vectors ADD COLUMN input_hash TEXT CHECK(input_hash IS NULL OR length(input_hash)=64);
|
|
5
|
+
CREATE TABLE index_jobs(id TEXT PRIMARY KEY,generation_id TEXT NOT NULL UNIQUE REFERENCES index_generations(id),state TEXT NOT NULL CHECK(state IN ('queued','running','failed','cancelled','succeeded','cleanup-pending')),cancel_requested INTEGER NOT NULL DEFAULT 0 CHECK(cancel_requested IN (0,1)),attempt_count INTEGER NOT NULL DEFAULT 0,reason TEXT,created_at TEXT NOT NULL) STRICT;
|
|
6
|
+
CREATE INDEX index_jobs_ready ON index_jobs(state,created_at);
|
|
7
|
+
CREATE TABLE index_attempts(id TEXT PRIMARY KEY,job_id TEXT NOT NULL REFERENCES index_jobs(id),owner_incarnation TEXT NOT NULL,fence INTEGER NOT NULL,stage TEXT NOT NULL,cleanup TEXT NOT NULL CHECK(cleanup IN ('pending','settled')),created_at TEXT NOT NULL,finished_at TEXT) STRICT;
|
|
8
|
+
CREATE INDEX index_attempts_job ON index_attempts(job_id);
|
|
9
|
+
`;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CaptureResult, ErrorEnvelope } from '../../contracts/index.js';
|
|
2
|
+
import { runOwned } from './process.js';
|
|
3
|
+
import { type CaptureRuntime } from './runtime.js';
|
|
4
|
+
export type CaptureConnection = {
|
|
5
|
+
kind: 'account';
|
|
6
|
+
stateHome: string;
|
|
7
|
+
accountId: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'cdp';
|
|
10
|
+
endpoint: string;
|
|
11
|
+
};
|
|
12
|
+
export interface CaptureRequest {
|
|
13
|
+
runtime: CaptureRuntime;
|
|
14
|
+
stagingRoot: string;
|
|
15
|
+
sourceUrl: string;
|
|
16
|
+
requestId: string;
|
|
17
|
+
connection?: CaptureConnection;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
deadlineMs?: number;
|
|
20
|
+
allowPrivateNetwork?: boolean;
|
|
21
|
+
runProcess?: typeof runOwned;
|
|
22
|
+
}
|
|
23
|
+
export type CaptureOutcome = {
|
|
24
|
+
ok: true;
|
|
25
|
+
capture: CaptureResult;
|
|
26
|
+
bundleRoot: string;
|
|
27
|
+
dispose: () => Promise<void>;
|
|
28
|
+
} | ErrorEnvelope;
|
|
29
|
+
export declare function captureArticle(request: CaptureRequest): Promise<CaptureOutcome>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, writeFile, statfs, realpath } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { limits } from '../../contracts/index.js';
|
|
5
|
+
import { privateRoot, physicalBytes } from './files.js';
|
|
6
|
+
import { runOwned, isolatedEnvironment } from './process.js';
|
|
7
|
+
import { inspectCaptureRuntime, CaptureSetupError } from './runtime.js';
|
|
8
|
+
import { settleCaptureRoot, CaptureCleanupError } from './cleanup.js';
|
|
9
|
+
import { failure, upstreamFailure } from './errors.js';
|
|
10
|
+
import { normalizeBundle, object } from './normalize.js';
|
|
11
|
+
export async function captureArticle(request) {
|
|
12
|
+
const { requestId, signal } = request;
|
|
13
|
+
if (!/^[a-zA-Z0-9_-]{1,128}$/.test(requestId))
|
|
14
|
+
return failure('invalid-request', 'invalid-input');
|
|
15
|
+
let url;
|
|
16
|
+
try {
|
|
17
|
+
url = new URL(request.sourceUrl);
|
|
18
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || request.sourceUrl.length > 16384)
|
|
19
|
+
throw Error();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return failure(requestId, 'invalid-input');
|
|
23
|
+
}
|
|
24
|
+
if (signal?.aborted)
|
|
25
|
+
return failure(requestId, 'cancelled');
|
|
26
|
+
let runtime;
|
|
27
|
+
try {
|
|
28
|
+
runtime = await inspectCaptureRuntime(request.runtime.root, request.runtime.pins);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return failure(requestId, error instanceof CaptureSetupError ? error.code : 'runtime-missing', 'setup');
|
|
32
|
+
}
|
|
33
|
+
let base;
|
|
34
|
+
try {
|
|
35
|
+
base = await privateRoot(request.stagingRoot);
|
|
36
|
+
const disk = await statfs(base);
|
|
37
|
+
if (disk.bavail * disk.bsize < limits.stagingBytes + limits.freeReserveBytes)
|
|
38
|
+
return failure(requestId, 'limit-exceeded');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return failure(requestId, 'invalid-input');
|
|
42
|
+
}
|
|
43
|
+
const deadline = request.deadlineMs ?? limits.captureDeadlineMs;
|
|
44
|
+
if (!Number.isSafeInteger(deadline) || deadline < 100 || deadline > limits.captureDeadlineMs)
|
|
45
|
+
return failure(requestId, 'invalid-input');
|
|
46
|
+
const args = [runtime.ghostget, 'clip', request.sourceUrl, 'article', '--media', 'images', '--timeout-ms', String(deadline), '--max-html-bytes', String(limits.markdownBytes), '--max-asset-bytes', String(limits.imageBytes), '--max-total-asset-bytes', String(500 * 1024 ** 2), '--quiet', '--json'];
|
|
47
|
+
if (request.allowPrivateNetwork)
|
|
48
|
+
args.push('--allow-private-network');
|
|
49
|
+
let selectedState;
|
|
50
|
+
if (request.connection?.kind === 'account') {
|
|
51
|
+
if (!/^[a-zA-Z0-9_.-]{1,128}$/.test(request.connection.accountId))
|
|
52
|
+
return failure(requestId, 'invalid-input');
|
|
53
|
+
try {
|
|
54
|
+
selectedState = await privateRoot(request.connection.stateHome);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return failure(requestId, 'repair-auth');
|
|
58
|
+
}
|
|
59
|
+
args.push('--auth', request.connection.accountId, '--mode', 'http');
|
|
60
|
+
}
|
|
61
|
+
else if (request.connection?.kind === 'cdp') {
|
|
62
|
+
if (!/^(?:[1-9][0-9]{0,4}|(?:http|ws):\/\/127\.0\.0\.1:[1-9][0-9]{0,4}(?:\/[^\s]*)?)$/.test(request.connection.endpoint))
|
|
63
|
+
return failure(requestId, 'invalid-input');
|
|
64
|
+
args.push('--cdp', request.connection.endpoint);
|
|
65
|
+
}
|
|
66
|
+
else
|
|
67
|
+
args.push('--mode', 'http');
|
|
68
|
+
const root = await realpath(await mkdtemp(join(base, 'capture-'))), home = join(root, 'home'), output = join(root, 'bundles');
|
|
69
|
+
args.push('--output', output);
|
|
70
|
+
const env = isolatedEnvironment(home, home, selectedState ?? join(home, 'state'));
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
const abort = () => controller.abort();
|
|
73
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
74
|
+
if (signal?.aborted)
|
|
75
|
+
abort();
|
|
76
|
+
let cleanup = 'settled', keep = false, quota = false, checking = false;
|
|
77
|
+
const monitor = setInterval(async () => { if (checking)
|
|
78
|
+
return; checking = true; try {
|
|
79
|
+
if (await physicalBytes(root) > limits.stagingBytes) {
|
|
80
|
+
quota = true;
|
|
81
|
+
controller.abort();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
quota = true;
|
|
86
|
+
controller.abort();
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
checking = false;
|
|
90
|
+
} }, 100);
|
|
91
|
+
const started = Date.now();
|
|
92
|
+
try {
|
|
93
|
+
await mkdir(home, { mode: 0o700 });
|
|
94
|
+
await mkdir(output, { mode: 0o700 });
|
|
95
|
+
await writeFile(join(root, 'ownership.json'), JSON.stringify({ schemaVersion: 1, requestId, state: 'acquiring' }), { mode: 0o600 });
|
|
96
|
+
cleanup = 'pending';
|
|
97
|
+
const result = await (request.runProcess ?? runOwned)(runtime.bun, args, { cwd: root, env, signal: controller.signal, deadlineMs: deadline, maxOutputBytes: limits.metadataBytes, graceMs: 5000 });
|
|
98
|
+
cleanup = result.cleanup;
|
|
99
|
+
// Ghostget reports escaped browser custody separately from generic descendants.
|
|
100
|
+
let summary;
|
|
101
|
+
try {
|
|
102
|
+
summary = JSON.parse(result.stdout);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
summary = null;
|
|
106
|
+
}
|
|
107
|
+
const upstream = upstreamFailure(summary, requestId);
|
|
108
|
+
if (upstream.code === 'cleanup-required')
|
|
109
|
+
cleanup = 'pending';
|
|
110
|
+
if (cleanup === 'settled' && request.connection?.kind === 'cdp') {
|
|
111
|
+
cleanup = 'pending';
|
|
112
|
+
const census = await (request.runProcess ?? runOwned)('/bin/ps', ['-axo', 'command'], { env, deadlineMs: 3000, maxOutputBytes: 4 * 1024 ** 2 });
|
|
113
|
+
cleanup = census.cleanup;
|
|
114
|
+
if (census.code !== 0 || census.stopped || census.stdout.split('\n').some(line => line.includes(root)))
|
|
115
|
+
cleanup = 'pending';
|
|
116
|
+
}
|
|
117
|
+
if (cleanup !== 'settled')
|
|
118
|
+
return failure(requestId, 'cleanup-required', 'cleanup');
|
|
119
|
+
if (quota)
|
|
120
|
+
return failure(requestId, 'limit-exceeded');
|
|
121
|
+
if (result.stopped)
|
|
122
|
+
return failure(requestId, result.stopped);
|
|
123
|
+
if (result.code !== 0 || object(summary).ok !== true)
|
|
124
|
+
return upstream;
|
|
125
|
+
const outputDirectory = object(summary).outputDirectory;
|
|
126
|
+
if (typeof outputDirectory !== 'string' || !outputDirectory.startsWith(output + '/') || await realpath(outputDirectory) !== outputDirectory)
|
|
127
|
+
return failure(requestId, 'integrity', 'verification');
|
|
128
|
+
const helper = fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? './verify-helper.ts' : './verify-helper.js', import.meta.url));
|
|
129
|
+
cleanup = 'pending';
|
|
130
|
+
const checked = await (request.runProcess ?? runOwned)(runtime.bun, [helper, runtime.root, outputDirectory], { cwd: root, env, signal: controller.signal, deadlineMs: Math.max(1, deadline - (Date.now() - started)), maxOutputBytes: limits.helperFrameBytes });
|
|
131
|
+
cleanup = checked.cleanup;
|
|
132
|
+
if (cleanup !== 'settled')
|
|
133
|
+
return failure(requestId, 'cleanup-required', 'cleanup');
|
|
134
|
+
if (checked.stopped)
|
|
135
|
+
return failure(requestId, checked.stopped);
|
|
136
|
+
if (checked.code !== 0)
|
|
137
|
+
return failure(requestId, 'integrity', 'verification');
|
|
138
|
+
const capture = await normalizeBundle({ bundleRoot: outputDirectory, inspection: JSON.parse(checked.stdout), summary, requestId, sourceUrl: request.sourceUrl, wordcellVersion: runtime.pins.wordcell.version });
|
|
139
|
+
if (controller.signal.aborted)
|
|
140
|
+
return failure(requestId, quota ? 'limit-exceeded' : 'cancelled');
|
|
141
|
+
await writeFile(join(root, 'ownership.json'), JSON.stringify({ schemaVersion: 1, requestId, state: 'verified' }), { mode: 0o600 });
|
|
142
|
+
keep = true;
|
|
143
|
+
return { ok: true, capture, bundleRoot: outputDirectory, dispose: async () => { if (await settleCaptureRoot(root, requestId) === 'pending')
|
|
144
|
+
throw new CaptureCleanupError(); } };
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (cleanup !== 'settled')
|
|
148
|
+
return failure(requestId, 'cleanup-required', 'cleanup');
|
|
149
|
+
const code = (error instanceof Error && ['integrity', 'limit-exceeded', 'schema-newer'].includes(error.message) ? error.message : 'unclassified');
|
|
150
|
+
return failure(requestId, code, 'verification');
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
clearInterval(monitor);
|
|
154
|
+
while (checking)
|
|
155
|
+
await new Promise(r => setTimeout(r, 5));
|
|
156
|
+
signal?.removeEventListener('abort', abort);
|
|
157
|
+
if (!keep && cleanup === 'settled' && await settleCaptureRoot(root, requestId) === 'pending')
|
|
158
|
+
return failure(requestId, 'cleanup-required', 'cleanup');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare class CaptureCleanupError extends Error {
|
|
2
|
+
readonly code = "cleanup-required";
|
|
3
|
+
constructor();
|
|
4
|
+
}
|
|
5
|
+
/** Cleanup failure retains ownership evidence and must not settle the queue attempt. */
|
|
6
|
+
export declare function settleCaptureRoot(root: string, requestId: string): Promise<'settled' | 'pending'>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export class CaptureCleanupError extends Error {
|
|
4
|
+
code = 'cleanup-required';
|
|
5
|
+
constructor() { super('cleanup-required'); }
|
|
6
|
+
}
|
|
7
|
+
/** Cleanup failure retains ownership evidence and must not settle the queue attempt. */
|
|
8
|
+
export async function settleCaptureRoot(root, requestId) {
|
|
9
|
+
try {
|
|
10
|
+
await rm(root, { recursive: true, force: true });
|
|
11
|
+
return 'settled';
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
try {
|
|
15
|
+
await writeFile(join(root, 'ownership.json'), JSON.stringify({ schemaVersion: 1, requestId, state: 'cleanup-required' }), { mode: 0o600 });
|
|
16
|
+
}
|
|
17
|
+
catch { /* Existing ownership record remains when directory access itself failed. */ }
|
|
18
|
+
return 'pending';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ErrorCode, ErrorEnvelope } from '../../contracts/index.js';
|
|
2
|
+
export declare const failure: (requestId: string, code: ErrorCode, phase?: ErrorEnvelope["phase"], retryable?: boolean) => ErrorEnvelope;
|
|
3
|
+
export declare function upstreamFailure(value: unknown, requestId: string): ErrorEnvelope;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const mappings = { 'unknown': 'unclassified', 'permission-required': 'grant-permission', 'auth-required': 'repair-auth', 'invalid-input': 'invalid-input', 'cancelled': 'cancelled', 'timeout': 'unavailable', 'transient': 'unavailable', 'unsupported': 'unsupported', 'cleanup-pending': 'cleanup-required', 'rate-limited': 'rate-limited' };
|
|
2
|
+
export const failure = (requestId, code, phase = 'acquisition', retryable = false) => ({ schemaVersion: 1, requestId, ok: false, code, phase, retryable });
|
|
3
|
+
export function upstreamFailure(value, requestId) {
|
|
4
|
+
const fallback = failure(requestId, 'unclassified');
|
|
5
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
6
|
+
return fallback;
|
|
7
|
+
const outer = value;
|
|
8
|
+
if (outer.ok !== false)
|
|
9
|
+
return fallback;
|
|
10
|
+
const raw = outer.captureError;
|
|
11
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
12
|
+
return fallback;
|
|
13
|
+
const typed = raw;
|
|
14
|
+
if (Object.keys(typed).some(k => !['schemaVersion', 'code', 'retryable'].includes(k)) || typed.schemaVersion !== 1 || typeof typed.code !== 'string' || !Object.hasOwn(mappings, typed.code) || typeof typed.retryable !== 'boolean')
|
|
15
|
+
return fallback;
|
|
16
|
+
const code = mappings[typed.code];
|
|
17
|
+
if (typed.retryable && !['timeout', 'transient', 'rate-limited'].includes(typed.code))
|
|
18
|
+
return fallback;
|
|
19
|
+
return failure(requestId, code, code === 'cleanup-required' ? 'cleanup' : 'acquisition', typed.retryable);
|
|
20
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const hash: (bytes: Uint8Array | string) => string;
|
|
2
|
+
export declare function privateRoot(path: string): Promise<string>;
|
|
3
|
+
export declare function readConfined(root: string, name: string, maxBytes: number): Promise<Buffer>;
|
|
4
|
+
export declare function physicalBytes(root: string): Promise<number>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { open, lstat, realpath, readdir } from 'node:fs/promises';
|
|
3
|
+
import { resolve, relative, isAbsolute, join } from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
export const hash = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
6
|
+
export async function privateRoot(path) {
|
|
7
|
+
if (!isAbsolute(path))
|
|
8
|
+
throw Error('invalid-input');
|
|
9
|
+
const canonical = await realpath(path), stat = await lstat(path);
|
|
10
|
+
if (canonical !== path || !stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || (stat.mode & 0o077) !== 0)
|
|
11
|
+
throw Error('invalid-input');
|
|
12
|
+
return canonical;
|
|
13
|
+
}
|
|
14
|
+
export async function readConfined(root, name, maxBytes) {
|
|
15
|
+
if (!name || isAbsolute(name) || name.includes('\\') || name.split('/').some(p => !p || p === '.' || p === '..'))
|
|
16
|
+
throw Error('integrity');
|
|
17
|
+
const path = resolve(root, name);
|
|
18
|
+
if (relative(root, path).startsWith('..') || await realpath(path) !== path)
|
|
19
|
+
throw Error('integrity');
|
|
20
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
21
|
+
try {
|
|
22
|
+
const stat = await file.stat();
|
|
23
|
+
if (!stat.isFile() || stat.size > maxBytes)
|
|
24
|
+
throw Error('limit-exceeded');
|
|
25
|
+
const data = Buffer.alloc(stat.size + 1);
|
|
26
|
+
const { bytesRead } = await file.read(data, 0, data.length, 0);
|
|
27
|
+
if (bytesRead !== stat.size)
|
|
28
|
+
throw Error('integrity');
|
|
29
|
+
return data.subarray(0, bytesRead);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
await file.close();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function physicalBytes(root) {
|
|
36
|
+
let total = 0;
|
|
37
|
+
for (const entry of await readdir(root)) {
|
|
38
|
+
const path = join(root, entry);
|
|
39
|
+
try {
|
|
40
|
+
const stat = await lstat(path);
|
|
41
|
+
total += stat.isDirectory() ? await physicalBytes(path) : stat.size;
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
if (e.code !== 'ENOENT')
|
|
45
|
+
throw e;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return total;
|
|
49
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { captureArticle } from './capture.js';
|
|
2
|
+
export type { CaptureConnection, CaptureRequest, CaptureOutcome } from './capture.js';
|
|
3
|
+
export { provisionCapture, inspectCaptureRuntime, captureSetupPlan, CaptureSetupError } from './runtime.js';
|
|
4
|
+
export type { CaptureRuntime, CapturePins, SetupPlan } from './runtime.js';
|
|
5
|
+
export { defaultCapturePins } from './pins.js';
|
|
6
|
+
export { CaptureCleanupError } from './cleanup.js';
|