nervur 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/GETTING_STARTED.md +138 -0
  2. package/NOTICE +1 -1
  3. package/README.md +108 -20
  4. package/dist/being/being.d.ts +24 -0
  5. package/dist/being/being.js +109 -0
  6. package/dist/being/digest.d.ts +3 -0
  7. package/dist/being/digest.js +37 -0
  8. package/dist/being/index.d.ts +5 -0
  9. package/dist/being/index.js +6 -0
  10. package/dist/being/silence.d.ts +12 -0
  11. package/dist/being/silence.js +41 -0
  12. package/dist/being/types.d.ts +89 -0
  13. package/dist/being/types.js +58 -0
  14. package/dist/conformance/assert.d.ts +11 -0
  15. package/dist/conformance/assert.js +106 -0
  16. package/dist/conformance/beings.d.ts +198 -0
  17. package/dist/conformance/beings.js +183 -0
  18. package/dist/conformance/estate.d.ts +5 -0
  19. package/dist/conformance/estate.js +388 -0
  20. package/dist/conformance/index.d.ts +81 -0
  21. package/dist/conformance/index.js +816 -0
  22. package/dist/conformance/reach.d.ts +10 -0
  23. package/dist/conformance/reach.js +72 -0
  24. package/dist/conformance/store.d.ts +5 -0
  25. package/dist/conformance/store.js +123 -0
  26. package/dist/harbor/core.d.ts +53 -0
  27. package/dist/harbor/core.js +621 -0
  28. package/dist/harbor/dial.d.ts +9 -0
  29. package/dist/harbor/dial.js +81 -0
  30. package/dist/harbor/index.d.ts +8 -0
  31. package/dist/harbor/index.js +16 -0
  32. package/dist/harbor/memory.d.ts +25 -0
  33. package/dist/harbor/memory.js +72 -0
  34. package/dist/harbor/reach.d.ts +36 -0
  35. package/dist/harbor/reach.js +191 -0
  36. package/dist/harbor/store.d.ts +40 -0
  37. package/dist/harbor/store.js +70 -0
  38. package/dist/vector/cases.d.ts +41 -0
  39. package/dist/vector/cases.js +195 -0
  40. package/dist/vector/index.d.ts +6 -0
  41. package/dist/vector/index.js +8 -0
  42. package/dist/vector/stand.d.ts +9 -0
  43. package/dist/vector/stand.js +76 -0
  44. package/dist/vector/world.d.ts +143 -0
  45. package/dist/vector/world.js +198 -0
  46. package/dist/ward/allowance.d.ts +10 -0
  47. package/dist/ward/allowance.js +71 -0
  48. package/dist/ward/arithmetic.d.ts +31 -0
  49. package/dist/ward/arithmetic.js +245 -0
  50. package/dist/ward/cells.d.ts +7 -0
  51. package/dist/ward/cells.js +186 -0
  52. package/dist/ward/door.d.ts +18 -0
  53. package/dist/ward/door.js +186 -0
  54. package/dist/ward/ground.d.ts +23 -0
  55. package/dist/ward/ground.js +38 -0
  56. package/dist/ward/heirs.d.ts +13 -0
  57. package/dist/ward/heirs.js +115 -0
  58. package/dist/ward/index.d.ts +9 -0
  59. package/dist/ward/index.js +13 -0
  60. package/dist/ward/owner.d.ts +13 -0
  61. package/dist/ward/owner.js +220 -0
  62. package/dist/ward/partition.d.ts +65 -0
  63. package/dist/ward/partition.js +295 -0
  64. package/dist/ward/seal.d.ts +52 -0
  65. package/dist/ward/seal.js +145 -0
  66. package/dist/ward/stance.d.ts +25 -0
  67. package/dist/ward/stance.js +413 -0
  68. package/dist/ward/ward.d.ts +12 -0
  69. package/dist/ward/ward.js +361 -0
  70. package/package.json +37 -15
  71. package/protocol/SPEC.md +1843 -0
  72. package/protocol/vectors/arithmetic.json +117 -0
  73. package/protocol/vectors/door.json +345 -0
  74. package/protocol/vectors/framing.json +92 -0
  75. package/protocol/vectors/wire.json +48 -0
  76. package/quo-kit.md +652 -0
  77. package/src/being/being.ts +123 -0
  78. package/src/being/digest.ts +46 -0
  79. package/src/being/index.ts +7 -0
  80. package/src/being/silence.ts +46 -0
  81. package/src/being/types.ts +170 -0
  82. package/src/conformance/assert.ts +100 -0
  83. package/src/conformance/beings.ts +183 -0
  84. package/src/conformance/estate.ts +412 -0
  85. package/src/conformance/index.ts +962 -0
  86. package/src/conformance/reach.ts +83 -0
  87. package/src/conformance/store.ts +136 -0
  88. package/src/harbor/core.ts +660 -0
  89. package/src/harbor/dial.ts +112 -0
  90. package/src/harbor/index.ts +17 -0
  91. package/src/harbor/memory.ts +83 -0
  92. package/src/harbor/reach.ts +215 -0
  93. package/src/harbor/store.ts +101 -0
  94. package/src/vector/cases.ts +229 -0
  95. package/src/vector/index.ts +11 -0
  96. package/src/vector/stand.ts +75 -0
  97. package/src/vector/world.ts +220 -0
  98. package/src/ward/allowance.ts +85 -0
  99. package/src/ward/arithmetic.ts +247 -0
  100. package/src/ward/cells.ts +190 -0
  101. package/src/ward/door.ts +186 -0
  102. package/src/ward/ground.ts +160 -0
  103. package/src/ward/heirs.ts +114 -0
  104. package/src/ward/index.ts +17 -0
  105. package/src/ward/owner.ts +214 -0
  106. package/src/ward/partition.ts +353 -0
  107. package/src/ward/seal.ts +174 -0
  108. package/src/ward/stance.ts +433 -0
  109. package/src/ward/ward.ts +378 -0
  110. package/index.js +0 -17
  111. package/nervur.js +0 -5
