@lapxo/bound 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/TARGET.bound +39 -26
- package/dist/cli/owner.js +2 -1
- package/dist/cli/place.js +7 -26
- package/dist/cli/retained-judgment.js +25 -0
- package/dist/fold/admission-history.js +6 -11
- package/dist/fold/bootstrap-anchor.js +28 -0
- package/dist/fold/ceilings.js +13 -0
- package/dist/fold/own.js +7 -5
- package/dist/fold/signers.js +5 -9
- package/dist/host/native/loader.js +1 -1
- package/dist/host/native/module-closure.d.ts +35 -0
- package/dist/host/native/module-closure.js +34 -3
- package/dist/host/native/module-delivery.d.ts +62 -0
- package/dist/host/native/module-delivery.js +151 -0
- package/dist/host/native/module-syntax.js +11 -5
- package/dist/host/order.js +2 -0
- package/docs/contracts/closure-delivery.md +23 -0
- package/docs/contracts/closure-delivery.publication.json +25 -0
- package/docs/contracts/readme.publication.json +1 -1
- package/package.json +16 -4
- package/receipts.bound +9837 -9572
- package/samples/engine-place/inputs/closure.json +1 -0
- package/samples/module-delivery/inputs/delivery.json +1 -0
- package/samples/no/module-delivery/delivery.json +1 -0
- package/vectors/native/module-delivery.vector.json +32 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { storeOf } from "../land/ledger.js";
|
|
4
|
+
/** Read the existing place-local root of trust. Names and current claims select no authority. */
|
|
5
|
+
export function bootstrapAnchor(root) {
|
|
6
|
+
const store = storeOf(root);
|
|
7
|
+
const files = existsSync(store) ? readdirSync(store).filter(name => name.endsWith('.keys')).sort() : [];
|
|
8
|
+
if (!files.length)
|
|
9
|
+
return undefined;
|
|
10
|
+
const candidates = files.map(name => {
|
|
11
|
+
let keys;
|
|
12
|
+
try {
|
|
13
|
+
keys = JSON.parse(readFileSync(join(store, name), 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw Error('REFUSE·act bootstrap anchor is unreadable');
|
|
17
|
+
}
|
|
18
|
+
const anchors = Object.values(keys ?? {}), anchor = anchors[0];
|
|
19
|
+
if (anchors.length !== 1 || !anchor || typeof anchor.id !== 'string' || !anchor.id || typeof anchor.publicKey !== 'string' || !anchor.publicKey || !Array.isArray(anchor.coverage) || !anchor.coverage.length || anchor.coverage.some(scope => typeof scope !== 'string' || !scope))
|
|
20
|
+
throw Error('REFUSE·act historical admission requires one bootstrap anchor');
|
|
21
|
+
return anchor;
|
|
22
|
+
});
|
|
23
|
+
const identities = new Set(candidates.map(anchor => JSON.stringify([anchor.id, anchor.publicKey, [...new Set(anchor.coverage)].sort()])));
|
|
24
|
+
if (identities.size !== 1)
|
|
25
|
+
throw Error('REFUSE·act conflicting bootstrap anchors');
|
|
26
|
+
const anchor = candidates[0];
|
|
27
|
+
return { id: anchor.id, publicKey: anchor.publicKey, coverage: anchor.coverage };
|
|
28
|
+
}
|
package/dist/fold/ceilings.js
CHANGED
|
@@ -127,3 +127,16 @@ export function foldCeilings(input) {
|
|
|
127
127
|
readings: said,
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
|
+
function listed(misses) {
|
|
131
|
+
const by = new Map();
|
|
132
|
+
for (const o of misses)
|
|
133
|
+
by.set(o.ceiling, [...(by.get(o.ceiling) ?? []), o.question === o.ceiling ? o.got : `${o.question}=${o.got}`]);
|
|
134
|
+
return [...by].map(([scope, got]) => `${scope} · ${got.length > 4 ? `${got.slice(0, 4).join(' ')} +${got.length - 4}` : got.join(' ')}`);
|
|
135
|
+
}
|
|
136
|
+
/** One summary for fresh and retained judgments over the same declared bounds. */
|
|
137
|
+
export function ceilingSummary(fold, total, shared = new Set()) {
|
|
138
|
+
return { read: fold.read.length, total, unread: fold.unread.map(line => fieldOf(line, 'scope')),
|
|
139
|
+
vacuous: fold.vacuous.map(line => fieldOf(line, 'scope')),
|
|
140
|
+
short: listed(fold.over.filter(one => one.side === 'short' && !shared.has(one.question))),
|
|
141
|
+
over: listed(fold.over.filter(one => one.side === 'over')) };
|
|
142
|
+
}
|
package/dist/fold/own.js
CHANGED
|
@@ -27,6 +27,12 @@ import { writtenIn } from "./signed.js";
|
|
|
27
27
|
import { laidDiffers } from "./laid.js";
|
|
28
28
|
import { heldBytes } from "../land/held.js";
|
|
29
29
|
import { capsuleAt, placesIn, worldAt } from "./places.js";
|
|
30
|
+
/** Measurements of this invocation; never evidence of an earlier kept fold. */
|
|
31
|
+
export function currentActReadings(under) {
|
|
32
|
+
const receipts = receiptsSeen();
|
|
33
|
+
return placeOnly({ idle: idleCount(), s: actSeconds(), 'walk-read': receipts.count, 'walk-ms': receipts.ms,
|
|
34
|
+
'kept-folds-read-under-another-instrument': foldsElsewhere() }, under);
|
|
35
|
+
}
|
|
30
36
|
const entered = { foreign: 0 };
|
|
31
37
|
/** Host and instrument measurements. Domain contracts require their selected readers; missing readings are not fabricated zeroes. */
|
|
32
38
|
function shipped(root, store, standing, under) {
|
|
@@ -193,12 +199,8 @@ export function ownReadings(input) {
|
|
|
193
199
|
...Object.fromEntries(standing.filter((line) => isCeiling(line) && (fieldOf(line, 'measure') === 'reader-modules' || fieldOf(line, 'scope').split('/').pop() === 'reader-modules'))
|
|
194
200
|
.flatMap((line) => ((place) => place === undefined ? [] : [[fieldOf(line, 'scope'), tree(() => exporting(observed, `${place}/`, ['observe', 'run']))]])(fieldOf(line, 'scope').split('/').find((step) => capsuleAt(root, step))))),
|
|
195
201
|
...ownFrom(ceilingsNamed(standing, 'world-lines-outside-their-capsule').find((line) => fieldOf(line, 'form') === 'interval'), 'world-lines-outside-their-capsule', tree(() => outsideTheirCapsule(root, standing, standing).length)),
|
|
196
|
-
|
|
197
|
-
s: actSeconds(),
|
|
202
|
+
...currentActReadings(),
|
|
198
203
|
'cli-verbs': VERB_NAMES.length,
|
|
199
|
-
'walk-read': receiptsSeen().count,
|
|
200
|
-
'walk-ms': receiptsSeen().ms,
|
|
201
|
-
'kept-folds-read-under-another-instrument': tree(() => foldsElsewhere()),
|
|
202
204
|
'accepts-that-refolded': tree(() => counted(store, standing, 'beat/accept', 'folded')),
|
|
203
205
|
...(keyOf(store, 'attest') === undefined ? {} : { [`${keyOf(store, 'attest')}-ms`]: tree(() => attestBeat(store, standing)) }),
|
|
204
206
|
ship: took('ship', () => shipped(root, store, standing, under)),
|
package/dist/fold/signers.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bootstrapAnchor } from "./bootstrap-anchor.js";
|
|
2
2
|
import { authorityOf, isKeyClaim, signersOf } from '@lapxo/topos/wire';
|
|
3
3
|
import { verifiesFields } from "../land/sign.js";
|
|
4
|
-
import { fieldOf,
|
|
4
|
+
import { fieldOf, parsedLine as parse } from "./claims.js";
|
|
5
5
|
import { keyFor, lockStanding } from "./keys.js";
|
|
6
|
-
import { storeRoot } from "../host/ports/store.js";
|
|
7
6
|
import { storeOf } from "../land/ledger.js";
|
|
8
|
-
import { observeText } from "../observe/files.js";
|
|
9
7
|
/** A lock line about the signers: the authority fold reads it. */
|
|
10
8
|
export function isSignerLock(line) {
|
|
11
9
|
const got = parse(line);
|
|
@@ -38,11 +36,9 @@ export function writerFor(standing, kind) {
|
|
|
38
36
|
return undefined;
|
|
39
37
|
}
|
|
40
38
|
export function publicKeyOf(root, keyId) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (anchored !== undefined)
|
|
45
|
-
return anchored;
|
|
39
|
+
const anchor = bootstrapAnchor(root);
|
|
40
|
+
if (anchor?.id === keyId)
|
|
41
|
+
return anchor.publicKey;
|
|
46
42
|
const said = lockStanding(storeOf(root))
|
|
47
43
|
.find((line) => fieldOf(line, 'scope') === `keys/${keyId}` && fieldOf(line, 'measure') === 'public-key' && fieldOf(line, 'value') !== 'withdraw');
|
|
48
44
|
return said === undefined ? undefined : fieldOf(said, 'value');
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
|
+
import { sha256, isSha256 } from "../hash.js";
|
|
3
|
+
import { archiveCoordinate } from "../archive.js";
|
|
4
|
+
import { compareStrings as compare } from "../order.js";
|
|
5
|
+
import { moduleClosures } from "./module-closure.js";
|
|
6
|
+
export { moduleSnapshot } from "./module-closure.js";
|
|
7
|
+
const fail = (why) => { throw Error('REFUSE·closure delivery ' + why); };
|
|
8
|
+
const key = (identity, coordinate) => JSON.stringify([identity, coordinate]);
|
|
9
|
+
const pathOf = (identity, coordinate) => {
|
|
10
|
+
if (!isSha256(identity))
|
|
11
|
+
return fail('snapshot identity is not SHA-256');
|
|
12
|
+
return 'modules/' + identity.slice(7) + '/' + coordinate;
|
|
13
|
+
};
|
|
14
|
+
const urlOf = (path) => './' + path.split('/').map(encodeURIComponent).join('/');
|
|
15
|
+
const resourceLimits = (limits) => {
|
|
16
|
+
if (!limits || !Number.isSafeInteger(limits.members) || limits.members < 1 || !Number.isSafeInteger(limits.bytes) || limits.bytes < 1 || !Number.isSafeInteger(limits.responseBytes) || limits.responseBytes < 1)
|
|
17
|
+
fail('missing resources');
|
|
18
|
+
if (limits.signal?.aborted)
|
|
19
|
+
fail('cancelled');
|
|
20
|
+
};
|
|
21
|
+
/** The host selects already authenticated snapshots and resolution, never the
|
|
22
|
+
* offered program. This prepares unchanged ESM files, not a JS sandbox, signer,
|
|
23
|
+
* browser host or symbol-pruning bundler. Existing native/Acorn routes remain.
|
|
24
|
+
*/
|
|
25
|
+
export function deliverModuleClosure(needs, entries, library, limits, notices = []) {
|
|
26
|
+
resourceLimits(limits);
|
|
27
|
+
if (!Array.isArray(notices) || notices.length >= limits.members)
|
|
28
|
+
fail('notice resources');
|
|
29
|
+
let noticeBytes = 0;
|
|
30
|
+
const noticePaths = new Set();
|
|
31
|
+
const retainedNotices = notices.map(n => {
|
|
32
|
+
if (!n || typeof n.coordinate !== 'string' || archiveCoordinate(n.coordinate) !== n.coordinate || typeof n.source !== 'string' || !isSha256(n.digest) || sha256(n.source) !== n.digest || (noticeBytes += Buffer.byteLength(n.source)) >= limits.bytes)
|
|
33
|
+
fail('notice bytes/resources');
|
|
34
|
+
const path = 'notices/' + pathOf(n.snapshot, n.coordinate).slice('modules/'.length);
|
|
35
|
+
if (noticePaths.has(path))
|
|
36
|
+
fail('duplicate notice');
|
|
37
|
+
noticePaths.add(path);
|
|
38
|
+
return { snapshot: n.snapshot, coordinate: n.coordinate, digest: n.digest, path };
|
|
39
|
+
}).sort((a, b) => compare(a.path, b.path));
|
|
40
|
+
if (!Array.isArray(needs) || !needs.length || needs.length > limits.members || needs.some(n => typeof n !== 'string' || !n || Buffer.byteLength(n) > limits.bytes))
|
|
41
|
+
fail('missing/oversized needs');
|
|
42
|
+
const requested = [...new Set(needs)].sort(compare), snapshots = new Map(), bindings = new Map();
|
|
43
|
+
const hold = (one) => {
|
|
44
|
+
if (!one || !isSha256(one.identity))
|
|
45
|
+
return fail('snapshot identity is not SHA-256');
|
|
46
|
+
const old = snapshots.get(one.identity);
|
|
47
|
+
// Different entry points may share one immutable snapshot, not different members.
|
|
48
|
+
if (old && old.members !== one.members)
|
|
49
|
+
fail('ambiguous snapshot members');
|
|
50
|
+
snapshots.set(one.identity, one);
|
|
51
|
+
return one;
|
|
52
|
+
};
|
|
53
|
+
const roots = requested.map(name => { const one = entries.get(name); if (!one)
|
|
54
|
+
return fail('unknown need ' + name); return hold(one); });
|
|
55
|
+
const reached = moduleClosures(roots, (specifier, parent) => {
|
|
56
|
+
const found = library(specifier, parent);
|
|
57
|
+
if (!found)
|
|
58
|
+
return;
|
|
59
|
+
const one = hold(found), at = key(parent.identity, specifier), binding = { parent: parent.identity, specifier, snapshot: one.identity, entry: one.entry }, old = bindings.get(at);
|
|
60
|
+
if (old && (old.snapshot !== one.identity || old.entry !== one.entry))
|
|
61
|
+
fail('ambiguous library resolution');
|
|
62
|
+
if (!old && bindings.size >= limits.members)
|
|
63
|
+
fail('library resources');
|
|
64
|
+
bindings.set(at, binding);
|
|
65
|
+
return one;
|
|
66
|
+
}, { ...limits, members: limits.members - notices.length, bytes: limits.bytes - noticeBytes, portableESM: true });
|
|
67
|
+
if (notices.some(n => !reached.some(r => r.snapshot === n.snapshot)))
|
|
68
|
+
fail('notice outside reached snapshots');
|
|
69
|
+
const modules = [...reached].sort((a, b) => compare(key(a.snapshot, a.coordinate), key(b.snapshot, b.coordinate))).map(r => ({ ...r, path: pathOf(r.snapshot, r.coordinate) }));
|
|
70
|
+
const files = modules.map(r => {
|
|
71
|
+
const source = snapshots.get(r.snapshot)?.members.get(r.coordinate)?.source;
|
|
72
|
+
if (source === undefined || sha256(source) !== r.digest)
|
|
73
|
+
fail('snapshot changed during delivery');
|
|
74
|
+
return { path: r.path, source: source };
|
|
75
|
+
});
|
|
76
|
+
for (const n of retainedNotices)
|
|
77
|
+
files.push({ path: n.path, source: notices.find(one => one.snapshot === n.snapshot && one.coordinate === n.coordinate).source });
|
|
78
|
+
const manifest = { format: 'esm-file-closure/1', needs: requested.map((name, i) => ({ name, snapshot: roots[i].identity, entry: posix.normalize(roots[i].entry) })), modules, libraries: [...bindings.values()].sort((a, b) => compare(key(a.parent, a.specifier), key(b.parent, b.specifier))), notices: retainedNotices };
|
|
79
|
+
const closure = sha256(JSON.stringify(manifest));
|
|
80
|
+
const bytes = Buffer.from(JSON.stringify({ manifest, files }), 'utf8');
|
|
81
|
+
if (bytes.length > limits.responseBytes)
|
|
82
|
+
fail('response resources');
|
|
83
|
+
if (limits.signal?.aborted)
|
|
84
|
+
fail('cancelled');
|
|
85
|
+
const imports = Object.fromEntries(manifest.needs.map(n => [n.name, urlOf(pathOf(n.snapshot, n.entry))]));
|
|
86
|
+
const scopes = {};
|
|
87
|
+
for (const b of manifest.libraries) {
|
|
88
|
+
const scope = './modules/' + b.parent.slice(7) + '/';
|
|
89
|
+
const values = scopes[scope] ?? (scopes[scope] = Object.create(null));
|
|
90
|
+
values[b.specifier] = urlOf(pathOf(b.snapshot, posix.normalize(b.entry)));
|
|
91
|
+
}
|
|
92
|
+
return { closure, digest: sha256(bytes), bytes: new Uint8Array(bytes), manifest, files, importMap: { imports, scopes } };
|
|
93
|
+
}
|
|
94
|
+
/** Verify exact pinned delivery bytes and run the same pre-execution closure
|
|
95
|
+
* verifier again over the delivered set. No callbacks, install paths or verifier
|
|
96
|
+
* chosen by the payload. The returned import map is derived, never trusted input.
|
|
97
|
+
*/
|
|
98
|
+
export function verifyModuleDelivery(bytes, expected, limits) {
|
|
99
|
+
resourceLimits(limits);
|
|
100
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength > limits.responseBytes || !isSha256(expected) || sha256(bytes) !== expected)
|
|
101
|
+
fail('bundle bytes differ or exceed resources');
|
|
102
|
+
let value;
|
|
103
|
+
try {
|
|
104
|
+
value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return fail('invalid JSON bytes');
|
|
108
|
+
}
|
|
109
|
+
const manifest = value?.manifest;
|
|
110
|
+
if (manifest?.format !== 'esm-file-closure/1' || !Array.isArray(manifest.needs) || !Array.isArray(manifest.modules) || !Array.isArray(manifest.libraries) || !Array.isArray(manifest.notices) || !Array.isArray(value.files) || manifest.modules.length + manifest.notices.length > limits.members || value.files.length !== manifest.modules.length + manifest.notices.length || manifest.needs.length > limits.members || manifest.libraries.length > limits.members)
|
|
111
|
+
fail('invalid manifest resources');
|
|
112
|
+
const snapshots = new Map(), files = new Map();
|
|
113
|
+
let size = 0;
|
|
114
|
+
for (const file of value.files) {
|
|
115
|
+
if (typeof file?.path !== 'string' || typeof file.source !== 'string' || files.has(file.path) || (size += Buffer.byteLength(file.source)) > limits.bytes)
|
|
116
|
+
fail('invalid/duplicate file resources');
|
|
117
|
+
files.set(file.path, file.source);
|
|
118
|
+
}
|
|
119
|
+
for (const m of manifest.modules) {
|
|
120
|
+
if (!m || typeof m.coordinate !== 'string' || typeof m.path !== 'string' || m.path !== pathOf(m.snapshot, m.coordinate) || !isSha256(m.digest))
|
|
121
|
+
fail('invalid module');
|
|
122
|
+
const source = files.get(m.path);
|
|
123
|
+
if (source === undefined || sha256(source) !== m.digest)
|
|
124
|
+
fail('module bytes differ');
|
|
125
|
+
const members = snapshots.get(m.snapshot) ?? new Map();
|
|
126
|
+
if (members.has(m.coordinate))
|
|
127
|
+
fail('duplicate module');
|
|
128
|
+
members.set(m.coordinate, { source, digest: m.digest });
|
|
129
|
+
snapshots.set(m.snapshot, members);
|
|
130
|
+
}
|
|
131
|
+
const snapshot = (identity, entry) => { const members = snapshots.get(identity); if (!members || typeof entry !== 'string')
|
|
132
|
+
return fail('unknown snapshot/entry'); return { identity, entry, members }; };
|
|
133
|
+
const entries = new Map();
|
|
134
|
+
for (const n of manifest.needs) {
|
|
135
|
+
if (typeof n?.name !== 'string' || entries.has(n.name))
|
|
136
|
+
fail('invalid/duplicate need');
|
|
137
|
+
entries.set(n.name, snapshot(n.snapshot, n.entry));
|
|
138
|
+
}
|
|
139
|
+
const bindings = new Map();
|
|
140
|
+
for (const b of manifest.libraries) {
|
|
141
|
+
if (typeof b?.parent !== 'string' || typeof b.specifier !== 'string' || bindings.has(key(b.parent, b.specifier)))
|
|
142
|
+
fail('invalid/duplicate library');
|
|
143
|
+
bindings.set(key(b.parent, b.specifier), snapshot(b.snapshot, b.entry));
|
|
144
|
+
}
|
|
145
|
+
const notices = manifest.notices.map(n => { if (!n || typeof n.path !== 'string' || !files.has(n.path))
|
|
146
|
+
return fail('missing notice'); return { snapshot: n.snapshot, coordinate: n.coordinate, digest: n.digest, source: files.get(n.path) }; });
|
|
147
|
+
const prepared = deliverModuleClosure([...entries.keys()], entries, (specifier, parent) => bindings.get(key(parent.identity, specifier)), limits, notices);
|
|
148
|
+
if (prepared.digest !== expected)
|
|
149
|
+
fail('noncanonical, extra or unresolved delivery members');
|
|
150
|
+
return prepared;
|
|
151
|
+
}
|
|
@@ -7,7 +7,7 @@ const require = nodeModule.createRequire(import.meta.url);
|
|
|
7
7
|
const { parse } = require('acorn');
|
|
8
8
|
const stripTypeScriptTypes = nodeModule.stripTypeScriptTypes;
|
|
9
9
|
/** Syntax inventory is not an execution-closure certificate: unresolved expressions stay explicit. */
|
|
10
|
-
export function moduleImportSyntax(source, coordinate) {
|
|
10
|
+
export function moduleImportSyntax(source, coordinate, options) {
|
|
11
11
|
const code = extname(coordinate) === '.ts' ? stripTypeScriptTypes(source, { mode: 'transform' }) : source;
|
|
12
12
|
let tree;
|
|
13
13
|
try {
|
|
@@ -18,12 +18,12 @@ export function moduleImportSyntax(source, coordinate) {
|
|
|
18
18
|
}
|
|
19
19
|
const found = new Set(), unresolved = new Set();
|
|
20
20
|
const literal = stringLiteral;
|
|
21
|
-
const visit = (node, parent, key) => {
|
|
21
|
+
const visit = (node, parent, key, reexportSource = false) => {
|
|
22
22
|
if (!node || typeof node !== 'object')
|
|
23
23
|
return;
|
|
24
24
|
if (Array.isArray(node)) {
|
|
25
25
|
for (const child of node)
|
|
26
|
-
visit(child, parent, key);
|
|
26
|
+
visit(child, parent, key, reexportSource);
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
const n = node;
|
|
@@ -32,7 +32,11 @@ export function moduleImportSyntax(source, coordinate) {
|
|
|
32
32
|
if (n['type'] === 'Identifier' && n['name'] === 'require') {
|
|
33
33
|
const direct = parent?.['type'] === 'CallExpression' && key === 'callee';
|
|
34
34
|
const namedProperty = (parent?.['type'] === 'MemberExpression' && key === 'property' && !parent['computed']) || ((parent?.['type'] === 'Property' || parent?.['type'] === 'MethodDefinition') && key === 'key' && !parent['computed'] && !parent['shorthand']);
|
|
35
|
-
|
|
35
|
+
// Declaration names are not loader references. This does not resolve shadowing:
|
|
36
|
+
// references to a binding named require remain conservative, including local calls.
|
|
37
|
+
const declarationName = ((parent?.['type'] === 'ImportSpecifier' || parent?.['type'] === 'ImportDefaultSpecifier' || parent?.['type'] === 'ImportNamespaceSpecifier') && (key === 'imported' || key === 'local')) || (parent?.['type'] === 'FunctionDeclaration' && key === 'id');
|
|
38
|
+
const reexportName = parent?.['type'] === 'ExportSpecifier' && (key === 'exported' || (key === 'local' && reexportSource));
|
|
39
|
+
if (!direct && !namedProperty && !declarationName && !reexportName)
|
|
36
40
|
unresolved.add('require reference at ' + String(n['start']));
|
|
37
41
|
}
|
|
38
42
|
let specifier;
|
|
@@ -44,6 +48,8 @@ export function moduleImportSyntax(source, coordinate) {
|
|
|
44
48
|
if (n['type'] === 'CallExpression') {
|
|
45
49
|
const callee = n['callee'];
|
|
46
50
|
if (callee?.['type'] === 'Identifier' && callee['name'] === 'require') {
|
|
51
|
+
if (options?.esmOnly)
|
|
52
|
+
throw Error('REFUSE·module closure CommonJS load in ESM delivery ' + coordinate);
|
|
47
53
|
specifier = literal(n['arguments'][0]);
|
|
48
54
|
if (specifier === undefined)
|
|
49
55
|
unresolved.add('require expression at ' + String(n['start']));
|
|
@@ -52,7 +58,7 @@ export function moduleImportSyntax(source, coordinate) {
|
|
|
52
58
|
if (specifier !== undefined)
|
|
53
59
|
found.add(specifier);
|
|
54
60
|
for (const [childKey, child] of Object.entries(n))
|
|
55
|
-
visit(child, n, childKey);
|
|
61
|
+
visit(child, n, childKey, n['type'] === 'ExportNamedDeclaration' ? n['source'] !== null && n['source'] !== undefined : reexportSource);
|
|
56
62
|
};
|
|
57
63
|
visit(tree);
|
|
58
64
|
return { imports: [...found], unresolved: [...unresolved] };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Verified executable closure delivery
|
|
2
|
+
|
|
3
|
+
Needs are arbitrary declared names, not a kernel profile enumeration.
|
|
4
|
+
|
|
5
|
+
All requested entries share the existing moduleClosure traversal; no second walker, parser or algebra.
|
|
6
|
+
|
|
7
|
+
Host requires already authenticated snapshot identities; a bundle digest establishes content identity, not authority or correctness.
|
|
8
|
+
|
|
9
|
+
Delivery preserves each reached JS/JSON member and explicitly pinned notices byte for byte. Separate digests identify the closure manifest and the exact UTF-8 JSON container.
|
|
10
|
+
|
|
11
|
+
Canonical delivery sorts needs, reached modules, library bindings and notices; duplicate needs do not duplicate code; unsupported executable forms refuse by name.
|
|
12
|
+
|
|
13
|
+
Mandatory member, source byte and response byte budgets apply before delivery. Pre-aborted or observed cancellation refuses. Synchronous work is not a preemptible execution sandbox.
|
|
14
|
+
|
|
15
|
+
Portable ESM delivery refuses host builtins, runtime loader access, CommonJS loads, calculated/escaped loader expressions and unsupported URL spellings before entry execution.
|
|
16
|
+
|
|
17
|
+
Consumer verifies the expected container digest and repeats the same closure preflight over exactly the delivered members. Derived import maps are never trusted as authority.
|
|
18
|
+
|
|
19
|
+
The host must verify actual mounted bytes again before execution; the test host demonstrates this but is not a public browser host.
|
|
20
|
+
|
|
21
|
+
Existing native, program and worker custody remain unchanged. Acorn routes stay explicitly retained under A-F gate.
|
|
22
|
+
|
|
23
|
+
No external fetch or general demand resolver is introduced. Missing bytes remain outside completed delivery and must be resolved by the existing host contract.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scope": "contract/published/closure-delivery",
|
|
3
|
+
"members": [
|
|
4
|
+
{
|
|
5
|
+
"role": "contract",
|
|
6
|
+
"path": "docs/contracts/closure-delivery.md",
|
|
7
|
+
"digest": "sha256:bdadeefcf8042749e642a453adc7d07944ceab3fbd62de46209ea3ed25d05c2e"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"role": "yes",
|
|
11
|
+
"path": "samples/module-delivery/inputs/delivery.json",
|
|
12
|
+
"digest": "sha256:a6df1d11537a0792c9772b1874c7e127e0fda22203f756d6f0eb104d072a9df1"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"role": "vector",
|
|
16
|
+
"path": "vectors/native/module-delivery.vector.json",
|
|
17
|
+
"digest": "sha256:3de9f160f89764e1153a2719ab04447142fe187bf476b3060d860e81e9c37c02"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"role": "no",
|
|
21
|
+
"path": "samples/no/module-delivery/delivery.json",
|
|
22
|
+
"digest": "sha256:f95b1c3129896baaecb4ae7860afc11dcaaef0a2e47221b73044bc5a4681181f"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lapxo/bound",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Read the bounds you already wrote, fold them, and name the contradictions.",
|
|
6
6
|
"main": "dist/cli/verb.js",
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
"./reference/native": {
|
|
29
29
|
"types": "./dist/host/reference-native-api.d.ts",
|
|
30
30
|
"default": "./dist/host/reference-native-api.js"
|
|
31
|
+
},
|
|
32
|
+
"./closure/native": {
|
|
33
|
+
"types": "./dist/host/native/module-delivery.d.ts",
|
|
34
|
+
"default": "./dist/host/native/module-delivery.js"
|
|
31
35
|
}
|
|
32
36
|
},
|
|
33
37
|
"scripts": {
|
|
@@ -162,13 +166,21 @@
|
|
|
162
166
|
"docs/contracts/readme.publication.json",
|
|
163
167
|
"samples/readme/yes.json",
|
|
164
168
|
"samples/readme/no.json",
|
|
165
|
-
"vectors/native/readme.vector.json"
|
|
169
|
+
"vectors/native/readme.vector.json",
|
|
170
|
+
"samples/engine-place/inputs/closure.json",
|
|
171
|
+
"dist/host/native/module-delivery.d.ts",
|
|
172
|
+
"dist/host/native/module-closure.d.ts",
|
|
173
|
+
"docs/contracts/closure-delivery.md",
|
|
174
|
+
"docs/contracts/closure-delivery.publication.json",
|
|
175
|
+
"samples/module-delivery/inputs/delivery.json",
|
|
176
|
+
"samples/no/module-delivery/delivery.json",
|
|
177
|
+
"vectors/native/module-delivery.vector.json"
|
|
166
178
|
],
|
|
167
179
|
"author": "Lapxo",
|
|
168
180
|
"license": "MIT",
|
|
169
181
|
"dependencies": {
|
|
170
|
-
"@lapxo/obligations": "0.3
|
|
171
|
-
"@lapxo/topos": "^0.2.
|
|
182
|
+
"@lapxo/obligations": "0.4.3",
|
|
183
|
+
"@lapxo/topos": "^0.2.1",
|
|
172
184
|
"acorn": "8.19.0"
|
|
173
185
|
},
|
|
174
186
|
"devDependencies": {
|