@nodetool-ai/node-sdk 0.7.0-rc.23 → 0.8.2

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 (158) hide show
  1. package/README.md +34 -0
  2. package/dist/.nodetool-build-stamp +1 -0
  3. package/dist/base-node.d.ts +285 -14
  4. package/dist/base-node.d.ts.map +1 -1
  5. package/dist/base-node.js +359 -49
  6. package/dist/base-node.js.map +1 -1
  7. package/dist/class-name-to-title.d.ts +26 -0
  8. package/dist/class-name-to-title.d.ts.map +1 -0
  9. package/dist/class-name-to-title.js +59 -0
  10. package/dist/class-name-to-title.js.map +1 -0
  11. package/dist/code-analysis.d.ts +234 -0
  12. package/dist/code-analysis.d.ts.map +1 -0
  13. package/dist/code-analysis.js +919 -0
  14. package/dist/code-analysis.js.map +1 -0
  15. package/dist/code-body.d.ts +61 -0
  16. package/dist/code-body.d.ts.map +1 -0
  17. package/dist/code-body.js +288 -0
  18. package/dist/code-body.js.map +1 -0
  19. package/dist/code-node-validation.d.ts +67 -0
  20. package/dist/code-node-validation.d.ts.map +1 -0
  21. package/dist/code-node-validation.js +540 -0
  22. package/dist/code-node-validation.js.map +1 -0
  23. package/dist/content-card.d.ts +30 -0
  24. package/dist/content-card.d.ts.map +1 -0
  25. package/dist/content-card.js +68 -0
  26. package/dist/content-card.js.map +1 -0
  27. package/dist/correlation-validation.d.ts +19 -0
  28. package/dist/correlation-validation.d.ts.map +1 -0
  29. package/dist/correlation-validation.js +101 -0
  30. package/dist/correlation-validation.js.map +1 -0
  31. package/dist/cost-estimate.d.ts +244 -0
  32. package/dist/cost-estimate.d.ts.map +1 -0
  33. package/dist/cost-estimate.js +637 -0
  34. package/dist/cost-estimate.js.map +1 -0
  35. package/dist/decorators.d.ts.map +1 -1
  36. package/dist/decorators.js +4 -1
  37. package/dist/decorators.js.map +1 -1
  38. package/dist/docs/nodes.d.ts.map +1 -1
  39. package/dist/docs/nodes.js +3 -2
  40. package/dist/docs/nodes.js.map +1 -1
  41. package/dist/docs/overview.d.ts.map +1 -1
  42. package/dist/docs/overview.js +4 -3
  43. package/dist/docs/overview.js.map +1 -1
  44. package/dist/docs/workflows.d.ts.map +1 -1
  45. package/dist/docs/workflows.js +10 -9
  46. package/dist/docs/workflows.js.map +1 -1
  47. package/dist/field-classification.d.ts +31 -0
  48. package/dist/field-classification.d.ts.map +1 -0
  49. package/dist/field-classification.js +58 -0
  50. package/dist/field-classification.js.map +1 -0
  51. package/dist/graph-validation.d.ts +239 -0
  52. package/dist/graph-validation.d.ts.map +1 -0
  53. package/dist/graph-validation.js +1174 -0
  54. package/dist/graph-validation.js.map +1 -0
  55. package/dist/index.d.ts +27 -2
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +29 -1
  58. package/dist/index.js.map +1 -1
  59. package/dist/js-script-link.d.ts +53 -0
  60. package/dist/js-script-link.d.ts.map +1 -0
  61. package/dist/js-script-link.js +58 -0
  62. package/dist/js-script-link.js.map +1 -0
  63. package/dist/js-script-materialize.d.ts +52 -0
  64. package/dist/js-script-materialize.d.ts.map +1 -0
  65. package/dist/js-script-materialize.js +24 -0
  66. package/dist/js-script-materialize.js.map +1 -0
  67. package/dist/manifest-node-values.d.ts +106 -0
  68. package/dist/manifest-node-values.d.ts.map +1 -0
  69. package/dist/manifest-node-values.js +195 -0
  70. package/dist/manifest-node-values.js.map +1 -0
  71. package/dist/metadata.d.ts +75 -3
  72. package/dist/metadata.d.ts.map +1 -1
  73. package/dist/metadata.js +132 -49
  74. package/dist/metadata.js.map +1 -1
  75. package/dist/node-metadata.d.ts.map +1 -1
  76. package/dist/node-metadata.js +61 -56
  77. package/dist/node-metadata.js.map +1 -1
  78. package/dist/node-type-inventory.d.ts +51 -0
  79. package/dist/node-type-inventory.d.ts.map +1 -0
  80. package/dist/node-type-inventory.js +121 -0
  81. package/dist/node-type-inventory.js.map +1 -0
  82. package/dist/nodes/test-nodes.d.ts +77 -30
  83. package/dist/nodes/test-nodes.d.ts.map +1 -1
  84. package/dist/nodes/test-nodes.js +2 -34
  85. package/dist/nodes/test-nodes.js.map +1 -1
  86. package/dist/pack-loader.d.ts +202 -0
  87. package/dist/pack-loader.d.ts.map +1 -0
  88. package/dist/pack-loader.js +529 -0
  89. package/dist/pack-loader.js.map +1 -0
  90. package/dist/port-types.d.ts +15 -0
  91. package/dist/port-types.d.ts.map +1 -0
  92. package/dist/port-types.js +72 -0
  93. package/dist/port-types.js.map +1 -0
  94. package/dist/pricing-bundle.d.ts +83 -0
  95. package/dist/pricing-bundle.d.ts.map +1 -0
  96. package/dist/pricing-bundle.js +82 -0
  97. package/dist/pricing-bundle.js.map +1 -0
  98. package/dist/pricing-params.d.ts +59 -0
  99. package/dist/pricing-params.d.ts.map +1 -0
  100. package/dist/pricing-params.js +282 -0
  101. package/dist/pricing-params.js.map +1 -0
  102. package/dist/python-package-scan.d.ts +8 -0
  103. package/dist/python-package-scan.d.ts.map +1 -1
  104. package/dist/python-package-scan.js +21 -8
  105. package/dist/python-package-scan.js.map +1 -1
  106. package/dist/registry.d.ts +105 -2
  107. package/dist/registry.d.ts.map +1 -1
  108. package/dist/registry.js +403 -30
  109. package/dist/registry.js.map +1 -1
  110. package/dist/sandbox-bridge-packs.d.ts +39 -0
  111. package/dist/sandbox-bridge-packs.d.ts.map +1 -0
  112. package/dist/sandbox-bridge-packs.js +249 -0
  113. package/dist/sandbox-bridge-packs.js.map +1 -0
  114. package/dist/sandbox-catalog-host.d.ts +31 -0
  115. package/dist/sandbox-catalog-host.d.ts.map +1 -0
  116. package/dist/sandbox-catalog-host.js +78 -0
  117. package/dist/sandbox-catalog-host.js.map +1 -0
  118. package/dist/sandbox-module-catalog.d.ts +14 -0
  119. package/dist/sandbox-module-catalog.d.ts.map +1 -0
  120. package/dist/sandbox-module-catalog.js +402 -0
  121. package/dist/sandbox-module-catalog.js.map +1 -0
  122. package/dist/sandbox-npm-artifacts.d.ts +56 -0
  123. package/dist/sandbox-npm-artifacts.d.ts.map +1 -0
  124. package/dist/sandbox-npm-artifacts.js +17 -0
  125. package/dist/sandbox-npm-artifacts.js.map +1 -0
  126. package/dist/sandbox-pack-discovery.d.ts +93 -0
  127. package/dist/sandbox-pack-discovery.d.ts.map +1 -0
  128. package/dist/sandbox-pack-discovery.js +807 -0
  129. package/dist/sandbox-pack-discovery.js.map +1 -0
  130. package/dist/search.d.ts +29 -1
  131. package/dist/search.d.ts.map +1 -1
  132. package/dist/search.js +136 -9
  133. package/dist/search.js.map +1 -1
  134. package/dist/type-compat.d.ts +36 -0
  135. package/dist/type-compat.d.ts.map +1 -0
  136. package/dist/type-compat.js +98 -0
  137. package/dist/type-compat.js.map +1 -0
  138. package/dist/type-predicates.d.ts +25 -0
  139. package/dist/type-predicates.d.ts.map +1 -0
  140. package/dist/type-predicates.js +31 -0
  141. package/dist/type-predicates.js.map +1 -0
  142. package/dist/validation.d.ts +18 -0
  143. package/dist/validation.d.ts.map +1 -1
  144. package/dist/validation.js +70 -11
  145. package/dist/validation.js.map +1 -1
  146. package/dist/workflow-document-tools.d.ts +35 -0
  147. package/dist/workflow-document-tools.d.ts.map +1 -0
  148. package/dist/workflow-document-tools.js +402 -0
  149. package/dist/workflow-document-tools.js.map +1 -0
  150. package/dist/workflow-interface.d.ts +62 -0
  151. package/dist/workflow-interface.d.ts.map +1 -0
  152. package/dist/workflow-interface.js +351 -0
  153. package/dist/workflow-interface.js.map +1 -0
  154. package/package.json +67 -10
  155. package/dist/package-registry-client.d.ts +0 -12
  156. package/dist/package-registry-client.d.ts.map +0 -1
  157. package/dist/package-registry-client.js +0 -40
  158. package/dist/package-registry-client.js.map +0 -1
