nervur 0.16.0 → 0.17.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 (111) hide show
  1. package/GETTING_STARTED.md +138 -0
  2. package/NOTICE +1 -1
  3. package/README.md +108 -20
  4. package/dist/being/being.d.ts +24 -0
  5. package/dist/being/being.js +109 -0
  6. package/dist/being/digest.d.ts +3 -0
  7. package/dist/being/digest.js +37 -0
  8. package/dist/being/index.d.ts +5 -0
  9. package/dist/being/index.js +6 -0
  10. package/dist/being/silence.d.ts +12 -0
  11. package/dist/being/silence.js +41 -0
  12. package/dist/being/types.d.ts +89 -0
  13. package/dist/being/types.js +58 -0
  14. package/dist/conformance/assert.d.ts +11 -0
  15. package/dist/conformance/assert.js +106 -0
  16. package/dist/conformance/beings.d.ts +198 -0
  17. package/dist/conformance/beings.js +183 -0
  18. package/dist/conformance/estate.d.ts +5 -0
  19. package/dist/conformance/estate.js +388 -0
  20. package/dist/conformance/index.d.ts +81 -0
  21. package/dist/conformance/index.js +816 -0
  22. package/dist/conformance/reach.d.ts +10 -0
  23. package/dist/conformance/reach.js +72 -0
  24. package/dist/conformance/store.d.ts +5 -0
  25. package/dist/conformance/store.js +123 -0
  26. package/dist/harbor/core.d.ts +53 -0
  27. package/dist/harbor/core.js +621 -0
  28. package/dist/harbor/dial.d.ts +9 -0
  29. package/dist/harbor/dial.js +81 -0
  30. package/dist/harbor/index.d.ts +8 -0
  31. package/dist/harbor/index.js +16 -0
  32. package/dist/harbor/memory.d.ts +25 -0
  33. package/dist/harbor/memory.js +72 -0
  34. package/dist/harbor/reach.d.ts +36 -0
  35. package/dist/harbor/reach.js +191 -0
  36. package/dist/harbor/store.d.ts +40 -0
  37. package/dist/harbor/store.js +70 -0
  38. package/dist/vector/cases.d.ts +41 -0
  39. package/dist/vector/cases.js +195 -0
  40. package/dist/vector/index.d.ts +6 -0
  41. package/dist/vector/index.js +8 -0
  42. package/dist/vector/stand.d.ts +9 -0
  43. package/dist/vector/stand.js +76 -0
  44. package/dist/vector/world.d.ts +143 -0
  45. package/dist/vector/world.js +198 -0
  46. package/dist/ward/allowance.d.ts +10 -0
  47. package/dist/ward/allowance.js +71 -0
  48. package/dist/ward/arithmetic.d.ts +31 -0
  49. package/dist/ward/arithmetic.js +245 -0
  50. package/dist/ward/cells.d.ts +7 -0
  51. package/dist/ward/cells.js +186 -0
  52. package/dist/ward/door.d.ts +18 -0
  53. package/dist/ward/door.js +186 -0
  54. package/dist/ward/ground.d.ts +23 -0
  55. package/dist/ward/ground.js +38 -0
  56. package/dist/ward/heirs.d.ts +13 -0
  57. package/dist/ward/heirs.js +115 -0
  58. package/dist/ward/index.d.ts +9 -0
  59. package/dist/ward/index.js +13 -0
  60. package/dist/ward/owner.d.ts +13 -0
  61. package/dist/ward/owner.js +220 -0
  62. package/dist/ward/partition.d.ts +65 -0
  63. package/dist/ward/partition.js +295 -0
  64. package/dist/ward/seal.d.ts +52 -0
  65. package/dist/ward/seal.js +145 -0
  66. package/dist/ward/stance.d.ts +25 -0
  67. package/dist/ward/stance.js +413 -0
  68. package/dist/ward/ward.d.ts +12 -0
  69. package/dist/ward/ward.js +361 -0
  70. package/package.json +37 -15
  71. package/protocol/SPEC.md +1843 -0
  72. package/protocol/vectors/arithmetic.json +117 -0
  73. package/protocol/vectors/door.json +345 -0
  74. package/protocol/vectors/framing.json +92 -0
  75. package/protocol/vectors/wire.json +48 -0
  76. package/quo-kit.md +652 -0
  77. package/src/being/being.ts +123 -0
  78. package/src/being/digest.ts +46 -0
  79. package/src/being/index.ts +7 -0
  80. package/src/being/silence.ts +46 -0
  81. package/src/being/types.ts +170 -0
  82. package/src/conformance/assert.ts +100 -0
  83. package/src/conformance/beings.ts +183 -0
  84. package/src/conformance/estate.ts +412 -0
  85. package/src/conformance/index.ts +962 -0
  86. package/src/conformance/reach.ts +83 -0
  87. package/src/conformance/store.ts +136 -0
  88. package/src/harbor/core.ts +660 -0
  89. package/src/harbor/dial.ts +112 -0
  90. package/src/harbor/index.ts +17 -0
  91. package/src/harbor/memory.ts +83 -0
  92. package/src/harbor/reach.ts +215 -0
  93. package/src/harbor/store.ts +101 -0
  94. package/src/vector/cases.ts +229 -0
  95. package/src/vector/index.ts +11 -0
  96. package/src/vector/stand.ts +75 -0
  97. package/src/vector/world.ts +220 -0
  98. package/src/ward/allowance.ts +85 -0
  99. package/src/ward/arithmetic.ts +247 -0
  100. package/src/ward/cells.ts +190 -0
  101. package/src/ward/door.ts +186 -0
  102. package/src/ward/ground.ts +160 -0
  103. package/src/ward/heirs.ts +114 -0
  104. package/src/ward/index.ts +17 -0
  105. package/src/ward/owner.ts +214 -0
  106. package/src/ward/partition.ts +353 -0
  107. package/src/ward/seal.ts +174 -0
  108. package/src/ward/stance.ts +433 -0
  109. package/src/ward/ward.ts +378 -0
  110. package/index.js +0 -17
  111. package/nervur.js +0 -5
