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
@@ -1,251 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // Four algorithms, named once and never negotiated: Ed25519 signs, X25519
3
- // agrees, SHA-256 hashes, AES-256-GCM encrypts with the key derived through
4
- // HKDF-SHA-256 under a fixed label. All four live in `crypto.subtle`, so this
5
- // takes no package and runs the same in a browser tab as on a server -- on
6
- // any terrain that carries them. SHA-256, AES-GCM and HKDF are everywhere;
7
- // the two curves are recent, and a terrain without them is a terrain no ward
8
- // runs on. `test/floor.test.ts` names the floor and probes for it. Subtle is
9
- // asynchronous, so everything here is.
10
- //
11
- // `crypto.subtle` is read at every use and never captured at load. A browser
12
- // on a plain http:// origin has `crypto` without `subtle`, and a terrain may
13
- // install one after this module is first imported; a reference taken here
14
- // would turn either into an unreadable failure deep inside a key import.
15
- const subtle = (): SubtleCrypto => {
16
- const s = globalThis.crypto?.subtle;
17
- if (!s) throw new Error('this terrain has no crypto.subtle: a ward needs a secure context');
18
- return s;
19
- };
20
-
21
- export const KEY = 32;
22
- export const SIGNATURE = 64;
23
- export const NONCE = 12;
24
- export const TAG = 16;
25
- const SEAL_INFO = new TextEncoder().encode('quo-seal');
26
- const SALT = new Uint8Array(0);
27
-
28
- // A 32-byte secret plus a fixed prefix is the whole PKCS#8 wrapping for both curves.
29
- const ED_SECRET = unhex('302e020100300506032b657004220420');
30
- const X_SECRET = unhex('302e020100300506032b656e04220420');
31
- const ED = { name: 'Ed25519' };
32
- const X = { name: 'X25519' };
33
-
34
- const HEX = Array.from({ length: 256 }, (_, at) => at.toString(16).padStart(2, '0'));
35
- export function hex(bytes: Uint8Array | ArrayBuffer): string {
36
- let out = '';
37
- for (const byte of new Uint8Array(bytes)) out += HEX[byte];
38
- return out;
39
- }
40
- export function unhex(text: string): Uint8Array {
41
- const out = new Uint8Array(text.length / 2);
42
- for (let at = 0; at < out.length; at += 1) out[at] = parseInt(text.slice(at * 2, at * 2 + 2), 16);
43
- return out;
44
- }
45
- export function concat(parts: Uint8Array[]): Uint8Array {
46
- const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
47
- let at = 0;
48
- for (const part of parts) {
49
- out.set(part, at);
50
- at += part.length;
51
- }
52
- return out;
53
- }
54
- export function sameBytes(a: Uint8Array, b: Uint8Array): boolean {
55
- if (a.length !== b.length) return false;
56
- let diff = 0;
57
- for (let at = 0; at < a.length; at += 1) diff |= a[at] ^ b[at];
58
- return diff === 0;
59
- }
60
-
61
- // The eight small-order points of Ed25519, in their canonical encoding, and
62
- // the two points with x = 0 spelled with the sign bit set, which a decoder
63
- // that ignores the bit reads as the same point. A public key among them
64
- // verifies nothing. An encoding whose y coordinate is
65
- // not reduced, y at or above the field's prime p, names one of the same
66
- // points under another spelling, and a terrain's verify may accept a zero
67
- // signature under it; the field has room for the spelling, so it is refused
68
- // before the list is read.
69
- const SMALL_ORDER = [
70
- '0100000000000000000000000000000000000000000000000000000000000000',
71
- 'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
72
- '0000000000000000000000000000000000000000000000000000000000000000',
73
- '0000000000000000000000000000000000000000000000000000000000000080',
74
- '26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05',
75
- 'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a',
76
- '26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85',
77
- 'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa',
78
- '0100000000000000000000000000000000000000000000000000000000000080',
79
- 'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
80
- ].map(unhex);
81
- // y >= p, read little-endian with the sign bit masked off: the top byte is
82
- // 0x7f under the mask, every middle byte is 0xff, and the low byte is at
83
- // least 0xed, which is p's.
84
- const unreduced = (pk: Uint8Array): boolean => (pk[31] & 0x7f) === 0x7f && pk[0] >= 0xed && pk.subarray(1, 31).every((b) => b === 0xff);
85
- export const smallOrder = (pk: Uint8Array): boolean => unreduced(pk) || SMALL_ORDER.some((p) => sameBytes(p, pk));
86
-
87
- const key32 = (value: Uint8Array, what: string): Uint8Array => {
88
- if (!(value instanceof Uint8Array) || value.length !== KEY) throw new Error(`${what} is not a 32-byte key`);
89
- return value;
90
- };
91
- const pkcs8 = (prefix: Uint8Array, value: Uint8Array, what: string) => concat([prefix, key32(value, what)]);
92
-
93
- // Importing a key is the most expensive thing on the path of an ask, and most
94
- // of the imports are the same key again: a ward signs every reply with the one
95
- // key, opens every ask with the one padlock, and verifies a relation under the
96
- // key it verified it under last time. Measured over a round trip, fifteen of
97
- // the twenty-five imports were bytes already imported once.
98
- //
99
- // So an imported key is kept, by the bytes it was imported from. A CryptoKey
100
- // cannot be changed once it exists, so handing the same one out twice is
101
- // handing out what a second import would have built. Nothing here is a
102
- // decision a peer can see: two wards that cache differently, or not at all,
103
- // speak the same bytes.
104
- //
105
- // It is bounded, and that is not a detail. A relation mints a fresh key on
106
- // every ask, so a ward that talked all day would otherwise hold a key for
107
- // every ask it ever made. Past the bound the least recently used goes, which
108
- // is the key of a relation that has fallen quiet, and importing it again
109
- // costs what it cost the first time.
110
- //
111
- // The secret keys in here are the ones the partition already holds in this
112
- // process, as seeds. The cache is another shape of what the ward is already
113
- // standing on, and never a second place a secret comes from.
114
- const KEYS = 512;
115
- const imported = new Map<string, Promise<CryptoKey>>();
116
- const keep = (id: string, make: () => Promise<CryptoKey>): Promise<CryptoKey> => {
117
- const had = imported.get(id);
118
- if (had !== undefined) {
119
- imported.delete(id); // and set again below: the most recently used goes last
120
- imported.set(id, had);
121
- return had;
122
- }
123
- const made = make();
124
- // A key that would not import is not kept: the next call asks subtle again
125
- // and hears the same refusal, rather than reading one this cache remembered.
126
- // Node takes any thirty-two bytes as a public key and finds out at verify,
127
- // so nothing here reaches this line; a terrain that checks the point at the
128
- // import does, and a refusal it remembered would be a relation killed for
129
- // good by one bad arrival.
130
- made.catch(() => imported.delete(id));
131
- imported.set(id, made);
132
- // One in, at most one out: a map keeps what was put in the order it was put,
133
- // so the first key it names is the one used longest ago.
134
- if (imported.size > KEYS) imported.delete(imported.keys().next().value!);
135
- return made;
136
- };
137
-
138
- // How many imported keys are held, and the bound they are held under. Nothing
139
- // in the ward reads either: they are here to be looked at, and for the suite
140
- // that holds the bound to what it says.
141
- export const heldKeys = (): { held: number; bound: number } => ({ held: imported.size, bound: KEYS });
142
-
143
- const secretKey = (alg: { name: string }, prefix: Uint8Array, value: Uint8Array, what: string, uses: KeyUsage[]) => {
144
- const bytes = pkcs8(prefix, value, what);
145
- return keep(`${alg.name}|${uses.join('+')}|${hex(bytes)}`, () => subtle().importKey('pkcs8', bytes as BufferSource, alg, true, uses));
146
- };
147
- const publicKey = (alg: { name: string }, value: Uint8Array, what: string, uses: KeyUsage[]) => {
148
- const bytes = key32(value, what);
149
- return keep(`${alg.name}|${uses.join('+')}|pk|${hex(bytes)}`, () => subtle().importKey('raw', bytes as BufferSource, alg, true, uses));
150
- };
151
-
152
- // Subtle exports the public half of a private key only through a JWK, where
153
- // `x` is the 32 raw bytes in base64url. The answer is a fact about the key and
154
- // never changes, so it is kept beside the key it was read from and goes when
155
- // the key does.
156
- const publics = new WeakMap<CryptoKey, Promise<Uint8Array>>();
157
- function rawPublic(secret: CryptoKey): Promise<Uint8Array> {
158
- const had = publics.get(secret);
159
- if (had !== undefined) return had;
160
- const read = (async () => {
161
- const jwk = await subtle().exportKey('jwk', secret);
162
- const binary = atob(jwk.x!.replaceAll('-', '+').replaceAll('_', '/'));
163
- const out = new Uint8Array(binary.length);
164
- for (let at = 0; at < binary.length; at += 1) out[at] = binary.charCodeAt(at);
165
- return out;
166
- })();
167
- publics.set(secret, read);
168
- return read;
169
- }
170
-
171
- export async function sha256(...parts: Uint8Array[]): Promise<Uint8Array> {
172
- return new Uint8Array(await subtle().digest('SHA-256', concat(parts) as BufferSource));
173
- }
174
-
175
- export type Pair = { secret: Uint8Array; pk: Uint8Array };
176
- // Both halves are copies. The seed and the public key are kept behind the two
177
- // caches above, and a pair is handed to whoever asked for it: what she does
178
- // with the bytes in her hand is hers, and must not reach what the next caller
179
- // is given.
180
- export async function signingPair(seed: Uint8Array): Promise<Pair> {
181
- const secret = await secretKey(ED, ED_SECRET, seed, 'seed', ['sign']);
182
- return { secret: Uint8Array.from(seed), pk: Uint8Array.from(await rawPublic(secret)) };
183
- }
184
- export async function sealingPair(seed: Uint8Array): Promise<Pair> {
185
- const secret = await secretKey(X, X_SECRET, seed, 'seed', ['deriveBits']);
186
- return { secret: Uint8Array.from(seed), pk: Uint8Array.from(await rawPublic(secret)) };
187
- }
188
-
189
- export async function sign(message: Uint8Array, secret: Uint8Array): Promise<Uint8Array> {
190
- const key = await secretKey(ED, ED_SECRET, secret, 'secret', ['sign']);
191
- return new Uint8Array(await subtle().sign(ED, key, message as BufferSource));
192
- }
193
- export async function verify(message: Uint8Array, signature: Uint8Array, pk: Uint8Array): Promise<boolean> {
194
- if (!(signature instanceof Uint8Array) || signature.length !== SIGNATURE) return false;
195
- if (!(pk instanceof Uint8Array) || pk.length !== KEY || smallOrder(pk)) return false;
196
- try {
197
- return await subtle().verify(ED, await publicKey(ED, pk, 'pk', ['verify']), signature as BufferSource, message as BufferSource);
198
- } catch {
199
- return false;
200
- }
201
- }
202
-
203
- export async function agree(secret: Uint8Array, peerPk: Uint8Array): Promise<Uint8Array> {
204
- const key = await secretKey(X, X_SECRET, secret, 'secret', ['deriveBits']);
205
- const peer = await publicKey(X, peerPk, 'padlock', []);
206
- const shared = new Uint8Array(await subtle().deriveBits({ name: X.name, public: peer }, key, KEY * 8));
207
- if (shared.every((b) => b === 0)) throw new Error('dead agreement'); // a padlock that was not a real key
208
- return shared;
209
- }
210
-
211
- // HKDF-SHA-256: bytes from a secret, under a label that says what they are
212
- // for. No salt, because the secret handed in is already a secret of full
213
- // strength and the label is what keeps one use apart from another. Every
214
- // label in this kit is spelled where it is used, never here: this is
215
- // arithmetic and a label is a decision.
216
- export async function derive(secret: Uint8Array, label: Uint8Array, bytes: number): Promise<Uint8Array> {
217
- const material = await subtle().importKey('raw', secret as BufferSource, 'HKDF', false, ['deriveBits']);
218
- return new Uint8Array(await subtle().deriveBits({ name: 'HKDF', hash: 'SHA-256', salt: SALT, info: label as BufferSource }, material, bytes * 8));
219
- }
220
-
221
- // One HKDF-SHA-256 yields the AES key and the nonce together. The nonce needs
222
- // no randomness of its own: the key it pairs with is fresh on every message.
223
- async function cipherKey(shared: Uint8Array, use: KeyUsage) {
224
- const out = await derive(shared, SEAL_INFO, KEY + NONCE);
225
- return { key: await subtle().importKey('raw', out.subarray(0, KEY) as BufferSource, 'AES-GCM', false, [use]), nonce: out.subarray(KEY) };
226
- }
227
- // The additional authenticated data is the ephemeral public key: the one thing outside the seal, bound to it.
228
- export async function encrypt(shared: Uint8Array, plaintext: Uint8Array, aad: Uint8Array): Promise<Uint8Array> {
229
- const { key, nonce } = await cipherKey(shared, 'encrypt');
230
- return new Uint8Array(await subtle().encrypt({ name: 'AES-GCM', iv: nonce as BufferSource, additionalData: key32(aad, 'aad') as BufferSource, tagLength: TAG * 8 }, key, plaintext as BufferSource));
231
- }
232
- export async function decrypt(shared: Uint8Array, ciphertext: Uint8Array, aad: Uint8Array): Promise<Uint8Array> {
233
- if (ciphertext.length < TAG) throw new Error('short input');
234
- const { key, nonce } = await cipherKey(shared, 'decrypt');
235
- return new Uint8Array(await subtle().decrypt({ name: 'AES-GCM', iv: nonce as BufferSource, additionalData: key32(aad, 'aad') as BufferSource, tagLength: TAG * 8 }, key, ciphertext as BufferSource));
236
- }
237
-
238
- // A box: an ephemeral X25519 pk outside, one ciphertext sealed to the
239
- // padlock. The answer to a box is a box sealed to that ephemeral pk, so the
240
- // sender keeps the ephemeral secret until the answer comes.
241
- export async function box(inside: Uint8Array, padlock: Uint8Array, seed: Uint8Array): Promise<{ bytes: Uint8Array; ephemeral: Pair }> {
242
- const ephemeral = await sealingPair(seed);
243
- const shared = await agree(ephemeral.secret, padlock);
244
- return { bytes: concat([ephemeral.pk, await encrypt(shared, inside, ephemeral.pk)]), ephemeral };
245
- }
246
- export async function unbox(bytes: Uint8Array, padlockSecret: Uint8Array): Promise<Uint8Array> {
247
- if (bytes.length <= KEY) throw new Error('short input');
248
- const ephemeralPk = bytes.subarray(0, KEY);
249
- const shared = await agree(padlockSecret, ephemeralPk);
250
- return decrypt(shared, bytes.subarray(KEY), ephemeralPk);
251
- }
package/src/ward/door.ts DELETED
@@ -1,186 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // Judgment at the door. Sealed bytes in, sealed bytes out, and one bit for
3
- // the harbor: whether a key this door holds spoke. A stranger, bytes the
4
- // door cannot admit, meets one silence whatever the case, so that it learns
5
- // nothing, not even which case it hit. A key the door has bound hears the
6
- // ward's word for what happened, sealed to its own lid, because it has
7
- // already proven who it is and a reason to it is an oracle to nobody. What
8
- // passes is named and dispatched to her answer, and her digest for that
9
- // asker rides back with the object.
10
- import { isSilence, isWord } from '../being/silence.ts';
11
- import { digest } from '../being/digest.ts';
12
- import type { Asker, BeingLike, DoorWord, JsonObject, OccupantRecord } from '../being/types.ts';
13
- import { at } from './partition.ts';
14
- import type { Heirs } from './heirs.ts';
15
- import { openAsk, sealReply, verifyAsk, type ReplyPayload, type WardKey } from './seal.ts';
16
- import { spent } from './allowance.ts';
17
- import { KEY, sealingPair } from './arithmetic.ts';
18
- import { cellFault } from './cells.ts';
19
-
20
- export type Door = { key: string; being: BeingLike; cells: { occupants: Record<string, OccupantRecord> } };
21
- export type Judged = { bytes: Uint8Array; heard: boolean };
22
- const SILENCE: ReplyPayload = { silence: true };
23
- const said = (quo: DoorWord): ReplyPayload => ({ quo });
24
-
25
- // One arrival at one being, already named: the three choices, D11, D12 and
26
- // D13, and nothing else. Catches every throw. `bound` says whether the asker
27
- // is a key this door holds, or the ward's own owner: she hears `threw`; a
28
- // stranger at the public being hears silence, because her insides are hers.
29
- // Every caller that reaches a being's answer goes through here, the owner's
30
- // `ask` included, so the three choices are written once.
31
- export async function answered(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload> {
32
- const threw = bound ? said('threw') : SILENCE;
33
- let out: Awaited<ReturnType<BeingLike['answer']>>;
34
- try {
35
- out = await door.being.answer(asker, method, args);
36
- } catch {
37
- return threw; // she threw where she was asked. there is no answer.
38
- }
39
- // Nothing at all is not an answer either: a method that forgot to return
40
- // has said nothing, and nothing is silence, never a value that JSON drops
41
- // on the way out and the far side reads back as a fourth word.
42
- if (out === undefined || isSilence(out)) return SILENCE;
43
- if (isWord(out)) return threw; // a word is the ward's to say, never hers
44
- // Her answer is held to the rule her args and her cells are held to. A
45
- // shape JSON would drop or rewrite on the way out, a Date, a Map, a NaN, a
46
- // cycle, is not hers to make: the far side would read something she never
47
- // said, or the door itself would fail to write her reply after the number
48
- // was spent. It is threw, like a word out of her.
49
- if (cellFault(out, 'answer') !== null) return threw;
50
- return { object: out, seen: null };
51
- }
52
-
53
- // What crosses the wire: her answer, with her digest for this asker beside
54
- // it. The digest rides along, it is not the answer. She has already
55
- // answered: a describe that will not run costs the digest, and nothing else.
56
- // The owner does not take one, because the owner asks for a describe when it
57
- // wants one and nothing is sealed on its behalf.
58
- export async function arrive(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload> {
59
- const reply = await answered(door, asker, method, args, bound);
60
- if (method === undefined || !('object' in reply)) return reply;
61
- return { object: reply.object, seen: await seen(door, asker) };
62
- }
63
-
64
- // Her digest for this asker, or null when her describe threw or fell silent.
65
- // The owner's describe reads it the same way for every being of the ward.
66
- export async function seen(door: Door, asker: Asker): Promise<string | null> {
67
- try {
68
- const bp = await door.being.answer(asker);
69
- if (isSilence(bp) || isWord(bp)) return null;
70
- // Held to the same rule as her answer, and for the same reason plus one:
71
- // the digest is a walk, and a walk over a graph that shares or turns back
72
- // on itself is not a digest but a hang or a throw. A blueprint that could
73
- // not cross is a blueprint with no digest, which is what she has when her
74
- // describe says nothing.
75
- if (cellFault(bp, 'blueprint') !== null) return null;
76
- return await digest(bp);
77
- } catch {
78
- return null;
79
- }
80
- }
81
-
82
- type Verdict = { reply: ReplyPayload; ephemeralPk: Uint8Array; heard: boolean };
83
-
84
- export function makeDoor(
85
- key: WardKey,
86
- heirs: Heirs,
87
- doors: Map<string, Door>,
88
- publicKey: () => string | null,
89
- random: (n: number) => Uint8Array,
90
- keep: () => Promise<boolean>,
91
- ) {
92
- const judge = async (bytes: Uint8Array): Promise<Verdict | null> => {
93
- const a = await openAsk(bytes, key.padlock);
94
- if (!a) return null; // D1. it did not open. there is nobody to answer.
95
- const { to, payload, ephemeralPk } = a;
96
- const refuse = (): Verdict => ({ reply: SILENCE, ephemeralPk, heard: false });
97
- // D2. The allowance is read here, before anything is done under it, and
98
- // here only: a time that is not a whole number above zero is malformed and
99
- // is refused as one. Hops at zero is refused beside it, so that a relay
100
- // chain invented later meets doors that already stop it. Nothing sets hops.
101
- if (spent({ time: payload.time }) || payload.hops === 0) return refuse();
102
- const args = payload.args ?? {};
103
- if (to === null) {
104
- // For nobody: the public being, or D3 nobody home; D4 the signature
105
- // fails under the key the payload names. She is asked by strangers,
106
- // and a stranger hears silence for whatever she then does.
107
- const pk = publicKey();
108
- const pub = pk !== null ? doors.get(pk) : undefined;
109
- if (!pub || !(await verifyAsk(a, payload.by))) return refuse();
110
- // She answers, and the bit stays false: no key this door holds spoke.
111
- // The public being is the one place a stranger is answered by design,
112
- // so it is the one place a harbor must still be able to rate her.
113
- return { reply: await arrive(pub, {}, payload.method, args, false), ephemeralPk, heard: false };
114
- }
115
- const h = heirs.admits(to, payload.by);
116
- if (!h) {
117
- // D5 a heir not held, D6 a key not admitted. One more case: the heir
118
- // was held and the id removed. Its last keys are kept so that their
119
- // holder, and only their holder, D7 checked, hears that she is gone.
120
- if (!heirs.gone(to, payload.by) || !(await verifyAsk(a, payload.by))) return refuse();
121
- return { reply: said('removed'), ephemeralPk, heard: true };
122
- }
123
- if (!(await verifyAsk(a, payload.by))) return refuse(); // D7, under an admitted key
124
- // The signature took time, and another arrival on this heir may have been
125
- // honoured meanwhile: a knock that raced this one and won spent the heir,
126
- // and the key this ask speaks under may not be admitted any more. The
127
- // door judges concurrently, so admission is read again now that writing
128
- // is next, and what changed under the await is judged as it stands.
129
- if (!heirs.admits(to, payload.by)) return heirs.gone(to, payload.by) ? { reply: said('removed'), ephemeralPk, heard: true } : refuse();
130
- // From here the door has heard a key it holds. Every answer below is
131
- // sealed to that key's lid; nothing below is a stranger's.
132
- const door = doors.get(h.being);
133
- if (!door) return { reply: said('absent'), ephemeralPk, heard: true }; // D8. she did not come back this run
134
- if (!at(door.cells.occupants, h.id)) return { reply: said('removed'), ephemeralPk, heard: true }; // D8. the record is gone
135
- // Once only, and only now: the number is spent after the signature, so a
136
- // stranger cannot burn a number she could not sign for, and together with
137
- // the keys, so the same bytes twice rotate nothing and refusal writes nothing.
138
- const honoured = heirs.honour(h, payload.by, payload.next, payload.seq); // D9, D10
139
- if (honoured !== true) return { reply: said(honoured), ephemeralPk, heard: true };
140
- return { reply: await arrive(door, { id: h.id }, payload.method, args, true), ephemeralPk, heard: true };
141
- };
142
-
143
- // The door itself. Always answers bytes. When the ask did not open, the
144
- // reply is sealed to the ephemeral pk on its lid, which is all a stranger
145
- // holds, and says silence.
146
- //
147
- // A lid that is not a key, a small-order point, of which the two curves
148
- // have several each, makes a dead agreement, and the seal refuses it. The
149
- // door never throws: that reply is noise, a plain silence sealed to a key
150
- // nobody holds, and the same is written for anything else the seal will
151
- // not take.
152
- // `judge` reads rows it does not check: open() reads the partition's shape
153
- // at birth, so a row it acts on is the shape it expects. That is the first
154
- // line and this is the second, because "the door never throws" is a promise
155
- // to the harbor, which has nobody to hand a rejection to and would count it
156
- // as no answer at all. Anything unforeseen is the silence a stranger hears.
157
- return async function door(bytes: Uint8Array): Promise<Judged> {
158
- const out = await judge(bytes).catch(() => null);
159
- let reply = out?.reply ?? SILENCE;
160
- const heard = out?.heard ?? false;
161
- // The last moment anything can still be said. She has answered and the
162
- // rows she wrote are named, so the harbor is asked to keep them before
163
- // the reply is sealed: after the seal there is no taking it back, and
164
- // the lid is the asker's. A store that refused makes this arrival a
165
- // failed ask, which is `threw` to a key this door holds and silence to
166
- // anyone else, the same two words a being who threw would have got. The
167
- // asker learns that nothing took effect, and never that a disk is full.
168
- //
169
- // A keep that throws is a refusal: a harbor whose store is unreachable
170
- // has kept nothing, and the door that never throws does not start here.
171
- if (!(await keep().catch(() => false))) reply = heard ? said('threw') : SILENCE;
172
- try {
173
- return { bytes: await sealReply(reply, out?.ephemeralPk ?? lid(bytes, random), key.sign, random(32)), heard };
174
- } catch {
175
- return { bytes: await sealReply(SILENCE, (await sealingPair(random(32))).pk, key.sign, random(32)), heard };
176
- }
177
- };
178
- }
179
-
180
- // The ephemeral pk on an ask that would not open, if the bytes are long
181
- // enough to carry one. Otherwise a fresh key nobody holds: the reply is
182
- // bytes, and it is noise.
183
- function lid(bytes: Uint8Array, random: (n: number) => Uint8Array): Uint8Array {
184
- if (bytes instanceof Uint8Array && bytes.length >= KEY) return bytes.subarray(0, KEY);
185
- return random(KEY);
186
- }
@@ -1,142 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // The ground. The one object a harbor passes a ward at birth, and the whole
3
- // of the device to it: six things, and everything a runtime differs on
4
- // arrives in one of them, which is why the ward itself knows no runtime.
5
- //
6
- // seed who the ward is. the pk derives from it and from nothing else
7
- // random entropy. every key the ward mints is drawn from it
8
- // memory what the ward remembers: one interface, a body per terrain
9
- // instantiate the dna: a class name and a stance in, a being or nothing out
10
- // carry the wire: a ward pk and sealed bytes in, sealed bytes or nothing out
11
- // box the device, as one invitation on the box's own being
12
- //
13
- // The spec lists nine things and says a kit gathers them however its
14
- // language gathers things. This kit gathers the partition, wrote, keep and
15
- // calling into memory, because they are one concern with four moments, and
16
- // a ward is handed one body and told how to use it rather than four calls
17
- // it must keep in step.
18
- import type { Stance, BeingLike, BeingClass, Invitation } from '../being/types.ts';
19
-
20
- // Memory. Four moments, and they are the promise a body keeps:
21
- //
22
- // rows the values the ward holds and writes into directly. synchronous,
23
- // always, because the door judges an arrival in one pass and a
24
- // disk in the middle of it would be a door that waits on a device
25
- // told the ward says it after every write, naming the row: a being by
26
- // her key, or `HEAD` for everything that is not one being's.
27
- // nothing comes back. a body that keeps the rows saves in the
28
- // order it was told, so a being driven in process is kept the way
29
- // one reached through a door is
30
- // kept keep what has been written, now, and say whether it was. the
31
- // ward asks it once at the end of an arrival, after the being has
32
- // answered and before the answer is sealed, the last moment
33
- // anything can still be said about it. false is a store that
34
- // refused, and the ward says so with the word a failed ask already
35
- // has: `threw` to an asker the door holds, silence to the ask
36
- // pointer. never a reason
37
- // during a call of this ward's has begun, and what comes back lowers it.
38
- // the ward raises it before it reaches out and lowers it in a
39
- // finally, so a call that threw lowers too, and lowering twice is
40
- // lowering once. calls nest. a body may go on writing while it is
41
- // raised; what it may not do is take a save made there as the
42
- // point it puts a refused ward back to
43
- //
44
- // A body that keeps nothing has rows, is told and does nothing, answers yes,
45
- // and raises nothing: `volatile` below. The ward has one path either way.
46
- export type Memory = {
47
- readonly rows: Record<string, unknown>;
48
- told(row: string): void;
49
- kept(): Promise<boolean>;
50
- during(): () => void;
51
- };
52
-
53
- // The body that keeps nothing: the process is the memory.
54
- export const volatile = (rows: Record<string, unknown> = {}): Memory => ({ rows, told: () => {}, kept: () => Promise.resolve(true), during: () => () => {} });
55
-
56
- // The device is one standing. What a box can do is beings, in a ward its
57
- // harbor booted and roots, and one being there, the box's own, holds a
58
- // standing at each of them under the name it is lent as. The ground carries
59
- // one invitation on her, minted for this ward alone; the ward takes it at
60
- // boot under `BOX`, as its own first being, and from then on the device is
61
- // reached the way anything is reached: what it offers is her describe, and a
62
- // lend is the ask `OFFER` on that standing, naming what a being wants.
63
- //
64
- // She answers the lent being's own invitation, minted by that being on herself,
65
- // and the ward knocks and takes it in the being's name. No root mints for a
66
- // lend, and the invitation never reaches the being: it is the device's, a
67
- // value she could copy is one she could hand to anyone, so the ward holds
68
- // it and hands her the id. Which ward may have which name is the box's own
69
- // gate, reading who asks, and a stranger's ward holds no standing at her.
70
- //
71
- // A harbor with no box leaves it out, and every lend is null. A ward that
72
- // could not take it at boot, the box down or the invitation spent, boots all
73
- // the same, lends nothing this run, and tries again at the next.
74
- export { BOX } from '../being/lent.ts';
75
- export const OFFER = 'offer';
76
- // A lend that was offered and not taken leaves nothing: the ward says so to
77
- // the box, naming the heir, and the lent being removes what she minted.
78
- export const RETRACT = 'retract';
79
-
80
- export type Ground = {
81
- seed: string | Uint8Array;
82
- random(n: number): Uint8Array;
83
- memory: Memory;
84
- instantiate(className: string, stance: Stance): BeingLike | null;
85
- // Sealed bytes to a ward pk. What comes back, or undefined.
86
- //
87
- // undefined is a promise, not a shrug: no door was reached, and nothing was
88
- // delivered. The ward hands it to a being as unreached, which is the one
89
- // answer that says asking again is safe, so a harbor may only return it
90
- // when it knows the bytes never arrived: no reach for that pk, a socket
91
- // that would not open, a link that is down. A harbor that sent the bytes
92
- // and then gave up waiting knows no such thing and must not answer at all:
93
- // the ward bounds every ask itself, and an answer that never comes inside
94
- // that bound is `late`, which promises nothing either way.
95
- carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
96
- box?: Invitation;
97
- };
98
-
99
- // What a ward hands back. Two pointers. The door answers bytes, always, and
100
- // one bit beside them: whether a key it holds spoke. That is all a harbor
101
- // learns from an arrival, and it is what a harbor can act on -- a pk that
102
- // only ever brings strangers' bytes is the harbor's to rate or refuse. Never
103
- // a reason: the reason is sealed to the asker's lid, and is theirs.
104
- export type WardPointers = {
105
- door(bytes: Uint8Array): Promise<{ bytes: Uint8Array; heard: boolean }>;
106
- ask(method?: string, args?: Record<string, unknown>): Promise<unknown>;
107
- };
108
-
109
- // ---- what every harbor builds a ground out of. Convenience, never contract:
110
- // a second kit writes its own harbor and may write these again.
111
-
112
- // The code half of a ground, and the map back to what it made. A class is
113
- // found by own key only, since `constructor` is a name Object lends every
114
- // registry, and the first registry holding the name wins, so a ward's own
115
- // classes stand in front of the harbor's. The object is remembered by the
116
- // cells the ward handed in, which is how a side reaches a being it made and
117
- // how that hand follows her out when she is unbooted.
118
- export const maker =
119
- (objects: WeakMap<object, BeingLike>, ...registries: (Record<string, BeingClass> | undefined)[]): Ground['instantiate'] =>
120
- (className: string, stance: Stance) => {
121
- const r = registries.find((reg) => reg !== undefined && Object.hasOwn(reg, className));
122
- const C = r?.[className];
123
- if (!C) return null;
124
- const obj = new C(stance);
125
- objects.set(stance.cells, obj);
126
- return obj;
127
- };
128
-
129
- // Entropy, from the one place every terrain that runs Quo has it.
130
- export const entropy = (n: number): Uint8Array => globalThis.crypto.getRandomValues(new Uint8Array(n));
131
-
132
- // A ward's pk, learned the way anyone learns anything: by asking. The empty
133
- // ask on the ask pointer is the ward's own describe and its notes carry the
134
- // pk. A harbor has no other way to it and wants none: the ward mints it from
135
- // the seed, and a harbor that read it off the seed itself would be a second
136
- // derivation to keep in step with the first.
137
- export async function learnPk(w: WardPointers): Promise<string> {
138
- const notes = (await w.ask()) as { notes?: { pk?: unknown } } | null;
139
- const pk = notes?.notes?.pk;
140
- if (typeof pk !== 'string') throw new Error('the ward did not say its pk');
141
- return pk;
142
- }