@lapxo/bound 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/CONTRIBUTING.md +44 -0
  2. package/LICENSE +22 -0
  3. package/README.md +97 -0
  4. package/TARGET.bound +361 -0
  5. package/dist/cli/accept.d.ts +23 -0
  6. package/dist/cli/accept.js +423 -0
  7. package/dist/cli/args.d.ts +10 -0
  8. package/dist/cli/args.js +34 -0
  9. package/dist/cli/host.d.ts +1 -0
  10. package/dist/cli/host.js +32 -0
  11. package/dist/cli/names.d.ts +25 -0
  12. package/dist/cli/names.js +12 -0
  13. package/dist/cli/observe.d.ts +1 -0
  14. package/dist/cli/observe.js +6 -0
  15. package/dist/cli/owner.d.ts +35 -0
  16. package/dist/cli/owner.js +119 -0
  17. package/dist/cli/pass.d.ts +6 -0
  18. package/dist/cli/pass.js +309 -0
  19. package/dist/cli/place.d.ts +74 -0
  20. package/dist/cli/place.js +290 -0
  21. package/dist/cli/takes.d.ts +30 -0
  22. package/dist/cli/takes.js +108 -0
  23. package/dist/cli/verb.d.ts +2 -0
  24. package/dist/cli/verb.js +478 -0
  25. package/dist/fold/ceilings.d.ts +24 -0
  26. package/dist/fold/ceilings.js +129 -0
  27. package/dist/fold/claims.d.ts +25 -0
  28. package/dist/fold/claims.js +107 -0
  29. package/dist/fold/closed.d.ts +15 -0
  30. package/dist/fold/closed.js +168 -0
  31. package/dist/fold/configures.d.ts +4 -0
  32. package/dist/fold/configures.js +22 -0
  33. package/dist/fold/digests.d.ts +29 -0
  34. package/dist/fold/digests.js +211 -0
  35. package/dist/fold/evidence.d.ts +32 -0
  36. package/dist/fold/evidence.js +22 -0
  37. package/dist/fold/fold-worker.d.ts +1 -0
  38. package/dist/fold/fold-worker.js +13 -0
  39. package/dist/fold/kept.d.ts +29 -0
  40. package/dist/fold/kept.js +142 -0
  41. package/dist/fold/keys.d.ts +17 -0
  42. package/dist/fold/keys.js +84 -0
  43. package/dist/fold/laid.d.ts +17 -0
  44. package/dist/fold/laid.js +90 -0
  45. package/dist/fold/nos.d.ts +8 -0
  46. package/dist/fold/nos.js +48 -0
  47. package/dist/fold/object.d.ts +17 -0
  48. package/dist/fold/object.js +38 -0
  49. package/dist/fold/observed.d.ts +33 -0
  50. package/dist/fold/observed.js +444 -0
  51. package/dist/fold/opened.d.ts +5 -0
  52. package/dist/fold/opened.js +33 -0
  53. package/dist/fold/own.d.ts +24 -0
  54. package/dist/fold/own.js +215 -0
  55. package/dist/fold/paid.d.ts +11 -0
  56. package/dist/fold/paid.js +92 -0
  57. package/dist/fold/place-inputs.d.ts +17 -0
  58. package/dist/fold/place-inputs.js +70 -0
  59. package/dist/fold/places.d.ts +20 -0
  60. package/dist/fold/places.js +84 -0
  61. package/dist/fold/rank.d.ts +3 -0
  62. package/dist/fold/rank.js +9 -0
  63. package/dist/fold/reach.d.ts +13 -0
  64. package/dist/fold/reach.js +65 -0
  65. package/dist/fold/readers.d.ts +44 -0
  66. package/dist/fold/readers.js +132 -0
  67. package/dist/fold/receipts.d.ts +8 -0
  68. package/dist/fold/receipts.js +47 -0
  69. package/dist/fold/region.d.ts +72 -0
  70. package/dist/fold/region.js +286 -0
  71. package/dist/fold/release-policy.d.ts +67 -0
  72. package/dist/fold/release-policy.js +103 -0
  73. package/dist/fold/resolved.d.ts +18 -0
  74. package/dist/fold/resolved.js +123 -0
  75. package/dist/fold/roles.d.ts +17 -0
  76. package/dist/fold/roles.js +59 -0
  77. package/dist/fold/second.d.ts +18 -0
  78. package/dist/fold/second.js +68 -0
  79. package/dist/fold/shards.d.ts +29 -0
  80. package/dist/fold/shards.js +100 -0
  81. package/dist/fold/signed.d.ts +18 -0
  82. package/dist/fold/signed.js +75 -0
  83. package/dist/fold/signers.d.ts +24 -0
  84. package/dist/fold/signers.js +58 -0
  85. package/dist/fold/stranger.d.ts +3 -0
  86. package/dist/fold/stranger.js +24 -0
  87. package/dist/fold/views.d.ts +27 -0
  88. package/dist/fold/views.js +66 -0
  89. package/dist/fold/wire.d.ts +19 -0
  90. package/dist/fold/wire.js +41 -0
  91. package/dist/fold/zoom.d.ts +16 -0
  92. package/dist/fold/zoom.js +80 -0
  93. package/dist/host/adapters/keys/ed25519.d.ts +7 -0
  94. package/dist/host/adapters/keys/ed25519.js +30 -0
  95. package/dist/host/archive.d.ts +11 -0
  96. package/dist/host/archive.js +152 -0
  97. package/dist/host/capsule-process.d.ts +20 -0
  98. package/dist/host/capsule-process.js +54 -0
  99. package/dist/host/capsule.d.ts +13 -0
  100. package/dist/host/capsule.js +149 -0
  101. package/dist/host/cell-receipts.d.ts +2 -0
  102. package/dist/host/cell-receipts.js +124 -0
  103. package/dist/host/digest.d.ts +3 -0
  104. package/dist/host/digest.js +15 -0
  105. package/dist/host/hash.d.ts +2 -0
  106. package/dist/host/hash.js +3 -0
  107. package/dist/host/io.d.ts +9 -0
  108. package/dist/host/io.js +9 -0
  109. package/dist/host/located.d.ts +4 -0
  110. package/dist/host/located.js +105 -0
  111. package/dist/host/objects.d.ts +5 -0
  112. package/dist/host/objects.js +18 -0
  113. package/dist/host/ports/store.d.ts +2 -0
  114. package/dist/host/ports/store.js +18 -0
  115. package/dist/host/provide.d.ts +23 -0
  116. package/dist/host/provide.js +81 -0
  117. package/dist/host/release.d.ts +12 -0
  118. package/dist/host/release.js +213 -0
  119. package/dist/host/selected-capsules.d.ts +8 -0
  120. package/dist/host/selected-capsules.js +19 -0
  121. package/dist/host/selected-topos.d.ts +17 -0
  122. package/dist/host/selected-topos.js +61 -0
  123. package/dist/host/source-content.d.ts +10 -0
  124. package/dist/host/source-content.js +94 -0
  125. package/dist/host/timing.d.ts +2 -0
  126. package/dist/host/timing.js +7 -0
  127. package/dist/judge/late.d.ts +7 -0
  128. package/dist/judge/late.js +33 -0
  129. package/dist/judge/take.d.ts +57 -0
  130. package/dist/judge/take.js +75 -0
  131. package/dist/judge/widening.d.ts +1 -0
  132. package/dist/judge/widening.js +25 -0
  133. package/dist/land/act.d.ts +4 -0
  134. package/dist/land/act.js +209 -0
  135. package/dist/land/alias.d.ts +10 -0
  136. package/dist/land/alias.js +41 -0
  137. package/dist/land/held.d.ts +7 -0
  138. package/dist/land/held.js +23 -0
  139. package/dist/land/inert.d.ts +26 -0
  140. package/dist/land/inert.js +33 -0
  141. package/dist/land/leaving.d.ts +14 -0
  142. package/dist/land/leaving.js +51 -0
  143. package/dist/land/ledger.d.ts +28 -0
  144. package/dist/land/ledger.js +148 -0
  145. package/dist/land/owned.d.ts +23 -0
  146. package/dist/land/owned.js +65 -0
  147. package/dist/land/sign.d.ts +16 -0
  148. package/dist/land/sign.js +30 -0
  149. package/dist/land/vouched.d.ts +21 -0
  150. package/dist/land/vouched.js +112 -0
  151. package/dist/land/withdraw.d.ts +14 -0
  152. package/dist/land/withdraw.js +24 -0
  153. package/dist/observe/files.d.ts +14 -0
  154. package/dist/observe/files.js +68 -0
  155. package/dist/observe/identity.d.ts +19 -0
  156. package/dist/observe/identity.js +61 -0
  157. package/dist/observe/match.d.ts +1 -0
  158. package/dist/observe/match.js +8 -0
  159. package/dist/observe/regions.d.ts +29 -0
  160. package/dist/observe/regions.js +200 -0
  161. package/dist/observe/run.d.ts +30 -0
  162. package/dist/observe/run.js +34 -0
  163. package/dist/observe/runner.d.ts +16 -0
  164. package/dist/observe/runner.js +83 -0
  165. package/dist/render/capsule.d.ts +24 -0
  166. package/dist/render/capsule.js +30 -0
  167. package/dist/render/field.d.ts +9 -0
  168. package/dist/render/field.js +46 -0
  169. package/dist/render/names.d.ts +3 -0
  170. package/dist/render/names.js +4 -0
  171. package/dist/render/open.d.ts +27 -0
  172. package/dist/render/open.js +61 -0
  173. package/dist/render/page.d.ts +3 -0
  174. package/dist/render/page.js +7 -0
  175. package/dist/render/receipts.d.ts +3 -0
  176. package/dist/render/receipts.js +46 -0
  177. package/dist/render/shape.d.ts +16 -0
  178. package/dist/render/shape.js +74 -0
  179. package/dist/render/target.d.ts +18 -0
  180. package/dist/render/target.js +52 -0
  181. package/dist/render/view.d.ts +22 -0
  182. package/dist/render/view.js +191 -0
  183. package/docs/readme-assets/atlas.svg +169 -0
  184. package/docs/usage.md +53 -0
  185. package/package.json +48 -0
  186. package/receipts.bound +59 -0
