@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.
Files changed (174) hide show
  1. package/README.md +163 -17
  2. package/bin/sponge.js +9 -2
  3. package/dist/contracts/index.d.ts +164 -0
  4. package/dist/contracts/index.js +6 -0
  5. package/dist/contracts/validate.d.ts +14 -0
  6. package/dist/contracts/validate.js +187 -0
  7. package/dist/migrations/001.d.ts +3 -0
  8. package/dist/migrations/001.js +33 -0
  9. package/dist/migrations/002.d.ts +3 -0
  10. package/dist/migrations/002.js +6 -0
  11. package/dist/migrations/003.d.ts +3 -0
  12. package/dist/migrations/003.js +10 -0
  13. package/dist/migrations/004.d.ts +3 -0
  14. package/dist/migrations/004.js +9 -0
  15. package/dist/src/acquisition/capture.d.ts +29 -0
  16. package/dist/src/acquisition/capture.js +160 -0
  17. package/dist/src/acquisition/cleanup.d.ts +6 -0
  18. package/dist/src/acquisition/cleanup.js +20 -0
  19. package/dist/src/acquisition/errors.d.ts +3 -0
  20. package/dist/src/acquisition/errors.js +20 -0
  21. package/dist/src/acquisition/files.d.ts +4 -0
  22. package/dist/src/acquisition/files.js +49 -0
  23. package/dist/src/acquisition/index.d.ts +6 -0
  24. package/dist/src/acquisition/index.js +4 -0
  25. package/dist/src/acquisition/lock.d.ts +1 -0
  26. package/dist/src/acquisition/lock.js +2 -0
  27. package/dist/src/acquisition/normalize.d.ts +11 -0
  28. package/dist/src/acquisition/normalize.js +62 -0
  29. package/dist/src/acquisition/pins.d.ts +3 -0
  30. package/dist/src/acquisition/pins.js +9 -0
  31. package/dist/src/acquisition/process.d.ts +16 -0
  32. package/dist/src/acquisition/process.js +63 -0
  33. package/dist/src/acquisition/runtime.d.ts +53 -0
  34. package/dist/src/acquisition/runtime.js +216 -0
  35. package/dist/src/acquisition/verify-helper.d.ts +1 -0
  36. package/dist/src/acquisition/verify-helper.js +19 -0
  37. package/dist/src/app/article.d.ts +23 -0
  38. package/dist/src/app/article.js +82 -0
  39. package/dist/src/app/bundle-files.d.ts +8 -0
  40. package/dist/src/app/bundle-files.js +66 -0
  41. package/dist/src/app/markdown.d.ts +6 -0
  42. package/dist/src/app/markdown.js +90 -0
  43. package/dist/src/app/source.d.ts +15 -0
  44. package/dist/src/app/source.js +40 -0
  45. package/dist/src/cli/arguments.d.ts +9 -0
  46. package/dist/src/cli/arguments.js +106 -0
  47. package/dist/src/cli/config.d.ts +8 -0
  48. package/dist/src/cli/config.js +45 -0
  49. package/dist/src/cli/import.d.ts +4 -0
  50. package/dist/src/cli/import.js +3 -0
  51. package/dist/src/cli/indexing.d.ts +7 -0
  52. package/dist/src/cli/indexing.js +21 -0
  53. package/dist/src/cli/main.d.ts +1 -0
  54. package/dist/src/cli/main.js +333 -0
  55. package/dist/src/cli/portability.d.ts +9 -0
  56. package/dist/src/cli/portability.js +39 -0
  57. package/dist/src/cli/queue.d.ts +2 -0
  58. package/dist/src/cli/queue.js +23 -0
  59. package/dist/src/cli/terminal.d.ts +2 -0
  60. package/dist/src/cli/terminal.js +4 -0
  61. package/dist/src/cli/worker.d.ts +16 -0
  62. package/dist/src/cli/worker.js +62 -0
  63. package/dist/src/importers/apply.d.ts +29 -0
  64. package/dist/src/importers/apply.js +85 -0
  65. package/dist/src/importers/custody.d.ts +6 -0
  66. package/dist/src/importers/custody.js +35 -0
  67. package/dist/src/importers/inspect.d.ts +21 -0
  68. package/dist/src/importers/inspect.js +97 -0
  69. package/dist/src/importers/manifest.d.ts +22 -0
  70. package/dist/src/importers/manifest.js +43 -0
  71. package/dist/src/importers/verify-helper.d.ts +1 -0
  72. package/dist/src/importers/verify-helper.js +27 -0
  73. package/dist/src/indexing/image.d.ts +10 -0
  74. package/dist/src/indexing/image.js +34 -0
  75. package/dist/src/indexing/index.d.ts +53 -0
  76. package/dist/src/indexing/index.js +123 -0
  77. package/dist/src/indexing/jobs.d.ts +14 -0
  78. package/dist/src/indexing/jobs.js +49 -0
  79. package/dist/src/indexing/runner.d.ts +23 -0
  80. package/dist/src/indexing/runner.js +114 -0
  81. package/dist/src/inference/asr/bridge.d.ts +1 -0
  82. package/dist/src/inference/asr/bridge.js +16 -0
  83. package/dist/src/inference/asr/protocol.d.ts +23 -0
  84. package/dist/src/inference/asr/protocol.js +60 -0
  85. package/dist/src/inference/asr/runtime.d.ts +29 -0
  86. package/dist/src/inference/asr/runtime.js +103 -0
  87. package/dist/src/inference/asr/windows.d.ts +9 -0
  88. package/dist/src/inference/asr/windows.js +41 -0
  89. package/dist/src/inference/embedding/index.d.ts +47 -0
  90. package/dist/src/inference/embedding/index.js +122 -0
  91. package/dist/src/media/capture-helper.d.ts +1 -0
  92. package/dist/src/media/capture-helper.js +16 -0
  93. package/dist/src/media/capture.d.ts +32 -0
  94. package/dist/src/media/capture.js +193 -0
  95. package/dist/src/media/handler.d.ts +22 -0
  96. package/dist/src/media/handler.js +82 -0
  97. package/dist/src/media/transcribe.d.ts +29 -0
  98. package/dist/src/media/transcribe.js +93 -0
  99. package/dist/src/metadata/article.d.ts +5 -0
  100. package/dist/src/metadata/article.js +51 -0
  101. package/dist/src/models/artifacts.d.ts +36 -0
  102. package/dist/src/models/artifacts.js +274 -0
  103. package/dist/src/models/catalog.d.ts +35 -0
  104. package/dist/src/models/catalog.js +71 -0
  105. package/dist/src/models/download.d.ts +9 -0
  106. package/dist/src/models/download.js +147 -0
  107. package/dist/src/models/runtime.d.ts +24 -0
  108. package/dist/src/models/runtime.js +37 -0
  109. package/dist/src/pdf/capture-helper.d.ts +1 -0
  110. package/dist/src/pdf/capture-helper.js +12 -0
  111. package/dist/src/pdf/capture.d.ts +29 -0
  112. package/dist/src/pdf/capture.js +156 -0
  113. package/dist/src/pdf/errors.d.ts +2 -0
  114. package/dist/src/pdf/errors.js +13 -0
  115. package/dist/src/pdf/handler.d.ts +24 -0
  116. package/dist/src/pdf/handler.js +84 -0
  117. package/dist/src/pdf/index.d.ts +4 -0
  118. package/dist/src/pdf/index.js +4 -0
  119. package/dist/src/pdf/input.d.ts +2 -0
  120. package/dist/src/pdf/input.js +46 -0
  121. package/dist/src/pdf/normalize.d.ts +10 -0
  122. package/dist/src/pdf/normalize.js +64 -0
  123. package/dist/src/pdf/source-identity.d.ts +4 -0
  124. package/dist/src/pdf/source-identity.js +12 -0
  125. package/dist/src/pdf/source.d.ts +6 -0
  126. package/dist/src/pdf/source.js +22 -0
  127. package/dist/src/pdf/verify-helper.d.ts +1 -0
  128. package/dist/src/pdf/verify-helper.js +17 -0
  129. package/dist/src/portability/backup.d.ts +23 -0
  130. package/dist/src/portability/backup.js +138 -0
  131. package/dist/src/portability/database.d.ts +14 -0
  132. package/dist/src/portability/database.js +78 -0
  133. package/dist/src/portability/export.d.ts +8 -0
  134. package/dist/src/portability/export.js +36 -0
  135. package/dist/src/portability/files.d.ts +33 -0
  136. package/dist/src/portability/files.js +78 -0
  137. package/dist/src/portability/gc.d.ts +21 -0
  138. package/dist/src/portability/gc.js +66 -0
  139. package/dist/src/search/extension.d.ts +3 -0
  140. package/dist/src/search/extension.js +17 -0
  141. package/dist/src/search/hybrid.d.ts +39 -0
  142. package/dist/src/search/hybrid.js +118 -0
  143. package/dist/src/search/query.d.ts +12 -0
  144. package/dist/src/search/query.js +47 -0
  145. package/dist/src/search/vector.d.ts +21 -0
  146. package/dist/src/search/vector.js +21 -0
  147. package/dist/src/service/launch-agent.d.ts +47 -0
  148. package/dist/src/service/launch-agent.js +179 -0
  149. package/dist/src/service/lifetime.d.ts +7 -0
  150. package/dist/src/service/lifetime.js +73 -0
  151. package/dist/src/service/loop.d.ts +21 -0
  152. package/dist/src/service/loop.js +94 -0
  153. package/dist/src/store/files.d.ts +12 -0
  154. package/dist/src/store/files.js +101 -0
  155. package/dist/src/store/index.d.ts +118 -0
  156. package/dist/src/store/index.js +727 -0
  157. package/dist/src/store/types.d.ts +90 -0
  158. package/dist/src/store/types.js +8 -0
  159. package/dist/src/worker/basic.d.ts +10 -0
  160. package/dist/src/worker/basic.js +27 -0
  161. package/dist/src/worker/custody.d.ts +13 -0
  162. package/dist/src/worker/custody.js +185 -0
  163. package/dist/src/worker/engine.d.ts +28 -0
  164. package/dist/src/worker/engine.js +78 -0
  165. package/dist/src/worker/identity.d.ts +7 -0
  166. package/dist/src/worker/identity.js +27 -0
  167. package/dist/src/worker/journal.d.ts +5 -0
  168. package/dist/src/worker/journal.js +30 -0
  169. package/dist/src/worker/supervisor-child.d.ts +1 -0
  170. package/dist/src/worker/supervisor-child.js +33 -0
  171. package/dist/src/worker/supervisor.d.ts +3 -0
  172. package/dist/src/worker/supervisor.js +47 -0
  173. package/native/runtime-manifest.json +40 -0
  174. package/package.json +36 -6