@@ -0,0 +1,807 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
3
+ import { dirname, join, relative, resolve, sep } from "node:path";
4
+ import { parse } from "acorn";
5
+ import { isString } from "./type-predicates.js";
6
+ import { generateSandboxHostFacade, generateSandboxWasmFacade, normalizeSandboxSpecifier, sandboxHostModule, sandboxHostModuleViolation, SANDBOX_HOST_FACADE_VERSION, parseSkillDocument, parseWasmBinary, skillSections, RESERVED_IDENTIFIERS, SANDBOX_CAPABILITY_PACK, SANDBOX_WASM_FACADE_VERSION, SandboxModuleManifestSchema, SandboxPackManifestSchema, wasmExportContractViolation, WasmBinaryError } from "@nodetool-ai/protocol";
7
+ import { npmModuleId } from "./sandbox-npm-artifacts.js";
8
+ export { normalizeSandboxSpecifier } from "@nodetool-ai/protocol";
9
+ export { npmModuleId, isNpmModuleId } from "./sandbox-npm-artifacts.js";
10
+ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
11
+ const ENCODED_PATH_CHARACTER = /%(?:2e|2f|5c)/i;
12
+ const JS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
13
+ /** Static resource limits enforced while discovering sandbox package artifacts. */
14
+ export const SANDBOX_PACKAGE_LIMITS = {
15
+ authoredJsBytes: 256 * 1024,
16
+ npmBundledJsBytes: 1024 * 1024,
17
+ wasmBytes: 4 * 1024 * 1024,
18
+ packBytes: 8 * 1024 * 1024,
19
+ skillBytes: 16 * 1024
20
+ };
21
+ /** The id a host module carries in a pack's module table. Never a file path. */
22
+ export function hostModuleId(hostId) {
23
+ return `host:${hostId}`;
24
+ }
25
+ /** Raised when a sandbox package's static artifacts violate the discovery contract. */
26
+ export class SandboxPackDiscoveryError extends Error {
27
+ constructor(message, options) {
28
+ super(message, options);
29
+ this.name = "SandboxPackDiscoveryError";
30
+ }
31
+ }
32
+ /** Discover a single installed pack without importing or executing pack code. */
33
+ export function discoverSandboxPack(packDir, options = {}) {
34
+ const dir = realpathOrThrow(packDir);
35
+ const packageJsonPath = join(dir, "package.json");
36
+ if (!existsSync(packageJsonPath) || !isRegularFile(packageJsonPath))
37
+ return undefined;
38
+ const packageJson = readJson(packageJsonPath);
39
+ const packageName = isString(packageJson.name) ? packageJson.name : undefined;
40
+ const nodetool = isRecord(packageJson.nodetool)
41
+ ? packageJson.nodetool
42
+ : undefined;
43
+ if (nodetool === undefined || nodetool.sandboxModules === undefined)
44
+ return undefined;
45
+ if (packageName === undefined) {
46
+ throw new SandboxPackDiscoveryError("sandbox pack has no valid package name");
47
+ }
48
+ const manifestResult = SandboxPackManifestSchema.safeParse({
49
+ apiVersion: nodetool.apiVersion,
50
+ sandboxModules: nodetool.sandboxModules,
51
+ internal: nodetool.internal
52
+ });
53
+ if (!manifestResult.success) {
54
+ throw new SandboxPackDiscoveryError(`${packageName}: invalid nodetool.sandboxModules manifest: ${manifestResult.error.message}`);
55
+ }
56
+ const manifest = manifestResult.data;
57
+ if (manifest.sandboxModules.length === 0)
58
+ return undefined;
59
+ const statuses = [];
60
+ const files = new Map();
61
+ const normalizedInternal = manifest.internal.map((file) => normalizeRelativeId(file));
62
+ const internalIds = new Set(normalizedInternal);
63
+ for (const internal of manifest.internal) {
64
+ const id = normalizeRelativeId(internal);
65
+ if (!id.endsWith(".js")) {
66
+ throw new SandboxPackDiscoveryError(`${packageName}: internal file must be JavaScript: ${internal}`);
67
+ }
68
+ addFile(dir, internal, "js", undefined, files);
69
+ }
70
+ const publicModules = [];
71
+ const npmArtifacts = new Map();
72
+ for (const module of manifest.sandboxModules) {
73
+ const specifier = normalizeSandboxSpecifier(packageName, module.name);
74
+ if (module.kind === "js" && module.npm !== undefined) {
75
+ const id = npmModuleId(module.npm);
76
+ const outcome = options.compiled?.({
77
+ packName: packageName,
78
+ packDir: dir,
79
+ specifier,
80
+ npmName: module.npm
81
+ });
82
+ const pending = {
83
+ specifier,
84
+ kind: "js",
85
+ id,
86
+ npm: module.npm,
87
+ dependencies: [],
88
+ declaration: module
89
+ };
90
+ if (outcome === undefined) {
91
+ statuses.push({
92
+ packName: packageName,
93
+ specifier,
94
+ status: "skipped",
95
+ code: "pending-compile",
96
+ message: `npm module ${module.npm} is not compiled yet; run \`nodetool packs compile\` to build it`
97
+ });
98
+ publicModules.push(pending);
99
+ continue;
100
+ }
101
+ if (outcome.status === "skipped") {
102
+ statuses.push({
103
+ packName: packageName,
104
+ specifier,
105
+ status: "skipped",
106
+ code: outcome.code,
107
+ message: outcome.message
108
+ });
109
+ publicModules.push(pending);
110
+ continue;
111
+ }
112
+ const source = outcome.artifact.source;
113
+ const bytes = Buffer.from(source, "utf8");
114
+ if (bytes.byteLength > SANDBOX_PACKAGE_LIMITS.npmBundledJsBytes) {
115
+ statuses.push({
116
+ packName: packageName,
117
+ specifier,
118
+ status: "skipped",
119
+ code: "npm-module-too-large",
120
+ message: `npm module ${module.npm} compiles to ${bytes.byteLength} bytes, over the ${SANDBOX_PACKAGE_LIMITS.npmBundledJsBytes} byte cap`
121
+ });
122
+ publicModules.push(pending);
123
+ continue;
124
+ }
125
+ if (files.has(id)) {
126
+ throw new SandboxPackDiscoveryError(`${packageName}: duplicate npm module ${module.npm}`);
127
+ }
128
+ files.set(id, { bytes, kind: "js", specifier });
129
+ npmArtifacts.set(id, outcome.artifact);
130
+ for (const warning of outcome.warnings ?? []) {
131
+ statuses.push({
132
+ packName: packageName,
133
+ specifier,
134
+ status: "warning",
135
+ code: "npm-module-warning",
136
+ message: warning
137
+ });
138
+ }
139
+ statuses.push({
140
+ packName: packageName,
141
+ specifier,
142
+ status: "ready",
143
+ code: "npm-module-compiled",
144
+ message: `npm module ${module.npm} compiled to ${bytes.byteLength} bytes`
145
+ });
146
+ publicModules.push({ ...pending, source });
147
+ continue;
148
+ }
149
+ if (module.kind === "host") {
150
+ // The trust rule, enforced where a manifest first meets NodeTool: an id
151
+ // that is not in the registry, or one claimed by a package the registry
152
+ // does not pin it to, never becomes a module. A pack ships no code for a
153
+ // host module, so there is nothing else to read.
154
+ const violation = sandboxHostModuleViolation(packageName, module.host);
155
+ if (violation !== undefined) {
156
+ throw new SandboxPackDiscoveryError(`${packageName}/${specifier}: ${violation}`);
157
+ }
158
+ const spec = sandboxHostModule(module.host);
159
+ if (spec === undefined) {
160
+ throw new SandboxPackDiscoveryError(`${packageName}/${specifier}: unknown host module`);
161
+ }
162
+ publicModules.push({
163
+ specifier,
164
+ kind: "host",
165
+ id: hostModuleId(module.host),
166
+ hostId: module.host,
167
+ // The generated facade is the module's whole guest-visible surface, so
168
+ // it is both what delivery serves and what the digest is over.
169
+ source: generateSandboxHostFacade(specifier, spec),
170
+ dependencies: [],
171
+ declaration: module
172
+ });
173
+ continue;
174
+ }
175
+ const moduleFile = module.file;
176
+ if (moduleFile === undefined) {
177
+ throw new SandboxPackDiscoveryError(`${packageName}: module ${specifier} has no file`);
178
+ }
179
+ const normalizedModuleId = normalizeRelativeId(moduleFile);
180
+ if (internalIds.has(normalizedModuleId)) {
181
+ throw new SandboxPackDiscoveryError(`${packageName}: public module duplicates internal file ${moduleFile}`);
182
+ }
183
+ const id = addFile(dir, moduleFile, module.kind, specifier, files);
184
+ const artifact = files.get(id);
185
+ if (artifact === undefined)
186
+ throw new SandboxPackDiscoveryError(`${packageName}: missing ${module.file}`);
187
+ const contract = module.kind === "wasm"
188
+ ? validateWasmModule(artifact.bytes, module, packageName, specifier)
189
+ : undefined;
190
+ const publicModule = {
191
+ specifier,
192
+ kind: module.kind,
193
+ id,
194
+ file: artifact.path,
195
+ dependencies: [],
196
+ declaration: module
197
+ };
198
+ if (module.kind === "js") {
199
+ publicModule.source = artifact.bytes.toString("utf8");
200
+ }
201
+ else {
202
+ publicModule.bytes = artifact.bytes;
203
+ }
204
+ if (contract !== undefined) {
205
+ publicModule.wasm = contract;
206
+ }
207
+ publicModules.push(publicModule);
208
+ }
209
+ const dependenciesById = new Map();
210
+ const pending = [...files.keys()];
211
+ for (let index = 0; index < pending.length; index += 1) {
212
+ const id = pending[index];
213
+ if (id === undefined || dependenciesById.has(id))
214
+ continue;
215
+ const artifact = files.get(id);
216
+ if (artifact === undefined)
217
+ throw new SandboxPackDiscoveryError(`${packageName}: missing module ${id}`);
218
+ // A compiled npm bundle is a self-contained leaf: the compiler already
219
+ // proved it resolves nothing, with a stricter scope-aware analysis than the
220
+ // authored-file scan below.
221
+ if (artifact.kind !== "js" || npmArtifacts.has(id)) {
222
+ dependenciesById.set(id, []);
223
+ continue;
224
+ }
225
+ const dependencies = inspectJavaScript(artifact.bytes.toString("utf8"), id, dir, files, packageName);
226
+ dependenciesById.set(id, dependencies);
227
+ for (const dependency of dependencies) {
228
+ if (!pending.includes(dependency))
229
+ pending.push(dependency);
230
+ }
231
+ }
232
+ const totalBytes = [...files.values()].reduce((sum, file) => sum + file.bytes.byteLength, 0);
233
+ if (totalBytes > SANDBOX_PACKAGE_LIMITS.packBytes) {
234
+ throw new SandboxPackDiscoveryError(`${packageName}: sandbox files exceed the ${SANDBOX_PACKAGE_LIMITS.packBytes} byte pack cap`);
235
+ }
236
+ assertAcyclic(dependenciesById, packageName);
237
+ const version = isString(packageJson.version)
238
+ ? packageJson.version
239
+ : undefined;
240
+ const resolvedModules = publicModules.map(({ declaration, ...module }) => {
241
+ const artifact = npmArtifacts.get(module.id);
242
+ if (module.kind === "host") {
243
+ return {
244
+ ...module,
245
+ dependencies: [],
246
+ digest: computeHostDigest({ ...module, declaration }, packageName, version)
247
+ };
248
+ }
249
+ return {
250
+ ...module,
251
+ dependencies: dependenciesById.get(module.id) ?? [],
252
+ digest: module.npm === undefined
253
+ ? computeModuleDigest({ ...module, declaration }, files, dependenciesById, packageName, version)
254
+ : artifact === undefined
255
+ ? computeNpmPlaceholderDigest({ ...module, declaration }, packageName, version)
256
+ : computeNpmCompiledDigest({ ...module, declaration }, artifact, packageName, version)
257
+ };
258
+ });
259
+ const graph = [...files.entries()]
260
+ .map(([id, file]) => ({
261
+ id,
262
+ kind: file.kind,
263
+ ...(file.kind === "js"
264
+ ? { source: file.bytes.toString("utf8") }
265
+ : { bytes: new Uint8Array(file.bytes) }),
266
+ dependencies: dependenciesById.get(id) ?? [],
267
+ internal: internalIds.has(id)
268
+ }))
269
+ .sort(compareById);
270
+ const skillCandidate = join(dir, "SKILL.md");
271
+ let skill;
272
+ if (!existsSync(skillCandidate)) {
273
+ statuses.push({
274
+ packName: packageName,
275
+ status: "warning",
276
+ code: "skill-missing",
277
+ message: "SKILL.md is missing; agent discoverability is disabled"
278
+ });
279
+ }
280
+ else {
281
+ let source;
282
+ try {
283
+ const skillPath = resolveContainedFile(dir, "SKILL.md");
284
+ source = readFileWithinLimit(skillPath, SANDBOX_PACKAGE_LIMITS.skillBytes);
285
+ }
286
+ catch {
287
+ // SKILL.md is optional; read and containment failures only disable discoverability.
288
+ source = undefined;
289
+ }
290
+ skill =
291
+ source === undefined
292
+ ? undefined
293
+ : parseSandboxPackSkill(source.toString("utf8"));
294
+ if (skill === undefined) {
295
+ statuses.push({
296
+ packName: packageName,
297
+ status: "warning",
298
+ code: "skill-invalid",
299
+ message: "SKILL.md is invalid, unavailable, or exceeds the 16 KB cap; agent discoverability is disabled"
300
+ });
301
+ }
302
+ }
303
+ const discovery = {
304
+ name: packageName,
305
+ dir,
306
+ modules: resolvedModules,
307
+ graph,
308
+ internal: [...internalIds].sort(compareStrings),
309
+ digest: computeDigest(resolvedModules, files, internalIds, manifest, packageName, version, npmArtifacts),
310
+ statuses
311
+ };
312
+ if (version !== undefined) {
313
+ discovery.version = version;
314
+ }
315
+ if (skill !== undefined) {
316
+ discovery.skill = skill;
317
+ }
318
+ return discovery;
319
+ }
320
+ /** Parse a pack's SKILL.md with the skill system's own rules. */
321
+ function parseSandboxPackSkill(source) {
322
+ const document = parseSkillDocument(source);
323
+ if (document === null)
324
+ return undefined;
325
+ return {
326
+ name: document.name,
327
+ description: document.description,
328
+ body: document.instructions,
329
+ sections: skillSections(document.instructions)
330
+ };
331
+ }
332
+ /** Discover sandbox modules from package directories without ambiguous pack ownership. */
333
+ export function discoverSandboxPacks(packageDirs, options = {}) {
334
+ const found = new Map();
335
+ for (const packageDir of packageDirs) {
336
+ const discovery = discoverSandboxPack(packageDir, options);
337
+ if (discovery === undefined)
338
+ continue;
339
+ if (found.has(discovery.name)) {
340
+ throw new SandboxPackDiscoveryError(`duplicate sandbox package discovery: ${discovery.name}`);
341
+ }
342
+ found.set(discovery.name, discovery);
343
+ }
344
+ return [...found.values()];
345
+ }
346
+ /** Compute the canonical digest for a complete sandbox package graph. */
347
+ export function computeSandboxModuleGraphDigest(modules, files, internal, metadata) {
348
+ return computeDigest(modules, files, internal, metadata?.manifest, metadata?.packageName, metadata?.packageVersion, metadata?.npmArtifacts);
349
+ }
350
+ function computeDigest(modules, files, internal, manifest, packageName, packageVersion, npmArtifacts) {
351
+ // An npm module without a compiled artifact has no file in the graph, so it
352
+ // cannot contribute an entry; one with an artifact is a file like any other.
353
+ const publicById = new Map(modules
354
+ .filter((module) => files.has(module.id))
355
+ .map((module) => [module.id, module]));
356
+ const compiled = [...(npmArtifacts?.entries() ?? [])]
357
+ .map(([id, artifact]) => ({
358
+ id,
359
+ compilerVersion: artifact.compilerVersion,
360
+ optionsDigest: artifact.optionsDigest,
361
+ inputsDigest: artifact.inputsDigest
362
+ }))
363
+ .sort(compareById);
364
+ const entries = [...files.entries()]
365
+ .map(([id, file]) => {
366
+ const module = publicById.get(id);
367
+ return {
368
+ id,
369
+ kind: file.kind ?? "js",
370
+ specifier: module?.specifier,
371
+ dependencies: module === undefined
372
+ ? []
373
+ : [...module.dependencies].sort(compareStrings),
374
+ internal: internal.has(id),
375
+ bytes: bytesDigest(file.bytes)
376
+ };
377
+ })
378
+ .sort(compareById);
379
+ const manifestMetadata = manifest === undefined
380
+ ? undefined
381
+ : {
382
+ apiVersion: manifest.apiVersion,
383
+ internal: [...manifest.internal].sort(compareStrings),
384
+ sandboxModules: [...manifest.sandboxModules]
385
+ .sort((left, right) => compareStrings(left.name, right.name))
386
+ .map((module) => moduleDigestDeclaration(module))
387
+ };
388
+ return createHash("sha256")
389
+ .update(JSON.stringify({
390
+ packageName,
391
+ packageVersion,
392
+ manifest: manifestMetadata,
393
+ entries,
394
+ compiled
395
+ }))
396
+ .digest("hex");
397
+ }
398
+ function computeModuleDigest(module, files, dependenciesById, packageName, packageVersion) {
399
+ const pending = [module.id];
400
+ const reachable = new Set();
401
+ for (let index = 0; index < pending.length; index += 1) {
402
+ const id = pending[index];
403
+ if (id === undefined || reachable.has(id))
404
+ continue;
405
+ reachable.add(id);
406
+ pending.push(...(dependenciesById.get(id) ?? []));
407
+ }
408
+ const entries = [...reachable]
409
+ .map((id) => {
410
+ const file = files.get(id);
411
+ if (file === undefined) {
412
+ throw new SandboxPackDiscoveryError(`${packageName}: missing sandbox graph file ${id}`);
413
+ }
414
+ return {
415
+ id,
416
+ kind: file.kind ?? "js",
417
+ dependencies: [...(dependenciesById.get(id) ?? [])].sort(compareStrings),
418
+ bytes: bytesDigest(file.bytes)
419
+ };
420
+ })
421
+ .sort(compareById);
422
+ const payload = {
423
+ packageName,
424
+ packageVersion,
425
+ specifier: module.specifier,
426
+ moduleId: module.id,
427
+ kind: module.kind,
428
+ declaration: moduleDigestDeclaration(module.declaration),
429
+ entries
430
+ };
431
+ // A WASM module's guest surface is generated, so the generator is part of
432
+ // what the digest pins: regenerate the facade differently and a workflow's
433
+ // saved digest stops matching, exactly as a changed binary would.
434
+ if (module.wasm !== undefined) {
435
+ payload.facadeVersion = SANDBOX_WASM_FACADE_VERSION;
436
+ payload.facade = generateSandboxWasmFacade(module.specifier, module.wasm);
437
+ }
438
+ return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
439
+ }
440
+ /**
441
+ * Digest for an npm module that has a compiled artifact.
442
+ *
443
+ * The bundled source is what the guest evaluates, so it is what the digest is
444
+ * over — plus the compiler's version and options digest, because the same
445
+ * package version compiled under different resolver conditions is a different
446
+ * module. A saved workflow whose pack was recompiled therefore reports drift
447
+ * instead of quietly running different code.
448
+ */
449
+ function computeNpmCompiledDigest(module, artifact, packageName, packageVersion) {
450
+ return createHash("sha256")
451
+ .update(JSON.stringify({
452
+ packageName,
453
+ packageVersion,
454
+ specifier: module.specifier,
455
+ moduleId: module.id,
456
+ npm: module.npm,
457
+ declaration: moduleDigestDeclaration(module.declaration),
458
+ compilerVersion: artifact.compilerVersion,
459
+ optionsDigest: artifact.optionsDigest,
460
+ inputsDigest: artifact.inputsDigest,
461
+ source: createHash("sha256").update(artifact.source).digest("hex")
462
+ }))
463
+ .digest("hex");
464
+ }
465
+ /**
466
+ * Digest for a host module.
467
+ *
468
+ * A host module ships no bytes, so what versions it is the surface the guest
469
+ * sees: the registry id, the export list behind it, and the facade generator.
470
+ * Drop an export from the registry and every workflow that pinned this digest
471
+ * reports drift, which is the point — the guest surface really did change.
472
+ */
473
+ function computeHostDigest(module, packageName, packageVersion) {
474
+ const spec = module.hostId === undefined ? undefined : sandboxHostModule(module.hostId);
475
+ return createHash("sha256")
476
+ .update(JSON.stringify({
477
+ packageName,
478
+ packageVersion,
479
+ specifier: module.specifier,
480
+ moduleId: module.id,
481
+ hostId: module.hostId,
482
+ exports: [...(spec?.exports ?? [])].sort(compareStrings),
483
+ facadeVersion: SANDBOX_HOST_FACADE_VERSION,
484
+ facade: module.source
485
+ }))
486
+ .digest("hex");
487
+ }
488
+ function computeNpmPlaceholderDigest(module, packageName, packageVersion) {
489
+ return createHash("sha256")
490
+ .update(JSON.stringify({
491
+ packageName,
492
+ packageVersion,
493
+ specifier: module.specifier,
494
+ npm: module.npm,
495
+ declaration: moduleDigestDeclaration(module.declaration)
496
+ }))
497
+ .digest("hex");
498
+ }
499
+ function moduleDigestDeclaration(module) {
500
+ if (module.kind === "js") {
501
+ return {
502
+ name: module.name,
503
+ kind: module.kind,
504
+ file: module.file,
505
+ npm: module.npm
506
+ };
507
+ }
508
+ if (module.kind === "host") {
509
+ return { name: module.name, kind: module.kind, host: module.host };
510
+ }
511
+ return {
512
+ name: module.name,
513
+ kind: module.kind,
514
+ file: module.file,
515
+ memoryPagesMax: module.memoryPagesMax,
516
+ limits: module.limits,
517
+ exports: [...module.exports].sort(compareExportNames)
518
+ };
519
+ }
520
+ function bytesDigest(bytes) {
521
+ return createHash("sha256").update(bytes).digest("hex");
522
+ }
523
+ function addFile(packDir, relativeFile, kind, specifier, files) {
524
+ const id = normalizeRelativeId(relativeFile);
525
+ const path = resolveContainedFile(packDir, relativeFile);
526
+ const limit = kind === "js"
527
+ ? SANDBOX_PACKAGE_LIMITS.authoredJsBytes
528
+ : SANDBOX_PACKAGE_LIMITS.wasmBytes;
529
+ const bytes = readRequiredFileWithinLimit(path, limit, `${id}: ${kind} module cap`);
530
+ const existing = files.get(id);
531
+ if (existing !== undefined) {
532
+ throw new SandboxPackDiscoveryError(`${id}: duplicate sandbox file`);
533
+ }
534
+ const stored = { path, bytes, kind };
535
+ if (specifier !== undefined) {
536
+ stored.specifier = specifier;
537
+ }
538
+ files.set(id, stored);
539
+ return id;
540
+ }
541
+ function inspectJavaScript(source, moduleId, packDir, files, packageName) {
542
+ let parsed;
543
+ try {
544
+ parsed = parse(source, { ecmaVersion: "latest", sourceType: "module" });
545
+ }
546
+ catch (error) {
547
+ throw new SandboxPackDiscoveryError(`${packageName}/${moduleId}: invalid JavaScript`, { cause: error });
548
+ }
549
+ const dependencies = [];
550
+ walkAst(parsed, (node) => {
551
+ if (node.type === "ImportExpression")
552
+ throw new SandboxPackDiscoveryError(`${packageName}/${moduleId}: dynamic import() is not allowed`);
553
+ if (node.type === "CallExpression" &&
554
+ node.callee?.type === "Identifier" &&
555
+ node.callee.name === "require") {
556
+ throw new SandboxPackDiscoveryError(`${packageName}/${moduleId}: require() is not allowed`);
557
+ }
558
+ if (node.type !== "ImportDeclaration" &&
559
+ node.type !== "ExportNamedDeclaration" &&
560
+ node.type !== "ExportAllDeclaration")
561
+ return;
562
+ const value = node.source?.value;
563
+ if (!isString(value))
564
+ return;
565
+ if (!value.startsWith(".")) {
566
+ // Capability-module specifiers are the one sanctioned external import:
567
+ // the guest loader resolves them to a mounted facade, mounting still
568
+ // requires the node body to import the module, and every call passes
569
+ // the per-call permission gate — so a pack module holding this import
570
+ // gains nothing the body did not already consent to.
571
+ if (value === SANDBOX_CAPABILITY_PACK ||
572
+ value.startsWith(`${SANDBOX_CAPABILITY_PACK}/`))
573
+ return;
574
+ throw new SandboxPackDiscoveryError(`${packageName}/${moduleId}: import outside the pack: ${value}`);
575
+ }
576
+ const packRoot = realpathOrThrow(packDir);
577
+ const importedCandidate = resolve(packRoot, dirname(moduleId), value);
578
+ if (importedCandidate !== packRoot &&
579
+ !importedCandidate.startsWith(`${packRoot}${sep}`) // key-boundary-ok: `${sep}` anchors this boundary.
580
+ ) {
581
+ throw new SandboxPackDiscoveryError(`${packageName}/${moduleId}: import escapes the package: ${value}`);
582
+ }
583
+ const importedId = normalizeRelativeId(relative(packRoot, importedCandidate));
584
+ const imported = files.get(importedId);
585
+ if (imported === undefined) {
586
+ throw new SandboxPackDiscoveryError(`${packageName}/${moduleId}: imported file ${value} is not declared as a public or internal file`);
587
+ }
588
+ else if (imported.kind !== "js") {
589
+ throw new SandboxPackDiscoveryError(`${packageName}/${moduleId}: JS import targets non-JS module ${value}`);
590
+ }
591
+ if (!dependencies.includes(importedId))
592
+ dependencies.push(importedId);
593
+ });
594
+ return dependencies.sort(compareStrings);
595
+ }
596
+ /**
597
+ * Validate a WASM binary against its manifest and return the call contract.
598
+ *
599
+ * Every rejection names the export and the rule it broke — "is a memory, not a
600
+ * function", "uses i64, which maps to bigint rather than number" — so a pack
601
+ * author is told what to change, not that something is unsupported.
602
+ */
603
+ function validateWasmModule(bytes, manifest, packageName, specifier) {
604
+ const validationBuffer = new ArrayBuffer(bytes.byteLength);
605
+ new Uint8Array(validationBuffer).set(bytes);
606
+ if (!WebAssembly.validate(validationBuffer))
607
+ throw new SandboxPackDiscoveryError(`${packageName}/${specifier}: invalid WASM binary`);
608
+ let module;
609
+ try {
610
+ module = parseWasmBinary(bytes);
611
+ }
612
+ catch (error) {
613
+ if (error instanceof WasmBinaryError) {
614
+ throw new SandboxPackDiscoveryError(`${packageName}/${specifier}: ${error.message}`, { cause: error });
615
+ }
616
+ throw error;
617
+ }
618
+ if (module.importCount !== 0)
619
+ throw new SandboxPackDiscoveryError(`${packageName}/${specifier}: WASM imports are not allowed`);
620
+ for (const memory of module.memories) {
621
+ if (memory.shared || memory.maximum === undefined)
622
+ throw new SandboxPackDiscoveryError(`${packageName}/${specifier}: every WASM memory must be unshared and declare a maximum`);
623
+ if (memory.maximum < memory.minimum ||
624
+ memory.maximum > manifest.memoryPagesMax ||
625
+ memory.maximum > 4096)
626
+ throw new SandboxPackDiscoveryError(`${packageName}/${specifier}: memory maximum exceeds the declared or host ceiling`);
627
+ }
628
+ const exports = manifest.exports.map((exported) => {
629
+ const wasmName = isString(exported) ? exported : exported.wasm;
630
+ const alias = isString(exported) ? exported : exported.as;
631
+ // The manifest schema already holds this, but discovery is its own
632
+ // enforcement point: a binary export that is not an identifier must be
633
+ // renamed with `{ "wasm": …, "as": … }`, and the alias must be one.
634
+ if (!JS_IDENTIFIER.test(alias) || RESERVED_IDENTIFIERS.has(alias)) {
635
+ throw new SandboxPackDiscoveryError(`${packageName}/${specifier}: export ${wasmName} is exposed as "${alias}", which is not a usable JavaScript identifier — map it with { "wasm": "${wasmName}", "as": "<identifier>" }`);
636
+ }
637
+ const entry = module.exports.get(wasmName);
638
+ const violation = wasmExportContractViolation(entry, entry === undefined ? undefined : module.functions[entry.index]);
639
+ if (violation !== undefined) {
640
+ throw new SandboxPackDiscoveryError(`${packageName}/${specifier}: export ${wasmName} ${violation}`);
641
+ }
642
+ return { wasm: wasmName, as: alias };
643
+ });
644
+ const contract = {
645
+ memoryPagesMax: manifest.memoryPagesMax,
646
+ exports
647
+ };
648
+ if (manifest.limits !== undefined) {
649
+ contract.limits = manifest.limits;
650
+ }
651
+ return contract;
652
+ }
653
+ function walkAst(value, visit) {
654
+ if (Array.isArray(value)) {
655
+ for (const item of value)
656
+ walkAst(item, visit);
657
+ return;
658
+ }
659
+ if (!isRecord(value))
660
+ return;
661
+ if (isString(value.type))
662
+ visit(value);
663
+ for (const child of Object.values(value))
664
+ walkAst(child, visit);
665
+ }
666
+ function assertAcyclic(graph, packageName) {
667
+ const visiting = new Set();
668
+ const visited = new Set();
669
+ const visit = (id) => {
670
+ if (visiting.has(id))
671
+ throw new SandboxPackDiscoveryError(`${packageName}: internal module cycle includes ${id}`);
672
+ if (visited.has(id))
673
+ return;
674
+ visiting.add(id);
675
+ for (const dependency of graph.get(id) ?? [])
676
+ visit(dependency);
677
+ visiting.delete(id);
678
+ visited.add(id);
679
+ };
680
+ for (const id of graph.keys())
681
+ visit(id);
682
+ }
683
+ function resolveContainedFile(packDir, relativeFile) {
684
+ const root = realpathOrThrow(packDir);
685
+ const candidate = resolve(root, relativeFile);
686
+ const resolved = realpathOrThrow(candidate);
687
+ if (resolved !== root && !resolved.startsWith(`${root}${sep}`)) // key-boundary-ok: `${sep}` anchors this boundary.
688
+ throw new SandboxPackDiscoveryError(`${relativeFile}: file escapes the package directory`);
689
+ assertNoSymlink(root, candidate);
690
+ return resolved;
691
+ }
692
+ function assertNoSymlink(root, candidate) {
693
+ const relativePath = relative(root, candidate);
694
+ let current = root;
695
+ for (const part of relativePath.split(sep)) {
696
+ if (part.length === 0)
697
+ continue;
698
+ current = join(current, part);
699
+ if (lstatSync(current).isSymbolicLink())
700
+ throw new SandboxPackDiscoveryError(`${relativePath}: symlinks are not allowed in sandbox modules`);
701
+ }
702
+ }
703
+ function normalizeRelativeId(file) {
704
+ const normalized = file.replaceAll("\\", "/");
705
+ if (CONTROL_CHARACTERS.test(normalized) ||
706
+ ENCODED_PATH_CHARACTER.test(normalized) ||
707
+ normalized.startsWith("/") ||
708
+ normalized.split("/").some((part) => part === ".." || part.length === 0)) {
709
+ throw new SandboxPackDiscoveryError(`${file}: invalid package-relative path`);
710
+ }
711
+ const id = normalized
712
+ .split("/")
713
+ .filter((part) => part !== ".")
714
+ .join("/");
715
+ if (id.length === 0)
716
+ throw new SandboxPackDiscoveryError(`${file}: invalid package-relative path`);
717
+ return id;
718
+ }
719
+ function realpathOrThrow(path) {
720
+ try {
721
+ return realpathSync(path);
722
+ }
723
+ catch (error) {
724
+ throw new SandboxPackDiscoveryError(`sandbox path does not exist: ${path}`, { cause: error });
725
+ }
726
+ }
727
+ function readFileOrThrow(path) {
728
+ try {
729
+ return readFileSync(path);
730
+ }
731
+ catch (error) {
732
+ throw new SandboxPackDiscoveryError(`cannot read sandbox file: ${path}`, {
733
+ cause: error
734
+ });
735
+ }
736
+ }
737
+ function readFileWithinLimit(path, limit, description) {
738
+ let size;
739
+ try {
740
+ size = statSync(path).size;
741
+ }
742
+ catch (error) {
743
+ throw new SandboxPackDiscoveryError(`cannot stat sandbox file: ${path}`, {
744
+ cause: error
745
+ });
746
+ }
747
+ if (size > limit) {
748
+ if (description !== undefined) {
749
+ throw new SandboxPackDiscoveryError(`${description} exceeds the ${limit} byte limit`);
750
+ }
751
+ return undefined;
752
+ }
753
+ const bytes = readFileOrThrow(path);
754
+ if (bytes.byteLength > limit) {
755
+ if (description !== undefined) {
756
+ throw new SandboxPackDiscoveryError(`${description} exceeds the ${limit} byte limit`);
757
+ }
758
+ return undefined;
759
+ }
760
+ return bytes;
761
+ }
762
+ function readRequiredFileWithinLimit(path, limit, description) {
763
+ const bytes = readFileWithinLimit(path, limit, description);
764
+ if (bytes === undefined) {
765
+ throw new SandboxPackDiscoveryError(`${description} is unavailable`);
766
+ }
767
+ return bytes;
768
+ }
769
+ function isRegularFile(path) {
770
+ try {
771
+ return statSync(path).isFile();
772
+ }
773
+ catch {
774
+ // An unreadable or missing candidate is not a package file.
775
+ return false;
776
+ }
777
+ }
778
+ function readJson(path) {
779
+ try {
780
+ const value = JSON.parse(readFileSync(path, "utf8"));
781
+ if (!isRecord(value))
782
+ throw new Error("package.json must be an object");
783
+ return value;
784
+ }
785
+ catch (error) {
786
+ throw new SandboxPackDiscoveryError(`invalid package.json: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
787
+ }
788
+ }
789
+ function isRecord(value) {
790
+ return typeof value === "object" && value !== null && !Array.isArray(value);
791
+ }
792
+ function compareStrings(left, right) {
793
+ return left < right ? -1 : left > right ? 1 : 0;
794
+ }
795
+ function compareExportNames(left, right) {
796
+ const leftName = isString(left) ? left : left.wasm;
797
+ const rightName = isString(right) ? right : right.wasm;
798
+ return compareStrings(leftName, rightName);
799
+ }
800
+ function compareById(left, right) {
801
+ return compareStrings(left.id, right.id);
802
+ }
803
+ /** Parse one untrusted sandbox-module declaration at the package boundary. */
804
+ export function sandboxModuleManifest(value) {
805
+ return SandboxModuleManifestSchema.parse(value);
806
+ }
807
+ //# sourceMappingURL=sandbox-pack-discovery.js.map