nervur 0.19.2 → 0.20.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 (205) hide show
  1. package/README.md +66 -101
  2. package/dist/being/being.d.ts +7 -15
  3. package/dist/being/being.js +16 -67
  4. package/dist/being/digest.d.ts +1 -1
  5. package/dist/being/digest.js +7 -27
  6. package/dist/being/faculty.d.ts +9 -0
  7. package/dist/being/faculty.js +68 -0
  8. package/dist/being/index.d.ts +4 -5
  9. package/dist/being/index.js +5 -5
  10. package/dist/being/types.d.ts +35 -62
  11. package/dist/being/types.js +3 -57
  12. package/dist/being/words.d.ts +13 -0
  13. package/dist/being/words.js +14 -0
  14. package/dist/contract/index.d.ts +26 -0
  15. package/dist/contract/index.js +19 -0
  16. package/dist/crypto/aes.d.ts +3 -0
  17. package/dist/crypto/aes.js +24 -0
  18. package/dist/crypto/bytes.d.ts +6 -0
  19. package/dist/crypto/bytes.js +33 -0
  20. package/dist/crypto/ed25519.d.ts +3 -0
  21. package/dist/crypto/ed25519.js +85 -0
  22. package/dist/crypto/hash.d.ts +2 -0
  23. package/dist/crypto/hash.js +11 -0
  24. package/dist/crypto/index.d.ts +7 -0
  25. package/dist/crypto/index.js +10 -0
  26. package/dist/crypto/json.d.ts +8 -0
  27. package/dist/crypto/json.js +250 -0
  28. package/dist/crypto/mlkem.d.ts +12 -0
  29. package/dist/crypto/mlkem.js +36 -0
  30. package/dist/crypto/subtle.d.ts +10 -0
  31. package/dist/crypto/subtle.js +29 -0
  32. package/dist/crypto/x25519.d.ts +2 -0
  33. package/dist/crypto/x25519.js +21 -0
  34. package/dist/folder/index.d.ts +13 -0
  35. package/dist/folder/index.js +125 -0
  36. package/dist/harbor/dock.d.ts +19 -0
  37. package/dist/harbor/dock.js +92 -0
  38. package/dist/harbor/harbor.d.ts +22 -0
  39. package/dist/harbor/harbor.js +84 -0
  40. package/dist/harbor/index.d.ts +4 -10
  41. package/dist/harbor/index.js +5 -18
  42. package/dist/harbor/registry.d.ts +7 -0
  43. package/dist/harbor/registry.js +13 -0
  44. package/dist/harbor/terrain.d.ts +8 -0
  45. package/dist/harbor/terrain.js +2 -0
  46. package/dist/index.d.ts +7 -0
  47. package/dist/index.js +14 -0
  48. package/dist/pointer/bodies.d.ts +20 -0
  49. package/dist/pointer/bodies.js +45 -0
  50. package/dist/pointer/index.d.ts +2 -0
  51. package/dist/pointer/index.js +4 -0
  52. package/dist/pointer/world.d.ts +34 -0
  53. package/dist/pointer/world.js +76 -0
  54. package/dist/quo/door.d.ts +34 -0
  55. package/dist/quo/door.js +172 -0
  56. package/dist/quo/index.d.ts +8 -0
  57. package/dist/quo/index.js +11 -0
  58. package/dist/quo/invitation.d.ts +7 -0
  59. package/dist/quo/invitation.js +15 -0
  60. package/dist/quo/keys.d.ts +40 -0
  61. package/dist/quo/keys.js +79 -0
  62. package/dist/quo/payload.d.ts +15 -0
  63. package/dist/quo/payload.js +55 -0
  64. package/dist/quo/relations.d.ts +40 -0
  65. package/dist/quo/relations.js +33 -0
  66. package/dist/quo/reply.d.ts +13 -0
  67. package/dist/quo/reply.js +35 -0
  68. package/dist/quo/seal.d.ts +42 -0
  69. package/dist/quo/seal.js +78 -0
  70. package/dist/quo/standing.d.ts +39 -0
  71. package/dist/quo/standing.js +90 -0
  72. package/dist/ward/allowance.d.ts +13 -9
  73. package/dist/ward/allowance.js +28 -67
  74. package/dist/ward/cells.d.ts +6 -6
  75. package/dist/ward/cells.js +67 -179
  76. package/dist/ward/index.d.ts +5 -10
  77. package/dist/ward/index.js +6 -13
  78. package/dist/ward/partition.d.ts +44 -60
  79. package/dist/ward/partition.js +56 -285
  80. package/dist/ward/stance.d.ts +29 -20
  81. package/dist/ward/stance.js +173 -406
  82. package/dist/ward/ward-being.d.ts +33 -0
  83. package/dist/ward/ward-being.js +76 -0
  84. package/dist/ward/ward.d.ts +43 -11
  85. package/dist/ward/ward.js +215 -363
  86. package/package.json +19 -33
  87. package/src/being/being.ts +31 -78
  88. package/src/being/digest.ts +7 -35
  89. package/src/being/faculty.ts +66 -0
  90. package/src/being/index.ts +5 -6
  91. package/src/being/types.ts +47 -145
  92. package/src/being/words.ts +31 -0
  93. package/src/contract/index.ts +44 -0
  94. package/src/crypto/aes.ts +26 -0
  95. package/src/crypto/bytes.ts +37 -0
  96. package/src/crypto/ed25519.ts +84 -0
  97. package/src/crypto/hash.ts +14 -0
  98. package/src/crypto/index.ts +10 -0
  99. package/src/crypto/json.ts +241 -0
  100. package/src/crypto/mlkem.ts +38 -0
  101. package/src/crypto/subtle.ts +33 -0
  102. package/src/crypto/x25519.ts +21 -0
  103. package/src/folder/index.ts +134 -0
  104. package/src/harbor/dock.ts +101 -0
  105. package/src/harbor/harbor.ts +105 -0
  106. package/src/harbor/index.ts +5 -19
  107. package/src/harbor/registry.ts +20 -0
  108. package/src/harbor/terrain.ts +13 -0
  109. package/src/index.ts +20 -0
  110. package/src/pointer/bodies.ts +47 -0
  111. package/src/pointer/index.ts +4 -0
  112. package/src/pointer/world.ts +91 -0
  113. package/src/quo/door.ts +178 -0
  114. package/src/quo/index.ts +11 -0
  115. package/src/quo/invitation.ts +15 -0
  116. package/src/quo/keys.ts +91 -0
  117. package/src/quo/payload.ts +61 -0
  118. package/src/quo/relations.ts +62 -0
  119. package/src/quo/reply.ts +38 -0
  120. package/src/quo/seal.ts +101 -0
  121. package/src/quo/standing.ts +111 -0
  122. package/src/stand/main.ts +19 -0
  123. package/src/stand/stand.ts +178 -0
  124. package/src/ward/allowance.ts +37 -75
  125. package/src/ward/cells.ts +63 -176
  126. package/src/ward/index.ts +6 -17
  127. package/src/ward/partition.ts +86 -326
  128. package/src/ward/stance.ts +185 -420
  129. package/src/ward/ward-being.ts +97 -0
  130. package/src/ward/ward.ts +229 -363
  131. package/dist/being/lent.d.ts +0 -32
  132. package/dist/being/lent.js +0 -72
  133. package/dist/being/silence.d.ts +0 -12
  134. package/dist/being/silence.js +0 -41
  135. package/dist/conformance/assert.d.ts +0 -11
  136. package/dist/conformance/assert.js +0 -106
  137. package/dist/conformance/beings.d.ts +0 -199
  138. package/dist/conformance/beings.js +0 -188
  139. package/dist/conformance/estate.d.ts +0 -5
  140. package/dist/conformance/estate.js +0 -388
  141. package/dist/conformance/index.d.ts +0 -80
  142. package/dist/conformance/index.js +0 -819
  143. package/dist/conformance/reach.d.ts +0 -10
  144. package/dist/conformance/reach.js +0 -72
  145. package/dist/conformance/store.d.ts +0 -5
  146. package/dist/conformance/store.js +0 -113
  147. package/dist/harbor/box.d.ts +0 -121
  148. package/dist/harbor/box.js +0 -121
  149. package/dist/harbor/core.d.ts +0 -55
  150. package/dist/harbor/core.js +0 -662
  151. package/dist/harbor/dial.d.ts +0 -9
  152. package/dist/harbor/dial.js +0 -81
  153. package/dist/harbor/memory.d.ts +0 -29
  154. package/dist/harbor/memory.js +0 -104
  155. package/dist/harbor/reach.d.ts +0 -36
  156. package/dist/harbor/reach.js +0 -199
  157. package/dist/harbor/store.d.ts +0 -35
  158. package/dist/harbor/store.js +0 -62
  159. package/dist/vector/cases.d.ts +0 -42
  160. package/dist/vector/cases.js +0 -223
  161. package/dist/vector/index.d.ts +0 -6
  162. package/dist/vector/index.js +0 -8
  163. package/dist/vector/stand.d.ts +0 -9
  164. package/dist/vector/stand.js +0 -77
  165. package/dist/vector/world.d.ts +0 -144
  166. package/dist/vector/world.js +0 -209
  167. package/dist/ward/arithmetic.d.ts +0 -31
  168. package/dist/ward/arithmetic.js +0 -249
  169. package/dist/ward/door.d.ts +0 -18
  170. package/dist/ward/door.js +0 -186
  171. package/dist/ward/ground.d.ts +0 -29
  172. package/dist/ward/ground.js +0 -56
  173. package/dist/ward/heirs.d.ts +0 -13
  174. package/dist/ward/heirs.js +0 -117
  175. package/dist/ward/json.d.ts +0 -2
  176. package/dist/ward/json.js +0 -163
  177. package/dist/ward/owner.d.ts +0 -13
  178. package/dist/ward/owner.js +0 -220
  179. package/dist/ward/seal.d.ts +0 -52
  180. package/dist/ward/seal.js +0 -150
  181. package/src/being/lent.ts +0 -72
  182. package/src/being/silence.ts +0 -46
  183. package/src/conformance/assert.ts +0 -100
  184. package/src/conformance/beings.ts +0 -188
  185. package/src/conformance/estate.ts +0 -412
  186. package/src/conformance/index.ts +0 -965
  187. package/src/conformance/reach.ts +0 -83
  188. package/src/conformance/store.ts +0 -125
  189. package/src/harbor/box.ts +0 -131
  190. package/src/harbor/core.ts +0 -699
  191. package/src/harbor/dial.ts +0 -112
  192. package/src/harbor/memory.ts +0 -123
  193. package/src/harbor/reach.ts +0 -221
  194. package/src/harbor/store.ts +0 -91
  195. package/src/vector/cases.ts +0 -257
  196. package/src/vector/index.ts +0 -11
  197. package/src/vector/stand.ts +0 -76
  198. package/src/vector/world.ts +0 -232
  199. package/src/ward/arithmetic.ts +0 -251
  200. package/src/ward/door.ts +0 -186
  201. package/src/ward/ground.ts +0 -142
  202. package/src/ward/heirs.ts +0 -116
  203. package/src/ward/json.ts +0 -144
  204. package/src/ward/owner.ts +0 -214
  205. package/src/ward/seal.ts +0 -178
