@lapxo/bound 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.yml +67 -0
- package/.github/workflows/test.yml +35 -0
- package/CITATION.cff +19 -0
- package/TARGET.bound +80 -21
- package/dist/cli/accept.js +103 -71
- package/dist/cli/place.js +3 -2
- package/dist/cli/standing-result.js +3 -5
- package/dist/cli/takes.js +4 -3
- package/dist/cli/verb.js +2 -1
- package/dist/engine/ports.d.ts +6 -1
- package/dist/engine/ports.js +23 -9
- package/dist/fold/claims.d.ts +29 -0
- package/dist/fold/claims.js +2 -1
- package/dist/fold/closed.js +2 -1
- package/dist/fold/digests.js +7 -7
- package/dist/host/admitted-place.js +3 -31
- package/dist/host/call-context.d.ts +20 -0
- package/dist/host/call-context.js +14 -0
- package/dist/host/capsule-process.js +2 -1
- package/dist/host/capsule.js +7 -4
- package/dist/host/commit-result.d.ts +6 -0
- package/dist/host/commit-result.js +5 -0
- package/dist/host/contextual-place.js +9 -14
- package/dist/host/host-controls.js +19 -0
- package/dist/host/in-process-act.d.ts +82 -0
- package/dist/host/in-process-act.js +84 -0
- package/dist/host/in-process-context.d.ts +46 -0
- package/dist/host/in-process-context.js +91 -0
- package/dist/host/in-process-root.d.ts +117 -0
- package/dist/host/in-process-root.js +84 -0
- package/dist/host/in-process.d.ts +41 -0
- package/dist/host/in-process.js +55 -0
- package/dist/host/index.d.ts +15 -0
- package/dist/host/index.js +7 -0
- package/dist/host/native/loader.js +1 -1
- package/dist/host/native/module-closure.d.ts +35 -0
- package/dist/host/native/module-closure.js +34 -3
- package/dist/host/native/module-delivery.d.ts +62 -0
- package/dist/host/native/module-delivery.js +151 -0
- package/dist/host/native/module-syntax.js +3 -1
- package/dist/host/native-receiver.d.ts +19 -0
- package/dist/host/native-receiver.js +25 -0
- package/dist/host/order.js +2 -0
- package/dist/host/place-act-transaction.d.ts +24 -0
- package/dist/host/place-act-transaction.js +78 -0
- package/dist/host/place-clock-selection.d.ts +55 -0
- package/dist/host/place-clock-selection.js +1 -1
- package/dist/host/ports/local-act.d.ts +44 -0
- package/dist/host/ports/local-act.js +13 -2
- package/dist/host/ports/place-act.d.ts +49 -0
- package/dist/host/ports/place-act.js +19 -54
- package/dist/host/ports/publication.d.ts +10 -0
- package/dist/host/root-clock-source.d.ts +86 -0
- package/dist/host/root-clock-source.js +7 -5
- package/dist/host/signature-era.js +12 -0
- package/dist/host/signing.js +1 -1
- package/dist/host/source-content.js +2 -1
- package/dist/host/standing-carrier.js +18 -0
- package/dist/host/standing-projection.js +50 -0
- package/dist/land/act.js +60 -26
- package/dist/observe/run.js +2 -1
- package/dist/observe/runner.js +16 -8
- package/docs/contracts/closure-delivery.md +23 -0
- package/docs/contracts/closure-delivery.publication.json +25 -0
- package/docs/contracts/native-host.md +21 -0
- package/docs/contracts/native-host.publication.json +25 -0
- package/docs/contracts/native-publication.inventory.json +2 -1
- package/docs/contracts/publication.inventory.json +29 -0
- package/docs/guide.md +10 -0
- package/docs/reference.md +39 -0
- package/docs/why.md +109 -0
- package/package.json +43 -2
- package/receipts.bound +10405 -9625
- package/samples/module-delivery/inputs/delivery.json +1 -0
- package/samples/native-host/no.json +10 -0
- package/samples/native-host/yes.json +20 -0
- package/samples/no/module-delivery/delivery.json +1 -0
- package/vectors/native/module-delivery.vector.json +32 -0
- package/vectors/native/native-host.vector.json +62 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
|
+
import { sha256, isSha256 } from "../hash.js";
|
|
3
|
+
import { archiveCoordinate } from "../archive.js";
|
|
4
|
+
import { compareStrings as compare } from "../order.js";
|
|
5
|
+
import { moduleClosures } from "./module-closure.js";
|
|
6
|
+
export { moduleSnapshot } from "./module-closure.js";
|
|
7
|
+
const fail = (why) => { throw Error('REFUSE·closure delivery ' + why); };
|
|
8
|
+
const key = (identity, coordinate) => JSON.stringify([identity, coordinate]);
|
|
9
|
+
const pathOf = (identity, coordinate) => {
|
|
10
|
+
if (!isSha256(identity))
|
|
11
|
+
return fail('snapshot identity is not SHA-256');
|
|
12
|
+
return 'modules/' + identity.slice(7) + '/' + coordinate;
|
|
13
|
+
};
|
|
14
|
+
const urlOf = (path) => './' + path.split('/').map(encodeURIComponent).join('/');
|
|
15
|
+
const resourceLimits = (limits) => {
|
|
16
|
+
if (!limits || !Number.isSafeInteger(limits.members) || limits.members < 1 || !Number.isSafeInteger(limits.bytes) || limits.bytes < 1 || !Number.isSafeInteger(limits.responseBytes) || limits.responseBytes < 1)
|
|
17
|
+
fail('missing resources');
|
|
18
|
+
if (limits.signal?.aborted)
|
|
19
|
+
fail('cancelled');
|
|
20
|
+
};
|
|
21
|
+
/** The host selects already authenticated snapshots and resolution, never the
|
|
22
|
+
* offered program. This prepares unchanged ESM files, not a JS sandbox, signer,
|
|
23
|
+
* browser host or symbol-pruning bundler. Existing native/Acorn routes remain.
|
|
24
|
+
*/
|
|
25
|
+
export function deliverModuleClosure(needs, entries, library, limits, notices = []) {
|
|
26
|
+
resourceLimits(limits);
|
|
27
|
+
if (!Array.isArray(notices) || notices.length >= limits.members)
|
|
28
|
+
fail('notice resources');
|
|
29
|
+
let noticeBytes = 0;
|
|
30
|
+
const noticePaths = new Set();
|
|
31
|
+
const retainedNotices = notices.map(n => {
|
|
32
|
+
if (!n || typeof n.coordinate !== 'string' || archiveCoordinate(n.coordinate) !== n.coordinate || typeof n.source !== 'string' || !isSha256(n.digest) || sha256(n.source) !== n.digest || (noticeBytes += Buffer.byteLength(n.source)) >= limits.bytes)
|
|
33
|
+
fail('notice bytes/resources');
|
|
34
|
+
const path = 'notices/' + pathOf(n.snapshot, n.coordinate).slice('modules/'.length);
|
|
35
|
+
if (noticePaths.has(path))
|
|
36
|
+
fail('duplicate notice');
|
|
37
|
+
noticePaths.add(path);
|
|
38
|
+
return { snapshot: n.snapshot, coordinate: n.coordinate, digest: n.digest, path };
|
|
39
|
+
}).sort((a, b) => compare(a.path, b.path));
|
|
40
|
+
if (!Array.isArray(needs) || !needs.length || needs.length > limits.members || needs.some(n => typeof n !== 'string' || !n || Buffer.byteLength(n) > limits.bytes))
|
|
41
|
+
fail('missing/oversized needs');
|
|
42
|
+
const requested = [...new Set(needs)].sort(compare), snapshots = new Map(), bindings = new Map();
|
|
43
|
+
const hold = (one) => {
|
|
44
|
+
if (!one || !isSha256(one.identity))
|
|
45
|
+
return fail('snapshot identity is not SHA-256');
|
|
46
|
+
const old = snapshots.get(one.identity);
|
|
47
|
+
// Different entry points may share one immutable snapshot, not different members.
|
|
48
|
+
if (old && old.members !== one.members)
|
|
49
|
+
fail('ambiguous snapshot members');
|
|
50
|
+
snapshots.set(one.identity, one);
|
|
51
|
+
return one;
|
|
52
|
+
};
|
|
53
|
+
const roots = requested.map(name => { const one = entries.get(name); if (!one)
|
|
54
|
+
return fail('unknown need ' + name); return hold(one); });
|
|
55
|
+
const reached = moduleClosures(roots, (specifier, parent) => {
|
|
56
|
+
const found = library(specifier, parent);
|
|
57
|
+
if (!found)
|
|
58
|
+
return;
|
|
59
|
+
const one = hold(found), at = key(parent.identity, specifier), binding = { parent: parent.identity, specifier, snapshot: one.identity, entry: one.entry }, old = bindings.get(at);
|
|
60
|
+
if (old && (old.snapshot !== one.identity || old.entry !== one.entry))
|
|
61
|
+
fail('ambiguous library resolution');
|
|
62
|
+
if (!old && bindings.size >= limits.members)
|
|
63
|
+
fail('library resources');
|
|
64
|
+
bindings.set(at, binding);
|
|
65
|
+
return one;
|
|
66
|
+
}, { ...limits, members: limits.members - notices.length, bytes: limits.bytes - noticeBytes, portableESM: true });
|
|
67
|
+
if (notices.some(n => !reached.some(r => r.snapshot === n.snapshot)))
|
|
68
|
+
fail('notice outside reached snapshots');
|
|
69
|
+
const modules = [...reached].sort((a, b) => compare(key(a.snapshot, a.coordinate), key(b.snapshot, b.coordinate))).map(r => ({ ...r, path: pathOf(r.snapshot, r.coordinate) }));
|
|
70
|
+
const files = modules.map(r => {
|
|
71
|
+
const source = snapshots.get(r.snapshot)?.members.get(r.coordinate)?.source;
|
|
72
|
+
if (source === undefined || sha256(source) !== r.digest)
|
|
73
|
+
fail('snapshot changed during delivery');
|
|
74
|
+
return { path: r.path, source: source };
|
|
75
|
+
});
|
|
76
|
+
for (const n of retainedNotices)
|
|
77
|
+
files.push({ path: n.path, source: notices.find(one => one.snapshot === n.snapshot && one.coordinate === n.coordinate).source });
|
|
78
|
+
const manifest = { format: 'esm-file-closure/1', needs: requested.map((name, i) => ({ name, snapshot: roots[i].identity, entry: posix.normalize(roots[i].entry) })), modules, libraries: [...bindings.values()].sort((a, b) => compare(key(a.parent, a.specifier), key(b.parent, b.specifier))), notices: retainedNotices };
|
|
79
|
+
const closure = sha256(JSON.stringify(manifest));
|
|
80
|
+
const bytes = Buffer.from(JSON.stringify({ manifest, files }), 'utf8');
|
|
81
|
+
if (bytes.length > limits.responseBytes)
|
|
82
|
+
fail('response resources');
|
|
83
|
+
if (limits.signal?.aborted)
|
|
84
|
+
fail('cancelled');
|
|
85
|
+
const imports = Object.fromEntries(manifest.needs.map(n => [n.name, urlOf(pathOf(n.snapshot, n.entry))]));
|
|
86
|
+
const scopes = {};
|
|
87
|
+
for (const b of manifest.libraries) {
|
|
88
|
+
const scope = './modules/' + b.parent.slice(7) + '/';
|
|
89
|
+
const values = scopes[scope] ?? (scopes[scope] = Object.create(null));
|
|
90
|
+
values[b.specifier] = urlOf(pathOf(b.snapshot, posix.normalize(b.entry)));
|
|
91
|
+
}
|
|
92
|
+
return { closure, digest: sha256(bytes), bytes: new Uint8Array(bytes), manifest, files, importMap: { imports, scopes } };
|
|
93
|
+
}
|
|
94
|
+
/** Verify exact pinned delivery bytes and run the same pre-execution closure
|
|
95
|
+
* verifier again over the delivered set. No callbacks, install paths or verifier
|
|
96
|
+
* chosen by the payload. The returned import map is derived, never trusted input.
|
|
97
|
+
*/
|
|
98
|
+
export function verifyModuleDelivery(bytes, expected, limits) {
|
|
99
|
+
resourceLimits(limits);
|
|
100
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength > limits.responseBytes || !isSha256(expected) || sha256(bytes) !== expected)
|
|
101
|
+
fail('bundle bytes differ or exceed resources');
|
|
102
|
+
let value;
|
|
103
|
+
try {
|
|
104
|
+
value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return fail('invalid JSON bytes');
|
|
108
|
+
}
|
|
109
|
+
const manifest = value?.manifest;
|
|
110
|
+
if (manifest?.format !== 'esm-file-closure/1' || !Array.isArray(manifest.needs) || !Array.isArray(manifest.modules) || !Array.isArray(manifest.libraries) || !Array.isArray(manifest.notices) || !Array.isArray(value.files) || manifest.modules.length + manifest.notices.length > limits.members || value.files.length !== manifest.modules.length + manifest.notices.length || manifest.needs.length > limits.members || manifest.libraries.length > limits.members)
|
|
111
|
+
fail('invalid manifest resources');
|
|
112
|
+
const snapshots = new Map(), files = new Map();
|
|
113
|
+
let size = 0;
|
|
114
|
+
for (const file of value.files) {
|
|
115
|
+
if (typeof file?.path !== 'string' || typeof file.source !== 'string' || files.has(file.path) || (size += Buffer.byteLength(file.source)) > limits.bytes)
|
|
116
|
+
fail('invalid/duplicate file resources');
|
|
117
|
+
files.set(file.path, file.source);
|
|
118
|
+
}
|
|
119
|
+
for (const m of manifest.modules) {
|
|
120
|
+
if (!m || typeof m.coordinate !== 'string' || typeof m.path !== 'string' || m.path !== pathOf(m.snapshot, m.coordinate) || !isSha256(m.digest))
|
|
121
|
+
fail('invalid module');
|
|
122
|
+
const source = files.get(m.path);
|
|
123
|
+
if (source === undefined || sha256(source) !== m.digest)
|
|
124
|
+
fail('module bytes differ');
|
|
125
|
+
const members = snapshots.get(m.snapshot) ?? new Map();
|
|
126
|
+
if (members.has(m.coordinate))
|
|
127
|
+
fail('duplicate module');
|
|
128
|
+
members.set(m.coordinate, { source, digest: m.digest });
|
|
129
|
+
snapshots.set(m.snapshot, members);
|
|
130
|
+
}
|
|
131
|
+
const snapshot = (identity, entry) => { const members = snapshots.get(identity); if (!members || typeof entry !== 'string')
|
|
132
|
+
return fail('unknown snapshot/entry'); return { identity, entry, members }; };
|
|
133
|
+
const entries = new Map();
|
|
134
|
+
for (const n of manifest.needs) {
|
|
135
|
+
if (typeof n?.name !== 'string' || entries.has(n.name))
|
|
136
|
+
fail('invalid/duplicate need');
|
|
137
|
+
entries.set(n.name, snapshot(n.snapshot, n.entry));
|
|
138
|
+
}
|
|
139
|
+
const bindings = new Map();
|
|
140
|
+
for (const b of manifest.libraries) {
|
|
141
|
+
if (typeof b?.parent !== 'string' || typeof b.specifier !== 'string' || bindings.has(key(b.parent, b.specifier)))
|
|
142
|
+
fail('invalid/duplicate library');
|
|
143
|
+
bindings.set(key(b.parent, b.specifier), snapshot(b.snapshot, b.entry));
|
|
144
|
+
}
|
|
145
|
+
const notices = manifest.notices.map(n => { if (!n || typeof n.path !== 'string' || !files.has(n.path))
|
|
146
|
+
return fail('missing notice'); return { snapshot: n.snapshot, coordinate: n.coordinate, digest: n.digest, source: files.get(n.path) }; });
|
|
147
|
+
const prepared = deliverModuleClosure([...entries.keys()], entries, (specifier, parent) => bindings.get(key(parent.identity, specifier)), limits, notices);
|
|
148
|
+
if (prepared.digest !== expected)
|
|
149
|
+
fail('noncanonical, extra or unresolved delivery members');
|
|
150
|
+
return prepared;
|
|
151
|
+
}
|
|
@@ -7,7 +7,7 @@ const require = nodeModule.createRequire(import.meta.url);
|
|
|
7
7
|
const { parse } = require('acorn');
|
|
8
8
|
const stripTypeScriptTypes = nodeModule.stripTypeScriptTypes;
|
|
9
9
|
/** Syntax inventory is not an execution-closure certificate: unresolved expressions stay explicit. */
|
|
10
|
-
export function moduleImportSyntax(source, coordinate) {
|
|
10
|
+
export function moduleImportSyntax(source, coordinate, options) {
|
|
11
11
|
const code = extname(coordinate) === '.ts' ? stripTypeScriptTypes(source, { mode: 'transform' }) : source;
|
|
12
12
|
let tree;
|
|
13
13
|
try {
|
|
@@ -48,6 +48,8 @@ export function moduleImportSyntax(source, coordinate) {
|
|
|
48
48
|
if (n['type'] === 'CallExpression') {
|
|
49
49
|
const callee = n['callee'];
|
|
50
50
|
if (callee?.['type'] === 'Identifier' && callee['name'] === 'require') {
|
|
51
|
+
if (options?.esmOnly)
|
|
52
|
+
throw Error('REFUSE·module closure CommonJS load in ESM delivery ' + coordinate);
|
|
51
53
|
specifier = literal(n['arguments'][0]);
|
|
52
54
|
if (specifier === undefined)
|
|
53
55
|
unresolved.add('require expression at ' + String(n['start']));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type HostInstrumentRuntime } from './call-context.ts';
|
|
2
|
+
export interface NativeHostInstrument {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly runtime: HostInstrumentRuntime;
|
|
5
|
+
}
|
|
6
|
+
/** Explicit installed/source ESM selection, never consumer argv or packet data.
|
|
7
|
+
* Identity and public configuration do not certify implementation correctness. */
|
|
8
|
+
export declare function nativeHostInstrument(root: string, entry: string): NativeHostInstrument;
|
|
9
|
+
/** Bind existing native authority construction to an explicit instrument for
|
|
10
|
+
* every later current/historical observation, not only the constructor call. */
|
|
11
|
+
export declare function nativePlaceActReceiver(root: string, instrument: NativeHostInstrument): {
|
|
12
|
+
selection: import("./place-clock-selection.ts").HostClockSelection;
|
|
13
|
+
trust: {
|
|
14
|
+
current: () => import("./ports/place-act.ts").PlaceActSnapshot;
|
|
15
|
+
historical: (encoded: string) => import("@lapxo/topos/wire").TrustedClockContract;
|
|
16
|
+
contract: import("@lapxo/topos/wire").TrustedClockContract;
|
|
17
|
+
initialHeads: readonly import("./ports/place-act.ts").PlaceActHead[];
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, extname, isAbsolute, join, relative } from 'node:path';
|
|
3
|
+
import { publicLock } from '@lapxo/topos/wire';
|
|
4
|
+
import { foldClaims, wireLinesOf } from "../fold/claims.js";
|
|
5
|
+
import { declaredRootClockSource } from "./root-clock-source.js";
|
|
6
|
+
import { withHostCall } from "./call-context.js";
|
|
7
|
+
import { BoundRefusal } from "./refusals.js";
|
|
8
|
+
/** Explicit installed/source ESM selection, never consumer argv or packet data.
|
|
9
|
+
* Identity and public configuration do not certify implementation correctness. */
|
|
10
|
+
export function nativeHostInstrument(root, entry) {
|
|
11
|
+
const at = realpathSync(root), file = realpathSync(entry), inside = relative(at, file), extension = extname(file);
|
|
12
|
+
if (!isAbsolute(root) || !isAbsolute(entry) || inside.startsWith('..') || isAbsolute(inside) || !['.ts', '.js'].includes(extension))
|
|
13
|
+
throw new BoundRefusal('host/context', 'REFUSE·host/context');
|
|
14
|
+
const published = publicLock(wireLinesOf(readFileSync(join(at, 'TARGET.bound'), 'utf8')));
|
|
15
|
+
if (!published)
|
|
16
|
+
throw new BoundRefusal('host/context', 'REFUSE·host requires a public instrument lock');
|
|
17
|
+
return { name: basename(at), runtime: { root: at, entry: file, directory: dirname(dirname(file)), extension: extension, lock: foldClaims(published).standing } };
|
|
18
|
+
}
|
|
19
|
+
/** Bind existing native authority construction to an explicit instrument for
|
|
20
|
+
* every later current/historical observation, not only the constructor call. */
|
|
21
|
+
export function nativePlaceActReceiver(root, instrument) {
|
|
22
|
+
const call = { instrumentName: instrument.name, runtime: structuredClone(instrument.runtime), output: () => { } };
|
|
23
|
+
const selected = withHostCall(call, () => declaredRootClockSource(root)), original = selected.trust;
|
|
24
|
+
return { selection: selected.selection, trust: { ...original, current: () => withHostCall(call, () => original.current()), historical: (encoded) => withHostCall(call, () => original.historical(encoded)) } };
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type PlaceReceipt } from '@lapxo/topos/core';
|
|
2
|
+
import type { PlaceActTrust, PlaceActBudget, PlaceActBundle, PlaceActHead } from './ports/place-act.ts';
|
|
3
|
+
export { ActCommitUnknown as PlaceActCommitUnknown } from './commit-result.ts';
|
|
4
|
+
/** Host-selected persistence, never supplied by offered content. exclusive must
|
|
5
|
+
* share the CLI's region/authority/projection protocol. publish is synchronous
|
|
6
|
+
* with exactly one atomic visibility point; a thrown reply may follow commit. */
|
|
7
|
+
export interface PlaceActPersistence {
|
|
8
|
+
readonly exclusive: <T>(regions: readonly string[], run: () => Promise<T>) => Promise<T>;
|
|
9
|
+
readonly result: (identity: string, trust: PlaceActTrust, limits: PlaceActBudget) => PlaceActBundle | undefined;
|
|
10
|
+
readonly inventory: (limits: PlaceActBudget) => {
|
|
11
|
+
readonly names: readonly string[];
|
|
12
|
+
readonly bytes: number;
|
|
13
|
+
};
|
|
14
|
+
readonly heads: (trust: PlaceActTrust, limits: PlaceActBudget) => readonly PlaceActHead[];
|
|
15
|
+
readonly publish: (identity: string, files: Readonly<Record<string, string>>) => void;
|
|
16
|
+
}
|
|
17
|
+
export type PlaceActReceiptSource = readonly PlaceReceipt[] | (() => Promise<readonly PlaceReceipt[]>);
|
|
18
|
+
/** Shared transaction, including historical replay, current authorization and
|
|
19
|
+
* atomic publication. Signing may await; current Root and receiver heads are
|
|
20
|
+
* checked again afterwards while the same exclusion remains held. */
|
|
21
|
+
export declare function transactPlaceAct(encoded: string, source: PlaceActReceiptSource, trust: PlaceActTrust, limits: PlaceActBudget, persistence: PlaceActPersistence, signal?: AbortSignal): Promise<PlaceActBundle & {
|
|
22
|
+
readonly kind: 'landed' | 'once';
|
|
23
|
+
readonly identity: string;
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readPlaceActContext, verifyPlaceActContext, placeActReceiptsOf } from '@lapxo/topos/core';
|
|
2
|
+
import { requireEffects } from "./read-only.js";
|
|
3
|
+
import { BoundRefusal } from "./refusals.js";
|
|
4
|
+
import { ActCommitUnknown } from "./commit-result.js";
|
|
5
|
+
import { assertHostNotCancelled as cancelled, receiverClock, placeRegionOf } from "./host-controls.js";
|
|
6
|
+
export { ActCommitUnknown as PlaceActCommitUnknown } from "./commit-result.js";
|
|
7
|
+
/** The visibility point may have happened. Only an authenticated result query
|
|
8
|
+
* or replay can resolve this; the error never asserts rollback. */
|
|
9
|
+
const fail = (why) => { throw new BoundRefusal('host/act', 'REFUSE·place act ' + why); };
|
|
10
|
+
const rootBytes = (r) => JSON.stringify([r.namespace, r.snapshot, r.epoch, r.prefix, r.head, r.policies]);
|
|
11
|
+
/** Shared transaction, including historical replay, current authorization and
|
|
12
|
+
* atomic publication. Signing may await; current Root and receiver heads are
|
|
13
|
+
* checked again afterwards while the same exclusion remains held. */
|
|
14
|
+
export async function transactPlaceAct(encoded, source, trust, limits, persistence, signal) {
|
|
15
|
+
requireEffects('place act commitment');
|
|
16
|
+
cancelled(signal);
|
|
17
|
+
if (!limits || !Number.isSafeInteger(limits.acts) || limits.acts < 1 || !Number.isSafeInteger(limits.bytes) || limits.bytes < 1)
|
|
18
|
+
fail('missing storage budget');
|
|
19
|
+
const decoded = readPlaceActContext(encoded, trust.contract.budget), id = trust.contract.digest(encoded);
|
|
20
|
+
const regions = decoded.places.map(p => placeRegionOf(p.place));
|
|
21
|
+
return persistence.exclusive(regions, async () => {
|
|
22
|
+
cancelled(signal);
|
|
23
|
+
const prior = persistence.result(id, trust, limits);
|
|
24
|
+
if (prior) {
|
|
25
|
+
if (prior.context !== encoded)
|
|
26
|
+
fail('replay bytes differ');
|
|
27
|
+
// A signing callback is never invoked for a historically authenticated replay.
|
|
28
|
+
if (typeof source !== 'function')
|
|
29
|
+
placeActReceiptsOf(encoded, source, trust.historical(encoded));
|
|
30
|
+
return { ...prior, identity: id, kind: 'once' };
|
|
31
|
+
}
|
|
32
|
+
const verified = verifyPlaceActContext(encoded, trust.contract);
|
|
33
|
+
const originals = decoded.places.map(p => p.records);
|
|
34
|
+
const capacity = (size) => {
|
|
35
|
+
const inventory = persistence.inventory(limits);
|
|
36
|
+
if (inventory.bytes + size > limits.bytes)
|
|
37
|
+
fail('publication byte budget');
|
|
38
|
+
if (inventory.names.length >= limits.acts)
|
|
39
|
+
fail('publication act budget');
|
|
40
|
+
};
|
|
41
|
+
const sizeOf = (files) => Object.values(files).reduce((n, v) => n + new TextEncoder().encode(v).length, 0);
|
|
42
|
+
capacity(sizeOf({ 'context.json': encoded, 'originals.json': JSON.stringify(originals) + '\n' }));
|
|
43
|
+
const assertCurrent = () => {
|
|
44
|
+
const current = trust.current();
|
|
45
|
+
if (rootBytes(current.root) !== rootBytes(verified.dependencies))
|
|
46
|
+
fail('authority changed before publication');
|
|
47
|
+
if (new Set(current.heads.map(h => h.place)).size !== current.heads.length)
|
|
48
|
+
fail('ambiguous receiver heads');
|
|
49
|
+
const stored = persistence.heads(trust, limits);
|
|
50
|
+
for (const p of decoded.places) {
|
|
51
|
+
const actual = stored.find(h => h.place === p.place), selected = current.heads.find(h => h.place === p.place);
|
|
52
|
+
if (!actual || !selected || actual.identity !== selected.identity || actual.epoch !== selected.epoch)
|
|
53
|
+
fail('receiver snapshot is not committed head ' + p.place);
|
|
54
|
+
if (selected.identity !== p.previous)
|
|
55
|
+
fail('receiver head moved ' + p.place);
|
|
56
|
+
const next = receiverClock(selected.epoch, fail) + 1;
|
|
57
|
+
if (!Number.isSafeInteger(next) || receiverClock(p.epoch, fail) !== next)
|
|
58
|
+
fail('receiver epoch not next ' + p.place);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
assertCurrent();
|
|
62
|
+
cancelled(signal);
|
|
63
|
+
const receipts = structuredClone(typeof source === 'function' ? await source() : source);
|
|
64
|
+
cancelled(signal);
|
|
65
|
+
placeActReceiptsOf(encoded, receipts, trust.contract);
|
|
66
|
+
const files = { 'context.json': encoded, 'receipts.json': JSON.stringify(receipts) + '\n', 'originals.json': JSON.stringify(originals) + '\n' };
|
|
67
|
+
capacity(sizeOf(files));
|
|
68
|
+
assertCurrent();
|
|
69
|
+
cancelled(signal);
|
|
70
|
+
try {
|
|
71
|
+
persistence.publish(id, files);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw new ActCommitUnknown(id, error);
|
|
75
|
+
}
|
|
76
|
+
return { context: encoded, receipts, originals, identity: id, kind: 'landed' };
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { PLACE_ACT_CONTEXT, type ClockBudget, type TrustedClockContract } from '@lapxo/topos/core';
|
|
2
|
+
import { type Signer } from '@lapxo/topos/wire';
|
|
3
|
+
import { type PlaceActTrust, type PlaceActBudget, type PlaceActHead } from './ports/place-act.ts';
|
|
4
|
+
export interface HostClockSelection {
|
|
5
|
+
readonly profile: typeof PLACE_ACT_CONTEXT;
|
|
6
|
+
readonly namespace: string;
|
|
7
|
+
readonly regionFamily: string;
|
|
8
|
+
readonly receiptSigner: string;
|
|
9
|
+
readonly receiptScope: string;
|
|
10
|
+
readonly digestAlgorithm: 'sha256';
|
|
11
|
+
readonly budget: ClockBudget;
|
|
12
|
+
readonly storageBudget: PlaceActBudget;
|
|
13
|
+
readonly initial: readonly {
|
|
14
|
+
place: string;
|
|
15
|
+
receiver: {
|
|
16
|
+
identity: string;
|
|
17
|
+
epoch: string;
|
|
18
|
+
};
|
|
19
|
+
origin: {
|
|
20
|
+
prefix: string;
|
|
21
|
+
epoch: string;
|
|
22
|
+
};
|
|
23
|
+
}[];
|
|
24
|
+
}
|
|
25
|
+
export interface HostClockInput {
|
|
26
|
+
readonly store: string;
|
|
27
|
+
readonly selector: string;
|
|
28
|
+
readonly profileSelector: string;
|
|
29
|
+
readonly history: readonly string[];
|
|
30
|
+
readonly anchor: Signer;
|
|
31
|
+
readonly algorithms: readonly string[];
|
|
32
|
+
readonly legacyOriginals: readonly string[];
|
|
33
|
+
/** Actual protected Root authority cursor: an authenticated signed Root config
|
|
34
|
+
* checkpoint, never a sibling receipt/evidence max epoch. */
|
|
35
|
+
readonly authorityCheckpoint: string;
|
|
36
|
+
/** Fixed host historical predecessor proof, never taken from a packet. */
|
|
37
|
+
readonly authenticatedReceiverHeads?: readonly PlaceActHead[];
|
|
38
|
+
readonly readCurrent: () => Pick<HostClockInput, 'history' | 'anchor' | 'algorithms' | 'legacyOriginals' | 'authorityCheckpoint'>;
|
|
39
|
+
readonly resources: {
|
|
40
|
+
bytes: number;
|
|
41
|
+
lines: number;
|
|
42
|
+
places: number;
|
|
43
|
+
};
|
|
44
|
+
readonly inherited: {
|
|
45
|
+
clock: ClockBudget;
|
|
46
|
+
storage: PlaceActBudget;
|
|
47
|
+
};
|
|
48
|
+
readonly historical: (encoded: string) => TrustedClockContract;
|
|
49
|
+
}
|
|
50
|
+
/** Root chooses selector and anchors/resources from its authenticated standing.
|
|
51
|
+
* Selection must be ROOT signed; a world's signature grants no host capability. */
|
|
52
|
+
export declare function hostClockSelection(input: HostClockInput): {
|
|
53
|
+
selection: HostClockSelection;
|
|
54
|
+
trust: PlaceActTrust;
|
|
55
|
+
};
|
|
@@ -4,7 +4,7 @@ import { createHash } from 'node:crypto';
|
|
|
4
4
|
import { PLACE_ACT_CONTEXT } from '@lapxo/topos/core';
|
|
5
5
|
import { canonical, signedBytes, wireAt } from '@lapxo/topos/wire';
|
|
6
6
|
import { foldClaims, isConfig, fieldOf, parsedLine as parse } from "../fold/claims.js";
|
|
7
|
-
import { eraOf } from "
|
|
7
|
+
import { signatureEraOf as eraOf } from "./signature-era.js";
|
|
8
8
|
import { authorityFor } from "../fold/signers.js";
|
|
9
9
|
import { verifyBytes } from "./adapters/keys/ed25519.js";
|
|
10
10
|
import { verifiesOwnerLine } from "../land/sign.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { PublicationStorage } from './publication.ts';
|
|
2
|
+
/** Prepared and verified by the selected Topos admission/result contract. */
|
|
3
|
+
export interface LocalActBundle {
|
|
4
|
+
readonly records: string;
|
|
5
|
+
readonly receipt: string;
|
|
6
|
+
readonly placements?: readonly {
|
|
7
|
+
place: string;
|
|
8
|
+
records: string;
|
|
9
|
+
}[];
|
|
10
|
+
}
|
|
11
|
+
export type LocalActVerification = (identity: string, bundle: LocalActBundle) => boolean;
|
|
12
|
+
export declare const actLines: (bytes: string) => readonly string[];
|
|
13
|
+
/** Journal integrity is not authority. Signature/coverage checks remain the
|
|
14
|
+
* same admission boundary; the hash algorithm here is the stored seal's named
|
|
15
|
+
* algorithm, not a new selection of the place's wire.
|
|
16
|
+
*/
|
|
17
|
+
export declare function intactLocalAct(identity: string, bundle: LocalActBundle): boolean;
|
|
18
|
+
/** Internal integrity key only, not an act identity or a wire profile. */
|
|
19
|
+
export declare function committedInventoryFingerprint(rows: readonly {
|
|
20
|
+
identity: string;
|
|
21
|
+
bundle: LocalActBundle;
|
|
22
|
+
}[]): string;
|
|
23
|
+
/** Committed segments are part of this ledger, not a second authority source. */
|
|
24
|
+
export declare function committedLocalActs(store: string): readonly {
|
|
25
|
+
identity: string;
|
|
26
|
+
bundle: LocalActBundle;
|
|
27
|
+
}[];
|
|
28
|
+
/** Fixed host observation only: one synchronous, coherent committed inventory.
|
|
29
|
+
* It supplies no authority, verifier or async lifetime. Each outer invocation
|
|
30
|
+
* rereads exact bodies before entering; current authorization still runs inside.
|
|
31
|
+
*/
|
|
32
|
+
export declare function withCommittedLocalActObservation<T>(store: string, body: () => T): T;
|
|
33
|
+
/** A public result is available only as a verified committed pair.
|
|
34
|
+
* No writer-tail guess, newest-result selection or implicit authority.
|
|
35
|
+
*/
|
|
36
|
+
export declare function localActResult(store: string, identity: string, verify: LocalActVerification): LocalActBundle | undefined;
|
|
37
|
+
/** One publication point for records and their native result. Re-delivery
|
|
38
|
+
* reads the original pair without preparation, signing or a new local epoch.
|
|
39
|
+
* The caller must also integrate this boundary into admission-history readers;
|
|
40
|
+
* this storage port alone does not activate a grammar or land authority.
|
|
41
|
+
*/
|
|
42
|
+
export declare function commitLocalAct(store: string, identity: string, regions: readonly string[], prepare: () => Promise<LocalActBundle>, verify: LocalActVerification, storage?: PublicationStorage, signal?: AbortSignal): Promise<LocalActBundle & {
|
|
43
|
+
readonly kind: 'landed' | 'once';
|
|
44
|
+
}>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { hashText } from "../hash.js";
|
|
2
|
+
import { ActCommitUnknown } from "../commit-result.js";
|
|
3
|
+
import { assertHostNotCancelled } from "../host-controls.js";
|
|
2
4
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
5
|
import { join } from 'node:path';
|
|
4
6
|
import { createHash } from 'node:crypto';
|
|
@@ -154,19 +156,28 @@ export function localActResult(store, identity, verify) {
|
|
|
154
156
|
* The caller must also integrate this boundary into admission-history readers;
|
|
155
157
|
* this storage port alone does not activate a grammar or land authority.
|
|
156
158
|
*/
|
|
157
|
-
export async function commitLocalAct(store, identity, regions, prepare, verify, storage = nativePublicationStorage) {
|
|
159
|
+
export async function commitLocalAct(store, identity, regions, prepare, verify, storage = nativePublicationStorage, signal) {
|
|
158
160
|
requireEffects('local act commitment');
|
|
161
|
+
assertHostNotCancelled(signal);
|
|
159
162
|
return underTheRegions(store, regions, 'land', async () => {
|
|
163
|
+
assertHostNotCancelled(signal);
|
|
160
164
|
const prior = localActResult(store, identity, verify);
|
|
161
165
|
if (prior)
|
|
162
166
|
return { kind: 'once', ...prior };
|
|
163
167
|
const bundle = await prepare();
|
|
168
|
+
assertHostNotCancelled(signal);
|
|
164
169
|
if (!bundle.records || !bundle.receipt || verify(identity, bundle) !== true)
|
|
165
170
|
throw Error('REFUSE·act prepared result does not verify');
|
|
166
171
|
const files = { 'records.bound': bundle.records, 'receipt.bound': bundle.receipt };
|
|
167
172
|
for (const p of bundle.placements ?? [])
|
|
168
173
|
files['place-' + encodeURIComponent(p.place) + '.bound'] = p.records;
|
|
169
|
-
|
|
174
|
+
assertHostNotCancelled(signal);
|
|
175
|
+
try {
|
|
176
|
+
publishBundle(atOf(store, identity), files, storage);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
throw new ActCommitUnknown(identity, error);
|
|
180
|
+
}
|
|
170
181
|
return { kind: 'landed', ...bundle };
|
|
171
182
|
});
|
|
172
183
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type TrustedClockContract, type PlaceReceipt, type RootContext } from '@lapxo/topos/core';
|
|
2
|
+
import { type PlaceActPersistence } from '../place-act-transaction.ts';
|
|
3
|
+
import { type PublicationStorage } from './publication.ts';
|
|
4
|
+
export interface PlaceActBudget {
|
|
5
|
+
readonly acts: number;
|
|
6
|
+
readonly bytes: number;
|
|
7
|
+
}
|
|
8
|
+
export interface PlaceActHead {
|
|
9
|
+
readonly place: string;
|
|
10
|
+
readonly identity: string;
|
|
11
|
+
readonly epoch: string;
|
|
12
|
+
}
|
|
13
|
+
export interface PlaceActSnapshot {
|
|
14
|
+
readonly root: RootContext;
|
|
15
|
+
readonly heads: readonly PlaceActHead[];
|
|
16
|
+
}
|
|
17
|
+
/** Fixed instrument construction, never supplied by world JSON. Historical
|
|
18
|
+
* authentication permits inspection only; current() is read under Root guards. */
|
|
19
|
+
export interface PlaceActTrust {
|
|
20
|
+
readonly contract: TrustedClockContract;
|
|
21
|
+
readonly initialHeads: readonly PlaceActHead[];
|
|
22
|
+
readonly current: () => PlaceActSnapshot;
|
|
23
|
+
readonly historical: (encoded: string) => TrustedClockContract;
|
|
24
|
+
}
|
|
25
|
+
export interface PlaceActBundle {
|
|
26
|
+
readonly context: string;
|
|
27
|
+
readonly receipts: readonly PlaceReceipt[];
|
|
28
|
+
readonly originals: readonly (readonly string[])[];
|
|
29
|
+
}
|
|
30
|
+
/** Historical result reads never produce a current authorization permit. */
|
|
31
|
+
export declare function placeActResult(store: string, id: string, trust: Pick<PlaceActTrust, 'historical'>, limits: PlaceActBudget): PlaceActBundle | undefined;
|
|
32
|
+
/** Each receiver chain advances independently. Initial heads are explicit,
|
|
33
|
+
* authenticated host inputs, not inferred zero or legacy global epochs. */
|
|
34
|
+
export declare function placeActHeads(store: string, initial: readonly PlaceActHead[], trust: Pick<PlaceActTrust, 'historical' | 'contract'>, limits: PlaceActBudget): readonly PlaceActHead[];
|
|
35
|
+
/** Context, exact original bytes and complete signed receipts have one visibility
|
|
36
|
+
* point. Real Root guard exclusion spans final authority/head comparison and
|
|
37
|
+
* publication; this does not activate a CLI profile or reinterpret local-act@1. */
|
|
38
|
+
/** Native persistence adapter. The transaction below is also the CLI path. */
|
|
39
|
+
export declare function nativePlaceActPersistence(store: string, storage?: PublicationStorage): PlaceActPersistence;
|
|
40
|
+
export declare function commitPlaceAct(store: string, encoded: string, receipts: readonly PlaceReceipt[], trust: PlaceActTrust, limits: PlaceActBudget, storage?: PublicationStorage): Promise<PlaceActBundle & {
|
|
41
|
+
readonly kind: "landed" | "once";
|
|
42
|
+
readonly identity: string;
|
|
43
|
+
}>;
|
|
44
|
+
/** Enumerates only committed, historically authenticated carriers. Namespace
|
|
45
|
+
* projection is a caller's separate pure operation, never current permission. */
|
|
46
|
+
export declare function committedPlaceActResults(store: string, trust: Pick<PlaceActTrust, 'historical'>, limits: PlaceActBudget): readonly {
|
|
47
|
+
identity: string;
|
|
48
|
+
bundle: PlaceActBundle;
|
|
49
|
+
}[];
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync, openSync, fstatSync, readSync, closeSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { readPlaceActContext,
|
|
4
|
-
import {
|
|
3
|
+
import { readPlaceActContext, placeActReceiptsOf } from '@lapxo/topos/core';
|
|
4
|
+
import { transactPlaceAct } from "../place-act-transaction.js";
|
|
5
|
+
import { canonicalActStore, underAuthorityRegions, underTheRegions, underRootProjection } from "../../land/act.js";
|
|
5
6
|
import { requireEffects } from "../read-only.js";
|
|
7
|
+
import { receiverClock } from "../host-controls.js";
|
|
6
8
|
import { publishBundle, nativePublicationStorage } from "./publication.js";
|
|
7
9
|
const fail = (why) => { throw Error('REFUSE·place act ' + why); };
|
|
8
|
-
const clock = (n) => { if (!/^(0|[1-9][0-9]*)$/.test(n) || !Number.isSafeInteger(Number(n)))
|
|
9
|
-
fail('unknown receiver clock'); return Number(n); };
|
|
10
|
-
const rootBytes = (r) => JSON.stringify([r.namespace, r.snapshot, r.epoch, r.prefix, r.head, r.policies]);
|
|
11
10
|
const budget = (b) => { if (!b || !Number.isSafeInteger(b.acts) || b.acts < 1 || !Number.isSafeInteger(b.bytes) || b.bytes < 1)
|
|
12
11
|
fail('missing storage budget'); };
|
|
13
12
|
const base = (store) => join(store, 'ledger', 'place-acts');
|
|
@@ -86,7 +85,7 @@ export function placeActHeads(store, initial, trust, limits) {
|
|
|
86
85
|
budget(limits);
|
|
87
86
|
if (!initial.length || new Set(initial.map(h => h.place)).size !== initial.length)
|
|
88
87
|
fail('missing/ambiguous initial heads');
|
|
89
|
-
const heads = new Map(initial.map(h => {
|
|
88
|
+
const heads = new Map(initial.map(h => { receiverClock(h.epoch, fail); if (!h.identity)
|
|
90
89
|
fail('missing initial head'); return [h.place, { ...h }]; }));
|
|
91
90
|
if (!existsSync(base(store)))
|
|
92
91
|
return [...heads.values()];
|
|
@@ -100,8 +99,8 @@ export function placeActHeads(store, initial, trust, limits) {
|
|
|
100
99
|
const act = pending.splice(next, 1)[0];
|
|
101
100
|
for (const p of act.context.places) {
|
|
102
101
|
const prior = heads.get(p.place);
|
|
103
|
-
const epoch =
|
|
104
|
-
if (!Number.isSafeInteger(epoch) ||
|
|
102
|
+
const epoch = receiverClock(prior.epoch, fail) + 1;
|
|
103
|
+
if (!Number.isSafeInteger(epoch) || receiverClock(p.epoch, fail) !== epoch)
|
|
105
104
|
fail('receiver chain clock mismatch');
|
|
106
105
|
heads.set(p.place, { place: p.place, identity: act.id, epoch: p.epoch });
|
|
107
106
|
}
|
|
@@ -111,54 +110,20 @@ export function placeActHeads(store, initial, trust, limits) {
|
|
|
111
110
|
/** Context, exact original bytes and complete signed receipts have one visibility
|
|
112
111
|
* point. Real Root guard exclusion spans final authority/head comparison and
|
|
113
112
|
* publication; this does not activate a CLI profile or reinterpret local-act@1. */
|
|
113
|
+
/** Native persistence adapter. The transaction below is also the CLI path. */
|
|
114
|
+
export function nativePlaceActPersistence(store, storage = nativePublicationStorage) {
|
|
115
|
+
store = canonicalActStore(store);
|
|
116
|
+
return {
|
|
117
|
+
exclusive: (regions, run) => { const enter = regions.includes('') ? underTheRegions : underAuthorityRegions; return enter(store, regions, 'place act', () => underRootProjection(store, 'place act publication', run)); },
|
|
118
|
+
result: (identity, trust, limits) => placeActResult(store, identity, trust, limits),
|
|
119
|
+
inventory: limits => storedInventory(store, limits),
|
|
120
|
+
heads: (trust, limits) => placeActHeads(store, trust.initialHeads, trust, limits),
|
|
121
|
+
publish: (identity, files) => publishBundle(at(store, identity), files, storage),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
114
124
|
export async function commitPlaceAct(store, encoded, receipts, trust, limits, storage = nativePublicationStorage) {
|
|
115
125
|
requireEffects('place act commitment');
|
|
116
|
-
|
|
117
|
-
const decoded = readPlaceActContext(encoded, trust.contract.budget), id = trust.contract.digest(encoded);
|
|
118
|
-
const regions = decoded.places.map(p => { if (p.place === '.')
|
|
119
|
-
return ''; const parts = p.place.split('/'); if (parts.length < 2 || parts.pop() !== 'TARGET.bound' || parts.some(s => s === '' || s === '.' || s === '..'))
|
|
120
|
-
fail('unknown place namespace'); return parts.join('/'); });
|
|
121
|
-
const enter = regions.includes('') ? underTheRegions : underAuthorityRegions;
|
|
122
|
-
return enter(store, regions, 'place act', () => underRootProjection(store, 'place act publication', () => {
|
|
123
|
-
const prior = placeActResult(store, id, trust, limits);
|
|
124
|
-
if (prior) {
|
|
125
|
-
if (prior.context !== encoded)
|
|
126
|
-
fail('replay bytes differ');
|
|
127
|
-
// Replay authenticates the supplied evidence too. Historical authentication
|
|
128
|
-
// grants no current authority and leaves the committed result unchanged.
|
|
129
|
-
placeActReceiptsOf(encoded, receipts, trust.historical(encoded));
|
|
130
|
-
return { ...prior, identity: id, kind: 'once' };
|
|
131
|
-
}
|
|
132
|
-
const verified = verifyPlaceActContext(encoded, trust.contract);
|
|
133
|
-
placeActReceiptsOf(encoded, receipts, trust.contract);
|
|
134
|
-
const originals = decoded.places.map(p => p.records), files = { 'context.json': encoded, 'receipts.json': JSON.stringify(receipts) + '\n', 'originals.json': JSON.stringify(originals) + '\n' };
|
|
135
|
-
const inventory = storedInventory(store, limits);
|
|
136
|
-
if (inventory.bytes + Object.values(files).reduce((n, v) => n + Buffer.byteLength(v), 0) > limits.bytes)
|
|
137
|
-
fail('publication byte budget');
|
|
138
|
-
if (inventory.names.length >= limits.acts)
|
|
139
|
-
fail('publication act budget');
|
|
140
|
-
const current = trust.current();
|
|
141
|
-
if (rootBytes(current.root) !== rootBytes(verified.dependencies))
|
|
142
|
-
fail('authority changed before publication');
|
|
143
|
-
if (new Set(current.heads.map(h => h.place)).size !== current.heads.length)
|
|
144
|
-
fail('ambiguous receiver heads');
|
|
145
|
-
const storedHeads = placeActHeads(store, trust.initialHeads, trust, limits);
|
|
146
|
-
for (const p of decoded.places) {
|
|
147
|
-
const actual = storedHeads.find(h => h.place === p.place), selected = current.heads.find(h => h.place === p.place);
|
|
148
|
-
if (!actual || !selected || actual.identity !== selected.identity || actual.epoch !== selected.epoch)
|
|
149
|
-
fail('receiver snapshot is not committed head ' + p.place);
|
|
150
|
-
}
|
|
151
|
-
for (const p of decoded.places) {
|
|
152
|
-
const head = current.heads.find(h => h.place === p.place) ?? fail('missing receiver head ' + p.place);
|
|
153
|
-
if (head.identity !== p.previous)
|
|
154
|
-
fail('receiver head moved ' + p.place);
|
|
155
|
-
const next = clock(head.epoch) + 1;
|
|
156
|
-
if (!Number.isSafeInteger(next) || clock(p.epoch) !== next)
|
|
157
|
-
fail('receiver epoch not next ' + p.place);
|
|
158
|
-
}
|
|
159
|
-
publishBundle(at(store, id), files, storage);
|
|
160
|
-
return { context: encoded, receipts, originals, identity: id, kind: 'landed' };
|
|
161
|
-
}));
|
|
126
|
+
return transactPlaceAct(encoded, receipts, trust, limits, nativePlaceActPersistence(store, storage));
|
|
162
127
|
}
|
|
163
128
|
/** Enumerates only committed, historically authenticated carriers. Namespace
|
|
164
129
|
* projection is a caller's separate pure operation, never current permission. */
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface PublicationStorage {
|
|
2
|
+
publish(staged: string, committed: string): void;
|
|
3
|
+
}
|
|
4
|
+
export declare const nativePublicationStorage: PublicationStorage;
|
|
5
|
+
/** Publish prepared bytes as one directory. No identity, grammar or authority
|
|
6
|
+
* is inferred here. Readers address the committed name, never staging files.
|
|
7
|
+
* A failure after rename is not evidence of rollback: the caller must retain
|
|
8
|
+
* the uncertainty and allow a verified read of the committed name.
|
|
9
|
+
*/
|
|
10
|
+
export declare function publishBundle(committed: string, files: Readonly<Record<string, string>>, storage?: PublicationStorage): void;
|