@@ -0,0 +1,24 @@
1
+ import type { Published, Signer, SignerVerdict } from '@lapxo/topos/wire';
2
+ /** A lock line about the signers: the authority fold reads it. */
3
+ export declare function isSignerLock(line: string): boolean;
4
+ /** The authority view of a ledger: which signers the lock admits from its root, and what each line carries. */
5
+ export declare function authorityFor(lines: readonly string[], root: Signer, admitted: readonly string[], published?: Published): {
6
+ readonly admitted: readonly Signer[];
7
+ readonly potential: readonly string[];
8
+ readonly of: (line: string) => SignerVerdict;
9
+ };
10
+ /** The key this lock admits for a class of writing: the fold is kept by whoever runs it, and the lock says who that is. */
11
+ export declare function writerFor(standing: readonly string[], kind: string): string | undefined;
12
+ export declare function publicKeyOf(root: string, keyId: string): string | undefined;
13
+ export declare function releaseSigner(lines: readonly string[]): Signer;
14
+ export declare function rootSigner(root: string): {
15
+ id: string;
16
+ keyClass: 'authorize';
17
+ publicKey: string;
18
+ coverage: string[];
19
+ depth: {
20
+ lo: number;
21
+ hi: number;
22
+ };
23
+ admittedBy: string[];
24
+ };
@@ -0,0 +1,58 @@
1
+ import { existsSync, join } from "../host/io.js";
2
+ import { authorityOf, isKeyClaim, parse, signersOf } from '@lapxo/topos/wire';
3
+ import { verifiesFields } from "../land/sign.js";
4
+ import { fieldOf, selfName } from "./claims.js";
5
+ import { keyFor, lockStanding } from "./keys.js";
6
+ import { storeRoot } from "../host/ports/store.js";
7
+ import { storeOf } from "../land/ledger.js";
8
+ import { observeText } from "../observe/files.js";
9
+ /** A lock line about the signers: the authority fold reads it. */
10
+ export function isSignerLock(line) {
11
+ const got = parse(line);
12
+ return got.kind === 'fact' && isKeyClaim(got.value.fields);
13
+ }
14
+ /** The authority view of a ledger: which signers the lock admits from its root, and what each line carries. */
15
+ export function authorityFor(lines, root, admitted, published) {
16
+ const fields = lines
17
+ .map((line) => parse(line))
18
+ .flatMap((got) => (got.kind === 'fact' ? [got.value.fields] : []));
19
+ const lock = signersOf(fields, root, (f, key) => verifiesFields(f, key, admitted));
20
+ return {
21
+ admitted: lock.admitted,
22
+ potential: lock.potential,
23
+ of: (line) => {
24
+ const got = parse(line);
25
+ return got.kind === 'fact'
26
+ ? authorityOf(got.value.fields, lock.admitted, (f, key) => verifiesFields(f, key, admitted), published)
27
+ : { kind: 'grey', why: `not a claim line: ${got.why}` };
28
+ },
29
+ };
30
+ }
31
+ /** The key this lock admits for a class of writing: the fold is kept by whoever runs it, and the lock says who that is. */
32
+ export function writerFor(standing, kind) {
33
+ for (const line of standing) {
34
+ const key = /^keys\/([^/]+)$/.exec(fieldOf(line, 'scope'))?.[1];
35
+ if (key && fieldOf(line, 'measure') === 'class' && fieldOf(line, 'value') === kind)
36
+ return key;
37
+ }
38
+ return undefined;
39
+ }
40
+ export function publicKeyOf(root, keyId) {
41
+ const at = [join(storeRoot(root), `${selfName()}.keys`), join(storeOf(root), `${selfName()}.keys`)].find((one) => existsSync(one));
42
+ const keys = at === undefined ? {} : JSON.parse(observeText(at) ?? '{}');
43
+ const anchored = keys[keyId]?.publicKey;
44
+ if (anchored !== undefined)
45
+ return anchored;
46
+ const said = lockStanding(storeOf(root))
47
+ .find((line) => fieldOf(line, 'scope') === `keys/${keyId}` && fieldOf(line, 'measure') === 'public-key' && fieldOf(line, 'value') !== 'withdraw');
48
+ return said === undefined ? undefined : fieldOf(said, 'value');
49
+ }
50
+ export function releaseSigner(lines) {
51
+ const said = (id, measure) => fieldOf(lines.find((line) => fieldOf(line, 'scope') === `keys/${id}` && fieldOf(line, 'measure') === measure && fieldOf(line, 'value') !== 'withdraw') ?? '', 'value') || undefined;
52
+ const id = writerFor(lines, 'authorize') ?? '';
53
+ return { id, keyClass: 'authorize', publicKey: said(id, 'public-key') ?? '', coverage: ['*'], depth: { lo: 1, hi: 16 }, admittedBy: [] };
54
+ }
55
+ export function rootSigner(root) {
56
+ const id = keyFor(storeOf(root), 'authorize');
57
+ return { id, keyClass: 'authorize', publicKey: publicKeyOf(root, id) ?? '', coverage: ['*'], depth: { lo: 1, hi: 16 }, admittedBy: [] };
58
+ }
@@ -0,0 +1,3 @@
1
+ /** A clone that holds a lock and no ledger: receipts none, one place opened, the needs the lock names, each uses/ pin still open, and README.md to render. Nothing is folded and nothing is written. */
2
+ export declare function emptyLedger(store: string): boolean;
3
+ export declare function sayStranger(root: string): void;
@@ -0,0 +1,24 @@
1
+ import { existsSync, join, readdirSync } from "../host/io.js";
2
+ import { LOCK } from '@lapxo/topos/wire';
3
+ import { fieldOf, isWire } from "./claims.js";
4
+ import { observeText } from "../observe/files.js";
5
+ /** A clone that holds a lock and no ledger: receipts none, one place opened, the needs the lock names, each uses/ pin still open, and README.md to render. Nothing is folded and nothing is written. */
6
+ export function emptyLedger(store) {
7
+ const dir = join(store, 'ledger');
8
+ return !existsSync(dir) || readdirSync(dir).length === 0;
9
+ }
10
+ export function sayStranger(root) {
11
+ const lines = (observeText(join(root, LOCK)) ?? '').split('\n').filter(isWire);
12
+ const needs = [...new Set(lines.flatMap((line) => fieldOf(line, 'needs').split('|').filter(Boolean)))].sort();
13
+ const uses = lines.filter((line) => {
14
+ const scope = fieldOf(line, 'scope');
15
+ return scope.startsWith('uses/') && !scope.slice('uses/'.length).includes('/') && fieldOf(line, 'value') !== 'withdraw';
16
+ }).map((line) => fieldOf(line, 'scope'));
17
+ const shape = fieldOf(lines.find((line) => fieldOf(line, 'scope').startsWith('view/') && fieldOf(line, 'shape') !== '' && fieldOf(line, 'value') !== 'withdraw') ?? '', 'shape');
18
+ process.stdout.write('RECEIPTS 0 read · 1 opened\n');
19
+ process.stdout.write(`NEEDS ${needs.join(' ')}\n`);
20
+ for (const scope of uses)
21
+ process.stdout.write(`OPEN ${scope}/\n`);
22
+ if (shape)
23
+ process.stdout.write(`RENDER ${shape}\n`);
24
+ }
@@ -0,0 +1,27 @@
1
+ interface Asked {
2
+ readonly name: string;
3
+ readonly at: number;
4
+ readonly called?: boolean;
5
+ }
6
+ export interface View {
7
+ readonly name: string;
8
+ readonly regions: readonly Asked[];
9
+ readonly shape: string;
10
+ }
11
+ /**
12
+ * A view is a signed line `view/<name>` whose value lists regions in the order they are written, each at the resolution
13
+ * it asks for (`region@n`, the steps of each line it keeps, three when it asks for none, and the one the view is called
14
+ * at for `region@*`), and whose shape, when it has one, names the coordinate the view renders into. What renders a region is
15
+ * read where the fold is known, and a view asking one nothing renders is refused there, by name.
16
+ */
17
+ export declare function viewsOf(standing: readonly string[]): ReadonlyMap<string, View>;
18
+ /**
19
+ * A view the wire's views/world list names writes at a world that holds no view of that name of its own: the tree's line
20
+ * of the name, at the shape it names. A place's own view of a shape shadows the list at that shape: own over root, one
21
+ * writer. Two views naming the same shape in one place is a refuse at rank, before any render.
22
+ */
23
+ export declare const defaulted: (standing: readonly string[], view: View, own: readonly string[], tree: ReadonlyMap<string, View>) => boolean;
24
+ /** Shapes two standing views of one place both name: rank refuses before a render, one path, one writer. */
25
+ export declare function sharedShapes(standing: readonly string[]): readonly string[];
26
+ export declare function placesOf(standing: readonly string[], view: View): readonly string[];
27
+ export {};
@@ -0,0 +1,66 @@
1
+ import { fieldOf } from "./claims.js";
2
+ import { wordOf, wordsOf } from "./wire.js";
3
+ /**
4
+ * A view is a signed line `view/<name>` whose value lists regions in the order they are written, each at the resolution
5
+ * it asks for (`region@n`, the steps of each line it keeps, three when it asks for none, and the one the view is called
6
+ * at for `region@*`), and whose shape, when it has one, names the coordinate the view renders into. What renders a region is
7
+ * read where the fold is known, and a view asking one nothing renders is refused there, by name.
8
+ */
9
+ export function viewsOf(standing) {
10
+ const held = wordsOf(standing, 'families');
11
+ const family = `${held.includes('view') || !held.length ? 'view' : wordOf(standing, 'families', 'view')}/`;
12
+ const views = new Map();
13
+ for (const line of standing) {
14
+ const scope = fieldOf(line, 'scope');
15
+ if (!scope.startsWith(family) || fieldOf(line, 'role') !== 'demands' || fieldOf(line, 'value') === 'withdraw')
16
+ continue;
17
+ const name = scope.slice(family.length);
18
+ if (views.has(name))
19
+ continue;
20
+ const regions = fieldOf(line, 'value').split('|').filter(Boolean)
21
+ .map((token) => ({ name: token.split('@')[0], at: token.includes('@') && Number.isFinite(Number(token.split('@')[1])) ? Number(token.split('@')[1]) : 3, ...(token.endsWith('@*') ? { called: true } : {}) }));
22
+ views.set(name, { name, regions, shape: fieldOf(line, 'shape') });
23
+ }
24
+ return views;
25
+ }
26
+ /**
27
+ * A view the wire's views/world list names writes at a world that holds no view of that name of its own: the tree's line
28
+ * of the name, at the shape it names. A place's own view of a shape shadows the list at that shape: own over root, one
29
+ * writer. Two views naming the same shape in one place is a refuse at rank, before any render.
30
+ */
31
+ export const defaulted = (standing, view, own, tree) => {
32
+ const family = `${wordOf(standing, 'families', 'view')}/`;
33
+ const owns = (name) => own.some((line) => fieldOf(line, 'scope') === `${family}${name}` && fieldOf(line, 'value') !== 'withdraw');
34
+ const writes = (shape) => shape !== '' && own.some((line) => fieldOf(line, 'scope').startsWith(family) && fieldOf(line, 'shape') === shape && fieldOf(line, 'value') !== 'withdraw');
35
+ return !writes(view.shape) && wordsOf(standing, 'views/world').includes(view.name) && tree.get(view.name)?.shape === view.shape && !owns(view.name);
36
+ };
37
+ /** Shapes two standing views of one place both name: rank refuses before a render, one path, one writer. */
38
+ export function sharedShapes(standing) {
39
+ const named = new Map();
40
+ for (const line of standing) {
41
+ const scope = fieldOf(line, 'scope');
42
+ if (!scope.startsWith('view/') || fieldOf(line, 'role') !== 'demands' || fieldOf(line, 'value') === 'withdraw')
43
+ continue;
44
+ const shape = fieldOf(line, 'shape');
45
+ if (!shape)
46
+ continue;
47
+ named.set(shape, [...(named.get(shape) ?? []), scope.slice('view/'.length)]);
48
+ }
49
+ return [...named].filter(([, names]) => new Set(names).size > 1).map(([shape, names]) => `${shape} · view/${[...new Set(names)].join(' · view/')}`);
50
+ }
51
+ ;
52
+ const placed = new WeakMap();
53
+ export function placesOf(standing, view) {
54
+ const known = placed.get(standing) ?? new Map();
55
+ placed.set(standing, known);
56
+ const asked = known.get(`${view.name} ${view.shape}`);
57
+ if (asked !== undefined)
58
+ return asked;
59
+ const scope = `${wordOf(standing, 'families', 'view')}/${view.name}`;
60
+ const lines = standing.filter((one) => fieldOf(one, 'scope') === scope && fieldOf(one, 'value') !== 'withdraw');
61
+ const same = lines.filter((one) => fieldOf(one, 'value') === fieldOf(lines[0] ?? '', 'value') && fieldOf(one, 'shape') === view.shape);
62
+ const places = !view.shape ? [] : [...new Set(same.flatMap((one) => fieldOf(one, 'needs').split('|'))
63
+ .filter((need) => need === view.shape || need.endsWith(`/${view.shape}`)).map((need) => need.slice(0, need.length - view.shape.length)))];
64
+ known.set(`${view.name} ${view.shape}`, places);
65
+ return places;
66
+ }
@@ -0,0 +1,19 @@
1
+ import type { RoleClaim } from '@lapxo/topos/wire';
2
+ export type CoordinateRole = 'source' | 'derived' | 'observed' | 'foreign';
3
+ export declare const namesAt: (at: string, name: string) => boolean;
4
+ export declare const roleAt: (at: string, claims: readonly RoleClaim[]) => CoordinateRole;
5
+ /** The standing line that names a wire list, under `wire/`, else the older `audit/wire/` for one era. A word the wire names is read from the lock by the list it belongs to: the instrument asks for one and refuses if the lock no longer lists it, so an alphabet is never frozen here and a family that moves is a refusal, not a silence. */
6
+ export declare const wireLine: (standing: readonly string[], list: string) => string | undefined;
7
+ export declare function wordsOf(standing: readonly string[], list: string): readonly string[];
8
+ /**
9
+ * A word asked in its eras, newest first, is read in the lock it is asked of: the first era that lock's own wire lists
10
+ * is the word there, so a place published under an older wire is read in the word it was written in, and none is a
11
+ * refusal. A list that names exactly one word is read as that word; a list that names none, or more than one, is a
12
+ * refusal. The older `audit/wire/` prefix is one era of `wire/`, not a fall-back to another lock.
13
+ */
14
+ export declare function wordOf(standing: readonly string[], list: string, name: string | readonly string[]): string;
15
+ export declare function theWord(standing: readonly string[], list: string): string;
16
+ export declare const wordOn: (standing: readonly string[], list: string, fallback: readonly string[]) => string;
17
+ /** A named word, read from the standing that lists it, else from the fallback lock. */
18
+ export declare function wordFrom(standing: readonly string[], list: string, name: string | readonly string[], fallback: readonly string[]): string;
19
+ export declare const said: (lines: readonly string[], scope: string) => readonly string[];
@@ -0,0 +1,41 @@
1
+ import * as topos from '@lapxo/topos/wire';
2
+ import { fieldOf, selfName } from "./claims.js";
3
+ const spoken = topos;
4
+ export const namesAt = (spoken['namesAt'] ?? spoken['namesLeaf']);
5
+ export const roleAt = (spoken['roleAt'] ?? spoken['leafRoleOf']);
6
+ /** The standing line that names a wire list, under `wire/`, else the older `audit/wire/` for one era. A word the wire names is read from the lock by the list it belongs to: the instrument asks for one and refuses if the lock no longer lists it, so an alphabet is never frozen here and a family that moves is a refusal, not a silence. */
7
+ export const wireLine = (standing, list) => standing.find((line) => fieldOf(line, 'scope') === `wire/${list}` && fieldOf(line, 'value') !== 'withdraw')
8
+ ?? standing.find((line) => fieldOf(line, 'scope') === `audit/wire/${list}` && fieldOf(line, 'value') !== 'withdraw');
9
+ export function wordsOf(standing, list) {
10
+ const said = wireLine(standing, list);
11
+ return (said === undefined ? [] : fieldOf(said, 'value').split('|')).filter(Boolean);
12
+ }
13
+ /**
14
+ * A word asked in its eras, newest first, is read in the lock it is asked of: the first era that lock's own wire lists
15
+ * is the word there, so a place published under an older wire is read in the word it was written in, and none is a
16
+ * refusal. A list that names exactly one word is read as that word; a list that names none, or more than one, is a
17
+ * refusal. The older `audit/wire/` prefix is one era of `wire/`, not a fall-back to another lock.
18
+ */
19
+ export function wordOf(standing, list, name) {
20
+ const held = wordsOf(standing, list);
21
+ const eras = typeof name === 'string' ? [name] : name;
22
+ const found = eras.find((one) => held.includes(one));
23
+ if (found !== undefined)
24
+ return found;
25
+ throw new Error(`${selfName()}: REFUSE·wire \`${eras.join('|')}\` is not one of \`wire/${list}\`: ${held.join('|') || 'the lock lists none'}`);
26
+ }
27
+ export function theWord(standing, list) {
28
+ const held = wordsOf(standing, list);
29
+ if (held.length !== 1) {
30
+ throw new Error(`${selfName()}: REFUSE·wire \`wire/${list}\` names ${held.length} words, not one: ${held.join('|') || 'the lock lists none'}`);
31
+ }
32
+ return wordOf(standing, list, held);
33
+ }
34
+ export const wordOn = (standing, list, fallback) => theWord(wordsOf(standing, list).length ? standing : fallback, list);
35
+ /** A named word, read from the standing that lists it, else from the fallback lock. */
36
+ export function wordFrom(standing, list, name, fallback) {
37
+ const eras = typeof name === 'string' ? [name] : name;
38
+ return wordOf(eras.some((one) => wordsOf(standing, list).includes(one)) ? standing : fallback, list, name);
39
+ }
40
+ export const said = (lines, scope) => lines
41
+ .filter((line) => fieldOf(line, 'scope') === scope && fieldOf(line, 'value') !== 'withdraw').flatMap((line) => fieldOf(line, 'value').split('|').filter(Boolean));
@@ -0,0 +1,16 @@
1
+ /**
2
+ * A line of the tree seen from inside a place: every path it names under the place is read from the place's root, and
3
+ * a path outside it is dropped, since the place alone cannot read it. The place's name drops out of the scope, the condition
4
+ * and the policy a line is held at, and a cone names the coordinates it covers the same way; the place itself is `./`. In a
5
+ * scope the name drops out of the step after the family, or after the one word that qualifies it (prose/en, hazard/<kind>,
6
+ * source/<measure>); a deeper step that spells it is inside a name, such as a package a module imports, and stays.
7
+ */
8
+ export declare function zoomed(line: string, place: string): string;
9
+ /** The lines of each place's own lock that still name the place: seen from inside a place, no scope carries its name. */
10
+ export declare function namingTheirPlace(root: string): number;
11
+ /**
12
+ * What a zoom rewrote that is not the place: over each place the lock publishes, every scope and condition of the tree
13
+ * and of the place's receipts that spells the place as a step, zoomed from inside it. Losing the place's step after the
14
+ * family, or after the one word qualifying it, is the zoom; any other rewrite took a value for the place.
15
+ */
16
+ export declare function zoomedOutsideThePlace(root: string, standing: readonly string[]): number;
@@ -0,0 +1,80 @@
1
+ import { join } from "../host/io.js";
2
+ import { canonical, LOCK, parse, RECEIPTS } from '@lapxo/topos/wire';
3
+ import { fieldOf, foldClaims, isWire } from "./claims.js";
4
+ import { entriesIn, observeText } from "../observe/files.js";
5
+ /**
6
+ * A line of the tree seen from inside a place: every path it names under the place is read from the place's root, and
7
+ * a path outside it is dropped, since the place alone cannot read it. The place's name drops out of the scope, the condition
8
+ * and the policy a line is held at, and a cone names the coordinates it covers the same way; the place itself is `./`. In a
9
+ * scope the name drops out of the step after the family, or after the one word that qualifies it (prose/en, hazard/<kind>,
10
+ * source/<measure>); a deeper step that spells it is inside a name, such as a package a module imports, and stays.
11
+ */
12
+ export function zoomed(line, place) {
13
+ const got = parse(line);
14
+ if (got.kind !== 'fact')
15
+ return line;
16
+ const fields = { ...got.value.fields };
17
+ const under = `${place}/`;
18
+ const paths = (value) => value.split('|').filter(Boolean)
19
+ .flatMap((one) => (one === under ? ['./'] : one.startsWith(under) ? [one.slice(under.length)] : [])).join('|');
20
+ const scope = (value) => {
21
+ if (value === under)
22
+ return './';
23
+ if (value.startsWith(under))
24
+ return value.slice(under.length);
25
+ const steps = value.split('/');
26
+ const at = steps.findIndex((step, i) => step === place && (i === 1 || i === 2) && i < steps.length - 1);
27
+ return at < 0 ? value : [...steps.slice(0, at), ...steps.slice(at + 1)].join('/');
28
+ };
29
+ if (fields['needs'] !== undefined) {
30
+ const kept = paths(fields['needs']);
31
+ if (kept)
32
+ fields['needs'] = kept;
33
+ else
34
+ delete fields['needs'];
35
+ }
36
+ for (const name of ['scope', 'condition'])
37
+ if (fields[name])
38
+ fields[name] = scope(fields[name]);
39
+ const at = fields['at'] ?? '';
40
+ const colon = at.indexOf(':');
41
+ if (colon > 0 && at.startsWith(under, colon + 1))
42
+ fields['at'] = `${at.slice(0, colon + 1)}${at.slice(colon + 1 + under.length)}`;
43
+ if (fields['measure'] === 'cone' && fields['value'])
44
+ fields['value'] = paths(fields['value']);
45
+ return canonical(fields);
46
+ }
47
+ /** The lines of each place's own lock that still name the place: seen from inside a place, no scope carries its name. */
48
+ export function namingTheirPlace(root) {
49
+ return entriesIn(root).filter((entry) => entry.dir && !entry.name.startsWith('.')).flatMap((entry) => {
50
+ const own = (observeText(join(root, entry.name, LOCK)) ?? '').split('\n').filter(isWire);
51
+ return foldClaims(own).standing.filter((line) => fieldOf(line, 'value') !== 'withdraw')
52
+ .map((line) => fieldOf(line, 'scope')).filter((scope) => scope.startsWith(`${entry.name}/`) || scope.includes(`/${entry.name}/`));
53
+ }).length;
54
+ }
55
+ /**
56
+ * What a zoom rewrote that is not the place: over each place the lock publishes, every scope and condition of the tree
57
+ * and of the place's receipts that spells the place as a step, zoomed from inside it. Losing the place's step after the
58
+ * family, or after the one word qualifying it, is the zoom; any other rewrite took a value for the place.
59
+ */
60
+ export function zoomedOutsideThePlace(root, standing) {
61
+ const places = standing.filter((line) => /^publish\/[^/]+\/bootstrap$/.test(fieldOf(line, 'scope')) && fieldOf(line, 'value') !== 'withdraw')
62
+ .map((line) => fieldOf(line, 'scope').split('/')[1]);
63
+ const lost = (before, after, place) => {
64
+ if (after === before)
65
+ return false;
66
+ if (before === `${place}/`)
67
+ return after !== './';
68
+ if (before.startsWith(`${place}/`))
69
+ return after !== before.slice(place.length + 1);
70
+ const steps = before.split('/');
71
+ return ![1, 2].some((i) => steps[i] === place && i < steps.length - 1 && after === [...steps.slice(0, i), ...steps.slice(i + 1)].join('/'));
72
+ };
73
+ return places.reduce((count, place) => {
74
+ const text = observeText(join(root, place, RECEIPTS));
75
+ const receipts = text === undefined ? [] : text.split('\n').filter(isWire);
76
+ return count + [...standing, ...receipts].flatMap((line) => ['scope', 'condition']
77
+ .filter((field) => fieldOf(line, field).split('/').includes(place))
78
+ .filter((field) => lost(fieldOf(line, field), fieldOf(zoomed(line, place), field), place))).length;
79
+ }, 0);
80
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The act of signing: canonical bytes, a private key and the algorithm the wire names for this era in; a signature
3
+ * value out, whose form is the algebra's. The host runs the act and knows no name: which algorithms a signature may
4
+ * carry, this era's and the ones the lines before it were signed under, is told by whoever read the lock.
5
+ */
6
+ export declare const signBytes: (bytes: string, privateKeyPem: string, algorithm: string) => string;
7
+ export declare function verifyBytes(bytes: string, publicKeyPem: string, signature: string, admitted: readonly string[]): boolean;
@@ -0,0 +1,30 @@
1
+ import { createPrivateKey, createPublicKey, sign, verify } from 'node:crypto';
2
+ import { formatSignature, parseSignature } from '@lapxo/topos/wire';
3
+ /**
4
+ * The act of signing: canonical bytes, a private key and the algorithm the wire names for this era in; a signature
5
+ * value out, whose form is the algebra's. The host runs the act and knows no name: which algorithms a signature may
6
+ * carry, this era's and the ones the lines before it were signed under, is told by whoever read the lock.
7
+ */
8
+ export const signBytes = (bytes, privateKeyPem, algorithm) => formatSignature(algorithm, sign(null, Buffer.from(bytes, 'utf8'), createPrivateKey(privateKeyPem)).toString('base64'));
9
+ /** A public key as the lock carries it: base64 SPKI. A PEM is accepted where a tree still holds one. */
10
+ function publicKeyOf(publicKey) {
11
+ return publicKey.startsWith('-----')
12
+ ? createPublicKey(publicKey)
13
+ : createPublicKey({ key: Buffer.from(publicKey, 'base64'), format: 'der', type: 'spki' });
14
+ }
15
+ export function verifyBytes(bytes, publicKeyPem, signature, admitted) {
16
+ const value = parseSignature(signature);
17
+ if (!value || !admitted.includes(value.algorithm))
18
+ return false;
19
+ const raw = Buffer.from(value.raw, 'base64');
20
+ if (raw.length === 0)
21
+ return false;
22
+ try {
23
+ return verify(null, Buffer.from(bytes, 'utf8'), publicKeyOf(publicKeyPem), raw);
24
+ }
25
+ catch (err) {
26
+ if (err instanceof Error)
27
+ return false;
28
+ throw err;
29
+ }
30
+ }
@@ -0,0 +1,11 @@
1
+ export interface Member {
2
+ readonly name: string;
3
+ readonly bytes: Uint8Array;
4
+ }
5
+ /** POSIX archive coordinates; normalization cannot make two members share one destination. */
6
+ export declare function archiveCoordinate(name: string): string;
7
+ /** The gzip/POSIX tar adapter reads regular files, directories and PAX attributes, or rejects the whole archive. */
8
+ export declare function members(archive: Uint8Array): readonly Member[];
9
+ /** Existing links or files are never followed or overwritten by extraction. */
10
+ export declare function archiveDestination(into: string, name: string): string;
11
+ export declare function unpack(archive: Uint8Array, into: string): readonly string[];
@@ -0,0 +1,152 @@
1
+ import { lstatSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { gunzipSync } from 'node:zlib';
4
+ const BLOCK = 512; // POSIX tar record size.
5
+ const refuse = (why) => { throw new Error(`REFUSE·archive ${why}`); };
6
+ /** POSIX archive coordinates; normalization cannot make two members share one destination. */
7
+ export function archiveCoordinate(name) {
8
+ if (!name || name.startsWith('/') || /^[A-Za-z]:/.test(name) || name.includes('\\') || name.includes('\0'))
9
+ refuse(`invalid coordinate ${JSON.stringify(name)}`);
10
+ const parts = name.replace(/\/+$/, '').split('/');
11
+ while (parts[0] === '.')
12
+ parts.shift();
13
+ if (!parts.length || parts.some((part) => !part || part === '.' || part === '..'))
14
+ refuse(`invalid coordinate ${JSON.stringify(name)}`);
15
+ return parts.join('/');
16
+ }
17
+ function pax(bytes) {
18
+ const fields = {};
19
+ for (let at = 0; at < bytes.length;) {
20
+ const space = bytes.indexOf(32, at);
21
+ if (space < 0)
22
+ refuse('malformed PAX record length');
23
+ const lengthText = new TextDecoder().decode(bytes.subarray(at, space));
24
+ if (!/^\d+$/.test(lengthText))
25
+ refuse('malformed PAX record length');
26
+ const length = Number(lengthText);
27
+ const end = at + length;
28
+ if (!Number.isSafeInteger(length) || length <= space - at + 1 || end > bytes.length || bytes[end - 1] !== 10)
29
+ refuse('truncated PAX record');
30
+ const record = new TextDecoder().decode(bytes.subarray(space + 1, end - 1));
31
+ const equals = record.indexOf('=');
32
+ if (equals < 1)
33
+ refuse('malformed PAX attribute');
34
+ const key = record.slice(0, equals);
35
+ if (key.startsWith('GNU.sparse') || key.startsWith('SCHILY.realsize'))
36
+ refuse('sparse PAX members are unsupported');
37
+ fields[key] = record.slice(equals + 1);
38
+ at = end;
39
+ }
40
+ return fields;
41
+ }
42
+ /** The gzip/POSIX tar adapter reads regular files, directories and PAX attributes, or rejects the whole archive. */
43
+ export function members(archive) {
44
+ const bytes = gunzipSync(archive);
45
+ const text = (at, length) => bytes.subarray(at, at + length).toString('utf8').replace(/\0[\s\S]*$/, '');
46
+ const octal = (at, length) => {
47
+ const raw = text(at, length).trim();
48
+ if (!/^[0-7]+$/.test(raw))
49
+ refuse(`invalid octal field at ${at}`);
50
+ const n = Number.parseInt(raw, 8);
51
+ if (!Number.isSafeInteger(n) || n < 0)
52
+ refuse(`unrepresentable field at ${at}`);
53
+ return n;
54
+ };
55
+ const out = [];
56
+ const files = new Set();
57
+ const directories = new Set();
58
+ let global = {};
59
+ let local;
60
+ for (let at = 0;;) {
61
+ if (at + BLOCK > bytes.length)
62
+ refuse('missing complete end records');
63
+ const header = bytes.subarray(at, at + BLOCK);
64
+ if (header.every((byte) => byte === 0)) {
65
+ if (local !== undefined)
66
+ refuse('PAX attributes have no following member');
67
+ if (at + 2 * BLOCK > bytes.length || !bytes.subarray(at).every((byte) => byte === 0))
68
+ refuse('invalid end records or trailing data');
69
+ return out;
70
+ }
71
+ const checksum = header.reduce((sum, byte, i) => sum + (i >= 148 && i < 156 ? 32 : byte), 0);
72
+ if (octal(at + 148, 8) !== checksum)
73
+ refuse(`header checksum mismatch at ${at}`);
74
+ const magic = text(at + 257, 6);
75
+ if (magic && magic !== 'ustar' && magic !== 'ustar ')
76
+ refuse(`unsupported tar format ${magic}`);
77
+ const type = text(at + 156, 1);
78
+ const attrs = { ...global, ...local };
79
+ let size = octal(at + 124, 12);
80
+ if (type !== 'x' && type !== 'g' && attrs['size'] !== undefined) {
81
+ if (!/^\d+$/.test(attrs['size']))
82
+ refuse('invalid PAX size');
83
+ size = Number(attrs['size']);
84
+ if (!Number.isSafeInteger(size))
85
+ refuse('unrepresentable PAX size');
86
+ }
87
+ const next = at + BLOCK + Math.ceil(size / BLOCK) * BLOCK;
88
+ if (next > bytes.length)
89
+ refuse(`truncated member at ${at}`);
90
+ const body = bytes.subarray(at + BLOCK, at + BLOCK + size);
91
+ if (type === 'x' || type === 'g') {
92
+ const fields = pax(body);
93
+ if (type === 'g')
94
+ global = { ...global, ...fields };
95
+ else
96
+ local = { ...local, ...fields };
97
+ }
98
+ else {
99
+ const rawName = attrs['path'] ?? [magic ? text(at + 345, 155) : '', text(at, 100)].filter(Boolean).join('/');
100
+ local = undefined;
101
+ if (type === '5' && /^\.\/?$/.test(rawName) && size === 0) {
102
+ at = next;
103
+ continue;
104
+ }
105
+ const name = archiveCoordinate(rawName);
106
+ if (!['', '0', '5'].includes(type))
107
+ refuse(`unsupported member type ${type} at ${name}`);
108
+ const steps = name.split('/');
109
+ if (steps.slice(0, -1).some((_, i) => files.has(steps.slice(0, i + 1).join('/'))))
110
+ refuse(`file is an ancestor of ${name}`);
111
+ if (type === '5') {
112
+ if (size !== 0 || files.has(name))
113
+ refuse(`invalid directory ${name}`);
114
+ directories.add(name);
115
+ }
116
+ else {
117
+ if (files.has(name) || directories.has(name))
118
+ refuse(`duplicate or conflicting member ${name}`);
119
+ files.add(name);
120
+ steps.slice(0, -1).forEach((_, i) => directories.add(steps.slice(0, i + 1).join('/')));
121
+ out.push({ name, bytes: body });
122
+ }
123
+ }
124
+ at = next;
125
+ }
126
+ }
127
+ /** Existing links or files are never followed or overwritten by extraction. */
128
+ export function archiveDestination(into, name) {
129
+ const root = resolve(into);
130
+ const parts = archiveCoordinate(name).split('/');
131
+ let at = root;
132
+ const base = lstatSync(at, { throwIfNoEntry: false });
133
+ if (base !== undefined && !base.isDirectory())
134
+ refuse(`destination is not a directory: ${root}`);
135
+ for (let i = 0; i < parts.length; i += 1) {
136
+ at = join(at, parts[i]);
137
+ const st = lstatSync(at, { throwIfNoEntry: false });
138
+ if (st !== undefined && (i === parts.length - 1 || !st.isDirectory()))
139
+ refuse(`destination already occupied: ${at}`);
140
+ }
141
+ return at;
142
+ }
143
+ export function unpack(archive, into) {
144
+ const files = members(archive);
145
+ const destinations = files.map((one) => archiveDestination(into, one.name));
146
+ for (const [i, one] of files.entries()) {
147
+ const at = destinations[i];
148
+ mkdirSync(dirname(at), { recursive: true });
149
+ writeFileSync(at, one.bytes, { flag: 'wx' });
150
+ }
151
+ return files.map((one) => one.name);
152
+ }
@@ -0,0 +1,20 @@
1
+ import type { SpawnSyncOptionsWithStringEncoding } from 'node:child_process';
2
+ import type { Request, Response } from '@lapxo/topos/contract';
3
+ /** Operational process failure, distinct from a valid contract refusal or abstention. */
4
+ export declare class CapsuleProcessError extends Error {
5
+ readonly stdout: string;
6
+ readonly stderr: string;
7
+ readonly status: number | null;
8
+ readonly signal: NodeJS.Signals | null;
9
+ readonly cause: unknown;
10
+ constructor(reason: string, result: {
11
+ stdout?: string;
12
+ stderr?: string;
13
+ status: number | null;
14
+ signal: NodeJS.Signals | null;
15
+ error?: unknown;
16
+ });
17
+ }
18
+ export declare function validResponse(value: unknown, request: Request): value is Response;
19
+ /** Last-line JSON is the wire framing. No partial batch, failed exit or synthetic missing response is accepted. */
20
+ export declare function executeCapsule(command: string, args: readonly string[], requests: readonly Request[], limits?: Pick<SpawnSyncOptionsWithStringEncoding, 'timeout' | 'maxBuffer'>): readonly Response[];