@@ -0,0 +1,71 @@
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.
14
+ // How wide the default is, and how wide she may ask for, are the ward's own —
15
+ // wider is more patient, and no peer can tell the difference except by being
16
+ // refused. A being who never learns the third parameter exists writes correct
17
+ // code forever under these. They are two numbers because the third argument
18
+ // goes both ways: less for the ask she wants back quickly, more for the one
19
+ // she knows is slow.
20
+ export const DEFAULT = { time: 30_000 };
21
+ export const CEILING = { time: 300_000 };
22
+ // What she asked for, held to what her ward allows. A being cannot mint budget
23
+ // nobody granted her: asking for a minute where the ward gives thirty seconds
24
+ // is thirty seconds, silently, because the ceiling is not hers to know.
25
+ // Asking for nothing at all is the default, which is the whole point.
26
+ export function allow(wanted, ceiling = CEILING, base = DEFAULT) {
27
+ // Floored first, then tested: a fraction of a millisecond is not a budget,
28
+ // and falls to the default like every other number that is not a positive
29
+ // whole one.
30
+ const whole = (n, fallback) => {
31
+ const w = typeof n === 'number' ? Math.floor(n) : NaN;
32
+ return Number.isFinite(w) && w > 0 ? w : fallback;
33
+ };
34
+ return { time: Math.min(whole(wanted?.time, base.time), ceiling.time) };
35
+ }
36
+ // Whether a budget has anything left to spend, and the one reader of it: the
37
+ // door calls this on arrival and nothing else asks the question anywhere. A
38
+ // time that is not a whole number above zero is not a budget that ran out, it
39
+ // is no budget at all, and the door refuses both the same way, since a peer
40
+ // learns nothing from the difference. `allow` never makes one, so only bytes
41
+ // off the road ever carry one.
42
+ export const spent = (a) => !(Number.isSafeInteger(a.time) && a.time > 0);
43
+ // What a wait that ran out comes back as. Its own value, held by nobody
44
+ // outside the ward, so no answer from any door can be mistaken for it.
45
+ export const LATE = Symbol('late');
46
+ // A wait with an end. This is the one thing the ward times, and it covers the
47
+ // whole of an ask, not only the wire: a relation that comes back round is
48
+ // stopped at its own lane, before a single byte is sealed, and a bound that
49
+ // only watched the wire would never see it. A timer is the only way to stop
50
+ // waiting and every language that runs Quo has one; no runtime is named here.
51
+ //
52
+ // `rang` is called the moment the bell rings, before anyone waiting on the
53
+ // wait is told: whoever holds work that has not started yet reads it and does
54
+ // not start it. What comes back late is not read, and what has not left yet
55
+ // does not leave.
56
+ export async function within(ms, work, rang) {
57
+ let timer;
58
+ const bell = new Promise((ring) => {
59
+ timer = setTimeout(() => {
60
+ rang?.();
61
+ ring(LATE);
62
+ }, ms);
63
+ });
64
+ try {
65
+ return await Promise.race([work, bell]);
66
+ }
67
+ finally {
68
+ clearTimeout(timer);
69
+ void work.then(() => { }, () => { }); // whatever it was, it is nobody's answer now
70
+ }
71
+ }
@@ -0,0 +1,31 @@
1
+ export declare const KEY = 32;
2
+ export declare const SIGNATURE = 64;
3
+ export declare const NONCE = 12;
4
+ export declare const TAG = 16;
5
+ export declare function hex(bytes: Uint8Array | ArrayBuffer): string;
6
+ export declare function unhex(text: string): Uint8Array;
7
+ export declare function concat(parts: Uint8Array[]): Uint8Array;
8
+ export declare function sameBytes(a: Uint8Array, b: Uint8Array): boolean;
9
+ export declare const smallOrder: (pk: Uint8Array) => boolean;
10
+ export declare const heldKeys: () => {
11
+ held: number;
12
+ bound: number;
13
+ };
14
+ export declare function sha256(...parts: Uint8Array[]): Promise<Uint8Array>;
15
+ export type Pair = {
16
+ secret: Uint8Array;
17
+ pk: Uint8Array;
18
+ };
19
+ export declare function signingPair(seed: Uint8Array): Promise<Pair>;
20
+ export declare function sealingPair(seed: Uint8Array): Promise<Pair>;
21
+ export declare function sign(message: Uint8Array, secret: Uint8Array): Promise<Uint8Array>;
22
+ export declare function verify(message: Uint8Array, signature: Uint8Array, pk: Uint8Array): Promise<boolean>;
23
+ export declare function agree(secret: Uint8Array, peerPk: Uint8Array): Promise<Uint8Array>;
24
+ export declare function derive(secret: Uint8Array, label: Uint8Array, bytes: number): Promise<Uint8Array>;
25
+ export declare function encrypt(shared: Uint8Array, plaintext: Uint8Array, aad: Uint8Array): Promise<Uint8Array>;
26
+ export declare function decrypt(shared: Uint8Array, ciphertext: Uint8Array, aad: Uint8Array): Promise<Uint8Array>;
27
+ export declare function box(inside: Uint8Array, padlock: Uint8Array, seed: Uint8Array): Promise<{
28
+ bytes: Uint8Array;
29
+ ephemeral: Pair;
30
+ }>;
31
+ export declare function unbox(bytes: Uint8Array, padlockSecret: Uint8Array): Promise<Uint8Array>;
@@ -0,0 +1,245 @@
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 = () => {
16
+ const s = globalThis.crypto?.subtle;
17
+ if (!s)
18
+ throw new Error('this terrain has no crypto.subtle: a ward needs a secure context');
19
+ return s;
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
+ // A 32-byte secret plus a fixed prefix is the whole PKCS#8 wrapping for both curves.
28
+ const ED_SECRET = unhex('302e020100300506032b657004220420');
29
+ const X_SECRET = unhex('302e020100300506032b656e04220420');
30
+ const ED = { name: 'Ed25519' };
31
+ const X = { name: 'X25519' };
32
+ const HEX = Array.from({ length: 256 }, (_, at) => at.toString(16).padStart(2, '0'));
33
+ export function hex(bytes) {
34
+ let out = '';
35
+ for (const byte of new Uint8Array(bytes))
36
+ out += HEX[byte];
37
+ return out;
38
+ }
39
+ export function unhex(text) {
40
+ const out = new Uint8Array(text.length / 2);
41
+ for (let at = 0; at < out.length; at += 1)
42
+ out[at] = parseInt(text.slice(at * 2, at * 2 + 2), 16);
43
+ return out;
44
+ }
45
+ export function concat(parts) {
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, b) {
55
+ if (a.length !== b.length)
56
+ return false;
57
+ let diff = 0;
58
+ for (let at = 0; at < a.length; at += 1)
59
+ diff |= a[at] ^ b[at];
60
+ return diff === 0;
61
+ }
62
+ // The eight small-order points of Ed25519, in their canonical encoding. A
63
+ // public key among them verifies nothing. An encoding whose y coordinate is
64
+ // not reduced, y at or above the field's prime p, names one of the same
65
+ // points under another spelling, and a terrain's verify may accept a zero
66
+ // signature under it; the field has room for the spelling, so it is refused
67
+ // before the list is read.
68
+ const SMALL_ORDER = [
69
+ '0100000000000000000000000000000000000000000000000000000000000000',
70
+ 'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
71
+ '0000000000000000000000000000000000000000000000000000000000000000',
72
+ '0000000000000000000000000000000000000000000000000000000000000080',
73
+ '26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05',
74
+ 'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a',
75
+ '26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85',
76
+ 'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa',
77
+ ].map(unhex);
78
+ // y >= p, read little-endian with the sign bit masked off: the top byte is
79
+ // 0x7f under the mask, every middle byte is 0xff, and the low byte is at
80
+ // least 0xed, which is p's.
81
+ const unreduced = (pk) => (pk[31] & 0x7f) === 0x7f && pk[0] >= 0xed && pk.subarray(1, 31).every((b) => b === 0xff);
82
+ export const smallOrder = (pk) => unreduced(pk) || SMALL_ORDER.some((p) => sameBytes(p, pk));
83
+ const key32 = (value, what) => {
84
+ if (!(value instanceof Uint8Array) || value.length !== KEY)
85
+ throw new Error(`${what} is not a 32-byte key`);
86
+ return value;
87
+ };
88
+ const pkcs8 = (prefix, value, what) => concat([prefix, key32(value, what)]);
89
+ // Importing a key is the most expensive thing on the path of an ask, and most
90
+ // of the imports are the same key again: a ward signs every reply with the one
91
+ // key, opens every ask with the one padlock, and verifies a relation under the
92
+ // key it verified it under last time. Measured over a round trip, fifteen of
93
+ // the twenty-five imports were bytes already imported once.
94
+ //
95
+ // So an imported key is kept, by the bytes it was imported from. A CryptoKey
96
+ // cannot be changed once it exists, so handing the same one out twice is
97
+ // handing out what a second import would have built. Nothing here is a
98
+ // decision a peer can see: two wards that cache differently, or not at all,
99
+ // speak the same bytes.
100
+ //
101
+ // It is bounded, and that is not a detail. A relation mints a fresh key on
102
+ // every ask, so a ward that talked all day would otherwise hold a key for
103
+ // every ask it ever made. Past the bound the least recently used goes, which
104
+ // is the key of a relation that has fallen quiet, and importing it again
105
+ // costs what it cost the first time.
106
+ //
107
+ // The secret keys in here are the ones the partition already holds in this
108
+ // process, as seeds. The cache is another shape of what the ward is already
109
+ // standing on, and never a second place a secret comes from.
110
+ const KEYS = 512;
111
+ const imported = new Map();
112
+ const keep = (id, make) => {
113
+ const had = imported.get(id);
114
+ if (had !== undefined) {
115
+ imported.delete(id); // and set again below: the most recently used goes last
116
+ imported.set(id, had);
117
+ return had;
118
+ }
119
+ const made = make();
120
+ // A key that would not import is not kept: the next call asks subtle again
121
+ // and hears the same refusal, rather than reading one this cache remembered.
122
+ // Node takes any thirty-two bytes as a public key and finds out at verify,
123
+ // so nothing here reaches this line; a terrain that checks the point at the
124
+ // import does, and a refusal it remembered would be a relation killed for
125
+ // good by one bad arrival.
126
+ made.catch(() => imported.delete(id));
127
+ imported.set(id, made);
128
+ // One in, at most one out: a map keeps what was put in the order it was put,
129
+ // so the first key it names is the one used longest ago.
130
+ if (imported.size > KEYS)
131
+ imported.delete(imported.keys().next().value);
132
+ return made;
133
+ };
134
+ // How many imported keys are held, and the bound they are held under. Nothing
135
+ // in the ward reads either: they are here to be looked at, and for the suite
136
+ // that holds the bound to what it says.
137
+ export const heldKeys = () => ({ held: imported.size, bound: KEYS });
138
+ const secretKey = (alg, prefix, value, what, uses) => {
139
+ const bytes = pkcs8(prefix, value, what);
140
+ return keep(`${alg.name}|${uses.join('+')}|${hex(bytes)}`, () => subtle().importKey('pkcs8', bytes, alg, true, uses));
141
+ };
142
+ const publicKey = (alg, value, what, uses) => {
143
+ const bytes = key32(value, what);
144
+ return keep(`${alg.name}|${uses.join('+')}|pk|${hex(bytes)}`, () => subtle().importKey('raw', bytes, alg, true, uses));
145
+ };
146
+ // Subtle exports the public half of a private key only through a JWK, where
147
+ // `x` is the 32 raw bytes in base64url. The answer is a fact about the key and
148
+ // never changes, so it is kept beside the key it was read from and goes when
149
+ // the key does.
150
+ const publics = new WeakMap();
151
+ function rawPublic(secret) {
152
+ const had = publics.get(secret);
153
+ if (had !== undefined)
154
+ return had;
155
+ const read = (async () => {
156
+ const jwk = await subtle().exportKey('jwk', secret);
157
+ const binary = atob(jwk.x.replaceAll('-', '+').replaceAll('_', '/'));
158
+ const out = new Uint8Array(binary.length);
159
+ for (let at = 0; at < binary.length; at += 1)
160
+ out[at] = binary.charCodeAt(at);
161
+ return out;
162
+ })();
163
+ publics.set(secret, read);
164
+ return read;
165
+ }
166
+ export async function sha256(...parts) {
167
+ return new Uint8Array(await subtle().digest('SHA-256', concat(parts)));
168
+ }
169
+ // Both halves are copies. The seed and the public key are kept behind the two
170
+ // caches above, and a pair is handed to whoever asked for it: what she does
171
+ // with the bytes in her hand is hers, and must not reach what the next caller
172
+ // is given.
173
+ export async function signingPair(seed) {
174
+ const secret = await secretKey(ED, ED_SECRET, seed, 'seed', ['sign']);
175
+ return { secret: Uint8Array.from(seed), pk: Uint8Array.from(await rawPublic(secret)) };
176
+ }
177
+ export async function sealingPair(seed) {
178
+ const secret = await secretKey(X, X_SECRET, seed, 'seed', ['deriveBits']);
179
+ return { secret: Uint8Array.from(seed), pk: Uint8Array.from(await rawPublic(secret)) };
180
+ }
181
+ export async function sign(message, secret) {
182
+ const key = await secretKey(ED, ED_SECRET, secret, 'secret', ['sign']);
183
+ return new Uint8Array(await subtle().sign(ED, key, message));
184
+ }
185
+ export async function verify(message, signature, pk) {
186
+ if (!(signature instanceof Uint8Array) || signature.length !== SIGNATURE)
187
+ return false;
188
+ if (!(pk instanceof Uint8Array) || pk.length !== KEY || smallOrder(pk))
189
+ return false;
190
+ try {
191
+ return await subtle().verify(ED, await publicKey(ED, pk, 'pk', ['verify']), signature, message);
192
+ }
193
+ catch {
194
+ return false;
195
+ }
196
+ }
197
+ export async function agree(secret, peerPk) {
198
+ const key = await secretKey(X, X_SECRET, secret, 'secret', ['deriveBits']);
199
+ const peer = await publicKey(X, peerPk, 'padlock', []);
200
+ const shared = new Uint8Array(await subtle().deriveBits({ name: X.name, public: peer }, key, KEY * 8));
201
+ if (shared.every((b) => b === 0))
202
+ throw new Error('dead agreement'); // a padlock that was not a real key
203
+ return shared;
204
+ }
205
+ // HKDF-SHA-256: bytes from a secret, under a label that says what they are
206
+ // for. No salt, because the secret handed in is already a secret of full
207
+ // strength and the label is what keeps one use apart from another. Every
208
+ // label in this kit is spelled where it is used, never here: this is
209
+ // arithmetic and a label is a decision.
210
+ export async function derive(secret, label, bytes) {
211
+ const material = await subtle().importKey('raw', secret, 'HKDF', false, ['deriveBits']);
212
+ return new Uint8Array(await subtle().deriveBits({ name: 'HKDF', hash: 'SHA-256', salt: SALT, info: label }, material, bytes * 8));
213
+ }
214
+ // One HKDF-SHA-256 yields the AES key and the nonce together. The nonce needs
215
+ // no randomness of its own: the key it pairs with is fresh on every message.
216
+ async function cipherKey(shared, use) {
217
+ const out = await derive(shared, SEAL_INFO, KEY + NONCE);
218
+ return { key: await subtle().importKey('raw', out.subarray(0, KEY), 'AES-GCM', false, [use]), nonce: out.subarray(KEY) };
219
+ }
220
+ // The additional authenticated data is the ephemeral public key: the one thing outside the seal, bound to it.
221
+ export async function encrypt(shared, plaintext, aad) {
222
+ const { key, nonce } = await cipherKey(shared, 'encrypt');
223
+ return new Uint8Array(await subtle().encrypt({ name: 'AES-GCM', iv: nonce, additionalData: key32(aad, 'aad'), tagLength: TAG * 8 }, key, plaintext));
224
+ }
225
+ export async function decrypt(shared, ciphertext, aad) {
226
+ if (ciphertext.length < TAG)
227
+ throw new Error('short input');
228
+ const { key, nonce } = await cipherKey(shared, 'decrypt');
229
+ return new Uint8Array(await subtle().decrypt({ name: 'AES-GCM', iv: nonce, additionalData: key32(aad, 'aad'), tagLength: TAG * 8 }, key, ciphertext));
230
+ }
231
+ // A box: an ephemeral X25519 pk outside, one ciphertext sealed to the
232
+ // padlock. The answer to a box is a box sealed to that ephemeral pk, so the
233
+ // sender keeps the ephemeral secret until the answer comes.
234
+ export async function box(inside, padlock, seed) {
235
+ const ephemeral = await sealingPair(seed);
236
+ const shared = await agree(ephemeral.secret, padlock);
237
+ return { bytes: concat([ephemeral.pk, await encrypt(shared, inside, ephemeral.pk)]), ephemeral };
238
+ }
239
+ export async function unbox(bytes, padlockSecret) {
240
+ if (bytes.length <= KEY)
241
+ throw new Error('short input');
242
+ const ephemeralPk = bytes.subarray(0, KEY);
243
+ const shared = await agree(padlockSecret, ephemeralPk);
244
+ return decrypt(shared, bytes.subarray(KEY), ephemeralPk);
245
+ }
@@ -0,0 +1,7 @@
1
+ import type { Cells } from '../being/types.ts';
2
+ export declare const DEPTH = 64;
3
+ export declare const BREADTH: number;
4
+ export declare const cellFault: (v: unknown, path: string) => string | null;
5
+ export declare const unkeepable: (k: string) => boolean;
6
+ export declare const guardCells: (cells: Cells, wrote?: () => void) => Cells;
7
+ export declare const unguarded: (cells: Cells) => Cells;
@@ -0,0 +1,186 @@
1
+ // How deep a value may nest. Every walk over a value in the kit recurses,
2
+ // and a peer chooses the depth of what she answers; a bound keeps a hostile
3
+ // reply from ending a walk in a stack overflow instead of a refusal.
4
+ export const DEPTH = 64;
5
+ // How many values one written out may come to. Depth alone does not bound
6
+ // what a value costs to keep: one subvalue may sit under two keys, and
7
+ // nesting that forty levels deep is inside the depth bound and is a trillion
8
+ // values once written down. What is walked here is the object graph, which is
9
+ // small; what a harbor keeps is the tree it expands to, which is not. So the
10
+ // walk counts the tree while it proves the graph, and refuses a value nobody
11
+ // could write down. This is a bound on what a being holds, so it is this
12
+ // kit's number and no word of the spec.
13
+ export const BREADTH = 1 << 20;
14
+ // Text a harbor can write down: every surrogate in a pair. A lone one is a
15
+ // code unit with no character, which JSON escapes and UTF-8 cannot encode at
16
+ // all, so a harbor that keeps strings as bytes loses it and one that keeps
17
+ // the object does not. I-JSON says a string is text, and this is that rule
18
+ // where the value is written rather than where it is read.
19
+ const WELL_FORMED = /^(?:[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])*$/;
20
+ // I-JSON, all the way down. A number that JSON cannot write is not a number
21
+ // a harbor can keep, a key on the prototype is not a key she wrote, and a
22
+ // hole in a list, or a key named `__proto__`, is a thing JSON writes one way
23
+ // and a runtime reads another.
24
+ //
25
+ // A fault is a sentence; anything else is the count of values under this one,
26
+ // itself included. A value already counted is a value already proven, so a
27
+ // graph that shares is walked once per node rather than once per path.
28
+ function walk(v, path, seen, sized, depth) {
29
+ if (v === null || typeof v === 'boolean')
30
+ return 1;
31
+ if (typeof v === 'string')
32
+ return WELL_FORMED.test(v) ? 1 : `${path} carries a lone surrogate, which is no text a harbor can write down`;
33
+ if (typeof v === 'number') {
34
+ if (!Number.isFinite(v))
35
+ return `${path} is ${String(v)}, which no harbor can write down`;
36
+ // Minus zero is a number JSON writes as `0` and reads back as `0`. A
37
+ // harbor that keeps values would hand her the sign she wrote and one that
38
+ // writes JSON would not, and she is never told which she is standing in.
39
+ return Object.is(v, -0) ? `${path} is minus zero, which comes back as zero from every harbor that writes JSON` : 1;
40
+ }
41
+ if (typeof v !== 'object')
42
+ return `${path} is a ${typeof v}, which is not a value`;
43
+ if (seen.has(v))
44
+ return `${path} refers back to itself`;
45
+ const counted = sized.get(v);
46
+ if (counted !== undefined)
47
+ return counted;
48
+ if (depth >= DEPTH)
49
+ return `${path} is nested past ${DEPTH} levels, which no harbor can keep`;
50
+ seen.add(v);
51
+ try {
52
+ let size = 1;
53
+ const under = (where, x) => {
54
+ const r = walk(x, where, seen, sized, depth + 1);
55
+ if (typeof r === 'string')
56
+ return r;
57
+ size += r;
58
+ return size > BREADTH ? `${path} writes out to more than ${BREADTH} values, which no harbor can keep` : null;
59
+ };
60
+ if (Array.isArray(v)) {
61
+ for (let i = 0; i < v.length; i += 1) {
62
+ if (!(i in v))
63
+ return `${path}[${i}] is a hole, which no harbor can write down`;
64
+ const f = under(`${path}[${i}]`, v[i]);
65
+ if (f)
66
+ return f;
67
+ }
68
+ sized.set(v, size);
69
+ return size;
70
+ }
71
+ const proto = Object.getPrototypeOf(v);
72
+ if (proto !== Object.prototype && proto !== null)
73
+ return `${path} is a ${(v).constructor?.name ?? 'object'}, which is not a value`;
74
+ for (const k of Object.keys(v)) {
75
+ if (k === '__proto__')
76
+ return `${path}.__proto__ is a key no harbor can keep`;
77
+ const f = under(`${path}.${k}`, v[k]);
78
+ if (f)
79
+ return f;
80
+ }
81
+ sized.set(v, size);
82
+ return size;
83
+ }
84
+ finally {
85
+ seen.delete(v);
86
+ }
87
+ }
88
+ export const cellFault = (v, path) => {
89
+ const out = walk(v, path, new Set(), new Map(), 0);
90
+ return typeof out === 'string' ? out : null;
91
+ };
92
+ // The three keys at the root of her cells that are the ward's: it writes
93
+ // them, she reads them, and a write of hers there is refused like a non-value.
94
+ const WARDS = new Set(['standings', 'occupants', 'class']);
95
+ // The guard is one proxy at the root and one for every container read through
96
+ // it, so a write nested three deep is refused the same way a write at the top
97
+ // is. Wrappers are remembered, so reading the same array twice is the same
98
+ // object twice and a being may still compare what she holds.
99
+ const wrapped = new WeakMap();
100
+ const targets = new WeakMap();
101
+ const guards = new WeakSet();
102
+ // A name no partition can hold, wherever it appears. It is here beside the
103
+ // guard that refuses it, because the ward refuses it too: a relation named
104
+ // this one would be written into the bind table and then throw on her cells,
105
+ // which is half a relation and a throw where a null is promised. One rule,
106
+ // one place, read by the guard and by the stance.
107
+ export const unkeepable = (k) => k === '__proto__';
108
+ // A key a write may land on: a string that is not `__proto__`, and not one
109
+ // of the ward's at the root. A symbol key is a thing JSON never writes.
110
+ const refuse = (why) => {
111
+ throw new TypeError(`cells hold values: ${why}`);
112
+ };
113
+ function keyFault(t, k, v, path, root) {
114
+ if (typeof k !== 'string')
115
+ refuse(`${path} takes no symbol key`);
116
+ if (unkeepable(k))
117
+ refuse(`${path}.__proto__ is a key no harbor can keep`);
118
+ if (root && WARDS.has(k))
119
+ refuse(`${path}.${k} is the ward's to write`);
120
+ // A list grows by one at its end, or it has holes JSON cannot write. A
121
+ // push sets the slot at its length and then the length: both pass. A
122
+ // length set past what she wrote, or a slot beyond it, would leave holes.
123
+ if (Array.isArray(t)) {
124
+ if (k === 'length') {
125
+ if (typeof v !== 'number' || v > t.length)
126
+ refuse(`${path}.length set past what she wrote would leave holes`);
127
+ return;
128
+ }
129
+ const i = Number(k);
130
+ if (Number.isInteger(i) && i > t.length)
131
+ refuse(`${path}[${i}] would leave a hole`);
132
+ }
133
+ }
134
+ function guard(target, path, wrote, root = false) {
135
+ if (guards.has(target))
136
+ return target;
137
+ const had = wrapped.get(target);
138
+ if (had)
139
+ return had;
140
+ const p = new Proxy(target, {
141
+ get(t, k, r) {
142
+ const v = Reflect.get(t, k, r);
143
+ // A container reached through her cells is part of her cells.
144
+ return v !== null && typeof v === 'object' && !ArrayBuffer.isView(v) ? guard(v, `${path}.${String(k)}`, wrote) : v;
145
+ },
146
+ set(t, k, v, r) {
147
+ keyFault(t, k, v, path, root);
148
+ const f = cellFault(v, `${path}.${String(k)}`);
149
+ if (f)
150
+ refuse(f);
151
+ const ok = Reflect.set(t, k, v, r);
152
+ wrote();
153
+ return ok;
154
+ },
155
+ defineProperty(t, k, d) {
156
+ if (!('value' in d))
157
+ refuse(`${path}.${String(k)} is an accessor, which no harbor can keep`);
158
+ keyFault(t, k, d.value, path, root);
159
+ const f = cellFault(d.value, `${path}.${String(k)}`);
160
+ if (f)
161
+ refuse(f);
162
+ const ok = Reflect.defineProperty(t, k, d);
163
+ wrote();
164
+ return ok;
165
+ },
166
+ deleteProperty(t, k) {
167
+ if (root && typeof k === 'string' && WARDS.has(k))
168
+ refuse(`${path}.${k} is the ward's to keep`);
169
+ const ok = Reflect.deleteProperty(t, k);
170
+ wrote();
171
+ return ok;
172
+ },
173
+ });
174
+ wrapped.set(target, p);
175
+ targets.set(p, target);
176
+ guards.add(p);
177
+ return p;
178
+ }
179
+ // Her cells, guarded. Called once per being at boot; the guarded object is
180
+ // what goes into the partition and what the stance hands her, so there is no
181
+ // second door onto the same cells. Every write that passes says so to the
182
+ // ward, which says so to its harbor.
183
+ export const guardCells = (cells, wrote = () => { }) => guard(cells, 'cells', wrote, true);
184
+ // The cells behind the guard, for the ward alone: the one writer of the
185
+ // three keys that are its own.
186
+ export const unguarded = (cells) => (targets.get(cells) ?? cells);
@@ -0,0 +1,18 @@
1
+ import type { Asker, BeingLike, JsonObject, OccupantRecord } from '../being/types.ts';
2
+ import type { Heirs } from './heirs.ts';
3
+ import { type ReplyPayload, type WardKey } from './seal.ts';
4
+ export type Door = {
5
+ key: string;
6
+ being: BeingLike;
7
+ cells: {
8
+ occupants: Record<string, OccupantRecord>;
9
+ };
10
+ };
11
+ export type Judged = {
12
+ bytes: Uint8Array;
13
+ heard: boolean;
14
+ };
15
+ export declare function answered(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload>;
16
+ export declare function arrive(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload>;
17
+ export declare function seen(door: Door, asker: Asker): Promise<string | null>;
18
+ export declare function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, publicKey: () => string | null, random: (n: number) => Uint8Array, keep: () => Promise<boolean>): (bytes: Uint8Array) => Promise<Judged>;