@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.
@@ -1,20 +1,22 @@
1
+ import { selectedWireAt } from "../host/selected-topos.js";
1
2
  import { timing } from "../host/timing.js";
2
3
  import { admittedObjects } from "../host/objects.js";
3
4
  import { existsSync, rmdirSync, rmSync } from 'node:fs';
4
5
  import { dirname, join, basename } from 'node:path';
5
6
  import { closeReceipts, instrumentKeep, instrumentKept } from "../fold/closed.js";
6
7
  import { bytesDigest, fullDigest, instrumentOf, signaturesOf } from "../fold/digests.js";
7
- import { canonical, fromLine, LOCK, parse, wireAt } from '@lapxo/topos/wire';
8
+ import { canonical, fromLine, LOCK, parse } from '@lapxo/topos/wire';
8
9
  import { fieldOf, isConfig, foldClaims, sayingOf, selfName } from "../fold/claims.js";
9
10
  import { isReaderLock } from "../fold/observed.js";
10
11
  import { authorityFor, publicKeyOf, rootSigner, writerFor } from "../fold/signers.js";
11
12
  import { lockLines, lockStanding } from "../fold/keys.js";
12
13
  import { keepVerdict, keptVerdict, land, landBlob, storeOf } from "../land/ledger.js";
13
- import { signConsentLine, verifiesOwnerLine } from "../land/sign.js";
14
- import { appliedIn } from "../fold/signed.js";
14
+ import { signConsentLot, verifiesOwnerLine } from "../land/sign.js";
15
+ import { signerFor } from "../host/signing.js";
16
+ import { appliedIn, ownLockOf } from "../fold/signed.js";
15
17
  import { wordOf } from "../fold/wire.js";
16
18
  import { hasVouchedTree, landCoordinates, locationsOf, vouchedCoordinates } from "../land/vouched.js";
17
- import { entriesIn, observeFile, observeText } from "../observe/files.js";
19
+ import { entriesIn, observeFile } from "../observe/files.js";
18
20
  import { coneOf, standingOf } from "./place.js";
19
21
  import { ownLock, ownRoot, ownStore } from "../observe/runner.js";
20
22
  import { widenedWithoutWitness } from "../judge/widening.js";
@@ -85,40 +87,47 @@ export async function signedBatch(root, args, files) {
85
87
  }
86
88
  const keyFile = valueOf(args, FLAGS.keyFile);
87
89
  const keyId = valueOf(args, FLAGS.key) ?? ownerOf(store);
88
- if (drafted.length && (!keyFile || !existsSync(keyFile))) {
89
- process.stderr.write(`${selfName()}: a drafted batch is signed with --key-file (PKCS8 PEM)\n`);
90
- return { kind: 'exit', code: 2 };
91
- }
92
90
  const publicKey = drafted.length ? publicKeyOf(root, keyId) : undefined;
93
91
  if (drafted.length && !publicKey) {
94
92
  process.stderr.write(`${selfName()}: unknown key ${keyId} — name it in ${selfName()}.keys\n`);
95
93
  return { kind: 'exit', code: 2 };
96
94
  }
