@lapxo/bound 0.2.2 → 0.2.4

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 (79) hide show
  1. package/.github/workflows/release.yml +67 -0
  2. package/.github/workflows/test.yml +35 -0
  3. package/CITATION.cff +19 -0
  4. package/TARGET.bound +80 -21
  5. package/dist/cli/accept.js +103 -71
  6. package/dist/cli/place.js +3 -2
  7. package/dist/cli/standing-result.js +3 -5
  8. package/dist/cli/takes.js +4 -3
  9. package/dist/cli/verb.js +2 -1
  10. package/dist/engine/ports.d.ts +6 -1
  11. package/dist/engine/ports.js +23 -9
  12. package/dist/fold/claims.d.ts +29 -0
  13. package/dist/fold/claims.js +2 -1
  14. package/dist/fold/closed.js +2 -1
  15. package/dist/fold/digests.js +7 -7
  16. package/dist/host/admitted-place.js +3 -31
  17. package/dist/host/call-context.d.ts +20 -0
  18. package/dist/host/call-context.js +14 -0
  19. package/dist/host/capsule-process.js +2 -1
  20. package/dist/host/capsule.js +7 -4
  21. package/dist/host/commit-result.d.ts +6 -0
  22. package/dist/host/commit-result.js +5 -0
  23. package/dist/host/contextual-place.js +9 -14
  24. package/dist/host/host-controls.js +19 -0
  25. package/dist/host/in-process-act.d.ts +82 -0
  26. package/dist/host/in-process-act.js +84 -0
  27. package/dist/host/in-process-context.d.ts +46 -0
  28. package/dist/host/in-process-context.js +91 -0
  29. package/dist/host/in-process-root.d.ts +117 -0
  30. package/dist/host/in-process-root.js +84 -0
  31. package/dist/host/in-process.d.ts +41 -0
  32. package/dist/host/in-process.js +55 -0
  33. package/dist/host/index.d.ts +15 -0
  34. package/dist/host/index.js +7 -0
  35. package/dist/host/native/loader.js +1 -1
  36. package/dist/host/native/module-closure.d.ts +35 -0
  37. package/dist/host/native/module-closure.js +34 -3
  38. package/dist/host/native/module-delivery.d.ts +62 -0
  39. package/dist/host/native/module-delivery.js +151 -0
  40. package/dist/host/native/module-syntax.js +3 -1
  41. package/dist/host/native-receiver.d.ts +19 -0
  42. package/dist/host/native-receiver.js +25 -0
  43. package/dist/host/order.js +2 -0
  44. package/dist/host/place-act-transaction.d.ts +24 -0
  45. package/dist/host/place-act-transaction.js +78 -0
  46. package/dist/host/place-clock-selection.d.ts +55 -0
  47. package/dist/host/place-clock-selection.js +1 -1
  48. package/dist/host/ports/local-act.d.ts +44 -0
  49. package/dist/host/ports/local-act.js +13 -2
  50. package/dist/host/ports/place-act.d.ts +49 -0
  51. package/dist/host/ports/place-act.js +19 -54
  52. package/dist/host/ports/publication.d.ts +10 -0
  53. package/dist/host/root-clock-source.d.ts +86 -0
  54. package/dist/host/root-clock-source.js +7 -5
  55. package/dist/host/signature-era.js +12 -0
  56. package/dist/host/signing.js +1 -1
  57. package/dist/host/source-content.js +2 -1
  58. package/dist/host/standing-carrier.js +18 -0
  59. package/dist/host/standing-projection.js +50 -0
  60. package/dist/land/act.js +60 -26
  61. package/dist/observe/run.js +2 -1
  62. package/dist/observe/runner.js +16 -8
  63. package/docs/contracts/closure-delivery.md +23 -0
  64. package/docs/contracts/closure-delivery.publication.json +25 -0
  65. package/docs/contracts/native-host.md +21 -0
  66. package/docs/contracts/native-host.publication.json +25 -0
  67. package/docs/contracts/native-publication.inventory.json +2 -1
  68. package/docs/contracts/publication.inventory.json +29 -0
  69. package/docs/guide.md +10 -0
  70. package/docs/reference.md +39 -0
  71. package/docs/why.md +109 -0
  72. package/package.json +43 -2
  73. package/receipts.bound +10405 -9625
  74. package/samples/module-delivery/inputs/delivery.json +1 -0
  75. package/samples/native-host/no.json +10 -0
  76. package/samples/native-host/yes.json +20 -0
  77. package/samples/no/module-delivery/delivery.json +1 -0
  78. package/vectors/native/module-delivery.vector.json +32 -0
  79. package/vectors/native/native-host.vector.json +62 -0
