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.
- package/README.md +66 -101
- package/dist/being/being.d.ts +7 -15
- package/dist/being/being.js +16 -67
- package/dist/being/digest.d.ts +1 -1
- package/dist/being/digest.js +7 -27
- package/dist/being/faculty.d.ts +9 -0
- package/dist/being/faculty.js +68 -0
- package/dist/being/index.d.ts +4 -5
- package/dist/being/index.js +5 -5
- package/dist/being/types.d.ts +35 -62
- package/dist/being/types.js +3 -57
- package/dist/being/words.d.ts +13 -0
- package/dist/being/words.js +14 -0
- package/dist/contract/index.d.ts +26 -0
- package/dist/contract/index.js +19 -0
- package/dist/crypto/aes.d.ts +3 -0
- package/dist/crypto/aes.js +24 -0
- package/dist/crypto/bytes.d.ts +6 -0
- package/dist/crypto/bytes.js +33 -0
- package/dist/crypto/ed25519.d.ts +3 -0
- package/dist/crypto/ed25519.js +85 -0
- package/dist/crypto/hash.d.ts +2 -0
- package/dist/crypto/hash.js +11 -0
- package/dist/crypto/index.d.ts +7 -0
- package/dist/crypto/index.js +10 -0
- package/dist/crypto/json.d.ts +8 -0
- package/dist/crypto/json.js +250 -0
- package/dist/crypto/mlkem.d.ts +12 -0
- package/dist/crypto/mlkem.js +36 -0
- package/dist/crypto/subtle.d.ts +10 -0
- package/dist/crypto/subtle.js +29 -0
- package/dist/crypto/x25519.d.ts +2 -0
- package/dist/crypto/x25519.js +21 -0
- package/dist/folder/index.d.ts +13 -0
- package/dist/folder/index.js +125 -0
- package/dist/harbor/dock.d.ts +19 -0
- package/dist/harbor/dock.js +92 -0
- package/dist/harbor/harbor.d.ts +22 -0
- package/dist/harbor/harbor.js +84 -0
- package/dist/harbor/index.d.ts +4 -10
- package/dist/harbor/index.js +5 -18
- package/dist/harbor/registry.d.ts +7 -0
- package/dist/harbor/registry.js +13 -0
- package/dist/harbor/terrain.d.ts +8 -0
- package/dist/harbor/terrain.js +2 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/pointer/bodies.d.ts +20 -0
- package/dist/pointer/bodies.js +45 -0
- package/dist/pointer/index.d.ts +2 -0
- package/dist/pointer/index.js +4 -0
- package/dist/pointer/world.d.ts +34 -0
- package/dist/pointer/world.js +76 -0
- package/dist/quo/door.d.ts +34 -0
- package/dist/quo/door.js +172 -0
- package/dist/quo/index.d.ts +8 -0
- package/dist/quo/index.js +11 -0
- package/dist/quo/invitation.d.ts +7 -0
- package/dist/quo/invitation.js +15 -0
- package/dist/quo/keys.d.ts +40 -0
- package/dist/quo/keys.js +79 -0
- package/dist/quo/payload.d.ts +15 -0
- package/dist/quo/payload.js +55 -0
- package/dist/quo/relations.d.ts +40 -0
- package/dist/quo/relations.js +33 -0
- package/dist/quo/reply.d.ts +13 -0
- package/dist/quo/reply.js +35 -0
- package/dist/quo/seal.d.ts +42 -0
- package/dist/quo/seal.js +78 -0
- package/dist/quo/standing.d.ts +39 -0
- package/dist/quo/standing.js +90 -0
- package/dist/ward/allowance.d.ts +13 -9
- package/dist/ward/allowance.js +28 -67
- package/dist/ward/cells.d.ts +6 -6
- package/dist/ward/cells.js +67 -179
- package/dist/ward/index.d.ts +5 -10
- package/dist/ward/index.js +6 -13
- package/dist/ward/partition.d.ts +44 -60
- package/dist/ward/partition.js +56 -285
- package/dist/ward/stance.d.ts +29 -20
- package/dist/ward/stance.js +173 -406
- package/dist/ward/ward-being.d.ts +33 -0
- package/dist/ward/ward-being.js +76 -0
- package/dist/ward/ward.d.ts +43 -11
- package/dist/ward/ward.js +215 -363
- package/package.json +19 -33
- package/src/being/being.ts +31 -78
- package/src/being/digest.ts +7 -35
- package/src/being/faculty.ts +66 -0
- package/src/being/index.ts +5 -6
- package/src/being/types.ts +47 -145
- package/src/being/words.ts +31 -0
- package/src/contract/index.ts +44 -0
- package/src/crypto/aes.ts +26 -0
- package/src/crypto/bytes.ts +37 -0
- package/src/crypto/ed25519.ts +84 -0
- package/src/crypto/hash.ts +14 -0
- package/src/crypto/index.ts +10 -0
- package/src/crypto/json.ts +241 -0
- package/src/crypto/mlkem.ts +38 -0
- package/src/crypto/subtle.ts +33 -0
- package/src/crypto/x25519.ts +21 -0
- package/src/folder/index.ts +134 -0
- package/src/harbor/dock.ts +101 -0
- package/src/harbor/harbor.ts +105 -0
- package/src/harbor/index.ts +5 -19
- package/src/harbor/registry.ts +20 -0
- package/src/harbor/terrain.ts +13 -0
- package/src/index.ts +20 -0
- package/src/pointer/bodies.ts +47 -0
- package/src/pointer/index.ts +4 -0
- package/src/pointer/world.ts +91 -0
- package/src/quo/door.ts +178 -0
- package/src/quo/index.ts +11 -0
- package/src/quo/invitation.ts +15 -0
- package/src/quo/keys.ts +91 -0
- package/src/quo/payload.ts +61 -0
- package/src/quo/relations.ts +62 -0
- package/src/quo/reply.ts +38 -0
- package/src/quo/seal.ts +101 -0
- package/src/quo/standing.ts +111 -0
- package/src/stand/main.ts +19 -0
- package/src/stand/stand.ts +178 -0
- package/src/ward/allowance.ts +37 -75
- package/src/ward/cells.ts +63 -176
- package/src/ward/index.ts +6 -17
- package/src/ward/partition.ts +86 -326
- package/src/ward/stance.ts +185 -420
- package/src/ward/ward-being.ts +97 -0
- package/src/ward/ward.ts +229 -363
- package/dist/being/lent.d.ts +0 -32
- package/dist/being/lent.js +0 -72
- package/dist/being/silence.d.ts +0 -12
- package/dist/being/silence.js +0 -41
- package/dist/conformance/assert.d.ts +0 -11
- package/dist/conformance/assert.js +0 -106
- package/dist/conformance/beings.d.ts +0 -199
- package/dist/conformance/beings.js +0 -188
- package/dist/conformance/estate.d.ts +0 -5
- package/dist/conformance/estate.js +0 -388
- package/dist/conformance/index.d.ts +0 -80
- package/dist/conformance/index.js +0 -819
- package/dist/conformance/reach.d.ts +0 -10
- package/dist/conformance/reach.js +0 -72
- package/dist/conformance/store.d.ts +0 -5
- package/dist/conformance/store.js +0 -113
- package/dist/harbor/box.d.ts +0 -121
- package/dist/harbor/box.js +0 -121
- package/dist/harbor/core.d.ts +0 -55
- package/dist/harbor/core.js +0 -662
- package/dist/harbor/dial.d.ts +0 -9
- package/dist/harbor/dial.js +0 -81
- package/dist/harbor/memory.d.ts +0 -29
- package/dist/harbor/memory.js +0 -104
- package/dist/harbor/reach.d.ts +0 -36
- package/dist/harbor/reach.js +0 -199
- package/dist/harbor/store.d.ts +0 -35
- package/dist/harbor/store.js +0 -62
- package/dist/vector/cases.d.ts +0 -42
- package/dist/vector/cases.js +0 -223
- package/dist/vector/index.d.ts +0 -6
- package/dist/vector/index.js +0 -8
- package/dist/vector/stand.d.ts +0 -9
- package/dist/vector/stand.js +0 -77
- package/dist/vector/world.d.ts +0 -144
- package/dist/vector/world.js +0 -209
- package/dist/ward/arithmetic.d.ts +0 -31
- package/dist/ward/arithmetic.js +0 -249
- package/dist/ward/door.d.ts +0 -18
- package/dist/ward/door.js +0 -186
- package/dist/ward/ground.d.ts +0 -29
- package/dist/ward/ground.js +0 -56
- package/dist/ward/heirs.d.ts +0 -13
- package/dist/ward/heirs.js +0 -117
- package/dist/ward/json.d.ts +0 -2
- package/dist/ward/json.js +0 -163
- package/dist/ward/owner.d.ts +0 -13
- package/dist/ward/owner.js +0 -220
- package/dist/ward/seal.d.ts +0 -52
- package/dist/ward/seal.js +0 -150
- package/src/being/lent.ts +0 -72
- package/src/being/silence.ts +0 -46
- package/src/conformance/assert.ts +0 -100
- package/src/conformance/beings.ts +0 -188
- package/src/conformance/estate.ts +0 -412
- package/src/conformance/index.ts +0 -965
- package/src/conformance/reach.ts +0 -83
- package/src/conformance/store.ts +0 -125
- package/src/harbor/box.ts +0 -131
- package/src/harbor/core.ts +0 -699
- package/src/harbor/dial.ts +0 -112
- package/src/harbor/memory.ts +0 -123
- package/src/harbor/reach.ts +0 -221
- package/src/harbor/store.ts +0 -91
- package/src/vector/cases.ts +0 -257
- package/src/vector/index.ts +0 -11
- package/src/vector/stand.ts +0 -76
- package/src/vector/world.ts +0 -232
- package/src/ward/arithmetic.ts +0 -251
- package/src/ward/door.ts +0 -186
- package/src/ward/ground.ts +0 -142
- package/src/ward/heirs.ts +0 -116
- package/src/ward/json.ts +0 -144
- package/src/ward/owner.ts +0 -214
- package/src/ward/seal.ts +0 -178
package/src/ward/arithmetic.ts
DELETED
|
@@ -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
|
-
}
|
package/src/ward/ground.ts
DELETED
|
@@ -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
|
-
}
|