97
- const privateKeyPem = keyFile ? observeText(keyFile) ?? '' : '';
98
- const signed = proposed.filter((line) => fieldOf(line, 'sig'));
99
- for (const line of drafted) {
100
- const got = signConsentLine(line, privateKeyPem, { epoch, by: keyId, algorithm: signaturesOf(store).era });
101
- if (got.kind === 'refuse') {
102
- process.stderr.write(`${selfName()}: ${got.why}\n`);
95
+ let signed = proposed.filter((line) => fieldOf(line, 'sig'));
96
+ if (drafted.length) {
97
+ let invoke;
98
+ try {
99
+ invoke = signerFor([...lockStanding(store), ...destinations.flatMap(place => ownLockOf(root, place))], keyId, valueOf(args, FLAGS.signer), keyFile);
100
+ }
101
+ catch (error) {
102
+ process.stderr.write(`${selfName()}: ${error instanceof Error ? error.message : 'REFUSE·signer unavailable'}\n`);
103
103
  return { kind: 'exit', code: 1 };
104
104
  }
105
- if (!verifiesOwnerLine(got.line, publicKey, signaturesOf(store).admitted)) {
106
- process.stderr.write(`${selfName()}: REFUSE·accept the signature does not hold for ${keyId}\n`);
105
+ const got = await signConsentLot(drafted, { epoch, by: keyId, algorithm: signaturesOf(store).era }, publicKey, signaturesOf(store).admitted, invoke);
106
+ if (got.kind === 'refuse') {
107
+ process.stderr.write(`${selfName()}: ${got.why}\n`);
107
108
  return { kind: 'exit', code: 1 };
108
109
  }
109
- signed.push(got.line);
110
+ let index = 0;
111
+ signed = proposed.map(line => fieldOf(line, 'sig') ? line : got.lines[index++]);
110
112
  }
111
113
  const wireLines = [...signedOwnerLines(store), ...signed].flatMap((line) => {
112
114
  const got = parse(line);
113
115
  return got.kind === 'fact' ? [got.value.fields] : [];
114
116
  });
115
117
  for (const line of signed) {
116
- const got = fromLine(line, wireAt(wireLines, Number(fieldOf(line, 'epoch'))));
118
+ const got = fromLine(line, selectedWireAt(wireLines, Number(fieldOf(line, 'epoch')), foldClaims(signedOwnerLines(store)).standing.filter(one => fieldOf(one, 'scope').startsWith('uses/') && fieldOf(one, 'value') !== 'withdraw')));
117
119
  if (got.kind !== 'fact') {
118
120
  process.stderr.write(`${selfName()}: REFUSE·wire ${fieldOf(line, 'scope')} · ${got.why} · nothing landed\n`);
119
121
  return { kind: 'exit', code: 1 };
120
122
  }
121
123
  }
124
+ const authority = authorityFor([...lockLines(store), ...signed], rootSigner(root), signaturesOf(store).admitted);
125
+ const ownership = ownedOf(root, signed, line => authority.of(line).kind === 'admitted', by => authority.admitted.find(one => one.id === by)?.coverage ?? [], named, line => { const heads = byFile.flatMap(([heads, lines]) => lines.some(draft => sayingOf(draft) === sayingOf(line)) ? heads : []); return heads.length ? heads : undefined; });
126
+ if (ownership.nowhere.length) {
127
+ for (const line of ownership.nowhere)
128
+ process.stderr.write(`${selfName()}: REFUSE·signer ${keyId} uncovered ${fieldOf(line, 'scope')} · nothing signed for delivery\n`);
129
+ return { kind: 'exit', code: 1 };
130
+ }
122
131
  if (signed.some(line => !isConfig(line))) {
123
132
  try {
124
133
  await admittedObjects(root, [...signedOwnerLines(store), ...signed], text => process.stdout.write(text + '\n'));
package/dist/cli/args.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { relative, sep } from 'node:path';
2
2
  import { observeText } from "../observe/files.js";
3
3
  import { sourceOf } from "../observe/runner.js";
4
- import { PROTOCOL } from '@lapxo/topos/wire';
4
+ import { PROTOCOL, parse } from '@lapxo/topos/wire';
5
5
  import { FLAGS, VALUED } from "./names.js";
6
6
  export { VALUED };
7
7
  /** What the loop runs that is not source: its entry, the packages it imports and the readers it loads, read by where they lie. */
@@ -30,5 +30,12 @@ export function positionalsOf(args) {
30
30
  export const placesOfFile = (file) => (observeText(file) ?? '').split('\n').map((line) => line.trim())
31
31
  .filter((line) => line.startsWith('# place ')).map((line) => line.slice('# place '.length).trim().replace(/\/$/, '')).filter(Boolean);
32
32
  export function claimLinesIn(file) {
33
- return (observeText(file) ?? '').split('\n').map((line) => line.trim()).filter((line) => line.startsWith(PROTOCOL));
33
+ const lines = (observeText(file) ?? '').split('\n').map(line => line.trim())
34
+ .filter(line => line.startsWith(`${PROTOCOL.split('/')[0]}/`));
35
+ for (const line of lines) {
36
+ const got = parse(line);
37
+ if (got.kind !== 'fact')
38
+ throw new Error(`REFUSE·wire batch ${got.why} · nothing signed or landed`);
39
+ }
40
+ return lines;
34
41
  }
@@ -7,7 +7,7 @@ export declare const FLAGS: {
7
7
  readonly check: "--check";
8
8
  readonly key: "--key";
9
9
  readonly keyFile: "--key-file";
10
- readonly out: "--out";
10
+ readonly signer: "--signer";
11
11
  readonly place: "--place";
12
12
  };
13
13
  export declare const VALUED: readonly string[];
package/dist/cli/names.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /** The verbs bound answers by name: fold reads, land writes signed lines, sign is a key's act. A directory with no verb is a fold of that place. */
2
2
  export const VERB_NAMES = ['fold', 'land', 'sign'];
3
3
  /** Every flag the CLI reads, spelled once; a valued flag takes the argument after it. */
4
- export const FLAGS = { as: '--as', check: '--check', key: '--key', keyFile: '--key-file', out: '--out', place: '--place' };
5
- export const VALUED = [FLAGS.keyFile, FLAGS.key, FLAGS.out, FLAGS.as, FLAGS.place];
4
+ export const FLAGS = { as: '--as', check: '--check', key: '--key', keyFile: '--key-file', signer: '--signer', place: '--place' };
5
+ export const VALUED = [FLAGS.keyFile, FLAGS.key, FLAGS.signer, FLAGS.as, FLAGS.place];
6
6
  /** Views the CLI renders from these names, never from a handwritten usage string. */
7
7
  export const NAMED_VIEWS = { help: 'help', lines: 'lines', because: 'because' };
8
8
  export const EXIT = { closed: 0, refuse: 1, usage: 2 };
package/dist/cli/verb.js CHANGED
@@ -20,7 +20,8 @@ import { rolesOf } from "../fold/roles.js";
20
20
  import { authorityFor, releaseSigner, rootSigner, writerFor } from "../fold/signers.js";
21
21
  import { land, ledgerLines, storeOf } from "../land/ledger.js";
22
22
  import { lockLines, releaseOf } from "../fold/keys.js";
23
- import { signConsentLine } from "../land/sign.js";
23
+ import { signConsentLot } from "../land/sign.js";
24
+ import { signerFor } from "../host/signing.js";
24
25
  import { admittedIn, ownLockOf, tellOwn } from "../fold/signed.js";
25
26
  import { hasVouchedTree, landCoordinates, coordinatesOf, locationsOf, vouching, sourced } from "../land/vouched.js";
26
27
  import { observeText } from "../observe/files.js";
@@ -39,7 +40,7 @@ import { claimLinesIn, entryOf, namedPlaces, positionalsOf, valueOf } from "./ar
39
40
  import { EXIT, FLAGS, NAMED_VIEWS, helpLines } from "./names.js";
40
41
  import { accept, signedBatch } from "./accept.js";
41
42
  /** The first land of a lock: TARGET.bound into an empty owner ledger, then the sources that lock names. */
42
- function firstLand(root, args) {
43
+ async function firstLand(root, args) {
43
44
  const started = Date.now();
44
45
  const store = storeOf(root);
45
46
  const today = claimLinesIn(targetFile(root));
@@ -63,24 +64,6 @@ function firstLand(root, args) {
63
64
  keyId = valueOf(args, FLAGS.key) ?? owner;
64
65
  if (ledgerLines(store, owner).length)
65
66
  throw new Error('REFUSE·bootstrap the owner ledger already holds claims; the first land happens once');
66
- const keyFile = valueOf(args, FLAGS.keyFile);
67
- if (keyFile !== undefined) {
68
- const pem = observeText(keyFile);
69
- if (pem === undefined)
70
- throw new Error('REFUSE·bootstrap the requested signing key is unreadable');
71
- const signed = [];
72
- for (const line of today) {
73
- if (/\bsig=/.test(line)) {
74
- signed.push(line);
75
- continue;
76
- }
77
- const got = signConsentLine(line, pem, { epoch: 1, by: keyId, algorithm: config.era });
78
- if (got.kind === 'refuse')
79
- throw new Error(got.why);
80
- signed.push(got.line);
81
- }
82
- today.splice(0, today.length, ...signed);
83
- }
84
67
  const said = (measure) => {
85
68
  const values = new Set(today.filter((line) => fieldOf(line, 'scope') === `keys/${owner}` && fieldOf(line, 'measure') === measure && fieldOf(line, 'value') !== 'withdraw').map((line) => fieldOf(line, 'value')));
86
69
  if (values.size !== 1)
@@ -93,6 +76,15 @@ function firstLand(root, args) {
93
76
  const coverage = said('coverage').split('|').filter(Boolean);
94
77
  if (!publicKey || !coverage.length)
95
78
  throw new Error('REFUSE·bootstrap the root key must declare its public key and coverage');
79
+ const drafted = today.filter(line => !fieldOf(line, 'sig'));
80
+ if (drafted.length) {
81
+ const invoke = signerFor(today, keyId, valueOf(args, FLAGS.signer), valueOf(args, FLAGS.keyFile));
82
+ const got = await signConsentLot(drafted, { epoch: 1, by: keyId, algorithm: config.era }, publicKey, config.admitted, invoke);
83
+ if (got.kind === 'refuse')
84
+ throw new Error(got.why);
85
+ let i = 0;
86
+ today.splice(0, today.length, ...today.map(line => fieldOf(line, 'sig') ? line : got.lines[i++]));
87
+ }
96
88
  const authority = authorityFor(today, { ...releaseSigner(today), id: owner, publicKey, coverage }, config.admitted);
97
89
  for (const line of today) {
98
90
  const verdict = authority.of(line);
@@ -395,8 +387,10 @@ function foldVerb(_root, rest) {
395
387
  }
396
388
  async function foldAt(root, under, argv) {
397
389
  const as = valueOf(argv, FLAGS.as);
398
- if (as !== NAMED_VIEWS.help && signedOwnerLines(storeOf(root)).length)
399
- await resolveSources(standingOf(root, under).standing);
390
+ // Public content is authenticated by its digest, independently of owner history.
391
+ // A named view does not request mounting every release of the instrument.
392
+ if (as !== NAMED_VIEWS.help && lockLines(storeOf(root)).length)
393
+ await resolveSources(standingOf(root, under).standing, as === undefined);
400
394
  if (as === OBJECT_VIEW) {
401
395
  const inputs = positionalsOf(argv).filter(one => existsSync(resolve(one)) && !lstatSync(resolve(one)).isDirectory());
402
396
  if (inputs.length > 1)
@@ -446,6 +440,22 @@ const VERBS = {
446
440
  sign: (root, rest) => signVerb(root, rest),
447
441
  };
448
442
  function main(argv) {
443
+ for (let i = 0; i < argv.length; i += 1) {
444
+ const arg = argv[i];
445
+ if (!arg.startsWith('--'))
446
+ continue;
447
+ if (!Object.values(FLAGS).includes(arg)) {
448
+ process.stderr.write(`${selfName()}: REFUSE·flag unknown option ${arg}\n`);
449
+ return EXIT.usage;
450
+ }
451
+ if (arg !== FLAGS.check) {
452
+ if (argv[i + 1] === undefined || argv[i + 1].startsWith('--')) {
453
+ process.stderr.write(`${selfName()}: REFUSE·flag ${arg} needs a value\n`);
454
+ return EXIT.usage;
455
+ }
456
+ i += 1;
457
+ }
458
+ }
449
459
  const verb = argv[0] ?? '';
450
460
  const run = verb in VERBS ? VERBS[verb] : undefined;
451
461
  const places = positionalsOf(run === undefined ? argv : argv.slice(1)).filter((one) => existsSync(resolve(one)) && lstatSync(resolve(one)).isDirectory());
@@ -3,6 +3,7 @@ import { fieldOf, isConfig } from "./claims.js";
3
3
  import { isReaderLock } from "./observed.js";
4
4
  import { isRoleLock } from "./roles.js";
5
5
  import { isSignerLock } from "./signers.js";
6
+ import { isReleaseBinding } from "../host/release.js";
6
7
  function isEffectLock(line) {
7
8
  const got = parse(line);
8
9
  return got.kind === 'fact' && got.value.fields['measure'] === 'effect';
@@ -10,7 +11,7 @@ function isEffectLock(line) {
10
11
  /** A lock line the instrument itself consumes — an effect it runs, a reader it loads, a role it walks by, a signer it admits, the wire it reads by. */
11
12
  function configures(line) {
12
13
  const got = parse(line);
13
- return isEffectLock(line) || isReaderLock(line) || isRoleLock(line) || isSignerLock(line) || (got.kind === 'fact' && isWireClaim(got.value.fields));
14
+ return isReleaseBinding(line) || isEffectLock(line) || isReaderLock(line) || isRoleLock(line) || isSignerLock(line) || (got.kind === 'fact' && isWireClaim(got.value.fields));
14
15
  }
15
16
  /** Every reads line that configures nothing is a ceiling: a bound some reading has to meet. */
16
17
  export function isCeiling(line) {
package/dist/fold/kept.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { createHash, dirname, existsSync, join, mkdirSync, readdirSync, renameSync, writeFileSync } from "../host/io.js";
2
2
  import { bytesDigest, coordinateDigest } from "./digests.js";
3
3
  import { viewsOf } from "./views.js";
4
- import { landMemo, ledgerLines, memoAt } from "../land/ledger.js";
5
- import { observeText } from "../observe/files.js";
4
+ import { landMemo, ledgerLines, memoAt, shardFile, writerFile } from "../land/ledger.js";
5
+ import { coordinatesUnder, observeFile, observeText } from "../observe/files.js";
6
6
  import { EXTENSION, fieldOf } from "./claims.js";
7
- import { keyFor, lockSigners } from "./keys.js";
7
+ import { keyFor, keyOf, lockSigners } from "./keys.js";
8
8
  import { sourceOf, vouching } from "../land/vouched.js";
9
9
  import { wordsOf } from "./wire.js";
10
10
  import { ownLock } from "../observe/runner.js";
@@ -45,6 +45,7 @@ export function foldPoints(store, standing, under, keeper, instrument = '', obse
45
45
  return [
46
46
  `place ${under ?? '.'}`,
47
47
  `instrument ${instrument}`,
48
+ `reading context ${readingContext(store)}`,
48
49
  ...written,
49
50
  ...lined,
50
51
  ...[...new Set(coordinates)].sort(),
@@ -93,6 +94,14 @@ export const foldsElsewhere = () => elsewhere.folds;
93
94
  const joined = (lines) => lines.join('\n');
94
95
  const split = (text) => (text === '' ? [] : text.split('\n'));
95
96
  const linesOf = (value) => value === undefined ? [] : typeof value === 'string' ? split(value) : value;
97
+ /** Operational reader history guards file-fold reuse, never cell-region identity. */
98
+ function readingContext(store) {
99
+ const speaker = keyOf(store, 'read');
100
+ const files = speaker === undefined ? [] : [writerFile(store, speaker), ...coordinatesUnder(dirname(shardFile(store, speaker, '')), store)
101
+ .filter(file => file.endsWith(EXTENSION)).map(file => join(store, file))];
102
+ return bytesDigest(store, text(files.filter(file => existsSync(file)).sort()
103
+ .map(file => bytesDigest(store, observeFile(file) ?? new Uint8Array())).join('\n')));
104
+ }
96
105
  export function keptPlace(store, under) {
97
106
  const key = observeText(memoAt(store, lastOf([`place ${under ?? '.'}`])))?.trim();
98
107
  if (key === undefined || key === '')
@@ -101,6 +110,8 @@ export function keptPlace(store, under) {
101
110
  if (held === undefined)
102
111
  return undefined;
103
112
  const blob = JSON.parse(held);
113
+ if (blob.readingContext !== readingContext(store))
114
+ return undefined;
104
115
  const fold = blob.fold;
105
116
  return {
106
117
  ...fold,
@@ -134,7 +145,7 @@ export function keepFold(store, key, fold, points, instrument = '') {
134
145
  signed: [...fold.signed],
135
146
  kept: false,
136
147
  };
137
- landMemo(store, `fold/${key}`, text(JSON.stringify({ fold: packed, points, instrument })));
148
+ landMemo(store, `fold/${key}`, text(JSON.stringify({ fold: packed, points, instrument, readingContext: readingContext(store) })));
138
149
  const at = memoAt(store, lastOf(points));
139
150
  mkdirSync(dirname(at), { recursive: true });
140
151
  writeFileSync(`${at}.${process.pid}.part`, `${key}\n`);
@@ -389,7 +389,11 @@ export function observedClaims(root, store, options = {}) {
389
389
  ? asked.ask(due.map((d) => ({
390
390
  protocol: PROTOCOL, verb: 'read', rootScope: d.place, region: reader.capsule.region, held: heldOf(d.place, d.region),
391
391
  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)) ?? '' })),
392
- }))).map((answer) => (answer?.claims ?? []))
392
+ }))).map((answer) => {
393
+ if (answer?.kind !== 'fact')
394
+ throw new Error(`REFUSE·reader ${reader.module} · ${answer?.why ?? 'no capsule answer'}`);
395
+ return (answer.claims ?? []);
396
+ })
393
397
  : runReader(module, root, due.map((d) => ({ place: d.place, ...(reader.kind === 'js' ? { text: new TextDecoder().decode(d.bytes) } : {}), held: heldOf(d.place, d.region) })));
394
398
  }
395
399
  catch (error) {
@@ -7,6 +7,11 @@ export declare const selectionLines: (fold: {
7
7
  readonly standing: readonly string[];
8
8
  readonly under?: string;
9
9
  }) => readonly string[];
10
+ export declare const projectionLines: (fold: {
11
+ readonly root: string;
12
+ readonly standing: readonly string[];
13
+ readonly under?: string;
14
+ }) => readonly string[];
10
15
  export declare function capsulesFor(standing: readonly string[], place: string, root?: string): readonly {
11
16
  readonly capsule: Capsule;
12
17
  readonly offer: string;
@@ -1,6 +1,6 @@
1
1
  import { selectedCapsules } from "../host/selected-capsules.js";
2
2
  import { basename } from "../host/io.js";
3
- import { matches } from '@lapxo/topos/wire';
3
+ import { canonical, matches, parse } from '@lapxo/topos/wire';
4
4
  import { fieldOf } from "./claims.js";
5
5
  import { spoken, worldsIn } from "./places.js";
6
6
  import { wordOf, wordsOf } from "./wire.js";
@@ -57,9 +57,27 @@ export const selectionLines = (fold) => {
57
57
  const qualified = fold.standing.filter((line) => fieldOf(line, 'scope').startsWith(`${fold.under ?? ''}uses/`));
58
58
  return [...new Set([...local, ...qualified])].filter((line) => fieldOf(line, 'value') !== 'withdraw');
59
59
  };
60
+ /** Host bindings belong to the selected place, never to another sibling's lock. */
61
+ const relativeProjections = (standing, prefix) => standing.filter(line => fieldOf(line, 'scope').startsWith(`${prefix}dep/`)).map(line => {
62
+ const got = parse(line, { preserveKeys: true });
63
+ if (got.kind !== 'fact')
64
+ throw Error(`REFUSE·wire ${got.why}`);
65
+ // An unsigned host projection of admitted fields, never a newly signed claim.
66
+ const { sig, by, epoch, expires, repo, ...fields } = got.value.fields;
67
+ return canonical({ ...fields, scope: fields.scope.slice(prefix.length) }, got.value.version);
68
+ });
69
+ export const projectionLines = (fold) => {
70
+ const place = fold.under?.replace(/\/$/, '') ?? '';
71
+ const prefix = place ? `${place}/` : '';
72
+ const local = ownLockOf(fold.root, place).filter(line => fieldOf(line, 'scope').startsWith('dep/'));
73
+ const qualified = relativeProjections(fold.standing, prefix);
74
+ return [...new Set([...ownLock(), ...local, ...qualified])];
75
+ };
60
76
  export function capsulesFor(standing, place, root) {
61
77
  const prefix = place ? `${place}/` : '';
62
78
  const pins = root === undefined ? standing.filter(line => fieldOf(line, 'scope').startsWith(`${prefix}uses/`) && fieldOf(line, 'value') !== 'withdraw')
63
79
  : selectionLines({ root, standing, ...(prefix ? { under: prefix } : {}) });
64
- return selectedCapsules(pins);
80
+ const host = root === undefined ? [...ownLock(), ...relativeProjections(standing, prefix)]
81
+ : projectionLines({ root, standing, ...(prefix ? { under: prefix } : {}) });
82
+ return selectedCapsules(pins, undefined, undefined, host);
65
83
  }
@@ -8,6 +8,7 @@ import { capsuleAt, placesIn } from "./places.js";
8
8
  import { capsulesFor } from "./reach.js";
9
9
  import { ownLockOf } from "./signed.js";
10
10
  import { ownLock } from "../observe/runner.js";
11
+ import { selectedWireList } from "../host/selected-topos.js";
11
12
  import { entriesIn, coordinatesUnder, observeFile, stampOf } from "../observe/files.js";
12
13
  export function lookOf(root, roleOf) {
13
14
  const walked = new Map();
@@ -42,7 +43,10 @@ const reachOfLine = (module, where) => {
42
43
  */
43
44
  export function capsuleReadersOf(root, store, standing, lines, resolve = capsulesFor) {
44
45
  const reaching = new Set(lines.flatMap((reader) => reachOfLine(reader.module, reader.where)));
45
- const measures = wordsOf(standing, 'region-measures');
46
+ const localMeasures = wordsOf(standing, 'region-measures');
47
+ const pins = standing.filter(line => fieldOf(line, 'scope').startsWith('uses/') && fieldOf(line, 'value') !== 'withdraw');
48
+ const measures = localMeasures.length || !pins.length ? localMeasures
49
+ : selectedWireList(standing, Number.MAX_SAFE_INTEGER, pins, 'region-measures');
46
50
  const world = fields(fieldOf(wireLine(standing, 'offers/world') ?? '', 'value')).map(([region, globs]) => canonical({ scope: `region/${region}`, measure: wordOn(standing, 'region-coordinate', ownLock()), value: globs }));
47
51
  const groups = new Map();
48
52
  for (const place of ['', ...placesIn(root)]) {
@@ -6,7 +6,7 @@ import { signaturesOf } from "./digests.js";
6
6
  import { lockLines, lockStanding } from "./keys.js";
7
7
  import { wordOf } from "./wire.js";
8
8
  import { authorityFor, rootSigner } from "./signers.js";
9
- import { shardLines, storeOf } from "../land/ledger.js";
9
+ import { ledgerLines, shardLines, storeOf } from "../land/ledger.js";
10
10
  import { verifiesFields } from "../land/sign.js";
11
11
  import { observeText } from "../observe/files.js";
12
12
  const held = new Map();
@@ -20,14 +20,16 @@ function verified(root, place) {
20
20
  const hasAuthority = lock.some(line => fieldOf(line, 'sig') !== '');
21
21
  const algorithms = hasAuthority ? signaturesOf(store).admitted : [];
22
22
  const signers = algorithms.length ? authorityFor(lock, rootSigner(root), algorithms).admitted : [];
23
- const history = signers.flatMap(signer => shardLines(store, signer.id, `${place}/${LOCK}`));
23
+ const history = signers.flatMap(signer => place ? shardLines(store, signer.id, `${place}/${LOCK}`) : ledgerLines(store, signer.id)).filter(line => fieldOf(line, 'sig') !== '');
24
24
  const text = [publicText, ...history, ...(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)
28
28
  return last;
29
29
  const lines = [...new Set(text.split('\n').filter(isWire))];
30
- const family = signers.length ? wordOf(lockStanding(store), 'families', 'region') : '';
30
+ // Unrestricted coverage does not construct a region coordinate. Restricted
31
+ // coverage still requires the lock's declared namespace before comparison.
32
+ const family = signers.some(signer => !signer.coverage.includes('*')) ? wordOf(lockStanding(store), 'families', 'region') : '';
31
33
  const stands = (line) => {
32
34
  const got = parse(line);
33
35
  if (got.kind !== 'fact' || got.value.fields['sig'] === undefined)
package/dist/fold/wire.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as topos from '@lapxo/topos/wire';
2
2
  import { fieldOf, selfName } from "./claims.js";
3
+ import { selectedWireList } from "../host/selected-topos.js";
3
4
  const spoken = topos;
4
5
  export const namesAt = (spoken['namesAt'] ?? spoken['namesLeaf']);
5
6
  export const roleAt = (spoken['roleAt'] ?? spoken['leafRoleOf']);
@@ -17,7 +18,11 @@ export function wordsOf(standing, list) {
17
18
  * refusal. The older `audit/wire/` prefix is one era of `wire/`, not a fall-back to another lock.
18
19
  */
19
20
  export function wordOf(standing, list, name) {
20
- const held = wordsOf(standing, list);
21
+ const local = wordsOf(standing, list);
22
+ const pins = standing.filter(line => fieldOf(line, 'scope').startsWith('uses/') && fieldOf(line, 'value') !== 'withdraw');
23
+ // A required word may come from selected standing without copying foreign claims.
24
+ const held = wireLine(standing, list) === undefined && pins.length
25
+ ? selectedWireList(standing, Number.MAX_SAFE_INTEGER, pins, list) : local;
21
26
  const eras = typeof name === 'string' ? [name] : name;
22
27
  const found = eras.find((one) => held.includes(one));
23
28
  if (found !== undefined)
@@ -0,0 +1,31 @@
1
+ /** Contains only host-controlled diagnostics, never child output. */
2
+ export declare class SigningFailure extends Error {
3
+ }
4
+ export interface SigningLot {
5
+ readonly bytes: readonly string[];
6
+ readonly keyId: string;
7
+ readonly algorithm: string;
8
+ }
9
+ export interface SignatureRecord {
10
+ readonly keyId: string;
11
+ readonly signature: string;
12
+ }
13
+ export interface SigningProtocol {
14
+ readonly request: (lot: SigningLot) => string;
15
+ readonly response: (records: readonly SignatureRecord[]) => string;
16
+ readonly reply: (text: string, lot: SigningLot) => readonly SignatureRecord[];
17
+ }
18
+ export type SigningMechanism = {
19
+ readonly kind: 'file';
20
+ readonly path: string;
21
+ } | {
22
+ readonly kind: 'command';
23
+ readonly executable: string;
24
+ readonly args: readonly string[];
25
+ };
26
+ export interface SigningLimits {
27
+ readonly timeoutMs: number;
28
+ readonly responseBytes: number;
29
+ }
30
+ /** Host-approved mechanism only. Repository declarations do not authorize execution. */
31
+ export declare function invokeSigner(lot: SigningLot, mechanism: SigningMechanism, protocol: SigningProtocol, limits: SigningLimits): Promise<readonly SignatureRecord[]>;
@@ -0,0 +1,74 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { signBytes } from "../adapters/keys/ed25519.js";
4
+ /** Contains only host-controlled diagnostics, never child output. */
5
+ export class SigningFailure extends Error {
6
+ }
7
+ /** Host-approved mechanism only. Repository declarations do not authorize execution. */
8
+ export async function invokeSigner(lot, mechanism, protocol, limits) {
9
+ if (!Number.isSafeInteger(limits.timeoutMs) || limits.timeoutMs <= 0
10
+ || !Number.isSafeInteger(limits.responseBytes) || limits.responseBytes <= 0) {
11
+ throw new SigningFailure(`REFUSE·signer ${lot.keyId} has no valid declared limits`);
12
+ }
13
+ const request = protocol.request(lot);
14
+ let output;
15
+ if (mechanism.kind === 'file') {
16
+ try {
17
+ const key = readFileSync(mechanism.path, 'utf8');
18
+ output = protocol.response(lot.bytes.map(bytes => ({ keyId: lot.keyId, signature: signBytes(bytes, key, lot.algorithm) })));
19
+ }
20
+ catch {
21
+ throw new SigningFailure(`REFUSE·signer ${lot.keyId} file mechanism failed`);
22
+ }
23
+ }
24
+ else
25
+ output = await new Promise((resolve, reject) => {
26
+ const child = spawn(mechanism.executable, [...mechanism.args], { stdio: ['pipe', 'pipe', 'pipe'], shell: false });
27
+ let size = 0;
28
+ const chunks = [];
29
+ let failure;
30
+ const refuse = (why) => {
31
+ if (failure)
32
+ return;
33
+ failure = why;
34
+ child.kill('SIGKILL');
35
+ clearTimeout(timer);
36
+ child.stdin.destroy();
37
+ child.stdout.destroy();
38
+ child.stderr.destroy();
39
+ reject(new SigningFailure(`REFUSE·signer ${lot.keyId} ${why} · status=none · signal=SIGKILL`));
40
+ };
41
+ const timer = setTimeout(() => refuse('timeout'), limits.timeoutMs);
42
+ child.stdout.on('data', (chunk) => {
43
+ size += chunk.length;
44
+ if (size > limits.responseBytes)
45
+ refuse('response exceeds declared limit');
46
+ if (!failure)
47
+ chunks.push(chunk);
48
+ });
49
+ // Drain, never forward or retain signer diagnostics.
50
+ child.stderr.on('data', () => { });
51
+ child.stdin.on('error', () => refuse('input delivery failed'));
52
+ child.on('error', () => {
53
+ clearTimeout(timer);
54
+ reject(new SigningFailure(`REFUSE·signer ${lot.keyId} command could not start`));
55
+ });
56
+ child.on('close', (status, signal) => {
57
+ clearTimeout(timer);
58
+ if (failure || status !== 0) {
59
+ reject(new SigningFailure(`REFUSE·signer ${lot.keyId} ${failure ?? 'command failed'} · status=${status ?? 'none'} · signal=${signal ?? 'none'}`));
60
+ }
61
+ else
62
+ resolve(Buffer.concat(chunks).toString('utf8'));
63
+ });
64
+ child.stdin.end(request);
65
+ });
66
+ if (Buffer.byteLength(output) > limits.responseBytes)
67
+ throw new SigningFailure(`REFUSE·signer ${lot.keyId} response exceeds declared limit`);
68
+ try {
69
+ return protocol.reply(output, lot);
70
+ }
71
+ catch {
72
+ throw new SigningFailure(`REFUSE·signer ${lot.keyId} invalid response`);
73
+ }
74
+ }
@@ -6,8 +6,8 @@ interface ResolveResult {
6
6
  }
7
7
  type NextResolve = (specifier: string, context: ResolveContext) => ResolveResult;
8
8
  /**
9
- * Where a named package or world lies: `dep/<name>` for a library the instrument installs, `uses/<name>` for a world
10
- * pinned by its GitHub release. Both are the digest, laid. Nothing here spells a path.
9
+ * Resolve declared library bytes for this host. A library's shape may select an
10
+ * archive coordinate; world selection remains the standing contract.
11
11
  */
12
12
  export declare function pinPlace(name: string): string;
13
13
  export declare function pinExport(name: string, sub: string): string;
@@ -10,8 +10,8 @@ import { theWord } from "../fold/wire.js";
10
10
  const registerHooks = nodeModule.registerHooks;
11
11
  const taken = (exp) => (typeof exp === 'string' ? exp : exp && typeof exp === 'object' && 'default' in exp && typeof exp.default === 'string' ? exp.default : '');
12
12
  /**
13
- * Where a named package or world lies: `dep/<name>` for a library the instrument installs, `uses/<name>` for a world
14
- * pinned by its GitHub release. Both are the digest, laid. Nothing here spells a path.
13
+ * Resolve declared library bytes for this host. A library's shape may select an
14
+ * archive coordinate; world selection remains the standing contract.
15
15
  */
16
16
  export function pinPlace(name) {
17
17
  const lock = ownLock();
@@ -20,7 +20,7 @@ export function pinPlace(name) {
20
20
  if (line === undefined)
21
21
  return '';
22
22
  const digest = fieldOf(line, 'value');
23
- return digest ? runtimeTreeAt(ownStore(), ownRoot(), digest, ownStore()) ?? '' : '';
23
+ return digest ? runtimeTreeAt(ownStore(), ownRoot(), digest, ownStore(), fieldOf(line, 'role') === 'reads' ? fieldOf(line, 'shape') : '') ?? '' : '';
24
24
  }
25
25
  export function pinExport(name, sub) {
26
26
  const at = pinPlace(name);
@@ -1,4 +1,6 @@
1
1
  export declare const laidAt: (store: string, digest: string) => string;
2
+ /** A host artifact binding, not a measured ceiling or an object act. */
3
+ export declare const isReleaseBinding: (line: string) => boolean;
2
4
  export declare const releasesOf: (lock: readonly string[]) => readonly string[];
3
5
  /**
4
6
  * What the host does before anything runs: each release the instrument's own lock names by digest is a snapshot of
@@ -9,4 +11,4 @@ export declare const releasesOf: (lock: readonly string[]) => readonly string[];
9
11
  export declare function layReleases(store: string, own: string, lock: readonly string[], contentStore?: string): number;
10
12
  export declare const resolvedOf: (store: string, own: string, lock: readonly string[], contentStore?: string) => readonly string[];
11
13
  /** A host loads verified release bytes from a runtime tree, never interprets a snapshot file as a directory. */
12
- export declare function runtimeTreeAt(store: string, own: string, digest: string, contentStore?: string): string | undefined;
14
+ export declare function runtimeTreeAt(store: string, own: string, digest: string, contentStore?: string, root?: string): string | undefined;