@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.
Files changed (60) hide show
  1. package/README.md +8 -11
  2. package/dist/capture/NOTICES.txt +472 -0
  3. package/dist/capture/worker.js +553 -0
  4. package/dist/src/acquisition/capture.d.ts +3 -10
  5. package/dist/src/acquisition/capture.js +28 -202
  6. package/dist/src/acquisition/child-request.d.ts +40 -0
  7. package/dist/src/acquisition/child.d.ts +31 -0
  8. package/dist/src/acquisition/child.js +130 -0
  9. package/dist/src/acquisition/errors.d.ts +2 -2
  10. package/dist/src/acquisition/errors.js +15 -4
  11. package/dist/src/acquisition/index.d.ts +0 -3
  12. package/dist/src/acquisition/index.js +0 -2
  13. package/dist/src/acquisition/normalize.d.ts +1 -3
  14. package/dist/src/acquisition/normalize.js +20 -22
  15. package/dist/src/app/article.d.ts +1 -2
  16. package/dist/src/app/article.js +1 -2
  17. package/dist/src/cli/commands.d.ts +0 -1
  18. package/dist/src/cli/commands.js +8 -35
  19. package/dist/src/cli/import.d.ts +1 -3
  20. package/dist/src/cli/import.js +3 -3
  21. package/dist/src/cli/main.js +0 -2
  22. package/dist/src/cli/runtimes.js +2 -3
  23. package/dist/src/cli/worker.d.ts +1 -4
  24. package/dist/src/cli/worker.js +0 -4
  25. package/dist/src/importers/apply.d.ts +0 -2
  26. package/dist/src/importers/apply.js +2 -2
  27. package/dist/src/importers/inspect.d.ts +2 -2
  28. package/dist/src/importers/inspect.js +24 -23
  29. package/dist/src/metadata/article.d.ts +0 -1
  30. package/dist/src/metadata/article.js +3 -2
  31. package/dist/src/pdf/capture.d.ts +7 -16
  32. package/dist/src/pdf/capture.js +44 -198
  33. package/dist/src/pdf/handler.d.ts +1 -2
  34. package/dist/src/pdf/handler.js +0 -1
  35. package/dist/src/pdf/normalize.d.ts +2 -3
  36. package/dist/src/pdf/normalize.js +34 -38
  37. package/dist/src/pdf/tools.d.ts +2 -0
  38. package/dist/src/pdf/tools.js +21 -0
  39. package/native/runtime-manifest.json +5 -5
  40. package/package.json +11 -2
  41. package/dist/src/acquisition/clip-helper.js +0 -14
  42. package/dist/src/acquisition/lock.d.ts +0 -1
  43. package/dist/src/acquisition/lock.js +0 -2
  44. package/dist/src/acquisition/pins.d.ts +0 -3
  45. package/dist/src/acquisition/pins.js +0 -21
  46. package/dist/src/acquisition/runtime.d.ts +0 -48
  47. package/dist/src/acquisition/runtime.js +0 -273
  48. package/dist/src/acquisition/verify-helper.d.ts +0 -1
  49. package/dist/src/acquisition/verify-helper.js +0 -26
  50. package/dist/src/importers/verify-helper.d.ts +0 -1
  51. package/dist/src/importers/verify-helper.js +0 -41
  52. package/dist/src/pdf/capture-helper.d.ts +0 -1
  53. package/dist/src/pdf/capture-helper.js +0 -15
  54. package/dist/src/pdf/errors.d.ts +0 -2
  55. package/dist/src/pdf/errors.js +0 -23
  56. package/dist/src/pdf/source-identity.d.ts +0 -4
  57. package/dist/src/pdf/source-identity.js +0 -13
  58. package/dist/src/pdf/verify-helper.d.ts +0 -1
  59. package/dist/src/pdf/verify-helper.js +0 -29
  60. /package/dist/src/acquisition/{clip-helper.d.ts → child-request.js} +0 -0
@@ -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 { privateRoot, physicalBytes } from '../acquisition/files.js';
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
- import { pdfFailure } from './errors.js';
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
- runtime = await inspectCaptureRuntime(request.runtime.root, request.runtime.pins);
36
- }
37
- catch (error) {
38
- return failure(requestId, publicCode(error, 'runtime-missing'), 'setup');
39
- }
40
- let base;
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 failure(requestId, 'invalid-input');
18
+ return;
49
19
  }
