@lapxo/bound 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -20
- package/TARGET.bound +25 -25
- package/dist/cli/accept.js +83 -59
- package/dist/cli/act-result.d.ts +9 -0
- package/dist/cli/act-result.js +68 -13
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.js +5 -2
- package/dist/cli/custodian.d.ts +1 -0
- package/dist/cli/custodian.js +29 -0
- package/dist/cli/native-descendant.d.ts +1 -0
- package/dist/cli/native-descendant.js +19 -0
- package/dist/cli/native.d.ts +1 -0
- package/dist/cli/native.js +60 -0
- package/dist/cli/observe.js +9 -2
- package/dist/cli/pass.js +6 -1
- package/dist/cli/standing-result.js +7 -26
- package/dist/cli/verb.js +24 -5
- package/dist/fold/admission-history.js +2 -2
- package/dist/fold/claims.d.ts +4 -0
- package/dist/fold/claims.js +23 -3
- package/dist/fold/closed.d.ts +1 -1
- package/dist/fold/closed.js +16 -4
- package/dist/fold/digests.d.ts +1 -1
- package/dist/fold/digests.js +3 -2
- package/dist/fold/evidence.d.ts +1 -1
- package/dist/fold/kept.js +2 -1
- package/dist/fold/observed.d.ts +1 -0
- package/dist/fold/observed.js +54 -9
- package/dist/fold/readers.d.ts +4 -3
- package/dist/fold/readers.js +18 -13
- package/dist/fold/resolved.js +11 -1
- package/dist/fold/signers.js +2 -2
- package/dist/host/adapters/modules/javascript.d.ts +2 -0
- package/dist/host/adapters/modules/javascript.js +8 -6
- package/dist/host/admitted-place.d.ts +5 -0
- package/dist/host/admitted-place.js +54 -0
- package/dist/host/capsule-process.d.ts +1 -1
- package/dist/host/capsule-process.js +4 -1
- package/dist/host/capsule.d.ts +4 -0
- package/dist/host/capsule.js +16 -2
- package/dist/host/contextual-place.d.ts +59 -0
- package/dist/host/contextual-place.js +77 -0
- package/dist/host/hash.d.ts +7 -0
- package/dist/host/hash.js +6 -0
- package/dist/host/io.d.ts +2 -0
- package/dist/host/io.js +2 -0
- package/dist/host/lifetime.js +61 -22
- package/dist/host/native/bounded-file.d.ts +8 -0
- package/dist/host/native/bounded-file.js +32 -0
- package/dist/host/native/command.d.ts +36 -0
- package/dist/host/native/command.js +91 -0
- package/dist/host/native/control-bridge-worker.d.ts +1 -0
- package/dist/host/native/control-bridge-worker.js +88 -0
- package/dist/host/native/control-bridge.d.ts +30 -0
- package/dist/host/native/control-bridge.js +110 -0
- package/dist/host/native/control-selection.d.ts +29 -0
- package/dist/host/native/control-selection.js +32 -0
- package/dist/host/native/custody.d.ts +5 -0
- package/dist/host/native/custody.js +91 -0
- package/dist/host/native/deadline.d.ts +12 -0
- package/dist/host/native/deadline.js +28 -0
- package/dist/host/native/descendants.d.ts +1 -0
- package/dist/host/native/descendants.js +528 -0
- package/dist/host/native/generated-custodian.d.ts +122 -0
- package/dist/host/native/generated-custodian.js +267 -0
- package/dist/host/native/generated.d.ts +217 -0
- package/dist/host/native/generated.js +379 -0
- package/dist/host/native/image.d.ts +36 -0
- package/dist/host/native/image.js +45 -0
- package/dist/host/native/jobs.d.ts +75 -0
- package/dist/host/native/jobs.js +148 -0
- package/dist/host/native/loader.d.ts +80 -0
- package/dist/host/native/loader.js +269 -0
- package/dist/host/native/materialize.d.ts +22 -0
- package/dist/host/native/materialize.js +175 -0
- package/dist/host/native/module-closure.d.ts +27 -0
- package/dist/host/native/module-closure.js +99 -0
- package/dist/host/native/module-syntax.d.ts +8 -0
- package/dist/host/native/module-syntax.js +63 -0
- package/dist/host/native/native-binary.d.ts +54 -0
- package/dist/host/native/native-binary.js +115 -0
- package/dist/host/native/native-program.d.ts +56 -0
- package/dist/host/native/native-program.js +72 -0
- package/dist/host/native/node-jobs.d.ts +84 -0
- package/dist/host/native/node-jobs.js +187 -0
- package/dist/host/native/node-launch.d.ts +72 -0
- package/dist/host/native/node-launch.js +159 -0
- package/dist/host/native/preload.d.ts +1 -0
- package/dist/host/native/preload.js +45 -0
- package/dist/host/native/reading.d.ts +91 -0
- package/dist/host/native/reading.js +249 -0
- package/dist/host/native/receipt-environment.d.ts +3 -0
- package/dist/host/native/receipt-environment.js +41 -0
- package/dist/host/native/references.d.ts +5 -0
- package/dist/host/native/references.js +30 -0
- package/dist/host/native/root.d.ts +17 -0
- package/dist/host/native/root.js +37 -0
- package/dist/host/native/route.d.ts +66 -0
- package/dist/host/native/route.js +216 -0
- package/dist/host/native/runtime.d.ts +4 -0
- package/dist/host/native/runtime.js +9 -0
- package/dist/host/native-reader-context.d.ts +3 -0
- package/dist/host/native-reader-context.js +8 -0
- package/dist/host/place-clock-selection.d.ts +55 -0
- package/dist/host/place-clock-selection.js +124 -0
- package/dist/host/ports/local-act.d.ts +10 -0
- package/dist/host/ports/local-act.js +78 -6
- package/dist/host/ports/place-act.d.ts +46 -0
- package/dist/host/ports/place-act.js +169 -0
- package/dist/host/process-lifetime.js +8 -3
- package/dist/host/provide.d.ts +25 -0
- package/dist/host/provide.js +115 -13
- package/dist/host/reading-closures.d.ts +15 -1
- package/dist/host/reading-closures.js +88 -6
- package/dist/host/reading-context.d.ts +29 -0
- package/dist/host/reading-context.js +53 -0
- package/dist/host/root-clock-source.d.ts +85 -0
- package/dist/host/root-clock-source.js +305 -0
- package/dist/host/selected-capsules.js +3 -2
- package/dist/land/act.d.ts +26 -0
- package/dist/land/act.js +96 -3
- package/dist/land/ledger.d.ts +4 -0
- package/dist/land/ledger.js +20 -2
- package/dist/observe/runner.d.ts +4 -0
- package/dist/observe/runner.js +9 -2
- package/docs/contracts/cli.md +13 -0
- package/package.json +2 -2
- package/receipts.bound +296 -256
|
@@ -0,0 +1,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,85 @@
|
|
|
1
|
+
import { type PlaceReceipt, type ClockBudget } from '@lapxo/topos/core';
|
|
2
|
+
import type { PlaceActBudget } from './ports/place-act.ts';
|
|
3
|
+
export interface RootClockSourceSelection {
|
|
4
|
+
readonly selector: string;
|
|
5
|
+
readonly profileSelector: string;
|
|
6
|
+
readonly bootstrapCheckpoint: {
|
|
7
|
+
scope: string;
|
|
8
|
+
measure: string;
|
|
9
|
+
};
|
|
10
|
+
readonly resources: {
|
|
11
|
+
bytes: number;
|
|
12
|
+
lines: number;
|
|
13
|
+
places: number;
|
|
14
|
+
acts: number;
|
|
15
|
+
};
|
|
16
|
+
readonly inherited: {
|
|
17
|
+
clock: ClockBudget;
|
|
18
|
+
storage: PlaceActBudget;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export declare function rootClockSource(root: string, selected: RootClockSourceSelection): {
|
|
22
|
+
observation: Readonly<{
|
|
23
|
+
history: readonly string[];
|
|
24
|
+
anchor: import("@lapxo/topos/wire").Signer;
|
|
25
|
+
algorithms: readonly string[];
|
|
26
|
+
}>;
|
|
27
|
+
resources: Readonly<{
|
|
28
|
+
bytes: number;
|
|
29
|
+
lines: number;
|
|
30
|
+
places: number;
|
|
31
|
+
acts: number;
|
|
32
|
+
}>;
|
|
33
|
+
authority: (encoded: string) => {
|
|
34
|
+
anchor: import("@lapxo/topos/wire").Signer;
|
|
35
|
+
history: readonly string[];
|
|
36
|
+
checkpoint: string;
|
|
37
|
+
};
|
|
38
|
+
selection: import("./place-clock-selection.ts").HostClockSelection;
|
|
39
|
+
trust: import("./ports/place-act.ts").PlaceActTrust;
|
|
40
|
+
};
|
|
41
|
+
/** Source mapping is an explicit Root declaration; absence never means a zero
|
|
42
|
+
* clock or an inferred cursor. Existing legacy callers do not request it. */
|
|
43
|
+
export declare function declaredRootClockSource(root: string): {
|
|
44
|
+
observation: Readonly<{
|
|
45
|
+
history: readonly string[];
|
|
46
|
+
anchor: import("@lapxo/topos/wire").Signer;
|
|
47
|
+
algorithms: readonly string[];
|
|
48
|
+
}>;
|
|
49
|
+
resources: Readonly<{
|
|
50
|
+
bytes: number;
|
|
51
|
+
lines: number;
|
|
52
|
+
places: number;
|
|
53
|
+
acts: number;
|
|
54
|
+
}>;
|
|
55
|
+
authority: (encoded: string) => {
|
|
56
|
+
anchor: import("@lapxo/topos/wire").Signer;
|
|
57
|
+
history: readonly string[];
|
|
58
|
+
checkpoint: string;
|
|
59
|
+
};
|
|
60
|
+
selection: import("./place-clock-selection.ts").HostClockSelection;
|
|
61
|
+
trust: import("./ports/place-act.ts").PlaceActTrust;
|
|
62
|
+
};
|
|
63
|
+
export declare function hasRootClockSource(root: string): boolean;
|
|
64
|
+
export interface PortablePlaceContext {
|
|
65
|
+
readonly identity: string;
|
|
66
|
+
readonly context: string;
|
|
67
|
+
readonly receipts: readonly PlaceReceipt[];
|
|
68
|
+
readonly authority: {
|
|
69
|
+
anchor: import('@lapxo/topos/wire').Signer;
|
|
70
|
+
history: readonly string[];
|
|
71
|
+
checkpoint: string;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Historical inspection ONLY. The expected acting Root anchor and resource
|
|
75
|
+
* selection come from the consumer's selected place, never from this packet.
|
|
76
|
+
* This function returns data and cannot construct a current command permit. */
|
|
77
|
+
export declare function inspectPortablePlaceContexts(encoded: string, expectedAnchor: import('@lapxo/topos/wire').Signer, selected: RootClockSourceSelection): {
|
|
78
|
+
identity: string;
|
|
79
|
+
context: import("@lapxo/topos/core").PlaceAct;
|
|
80
|
+
}[];
|
|
81
|
+
/** Prospective configuration is checked as authenticated Root history, never
|
|
82
|
+
* against a prose birth condition. Initial heads are explicit typed selections;
|
|
83
|
+
* stored committed chains cannot disappear when a selection is activated or
|
|
84
|
+
* replaced. Preview reads only; the real admission repeats under its Root lock. */
|
|
85
|
+
export declare function assertProspectiveRootClockSelection(root: string, signed: readonly string[]): void;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { existsSync, opendirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, basename } from 'node:path';
|
|
3
|
+
/** Actual stored Root cursor. Inputs are fixed host selections from admitted
|
|
4
|
+
* lines, never packet epochs, receipt maxima, or caller-supplied verification. */
|
|
5
|
+
import { committedLocalActs, actLines } from "./ports/local-act.js";
|
|
6
|
+
import { verifiedLegacyActResult } from "../cli/act-result.js";
|
|
7
|
+
import { admissionAnchor, admissionHistoryAt } from "../fold/admission-history.js";
|
|
8
|
+
import { authorityFor } from "../fold/signers.js";
|
|
9
|
+
import { eraOf } from "../fold/digests.js";
|
|
10
|
+
import { fieldOf, foldClaims, isConfig, selfName } from "../fold/claims.js";
|
|
11
|
+
import { authenticatedRootStanding } from "./admitted-place.js";
|
|
12
|
+
import { verifiesOwnerLine } from "../land/sign.js";
|
|
13
|
+
import { placesIn } from "../fold/places.js";
|
|
14
|
+
import { appliedHistoryIn } from "../fold/signed.js";
|
|
15
|
+
import { storeOf } from "../land/ledger.js";
|
|
16
|
+
import { hostClockSelection } from "./place-clock-selection.js";
|
|
17
|
+
import { readPlaceActContext, placeActReceiptsOf } from '@lapxo/topos/core';
|
|
18
|
+
import { placeActResult, placeActHeads } from "./ports/place-act.js";
|
|
19
|
+
import { rootAuthorityProtection } from "../land/act.js";
|
|
20
|
+
import { readOnly } from "./read-only.js";
|
|
21
|
+
const fail = (s) => { throw Error('REFUSE·Root cursor ' + s); };
|
|
22
|
+
function sourceSelection(selected) {
|
|
23
|
+
const b = selected?.resources;
|
|
24
|
+
if (!b || Object.keys(b).sort().join(',') !== 'acts,bytes,lines,places' || Object.values(b).some(n => !Number.isSafeInteger(n) || n < 1) || typeof selected.selector !== 'string' || !selected.selector || typeof selected.profileSelector !== 'string' || !selected.profileSelector || !selected.bootstrapCheckpoint || Object.keys(selected.bootstrapCheckpoint).sort().join(',') !== 'measure,scope' || typeof selected.bootstrapCheckpoint.scope !== 'string' || !selected.bootstrapCheckpoint.scope || typeof selected.bootstrapCheckpoint.measure !== 'string' || !selected.bootstrapCheckpoint.measure)
|
|
25
|
+
fail('missing admitted source selection');
|
|
26
|
+
if (!selected.inherited?.clock || Object.keys(selected.inherited.clock).sort().join(',') !== 'bytes,placements,policies,proofs,records,signers' || !selected.inherited.storage || Object.keys(selected.inherited.storage).sort().join(',') !== 'acts,bytes' || [...Object.values(selected.inherited.clock), ...Object.values(selected.inherited.storage)].some(n => !Number.isSafeInteger(n) || n < 1))
|
|
27
|
+
fail('missing inherited source budgets');
|
|
28
|
+
return b;
|
|
29
|
+
}
|
|
30
|
+
function boundedInventory(root, selected) {
|
|
31
|
+
const b = sourceSelection(selected), store = storeOf(root);
|
|
32
|
+
let bytes = 0, files = 0, entries = 0;
|
|
33
|
+
const limit = b.acts * (b.places + 2) + b.lines;
|
|
34
|
+
if (!Number.isSafeInteger(limit))
|
|
35
|
+
fail('source file bound');
|
|
36
|
+
const visit = (path) => { if (!existsSync(path))
|
|
37
|
+
return; const s = statSync(path); if (s.isDirectory()) {
|
|
38
|
+
const directory = opendirSync(path);
|
|
39
|
+
try {
|
|
40
|
+
for (let entry = directory.readSync(); entry; entry = directory.readSync()) {
|
|
41
|
+
if (++entries > limit)
|
|
42
|
+
fail('source entry budget');
|
|
43
|
+
if (basename(path) === 'ledger' && entry.name === 'place-acts')
|
|
44
|
+
continue;
|
|
45
|
+
visit(join(path, entry.name));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
directory.closeSync();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
if (!s.isFile() || ++files > limit || (bytes += s.size) > b.bytes)
|
|
54
|
+
fail('source file/byte budget');
|
|
55
|
+
} };
|
|
56
|
+
visit(join(store, 'ledger'));
|
|
57
|
+
visit(join(store, selfName() + '.keys'));
|
|
58
|
+
}
|
|
59
|
+
function current(root, selected) {
|
|
60
|
+
const b = sourceSelection(selected);
|
|
61
|
+
boundedInventory(root, selected);
|
|
62
|
+
const history = admissionHistoryAt(root, Number.MAX_SAFE_INTEGER);
|
|
63
|
+
if (history.length > b.lines)
|
|
64
|
+
fail('history line budget');
|
|
65
|
+
let bytes = 0;
|
|
66
|
+
for (const line of history)
|
|
67
|
+
if ((bytes += Buffer.byteLength(line)) > b.bytes)
|
|
68
|
+
fail('history byte budget');
|
|
69
|
+
const acts = committedLocalActs(storeOf(root));
|
|
70
|
+
if (acts.length > b.acts)
|
|
71
|
+
fail('act inventory budget');
|
|
72
|
+
const rooted = acts.flatMap(a => { const roots = a.bundle.placements?.filter(p => p.place === '.') ?? []; if (!roots.some(p => actLines(p.records).some(isConfig)))
|
|
73
|
+
return []; const verified = verifiedLegacyActResult(root, a.identity); return [{ ...verified, identity: a.identity, roots }]; });
|
|
74
|
+
// Comparison is over verified committed Root CONFIGURATION acts only. This
|
|
75
|
+
// is the existing local-act cursor, not a new maximum over arbitrary records.
|
|
76
|
+
rooted.sort((a, b) => a.epoch - b.epoch);
|
|
77
|
+
if (rooted.some((a, i) => i > 0 && a.epoch === rooted[i - 1].epoch && a.identity !== rooted[i - 1].identity))
|
|
78
|
+
fail('ambiguous committed Root cursor');
|
|
79
|
+
const latest = rooted.at(-1), anchor = admissionAnchor(root);
|
|
80
|
+
let checkpoint;
|
|
81
|
+
if (latest) {
|
|
82
|
+
const rows = latest.roots.flatMap(p => actLines(p.records)).filter(isConfig);
|
|
83
|
+
if (!rows.length)
|
|
84
|
+
fail('verified Root act has no configuration checkpoint');
|
|
85
|
+
checkpoint = rows[0];
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const rows = history.filter(l => fieldOf(l, 'scope') === selected.bootstrapCheckpoint.scope && fieldOf(l, 'measure') === selected.bootstrapCheckpoint.measure);
|
|
89
|
+
if (rows.length !== 1)
|
|
90
|
+
fail('bootstrap checkpoint missing/ambiguous');
|
|
91
|
+
checkpoint = rows[0];
|
|
92
|
+
}
|
|
93
|
+
const epoch = Number(fieldOf(checkpoint, 'epoch'));
|
|
94
|
+
if (!Number.isSafeInteger(epoch) || epoch < 0)
|
|
95
|
+
fail('unknown committed Root epoch');
|
|
96
|
+
// This synchronous observation already read the full current inventory. Select
|
|
97
|
+
// its admission prefix without opening that same inventory a second time;
|
|
98
|
+
// the next current() invocation still reads and authenticates fresh bytes.
|
|
99
|
+
const prefix = history.filter(line => !isConfig(line) || Number(fieldOf(line, 'epoch')) <= epoch), algorithms = eraOf(foldClaims(prefix.filter(l => fieldOf(l, 'scope').startsWith('wire/'))).standing).admitted;
|
|
100
|
+
const auth = authorityFor(prefix, anchor, algorithms);
|
|
101
|
+
if (auth.of(checkpoint).kind !== 'admitted')
|
|
102
|
+
fail('checkpoint is not authenticated');
|
|
103
|
+
const places = placesIn(root);
|
|
104
|
+
if (places.length > b.places)
|
|
105
|
+
fail('legacy namespace inventory budget');
|
|
106
|
+
const legacy = [...new Set([...history, ...places.flatMap(p => appliedHistoryIn(root, p))])];
|
|
107
|
+
if (legacy.length + prefix.length > b.lines || legacy.reduce((n, l) => n + Buffer.byteLength(l), 0) + prefix.reduce((n, l) => n + Buffer.byteLength(l), 0) > b.bytes)
|
|
108
|
+
fail('legacy origin inventory budget');
|
|
109
|
+
return { history: prefix, anchor, algorithms, authorityCheckpoint: checkpoint, legacyOriginals: legacy };
|
|
110
|
+
}
|
|
111
|
+
export function rootClockSource(root, selected) {
|
|
112
|
+
const state = current(root, selected), visiting = new Set(), cache = new Map();
|
|
113
|
+
const historical = (encoded) => {
|
|
114
|
+
const cached = cache.get(encoded);
|
|
115
|
+
if (cached)
|
|
116
|
+
return cached;
|
|
117
|
+
if (visiting.has(encoded) || visiting.size >= selected.resources.acts)
|
|
118
|
+
fail('historical predecessor cycle/budget');
|
|
119
|
+
visiting.add(encoded);
|
|
120
|
+
try {
|
|
121
|
+
const context = readPlaceActContext(encoded, selected.inherited.clock), epoch = context.root.epoch;
|
|
122
|
+
const saved = admissionHistoryAt(root, Number(epoch));
|
|
123
|
+
const rows = saved.filter(l => isConfig(l) && fieldOf(l, 'epoch') === epoch);
|
|
124
|
+
if (!rows.length)
|
|
125
|
+
fail('historical checkpoint absent');
|
|
126
|
+
// First obtain the explicitly declared historical genesis, without scanning
|
|
127
|
+
// current receiver chains. Each non-genesis predecessor is then authenticated
|
|
128
|
+
// through its stored signed receipt before supplying these HOST-only heads.
|
|
129
|
+
const basic = hostClockSelection({ ...input, history: saved, authorityCheckpoint: rows[0], legacyOriginals: saved, authenticatedReceiverHeads: [], readCurrent: () => current(root, selected) });
|
|
130
|
+
const heads = context.places.map(p => {
|
|
131
|
+
const initial = basic.trust.initialHeads.find(h => h.place === p.place);
|
|
132
|
+
if (initial?.identity === p.previous)
|
|
133
|
+
return initial;
|
|
134
|
+
const prior = placeActResult(input.store, p.previous, { historical }, basic.selection.storageBudget);
|
|
135
|
+
if (!prior)
|
|
136
|
+
fail('historical predecessor absent');
|
|
137
|
+
const decoded = readPlaceActContext(prior.context, selected.inherited.clock), place = decoded.places.find(one => one.place === p.place);
|
|
138
|
+
if (!place)
|
|
139
|
+
fail('historical predecessor namespace absent');
|
|
140
|
+
return { place: p.place, identity: p.previous, epoch: place.epoch };
|
|
141
|
+
});
|
|
142
|
+
const result = hostClockSelection({ ...input, history: saved, authorityCheckpoint: rows[0], legacyOriginals: saved, authenticatedReceiverHeads: heads, readCurrent: () => current(root, selected) }).trust.contract;
|
|
143
|
+
cache.set(encoded, result);
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
visiting.delete(encoded);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const input = { store: storeOf(root), selector: selected.selector, profileSelector: selected.profileSelector, ...state, resources: { bytes: selected.resources.bytes, lines: selected.resources.lines, places: selected.resources.places }, inherited: selected.inherited, readCurrent: () => current(root, selected), historical };
|
|
151
|
+
const resolved = hostClockSelection(input);
|
|
152
|
+
// Host-only bytes from this exact authenticated constructor observation. This
|
|
153
|
+
// is not a retained authority cache: every constructor calls current() again.
|
|
154
|
+
const observedAnchor = Object.freeze({ ...state.anchor, coverage: Object.freeze([...state.anchor.coverage]), depth: Object.freeze({ ...state.anchor.depth }), admittedBy: Object.freeze([...state.anchor.admittedBy]) });
|
|
155
|
+
const observation = Object.freeze({ history: Object.freeze([...state.history]), anchor: observedAnchor, algorithms: Object.freeze([...state.algorithms]) });
|
|
156
|
+
return { ...resolved, observation, resources: Object.freeze({ ...selected.resources }), authority: (encoded) => { const context = readPlaceActContext(encoded, selected.inherited.clock), saved = admissionHistoryAt(root, Number(context.root.epoch)); const rows = saved.filter(l => isConfig(l) && fieldOf(l, 'epoch') === context.root.epoch); if (!rows.length)
|
|
157
|
+
fail('public historical checkpoint absent'); return { anchor: state.anchor, history: saved, checkpoint: rows[0] }; } };
|
|
158
|
+
}
|
|
159
|
+
/** Source mapping is an explicit Root declaration; absence never means a zero
|
|
160
|
+
* clock or an inferred cursor. Existing legacy callers do not request it. */
|
|
161
|
+
export function declaredRootClockSource(root) {
|
|
162
|
+
const live = authenticatedRootStanding(root), rows = live.filter(l => fieldOf(l, 'scope') === 'wire/place-act-context-source' && fieldOf(l, 'measure') === 'text' && fieldOf(l, 'value') !== 'withdraw');
|
|
163
|
+
if (rows.length !== 1)
|
|
164
|
+
fail('requires one admitted source mapping');
|
|
165
|
+
const line = rows[0], anchor = admissionAnchor(root), algorithms = eraOf(live).admitted;
|
|
166
|
+
if (fieldOf(line, 'by') !== anchor.id || fieldOf(line, 'role') !== 'writes' || fieldOf(line, 'form') !== 'alphabet' || fieldOf(line, 'value') !== 'lock' || !verifiesOwnerLine(line, anchor.publicKey, algorithms))
|
|
167
|
+
fail('mapping is not Root signed');
|
|
168
|
+
const about = fieldOf(line, 'about');
|
|
169
|
+
let value;
|
|
170
|
+
try {
|
|
171
|
+
value = JSON.parse(about);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return fail('mapping JSON invalid');
|
|
175
|
+
}
|
|
176
|
+
if (!value || Object.keys(value).sort().join(',') !== 'bootstrapCheckpoint,inherited,profile,profileSelector,resources,selector' || value.profile !== 'place-act-context@1' || JSON.stringify(value) !== about)
|
|
177
|
+
fail('unknown/noncanonical mapping');
|
|
178
|
+
return rootClockSource(root, value);
|
|
179
|
+
}
|
|
180
|
+
export function hasRootClockSource(root) { return authenticatedRootStanding(root).some(l => fieldOf(l, 'scope') === 'wire/place-act-context-source' && fieldOf(l, 'measure') === 'text' && fieldOf(l, 'value') !== 'withdraw'); }
|
|
181
|
+
/** Historical inspection ONLY. The expected acting Root anchor and resource
|
|
182
|
+
* selection come from the consumer's selected place, never from this packet.
|
|
183
|
+
* This function returns data and cannot construct a current command permit. */
|
|
184
|
+
export function inspectPortablePlaceContexts(encoded, expectedAnchor, selected) {
|
|
185
|
+
sourceSelection(selected);
|
|
186
|
+
if (typeof encoded !== 'string' || Buffer.byteLength(encoded) > selected.resources.bytes)
|
|
187
|
+
fail('portable byte budget');
|
|
188
|
+
let rows;
|
|
189
|
+
try {
|
|
190
|
+
rows = JSON.parse(encoded);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return fail('portable JSON');
|
|
194
|
+
}
|
|
195
|
+
if (!Array.isArray(rows) || rows.length > selected.resources.acts)
|
|
196
|
+
fail('portable context inventory budget');
|
|
197
|
+
const byId = new Map();
|
|
198
|
+
for (const row of rows) {
|
|
199
|
+
if (!row || typeof row.identity !== 'string' || typeof row.context !== 'string' || !row.authority || JSON.stringify(row.authority.anchor) !== JSON.stringify(expectedAnchor) || !Array.isArray(row.authority.history) || !Array.isArray(row.receipts) || byId.has(row.identity))
|
|
200
|
+
fail('portable anchor/shape/identity');
|
|
201
|
+
byId.set(row.identity, row);
|
|
202
|
+
}
|
|
203
|
+
const done = new Map(), visiting = new Set();
|
|
204
|
+
const inspect = (id) => {
|
|
205
|
+
const saved = done.get(id);
|
|
206
|
+
if (saved)
|
|
207
|
+
return saved;
|
|
208
|
+
const row = byId.get(id);
|
|
209
|
+
if (!row || visiting.has(id))
|
|
210
|
+
fail('portable predecessor missing/cyclic');
|
|
211
|
+
visiting.add(id);
|
|
212
|
+
try {
|
|
213
|
+
const context = readPlaceActContext(row.context, selected.inherited.clock), input = { store: '', selector: selected.selector, profileSelector: selected.profileSelector, history: row.authority.history, anchor: expectedAnchor, algorithms: eraOf(foldClaims(row.authority.history.filter(l => fieldOf(l, 'scope').startsWith('wire/'))).standing).admitted, legacyOriginals: row.authority.history, authorityCheckpoint: row.authority.checkpoint, resources: { bytes: selected.resources.bytes, lines: selected.resources.lines, places: selected.resources.places }, inherited: selected.inherited, authenticatedReceiverHeads: [], readCurrent: () => fail('portable inspection has no current Root authority'), historical: () => fail('portable inspection uses declared predecessor graph') };
|
|
214
|
+
const basic = hostClockSelection(input), heads = context.places.map(p => { const initial = basic.trust.initialHeads.find(h => h.place === p.place); if (initial?.identity === p.previous)
|
|
215
|
+
return initial; const parent = inspect(p.previous), place = parent.context.places.find(one => one.place === p.place); if (!place)
|
|
216
|
+
fail('portable predecessor namespace'); return { place: p.place, identity: p.previous, epoch: place.epoch }; });
|
|
217
|
+
const contract = hostClockSelection({ ...input, authenticatedReceiverHeads: heads }).trust.contract, result = placeActReceiptsOf(row.context, row.receipts, contract);
|
|
218
|
+
if (result.identity !== id)
|
|
219
|
+
fail('portable identity differs');
|
|
220
|
+
const value = { contract, context };
|
|
221
|
+
done.set(id, value);
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
visiting.delete(id);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
for (const id of byId.keys())
|
|
229
|
+
inspect(id);
|
|
230
|
+
return [...done].map(([identity, value]) => ({ identity, context: value.context }));
|
|
231
|
+
}
|
|
232
|
+
/** Prospective configuration is checked as authenticated Root history, never
|
|
233
|
+
* against a prose birth condition. Initial heads are explicit typed selections;
|
|
234
|
+
* stored committed chains cannot disappear when a selection is activated or
|
|
235
|
+
* replaced. Preview reads only; the real admission repeats under its Root lock. */
|
|
236
|
+
export function assertProspectiveRootClockSelection(root, signed) {
|
|
237
|
+
const sourceScope = 'wire/place-act-context-source';
|
|
238
|
+
const before = admissionHistoryAt(root, Number.MAX_SAFE_INTEGER);
|
|
239
|
+
const selectors = [sourceScope, 'wire/place-act-context-selection', 'wire/place-act-context'];
|
|
240
|
+
// Ordinary lots need no clock projection, including bootstrap before an
|
|
241
|
+
// admission anchor exists. Raw stored delivery is not authenticated claims.
|
|
242
|
+
if (!before.some(l => fieldOf(l, 'scope') === sourceScope) && !signed.some(l => selectors.includes(fieldOf(l, 'scope'))))
|
|
243
|
+
return;
|
|
244
|
+
const oldLive = authenticatedRootStanding(root).filter(l => fieldOf(l, 'value') !== 'withdraw');
|
|
245
|
+
const oldMapping = oldLive.find(l => fieldOf(l, 'scope') === sourceScope && fieldOf(l, 'measure') === 'text');
|
|
246
|
+
if (oldMapping) {
|
|
247
|
+
const anchor = admissionAnchor(root), algorithms = eraOf(oldLive.filter(l => fieldOf(l, 'scope').startsWith('wire/'))).admitted;
|
|
248
|
+
if (fieldOf(oldMapping, 'by') !== anchor.id || !verifiesOwnerLine(oldMapping, anchor.publicKey, algorithms))
|
|
249
|
+
fail('existing source mapping is not Root signed');
|
|
250
|
+
let old;
|
|
251
|
+
try {
|
|
252
|
+
old = JSON.parse(fieldOf(oldMapping, 'about'));
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return fail('existing source mapping JSON invalid');
|
|
256
|
+
}
|
|
257
|
+
selectors.push(old.selector, old.profileSelector);
|
|
258
|
+
}
|
|
259
|
+
if (!signed.some(l => selectors.includes(fieldOf(l, 'scope'))))
|
|
260
|
+
return;
|
|
261
|
+
if (signed.every(line => before.includes(line)))
|
|
262
|
+
return;
|
|
263
|
+
const store = storeOf(root);
|
|
264
|
+
if (!readOnly())
|
|
265
|
+
rootAuthorityProtection(store);
|
|
266
|
+
const history = [...new Set([...before, ...signed])], live = foldClaims([...oldLive, ...signed].filter(isConfig)).standing.filter(l => fieldOf(l, 'value') !== 'withdraw'), rows = live.filter(l => fieldOf(l, 'scope') === sourceScope && fieldOf(l, 'measure') === 'text');
|
|
267
|
+
if (!rows.length) {
|
|
268
|
+
if (signed.some(l => fieldOf(l, 'scope') === sourceScope && fieldOf(l, 'value') === 'withdraw'))
|
|
269
|
+
return;
|
|
270
|
+
return fail('activation requires source and selection together');
|
|
271
|
+
}
|
|
272
|
+
if (rows.length !== 1)
|
|
273
|
+
fail('prospective source mapping ambiguous');
|
|
274
|
+
const line = rows[0], anchor = admissionAnchor(root), algorithms = eraOf(live.filter(l => fieldOf(l, 'scope').startsWith('wire/'))).admitted;
|
|
275
|
+
if (fieldOf(line, 'by') !== anchor.id || fieldOf(line, 'role') !== 'writes' || fieldOf(line, 'form') !== 'alphabet' || fieldOf(line, 'value') !== 'lock' || !verifiesOwnerLine(line, anchor.publicKey, algorithms))
|
|
276
|
+
fail('prospective mapping is not Root signed');
|
|
277
|
+
const about = fieldOf(line, 'about');
|
|
278
|
+
let selected;
|
|
279
|
+
try {
|
|
280
|
+
selected = JSON.parse(about);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return fail('prospective source mapping JSON invalid');
|
|
284
|
+
}
|
|
285
|
+
if (!selected || Object.keys(selected).sort().join(',') !== 'bootstrapCheckpoint,inherited,profile,profileSelector,resources,selector' || selected.profile !== 'place-act-context@1' || JSON.stringify(selected) !== about)
|
|
286
|
+
fail('unsupported prospective source mapping');
|
|
287
|
+
sourceSelection(selected);
|
|
288
|
+
const positive = (scope, measure) => signed.filter(l => fieldOf(l, 'scope') === scope && fieldOf(l, 'measure') === measure && fieldOf(l, 'value') !== 'withdraw');
|
|
289
|
+
if (!oldMapping && (positive(sourceScope, 'text').length !== 1 || positive(selected.selector, 'text').length !== 1))
|
|
290
|
+
fail('activation requires source and selection together');
|
|
291
|
+
const checkpoint = signed.filter(isConfig).at(-1) ?? fail('prospective authority checkpoint absent');
|
|
292
|
+
if (!oldMapping && positive(selected.bootstrapCheckpoint.scope, selected.bootstrapCheckpoint.measure).length !== 1)
|
|
293
|
+
fail('initial authority checkpoint must land with selection');
|
|
294
|
+
const old = oldMapping ? declaredRootClockSource(root) : undefined;
|
|
295
|
+
const actualLegacy = [...new Set([...before, ...placesIn(root).flatMap(place => appliedHistoryIn(root, place))])];
|
|
296
|
+
const observed = { history, anchor, algorithms, authorityCheckpoint: checkpoint, legacyOriginals: actualLegacy };
|
|
297
|
+
const resolved = hostClockSelection({ store, selector: selected.selector, profileSelector: selected.profileSelector, ...observed, resources: { bytes: selected.resources.bytes, lines: selected.resources.lines, places: selected.resources.places }, inherited: selected.inherited, readCurrent: () => observed, historical: encoded => { if (!old)
|
|
298
|
+
return fail('committed receiver history precedes initial selection'); return old.trust.historical(encoded); } });
|
|
299
|
+
const heads = placeActHeads(store, resolved.trust.initialHeads, resolved.trust, resolved.selection.storageBudget), expected = old ? old.trust.current().heads : resolved.trust.initialHeads;
|
|
300
|
+
for (const head of expected) {
|
|
301
|
+
const actual = heads.find(one => one.place === head.place);
|
|
302
|
+
if (!actual || actual.identity !== head.identity || actual.epoch !== head.epoch)
|
|
303
|
+
fail('prospective selection resets committed receiver ' + head.place);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -24,7 +24,7 @@ export function selectedCapsules(pins, resolve = capsuleAt, select = toposAt, ho
|
|
|
24
24
|
};
|
|
25
25
|
const descriptor = capsuleDescriptor(topos, offer.scope ?? '');
|
|
26
26
|
const loaded = descriptor === undefined ? load() : undefined;
|
|
27
|
-
const
|
|
27
|
+
const supplied = loaded === undefined ? {
|
|
28
28
|
digest: offer.value, selection: `${digest} ${offer.scope}`, ...descriptor, ask: (requests, reader) => {
|
|
29
29
|
if (!requests.length)
|
|
30
30
|
return [];
|
|
@@ -32,7 +32,8 @@ export function selectedCapsules(pins, resolve = capsuleAt, select = toposAt, ho
|
|
|
32
32
|
assertCapsuleDescriptor(descriptor.lines, actual.capsule.lines);
|
|
33
33
|
return actual.capsule.ask(requests, reader);
|
|
34
34
|
},
|
|
35
|
-
};
|
|
35
|
+
} : { ...loaded.capsule, selection: `${digest} ${offer.scope}` };
|
|
36
|
+
const capsule = { ...supplied, selectedWorld: { standing: topos, offer: offer.scope ?? '' } };
|
|
36
37
|
const projected = loaded?.offer ?? canonical(offer);
|
|
37
38
|
if (!providerInputRegions(topos, offer.scope ?? '').length)
|
|
38
39
|
return { capsule, offer: projected };
|
package/dist/land/act.d.ts
CHANGED
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
export declare function underTheRegions<T>(store: string, regions: readonly string[], what: string, act: () => T | Promise<T>): Promise<T>;
|
|
2
|
+
/** Protect the root authority/policy read through child publication. This is
|
|
3
|
+
* exclusion only: callers must authenticate contexts and compare prepared heads
|
|
4
|
+
* while held. It creates no grant, clock advance, lease or automatic recovery. */
|
|
5
|
+
export declare function underAuthorityRegions<T>(store: string, regions: readonly string[], what: string, act: () => T | Promise<T>): Promise<T>;
|
|
6
|
+
/** Protect an authority snapshot without taking a child region. The acquisition
|
|
7
|
+
* gate and temporary root marker exclude a root mutation while registering the
|
|
8
|
+
* shared read; existing child acts continue independently. This is exclusion,
|
|
9
|
+
* never authorization or a clock advance. */
|
|
10
|
+
export declare function underRootAuthority<T>(store: string, what: string, act: () => T | Promise<T>): Promise<T>;
|
|
11
|
+
export interface RootAuthorityProtection {
|
|
12
|
+
readonly store: string;
|
|
13
|
+
readonly lock: string;
|
|
14
|
+
readonly pid: number;
|
|
15
|
+
readonly token: string;
|
|
16
|
+
}
|
|
17
|
+
/** The fixed host may hand its child evidence of an actual held exclusion.
|
|
18
|
+
* It is an operational envelope, not a grant, signer or receipt identity. */
|
|
19
|
+
export declare function rootAuthorityProtection(store: string): RootAuthorityProtection;
|
|
20
|
+
/** expectedStore and expectedParent come from the fixed host's inheritance
|
|
21
|
+
* channel, never from world data. Matching a live lock proves exclusion only;
|
|
22
|
+
* signature, coverage and current context still require verification. */
|
|
23
|
+
export declare function assertRootAuthorityProtection(proof: RootAuthorityProtection, expectedStore: string, expectedParent: number): void;
|
|
24
|
+
/** A child can publish independently, but the joined root projection and its
|
|
25
|
+
* derived ledger appends have one writer. Authority protection already excludes
|
|
26
|
+
* root mutations, so this lock needs no acquisition-gate upgrade. */
|
|
27
|
+
export declare function underRootProjection<T>(store: string, what: string, act: () => T | Promise<T>): Promise<T>;
|
|
2
28
|
export declare const holdRoot: (store: string, what: string) => Promise<void>;
|
|
3
29
|
export declare function underTheLock<T>(store: string, what: string, act: () => T | Promise<T>): Promise<T>;
|
|
4
30
|
export declare const holds: (store: string, region: string) => boolean;
|
package/dist/land/act.js
CHANGED
|
@@ -59,7 +59,7 @@ const lockFiles = (locks) => coordinatesUnder(locks, locks).filter((file) => {
|
|
|
59
59
|
});
|
|
60
60
|
const overlaps = (a, b) => !a || !b || a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
|
|
61
61
|
const treeAt = (store) => join(store, 'lock');
|
|
62
|
-
const named = (at) => (basename(at) === 'lock' ? 'the store' : `region ${basename(at) === ROOT ? '.' : basename(at)}`);
|
|
62
|
+
const named = (at) => (basename(at) === 'lock' ? 'the store' : basename(at).startsWith('@authority-') ? 'root authority' : basename(at) === '@projection' ? 'root projection' : `region ${basename(at) === ROOT ? '.' : basename(at)}`);
|
|
63
63
|
const owns = (at) => {
|
|
64
64
|
for (let frame = context.getStore(); frame !== undefined; frame = frame.parent) {
|
|
65
65
|
const token = frame.locks.get(at);
|
|
@@ -69,6 +69,8 @@ const owns = (at) => {
|
|
|
69
69
|
return false;
|
|
70
70
|
};
|
|
71
71
|
const ownsRegion = (store) => [...held.keys()].some((at) => at.startsWith(`${join(store, 'locks')}/`) && owns(at));
|
|
72
|
+
const ownsAuthority = (store) => [...held.keys()].some((at) => dirname(at) === join(store, 'locks') && basename(at).startsWith('@authority-') && owns(at));
|
|
73
|
+
const projectionAt = (store) => join(store, 'locks', '@projection');
|
|
72
74
|
/** Reentry belongs to an awaited act's context. Another process or async act has no inherited authority. */
|
|
73
75
|
async function hold(at, what) {
|
|
74
76
|
if (owns(at))
|
|
@@ -142,10 +144,12 @@ async function framed(takes, act) {
|
|
|
142
144
|
}
|
|
143
145
|
});
|
|
144
146
|
}
|
|
145
|
-
|
|
147
|
+
function guardedRegions(store, regions, what, act, rootAuthority) {
|
|
146
148
|
return framed(async () => {
|
|
147
149
|
const tree = treeAt(store);
|
|
148
150
|
const asked = [...new Set((regions.length ? regions : ['']).map((one) => one.replace(/\/+$/, '')))];
|
|
151
|
+
if (rootAuthority && (asked.includes('') || (ownsRegion(store) && !owns(tree) && !ownsAuthority(store))))
|
|
152
|
+
throw new Error(`${selfName()}: REFUSE·lock authority dependencies and child regions must be declared together`);
|
|
149
153
|
if (owns(tree) || asked.every((region) => owns(regionAt(store, region)))) {
|
|
150
154
|
for (const region of asked)
|
|
151
155
|
await hold(regionAt(store, region), what);
|
|
@@ -159,18 +163,32 @@ export function underTheRegions(store, regions, what, act) {
|
|
|
159
163
|
try {
|
|
160
164
|
const locks = join(store, 'locks');
|
|
161
165
|
for (;;) {
|
|
162
|
-
const conflict = lockFiles(locks).find((file) =>
|
|
166
|
+
const conflict = lockFiles(locks).find((file) => !owns(join(locks, file))
|
|
167
|
+
&& (file.startsWith('@authority-') || file === '@projection' ? asked.includes('')
|
|
168
|
+
: asked.some((region) => overlaps(region, file) || overlaps(region, coordinateOf(file)))));
|
|
163
169
|
if (conflict === undefined)
|
|
164
170
|
break;
|
|
165
171
|
const at = join(locks, conflict);
|
|
166
172
|
const began = Date.now();
|
|
173
|
+
let told = false;
|
|
167
174
|
for (let holder = holderOf(at); holder !== undefined; holder = holderOf(at)) {
|
|
168
175
|
if (!alive(holder.pid))
|
|
169
176
|
throw new Error(`${selfName()}: REFUSE·stale ${named(at)} belongs to exited process ${holder.pid}; host recovery required`);
|
|
170
177
|
overAMinute(at, began, holder);
|
|
178
|
+
if (!told) {
|
|
179
|
+
process.stderr.write(`WAIT ${named(at)} is held by ${holder.pid} · ${holder.what} since ${holder.since}\n`);
|
|
180
|
+
told = true;
|
|
181
|
+
}
|
|
171
182
|
await pauseFor(250);
|
|
172
183
|
}
|
|
184
|
+
if (told)
|
|
185
|
+
process.stderr.write(`HELD ${named(at)} after ${Date.now() - began} ms waiting\n`);
|
|
173
186
|
}
|
|
187
|
+
// Registered under the same acquisition gate as every legacy writer.
|
|
188
|
+
// Root writes overlap all locks; siblings keep distinct region locks.
|
|
189
|
+
// '@' is encoded for real region coordinates, so this is host bookkeeping.
|
|
190
|
+
if (rootAuthority)
|
|
191
|
+
await hold(join(store, 'locks', `@authority-${randomUUID()}`), what);
|
|
174
192
|
for (const region of asked.filter(Boolean).sort())
|
|
175
193
|
await hold(regionAt(store, region), what);
|
|
176
194
|
if (asked.includes(''))
|
|
@@ -188,6 +206,81 @@ export function underTheRegions(store, regions, what, act) {
|
|
|
188
206
|
}
|
|
189
207
|
}, act);
|
|
190
208
|
}
|
|
209
|
+
export function underTheRegions(store, regions, what, act) {
|
|
210
|
+
return guardedRegions(store, regions, what, act, false);
|
|
211
|
+
}
|
|
212
|
+
/** Protect the root authority/policy read through child publication. This is
|
|
213
|
+
* exclusion only: callers must authenticate contexts and compare prepared heads
|
|
214
|
+
* while held. It creates no grant, clock advance, lease or automatic recovery. */
|
|
215
|
+
export function underAuthorityRegions(store, regions, what, act) {
|
|
216
|
+
return guardedRegions(store, regions, what, act, true);
|
|
217
|
+
}
|
|
218
|
+
/** Protect an authority snapshot without taking a child region. The acquisition
|
|
219
|
+
* gate and temporary root marker exclude a root mutation while registering the
|
|
220
|
+
* shared read; existing child acts continue independently. This is exclusion,
|
|
221
|
+
* never authorization or a clock advance. */
|
|
222
|
+
export function underRootAuthority(store, what, act) {
|
|
223
|
+
return framed(async () => {
|
|
224
|
+
const tree = treeAt(store), root = regionAt(store, '');
|
|
225
|
+
if (owns(tree) || owns(root) || ownsAuthority(store))
|
|
226
|
+
return;
|
|
227
|
+
if (ownsRegion(store))
|
|
228
|
+
throw new Error(`${selfName()}: REFUSE·lock authority dependencies must be declared before entering a region act`);
|
|
229
|
+
await hold(tree, what);
|
|
230
|
+
try {
|
|
231
|
+
await hold(root, what);
|
|
232
|
+
await hold(join(store, 'locks', `@authority-${randomUUID()}`), what);
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
const frame = context.getStore();
|
|
236
|
+
for (const at of [root, tree]) {
|
|
237
|
+
const token = frame.locks.get(at);
|
|
238
|
+
if (token !== undefined) {
|
|
239
|
+
release(at, token);
|
|
240
|
+
frame.locks.delete(at);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}, act);
|
|
245
|
+
}
|
|
246
|
+
/** The fixed host may hand its child evidence of an actual held exclusion.
|
|
247
|
+
* It is an operational envelope, not a grant, signer or receipt identity. */
|
|
248
|
+
export function rootAuthorityProtection(store) {
|
|
249
|
+
const locks = [treeAt(store), regionAt(store, ''), ...held.keys()].filter(at => at === treeAt(store) || at === regionAt(store, '') || dirname(at) === join(store, 'locks') && basename(at).startsWith('@authority-'));
|
|
250
|
+
const lock = locks.find(owns);
|
|
251
|
+
if (lock === undefined)
|
|
252
|
+
throw Error(`${selfName()}: REFUSE·lock root authority is not protected`);
|
|
253
|
+
const holder = holderOf(lock);
|
|
254
|
+
if (holder?.pid !== process.pid || holder.token !== held.get(lock))
|
|
255
|
+
throw Error(`${selfName()}: REFUSE·lock authority protection changed`);
|
|
256
|
+
return Object.freeze({ store, lock, pid: holder.pid, token: holder.token });
|
|
257
|
+
}
|
|
258
|
+
/** expectedStore and expectedParent come from the fixed host's inheritance
|
|
259
|
+
* channel, never from world data. Matching a live lock proves exclusion only;
|
|
260
|
+
* signature, coverage and current context still require verification. */
|
|
261
|
+
export function assertRootAuthorityProtection(proof, expectedStore, expectedParent) {
|
|
262
|
+
if (!proof || proof.store !== expectedStore || !Number.isSafeInteger(expectedParent) || expectedParent <= 0 || proof.pid !== expectedParent
|
|
263
|
+
|| typeof proof.lock !== 'string' || typeof proof.token !== 'string' || !proof.token
|
|
264
|
+
|| !(proof.lock === treeAt(expectedStore) || proof.lock === regionAt(expectedStore, '')
|
|
265
|
+
|| dirname(proof.lock) === join(expectedStore, 'locks') && basename(proof.lock).startsWith('@authority-'))) {
|
|
266
|
+
throw Error(`${selfName()}: REFUSE·lock invalid inherited authority protection`);
|
|
267
|
+
}
|
|
268
|
+
const holder = holderOf(proof.lock);
|
|
269
|
+
if (!alive(expectedParent) || holder?.pid !== proof.pid || holder.token !== proof.token)
|
|
270
|
+
throw Error(`${selfName()}: REFUSE·lock inherited authority protection ended`);
|
|
271
|
+
}
|
|
272
|
+
/** A child can publish independently, but the joined root projection and its
|
|
273
|
+
* derived ledger appends have one writer. Authority protection already excludes
|
|
274
|
+
* root mutations, so this lock needs no acquisition-gate upgrade. */
|
|
275
|
+
export function underRootProjection(store, what, act) {
|
|
276
|
+
return framed(async () => {
|
|
277
|
+
if (owns(treeAt(store)) || owns(regionAt(store, '')))
|
|
278
|
+
return;
|
|
279
|
+
if (!ownsAuthority(store))
|
|
280
|
+
throw new Error(`${selfName()}: REFUSE·lock root projection requires authority protection`);
|
|
281
|
+
await hold(projectionAt(store), what);
|
|
282
|
+
}, act);
|
|
283
|
+
}
|
|
191
284
|
export const holdRoot = async (store, what) => {
|
|
192
285
|
if (!owns(treeAt(store)))
|
|
193
286
|
await hold(regionAt(store, ''), what);
|