@@ -0,0 +1,195 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The door's thirteen cases as records. One record per case, D1 through
3
+ // D13, both halves of D8 and both of D9. Each is taken in its own world, so
4
+ // the stream is at the same place every time and a record is reproduced
5
+ // without running the ones before it.
6
+ //
7
+ // A case is its setup and its arrival, held apart: `arrive` stands the world
8
+ // in the case's state and seals the ask, and whoever holds the record knocks.
9
+ // `doorCases` knocks at once and is what `door.json` is written from and
10
+ // held to. Vector mode stops after `arrive` and lets a verifier bring the
11
+ // bytes, which is the whole reason the two halves are two.
12
+ //
13
+ // `kind` is what the reply is, not what it says: `silence` is the one shape
14
+ // every stranger gets, `word` is what a ward says to a key it has bound, and
15
+ // `object` is an answer. The law of one silence is a law about `silence`
16
+ // alone, and the words are here so a kit sees they are a different length on
17
+ // purpose.
18
+ import { Ward } from '../ward/ward.js';
19
+ import * as seal from '../ward/seal.js';
20
+ import { hex } from '../ward/arithmetic.js';
21
+ import { world, sealed, bound, secret, rnd, partitionDigest, Caller } from './world.js';
22
+ // Stand one case: its world, its setup, its ask. What comes back is enough
23
+ // to knock, and enough for a verifier to check the hand before the door.
24
+ export async function stand(c) {
25
+ const w = await world();
26
+ let before = await partitionDigest(w.b, c.ward);
27
+ const arrival = await c.arrive(w, async () => {
28
+ before = await partitionDigest(w.b, c.ward);
29
+ });
30
+ return { w, before, arrival };
31
+ }
32
+ async function record(c) {
33
+ const { w, before, arrival } = await stand(c);
34
+ const judged = await arrival.door.door(arrival.ask.bytes);
35
+ const after = await partitionDigest(w.b, c.ward);
36
+ const opens = arrival.ask.open ? (await arrival.ask.open(judged.bytes)) : null;
37
+ return { case: c.case, name: c.name, state: c.state, ward: c.ward, kind: arrival.kind, heard: judged.heard, ask: hex(arrival.ask.bytes), reply: hex(judged.bytes), opens, before, after, wrote: before !== after };
38
+ }
39
+ // One after another and never at once: the cases share one stream, and two
40
+ // of them in flight together would draw from it in whatever order the
41
+ // machine's crypto happened to finish in.
42
+ export async function doorCases() {
43
+ const out = [];
44
+ for (const c of cases)
45
+ out.push(await record(c));
46
+ return out;
47
+ }
48
+ // A case by its two names, which together are unique in the corpus.
49
+ export const caseNamed = (kase, name) => cases.find((c) => c.case === kase && c.name === name);
50
+ const at = async (door, to, kind, h = {}) => ({ kind, door, ask: await sealed(door.pk, to, h) });
51
+ // The D2 body: a bound relation, then one field of the wrong shape.
52
+ async function bad(w, pin, h) {
53
+ const b = await bound(w);
54
+ await pin();
55
+ return at(w.WB, b.inv.heir, 'silence', { signer: b.own, ...h });
56
+ }
57
+ export const cases = [
58
+ {
59
+ case: 'D1',
60
+ name: 'wrong padlock',
61
+ state: "a heir the door holds, sealed to another ward's padlock",
62
+ ward: 'B',
63
+ arrive: async (w) => at(w.WB, w.inv.heir, 'silence', { signer: secret(w.inv), padlock: (await seal.wardKey('other')).padlockPk }),
64
+ },
65
+ {
66
+ case: 'D1',
67
+ name: 'noise',
68
+ state: 'bytes that are not a sealed ask at all',
69
+ ward: 'B',
70
+ // Nobody holds a secret for the lid this one is sealed to: the door
71
+ // answers to the first thirty-two bytes of the noise, and those are not
72
+ // a key anyone made. That is the case, not a gap in it.
73
+ arrive: async (w) => ({ kind: 'silence', door: w.WB, ask: { bytes: Uint8Array.of(1, ...rnd(32), ...rnd(80)), open: null } }),
74
+ },
75
+ { case: 'D2', name: 'method is not a string', state: 'a bound key, one field of the wrong shape', ward: 'B', arrive: (w, pin) => bad(w, pin, { method: 7 }) },
76
+ { case: 'D2', name: 'args is not an object', state: 'a bound key, one field of the wrong shape', ward: 'B', arrive: (w, pin) => bad(w, pin, { args: 7 }) },
77
+ { case: 'D2', name: 'seq is zero', state: 'a bound key, one field of the wrong shape', ward: 'B', arrive: (w, pin) => bad(w, pin, { seq: 0 }) },
78
+ { case: 'D2', name: 'time is negative', state: 'a bound key, one field of the wrong shape', ward: 'B', arrive: (w, pin) => bad(w, pin, { time: -1 }) },
79
+ { case: 'D2', name: 'by is not a key', state: 'a bound key, one field of the wrong shape', ward: 'B', arrive: (w, pin) => bad(w, pin, { by: 'nope' }) },
80
+ { case: 'D2', name: 'hops at zero', state: 'a bound key, and a chain that has run out', ward: 'B', arrive: (w, pin) => bad(w, pin, { hops: 0 }) },
81
+ { case: 'D3', name: 'for nobody, nobody home', state: 'a ward with no public being', ward: 'B', arrive: (w) => at(w.WB, null, 'silence') },
82
+ { case: 'D4', name: 'for nobody, the signature fails', state: 'a ward with a public being, a body signed by another key', ward: 'P', arrive: async (w) => at(w.WP, null, 'silence', { by: (await seal.beingKey(rnd())).pk }) },
83
+ { case: 'D5', name: 'a heir not held', state: 'a heir this ward never minted', ward: 'B', arrive: (w) => at(w.WB, 'ff'.repeat(32), 'silence') },
84
+ {
85
+ case: 'D6',
86
+ name: 'a key not admitted',
87
+ state: 'a heir the door holds, a key nobody announced',
88
+ ward: 'B',
89
+ arrive: async (w, pin) => {
90
+ const b = await bound(w);
91
+ await pin();
92
+ return at(w.WB, b.inv.heir, 'silence');
93
+ },
94
+ },
95
+ {
96
+ case: 'D7',
97
+ name: 'the signature fails under an admitted key',
98
+ state: 'a bound key named in by, a body signed by another',
99
+ ward: 'B',
100
+ arrive: async (w, pin) => {
101
+ const b = await bound(w);
102
+ await pin();
103
+ return at(w.WB, b.inv.heir, 'silence', { signer: rnd(), by: b.ownPk });
104
+ },
105
+ },
106
+ {
107
+ case: 'D8',
108
+ name: 'she is not there',
109
+ state: 'the ward restarted without her class: her heirs stand and she does not',
110
+ ward: 'B',
111
+ arrive: async (w, pin) => {
112
+ const WB = await w.b.reboot('B', Ward, { Caller });
113
+ await pin();
114
+ return at(WB, w.inv.heir, 'word', { signer: secret(w.inv), next: 'a'.repeat(64) });
115
+ },
116
+ },
117
+ {
118
+ case: 'D8',
119
+ name: 'she is there and the record is not',
120
+ state: 'the being is booted and the occupant her heir was opened for is gone',
121
+ ward: 'B',
122
+ arrive: async (w, pin) => {
123
+ const b = await bound(w);
124
+ delete w.host.cells.occupants[b.rec().id];
125
+ await pin();
126
+ return at(w.WB, b.inv.heir, 'word', { signer: b.own });
127
+ },
128
+ },
129
+ {
130
+ case: 'D9',
131
+ name: 'a heir that announces nothing',
132
+ state: 'a fresh heir, and no next in the body',
133
+ ward: 'B',
134
+ arrive: async (w, pin) => {
135
+ const inv = await w.host.welcome();
136
+ await pin();
137
+ return at(w.WB, inv.heir, 'word', { signer: secret(inv), next: null });
138
+ },
139
+ },
140
+ {
141
+ case: 'D9',
142
+ name: 'a bound key that announces nothing',
143
+ state: 'a key the door already vouched for, and no next in the body',
144
+ ward: 'B',
145
+ arrive: async (w, pin) => {
146
+ const b = await bound(w);
147
+ await pin();
148
+ return at(w.WB, b.inv.heir, 'object', { signer: b.own, next: null });
149
+ },
150
+ },
151
+ {
152
+ case: 'D10',
153
+ name: 'a number already spent',
154
+ state: 'a bound key, a seq at or below the mark',
155
+ ward: 'B',
156
+ arrive: async (w, pin) => {
157
+ const b = await bound(w);
158
+ await pin();
159
+ return at(w.WB, b.inv.heir, 'word', { signer: b.own, seq: 1 });
160
+ },
161
+ },
162
+ {
163
+ case: 'D11',
164
+ name: 'she threw',
165
+ state: 'a bound key on an ask that raises',
166
+ ward: 'B',
167
+ arrive: async (w, pin) => {
168
+ const b = await bound(w);
169
+ await pin();
170
+ return at(w.WB, b.inv.heir, 'word', { signer: b.own, method: 'boom' });
171
+ },
172
+ },
173
+ {
174
+ case: 'D12',
175
+ name: 'she said silence',
176
+ state: 'a bound key on an ask that answers the value silence',
177
+ ward: 'B',
178
+ arrive: async (w, pin) => {
179
+ const b = await bound(w);
180
+ await pin();
181
+ return at(w.WB, b.inv.heir, 'silence', { signer: b.own, method: 'quiet' });
182
+ },
183
+ },
184
+ {
185
+ case: 'D13',
186
+ name: 'a word out of her',
187
+ state: 'a bound key on an ask that returns a shape only a ward may make',
188
+ ward: 'B',
189
+ arrive: async (w, pin) => {
190
+ const b = await bound(w);
191
+ await pin();
192
+ return at(w.WB, b.inv.heir, 'word', { signer: b.own, method: 'bad' });
193
+ },
194
+ },
195
+ ];
@@ -0,0 +1,6 @@
1
+ export { splitmix64, STREAM_SEED, rnd, must, P, B, SILENT, WORD, said, secret, snap, heardBy, partitionDigest, Host, Caller, classes, world, sealed, hand, bound } from './world.ts';
2
+ export type { World, Door, Hand, Sealed } from './world.ts';
3
+ export { cases, caseNamed, stand, doorCases } from './cases.ts';
4
+ export type { DoorRecord, Pin, Arrival, Case } from './cases.ts';
5
+ export { Stand } from './stand.ts';
6
+ export type { Answer } from './stand.ts';
@@ -0,0 +1,8 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The vector shelf: the world the door's cases are taken in, the cases as
3
+ // records, and the stand a verifier replays them against. `door.json` is
4
+ // written from `doorCases` by a hand and held to by the suite; `Stand` is
5
+ // vector mode, the spec's chapter of that name.
6
+ export { splitmix64, STREAM_SEED, rnd, must, P, B, SILENT, WORD, said, secret, snap, heardBy, partitionDigest, Host, Caller, classes, world, sealed, hand, bound } from './world.js';
7
+ export { cases, caseNamed, stand, doorCases } from './cases.js';
8
+ export { Stand } from './stand.js';
@@ -0,0 +1,9 @@
1
+ export type Answer = {
2
+ status: number;
3
+ headers: Record<string, string>;
4
+ body: Uint8Array;
5
+ };
6
+ export declare class Stand {
7
+ #private;
8
+ handle(method: string, path: string, body: Uint8Array, headers?: Record<string, string | undefined>): Promise<Answer>;
9
+ }
@@ -0,0 +1,76 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Vector mode: the harness the spec's chapter of that name describes, as one
3
+ // handler that names no runtime. A listener on any terrain turns its own
4
+ // request into `(method, path, body)` and writes the answer back; on Node
5
+ // that is `test/stand.ts`. What the handler does with an ask is what the
6
+ // request reach does everywhere: post the bytes to `<url>/<pk>`, get the
7
+ // reply bytes back. Beside it, two routes that exist only here: `stand`
8
+ // puts the world in the state of one record of `door.json` and `digest`
9
+ // reads the judged ward's partition as one value.
10
+ //
11
+ // Every answer carries `access-control-allow-origin: *` and an OPTIONS is
12
+ // answered, because a verifier is a page in a stranger's tab as often as a
13
+ // shell, and a stand that only its own origin can reach passes from one and
14
+ // fails from the other while the door is the same.
15
+ //
16
+ // Nothing here is reachable from a harbor that is not in the mode. This file
17
+ // is imported by a hand that stands a kit for a verifier, and by nothing a
18
+ // world runs on.
19
+ import { SUITE } from '../harbor/reach.js';
20
+ import { hex } from '../ward/arithmetic.js';
21
+ import { caseNamed, stand } from './cases.js';
22
+ import { partitionDigest } from './world.js';
23
+ const CORS = {
24
+ 'access-control-allow-origin': '*',
25
+ 'access-control-allow-methods': 'GET, POST, OPTIONS',
26
+ 'access-control-allow-headers': 'content-type, quo-suite',
27
+ };
28
+ const text = (s) => new TextEncoder().encode(s);
29
+ const json = (status, v) => ({ status, headers: { ...CORS, 'content-type': 'application/json' }, body: text(JSON.stringify(v)) });
30
+ const bytes = (b) => ({ status: 200, headers: { ...CORS, 'content-type': 'application/octet-stream' }, body: b });
31
+ const NONE = { status: 404, headers: CORS, body: new Uint8Array(0) };
32
+ export class Stand {
33
+ #w = null;
34
+ #ward = '';
35
+ async handle(method, path, body, headers = {}) {
36
+ const p = path.replace(/\/$/, '');
37
+ if (method === 'OPTIONS')
38
+ return { status: 204, headers: CORS, body: new Uint8Array(0) };
39
+ if (method === 'POST' && p === '/stand') {
40
+ let named;
41
+ try {
42
+ named = JSON.parse(new TextDecoder().decode(body));
43
+ }
44
+ catch {
45
+ return NONE;
46
+ }
47
+ const c = typeof named.case === 'string' && typeof named.name === 'string' ? caseNamed(named.case, named.name) : undefined;
48
+ if (!c)
49
+ return NONE;
50
+ const stood = await stand(c);
51
+ this.#w = stood.w;
52
+ this.#ward = c.ward;
53
+ // What is returned is hex, the corpus's own encoding, so a verifier
54
+ // compares strings and never decodes anything.
55
+ return json(200, { ward: stood.arrival.door.pk, before: stood.before, ask: hex(stood.arrival.ask.bytes) });
56
+ }
57
+ if (method === 'GET' && p === '/digest') {
58
+ if (!this.#w)
59
+ return NONE;
60
+ return json(200, { after: await partitionDigest(this.#w.b, this.#ward) });
61
+ }
62
+ if (method === 'POST' && /^\/[0-9a-f]{128}$/.test(p)) {
63
+ const suite = headers['quo-suite'];
64
+ if (suite !== undefined && suite !== String(SUITE))
65
+ return NONE; // a suite this door does not speak: nothing delivered
66
+ if (!this.#w)
67
+ return NONE;
68
+ const pk = p.slice(1);
69
+ const door = this.#w.b.doors.get(pk) ?? this.#w.a.doors.get(pk);
70
+ if (!door)
71
+ return NONE;
72
+ return bytes((await door(body)).bytes);
73
+ }
74
+ return NONE;
75
+ }
76
+ }
@@ -0,0 +1,143 @@
1
+ import { MemoryHarbor } from '../harbor/memory.ts';
2
+ import * as seal from '../ward/seal.ts';
3
+ import type { Partition } from '../ward/partition.ts';
4
+ import { Being } from '../being/being.ts';
5
+ import type { Answer, Asker, BeingClass, Blueprint, Invitation, JsonObject, Reply, Wanted } from '../being/types.ts';
6
+ export declare function splitmix64(seed: bigint): () => Uint8Array;
7
+ export declare const STREAM_SEED = 20250901n;
8
+ export declare const rnd: (n?: number) => Uint8Array;
9
+ export declare const must: (ok: boolean, what: string) => void;
10
+ export declare const P: (h: MemoryHarbor, seed: string) => Partition;
11
+ export declare const B: <T>(h: MemoryHarbor, seed: string, key: string) => T;
12
+ export declare const SILENT: {
13
+ silence: boolean;
14
+ };
15
+ export declare const WORD: (quo: string) => {
16
+ quo: string;
17
+ };
18
+ export declare const said: (x: unknown, w: string) => boolean;
19
+ export declare const secret: (inv: Invitation) => Uint8Array<ArrayBufferLike>;
20
+ export declare const snap: <T>(v: T) => T;
21
+ export declare const heardBy: (h: Host) => number;
22
+ export declare const partitionDigest: (h: MemoryHarbor, seed: string) => Promise<string>;
23
+ export declare class Host extends Being {
24
+ static cells: {
25
+ heard: JsonObject[];
26
+ describe: string;
27
+ minted: number;
28
+ };
29
+ static asks: {
30
+ hello: {
31
+ input: {
32
+ type: string;
33
+ };
34
+ };
35
+ err: {
36
+ input: {
37
+ type: string;
38
+ };
39
+ };
40
+ nul: {
41
+ input: {
42
+ type: string;
43
+ };
44
+ };
45
+ quiet: {
46
+ input: {
47
+ type: string;
48
+ };
49
+ };
50
+ boom: {
51
+ input: {
52
+ type: string;
53
+ };
54
+ };
55
+ bad: {
56
+ input: {
57
+ type: string;
58
+ };
59
+ };
60
+ never: {
61
+ input: {
62
+ type: string;
63
+ };
64
+ };
65
+ hidden: {
66
+ input: {
67
+ type: string;
68
+ };
69
+ for: () => boolean;
70
+ };
71
+ };
72
+ hello(args: JsonObject, asker: Asker): {
73
+ hi: string | null;
74
+ };
75
+ err(): {
76
+ error: string;
77
+ };
78
+ nul(): null;
79
+ quiet(): symbol;
80
+ boom(): never;
81
+ bad(): Reply;
82
+ never(): Promise<Reply>;
83
+ hidden(): {
84
+ secret: boolean;
85
+ };
86
+ describe(asker: Asker): Blueprint;
87
+ welcome(id?: string): Promise<Invitation>;
88
+ }
89
+ export declare class Caller extends Being {
90
+ static asks: {
91
+ hi: {
92
+ input: {
93
+ type: string;
94
+ };
95
+ };
96
+ };
97
+ hi(): {
98
+ hi: boolean;
99
+ };
100
+ join(id: string, inv: Invitation): Promise<Answer>;
101
+ ask(id: string, method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
102
+ }
103
+ export declare const classes: Record<string, BeingClass>;
104
+ export declare function world(): Promise<{
105
+ a: MemoryHarbor;
106
+ b: MemoryHarbor;
107
+ WA: import("../harbor/memory.ts").Booted;
108
+ WB: import("../harbor/memory.ts").Booted;
109
+ WP: import("../harbor/memory.ts").Booted;
110
+ alice: Caller;
111
+ host: Host;
112
+ pub: Host;
113
+ inv: Invitation;
114
+ heir: () => import("../ward/partition.ts").Heir;
115
+ alicesSeq: () => number;
116
+ }>;
117
+ export type World = Awaited<ReturnType<typeof world>>;
118
+ export type Door = (b: Uint8Array) => Promise<{
119
+ bytes: Uint8Array;
120
+ heard: boolean;
121
+ }>;
122
+ export type Hand = Partial<seal.AskPayload> & {
123
+ signer?: Uint8Array;
124
+ padlock?: Uint8Array;
125
+ };
126
+ export type Sealed = {
127
+ bytes: Uint8Array;
128
+ open(out: Uint8Array): Promise<seal.ReplyPayload | null>;
129
+ };
130
+ export declare function sealed(wardPk: string, to: string | null, h?: Hand): Promise<Sealed>;
131
+ export declare function hand(door: Door, wardPk: string, to: string | null, h?: Hand): Promise<{
132
+ bytes: Uint8Array<ArrayBufferLike>;
133
+ out: Uint8Array<ArrayBufferLike>;
134
+ heard: boolean;
135
+ reply: seal.ReplyPayload | null;
136
+ again: () => Promise<seal.ReplyPayload | null>;
137
+ }>;
138
+ export declare function bound(w: World): Promise<{
139
+ inv: Invitation;
140
+ own: Uint8Array<ArrayBufferLike>;
141
+ ownPk: string;
142
+ rec: () => import("../ward/partition.ts").Heir;
143
+ }>;
@@ -0,0 +1,198 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The world the door's cases are proven in. Two harbors linked as a wire.
3
+ // Alice, a caller, on ward A of harbor a. A host on ward B of harbor b, with
4
+ // an ask for every way of not answering. A public host on ward P of harbor
5
+ // b. And a hand that seals bytes exactly as a ward would, for the cases only
6
+ // bytes can reach.
7
+ //
8
+ // Nothing here draws from the device. A door's reply is sealed under a fresh
9
+ // ephemeral key and a fresh nonce, so the bytes are fixed only where the
10
+ // entropy is, and every draw in this file comes from one written-down stream.
11
+ // That is what lets `protocol/vectors/door.json` be a corpus and not a
12
+ // sample, and what lets a kit stand in vector mode for a verifier that holds
13
+ // no key: the spec's chapter of that name is the contract this file keeps.
14
+ import { MemoryHarbor } from '../harbor/memory.js';
15
+ import { Ward } from '../ward/ward.js';
16
+ import * as seal from '../ward/seal.js';
17
+ import { unhex } from '../ward/arithmetic.js';
18
+ import { Being } from '../being/being.js';
19
+ import { silence, unreached, isSilence, isUnreached, told } from '../being/silence.js';
20
+ import { digest } from '../being/digest.js';
21
+ // ---- the stream
22
+ //
23
+ // SplitMix64. Sixteen lines a kit in any language writes from this comment
24
+ // alone: the state starts at the seed, every draw adds the golden gamma
25
+ // 0x9e3779b97f4a7c15, mixes with two xor-shift-multiplies, and the sixty-four
26
+ // bit result is spent eight bytes at a time, least significant first. It is
27
+ // not a cipher and is not meant to be one. Its whole job is that the bytes
28
+ // this file produces are the same bytes twice.
29
+ const GAMMA = 0x9e3779b97f4a7c15n;
30
+ const M64 = (1n << 64n) - 1n;
31
+ export function splitmix64(seed) {
32
+ let s = seed & M64;
33
+ return () => {
34
+ s = (s + GAMMA) & M64;
35
+ let z = s;
36
+ z = ((z ^ (z >> 30n)) * 0xbf58476d1ce4e5b9n) & M64;
37
+ z = ((z ^ (z >> 27n)) * 0x94d049bb133111ebn) & M64;
38
+ z = z ^ (z >> 31n);
39
+ const out = new Uint8Array(8);
40
+ for (let i = 0; i < 8; i++)
41
+ out[i] = Number((z >> BigInt(8 * i)) & 0xffn);
42
+ return out;
43
+ };
44
+ }
45
+ // The one seed every case stands on, and the first number a kit sets its own
46
+ // stream to when it reproduces `door.json`.
47
+ export const STREAM_SEED = 20250901n;
48
+ let draw = splitmix64(STREAM_SEED);
49
+ // Entropy, from the stream and never from the device. Every signer secret,
50
+ // every ephemeral seed, every byte of noise and every key a ward in this
51
+ // world mints comes through here, in the order the code asks for it.
52
+ export const rnd = (n = 32) => {
53
+ const out = new Uint8Array(n);
54
+ for (let i = 0; i < n; i += 8)
55
+ out.set(draw().subarray(0, Math.min(8, n - i)), i);
56
+ return out;
57
+ };
58
+ // A claim the world stands on. It throws, because a world that is not the
59
+ // one the corpus was taken in cannot produce the corpus, and the sooner that
60
+ // is said the nearer the fault is named.
61
+ export const must = (ok, what) => {
62
+ if (!ok)
63
+ throw new Error(what);
64
+ };
65
+ export const P = (h, seed) => h.partitions.get(seed);
66
+ export const B = (h, seed, key) => h.objects.get(P(h, seed).beings[key]);
67
+ export const SILENT = { silence: true };
68
+ export const WORD = (quo) => ({ quo });
69
+ // Her ward's word for what happened, as she receives it. `told` names the
70
+ // word and leaves everything else alone, so an assertion that goes red says
71
+ // which answer arrived instead of collapsing to false.
72
+ export const said = (x, w) => told(x) === w;
73
+ export const secret = (inv) => unhex(inv.secret);
74
+ // A snapshot that survives the guard on cells: values only, which is all a partition holds.
75
+ export const snap = (v) => JSON.parse(JSON.stringify(v));
76
+ export const heardBy = (h) => h.cells.heard.length;
77
+ // What a partition is, as one value a kit compares. `nothing written` is this
78
+ // number before and after, and never a sentence a reader has to trust.
79
+ export const partitionDigest = (h, seed) => digest(snap(P(h, seed)));
80
+ // A host. One ask per way of not answering, and a describe that can be told
81
+ // to throw or fall silent, so the empty ask has its cases too.
82
+ export class Host extends Being {
83
+ static cells = { heard: [], describe: 'ok', minted: 0 };
84
+ static asks = {
85
+ hello: { input: { type: 'object' } },
86
+ err: { input: { type: 'object' } },
87
+ nul: { input: { type: 'object' } },
88
+ quiet: { input: { type: 'object' } },
89
+ boom: { input: { type: 'object' } },
90
+ bad: { input: { type: 'object' } },
91
+ never: { input: { type: 'object' } },
92
+ hidden: { input: { type: 'object' }, for: () => false },
93
+ };
94
+ hello(args, asker) {
95
+ this.cells.heard.push({ id: asker.id ?? null, ...args });
96
+ return { hi: asker.id ?? null };
97
+ }
98
+ err() {
99
+ return { error: 'nope' };
100
+ }
101
+ nul() {
102
+ return null;
103
+ }
104
+ quiet() {
105
+ return silence;
106
+ }
107
+ boom() {
108
+ throw new Error('she threw');
109
+ }
110
+ bad() {
111
+ return unreached(); // a shape that is not hers to make: her ward reads it as silence
112
+ }
113
+ never() {
114
+ return new Promise(() => { });
115
+ }
116
+ hidden() {
117
+ return { secret: true };
118
+ }
119
+ describe(asker) {
120
+ if (this.cells.describe === 'throw')
121
+ throw new Error('not now');
122
+ if (this.cells.describe === 'silent')
123
+ return silence;
124
+ return super.describe(asker);
125
+ }
126
+ async welcome(id) {
127
+ return (await this.invite(id ?? `g${++this.cells.minted}`));
128
+ }
129
+ }
130
+ // A caller. Knocks, takes, asks, and hands back exactly what her ward said.
131
+ export class Caller extends Being {
132
+ static asks = { hi: { input: { type: 'object' } } };
133
+ hi() {
134
+ return { hi: true };
135
+ }
136
+ async join(id, inv) {
137
+ const out = await this.knock(inv, 'hello', { via: 'knock' });
138
+ if (!isSilence(out) && !isUnreached(out))
139
+ must((await this.take(id, inv)) === id, `take ${id}`);
140
+ return out;
141
+ }
142
+ ask(id, method, args = {}, wanted) {
143
+ return this.standings[id].ask(method, args, wanted);
144
+ }
145
+ }
146
+ export const classes = { Host, Caller };
147
+ // The number a hand's asks count from. Reset with the stream, so a case seals
148
+ // the same seq every run and not merely a fresh one.
149
+ let counted = 1000;
150
+ export async function world() {
151
+ draw = splitmix64(STREAM_SEED); // a fresh world is a fresh stream: two runs, one corpus
152
+ counted = 1000;
153
+ const a = new MemoryHarbor(), b = new MemoryHarbor();
154
+ a.random = rnd;
155
+ b.random = rnd;
156
+ a.link(b);
157
+ const WA = await a.boot('A', Ward, classes);
158
+ const WB = await b.boot('B', Ward, classes);
159
+ const WP = await b.boot('P', Ward, classes);
160
+ await WA.ask('boot', { key: 'alice', class: 'Caller' });
161
+ await WB.ask('boot', { key: 'host', class: 'Host' });
162
+ await WP.ask('boot', { key: 'pub', class: 'Host' });
163
+ await WP.ask('public', { key: 'pub' });
164
+ const alice = B(a, 'A', 'alice');
165
+ const host = B(b, 'B', 'host');
166
+ const pub = B(b, 'P', 'pub');
167
+ const inv = await host.welcome('alice');
168
+ const joined = await alice.join('host', inv);
169
+ must(JSON.stringify(joined) === JSON.stringify({ hi: 'alice' }), 'alice joins the host');
170
+ const heir = () => P(b, 'B').heirs[inv.heir];
171
+ const alicesSeq = () => P(a, 'A').bind.alice.standings.host.seq;
172
+ return { a, b, WA, WB, WP, alice, host, pub, inv, heir, alicesSeq };
173
+ }
174
+ export async function sealed(wardPk, to, h = {}) {
175
+ const signer = h.signer ?? rnd();
176
+ const by = h.by ?? (await seal.beingKey(signer)).pk;
177
+ const payload = { by, next: null, seq: ++counted, time: 30_000, method: 'hello', args: {}, ...h };
178
+ delete payload.signer;
179
+ delete payload.padlock;
180
+ const { bytes, ephemeral } = await seal.sealAsk(to, payload, signer, h.padlock ?? seal.wardPadlock(wardPk), rnd());
181
+ return { bytes, open: (out) => seal.openReply(out, ephemeral, seal.wardSignPk(wardPk)) };
182
+ }
183
+ export async function hand(door, wardPk, to, h = {}) {
184
+ const s = await sealed(wardPk, to, h);
185
+ const judged = await door(s.bytes);
186
+ const out = judged.bytes;
187
+ return { bytes: s.bytes, out, heard: judged.heard, reply: await s.open(out), again: async () => s.open((await door(s.bytes)).bytes) };
188
+ }
189
+ // A fresh relation on the host, knocked by hand so the test holds the keys.
190
+ export async function bound(w) {
191
+ const inv = await w.host.welcome();
192
+ const own = rnd();
193
+ const ownPk = (await seal.beingKey(own)).pk;
194
+ const knock = await hand(w.WB.door, w.WB.pk, inv.heir, { signer: secret(inv), next: ownPk });
195
+ const minted = w.host.cells.minted;
196
+ must(JSON.stringify(knock.reply.object) === JSON.stringify({ hi: `g${minted}` }), `bound g${minted}`);
197
+ return { inv, own, ownPk, rec: () => P(w.b, 'B').heirs[inv.heir] };
198
+ }
@@ -0,0 +1,10 @@
1
+ import type { Wanted } from '../being/types.ts';
2
+ export type Allowance = {
3
+ time: number;
4
+ };
5
+ export declare const DEFAULT: Allowance;
6
+ export declare const CEILING: Allowance;
7
+ export declare function allow(wanted: Wanted | undefined, ceiling?: Allowance, base?: Allowance): Allowance;
8
+ export declare const spent: (a: Allowance) => boolean;
9
+ export declare const LATE: unique symbol;
10
+ export declare function within<T>(ms: number, work: Promise<T>, rang?: () => void): Promise<T | typeof LATE>;