@@ -0,0 +1,111 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The standing's side of a relation: it seals asks and reads replies, and
3
+ // moves to its announced key and the following edge key when an object
4
+ // comes back to an ask above every ask it has moved on, and on nothing else.
5
+ //
6
+ // Its state is a value, so a ward keeps it in its partition.
7
+ import { encapsulate, hex, unhex } from '../crypto/index.ts';
8
+ import type { Invitation } from './invitation.ts';
9
+ import { Ephemeral, SigningKey, wardHalves, type Draw } from './keys.ts';
10
+ import { writePayload } from './payload.ts';
11
+ import { readReply, type Reply } from './reply.ts';
12
+ import { checkBody, follow, knockEdge, openReply, sealAsk, signBody, splitBody } from './seal.ts';
13
+
14
+ export type Read = Reply | { readonly nothing: true };
15
+
16
+ // A knock that brought no object back: its bytes, sent again as they are,
17
+ // and what the standing moves to if it bound.
18
+ type Knock = { readonly bytes: string; readonly lid: string; readonly next: string; readonly edge: string; readonly seq: number };
19
+
20
+ export type StandingState = {
21
+ readonly key: string | null;
22
+ readonly edge: string | null;
23
+ readonly sent: number;
24
+ readonly moved: number;
25
+ readonly knock: Knock | null;
26
+ // Whether the last ask after an unanswered knock asked under its own key,
27
+ // so the next sends the knock again.
28
+ readonly probed: boolean;
29
+ };
30
+
31
+ export const freshStanding = (): StandingState => ({ key: null, edge: null, sent: 0, moved: 0, knock: null, probed: false });
32
+
33
+ // One ask out: what `read` needs to read its reply and to move.
34
+ export type Sent = { readonly bytes: Uint8Array; readonly lid: Ephemeral; readonly next: string; readonly edge: string; readonly seq: number };
35
+
36
+ export class Standing {
37
+ readonly invitation: Invitation;
38
+ state: StandingState;
39
+ readonly draw: Draw;
40
+
41
+ constructor(invitation: Invitation, state: StandingState, draw: Draw) {
42
+ this.invitation = invitation;
43
+ this.state = state;
44
+ this.draw = draw;
45
+ }
46
+
47
+ async ask(method?: string, args?: string): Promise<Sent | null> {
48
+ const s = this.state;
49
+ if (s.key !== null) return this.#ask(unhex(s.key), unhex(s.edge!), method, args);
50
+ if (s.knock === null) return this.#knock(method, args);
51
+ if (!s.probed) {
52
+ this.state = { ...s, probed: true };
53
+ return this.#ask(unhex(s.knock.next), unhex(s.knock.edge), method, args);
54
+ }
55
+ this.state = { ...s, probed: false };
56
+ // The same bytes again: a new lid would be a new box.
57
+ return { bytes: unhex(s.knock.bytes), lid: await Ephemeral.from(unhex(s.knock.lid)), next: s.knock.next, edge: s.knock.edge, seq: s.knock.seq };
58
+ }
59
+
60
+ async read(sent: Sent, bytes: Uint8Array | null): Promise<Read> {
61
+ if (bytes === null) return { nothing: true };
62
+ const reply = await this.#open(sent, bytes);
63
+ if (reply === null) return { silence: true };
64
+ if ('object' in reply.text && sent.seq > this.state.moved) {
65
+ const edge = await follow(unhex(sent.edge), reply.agreement);
66
+ this.state = { ...this.state, key: sent.next, edge: hex(edge), moved: sent.seq, knock: null, probed: false };
67
+ }
68
+ return reply.text;
69
+ }
70
+
71
+ async #knock(method?: string, args?: string): Promise<Sent | null> {
72
+ const heir = await SigningKey.from(unhex(this.invitation.secret));
73
+ const next = await SigningKey.draw(this.draw);
74
+ const lid = await Ephemeral.draw(this.draw);
75
+ const sent = encapsulate(unhex(this.invitation.lock), this.draw(32));
76
+ if (sent === null) return null;
77
+ const edge = await knockEdge(sent.shared);
78
+ const seq = this.state.sent + 1;
79
+ const body = await this.#body(heir, next, seq, method, args);
80
+ const bytes = await sealAsk(lid, unhex(wardHalves(this.invitation.ward).padlock), unhex(this.invitation.heir), edge, body, sent.ciphertext);
81
+ if (bytes === null) return null;
82
+ this.state = { ...this.state, sent: seq, knock: { bytes: hex(bytes), lid: hex(lid.secret), next: hex(next.secret), edge: hex(edge), seq } };
83
+ return { bytes, lid, next: hex(next.secret), edge: hex(edge), seq };
84
+ }
85
+
86
+ async #ask(secret: Uint8Array, edge: Uint8Array, method?: string, args?: string): Promise<Sent | null> {
87
+ const by = await SigningKey.from(secret);
88
+ const next = await SigningKey.draw(this.draw);
89
+ const lid = await Ephemeral.draw(this.draw);
90
+ const seq = this.state.sent + 1;
91
+ const body = await this.#body(by, next, seq, method, args);
92
+ const bytes = await sealAsk(lid, unhex(wardHalves(this.invitation.ward).padlock), unhex(this.invitation.heir), edge, body);
93
+ if (bytes === null) return null;
94
+ this.state = { ...this.state, sent: seq };
95
+ return { bytes, lid, next: hex(next.secret), edge: hex(edge), seq };
96
+ }
97
+
98
+ #body(by: SigningKey, next: SigningKey, seq: number, method?: string, args?: string): Promise<Uint8Array> {
99
+ const text = writePayload({ to: this.invitation.heir, by: by.id, next: next.id, seq, ...(method === undefined ? {} : { method }), ...(args === undefined ? {} : { args }) });
100
+ return signBody(new TextEncoder().encode(text), by);
101
+ }
102
+
103
+ async #open(sent: Sent, bytes: Uint8Array): Promise<{ text: Reply; agreement: Uint8Array } | null> {
104
+ const opened = await openReply(bytes, sent.lid);
105
+ const body = opened && splitBody(opened.bytes);
106
+ if (!opened || !body) return null;
107
+ if (!(await checkBody(unhex(wardHalves(this.invitation.ward).signing), body))) return null;
108
+ const text = readReply(body.text);
109
+ return text && { text, agreement: opened.agreement };
110
+ }
111
+ }
@@ -0,0 +1,19 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The stand program: one JSON request a line on stdin, one answer a line on
3
+ // stdout, until stdin closes.
4
+ import { createInterface } from 'node:readline';
5
+ import { Stand } from './stand.ts';
6
+
7
+ const stand = new Stand((n) => globalThis.crypto.getRandomValues(new Uint8Array(n)));
8
+ const pending: Promise<void>[] = [];
9
+
10
+ for await (const line of createInterface({ input: process.stdin, crlfDelay: Infinity })) {
11
+ if (line === '') continue;
12
+ pending.push(
13
+ stand.handle(line).then(
14
+ (answer) => void process.stdout.write(`${JSON.stringify(answer)}\n`),
15
+ (error: unknown) => void process.stderr.write(`${String(error)}\n`),
16
+ ),
17
+ );
18
+ }
19
+ await Promise.all(pending);
@@ -0,0 +1,178 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The stand of `quo/vectors/HARNESS.md`, part one, over `quo/` alone. Part
3
+ // two carries bytes over TCP, which is the dock's, so its three requests
4
+ // are `bad request`.
5
+ import { hex, isHex, unhex } from '../crypto/index.ts';
6
+ import { Door, freshStanding, MemoryRelations, readInvitation, Standing, WardKey, type Behind, type Choice, type Draw, type Invitation, type Payload, type Read, type Sent } from '../quo/index.ts';
7
+
8
+ type Answer = Record<string, unknown>;
9
+ const ERRORS = ['bad request', 'no such op', 'no such ward', 'not reached', 'ward stood', 'name held'] as const;
10
+ type Error = (typeof ERRORS)[number];
11
+ class Refused {
12
+ readonly error: Error;
13
+ constructor(error: Error) {
14
+ this.error = error;
15
+ }
16
+ }
17
+ const refuse = (error: Error): never => {
18
+ throw new Refused(error);
19
+ };
20
+
21
+ // What the verifier names behind a door.
22
+ const REACHES: Record<string, (p: Payload) => Choice> = {
23
+ echo: (p) => ({ object: p.method === undefined ? '{}' : (p.args ?? '{}'), seen: null }),
24
+ marked: (p) => ({ object: p.method === undefined ? '{}' : (p.args ?? '{}'), seen: p.method === undefined ? null : '1' }),
25
+ null: () => ({ object: 'null', seen: null }),
26
+ silent: () => ({ silence: true }),
27
+ };
28
+
29
+ class StandWard implements Behind {
30
+ readonly door: Door;
31
+ readonly zero: boolean;
32
+ readonly #zero: string | undefined;
33
+ readonly names = new Map<string, string>();
34
+ readonly reaches = new Map<string, string>();
35
+ readonly standings = new Map<string, { standing: Standing; last?: Sent }>();
36
+
37
+ constructor(key: WardKey, zero: string | undefined, draw: Draw) {
38
+ this.#zero = zero;
39
+ this.zero = zero !== undefined;
40
+ this.door = new Door(key, new MemoryRelations(), this, draw);
41
+ }
42
+
43
+ answer(heir: string | null, p: Payload): Promise<Choice> {
44
+ const reach = heir === null ? this.#zero! : this.reaches.get(heir);
45
+ return Promise.resolve(reach === undefined ? { silence: true } : REACHES[reach]!(p));
46
+ }
47
+ }
48
+
49
+ const OPS = new Set(['ward', 'invite', 'release', 'arrive', 'ask', 'read', 'listen', 'route', 'send']);
50
+
51
+ export class Stand {
52
+ readonly #wards = new Map<string, StandWard>();
53
+ readonly #draw: Draw;
54
+
55
+ constructor(draw: Draw) {
56
+ this.#draw = draw;
57
+ }
58
+
59
+ async handle(line: string): Promise<Answer> {
60
+ let request: unknown;
61
+ try {
62
+ request = JSON.parse(line);
63
+ } catch {
64
+ return { id: null, error: 'bad request' };
65
+ }
66
+ if (typeof request !== 'object' || request === null || Array.isArray(request)) return { id: null, error: 'bad request' };
67
+ const r = request as Record<string, unknown>;
68
+ if (typeof r.id !== 'string') return { id: null, error: 'bad request' };
69
+ try {
70
+ return { id: r.id, ...(await this.#op(r)) };
71
+ } catch (e) {
72
+ if (e instanceof Refused) return { id: r.id, error: e.error };
73
+ throw e;
74
+ }
75
+ }
76
+
77
+ async #op(r: Record<string, unknown>): Promise<Answer> {
78
+ if (typeof r.op !== 'string') return refuse('bad request');
79
+ if (!OPS.has(r.op)) return refuse('no such op');
80
+ switch (r.op) {
81
+ case 'ward':
82
+ return this.#ward(text(r.seed), optional(r.reach));
83
+ case 'invite':
84
+ return this.#invite(r);
85
+ case 'release':
86
+ return this.#release(r);
87
+ case 'arrive':
88
+ return this.#arrive(r);
89
+ case 'ask':
90
+ return this.#ask(r);
91
+ case 'read':
92
+ return this.#read(r);
93
+ default:
94
+ return refuse('bad request');
95
+ }
96
+ }
97
+
98
+ async #ward(seed: string, reach: string | undefined): Promise<Answer> {
99
+ reached(reach);
100
+ const key = await WardKey.from(seed);
101
+ if (this.#wards.has(key.pk)) refuse('ward stood');
102
+ this.#wards.set(key.pk, new StandWard(key, reach, this.#draw));
103
+ return { ward: key.pk };
104
+ }
105
+
106
+ async #invite(r: Record<string, unknown>): Promise<Answer> {
107
+ const name = text(r.heir);
108
+ const reach = optional(r.reach) ?? 'echo';
109
+ const ward = this.#ward_(r.ward);
110
+ reached(reach);
111
+ if (ward.names.has(name)) refuse('name held');
112
+ const { heir, invitation } = await ward.door.invite();
113
+ ward.names.set(name, heir);
114
+ ward.reaches.set(heir, reach);
115
+ return { invitation };
116
+ }
117
+
118
+ #release(r: Record<string, unknown>): Answer {
119
+ const name = text(r.heir);
120
+ const ward = this.#ward_(r.ward);
121
+ const heir = ward.names.get(name);
122
+ if (heir === undefined) return { released: null };
123
+ ward.names.delete(name);
124
+ ward.door.remove(heir);
125
+ return { released: name };
126
+ }
127
+
128
+ async #arrive(r: Record<string, unknown>): Promise<Answer> {
129
+ const box = r.box;
130
+ if (!isHex(box)) return refuse('bad request');
131
+ const ward = this.#ward_(r.ward);
132
+ const judged = await ward.door.arrive(unhex(box));
133
+ return { reply: hex(judged.bytes) };
134
+ }
135
+
136
+ async #ask(r: Record<string, unknown>): Promise<Answer> {
137
+ const invitation = inviteOf(r.invitation);
138
+ const method = r.method === undefined ? undefined : text(r.method);
139
+ const args = r.args === undefined ? undefined : objectText(r.args);
140
+ const ward = this.#ward_(r.ward);
141
+ const held = this.#standing(ward, invitation);
142
+ const sent = await held.standing.ask(method, args);
143
+ if (sent === null) return refuse('bad request');
144
+ held.last = sent;
145
+ return { box: hex(sent.bytes) };
146
+ }
147
+
148
+ async #read(r: Record<string, unknown>): Promise<Answer> {
149
+ const invitation = inviteOf(r.invitation);
150
+ if (r.reply !== null && !isHex(r.reply)) return refuse('bad request');
151
+ const ward = this.#ward_(r.ward);
152
+ const held = this.#standing(ward, invitation);
153
+ if (held.last === undefined) return refuse('bad request');
154
+ const read = await held.standing.read(held.last, r.reply === null ? null : unhex(r.reply as string));
155
+ return { read: readOf(read) };
156
+ }
157
+
158
+ #standing(ward: StandWard, invitation: Invitation): { standing: Standing; last?: Sent } {
159
+ const key = `${invitation.ward}.${invitation.heir}`;
160
+ let held = ward.standings.get(key);
161
+ if (!held) ward.standings.set(key, (held = { standing: new Standing(invitation, freshStanding(), this.#draw) }));
162
+ return held;
163
+ }
164
+
165
+ #ward_(pk: unknown): StandWard {
166
+ if (!isHex(pk, 64)) return refuse('bad request');
167
+ return this.#wards.get(pk) ?? refuse('no such ward');
168
+ }
169
+ }
170
+
171
+ const text = (value: unknown): string => (typeof value === 'string' ? value : refuse('bad request'));
172
+ const optional = (value: unknown): string | undefined => (value === undefined ? undefined : text(value));
173
+ const reached = (reach: string | undefined): void => {
174
+ if (reach !== undefined && !Object.hasOwn(REACHES, reach)) refuse('not reached');
175
+ };
176
+ const inviteOf = (value: unknown): Invitation => readInvitation(value) ?? refuse('bad request');
177
+ const objectText = (value: unknown): string => (typeof value === 'object' && value !== null && !Array.isArray(value) ? JSON.stringify(value) : refuse('bad request'));
178
+ const readOf = (read: Read): unknown => ('object' in read ? { object: JSON.parse(read.object) as unknown, seen: read.seen } : read);
@@ -1,85 +1,47 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // The allowance. Every ask carries, in its signed body, the time it may still
3
- // spend, in milliseconds. A being who says nothing gets her ward's default and
4
- // never thinks about it; one who wants to say so passes a third argument, and
5
- // is held to the ward's ceiling. The door reads it before anything is done
6
- // under it and refuses a budget already gone, as one silence like every other
7
- // refusal there. The sender bounds the whole of her ask to the same number,
8
- // and a wait that ran out is the word late, never silence and never unreached.
9
- //
10
- // Each ask is bounded on its own. The time an arriving call has left does not
11
- // bound the asks a being makes while answering it: attributing her onward ask
12
- // to the arrival that caused it needs a context the ward cannot get without
13
- // naming a runtime, and the ward names none. Every wait still ends.
2
+ // The allowance: how long a being waits on one ask. It is on no wire. A
3
+ // being who says nothing gets the default, and nobody gets past the
4
+ // ceiling.
5
+ import type { Clock } from '../contract/index.ts';
6
+ import type { Wanted } from '../being/index.ts';
14
7
 
15
- // What a being may ask for is Wanted, and it belongs to the stance, where she
16
- // reads it. This file holds only what the ward does with it.
17
- import type { Wanted } from '../being/types.ts';
8
+ export const DEFAULT = 30_000;
9
+ export const CEILING = 300_000;
18
10
 
19
- // A budget, as it crosses a door. Time in milliseconds.
20
- export type Allowance = { time: number };
11
+ export const allow = (wanted: Wanted | undefined): number => {
12
+ const whole = typeof wanted?.time === 'number' ? Math.floor(wanted.time) : NaN;
13
+ return Math.min(Number.isFinite(whole) && whole > 0 ? whole : DEFAULT, CEILING);
14
+ };
21
15
 
22
- // How wide the default is, and how wide she may ask for, are the ward's own —
23
- // wider is more patient, and no peer can tell the difference except by being
24
- // refused. A being who never learns the third parameter exists writes correct
25
- // code forever under these. They are two numbers because the third argument
26
- // goes both ways: less for the ask she wants back quickly, more for the one
27
- // she knows is slow.
28
- export const DEFAULT: Allowance = { time: 30_000 };
29
- export const CEILING: Allowance = { time: 300_000 };
16
+ export const LATE: unique symbol = Symbol('late');
17
+ export type Late = typeof LATE;
30
18
 
31
- // What she asked for, held to what her ward allows. A being cannot mint budget
32
- // nobody granted her: asking for a minute where the ward gives thirty seconds
33
- // is thirty seconds, silently, because the ceiling is not hers to know.
34
- // Asking for nothing at all is the default, which is the whole point.
35
- export function allow(wanted: Wanted | undefined, ceiling: Allowance = CEILING, base: Allowance = DEFAULT): Allowance {
36
- // Floored first, then tested: a fraction of a millisecond is not a budget,
37
- // and falls to the default like every other number that is not a positive
38
- // whole one.
39
- const whole = (n: unknown, fallback: number) => {
40
- const w = typeof n === 'number' ? Math.floor(n) : NaN;
41
- return Number.isFinite(w) && w > 0 ? w : fallback;
42
- };
43
- return { time: Math.min(whole(wanted?.time, base.time), ceiling.time) };
44
- }
19
+ // One ask's bell. It bounds the wait of the being who asked and the send
20
+ // that holds the relation's line, so a far side that never answers holds
21
+ // neither for longer than the allowance.
22
+ export class Bell {
23
+ readonly #wait: { done: Promise<void>; cancel(): void };
24
+ readonly #late: Promise<Late>;
25
+ #rang = false;
45
26
 
46
- // Whether a budget has anything left to spend, and the one reader of it: the
47
- // door calls this on arrival and nothing else asks the question anywhere. A
48
- // time that is not a whole number above zero is not a budget that ran out, it
49
- // is no budget at all, and the door refuses both the same way, since a peer
50
- // learns nothing from the difference. `allow` never makes one, so only bytes
51
- // off the road ever carry one.
52
- export const spent = (a: Allowance): boolean => !(Number.isSafeInteger(a.time) && a.time > 0);
27
+ constructor(clock: Clock, ms: number) {
28
+ this.#wait = clock.wait(ms);
29
+ this.#late = this.#wait.done.then((): Late => {
30
+ this.#rang = true;
31
+ return LATE;
32
+ });
33
+ }
53
34
 