@@ -0,0 +1,117 @@
1
+ import { type ReadingIdentityContext } from '@lapxo/topos/contract';
2
+ import { type ContentReadPort, type SigningPort, type PortLimits } from '../engine/ports.ts';
3
+ import { type HostInstrumentRuntime } from './call-context.ts';
4
+ import type { LocalActBundle } from './ports/local-act.ts';
5
+ import type { PublicationStorage } from './ports/publication.ts';
6
+ export interface RootLotContext {
7
+ readonly place: string;
8
+ readonly snapshot: string;
9
+ readonly source: string;
10
+ readonly digest: string;
11
+ readonly reading: ReadingIdentityContext;
12
+ readonly limits: PortLimits & {
13
+ readonly records: number;
14
+ };
15
+ readonly signal?: AbortSignal;
16
+ }
17
+ /** The receiver selects the actual native place, instrument and signer. The lot
18
+ * supplies only canonical unsigned Root configuration, never host capabilities. */
19
+ export interface RootLotPorts {
20
+ readonly root: string;
21
+ readonly instrumentName: string;
22
+ readonly runtime: HostInstrumentRuntime;
23
+ readonly read: ContentReadPort;
24
+ readonly signer: {
25
+ readonly keyId: string;
26
+ readonly name: string;
27
+ readonly sign: SigningPort;
28
+ };
29
+ readonly storage?: PublicationStorage;
30
+ readonly prepared?: (identity: string) => void;
31
+ readonly limits: RootLotContext['limits'] & {
32
+ readonly diagnostics: number;
33
+ };
34
+ }
35
+ /** Snapshot identity, not a new wire, authority algorithm or current permit. */
36
+ export declare const rootLotSnapshot: (root: string) => string;
37
+ export declare function landRootLotInProcess(context: RootLotContext, ports: RootLotPorts): Promise<{
38
+ kind: "refuse";
39
+ code: string;
40
+ diagnostics: {
41
+ stream: "out" | "error";
42
+ text: string;
43
+ }[];
44
+ value?: undefined;
45
+ identity?: undefined;
46
+ diagnostic?: undefined;
47
+ } | {
48
+ kind: "fact";
49
+ value: {
50
+ result: string;
51
+ commitment: string;
52
+ conformance: "refused" | "complete";
53
+ reading: string;
54
+ identity?: string | undefined;
55
+ act?: import("@lapxo/topos/wire").ActResult | undefined;
56
+ place: string;
57
+ status: "once" | "landed" | "already-held";
58
+ };
59
+ diagnostics: {
60
+ stream: "out" | "error";
61
+ text: string;
62
+ }[];
63
+ code?: undefined;
64
+ identity?: undefined;
65
+ diagnostic?: undefined;
66
+ } | {
67
+ kind: "uncertain";
68
+ identity: string;
69
+ code: string;
70
+ diagnostic: string;
71
+ diagnostics: {
72
+ stream: "out" | "error";
73
+ text: string;
74
+ }[];
75
+ value?: undefined;
76
+ } | {
77
+ kind: "fact";
78
+ value: {
79
+ place: string;
80
+ status: "once" | "landed";
81
+ identity: string;
82
+ conformance: "incomplete";
83
+ };
84
+ diagnostic: string;
85
+ diagnostics: {
86
+ stream: "out" | "error";
87
+ text: string;
88
+ }[];
89
+ code?: undefined;
90
+ identity?: undefined;
91
+ } | {
92
+ kind: "refuse";
93
+ code: string;
94
+ diagnostic: string;
95
+ diagnostics: {
96
+ stream: "out" | "error";
97
+ text: string;
98
+ }[];
99
+ value?: undefined;
100
+ identity?: undefined;
101
+ }>;
102
+ /** Authenticated historical query only; never revives authority or signs. */
103
+ export declare function recoverRootActInProcess(identity: string, ports: Pick<RootLotPorts, 'root' | 'instrumentName' | 'runtime'>): {
104
+ kind: "fact";
105
+ value: {
106
+ historical: true;
107
+ act: import("@lapxo/topos/wire").ActResult;
108
+ bundle: LocalActBundle;
109
+ };
110
+ code?: undefined;
111
+ diagnostic?: undefined;
112
+ } | {
113
+ kind: "refuse";
114
+ code: string;
115
+ diagnostic: string;
116
+ value?: undefined;
117
+ };
@@ -0,0 +1,84 @@
1
+ import { canonical, parse } from '@lapxo/topos/wire';
2
+ import { readingIdentity, readingReceiptIdentity } from '@lapxo/topos/contract';
3
+ import { readContent } from "../engine/ports.js";
4
+ import { acceptRootRecords } from "../cli/accept.js";
5
+ import { verifiedLegacyActResult } from "../cli/act-result.js";
6
+ import { admissionHistoryAt, admissionAnchor } from "../fold/admission-history.js";
7
+ import { isConfig } from "../fold/claims.js";
8
+ import { sha256, isSha256 } from "./hash.js";
9
+ import { BoundRefusal } from "./refusals.js";
10
+ import { refuseHost as fail } from "./host-controls.js";
11
+ import { ActCommitUnknown } from "./commit-result.js";
12
+ import { requireEffects } from "./read-only.js";
13
+ import { withHostCall } from "./call-context.js";
14
+ import { withActHost } from "../land/act.js";
15
+ /** Snapshot identity, not a new wire, authority algorithm or current permit. */
16
+ export const rootLotSnapshot = (root) => sha256(admissionHistoryAt(root, Number.MAX_SAFE_INTEGER).join('\n') + '\n');
17
+ export async function landRootLotInProcess(context, ports) {
18
+ const diagnostics = [];
19
+ let size = 0;
20
+ let committed;
21
+ const runtime = structuredClone(ports.runtime), limits = { ...context.limits };
22
+ return withHostCall({ instrumentName: ports.instrumentName, runtime, output: (stream, text) => { size += new TextEncoder().encode(text).length; if (size > ports.limits.diagnostics)
23
+ fail('budget'); diagnostics.push({ stream, text }); } }, () => withActHost({ name: ports.instrumentName, report: text => { size += new TextEncoder().encode(text).length; if (size > ports.limits.diagnostics)
24
+ fail('budget'); diagnostics.push({ stream: 'error', text }); }, signal: context.signal }, async () => {
25
+ try {
26
+ requireEffects('Root lot admission');
27
+ if (!context.place || !ports.root || !ports.instrumentName || !runtime.root || !runtime.entry || !runtime.directory || !['.ts', '.js'].includes(runtime.extension) || !Array.isArray(runtime.lock) || !runtime.lock.length)
28
+ fail('context');
29
+ if (limits.records > ports.limits.records || limits.maxBytes > ports.limits.maxBytes || limits.timeoutMs > ports.limits.timeoutMs || !Number.isSafeInteger(limits.records) || limits.records < 1 || !Number.isSafeInteger(ports.limits.diagnostics) || ports.limits.diagnostics < 1)
30
+ fail('budget');
31
+ const { place, snapshot, source, digest, signal } = context, reading = structuredClone(context.reading);
32
+ if (!isSha256(snapshot) || rootLotSnapshot(ports.root) !== snapshot)
33
+ fail('stale');
34
+ const anchor = admissionAnchor(ports.root);
35
+ const identity = readingIdentity({ ...reading, inputs: [...reading.inputs, ['host/place', sha256(place)], ['host/snapshot', snapshot], ['host/anchor', sha256(JSON.stringify(anchor))], ['host/draft', digest], ['host/principal', sha256(ports.signer.keyId)], ['host/operation', sha256(JSON.stringify(['root-lot', limits]))]] }, sha256);
36
+ const bytes = await readContent(source, digest, { read: ports.read, digestBytes: sha256 }, limits, signal);
37
+ let text;
38
+ try {
39
+ text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
40
+ }
41
+ catch {
42
+ return fail('snapshot-format');
43
+ }
44
+ const records = text.endsWith('\n') ? text.slice(0, -1).split('\n') : [];
45
+ if (!records.length || records.length > limits.records)
46
+ fail('budget');
47
+ for (const line of records) {
48
+ const parsed = parse(line);
49
+ if (parsed.kind !== 'fact' || canonical(parsed.value.fields, parsed.value.version) !== line || !isConfig(line) || parsed.value.fields.by !== 'target' || parsed.value.fields.epoch !== undefined || parsed.value.fields.sig !== undefined)
50
+ fail('root-lot-format');
51
+ }
52
+ if (rootLotSnapshot(ports.root) !== snapshot)
53
+ fail('stale');
54
+ const code = await acceptRootRecords(ports.root, { records, keyId: ports.signer.keyId, signer: { name: ports.signer.name, sign: ports.signer.sign, signal }, signal, storage: ports.storage, prepared: ports.prepared, committed: (identity, bundle, status) => { committed = { identity, bundle, status }; } });
55
+ if (code !== 0 && !committed)
56
+ return { kind: 'refuse', code: 'host/act', diagnostics };
57
+ const value = { place, status: committed?.status ?? 'already-held', ...(committed ? { identity: committed.identity, act: verifiedLegacyActResult(ports.root, committed.identity).result } : {}), conformance: code === 0 ? 'complete' : 'refused', reading: identity };
58
+ const result = sha256(JSON.stringify(value) + '\n');
59
+ return { kind: 'fact', value: { ...value, result, commitment: readingReceiptIdentity(identity, result, sha256) }, diagnostics };
60
+ }
61
+ catch (error) {
62
+ if (error instanceof ActCommitUnknown)
63
+ return { kind: 'uncertain', identity: error.identity, code: error.code, diagnostic: error.message, diagnostics };
64
+ if (committed)
65
+ return { kind: 'fact', value: { place: context.place, status: committed.status, identity: committed.identity, conformance: 'incomplete' }, diagnostic: error instanceof Error ? error.message : 'post-commit failure', diagnostics };
66
+ return { kind: 'refuse', code: error instanceof BoundRefusal ? error.code : 'host/failed', diagnostic: error instanceof Error ? error.message : 'REFUSE·host/failed', diagnostics };
67
+ }
68
+ }));
69
+ }
70
+ /** Authenticated historical query only; never revives authority or signs. */
71
+ export function recoverRootActInProcess(identity, ports) {
72
+ return withHostCall({ instrumentName: ports.instrumentName, runtime: structuredClone(ports.runtime), output: () => { } }, () => {
73
+ try {
74
+ if (!isSha256(identity))
75
+ fail('context');
76
+ const recovered = verifiedLegacyActResult(ports.root, identity);
77
+ return { kind: 'fact', value: { historical: true, act: recovered.result, bundle: recovered.bundle } };
78
+ }
79
+ catch (error) {
80
+ return { kind: 'refuse', code: error instanceof BoundRefusal ? error.code : 'host/failed', diagnostic: error instanceof Error ? error.message : 'REFUSE·host/failed' };
81
+ }
82
+ ;
83
+ });
84
+ }
@@ -0,0 +1,41 @@
1
+ import type { Signer } from '@lapxo/topos/wire';
2
+ import type { ReadingIdentityContext } from '@lapxo/topos/contract';
3
+ import type { ContentReadPort, PortLimits } from '../engine/ports.ts';
4
+ /** Initial read-only host slice. The receiver supplies the original anchor and
5
+ * verified implementation/policy references; the offered bytes choose none of them.
6
+ * Context references commit identity, not authority or conformance certification.
7
+ */
8
+ export interface RootStandingHostContext {
9
+ readonly place: string;
10
+ readonly source: string;
11
+ readonly snapshot: string;
12
+ readonly anchor: Signer;
13
+ readonly reading: ReadingIdentityContext;
14
+ readonly limits: PortLimits & {
15
+ readonly records: number;
16
+ };
17
+ readonly signal?: AbortSignal;
18
+ }
19
+ export type RootStandingHostResult = {
20
+ readonly kind: 'fact';
21
+ readonly value: {
22
+ readonly place: string;
23
+ readonly rootStanding: readonly string[];
24
+ readonly reading: string;
25
+ readonly result: string;
26
+ readonly commitment: string;
27
+ };
28
+ } | {
29
+ readonly kind: 'refuse';
30
+ readonly code: string;
31
+ readonly diagnostic: string;
32
+ };
33
+ /** Uses the existing bounded content port, authority evaluator and projection.
34
+ * It projects the Root component of a complete signed Root wire snapshot; it has no persistence callback,
35
+ * current-root global, console output, process entry point or CLI subprocess.
36
+ * Contextual places and their carriers are not this Root-only projection.
37
+ * The returned commitment is unsigned and is not a paid native receipt.
38
+ */
39
+ export declare function readRootStandingInProcess(context: RootStandingHostContext, ports: {
40
+ readonly read: ContentReadPort;
41
+ }): Promise<RootStandingHostResult>;
@@ -0,0 +1,55 @@
1
+ import { parse } from '@lapxo/topos/wire';
2
+ import { readingIdentity, readingReceiptIdentity } from '@lapxo/topos/contract';
3
+ import { readContent } from "../engine/ports.js";
4
+ import { sha256, isSha256 } from "./hash.js";
5
+ import { BoundRefusal } from "./refusals.js";
6
+ import { refuseHost as refuse } from "./host-controls.js";
7
+ import { assertStandingView, publicStandingProjection } from "./standing-projection.js";
8
+ /** Uses the existing bounded content port, authority evaluator and projection.
9
+ * It projects the Root component of a complete signed Root wire snapshot; it has no persistence callback,
10
+ * current-root global, console output, process entry point or CLI subprocess.
11
+ * Contextual places and their carriers are not this Root-only projection.
12
+ * The returned commitment is unsigned and is not a paid native receipt.
13
+ */
14
+ export async function readRootStandingInProcess(context, ports) {
15
+ try {
16
+ if (!context || typeof context.place !== 'string' || !context.place || typeof context.source !== 'string' || !context.source || !isSha256(context.snapshot))
17
+ refuse('context');
18
+ if (context.signal?.aborted)
19
+ refuse('cancelled');
20
+ const { place, source, snapshot, signal } = context, limits = { ...context.limits };
21
+ if (!Number.isSafeInteger(limits.records) || limits.records < 1)
22
+ refuse('budget');
23
+ const anchor = structuredClone(context.anchor), reading = structuredClone(context.reading);
24
+ if (!anchor || anchor.keyClass !== 'authorize' || !anchor.id || !anchor.publicKey || !Array.isArray(anchor.coverage) || !anchor.coverage.length)
25
+ refuse('context');
26
+ // Validate the complete declared environment before invoking a host port.
27
+ readingIdentity(reading, sha256);
28
+ const identity = readingIdentity({ ...reading, inputs: [...reading.inputs, ['host/place', sha256(place)], ['host/anchor', sha256(JSON.stringify(anchor))], ['host/snapshot', snapshot]] }, sha256);
29
+ const bytes = await readContent(source, snapshot, { read: ports.read, digestBytes: sha256 }, limits, signal);
30
+ let text;
31
+ try {
32
+ text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
33
+ }
34
+ catch {
35
+ return refuse('snapshot-format');
36
+ }
37
+ const history = text.split('\n').filter(line => line.length > 0);
38
+ if (history.length > limits.records)
39
+ refuse('budget');
40
+ if (!history.length || history.some(line => parse(line).kind !== 'fact'))
41
+ refuse('snapshot-format');
42
+ if (signal?.aborted)
43
+ refuse('cancelled');
44
+ const standing = publicStandingProjection(history, anchor);
45
+ assertStandingView(standing);
46
+ if (signal?.aborted)
47
+ refuse('cancelled');
48
+ const result = sha256(standing.join('\n') + (standing.length ? '\n' : ''));
49
+ return { kind: 'fact', value: { place, rootStanding: standing, reading: identity, result, commitment: readingReceiptIdentity(identity, result, sha256) } };
50
+ }
51
+ catch (error) {
52
+ return { kind: 'refuse', code: error instanceof BoundRefusal ? error.code : 'host/failed', diagnostic: error instanceof Error ? error.message : 'REFUSE·host/failed' };
53
+ }
54
+ ;
55
+ }
@@ -0,0 +1,15 @@
1
+ /** Native host boundary. Uses the same admission and projection as the CLI. */
2
+ export { readRootStandingInProcess } from './in-process.ts';
3
+ export type { RootStandingHostContext, RootStandingHostResult } from './in-process.ts';
4
+ export { readContextualStandingInProcess } from './in-process-context.ts';
5
+ export { landPlaceActInProcess, recoverPlaceActInProcess } from './in-process-act.ts';
6
+ export type { PlaceActHostContext, PlaceActHostPorts } from './in-process-act.ts';
7
+ export { landRootLotInProcess, recoverRootActInProcess, rootLotSnapshot } from './in-process-root.ts';
8
+ export type { RootLotContext, RootLotPorts } from './in-process-root.ts';
9
+ export { nativeHostInstrument, nativePlaceActReceiver } from './native-receiver.ts';
10
+ export type { NativeHostInstrument } from './native-receiver.ts';
11
+ export type { HostInstrumentRuntime } from './call-context.ts';
12
+ export { nativePlaceActPersistence } from './ports/place-act.ts';
13
+ export type { PlaceActTrust, PlaceActBudget } from './ports/place-act.ts';
14
+ export type { PlaceActPersistence } from './place-act-transaction.ts';
15
+ export type { PublicationStorage } from './ports/publication.ts';
@@ -0,0 +1,7 @@
1
+ /** Native host boundary. Uses the same admission and projection as the CLI. */
2
+ export { readRootStandingInProcess } from "./in-process.js";
3
+ export { readContextualStandingInProcess } from "./in-process-context.js";
4
+ export { landPlaceActInProcess, recoverPlaceActInProcess } from "./in-process-act.js";
5
+ export { landRootLotInProcess, recoverRootActInProcess, rootLotSnapshot } from "./in-process-root.js";
6
+ export { nativeHostInstrument, nativePlaceActReceiver } from "./native-receiver.js";
7
+ export { nativePlaceActPersistence } from "./ports/place-act.js";
@@ -7,6 +7,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
7
7
  return path;
