@lapxo/bound 0.1.5 → 0.1.6
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 +8 -8
- package/TARGET.bound +8 -7
- package/dist/cli/accept.d.ts +5 -0
- package/dist/cli/accept.js +40 -6
- package/dist/cli/act-result.d.ts +3 -0
- package/dist/cli/act-result.js +48 -0
- package/dist/cli/owner.js +3 -2
- package/dist/cli/verb.js +13 -1
- package/dist/fold/admission-history.d.ts +6 -0
- package/dist/fold/admission-history.js +22 -0
- package/dist/fold/digests.js +13 -9
- package/dist/fold/kept.js +9 -4
- package/dist/fold/keys.js +9 -6
- package/dist/fold/observed.js +8 -7
- package/dist/fold/signed.d.ts +5 -0
- package/dist/fold/signed.js +19 -15
- package/dist/host/capsule.d.ts +2 -1
- package/dist/host/capsule.js +8 -6
- package/dist/host/io.d.ts +1 -1
- package/dist/host/io.js +1 -1
- package/dist/host/ports/ingress.js +4 -28
- package/dist/host/ports/local-act.d.ts +34 -0
- package/dist/host/ports/local-act.js +100 -0
- package/dist/host/ports/publication.d.ts +10 -0
- package/dist/host/ports/publication.js +56 -0
- package/dist/host/reading-closures.d.ts +3 -0
- package/dist/host/reading-closures.js +3 -3
- package/dist/host/selected-capsules.js +4 -4
- package/dist/land/ledger.d.ts +1 -0
- package/dist/land/ledger.js +21 -2
- package/dist/land/owned.d.ts +1 -1
- package/dist/land/owned.js +5 -4
- package/dist/observe/files.js +12 -2
- package/docs/contracts/cli.md +8 -0
- package/package.json +2 -2
- package/receipts.bound +382 -314
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Signer } from '@lapxo/topos/wire';
|
|
2
|
+
/** Local admission history only. Foreign ingress is evidence, never coverage.
|
|
3
|
+
* The clock cutoff is meaningful for configuration; object clocks stay intact. */
|
|
4
|
+
export declare function admissionHistoryAt(root: string, epoch: number): readonly string[];
|
|
5
|
+
/** The existing bootstrap's public root of trust, not today's key selection. */
|
|
6
|
+
export declare function admissionAnchor(root: string): Signer;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { ledgerLines, ledgerWriters, storeOf } from "../land/ledger.js";
|
|
4
|
+
import { fieldOf, isConfig, selfName } from "./claims.js";
|
|
5
|
+
/** Local admission history only. Foreign ingress is evidence, never coverage.
|
|
6
|
+
* The clock cutoff is meaningful for configuration; object clocks stay intact. */
|
|
7
|
+
export function admissionHistoryAt(root, epoch) {
|
|
8
|
+
const store = storeOf(root);
|
|
9
|
+
return [...new Set(ledgerWriters(store).flatMap(writer => ledgerLines(store, writer)))]
|
|
10
|
+
.filter(line => fieldOf(line, 'sig') !== '' && (!isConfig(line) || Number(fieldOf(line, 'epoch')) <= epoch));
|
|
11
|
+
}
|
|
12
|
+
/** The existing bootstrap's public root of trust, not today's key selection. */
|
|
13
|
+
export function admissionAnchor(root) {
|
|
14
|
+
const at = join(storeOf(root), `${selfName()}.keys`);
|
|
15
|
+
const keys = JSON.parse(readFileSync(at, 'utf8'));
|
|
16
|
+
const anchors = Object.values(keys);
|
|
17
|
+
if (anchors.length !== 1 || !anchors[0]?.publicKey || !anchors[0].coverage.length)
|
|
18
|
+
throw Error('REFUSE·act historical admission requires one bootstrap anchor');
|
|
19
|
+
const anchor = anchors[0];
|
|
20
|
+
return { id: anchor.id, keyClass: 'authorize', publicKey: anchor.publicKey, coverage: anchor.coverage,
|
|
21
|
+
depth: { lo: 1, hi: 16 }, admittedBy: [] };
|
|
22
|
+
}
|
package/dist/fold/digests.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { ledgerLines, ledgerWriters } from "../land/ledger.js";
|
|
1
2
|
import { readOnly } from "../host/read-only.js";
|
|
2
|
-
import { appendFileSync, dirname, join, mkdirSync, readdirSync, relative, resolve } from "../host/io.js";
|
|
3
|
+
import { appendFileSync, dirname, join, mkdirSync, readdirSync, realpathSync, relative, resolve } from "../host/io.js";
|
|
3
4
|
import { bytesDigest as bytesWith, fullDigest as fullWith } from "../host/digest.js";
|
|
4
5
|
import { coordinatesUnder, observeFile, observeText, stampOf } from "../observe/files.js";
|
|
5
6
|
import { releaseOf, signedLockLines } from "./keys.js";
|
|
@@ -32,10 +33,8 @@ function namedAlgorithm(store) {
|
|
|
32
33
|
const contract = published === undefined ? undefined : wireLine(published, 'digest-algorithms');
|
|
33
34
|
return contract === undefined ? undefined : fieldOf(contract, 'value').split('|')[0]?.split(':')[0];
|
|
34
35
|
}
|
|
35
|
-
for (const
|
|
36
|
-
|
|
37
|
-
continue;
|
|
38
|
-
const text = observeText(join(store, 'ledger', entry.name)) ?? '';
|
|
36
|
+
for (const writer of ledgerWriters(store)) {
|
|
37
|
+
const text = ledgerLines(store, writer).join('\n');
|
|
39
38
|
let from = 0;
|
|
40
39
|
while (from < text.length) {
|
|
41
40
|
const at = (() => {
|
|
@@ -176,14 +175,19 @@ export function instrumentCoordinates(store, entry, standing = [], root = dirnam
|
|
|
176
175
|
const digest = coordinateDigest(store, at);
|
|
177
176
|
if (digest === undefined)
|
|
178
177
|
return;
|
|
179
|
-
|
|
180
|
-
|
|
178
|
+
// The host loader resolves links before loading a module. Count that module
|
|
179
|
+
// once, while retaining distinct modules even when their bytes are equal.
|
|
180
|
+
const module = realpathSync(at);
|
|
181
|
+
if (seen.has(module))
|
|
182
|
+
return;
|
|
183
|
+
seen.set(module, digest);
|
|
184
|
+
for (const one of importsOf(store, module, digest)) {
|
|
181
185
|
if (one.startsWith('node:') || builtinModules.includes(one))
|
|
182
186
|
continue;
|
|
183
187
|
if (one.startsWith('.'))
|
|
184
|
-
walk(resolve(dirname(
|
|
188
|
+
walk(resolve(dirname(module), one));
|
|
185
189
|
else
|
|
186
|
-
walk(createRequire(
|
|
190
|
+
walk(createRequire(module).resolve(one));
|
|
187
191
|
}
|
|
188
192
|
};
|
|
189
193
|
walk(resolve(entry));
|
package/dist/fold/kept.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { actLines, committedLocalActs } from "../host/ports/local-act.js";
|
|
1
2
|
import { readOnly } from "../host/read-only.js";
|
|
2
3
|
import { createHash, dirname, existsSync, join, mkdirSync, readdirSync, renameSync, writeFileSync } from "../host/io.js";
|
|
3
4
|
import { bytesDigest, coordinateDigest } from "./digests.js";
|
|
4
5
|
import { viewsOf } from "./views.js";
|
|
5
|
-
import { landMemo, ledgerLines, memoAt, shardFile, writerFile } from "../land/ledger.js";
|
|
6
|
+
import { landMemo, ledgerLines, ledgerWriters, memoAt, shardFile, writerFile } from "../land/ledger.js";
|
|
6
7
|
import { coordinatesUnder, observeFile, observeText } from "../observe/files.js";
|
|
7
8
|
import { EXTENSION, fieldOf } from "./claims.js";
|
|
8
9
|
import { keyFor, keyOf, lockSigners } from "./keys.js";
|
|
@@ -36,7 +37,7 @@ export function foldPoints(store, standing, under, keeper, instrument = '', obse
|
|
|
36
37
|
`take ${digestOf(ledgerLines(store, 'take').filter((line) => reads.scopes.has(fieldOf(line, 'scope').replace(/^take\//, ''))))}`,
|
|
37
38
|
`tree ${digestOf(ledgerLines(store, tree ?? '').filter((line) => (sourceOf(line) ?? '').startsWith(reads.place)))}`,
|
|
38
39
|
];
|
|
39
|
-
const heads = (
|
|
40
|
+
const heads = ledgerWriters(store)
|
|
40
41
|
.filter((writer) => writer !== keeper && writer !== 'gate' && !writer.startsWith('reader') && !(reads && (signers.has(writer) || writer === 'judge' || writer === 'take' || writer === tree)))
|
|
41
42
|
.map((writer) => `${writer} ${writer === liveWriter(store, standing) ? lastWord(store, writer) : bytesDigest(store, text(ledgerLines(store, writer).join('\n')))}`);
|
|
42
43
|
const written = [...new Set([...viewsOf(standing).values()].flatMap((view) => ('regions' in view && view.shape ? [view.shape] : [])))].sort()
|
|
@@ -100,8 +101,12 @@ function readingContext(store) {
|
|
|
100
101
|
const speaker = keyOf(store, 'read');
|
|
101
102
|
const files = speaker === undefined ? [] : [writerFile(store, speaker), ...coordinatesUnder(dirname(shardFile(store, speaker, '')), store)
|
|
102
103
|
.filter(file => file.endsWith(EXTENSION)).map(file => join(store, file))];
|
|
103
|
-
|
|
104
|
-
.
|
|
104
|
+
const segments = speaker === undefined ? [] : committedLocalActs(store).flatMap(act => act.bundle.placements?.flatMap(placement => {
|
|
105
|
+
const records = actLines(placement.records).filter(line => fieldOf(line, 'by') === speaker);
|
|
106
|
+
return records.length ? [placement.place + '\n' + records.join('\n')] : [];
|
|
107
|
+
}) ?? []);
|
|
108
|
+
return bytesDigest(store, text([...files.filter(file => existsSync(file)).sort()
|
|
109
|
+
.map(file => bytesDigest(store, observeFile(file) ?? new Uint8Array())), ...segments].join('\n')));
|
|
105
110
|
}
|
|
106
111
|
export function keptPlace(store, under) {
|
|
107
112
|
const key = observeText(memoAt(store, lastOf([`place ${under ?? '.'}`])))?.trim();
|
package/dist/fold/keys.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { dirname, existsSync, join } from "../host/io.js";
|
|
2
|
-
import { ledgerLines, signedLines } from "../land/ledger.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { ledgerLines, ledgerWriters, signedLines } from "../land/ledger.js";
|
|
3
|
+
import { fieldOf, foldClaims, isWire, LOCK, selfName } from "./claims.js";
|
|
4
|
+
import { observeText } from "../observe/files.js";
|
|
5
5
|
const held = new Map();
|
|
6
|
+
const keyInputs = new Map();
|
|
6
7
|
const merged = new Map();
|
|
7
8
|
/**
|
|
8
9
|
* Whose lines carry a class of writing, as the lock says it. Every ledger the store keeps is read, the key claims
|
|
@@ -47,13 +48,15 @@ export function signedLockLines(store, told = []) {
|
|
|
47
48
|
return lockSigners(store, told).flatMap((id) => signedLines(store, id));
|
|
48
49
|
}
|
|
49
50
|
function keysOf(store, told) {
|
|
50
|
-
const
|
|
51
|
+
const input = ledgerKeyLines(store).join('\n');
|
|
52
|
+
const mine = told.length ? read(store, told) : keyInputs.get(store) === input ? held.get(store) ?? read(store) : read(store);
|
|
53
|
+
if (!told.length)
|
|
54
|
+
keyInputs.set(store, input);
|
|
51
55
|
if (!told.length)
|
|
52
56
|
held.set(store, mine);
|
|
53
57
|
return mine;
|
|
54
58
|
}
|
|
55
|
-
const ledgerKeyLines = (store) => (
|
|
56
|
-
.filter((name) => name.endsWith(EXTENSION)).flatMap((name) => ledgerLines(store, name.slice(0, -EXTENSION.length)))
|
|
59
|
+
const ledgerKeyLines = (store) => ledgerWriters(store).flatMap(writer => ledgerLines(store, writer))
|
|
57
60
|
.filter((line) => /\bsig=/.test(line) && /(?:^|\s)scope=keys\/[^/\s]+\s/.test(line));
|
|
58
61
|
const releases = new Map();
|
|
59
62
|
/**
|
package/dist/fold/observed.js
CHANGED
|
@@ -267,13 +267,14 @@ export function observedClaims(root, store, options = {}) {
|
|
|
267
267
|
}
|
|
268
268
|
function observeOne(reader, started) {
|
|
269
269
|
const module = resolve(root, reader.module);
|
|
270
|
+
const lifetime = readerLifetime(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), { id: reader.id, module: reader.module });
|
|
270
271
|
const asked = reader.capsule === undefined ? undefined : reader.capsule.selected ?? capsuleAt(reader.capsule.digest, reader.capsule.entry);
|
|
271
272
|
const projection = reader.capsule === undefined ? [] : (asked?.lines ?? []).filter(line => fieldOf(line, 'scope') === `region/${reader.capsule.region}` && fieldOf(line, 'measure') === 'input-projection' && fieldOf(line, 'value') !== 'withdraw');
|
|
272
273
|
if (projection.length) {
|
|
273
274
|
if (projection.length !== 1 || !asked)
|
|
274
275
|
throw Error('REFUSE·input projection is ambiguous ' + reader.module);
|
|
275
276
|
const files = [...new Set(reader.where.flatMap(where => handedAt(root, reader, roleOf, look, where)))].sort().map(place => ({ place, text: observeText(resolve(root, place)) ?? '' }));
|
|
276
|
-
const result = readingClosures({ root, store, reader: reader.module, speaker, digest: text => bytesDigest(store, Buffer.from(text)), capsule: asked, region: reader.capsule.region, projection: fieldOf(projection[0], 'value'), allowsEmpty: readerAllowsEmpty(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module), files, emit: line => process.stderr.write(line + '\n') });
|
|
277
|
+
const result = readingClosures({ root, store, reader: reader.module, speaker, digest: text => bytesDigest(store, Buffer.from(text)), capsule: asked, region: reader.capsule.region, projection: fieldOf(projection[0], 'value'), readerReference: { id: reader.id, module: reader.module }, lifetime, allowsEmpty: readerAllowsEmpty(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module), files, emit: line => process.stderr.write(line + '\n') });
|
|
277
278
|
out.push(...result.lines);
|
|
278
279
|
ran += result.executed;
|
|
279
280
|
read += result.read - result.opened;
|
|
@@ -287,7 +288,7 @@ export function observedClaims(root, store, options = {}) {
|
|
|
287
288
|
assertReaderInputs(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module, offered.length);
|
|
288
289
|
const vectors = reader.capsule === undefined ? observeText(resolve(root, vectorsOf(reader.module, standing))) : undefined;
|
|
289
290
|
const kept = whole && legacy === undefined && answered === undefined && source ? sha([
|
|
290
|
-
speaker, reader.module, reader.kind, reader.shape.join('|'), reader.where.join('|'), bundle, vectors === undefined ? 'no vectors' : sha(vectors),
|
|
291
|
+
speaker, reader.module, JSON.stringify(lifetime), reader.kind, reader.shape.join('|'), reader.where.join('|'), bundle, vectors === undefined ? 'no vectors' : sha(vectors),
|
|
291
292
|
reader.capsule === undefined ? '' : `${reader.capsule.selected?.selection ?? reader.capsule.digest} ${reader.capsule.region} ${reader.capsule.reads.join('|')} ${[...reader.capsule.globs].map(([place, globs]) => `${place}${globs.join('|')}`).join(' ')}`,
|
|
292
293
|
...offered.map((offer) => `${offer.place} ${offer.digest?.() ?? bytesDigest(store, offer.bytes())}`),
|
|
293
294
|
].join('\n')) : undefined;
|
|
@@ -302,9 +303,9 @@ export function observedClaims(root, store, options = {}) {
|
|
|
302
303
|
const home = regionIn(regions, reader.module);
|
|
303
304
|
const own = shardLines(store, speaker, home).filter((line) => fieldOf(line, 'scope') === `reader/${reader.module}`);
|
|
304
305
|
const lineage = own.filter((line) => fieldOf(line, 'measure') === 'identity').map((line) => fieldOf(line, 'value'));
|
|
305
|
-
const sum = source && reader.capsule === undefined ? bytesDigest(store, new TextEncoder().encode(`${reader.module} ${bundle}`)).replace(/^sha256:/, '').slice(0, 12) : 'none';
|
|
306
|
-
const measured = reader.capsule !== undefined ? { by: source ? sha(`${reader.capsule.selected?.selection ?? reader.capsule.digest}\n${reader.capsule.region}`).slice(0, 12) : 'none', lines: [] }
|
|
307
|
-
: source ? identityOf({ store, root, speaker, module: reader.module, kind: reader.kind, code: sum, lineage, lifetime
|
|
306
|
+
const sum = source && reader.capsule === undefined ? bytesDigest(store, new TextEncoder().encode(`${reader.module} ${bundle}${lifetime === undefined ? "" : "\n" + JSON.stringify(lifetime)}`)).replace(/^sha256:/, '').slice(0, 12) : 'none';
|
|
307
|
+
const measured = reader.capsule !== undefined ? { by: source ? sha(`${reader.capsule.selected?.selection ?? reader.capsule.digest}\n${reader.capsule.region}${lifetime === undefined ? "" : "\n" + JSON.stringify(lifetime)}`).slice(0, 12) : 'none', lines: [] }
|
|
308
|
+
: source ? identityOf({ store, root, speaker, module: reader.module, kind: reader.kind, code: sum, lineage, lifetime, kept: own.filter((line) => /(?:^|\s)measure=(?:extension|emits)\s/.test(line)) })
|
|
308
309
|
: { by: 'none', lines: [] };
|
|
309
310
|
const by = measured.by;
|
|
310
311
|
const level = [...measured.lines];
|
|
@@ -419,12 +420,12 @@ export function observedClaims(root, store, options = {}) {
|
|
|
419
420
|
? asked.ask(due.map((d) => ({
|
|
420
421
|
protocol: PROTOCOL, verb: 'read', rootScope: d.place, region: reader.capsule.region, held: heldOf(d.place, d.region),
|
|
421
422
|
files: (reader.capsule.reads.some((read) => read.includes('@')) ? handedAt(root, reader, roleOf, look, d.place) : [d.place]).map((coordinate) => ({ place: coordinate, text: observeText(resolve(root, coordinate)) ?? '' })),
|
|
422
|
-
}))).map((answer) => {
|
|
423
|
+
})), { id: reader.id, module: reader.module }).map((answer) => {
|
|
423
424
|
if (answer?.kind !== 'fact')
|
|
424
425
|
throw new Error(`REFUSE·reader ${reader.module} · ${answer?.why ?? 'no capsule answer'}`);
|
|
425
426
|
return (answer.claims ?? []);
|
|
426
427
|
})
|
|
427
|
-
: runReader(module, root, due.map((d) => ({ place: d.place, ...(reader.kind === 'js' ? { text: new TextDecoder().decode(d.bytes) } : {}), held: heldOf(d.place, d.region) })),
|
|
428
|
+
: runReader(module, root, due.map((d) => ({ place: d.place, ...(reader.kind === 'js' ? { text: new TextDecoder().decode(d.bytes) } : {}), held: heldOf(d.place, d.region) })), lifetime);
|
|
428
429
|
}
|
|
429
430
|
catch (error) {
|
|
430
431
|
const why = `REFUSE·reader ${reader.module} · ${error instanceof Error ? error.message : String(error)}`;
|
package/dist/fold/signed.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { authorityFor } from './signers.ts';
|
|
1
2
|
/** Lines told to a place's own lock for a preview: folded as if the file held them, and standing only as its signed lines do. */
|
|
2
3
|
export declare const tellOwn: (place: string, lines: readonly string[]) => void;
|
|
3
4
|
/**
|
|
@@ -16,3 +17,7 @@ export declare const admittedIn: (root: string, place: string, line: string) =>
|
|
|
16
17
|
export declare const appliedIn: (root: string, place: string) => readonly string[];
|
|
17
18
|
export declare const forksIn: (root: string, place: string) => readonly string[];
|
|
18
19
|
export declare const writtenIn: (root: string, place: string) => number;
|
|
20
|
+
/** Authenticated history excludes prospective lines told only for admission. */
|
|
21
|
+
export declare const appliedHistoryIn: (root: string, place: string) => readonly string[];
|
|
22
|
+
/** Shared signature/window/coordinate coverage boundary for own history. */
|
|
23
|
+
export declare function ownRecordAdmitted(line: string, place: string, family: string, signers: ReturnType<typeof authorityFor>['admitted'], algorithms: readonly string[]): boolean;
|
package/dist/fold/signed.js
CHANGED
|
@@ -13,7 +13,7 @@ const held = new Map();
|
|
|
13
13
|
const told = new Map();
|
|
14
14
|
/** Lines told to a place's own lock for a preview: folded as if the file held them, and standing only as its signed lines do. */
|
|
15
15
|
export const tellOwn = (place, lines) => void told.set(place, lines);
|
|
16
|
-
function verified(root, place) {
|
|
16
|
+
function verified(root, place, preview = true) {
|
|
17
17
|
const store = storeOf(root);
|
|
18
18
|
const lock = lockLines(store);
|
|
19
19
|
const publicText = observeText(join(root, place, LOCK)) ?? '';
|
|
@@ -21,7 +21,7 @@ function verified(root, place) {
|
|
|
21
21
|
const algorithms = hasAuthority ? signaturesOf(store).admitted : [];
|
|
22
22
|
const signers = algorithms.length ? authorityFor(lock, rootSigner(root), algorithms).admitted : [];
|
|
23
23
|
const history = signers.flatMap(signer => place ? shardLines(store, signer.id, `${place}/${LOCK}`) : ledgerLines(store, signer.id)).filter(line => fieldOf(line, 'sig') !== '');
|
|
24
|
-
const text = [publicText, ...history, ...(told.get(place) ?? [])].join('\n');
|
|
24
|
+
const text = [publicText, ...history, ...(preview ? told.get(place) ?? [] : [])].join('\n');
|
|
25
25
|
const key = `${root} ${place}`;
|
|
26
26
|
const last = held.get(key);
|
|
27
27
|
if (last !== undefined && last.text === text && last.lock === lock)
|
|
@@ -30,19 +30,7 @@ function verified(root, place) {
|
|
|
30
30
|
// Unrestricted coverage does not construct a region coordinate. Restricted
|
|
31
31
|
// coverage still requires the lock's declared namespace before comparison.
|
|
32
32
|
const family = signers.some(signer => !signer.coverage.includes('*')) ? wordOf(lockStanding(store), 'families', 'region') : '';
|
|
33
|
-
const stands = (line) =>
|
|
34
|
-
const got = parse(line);
|
|
35
|
-
if (got.kind !== 'fact' || got.value.fields['sig'] === undefined)
|
|
36
|
-
return false;
|
|
37
|
-
const fields = got.value.fields;
|
|
38
|
-
const signer = signers.find((one) => one.id === fields['by']);
|
|
39
|
-
const epoch = Number(fields['epoch']) || 0;
|
|
40
|
-
if (signer === undefined || !verifiesFields(fields, signer.publicKey, algorithms))
|
|
41
|
-
return false;
|
|
42
|
-
const window = windowAt(signer, epoch);
|
|
43
|
-
return epoch >= window.start && (window.close === null || epoch <= window.close)
|
|
44
|
-
&& signer.coverage.some((prefix) => prefix === '*' || within(region(`${family}/${place}/${fields['scope'] ?? ''}`, './'), region(prefix, './')));
|
|
45
|
-
};
|
|
33
|
+
const stands = (line) => ownRecordAdmitted(line, place, family, signers, algorithms);
|
|
46
34
|
const applied = new Set(lines.filter(stands));
|
|
47
35
|
// A published copy of an authenticated delivery is not another inscription.
|
|
48
36
|
const projected = new Set([...applied].map(sayingOf));
|
|
@@ -75,3 +63,19 @@ export const admittedIn = (root, place, line) => verified(root, place).applied.h
|
|
|
75
63
|
export const appliedIn = (root, place) => [...verified(root, place).applied];
|
|
76
64
|
export const forksIn = (root, place) => ((got) => foldClaims(got.lines.filter((line) => got.applied.has(line) || (!/ sig=/.test(line) && !got.projected.has(sayingOf(line))))).forks.map((fork) => fork.key))(verified(root, place));
|
|
77
65
|
export const writtenIn = (root, place) => ((got) => got.lines.filter((line) => !got.applied.has(line)).length)(verified(root, place));
|
|
66
|
+
/** Authenticated history excludes prospective lines told only for admission. */
|
|
67
|
+
export const appliedHistoryIn = (root, place) => [...verified(root, place, false).applied];
|
|
68
|
+
/** Shared signature/window/coordinate coverage boundary for own history. */
|
|
69
|
+
export function ownRecordAdmitted(line, place, family, signers, algorithms) {
|
|
70
|
+
const got = parse(line);
|
|
71
|
+
if (got.kind !== 'fact' || got.value.fields['sig'] === undefined)
|
|
72
|
+
return false;
|
|
73
|
+
const fields = got.value.fields;
|
|
74
|
+
const signer = signers.find((one) => one.id === fields['by']);
|
|
75
|
+
const epoch = Number(fields['epoch']) || 0;
|
|
76
|
+
if (signer === undefined || !verifiesFields(fields, signer.publicKey, algorithms))
|
|
77
|
+
return false;
|
|
78
|
+
const window = windowAt(signer, epoch);
|
|
79
|
+
return epoch >= window.start && (window.close === null || epoch <= window.close)
|
|
80
|
+
&& signer.coverage.some((prefix) => prefix === '*' || within(region(`${family}/${place}/${fields['scope'] ?? ''}`, './'), region(prefix, './')));
|
|
81
|
+
}
|
package/dist/host/capsule.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReaderReference } from '@lapxo/topos/wire';
|
|
1
2
|
import type { Declaration } from '@lapxo/topos/capsule';
|
|
2
3
|
import type { Request, Response } from '@lapxo/topos/contract';
|
|
3
4
|
export interface Capsule {
|
|
@@ -5,7 +6,7 @@ export interface Capsule {
|
|
|
5
6
|
readonly digest: string;
|
|
6
7
|
readonly declaration: Declaration;
|
|
7
8
|
readonly lines: readonly string[];
|
|
8
|
-
readonly ask: (requests: readonly Request[]) => readonly (Response | undefined)[];
|
|
9
|
+
readonly ask: (requests: readonly Request[], reader?: ReaderReference) => readonly (Response | undefined)[];
|
|
9
10
|
}
|
|
10
11
|
/** A blob is loaded once in an act. A later ask for the same digest reads that capsule, and does not hash or unpack it again. */
|
|
11
12
|
export declare function capsuleAt(digest: string, entry: string, store?: string): Capsule | undefined;
|
package/dist/host/capsule.js
CHANGED
|
@@ -60,10 +60,10 @@ export function capsuleAt(digest, entry, store) {
|
|
|
60
60
|
if (!entry || isAbsolute(entry) || (coordinate === '..' || coordinate.startsWith(`..${sep}`)) || entry.split(/[\\/]/).includes('..')) {
|
|
61
61
|
throw new Error(`REFUSE·capsule ${digest} declared entry ${entry || '(absent)'} unavailable`);
|
|
62
62
|
}
|
|
63
|
-
const capsule = { digest, ...known, ask: (requests) => {
|
|
63
|
+
const capsule = { digest, ...known, ask: (requests, reader) => {
|
|
64
64
|
if (!statSync(module, { throwIfNoEntry: false })?.isFile())
|
|
65
65
|
throw new Error(`REFUSE·capsule ${digest} declared entry ${entry} unavailable`);
|
|
66
|
-
return answered(digest, bytes, requests, names, root, cacheStore, module);
|
|
66
|
+
return answered(digest, bytes, requests, names, root, cacheStore, module, reader);
|
|
67
67
|
} };
|
|
68
68
|
capsules.set(identity, capsule);
|
|
69
69
|
return capsule;
|
|
@@ -86,14 +86,16 @@ const spawnOf = (at, names) => {
|
|
|
86
86
|
return child;
|
|
87
87
|
};
|
|
88
88
|
export const childFlags = (argv = process.execArgv) => argv.filter((flag, i) => !/^(-e|-p|--eval|--print)$/.test(argv[i - 1] ?? '') && !/^(-e|-p|--eval|--print|--input-type)(=|$)/.test(flag));
|
|
89
|
-
function answered(digest, bytes, requests, names, tree, cacheStore = ownStore(), entry) {
|
|
89
|
+
function answered(digest, bytes, requests, names, tree, cacheStore = ownStore(), entry, reader) {
|
|
90
90
|
const child = entry === undefined ? spawnOf(tree ?? unpacked(digest, bytes), names) : [entry];
|
|
91
91
|
if (!child.length) {
|
|
92
92
|
process.stderr.write(`LOAD regions · none of ${names.length} under the directories release/layout names\n`);
|
|
93
93
|
return requests.map(() => undefined);
|
|
94
94
|
}
|
|
95
95
|
const loader = entry !== undefined ? `${createHash('sha256').update(`${relative(tree, entry)}\n`).digest('hex')}\n` : child.length > 1 ? `${createHash('sha256').update(observeFile(child[0]) ?? new Uint8Array()).digest('hex')}\n` : '';
|
|
96
|
-
const
|
|
96
|
+
const lifetime = readerLifetime(contentPolicy().flatMap(line => { const p = parse(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader ?? digest);
|
|
97
|
+
const policy = lifetime === undefined ? '' : JSON.stringify(lifetime) + '\n';
|
|
98
|
+
const hitOf = (request) => sha(`${digest}\n${loader}${policy}${JSON.stringify(request)}`);
|
|
97
99
|
const hits = requests.map(hitOf);
|
|
98
100
|
const kept = hits.map(hit => storeAt(cacheStore, 'cas', 'answers', hit));
|
|
99
101
|
const held = kept.map((at, i) => {
|
|
@@ -114,7 +116,7 @@ function answered(digest, bytes, requests, names, tree, cacheStore = ownStore(),
|
|
|
114
116
|
if (missing.length)
|
|
115
117
|
process.stderr.write(`CAPSULE ${digest.slice(7, 19)} · ${requests.length} asked · ${missing.length} run · ${missing.map((one) => `${one.rootScope}${one.region}`).join(' ')}\n`);
|
|
116
118
|
// Execute and validate the entire batch before committing any answer to the cache.
|
|
117
|
-
const ran = !missing.length ? [] : executeCapsule(process.execPath, [...childFlags(), hostOf(), ...child], missing, {},
|
|
119
|
+
const ran = !missing.length ? [] : executeCapsule(process.execPath, [...childFlags(), hostOf(), ...child], missing, {}, lifetime);
|
|
118
120
|
return requests.map((request, i) => {
|
|
119
121
|
if (held[i] !== undefined)
|
|
120
122
|
return held[i];
|
|
@@ -148,5 +150,5 @@ export function locatedAt(tree, place, cacheStore = ownStore()) {
|
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
152
|
const digest = `sha256:${hash.digest('hex')}`;
|
|
151
|
-
return { digest, ...known, ask: (requests) => answered(digest, new Uint8Array(), requests, names, at, cacheStore) };
|
|
153
|
+
return { digest, ...known, ask: (requests, reader) => answered(digest, new Uint8Array(), requests, names, at, cacheStore, undefined, reader) };
|
|
152
154
|
}
|
package/dist/host/io.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The host's node surface: kernel folders import here, never `node:`. World reads still go through observeFile. */
|
|
2
2
|
export { spawn, spawnSync } from 'node:child_process';
|
|
3
3
|
export { createHash } from 'node:crypto';
|
|
4
|
-
export { appendFileSync, closeSync, existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
export { appendFileSync, closeSync, existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
5
5
|
export { tmpdir } from 'node:os';
|
|
6
6
|
export { basename, dirname, join, relative, resolve } from 'node:path';
|
|
7
7
|
export { fileURLToPath } from 'node:url';
|
package/dist/host/io.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The host's node surface: kernel folders import here, never `node:`. World reads still go through observeFile. */
|
|
2
2
|
export { spawn, spawnSync } from 'node:child_process';
|
|
3
3
|
export { createHash } from 'node:crypto';
|
|
4
|
-
export { appendFileSync, closeSync, existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
export { appendFileSync, closeSync, existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
5
5
|
export { tmpdir } from 'node:os';
|
|
6
6
|
export { basename, dirname, join, relative, resolve } from 'node:path';
|
|
7
7
|
export { fileURLToPath } from 'node:url';
|
|
@@ -1,23 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
3
|
import { requireEffects } from "../read-only.js";
|
|
5
4
|
import { underTheLock } from "../../land/act.js";
|
|
6
|
-
|
|
7
|
-
fsyncSync(fd);
|
|
8
|
-
}
|
|
9
|
-
finally {
|
|
10
|
-
closeSync(fd);
|
|
11
|
-
} };
|
|
12
|
-
const durableFile = (path, text) => { const fd = openSync(path, 'wx', 0o600); try {
|
|
13
|
-
writeFileSync(fd, text);
|
|
14
|
-
fsyncSync(fd);
|
|
15
|
-
}
|
|
16
|
-
finally {
|
|
17
|
-
closeSync(fd);
|
|
18
|
-
} };
|
|
5
|
+
import { nativePublicationStorage, publishBundle } from "./publication.js";
|
|
19
6
|
/** Directory publication makes the pair visible together; there is no later ledger append that can expose half. */
|
|
20
|
-
export const nativeIngressStorage =
|
|
7
|
+
export const nativeIngressStorage = nativePublicationStorage;
|
|
21
8
|
const directory = (store) => join(store, 'ingress');
|
|
22
9
|
const nameOf = (identity) => {
|
|
23
10
|
if (!identity || identity.includes('\0'))
|
|
@@ -58,18 +45,7 @@ export async function commitIngress(store, identity, prepare, verify, storage =
|
|
|
58
45
|
const bundle = await prepare(ingressReceipts(store));
|
|
59
46
|
if (!bundle.evidence || !bundle.receipt || verify(bundle) !== true)
|
|
60
47
|
throw Error('REFUSE·ingress evidence and receipt do not verify');
|
|
61
|
-
|
|
62
|
-
const staged = join(root, '.stage-' + randomUUID());
|
|
63
|
-
mkdirSync(staged, { mode: 0o700 });
|
|
64
|
-
try {
|
|
65
|
-
durableFile(join(staged, 'evidence.bound'), bundle.evidence);
|
|
66
|
-
durableFile(join(staged, 'receipt.bound'), bundle.receipt);
|
|
67
|
-
syncDirectory(staged);
|
|
68
|
-
storage.publish(staged, at);
|
|
69
|
-
}
|
|
70
|
-
finally {
|
|
71
|
-
rmSync(staged, { recursive: true, force: true });
|
|
72
|
-
}
|
|
48
|
+
publishBundle(at, { 'evidence.bound': bundle.evidence, 'receipt.bound': bundle.receipt }, storage);
|
|
73
49
|
return measured('landed', bundle);
|
|
74
50
|
});
|
|
75
51
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
/** Committed segments are part of this ledger, not a second authority source. */
|
|
19
|
+
export declare function committedLocalActs(store: string): readonly {
|
|
20
|
+
identity: string;
|
|
21
|
+
bundle: LocalActBundle;
|
|
22
|
+
}[];
|
|
23
|
+
/** A public result is available only as a verified committed pair.
|
|
24
|
+
* No writer-tail guess, newest-result selection or implicit authority.
|
|
25
|
+
*/
|
|
26
|
+
export declare function localActResult(store: string, identity: string, verify: LocalActVerification): LocalActBundle | undefined;
|
|
27
|
+
/** One publication point for records and their native result. Re-delivery
|
|
28
|
+
* reads the original pair without preparation, signing or a new local epoch.
|
|
29
|
+
* The caller must also integrate this boundary into admission-history readers;
|
|
30
|
+
* this storage port alone does not activate a grammar or land authority.
|
|
31
|
+
*/
|
|
32
|
+
export declare function commitLocalAct(store: string, identity: string, regions: readonly string[], prepare: () => Promise<LocalActBundle>, verify: LocalActVerification, storage?: PublicationStorage): Promise<LocalActBundle & {
|
|
33
|
+
readonly kind: 'landed' | 'once';
|
|
34
|
+
}>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { requireEffects } from "../read-only.js";
|
|
4
|
+
import { underTheRegions } from "../../land/act.js";
|
|
5
|
+
import { publishBundle, nativePublicationStorage } from "./publication.js";
|
|
6
|
+
import { actIdentity, parse, canonical } from '@lapxo/topos/wire';
|
|
7
|
+
import { fullDigest } from "../digest.js";
|
|
8
|
+
const atOf = (store, identity) => {
|
|
9
|
+
if (!identity || identity.includes('\0'))
|
|
10
|
+
throw Error('REFUSE·act missing identity');
|
|
11
|
+
return join(store, 'ledger', 'acts', 'committed-' + encodeURIComponent(identity));
|
|
12
|
+
};
|
|
13
|
+
const read = (at) => {
|
|
14
|
+
try {
|
|
15
|
+
const placements = readdirSync(at).filter(name => name.startsWith('place-') && name.endsWith('.bound')).map(name => ({
|
|
16
|
+
place: decodeURIComponent(name.slice(6, -6)), records: readFileSync(join(at, name), 'utf8'),
|
|
17
|
+
}));
|
|
18
|
+
return {
|
|
19
|
+
records: readFileSync(join(at, 'records.bound'), 'utf8'),
|
|
20
|
+
receipt: readFileSync(join(at, 'receipt.bound'), 'utf8'),
|
|
21
|
+
...(placements.length ? { placements } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
throw Error(`REFUSE·act committed result unreadable · ${error.code ?? 'read failed'}`);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
export const actLines = (bytes) => {
|
|
29
|
+
if (!bytes.endsWith('\n'))
|
|
30
|
+
throw Error('REFUSE·act truncated committed lines');
|
|
31
|
+
return bytes.slice(0, -1).split('\n');
|
|
32
|
+
};
|
|
33
|
+
/** Journal integrity is not authority. Signature/coverage checks remain the
|
|
34
|
+
* same admission boundary; the hash algorithm here is the stored seal's named
|
|
35
|
+
* algorithm, not a new selection of the place's wire.
|
|
36
|
+
*/
|
|
37
|
+
export function intactLocalAct(identity, bundle) {
|
|
38
|
+
if (!bundle.receipt.endsWith('\n') || bundle.receipt.slice(0, -1).includes('\n'))
|
|
39
|
+
return false;
|
|
40
|
+
const receipt = parse(bundle.receipt.slice(0, -1));
|
|
41
|
+
if (receipt.kind !== 'fact' || !bundle.placements?.length)
|
|
42
|
+
return false;
|
|
43
|
+
const f = receipt.value.fields;
|
|
44
|
+
if (canonical(f) !== bundle.receipt.slice(0, -1) || !/^(0|[1-9][0-9]*)$/.test(f['epoch'] ?? ''))
|
|
45
|
+
return false;
|
|
46
|
+
if (f['value'] !== identity || f['role'] !== 'writes' || f['form'] !== 'alphabet' || f['measure'] !== 'digest')
|
|
47
|
+
return false;
|
|
48
|
+
return actIdentity(actLines(bundle.records), { scope: f['scope'] ?? '', context: f['at'] ?? '',
|
|
49
|
+
localEpoch: Number(f['epoch']), digest: bytes => fullDigest(bytes, identity.slice(0, identity.indexOf(':'))),
|
|
50
|
+
placements: bundle.placements.map(p => ({ place: p.place, records: actLines(p.records) })),
|
|
51
|
+
}) === identity;
|
|
52
|
+
}
|
|
53
|
+
/** Committed segments are part of this ledger, not a second authority source. */
|
|
54
|
+
export function committedLocalActs(store) {
|
|
55
|
+
const root = join(store, 'ledger', 'acts');
|
|
56
|
+
if (!existsSync(root))
|
|
57
|
+
return [];
|
|
58
|
+
return readdirSync(root).filter(name => name.startsWith('committed-')).map(name => {
|
|
59
|
+
const identity = decodeURIComponent(name.slice(10)), bundle = read(join(root, name));
|
|
60
|
+
if (!intactLocalAct(identity, bundle))
|
|
61
|
+
throw Error('REFUSE·act committed journal seal does not verify');
|
|
62
|
+
return { identity, bundle };
|
|
63
|
+
}).sort((a, b) => {
|
|
64
|
+
const epoch = (one) => { const p = parse(one.receipt.trim()); return p.kind === 'fact' ? Number(p.value.fields['epoch']) : 0; };
|
|
65
|
+
return epoch(a.bundle) - epoch(b.bundle);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** A public result is available only as a verified committed pair.
|
|
69
|
+
* No writer-tail guess, newest-result selection or implicit authority.
|
|
70
|
+
*/
|
|
71
|
+
export function localActResult(store, identity, verify) {
|
|
72
|
+
const at = atOf(store, identity);
|
|
73
|
+
if (!existsSync(at))
|
|
74
|
+
return undefined;
|
|
75
|
+
const bundle = read(at);
|
|
76
|
+
if (verify(identity, bundle) !== true)
|
|
77
|
+
throw Error('REFUSE·act committed result does not verify');
|
|
78
|
+
return bundle;
|
|
79
|
+
}
|
|
80
|
+
/** One publication point for records and their native result. Re-delivery
|
|
81
|
+
* reads the original pair without preparation, signing or a new local epoch.
|
|
82
|
+
* The caller must also integrate this boundary into admission-history readers;
|
|
83
|
+
* this storage port alone does not activate a grammar or land authority.
|
|
84
|
+
*/
|
|
85
|
+
export async function commitLocalAct(store, identity, regions, prepare, verify, storage = nativePublicationStorage) {
|
|
86
|
+
requireEffects('local act commitment');
|
|
87
|
+
return underTheRegions(store, regions, 'land', async () => {
|
|
88
|
+
const prior = localActResult(store, identity, verify);
|
|
89
|
+
if (prior)
|
|
90
|
+
return { kind: 'once', ...prior };
|
|
91
|
+
const bundle = await prepare();
|
|
92
|
+
if (!bundle.records || !bundle.receipt || verify(identity, bundle) !== true)
|
|
93
|
+
throw Error('REFUSE·act prepared result does not verify');
|
|
94
|
+
const files = { 'records.bound': bundle.records, 'receipt.bound': bundle.receipt };
|
|
95
|
+
for (const p of bundle.placements ?? [])
|
|
96
|
+
files['place-' + encodeURIComponent(p.place) + '.bound'] = p.records;
|
|
97
|
+
publishBundle(atOf(store, identity), files, storage);
|
|
98
|
+
return { kind: 'landed', ...bundle };
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -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;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { closeSync, fsyncSync, mkdirSync, openSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { requireEffects } from "../read-only.js";
|
|
5
|
+
const syncDirectory = (path) => {
|
|
6
|
+
const fd = openSync(path, 'r');
|
|
7
|
+
try {
|
|
8
|
+
fsyncSync(fd);
|
|
9
|
+
}
|
|
10
|
+
finally {
|
|
11
|
+
closeSync(fd);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
export const nativePublicationStorage = {
|
|
15
|
+
publish(staged, committed) { renameSync(staged, committed); syncDirectory(dirname(committed)); },
|
|
16
|
+
};
|
|
17
|
+
/** Publish prepared bytes as one directory. No identity, grammar or authority
|
|
18
|
+
* is inferred here. Readers address the committed name, never staging files.
|
|
19
|
+
* A failure after rename is not evidence of rollback: the caller must retain
|
|
20
|
+
* the uncertainty and allow a verified read of the committed name.
|
|
21
|
+
*/
|
|
22
|
+
export function publishBundle(committed, files, storage = nativePublicationStorage) {
|
|
23
|
+
requireEffects('atomic publication');
|
|
24
|
+
const entries = Object.entries(files);
|
|
25
|
+
if (!entries.length || entries.some(([name]) => !name || name === '.' || name === '..' || /[\\/\0]/.test(name))) {
|
|
26
|
+
throw Error('REFUSE·commit invalid publication members');
|
|
27
|
+
}
|
|
28
|
+
const root = dirname(committed);
|
|
29
|
+
const created = mkdirSync(root, { recursive: true });
|
|
30
|
+
// Persist newly created directory entries up to their existing parent too.
|
|
31
|
+
if (created !== undefined)
|
|
32
|
+
for (let at = root;; at = dirname(at)) {
|
|
33
|
+
syncDirectory(at);
|
|
34
|
+
if (at === dirname(created))
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
const staged = join(root, '.stage-' + randomUUID());
|
|
38
|
+
mkdirSync(staged, { mode: 0o700 });
|
|
39
|
+
try {
|
|
40
|
+
for (const [name, bytes] of entries) {
|
|
41
|
+
const fd = openSync(join(staged, name), 'wx', 0o600);
|
|
42
|
+
try {
|
|
43
|
+
writeFileSync(fd, bytes);
|
|
44
|
+
fsyncSync(fd);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
closeSync(fd);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
syncDirectory(staged);
|
|
51
|
+
storage.publish(staged, committed);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
rmSync(staged, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReaderLifetime, ReaderReference } from '@lapxo/topos/wire';
|
|
1
2
|
import type { Capsule } from './capsule.ts';
|
|
2
3
|
type File = {
|
|
3
4
|
readonly place: string;
|
|
@@ -14,6 +15,8 @@ export declare function readingClosures(options: {
|
|
|
14
15
|
capsule: Capsule;
|
|
15
16
|
region: string;
|
|
16
17
|
projection: string;
|
|
18
|
+
readerReference?: ReaderReference;
|
|
19
|
+
lifetime?: ReaderLifetime;
|
|
17
20
|
allowsEmpty?: boolean;
|
|
18
21
|
files: readonly File[];
|
|
19
22
|
emit?: (line: string) => void;
|