54
- // What a wait that ran out comes back as. Its own value, held by nobody
55
- // outside the ward, so no answer from any door can be mistaken for it.
56
- export const LATE = Symbol('late');
35
+ get rang(): boolean {
36
+ return this.#rang;
37
+ }
38
+
39
+ race<T>(work: Promise<T>): Promise<T | Late> {
40
+ work.catch(() => {});
41
+ return Promise.race([work, this.#late]);
42
+ }
57
43
 
58
- // A wait with an end. This is the one thing the ward times, and it covers the
59
- // whole of an ask, not only the wire: a relation that comes back round is
60
- // stopped at its own lane, before a single byte is sealed, and a bound that
61
- // only watched the wire would never see it. A timer is the only way to stop
62
- // waiting and every language that runs Quo has one; no runtime is named here.
63
- //
64
- // `rang` is called the moment the bell rings, before anyone waiting on the
65
- // wait is told: whoever holds work that has not started yet reads it and does
66
- // not start it. What comes back late is not read, and what has not left yet
67
- // does not leave.
68
- export async function within<T>(ms: number, work: Promise<T>, rang?: () => void): Promise<T | typeof LATE> {
69
- let timer: ReturnType<typeof setTimeout> | undefined;
70
- const bell = new Promise<typeof LATE>((ring) => {
71
- timer = setTimeout(() => {
72
- rang?.();
73
- ring(LATE);
74
- }, ms);
75
- });
76
- try {
77
- return await Promise.race([work, bell]);
78
- } finally {
79
- clearTimeout(timer);
80
- void work.then(
81
- () => {},
82
- () => {},
83
- ); // whatever it was, it is nobody's answer now
44
+ cancel(): void {
45
+ this.#wait.cancel();
84
46
  }
85
47
  }