8
8
  };
9
9
  import { sha256 as digest } from "../hash.js";
10
+ import { compareStrings as order } from "../order.js";
10
11
  import { realpathSync } from 'node:fs';
11
12
  import { dirname, join, relative, isAbsolute, sep } from 'node:path';
12
13
  import { readBoundedFile } from "./bounded-file.js";
@@ -70,7 +71,6 @@ export function installNativeResolutionReuse(declaration) {
70
71
  fail('invalid/ambiguous logical library coordinate');
71
72
  namedCoordinates.add(coordinate);
72
73
  }
73
- const order = (a, b) => a < b ? -1 : a > b ? 1 : 0;
74
74
  const identity = digest(Buffer.from(JSON.stringify([declaration.platform, files.map(member => [member.path, member.digest, member.bytes]).sort((a, b) => order(String(a[0]), String(b[0]))), [...libraries].sort(([a], [b]) => order(a, b)).map(([name, manifest]) => [name, manifest, coordinates.get(name) ?? null])])));
75
75
  if (resolutionIdentity !== undefined) {
76
76
  if (identity !== resolutionIdentity)
@@ -0,0 +1,35 @@
1
+ export interface ModuleClosureMember {
2
+ readonly digest: string;
3
+ readonly source: string;
4
+ }
5
+ export interface ModuleClosureSnapshot {
6
+ readonly identity: string;
7
+ readonly entry: string;
8
+ readonly members: ReadonlyMap<string, ModuleClosureMember>;
9
+ }
10
+ export interface ModuleClosureReading {
11
+ readonly snapshot: string;
12
+ readonly coordinate: string;
13
+ readonly digest: string;
14
+ }
15
+ export interface ModuleClosureLimits {
16
+ readonly members: number;
17
+ readonly bytes: number;
18
+ readonly portableESM?: boolean;
19
+ readonly signal?: AbortSignal;
20
+ }
21
+ /** Host-selected archive and entry, never a verifier assertion from the program.
22
+ * This proves content identity only; standing admission and runtime capabilities
23
+ * are separate prerequisites. Binary resources are not executable text members.
24
+ */
25
+ export declare function moduleSnapshot(archive: Uint8Array, expected: string, entry: string): ModuleClosureSnapshot;
26
+ /** The host supplies authenticated snapshot identities and its library resolver.
27
+ * Each reached text member is verified here against its exact UTF-8 bytes.
28
+ * Authentication of the snapshot envelope remains a separate host prerequisite.
29
+ * A capsule cannot select either verifier or resolver.
30
+ * This reads declared module edges, not arbitrary I/O or a JavaScript sandbox. Computed edges are unpaid, never absent.
31
+ * Activation requires an admitted host policy; existing inventory callers are not silently changed.
32
+ */
33
+ export declare function moduleClosure(snapshot: ModuleClosureSnapshot, library: (specifier: string, parent: ModuleClosureSnapshot) => ModuleClosureSnapshot | undefined): readonly ModuleClosureReading[];
34
+ /** Multiple needs share the existing traversal and one optional act budget. */
35
+ export declare function moduleClosures(snapshots: readonly ModuleClosureSnapshot[], library: (specifier: string, parent: ModuleClosureSnapshot) => ModuleClosureSnapshot | undefined, limits?: ModuleClosureLimits): readonly ModuleClosureReading[];
@@ -38,7 +38,14 @@ export function moduleSnapshot(archive, expected, entry) {
38
38
  * Activation requires an admitted host policy; existing inventory callers are not silently changed.
39
39
  */
40
40
  export function moduleClosure(snapshot, library) {
41
+ return moduleClosures([snapshot], library);
42
+ }
43
+ /** Multiple needs share the existing traversal and one optional act budget. */
44
+ export function moduleClosures(snapshots, library, limits) {
45
+ if (limits && (!Number.isSafeInteger(limits.members) || limits.members < 1 || !Number.isSafeInteger(limits.bytes) || limits.bytes < 1))
46
+ throw Error('REFUSE·module closure missing input resources');
41
47
  const seen = new Map();
48
+ let totalBytes = 0;
42
49
  const coordinate = (name) => {
43
50
  const normalized = posix.normalize(name);
44
51
  if (!name || name.includes('\0') || name.includes('\\') || posix.isAbsolute(name) || normalized === '..' || normalized.startsWith('../'))
@@ -46,12 +53,19 @@ export function moduleClosure(snapshot, library) {
46
53
  return normalized;
47
54
  };
48
55
  const walk = (one, name) => {
56
+ if (limits?.signal?.aborted)
57
+ throw Error('REFUSE·module closure cancelled');
49
58
  const at = coordinate(name), key = JSON.stringify([one.identity, at]);
59
+ if (limits?.portableESM && /[%?#]/.test(at))
60
+ throw Error('REFUSE·module closure unsupported URL coordinate ' + at);
50
61
  const member = one.members.get(at);
51
62
  if (member === undefined)
52
63
  throw Error('REFUSE·module closure missing ' + one.identity + ' ' + at);
53
64
  if (typeof member.source !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(member.digest))
54
65
  throw Error('REFUSE·module closure unnamed bytes ' + one.identity + ' ' + at);
66
+ const size = Buffer.byteLength(member.source, 'utf8');
67
+ if (limits && !seen.has(key) && (seen.size >= limits.members || size > limits.bytes - totalBytes))
68
+ throw Error('REFUSE·module closure input resources');
55
69
  const bytes = Buffer.from(member.source, 'utf8');
56
70
  if (bytes.toString('utf8') !== member.source)
57
71
  throw Error('REFUSE·module closure invalid UTF-8 text ' + one.identity + ' ' + at);
@@ -63,18 +77,34 @@ export function moduleClosure(snapshot, library) {
63
77
  throw Error('REFUSE·module closure conflicting member ' + one.identity + ' ' + at);
64
78
  return;
65
79
  }
80
+ totalBytes += size;
66
81
  seen.set(key, { snapshot: one.identity, coordinate: at, digest: member.digest });
82
+ if (limits?.portableESM && !['.js', '.json'].includes(posix.extname(at)))
83
+ throw Error('REFUSE·module closure unsupported ESM member ' + at);
67
84
  // JSON is data under the loader contract, not module syntax.
68
- if (posix.extname(at) === '.json')
85
+ if (posix.extname(at) === '.json') {
86
+ if (limits?.portableESM) {
87
+ try {
88
+ JSON.parse(member.source);
89
+ }
90
+ catch {
91
+ throw Error('REFUSE·module closure invalid JSON member ' + at);
92
+ }
93
+ }
69
94
  return;
70
- const syntax = moduleImportSyntax(member.source, at);
95
+ }
96
+ const syntax = moduleImportSyntax(member.source, at, limits?.portableESM ? { esmOnly: true } : undefined);
71
97
  if (syntax.unresolved.length)
72
98
  throw Error('REFUSE·module closure unresolved ' + at + ' ' + syntax.unresolved.join(' · '));
73
99
  for (const specifier of syntax.imports) {
100
+ if (limits?.portableESM && /[%?#\\]/.test(specifier))
101
+ throw Error('REFUSE·module closure unsupported URL specifier ' + specifier + ' from ' + at);
74
102
  // createRequire/registerHooks can select executable edges not present in this graph.
75
103
  // Their runtime closure needs its own host contract; an arbitrary alias is not a literal edge.
76
104
  if (specifier === 'node:module' || specifier === 'module')
77
105
  throw Error('REFUSE·module closure runtime loader requires declared closure ' + specifier + ' from ' + at);
106
+ if (limits?.portableESM && (specifier.startsWith('node:') || builtinModules.includes(specifier)))
107
+ throw Error('REFUSE·module closure host builtin in portable delivery ' + specifier + ' from ' + at);
78
108
  if (specifier.startsWith('node:')) {
79
109
  if (!builtinModules.includes(specifier) && !builtinModules.includes(specifier.slice(5)))
80
110
  throw Error('REFUSE·module closure unknown builtin ' + specifier + ' from ' + at);
@@ -94,6 +124,7 @@ export function moduleClosure(snapshot, library) {
94
124
  walk(pinned, pinned.entry);
95
125
  }
96
126
  };
97
- walk(snapshot, snapshot.entry);
127
+ for (const snapshot of snapshots)
128
+ walk(snapshot, snapshot.entry);
98
129
  return [...seen.values()];
99
130
  }
@@ -0,0 +1,62 @@
1
+ import { type ModuleClosureSnapshot, type ModuleClosureLimits } from './module-closure.ts';
2
+ export { moduleSnapshot, type ModuleClosureSnapshot } from './module-closure.ts';
3
+ export interface ClosureDeliveryLimits extends Omit<ModuleClosureLimits, 'portableESM'> {
4
+ readonly responseBytes: number;
5
+ }
6
+ export interface ClosureNotice {
7
+ readonly snapshot: string;
8
+ readonly coordinate: string;
9
+ readonly digest: string;
10
+ readonly source: string;
11
+ }
12
+ export interface ClosureDeliveryManifest {
13
+ readonly format: 'esm-file-closure/1';
14
+ readonly needs: readonly {
15
+ name: string;
16
+ snapshot: string;
17
+ entry: string;
18
+ }[];
19
+ readonly modules: readonly {
20
+ snapshot: string;
21
+ coordinate: string;
22
+ digest: string;
23
+ path: string;
24
+ }[];
25
+ readonly libraries: readonly {
26
+ parent: string;
27
+ specifier: string;
28
+ snapshot: string;
29
+ entry: string;
30
+ }[];
31
+ readonly notices: readonly {
32
+ snapshot: string;
33
+ coordinate: string;
34
+ digest: string;
35
+ path: string;
36
+ }[];
37
+ }
38
+ export interface ClosureDelivery {
39
+ readonly closure: string;
40
+ readonly digest: string;
41
+ readonly bytes: Uint8Array;
42
+ readonly manifest: ClosureDeliveryManifest;
43
+ readonly files: readonly {
44
+ path: string;
45
+ source: string;
46
+ }[];
47
+ readonly importMap: {
48
+ imports: Record<string, string>;
49
+ scopes: Record<string, Record<string, string>>;
50
+ };
51
+ }
52
+ type Library = (specifier: string, parent: ModuleClosureSnapshot) => ModuleClosureSnapshot | undefined;
53
+ /** The host selects already authenticated snapshots and resolution, never the
54
+ * offered program. This prepares unchanged ESM files, not a JS sandbox, signer,
55
+ * browser host or symbol-pruning bundler. Existing native/Acorn routes remain.
56
+ */
57
+ export declare function deliverModuleClosure(needs: readonly string[], entries: ReadonlyMap<string, ModuleClosureSnapshot>, library: Library, limits: ClosureDeliveryLimits, notices?: readonly ClosureNotice[]): ClosureDelivery;
58
+ /** Verify exact pinned delivery bytes and run the same pre-execution closure
59
+ * verifier again over the delivered set. No callbacks, install paths or verifier
60
+ * chosen by the payload. The returned import map is derived, never trusted input.
61
+ */
62
+ export declare function verifyModuleDelivery(bytes: Uint8Array, expected: string, limits: ClosureDeliveryLimits): ClosureDelivery;