@@ -0,0 +1,51 @@
1
+ import { limits } from '../../contracts/index.js';
2
+ import { validateMetadataClaim, validateMetadataWarnings } from '../../contracts/validate.js';
3
+ function object(v) { if (!v || typeof v !== 'object' || Array.isArray(v))
4
+ throw Error('integrity'); return v; }
5
+ /** Consume bounded observed Wordcell evidence without fetching linked JSON-LD contexts. */
6
+ export function articleMetadata(input, observedAt) {
7
+ const envelope = object(input);
8
+ articleMetadataWarnings(input);
9
+ if (envelope.schemaVersion !== 1 || envelope.extractorVersion !== 'wordcell-metadata-v1')
10
+ throw Error('schema-newer');
11
+ if (!Array.isArray(envelope.claims) || envelope.claims.length > limits.metadataClaims || Buffer.byteLength(JSON.stringify(input)) > limits.metadataBytes)
12
+ throw Error('limit-exceeded');
13
+ const claims = envelope.claims.map(raw => {
14
+ const claim = object(raw), locator = claim.locator;
15
+ if (typeof locator !== 'string' || typeof claim.selected !== 'boolean')
16
+ throw Error('integrity');
17
+ const sources = { 'html-title': 'html', 'html-meta': 'html', 'html-link': 'html', 'html-language': 'html', 'json-ld': 'jsonld', extractor: 'html' };
18
+ if (typeof claim.source !== 'string' || !Object.hasOwn(sources, claim.source))
19
+ throw Error('integrity');
20
+ const source = /^meta\[(?:property|name)=og:/.test(locator) ? 'opengraph' : /^meta\[(?:property|name)=twitter:/.test(locator) ? 'twitter' : sources[claim.source];
21
+ if (claim.timezone !== undefined && (typeof claim.timezone !== 'string' || !['explicit', 'unknown', 'not-applicable'].includes(claim.timezone)))
22
+ throw Error('integrity');
23
+ const result = { field: claim.field, value: claim.value, observed: claim.observed, source, locator, selected: claim.selected, observedAt, rule: claim.rule, extractionVersion: envelope.extractorVersion, ...(claim.datePrecision !== undefined ? { datePrecision: claim.datePrecision } : {}), ...(claim.timezone === 'explicit' || claim.timezone === 'unknown' ? { timezoneKnown: claim.timezone === 'explicit' } : {}) };
24
+ validateMetadataClaim(result);
25
+ return result;
26
+ });
27
+ // Upstream selects a value per scalar field; reject contradictory accepted envelopes.
28
+ const selected = new Set();
29
+ for (const claim of claims) {
30
+ if (!claim.selected)
31
+ continue;
32
+ if (selected.has(claim.field) && !['authors', 'tags', 'alternate'].includes(claim.field))
33
+ throw Error('integrity');
34
+ selected.add(claim.field);
35
+ }
36
+ return claims;
37
+ }
38
+ export function selectedMetadata(claims, field) {
39
+ const matches = claims.filter(claim => claim.field === field);
40
+ return matches.find(claim => claim.selected === true) ?? matches.find(claim => claim.selected === undefined);
41
+ }
42
+ export function articleMetadataWarnings(input) {
43
+ const envelope = object(input);
44
+ try {
45
+ validateMetadataWarnings(envelope.warnings);
46
+ }
47
+ catch {
48
+ throw Error('integrity');
49
+ }
50
+ return [...envelope.warnings];
51
+ }
@@ -0,0 +1,36 @@
1
+ export interface ModelArtifact {
2
+ name: string;
3
+ url: string;
4
+ sha256: string;
5
+ bytes: number;
6
+ executable?: boolean;
7
+ }
8
+ export interface ModelPackage {
9
+ id: string;
10
+ license: string;
11
+ artifacts: readonly ModelArtifact[];
12
+ }
13
+ export declare class ModelSetupError extends Error {
14
+ readonly code: 'invalid-input' | 'integrity' | 'unavailable' | 'cancelled' | 'owner-busy' | 'limit-exceeded' | 'cleanup-required' | 'repair-auth';
15
+ constructor(code: 'invalid-input' | 'integrity' | 'unavailable' | 'cancelled' | 'owner-busy' | 'limit-exceeded' | 'cleanup-required' | 'repair-auth');
16
+ }
17
+ export declare function modelSetupPlan(manifest: ModelPackage): {
18
+ id: string;
19
+ license: string;
20
+ fingerprint: string;
21
+ downloadBytes: number;
22
+ requiredFreeBytes: number;
23
+ artifacts: readonly ModelArtifact[];
24
+ };
25
+ export declare function inspectModelPackage(cacheRoot: string, manifest: ModelPackage): Promise<Record<string, string>>;
26
+ /** Explicit recovery only: an absent or different process birth identity proves the downloader is gone.
27
+ * Downloading is in-process fetch; no native child can outlive this owner. A missing or
28
+ * foreign stage receipt remains pending, as does an interrupted recovery guard. */
29
+ export declare function recoverModelPackage(cacheRoot: string, manifest: ModelPackage): Promise<{
30
+ recovered: boolean;
31
+ }>;
32
+ /** Explicit setup only. Never called by install hooks or by inference fallback. */
33
+ export declare function provisionModelPackage(cacheRoot: string, manifest: ModelPackage, options?: {
34
+ signal?: AbortSignal;
35
+ fetch?: typeof fetch;
36
+ }): Promise<Record<string, string>>;
@@ -0,0 +1,274 @@
1
+ import { constants } from 'node:fs';
2
+ import { chmod, lstat, mkdir, open, rename, rm, statfs, link, unlink, readdir, realpath } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ import { processIdentity, identityAlive } from '../worker/identity.js';
6
+ import { readJournal, writeJournal } from '../worker/journal.js';
7
+ import { privateRoot } from '../acquisition/files.js';
8
+ export class ModelSetupError extends Error {
9
+ code;
10
+ constructor(code) {
11
+ super(code);
12
+ this.code = code;
13
+ }
14
+ }
15
+ const maximumBytes = 2 * 1024 ** 3;
16
+ function validate(manifest) {
17
+ if (!/^[a-z0-9][a-z0-9.-]{0,127}$/.test(manifest.id) || !manifest.license || manifest.license.length > 256 || !manifest.artifacts.length || manifest.artifacts.length > 16)
18
+ throw new ModelSetupError('invalid-input');
19
+ const names = new Set();
20
+ let bytes = 0;
21
+ for (const artifact of manifest.artifacts) {
22
+ let url;
23
+ try {
24
+ url = new URL(artifact.url);
25
+ }
26
+ catch {
27
+ throw new ModelSetupError('invalid-input');
28
+ }
29
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(artifact.name) || names.has(artifact.name) || url.protocol !== 'https:' || url.username || url.password || !/^[a-f0-9]{64}$/.test(artifact.sha256) || !Number.isSafeInteger(artifact.bytes) || artifact.bytes < 1 || artifact.bytes > maximumBytes)
30
+ throw new ModelSetupError('invalid-input');
31
+ names.add(artifact.name);
32
+ bytes += artifact.bytes;
33
+ }
34
+ if (bytes > maximumBytes)
35
+ throw new ModelSetupError('limit-exceeded');
36
+ }
37
+ function identity(manifest) {
38
+ return createHash('sha256').update(JSON.stringify({ id: manifest.id, license: manifest.license, artifacts: manifest.artifacts.map(a => ({ name: a.name, url: a.url, sha256: a.sha256, bytes: a.bytes, executable: !!a.executable })).sort((a, b) => a.name.localeCompare(b.name)) })).digest('hex');
39
+ }
40
+ export function modelSetupPlan(manifest) {
41
+ validate(manifest);
42
+ const downloadBytes = manifest.artifacts.reduce((n, a) => n + a.bytes, 0);
43
+ return { id: manifest.id, license: manifest.license, fingerprint: identity(manifest), downloadBytes, requiredFreeBytes: downloadBytes + 512 * 1024 ** 2, artifacts: manifest.artifacts };
44
+ }
45
+ async function syncDirectory(path) { const file = await open(path, constants.O_RDONLY); try {
46
+ await file.sync();
47
+ }
48
+ finally {
49
+ await file.close();
50
+ } }
51
+ async function verifyArtifact(path, artifact) {
52
+ const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
53
+ try {
54
+ const before = await file.stat();
55
+ if (!before.isFile() || before.nlink !== 1 || before.size !== artifact.bytes || (before.mode & 0o077) !== 0 || (artifact.executable && (before.mode & 0o100) === 0))
56
+ throw new ModelSetupError('integrity');
57
+ const hash = createHash('sha256'), buffer = Buffer.alloc(65536);
58
+ let bytes = 0;
59
+ for (;;) {
60
+ const read = await file.read(buffer, 0, buffer.length, null);
61
+ if (!read.bytesRead)
62
+ break;
63
+ bytes += read.bytesRead;
64
+ if (bytes > artifact.bytes)
65
+ throw new ModelSetupError('integrity');
66
+ hash.update(buffer.subarray(0, read.bytesRead));
67
+ }
68
+ const after = await file.stat();
69
+ if (bytes !== artifact.bytes || hash.digest('hex') !== artifact.sha256 || before.mtimeMs !== after.mtimeMs)
70
+ throw new ModelSetupError('integrity');
71
+ }
72
+ finally {
73
+ await file.close();
74
+ }
75
+ }
76
+ export async function inspectModelPackage(cacheRoot, manifest) {
77
+ const plan = modelSetupPlan(manifest), root = join(await privateRoot(cacheRoot), plan.fingerprint);
78
+ await privateRoot(root);
79
+ const paths = Object.create(null);
80
+ for (const artifact of manifest.artifacts) {
81
+ const path = join(root, artifact.name);
82
+ await verifyArtifact(path, artifact);
83
+ paths[artifact.name] = path;
84
+ }
85
+ return paths;
86
+ }
87
+ const pending = () => { throw new ModelSetupError('cleanup-required'); };
88
+ async function sameFile(a, b) { try {
89
+ const [x, y] = await Promise.all([lstat(a), lstat(b)]);
90
+ if (!x.isFile() || !y.isFile() || x.isSymbolicLink() || y.isSymbolicLink() || x.ino !== y.ino || x.dev !== y.dev)
91
+ pending();
92
+ }
93
+ catch {
94
+ return pending();
95
+ } }
96
+ async function readOwner(path, fingerprint) { const owner = await readJournal(path); if (owner.schemaVersion !== 1 || owner.fingerprint !== fingerprint || !/^[-a-f0-9]{36}$/.test(owner.incarnation) || owner.stage !== '.model-install-' + owner.incarnation || !owner.identity || !Number.isSafeInteger(owner.identity.pid) || owner.identity.pid < 1 || owner.identity.uid !== process.getuid?.() || typeof owner.identity.started !== 'string' || !owner.identity.started.trim())
97
+ pending(); return owner; }
98
+ async function cleanupStage(root, owner, file) { const stage = join(root, owner.stage); let stat; try {
99
+ stat = await lstat(stage);
100
+ }
101
+ catch (error) {
102
+ if (error.code === 'ENOENT')
103
+ return;
104
+ throw error;
105
+ } if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || (stat.mode & 0o077) !== 0 || await realpath(stage) !== stage)
106
+ pending(); const entries = await readdir(stage); if (entries.length) {
107
+ await sameFile(file, join(stage, 'ownership.json'));
108
+ } await rm(stage, { recursive: true }); await syncDirectory(root); }
109
+ /** Explicit recovery only: an absent or different process birth identity proves the downloader is gone.
110
+ * Downloading is in-process fetch; no native child can outlive this owner. A missing or
111
+ * foreign stage receipt remains pending, as does an interrupted recovery guard. */
112
+ export async function recoverModelPackage(cacheRoot, manifest) {
113
+ const plan = modelSetupPlan(manifest), root = await privateRoot(cacheRoot), lock = join(root, `.${plan.fingerprint}.lock`);
114
+ let owner;
115
+ try {
116
+ owner = await readOwner(lock, plan.fingerprint);
117
+ }
118
+ catch (error) {
119
+ if (error.code === 'ENOENT')
120
+ return { recovered: false };
121
+ return pending();
122
+ }
123
+ try {
124
+ if (await identityAlive(owner.identity))
125
+ throw new ModelSetupError('owner-busy');
126
+ }
127
+ catch (error) {
128
+ if (error instanceof ModelSetupError)
129
+ throw error;
130
+ return pending();
131
+ }
132
+ const file = join(root, `.model-owner-${owner.incarnation}.json`), guard = join(root, `.${plan.fingerprint}.recovery-lock`);
133
+ await sameFile(file, lock);
134
+ try {
135
+ await link(file, guard);
136
+ await syncDirectory(root);
137
+ }
138
+ catch (error) {
139
+ if (error.code === 'EEXIST')
140
+ return pending();
141
+ throw error;
142
+ }
143
+ try {
144
+ await sameFile(file, lock);
145
+ if (await identityAlive(owner.identity))
146
+ throw new ModelSetupError('owner-busy');
147
+ await cleanupStage(root, owner, file);
148
+ await sameFile(file, lock);
149
+ await unlink(lock);
150
+ await syncDirectory(root);
151
+ return { recovered: true };
152
+ }
153
+ finally {
154
+ await sameFile(file, guard);
155
+ await unlink(guard);
156
+ await syncDirectory(root);
157
+ try {
158
+ await lstat(lock);
159
+ }
160
+ catch (error) {
161
+ if (error.code === 'ENOENT') {
162
+ await unlink(file);
163
+ await syncDirectory(root);
164
+ }
165
+ else
166
+ throw error;
167
+ }
168
+ }
169
+ }
170
+ /** Explicit setup only. Never called by install hooks or by inference fallback. */
171
+ export async function provisionModelPackage(cacheRoot, manifest, options = {}) {
172
+ const plan = modelSetupPlan(manifest), root = await privateRoot(cacheRoot), destination = join(root, plan.fingerprint);
173
+ if (options.signal?.aborted)
174
+ throw new ModelSetupError('cancelled');
175
+ try {
176
+ await lstat(destination);
177
+ return await inspectModelPackage(root, manifest);
178
+ }
179
+ catch (error) {
180
+ if (error.code !== 'ENOENT')
181
+ throw error;
182
+ }
183
+ const lock = join(root, `.${plan.fingerprint}.lock`), incarnation = randomUUID(), ownerFile = join(root, `.model-owner-${incarnation}.json`);
184
+ const identity = await processIdentity(process.pid);
185
+ if (!identity)
186
+ pending();
187
+ const owner = { schemaVersion: 1, incarnation, fingerprint: plan.fingerprint, stage: '.model-install-' + incarnation, identity: identity };
188
+ await writeJournal(ownerFile, owner);
189
+ let linked = false;
190
+ try {
191
+ await link(ownerFile, lock);
192
+ linked = true;
193
+ await syncDirectory(root);
194
+ }
195
+ catch (error) {
196
+ if (linked)
197
+ return pending();
198
+ await unlink(ownerFile);
199
+ await syncDirectory(root);
200
+ if (error.code === 'EEXIST')
201
+ throw new ModelSetupError('owner-busy');
202
+ throw error;
203
+ }
204
+ let stage;
205
+ try {
206
+ const disk = await statfs(root);
207
+ if (disk.bavail * disk.bsize < plan.requiredFreeBytes)
208
+ throw new ModelSetupError('limit-exceeded');
209
+ stage = join(root, owner.stage);
210
+ await mkdir(stage, { mode: 0o700 });
211
+ await chmod(stage, 0o700);
212
+ await link(ownerFile, join(stage, 'ownership.json'));
213
+ await syncDirectory(stage);
214
+ await syncDirectory(root);
215
+ for (const artifact of manifest.artifacts) {
216
+ const signal = options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(600000)]) : AbortSignal.timeout(600000);
217
+ const response = await (options.fetch ?? fetch)(artifact.url, { signal });
218
+ if (!response.ok || !response.body || (response.url && new URL(response.url).protocol !== 'https:'))
219
+ throw new ModelSetupError('unavailable');
220
+ const declared = response.headers.get('content-length');
221
+ if (declared !== null && Number(declared) !== artifact.bytes) {
222
+ await response.body.cancel();
223
+ throw new ModelSetupError('integrity');
224
+ }
225
+ const file = await open(join(stage, artifact.name), constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
226
+ const hash = createHash('sha256');
227
+ let bytes = 0;
228
+ try {
229
+ for await (const chunk of response.body) {
230
+ if (signal.aborted)
231
+ throw new ModelSetupError('cancelled');
232
+ bytes += chunk.length;
233
+ if (bytes > artifact.bytes)
234
+ throw new ModelSetupError('integrity');
235
+ hash.update(chunk);
236
+ let offset = 0;
237
+ while (offset < chunk.length) {
238
+ const result = await file.write(chunk, offset, chunk.length - offset);
239
+ if (!result.bytesWritten)
240
+ throw new ModelSetupError('unavailable');
241
+ offset += result.bytesWritten;
242
+ }
243
+ }
244
+ if (bytes !== artifact.bytes || hash.digest('hex') !== artifact.sha256)
245
+ throw new ModelSetupError('integrity');
246
+ await file.chmod(artifact.executable ? 0o700 : 0o600);
247
+ await file.sync();
248
+ }
249
+ finally {
250
+ await file.close();
251
+ }
252
+ }
253
+ if (options.signal?.aborted)
254
+ throw new ModelSetupError('cancelled');
255
+ await syncDirectory(stage);
256
+ await rename(stage, destination);
257
+ stage = undefined;
258
+ await syncDirectory(root);
259
+ return await inspectModelPackage(root, manifest);
260
+ }
261
+ catch (error) {
262
+ if (options.signal?.aborted)
263
+ throw new ModelSetupError('cancelled');
264
+ throw error;
265
+ }
266
+ finally {
267
+ if (stage)
268
+ await cleanupStage(root, owner, ownerFile);
269
+ await sameFile(ownerFile, lock);
270
+ await unlink(lock);
271
+ await unlink(ownerFile);
272
+ await syncDirectory(root);
273
+ }
274
+ }
@@ -0,0 +1,35 @@
1
+ import type { ModelPackage } from './artifacts.js';
2
+ export type NativeKind = 'asr' | 'embedding';
3
+ export type NativeRole = 'asr-helper' | 'embedding-helper' | 'embedding-library' | 'notices';
4
+ export declare const nativeAssetNames: Record<NativeRole, string>;
5
+ export interface NativeArtifact {
6
+ role: NativeRole;
7
+ name: string;
8
+ bytes: number;
9
+ sha256: string;
10
+ }
11
+ export interface NativeCatalog {
12
+ schemaVersion: 1;
13
+ packageVersion: string;
14
+ sourceCommit: string;
15
+ platform: 'darwin';
16
+ architecture: 'arm64';
17
+ minimumMacOS: '26.0';
18
+ artifacts: NativeArtifact[];
19
+ build: {
20
+ asrSourceSha256: string;
21
+ embeddingSourceSha256: string;
22
+ swiftVersion: string;
23
+ clangVersion: string;
24
+ };
25
+ }
26
+ export declare class NativeCatalogError extends Error {
27
+ readonly code: 'runtime-missing' | 'unsupported' | 'integrity';
28
+ constructor(code: 'runtime-missing' | 'unsupported' | 'integrity');
29
+ }
30
+ /** Package-embedded release hashes are authority; local probe helper hashes never enter this catalog. */
31
+ export declare function parseNativeCatalog(value: unknown, expectedVersion: string): NativeCatalog;
32
+ export declare function nativePlatformSupported(platform?: NodeJS.Platform, architecture?: NodeJS.Architecture, kernel?: string): boolean;
33
+ export declare function nativeCatalogPath(): string;
34
+ export declare function loadNativeCatalog(expectedVersion: string, path?: string): Promise<NativeCatalog>;
35
+ export declare function nativeModelPackage(catalog: NativeCatalog, kind: NativeKind): ModelPackage;
@@ -0,0 +1,71 @@
1
+ import { constants } from 'node:fs';
2
+ import { open } from 'node:fs/promises';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { release } from 'node:os';
5
+ import { asrIdentity } from '../inference/asr/protocol.js';
6
+ import { embeddingIdentity } from '../inference/embedding/index.js';
7
+ export const nativeAssetNames = { 'asr-helper': 'sponge-speech-macos26-arm64', 'embedding-helper': 'sponge-embedding-macos26-arm64', 'embedding-library': 'libCLiteRTLM_mac.dylib', notices: 'native-NOTICES.txt' };
8
+ export class NativeCatalogError extends Error {
9
+ code;
10
+ constructor(code) {
11
+ super(code);
12
+ this.code = code;
13
+ }
14
+ }
15
+ const fail = () => { throw new NativeCatalogError('integrity'); };
16
+ function object(value, keys) { if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).sort().join(',') !== keys.sort().join(','))
17
+ fail(); return value; }
18
+ const sha = (v) => typeof v === 'string' && /^[a-f0-9]{64}$/u.test(v);
19
+ function version(v) { return typeof v === 'string' && /^(0|[1-9]\d{0,15})\.(0|[1-9]\d{0,15})\.(0|[1-9]\d{0,15})$/u.test(v) && v.split('.').every(n => Number.isSafeInteger(Number(n))); }
20
+ /** Package-embedded release hashes are authority; local probe helper hashes never enter this catalog. */
21
+ export function parseNativeCatalog(value, expectedVersion) {
22
+ const r = object(value, ['schemaVersion', 'packageVersion', 'sourceCommit', 'platform', 'architecture', 'minimumMacOS', 'artifacts', 'build']);
23
+ if (r.schemaVersion !== 1 || !version(expectedVersion) || r.packageVersion !== expectedVersion || typeof r.sourceCommit !== 'string' || !/^[a-f0-9]{40}$/u.test(r.sourceCommit) || r.platform !== 'darwin' || r.architecture !== 'arm64' || r.minimumMacOS !== '26.0' || !Array.isArray(r.artifacts) || r.artifacts.length !== 4)
24
+ fail();
25
+ const roles = new Set();
26
+ const artifacts = r.artifacts.map((value) => { const a = object(value, ['role', 'name', 'bytes', 'sha256']); if (typeof a.role !== 'string' || !Object.hasOwn(nativeAssetNames, a.role) || roles.has(a.role) || a.name !== nativeAssetNames[a.role] || typeof a.bytes !== 'number' || !Number.isSafeInteger(a.bytes) || a.bytes < 1 || a.bytes > (a.role === 'embedding-library' ? 160 * 1024 ** 2 : a.role === 'notices' ? 1024 ** 2 : 32 * 1024 ** 2) || !sha(a.sha256))
27
+ fail(); roles.add(a.role); if (a.role === 'embedding-library' && a.sha256 !== embeddingIdentity.librarySha256)
28
+ fail(); return a; });
29
+ const build = object(r.build, ['asrSourceSha256', 'embeddingSourceSha256', 'swiftVersion', 'clangVersion']);
30
+ if (!sha(build.asrSourceSha256) || !sha(build.embeddingSourceSha256) || ['swiftVersion', 'clangVersion'].some(k => typeof build[k] !== 'string' || !build[k] || Buffer.byteLength(String(build[k])) > 2048))
31
+ fail();
32
+ return { ...r, artifacts, build };
33
+ }
34
+ export function nativePlatformSupported(platform = process.platform, architecture = process.arch, kernel = release()) { return platform === 'darwin' && architecture === 'arm64' && /^\d+\./u.test(kernel) && Number(kernel.split('.')[0]) >= 25; }
35
+ export function nativeCatalogPath() { return fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? '../../native/runtime-manifest.json' : '../../../native/runtime-manifest.json', import.meta.url)); }
36
+ export async function loadNativeCatalog(expectedVersion, path = nativeCatalogPath()) {
37
+ let file;
38
+ try {
39
+ file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
40
+ }
41
+ catch (error) {
42
+ if (error.code === 'ENOENT')
43
+ throw new NativeCatalogError('runtime-missing');
44
+ throw new NativeCatalogError('integrity');
45
+ }
46
+ try {
47
+ const stat = await file.stat();
48
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size < 1 || stat.size > 16384)
49
+ fail();
50
+ let parsed;
51
+ try {
52
+ parsed = JSON.parse(await file.readFile('utf8'));
53
+ }
54
+ catch {
55
+ fail();
56
+ }
57
+ return parseNativeCatalog(parsed, expectedVersion);
58
+ }
59
+ finally {
60
+ await file.close();
61
+ }
62
+ }
63
+ export function nativeModelPackage(catalog, kind) {
64
+ parseNativeCatalog(catalog, catalog.packageVersion);
65
+ if (kind !== 'asr' && kind !== 'embedding')
66
+ fail();
67
+ const roles = kind === 'asr' ? ['asr-helper', 'notices'] : ['embedding-helper', 'embedding-library', 'notices'];
68
+ const artifacts = roles.map(role => { const a = catalog.artifacts.find(a => a.role === role); return { name: role === 'asr-helper' ? 'sponge-speech' : role === 'embedding-helper' ? 'sponge-embedding' : a.name, url: `https://github.com/hraness/sponge/releases/download/v${catalog.packageVersion}/${a.name}`, sha256: a.sha256, bytes: a.bytes, ...(role.endsWith('helper') ? { executable: true } : {}) }; });
69
+ artifacts.push(kind === 'asr' ? { name: 'whistle.cact', url: 'https://huggingface.co/Cactus-Compute/whistle/resolve/ca5287601bef25af26dcf2e1b2bdc0843a7c19e5/whistle.cact', sha256: asrIdentity.modelSha256, bytes: asrIdentity.modelBytes } : { name: 'embeddinggemma-2-text-vision-440m.litertlm', url: 'https://huggingface.co/litert-community/embeddinggemma-2-text-vision-440m-litert-lm/resolve/e301f74d5551b0c2641bd5cb4652a76239d5c5f8/embeddinggemma-2-text-vision-440m.litertlm', sha256: embeddingIdentity.modelSha256, bytes: 387710976 });
70
+ return { id: `sponge-${kind}-${catalog.packageVersion}-macos26-arm64`, license: 'Sponge helpers: proprietary; Needle, Whistle, LiteRT-LM and model: Apache-2.0; see native-NOTICES.txt', artifacts };
71
+ }
@@ -0,0 +1,9 @@
1
+ import { type ModelPackage } from './artifacts.js';
2
+ export declare const nativeDownloadAuthGuidance = "Set GH_TOKEN or GITHUB_TOKEN with Contents: read access to hraness/sponge, then retry setup. Credentials are never saved.";
3
+ /** A manifest-bound, in-process transport. Only exact repository API calls carry auth.
4
+ * Manual CDN redirects always discard auth; returned bytes still pass package hashes.
5
+ * Public model downloads never read credentials or receive authorization headers. */
6
+ export declare function nativeArtifactFetch(manifest: ModelPackage, options?: {
7
+ env?: NodeJS.ProcessEnv;
8
+ fetch?: typeof fetch;
9
+ }): typeof fetch;
@@ -0,0 +1,147 @@
1
+ import { ModelSetupError } from './artifacts.js';
2
+ const api = 'https://api.github.com/repos/hraness/sponge';
3
+ const nativeUrl = /^https:\/\/github\.com\/hraness\/sponge\/releases\/download\/(v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))\/([A-Za-z0-9_.-]+)$/;
4
+ const cdnHosts = new Set(['release-assets.githubusercontent.com', 'objects.githubusercontent.com', 'github-releases.githubusercontent.com']);
5
+ const redirectCodes = new Set([301, 302, 303, 307, 308]);
6
+ export const nativeDownloadAuthGuidance = 'Set GH_TOKEN or GITHUB_TOKEN with Contents: read access to hraness/sponge, then retry setup. Credentials are never saved.';
7
+ function fail(code) { throw new ModelSetupError(code); }
8
+ async function discard(response) { try {
9
+ await response.body?.cancel();
10
+ }
11
+ catch { } }
12
+ async function boundedJson(response) {
13
+ if (!response.body)
14
+ fail('integrity');
15
+ const chunks = [];
16
+ let bytes = 0;
17
+ try {
18
+ for await (const chunk of response.body) {
19
+ bytes += chunk.length;
20
+ if (bytes > 1024 * 1024)
21
+ fail('integrity');
22
+ chunks.push(chunk);
23
+ }
24
+ const value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
25
+ if (!value || typeof value !== 'object' || Array.isArray(value))
26
+ fail('integrity');
27
+ return value;
28
+ }
29
+ catch (error) {
30
+ if (error instanceof ModelSetupError)
31
+ throw error;
32
+ fail('integrity');
33
+ }
34
+ }
35
+ function credential(env) {
36
+ // An explicitly configured higher-priority identity never falls through to another.
37
+ const token = env.GH_TOKEN !== undefined ? env.GH_TOKEN : env.GITHUB_TOKEN;
38
+ if (!token || token.length > 4096 || /[^\x21-\x7e]/u.test(token))
39
+ fail('repair-auth');
40
+ return token;
41
+ }
42
+ /** A manifest-bound, in-process transport. Only exact repository API calls carry auth.
43
+ * Manual CDN redirects always discard auth; returned bytes still pass package hashes.
44
+ * Public model downloads never read credentials or receive authorization headers. */
45
+ export function nativeArtifactFetch(manifest, options = {}) {
46
+ const transport = options.fetch ?? globalThis.fetch, env = options.env ?? process.env, artifacts = new Map(manifest.artifacts.map(a => [a.url, a]));
47
+ const request = async (url, signal, headers) => {
48
+ if (signal?.aborted)
49
+ fail('cancelled');
50
+ try {
51
+ return await transport(url, { signal, redirect: 'manual', headers });
52
+ }
53
+ catch {
54
+ if (signal?.aborted)
55
+ fail('cancelled');
56
+ fail('unavailable');
57
+ }
58
+ };
59
+ const download = async (artifact, tag, name, signal) => {
60
+ const token = credential(env), auth = { Authorization: `Bearer ${token}`, 'X-GitHub-Api-Version': '2022-11-28', Accept: 'application/vnd.github+json' };
61
+ const metadataResponse = await request(`${api}/releases/tags/${tag}`, signal, auth);
62
+ if (!metadataResponse.ok) {
63
+ await discard(metadataResponse);
64
+ if ([401, 403, 404].includes(metadataResponse.status))
65
+ fail('repair-auth');
66
+ fail('unavailable');
67
+ }
68
+ const release = await boundedJson(metadataResponse);
69
+ if (release.tag_name !== tag || release.immutable !== true || release.draft !== false || release.prerelease !== false || !Number.isSafeInteger(release.id) || Number(release.id) <= 0 || release.url !== `${api}/releases/${release.id}` || release.html_url !== `https://github.com/hraness/sponge/releases/tag/${tag}` || !Array.isArray(release.assets) || release.assets.length > 1000)
70
+ fail('integrity');
71
+ const matches = release.assets.filter(a => a && typeof a === 'object' && a.name === name);
72
+ if (matches.length !== 1)
73
+ fail('integrity');
74
+ const asset = matches[0];
75
+ if (!Number.isSafeInteger(asset.id) || asset.id <= 0 || asset.state !== 'uploaded' || asset.size !== artifact.bytes || asset.digest !== `sha256:${artifact.sha256}` || asset.url !== `${api}/releases/assets/${asset.id}` || asset.browser_download_url !== artifact.url)
76
+ fail('integrity');
77
+ let response = await request(asset.url, signal, { ...auth, Accept: 'application/octet-stream' });
78
+ for (let redirects = 0; redirectCodes.has(response.status); redirects++) {
79
+ const location = response.headers.get('location');
80
+ await discard(response);
81
+ if (redirects >= 5 || !location)
82
+ fail('integrity');
83
+ let next;
84
+ try {
85
+ next = new URL(location);
86
+ }
87
+ catch {
88
+ fail('integrity');
89
+ }
90
+ if (next.protocol !== 'https:' || next.username || next.password || next.port || next.hash || !cdnHosts.has(next.hostname))
91
+ fail('integrity');
92
+ response = await request(next.href, signal); // No credential or GitHub API headers cross this boundary.
93
+ }
94
+ if (!response.ok) {
95
+ await discard(response);
96
+ if ([401, 403, 404].includes(response.status))
97
+ fail('repair-auth');
98
+ fail('unavailable');
99
+ }
100
+ return response;
101
+ };
102
+ return (async (input, init) => {
103
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url, artifact = artifacts.get(url), signal = init?.signal ?? undefined;
104
+ if (!artifact || init?.headers || init?.body || init?.method && init.method !== 'GET')
105
+ fail('integrity');
106
+ if (signal?.aborted)
107
+ fail('cancelled');
108
+ const match = nativeUrl.exec(url);
109
+ if (match) {
110
+ try {
111
+ return await download(artifact, match[1], match[2], signal);
112
+ }
113
+ catch (error) {
114
+ if (signal?.aborted)
115
+ fail('cancelled');
116
+ throw error;
117
+ }
118
+ }
119
+ // Only the catalog's pinned public Hugging Face models use public redirect handling.
120
+ const parsed = new URL(url);
121
+ if (parsed.origin !== 'https://huggingface.co' || parsed.username || parsed.password)
122
+ fail('integrity');
123
+ let currentUrl = url, response = await request(currentUrl, signal);
124
+ for (let redirects = 0; redirectCodes.has(response.status); redirects++) {
125
+ const location = response.headers.get('location');
126
+ await discard(response);
127
+ if (redirects >= 5 || !location)
128
+ fail('integrity');
129
+ let next;
130
+ try {
131
+ next = new URL(location, currentUrl);
132
+ }
133
+ catch {
134
+ fail('integrity');
135
+ }
136
+ if (next.protocol !== 'https:' || next.username || next.password || next.port || next.hash || !(next.hostname === 'huggingface.co' || next.hostname.endsWith('.huggingface.co') || next.hostname === 'hf.co' || next.hostname.endsWith('.hf.co')))
137
+ fail('integrity');
138
+ currentUrl = next.href;
139
+ response = await request(currentUrl, signal);
140
+ }
141
+ if (!response.ok) {
142
+ await discard(response);
143
+ fail('unavailable');
144
+ }
145
+ return response;
146
+ });
147
+ }
@@ -0,0 +1,24 @@
1
+ import type { AsrRuntime } from '../inference/asr/runtime.js';
2
+ import type { EmbeddingRuntime } from '../inference/embedding/index.js';
3
+ import { type NativeCatalog, type NativeKind } from './catalog.js';
4
+ export declare function nativeSetupPlan(kind: NativeKind, version: string): Promise<{
5
+ supported: boolean;
6
+ minimumMacOS: "26.0";
7
+ id: string;
8
+ license: string;
9
+ fingerprint: string;
10
+ downloadBytes: number;
11
+ requiredFreeBytes: number;
12
+ artifacts: readonly import("./artifacts.js").ModelArtifact[];
13
+ }>;
14
+ export declare function setupNativeRuntime(cacheRoot: string, catalog: NativeCatalog, kind: NativeKind, options?: {
15
+ signal?: AbortSignal;
16
+ }): Promise<AsrRuntime | EmbeddingRuntime>;
17
+ export declare function inspectAsrRuntime(cacheRoot: string, catalog: NativeCatalog): Promise<AsrRuntime>;
18
+ export declare function inspectEmbeddingRuntime(cacheRoot: string, catalog: NativeCatalog): Promise<EmbeddingRuntime>;
19
+ export declare function provisionNative(kind: 'asr', cacheRoot: string, version: string, signal?: AbortSignal): Promise<AsrRuntime>;
20
+ export declare function provisionNative(kind: 'embedding', cacheRoot: string, version: string, signal?: AbortSignal): Promise<EmbeddingRuntime>;
21
+ export declare function provisionNative(kind: NativeKind, cacheRoot: string, version: string, signal?: AbortSignal): Promise<AsrRuntime | EmbeddingRuntime>;
22
+ export declare function inspectNative(kind: 'asr', cacheRoot: string, version: string): Promise<AsrRuntime>;
23
+ export declare function inspectNative(kind: 'embedding', cacheRoot: string, version: string): Promise<EmbeddingRuntime>;
24
+ export declare function inspectNative(kind: NativeKind, cacheRoot: string, version: string): Promise<AsrRuntime | EmbeddingRuntime>;