@quo-systems/quo 0.2.15 → 0.2.17
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/GETTING_STARTED.md +128 -0
- package/README.md +38 -26
- package/dist/conformance/store.js +36 -10
- package/dist/harbor/core.js +12 -9
- package/dist/harbor/index.d.ts +2 -1
- package/dist/harbor/index.js +4 -1
- package/dist/harbor/memory.d.ts +1 -0
- package/dist/harbor/memory.js +7 -1
- package/dist/harbor/store.d.ts +10 -3
- package/dist/harbor/store.js +34 -5
- package/dist/ward/arithmetic.d.ts +4 -0
- package/dist/ward/arithmetic.js +82 -12
- package/dist/ward/ground.d.ts +1 -1
- package/dist/ward/partition.d.ts +4 -0
- package/dist/ward/partition.js +53 -0
- package/dist/ward/seal.js +1 -1
- package/dist/ward/ward.js +37 -23
- package/package.json +7 -7
- package/{SPEC.md → protocol/SPEC.md} +16 -4
- package/protocol/vectors/door.json +345 -0
- package/quo-kit.md +98 -15
- package/src/conformance/store.ts +37 -10
- package/src/harbor/core.ts +17 -10
- package/src/harbor/index.ts +4 -1
- package/src/harbor/memory.ts +7 -1
- package/src/harbor/store.ts +39 -7
- package/src/ward/arithmetic.ts +83 -14
- package/src/ward/ground.ts +11 -4
- package/src/ward/partition.ts +53 -0
- package/src/ward/seal.ts +1 -1
- package/src/ward/ward.ts +37 -23
- /package/{vectors → protocol/vectors}/arithmetic.json +0 -0
- /package/{vectors → protocol/vectors}/framing.json +0 -0
- /package/{vectors → protocol/vectors}/wire.json +0 -0
package/src/conformance/store.ts
CHANGED
|
@@ -9,7 +9,15 @@ import type { Kept, Store } from '../harbor/store.ts';
|
|
|
9
9
|
import type { Runner } from './index.ts';
|
|
10
10
|
|
|
11
11
|
const seed = (b: number) => new Uint8Array(32).fill(b);
|
|
12
|
-
const kept = (b: number, extra: Record<string, unknown> = {}): Kept => ({ seed: seed(b), partition: { beings: {}, ...extra }, record: { pk: '', code: 'classes/index.ts', user: 'me' } });
|
|
12
|
+
const kept = (b: number, extra: Record<string, unknown> = {}): Kept => ({ seed: seed(b), partition: { beings: {}, bind: {}, ...extra }, record: { pk: '', code: 'classes/index.ts', user: 'me' } });
|
|
13
|
+
// A partition as a ward writes one: beings under their keys, a bind table
|
|
14
|
+
// beside each, and whatever else the head carries.
|
|
15
|
+
const ward = (beings: Record<string, unknown>, extra: Record<string, unknown> = {}): Record<string, unknown> => ({
|
|
16
|
+
beings,
|
|
17
|
+
bind: Object.fromEntries(Object.keys(beings).map((k) => [k, { standings: {} }])),
|
|
18
|
+
...extra,
|
|
19
|
+
});
|
|
20
|
+
const ALL = (p: Record<string, unknown>): string[] => ['', ...Object.keys(p.beings as object)];
|
|
13
21
|
|
|
14
22
|
export function conformStore(label: string, make: () => Promise<Store>, { test }: { test: Runner }) {
|
|
15
23
|
const t = (name: string, fn: () => Promise<void>) => test(`[${label}] ${name}`, {}, fn);
|
|
@@ -29,7 +37,7 @@ export function conformStore(label: string, make: () => Promise<Store>, { test }
|
|
|
29
37
|
const back = await s.load('main');
|
|
30
38
|
assert.ok(back);
|
|
31
39
|
assert.deepEqual(back.seed, seed(7));
|
|
32
|
-
assert.deepEqual(back.partition, { beings: {}, n: 1, deep: { list: [1, 'two', null], flag: true } });
|
|
40
|
+
assert.deepEqual(back.partition, { beings: {}, bind: {}, n: 1, deep: { list: [1, 'two', null], flag: true } });
|
|
33
41
|
assert.deepEqual(back.record, { pk: '', code: 'classes/index.ts', user: 'me' });
|
|
34
42
|
});
|
|
35
43
|
|
|
@@ -46,28 +54,47 @@ export function conformStore(label: string, make: () => Promise<Store>, { test }
|
|
|
46
54
|
assert.deepEqual((await s.load('main'))!.seed, seed(1));
|
|
47
55
|
});
|
|
48
56
|
|
|
49
|
-
t('save
|
|
57
|
+
t('save writes every row it is given, and touches nothing else', async () => {
|
|
50
58
|
const s = await make();
|
|
51
59
|
await s.put('main', kept(3));
|
|
52
|
-
|
|
60
|
+
const p = ward({ a: { x: 1 } });
|
|
61
|
+
await s.save('main', p, ALL(p));
|
|
53
62
|
const back = (await s.load('main'))!;
|
|
54
|
-
assert.deepEqual(back.partition,
|
|
63
|
+
assert.deepEqual(back.partition, p);
|
|
55
64
|
assert.deepEqual(back.seed, seed(3));
|
|
56
65
|
assert.deepEqual(back.record, kept(3).record);
|
|
57
66
|
});
|
|
58
67
|
|
|
68
|
+
// What a store may do with the rows it is told about is its own: writing
|
|
69
|
+
// the whole partition every time is correct. What it may never do is write
|
|
70
|
+
// less than it was told, or keep a being the rows say is gone.
|
|
71
|
+
t('save writes the rows it names, and a being the rows drop is gone', async () => {
|
|
72
|
+
const s = await make();
|
|
73
|
+
const p = ward({ a: { x: 1 }, b: { y: 1 } });
|
|
74
|
+
await s.put('main', { ...kept(3), partition: p });
|
|
75
|
+
// one being moves, and only her row is named
|
|
76
|
+
(p.beings as Record<string, { x: number }>).a.x = 2;
|
|
77
|
+
await s.save('main', p, ['a']);
|
|
78
|
+
assert.deepEqual((await s.load('main'))!.partition, p, 'the row that was named is written');
|
|
79
|
+
// she leaves: her row is named and her key is gone from the partition
|
|
80
|
+
delete (p.beings as Record<string, unknown>).a;
|
|
81
|
+
delete (p.bind as Record<string, unknown>).a;
|
|
82
|
+
await s.save('main', p, ['a']);
|
|
83
|
+
assert.deepEqual((await s.load('main'))!.partition, p, 'a row whose being is gone is dropped');
|
|
84
|
+
});
|
|
85
|
+
|
|
59
86
|
t('record replaces the record and touches nothing else', async () => {
|
|
60
87
|
const s = await make();
|
|
61
88
|
await s.put('main', kept(4, { n: 9 }));
|
|
62
89
|
await s.record('main', { pk: 'ab'.repeat(64), code: 'elsewhere.ts', user: 'her' });
|
|
63
90
|
const back = (await s.load('main'))!;
|
|
64
91
|
assert.deepEqual(back.record, { pk: 'ab'.repeat(64), code: 'elsewhere.ts', user: 'her' });
|
|
65
|
-
assert.deepEqual(back.partition, { beings: {}, n: 9 });
|
|
92
|
+
assert.deepEqual(back.partition, { beings: {}, bind: {}, n: 9 });
|
|
66
93
|
});
|
|
67
94
|
|
|
68
95
|
t('save and record on a name not kept are nothing', async () => {
|
|
69
96
|
const s = await make();
|
|
70
|
-
await s.save('ghost', { beings: {} });
|
|
97
|
+
await s.save('ghost', { beings: {}, bind: {} }, ['']);
|
|
71
98
|
await s.record('ghost', { pk: '', code: '', user: '' });
|
|
72
99
|
assert.deepEqual(await s.list(), []);
|
|
73
100
|
});
|
|
@@ -77,10 +104,10 @@ export function conformStore(label: string, make: () => Promise<Store>, { test }
|
|
|
77
104
|
const k = kept(5, { n: 1 });
|
|
78
105
|
await s.put('main', k);
|
|
79
106
|
(k.partition as { n: number }).n = 2;
|
|
80
|
-
assert.deepEqual((await s.load('main'))!.partition, { beings: {}, n: 1 });
|
|
107
|
+
assert.deepEqual((await s.load('main'))!.partition, { beings: {}, bind: {}, n: 1 });
|
|
81
108
|
const a = (await s.load('main'))!;
|
|
82
109
|
(a.partition as { n: number }).n = 3;
|
|
83
|
-
assert.deepEqual((await s.load('main'))!.partition, { beings: {}, n: 1 });
|
|
110
|
+
assert.deepEqual((await s.load('main'))!.partition, { beings: {}, bind: {}, n: 1 });
|
|
84
111
|
});
|
|
85
112
|
|
|
86
113
|
t('take hands the ward out and forgets it; the name is free again', async () => {
|
|
@@ -90,7 +117,7 @@ export function conformStore(label: string, make: () => Promise<Store>, { test }
|
|
|
90
117
|
const out = await s.take('main');
|
|
91
118
|
assert.ok(out);
|
|
92
119
|
assert.deepEqual(out.seed, seed(6));
|
|
93
|
-
assert.deepEqual(out.partition, { beings: {}, n: 6 });
|
|
120
|
+
assert.deepEqual(out.partition, { beings: {}, bind: {}, n: 6 });
|
|
94
121
|
assert.equal(await s.load('main'), undefined);
|
|
95
122
|
assert.deepEqual(await s.list(), ['other']);
|
|
96
123
|
await s.put('main', kept(9));
|
package/src/harbor/core.ts
CHANGED
|
@@ -50,7 +50,11 @@ export type Hosted = WardPointers & { pk: string; name: string; record: WardReco
|
|
|
50
50
|
// snapshot cannot land after a newer one. A save that fails is the harbor's
|
|
51
51
|
// to count and never a door's to answer: the door answered bytes, and a
|
|
52
52
|
// disk that is full is not a reason the far side may hear.
|
|
53
|
-
|
|
53
|
+
// `dirty` holds the rows the ward has named since the last write, and never
|
|
54
|
+
// what changed in them: the harbor keeps the partition and reads none of it,
|
|
55
|
+
// so what a row is worth is the store's to read off the object when it
|
|
56
|
+
// writes. Empty is nothing to do.
|
|
57
|
+
type Saving = { memory: Record<string, unknown>; dirty: Set<string>; running: Promise<void> | null; timer: ReturnType<typeof setTimeout> | undefined; gone: boolean };
|
|
54
58
|
// A reach in the directory, and whether this harbor holds it as a socket a
|
|
55
59
|
// dialer opened: only those, and its own doors, take bytes from the wire.
|
|
56
60
|
export type Bound = { reach: Reach; held: boolean };
|
|
@@ -155,12 +159,12 @@ export class Harbor {
|
|
|
155
159
|
|
|
156
160
|
// ---- saving
|
|
157
161
|
|
|
158
|
-
// The ward wrote. Its partition is saved once the line is free, and
|
|
159
|
-
// for every burst of writes,
|
|
160
|
-
#wrote(name: string): void {
|
|
162
|
+
// The ward wrote one row. Its partition is saved once the line is free, and
|
|
163
|
+
// once for every burst of writes, with every row named in that burst.
|
|
164
|
+
#wrote(name: string, row: string): void {
|
|
161
165
|
const s = this.#saving.get(name);
|
|
162
166
|
if (!s || s.gone) return;
|
|
163
|
-
s.dirty
|
|
167
|
+
s.dirty.add(row);
|
|
164
168
|
if (s.timer === undefined) s.timer = setTimeout(() => void this.#flush(name), 0);
|
|
165
169
|
}
|
|
166
170
|
async #flush(name: string): Promise<void> {
|
|
@@ -172,10 +176,13 @@ export class Harbor {
|
|
|
172
176
|
}
|
|
173
177
|
if (s.running) return s.running;
|
|
174
178
|
s.running = (async () => {
|
|
175
|
-
while (s.dirty && !s.gone) {
|
|
176
|
-
|
|
179
|
+
while (s.dirty.size > 0 && !s.gone) {
|
|
180
|
+
// Taken before the write, so a row named while this one is in flight
|
|
181
|
+
// is written by the next turn of the loop and not lost with it.
|
|
182
|
+
const rows = [...s.dirty];
|
|
183
|
+
s.dirty.clear();
|
|
177
184
|
try {
|
|
178
|
-
await this.store.save(name, s.memory);
|
|
185
|
+
await this.store.save(name, s.memory, rows);
|
|
179
186
|
} catch {
|
|
180
187
|
this.faults.set(name, (this.faults.get(name) ?? 0) + 1);
|
|
181
188
|
}
|
|
@@ -352,11 +359,11 @@ export class Harbor {
|
|
|
352
359
|
instantiate: maker(objects, classes, this.classes),
|
|
353
360
|
carry: (pk, bytes) => this.carry(pk, new Uint8Array(bytes)),
|
|
354
361
|
random,
|
|
355
|
-
wrote: () => this.#wrote(name),
|
|
362
|
+
wrote: (row) => this.#wrote(name, row),
|
|
356
363
|
};
|
|
357
364
|
const lend = this.lendFor(name, kept.record);
|
|
358
365
|
if (lend) ground.lend = lend;
|
|
359
|
-
const saving: Saving = { memory, dirty:
|
|
366
|
+
const saving: Saving = { memory, dirty: new Set(), running: null, timer: undefined, gone: false };
|
|
360
367
|
this.#saving.set(name, saving);
|
|
361
368
|
// A ward that will not be born leaves nothing behind: a partition of a
|
|
362
369
|
// shape this kit cannot read throws here, and a name the harbor does not
|
package/src/harbor/index.ts
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
export { MemoryHarbor, type Booted, type WardFactory } from './memory.ts';
|
|
7
7
|
export { Harbor, DEFAULT_CODE, type Hosted, type Bound, type Loader } from './core.ts';
|
|
8
8
|
export { dial, type Dialer } from './dial.ts';
|
|
9
|
-
export { MemoryStore, values, type Store, type Kept, type WardRecord } from './store.ts';
|
|
9
|
+
export { MemoryStore, values, rowsOf, type Store, type Kept, type WardRecord } from './store.ts';
|
|
10
|
+
// What a store keeping a ward in rows needs: the name of the row that is not
|
|
11
|
+
// a being, and the partition back from the rows it was cut into.
|
|
12
|
+
export { HEAD, fromRows, rowsIn } from '../ward/partition.ts';
|
|
10
13
|
export { request, Socket, SUITE, REFUSED, type Reach, type Carry, type Line, type Announce } from './reach.ts';
|
|
11
14
|
export type { Ground, WardPointers, Lend } from '../ward/ground.ts';
|
|
12
15
|
// What a harbor builds a ground out of. Convenience, never contract: a kit
|
package/src/harbor/memory.ts
CHANGED
|
@@ -21,6 +21,12 @@ export class MemoryHarbor {
|
|
|
21
21
|
readonly partitions = new Map<string, Record<string, unknown>>(); // seed -> memory. the harbor keeps it and reads nothing
|
|
22
22
|
readonly wards = new Map<string, Booted>(); // seed -> pointers
|
|
23
23
|
readonly objects = new WeakMap<object, BeingLike>(); // cells -> being object. what instantiate constructed. a hand for tests, never the ward's; it follows the cells out when she is unbooted
|
|
24
|
+
// Where the wards this harbor boots draw their entropy. The device's own
|
|
25
|
+
// by default. A harbor may hand a fixed stream instead, which is what pins
|
|
26
|
+
// bytes: every key a ward mints and every nonce it seals under comes from
|
|
27
|
+
// here, so a ward on a known seed with a known stream answers the same
|
|
28
|
+
// bytes every run, and a corpus of them is a corpus and not a sample.
|
|
29
|
+
random: Ground['random'] = entropy;
|
|
24
30
|
|
|
25
31
|
// the directory: its own doors, else a peer it is linked to. Quo says nothing about how.
|
|
26
32
|
async route(farPk: string, bytes: Uint8Array): Promise<Uint8Array | undefined> {
|
|
@@ -66,7 +72,7 @@ export class MemoryHarbor {
|
|
|
66
72
|
const back = await this.route(farPk, new Uint8Array(bytes));
|
|
67
73
|
return back === undefined ? undefined : new Uint8Array(back);
|
|
68
74
|
},
|
|
69
|
-
random:
|
|
75
|
+
random: (n) => this.random(n),
|
|
70
76
|
};
|
|
71
77
|
const w = await Ward(ground);
|
|
72
78
|
const booted: Booted = { ...w, pk: await learnPk(w) };
|
package/src/harbor/store.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// storage on the edge; the memory store below is the library's own, for a
|
|
10
10
|
// harbor core with no device under it. `src/conformance/store.ts` is what
|
|
11
11
|
// every one of them passes.
|
|
12
|
+
import { fromRows, rowOf, rowsIn, type Partition } from '../ward/partition.ts';
|
|
13
|
+
|
|
12
14
|
export type WardRecord = { pk: string; code: string; user: string };
|
|
13
15
|
export type Kept = { seed: Uint8Array; partition: Record<string, unknown>; record: WardRecord };
|
|
14
16
|
|
|
@@ -19,7 +21,14 @@ export type Store = {
|
|
|
19
21
|
put(name: string, kept: Kept): Promise<void>;
|
|
20
22
|
// The partition back, after a call; the record back, after a boot learned
|
|
21
23
|
// the pk. A name not kept is nothing, on both.
|
|
22
|
-
|
|
24
|
+
//
|
|
25
|
+
// `rows` names what the ward wrote since the last save: a being by her key,
|
|
26
|
+
// or `HEAD`. A store may write only those, which is what `rowsOf` is for,
|
|
27
|
+
// and a store that writes the whole partition every time is correct and
|
|
28
|
+
// slower. A being's cells are the one part of a partition that grows
|
|
29
|
+
// without limit, so a store that keeps her apart lets her neighbour be
|
|
30
|
+
// asked without her being written down again.
|
|
31
|
+
save(name: string, partition: Record<string, unknown>, rows: readonly string[]): Promise<void>;
|
|
23
32
|
record(name: string, record: WardRecord): Promise<void>;
|
|
24
33
|
// Take a ward out: the first move of a migration. What was kept comes back.
|
|
25
34
|
take(name: string): Promise<Kept | undefined>;
|
|
@@ -31,9 +40,30 @@ export type Store = {
|
|
|
31
40
|
// so a store takes a copy of the values the way a file would: through JSON.
|
|
32
41
|
export const values = (p: Record<string, unknown>): Record<string, unknown> => JSON.parse(JSON.stringify(p)) as Record<string, unknown>;
|
|
33
42
|
|
|
34
|
-
// The store
|
|
43
|
+
// The rows a store was told about, as values, ready to be written down. A row
|
|
44
|
+
// whose key names nobody is a being who left, and comes back undefined, which
|
|
45
|
+
// is the store dropping what it holds for her.
|
|
46
|
+
export const rowsOf = (partition: Record<string, unknown>, rows: readonly string[]): [string, Record<string, unknown> | undefined][] =>
|
|
47
|
+
rows.map((row) => {
|
|
48
|
+
const value = rowOf(partition as unknown as Partition, row);
|
|
49
|
+
return [row, value === undefined ? undefined : (values(value) as Record<string, unknown>)];
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// One store's copy, brought up to date with the rows it was told about.
|
|
53
|
+
const keep = (row: Row, partition: Record<string, unknown>, rows: readonly string[]): void => {
|
|
54
|
+
for (const [id, value] of rowsOf(partition, rows)) {
|
|
55
|
+
if (value === undefined) row.parts.delete(id);
|
|
56
|
+
else row.parts.set(id, value);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// The store as a map, one process, nothing kept past it. It keeps a ward as
|
|
61
|
+
// rows, the way a store on a device does, so what it costs to save is what a
|
|
62
|
+
// device pays and the library's own measurements mean something.
|
|
63
|
+
type Row = { seed: Uint8Array; parts: Map<string, Record<string, unknown>>; record: WardRecord };
|
|
64
|
+
|
|
35
65
|
export class MemoryStore implements Store {
|
|
36
|
-
readonly rows = new Map<string,
|
|
66
|
+
readonly rows = new Map<string, Row>();
|
|
37
67
|
readonly reach = new Map<string, string>();
|
|
38
68
|
|
|
39
69
|
async list(): Promise<string[]> {
|
|
@@ -41,15 +71,17 @@ export class MemoryStore implements Store {
|
|
|
41
71
|
}
|
|
42
72
|
async load(name: string): Promise<Kept | undefined> {
|
|
43
73
|
const row = this.rows.get(name);
|
|
44
|
-
return row && { seed: new Uint8Array(row.seed), partition:
|
|
74
|
+
return row && { seed: new Uint8Array(row.seed), partition: fromRows(Object.fromEntries(row.parts)), record: { ...row.record } };
|
|
45
75
|
}
|
|
46
76
|
async put(name: string, kept: Kept): Promise<void> {
|
|
47
77
|
if (this.rows.has(name)) throw new Error(`ward ${name} already exists here`);
|
|
48
|
-
|
|
78
|
+
const row: Row = { seed: new Uint8Array(kept.seed), parts: new Map(), record: { ...kept.record } };
|
|
79
|
+
this.rows.set(name, row);
|
|
80
|
+
keep(row, kept.partition, rowsIn(kept.partition));
|
|
49
81
|
}
|
|
50
|
-
async save(name: string, partition: Record<string, unknown
|
|
82
|
+
async save(name: string, partition: Record<string, unknown>, rows: readonly string[]): Promise<void> {
|
|
51
83
|
const row = this.rows.get(name);
|
|
52
|
-
if (row) row
|
|
84
|
+
if (row) keep(row, partition, rows);
|
|
53
85
|
}
|
|
54
86
|
async record(name: string, record: WardRecord): Promise<void> {
|
|
55
87
|
const row = this.rows.get(name);
|
package/src/ward/arithmetic.ts
CHANGED
|
@@ -85,18 +85,83 @@ const key32 = (value: Uint8Array, what: string): Uint8Array => {
|
|
|
85
85
|
return value;
|
|
86
86
|
};
|
|
87
87
|
const pkcs8 = (prefix: Uint8Array, value: Uint8Array, what: string) => concat([prefix, key32(value, what)]);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
88
|
+
|
|
89
|
+
// Importing a key is the most expensive thing on the path of an ask, and most
|
|
90
|
+
// of the imports are the same key again: a ward signs every reply with the one
|
|
91
|
+
// key, opens every ask with the one padlock, and verifies a relation under the
|
|
92
|
+
// key it verified it under last time. Measured over a round trip, fifteen of
|
|
93
|
+
// the twenty-five imports were bytes already imported once.
|
|
94
|
+
//
|
|
95
|
+
// So an imported key is kept, by the bytes it was imported from. A CryptoKey
|
|
96
|
+
// cannot be changed once it exists, so handing the same one out twice is
|
|
97
|
+
// handing out what a second import would have built. Nothing here is a
|
|
98
|
+
// decision a peer can see: two wards that cache differently, or not at all,
|
|
99
|
+
// speak the same bytes.
|
|
100
|
+
//
|
|
101
|
+
// It is bounded, and that is not a detail. A relation mints a fresh key on
|
|
102
|
+
// every ask, so a ward that talked all day would otherwise hold a key for
|
|
103
|
+
// every ask it ever made. Past the bound the least recently used goes, which
|
|
104
|
+
// is the key of a relation that has fallen quiet, and importing it again
|
|
105
|
+
// costs what it cost the first time.
|
|
106
|
+
//
|
|
107
|
+
// The secret keys in here are the ones the partition already holds in this
|
|
108
|
+
// process, as seeds. The cache is another shape of what the ward is already
|
|
109
|
+
// standing on, and never a second place a secret comes from.
|
|
110
|
+
const KEYS = 512;
|
|
111
|
+
const imported = new Map<string, Promise<CryptoKey>>();
|
|
112
|
+
const keep = (id: string, make: () => Promise<CryptoKey>): Promise<CryptoKey> => {
|
|
113
|
+
const had = imported.get(id);
|
|
114
|
+
if (had !== undefined) {
|
|
115
|
+
imported.delete(id); // and set again below: the most recently used goes last
|
|
116
|
+
imported.set(id, had);
|
|
117
|
+
return had;
|
|
118
|
+
}
|
|
119
|
+
const made = make();
|
|
120
|
+
// A key that would not import is not kept: the next call asks subtle again
|
|
121
|
+
// and hears the same refusal, rather than reading one this cache remembered.
|
|
122
|
+
// Node takes any thirty-two bytes as a public key and finds out at verify,
|
|
123
|
+
// so nothing here reaches this line; a terrain that checks the point at the
|
|
124
|
+
// import does, and a refusal it remembered would be a relation killed for
|
|
125
|
+
// good by one bad arrival.
|
|
126
|
+
made.catch(() => imported.delete(id));
|
|
127
|
+
imported.set(id, made);
|
|
128
|
+
// One in, at most one out: a map keeps what was put in the order it was put,
|
|
129
|
+
// so the first key it names is the one used longest ago.
|
|
130
|
+
if (imported.size > KEYS) imported.delete(imported.keys().next().value!);
|
|
131
|
+
return made;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// How many imported keys are held, and the bound they are held under. Nothing
|
|
135
|
+
// in the ward reads either: they are here to be looked at, and for the suite
|
|
136
|
+
// that holds the bound to what it says.
|
|
137
|
+
export const heldKeys = (): { held: number; bound: number } => ({ held: imported.size, bound: KEYS });
|
|
138
|
+
|
|
139
|
+
const secretKey = (alg: { name: string }, prefix: Uint8Array, value: Uint8Array, what: string, uses: KeyUsage[]) => {
|
|
140
|
+
const bytes = pkcs8(prefix, value, what);
|
|
141
|
+
return keep(`${alg.name}|${uses.join('+')}|${hex(bytes)}`, () => subtle().importKey('pkcs8', bytes as BufferSource, alg, true, uses));
|
|
142
|
+
};
|
|
143
|
+
const publicKey = (alg: { name: string }, value: Uint8Array, what: string, uses: KeyUsage[]) => {
|
|
144
|
+
const bytes = key32(value, what);
|
|
145
|
+
return keep(`${alg.name}|${uses.join('+')}|pk|${hex(bytes)}`, () => subtle().importKey('raw', bytes as BufferSource, alg, true, uses));
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Subtle exports the public half of a private key only through a JWK, where
|
|
149
|
+
// `x` is the 32 raw bytes in base64url. The answer is a fact about the key and
|
|
150
|
+
// never changes, so it is kept beside the key it was read from and goes when
|
|
151
|
+
// the key does.
|
|
152
|
+
const publics = new WeakMap<CryptoKey, Promise<Uint8Array>>();
|
|
153
|
+
function rawPublic(secret: CryptoKey): Promise<Uint8Array> {
|
|
154
|
+
const had = publics.get(secret);
|
|
155
|
+
if (had !== undefined) return had;
|
|
156
|
+
const read = (async () => {
|
|
157
|
+
const jwk = await subtle().exportKey('jwk', secret);
|
|
158
|
+
const binary = atob(jwk.x!.replaceAll('-', '+').replaceAll('_', '/'));
|
|
159
|
+
const out = new Uint8Array(binary.length);
|
|
160
|
+
for (let at = 0; at < binary.length; at += 1) out[at] = binary.charCodeAt(at);
|
|
161
|
+
return out;
|
|
162
|
+
})();
|
|
163
|
+
publics.set(secret, read);
|
|
164
|
+
return read;
|
|
100
165
|
}
|
|
101
166
|
|
|
102
167
|
export async function sha256(...parts: Uint8Array[]): Promise<Uint8Array> {
|
|
@@ -104,13 +169,17 @@ export async function sha256(...parts: Uint8Array[]): Promise<Uint8Array> {
|
|
|
104
169
|
}
|
|
105
170
|
|
|
106
171
|
export type Pair = { secret: Uint8Array; pk: Uint8Array };
|
|
172
|
+
// Both halves are copies. The seed and the public key are kept behind the two
|
|
173
|
+
// caches above, and a pair is handed to whoever asked for it: what she does
|
|
174
|
+
// with the bytes in her hand is hers, and must not reach what the next caller
|
|
175
|
+
// is given.
|
|
107
176
|
export async function signingPair(seed: Uint8Array): Promise<Pair> {
|
|
108
177
|
const secret = await secretKey(ED, ED_SECRET, seed, 'seed', ['sign']);
|
|
109
|
-
return { secret: Uint8Array.from(seed), pk: await rawPublic(secret) };
|
|
178
|
+
return { secret: Uint8Array.from(seed), pk: Uint8Array.from(await rawPublic(secret)) };
|
|
110
179
|
}
|
|
111
180
|
export async function sealingPair(seed: Uint8Array): Promise<Pair> {
|
|
112
181
|
const secret = await secretKey(X, X_SECRET, seed, 'seed', ['deriveBits']);
|
|
113
|
-
return { secret: Uint8Array.from(seed), pk: await rawPublic(secret) };
|
|
182
|
+
return { secret: Uint8Array.from(seed), pk: Uint8Array.from(await rawPublic(secret)) };
|
|
114
183
|
}
|
|
115
184
|
|
|
116
185
|
export async function sign(message: Uint8Array, secret: Uint8Array): Promise<Uint8Array> {
|
package/src/ward/ground.ts
CHANGED
|
@@ -31,13 +31,20 @@ export type Ground = {
|
|
|
31
31
|
seed: string | Uint8Array; // the ward derives its pk from it and nothing else
|
|
32
32
|
memory: Record<string, unknown>; // the partition. the ward's files. opaque to the harbor
|
|
33
33
|
instantiate(className: string, stance: Stance): BeingLike | null; // the code half
|
|
34
|
-
// The ward wrote its partition
|
|
35
|
-
//
|
|
36
|
-
//
|
|
34
|
+
// The ward wrote its partition, and which row of it: a being by her key,
|
|
35
|
+
// or `HEAD` for everything that is not one being's. It says so after every
|
|
36
|
+
// write, a key rotated, a relation taken, a cell she set, and says nothing
|
|
37
|
+
// else: it never learns whether anything was kept. A harbor that keeps the
|
|
37
38
|
// partition saves after this, when it likes and in the order it was told,
|
|
38
39
|
// so a being driven in process is kept the way one reached through a door
|
|
39
40
|
// is. A harbor that keeps nothing leaves it out.
|
|
40
|
-
|
|
41
|
+
//
|
|
42
|
+
// The row is named because a being's cells are the one part of a partition
|
|
43
|
+
// that grows without limit, and a harbor told only that something moved has
|
|
44
|
+
// to write down every being in the ward to be sure of one. What a harbor
|
|
45
|
+
// does with the name is its own: writing the whole partition on every word
|
|
46
|
+
// is correct, and slower.
|
|
47
|
+
wrote?: (row: string) => void;
|
|
41
48
|
// Sealed bytes to a ward pk. What comes back, or undefined.
|
|
42
49
|
//
|
|
43
50
|
// undefined is a promise, not a shrug: no door was reached, and nothing was
|
package/src/ward/partition.ts
CHANGED
|
@@ -235,3 +235,56 @@ export const drop = (rec: Record<string, unknown>, key: string): void => {
|
|
|
235
235
|
|
|
236
236
|
export const emptyCells = (): Cells => ({ standings: {}, occupants: {} });
|
|
237
237
|
export const emptyBind = (): Bind => ({ standings: {}, occupants: {}, knocks: {}, answered: {}, minted: [] });
|
|
238
|
+
|
|
239
|
+
// ---- the partition in rows, for a store that keeps it
|
|
240
|
+
//
|
|
241
|
+
// A ward hands a harbor one object, so a being who writes one cell has every
|
|
242
|
+
// other being in her ward written down beside her. What a being holds is the
|
|
243
|
+
// one thing here that grows without limit, so she is a row: her cells and her
|
|
244
|
+
// bind table, under her key. Everything else is one head row, and every part
|
|
245
|
+
// of it is bounded already, one record per occupant and two lists that drop
|
|
246
|
+
// their oldest.
|
|
247
|
+
//
|
|
248
|
+
// The split is here, where the shape is, and not in the harbor, which keeps
|
|
249
|
+
// what it is given and reads none of it. A store is handed the rows that
|
|
250
|
+
// moved; what it does with that is its own, and one that writes every row is
|
|
251
|
+
// as correct as one that writes the few.
|
|
252
|
+
export const HEAD = ''; // the row that is not a being: no being's key is empty
|
|
253
|
+
|
|
254
|
+
// One row's value: a being, or the head, which is everything the partition
|
|
255
|
+
// holds that is not filed under a being. The head is read as what is left
|
|
256
|
+
// rather than as a list of names, so a store keeps whatever it was handed and
|
|
257
|
+
// hands the same thing back, and a field this file grows later needs no line
|
|
258
|
+
// in a store to survive a round trip.
|
|
259
|
+
//
|
|
260
|
+
// Undefined where the key names nobody, which is a row the store drops.
|
|
261
|
+
export const rowOf = (p: Partition, row: string): Record<string, unknown> | undefined => {
|
|
262
|
+
const all = p as unknown as Record<string, unknown>;
|
|
263
|
+
if (row === HEAD) return Object.fromEntries(Object.entries(all).filter(([k]) => k !== 'beings' && k !== 'bind'));
|
|
264
|
+
const cells = at(p.beings ?? {}, row);
|
|
265
|
+
return cells === undefined ? undefined : { cells, bind: at(p.bind ?? {}, row) ?? emptyBind() };
|
|
266
|
+
};
|
|
267
|
+
export const rowsIn = (p: Record<string, unknown>): string[] => [HEAD, ...Object.keys((p.beings as Record<string, unknown>) ?? {})];
|
|
268
|
+
|
|
269
|
+
// The partition back from its rows. A head that is absent is a partition
|
|
270
|
+
// nobody has written, and `open` makes an empty one of it.
|
|
271
|
+
export const fromRows = (rows: Record<string, Record<string, unknown>>): Record<string, unknown> => {
|
|
272
|
+
const out: Record<string, unknown> = { ...(rows[HEAD] ?? {}) };
|
|
273
|
+
const beings: Record<string, unknown> = {};
|
|
274
|
+
const bind: Record<string, unknown> = {};
|
|
275
|
+
let any = Object.keys(out).length > 0;
|
|
276
|
+
for (const [row, value] of Object.entries(rows)) {
|
|
277
|
+
if (row === HEAD) continue;
|
|
278
|
+
put(beings, row, value.cells);
|
|
279
|
+
put(bind, row, value.bind);
|
|
280
|
+
any = true;
|
|
281
|
+
}
|
|
282
|
+
// Nothing kept is nothing handed back. Rows that say only that there are no
|
|
283
|
+
// rows must come back as the empty memory they are, because `open` reads an
|
|
284
|
+
// empty one as a ward's first breath and anything else as a partition
|
|
285
|
+
// somebody wrote, which must then say which version wrote it.
|
|
286
|
+
if (!any) return {};
|
|
287
|
+
out.beings = beings;
|
|
288
|
+
out.bind = bind;
|
|
289
|
+
return out;
|
|
290
|
+
};
|
package/src/ward/seal.ts
CHANGED
|
@@ -54,7 +54,7 @@ export const SIZE = 1024 * 1024;
|
|
|
54
54
|
// designs and no separation at all: one secret would be doing two jobs with
|
|
55
55
|
// nothing said about it, and a second kit would have to reproduce a
|
|
56
56
|
// construction nobody named. HKDF-SHA-256 under a label is the separation
|
|
57
|
-
// said out loud, and it is what `vectors/framing.json` pins.
|
|
57
|
+
// said out loud, and it is what `protocol/vectors/framing.json` pins.
|
|
58
58
|
//
|
|
59
59
|
// Bytes are key material and text is not. A seed handed in as bytes of the
|
|
60
60
|
// key length is taken as it stands, which is what a harbor mints; anything
|