@lapxo/bound 0.1.8 → 0.1.9
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 +56 -20
- package/TARGET.bound +25 -25
- package/dist/cli/accept.js +83 -59
- package/dist/cli/act-result.d.ts +9 -0
- package/dist/cli/act-result.js +68 -13
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.js +5 -2
- package/dist/cli/custodian.d.ts +1 -0
- package/dist/cli/custodian.js +29 -0
- package/dist/cli/native-descendant.d.ts +1 -0
- package/dist/cli/native-descendant.js +19 -0
- package/dist/cli/native.d.ts +1 -0
- package/dist/cli/native.js +60 -0
- package/dist/cli/observe.js +9 -2
- package/dist/cli/pass.js +6 -1
- package/dist/cli/standing-result.js +7 -26
- package/dist/cli/verb.js +24 -5
- package/dist/fold/admission-history.js +2 -2
- package/dist/fold/claims.d.ts +4 -0
- package/dist/fold/claims.js +23 -3
- package/dist/fold/closed.d.ts +1 -1
- package/dist/fold/closed.js +16 -4
- package/dist/fold/digests.d.ts +1 -1
- package/dist/fold/digests.js +3 -2
- package/dist/fold/evidence.d.ts +1 -1
- package/dist/fold/kept.js +2 -1
- package/dist/fold/observed.d.ts +1 -0
- package/dist/fold/observed.js +54 -9
- package/dist/fold/readers.d.ts +4 -3
- package/dist/fold/readers.js +18 -13
- package/dist/fold/resolved.js +11 -1
- package/dist/fold/signers.js +2 -2
- package/dist/host/adapters/modules/javascript.d.ts +2 -0
- package/dist/host/adapters/modules/javascript.js +8 -6
- package/dist/host/admitted-place.d.ts +5 -0
- package/dist/host/admitted-place.js +54 -0
- package/dist/host/capsule-process.d.ts +1 -1
- package/dist/host/capsule-process.js +4 -1
- package/dist/host/capsule.d.ts +4 -0
- package/dist/host/capsule.js +16 -2
- package/dist/host/contextual-place.d.ts +59 -0
- package/dist/host/contextual-place.js +77 -0
- package/dist/host/hash.d.ts +7 -0
- package/dist/host/hash.js +6 -0
- package/dist/host/io.d.ts +2 -0
- package/dist/host/io.js +2 -0
- package/dist/host/lifetime.js +61 -22
- package/dist/host/native/bounded-file.d.ts +8 -0
- package/dist/host/native/bounded-file.js +32 -0
- package/dist/host/native/command.d.ts +36 -0
- package/dist/host/native/command.js +91 -0
- package/dist/host/native/control-bridge-worker.d.ts +1 -0
- package/dist/host/native/control-bridge-worker.js +88 -0
- package/dist/host/native/control-bridge.d.ts +30 -0
- package/dist/host/native/control-bridge.js +110 -0
- package/dist/host/native/control-selection.d.ts +29 -0
- package/dist/host/native/control-selection.js +32 -0
- package/dist/host/native/custody.d.ts +5 -0
- package/dist/host/native/custody.js +91 -0
- package/dist/host/native/deadline.d.ts +12 -0
- package/dist/host/native/deadline.js +28 -0
- package/dist/host/native/descendants.d.ts +1 -0
- package/dist/host/native/descendants.js +528 -0
- package/dist/host/native/generated-custodian.d.ts +122 -0
- package/dist/host/native/generated-custodian.js +267 -0
- package/dist/host/native/generated.d.ts +217 -0
- package/dist/host/native/generated.js +379 -0
- package/dist/host/native/image.d.ts +36 -0
- package/dist/host/native/image.js +45 -0
- package/dist/host/native/jobs.d.ts +75 -0
- package/dist/host/native/jobs.js +148 -0
- package/dist/host/native/loader.d.ts +80 -0
- package/dist/host/native/loader.js +269 -0
- package/dist/host/native/materialize.d.ts +22 -0
- package/dist/host/native/materialize.js +175 -0
- package/dist/host/native/module-closure.d.ts +27 -0
- package/dist/host/native/module-closure.js +99 -0
- package/dist/host/native/module-syntax.d.ts +8 -0
- package/dist/host/native/module-syntax.js +63 -0
- package/dist/host/native/native-binary.d.ts +54 -0
- package/dist/host/native/native-binary.js +115 -0
- package/dist/host/native/native-program.d.ts +56 -0
- package/dist/host/native/native-program.js +72 -0
- package/dist/host/native/node-jobs.d.ts +84 -0
- package/dist/host/native/node-jobs.js +187 -0
- package/dist/host/native/node-launch.d.ts +72 -0
- package/dist/host/native/node-launch.js +159 -0
- package/dist/host/native/preload.d.ts +1 -0
- package/dist/host/native/preload.js +45 -0
- package/dist/host/native/reading.d.ts +91 -0
- package/dist/host/native/reading.js +249 -0
- package/dist/host/native/receipt-environment.d.ts +3 -0
- package/dist/host/native/receipt-environment.js +41 -0
- package/dist/host/native/references.d.ts +5 -0
- package/dist/host/native/references.js +30 -0
- package/dist/host/native/root.d.ts +17 -0
- package/dist/host/native/root.js +37 -0
- package/dist/host/native/route.d.ts +66 -0
- package/dist/host/native/route.js +216 -0
- package/dist/host/native/runtime.d.ts +4 -0
- package/dist/host/native/runtime.js +9 -0
- package/dist/host/native-reader-context.d.ts +3 -0
- package/dist/host/native-reader-context.js +8 -0
- package/dist/host/place-clock-selection.d.ts +55 -0
- package/dist/host/place-clock-selection.js +124 -0
- package/dist/host/ports/local-act.d.ts +10 -0
- package/dist/host/ports/local-act.js +78 -6
- package/dist/host/ports/place-act.d.ts +46 -0
- package/dist/host/ports/place-act.js +169 -0
- package/dist/host/process-lifetime.js +8 -3
- package/dist/host/provide.d.ts +25 -0
- package/dist/host/provide.js +115 -13
- package/dist/host/reading-closures.d.ts +15 -1
- package/dist/host/reading-closures.js +88 -6
- package/dist/host/reading-context.d.ts +29 -0
- package/dist/host/reading-context.js +53 -0
- package/dist/host/root-clock-source.d.ts +85 -0
- package/dist/host/root-clock-source.js +305 -0
- package/dist/host/selected-capsules.js +3 -2
- package/dist/land/act.d.ts +26 -0
- package/dist/land/act.js +96 -3
- package/dist/land/ledger.d.ts +4 -0
- package/dist/land/ledger.js +20 -2
- package/dist/observe/runner.d.ts +4 -0
- package/dist/observe/runner.js +9 -2
- package/docs/contracts/cli.md +13 -0
- package/package.json +2 -2
- package/receipts.bound +296 -256
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { sha256 as sha } from "../hash.js";
|
|
2
|
+
/** Fixed host materialization from admitted logical references. Physical locations
|
|
3
|
+
* never arrive from a reader packet and never become semantic source identity.
|
|
4
|
+
*/
|
|
5
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, realpathSync, renameSync, chmodSync } from 'node:fs';
|
|
6
|
+
import { join, dirname, relative, sep, extname } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { gunzipSync } from 'node:zlib';
|
|
9
|
+
import { members as archiveMembers, archiveCoordinate } from "../archive.js";
|
|
10
|
+
import { blobAt, storeAt, holdStore } from "../../land/ledger.js";
|
|
11
|
+
import { ContentNeeded } from "../content-needed.js";
|
|
12
|
+
import { processOf, ownRoot } from "../../observe/runner.js";
|
|
13
|
+
import { instrumentCoordinates } from "../../fold/digests.js";
|
|
14
|
+
import { readBoundedFile } from "./bounded-file.js";
|
|
15
|
+
import { assertNativeRouteCurrent, nativeRouteStore, nativeMountIdentity } from "./route.js";
|
|
16
|
+
const fail = (why) => { throw Error('REFUSE·native materialize ' + why); };
|
|
17
|
+
const mounted = new Map();
|
|
18
|
+
function laid(store, digest, remaining) { const path = blobAt(store, digest); if (!existsSync(path))
|
|
19
|
+
throw new ContentNeeded(digest); const bytes = readBoundedFile(path, remaining, digest); if (sha(bytes) !== digest)
|
|
20
|
+
fail('laid bytes differ ' + digest); return { path, bytes }; }
|
|
21
|
+
/** Called under the actual Root lease before identity/cache preparation. Missing
|
|
22
|
+
* laid content is a named content demand, never a publisher authority fallback.
|
|
23
|
+
* One retained mount per immutable selection avoids copying it on each ask.
|
|
24
|
+
*/
|
|
25
|
+
export function materializeNativeRoute(route) {
|
|
26
|
+
assertNativeRouteCurrent(route);
|
|
27
|
+
const store = nativeRouteStore(route), mountIdentity = nativeMountIdentity(route.profile, route.binaries, route.control), key = store + '\0' + mountIdentity, old = mounted.get(key);
|
|
28
|
+
if (old) {
|
|
29
|
+
const members = new Map();
|
|
30
|
+
for (const member of route.profile.selection.members) {
|
|
31
|
+
if (members.has(member.coordinate))
|
|
32
|
+
fail('ambiguous image coordinate');
|
|
33
|
+
members.set(member.coordinate, member);
|
|
34
|
+
}
|
|
35
|
+
let bytes = 0;
|
|
36
|
+
for (const location of old.locations) {
|
|
37
|
+
const member = members.get(location.member) ?? fail('retained image member not selected ' + location.member);
|
|
38
|
+
const raw = readBoundedFile(location.path, route.profile.selection.budget.bytes - bytes, location.member);
|
|
39
|
+
bytes += raw.length;
|
|
40
|
+
if (sha(raw) !== member.digest)
|
|
41
|
+
fail('retained image member differs ' + member.coordinate);
|
|
42
|
+
if (location.kind === 'binary' && location.member !== route.profile.selection.runtime && !route.binaries.some(b => b.profile.entry.path === location.member && b.origin))
|
|
43
|
+
chmodSync(location.path, 0o555);
|
|
44
|
+
}
|
|
45
|
+
return old;
|
|
46
|
+
}
|
|
47
|
+
const selection = route.profile.selection, budget = selection.budget;
|
|
48
|
+
if (!Number.isSafeInteger(budget.bytes) || budget.bytes < 1 || !Number.isSafeInteger(budget.members) || budget.members < 1 || selection.members.length > budget.members)
|
|
49
|
+
fail('missing image resources');
|
|
50
|
+
const archive = laid(store, selection.archive, budget.bytes);
|
|
51
|
+
gunzipSync(archive.bytes, { maxOutputLength: budget.bytes });
|
|
52
|
+
const files = new Map(archiveMembers(archive.bytes).map(member => [member.name, member.bytes]));
|
|
53
|
+
let charged = archive.bytes.length, retainedBytes = 0;
|
|
54
|
+
const root = storeAt(store, 'cas', 'laid', mountIdentity.slice(7)), present = existsSync(root);
|
|
55
|
+
mkdirSync(dirname(root), { recursive: true });
|
|
56
|
+
const building = present ? root : mkdtempSync(root + '.part-'), seen = new Set();
|
|
57
|
+
try {
|
|
58
|
+
const locations = selection.members.map(member => {
|
|
59
|
+
const coordinate = archiveCoordinate(member.coordinate);
|
|
60
|
+
if (coordinate !== member.coordinate || seen.has(coordinate))
|
|
61
|
+
return fail('ambiguous image coordinate');
|
|
62
|
+
seen.add(coordinate);
|
|
63
|
+
const binary = route.binaries.filter(b => b.profile.entry.path === coordinate), origins = binary.flatMap(b => b.origin ? [b.origin] : []);
|
|
64
|
+
if (new Set(origins.map(o => o.path)).size > 1)
|
|
65
|
+
fail('conflicting binary origins');
|
|
66
|
+
const origin = origins[0];
|
|
67
|
+
if (origin && binary.some(b => !b.origin))
|
|
68
|
+
fail('conflicting relocation/origin meaning');
|
|
69
|
+
let bytes, path;
|
|
70
|
+
if (coordinate === selection.runtime) {
|
|
71
|
+
path = realpathSync(process.execPath);
|
|
72
|
+
bytes = readBoundedFile(path, budget.bytes - charged, coordinate);
|
|
73
|
+
}
|
|
74
|
+
else if (origin) {
|
|
75
|
+
path = origin.path;
|
|
76
|
+
bytes = readBoundedFile(path, budget.bytes - charged, coordinate);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const packed = files.get(coordinate);
|
|
80
|
+
if (packed !== undefined)
|
|
81
|
+
bytes = packed;
|
|
82
|
+
else
|
|
83
|
+
bytes = laid(store, member.digest, budget.bytes - charged).bytes;
|
|
84
|
+
path = join(building, coordinate);
|
|
85
|
+
}
|
|
86
|
+
if (binary.some(b => b.profile.entry.bytes !== bytes.length))
|
|
87
|
+
fail('binary entry byte count differs ' + coordinate);
|
|
88
|
+
if (bytes.length > budget.bytes - charged || (charged += bytes.length) > budget.bytes)
|
|
89
|
+
fail('aggregate image and archive bytes exceeded');
|
|
90
|
+
if (sha(bytes) !== member.digest)
|
|
91
|
+
fail('image member differs ' + coordinate);
|
|
92
|
+
if (coordinate !== selection.runtime && !origin) {
|
|
93
|
+
retainedBytes += bytes.length;
|
|
94
|
+
if (present) {
|
|
95
|
+
const actual = readBoundedFile(path, bytes.length, coordinate);
|
|
96
|
+
if (sha(actual) !== member.digest)
|
|
97
|
+
fail('retained image member differs ' + coordinate);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
101
|
+
writeFileSync(path, bytes, { flag: 'wx', mode: binary.length ? 0o555 : 0o444 });
|
|
102
|
+
}
|
|
103
|
+
if (binary.length)
|
|
104
|
+
chmodSync(path, 0o555);
|
|
105
|
+
}
|
|
106
|
+
return { member: coordinate, path, kind: coordinate === selection.runtime || binary.length ? 'binary' : /\.(?:[mc]?js|ts)$/.test(coordinate) ? 'module' : 'data' };
|
|
107
|
+
});
|
|
108
|
+
if (!present) {
|
|
109
|
+
renameSync(building, root);
|
|
110
|
+
for (const location of locations)
|
|
111
|
+
if (location.member !== selection.runtime && !route.binaries.some(b => b.profile.entry.path === location.member && b.origin))
|
|
112
|
+
location.path = join(root, location.member);
|
|
113
|
+
holdStore(store, 'laid', mountIdentity, retainedBytes);
|
|
114
|
+
}
|
|
115
|
+
const result = { archive: archive.path, locations, cwd: root, mountIdentity };
|
|
116
|
+
mounted.set(key, result);
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (!present)
|
|
121
|
+
rmSync(building, { recursive: true, force: true });
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Fixed child, lifetime supervisor and transitive host modules enter identity
|
|
126
|
+
* before the cache key. Existing instrument closure resolves actual dependencies;
|
|
127
|
+
* this descriptor never executes a module and never accepts a world verifier.
|
|
128
|
+
*/
|
|
129
|
+
export function nativeExecutorInputs(route) {
|
|
130
|
+
const store = nativeRouteStore(route), all = new Map();
|
|
131
|
+
for (const entry of [...(route.control ? [fileURLToPath(new URL('./control-bridge-worker' + extname(new URL(import.meta.url).pathname), import.meta.url))] : []), processOf('native'), processOf('lifetime', 'host'), processOf('custodian'), processOf('native-descendant'), processOf('preload', 'host/native')])
|
|
132
|
+
for (const [path, digest] of instrumentCoordinates(store, entry, [], ownRoot(), [], true))
|
|
133
|
+
all.set(path, digest);
|
|
134
|
+
const refs = [];
|
|
135
|
+
let bytes = 0;
|
|
136
|
+
for (const [path, digest] of all) {
|
|
137
|
+
const held = readBoundedFile(path, route.profile.selection.budget.bytes - bytes, path);
|
|
138
|
+
bytes += held.length;
|
|
139
|
+
if (sha(held) !== digest)
|
|
140
|
+
fail('fixed executor changed during descriptor');
|
|
141
|
+
const local = relative(ownRoot(), path);
|
|
142
|
+
let name;
|
|
143
|
+
if (!local.startsWith('..' + sep) && local !== '..')
|
|
144
|
+
name = local.split(sep).join('/');
|
|
145
|
+
else {
|
|
146
|
+
let cursor = dirname(path);
|
|
147
|
+
for (;;) {
|
|
148
|
+
const manifest = join(cursor, 'package.json');
|
|
149
|
+
if (existsSync(manifest)) {
|
|
150
|
+
let pkg;
|
|
151
|
+
try {
|
|
152
|
+
const raw = readBoundedFile(manifest, route.profile.selection.budget.bytes - bytes, manifest);
|
|
153
|
+
bytes += raw.length;
|
|
154
|
+
pkg = JSON.parse(raw.toString('utf8'));
|
|
155
|
+
if (typeof pkg.name === 'string')
|
|
156
|
+
refs.push([['native/executor/library', pkg.name, 'package.json'].join('/'), sha(raw)]);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return fail('fixed dependency manifest invalid');
|
|
160
|
+
}
|
|
161
|
+
if (typeof pkg.name !== 'string')
|
|
162
|
+
return fail('fixed dependency has no declared name');
|
|
163
|
+
name = 'library/' + pkg.name + '/' + relative(cursor, path).split(sep).join('/');
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
const parent = dirname(cursor);
|
|
167
|
+
if (parent === cursor)
|
|
168
|
+
return fail('fixed dependency logical coordinates absent');
|
|
169
|
+
cursor = parent;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
refs.push(['native/executor/' + name, digest]);
|
|
173
|
+
}
|
|
174
|
+
return [...new Map(refs).entries()];
|
|
175
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
/** Host-selected archive and entry, never a verifier assertion from the program.
|
|
16
|
+
* This proves content identity only; standing admission and runtime capabilities
|
|
17
|
+
* are separate prerequisites. Binary resources are not executable text members.
|
|
18
|
+
*/
|
|
19
|
+
export declare function moduleSnapshot(archive: Uint8Array, expected: string, entry: string): ModuleClosureSnapshot;
|
|
20
|
+
/** The host supplies authenticated snapshot identities and its library resolver.
|
|
21
|
+
* Each reached text member is verified here against its exact UTF-8 bytes.
|
|
22
|
+
* Authentication of the snapshot envelope remains a separate host prerequisite.
|
|
23
|
+
* A capsule cannot select either verifier or resolver.
|
|
24
|
+
* This reads declared module edges, not arbitrary I/O or a JavaScript sandbox. Computed edges are unpaid, never absent.
|
|
25
|
+
* Activation requires an admitted host policy; existing inventory callers are not silently changed.
|
|
26
|
+
*/
|
|
27
|
+
export declare function moduleClosure(snapshot: ModuleClosureSnapshot, library: (specifier: string, parent: ModuleClosureSnapshot) => ModuleClosureSnapshot | undefined): readonly ModuleClosureReading[];
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { builtinModules } from 'node:module';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { posix } from 'node:path';
|
|
4
|
+
import { moduleImportSyntax } from "./module-syntax.js";
|
|
5
|
+
import { members } from "../archive.js";
|
|
6
|
+
/** Host-selected archive and entry, never a verifier assertion from the program.
|
|
7
|
+
* This proves content identity only; standing admission and runtime capabilities
|
|
8
|
+
* are separate prerequisites. Binary resources are not executable text members.
|
|
9
|
+
*/
|
|
10
|
+
export function moduleSnapshot(archive, expected, entry) {
|
|
11
|
+
const identity = 'sha256:' + createHash('sha256').update(archive).digest('hex');
|
|
12
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(expected) || identity !== expected)
|
|
13
|
+
throw Error('REFUSE·module closure archive bytes differ');
|
|
14
|
+
const textMembers = new Map();
|
|
15
|
+
for (const member of members(archive)) {
|
|
16
|
+
// Platform archive metadata remains covered by the archive digest. Its
|
|
17
|
+
// binary header, rather than a filename convention, identifies data.
|
|
18
|
+
if (member.bytes.length >= 4 && new DataView(member.bytes.buffer, member.bytes.byteOffset, 4).getUint32(0) === 0x00051607)
|
|
19
|
+
continue;
|
|
20
|
+
if (!['.js', '.mjs', '.cjs', '.ts', '.json'].includes(posix.extname(member.name)))
|
|
21
|
+
continue;
|
|
22
|
+
let source;
|
|
23
|
+
try {
|
|
24
|
+
source = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(member.bytes);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw Error('REFUSE·module closure invalid UTF-8 bytes ' + member.name);
|
|
28
|
+
}
|
|
29
|
+
textMembers.set(member.name, { source, digest: 'sha256:' + createHash('sha256').update(member.bytes).digest('hex') });
|
|
30
|
+
}
|
|
31
|
+
return { identity, entry, members: textMembers };
|
|
32
|
+
}
|
|
33
|
+
/** The host supplies authenticated snapshot identities and its library resolver.
|
|
34
|
+
* Each reached text member is verified here against its exact UTF-8 bytes.
|
|
35
|
+
* Authentication of the snapshot envelope remains a separate host prerequisite.
|
|
36
|
+
* A capsule cannot select either verifier or resolver.
|
|
37
|
+
* This reads declared module edges, not arbitrary I/O or a JavaScript sandbox. Computed edges are unpaid, never absent.
|
|
38
|
+
* Activation requires an admitted host policy; existing inventory callers are not silently changed.
|
|
39
|
+
*/
|
|
40
|
+
export function moduleClosure(snapshot, library) {
|
|
41
|
+
const seen = new Map();
|
|
42
|
+
const coordinate = (name) => {
|
|
43
|
+
const normalized = posix.normalize(name);
|
|
44
|
+
if (!name || name.includes('\0') || name.includes('\\') || posix.isAbsolute(name) || normalized === '..' || normalized.startsWith('../'))
|
|
45
|
+
throw Error('REFUSE·module closure coordinate leaves snapshot ' + name);
|
|
46
|
+
return normalized;
|
|
47
|
+
};
|
|
48
|
+
const walk = (one, name) => {
|
|
49
|
+
const at = coordinate(name), key = JSON.stringify([one.identity, at]);
|
|
50
|
+
const member = one.members.get(at);
|
|
51
|
+
if (member === undefined)
|
|
52
|
+
throw Error('REFUSE·module closure missing ' + one.identity + ' ' + at);
|
|
53
|
+
if (typeof member.source !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(member.digest))
|
|
54
|
+
throw Error('REFUSE·module closure unnamed bytes ' + one.identity + ' ' + at);
|
|
55
|
+
const bytes = Buffer.from(member.source, 'utf8');
|
|
56
|
+
if (bytes.toString('utf8') !== member.source)
|
|
57
|
+
throw Error('REFUSE·module closure invalid UTF-8 text ' + one.identity + ' ' + at);
|
|
58
|
+
if ('sha256:' + createHash('sha256').update(bytes).digest('hex') !== member.digest)
|
|
59
|
+
throw Error('REFUSE·module closure bytes differ ' + one.identity + ' ' + at);
|
|
60
|
+
const kept = seen.get(key);
|
|
61
|
+
if (kept) {
|
|
62
|
+
if (kept.digest !== member.digest)
|
|
63
|
+
throw Error('REFUSE·module closure conflicting member ' + one.identity + ' ' + at);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
seen.set(key, { snapshot: one.identity, coordinate: at, digest: member.digest });
|
|
67
|
+
// JSON is data under the loader contract, not module syntax.
|
|
68
|
+
if (posix.extname(at) === '.json')
|
|
69
|
+
return;
|
|
70
|
+
const syntax = moduleImportSyntax(member.source, at);
|
|
71
|
+
if (syntax.unresolved.length)
|
|
72
|
+
throw Error('REFUSE·module closure unresolved ' + at + ' ' + syntax.unresolved.join(' · '));
|
|
73
|
+
for (const specifier of syntax.imports) {
|
|
74
|
+
// createRequire/registerHooks can select executable edges not present in this graph.
|
|
75
|
+
// Their runtime closure needs its own host contract; an arbitrary alias is not a literal edge.
|
|
76
|
+
if (specifier === 'node:module' || specifier === 'module')
|
|
77
|
+
throw Error('REFUSE·module closure runtime loader requires declared closure ' + specifier + ' from ' + at);
|
|
78
|
+
if (specifier.startsWith('node:')) {
|
|
79
|
+
if (!builtinModules.includes(specifier) && !builtinModules.includes(specifier.slice(5)))
|
|
80
|
+
throw Error('REFUSE·module closure unknown builtin ' + specifier + ' from ' + at);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (builtinModules.includes(specifier))
|
|
84
|
+
continue;
|
|
85
|
+
if (specifier.startsWith('.')) {
|
|
86
|
+
walk(one, posix.join(posix.dirname(at), specifier));
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (specifier.includes(':') || posix.isAbsolute(specifier))
|
|
90
|
+
throw Error('REFUSE·module closure unsupported location ' + specifier + ' from ' + at);
|
|
91
|
+
const pinned = library(specifier, one);
|
|
92
|
+
if (pinned === undefined)
|
|
93
|
+
throw Error('REFUSE·module closure unpinned library ' + specifier + ' from ' + at);
|
|
94
|
+
walk(pinned, pinned.entry);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
walk(snapshot, snapshot.entry);
|
|
98
|
+
return [...seen.values()];
|
|
99
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ModuleImportSyntax {
|
|
2
|
+
readonly imports: readonly string[];
|
|
3
|
+
readonly unresolved: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
/** Syntax inventory is not an execution-closure certificate: unresolved expressions stay explicit. */
|
|
6
|
+
export declare function moduleImportSyntax(source: string, coordinate: string): ModuleImportSyntax;
|
|
7
|
+
/** Existing inventory keeps its meaning; callers requiring closure must inspect unresolved edges. */
|
|
8
|
+
export declare function moduleImports(source: string, coordinate: string): readonly string[];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { stringLiteral } from "../adapters/modules/javascript.js";
|
|
2
|
+
import * as nodeModule from 'node:module';
|
|
3
|
+
import { extname } from 'node:path';
|
|
4
|
+
/** Host module syntax, never wire or domain interpretation. Reading imports executes no module. */
|
|
5
|
+
// The package's minimum Node runtime provides this API; older build-only Node types do not.
|
|
6
|
+
const require = nodeModule.createRequire(import.meta.url);
|
|
7
|
+
const { parse } = require('acorn');
|
|
8
|
+
const stripTypeScriptTypes = nodeModule.stripTypeScriptTypes;
|
|
9
|
+
/** Syntax inventory is not an execution-closure certificate: unresolved expressions stay explicit. */
|
|
10
|
+
export function moduleImportSyntax(source, coordinate) {
|
|
11
|
+
const code = extname(coordinate) === '.ts' ? stripTypeScriptTypes(source, { mode: 'transform' }) : source;
|
|
12
|
+
let tree;
|
|
13
|
+
try {
|
|
14
|
+
tree = parse(code, { ecmaVersion: 'latest', sourceType: 'module', allowReturnOutsideFunction: true, allowHashBang: true });
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw Error('REFUSE·source module syntax cannot be read: ' + coordinate);
|
|
18
|
+
}
|
|
19
|
+
const found = new Set(), unresolved = new Set();
|
|
20
|
+
const literal = stringLiteral;
|
|
21
|
+
const visit = (node, parent, key) => {
|
|
22
|
+
if (!node || typeof node !== 'object')
|
|
23
|
+
return;
|
|
24
|
+
if (Array.isArray(node)) {
|
|
25
|
+
for (const child of node)
|
|
26
|
+
visit(child, parent, key);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const n = node;
|
|
30
|
+
// Escaped loader references do not become an empty inventory. This is
|
|
31
|
+
// conservative syntax, not a complete JavaScript alias/data-flow proof.
|
|
32
|
+
if (n['type'] === 'Identifier' && n['name'] === 'require') {
|
|
33
|
+
const direct = parent?.['type'] === 'CallExpression' && key === 'callee';
|
|
34
|
+
const namedProperty = (parent?.['type'] === 'MemberExpression' && key === 'property' && !parent['computed']) || ((parent?.['type'] === 'Property' || parent?.['type'] === 'MethodDefinition') && key === 'key' && !parent['computed'] && !parent['shorthand']);
|
|
35
|
+
if (!direct && !namedProperty)
|
|
36
|
+
unresolved.add('require reference at ' + String(n['start']));
|
|
37
|
+
}
|
|
38
|
+
let specifier;
|
|
39
|
+
if (n['type'] === 'ImportDeclaration' || n['type'] === 'ExportNamedDeclaration' || n['type'] === 'ExportAllDeclaration' || n['type'] === 'ImportExpression') {
|
|
40
|
+
specifier = literal(n['source']);
|
|
41
|
+
if (n['type'] === 'ImportExpression' && specifier === undefined)
|
|
42
|
+
unresolved.add('import expression at ' + String(n['start']));
|
|
43
|
+
}
|
|
44
|
+
if (n['type'] === 'CallExpression') {
|
|
45
|
+
const callee = n['callee'];
|
|
46
|
+
if (callee?.['type'] === 'Identifier' && callee['name'] === 'require') {
|
|
47
|
+
specifier = literal(n['arguments'][0]);
|
|
48
|
+
if (specifier === undefined)
|
|
49
|
+
unresolved.add('require expression at ' + String(n['start']));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (specifier !== undefined)
|
|
53
|
+
found.add(specifier);
|
|
54
|
+
for (const [childKey, child] of Object.entries(n))
|
|
55
|
+
visit(child, n, childKey);
|
|
56
|
+
};
|
|
57
|
+
visit(tree);
|
|
58
|
+
return { imports: [...found], unresolved: [...unresolved] };
|
|
59
|
+
}
|
|
60
|
+
/** Existing inventory keeps its meaning; callers requiring closure must inspect unresolved edges. */
|
|
61
|
+
export function moduleImports(source, coordinate) {
|
|
62
|
+
return moduleImportSyntax(source, coordinate).imports;
|
|
63
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type RootStanding } from './reading.ts';
|
|
2
|
+
import { type ParentExecution } from './generated.ts';
|
|
3
|
+
interface NativeBinaryFields {
|
|
4
|
+
coordinate: string;
|
|
5
|
+
alias: string;
|
|
6
|
+
selector: {
|
|
7
|
+
port: string;
|
|
8
|
+
binding: string;
|
|
9
|
+
implementation: string;
|
|
10
|
+
};
|
|
11
|
+
entry: {
|
|
12
|
+
path: string;
|
|
13
|
+
digest: string;
|
|
14
|
+
bytes: number;
|
|
15
|
+
};
|
|
16
|
+
limits: {
|
|
17
|
+
bytes: number;
|
|
18
|
+
};
|
|
19
|
+
condition: string;
|
|
20
|
+
}
|
|
21
|
+
export type NativeBinaryProfile = NativeBinaryFields & ({
|
|
22
|
+
profile: 'host-native-binary@1';
|
|
23
|
+
} | {
|
|
24
|
+
profile: 'host-native-binary-origin@1';
|
|
25
|
+
origin: string;
|
|
26
|
+
});
|
|
27
|
+
export interface NativeBinaryOrigin {
|
|
28
|
+
readonly coordinate: string;
|
|
29
|
+
readonly line: string;
|
|
30
|
+
readonly path: string;
|
|
31
|
+
}
|
|
32
|
+
/** A Root-selected local origin is a location constraint, not a World hint. */
|
|
33
|
+
export declare function hostOriginPath(root: RootStanding, coordinate: string): NativeBinaryOrigin;
|
|
34
|
+
export declare function nativeBinaryOrigin(root: RootStanding, p: NativeBinaryProfile, portLine: string): NativeBinaryOrigin | undefined;
|
|
35
|
+
export interface VerifiedNativeBinary {
|
|
36
|
+
readonly identity: string;
|
|
37
|
+
}
|
|
38
|
+
export interface NativeBinaryProjection {
|
|
39
|
+
kind: 'native-binary';
|
|
40
|
+
alias: string;
|
|
41
|
+
entry: string;
|
|
42
|
+
entryDigest: string;
|
|
43
|
+
entryBytes: number;
|
|
44
|
+
condition: string;
|
|
45
|
+
}
|
|
46
|
+
/** Call under the existing Root guard; privateBinding is from the already
|
|
47
|
+
* prepared host image, never the candidate profile's own expected digest. */
|
|
48
|
+
export declare function verifyNativeBinary(root: RootStanding, coordinate: string, parent: ParentExecution, privateBinding: {
|
|
49
|
+
coordinate: string;
|
|
50
|
+
path: string;
|
|
51
|
+
digest: string;
|
|
52
|
+
}): VerifiedNativeBinary;
|
|
53
|
+
export declare const nativeBinaryProjection: (handle: VerifiedNativeBinary) => NativeBinaryProjection;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { retainedProjection } from "./reading.js";
|
|
2
|
+
import { isSha256 } from "../hash.js";
|
|
3
|
+
import { fieldNames } from "./reading.js";
|
|
4
|
+
import { sha256 } from "../hash.js";
|
|
5
|
+
/** Host-only native binary selection. No PATH lookup, Node impersonation,
|
|
6
|
+
* transitive OS-library seal or arbitrary-process sandbox is claimed. */
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { isAbsolute } from 'node:path';
|
|
9
|
+
import { realpathSync } from 'node:fs';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { authorityAtEpoch } from '@lapxo/topos/wire';
|
|
12
|
+
import { parsedLine as parse } from "../../fold/claims.js";
|
|
13
|
+
import { authorityFor } from "../../fold/signers.js";
|
|
14
|
+
import { authenticateSignedPort, nativeParentInputBytes, canonicalJson } from "./reading.js";
|
|
15
|
+
import { readBounded } from "./generated.js";
|
|
16
|
+
const sha = sha256, keys = fieldNames, validDigest = isSha256, fail = (why) => { throw Error('REFUSE·native binary ' + why); };
|
|
17
|
+
/** A Root-selected local origin is a location constraint, not a World hint. */
|
|
18
|
+
export function hostOriginPath(root, coordinate) {
|
|
19
|
+
if (!/^region\/[^\s]+$/.test(coordinate))
|
|
20
|
+
fail('origin is not a declared region');
|
|
21
|
+
const rows = root.live.filter(line => { const row = parse(line); return row.kind === 'fact' && row.value.fields.scope === coordinate && row.value.fields.measure === 'origin' && row.value.fields.value !== 'withdraw'; });
|
|
22
|
+
if (rows.length !== 1)
|
|
23
|
+
fail('missing/conflicting origin ' + coordinate);
|
|
24
|
+
const line = rows[0];
|
|
25
|
+
if (!root.history.includes(line))
|
|
26
|
+
fail('origin absent from authenticated history');
|
|
27
|
+
const row = parse(line);
|
|
28
|
+
if (row.kind !== 'fact')
|
|
29
|
+
return fail('origin not a wire record');
|
|
30
|
+
const f = row.value.fields;
|
|
31
|
+
if (f.role !== 'writes' || f.form !== 'alphabet')
|
|
32
|
+
fail('origin declaration meaning');
|
|
33
|
+
const auth = authorityFor(root.history, root.signer, root.eras), signed = auth.of(line);
|
|
34
|
+
if (signed.kind !== 'admitted' || signed.signer !== root.signer.id)
|
|
35
|
+
fail('origin must be Root signed');
|
|
36
|
+
const current = authorityAtEpoch(f, root.signer, root.epoch, coordinate, 'authority');
|
|
37
|
+
if (current.kind !== 'admitted')
|
|
38
|
+
fail('current origin authority ' + current.why);
|
|
39
|
+
let url, path;
|
|
40
|
+
try {
|
|
41
|
+
url = new URL(f.value);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return fail('origin URL invalid');
|
|
45
|
+
}
|
|
46
|
+
if (url.protocol !== 'file:' || url.username || url.password || url.search || url.hash || (url.hostname && url.hostname !== 'localhost'))
|
|
47
|
+
fail('unsupported origin transport');
|
|
48
|
+
try {
|
|
49
|
+
path = fileURLToPath(url);
|
|
50
|
+
if (!isAbsolute(path) || path.includes('\0'))
|
|
51
|
+
return fail('origin path invalid');
|
|
52
|
+
path = realpathSync(path);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return fail('local origin absent/invalid');
|
|
56
|
+
}
|
|
57
|
+
return Object.freeze({ coordinate, line, path });
|
|
58
|
+
}
|
|
59
|
+
export function nativeBinaryOrigin(root, p, portLine) {
|
|
60
|
+
if (p.profile !== 'host-native-binary-origin@1')
|
|
61
|
+
return;
|
|
62
|
+
const signed = authorityFor(root.history, root.signer, root.eras).of(portLine);
|
|
63
|
+
if (signed.kind !== 'admitted' || signed.signer !== root.signer.id)
|
|
64
|
+
fail('origin selection must be Root signed');
|
|
65
|
+
const origin = hostOriginPath(root, p.origin);
|
|
66
|
+
const bytes = readBounded(origin.path, p.entry.bytes);
|
|
67
|
+
if (bytes.byteLength !== p.entry.bytes || sha(bytes) !== p.entry.digest)
|
|
68
|
+
fail('origin bytes differ');
|
|
69
|
+
return origin;
|
|
70
|
+
}
|
|
71
|
+
const verified = new WeakMap();
|
|
72
|
+
/** Call under the existing Root guard; privateBinding is from the already
|
|
73
|
+
* prepared host image, never the candidate profile's own expected digest. */
|
|
74
|
+
export function verifyNativeBinary(root, coordinate, parent, privateBinding) {
|
|
75
|
+
const auth = authenticateSignedPort(root, coordinate), p = auth.profile;
|
|
76
|
+
if (!p || keys(p) !== (p.profile === 'host-native-binary-origin@1' ? 'alias,condition,coordinate,entry,limits,origin,profile,selector' : 'alias,condition,coordinate,entry,limits,profile,selector') || !['host-native-binary@1', 'host-native-binary-origin@1'].includes(p.profile) || p.coordinate !== coordinate || typeof p.alias !== 'string' || !p.alias.length || p.alias.includes('\0') || typeof p.condition !== 'string' || !p.condition.length)
|
|
77
|
+
fail('selection absent/unknown');
|
|
78
|
+
if (keys(p.selector ?? {}) !== 'binding,implementation,port' || canonicalJson(p.selector) !== canonicalJson({ port: parent.port, binding: parent.binding, implementation: parent.implementation }))
|
|
79
|
+
fail('selection differs from parent');
|
|
80
|
+
if (!validDigest(parent.identity) || !validDigest(parent.instrument) || !validDigest(parent.policy) || !validDigest(parent.authority) || !validDigest(parent.implementation) || !Array.isArray(parent.inputs))
|
|
81
|
+
fail('parent identity absent');
|
|
82
|
+
if (keys(p.entry ?? {}) !== 'bytes,digest,path' || typeof p.entry.path !== 'string' || isAbsolute(p.entry.path) || p.entry.path.split('/').some(s => !s || s === '.' || s === '..') || !validDigest(p.entry.digest) || !Number.isSafeInteger(p.entry.bytes) || p.entry.bytes < 1 || keys(p.limits ?? {}) !== 'bytes' || !Number.isSafeInteger(p.limits.bytes) || p.limits.bytes < 1 || p.entry.bytes > p.limits.bytes || p.limits.bytes > nativeParentInputBytes(root, parent))
|
|
83
|
+
fail('entry byte resource absent/outside Root');
|
|
84
|
+
if (!privateBinding || typeof privateBinding.path !== 'string' || !isAbsolute(privateBinding.path) || !validDigest(privateBinding.digest) || p.entry.path !== privateBinding.coordinate || p.entry.digest !== privateBinding.digest || !parent.inputs.some(([_, digest]) => digest === privateBinding.digest))
|
|
85
|
+
fail('private image binding absent/differs');
|
|
86
|
+
const origin = nativeBinaryOrigin(root, p, auth.line);
|
|
87
|
+
let expected, actual;
|
|
88
|
+
try {
|
|
89
|
+
expected = realpathSync(privateBinding.path);
|
|
90
|
+
actual = realpathSync(privateBinding.path);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return fail('entry absent');
|
|
94
|
+
}
|
|
95
|
+
if (expected !== actual)
|
|
96
|
+
fail('entry path differs from private image');
|
|
97
|
+
let runtimePath;
|
|
98
|
+
try {
|
|
99
|
+
runtimePath = realpathSync(parent.runtime);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return fail('runtime binding absent');
|
|
103
|
+
}
|
|
104
|
+
if (actual === runtimePath)
|
|
105
|
+
fail('binary aliases actual runtime');
|
|
106
|
+
if (origin && actual !== origin.path)
|
|
107
|
+
fail('entry differs from declared origin');
|
|
108
|
+
const bytes = readBounded(actual, p.entry.bytes);
|
|
109
|
+
if (bytes.byteLength !== p.entry.bytes || sha(bytes) !== privateBinding.digest)
|
|
110
|
+
fail('entry bytes differ');
|
|
111
|
+
const handle = Object.freeze({ identity: 'sha256:' + createHash('sha256').update(canonicalJson([auth.line, parent.identity, ...(origin ? [origin.line] : [])])).digest('hex') });
|
|
112
|
+
verified.set(handle, { kind: 'native-binary', alias: p.alias, entry: actual, entryDigest: privateBinding.digest, entryBytes: bytes.byteLength, condition: p.condition });
|
|
113
|
+
return handle;
|
|
114
|
+
}
|
|
115
|
+
export const nativeBinaryProjection = retainedProjection(verified, fail);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type RootStanding } from './reading.ts';
|
|
2
|
+
import { type ParentExecution } from './generated.ts';
|
|
3
|
+
export interface NativeProgramProfile {
|
|
4
|
+
profile: 'host-native-program@1';
|
|
5
|
+
coordinate: string;
|
|
6
|
+
alias: string;
|
|
7
|
+
selector: {
|
|
8
|
+
port: string;
|
|
9
|
+
binding: string;
|
|
10
|
+
implementation: string;
|
|
11
|
+
};
|
|
12
|
+
interpreter: {
|
|
13
|
+
path: string;
|
|
14
|
+
digest: string;
|
|
15
|
+
bytes: number;
|
|
16
|
+
};
|
|
17
|
+
root: string;
|
|
18
|
+
entry: string;
|
|
19
|
+
members: readonly {
|
|
20
|
+
coordinate: string;
|
|
21
|
+
digest: string;
|
|
22
|
+
bytes: number;
|
|
23
|
+
}[];
|
|
24
|
+
limits: {
|
|
25
|
+
members: number;
|
|
26
|
+
bytes: number;
|
|
27
|
+
};
|
|
28
|
+
condition: string;
|
|
29
|
+
}
|
|
30
|
+
export interface VerifiedNativeProgram {
|
|
31
|
+
readonly identity: string;
|
|
32
|
+
}
|
|
33
|
+
export interface NativeProgramProjection {
|
|
34
|
+
alias: string;
|
|
35
|
+
interpreter: string;
|
|
36
|
+
interpreterDigest: string;
|
|
37
|
+
entry: string;
|
|
38
|
+
root: string;
|
|
39
|
+
members: readonly {
|
|
40
|
+
path: string;
|
|
41
|
+
digest: string;
|
|
42
|
+
}[];
|
|
43
|
+
condition: string;
|
|
44
|
+
}
|
|
45
|
+
export declare function verifyNativeProgram(root: RootStanding, coordinate: string, parent: ParentExecution, runtimeBinding: {
|
|
46
|
+
coordinate: string;
|
|
47
|
+
path: string;
|
|
48
|
+
digest: string;
|
|
49
|
+
members: readonly {
|
|
50
|
+
coordinate: string;
|
|
51
|
+
path: string;
|
|
52
|
+
}[];
|
|
53
|
+
}): VerifiedNativeProgram;
|
|
54
|
+
/** Only the guarded instrument can obtain this from a verified opaque handle.
|
|
55
|
+
* Put it in the signed native image; never accept it from a world proposal. */
|
|
56
|
+
export declare const nativeProgramProjection: (handle: VerifiedNativeProgram) => NativeProgramProjection;
|