@lapxo/bound 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -20
- package/TARGET.bound +70 -27
- package/assets/wasm/guest.js +4 -0
- package/assets/wasm/manifest.json +27 -0
- package/assets/wasm/runtime.js +6 -0
- package/assets/wasm/runtime.wasm +0 -0
- 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 +9 -4
- package/dist/cli/place.d.ts +2 -0
- package/dist/cli/place.js +7 -4
- package/dist/cli/standing-result.js +7 -26
- package/dist/cli/verb.js +65 -9
- package/dist/engine/index.d.ts +11 -0
- package/dist/engine/index.js +9 -0
- package/dist/engine/object.d.ts +17 -0
- package/dist/engine/object.js +38 -0
- 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 +36 -7
- package/dist/fold/digests.d.ts +1 -1
- package/dist/fold/digests.js +6 -4
- package/dist/fold/evidence.d.ts +1 -1
- package/dist/fold/kept.js +2 -1
- package/dist/fold/object.d.ts +3 -17
- package/dist/fold/object.js +2 -38
- 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 +12 -2
- 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 +57 -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/cell-receipts.js +1 -1
- 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/objects.d.ts +2 -1
- package/dist/host/objects.js +24 -4
- 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 +17 -1
- package/dist/host/reading-closures.js +98 -18
- package/dist/host/reading-context.d.ts +29 -0
- package/dist/host/reading-context.js +53 -0
- package/dist/host/reference-authenticated.d.ts +28 -0
- package/dist/host/reference-authenticated.js +84 -0
- package/dist/host/reference-native.d.ts +10 -0
- package/dist/host/reference-native.js +50 -0
- package/dist/host/reference-profile.d.ts +23 -0
- package/dist/host/reference-profile.js +62 -0
- package/dist/host/reference.d.ts +6 -0
- package/dist/host/reference.js +24 -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/host/selected-topos.d.ts +1 -1
- package/dist/host/selected-topos.js +9 -1
- 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/regions.d.ts +4 -0
- package/dist/observe/regions.js +109 -1
- package/dist/observe/runner.d.ts +4 -0
- package/dist/observe/runner.js +9 -2
- package/dist/wasm/adapter.d.ts +51 -0
- package/dist/wasm/adapter.js +180 -0
- package/dist/wasm/node-worker.d.ts +1 -0
- package/dist/wasm/node-worker.js +13 -0
- package/dist/wasm/object.d.ts +8 -0
- package/dist/wasm/object.js +76 -0
- package/dist/wasm/worker.d.ts +1 -0
- package/dist/wasm/worker.js +7 -0
- package/docs/contracts/authenticated-reference.publication.json +25 -0
- package/docs/contracts/cli.md +13 -0
- package/docs/contracts/engine-object-projection.publication.json +25 -0
- package/docs/contracts/engine.md +39 -0
- package/docs/contracts/native-publication.inventory.json +8 -0
- package/docs/contracts/optional-wasm-adapter.publication.json +25 -0
- package/docs/contracts/publication-additions.inventory.json +8 -0
- package/docs/contracts/reference-port.md +33 -0
- package/docs/contracts/reference-port.publication.json +25 -0
- package/licenses/wasm/bound.txt +21 -0
- package/licenses/wasm/fast-text-encoding.txt +201 -0
- package/licenses/wasm/jitl-quickjs-ffi-types.txt +21 -0
- package/licenses/wasm/jitl-quickjs-wasmfile-release-sync.txt +47 -0
- package/licenses/wasm/obligations.txt +21 -0
- package/licenses/wasm/quickjs-emscripten-core.txt +21 -0
- package/licenses/wasm/topos.txt +21 -0
- package/package.json +94 -4
- package/receipts.bound +7672 -573
- package/samples/authenticated-reference/no.json +31 -0
- package/samples/authenticated-reference/yes/editorial-state/TARGET.bound +57 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/arrival.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/coverage.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/era.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/order.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/signature.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/transport.arrival.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/transport.coverage.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/transport.era.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/transport.order.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/transport.signature.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/transport.yes.json +1 -0
- package/samples/authenticated-reference/yes/editorial-state/inputs/yes.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/TARGET.bound +57 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/arrival.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/coverage.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/era.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/order.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/signature.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/transport.arrival.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/transport.coverage.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/transport.era.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/transport.order.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/transport.signature.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/transport.yes.json +1 -0
- package/samples/authenticated-reference/yes/weather-reading/inputs/yes.json +1 -0
- package/samples/authenticated-reference/yes.json +19 -0
- package/samples/engine-alphabet/TARGET.bound +29 -0
- package/samples/engine-alphabet/inputs/sample.json +323 -0
- package/samples/engine-alphabet/package.json +1 -0
- package/samples/engine-alphabet/reader.js +1457 -0
- package/samples/engine-alphabet/receipts.bound +12 -0
- package/samples/engine-interval/TARGET.bound +29 -0
- package/samples/engine-interval/inputs/sample.json +277 -0
- package/samples/engine-interval/package.json +1 -0
- package/samples/engine-interval/reader.js +1457 -0
- package/samples/engine-interval/receipts.bound +12 -0
- package/samples/no/engine-order/TARGET.bound +29 -0
- package/samples/no/engine-order/inputs/sample.json +277 -0
- package/samples/no/engine-order/package.json +1 -0
- package/samples/no/engine-order/reader.js +1457 -0
- package/samples/reference-port/no/catalog/standing.json +1 -0
- package/samples/reference-port/no/catalog/transport.json +1 -0
- package/samples/reference-port/no/route-notes/standing.json +1 -0
- package/samples/reference-port/no/route-notes/transport.json +1 -0
- package/samples/reference-port/yes/catalog/TARGET.bound +33 -0
- package/samples/reference-port/yes/catalog/standing.json +1 -0
- package/samples/reference-port/yes/catalog/transport.json +1 -0
- package/samples/reference-port/yes/route-notes/TARGET.bound +33 -0
- package/samples/reference-port/yes/route-notes/standing.json +1 -0
- package/samples/reference-port/yes/route-notes/transport.json +1 -0
- package/samples/wasm/editorial-notes/TARGET.bound +65 -0
- package/samples/wasm/editorial-notes/esm.bound +59 -0
- package/samples/wasm/editorial-notes/no-deadline.bound +65 -0
- package/samples/wasm/editorial-notes/no-memory.bound +65 -0
- package/samples/wasm/editorial-notes/no-missing-limit.bound +64 -0
- package/samples/wasm/editorial-notes/receipts.bound +1 -0
- package/samples/wasm/water-level/TARGET.bound +65 -0
- package/samples/wasm/water-level/esm.bound +59 -0
- package/samples/wasm/water-level/no-deadline.bound +65 -0
- package/samples/wasm/water-level/no-memory.bound +65 -0
- package/samples/wasm/water-level/no-missing-limit.bound +64 -0
- package/samples/wasm/water-level/receipts.bound +1 -0
- package/vectors/engine-object.cli.json +548 -0
- package/vectors/native/authenticated-reference-formation.vector.json +53 -0
- package/vectors/native/authenticated-reference.cli.json +157 -0
- package/vectors/native/authenticated-reference.vector.json +440 -0
- package/vectors/reference-port.cli.json +688 -0
- package/vectors/wasm-object.cli.json +452 -0
|
@@ -5,12 +5,53 @@ import { canonical, parse, PROTOCOL, byBytes } from '@lapxo/topos/wire';
|
|
|
5
5
|
import { replaceWhole, landShard, shardLines } from "../land/ledger.js";
|
|
6
6
|
import { fieldOf } from "../fold/claims.js";
|
|
7
7
|
import { readOnly } from "./read-only.js";
|
|
8
|
+
import { readingIdentityBytes, readingReceiptIdentity, responseIdentity, snapshotAccess, placeInputs, withPlaceInputs } from '@lapxo/topos/contract';
|
|
9
|
+
import { withReadingBoundary } from "./reading-context.js";
|
|
10
|
+
const receiptScope = (scope) => [scope, 'identity'].join('/');
|
|
8
11
|
const fields = (line) => { const got = parse(line); if (got.kind !== 'fact')
|
|
9
12
|
throw Error('REFUSE·input projection contains a non-wire line'); return got.value.fields; };
|
|
10
13
|
export const inputLines = (files) => files.map(file => canonical({ scope: file.place, form: 'alphabet', measure: 'text', role: 'writes', value: 'lock', shape: file.place, about: file.text, by: 'host', at: 'policy:handed-input' }));
|
|
14
|
+
/** One portable native receipt carrier shared by file plans and native capsule batches. */
|
|
15
|
+
export function portableReadingReceipt(scope, by, closure, output, hash, modern = true, costs = []) {
|
|
16
|
+
const input = hash(closure), payload = hash(output), rows = output.split('\n').filter(Boolean);
|
|
17
|
+
return [...(modern ? [canonical({ scope: receiptScope(scope), role: 'writes', form: 'alphabet', measure: 'digest', value: readingReceiptIdentity(input, payload, bytes => hash(new TextDecoder().decode(bytes))), by, at: 'place:' + input })] : []), canonical({ scope, role: 'writes', form: 'alphabet', measure: 'observed', value: payload, by, at: 'place:' + payload }), canonical({ scope, role: 'writes', form: 'alphabet', measure: 'bytes', value: input, by, at: 'place:' + input, about: closure }), canonical({ scope, role: 'writes', form: 'interval', measure: 'count', value: `${rows.length}..${rows.length}`, by, at: 'place:' + payload, about: output }), ...costs];
|
|
18
|
+
}
|
|
11
19
|
/** The selected world owns input projection and reading. The host only verifies closures and keeps native count/bytes receipts. */
|
|
12
20
|
export function readingClosures(options) {
|
|
13
21
|
const { root, store, reader, speaker, digest: hash, capsule, region, projection, files } = options, emit = options.emit ?? (() => { });
|
|
22
|
+
options.assertCurrent?.();
|
|
23
|
+
if (options.context)
|
|
24
|
+
readingIdentityBytes(options.context);
|
|
25
|
+
let access = [];
|
|
26
|
+
if (options.current) {
|
|
27
|
+
if (!options.context)
|
|
28
|
+
throw Error('REFUSE·current reading requires a declared identity');
|
|
29
|
+
const { snapshot, kept, current, permission } = options.current;
|
|
30
|
+
if (current.implementation !== options.context.implementation)
|
|
31
|
+
throw Error('REFUSE·current reading implementation differs from its verified context');
|
|
32
|
+
const response = responseIdentity(snapshot, current, hash), allowed = snapshotAccess(snapshot, kept, current, permission, hash);
|
|
33
|
+
if (!allowed.inspect || !allowed.reuse)
|
|
34
|
+
throw Error('REFUSE·current reading is not admitted for reuse');
|
|
35
|
+
access = [['response', response], ['permission', hash(JSON.stringify([permission.inspect, permission.reuse, permission.freshness]))]];
|
|
36
|
+
}
|
|
37
|
+
if (options.execution && (!options.context || JSON.stringify(readingIdentityBytes(options.execution.context)) !== JSON.stringify(readingIdentityBytes(options.context))))
|
|
38
|
+
throw Error('REFUSE·reading execution differs from declared context');
|
|
39
|
+
const projectionFor = (name) => {
|
|
40
|
+
if (!capsule.selectedWorld)
|
|
41
|
+
return undefined;
|
|
42
|
+
const { standing, offer } = capsule.selectedWorld;
|
|
43
|
+
const selectors = standing.parts.some(part => part.scope === `${offer}/place-inputs/${name}` && part.measure === 'reads');
|
|
44
|
+
if (!selectors)
|
|
45
|
+
return undefined;
|
|
46
|
+
if (!options.context || !options.execution || !options.livePlace)
|
|
47
|
+
throw Error('REFUSE·place inputs require authenticated shadow and reading context ' + name);
|
|
48
|
+
return placeInputs(standing, offer, name, options.livePlace(), bytes => hash(new TextDecoder().decode(bytes)));
|
|
49
|
+
};
|
|
50
|
+
const projectedPlace = projectionFor(projection), readingPlace = projectionFor(region);
|
|
51
|
+
const commit = (context, name, place) => place && context ? withPlaceInputs(context, JSON.stringify(['place-inputs', capsule.selectedWorld.offer, name]), place) : context;
|
|
52
|
+
const ask = (identity, requests, context = options.context) => withReadingBoundary(identity, options.execution && context ? { ...options.execution, context } : options.execution, () => capsule.ask(requests, options.readerReference));
|
|
53
|
+
const projectionContext = commit(options.context, projection, projectedPlace);
|
|
54
|
+
const base = projectionContext ? hash(new TextDecoder().decode(readingIdentityBytes(projectionContext))) : '';
|
|
14
55
|
const algorithm = hash('').split(':')[0], handed = new Map(files.map(file => [file.place, file.text]));
|
|
15
56
|
if (handed.size !== files.length)
|
|
16
57
|
throw Error('REFUSE·input projection has duplicate handed coordinates');
|
|
@@ -18,7 +59,7 @@ export function readingClosures(options) {
|
|
|
18
59
|
if (!readFileSync(join(root, file.place)).equals(Buffer.from(file.text)))
|
|
19
60
|
throw Error('REFUSE·input changed ' + file.place); };
|
|
20
61
|
verify();
|
|
21
|
-
const projected =
|
|
62
|
+
const projected = ask(base, [{ ...(projectedPlace ? { place: projectedPlace } : {}), protocol: PROTOCOL, verb: 'render', rootScope: '', region: projection, at: 1, reads: ['**'], lines: inputLines(files), files: [] }], projectionContext)[0];
|
|
22
63
|
if (projected?.kind !== 'fact' || !Array.isArray(projected.lines))
|
|
23
64
|
throw Error('REFUSE·input projection ' + (projected?.why ?? 'no lines'));
|
|
24
65
|
const plans = new Map();
|
|
@@ -56,7 +97,12 @@ export function readingClosures(options) {
|
|
|
56
97
|
let read = 0, opened = 0, executed = 0;
|
|
57
98
|
for (const plan of ordered) {
|
|
58
99
|
actCosts.measure(() => {
|
|
59
|
-
|
|
100
|
+
// Retained evidence does not authorize a current answer.
|
|
101
|
+
options.assertCurrent?.();
|
|
102
|
+
const legacyClosure = [plan.line, canonical({ scope: 'implementation', role: 'writes', form: 'alphabet', measure: 'digest', value: identity, by: 'host', at: 'policy:input-identity' }), ...plan.inputs.map(input => canonical({ scope: input, role: 'writes', form: 'alphabet', measure: 'digest', value: hash(handed.get(input)), by: 'host', at: 'policy:input-identity' })), ...plan.needs.map(need => canonical({ scope: need, role: 'writes', form: 'alphabet', measure: 'digest', value: results.get(need).digest, by: 'host', at: 'policy:input-identity' }))].sort(byBytes).join('\n') + '\n';
|
|
103
|
+
const leafContext = commit(options.context, region, readingPlace);
|
|
104
|
+
const context = leafContext && { ...leafContext, policy: hash(JSON.stringify([leafContext.policy, options.allowsEmpty === true, options.lifetime ?? null, options.readerReference ?? null, capsule.selection ?? null, capsule.lines])), inputs: [...[...leafContext.inputs, ...access].map(([name, digest]) => [JSON.stringify(['context', name]), digest]), [JSON.stringify(['plan']), hash(plan.line)], ...plan.inputs.map(input => [JSON.stringify(['file', input]), hash(handed.get(input))]), ...plan.needs.map(need => [JSON.stringify(['receipt', need]), results.get(need).digest])] };
|
|
105
|
+
const closure = context ? new TextDecoder().decode(readingIdentityBytes(context)) : legacyClosure;
|
|
60
106
|
const input = hash(closure), scope = 'receipts/readings/' + hash(plan.scope).split(':')[1] + '/' + input.split(':')[1], matching = prior.filter(line => fieldOf(line, 'scope') === scope && fieldOf(line, 'measure') === 'bytes' && fieldOf(line, 'value') === input).at(-1);
|
|
61
107
|
const count = matching && prior.filter(line => fieldOf(line, 'scope') === scope && fieldOf(line, 'measure') === 'count' && fieldOf(line, 'by') === by).at(-1);
|
|
62
108
|
const heldOutput = count && load(fieldOf(count, 'at').replace(/^place:/, ''));
|
|
@@ -75,7 +121,7 @@ export function readingClosures(options) {
|
|
|
75
121
|
if (readOnly())
|
|
76
122
|
throw Error('REFUSE·preview reading closure is unpaid ' + plan.scope);
|
|
77
123
|
const started = process.hrtime.bigint();
|
|
78
|
-
const dependencies = plan.needs.flatMap(need => results.get(need).lines), answer =
|
|
124
|
+
const dependencies = plan.needs.flatMap(need => results.get(need).lines), answer = ask(input, [{ ...(readingPlace ? { place: readingPlace } : {}), protocol: PROTOCOL, verb: 'read', rootScope: plan.scope, name: plan.scope, region, at: plan.resolution, reads: ['**'], lines: [...inputLines(plan.inputs.map(place => ({ place, text: handed.get(place) }))), ...dependencies], files: [] }], context)[0];
|
|
79
125
|
if (answer?.kind !== 'fact' || !Array.isArray(answer.claims))
|
|
80
126
|
throw Error('REFUSE·reading ' + plan.scope + ' · ' + (answer?.why ?? 'no claims'));
|
|
81
127
|
lines = answer.claims.map((unknown) => { if (typeof unknown?.scope !== 'string' || typeof unknown?.measure !== 'string' || typeof unknown?.role !== 'string' || !unknown.bound || !['interval', 'enumerated'].includes(unknown.bound.kind))
|
|
@@ -84,6 +130,7 @@ export function readingClosures(options) {
|
|
|
84
130
|
throw Error('REFUSE·reading empty response is not admitted ' + plan.scope);
|
|
85
131
|
executed++;
|
|
86
132
|
verify();
|
|
133
|
+
options.assertCurrent?.();
|
|
87
134
|
payload = saved(lines.length ? [...lines].sort(byBytes).join('\n') + '\n' : '');
|
|
88
135
|
if (saved(closure) !== input)
|
|
89
136
|
throw Error('REFUSE·receipt input closure mismatch');
|
|
@@ -92,22 +139,24 @@ export function readingClosures(options) {
|
|
|
92
139
|
if (plan.resolution === 1)
|
|
93
140
|
opened++;
|
|
94
141
|
}
|
|
95
|
-
|
|
96
|
-
|
|
142
|
+
const receipt = context ? readingReceiptIdentity(input, payload, bytes => hash(new TextDecoder().decode(bytes))) : payload;
|
|
143
|
+
if (context && plan.resolution !== 1)
|
|
144
|
+
out.push(canonical({ scope: receiptScope(scope), role: 'writes', form: 'alphabet', measure: 'digest', value: receipt, by, at: 'place:' + input }));
|
|
145
|
+
results.set(plan.scope, { digest: receipt, lines });
|
|
146
|
+
out.push(canonical({ scope: plan.scope, role: 'writes', form: 'alphabet', measure: 'observed', value: input, by, at: 'place:' + input }),
|
|
147
|
+
// A logical reading name is not a physical input coordinate. Preserve the
|
|
148
|
+
// host-verified handoff beside the result so portable file seals retain
|
|
149
|
+
// every input, including explicitly read foreign files.
|
|
150
|
+
// Input witnesses do not locate the result at each input. Result attribution
|
|
151
|
+
// remains the logical reading's place; two inputs are not two new origins.
|
|
152
|
+
...plan.inputs.map(coordinate => canonical({ scope: coordinate, role: 'writes', form: 'alphabet', measure: 'observed', value: hash(handed.get(coordinate)), by, at: 'witness:' + input })), ...lines);
|
|
97
153
|
if (plan.resolution === 1) {
|
|
98
154
|
read++;
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (bytes === undefined)
|
|
105
|
-
throw Error('REFUSE·receipt missing verified carrier ' + id);
|
|
106
|
-
// The full-resolution native row carries the actual verified UTF-8 bytes.
|
|
107
|
-
// Its semantic count/digest is unchanged; a public copy needs no private CAS.
|
|
108
|
-
return canonical({ ...f, about: bytes });
|
|
109
|
-
});
|
|
110
|
-
out.push(canonical({ scope, role: 'writes', form: 'alphabet', measure: 'observed', value: payload, by, at: 'place:' + payload }), ...native);
|
|
155
|
+
const output = load(payload);
|
|
156
|
+
if (output === undefined)
|
|
157
|
+
throw Error('REFUSE·receipt missing verified carrier ' + payload);
|
|
158
|
+
const costs = shardLines(store, speaker, journal).filter(line => fieldOf(line, 'scope') === scope && fieldOf(line, 'measure') === 's' && fieldOf(line, 'at') === 'place:' + input);
|
|
159
|
+
out.push(...portableReadingReceipt(scope, by, closure, output, hash, !!context, costs));
|
|
111
160
|
}
|
|
112
161
|
});
|
|
113
162
|
}
|
|
@@ -116,7 +165,7 @@ export function readingClosures(options) {
|
|
|
116
165
|
return { lines: out, read, opened, executed };
|
|
117
166
|
}
|
|
118
167
|
/** Verify the native reading carriers before a file seal may reuse them. These are semantic receipts, not file-region seals. */
|
|
119
|
-
export function verifiedReadingReceipts(store, lines, digest) {
|
|
168
|
+
export function verifiedReadingReceipts(store, lines, digest, environment, requireContext = false) {
|
|
120
169
|
const units = lines.filter(line => fieldOf(line, 'scope').startsWith('receipts/readings/') && fieldOf(line, 'measure') === 'bytes' && fieldOf(line, 'role') === 'writes');
|
|
121
170
|
const load = (id, row) => {
|
|
122
171
|
const hex = id.split(':')[1];
|
|
@@ -139,6 +188,37 @@ export function verifiedReadingReceipts(store, lines, digest) {
|
|
|
139
188
|
const count = counts[0], output = load(fieldOf(count, 'at').replace(/^place:/, ''), count);
|
|
140
189
|
if (output === undefined)
|
|
141
190
|
return;
|
|
191
|
+
const inputBytes = load(input, unit);
|
|
192
|
+
if (inputBytes.trimStart().startsWith('[')) {
|
|
193
|
+
let context;
|
|
194
|
+
try {
|
|
195
|
+
context = JSON.parse(inputBytes);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (!Array.isArray(context) || context.length !== 6 || context[0] !== 'reading-context@1')
|
|
201
|
+
return;
|
|
202
|
+
const canonicalContext = { instrument: context[1], implementation: context[2], policy: context[3], inputs: context[4], environment: context[5] };
|
|
203
|
+
try {
|
|
204
|
+
if (new TextDecoder().decode(readingIdentityBytes(canonicalContext)) !== inputBytes)
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (environment) {
|
|
211
|
+
const expected = typeof environment === 'function' ? environment(canonicalContext) : environment;
|
|
212
|
+
if (expected === undefined || JSON.stringify([...expected].sort((a, b) => byBytes(a[0], b[0]))) !== JSON.stringify(context[5]))
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const identity = lines.filter(line => fieldOf(line, 'scope') === receiptScope(scope) && fieldOf(line, 'measure') === 'digest' && fieldOf(line, 'by') === by && fieldOf(line, 'role') === 'writes' && fieldOf(line, 'at') === 'place:' + input);
|
|
216
|
+
const expected = readingReceiptIdentity(input, fieldOf(count, 'at').replace(/^place:/, ''), bytes => digest(new TextDecoder().decode(bytes)));
|
|
217
|
+
if (identity.length !== 1 || fieldOf(identity[0], 'value') !== expected)
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
else if (requireContext || !inputBytes.endsWith('\n') || inputBytes.split('\n').filter(Boolean).some(line => parse(line).kind !== 'fact'))
|
|
221
|
+
return;
|
|
142
222
|
const rows = output.split('\n').filter(Boolean);
|
|
143
223
|
if (fieldOf(count, 'value') !== `${rows.length}..${rows.length}` || rows.some(line => fieldOf(line, 'by') !== by || fieldOf(line, 'at') !== 'place:' + input))
|
|
144
224
|
return;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReadingIdentityContext } from '@lapxo/topos/contract';
|
|
2
|
+
export type ReadingReference = readonly [string, string];
|
|
3
|
+
export interface ReadingExecution {
|
|
4
|
+
readonly context: ReadingIdentityContext;
|
|
5
|
+
readonly env: Readonly<Record<string, string>>;
|
|
6
|
+
readonly cwd: string;
|
|
7
|
+
readonly flags: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
interface Boundary {
|
|
10
|
+
readonly identity: string;
|
|
11
|
+
readonly execution?: ReadingExecution;
|
|
12
|
+
}
|
|
13
|
+
export declare const readingBoundary: () => Boundary | undefined;
|
|
14
|
+
export declare function withReadingBoundary<T>(identity: string, execution: ReadingExecution | undefined, run: () => T): T;
|
|
15
|
+
export declare function readingEnvironment(hash: (text: string) => string, env?: Readonly<Record<string, string | undefined>>, cwd?: string, flags?: readonly string[]): readonly ReadingReference[];
|
|
16
|
+
export declare function readingProfile(lines: readonly string[], field: (line: string, name: string) => string): boolean;
|
|
17
|
+
/** The caller supplies verified implementation and transitive references. This
|
|
18
|
+
* constructor adds the process boundary that the host will actually hand over. */
|
|
19
|
+
export declare function readingExecution(input: {
|
|
20
|
+
instrument: string;
|
|
21
|
+
implementation: string;
|
|
22
|
+
policy: string;
|
|
23
|
+
inputs: readonly ReadingReference[];
|
|
24
|
+
}, hash: (text: string) => string, boundary?: {
|
|
25
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
26
|
+
cwd: string;
|
|
27
|
+
flags: readonly string[];
|
|
28
|
+
}): ReadingExecution;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { release } from 'node:os';
|
|
5
|
+
import { readingIdentityBytes } from '@lapxo/topos/contract';
|
|
6
|
+
const active = new AsyncLocalStorage();
|
|
7
|
+
export const readingBoundary = active.getStore.bind(active);
|
|
8
|
+
export function withReadingBoundary(identity, execution, run) {
|
|
9
|
+
if (execution) {
|
|
10
|
+
const algorithm = identity.split(':')[0];
|
|
11
|
+
const hash = (text) => algorithm + ':' + createHash(algorithm).update(text).digest('hex');
|
|
12
|
+
const expected = readingEnvironment(hash, execution.env, execution.cwd, execution.flags);
|
|
13
|
+
const named = [...execution.context.environment].sort(([a], [b]) => Buffer.compare(Buffer.from(a), Buffer.from(b)));
|
|
14
|
+
if (JSON.stringify([...expected].sort(([a], [b]) => Buffer.compare(Buffer.from(a), Buffer.from(b)))) !== JSON.stringify(named))
|
|
15
|
+
throw Error('REFUSE·reading execution environment differs from its identity');
|
|
16
|
+
}
|
|
17
|
+
return active.run({ identity, execution }, run);
|
|
18
|
+
}
|
|
19
|
+
/** References commit the actual inherited boundary. Values are hashed, never
|
|
20
|
+
* carried as environment secrets. No empty map is substituted for unknown input. */
|
|
21
|
+
// The running runtime is resolved once for this act; mutable input bytes are not
|
|
22
|
+
// stored in this slot. Later processes establish their own runtime reference.
|
|
23
|
+
const runtimes = new Map();
|
|
24
|
+
const runtimeOf = (algorithm) => { const held = runtimes.get(algorithm); if (held)
|
|
25
|
+
return held; const digest = algorithm + ':' + createHash(algorithm).update(readFileSync(process.execPath)).digest('hex'); runtimes.set(algorithm, digest); return digest; };
|
|
26
|
+
export function readingEnvironment(hash, env = process.env, cwd = process.cwd(), flags = process.execArgv) {
|
|
27
|
+
const inherited = Object.entries(env).filter((entry) => entry[1] !== undefined);
|
|
28
|
+
return [
|
|
29
|
+
['variables', hash(JSON.stringify(inherited.sort(([a], [b]) => Buffer.compare(Buffer.from(a), Buffer.from(b)))))],
|
|
30
|
+
['cwd', hash(realpathSync(cwd))], ['flags', hash(JSON.stringify(flags))],
|
|
31
|
+
['runtime', runtimeOf(hash('').split(':')[0])],
|
|
32
|
+
['platform', hash(JSON.stringify([process.platform, process.arch, process.versions, release()]))],
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
export function readingProfile(lines, field) {
|
|
36
|
+
const values = (scope) => lines.filter(line => field(line, 'scope') === scope && field(line, 'role') === 'writes' && field(line, 'value') !== 'withdraw');
|
|
37
|
+
const contexts = values('wire/reading-context'), receipts = values('wire/reading-receipt');
|
|
38
|
+
if (!contexts.length && !receipts.length)
|
|
39
|
+
return false;
|
|
40
|
+
if (contexts.length !== 1 || receipts.length !== 1 || field(contexts[0], 'value') !== 'reading-context@1' || field(receipts[0], 'value') !== 'reading-receipt@1')
|
|
41
|
+
throw Error('REFUSE·reading identity ambiguous or incomplete profiles');
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
/** The caller supplies verified implementation and transitive references. This
|
|
45
|
+
* constructor adds the process boundary that the host will actually hand over. */
|
|
46
|
+
export function readingExecution(input, hash, boundary = { env: process.env, cwd: process.cwd(), flags: process.execArgv }) {
|
|
47
|
+
const env = Object.fromEntries(Object.entries(boundary.env).filter((entry) => entry[1] !== undefined));
|
|
48
|
+
const cwd = boundary.cwd, flags = Object.freeze([...boundary.flags]);
|
|
49
|
+
Object.freeze(env);
|
|
50
|
+
const context = { ...input, environment: readingEnvironment(hash, env, cwd, flags) };
|
|
51
|
+
readingIdentityBytes(context);
|
|
52
|
+
return { context, env, cwd, flags };
|
|
53
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { authorityOf, validateObjectContext } from '@lapxo/topos/wire';
|
|
2
|
+
type ObjectContext = Parameters<typeof validateObjectContext>[1];
|
|
3
|
+
type Signer = Parameters<typeof authorityOf>[1][number];
|
|
4
|
+
export interface ReferenceTrust {
|
|
5
|
+
readonly snapshot: string;
|
|
6
|
+
readonly epoch: number;
|
|
7
|
+
readonly proofScope: string;
|
|
8
|
+
readonly policy: string;
|
|
9
|
+
readonly formImplementation: string;
|
|
10
|
+
readonly signers: readonly Signer[];
|
|
11
|
+
readonly algorithms: readonly string[];
|
|
12
|
+
readonly objects: ObjectContext;
|
|
13
|
+
readonly maxBytes: number;
|
|
14
|
+
readonly maxRecords: number;
|
|
15
|
+
readonly digestBytes: (bytes: Uint8Array) => string | Promise<string>;
|
|
16
|
+
readonly verifyBytes: (bytes: string, key: string, signature: string, algorithms: readonly string[]) => boolean | Promise<boolean>;
|
|
17
|
+
readonly fetchBytes: (source: string, blob: string) => Promise<Uint8Array>;
|
|
18
|
+
}
|
|
19
|
+
/** Prospective authenticated-standing@1 host composition; no effects and no authority from transport. */
|
|
20
|
+
export declare function inspectAuthenticatedReference(fragment: string, trust: ReferenceTrust): Promise<{
|
|
21
|
+
reference: import("./reference-profile.ts").StandingReference;
|
|
22
|
+
records: readonly import("@lapxo/topos/wire").ObjectRecord[];
|
|
23
|
+
semanticIdentity: string;
|
|
24
|
+
authorityContextIdentity: string;
|
|
25
|
+
semanticStanding: string;
|
|
26
|
+
originalRecords: readonly string[];
|
|
27
|
+
}>;
|
|
28
|
+
export { encodeReference, decodeReference } from './reference-profile.ts';
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { parse, canonical, authorityOf, authorityAtEpoch, signedBytes, objectHistory, validateObjectContext } from '@lapxo/topos/wire';
|
|
2
|
+
import { cellInputs } from '@lapxo/topos/cell-inputs';
|
|
3
|
+
import { semanticStandingBytes } from '@lapxo/topos/standing';
|
|
4
|
+
import { decodeReference } from "./reference-profile.js";
|
|
5
|
+
function fail(why) { throw Error('REFUSE·authenticated-reference ' + why); }
|
|
6
|
+
const utf8 = (s) => new TextEncoder().encode(s);
|
|
7
|
+
/** Prospective authenticated-standing@1 host composition; no effects and no authority from transport. */
|
|
8
|
+
export async function inspectAuthenticatedReference(fragment, trust) {
|
|
9
|
+
if (new Set(trust.signers.map(s => s.id)).size !== trust.signers.length)
|
|
10
|
+
fail('duplicate host signer');
|
|
11
|
+
const reference = decodeReference(fragment);
|
|
12
|
+
if (!Number.isSafeInteger(trust.maxBytes) || trust.maxBytes < 1 || !Number.isSafeInteger(trust.maxRecords) || trust.maxRecords < 1)
|
|
13
|
+
fail('resource budget');
|
|
14
|
+
const received = await trust.fetchBytes(reference.source, reference.blob);
|
|
15
|
+
if (received.byteLength > trust.maxBytes)
|
|
16
|
+
fail('bytes budget');
|
|
17
|
+
if (typeof SharedArrayBuffer !== 'undefined' && received.buffer instanceof SharedArrayBuffer)
|
|
18
|
+
fail('shared transport bytes');
|
|
19
|
+
const bytes = new Uint8Array(received);
|
|
20
|
+
if (await trust.digestBytes(bytes) !== reference.blob)
|
|
21
|
+
fail('blob integrity');
|
|
22
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
23
|
+
let packet;
|
|
24
|
+
try {
|
|
25
|
+
packet = JSON.parse(text);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
fail('carrier framing');
|
|
29
|
+
}
|
|
30
|
+
if (!Array.isArray(packet) || packet.length !== 3 || packet[0] !== 'authenticated-standing@1' || !Array.isArray(packet[1]) || packet[1].length < 1 || packet[1].length > trust.maxRecords || packet[1].some((x) => typeof x !== 'string') || typeof packet[2] !== 'string' || JSON.stringify(packet) + '\n' !== text)
|
|
31
|
+
fail('carrier framing');
|
|
32
|
+
const originals = packet[1], proofLine = packet[2];
|
|
33
|
+
const parsed = [...originals, proofLine].map(line => { const p = parse(line); if (p.kind !== 'fact' || canonical(p.value.fields) !== line)
|
|
34
|
+
fail('original canonical'); return p.value.fields; });
|
|
35
|
+
const proof = parsed[parsed.length - 1];
|
|
36
|
+
if (proof.scope !== trust.proofScope || proof.at !== 'witness:' + trust.snapshot || proof.role !== 'writes' || proof.form !== 'alphabet' || proof.measure !== 'digest' || proof.epoch !== String(trust.epoch))
|
|
37
|
+
fail('untrusted snapshot binding');
|
|
38
|
+
if (proof.value !== await trust.digestBytes(utf8(JSON.stringify(['authenticated-standing@1', originals]) + '\n')))
|
|
39
|
+
fail('snapshot commitment');
|
|
40
|
+
// Async crypto port is called on exact native signed bytes; authorityOf consumes only those results.
|
|
41
|
+
const verifiedCrypto = new Set();
|
|
42
|
+
const key = (f, pub) => JSON.stringify([signedBytes(f), pub, f.sig, trust.algorithms]);
|
|
43
|
+
for (const f of parsed)
|
|
44
|
+
for (const s of trust.signers)
|
|
45
|
+
if (s.id === f.by && await trust.verifyBytes(signedBytes(f), s.publicKey, f.sig, trust.algorithms))
|
|
46
|
+
verifiedCrypto.add(key(f, s.publicKey));
|
|
47
|
+
const verifies = (f, pub) => verifiedCrypto.has(key(f, pub));
|
|
48
|
+
const authentic = authorityOf(proof, trust.signers, verifies);
|
|
49
|
+
if (authentic.kind !== 'admitted')
|
|
50
|
+
fail('proof ' + authentic.kind + ' ' + authentic.why);
|
|
51
|
+
const signer = trust.signers.find(s => s.id === proof.by);
|
|
52
|
+
if (!signer)
|
|
53
|
+
fail('proof signer');
|
|
54
|
+
const current = authorityAtEpoch(proof, signer, trust.epoch, trust.proofScope, 'authority');
|
|
55
|
+
if (current.kind !== 'admitted')
|
|
56
|
+
fail('current proof ' + current.kind + ' ' + current.why);
|
|
57
|
+
const verified = new Set();
|
|
58
|
+
for (let i = 0; i < originals.length; i++) {
|
|
59
|
+
const admitted = authorityOf(parsed[i], trust.signers, verifies);
|
|
60
|
+
if (admitted.kind !== 'admitted')
|
|
61
|
+
fail('original ' + admitted.kind + ' ' + admitted.why);
|
|
62
|
+
verified.add(originals[i]);
|
|
63
|
+
}
|
|
64
|
+
const history = objectHistory(originals);
|
|
65
|
+
if (history.kind !== 'fact')
|
|
66
|
+
fail('wire ' + history.why);
|
|
67
|
+
if (history.value.configuration.some(f => f.value === 'withdraw'))
|
|
68
|
+
fail('configuration history requires admitted standing fold');
|
|
69
|
+
const context = { ...trust.objects, admit: (r) => verified.has(canonical(r.fields)) && trust.objects.admit(r) };
|
|
70
|
+
const checked = validateObjectContext(history.value.objects, context);
|
|
71
|
+
if (checked.kind !== 'fact')
|
|
72
|
+
throw Error('REFUSE·' + checked.why);
|
|
73
|
+
const membership = new Set([...cellInputs(checked.value, 'semantic-live@2').values()].flat().map(l => semanticStandingBytes([l])));
|
|
74
|
+
const fields = (line) => { const p = parse(line); if (p.kind !== 'fact')
|
|
75
|
+
fail('original parse'); return p.value.fields; };
|
|
76
|
+
const effective = [...originals].sort((a, b) => Number(fields(a).epoch) - Number(fields(b).epoch));
|
|
77
|
+
const live = effective.filter(line => { const f = fields(line); return f.type === undefined || membership.has(semanticStandingBytes([line])); });
|
|
78
|
+
const semantic = semanticStandingBytes(live), semanticIdentity = await trust.digestBytes(utf8(semantic));
|
|
79
|
+
if (semanticIdentity !== 'sha256:' + reference.digest)
|
|
80
|
+
fail('standing identity');
|
|
81
|
+
const authorityContextIdentity = await trust.digestBytes(utf8(JSON.stringify(['reference-inspection-trust@1', trust.snapshot, trust.epoch, trust.proofScope, trust.policy, trust.formImplementation, trust.signers, trust.algorithms])));
|
|
82
|
+
return { reference, records: checked.value, semanticIdentity, authorityContextIdentity, semanticStanding: semantic, originalRecords: originals };
|
|
83
|
+
}
|
|
84
|
+
export { encodeReference, decodeReference } from "./reference-profile.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function authenticatedReferenceSelected(standing: readonly string[]): boolean;
|
|
2
|
+
export declare function openAuthenticatedReference(root: string, fragment: string): Promise<{
|
|
3
|
+
readings: readonly import("../engine/object.ts").ObjectReading[];
|
|
4
|
+
reference: import("./reference-profile.ts").StandingReference;
|
|
5
|
+
records: readonly import("@lapxo/topos/wire").ObjectRecord[];
|
|
6
|
+
semanticIdentity: string;
|
|
7
|
+
authorityContextIdentity: string;
|
|
8
|
+
semanticStanding: string;
|
|
9
|
+
originalRecords: readonly string[];
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Native ports for authenticated #bound opening; trusted policy belongs to the selected local place. */
|
|
2
|
+
import { sha256 as hash, isSha256 } from "./hash.js";
|
|
3
|
+
import { canonical, parse, transportSelection } from '@lapxo/topos/wire';
|
|
4
|
+
import { semanticStandingBytes } from '@lapxo/topos/standing';
|
|
5
|
+
import { authorityFor, rootSigner } from "../fold/signers.js";
|
|
6
|
+
import { eraOf } from "../fold/digests.js";
|
|
7
|
+
import { lockStanding, lockLines } from "../fold/keys.js";
|
|
8
|
+
import { signedOwnerLines } from "../cli/owner.js";
|
|
9
|
+
import { admittedObjects } from "./objects.js";
|
|
10
|
+
import { storeOf } from "../land/ledger.js";
|
|
11
|
+
import { objectFold } from "../engine/index.js";
|
|
12
|
+
import { toposAt, contentAt } from "./selected-topos.js";
|
|
13
|
+
import { transportBytes } from "./ports/transport.js";
|
|
14
|
+
import { decodeReference } from "./reference-profile.js";
|
|
15
|
+
import { inspectAuthenticatedReference } from "./reference-authenticated.js";
|
|
16
|
+
import { verifyBytes } from "./adapters/keys/ed25519.js";
|
|
17
|
+
export function authenticatedReferenceSelected(standing) {
|
|
18
|
+
const selected = standing.flatMap(line => { const p = parse(line); return p.kind === 'fact' && p.value.fields.scope === 'reference/authentication' ? [p.value.fields] : []; });
|
|
19
|
+
if (!selected.length)
|
|
20
|
+
return false;
|
|
21
|
+
if (selected.length !== 1 || selected[0].value !== 'authenticated-standing@1')
|
|
22
|
+
throw Error('REFUSE·reference conflicting or unknown authentication profile');
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
export async function openAuthenticatedReference(root, fragment) {
|
|
26
|
+
const store = storeOf(root), standing = lockStanding(store), era = eraOf(standing), authority = authorityFor(lockLines(store), rootSigner(root), era.admitted);
|
|
27
|
+
if (!authenticatedReferenceSelected(standing))
|
|
28
|
+
throw Error('REFUSE·reference no admitted authenticated-standing@1 profile');
|
|
29
|
+
const one = (scope) => { const found = standing.flatMap(line => { const p = parse(line); return p.kind === 'fact' && p.value.fields.scope === scope ? [{ line, fields: p.value.fields }] : []; }); if (found.length !== 1 || found[0].fields.role !== 'writes' || found[0].fields.measure !== 'id' || authority.of(found[0].line).kind !== 'admitted')
|
|
30
|
+
throw Error('REFUSE·reference missing, conflicting or unauthenticated ' + scope); return found[0].fields.value; };
|
|
31
|
+
if (one('reference/authentication') !== 'authenticated-standing@1')
|
|
32
|
+
throw Error('REFUSE·reference authentication profile');
|
|
33
|
+
const snapshot = one('reference/snapshot'), epochText = one('reference/epoch'), proofScope = one('reference/proof-scope'), recordsText = one('reference/records');
|
|
34
|
+
if (!isSha256(snapshot) || !/^\d+$/.test(epochText) || !Number.isSafeInteger(Number(epochText)) || !/^\d+$/.test(recordsText) || !Number.isSafeInteger(Number(recordsText)) || Number(recordsText) < 1 || !proofScope)
|
|
35
|
+
throw Error('REFUSE·reference authority snapshot or resource contract');
|
|
36
|
+
const reference = decodeReference(fragment);
|
|
37
|
+
// Provider selection is made from HOST-declared cells/configuration, never carrier-picked code.
|
|
38
|
+
const held = await admittedObjects(root, signedOwnerLines(store), () => { }, true);
|
|
39
|
+
if (!held.records.some(r => r.record === 'cell'))
|
|
40
|
+
throw Error('REFUSE·reference host has no declared object interpretation');
|
|
41
|
+
const context = held.context, policy = held.configuration.map(fields => canonical(fields)), selection = transportSelection(held.configuration, new URL(reference.source).protocol.slice(0, -1));
|
|
42
|
+
if (!selection)
|
|
43
|
+
throw Error('REFUSE·reference no admitted transport policy');
|
|
44
|
+
const implementations = [...new Set(held.records.filter(r => r.record === 'cell').map(r => r.fields.topos))].sort().map(pin => { const selected = toposAt(pin, store), selectors = selected.parts.filter(f => f.scope === 'wire/topos/context-view'); if (selectors.length !== 1)
|
|
45
|
+
throw Error('REFUSE·reference host context selector'); const offers = selected.parts.filter(f => f.kind === 'check' && f.view === selectors[0].value); if (offers.length !== 1)
|
|
46
|
+
throw Error('REFUSE·reference host context offer'); const offer = offers[0], closure = [...new Set((offer.restsOn ?? '').split('|').filter(Boolean))].sort(); for (const digest of closure)
|
|
47
|
+
contentAt(digest, store); return [pin, offer.value, closure]; });
|
|
48
|
+
const result = await inspectAuthenticatedReference(fragment, { snapshot, epoch: Number(epochText), proofScope, policy: hash(semanticStandingBytes(policy)), formImplementation: hash(JSON.stringify(implementations)), signers: authority.admitted, algorithms: era.admitted, objects: context, maxBytes: selection.responseBytes, maxRecords: Number(recordsText), digestBytes: hash, verifyBytes, fetchBytes: (source, blob) => transportBytes(source, blob, policy) });
|
|
49
|
+
return { ...result, readings: objectFold(result.records, context) };
|
|
50
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** reference-profile@1: platform URL parsing, no authority or transport effects. */
|
|
2
|
+
export interface StandingReference {
|
|
3
|
+
readonly class: 'standing';
|
|
4
|
+
readonly algorithm: 'sha256';
|
|
5
|
+
readonly digest: string;
|
|
6
|
+
readonly blob: string;
|
|
7
|
+
readonly source: string;
|
|
8
|
+
}
|
|
9
|
+
interface InspectionPort {
|
|
10
|
+
readonly maxBytes: number;
|
|
11
|
+
readonly readSemanticStanding: (bytes: string) => readonly string[];
|
|
12
|
+
readonly semanticStandingBytes: (lines: readonly string[]) => string;
|
|
13
|
+
readonly digestBytes: (bytes: Uint8Array) => string;
|
|
14
|
+
}
|
|
15
|
+
export declare function encodeReference(reference: StandingReference): string;
|
|
16
|
+
export declare function decodeReference(fragment: string): StandingReference;
|
|
17
|
+
export declare function inspectReferenceBytes(reference: StandingReference, bytes: Uint8Array, { maxBytes, readSemanticStanding, semanticStandingBytes, digestBytes }: InspectionPort): {
|
|
18
|
+
reference: StandingReference;
|
|
19
|
+
lines: readonly string[];
|
|
20
|
+
standing: string;
|
|
21
|
+
authentication: string;
|
|
22
|
+
};
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const hex = /^[0-9a-f]{64}$/;
|
|
2
|
+
const refuse = (cause) => { throw Error(`REFUSE·reference ${cause}`); };
|
|
3
|
+
const keys = ['class', 'algorithm', 'digest', 'blob', 'source'];
|
|
4
|
+
function check(reference) {
|
|
5
|
+
if (Object.keys(reference).length !== keys.length || keys.some(key => !Object.prototype.hasOwnProperty.call(reference, key)))
|
|
6
|
+
refuse('fields');
|
|
7
|
+
if (reference.class !== 'standing')
|
|
8
|
+
refuse('unknown class');
|
|
9
|
+
if (reference.algorithm !== 'sha256')
|
|
10
|
+
refuse('unknown algorithm');
|
|
11
|
+
if (!hex.test(reference.digest) || !/^sha256:[0-9a-f]{64}$/.test(reference.blob))
|
|
12
|
+
refuse('digest');
|
|
13
|
+
let source;
|
|
14
|
+
try {
|
|
15
|
+
source = new URL(reference.source);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return refuse('source URL');
|
|
19
|
+
}
|
|
20
|
+
if (source.username || source.password || source.hash)
|
|
21
|
+
refuse('source credentials or fragment');
|
|
22
|
+
if (source.href !== reference.source)
|
|
23
|
+
refuse('noncanonical source URL');
|
|
24
|
+
return reference;
|
|
25
|
+
}
|
|
26
|
+
export function encodeReference(reference) {
|
|
27
|
+
check(reference);
|
|
28
|
+
return '#bound:1?' + keys.map(key => key + '=' + encodeURIComponent(reference[key])).join('&');
|
|
29
|
+
}
|
|
30
|
+
export function decodeReference(fragment) {
|
|
31
|
+
if (!fragment.startsWith('#bound:1?'))
|
|
32
|
+
refuse('profile');
|
|
33
|
+
const parameters = new URLSearchParams(fragment.slice('#bound:1?'.length));
|
|
34
|
+
if ([...parameters].length !== keys.length || new Set(parameters.keys()).size !== keys.length)
|
|
35
|
+
refuse('duplicate or missing field');
|
|
36
|
+
const reference = check(Object.fromEntries(parameters));
|
|
37
|
+
if (encodeReference(reference) !== fragment)
|
|
38
|
+
refuse('noncanonical encoding');
|
|
39
|
+
return Object.freeze(reference);
|
|
40
|
+
}
|
|
41
|
+
export function inspectReferenceBytes(reference, bytes, { maxBytes, readSemanticStanding, semanticStandingBytes, digestBytes }) {
|
|
42
|
+
if (bytes.byteLength > maxBytes)
|
|
43
|
+
refuse('bytes budget');
|
|
44
|
+
if (digestBytes(bytes) !== reference.blob.slice(7))
|
|
45
|
+
refuse('blob integrity');
|
|
46
|
+
let document;
|
|
47
|
+
try {
|
|
48
|
+
document = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
refuse('document');
|
|
52
|
+
}
|
|
53
|
+
if (document === null || typeof document !== 'object' || document.profile !== 'semantic-standing@1' || typeof document.standing !== 'string' || Object.keys(document).length !== 2)
|
|
54
|
+
refuse('document profile');
|
|
55
|
+
if (new TextDecoder('utf-8', { fatal: true }).decode(bytes) !== JSON.stringify({ profile: document.profile, standing: document.standing }))
|
|
56
|
+
refuse('noncanonical document wrapper');
|
|
57
|
+
const lines = readSemanticStanding(document.standing);
|
|
58
|
+
const semantic = semanticStandingBytes(lines);
|
|
59
|
+
if (digestBytes(new TextEncoder().encode(semantic)) !== reference.digest)
|
|
60
|
+
refuse('standing identity');
|
|
61
|
+
return { reference, lines, standing: semantic, authentication: 'not demonstrated; inspect only' };
|
|
62
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Host composition. Location supplies bytes, never identity or authority. No store writes. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readSemanticStanding, semanticStandingBytes } from '@lapxo/topos/core';
|
|
4
|
+
import { parse, transportSelection } from '@lapxo/topos/wire';
|
|
5
|
+
import { transportBytes } from "./ports/transport.js";
|
|
6
|
+
import { decodeReference, inspectReferenceBytes } from "./reference-profile.js";
|
|
7
|
+
export async function inspectReference(fragment, policy) {
|
|
8
|
+
const reference = decodeReference(fragment), scheme = new URL(reference.source).protocol.slice(0, -1);
|
|
9
|
+
const fields = policy.flatMap(line => { const got = parse(line); return got.kind === 'fact' ? [got.value.fields] : []; });
|
|
10
|
+
const selected = transportSelection(fields, scheme);
|
|
11
|
+
if (selected === undefined)
|
|
12
|
+
throw Error(`REFUSE·transport ${scheme} no admitted content-request/1 policy`);
|
|
13
|
+
let bytes;
|
|
14
|
+
try {
|
|
15
|
+
bytes = await transportBytes(reference.source, reference.blob, policy);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
const cause = error instanceof Error ? error.message : 'transport failed';
|
|
19
|
+
if (cause.startsWith('REFUSE·'))
|
|
20
|
+
throw error;
|
|
21
|
+
throw Error(`REFUSE·transport ${scheme} ${cause}`);
|
|
22
|
+
}
|
|
23
|
+
return inspectReferenceBytes(reference, bytes, { maxBytes: selected.responseBytes, readSemanticStanding, semanticStandingBytes, digestBytes: value => createHash('sha256').update(value).digest('hex') });
|
|
24
|
+
}
|