50
- const deadline = request.deadlineMs ?? limits.captureDeadlineMs;
51
- if (!Number.isSafeInteger(deadline) || deadline < 100 || deadline > limits.captureDeadlineMs)
52
- return failure(requestId, 'invalid-input');
53
- if (url.protocol === 'file:' && (url.host || url.search || url.hash || request.connection))
54
- return failure(requestId, 'invalid-input');
55
- if (request.connection || request.allowPrivateNetwork)
56
- return failure(requestId, 'unsupported');
57
- const captureHelper = fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? './capture-helper.ts' : './capture-helper.js', import.meta.url));
58
- const args = [
59
- captureHelper,
60
- runtime.root,
61
- request.sourceUrl,
62
- '--slug',
63
- 'document',
64
- '--timeout-ms',
65
- String(deadline),
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
- catch {
99
- quota = true;
100
- controller.abort();
101
- }
102
- finally {
103
- checking = false;
104
- }
105
- }, 100);
106
- const started = Date.now();
107
- try {
108
- await mkdir(home, { mode: 0o700 });
109
- await mkdir(output, { mode: 0o700 });
110
- await writeFile(join(root, 'ownership.json'), JSON.stringify({ schemaVersion: 1, requestId, state: 'acquiring' }), {
111
- mode: 0o600,
112
- });
113
- let sourceHash;
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
- if (controller.signal.aborted)
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 CaptureRuntime, type CaptureConnection } from '../acquisition/index.js';
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;
@@ -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
- wordcellVersion: string;
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 asset(raw, mediaType) {
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
- function assertVerified(value, checked) {
28
- const record = object(checked);
29
- if (record.integrity !== 'verified' ||
30
- record.path !== value.relativePath ||
31
- record.sha256 !== value.sha256 ||
32
- record.bytes !== value.bytes)
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 image = object(raw), record = object(image.asset), value = asset(record, String(record.mimeType));
52
- if (!value.mediaType.startsWith('image/'))
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.set(value.relativePath, value);
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: 'wordcell-pdf-v1', selected: true });
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: 'wordcell-pdf-v1',
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: 'wordcell-pdf-v1',
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: 'wordcell-pdf-v1',
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: 'wordcell-pdf-v1',
96
+ rule,
101
97
  });
102
98
  }
103
99
  }
104
100
  }
105
- const partial = manifest.status !== 'complete' || inspection.status !== 'complete' || summary.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: { name: 'wordcell', version: input.wordcellVersion },
113
+ extractor: { ...pdfExtractor },
118
114
  manifestSha256: hash(manifestBytes),
119
115
  completeness: { text: partial ? 'partial' : 'complete', images: partial ? 'partial' : 'complete' },
120
116
  };
@@ -0,0 +1,2 @@
1
+ /** Whether the Poppler converters PDF capture runs are installed. */
2
+ export declare function pdfToolsAvailable(): boolean;
@@ -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": "7520570edfaae2e511814a9c463b977b87523ea8bb6494b244a59a5ca5e73d42",
36
+ "asrSourceSha256": "17934fedff0c472a1a11f93849b20ec142334a25e035d5dfad40c9151b72c9eb",
37
37
  "clangVersion": "Apple clang version 21.0.0 (clang-2100.1.1.101)",
38
- "embeddingSourceSha256": "0938c0368b954bc609ee2438b579121850919cefb51144270dfb0fb62ec86c34",
39
- "mediaSourceSha256": "db88df9dcfd4077b26bf1ef3595f1ce295fb3cdb085eccf4e1eea894698902bd",
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.5.3",
44
+ "packageVersion": "0.6.1",
45
45
  "platform": "darwin",
46
46
  "schemaVersion": 1,
47
- "sourceCommit": "2191f9e2296d89d5ef5b26750e262469ccf6c0d3"
47
+ "sourceCommit": "e5785b1c0df60222bf5be92d24ecefeab921712d"
48
48
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hraness/sponge",
3
- "version": "0.5.3",
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 {};