@hraness/sponge 0.5.3 → 0.6.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 +8 -11
- package/dist/capture/NOTICES.txt +472 -0
- package/dist/capture/worker.js +553 -0
- package/dist/src/acquisition/capture.d.ts +3 -10
- package/dist/src/acquisition/capture.js +28 -202
- package/dist/src/acquisition/child-request.d.ts +40 -0
- package/dist/src/acquisition/child.d.ts +31 -0
- package/dist/src/acquisition/child.js +130 -0
- package/dist/src/acquisition/errors.d.ts +2 -2
- package/dist/src/acquisition/errors.js +15 -4
- package/dist/src/acquisition/index.d.ts +0 -3
- package/dist/src/acquisition/index.js +0 -2
- package/dist/src/acquisition/normalize.d.ts +1 -3
- package/dist/src/acquisition/normalize.js +20 -22
- package/dist/src/app/article.d.ts +1 -2
- package/dist/src/app/article.js +1 -2
- package/dist/src/cli/commands.d.ts +0 -1
- package/dist/src/cli/commands.js +8 -35
- package/dist/src/cli/import.d.ts +1 -3
- package/dist/src/cli/import.js +3 -3
- package/dist/src/cli/main.js +0 -2
- package/dist/src/cli/runtimes.js +2 -3
- package/dist/src/cli/worker.d.ts +1 -4
- package/dist/src/cli/worker.js +0 -4
- package/dist/src/importers/apply.d.ts +0 -2
- package/dist/src/importers/apply.js +2 -2
- package/dist/src/importers/inspect.d.ts +2 -2
- package/dist/src/importers/inspect.js +24 -23
- package/dist/src/metadata/article.d.ts +0 -1
- package/dist/src/metadata/article.js +3 -2
- package/dist/src/pdf/capture.d.ts +7 -16
- package/dist/src/pdf/capture.js +44 -198
- package/dist/src/pdf/handler.d.ts +1 -2
- package/dist/src/pdf/handler.js +0 -1
- package/dist/src/pdf/normalize.d.ts +2 -3
- package/dist/src/pdf/normalize.js +34 -38
- package/dist/src/pdf/tools.d.ts +2 -0
- package/dist/src/pdf/tools.js +21 -0
- package/native/runtime-manifest.json +5 -5
- package/package.json +11 -2
- package/dist/src/acquisition/clip-helper.js +0 -14
- package/dist/src/acquisition/lock.d.ts +0 -1
- package/dist/src/acquisition/lock.js +0 -2
- package/dist/src/acquisition/pins.d.ts +0 -3
- package/dist/src/acquisition/pins.js +0 -21
- package/dist/src/acquisition/runtime.d.ts +0 -48
- package/dist/src/acquisition/runtime.js +0 -273
- package/dist/src/acquisition/verify-helper.d.ts +0 -1
- package/dist/src/acquisition/verify-helper.js +0 -26
- package/dist/src/importers/verify-helper.d.ts +0 -1
- package/dist/src/importers/verify-helper.js +0 -41
- package/dist/src/pdf/capture-helper.d.ts +0 -1
- package/dist/src/pdf/capture-helper.js +0 -15
- package/dist/src/pdf/errors.d.ts +0 -2
- package/dist/src/pdf/errors.js +0 -23
- package/dist/src/pdf/source-identity.d.ts +0 -4
- package/dist/src/pdf/source-identity.js +0 -13
- package/dist/src/pdf/verify-helper.d.ts +0 -1
- package/dist/src/pdf/verify-helper.js +0 -29
- /package/dist/src/acquisition/{clip-helper.d.ts → child-request.js} +0 -0
package/dist/src/pdf/capture.js
CHANGED
|
@@ -1,212 +1,58 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, writeFile, statfs, realpath } from 'node:fs/promises';
|
|
2
1
|
import { join } from 'node:path';
|
|
3
2
|
import { fileURLToPath } from 'node:url';
|
|
4
3
|
import { limits } from '../../contracts/index.js';
|
|
5
|
-
import {
|
|
6
|
-
import { runOwned, isolatedEnvironment } from '../acquisition/process.js';
|
|
7
|
-
import { inspectCaptureRuntime } from '../acquisition/runtime.js';
|
|
8
|
-
import { settleCaptureRoot } from '../acquisition/cleanup.js';
|
|
4
|
+
import { superviseChild } from '../acquisition/child.js';
|
|
9
5
|
import { failure } from '../acquisition/errors.js';
|
|
10
|
-
import { object } from '../acquisition/normalize.js';
|
|
11
6
|
import { normalizePdfBundle } from './normalize.js';
|
|
12
7
|
import { copyPdfInput } from './input.js';
|
|
13
|
-
|
|
14
|
-
import { SpongeError, publicCode, codeOf } from '../errors.js';
|
|
15
|
-
export async function capturePdf(request) {
|
|
16
|
-
const { requestId, signal } = request;
|
|
17
|
-
if (!/^[a-zA-Z0-9_-]{1,128}$/.test(requestId))
|
|
18
|
-
return failure('invalid-request', 'invalid-input');
|
|
19
|
-
let url;
|
|
20
|
-
try {
|
|
21
|
-
url = new URL(request.sourceUrl);
|
|
22
|
-
if (!['http:', 'https:', 'file:'].includes(url.protocol) ||
|
|
23
|
-
url.username ||
|
|
24
|
-
url.password ||
|
|
25
|
-
request.sourceUrl.length > 16384)
|
|
26
|
-
throw Error();
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return failure(requestId, 'invalid-input');
|
|
30
|
-
}
|
|
31
|
-
if (signal?.aborted)
|
|
32
|
-
return failure(requestId, 'cancelled');
|
|
33
|
-
let runtime;
|
|
8
|
+
function sourceOf(sourceUrl) {
|
|
34
9
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
base = await privateRoot(request.stagingRoot);
|
|
43
|
-
const disk = await statfs(base);
|
|
44
|
-
if (disk.bavail * disk.bsize < limits.stagingBytes + limits.freeReserveBytes)
|
|
45
|
-
return failure(requestId, 'limit-exceeded');
|
|
10
|
+
const url = new URL(sourceUrl);
|
|
11
|
+
if (!['http:', 'https:', 'file:'].includes(url.protocol) || url.username || url.password)
|
|
12
|
+
return;
|
|
13
|
+
if (sourceUrl.length > 16384 || (url.protocol === 'file:' && (url.host || url.search || url.hash)))
|
|
14
|
+
return;
|
|
15
|
+
return url;
|
|
46
16
|
}
|
|
47
17
|
catch {
|
|
48
|
-
return
|
|
18
|
+
return;
|
|
49
19
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
'--max-pdf-bytes',
|
|
67
|
-
String(limits.pdfBytes),
|
|
68
|
-
'--max-pages',
|
|
69
|
-
'500',
|
|
70
|
-
'--max-images',
|
|
71
|
-
String(limits.imagesPerItem),
|
|
72
|
-
'--max-asset-bytes',
|
|
73
|
-
String(limits.imageBytes),
|
|
74
|
-
'--max-total-asset-bytes',
|
|
75
|
-
String(500 * 1024 ** 2),
|
|
76
|
-
'--quiet',
|
|
77
|
-
'--json',
|
|
78
|
-
];
|
|
79
|
-
const root = await realpath(await mkdtemp(join(base, 'capture-'))), home = join(root, 'home'), output = join(root, 'bundles');
|
|
80
|
-
args.push('--output', output);
|
|
81
|
-
const env = isolatedEnvironment(home, home);
|
|
82
|
-
const controller = new AbortController();
|
|
83
|
-
const abort = () => controller.abort();
|
|
84
|
-
signal?.addEventListener('abort', abort, { once: true });
|
|
85
|
-
if (signal?.aborted)
|
|
86
|
-
abort();
|
|
87
|
-
let cleanup = 'settled', keep = false, quota = false, checking = false;
|
|
88
|
-
const monitor = setInterval(async () => {
|
|
89
|
-
if (checking)
|
|
90
|
-
return;
|
|
91
|
-
checking = true;
|
|
92
|
-
try {
|
|
93
|
-
if ((await physicalBytes(root)) > limits.stagingBytes) {
|
|
94
|
-
quota = true;
|
|
95
|
-
controller.abort();
|
|
20
|
+
}
|
|
21
|
+
export async function capturePdf(request) {
|
|
22
|
+
const url = sourceOf(request.sourceUrl);
|
|
23
|
+
// A PDF is fetched directly; a signed-in browser adds nothing.
|
|
24
|
+
if (url && request.connection)
|
|
25
|
+
return failure(request.requestId, 'unsupported');
|
|
26
|
+
let sourceHash;
|
|
27
|
+
return superviseChild({
|
|
28
|
+
...request,
|
|
29
|
+
valid: url !== undefined,
|
|
30
|
+
prepare: async ({ root, output, deadline, signal }) => {
|
|
31
|
+
let source = { url: request.sourceUrl };
|
|
32
|
+
if (url.protocol === 'file:') {
|
|
33
|
+
const path = join(root, 'input.pdf');
|
|
34
|
+
sourceHash = await copyPdfInput(fileURLToPath(url), path, signal);
|
|
35
|
+
source = { path };
|
|
96
36
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (url.protocol === 'file:') {
|
|
115
|
-
sourceHash = await copyPdfInput(fileURLToPath(url), join(root, 'input.pdf'), controller.signal);
|
|
116
|
-
args[2] = join(root, 'input.pdf');
|
|
117
|
-
}
|
|
118
|
-
cleanup = 'pending';
|
|
119
|
-
const result = await (request.runProcess ?? runOwned)(runtime.bun, args, {
|
|
120
|
-
cwd: root,
|
|
121
|
-
env,
|
|
122
|
-
signal: controller.signal,
|
|
123
|
-
deadlineMs: Math.max(1, deadline - (Date.now() - started)),
|
|
124
|
-
maxOutputBytes: limits.metadataBytes,
|
|
125
|
-
graceMs: 5000,
|
|
126
|
-
});
|
|
127
|
-
cleanup = result.cleanup;
|
|
128
|
-
let summary;
|
|
129
|
-
try {
|
|
130
|
-
summary = JSON.parse(result.stdout);
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
summary = null;
|
|
134
|
-
}
|
|
135
|
-
const upstream = pdfFailure(summary, requestId);
|
|
136
|
-
if (upstream.code === 'cleanup-required')
|
|
137
|
-
cleanup = 'pending';
|
|
138
|
-
if (cleanup !== 'settled')
|
|
139
|
-
return failure(requestId, 'cleanup-required', 'cleanup');
|
|
140
|
-
if (quota)
|
|
141
|
-
return failure(requestId, 'limit-exceeded');
|
|
142
|
-
if (result.stopped)
|
|
143
|
-
return failure(requestId, result.stopped);
|
|
144
|
-
if (result.code !== 0 || object(summary).ok !== true)
|
|
145
|
-
return upstream;
|
|
146
|
-
const outputDirectory = object(summary).outputDirectory;
|
|
147
|
-
if (typeof outputDirectory !== 'string' ||
|
|
148
|
-
!outputDirectory.startsWith(output + '/') ||
|
|
149
|
-
(await realpath(outputDirectory)) !== outputDirectory)
|
|
150
|
-
return failure(requestId, 'integrity', 'verification');
|
|
151
|
-
const helper = fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? './verify-helper.ts' : './verify-helper.js', import.meta.url));
|
|
152
|
-
cleanup = 'pending';
|
|
153
|
-
const checked = await (request.runProcess ?? runOwned)(runtime.bun, [helper, runtime.root, outputDirectory, request.sourceUrl], {
|
|
154
|
-
cwd: root,
|
|
155
|
-
env,
|
|
156
|
-
signal: controller.signal,
|
|
157
|
-
deadlineMs: Math.max(1, deadline - (Date.now() - started)),
|
|
158
|
-
maxOutputBytes: limits.helperFrameBytes,
|
|
159
|
-
});
|
|
160
|
-
cleanup = checked.cleanup;
|
|
161
|
-
if (cleanup !== 'settled')
|
|
162
|
-
return failure(requestId, 'cleanup-required', 'cleanup');
|
|
163
|
-
if (checked.stopped)
|
|
164
|
-
return failure(requestId, checked.stopped);
|
|
165
|
-
if (checked.code !== 0)
|
|
166
|
-
return failure(requestId, 'integrity', 'verification');
|
|
167
|
-
const capture = await normalizePdfBundle({
|
|
168
|
-
bundleRoot: outputDirectory,
|
|
169
|
-
inspection: JSON.parse(checked.stdout),
|
|
170
|
-
summary,
|
|
171
|
-
requestId,
|
|
37
|
+
return {
|
|
38
|
+
schemaVersion: 1,
|
|
39
|
+
kind: 'pdf',
|
|
40
|
+
source,
|
|
41
|
+
outputDirectory: output,
|
|
42
|
+
timeoutMs: deadline,
|
|
43
|
+
maxPdfBytes: limits.pdfBytes,
|
|
44
|
+
maxPages: 500,
|
|
45
|
+
maxImages: limits.imagesPerItem,
|
|
46
|
+
maxAssetBytes: limits.imageBytes,
|
|
47
|
+
maxTotalAssetBytes: 500 * 1024 ** 2,
|
|
48
|
+
allowPrivateNetwork: request.allowPrivateNetwork === true,
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
verify: (bundleRoot) => normalizePdfBundle({
|
|
52
|
+
bundleRoot,
|
|
53
|
+
requestId: request.requestId,
|
|
172
54
|
sourceUrl: request.sourceUrl,
|
|
173
|
-
wordcellVersion: runtime.pins.wordcell.version,
|
|
174
55
|
...(sourceHash ? { sourceHash } : {}),
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
return failure(requestId, quota ? 'limit-exceeded' : 'cancelled');
|
|
178
|
-
await writeFile(join(root, 'ownership.json'), JSON.stringify({ schemaVersion: 1, requestId, state: 'verified' }), {
|
|
179
|
-
mode: 0o600,
|
|
180
|
-
});
|
|
181
|
-
keep = true;
|
|
182
|
-
return {
|
|
183
|
-
ok: true,
|
|
184
|
-
capture,
|
|
185
|
-
bundleRoot: outputDirectory,
|
|
186
|
-
dispose: async () => {
|
|
187
|
-
if ((await settleCaptureRoot(root, requestId)) === 'pending')
|
|
188
|
-
throw new SpongeError('cleanup-required');
|
|
189
|
-
},
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
if (cleanup !== 'settled')
|
|
194
|
-
return failure(requestId, 'cleanup-required', 'cleanup');
|
|
195
|
-
const raw = codeOf(error), code = raw === 'integrity' ||
|
|
196
|
-
raw === 'limit-exceeded' ||
|
|
197
|
-
raw === 'schema-newer' ||
|
|
198
|
-
raw === 'invalid-input' ||
|
|
199
|
-
raw === 'cancelled'
|
|
200
|
-
? raw
|
|
201
|
-
: 'unclassified';
|
|
202
|
-
return failure(requestId, code, 'verification');
|
|
203
|
-
}
|
|
204
|
-
finally {
|
|
205
|
-
clearInterval(monitor);
|
|
206
|
-
while (checking)
|
|
207
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
208
|
-
signal?.removeEventListener('abort', abort);
|
|
209
|
-
if (!keep && cleanup === 'settled' && (await settleCaptureRoot(root, requestId)) === 'pending')
|
|
210
|
-
return failure(requestId, 'cleanup-required', 'cleanup');
|
|
211
|
-
}
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
212
58
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ErrorEnvelope, JobCheckpoint } from '../../contracts/index.js';
|
|
2
|
-
import { type
|
|
2
|
+
import { type CaptureConnection } from '../acquisition/index.js';
|
|
3
3
|
import { type PdfCaptureRequest as CaptureRequest, type CaptureOutcome } from './capture.js';
|
|
4
4
|
import type { LibraryStore, AttemptToken } from '../store/index.js';
|
|
5
5
|
interface PdfResult {
|
|
@@ -14,7 +14,6 @@ export type PdfOutcome = (PdfResult | ErrorEnvelope) & {
|
|
|
14
14
|
export interface PdfOptions {
|
|
15
15
|
checkpoint?: (stage: JobCheckpoint['stage']) => Promise<void>;
|
|
16
16
|
runProcess?: CaptureRequest['runProcess'];
|
|
17
|
-
runtime: CaptureRuntime;
|
|
18
17
|
connection?: CaptureConnection;
|
|
19
18
|
signal?: AbortSignal;
|
|
20
19
|
allowPrivateNetwork?: boolean;
|
package/dist/src/pdf/handler.js
CHANGED
|
@@ -24,7 +24,6 @@ export function ingestPdf(store, token, options) {
|
|
|
24
24
|
throw new SpongeError('invalid-input');
|
|
25
25
|
const result = await (options.capture ?? capturePdf)({
|
|
26
26
|
runProcess: options.runProcess,
|
|
27
|
-
runtime: options.runtime,
|
|
28
27
|
stagingRoot: join(store.root, 'staging'),
|
|
29
28
|
sourceUrl: source.locator,
|
|
30
29
|
requestId: token.attemptId,
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { CaptureResult } from '../../contracts/index.js';
|
|
2
|
+
/** Re-verify a PDF bundle written by the owned child: every declared file is re-read and rehashed here. */
|
|
2
3
|
export declare function normalizePdfBundle(input: {
|
|
3
4
|
bundleRoot: string;
|
|
4
|
-
inspection: unknown;
|
|
5
|
-
summary: unknown;
|
|
6
5
|
requestId: string;
|
|
7
6
|
sourceUrl: string;
|
|
8
|
-
|
|
7
|
+
/** The hash of the parent's own copy of a local input. */
|
|
9
8
|
sourceHash?: string;
|
|
10
9
|
}): Promise<CaptureResult>;
|
|
@@ -4,16 +4,16 @@ import { readConfined } from '../acquisition/files.js';
|
|
|
4
4
|
import { sha256 as hash } from '../util/hash.js';
|
|
5
5
|
import { object } from '../acquisition/normalize.js';
|
|
6
6
|
import { SpongeError } from '../errors.js';
|
|
7
|
+
/** Identifies Sponge's own PDF representation; bump when the bundle meaning changes. */
|
|
8
|
+
const pdfExtractor = { name: 'sponge', version: 'pdf-v1' }, rule = 'sponge-pdf-v1';
|
|
9
|
+
/** Re-verify a PDF bundle written by the owned child: every declared file is re-read and rehashed here. */
|
|
7
10
|
export async function normalizePdfBundle(input) {
|
|
8
|
-
const verified = object(input.inspection), inspection = object(verified.inspection), summary = object(input.summary);
|
|
9
|
-
if (verified.ok !== true || inspection.schemaVersion !== 1 || !Array.isArray(inspection.assets))
|
|
10
|
-
throw new SpongeError('integrity');
|
|
11
11
|
const manifestBytes = await readConfined(input.bundleRoot, 'capture.json', limits.metadataBytes), manifest = object(JSON.parse(manifestBytes.toString('utf8')));
|
|
12
12
|
if (manifest.schemaVersion !== 1 ||
|
|
13
13
|
manifest.kind !== 'pdf' ||
|
|
14
14
|
!['complete', 'partial'].includes(String(manifest.status)))
|
|
15
15
|
throw new SpongeError('integrity');
|
|
16
|
-
function
|
|
16
|
+
async function verified(raw, mediaType, maxBytes) {
|
|
17
17
|
const value = object(raw);
|
|
18
18
|
const result = {
|
|
19
19
|
relativePath: String(value.path),
|
|
@@ -22,55 +22,51 @@ export async function normalizePdfBundle(input) {
|
|
|
22
22
|
mediaType,
|
|
23
23
|
};
|
|
24
24
|
validateAsset(result);
|
|
25
|
+
if (result.bytes > maxBytes)
|
|
26
|
+
throw new SpongeError('limit-exceeded');
|
|
27
|
+
const content = await readConfined(input.bundleRoot, result.relativePath, maxBytes);
|
|
28
|
+
if (content.length !== result.bytes || hash(content) !== result.sha256)
|
|
29
|
+
throw new SpongeError('integrity');
|
|
25
30
|
return result;
|
|
26
31
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
const markdown = await verified(manifest.markdown, 'text/markdown', limits.markdownBytes), sourcePdf = await verified(manifest.source, 'application/pdf', limits.pdfBytes);
|
|
33
|
+
if (input.sourceHash !== undefined) {
|
|
34
|
+
if (sourcePdf.sha256 !== input.sourceHash)
|
|
35
|
+
throw new SpongeError('integrity');
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// The child records the requested URL with secrets redacted; its origin and path must still be ours.
|
|
39
|
+
const requested = new URL(String(object(manifest.source).requestedUrl)), expected = new URL(input.sourceUrl);
|
|
40
|
+
if (requested.origin !== expected.origin || requested.pathname !== expected.pathname)
|
|
33
41
|
throw new SpongeError('integrity');
|
|
34
42
|
}
|
|
35
|
-
const markdown = asset(manifest.markdown, 'text/markdown'), sourcePdf = asset(manifest.source, 'application/pdf');
|
|
36
|
-
assertVerified(markdown, inspection.markdown);
|
|
37
|
-
assertVerified(sourcePdf, inspection.source);
|
|
38
|
-
if (input.sourceHash && sourcePdf.sha256 !== input.sourceHash)
|
|
39
|
-
throw new SpongeError('integrity');
|
|
40
|
-
const source = object(manifest.source);
|
|
41
|
-
if (!input.sourceHash &&
|
|
42
|
-
(typeof verified.expectedSourceUrl !== 'string' || source.requestedUrl !== verified.expectedSourceUrl))
|
|
43
|
-
throw new SpongeError('integrity');
|
|
44
|
-
const markdownBytes = await readConfined(input.bundleRoot, markdown.relativePath, limits.markdownBytes);
|
|
45
|
-
if (hash(markdownBytes) !== markdown.sha256 || markdownBytes.length !== markdown.bytes)
|
|
46
|
-
throw new SpongeError('integrity');
|
|
47
43
|
if (!Array.isArray(manifest.images) || manifest.images.length > limits.imagesPerItem)
|
|
48
44
|
throw new SpongeError('limit-exceeded');
|
|
49
45
|
const byPath = new Map();
|
|
50
46
|
for (const raw of manifest.images) {
|
|
51
|
-
const
|
|
52
|
-
if (!
|
|
53
|
-
throw new SpongeError('integrity');
|
|
54
|
-
assertVerified(value, inspection.assets.find((raw) => object(raw).path === value.relativePath));
|
|
55
|
-
const prior = byPath.get(value.relativePath);
|
|
56
|
-
if (prior && JSON.stringify(prior) !== JSON.stringify(value))
|
|
47
|
+
const record = object(object(raw).asset), mediaType = String(record.mimeType), path = String(record.path);
|
|
48
|
+
if (!mediaType.startsWith('image/') || !path.startsWith('assets/'))
|
|
57
49
|
throw new SpongeError('integrity');
|
|
58
|
-
byPath.
|
|
50
|
+
const prior = byPath.get(path);
|
|
51
|
+
if (prior) {
|
|
52
|
+
if (prior.sha256 !== record.sha256 || prior.bytes !== record.bytes || prior.mediaType !== mediaType)
|
|
53
|
+
throw new SpongeError('integrity');
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
byPath.set(path, await verified(record, mediaType, limits.imageBytes));
|
|
59
57
|
}
|
|
60
|
-
if (byPath.size !== inspection.assets.length)
|
|
61
|
-
throw new SpongeError('integrity');
|
|
62
58
|
const metadata = [], document = object(manifest.document), observedAt = String(manifest.capturedAt);
|
|
63
59
|
for (const field of ['title', 'author', 'subject', 'creator', 'producer', 'createdAt', 'modifiedAt']) {
|
|
64
60
|
const value = document[field];
|
|
65
61
|
if (typeof value === 'string' && value.length > 0 && value.length <= 16384)
|
|
66
|
-
metadata.push({ field, value, source: 'pdf', observedAt, rule
|
|
62
|
+
metadata.push({ field, value, source: 'pdf', observedAt, rule, selected: true });
|
|
67
63
|
}
|
|
68
64
|
metadata.push({
|
|
69
65
|
field: 'pageCount',
|
|
70
66
|
value: String(document.pageCount),
|
|
71
67
|
source: 'pdf',
|
|
72
68
|
observedAt,
|
|
73
|
-
rule
|
|
69
|
+
rule,
|
|
74
70
|
});
|
|
75
71
|
if (manifest.quality) {
|
|
76
72
|
const quality = object(manifest.quality);
|
|
@@ -80,7 +76,7 @@ export async function normalizePdfBundle(input) {
|
|
|
80
76
|
value: String(quality.issues.length),
|
|
81
77
|
source: 'pdf',
|
|
82
78
|
observedAt,
|
|
83
|
-
rule
|
|
79
|
+
rule,
|
|
84
80
|
});
|
|
85
81
|
if (quality.issues.length > 400)
|
|
86
82
|
metadata.push({
|
|
@@ -88,7 +84,7 @@ export async function normalizePdfBundle(input) {
|
|
|
88
84
|
value: 'true',
|
|
89
85
|
source: 'pdf',
|
|
90
86
|
observedAt,
|
|
91
|
-
rule
|
|
87
|
+
rule,
|
|
92
88
|
});
|
|
93
89
|
for (const issue of quality.issues.slice(0, 400)) {
|
|
94
90
|
const row = object(issue);
|
|
@@ -97,12 +93,12 @@ export async function normalizePdfBundle(input) {
|
|
|
97
93
|
value: JSON.stringify({ code: row.code, page: row.page }),
|
|
98
94
|
source: 'pdf',
|
|
99
95
|
observedAt,
|
|
100
|
-
rule
|
|
96
|
+
rule,
|
|
101
97
|
});
|
|
102
98
|
}
|
|
103
99
|
}
|
|
104
100
|
}
|
|
105
|
-
const partial = manifest.status !== 'complete'
|
|
101
|
+
const partial = manifest.status !== 'complete';
|
|
106
102
|
const result = {
|
|
107
103
|
schemaVersion: 1,
|
|
108
104
|
requestId: input.requestId,
|
|
@@ -114,7 +110,7 @@ export async function normalizePdfBundle(input) {
|
|
|
114
110
|
assets: [...byPath.values()],
|
|
115
111
|
metadata,
|
|
116
112
|
warnings: partial ? ['partial'] : [],
|
|
117
|
-
extractor: {
|
|
113
|
+
extractor: { ...pdfExtractor },
|
|
118
114
|
manifestSha256: hash(manifestBytes),
|
|
119
115
|
completeness: { text: partial ? 'partial' : 'complete', images: partial ? 'partial' : 'complete' },
|
|
120
116
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { accessSync, constants } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
// The child finds the same tools: on PATH, then in the usual package-manager directories.
|
|
4
|
+
const directories = () => [
|
|
5
|
+
...(process.env.PATH ?? '').split(':').filter((d) => d.startsWith('/')),
|
|
6
|
+
'/opt/homebrew/bin',
|
|
7
|
+
'/usr/local/bin',
|
|
8
|
+
'/usr/bin',
|
|
9
|
+
];
|
|
10
|
+
/** Whether the Poppler converters PDF capture runs are installed. */
|
|
11
|
+
export function pdfToolsAvailable() {
|
|
12
|
+
return ['pdfinfo', 'pdftohtml'].every((tool) => directories().some((directory) => {
|
|
13
|
+
try {
|
|
14
|
+
accessSync(join(directory, tool), constants.X_OK);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
@@ -33,16 +33,16 @@
|
|
|
33
33
|
}
|
|
34
34
|
],
|
|
35
35
|
"build": {
|
|
36
|
-
"asrSourceSha256": "
|
|
36
|
+
"asrSourceSha256": "17934fedff0c472a1a11f93849b20ec142334a25e035d5dfad40c9151b72c9eb",
|
|
37
37
|
"clangVersion": "Apple clang version 21.0.0 (clang-2100.1.1.101)",
|
|
38
|
-
"embeddingSourceSha256": "
|
|
39
|
-
"mediaSourceSha256": "
|
|
38
|
+
"embeddingSourceSha256": "ca8d8bc22b03a3ca65a8ff4fb43f88d3d0cb3bd93591fe5842fd0106601aa6f1",
|
|
39
|
+
"mediaSourceSha256": "dc775291679fd69e554e28ee4cbab457f4e79080fb58542d92a83bc0004cac82",
|
|
40
40
|
"rustcVersion": "rustc 1.97.1 (8bab26f4f 2026-07-14)",
|
|
41
41
|
"swiftVersion": "Apple Swift version 6.3.3 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)"
|
|
42
42
|
},
|
|
43
43
|
"minimumMacOS": "26.0",
|
|
44
|
-
"packageVersion": "0.
|
|
44
|
+
"packageVersion": "0.6.1",
|
|
45
45
|
"platform": "darwin",
|
|
46
46
|
"schemaVersion": 1,
|
|
47
|
-
"sourceCommit": "
|
|
47
|
+
"sourceCommit": "e5785b1c0df60222bf5be92d24ecefeab921712d"
|
|
48
48
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hraness/sponge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "A local library for captured articles and verified offline assets.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"bin/",
|
|
16
|
+
"dist/capture/",
|
|
16
17
|
"dist/contracts/",
|
|
17
18
|
"dist/migrations/",
|
|
18
19
|
"dist/src/",
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
},
|
|
29
30
|
"scripts": {
|
|
30
31
|
"check": "node --check bin/sponge.js",
|
|
31
|
-
"build": "tsc -p tsconfig.build.json",
|
|
32
|
+
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && node scripts/bundle-capture.mjs",
|
|
32
33
|
"typecheck": "tsc --noEmit",
|
|
33
34
|
"cycles": "madge --circular --extensions ts src",
|
|
34
35
|
"deadcode": "knip --no-progress",
|
|
@@ -48,10 +49,18 @@
|
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@types/mdast": "4.0.4",
|
|
50
51
|
"@types/node": "24.10.1",
|
|
52
|
+
"defuddle": "0.19.1",
|
|
53
|
+
"entities": "7.0.1",
|
|
54
|
+
"esbuild": "0.25.11",
|
|
55
|
+
"htmlparser2": "10.1.0",
|
|
51
56
|
"knip": "6.39.0",
|
|
57
|
+
"linkedom": "0.18.14",
|
|
52
58
|
"madge": "8.0.0",
|
|
59
|
+
"mathml-to-latex": "1.8.0",
|
|
53
60
|
"prettier": "3.9.10",
|
|
61
|
+
"temml": "0.13.4",
|
|
54
62
|
"tsx": "4.20.6",
|
|
63
|
+
"turndown": "7.2.4",
|
|
55
64
|
"typescript": "5.9.3"
|
|
56
65
|
},
|
|
57
66
|
"dependencies": {
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
// Executed only by the pinned Bun runtime. The import is Wordcell's public clip entry point.
|
|
2
|
-
const runtime = process.argv[2];
|
|
3
|
-
try {
|
|
4
|
-
const { main } = await import(import.meta.resolve('@hraness/wordcell/clip/cli', `${runtime}/package.json`));
|
|
5
|
-
process.exitCode = await main(process.argv.slice(3), process.env, {
|
|
6
|
-
stdout: (value) => process.stdout.write(value),
|
|
7
|
-
stderr: (value) => process.stderr.write(value),
|
|
8
|
-
});
|
|
9
|
-
}
|
|
10
|
-
catch {
|
|
11
|
-
process.stdout.write(JSON.stringify({ ok: false }));
|
|
12
|
-
process.exitCode = 1;
|
|
13
|
-
}
|
|
14
|
-
export {};
|