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,44 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The contracts: what the onion cannot unpack from cells, each an abstract
3
+ // class, so a registry can tell at run time which class fulfils which. The
4
+ // class differs by terrain, and the contract does not.
5
+ import type { Draw } from '../quo/index.ts';
6
+
7
+ // Drawn bytes.
8
+ export abstract class Entropy {
9
+ abstract draw(length: number): Uint8Array;
10
+ // The same source, as the function `quo/` takes.
11
+ get drawer(): Draw {
12
+ return (length) => this.draw(length);
13
+ }
14
+ }
15
+
16
+ // The moment, and a wait that ends.
17
+ export abstract class Clock {
18
+ abstract now(): number;
19
+ // Resolves after `ms`, unless cancelled first.
20
+ abstract wait(ms: number): { done: Promise<void>; cancel(): void };
21
+ }
22
+
23
+ // One ward's partition, opened by name: the rows the ward writes into
24
+ // directly, told after every write, and kept once at the end of an arrival.
25
+ export interface Rows {
26
+ readonly rows: Record<string, unknown>;
27
+ told(row: string): void;
28
+ kept(): Promise<boolean>;
29
+ }
30
+
31
+ export abstract class Memory {
32
+ abstract open(name: string): Promise<Rows>;
33
+ }
34
+
35
+ // A ward's seed by the harbor's name for it: thirty-two bytes, the same
36
+ // every time that name is asked.
37
+ export abstract class Custody {
38
+ abstract seed(name: string): Promise<Uint8Array>;
39
+ }
40
+
41
+ // Bytes to a ward pk in; bytes, or null for nothing, out.
42
+ export abstract class Carrier {
43
+ abstract carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
44
+ }
@@ -0,0 +1,26 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // AES-256-GCM with a sixteen-byte tag at the end of the ciphertext.
3
+ import { own, subtle } from './subtle.ts';
4
+
5
+ export const TAG = 16;
6
+
7
+ const cipher = (key: Uint8Array, nonce: Uint8Array, additional: Uint8Array) => ({
8
+ params: { name: 'AES-GCM', iv: own(nonce), additionalData: own(additional), tagLength: TAG * 8 },
9
+ key: subtle().importKey('raw', own(key), 'AES-GCM', false, ['encrypt', 'decrypt']),
10
+ });
11
+
12
+ export const encrypt = async (key: Uint8Array, nonce: Uint8Array, additional: Uint8Array, plaintext: Uint8Array): Promise<Uint8Array> => {
13
+ const c = cipher(key, nonce, additional);
14
+ return new Uint8Array(await subtle().encrypt(c.params, await c.key, own(plaintext)));
15
+ };
16
+
17
+ // The plaintext, or null when the ciphertext does not open.
18
+ export const decrypt = async (key: Uint8Array, nonce: Uint8Array, additional: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array | null> => {
19
+ if (ciphertext.length < TAG) return null;
20
+ const c = cipher(key, nonce, additional);
21
+ try {
22
+ return new Uint8Array(await subtle().decrypt(c.params, await c.key, own(ciphertext)));
23
+ } catch {
24
+ return null;
25
+ }
26
+ };
@@ -0,0 +1,37 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Bytes and the one text spelling Quo gives them: hex, two lowercase digits
3
+ // a byte.
4
+ const DIGITS = Array.from({ length: 256 }, (_, at) => at.toString(16).padStart(2, '0'));
5
+ const HEX = /^(?:[0-9a-f]{2})*$/;
6
+
7
+ export const hex = (bytes: Uint8Array): string => {
8
+ let out = '';
9
+ for (const byte of bytes) out += DIGITS[byte];
10
+ return out;
11
+ };
12
+
13
+ // Hex of `length` bytes, or any length when none is named.
14
+ export const isHex = (text: unknown, length?: number): text is string =>
15
+ typeof text === 'string' && HEX.test(text) && (length === undefined || text.length === length * 2);
16
+
17
+ export const unhex = (text: string): Uint8Array => {
18
+ if (!isHex(text)) throw new TypeError('not hex');
19
+ const out = new Uint8Array(text.length / 2);
20
+ for (let at = 0; at < out.length; at += 1) out[at] = parseInt(text.slice(at * 2, at * 2 + 2), 16);
21
+ return out;
22
+ };
23
+
24
+ export const concat = (...parts: Uint8Array[]): Uint8Array => {
25
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
26
+ let at = 0;
27
+ for (const part of parts) {
28
+ out.set(part, at);
29
+ at += part.length;
30
+ }
31
+ return out;
32
+ };
33
+
34
+ export const zero = (bytes: Uint8Array): boolean => bytes.every((b) => b === 0);
35
+
36
+ const encoder = new TextEncoder();
37
+ export const utf8 = (text: string): Uint8Array => encoder.encode(text);
@@ -0,0 +1,84 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Ed25519 signs, as RFC 8032 defines it, and verifies with the cofactorless
3
+ // check. The spec names exactly where a check fails, and Web Crypto promises
4
+ // none of the decoding rules, so they are held here before Web Crypto runs:
5
+ //
6
+ // a signature of any length but sixty-four
7
+ // an `s` at or above the group order
8
+ // an `R` that does not decode, or whose bytes are not its point's encoding
9
+ // a public key that does not decode, or whose y is at or above the prime
10
+ // a public key of small order, in any spelling
11
+ import { importSecret, own, publicOf, subtle } from './subtle.ts';
12
+
13
+ const P = 2n ** 255n - 19n;
14
+ const L = 2n ** 252n + 27742317777372353535851937790883648493n;
15
+
16
+ const mod = (a: bigint): bigint => ((a % P) + P) % P;
17
+ const power = (base: bigint, exponent: bigint): bigint => {
18
+ let out = 1n;
19
+ let b = mod(base);
20
+ for (let e = exponent; e > 0n; e >>= 1n) {
21
+ if (e & 1n) out = (out * b) % P;
22
+ b = (b * b) % P;
23
+ }
24
+ return out;
25
+ };
26
+ const D = mod(-121665n * power(121666n, P - 2n));
27
+ const ROOT_MINUS_ONE = power(2n, (P - 1n) / 4n);
28
+
29
+ const little = (bytes: Uint8Array): bigint => bytes.reduceRight((n, b) => (n << 8n) | BigInt(b), 0n);
30
+
31
+ type Point = { x: bigint; y: bigint; z: bigint; t: bigint };
32
+
33
+ // A point from its thirty-two bytes, or null where RFC 8032 section 5.1.3
34
+ // refuses it, the sign bit set on x = 0 included.
35
+ const decode = (bytes: Uint8Array): Point | null => {
36
+ if (bytes.length !== 32) return null;
37
+ const sign = (bytes[31]! & 0x80) !== 0;
38
+ const y = little(bytes) & ((1n << 255n) - 1n);
39
+ if (y >= P) return null;
40
+ const u = mod(y * y - 1n);
41
+ const v = mod(D * y * y + 1n);
42
+ let x = mod(u * power(v, 3n) * power(u * power(v, 7n), (P - 5n) / 8n));
43
+ const check = mod(v * x * x);
44
+ if (check === mod(-u)) x = mod(x * ROOT_MINUS_ONE);
45
+ else if (check !== u) return null;
46
+ if (x === 0n && sign) return null;
47
+ if ((x & 1n) !== (sign ? 1n : 0n)) x = mod(-x);
48
+ return { x, y, z: 1n, t: mod(x * y) };
49
+ };
50
+
51
+ const add = (a: Point, b: Point): Point => {
52
+ const A = mod((a.y - a.x) * (b.y - b.x));
53
+ const B = mod((a.y + a.x) * (b.y + b.x));
54
+ const C = mod(2n * D * a.t * b.t);
55
+ const E = mod(2n * a.z * b.z);
56
+ const [e, f, g, h] = [B - A, E - C, E + C, B + A];
57
+ return { x: mod(e * f), y: mod(g * h), z: mod(f * g), t: mod(e * h) };
58
+ };
59
+
60
+ // Small order: eight times the point is the identity.
61
+ const smallOrder = (p: Point): boolean => {
62
+ let q = p;
63
+ for (let i = 0; i < 3; i += 1) q = add(q, q);
64
+ return q.x === 0n && q.y === q.z;
65
+ };
66
+
67
+ export const ed25519Public = async (secret: Uint8Array): Promise<Uint8Array> => publicOf(await importSecret('Ed25519', secret, ['sign']));
68
+
69
+ export const sign = async (secret: Uint8Array, message: Uint8Array): Promise<Uint8Array> =>
70
+ new Uint8Array(await subtle().sign('Ed25519', await importSecret('Ed25519', secret, ['sign']), own(message)));
71
+
72
+ export const verify = async (pk: Uint8Array, message: Uint8Array, signature: Uint8Array): Promise<boolean> => {
73
+ if (signature.length !== 64) return false;
74
+ const key = decode(pk);
75
+ if (key === null || smallOrder(key)) return false;
76
+ if (decode(signature.subarray(0, 32)) === null) return false;
77
+ if (little(signature.subarray(32)) >= L) return false;
78
+ try {
79
+ const imported = await subtle().importKey('raw', own(pk), { name: 'Ed25519' }, false, ['verify']);
80
+ return await subtle().verify('Ed25519', imported, own(signature), own(message));
81
+ } catch {
82
+ return false;
83
+ }
84
+ };
@@ -0,0 +1,14 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SHA-256 hashes, and HKDF-SHA-256 derives under the zero-length salt, with
3
+ // the label's ASCII bytes as `info`.
4
+ import { own, subtle } from './subtle.ts';
5
+
6
+ const SALT = new Uint8Array(0);
7
+
8
+ export const sha256 = async (bytes: Uint8Array): Promise<Uint8Array> => new Uint8Array(await subtle().digest('SHA-256', own(bytes)));
9
+
10
+ export const hkdf = async (ikm: Uint8Array, info: Uint8Array, length: number): Promise<Uint8Array> => {
11
+ const material = await subtle().importKey('raw', own(ikm), 'HKDF', false, ['deriveBits']);
12
+ const bits = await subtle().deriveBits({ name: 'HKDF', hash: 'SHA-256', salt: SALT, info: own(info) }, material, length * 8);
13
+ return new Uint8Array(bits);
14
+ };
@@ -0,0 +1,10 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The six algorithms Quo names, and bytes. Arithmetic only: no label, no
3
+ // key of Quo's, no decision.
4
+ export { concat, hex, isHex, unhex, utf8, zero } from './bytes.ts';
5
+ export { hkdf, sha256 } from './hash.ts';
6
+ export { isCount, readObject, readValue, writeValue, type Fields, type Json } from './json.ts';
7
+ export { decrypt, encrypt, TAG } from './aes.ts';
8
+ export { agree, x25519Public } from './x25519.ts';
9
+ export { ed25519Public, sign, verify } from './ed25519.ts';
10
+ export { CIPHERTEXT, decapsulate, EK, encapsulate, lockFrom, type Lock } from './mlkem.ts';
@@ -0,0 +1,241 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // JSON text as the spec's values chapter reads it. Two readers, for two
3
+ // jobs:
4
+ //
5
+ // readObject Quo's reading of a payload, a reply text or an invitation:
6
+ // UTF-8 with no byte order mark, one object, no two own keys
7
+ // of one name, and every field kept as the text it was
8
+ // written in. What is inside a field is held to the grammar
9
+ // alone, at any depth, and read no further.
10
+ //
11
+ // readValue Nervur's reading of a text it hands to a being: the
12
+ // grammar, a nesting bound, no two keys of one name at any
13
+ // depth, no lone surrogate, and no number a double does not
14
+ // hold as written. Anything else is undefined, since null
15
+ // is a value.
16
+ //
17
+ // A runtime's own parser keeps the last of two keys, rounds numbers and
18
+ // mends bad bytes, which is where two kits stop agreeing, so neither reader
19
+ // trusts it with more than the escapes of one string.
20
+ export type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
21
+
22
+ // A field's value exactly as it stood in the text, by its key.
23
+ export type Fields = ReadonlyMap<string, string>;
24
+
25
+ const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
26
+ const NUMBER = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
27
+ const ESCAPE = /\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4})/y;
28
+ const LONE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
29
+ const LITERALS = ['true', 'false', 'null'];
30
+
31
+ const skip = (s: string, at: number): number => {
32
+ while (at < s.length && ' \t\n\r'.includes(s[at]!)) at += 1;
33
+ return at;
34
+ };
35
+
36
+ // The end of the string opening at `at`, or -1.
37
+ const stringEnd = (s: string, at: number): number => {
38
+ if (s[at] !== '"') return -1;
39
+ for (at += 1; at < s.length; ) {
40
+ const c = s.charCodeAt(at);
41
+ if (c === 0x22) return at + 1;
42
+ if (c < 0x20) return -1;
43
+ if (c !== 0x5c) {
44
+ at += 1;
45
+ continue;
46
+ }
47
+ ESCAPE.lastIndex = at;
48
+ if (!ESCAPE.test(s)) return -1;
49
+ at = ESCAPE.lastIndex;
50
+ }
51
+ return -1;
52
+ };
53
+
54
+ const scalarEnd = (s: string, at: number): number => {
55
+ if (s[at] === '"') return stringEnd(s, at);
56
+ const literal = LITERALS.find((word) => s.startsWith(word, at));
57
+ if (literal) return at + literal.length;
58
+ NUMBER.lastIndex = at;
59
+ return NUMBER.test(s) ? NUMBER.lastIndex : -1;
60
+ };
61
+
62
+ // A key, its colon and the space after it: the start of the key's value, or -1.
63
+ const keyEnd = (s: string, at: number): number => {
64
+ const end = stringEnd(s, at);
65
+ if (end < 0) return -1;
66
+ const colon = skip(s, end);
67
+ return s[colon] === ':' ? skip(s, colon + 1) : -1;
68
+ };
69
+
70
+ // The end of the value starting at `at`, held to the grammar at any depth
71
+ // without recursion, or -1.
72
+ const valueEnd = (s: string, at: number): number => {
73
+ const open: string[] = [];
74
+ for (;;) {
75
+ const c = s[at];
76
+ if (c === '{' || c === '[') {
77
+ at = skip(s, at + 1);
78
+ if (s[at] === (c === '{' ? '}' : ']')) at += 1;
79
+ else {
80
+ open.push(c);
81
+ if (c === '{' && (at = keyEnd(s, at)) < 0) return -1;
82
+ continue;
83
+ }
84
+ } else if ((at = scalarEnd(s, at)) < 0) return -1;
85
+ for (;;) {
86
+ if (open.length === 0) return at;
87
+ at = skip(s, at);
88
+ const top = open.at(-1);
89
+ if (s[at] === ',') {
90
+ at = skip(s, at + 1);
91
+ if (top === '{' && (at = keyEnd(s, at)) < 0) return -1;
92
+ break;
93
+ }
94
+ if (s[at] !== (top === '{' ? '}' : ']')) return -1;
95
+ open.pop();
96
+ at += 1;
97
+ }
98
+ }
99
+ };
100
+
101
+ const text = (input: Uint8Array | string): string | null => {
102
+ if (typeof input === 'string') return input;
103
+ try {
104
+ return decoder.decode(input);
105
+ } catch {
106
+ return null;
107
+ }
108
+ };
109
+
110
+ export const readObject = (input: Uint8Array | string): Fields | null => {
111
+ const s = text(input);
112
+ if (s === null) return null;
113
+ const fields = new Map<string, string>();
114
+ let at = skip(s, 0);
115
+ if (s[at] !== '{') return null;
116
+ at = skip(s, at + 1);
117
+ if (s[at] === '}') return skip(s, at + 1) === s.length ? fields : null;
118
+ for (;;) {
119
+ const keyStart = at;
120
+ const valueStart = keyEnd(s, at);
121
+ if (valueStart < 0) return null;
122
+ const key = JSON.parse(s.slice(keyStart, stringEnd(s, keyStart))) as string;
123
+ if (fields.has(key)) return null;
124
+ const end = valueEnd(s, valueStart);
125
+ if (end < 0) return null;
126
+ fields.set(key, s.slice(valueStart, end));
127
+ at = skip(s, end);
128
+ if (s[at] === '}') return skip(s, at + 1) === s.length ? fields : null;
129
+ if (s[at] !== ',') return null;
130
+ at = skip(s, at + 1);
131
+ }
132
+ };
133
+
134
+ // A number text a double holds as written: a fraction, or a whole number
135
+ // the double it reads as is exactly, never past the largest double, never
136
+ // a value that rounds to zero, never minus zero.
137
+ const exactNumber = (literal: string): boolean => {
138
+ const [, sign, whole, frac = '', exp = '0'] = /^(-?)(0|[1-9]\d*)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(literal)!;
139
+ const v = Number(literal);
140
+ if (!Number.isFinite(v)) return false;
141
+ const digits = (whole! + frac).replace(/^0+/, '');
142
+ if (v === 0) return digits === '' && sign === '';
143
+ const trimmed = digits.replace(/0+$/, '');
144
+ const e = BigInt(exp) - BigInt(frac.length) + BigInt(digits.length - trimmed.length);
145
+ if (e < 0n) return true;
146
+ return BigInt(trimmed) * 10n ** e === (v < 0 ? -BigInt(v) : BigInt(v)) || literal === String(v);
147
+ };
148
+
149
+ const depthOf = (s: string): number => {
150
+ let depth = 0;
151
+ let deepest = 0;
152
+ for (let at = 0; at < s.length; at += 1) {
153
+ const c = s[at];
154
+ if (c === '"') at = stringEnd(s, at) - 1;
155
+ else if (c === '{' || c === '[') deepest = Math.max(deepest, (depth += 1));
156
+ else if (c === '}' || c === ']') depth -= 1;
157
+ }
158
+ return deepest;
159
+ };
160
+
161
+ // Parses a text already held to the grammar and to the depth bound.
162
+ const parse = (s: string): Json => {
163
+ let at = 0;
164
+ const value = (): Json => {
165
+ at = skip(s, at);
166
+ const c = s[at];
167
+ if (c === '{' || c === '[') {
168
+ const object = c === '{';
169
+ const out: Json[] | Record<string, Json> = object ? {} : [];
170
+ at = skip(s, at + 1);
171
+ if (s[at] === (object ? '}' : ']')) {
172
+ at += 1;
173
+ return out;
174
+ }
175
+ for (;;) {
176
+ if (object) {
177
+ const key = str();
178
+ if (Object.hasOwn(out, key)) throw new Error('two keys of one name');
179
+ at = skip(s, at) + 1;
180
+ Object.defineProperty(out, key, { value: value(), enumerable: true, writable: true, configurable: true });
181
+ } else (out as Json[]).push(value());
182
+ at = skip(s, at);
183
+ if (s[at++] !== ',') return out;
184
+ }
185
+ }
186
+ if (c === '"') return str();
187
+ const end = scalarEnd(s, at);
188
+ const literal = s.slice(at, end);
189
+ at = end;
190
+ if (LITERALS.includes(literal)) return JSON.parse(literal) as Json;
191
+ if (!exactNumber(literal)) throw new Error('a number no double holds as written');
192
+ return Number(literal);
193
+ };
194
+ const str = (): string => {
195
+ at = skip(s, at);
196
+ const end = stringEnd(s, at);
197
+ const out = JSON.parse(s.slice(at, end)) as string;
198
+ if (LONE.test(out)) throw new Error('a lone surrogate');
199
+ at = end;
200
+ return out;
201
+ };
202
+ return value();
203
+ };
204
+
205
+ export const readValue = (input: Uint8Array | string, depth: number): Json | undefined => {
206
+ const s = text(input);
207
+ if (s === null) return undefined;
208
+ const start = skip(s, 0);
209
+ const end = valueEnd(s, start);
210
+ if (end < 0 || skip(s, end) !== s.length || depthOf(s) > depth) return undefined;
211
+ try {
212
+ return parse(s);
213
+ } catch {
214
+ return undefined;
215
+ }
216
+ };
217
+
218
+ // A value as JSON text, or undefined for one that JSON would drop, rewrite
219
+ // or refuse: anything but null, a boolean, a finite number other than minus
220
+ // zero, well-formed text, a list with no holes, or a plain object of own
221
+ // string keys, nested no deeper than `depth`, with no cycle.
222
+ export const writeValue = (value: unknown, depth: number): string | undefined => {
223
+ const fits = (v: unknown, room: number, path: Set<object>): boolean => {
224
+ if (v === null || typeof v === 'boolean') return true;
225
+ if (typeof v === 'string') return !LONE.test(v);
226
+ if (typeof v === 'number') return Number.isFinite(v) && !Object.is(v, -0);
227
+ if (typeof v !== 'object' || room === 0 || path.has(v)) return false;
228
+ path.add(v);
229
+ const ok = Array.isArray(v)
230
+ ? Object.keys(v).length === v.length && v.every((x) => fits(x, room - 1, path))
231
+ : [Object.prototype, null].includes(Object.getPrototypeOf(v)) &&
232
+ Object.getOwnPropertySymbols(v).length === 0 &&
233
+ Object.entries(Object.getOwnPropertyDescriptors(v)).every(([k, d]) => !LONE.test(k) && d.enumerable === true && 'value' in d && fits(d.value, room - 1, path));
234
+ path.delete(v);
235
+ return ok;
236
+ };
237
+ return fits(value, depth, new Set()) ? JSON.stringify(value) : undefined;
238
+ };
239
+
240
+ // A count number: no sign, no fraction, no exponent, from 1 to 2^53 - 1.
241
+ export const isCount = (literal: string): boolean => /^[1-9]\d{0,15}$/.test(literal) && Number(literal) <= Number.MAX_SAFE_INTEGER;
@@ -0,0 +1,38 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // ML-KEM-768 of FIPS 203 encapsulates. Web Crypto has none on every terrain,
3
+ // so it comes from `@noble/post-quantum`.
4
+ import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
5
+
6
+ export const EK = 1184;
7
+ export const CIPHERTEXT = 1088;
8
+
9
+ export type Lock = { ek: Uint8Array; dk: Uint8Array };
10
+
11
+ // KeyGen_internal(d, z): the lock from sixty-four drawn bytes, d then z.
12
+ export const lockFrom = (d: Uint8Array, z: Uint8Array): Lock => {
13
+ const { publicKey, secretKey } = ml_kem768.keygen(Uint8Array.from([...d, ...z]));
14
+ return { ek: publicKey, dk: secretKey };
15
+ };
16
+
17
+ // Encaps_internal(ek, m), or null for an encapsulation key the modulus check
18
+ // of FIPS 203 section 7.2 refuses.
19
+ export const encapsulate = (ek: Uint8Array, m: Uint8Array): { ciphertext: Uint8Array; shared: Uint8Array } | null => {
20
+ if (ek.length !== EK) return null;
21
+ try {
22
+ const { cipherText, sharedSecret } = ml_kem768.encapsulate(ek, m);
23
+ return { ciphertext: cipherText, shared: sharedSecret };
24
+ } catch {
25
+ return null;
26
+ }
27
+ };
28
+
29
+ // Decapsulation is implicit rejection: a ciphertext for another lock gives a
30
+ // shared secret that opens nothing.
31
+ export const decapsulate = (dk: Uint8Array, ciphertext: Uint8Array): Uint8Array | null => {
32
+ if (ciphertext.length !== CIPHERTEXT) return null;
33
+ try {
34
+ return ml_kem768.decapsulate(ciphertext, dk);
35
+ } catch {
36
+ return null;
37
+ }
38
+ };
@@ -0,0 +1,33 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Web Crypto, read at every use. A page on a plain http origin has `crypto`
3
+ // without `subtle`, and a terrain may install it after this module loads.
4
+ export const subtle = (): SubtleCrypto => {
5
+ const found = globalThis.crypto?.subtle;
6
+ if (!found) throw new Error('this terrain has no crypto.subtle: a ward needs a secure context');
7
+ return found;
8
+ };
9
+
10
+ export type Bytes = Uint8Array<ArrayBuffer>;
11
+
12
+ // Web Crypto takes a buffer it owns, so every input is copied into one.
13
+ export const own = (bytes: Uint8Array): Bytes => Uint8Array.from(bytes);
14
+
15
+ // The PKCS#8 wrapping of a thirty-two byte secret, which is the whole of it
16
+ // for both curves.
17
+ const PKCS8 = { Ed25519: '302e020100300506032b657004220420', X25519: '302e020100300506032b656e04220420' } as const;
18
+
19
+ export const importSecret = (curve: keyof typeof PKCS8, secret: Uint8Array, uses: KeyUsage[]): Promise<CryptoKey> => {
20
+ if (secret.length !== 32) throw new TypeError('a secret is thirty-two bytes');
21
+ const prefix = Uint8Array.from(PKCS8[curve].match(/../g)!, (b) => parseInt(b, 16));
22
+ const wrapped = new Uint8Array(48);
23
+ wrapped.set(prefix);
24
+ wrapped.set(secret, 16);
25
+ return subtle().importKey('pkcs8', wrapped, { name: curve }, true, uses);
26
+ };
27
+
28
+ // Web Crypto gives a private key's public half only through a JWK.
29
+ export const publicOf = async (key: CryptoKey): Promise<Uint8Array> => {
30
+ const { x } = await subtle().exportKey('jwk', key);
31
+ const binary = atob(x!.replaceAll('-', '+').replaceAll('_', '/'));
32
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
33
+ };
@@ -0,0 +1,21 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // X25519 agrees. A secret is thirty-two bytes, clamped by the function and
3
+ // never before.
4
+ import { zero } from './bytes.ts';
5
+ import { importSecret, own, publicOf, subtle } from './subtle.ts';
6
+
7
+ export const x25519Public = async (secret: Uint8Array): Promise<Uint8Array> => publicOf(await importSecret('X25519', secret, ['deriveBits']));
8
+
9
+ // The agreement, or null for a public key that takes no seal: one whose
10
+ // agreement is thirty-two zero bytes, or one the terrain refuses to read.
11
+ export const agree = async (secret: Uint8Array, pk: Uint8Array): Promise<Uint8Array | null> => {
12
+ if (pk.length !== 32) return null;
13
+ try {
14
+ const mine = await importSecret('X25519', secret, ['deriveBits']);
15
+ const theirs = await subtle().importKey('raw', own(pk), { name: 'X25519' }, false, []);
16
+ const shared = new Uint8Array(await subtle().deriveBits({ name: 'X25519', public: theirs }, mine, 256));
17
+ return zero(shared) ? null : shared;
18
+ } catch {
19
+ return null;
20
+ }
21
+ };