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/heirs.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// Heirs. The door's view of every occupant of every being in the ward, by
|
|
3
|
-
// the heir pk the invitation carried. One rule: the key I hold for you may
|
|
4
|
-
// speak, and so may the key it announced last time. A knock is the first
|
|
5
|
-
// use of the heir, which the inviter announced on your behalf, and the heir
|
|
6
|
-
// dies as it speaks.
|
|
7
|
-
import type { DoorWord } from '../being/types.ts';
|
|
8
|
-
import { GONE, type Heir, type Partition } from './partition.ts';
|
|
9
|
-
|
|
10
|
-
// How wide the span is, is the spec's and never a ward's. Wider is more
|
|
11
|
-
// forgiving of a rough road, and that is exactly why it is not a ward's to
|
|
12
|
-
// choose: a peer tells two doors apart by being refused at one and answered
|
|
13
|
-
// at the other, which is one relation dying over one number.
|
|
14
|
-
const SPAN = 64;
|
|
15
|
-
|
|
16
|
-
export class Heirs {
|
|
17
|
-
#p: Partition;
|
|
18
|
-
#wrote: () => void;
|
|
19
|
-
constructor(p: Partition, wrote: () => void = () => {}) {
|
|
20
|
-
this.#p = p;
|
|
21
|
-
this.#wrote = wrote;
|
|
22
|
-
}
|
|
23
|
-
open(heir: string, being: string, id: string): void {
|
|
24
|
-
this.#p.heirs[heir] = { being, id, current: heir, announced: null, fresh: true, mark: 0, spent: [] };
|
|
25
|
-
this.#wrote();
|
|
26
|
-
}
|
|
27
|
-
// The id was removed. The heir is forgotten, and its last keys are kept
|
|
28
|
-
// apart, bounded, so that whoever still holds them hears `removed` at the
|
|
29
|
-
// door, once they have signed as themselves, and nobody else hears a thing.
|
|
30
|
-
close(heir: string): void {
|
|
31
|
-
const h = this.#p.heirs[heir];
|
|
32
|
-
if (!h) return;
|
|
33
|
-
delete this.#p.heirs[heir];
|
|
34
|
-
this.#p.gone[heir] = { current: h.current, announced: h.announced };
|
|
35
|
-
const keys = Object.keys(this.#p.gone);
|
|
36
|
-
for (const old of keys.slice(0, Math.max(0, keys.length - GONE))) delete this.#p.gone[old];
|
|
37
|
-
this.#wrote();
|
|
38
|
-
}
|
|
39
|
-
// May `by` speak for a relation she removed? True only for the keys the
|
|
40
|
-
// door held when the id went, which nobody but their holder has.
|
|
41
|
-
gone(heir: string, by: string): boolean {
|
|
42
|
-
const g = this.#p.gone[heir];
|
|
43
|
-
return !!g && (by === g.current || by === g.announced);
|
|
44
|
-
}
|
|
45
|
-
// May `by` speak for this heir? Returns the record if so, null if not.
|
|
46
|
-
// Does not write: the caller verifies the signature first, then settles.
|
|
47
|
-
admits(heir: string, by: string): Heir | null {
|
|
48
|
-
const h = this.#p.heirs[heir];
|
|
49
|
-
if (!h) return null;
|
|
50
|
-
if (h.fresh) return by === h.current ? h : null;
|
|
51
|
-
return by === h.current || by === h.announced ? h : null;
|
|
52
|
-
}
|
|
53
|
-
// Once only. A number above the mark is honoured and moves it; a number
|
|
54
|
-
// inside the span is honoured once and never again; a number at or below
|
|
55
|
-
// the span is refused, because a door that remembered every number ever
|
|
56
|
-
// seen would be a door with unbounded memory. Counting starts where
|
|
57
|
-
// strangers must agree: the first legal number is one.
|
|
58
|
-
//
|
|
59
|
-
// The number rides inside the signed body, so bytes caught on the road
|
|
60
|
-
// carry the number they were sent under and are refused as themselves. A
|
|
61
|
-
// caller who means to ask again asks again, under the next number, and is
|
|
62
|
-
// heard: retry and fire-and-forget stay hers to build. Only the accident
|
|
63
|
-
// and the interception are refused.
|
|
64
|
-
//
|
|
65
|
-
// Writes, so the caller settles it only once the signature has checked out.
|
|
66
|
-
// The same bytes twice are one honoured call and one silence.
|
|
67
|
-
spend(h: Heir, seq: number): boolean {
|
|
68
|
-
if (!Number.isSafeInteger(seq) || seq < 1) return false;
|
|
69
|
-
if (seq > h.mark) {
|
|
70
|
-
if (h.mark > 0) h.spent.push(h.mark);
|
|
71
|
-
h.mark = seq;
|
|
72
|
-
h.spent = h.spent.filter((past) => past > seq - SPAN);
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
if (seq === h.mark || seq <= h.mark - SPAN || h.spent.includes(seq)) return false;
|
|
76
|
-
h.spent.push(seq);
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// The signature checked out. Spend the number and settle the keys, or do
|
|
81
|
-
// neither: a call that binds nothing must not burn a number on its way to
|
|
82
|
-
// being refused, or a stranger who cannot be heard would still leave a mark
|
|
83
|
-
// behind her. Every write below this line is one that is going to hold.
|
|
84
|
-
// This is where a knock is judged and where `unannounced` is said. settle
|
|
85
|
-
// below is the writing alone, and it judges nothing: one owner for the
|
|
86
|
-
// question, one for the answer.
|
|
87
|
-
honour(h: Heir, by: string, next: string | null, seq: number): true | DoorWord {
|
|
88
|
-
if (h.fresh && (next === null || next === h.current)) return 'unannounced'; // a knock without a key of her own binds nothing, and the heir is not a key of her own
|
|
89
|
-
if (!this.spend(h, seq)) return 'repeated';
|
|
90
|
-
this.settle(h, by, next);
|
|
91
|
-
this.#wrote(); // the number and the keys moved together
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// The signature checked out and honour has judged. Settle the keys: a fresh
|
|
96
|
-
// heir rotates at once to what it announced, which honour has already held
|
|
97
|
-
// to being something other than itself, so that the heir dies as it speaks;
|
|
98
|
-
// a current key replaces its announcement; an announced key becomes current.
|
|
99
|
-
settle(h: Heir, by: string, next: string | null): void {
|
|
100
|
-
if (h.fresh) {
|
|
101
|
-
if (next === null) return; // honour judged this. the compiler has not read it, so the narrowing stays.
|
|
102
|
-
h.current = next;
|
|
103
|
-
h.announced = null;
|
|
104
|
-
h.fresh = false;
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
if (by === h.announced) h.current = by;
|
|
108
|
-
// A send that announced nothing leaves the spare standing. This kit
|
|
109
|
-
// announces on every send but a public one, which holds no heir and
|
|
110
|
-
// reaches none of this; a kit that skips one is a kit whose own next is
|
|
111
|
-
// still the key it announced last time, and forgetting it here would meet
|
|
112
|
-
// that key with silence and kill a healthy relation. Nothing is dropped
|
|
113
|
-
// that was vouched for until something replaces it.
|
|
114
|
-
if (next !== null) h.announced = next;
|
|
115
|
-
}
|
|
116
|
-
}
|
package/src/ward/json.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The one reader of JSON that crossed an edge. A runtime's own parser is
|
|
3
|
-
// where two kits stop agreeing: it keeps the last of two keys of one name,
|
|
4
|
-
// rounds a whole number no double holds, reads a number past the doubles as
|
|
5
|
-
// infinity, and mends bytes that are not UTF-8 into a replacement character.
|
|
6
|
-
// Each of those is text two kits read two ways, so each is refused here, where
|
|
7
|
-
// it is read, with a throw the caller already turns into what bytes that said
|
|
8
|
-
// nothing are.
|
|
9
|
-
//
|
|
10
|
-
// What a value is beyond its text, a lone surrogate, a key named `__proto__`,
|
|
11
|
-
// a nesting past the bound, is the value rule's, and `cellFault` in cells.ts
|
|
12
|
-
// is its one reader. This file holds the rules that live only in the text.
|
|
13
|
-
|
|
14
|
-
const utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
15
|
-
|
|
16
|
-
const fail = (at: number, why: string): never => {
|
|
17
|
-
throw new Error(`JSON at ${at}: ${why}`);
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
// A number text, held to the Values chapter. A text naming a whole number
|
|
21
|
-
// stands only if that number is exactly a double, or if the text is how
|
|
22
|
-
// ECMAScript writes the double it reads as. A text past the largest double,
|
|
23
|
-
// or naming a value other than zero that rounds to zero, is refused, and so
|
|
24
|
-
// is minus zero in any spelling.
|
|
25
|
-
export function numberFault(literal: string): string | null {
|
|
26
|
-
const m = /^(-?)(0|[1-9]\d*)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(literal);
|
|
27
|
-
if (!m) return 'not a number';
|
|
28
|
-
const [, sign, whole, frac = '', exp = '0'] = m;
|
|
29
|
-
const v = Number(literal);
|
|
30
|
-
if (!Number.isFinite(v)) return 'past the largest double';
|
|
31
|
-
let digits = (whole + frac).replace(/^0+/, '');
|
|
32
|
-
if (v === 0) {
|
|
33
|
-
if (digits !== '') return 'a value other than zero that rounds to zero';
|
|
34
|
-
return sign ? 'minus zero' : null;
|
|
35
|
-
}
|
|
36
|
-
// The exact value is digits times ten to e. A finite double bounds e from
|
|
37
|
-
// above, so the power below is never larger than the double's own width.
|
|
38
|
-
let e = BigInt(exp) - BigInt(frac.length);
|
|
39
|
-
const trimmed = digits.replace(/0+$/, '');
|
|
40
|
-
e += BigInt(digits.length - trimmed.length);
|
|
41
|
-
digits = trimmed;
|
|
42
|
-
if (e < 0n) return null; // a fraction: the double nearest it, as every reader takes it
|
|
43
|
-
const exact = BigInt(digits) * 10n ** e;
|
|
44
|
-
if (exact === (v < 0 ? -BigInt(v) : BigInt(v))) return null;
|
|
45
|
-
return literal === String(v) ? null : 'a whole number that is not exactly a double';
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Bytes to a value, or a throw.
|
|
49
|
-
export function readJson(input: Uint8Array | string): unknown {
|
|
50
|
-
const s = typeof input === 'string' ? input : utf8.decode(input);
|
|
51
|
-
let at = 0;
|
|
52
|
-
const space = () => {
|
|
53
|
-
for (;;) {
|
|
54
|
-
const c = s.charCodeAt(at);
|
|
55
|
-
if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) at += 1;
|
|
56
|
-
else return;
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
const string = (): string => {
|
|
60
|
-
const start = at;
|
|
61
|
-
at += 1;
|
|
62
|
-
for (;;) {
|
|
63
|
-
const c = s.charCodeAt(at);
|
|
64
|
-
if (Number.isNaN(c)) fail(start, 'an unterminated string');
|
|
65
|
-
if (c === 0x22) break;
|
|
66
|
-
if (c < 0x20) fail(at, 'a control character in a string');
|
|
67
|
-
at += c === 0x5c ? 2 : 1;
|
|
68
|
-
}
|
|
69
|
-
at += 1;
|
|
70
|
-
// The escapes are JSON's own, and the runtime reads them the one way.
|
|
71
|
-
return JSON.parse(s.slice(start, at)) as string;
|
|
72
|
-
};
|
|
73
|
-
const value = (): unknown => {
|
|
74
|
-
space();
|
|
75
|
-
const c = s[at];
|
|
76
|
-
if (c === '{') {
|
|
77
|
-
at += 1;
|
|
78
|
-
const out: Record<string, unknown> = {};
|
|
79
|
-
const keys = new Set<string>();
|
|
80
|
-
space();
|
|
81
|
-
if (s[at] === '}') {
|
|
82
|
-
at += 1;
|
|
83
|
-
return out;
|
|
84
|
-
}
|
|
85
|
-
for (;;) {
|
|
86
|
-
space();
|
|
87
|
-
if (s[at] !== '"') fail(at, 'a key that is not a string');
|
|
88
|
-
const where = at;
|
|
89
|
-
const k = string();
|
|
90
|
-
if (keys.has(k)) fail(where, `two keys named ${JSON.stringify(k)}`);
|
|
91
|
-
keys.add(k);
|
|
92
|
-
space();
|
|
93
|
-
if (s[at] !== ':') fail(at, 'a key with no colon');
|
|
94
|
-
at += 1;
|
|
95
|
-
// Defined, never assigned: a key named `__proto__` is an own key here
|
|
96
|
-
// as it is in JSON.parse, and the value rule refuses it after.
|
|
97
|
-
Object.defineProperty(out, k, { value: value(), enumerable: true, writable: true, configurable: true });
|
|
98
|
-
space();
|
|
99
|
-
if (s[at] === ',') at += 1;
|
|
100
|
-
else if (s[at] === '}') {
|
|
101
|
-
at += 1;
|
|
102
|
-
return out;
|
|
103
|
-
} else fail(at, 'an object not closed');
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (c === '[') {
|
|
107
|
-
at += 1;
|
|
108
|
-
const out: unknown[] = [];
|
|
109
|
-
space();
|
|
110
|
-
if (s[at] === ']') {
|
|
111
|
-
at += 1;
|
|
112
|
-
return out;
|
|
113
|
-
}
|
|
114
|
-
for (;;) {
|
|
115
|
-
out.push(value());
|
|
116
|
-
space();
|
|
117
|
-
if (s[at] === ',') at += 1;
|
|
118
|
-
else if (s[at] === ']') {
|
|
119
|
-
at += 1;
|
|
120
|
-
return out;
|
|
121
|
-
} else fail(at, 'a list not closed');
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if (c === '"') return string();
|
|
125
|
-
for (const [word, v] of [['true', true], ['false', false], ['null', null]] as const) {
|
|
126
|
-
if (s.startsWith(word, at)) {
|
|
127
|
-
at += word.length;
|
|
128
|
-
return v;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
const m = /-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
|
|
132
|
-
m.lastIndex = at;
|
|
133
|
-
const hit = m.exec(s);
|
|
134
|
-
if (!hit) return fail(at, 'no value');
|
|
135
|
-
const why = numberFault(hit[0]);
|
|
136
|
-
if (why !== null) fail(at, `${hit[0]} is ${why}`);
|
|
137
|
-
at += hit[0].length;
|
|
138
|
-
return Number(hit[0]);
|
|
139
|
-
};
|
|
140
|
-
const out = value();
|
|
141
|
-
space();
|
|
142
|
-
if (at !== s.length) fail(at, 'text after the value');
|
|
143
|
-
return out;
|
|
144
|
-
}
|
package/src/ward/owner.ts
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The owner's asks. The root owner holds the ask pointer; every other owner
|
|
3
|
-
// is an occupant of the ward itself, invited by the root. The ward is a
|
|
4
|
-
// being to her owner: the empty ask is her describe, and it carries her asks
|
|
5
|
-
// with a description and an input each, so a side renders them as it
|
|
6
|
-
// renders any being's. Seven asks: boot, public, invite, knock, remove and
|
|
7
|
-
// unboot, which is boot's inverse and takes her relations with her, and ask,
|
|
8
|
-
// which reaches into a being and asks her. The knock is a being's knock made
|
|
9
|
-
// for her, and the standing is written into her cells under the id the owner
|
|
10
|
-
// gave; remove takes a relation out of her by id, the mirror of it.
|
|
11
|
-
import { isSilence, isWord, wordOf } from '../being/silence.ts';
|
|
12
|
-
import { OWNER } from '../being/types.ts';
|
|
13
|
-
import type { Ask, Asker, Invitation, Json, JsonObject, Wanted } from '../being/types.ts';
|
|
14
|
-
import { put } from './partition.ts';
|
|
15
|
-
import { seen, answered } from './door.ts';
|
|
16
|
-
import { within, allow, DEFAULT, LATE } from './allowance.ts';
|
|
17
|
-
import type { Resident } from './ward.ts';
|
|
18
|
-
|
|
19
|
-
export type OwnerSide = {
|
|
20
|
-
pk: string;
|
|
21
|
-
doors: Map<string, Resident>;
|
|
22
|
-
absent(): Record<string, string | null>; // the rows no door holds this run: key to class
|
|
23
|
-
publicKey(): string | null;
|
|
24
|
-
// Class first, then key, as the ground and a being's own boot both say it.
|
|
25
|
-
instantiate(className: string, key: string): Resident | 'threw' | null;
|
|
26
|
-
unboot(key: string): string[] | null;
|
|
27
|
-
setPublic(key: string | null): void; // null takes the mark off, and the being stays
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const str = { type: 'string' };
|
|
31
|
-
export const OWNER_ASKS: Ask[] = [
|
|
32
|
-
{ name: 'boot', description: 'boot a being by class name, under a key the owner chooses', input: { type: 'object', properties: { key: str, class: str }, required: ['key', 'class'] } },
|
|
33
|
-
{ name: 'public', description: 'mark a booted being as the one public being of the ward, reached by anyone at the bare pk; null takes the mark off and leaves her booted', input: { type: 'object', properties: { key: str }, required: ['key'] } },
|
|
34
|
-
{ name: 'invite', description: 'mint an invitation on a being of the ward, under the id she will know the occupant by, with the notes she will read on it; on the ward pk it mints an owner, and only the root may', input: { type: 'object', properties: { being: str, id: str, notes: { type: 'object' } }, required: ['being', 'id'] } },
|
|
35
|
-
{
|
|
36
|
-
name: 'knock',
|
|
37
|
-
description: 'knock for a being of the ward with an invitation, and take the standing under id if answered; being is a key booted, or { boot: class, key } to boot her first',
|
|
38
|
-
input: { type: 'object', properties: { being: { description: 'a key, or { boot, key }' }, id: str, invitation: { type: 'object' }, method: str, args: { type: 'object' }, wanted: { type: 'object', properties: { time: { type: 'number' } } } }, required: ['being', 'id', 'invitation'] },
|
|
39
|
-
},
|
|
40
|
-
{ name: 'remove', description: 'take a relation out of a being of the ward by id, occupant or standing; on the ward pk it unseats an owner, and only the root may', input: { type: 'object', properties: { being: str, id: str }, required: ['being', 'id'] } },
|
|
41
|
-
{ name: 'unboot', description: 'take a being out of the ward, with every relation she holds; her occupants hear removed, and the ward itself is refused', input: { type: 'object', properties: { being: str }, required: ['being'] } },
|
|
42
|
-
{
|
|
43
|
-
name: 'ask',
|
|
44
|
-
description: 'ask a being of the ward, as the owner; with no being it is the public being asked as nobody, which is what a stranger would hear',
|
|
45
|
-
input: { type: 'object', properties: { being: str, method: str, args: { type: 'object' }, wanted: { type: 'object', properties: { time: { type: 'number' } } } } },
|
|
46
|
-
},
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | undefined, args: JsonObject): Promise<Json> {
|
|
50
|
-
// Every name the owner gives is a string, or it is nothing. Coerced instead
|
|
51
|
-
// of checked, an object passed where a name belongs becomes the string
|
|
52
|
-
// "[object Object]", and two owners who each got the shape wrong would be
|
|
53
|
-
// handed one being. Not `word`: a word in this tree is what a ward says when
|
|
54
|
-
// no object came back, and this file imports those.
|
|
55
|
-
const named = (v: unknown): string | null => (typeof v === 'string' ? v : null);
|
|
56
|
-
|
|
57
|
-
if (method === undefined) return describe(w);
|
|
58
|
-
if (method === 'boot') {
|
|
59
|
-
const key = named(args.key),
|
|
60
|
-
className = named(args.class);
|
|
61
|
-
if (key === null || className === null) return { error: 'no such class, or key taken' };
|
|
62
|
-
const door = w.instantiate(className, key);
|
|
63
|
-
if (door === 'threw') return { error: 'threw at birth' };
|
|
64
|
-
return door ? { booted: door.key } : { error: 'no such class, or key taken' };
|
|
65
|
-
}
|
|
66
|
-
if (method === 'public') {
|
|
67
|
-
// A ward may have one public being, and no more. Marking a second would
|
|
68
|
-
// leave the first holding every relation she had, reachable by nobody at
|
|
69
|
-
// the bare pk, and told by nobody that she had been replaced. One absent
|
|
70
|
-
// this run is reachable by nobody already, so the mark may move off her.
|
|
71
|
-
// Null takes the mark off: the being stays and keeps every relation she
|
|
72
|
-
// holds, and the ward answers arrivals for no heir with silence, as one
|
|
73
|
-
// that never had a public being does. Retiring her is not destroying her.
|
|
74
|
-
if (args.key === null) {
|
|
75
|
-
w.setPublic(null);
|
|
76
|
-
return { public: null };
|
|
77
|
-
}
|
|
78
|
-
const key = named(args.key);
|
|
79
|
-
if (key === null || key === w.pk || !w.doors.has(key)) return { error: 'no such being' };
|
|
80
|
-
const standing = w.publicKey();
|
|
81
|
-
if (standing !== null && standing !== key && w.doors.has(standing)) return { error: 'a ward has one public being' };
|
|
82
|
-
w.setPublic(key);
|
|
83
|
-
return { public: key };
|
|
84
|
-
}
|
|
85
|
-
if (method === 'invite') {
|
|
86
|
-
// On the ward itself, only the root may invite: an occupant of the ward
|
|
87
|
-
// is an owner, and ownership is minted by the ask pointer alone. An owner
|
|
88
|
-
// at the door asking to invite on the ward is refused, so that a carried
|
|
89
|
-
// key can pilot but never hand piloting on.
|
|
90
|
-
const being = named(args.being),
|
|
91
|
-
id = named(args.id);
|
|
92
|
-
if (being === null || id === null) return { error: 'no such being' };
|
|
93
|
-
if (being === w.pk && asker.id !== OWNER) return { error: 'no such being' };
|
|
94
|
-
// The notes are the terms the owner mints under, and the being reads them
|
|
95
|
-
// on her gate. Placing an occupant is the owner's, and saying what it may
|
|
96
|
-
// do is part of placing it; the work behind the gate is still hers alone.
|
|
97
|
-
const notes = args.notes;
|
|
98
|
-
const door = w.doors.get(being);
|
|
99
|
-
if (!door) return { error: 'no such being' };
|
|
100
|
-
return await door.stance.occupants.invite(id, notes !== null && typeof notes === 'object' && !Array.isArray(notes) ? (notes) : undefined);
|
|
101
|
-
}
|
|
102
|
-
if (method === 'knock') {
|
|
103
|
-
const b = args.being as string | { boot: unknown; key: unknown };
|
|
104
|
-
let door = typeof b === 'string' ? w.doors.get(b) : undefined;
|
|
105
|
-
if (!door && b && typeof b === 'object') {
|
|
106
|
-
// new or existing: the owner names a being of theirs, and a key already
|
|
107
|
-
// booted is a being of theirs, not a class that failed to instantiate.
|
|
108
|
-
const key = named(b.key),
|
|
109
|
-
className = named(b.boot);
|
|
110
|
-
if (key === null || className === null) return { error: 'no such being' };
|
|
111
|
-
const made = w.instantiate(className, key);
|
|
112
|
-
if (made === 'threw') return { error: 'threw at birth' };
|
|
113
|
-
// Made or already there, the door is the one under the key she named:
|
|
114
|
-
// instantiate boots under that key or refuses, and never another.
|
|
115
|
-
door = w.doors.get(key);
|
|
116
|
-
}
|
|
117
|
-
const id = named(args.id);
|
|
118
|
-
// Never for the ward itself, for the reason invite refuses it: the ward
|
|
119
|
-
// is a being to nobody outside, and holds no relation of its own. A
|
|
120
|
-
// standing written into its cells would be the owner's reach dressed as
|
|
121
|
-
// a being's, and no ask of the ward's could ever use it.
|
|
122
|
-
if (!door || id === null || door.key === w.pk) return { error: 'no such being' };
|
|
123
|
-
const inv = args.invitation as Invitation;
|
|
124
|
-
// The owner is a caller like any other and may say what this knock may
|
|
125
|
-
// spend. Saying nothing is the ward's default, as it is for a being.
|
|
126
|
-
const out = await door.stance.standings.knock(inv, args.method as string | undefined, (args.args as JsonObject) ?? {}, args.wanted as Wanted | undefined);
|
|
127
|
-
if (isSilence(out)) return { error: 'silence' };
|
|
128
|
-
if (isWord(out)) return { error: wordOf(out) }; // the owner hears objects: the word is the error's name
|
|
129
|
-
return { taken: await door.stance.standings.take(id, inv), answer: out };
|
|
130
|
-
}
|
|
131
|
-
if (method === 'remove') {
|
|
132
|
-
// The mirror of knock: a relation out of a being, by id, occupant or
|
|
133
|
-
// standing, since the two share one namespace. On the ward itself the
|
|
134
|
-
// id is an owner, and only the root may unseat one, for the reason only
|
|
135
|
-
// the root may invite one: ownership moves by the root alone.
|
|
136
|
-
const being = named(args.being),
|
|
137
|
-
id = named(args.id);
|
|
138
|
-
if (being === null || id === null) return { error: 'no such being' };
|
|
139
|
-
if (being === w.pk && asker.id !== OWNER) return { error: 'no such being' };
|
|
140
|
-
const door = w.doors.get(being);
|
|
141
|
-
if (!door) return { error: 'no such being' };
|
|
142
|
-
if (Object.hasOwn(door.cells.occupants, id)) door.stance.occupants.remove(id);
|
|
143
|
-
else if (Object.hasOwn(door.cells.standings, id)) door.stance.standings.remove(id);
|
|
144
|
-
else return { error: 'nothing to remove' };
|
|
145
|
-
return { removed: id };
|
|
146
|
-
}
|
|
147
|
-
if (method === 'unboot') {
|
|
148
|
-
// The inverse of boot, and the only way a being leaves a ward. Her
|
|
149
|
-
// relations go with her, so an occupant of hers hears `removed` from the
|
|
150
|
-
// keys the door kept, exactly as it would had the owner removed each id
|
|
151
|
-
// by hand; the owner hears which ids those were. The ward itself is
|
|
152
|
-
// refused, for the reason knock is: it is a being to nobody outside, and
|
|
153
|
-
// unbooting it would be a ward deleting itself from inside its own map,
|
|
154
|
-
// leaving its owners bound to a door that is gone.
|
|
155
|
-
const being = named(args.being);
|
|
156
|
-
if (being === null || being === w.pk) return { error: 'no such being' };
|
|
157
|
-
const removed = w.unboot(being);
|
|
158
|
-
return removed === null ? { error: 'no such being' } : { unbooted: being, removed };
|
|
159
|
-
}
|
|
160
|
-
if (method === 'ask') {
|
|
161
|
-
// The owner's other power: to reach into a being and ask her. It is
|
|
162
|
-
// strictly less than unboot, which takes her out with every relation she
|
|
163
|
-
// holds, and it is the third asker of the ward-to-being edge finally
|
|
164
|
-
// filled in by the ward rather than minted by whoever holds the pointer.
|
|
165
|
-
// Bounded, judged and named here, so no side outside the ward ever holds
|
|
166
|
-
// a being's answer and writes the door's discipline again beside it.
|
|
167
|
-
const being = args.being === undefined ? null : named(args.being);
|
|
168
|
-
if (args.being !== undefined && being === null) return { error: 'no such being' };
|
|
169
|
-
// A method is a name or it is the empty ask. Coerced instead of checked,
|
|
170
|
-
// a shape passed where a name belongs becomes a string she never declared,
|
|
171
|
-
// and she would answer `unknown ask` to something nobody asked.
|
|
172
|
-
if (args.method !== undefined && typeof args.method !== 'string') return { error: 'an ask is named by a word' };
|
|
173
|
-
// No being named is the in-process twin of bytes for nobody: the public
|
|
174
|
-
// being, asked as nobody, unbound, so a throw of hers is silence. Her
|
|
175
|
-
// insides are not a stranger's to read, and a device serving her to
|
|
176
|
-
// strangers must hear what a stranger hears and not one word more.
|
|
177
|
-
const key = being ?? w.publicKey();
|
|
178
|
-
const door = key === null ? undefined : w.doors.get(key);
|
|
179
|
-
if (!door || door.key === w.pk) return { error: 'no such being' };
|
|
180
|
-
const bound = being !== null;
|
|
181
|
-
// The owner is a caller like any other: wanted says what this ask may
|
|
182
|
-
// spend, and saying nothing is the ward's default. A wait that ran out is
|
|
183
|
-
// late, the same word a being would hear, said as the object an owner hears.
|
|
184
|
-
const out = await within(allow(args.wanted as Wanted | undefined).time, answered(door, bound ? { id: OWNER } : {}, args.method, (args.args as JsonObject) ?? {}, bound));
|
|
185
|
-
if (out === LATE) return { error: 'late' };
|
|
186
|
-
if ('quo' in out) return { error: out.quo };
|
|
187
|
-
if (!('object' in out)) return { error: 'silence' };
|
|
188
|
-
return out.object;
|
|
189
|
-
}
|
|
190
|
-
return { error: 'unknown ask' };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// The ward's describe for its owner: its beings, their classes, a digest each,
|
|
194
|
-
// as she would describe herself to the owner. A being absent this run is
|
|
195
|
-
// listed too, with a null digest and absent true, so the owner can see her
|
|
196
|
-
// and take her out.
|
|
197
|
-
async function describe(w: OwnerSide): Promise<Json> {
|
|
198
|
-
const beings: Record<string, Json> = {};
|
|
199
|
-
// Every being is asked at once and each is bounded on her own. One being
|
|
200
|
-
// whose describe never settles is one null digest, not a ward that never
|
|
201
|
-
// answers its owner: the harbor learns the pk by this very ask at boot, so
|
|
202
|
-
// an unbounded wait here is a ward no restart can bring back.
|
|
203
|
-
const asked = [...w.doors].filter(([key]) => key !== w.pk);
|
|
204
|
-
const digests = await Promise.all(asked.map(async ([, door]) => (await within(DEFAULT.time, seen(door, { id: OWNER })))));
|
|
205
|
-
for (const [i, [key, door]] of asked.entries()) {
|
|
206
|
-
const d = digests[i];
|
|
207
|
-
put(beings, key, { class: door.cells.class ?? null, public: w.publicKey() === key, digest: d === LATE ? null : d });
|
|
208
|
-
}
|
|
209
|
-
for (const [key, cls] of Object.entries(w.absent())) put(beings, key, { class: cls, public: w.publicKey() === key, digest: null, absent: true });
|
|
210
|
-
return {
|
|
211
|
-
asks: OWNER_ASKS,
|
|
212
|
-
notes: { pk: w.pk, beings },
|
|
213
|
-
};
|
|
214
|
-
}
|
package/src/ward/seal.ts
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The seal. What a ward does to bytes on the way out and on the way in.
|
|
3
|
-
//
|
|
4
|
-
// A being's key is one 32-byte seed: an Ed25519 pair. It signs, it never
|
|
5
|
-
// seals. The ward's key is one seed too, giving both an Ed25519 pair to sign
|
|
6
|
-
// replies and an X25519 padlock every ask is sealed to. Beings never own a
|
|
7
|
-
// padlock: asks are sealed to the ward, replies to the ephemeral key of the
|
|
8
|
-
// ask they answer.
|
|
9
|
-
//
|
|
10
|
-
// ask on the wire box = eph X pk || AESGCM( payload || signature ) sealed to the ward padlock
|
|
11
|
-
// payload = JSON { to, by, next, seq, time, hops?, method?, args } signed by `by`
|
|
12
|
-
//
|
|
13
|
-
// Nothing rides outside the box. `to` names the heir this ask is for, and it
|
|
14
|
-
// is inside with everything else: a heir is the one name in a relation that
|
|
15
|
-
// never rotates, so on the outside it would be a handle an intermediary could
|
|
16
|
-
// hold forever, and the same handle anyone who ever saw the invitation
|
|
17
|
-
// already has. What a harbor needs to route is the ward pk, which it carries
|
|
18
|
-
// itself. The door opens every ask with its own padlock and reads `to` after,
|
|
19
|
-
// so nothing was ever gained by having it in the clear.
|
|
20
|
-
// reply on the wire box = eph X pk || AESGCM( reply || signature ) sealed to the ask's eph pk
|
|
21
|
-
// reply = JSON { object, seen } | { silence: true } | { quo: word } signed by the ward key
|
|
22
|
-
import type { DoorWord, Json, JsonObject } from '../being/types.ts';
|
|
23
|
-
import { isDoorWord } from '../being/silence.ts';
|
|
24
|
-
import { KEY, SIGNATURE, box, concat, derive, hex, sha256, sign, signingPair, sealingPair, unbox, unhex, verify } from './arithmetic.ts';
|
|
25
|
-
import { cellFault } from './cells.ts';
|
|
26
|
-
import { readJson } from './json.ts';
|
|
27
|
-
|
|
28
|
-
const utf8 = new TextEncoder();
|
|
29
|
-
|
|
30
|
-
export type WardKey = { sign: Uint8Array; padlock: Uint8Array; signPk: Uint8Array; padlockPk: Uint8Array; pk: string };
|
|
31
|
-
|
|
32
|
-
// The two labels a ward's keys are derived under. They are here and not in
|
|
33
|
-
// arithmetic.ts because a label is a decision and that file is arithmetic.
|
|
34
|
-
// `quo-seal` is taken: it is the label the message cipher derives its key and
|
|
35
|
-
// nonce under, from an agreement rather than from a seed, and two derivations
|
|
36
|
-
// answering to one name is how a kit is read wrong.
|
|
37
|
-
const WARD_SIGN = new TextEncoder().encode('quo-ward-sign');
|
|
38
|
-
const WARD_SEAL = new TextEncoder().encode('quo-ward-seal');
|
|
39
|
-
|
|
40
|
-
// The one size in Quo: one mebibyte of bytes each way. It is read before
|
|
41
|
-
// anything is opened, so bytes above it are never decrypted, never parsed and
|
|
42
|
-
// never allocated against. Refusing them is bytes that said nothing, which is
|
|
43
|
-
// already silence, so there is no tenth word and no new door case. Breadth
|
|
44
|
-
// costs bytes, so this one number bounds every other breadth on the wire, and
|
|
45
|
-
// what a being holds is not the protocol's business. An ask is a message and
|
|
46
|
-
// not a file; what is larger is asked for in pieces.
|
|
47
|
-
export const SIZE = 1024 * 1024;
|
|
48
|
-
|
|
49
|
-
// The ward's key from its seed. Its pk on the wire is the signing pk then the padlock, 128 hex.
|
|
50
|
-
//
|
|
51
|
-
// One seed, two curves, and each secret derived from it under its own label.
|
|
52
|
-
// Fed the seed straight, the two scalars differ only because Ed25519 hashes
|
|
53
|
-
// what it is given and X25519 clamps it raw, which is an accident of the two
|
|
54
|
-
// designs and no separation at all: one secret would be doing two jobs with
|
|
55
|
-
// nothing said about it, and a second kit would have to reproduce a
|
|
56
|
-
// construction nobody named. HKDF-SHA-256 under a label is the separation
|
|
57
|
-
// said out loud, and it is what `quo/vectors/framing.json` pins.
|
|
58
|
-
//
|
|
59
|
-
// Bytes are key material and text is not. A seed handed in as bytes of the
|
|
60
|
-
// key length is taken as it stands, which is what a harbor mints; anything
|
|
61
|
-
// else, of any length, is hashed to that length first. So a thirty-two
|
|
62
|
-
// character name is a name like any other, and not a key because it happened
|
|
63
|
-
// to be the right size.
|
|
64
|
-
export async function wardKey(seed: string | Uint8Array): Promise<WardKey> {
|
|
65
|
-
const seed32 = typeof seed === 'string' ? await sha256(utf8.encode(seed)) : seed.length === KEY ? seed : await sha256(seed);
|
|
66
|
-
const s = await signingPair(await derive(seed32, WARD_SIGN, KEY));
|
|
67
|
-
const p = await sealingPair(await derive(seed32, WARD_SEAL, KEY));
|
|
68
|
-
return { sign: s.secret, padlock: p.secret, signPk: s.pk, padlockPk: p.pk, pk: hex(s.pk) + hex(p.pk) };
|
|
69
|
-
}
|
|
70
|
-
export const wardSignPk = (pk: string): Uint8Array => unhex(pk.slice(0, KEY * 2));
|
|
71
|
-
export const wardPadlock = (pk: string): Uint8Array => unhex(pk.slice(KEY * 2));
|
|
72
|
-
export const isWardPk = (pk: unknown): pk is string => typeof pk === 'string' && /^[0-9a-f]{128}$/.test(pk);
|
|
73
|
-
// Thirty-two bytes as hex, which is how this kit writes every single key: a
|
|
74
|
-
// heir pk, a heir secret, a being's own key, and a digest, which is the same
|
|
75
|
-
// thirty-two bytes of SHA-256. A ward pk is two of these and has its own
|
|
76
|
-
// shape above. The two are spelled here and nowhere else, so a door, a
|
|
77
|
-
// stance, a harbor and a conformance suite all refuse the same string.
|
|
78
|
-
export const isHex = (v: unknown): v is string => typeof v === 'string' && /^[0-9a-f]{64}$/.test(v);
|
|
79
|
-
|
|
80
|
-
// A being's relation key: a seed, and the pk it signs as.
|
|
81
|
-
export async function beingKey(seed: Uint8Array): Promise<{ seed: string; pk: string }> {
|
|
82
|
-
return { seed: hex(seed), pk: hex((await signingPair(seed)).pk) };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// time is the allowance: what this ask may still spend. It rides inside the
|
|
86
|
-
// signed body, so a budget caught on the road cannot be widened by whoever
|
|
87
|
-
// caught it.
|
|
88
|
-
// hops is reserved and nothing sets it. An ask carries no count of doors
|
|
89
|
-
// today, so a chain of relays is bounded by time alone. A relay chain that
|
|
90
|
-
// ever wants a count needs every door on it to refuse at zero, and a door
|
|
91
|
-
// written after the count was invented cannot make the doors before it
|
|
92
|
-
// enforce anything. So the field is named and refused here now, while it
|
|
93
|
-
// costs two lines: a door of this version already counts as a correct
|
|
94
|
-
// participant in a chain nobody has designed yet.
|
|
95
|
-
export type AskPayload = { by: string; next: string | null; seq: number; time: number; hops?: number; method?: string; args: JsonObject };
|
|
96
|
-
// What is actually sealed: the payload with the heir it is for, or null for
|
|
97
|
-
// the public being. `to` is signed with the rest, so it cannot be moved.
|
|
98
|
-
type Sealed = AskPayload & { to: string | null };
|
|
99
|
-
export type ReplyPayload = { object: Json; seen: string | null } | { silence: true } | { quo: DoorWord };
|
|
100
|
-
|
|
101
|
-
// Seal an ask. Returns the bytes and the ephemeral secret the reply will be sealed to.
|
|
102
|
-
export async function sealAsk(to: string | null, payload: AskPayload, signer: Uint8Array, padlock: Uint8Array, seed: Uint8Array): Promise<{ bytes: Uint8Array; ephemeral: Uint8Array }> {
|
|
103
|
-
const body = utf8.encode(JSON.stringify({ to, ...payload } satisfies Sealed));
|
|
104
|
-
const inside = concat([body, await sign(body, signer)]);
|
|
105
|
-
const { bytes, ephemeral } = await box(inside, padlock, seed);
|
|
106
|
-
return { bytes, ephemeral: ephemeral.secret };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Open an ask at the door. Null when it does not open or does not parse.
|
|
110
|
-
// Verification is the door's own step, because only the door knows which key may speak.
|
|
111
|
-
export async function openAsk(bytes: Uint8Array, padlockSecret: Uint8Array): Promise<{ to: string | null; payload: AskPayload; body: Uint8Array; signature: Uint8Array; ephemeralPk: Uint8Array } | null> {
|
|
112
|
-
try {
|
|
113
|
-
if (!(bytes instanceof Uint8Array) || bytes.length < 1 || bytes.length > SIZE) return null;
|
|
114
|
-
const inside = await unbox(bytes, padlockSecret);
|
|
115
|
-
if (inside.length <= SIGNATURE) return null;
|
|
116
|
-
const body = inside.subarray(0, inside.length - SIGNATURE);
|
|
117
|
-
const signature = inside.subarray(inside.length - SIGNATURE);
|
|
118
|
-
const payload = readJson(body) as Sealed;
|
|
119
|
-
// The heir, or null for the public being. Absent is not null: a payload
|
|
120
|
-
// that never named one is malformed, like any other missing field.
|
|
121
|
-
const to = payload.to;
|
|
122
|
-
if (to !== null && !isHex(to)) return null;
|
|
123
|
-
if (!isHex(payload.by)) return null;
|
|
124
|
-
if (payload.next !== null && !isHex(payload.next)) return null;
|
|
125
|
-
if (payload.method !== undefined && typeof payload.method !== 'string') return null; // a name, or the empty ask. never a number, never an object.
|
|
126
|
-
if (payload.args !== undefined && (payload.args === null || typeof payload.args !== 'object' || Array.isArray(payload.args))) return null; // args are one object, or absent. a string or a list is not an ask.
|
|
127
|
-
if (payload.args !== undefined && cellFault(payload.args, 'args') !== null) return null; // and values all the way down: no key named __proto__, no nesting past the bound
|
|
128
|
-
if (!Number.isSafeInteger(payload.seq) || payload.seq < 1) return null; // her count for this relation. one and up, and a whole number.
|
|
129
|
-
// The allowance is not read here. It is the door's, D2, and the door is
|
|
130
|
-
// its one reader: `spent` in allowance.ts says what a budget is and what
|
|
131
|
-
// is left of it, in one sentence, for a payload that opened like any other.
|
|
132
|
-
// The hop count, if this ask carries one: a whole number, never below
|
|
133
|
-
// zero. Absent is the ordinary ask and always will be.
|
|
134
|
-
if (payload.hops !== undefined && (!Number.isSafeInteger(payload.hops) || payload.hops < 0)) return null;
|
|
135
|
-
return { to, payload, body, signature, ephemeralPk: bytes.subarray(0, KEY) };
|
|
136
|
-
} catch {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
export const verifyAsk = (a: { body: Uint8Array; signature: Uint8Array }, pk: string): Promise<boolean> => verify(a.body, a.signature, unhex(pk));
|
|
141
|
-
|
|
142
|
-
// Seal a reply to the ephemeral pk of the ask it answers, signed by the ward.
|
|
143
|
-
export async function sealReply(reply: ReplyPayload, ephemeralPk: Uint8Array, wardSign: Uint8Array, seed: Uint8Array): Promise<Uint8Array> {
|
|
144
|
-
const body = utf8.encode(JSON.stringify(reply));
|
|
145
|
-
return (await box(concat([body, await sign(body, wardSign)]), ephemeralPk, seed)).bytes;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Open a reply with the ephemeral secret kept from the ask, and verify it
|
|
149
|
-
// came from the ward it was sent to. Null is silence: it did not open, it
|
|
150
|
-
// lied, or it is not a reply. Every ward on the wire is somebody else's code,
|
|
151
|
-
// so the shape is checked here and nowhere later: a reply is `{ silence }`,
|
|
152
|
-
// `{ quo: word }` with a word the door may say, or `{ object, seen }` with an
|
|
153
|
-
// object that is a value and seen a digest or null. A field beside a shape's
|
|
154
|
-
// own makes it none of the three, since a kit that dropped the field would
|
|
155
|
-
// hand her an answer where this one hands her silence.
|
|
156
|
-
// Anything else is bytes that said nothing, and her ward never hands her a
|
|
157
|
-
// throw or an absent value for what a stranger wrote.
|
|
158
|
-
export async function openReply(bytes: unknown, ephemeralSecret: Uint8Array, signPk: Uint8Array): Promise<ReplyPayload | null> {
|
|
159
|
-
try {
|
|
160
|
-
if (!(bytes instanceof Uint8Array) || bytes.length > SIZE) return null;
|
|
161
|
-
const inside = await unbox(bytes, ephemeralSecret);
|
|
162
|
-
if (inside.length <= SIGNATURE) return null;
|
|
163
|
-
const body = inside.subarray(0, inside.length - SIGNATURE);
|
|
164
|
-
if (!(await verify(body, inside.subarray(inside.length - SIGNATURE), signPk))) return null;
|
|
165
|
-
const reply = readJson(body);
|
|
166
|
-
if (reply === null || typeof reply !== 'object' || Array.isArray(reply)) return null;
|
|
167
|
-
const r = reply as Record<string, unknown>;
|
|
168
|
-
const keys = Object.keys(r).sort().join(',');
|
|
169
|
-
if (keys === 'silence') return r.silence === true ? { silence: true } : null;
|
|
170
|
-
if (keys === 'quo') return isDoorWord(r.quo) ? { quo: r.quo } : null;
|
|
171
|
-
if (keys !== 'object,seen') return null;
|
|
172
|
-
if (r.seen !== null && !isHex(r.seen)) return null;
|
|
173
|
-
if (cellFault(r.object, 'object') !== null) return null; // values in both directions: nested past the bound is none of the shapes
|
|
174
|
-
return { object: r.object as Json, seen: r.seen };
|
|
175
|
-
} catch {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
}
|