@lapxo/bound 0.1.0 → 0.1.2
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 +12 -10
- package/TARGET.bound +64 -42
- package/dist/cli/accept.js +27 -18
- package/dist/cli/args.js +9 -2
- package/dist/cli/names.d.ts +1 -1
- package/dist/cli/names.js +2 -2
- package/dist/cli/verb.js +32 -22
- package/dist/fold/configures.js +2 -1
- package/dist/fold/kept.js +15 -4
- package/dist/fold/observed.js +5 -1
- package/dist/fold/reach.d.ts +5 -0
- package/dist/fold/reach.js +20 -2
- package/dist/fold/readers.js +5 -1
- package/dist/fold/signed.js +5 -3
- package/dist/fold/wire.js +6 -1
- package/dist/host/ports/signing.d.ts +31 -0
- package/dist/host/ports/signing.js +74 -0
- package/dist/host/provide.d.ts +2 -2
- package/dist/host/provide.js +3 -3
- package/dist/host/release.d.ts +3 -1
- package/dist/host/release.js +33 -13
- package/dist/host/selected-capsules.d.ts +1 -1
- package/dist/host/selected-capsules.js +8 -4
- package/dist/host/selected-topos.d.ts +4 -0
- package/dist/host/selected-topos.js +23 -0
- package/dist/host/signing.d.ts +3 -0
- package/dist/host/signing.js +42 -0
- package/dist/host/source-content.d.ts +1 -1
- package/dist/host/source-content.js +13 -3
- package/dist/land/sign.d.ts +9 -10
- package/dist/land/sign.js +31 -8
- package/dist/observe/regions.js +2 -2
- package/dist/observe/runner.js +5 -2
- package/dist/render/view.js +2 -1
- package/docs/usage.md +3 -3
- package/package.json +19 -3
- package/receipts.bound +89 -57
package/dist/host/release.js
CHANGED
|
@@ -2,17 +2,31 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { existsSync, lstatSync, mkdirSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, sep } from 'node:path';
|
|
4
4
|
import { canonical } from '@lapxo/topos/wire';
|
|
5
|
-
import { fieldOf, isWire } from "../fold/claims.js";
|
|
5
|
+
import { fieldOf, isWire, isConfig } from "../fold/claims.js";
|
|
6
6
|
import { archiveCoordinate, archiveDestination, members } from "./archive.js";
|
|
7
7
|
import { blobAt, holdStore, landBlob, replaceWhole, storeAt } from "../land/ledger.js";
|
|
8
8
|
import { coordinatesUnder, observeFile, observeText } from "../observe/files.js";
|
|
9
9
|
export const laidAt = (store, digest) => storeAt(store, 'cas', 'laid', digest.replace(/^[^:]*:/, ''));
|
|
10
|
-
|
|
10
|
+
/** A host artifact binding, not a measured ceiling or an object act. */
|
|
11
|
+
export const isReleaseBinding = (line) => isConfig(line)
|
|
12
|
+
&& fieldOf(line, 'scope').startsWith('dep/') && fieldOf(line, 'role') === 'reads'
|
|
13
|
+
&& fieldOf(line, 'value') !== 'withdraw'
|
|
14
|
+
&& (fieldOf(line, 'measure') === 'digest' || /^[a-zA-Z][a-zA-Z0-9-]*:[a-f0-9]+$/.test(fieldOf(line, 'value')));
|
|
15
|
+
export const releasesOf = (lock) => lock.filter(isReleaseBinding);
|
|
11
16
|
const laidMark = '.laid';
|
|
12
17
|
const digestOf = (bytes) => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
13
18
|
const hashed = (digest, bytes) => createHash(digest.includes(':') ? digest.slice(0, digest.indexOf(':')) : 'sha256').update(bytes).digest('hex') === digest.replace(/^[^:]*:/, '');
|
|
14
19
|
const snapshotOf = (text) => text.split('\n').filter(isWire).filter((line) => fieldOf(line, 'measure') === 'digest' && fieldOf(line, 'scope').startsWith('file/'))
|
|
15
20
|
.map((line) => [fieldOf(line, 'scope').slice('file/'.length), fieldOf(line, 'value')]);
|
|
21
|
+
/** Mount an explicitly declared archive coordinate; never infer a package root. */
|
|
22
|
+
function mountedFiles(files, root) {
|
|
23
|
+
if (!root)
|
|
24
|
+
return files;
|
|
25
|
+
const prefix = archiveCoordinate(root) + '/';
|
|
26
|
+
if (!files.length || files.some(([name]) => !name.startsWith(prefix)))
|
|
27
|
+
throw new Error(`REFUSE·release archive root ${root} does not contain every member`);
|
|
28
|
+
return files.map(([name, digest]) => [archiveCoordinate(name.slice(prefix.length)), digest]);
|
|
29
|
+
}
|
|
16
30
|
function snapshotLines(files) {
|
|
17
31
|
return files.map(([coord, digest]) => canonical({
|
|
18
32
|
scope: `file/${coord}`, role: 'writes', form: 'alphabet', measure: 'digest', value: digest, at: `place:${digest}`, by: 'target',
|
|
@@ -110,6 +124,11 @@ function keepSnapshot(store, digest, bytes) {
|
|
|
110
124
|
export function layReleases(store, own, lock, contentStore = own) {
|
|
111
125
|
let laid = 0;
|
|
112
126
|
for (const line of releasesOf(lock)) {
|
|
127
|
+
// A release catalog entry without a mount is materialized only when requested.
|
|
128
|
+
// Its absence must not block an unrelated place's fold.
|
|
129
|
+
const needs = fieldOf(line, 'needs');
|
|
130
|
+
if (!needs)
|
|
131
|
+
continue;
|
|
113
132
|
const digest = fieldOf(line, 'value');
|
|
114
133
|
const at = laidAt(store, digest);
|
|
115
134
|
const dir = lstatSync(at, { throwIfNoEntry: false })?.isDirectory() ?? false;
|
|
@@ -125,20 +144,18 @@ export function layReleases(store, own, lock, contentStore = own) {
|
|
|
125
144
|
throw new Error(`REFUSE·release snapshot for ${digest} omits or changes archive members; explicit rebuild required`);
|
|
126
145
|
if (bytes === undefined && !dir && current === undefined)
|
|
127
146
|
throw new Error(`REFUSE·release ${fieldOf(line, 'scope')} has no available archive or snapshot`);
|
|
128
|
-
const
|
|
129
|
-
if (!needs)
|
|
130
|
-
continue;
|
|
147
|
+
const root = fieldOf(line, 'shape');
|
|
131
148
|
const place = join(own, archiveCoordinate(needs));
|
|
132
149
|
const link = lstatSync(place, { throwIfNoEntry: false });
|
|
133
150
|
if (link?.isSymbolicLink() && !realpathSync(place).includes(`${sep}cas${sep}laid${sep}`))
|
|
134
151
|
continue;
|
|
135
152
|
if (dir) {
|
|
136
|
-
const files = bytes === undefined ? undefined : members(bytes).map((one) => [one.name, digestOf(one.bytes)]);
|
|
153
|
+
const files = bytes === undefined ? undefined : mountedFiles(members(bytes).map((one) => [one.name, digestOf(one.bytes)]), root);
|
|
137
154
|
if (!lstatSync(place, { throwIfNoEntry: false })?.isSymbolicLink() && observeText(join(place, laidMark)) === digest && files !== undefined && layoutMatches(place, files))
|
|
138
155
|
continue;
|
|
139
156
|
const next = nextPlace(place);
|
|
140
157
|
try {
|
|
141
|
-
layDir(at, next);
|
|
158
|
+
layDir(root ? join(at, archiveCoordinate(root)) : at, next);
|
|
142
159
|
swapPlace(place, next, digest);
|
|
143
160
|
}
|
|
144
161
|
catch (error) {
|
|
@@ -151,11 +168,12 @@ export function layReleases(store, own, lock, contentStore = own) {
|
|
|
151
168
|
const snap = observeText(at);
|
|
152
169
|
if (snap === undefined)
|
|
153
170
|
throw new Error(`REFUSE·release ${fieldOf(line, 'scope')} has no available snapshot`);
|
|
154
|
-
|
|
171
|
+
const mounted = mountedFiles(snapshotOf(snap), root);
|
|
172
|
+
if (!lstatSync(place, { throwIfNoEntry: false })?.isSymbolicLink() && observeText(join(place, laidMark)) === digest && layoutMatches(place, mounted))
|
|
155
173
|
continue;
|
|
156
174
|
const next = nextPlace(place);
|
|
157
175
|
try {
|
|
158
|
-
layFiles(store, contentStore,
|
|
176
|
+
layFiles(store, contentStore, mounted, next);
|
|
159
177
|
swapPlace(place, next, digest);
|
|
160
178
|
}
|
|
161
179
|
catch (error) {
|
|
@@ -180,12 +198,12 @@ export const resolvedOf = (store, own, lock, contentStore = own) => releasesOf(l
|
|
|
180
198
|
const historicalDirectory = lstatSync(at, { throwIfNoEntry: false })?.isDirectory() === true;
|
|
181
199
|
const snapshot = observeText(at);
|
|
182
200
|
const complete = expected !== undefined && (needs
|
|
183
|
-
? layoutMatches(place, expected)
|
|
201
|
+
? layoutMatches(place, mountedFiles(expected, fieldOf(line, 'shape')))
|
|
184
202
|
: historicalDirectory ? layoutMatches(at, expected) : snapshot !== undefined && sameSnapshot(snapshotOf(snapshot), expected));
|
|
185
203
|
return ok && laid && complete ? [canonical({ scope: `resolved/${fieldOf(line, 'scope').slice('dep/'.length)}`, role: 'writes', form: 'alphabet', measure: 'digest', value: digest, at: `place:${digest}`, by: 'target' })] : [];
|
|
186
204
|
});
|
|
187
205
|
/** A host loads verified release bytes from a runtime tree, never interprets a snapshot file as a directory. */
|
|
188
|
-
export function runtimeTreeAt(store, own, digest, contentStore = own) {
|
|
206
|
+
export function runtimeTreeAt(store, own, digest, contentStore = own, root = '') {
|
|
189
207
|
const named = /^([a-zA-Z0-9-]+):([a-fA-F0-9]+)$/.exec(digest);
|
|
190
208
|
if (named === null)
|
|
191
209
|
throw new Error(`REFUSE·release invalid digest ${digest}`);
|
|
@@ -195,9 +213,11 @@ export function runtimeTreeAt(store, own, digest, contentStore = own) {
|
|
|
195
213
|
if (!hashed(digest, bytes))
|
|
196
214
|
throw new Error(`REFUSE·release ${digest} does not name its available bytes`);
|
|
197
215
|
const files = members(bytes).map((one) => [one.name, digestOf(one.bytes)]);
|
|
216
|
+
mountedFiles(files, root); // Validate the same declared projection used for installation.
|
|
198
217
|
const place = storeAt(store, 'cas', 'runtime', named[1], named[2]);
|
|
218
|
+
const selected = root ? join(place, archiveCoordinate(root)) : place;
|
|
199
219
|
if (observeText(join(place, laidMark)) === digest && layoutMatches(place, files))
|
|
200
|
-
return
|
|
220
|
+
return selected;
|
|
201
221
|
for (const member of members(bytes))
|
|
202
222
|
landBlob(store, digestOf(member.bytes), member.bytes);
|
|
203
223
|
const next = nextPlace(place);
|
|
@@ -209,5 +229,5 @@ export function runtimeTreeAt(store, own, digest, contentStore = own) {
|
|
|
209
229
|
rmSync(next, { recursive: true, force: true });
|
|
210
230
|
throw error;
|
|
211
231
|
}
|
|
212
|
-
return
|
|
232
|
+
return selected;
|
|
213
233
|
}
|
|
@@ -2,7 +2,7 @@ import { capsuleAt } from './capsule.ts';
|
|
|
2
2
|
import type { Capsule } from './capsule.ts';
|
|
3
3
|
import type { ToposResolver } from './selected-topos.ts';
|
|
4
4
|
/** Only explicit capsule offers request execution. Forms/classes/views do not need an executable alias. */
|
|
5
|
-
export declare function selectedCapsules(pins: readonly string[], resolve?: typeof capsuleAt, select?: ToposResolver): readonly {
|
|
5
|
+
export declare function selectedCapsules(pins: readonly string[], resolve?: typeof capsuleAt, select?: ToposResolver, host?: readonly string[]): readonly {
|
|
6
6
|
readonly capsule: Capsule;
|
|
7
7
|
readonly offer: string;
|
|
8
8
|
}[];
|
|
@@ -4,13 +4,17 @@ import { selectedTopoi, toposAt } from "./selected-topos.js";
|
|
|
4
4
|
import { canonical } from '@lapxo/topos/wire';
|
|
5
5
|
import { fieldOf } from "../fold/claims.js";
|
|
6
6
|
import { ownLock } from "../observe/runner.js";
|
|
7
|
+
import { archiveCoordinate } from "./archive.js";
|
|
7
8
|
/** Only explicit capsule offers request execution. Forms/classes/views do not need an executable alias. */
|
|
8
|
-
export function selectedCapsules(pins, resolve = capsuleAt, select = toposAt) {
|
|
9
|
+
export function selectedCapsules(pins, resolve = capsuleAt, select = toposAt, host = ownLock()) {
|
|
9
10
|
return selectedTopoi(pins, select).flatMap(({ digest, topos }) => capsuleOffers(topos).map(offer => {
|
|
10
|
-
const projections =
|
|
11
|
-
if (projections.length
|
|
11
|
+
const projections = host.filter(line => fieldOf(line, 'scope').startsWith('dep/') && fieldOf(line, 'role') === 'writes' && fieldOf(line, 'value') === offer.value);
|
|
12
|
+
if (!projections.length || projections.some(line => !fieldOf(line, 'shape')))
|
|
12
13
|
throw Error(`REFUSE·pin ${digest} capsule ${offer.value} needs one host projection`);
|
|
13
|
-
const
|
|
14
|
+
const entries = [...new Set(projections.map(line => archiveCoordinate(fieldOf(line, 'shape'))))];
|
|
15
|
+
if (entries.length !== 1)
|
|
16
|
+
throw Error(`REFUSE·pin ${digest} capsule ${offer.value} has conflicting host projections`);
|
|
17
|
+
const projected = { ...offer, shape: entries[0] };
|
|
14
18
|
const capsule = resolve(offer.value, projected.shape);
|
|
15
19
|
if (capsule === undefined)
|
|
16
20
|
throw Error(`REFUSE·pin ${digest} · ${offer.scope} · offered capsule ${offer.value} unavailable`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { StandingTopos } from '@lapxo/topos/standing';
|
|
2
|
+
import { wireAt } from '@lapxo/topos/wire';
|
|
2
3
|
export interface SelectedTopos {
|
|
3
4
|
readonly digest: string;
|
|
4
5
|
readonly topos: StandingTopos;
|
|
@@ -15,3 +16,6 @@ export declare function selectedTopoi(pins: readonly string[], resolve?: ToposRe
|
|
|
15
16
|
* Independent declarations must agree; selection order cannot choose a contract.
|
|
16
17
|
*/
|
|
17
18
|
export declare function selectedWireList(local: readonly string[], epoch: number, pins: readonly string[], list: string, resolve?: ToposResolver): readonly string[];
|
|
19
|
+
/** Complete an admission snapshot from the selected standing. Local declarations
|
|
20
|
+
* govern their own lists; independent selected contracts cannot choose by order. */
|
|
21
|
+
export declare function selectedWireAt(local: readonly Readonly<Record<string, string>>[], epoch: number, pins: readonly string[], resolve?: ToposResolver): ReturnType<typeof wireAt>;
|
|
@@ -59,3 +59,26 @@ export function selectedWireList(local, epoch, pins, list, resolve = toposAt) {
|
|
|
59
59
|
throw Error(`REFUSE·wire wire/${list} has conflicting contracts · ${contracts.map(one => one.name).join(' · ')}`);
|
|
60
60
|
return first.words;
|
|
61
61
|
}
|
|
62
|
+
/** Complete an admission snapshot from the selected standing. Local declarations
|
|
63
|
+
* govern their own lists; independent selected contracts cannot choose by order. */
|
|
64
|
+
export function selectedWireAt(local, epoch, pins, resolve = toposAt) {
|
|
65
|
+
const own = wireAt(local, epoch);
|
|
66
|
+
const worlds = selectedTopoi(pins, resolve).map(({ digest, topos }) => ({ digest, parts: topos.parts, wire: wireAt(topos.parts, Number.MAX_SAFE_INTEGER) }));
|
|
67
|
+
const key = (scope) => scope.startsWith('wire/') ? scope.slice(5) : scope.startsWith('audit/wire/') ? scope.slice(11) : undefined;
|
|
68
|
+
const declared = new Set(local.filter(f => Number(f.epoch ?? 0) <= epoch).map(f => key(f.scope ?? '')).filter((k) => k !== undefined));
|
|
69
|
+
const selected = new Map();
|
|
70
|
+
for (const world of worlds)
|
|
71
|
+
for (const f of world.parts) {
|
|
72
|
+
const name = key(f.scope ?? '');
|
|
73
|
+
if (name === undefined || declared.has(name))
|
|
74
|
+
continue;
|
|
75
|
+
const list = world.wire?.lists.get(name);
|
|
76
|
+
const value = list ? [...list].sort().join('|') : f.value ?? '';
|
|
77
|
+
const previous = selected.get(name);
|
|
78
|
+
if (previous && previous.value !== value)
|
|
79
|
+
throw Error(`REFUSE·wire wire/${name} has conflicting selected contracts · ${previous.digest} · ${world.digest}`);
|
|
80
|
+
if (!previous)
|
|
81
|
+
selected.set(name, { value, fields: f, digest: world.digest });
|
|
82
|
+
}
|
|
83
|
+
return selected.size ? wireAt([...selected.values()].map(one => one.fields).concat(local), epoch) : own;
|
|
84
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { SigningLot } from './ports/signing.ts';
|
|
2
|
+
/** Resolve data before acquisition; all mechanisms use one wire protocol and verification path. */
|
|
3
|
+
export declare function signerFor(lines: readonly string[], keyId: string, selected?: string, keyFile?: string): (lot: SigningLot) => Promise<readonly import("./ports/signing.ts").SignatureRecord[]>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { parse, signerSelection, signerRequestBytes, signerResponseBytes, signerResponseOf } from '@lapxo/topos/wire';
|
|
3
|
+
import { invokeSigner, SigningFailure } from "./ports/signing.js";
|
|
4
|
+
/** An explicitly supplied host registry; a repository cannot supply executable bindings. */
|
|
5
|
+
function binding(name, keyFile) {
|
|
6
|
+
if (name === 'file') {
|
|
7
|
+
if (!keyFile)
|
|
8
|
+
throw new SigningFailure('REFUSE·signer file needs --key-file');
|
|
9
|
+
return { kind: 'file', path: keyFile };
|
|
10
|
+
}
|
|
11
|
+
if (keyFile)
|
|
12
|
+
throw new SigningFailure('REFUSE·signer command cannot use --key-file');
|
|
13
|
+
const path = process.env['BOUND_SIGNERS'];
|
|
14
|
+
if (!path)
|
|
15
|
+
throw new SigningFailure('REFUSE·signer no host binding registry');
|
|
16
|
+
try {
|
|
17
|
+
const registry = JSON.parse(readFileSync(path, 'utf8'));
|
|
18
|
+
if (!registry || typeof registry !== 'object' || !Object.prototype.hasOwnProperty.call(registry, name))
|
|
19
|
+
throw Error();
|
|
20
|
+
const record = registry[name];
|
|
21
|
+
if (!record || typeof record !== 'object')
|
|
22
|
+
throw Error();
|
|
23
|
+
const f = record;
|
|
24
|
+
if (f['kind'] !== 'command' || typeof f['executable'] !== 'string' || !f['executable']
|
|
25
|
+
|| !Array.isArray(f['args']) || !f['args'].every(a => typeof a === 'string'))
|
|
26
|
+
throw Error();
|
|
27
|
+
return { kind: 'command', executable: f['executable'], args: f['args'] };
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new SigningFailure('REFUSE·signer host binding unavailable or invalid');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Resolve data before acquisition; all mechanisms use one wire protocol and verification path. */
|
|
34
|
+
export function signerFor(lines, keyId, selected, keyFile) {
|
|
35
|
+
const fields = lines.flatMap(line => { const got = parse(line); return got.kind === 'fact' ? [got.value.fields] : []; });
|
|
36
|
+
const declaration = signerSelection(fields, keyId);
|
|
37
|
+
if (selected !== undefined && selected !== declaration.name)
|
|
38
|
+
throw new SigningFailure(`REFUSE·signer ${keyId} CLI selection conflicts with declaration`);
|
|
39
|
+
const mechanism = binding(declaration.name, keyFile);
|
|
40
|
+
const protocol = { request: signerRequestBytes, response: signerResponseBytes, reply: signerResponseOf };
|
|
41
|
+
return (lot) => invokeSigner(lot, mechanism, protocol, declaration);
|
|
42
|
+
}
|
|
@@ -7,4 +7,4 @@ export interface ContentPort {
|
|
|
7
7
|
/** Transport uses declared locations; only the requested digest admits bytes into the cache. */
|
|
8
8
|
export declare function resolveSelectedContent(lines: readonly string[], port: ContentPort): Promise<readonly string[]>;
|
|
9
9
|
/** Exact resource URLs; no repository checkout or inferred release. */
|
|
10
|
-
export declare function resolveSources(standing: readonly string[]): Promise<void>;
|
|
10
|
+
export declare function resolveSources(standing: readonly string[], materializeInstrument?: boolean): Promise<void>;
|
|
@@ -8,7 +8,10 @@ import { ownLock, ownStore } from "../observe/runner.js";
|
|
|
8
8
|
export async function resolveSelectedContent(lines, port) {
|
|
9
9
|
const fields = lines.flatMap(line => { const p = parse(line); return p.kind === 'fact' && p.value.fields.value !== 'withdraw' ? [p.value.fields] : []; });
|
|
10
10
|
const selectionIndex = (scope) => { const match = /(^|\/)uses\//.exec(scope); return match === null ? -1 : match.index + match[1].length; };
|
|
11
|
-
|
|
11
|
+
// A dependency constraint is not a content binding. Historical artifact records
|
|
12
|
+
// use measure=id; explicit digest records still validate even when malformed.
|
|
13
|
+
const bindings = fields.filter(f => selectionIndex(f.scope ?? '') >= 0 ||
|
|
14
|
+
(f.scope?.startsWith('dep/') && (f.measure === 'digest' || /^[a-zA-Z][a-zA-Z0-9-]*:[a-f0-9]+$/.test(f.value ?? ''))));
|
|
12
15
|
const locations = new Map();
|
|
13
16
|
for (const binding of bindings) {
|
|
14
17
|
const scope = binding.scope ?? '';
|
|
@@ -73,12 +76,19 @@ export async function resolveSelectedContent(lines, port) {
|
|
|
73
76
|
return received;
|
|
74
77
|
}
|
|
75
78
|
/** Exact resource URLs; no repository checkout or inferred release. */
|
|
76
|
-
export async function resolveSources(standing) {
|
|
79
|
+
export async function resolveSources(standing, materializeInstrument = false) {
|
|
77
80
|
const instrument = ownLock().flatMap(line => { const p = parse(line); return p.kind === 'fact' ? [p.value.fields] : []; });
|
|
78
81
|
const wire = wireAt(instrument, Number.MAX_SAFE_INTEGER);
|
|
79
82
|
if (wire === null)
|
|
80
83
|
throw Error('REFUSE·pin instrument has no admitted wire');
|
|
81
|
-
const
|
|
84
|
+
const bindings = ownLock().filter(line => {
|
|
85
|
+
const p = parse(line);
|
|
86
|
+
if (p.kind !== 'fact')
|
|
87
|
+
return false;
|
|
88
|
+
const fields = p.value.fields;
|
|
89
|
+
return fields.scope?.startsWith('sources/') || (fields.scope?.startsWith('dep/') && (fields.role !== 'reads' || (materializeInstrument && !!fields.needs)));
|
|
90
|
+
});
|
|
91
|
+
const received = await resolveSelectedContent([...standing, ...bindings], {
|
|
82
92
|
algorithms: wire.digests,
|
|
83
93
|
read: digest => observeFile(blobAt(ownStore(), digest)),
|
|
84
94
|
write: (digest, bytes) => { landBlob(ownStore(), digest, bytes); },
|
package/dist/land/sign.d.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
type
|
|
1
|
+
import type { SigningLot, SignatureRecord } from '../host/ports/signing.ts';
|
|
2
|
+
/** Prepare and verify a whole lot. No key material and no output/admission side effects. */
|
|
3
|
+
export declare function signConsentLot(lines: readonly string[], stamp: {
|
|
4
|
+
readonly epoch?: number;
|
|
5
|
+
readonly by: string;
|
|
6
|
+
readonly algorithm: string;
|
|
7
|
+
}, publicKey: string, admitted: readonly string[], invoke: (lot: SigningLot) => Promise<readonly SignatureRecord[]>): Promise<{
|
|
2
8
|
readonly kind: 'fact';
|
|
3
|
-
readonly
|
|
9
|
+
readonly lines: readonly string[];
|
|
4
10
|
} | {
|
|
5
11
|
readonly kind: 'refuse';
|
|
6
12
|
readonly why: string;
|
|
7
|
-
}
|
|
8
|
-
/** A consent line (by=target, no sig) becomes the owner's claim: stamped with its epoch, then signed over the canonical bytes. */
|
|
9
|
-
export declare function signConsentLine(line: string, privateKeyPem: string, stamp: {
|
|
10
|
-
readonly epoch?: number;
|
|
11
|
-
readonly by: string;
|
|
12
|
-
readonly algorithm: string;
|
|
13
|
-
}): Signed;
|
|
13
|
+
}>;
|
|
14
14
|
export declare function verifiesOwnerLine(line: string, publicKeyPem: string, admitted: readonly string[]): boolean;
|
|
15
15
|
export declare function verifiesFields(fields: Readonly<Record<string, string>>, publicKey: string, admitted: readonly string[]): boolean;
|
|
16
|
-
export {};
|
package/dist/land/sign.js
CHANGED
|
@@ -1,21 +1,44 @@
|
|
|
1
1
|
import { canonical, parse, parseForSigning, signedBytes, signedFieldsOf } from '@lapxo/topos/wire';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { verifyBytes } from "../host/adapters/keys/ed25519.js";
|
|
3
|
+
import { SigningFailure } from "../host/ports/signing.js";
|
|
4
|
+
function prepareConsentLine(line, stamp) {
|
|
5
5
|
const got = parseForSigning(line);
|
|
6
6
|
if (got.kind !== 'fact')
|
|
7
|
-
return { kind: 'refuse', why:
|
|
7
|
+
return { kind: 'refuse', why: 'REFUSE·accept not a claim line' };
|
|
8
8
|
const fields = got.value.fields;
|
|
9
9
|
const scope = fields['scope'] ?? '?';
|
|
10
10
|
if (fields['sig'] !== undefined)
|
|
11
11
|
return { kind: 'refuse', why: `REFUSE·accept ${scope} is already signed` };
|
|
12
|
-
if (fields['by'] !== 'target')
|
|
13
|
-
return { kind: 'refuse', why: `REFUSE·accept ${scope}
|
|
14
|
-
}
|
|
12
|
+
if (fields['by'] !== 'target')
|
|
13
|
+
return { kind: 'refuse', why: `REFUSE·accept ${scope} must be drafted by=target` };
|
|
15
14
|
const covered = signedFieldsOf(fields, stamp.by);
|
|
16
15
|
if (stamp.epoch !== undefined)
|
|
17
16
|
covered['epoch'] = String(stamp.epoch);
|
|
18
|
-
return { kind: 'fact',
|
|
17
|
+
return { kind: 'fact', fields: covered };
|
|
18
|
+
}
|
|
19
|
+
/** Prepare and verify a whole lot. No key material and no output/admission side effects. */
|
|
20
|
+
export async function signConsentLot(lines, stamp, publicKey, admitted, invoke) {
|
|
21
|
+
const fields = [];
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
const prepared = prepareConsentLine(line, stamp);
|
|
24
|
+
if (prepared.kind === 'refuse')
|
|
25
|
+
return prepared;
|
|
26
|
+
fields.push(prepared.fields);
|
|
27
|
+
}
|
|
28
|
+
if (!fields.length)
|
|
29
|
+
return { kind: 'refuse', why: `REFUSE·signer ${stamp.by} empty lot` };
|
|
30
|
+
let records;
|
|
31
|
+
try {
|
|
32
|
+
records = await invoke({ bytes: fields.map(f => signedBytes(f)), keyId: stamp.by, algorithm: stamp.algorithm });
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
return { kind: 'refuse', why: error instanceof SigningFailure ? error.message : `REFUSE·signer ${stamp.by} acquisition failed` };
|
|
36
|
+
}
|
|
37
|
+
if (records.length !== fields.length || records.some((record, i) => record.keyId !== stamp.by
|
|
38
|
+
|| !verifyBytes(signedBytes(fields[i]), publicKey, record.signature, admitted))) {
|
|
39
|
+
return { kind: 'refuse', why: `REFUSE·signer ${stamp.by} signature does not verify` };
|
|
40
|
+
}
|
|
41
|
+
return { kind: 'fact', lines: fields.map((f, i) => canonical({ ...f, sig: records[i].signature })) };
|
|
19
42
|
}
|
|
20
43
|
export function verifiesOwnerLine(line, publicKeyPem, admitted) {
|
|
21
44
|
const got = parse(line);
|
package/dist/observe/regions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { selectedCapsules } from "../host/selected-capsules.js";
|
|
2
|
-
import { selectionLines } from "../fold/reach.js";
|
|
2
|
+
import { selectionLines, projectionLines } from "../fold/reach.js";
|
|
3
3
|
export { selectionLines } from "../fold/reach.js";
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { basename, dirname, join, relative } from 'node:path';
|
|
@@ -125,7 +125,7 @@ export function capsulesOf(fold) {
|
|
|
125
125
|
const cached = heldCapsules.get(fold);
|
|
126
126
|
if (cached !== undefined)
|
|
127
127
|
return cached;
|
|
128
|
-
const got = selectedCapsules(selectionLines(fold)).map(one => one.capsule);
|
|
128
|
+
const got = selectedCapsules(selectionLines(fold), undefined, undefined, projectionLines(fold)).map(one => one.capsule);
|
|
129
129
|
heldCapsules.set(fold, got);
|
|
130
130
|
return got;
|
|
131
131
|
}
|
package/dist/observe/runner.js
CHANGED
|
@@ -21,7 +21,8 @@ const moduleAt = fromBlob() ? undefined : fileURLToPath(import.meta.url);
|
|
|
21
21
|
const runtimeAt = moduleAt === undefined ? '' : resolve(dirname(moduleAt), '..');
|
|
22
22
|
const runtimeExtension = moduleAt === undefined ? '' : extname(moduleAt);
|
|
23
23
|
export const ownRoot = () => (fromBlob() ? join(resolve(dirname(self()), '..', '..', '..'), 'bound') : resolve(runtimeAt, '..'));
|
|
24
|
-
export const ownStore = () => join(ownRoot(), '
|
|
24
|
+
export const ownStore = () => existsSync(join(ownRoot(), EXTENSION, 'ledger'))
|
|
25
|
+
? join(ownRoot(), EXTENSION) : join(ownRoot(), '..', EXTENSION);
|
|
25
26
|
/** The colocated public artifact is read without an unrelated parent's authority store. */
|
|
26
27
|
// An act is judged by the instrument it started with, even when that act changes its colocated lock.
|
|
27
28
|
let instrumentLock;
|
|
@@ -30,7 +31,9 @@ export const ownLock = () => {
|
|
|
30
31
|
return instrumentLock;
|
|
31
32
|
const lines = (observeText(join(ownRoot(), LOCK)) ?? '').split('\n').filter(isWire);
|
|
32
33
|
const published = publicLock(lines);
|
|
33
|
-
instrumentLock = published === undefined
|
|
34
|
+
instrumentLock = published === undefined
|
|
35
|
+
? dirname(ownStore()) === ownRoot() ? ownLockOf(ownRoot(), '', true) : ownLockOf(join(ownRoot(), '..'), basename(ownRoot()), true)
|
|
36
|
+
: foldClaims(published).standing;
|
|
34
37
|
return instrumentLock;
|
|
35
38
|
};
|
|
36
39
|
export const sourceOf = () => (fromBlob() ? join(ownRoot(), 'src', 'cli', 'verb.ts') : self());
|
package/dist/render/view.js
CHANGED
|
@@ -166,7 +166,8 @@ export function prerender(fold, views) {
|
|
|
166
166
|
capsuleViews(fold, due.map((view) => ({ asking: view.regions.filter((asked) => REGIONS[asked.name] === undefined), shape: view.shape })));
|
|
167
167
|
}
|
|
168
168
|
export function viewOf(fold, view) {
|
|
169
|
-
|
|
169
|
+
// Native receipts project current file bytes, not only the cached fold's observations.
|
|
170
|
+
const live = !view.shape || view.regions.some(asked => asked.name === 'receipts');
|
|
170
171
|
const kept = !live && fold.key ? keptRender(fold.store, fold.key, view.name) : undefined;
|
|
171
172
|
if (kept !== undefined)
|
|
172
173
|
return kept;
|
package/docs/usage.md
CHANGED
|
@@ -12,11 +12,11 @@ Start by asking for help and folding the project. If a selected view is unavaila
|
|
|
12
12
|
|
|
13
13
|
## Keys and coverage
|
|
14
14
|
|
|
15
|
-
Use a named key already admitted by the project. The authority contract declares its public key, class and scope coverage. The
|
|
15
|
+
Use a named key already admitted by the project. The authority contract declares its public key, class and scope coverage. Signing uses a declared port. The lock names `keys/<who> measure=signer value=<name>`; `signer/timeout` and `signer/response-bytes` bound each invocation. The built-in `file` port reads a matching PKCS8 PEM outside the repository through `--key-file`. For a command port, the host supplies a `BOUND_SIGNERS` registry mapping that admitted name to `{ "kind": "command", "executable": "...", "args": [] }`. A repository declaration cannot authorize a program. `--signer <name>` must match the admitted selection.
|
|
16
16
|
|
|
17
17
|
A new device must have its public key and coverage admitted by an authority that already covers the intended scope. The CLI does not currently certify silent key creation or tap-to-sign. This guide does not substitute a self-authorized fixture key for the team's authority.
|
|
18
18
|
|
|
19
|
-
For automation, supply the private key through the runner's secret storage to a temporary external file with restricted permissions. Do not print, commit or upload it. A source-control token and a Bound authority key have different purposes. Key rotation and revocation are changes to the admitted authority contract, not edits that erase prior signatures.
|
|
19
|
+
For automation through the file port, supply the private key through the runner's secret storage to a temporary external file with restricted permissions. A command signer receives the lot once as canonical signed bytes, one line per record in order. It returns one canonical wire response per record, containing `by` and `sig`, in that same order. Bound verifies the entire reply against the admitted public key before any admission. Timeout, nonzero exit, missing or invalid replies refuse the whole lot; raw signer output is not logged. Hosts can keep private keys outside Bound through this port. Key generation, storage and rotation belong to the host. Do not print, commit or upload it. A source-control token and a Bound authority key have different purposes. Key rotation and revocation are changes to the admitted authority contract, not edits that erase prior signatures.
|
|
20
20
|
|
|
21
21
|
## Proposal → signature → admission → reading
|
|
22
22
|
|
|
@@ -42,7 +42,7 @@ receipts.bound is native fold output. Do not write it by hand, use JSON from a t
|
|
|
42
42
|
|
|
43
43
|
REFUSE·wire means the current grammar does not admit the record. REFUSE·pin/release means named authentic content or its closure is unavailable or does not match. REFUSE·view means the requested view cannot be rendered under its contract. Authority refusal means the signer is not covered. Keep the line and the failed exit status; follow an explicit fix without changing the named digest or dropping a conflicting statement.
|
|
44
44
|
|
|
45
|
-
release/blocks is policy read by the fold.
|
|
45
|
+
release/blocks is policy read by the fold. Its admitted category links select existing obligations. The fold prints each missing reading or crossed ceiling as BLOCK; a blocked or incomplete policy is not READY. Read the captured verdict for the exact candidate instead of inferring acceptance from a successful render.
|
|
46
46
|
|
|
47
47
|
## Repository and PR review
|
|
48
48
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lapxo/bound",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Read the bounds you already wrote, fold them, and name the contradictions.",
|
|
6
6
|
"main": "dist/cli/verb.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@lapxo/obligations": "0.3.5",
|
|
35
|
-
"@lapxo/topos": "0.1.
|
|
35
|
+
"@lapxo/topos": "0.1.8"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^20.0.0",
|
|
@@ -44,5 +44,21 @@
|
|
|
44
44
|
},
|
|
45
45
|
"bin": {
|
|
46
46
|
"bound": "./dist/cli/verb.js"
|
|
47
|
-
}
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/Lapxo/bound.git"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/Lapxo/bound#readme",
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/Lapxo/bound/issues"
|
|
55
|
+
},
|
|
56
|
+
"keywords": [
|
|
57
|
+
"cli",
|
|
58
|
+
"content-addressed",
|
|
59
|
+
"provenance",
|
|
60
|
+
"receipts",
|
|
61
|
+
"constraints",
|
|
62
|
+
"reproducible"
|
|
63
|
+
]
|
|
48
64
|
}
|