@quo-systems/quo 0.2.9 → 0.2.11
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 +19 -10
- package/SPEC.md +301 -110
- package/dist/being/being.d.ts +2 -2
- package/dist/being/being.js +34 -12
- package/dist/being/digest.js +26 -8
- package/dist/being/index.d.ts +2 -2
- package/dist/being/index.js +2 -2
- package/dist/being/silence.d.ts +2 -0
- package/dist/being/silence.js +12 -0
- package/dist/being/types.d.ts +4 -2
- package/dist/being/types.js +25 -0
- package/dist/conformance/assert.js +40 -6
- package/dist/conformance/beings.d.ts +48 -5
- package/dist/conformance/beings.js +39 -8
- package/dist/conformance/estate.js +110 -21
- package/dist/conformance/index.d.ts +5 -2
- package/dist/conformance/index.js +165 -7
- package/dist/harbor/core.d.ts +5 -2
- package/dist/harbor/core.js +195 -45
- package/dist/harbor/dial.js +32 -15
- package/dist/harbor/index.d.ts +1 -0
- package/dist/harbor/index.js +3 -0
- package/dist/harbor/memory.d.ts +3 -3
- package/dist/harbor/memory.js +7 -12
- package/dist/harbor/reach.js +42 -17
- package/dist/ward/allowance.js +15 -4
- package/dist/ward/arithmetic.d.ts +1 -0
- package/dist/ward/arithmetic.js +22 -6
- package/dist/ward/cells.d.ts +3 -1
- package/dist/ward/cells.js +79 -21
- package/dist/ward/door.d.ts +3 -2
- package/dist/ward/door.js +38 -10
- package/dist/ward/ground.d.ts +5 -1
- package/dist/ward/ground.js +38 -1
- package/dist/ward/heirs.d.ts +2 -3
- package/dist/ward/heirs.js +19 -13
- package/dist/ward/index.d.ts +1 -0
- package/dist/ward/index.js +3 -0
- package/dist/ward/owner.d.ts +6 -27
- package/dist/ward/owner.js +59 -33
- package/dist/ward/partition.d.ts +3 -0
- package/dist/ward/partition.js +109 -4
- package/dist/ward/seal.d.ts +1 -0
- package/dist/ward/seal.js +41 -13
- package/dist/ward/stance.d.ts +7 -3
- package/dist/ward/stance.js +156 -66
- package/dist/ward/ward.d.ts +10 -0
- package/dist/ward/ward.js +123 -51
- package/package.json +4 -2
- package/src/being/being.ts +33 -11
- package/src/being/digest.ts +28 -13
- package/src/being/index.ts +2 -2
- package/src/being/silence.ts +14 -0
- package/src/being/types.ts +39 -5
- package/src/conformance/assert.ts +37 -4
- package/src/conformance/beings.ts +41 -10
- package/src/conformance/estate.ts +107 -20
- package/src/conformance/index.ts +188 -13
- package/src/harbor/core.ts +203 -46
- package/src/harbor/dial.ts +46 -17
- package/src/harbor/index.ts +3 -0
- package/src/harbor/memory.ts +8 -13
- package/src/harbor/reach.ts +47 -21
- package/src/ward/allowance.ts +15 -4
- package/src/ward/arithmetic.ts +25 -8
- package/src/ward/cells.ts +76 -25
- package/src/ward/door.ts +38 -12
- package/src/ward/ground.ts +52 -3
- package/src/ward/heirs.ts +19 -13
- package/src/ward/index.ts +3 -0
- package/src/ward/owner.ts +65 -46
- package/src/ward/partition.ts +109 -5
- package/src/ward/seal.ts +41 -12
- package/src/ward/stance.ts +163 -64
- package/src/ward/ward.ts +124 -52
- package/vectors/arithmetic.json +7 -0
- package/vectors/framing.json +30 -15
- package/vectors/wire.json +4 -4
package/dist/ward/allowance.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// is held to the ward's ceiling. The door reads it before anything is done
|
|
6
6
|
// under it and refuses a budget already gone, as one silence like every other
|
|
7
7
|
// refusal there. The sender bounds the whole of her ask to the same number,
|
|
8
|
-
// and a wait that ran out is
|
|
8
|
+
// and a wait that ran out is the word late, never silence and never unreached.
|
|
9
9
|
//
|
|
10
10
|
// Each ask is bounded on its own. The time an arriving call has left does not
|
|
11
11
|
// bound the asks a being makes while answering it: attributing her onward ask
|
|
@@ -24,11 +24,22 @@ export const CEILING = { time: 300_000 };
|
|
|
24
24
|
// is thirty seconds, silently, because the ceiling is not hers to know.
|
|
25
25
|
// Asking for nothing at all is the default, which is the whole point.
|
|
26
26
|
export function allow(wanted, ceiling = CEILING, base = DEFAULT) {
|
|
27
|
-
|
|
27
|
+
// Floored first, then tested: a fraction of a millisecond is not a budget,
|
|
28
|
+
// and falls to the default like every other number that is not a positive
|
|
29
|
+
// whole one.
|
|
30
|
+
const whole = (n, fallback) => {
|
|
31
|
+
const w = typeof n === 'number' ? Math.floor(n) : NaN;
|
|
32
|
+
return Number.isFinite(w) && w > 0 ? w : fallback;
|
|
33
|
+
};
|
|
28
34
|
return { time: Math.min(whole(wanted?.time, base.time), ceiling.time) };
|
|
29
35
|
}
|
|
30
|
-
// Whether a budget has anything left to spend
|
|
31
|
-
|
|
36
|
+
// Whether a budget has anything left to spend, and the one reader of it: the
|
|
37
|
+
// door calls this on arrival and nothing else asks the question anywhere. A
|
|
38
|
+
// time that is not a whole number above zero is not a budget that ran out, it
|
|
39
|
+
// is no budget at all, and the door refuses both the same way, since a peer
|
|
40
|
+
// learns nothing from the difference. `allow` never makes one, so only bytes
|
|
41
|
+
// off the road ever carry one.
|
|
42
|
+
export const spent = (a) => !(Number.isSafeInteger(a.time) && a.time > 0);
|
|
32
43
|
// What a wait that ran out comes back as. Its own value, held by nobody
|
|
33
44
|
// outside the ward, so no answer from any door can be mistaken for it.
|
|
34
45
|
export const LATE = Symbol('late');
|
|
@@ -17,6 +17,7 @@ export declare function sealingPair(seed: Uint8Array): Promise<Pair>;
|
|
|
17
17
|
export declare function sign(message: Uint8Array, secret: Uint8Array): Promise<Uint8Array>;
|
|
18
18
|
export declare function verify(message: Uint8Array, signature: Uint8Array, pk: Uint8Array): Promise<boolean>;
|
|
19
19
|
export declare function agree(secret: Uint8Array, peerPk: Uint8Array): Promise<Uint8Array>;
|
|
20
|
+
export declare function derive(secret: Uint8Array, label: Uint8Array, bytes: number): Promise<Uint8Array>;
|
|
20
21
|
export declare function encrypt(shared: Uint8Array, plaintext: Uint8Array, aad: Uint8Array): Promise<Uint8Array>;
|
|
21
22
|
export declare function decrypt(shared: Uint8Array, ciphertext: Uint8Array, aad: Uint8Array): Promise<Uint8Array>;
|
|
22
23
|
export declare function box(inside: Uint8Array, padlock: Uint8Array, seed: Uint8Array): Promise<{
|
package/dist/ward/arithmetic.js
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
// runs on. `test/floor.test.ts` names the floor and probes for it. Subtle is
|
|
9
9
|
// asynchronous, so everything here is.
|
|
10
10
|
//
|
|
11
|
-
// Ported from an earlier kit's arithmetic. Same bytes, same vectors.
|
|
12
11
|
// `crypto.subtle` is read at every use and never captured at load. A browser
|
|
13
12
|
// on a plain http:// origin has `crypto` without `subtle`, and a terrain may
|
|
14
13
|
// install one after this module is first imported; a reference taken here
|
|
@@ -24,7 +23,7 @@ export const SIGNATURE = 64;
|
|
|
24
23
|
export const NONCE = 12;
|
|
25
24
|
export const TAG = 16;
|
|
26
25
|
const SEAL_INFO = new TextEncoder().encode('quo-seal');
|
|
27
|
-
const
|
|
26
|
+
const SALT = new Uint8Array(0);
|
|
28
27
|
// A 32-byte secret plus a fixed prefix is the whole PKCS#8 wrapping for both curves.
|
|
29
28
|
const ED_SECRET = unhex('302e020100300506032b657004220420');
|
|
30
29
|
const X_SECRET = unhex('302e020100300506032b656e04220420');
|
|
@@ -60,7 +59,12 @@ export function sameBytes(a, b) {
|
|
|
60
59
|
diff |= a[at] ^ b[at];
|
|
61
60
|
return diff === 0;
|
|
62
61
|
}
|
|
63
|
-
// The eight small-order points
|
|
62
|
+
// The eight small-order points of Ed25519, in their canonical encoding. A
|
|
63
|
+
// public key among them verifies nothing. An encoding whose y coordinate is
|
|
64
|
+
// not reduced, y at or above the field's prime p, names one of the same
|
|
65
|
+
// points under another spelling, and a terrain's verify may accept a zero
|
|
66
|
+
// signature under it; the field has room for the spelling, so it is refused
|
|
67
|
+
// before the list is read.
|
|
64
68
|
const SMALL_ORDER = [
|
|
65
69
|
'0100000000000000000000000000000000000000000000000000000000000000',
|
|
66
70
|
'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
|
|
@@ -71,7 +75,11 @@ const SMALL_ORDER = [
|
|
|
71
75
|
'26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85',
|
|
72
76
|
'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa',
|
|
73
77
|
].map(unhex);
|
|
74
|
-
|
|
78
|
+
// y >= p, read little-endian with the sign bit masked off: the top byte is
|
|
79
|
+
// 0x7f under the mask, every middle byte is 0xff, and the low byte is at
|
|
80
|
+
// least 0xed, which is p's.
|
|
81
|
+
const unreduced = (pk) => (pk[31] & 0x7f) === 0x7f && pk[0] >= 0xed && pk.subarray(1, 31).every((b) => b === 0xff);
|
|
82
|
+
export const smallOrder = (pk) => unreduced(pk) || SMALL_ORDER.some((p) => sameBytes(p, pk));
|
|
75
83
|
const key32 = (value, what) => {
|
|
76
84
|
if (!(value instanceof Uint8Array) || value.length !== KEY)
|
|
77
85
|
throw new Error(`${what} is not a 32-byte key`);
|
|
@@ -124,11 +132,19 @@ export async function agree(secret, peerPk) {
|
|
|
124
132
|
throw new Error('dead agreement'); // a padlock that was not a real key
|
|
125
133
|
return shared;
|
|
126
134
|
}
|
|
135
|
+
// HKDF-SHA-256: bytes from a secret, under a label that says what they are
|
|
136
|
+
// for. No salt, because the secret handed in is already a secret of full
|
|
137
|
+
// strength and the label is what keeps one use apart from another. Every
|
|
138
|
+
// label in this kit is spelled where it is used, never here: this is
|
|
139
|
+
// arithmetic and a label is a decision.
|
|
140
|
+
export async function derive(secret, label, bytes) {
|
|
141
|
+
const material = await subtle().importKey('raw', secret, 'HKDF', false, ['deriveBits']);
|
|
142
|
+
return new Uint8Array(await subtle().deriveBits({ name: 'HKDF', hash: 'SHA-256', salt: SALT, info: label }, material, bytes * 8));
|
|
143
|
+
}
|
|
127
144
|
// One HKDF-SHA-256 yields the AES key and the nonce together. The nonce needs
|
|
128
145
|
// no randomness of its own: the key it pairs with is fresh on every message.
|
|
129
146
|
async function cipherKey(shared, use) {
|
|
130
|
-
const
|
|
131
|
-
const out = new Uint8Array(await subtle().deriveBits({ name: 'HKDF', hash: 'SHA-256', salt: SEAL_SALT, info: SEAL_INFO }, material, (KEY + NONCE) * 8));
|
|
147
|
+
const out = await derive(shared, SEAL_INFO, KEY + NONCE);
|
|
132
148
|
return { key: await subtle().importKey('raw', out.subarray(0, KEY), 'AES-GCM', false, [use]), nonce: out.subarray(KEY) };
|
|
133
149
|
}
|
|
134
150
|
// The additional authenticated data is the ephemeral public key: the one thing outside the seal, bound to it.
|
package/dist/ward/cells.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { Cells } from '../being/types.ts';
|
|
2
|
+
export declare const DEPTH = 64;
|
|
2
3
|
export declare const cellFault: (v: unknown, path: string) => string | null;
|
|
3
|
-
export declare const guardCells: (cells: Cells) => Cells;
|
|
4
|
+
export declare const guardCells: (cells: Cells, wrote?: () => void) => Cells;
|
|
5
|
+
export declare const unguarded: (cells: Cells) => Cells;
|
package/dist/ward/cells.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
// How deep a value may nest. Every walk over a value in the kit recurses,
|
|
2
|
+
// and a peer chooses the depth of what she answers; a bound keeps a hostile
|
|
3
|
+
// reply from ending a walk in a stack overflow instead of a refusal.
|
|
4
|
+
export const DEPTH = 64;
|
|
1
5
|
// I-JSON, all the way down. A number that JSON cannot write is not a number
|
|
2
|
-
// a harbor can keep,
|
|
3
|
-
|
|
6
|
+
// a harbor can keep, a key on the prototype is not a key she wrote, and a
|
|
7
|
+
// hole in a list, or a key named `__proto__`, is a thing JSON writes one way
|
|
8
|
+
// and a runtime reads another.
|
|
9
|
+
function fault(v, path, seen, depth) {
|
|
4
10
|
if (v === null || typeof v === 'boolean' || typeof v === 'string')
|
|
5
11
|
return null;
|
|
6
12
|
if (typeof v === 'number')
|
|
@@ -9,11 +15,15 @@ function fault(v, path, seen) {
|
|
|
9
15
|
return `${path} is a ${typeof v}, which is not a value`;
|
|
10
16
|
if (seen.has(v))
|
|
11
17
|
return `${path} refers back to itself`;
|
|
18
|
+
if (depth >= DEPTH)
|
|
19
|
+
return `${path} is nested past ${DEPTH} levels, which no harbor can keep`;
|
|
12
20
|
seen.add(v);
|
|
13
21
|
try {
|
|
14
22
|
if (Array.isArray(v)) {
|
|
15
23
|
for (let i = 0; i < v.length; i += 1) {
|
|
16
|
-
|
|
24
|
+
if (!(i in v))
|
|
25
|
+
return `${path}[${i}] is a hole, which no harbor can write down`;
|
|
26
|
+
const f = fault(v[i], `${path}[${i}]`, seen, depth + 1);
|
|
17
27
|
if (f)
|
|
18
28
|
return f;
|
|
19
29
|
}
|
|
@@ -23,7 +33,9 @@ function fault(v, path, seen) {
|
|
|
23
33
|
if (proto !== Object.prototype && proto !== null)
|
|
24
34
|
return `${path} is a ${(v).constructor?.name ?? 'object'}, which is not a value`;
|
|
25
35
|
for (const k of Object.keys(v)) {
|
|
26
|
-
|
|
36
|
+
if (k === '__proto__')
|
|
37
|
+
return `${path}.__proto__ is a key no harbor can keep`;
|
|
38
|
+
const f = fault(v[k], `${path}.${k}`, seen, depth + 1);
|
|
27
39
|
if (f)
|
|
28
40
|
return f;
|
|
29
41
|
}
|
|
@@ -33,14 +45,44 @@ function fault(v, path, seen) {
|
|
|
33
45
|
seen.delete(v);
|
|
34
46
|
}
|
|
35
47
|
}
|
|
36
|
-
export const cellFault = (v, path) => fault(v, path, new Set());
|
|
48
|
+
export const cellFault = (v, path) => fault(v, path, new Set(), 0);
|
|
49
|
+
// The three keys at the root of her cells that are the ward's: it writes
|
|
50
|
+
// them, she reads them, and a write of hers there is refused like a non-value.
|
|
51
|
+
const WARDS = new Set(['standings', 'occupants', 'class']);
|
|
37
52
|
// The guard is one proxy at the root and one for every container read through
|
|
38
53
|
// it, so a write nested three deep is refused the same way a write at the top
|
|
39
54
|
// is. Wrappers are remembered, so reading the same array twice is the same
|
|
40
55
|
// object twice and a being may still compare what she holds.
|
|
41
56
|
const wrapped = new WeakMap();
|
|
57
|
+
const targets = new WeakMap();
|
|
42
58
|
const guards = new WeakSet();
|
|
43
|
-
|
|
59
|
+
// A key a write may land on: a string that is not `__proto__`, and not one
|
|
60
|
+
// of the ward's at the root. A symbol key is a thing JSON never writes.
|
|
61
|
+
const refuse = (why) => {
|
|
62
|
+
throw new TypeError(`cells hold values: ${why}`);
|
|
63
|
+
};
|
|
64
|
+
function keyFault(t, k, v, path, root) {
|
|
65
|
+
if (typeof k !== 'string')
|
|
66
|
+
refuse(`${path} takes no symbol key`);
|
|
67
|
+
if (k === '__proto__')
|
|
68
|
+
refuse(`${path}.__proto__ is a key no harbor can keep`);
|
|
69
|
+
if (root && WARDS.has(k))
|
|
70
|
+
refuse(`${path}.${k} is the ward's to write`);
|
|
71
|
+
// A list grows by one at its end, or it has holes JSON cannot write. A
|
|
72
|
+
// push sets the slot at its length and then the length: both pass. A
|
|
73
|
+
// length set past what she wrote, or a slot beyond it, would leave holes.
|
|
74
|
+
if (Array.isArray(t)) {
|
|
75
|
+
if (k === 'length') {
|
|
76
|
+
if (typeof v !== 'number' || v > t.length)
|
|
77
|
+
refuse(`${path}.length set past what she wrote would leave holes`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const i = Number(k);
|
|
81
|
+
if (Number.isInteger(i) && i > t.length)
|
|
82
|
+
refuse(`${path}[${i}] would leave a hole`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function guard(target, path, wrote, root = false) {
|
|
44
86
|
if (guards.has(target))
|
|
45
87
|
return target;
|
|
46
88
|
const had = wrapped.get(target);
|
|
@@ -50,30 +92,46 @@ function guard(target, path) {
|
|
|
50
92
|
get(t, k, r) {
|
|
51
93
|
const v = Reflect.get(t, k, r);
|
|
52
94
|
// A container reached through her cells is part of her cells.
|
|
53
|
-
return v !== null && typeof v === 'object' && !ArrayBuffer.isView(v) ? guard(v, `${path}.${String(k)}
|
|
95
|
+
return v !== null && typeof v === 'object' && !ArrayBuffer.isView(v) ? guard(v, `${path}.${String(k)}`, wrote) : v;
|
|
54
96
|
},
|
|
55
97
|
set(t, k, v, r) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
98
|
+
keyFault(t, k, v, path, root);
|
|
99
|
+
const f = cellFault(v, `${path}.${String(k)}`);
|
|
100
|
+
if (f)
|
|
101
|
+
refuse(f);
|
|
102
|
+
const ok = Reflect.set(t, k, v, r);
|
|
103
|
+
wrote();
|
|
104
|
+
return ok;
|
|
62
105
|
},
|
|
63
106
|
defineProperty(t, k, d) {
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
107
|
+
if (!('value' in d))
|
|
108
|
+
refuse(`${path}.${String(k)} is an accessor, which no harbor can keep`);
|
|
109
|
+
keyFault(t, k, d.value, path, root);
|
|
110
|
+
const f = cellFault(d.value, `${path}.${String(k)}`);
|
|
111
|
+
if (f)
|
|
112
|
+
refuse(f);
|
|
113
|
+
const ok = Reflect.defineProperty(t, k, d);
|
|
114
|
+
wrote();
|
|
115
|
+
return ok;
|
|
116
|
+
},
|
|
117
|
+
deleteProperty(t, k) {
|
|
118
|
+
if (root && typeof k === 'string' && WARDS.has(k))
|
|
119
|
+
refuse(`${path}.${k} is the ward's to keep`);
|
|
120
|
+
const ok = Reflect.deleteProperty(t, k);
|
|
121
|
+
wrote();
|
|
122
|
+
return ok;
|
|
70
123
|
},
|
|
71
124
|
});
|
|
72
125
|
wrapped.set(target, p);
|
|
126
|
+
targets.set(p, target);
|
|
73
127
|
guards.add(p);
|
|
74
128
|
return p;
|
|
75
129
|
}
|
|
76
130
|
// Her cells, guarded. Called once per being at boot; the guarded object is
|
|
77
131
|
// what goes into the partition and what the stance hands her, so there is no
|
|
78
|
-
// second door onto the same cells.
|
|
79
|
-
|
|
132
|
+
// second door onto the same cells. Every write that passes says so to the
|
|
133
|
+
// ward, which says so to its harbor.
|
|
134
|
+
export const guardCells = (cells, wrote = () => { }) => guard(cells, 'cells', wrote, true);
|
|
135
|
+
// The cells behind the guard, for the ward alone: the one writer of the
|
|
136
|
+
// three keys that are its own.
|
|
137
|
+
export const unguarded = (cells) => (targets.get(cells) ?? cells);
|
package/dist/ward/door.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { Asker, BeingLike, JsonObject } from '../being/types.ts';
|
|
1
|
+
import type { Asker, BeingLike, JsonObject, OccupantRecord } from '../being/types.ts';
|
|
2
2
|
import type { Heirs } from './heirs.ts';
|
|
3
3
|
import { type ReplyPayload, type WardKey } from './seal.ts';
|
|
4
4
|
export type Door = {
|
|
5
5
|
key: string;
|
|
6
6
|
being: BeingLike;
|
|
7
7
|
cells: {
|
|
8
|
-
occupants: Record<string,
|
|
8
|
+
occupants: Record<string, OccupantRecord>;
|
|
9
9
|
};
|
|
10
10
|
};
|
|
11
11
|
export type Judged = {
|
|
@@ -13,4 +13,5 @@ export type Judged = {
|
|
|
13
13
|
heard: boolean;
|
|
14
14
|
};
|
|
15
15
|
export declare function arrive(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload>;
|
|
16
|
+
export declare function seen(door: Door, asker: Asker): Promise<string | null>;
|
|
16
17
|
export declare function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, publicKey: () => string | null, random: (n: number) => Uint8Array): (bytes: Uint8Array) => Promise<Judged>;
|
package/dist/ward/door.js
CHANGED
|
@@ -13,6 +13,7 @@ import { at } from './partition.js';
|
|
|
13
13
|
import { openAsk, sealReply, verifyAsk } from './seal.js';
|
|
14
14
|
import { spent } from './allowance.js';
|
|
15
15
|
import { KEY, sealingPair } from './arithmetic.js';
|
|
16
|
+
import { cellFault } from './cells.js';
|
|
16
17
|
const SILENCE = { silence: true };
|
|
17
18
|
const said = (quo) => ({ quo });
|
|
18
19
|
// One arrival at one being, already named. Catches every throw. `bound` says
|
|
@@ -34,13 +35,22 @@ export async function arrive(door, asker, method, args, bound) {
|
|
|
34
35
|
return SILENCE;
|
|
35
36
|
if (isWord(out))
|
|
36
37
|
return threw; // a word is the ward's to say, never hers
|
|
38
|
+
// Her answer is held to the rule her args and her cells are held to. A
|
|
39
|
+
// shape JSON would drop or rewrite on the way out, a Date, a Map, a NaN, a
|
|
40
|
+
// cycle, is not hers to make: the far side would read something she never
|
|
41
|
+
// said, or the door itself would fail to write her reply after the number
|
|
42
|
+
// was spent. It is threw, like a word out of her.
|
|
43
|
+
if (cellFault(out, 'answer') !== null)
|
|
44
|
+
return threw;
|
|
37
45
|
if (method === undefined)
|
|
38
46
|
return { object: out, seen: null };
|
|
39
47
|
// The digest rides along, it is not the answer. She has already answered:
|
|
40
48
|
// a describe that will not run costs the digest, and nothing else.
|
|
41
49
|
return { object: out, seen: await seen(door, asker) };
|
|
42
50
|
}
|
|
43
|
-
|
|
51
|
+
// Her digest for this asker, or null when her describe threw or fell silent.
|
|
52
|
+
// The owner's describe reads it the same way for every being of the ward.
|
|
53
|
+
export async function seen(door, asker) {
|
|
44
54
|
try {
|
|
45
55
|
const bp = await door.being.answer(asker);
|
|
46
56
|
return isSilence(bp) || isWord(bp) ? null : await digest(bp);
|
|
@@ -56,9 +66,10 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
|
|
|
56
66
|
return null; // D1. it did not open. there is nobody to answer.
|
|
57
67
|
const { to, payload, ephemeralPk } = a;
|
|
58
68
|
const refuse = () => ({ reply: SILENCE, ephemeralPk, heard: false });
|
|
59
|
-
// D2
|
|
60
|
-
//
|
|
61
|
-
//
|
|
69
|
+
// D2. The allowance is read here, before anything is done under it, and
|
|
70
|
+
// here only: a time that is not a whole number above zero is malformed and
|
|
71
|
+
// is refused as one. Hops at zero is refused beside it, so that a relay
|
|
72
|
+
// chain invented later meets doors that already stop it. Nothing sets hops.
|
|
62
73
|
if (spent({ time: payload.time }) || payload.hops === 0)
|
|
63
74
|
return refuse();
|
|
64
75
|
const args = payload.args ?? {};
|
|
@@ -70,7 +81,10 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
|
|
|
70
81
|
const pub = pk !== null ? doors.get(pk) : undefined;
|
|
71
82
|
if (!pub || !(await verifyAsk(a, payload.by)))
|
|
72
83
|
return refuse();
|
|
73
|
-
|
|
84
|
+
// She answers, and the bit stays false: no key this door holds spoke.
|
|
85
|
+
// The public being is the one place a stranger is answered by design,
|
|
86
|
+
// so it is the one place a harbor must still be able to rate her.
|
|
87
|
+
return { reply: await arrive(pub, {}, payload.method, args, false), ephemeralPk, heard: false };
|
|
74
88
|
}
|
|
75
89
|
const h = heirs.admits(to, payload.by);
|
|
76
90
|
if (!h) {
|
|
@@ -83,6 +97,13 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
|
|
|
83
97
|
}
|
|
84
98
|
if (!(await verifyAsk(a, payload.by)))
|
|
85
99
|
return refuse(); // D7, under an admitted key
|
|
100
|
+
// The signature took time, and another arrival on this heir may have been
|
|
101
|
+
// honoured meanwhile: a knock that raced this one and won spent the heir,
|
|
102
|
+
// and the key this ask speaks under may not be admitted any more. The
|
|
103
|
+
// door judges concurrently, so admission is read again now that writing
|
|
104
|
+
// is next, and what changed under the await is judged as it stands.
|
|
105
|
+
if (!heirs.admits(to, payload.by))
|
|
106
|
+
return heirs.gone(to, payload.by) ? { reply: said('removed'), ephemeralPk, heard: true } : refuse();
|
|
86
107
|
// From here the door has heard a key it holds. Every answer below is
|
|
87
108
|
// sealed to that key's lid; nothing below is a stranger's.
|
|
88
109
|
const door = doors.get(h.being);
|
|
@@ -102,18 +123,25 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
|
|
|
102
123
|
// reply is sealed to the ephemeral pk on its lid, which is all a stranger
|
|
103
124
|
// holds, and says silence.
|
|
104
125
|
//
|
|
105
|
-
// A lid that is not a key
|
|
106
|
-
// have several each
|
|
107
|
-
// door never throws: that reply is noise, sealed to a key
|
|
126
|
+
// A lid that is not a key, a small-order point, of which the two curves
|
|
127
|
+
// have several each, makes a dead agreement, and the seal refuses it. The
|
|
128
|
+
// door never throws: that reply is noise, a plain silence sealed to a key
|
|
129
|
+
// nobody holds, and the same is written for anything else the seal will
|
|
130
|
+
// not take.
|
|
131
|
+
// `judge` reads rows it does not check: open() reads the partition's shape
|
|
132
|
+
// at birth, so a row it acts on is the shape it expects. That is the first
|
|
133
|
+
// line and this is the second, because "the door never throws" is a promise
|
|
134
|
+
// to the harbor, which has nobody to hand a rejection to and would count it
|
|
135
|
+
// as no answer at all. Anything unforeseen is the silence a stranger hears.
|
|
108
136
|
return async function door(bytes) {
|
|
109
|
-
const out = await judge(bytes);
|
|
137
|
+
const out = await judge(bytes).catch(() => null);
|
|
110
138
|
const reply = out?.reply ?? SILENCE;
|
|
111
139
|
const heard = out?.heard ?? false;
|
|
112
140
|
try {
|
|
113
141
|
return { bytes: await sealReply(reply, out?.ephemeralPk ?? lid(bytes, random), key.sign, random(32)), heard };
|
|
114
142
|
}
|
|
115
143
|
catch {
|
|
116
|
-
return { bytes: await sealReply(
|
|
144
|
+
return { bytes: await sealReply(SILENCE, (await sealingPair(random(32))).pk, key.sign, random(32)), heard };
|
|
117
145
|
}
|
|
118
146
|
};
|
|
119
147
|
}
|
package/dist/ward/ground.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { Stance, BeingLike } from '../being/types.ts';
|
|
1
|
+
import type { Stance, BeingLike, BeingClass } from '../being/types.ts';
|
|
2
2
|
export type Ground = {
|
|
3
3
|
seed: string | Uint8Array;
|
|
4
4
|
memory: Record<string, unknown>;
|
|
5
5
|
instantiate(className: string, stance: Stance): BeingLike | null;
|
|
6
|
+
wrote?: () => void;
|
|
6
7
|
carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
|
|
7
8
|
random(n: number): Uint8Array;
|
|
8
9
|
};
|
|
@@ -13,3 +14,6 @@ export type WardPointers = {
|
|
|
13
14
|
}>;
|
|
14
15
|
ask(method?: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
15
16
|
};
|
|
17
|
+
export declare const maker: (objects: WeakMap<object, BeingLike>, ...registries: (Record<string, BeingClass> | undefined)[]) => Ground['instantiate'];
|
|
18
|
+
export declare const entropy: (n: number) => Uint8Array;
|
|
19
|
+
export declare function learnPk(w: WardPointers): Promise<string>;
|
package/dist/ward/ground.js
CHANGED
|
@@ -1 +1,38 @@
|
|
|
1
|
-
|
|
1
|
+
// ---- what every harbor builds a ground out of. Three pieces, because every
|
|
2
|
+
// harbor in this tree writes the same three and the spec keeps the harbors
|
|
3
|
+
// themselves apart: what a memory harbor and a real one differ on is the
|
|
4
|
+
// route and the store, and nothing here. A second kit writes its own harbor
|
|
5
|
+
// and may write these again; they are convenience, never contract.
|
|
6
|
+
// The code half of a ground, and the map back to what it made. A class is
|
|
7
|
+
// found by own key only, since `constructor` is a name Object lends every
|
|
8
|
+
// registry, and the first registry holding the name wins, so a ward's own
|
|
9
|
+
// classes stand in front of the harbor's. The object is remembered by the
|
|
10
|
+
// cells the ward handed in, which is how a side reaches a being it made and
|
|
11
|
+
// how that hand follows her out when she is unbooted.
|
|
12
|
+
export const maker = (objects, ...registries) => (className, stance) => {
|
|
13
|
+
const r = registries.find((reg) => reg !== undefined && Object.hasOwn(reg, className));
|
|
14
|
+
const C = r?.[className];
|
|
15
|
+
if (!C)
|
|
16
|
+
return null;
|
|
17
|
+
const obj = new C(stance);
|
|
18
|
+
objects.set(stance.cells, obj);
|
|
19
|
+
return obj;
|
|
20
|
+
};
|
|
21
|
+
// Entropy, from the one place every terrain that runs Quo has it. Every key a
|
|
22
|
+
// ward mints is drawn from this, so a harbor that wants another source hands
|
|
23
|
+
// its own and nothing here has to know.
|
|
24
|
+
export const entropy = (n) => globalThis.crypto.getRandomValues(new Uint8Array(n));
|
|
25
|
+
// A ward's pk, learned the way anyone learns anything: by asking. The empty
|
|
26
|
+
// ask on the ask pointer is the ward's own describe and its notes carry the
|
|
27
|
+
// pk. A harbor has no other way to it and wants none: the ward mints it from
|
|
28
|
+
// the seed, and a harbor that read it off the seed itself would be a second
|
|
29
|
+
// derivation to keep in step with the first. A ward that answers anything
|
|
30
|
+
// else is not one this harbor can route to, and says so here rather than
|
|
31
|
+
// leaving an undefined pk in a directory.
|
|
32
|
+
export async function learnPk(w) {
|
|
33
|
+
const notes = (await w.ask());
|
|
34
|
+
const pk = notes?.notes?.pk;
|
|
35
|
+
if (typeof pk !== 'string')
|
|
36
|
+
throw new Error('the ward did not say its pk');
|
|
37
|
+
return pk;
|
|
38
|
+
}
|
package/dist/ward/heirs.d.ts
CHANGED
|
@@ -2,13 +2,12 @@ import type { DoorWord } from '../being/types.ts';
|
|
|
2
2
|
import { type Heir, type Partition } from './partition.ts';
|
|
3
3
|
export declare class Heirs {
|
|
4
4
|
#private;
|
|
5
|
-
constructor(p: Partition);
|
|
5
|
+
constructor(p: Partition, wrote?: () => void);
|
|
6
6
|
open(heir: string, being: string, id: string): void;
|
|
7
7
|
close(heir: string): void;
|
|
8
8
|
gone(heir: string, by: string): boolean;
|
|
9
|
-
get(heir: string): Heir | undefined;
|
|
10
9
|
admits(heir: string, by: string): Heir | null;
|
|
11
10
|
spend(h: Heir, seq: number): boolean;
|
|
12
11
|
honour(h: Heir, by: string, next: string | null, seq: number): true | DoorWord;
|
|
13
|
-
settle(h: Heir, by: string, next: string | null):
|
|
12
|
+
settle(h: Heir, by: string, next: string | null): void;
|
|
14
13
|
}
|
package/dist/ward/heirs.js
CHANGED
|
@@ -4,11 +4,14 @@ import { GONE } from './partition.js';
|
|
|
4
4
|
const SPAN = 64;
|
|
5
5
|
export class Heirs {
|
|
6
6
|
#p;
|
|
7
|
-
|
|
7
|
+
#wrote;
|
|
8
|
+
constructor(p, wrote = () => { }) {
|
|
8
9
|
this.#p = p;
|
|
10
|
+
this.#wrote = wrote;
|
|
9
11
|
}
|
|
10
12
|
open(heir, being, id) {
|
|
11
13
|
this.#p.heirs[heir] = { being, id, current: heir, announced: null, fresh: true, mark: 0, spent: [] };
|
|
14
|
+
this.#wrote();
|
|
12
15
|
}
|
|
13
16
|
// The id was removed. The heir is forgotten, and its last keys are kept
|
|
14
17
|
// apart, bounded, so that whoever still holds them hears `removed` at the
|
|
@@ -22,6 +25,7 @@ export class Heirs {
|
|
|
22
25
|
const keys = Object.keys(this.#p.gone);
|
|
23
26
|
for (const old of keys.slice(0, Math.max(0, keys.length - GONE)))
|
|
24
27
|
delete this.#p.gone[old];
|
|
28
|
+
this.#wrote();
|
|
25
29
|
}
|
|
26
30
|
// May `by` speak for a relation she removed? True only for the keys the
|
|
27
31
|
// door held when the id went, which nobody but their holder has.
|
|
@@ -29,9 +33,6 @@ export class Heirs {
|
|
|
29
33
|
const g = this.#p.gone[heir];
|
|
30
34
|
return !!g && (by === g.current || by === g.announced);
|
|
31
35
|
}
|
|
32
|
-
get(heir) {
|
|
33
|
-
return this.#p.heirs[heir];
|
|
34
|
-
}
|
|
35
36
|
// May `by` speak for this heir? Returns the record if so, null if not.
|
|
36
37
|
// Does not write: the caller verifies the signature first, then settles.
|
|
37
38
|
admits(heir, by) {
|
|
@@ -75,28 +76,33 @@ export class Heirs {
|
|
|
75
76
|
// neither: a call that binds nothing must not burn a number on its way to
|
|
76
77
|
// being refused, or a stranger who cannot be heard would still leave a mark
|
|
77
78
|
// behind her. Every write below this line is one that is going to hold.
|
|
79
|
+
// This is where a knock is judged and where `unannounced` is said. settle
|
|
80
|
+
// below is the writing alone, and it judges nothing: one owner for the
|
|
81
|
+
// question, one for the answer.
|
|
78
82
|
honour(h, by, next, seq) {
|
|
79
|
-
if (h.fresh && next === null)
|
|
80
|
-
return 'unannounced'; // a knock without a key of her own binds nothing
|
|
83
|
+
if (h.fresh && (next === null || next === h.current))
|
|
84
|
+
return 'unannounced'; // a knock without a key of her own binds nothing, and the heir is not a key of her own
|
|
81
85
|
if (!this.spend(h, seq))
|
|
82
86
|
return 'repeated';
|
|
83
|
-
|
|
87
|
+
this.settle(h, by, next);
|
|
88
|
+
this.#wrote(); // the number and the keys moved together
|
|
89
|
+
return true;
|
|
84
90
|
}
|
|
85
|
-
// The signature checked out. Settle the keys: a fresh
|
|
86
|
-
// to what it announced
|
|
87
|
-
//
|
|
91
|
+
// The signature checked out and honour has judged. Settle the keys: a fresh
|
|
92
|
+
// heir rotates at once to what it announced, which honour has already held
|
|
93
|
+
// to being something other than itself, so that the heir dies as it speaks;
|
|
94
|
+
// a current key replaces its announcement; an announced key becomes current.
|
|
88
95
|
settle(h, by, next) {
|
|
89
96
|
if (h.fresh) {
|
|
90
97
|
if (next === null)
|
|
91
|
-
return
|
|
98
|
+
return; // honour judged this. the compiler has not read it, so the narrowing stays.
|
|
92
99
|
h.current = next;
|
|
93
100
|
h.announced = null;
|
|
94
101
|
h.fresh = false;
|
|
95
|
-
return
|
|
102
|
+
return;
|
|
96
103
|
}
|
|
97
104
|
if (by === h.announced)
|
|
98
105
|
h.current = by;
|
|
99
106
|
h.announced = next;
|
|
100
|
-
return true;
|
|
101
107
|
}
|
|
102
108
|
}
|
package/dist/ward/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { Ward } from './ward.ts';
|
|
2
2
|
export type { Ground, WardPointers } from './ground.ts';
|
|
3
3
|
export type { Partition, Heir, Bind, StandingKeys } from './partition.ts';
|
|
4
|
+
export { GONE, MINTED } from './partition.ts';
|
|
4
5
|
export type { AskPayload, ReplyPayload } from './seal.ts';
|
|
5
6
|
export * as seal from './seal.ts';
|
|
6
7
|
export * as arithmetic from './arithmetic.ts';
|
package/dist/ward/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// @quo-systems/quo/ward — one function. Every ward is the same ward.
|
|
3
3
|
export { Ward } from './ward.js';
|
|
4
|
+
// The two bounds on what a partition may hold, for a kit that must keep the
|
|
5
|
+
// same ones and for a test that pins them.
|
|
6
|
+
export { GONE, MINTED } from './partition.js';
|
|
4
7
|
// The seal and the arithmetic, for a kit in another language to check its
|
|
5
8
|
// bytes against, and for tests that speak to a door directly.
|
|
6
9
|
export * as seal from './seal.js';
|
package/dist/ward/owner.d.ts
CHANGED
|
@@ -1,34 +1,13 @@
|
|
|
1
|
-
import type { Ask, Asker,
|
|
1
|
+
import type { Ask, Asker, Json, JsonObject } from '../being/types.ts';
|
|
2
|
+
import type { Resident } from './ward.ts';
|
|
2
3
|
export type OwnerSide = {
|
|
3
4
|
pk: string;
|
|
4
|
-
doors: Map<string,
|
|
5
|
-
|
|
6
|
-
being: {
|
|
7
|
-
answer: (...a: never[]) => unknown;
|
|
8
|
-
};
|
|
9
|
-
cells: {
|
|
10
|
-
class?: string;
|
|
11
|
-
occupants: Record<string, unknown>;
|
|
12
|
-
standings: Record<string, unknown>;
|
|
13
|
-
};
|
|
14
|
-
stance: {
|
|
15
|
-
occupants: {
|
|
16
|
-
invite(id: string): Promise<Invitation | null>;
|
|
17
|
-
remove(id: string): void;
|
|
18
|
-
};
|
|
19
|
-
standings: {
|
|
20
|
-
knock(inv: Invitation, m?: string, a?: JsonObject, w?: Wanted): Promise<unknown>;
|
|
21
|
-
take(id: string, inv: Invitation): Promise<string | null>;
|
|
22
|
-
remove(id: string): void;
|
|
23
|
-
};
|
|
24
|
-
};
|
|
25
|
-
}>;
|
|
5
|
+
doors: Map<string, Resident>;
|
|
6
|
+
absent(): Record<string, string | null>;
|
|
26
7
|
publicKey(): string | null;
|
|
27
|
-
instantiate(
|
|
28
|
-
key: string;
|
|
29
|
-
} | null;
|
|
8
|
+
instantiate(className: string, key: string): Resident | 'threw' | null;
|
|
30
9
|
unboot(key: string): string[] | null;
|
|
31
|
-
setPublic(key: string): void;
|
|
10
|
+
setPublic(key: string | null): void;
|
|
32
11
|
};
|
|
33
12
|
export declare const OWNER_ASKS: Ask[];
|
|
34
13
|
export declare function ownerAnswer(w: OwnerSide, asker: Asker, method: string | undefined, args: JsonObject): Promise<Json>;
|