@lapxo/bound 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/.github/workflows/release.yml +67 -0
  2. package/.github/workflows/test.yml +35 -0
  3. package/CITATION.cff +19 -0
  4. package/TARGET.bound +80 -21
  5. package/dist/cli/accept.js +103 -71
  6. package/dist/cli/place.js +3 -2
  7. package/dist/cli/standing-result.js +3 -5
  8. package/dist/cli/takes.js +4 -3
  9. package/dist/cli/verb.js +2 -1
  10. package/dist/engine/ports.d.ts +6 -1
  11. package/dist/engine/ports.js +23 -9
  12. package/dist/fold/claims.d.ts +29 -0
  13. package/dist/fold/claims.js +2 -1
  14. package/dist/fold/closed.js +2 -1
  15. package/dist/fold/digests.js +7 -7
  16. package/dist/host/admitted-place.js +3 -31
  17. package/dist/host/call-context.d.ts +20 -0
  18. package/dist/host/call-context.js +14 -0
  19. package/dist/host/capsule-process.js +2 -1
  20. package/dist/host/capsule.js +7 -4
  21. package/dist/host/commit-result.d.ts +6 -0
  22. package/dist/host/commit-result.js +5 -0
  23. package/dist/host/contextual-place.js +9 -14
  24. package/dist/host/host-controls.js +19 -0
  25. package/dist/host/in-process-act.d.ts +82 -0
  26. package/dist/host/in-process-act.js +84 -0
  27. package/dist/host/in-process-context.d.ts +46 -0
  28. package/dist/host/in-process-context.js +91 -0
  29. package/dist/host/in-process-root.d.ts +117 -0
  30. package/dist/host/in-process-root.js +84 -0
  31. package/dist/host/in-process.d.ts +41 -0
  32. package/dist/host/in-process.js +55 -0
  33. package/dist/host/index.d.ts +15 -0
  34. package/dist/host/index.js +7 -0
  35. package/dist/host/native/loader.js +1 -1
  36. package/dist/host/native/module-closure.d.ts +35 -0
  37. package/dist/host/native/module-closure.js +34 -3
  38. package/dist/host/native/module-delivery.d.ts +62 -0
  39. package/dist/host/native/module-delivery.js +151 -0
  40. package/dist/host/native/module-syntax.js +3 -1
  41. package/dist/host/native-receiver.d.ts +19 -0
  42. package/dist/host/native-receiver.js +25 -0
  43. package/dist/host/order.js +2 -0
  44. package/dist/host/place-act-transaction.d.ts +24 -0
  45. package/dist/host/place-act-transaction.js +78 -0
  46. package/dist/host/place-clock-selection.d.ts +55 -0
  47. package/dist/host/place-clock-selection.js +1 -1
  48. package/dist/host/ports/local-act.d.ts +44 -0
  49. package/dist/host/ports/local-act.js +13 -2
  50. package/dist/host/ports/place-act.d.ts +49 -0
  51. package/dist/host/ports/place-act.js +19 -54
  52. package/dist/host/ports/publication.d.ts +10 -0
  53. package/dist/host/root-clock-source.d.ts +86 -0
  54. package/dist/host/root-clock-source.js +7 -5
  55. package/dist/host/signature-era.js +12 -0
  56. package/dist/host/signing.js +1 -1
  57. package/dist/host/source-content.js +2 -1
  58. package/dist/host/standing-carrier.js +18 -0
  59. package/dist/host/standing-projection.js +50 -0
  60. package/dist/land/act.js +60 -26
  61. package/dist/observe/run.js +2 -1
  62. package/dist/observe/runner.js +16 -8
  63. package/docs/contracts/closure-delivery.md +23 -0
  64. package/docs/contracts/closure-delivery.publication.json +25 -0
  65. package/docs/contracts/native-host.md +21 -0
  66. package/docs/contracts/native-host.publication.json +25 -0
  67. package/docs/contracts/native-publication.inventory.json +2 -1
  68. package/docs/contracts/publication.inventory.json +29 -0
  69. package/docs/guide.md +10 -0
  70. package/docs/reference.md +39 -0
  71. package/docs/why.md +109 -0
  72. package/package.json +43 -2
  73. package/receipts.bound +10405 -9625
  74. package/samples/module-delivery/inputs/delivery.json +1 -0
  75. package/samples/native-host/no.json +10 -0
  76. package/samples/native-host/yes.json +20 -0
  77. package/samples/no/module-delivery/delivery.json +1 -0
  78. package/vectors/native/module-delivery.vector.json +32 -0
  79. package/vectors/native/native-host.vector.json +62 -0
@@ -0,0 +1,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,2 @@
1
+ /** Locale-independent UTF-16 code-unit ordering, shared by host delivery formats. */
2
+ export const compareStrings = (a, b) => a < b ? -1 : a > b ? 1 : 0;
@@ -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 "../fold/digests.js";
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
- publishBundle(atOf(store, identity), files, storage);
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, verifyPlaceActContext, placeActReceiptsOf } from '@lapxo/topos/core';
4
- import { underAuthorityRegions, underTheRegions, underRootProjection } from "../../land/act.js";
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 => { clock(h.epoch); if (!h.identity)
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 = clock(prior.epoch) + 1;
104
- if (!Number.isSafeInteger(epoch) || clock(p.epoch) !== 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
- budget(limits);
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;