@quo-systems/quo 0.2.13 → 0.2.15
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 +10 -4
- package/SPEC.md +354 -536
- package/dist/being/being.d.ts +1 -0
- package/dist/being/being.js +8 -3
- package/dist/being/index.d.ts +1 -1
- package/dist/being/index.js +1 -1
- package/dist/being/types.d.ts +2 -0
- package/dist/being/types.js +17 -0
- package/dist/conformance/beings.d.ts +41 -0
- package/dist/conformance/beings.js +28 -2
- package/dist/conformance/index.d.ts +10 -1
- package/dist/conformance/index.js +162 -6
- package/dist/harbor/core.d.ts +4 -2
- package/dist/harbor/core.js +25 -4
- package/dist/harbor/index.d.ts +1 -1
- package/dist/harbor/memory.d.ts +3 -3
- package/dist/harbor/memory.js +6 -3
- package/dist/harbor/reach.js +1 -1
- package/dist/ward/cells.d.ts +2 -0
- package/dist/ward/cells.js +60 -11
- package/dist/ward/door.d.ts +1 -0
- package/dist/ward/door.js +29 -10
- package/dist/ward/ground.d.ts +3 -1
- package/dist/ward/ground.js +1 -1
- package/dist/ward/heirs.js +8 -1
- package/dist/ward/index.d.ts +2 -2
- package/dist/ward/index.js +3 -3
- package/dist/ward/owner.js +49 -8
- package/dist/ward/seal.d.ts +1 -0
- package/dist/ward/seal.js +10 -2
- package/dist/ward/stance.d.ts +1 -0
- package/dist/ward/stance.js +62 -4
- package/dist/ward/ward.js +5 -0
- package/package.json +5 -3
- package/quo-kit.md +523 -0
- package/src/being/being.ts +8 -3
- package/src/being/index.ts +1 -1
- package/src/being/types.ts +34 -0
- package/src/conformance/beings.ts +25 -2
- package/src/conformance/estate.ts +9 -9
- package/src/conformance/index.ts +204 -7
- package/src/conformance/reach.ts +1 -1
- package/src/harbor/core.ts +26 -5
- package/src/harbor/index.ts +1 -1
- package/src/harbor/memory.ts +7 -4
- package/src/harbor/reach.ts +1 -1
- package/src/ward/cells.ts +59 -10
- package/src/ward/door.ts +27 -9
- package/src/ward/ground.ts +39 -11
- package/src/ward/heirs.ts +7 -1
- package/src/ward/index.ts +4 -4
- package/src/ward/owner.ts +45 -10
- package/src/ward/seal.ts +11 -2
- package/src/ward/stance.ts +60 -4
- package/src/ward/ward.ts +6 -1
package/src/ward/door.ts
CHANGED
|
@@ -22,10 +22,13 @@ export type Judged = { bytes: Uint8Array; heard: boolean };
|
|
|
22
22
|
const SILENCE: ReplyPayload = { silence: true };
|
|
23
23
|
const said = (quo: DoorWord): ReplyPayload => ({ quo });
|
|
24
24
|
|
|
25
|
-
// One arrival at one being, already named
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
25
|
+
// One arrival at one being, already named: the three choices, D11, D12 and
|
|
26
|
+
// D13, and nothing else. Catches every throw. `bound` says whether the asker
|
|
27
|
+
// is a key this door holds, or the ward's own owner: she hears `threw`; a
|
|
28
|
+
// stranger at the public being hears silence, because her insides are hers.
|
|
29
|
+
// Every caller that reaches a being's answer goes through here, the owner's
|
|
30
|
+
// `ask` included, so the three choices are written once.
|
|
31
|
+
export async function answered(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload> {
|
|
29
32
|
const threw = bound ? said('threw') : SILENCE;
|
|
30
33
|
let out: Awaited<ReturnType<BeingLike['answer']>>;
|
|
31
34
|
try {
|
|
@@ -44,10 +47,18 @@ export async function arrive(door: Door, asker: Asker, method: string | undefine
|
|
|
44
47
|
// said, or the door itself would fail to write her reply after the number
|
|
45
48
|
// was spent. It is threw, like a word out of her.
|
|
46
49
|
if (cellFault(out, 'answer') !== null) return threw;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
return { object: out, seen: null };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// What crosses the wire: her answer, with her digest for this asker beside
|
|
54
|
+
// it. The digest rides along, it is not the answer. She has already
|
|
55
|
+
// answered: a describe that will not run costs the digest, and nothing else.
|
|
56
|
+
// The owner does not take one, because the owner asks for a describe when it
|
|
57
|
+
// wants one and nothing is sealed on its behalf.
|
|
58
|
+
export async function arrive(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload> {
|
|
59
|
+
const reply = await answered(door, asker, method, args, bound);
|
|
60
|
+
if (method === undefined || !('object' in reply)) return reply;
|
|
61
|
+
return { object: reply.object, seen: await seen(door, asker) };
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
// Her digest for this asker, or null when her describe threw or fell silent.
|
|
@@ -55,7 +66,14 @@ export async function arrive(door: Door, asker: Asker, method: string | undefine
|
|
|
55
66
|
export async function seen(door: Door, asker: Asker): Promise<string | null> {
|
|
56
67
|
try {
|
|
57
68
|
const bp = await door.being.answer(asker);
|
|
58
|
-
|
|
69
|
+
if (isSilence(bp) || isWord(bp)) return null;
|
|
70
|
+
// Held to the same rule as her answer, and for the same reason plus one:
|
|
71
|
+
// the digest is a walk, and a walk over a graph that shares or turns back
|
|
72
|
+
// on itself is not a digest but a hang or a throw. A blueprint that could
|
|
73
|
+
// not cross is a blueprint with no digest, which is what she has when her
|
|
74
|
+
// describe says nothing.
|
|
75
|
+
if (cellFault(bp, 'blueprint') !== null) return null;
|
|
76
|
+
return await digest(bp);
|
|
59
77
|
} catch {
|
|
60
78
|
return null;
|
|
61
79
|
}
|
package/src/ward/ground.ts
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The ground. The one object a harbor passes a ward at birth
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
import type { Stance, BeingLike, BeingClass } from '../being/types.ts';
|
|
2
|
+
// The ground. The one object a harbor passes a ward at birth: seven things
|
|
3
|
+
// the ward uses. Everything a runtime differs on arrives here, which is why
|
|
4
|
+
// the ward itself knows no runtime.
|
|
5
|
+
import type { Stance, BeingLike, BeingClass, Invitation } from '../being/types.ts';
|
|
6
|
+
|
|
7
|
+
// What the device lends this ward's beings, by name. What a box can do is
|
|
8
|
+
// beings, in a ward its harbor booted and roots, and this is how a being of
|
|
9
|
+
// another ward comes to hold a standing at one: the harbor asks its own root
|
|
10
|
+
// to invite on that being, and hands the invitation to the taker.
|
|
11
|
+
//
|
|
12
|
+
// The taker is the ward's half, and it is inside this call rather than after
|
|
13
|
+
// it so that nothing half-lives. The harbor minted, so the harbor is the only
|
|
14
|
+
// one who can unmint: a ward that could not knock, or could not take, says so
|
|
15
|
+
// by answering false, and the root that minted removes the occupant it made.
|
|
16
|
+
// A lend that failed leaves no heir open at that being and no relation bound
|
|
17
|
+
// to a ward that does not hold it, which is the promise `boot` keeps with
|
|
18
|
+
// `unmake` and the same promise here.
|
|
19
|
+
//
|
|
20
|
+
// False is every kind of no, and they are one answer because a being would do
|
|
21
|
+
// nothing different for any of them: this harbor lends nothing, or nothing of
|
|
22
|
+
// that name, or nothing of that name to this ward, or the ward did not take
|
|
23
|
+
// what was minted.
|
|
24
|
+
//
|
|
25
|
+
// The invitation never reaches the being. It is the device's capability, and
|
|
26
|
+
// a value she could copy is one she could hand to anyone, so it lives in this
|
|
27
|
+
// call and nowhere else.
|
|
28
|
+
export type Lend = (name: string, take: (invitation: Invitation) => Promise<boolean>) => Promise<boolean>;
|
|
6
29
|
|
|
7
30
|
export type Ground = {
|
|
8
31
|
seed: string | Uint8Array; // the ward derives its pk from it and nothing else
|
|
@@ -20,19 +43,24 @@ export type Ground = {
|
|
|
20
43
|
// undefined is a promise, not a shrug: no door was reached, and nothing was
|
|
21
44
|
// delivered. The ward hands it to a being as unreached, which is the one
|
|
22
45
|
// answer that says asking again is safe, so a harbor may only return it
|
|
23
|
-
// when it knows the bytes never arrived
|
|
46
|
+
// when it knows the bytes never arrived: no reach for that pk, a socket
|
|
24
47
|
// that would not open, a link that is down.
|
|
25
48
|
//
|
|
26
49
|
// A harbor that sent the bytes and then gave up waiting knows no such
|
|
27
50
|
// thing: the far door may have heard and be working still. It must not
|
|
28
51
|
// answer at all in that case. The ward bounds every ask itself, and an
|
|
29
|
-
// answer that never comes is
|
|
30
|
-
// may hold a shorter patience than the
|
|
31
|
-
//
|
|
32
|
-
// need to read each other: whichever
|
|
33
|
-
// only what it can honestly say.
|
|
52
|
+
// answer that never comes inside that bound is `late`, which promises
|
|
53
|
+
// nothing either way. So a harbor may hold a shorter patience than the
|
|
54
|
+
// ward's for its own reasons, a socket it wants back or a queue it will
|
|
55
|
+
// not grow, and the two bounds never need to read each other: whichever
|
|
56
|
+
// ends first ends the ask, and each says only what it can honestly say.
|
|
34
57
|
carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
|
|
35
58
|
random(n: number): Uint8Array; // entropy. every key a ward mints is drawn from it
|
|
59
|
+
// A standing at one of the harbor's own beings, by the name that harbor
|
|
60
|
+
// knows it under. Built per ward, so which ward may ask for which name is
|
|
61
|
+
// the harbor's own decision and a stranger's ward is lent nothing. A harbor
|
|
62
|
+
// with nothing to lend leaves it out, and every name answers false.
|
|
63
|
+
lend?: Lend;
|
|
36
64
|
};
|
|
37
65
|
|
|
38
66
|
// What a ward hands back. Two pointers. The door answers bytes, always, and
|
|
@@ -46,7 +74,7 @@ export type WardPointers = {
|
|
|
46
74
|
};
|
|
47
75
|
|
|
48
76
|
// ---- what every harbor builds a ground out of. Three pieces, because every
|
|
49
|
-
// harbor in this tree writes the same three and the
|
|
77
|
+
// harbor in this tree writes the same three and the protocol keeps the harbors
|
|
50
78
|
// themselves apart: what a memory harbor and a real one differ on is the
|
|
51
79
|
// route and the store, and nothing here. A second kit writes its own harbor
|
|
52
80
|
// and may write these again; they are convenience, never contract.
|
package/src/ward/heirs.ts
CHANGED
|
@@ -103,6 +103,12 @@ export class Heirs {
|
|
|
103
103
|
return;
|
|
104
104
|
}
|
|
105
105
|
if (by === h.announced) h.current = by;
|
|
106
|
-
|
|
106
|
+
// A send that announced nothing leaves the spare standing. This kit
|
|
107
|
+
// announces on every send but a public one, which holds no heir and
|
|
108
|
+
// reaches none of this; a kit that skips one is a kit whose own next is
|
|
109
|
+
// still the key it announced last time, and forgetting it here would meet
|
|
110
|
+
// that key with silence and kill a healthy relation. Nothing is dropped
|
|
111
|
+
// that was vouched for until something replaces it.
|
|
112
|
+
if (next !== null) h.announced = next;
|
|
107
113
|
}
|
|
108
114
|
}
|
package/src/ward/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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.ts';
|
|
4
|
-
export type { Ground, WardPointers } from './ground.ts';
|
|
4
|
+
export type { Ground, WardPointers, Lend } from './ground.ts';
|
|
5
5
|
export type { Partition, Heir, Bind, StandingKeys } from './partition.ts';
|
|
6
|
-
// The
|
|
7
|
-
// same ones and for a test that pins them.
|
|
8
|
-
export { GONE, MINTED } from './partition.ts';
|
|
6
|
+
// The three bounds on what a partition may hold, for a kit that must keep
|
|
7
|
+
// the same ones and for a test that pins them.
|
|
8
|
+
export { GONE, MINTED, KNOCKS } from './partition.ts';
|
|
9
9
|
export type { AskPayload, ReplyPayload } from './seal.ts';
|
|
10
10
|
// The seal and the arithmetic, for a kit in another language to check its
|
|
11
11
|
// bytes against, and for tests that speak to a door directly.
|
package/src/ward/owner.ts
CHANGED
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
// is an occupant of the ward itself, invited by the root. The ward is a
|
|
4
4
|
// being to her owner: the empty ask is her describe, and it carries her asks
|
|
5
5
|
// with a description and an input each, so a side renders them as it
|
|
6
|
-
// renders any being's.
|
|
7
|
-
// unboot, which is boot's inverse and takes her relations with her
|
|
8
|
-
//
|
|
9
|
-
// her
|
|
10
|
-
// by id, the mirror of 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
11
|
import { isSilence, isWord, wordOf } from '../being/silence.ts';
|
|
12
12
|
import { OWNER } from '../being/types.ts';
|
|
13
13
|
import type { Ask, Asker, Invitation, Json, JsonObject, Wanted } from '../being/types.ts';
|
|
14
14
|
import { put } from './partition.ts';
|
|
15
|
-
import { seen } from './door.ts';
|
|
16
|
-
import { within, DEFAULT, LATE } from './allowance.ts';
|
|
15
|
+
import { seen, answered } from './door.ts';
|
|
16
|
+
import { within, allow, DEFAULT, LATE } from './allowance.ts';
|
|
17
17
|
import type { Resident } from './ward.ts';
|
|
18
18
|
|
|
19
19
|
export type OwnerSide = {
|
|
@@ -39,6 +39,11 @@ export const OWNER_ASKS: Ask[] = [
|
|
|
39
39
|
},
|
|
40
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
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
|
+
},
|
|
42
47
|
];
|
|
43
48
|
|
|
44
49
|
export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | undefined, args: JsonObject): Promise<Json> {
|
|
@@ -92,7 +97,7 @@ export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | u
|
|
|
92
97
|
const notes = args.notes;
|
|
93
98
|
const door = w.doors.get(being);
|
|
94
99
|
if (!door) return { error: 'no such being' };
|
|
95
|
-
return await door.stance.occupants.invite(id, notes !== null && typeof notes === 'object' && !Array.isArray(notes) ? (notes
|
|
100
|
+
return await door.stance.occupants.invite(id, notes !== null && typeof notes === 'object' && !Array.isArray(notes) ? (notes) : undefined);
|
|
96
101
|
}
|
|
97
102
|
if (method === 'knock') {
|
|
98
103
|
const b = args.being as string | { boot: unknown; key: unknown };
|
|
@@ -121,7 +126,7 @@ export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | u
|
|
|
121
126
|
const out = await door.stance.standings.knock(inv, args.method as string | undefined, (args.args as JsonObject) ?? {}, args.wanted as Wanted | undefined);
|
|
122
127
|
if (isSilence(out)) return { error: 'silence' };
|
|
123
128
|
if (isWord(out)) return { error: wordOf(out) }; // the owner hears objects: the word is the error's name
|
|
124
|
-
return { taken: await door.stance.standings.take(id, inv), answer: out
|
|
129
|
+
return { taken: await door.stance.standings.take(id, inv), answer: out };
|
|
125
130
|
}
|
|
126
131
|
if (method === 'remove') {
|
|
127
132
|
// The mirror of knock: a relation out of a being, by id, occupant or
|
|
@@ -152,6 +157,36 @@ export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | u
|
|
|
152
157
|
const removed = w.unboot(being);
|
|
153
158
|
return removed === null ? { error: 'no such being' } : { unbooted: being, removed };
|
|
154
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
|
+
}
|
|
155
190
|
return { error: 'unknown ask' };
|
|
156
191
|
}
|
|
157
192
|
|
|
@@ -166,7 +201,7 @@ async function describe(w: OwnerSide): Promise<Json> {
|
|
|
166
201
|
// answers its owner: the harbor learns the pk by this very ask at boot, so
|
|
167
202
|
// an unbounded wait here is a ward no restart can bring back.
|
|
168
203
|
const asked = [...w.doors].filter(([key]) => key !== w.pk);
|
|
169
|
-
const digests = await Promise.all(asked.map(async ([, door]) => (await within(DEFAULT.time, seen(door, { id: OWNER })))
|
|
204
|
+
const digests = await Promise.all(asked.map(async ([, door]) => (await within(DEFAULT.time, seen(door, { id: OWNER })))));
|
|
170
205
|
for (const [i, [key, door]] of asked.entries()) {
|
|
171
206
|
const d = digests[i];
|
|
172
207
|
put(beings, key, { class: door.cells.class ?? null, public: w.publicKey() === key, digest: d === LATE ? null : d });
|
package/src/ward/seal.ts
CHANGED
|
@@ -37,6 +37,15 @@ export type WardKey = { sign: Uint8Array; padlock: Uint8Array; signPk: Uint8Arra
|
|
|
37
37
|
const WARD_SIGN = new TextEncoder().encode('quo-ward-sign');
|
|
38
38
|
const WARD_SEAL = new TextEncoder().encode('quo-ward-seal');
|
|
39
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
|
+
|
|
40
49
|
// The ward's key from its seed. Its pk on the wire is the signing pk then the padlock, 128 hex.
|
|
41
50
|
//
|
|
42
51
|
// One seed, two curves, and each secret derived from it under its own label.
|
|
@@ -101,7 +110,7 @@ export async function sealAsk(to: string | null, payload: AskPayload, signer: Ui
|
|
|
101
110
|
// Verification is the door's own step, because only the door knows which key may speak.
|
|
102
111
|
export async function openAsk(bytes: Uint8Array, padlockSecret: Uint8Array): Promise<{ to: string | null; payload: AskPayload; body: Uint8Array; signature: Uint8Array; ephemeralPk: Uint8Array } | null> {
|
|
103
112
|
try {
|
|
104
|
-
if (!(bytes instanceof Uint8Array) || bytes.length < 1) return null;
|
|
113
|
+
if (!(bytes instanceof Uint8Array) || bytes.length < 1 || bytes.length > SIZE) return null;
|
|
105
114
|
const inside = await unbox(bytes, padlockSecret);
|
|
106
115
|
if (inside.length <= SIGNATURE) return null;
|
|
107
116
|
const body = inside.subarray(0, inside.length - SIGNATURE);
|
|
@@ -146,7 +155,7 @@ export async function sealReply(reply: ReplyPayload, ephemeralPk: Uint8Array, wa
|
|
|
146
155
|
// throw or an absent value for what a stranger wrote.
|
|
147
156
|
export async function openReply(bytes: unknown, ephemeralSecret: Uint8Array, signPk: Uint8Array): Promise<ReplyPayload | null> {
|
|
148
157
|
try {
|
|
149
|
-
if (!(bytes instanceof Uint8Array)) return null;
|
|
158
|
+
if (!(bytes instanceof Uint8Array) || bytes.length > SIZE) return null;
|
|
150
159
|
const inside = await unbox(bytes, ephemeralSecret);
|
|
151
160
|
if (inside.length <= SIGNATURE) return null;
|
|
152
161
|
const body = inside.subarray(0, inside.length - SIGNATURE);
|
package/src/ward/stance.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// The one stance builder. Used for every being the ward boots. Nothing
|
|
3
3
|
// outer is in what she holds: ids in, values out, keys in the bind table.
|
|
4
|
-
import { silence, isSilence, isWord, word, wordOf } from '../being/silence.ts';
|
|
4
|
+
import { silence, isSilence, isWord, unreached, word, wordOf } from '../being/silence.ts';
|
|
5
5
|
import { digest } from '../being/digest.ts';
|
|
6
6
|
import { RESERVED_IDS, isBlueprint } from '../being/types.ts';
|
|
7
7
|
import type { Answer, Cells, Invitation, JsonObject, Silence, Stance, Standing, Standings, Wanted, Word } from '../being/types.ts';
|
|
8
8
|
import { at, put, drop, prune, type Bind, type StandingKeys } from './partition.ts';
|
|
9
9
|
import { allow, within, LATE } from './allowance.ts';
|
|
10
10
|
import { isHex, isWardPk, type ReplyPayload } from './seal.ts';
|
|
11
|
-
import { cellFault } from './cells.ts';
|
|
11
|
+
import { cellFault, unkeepable } from './cells.ts';
|
|
12
12
|
|
|
13
13
|
// What the stance needs from its ward, and no more.
|
|
14
14
|
export type Inside = {
|
|
@@ -29,6 +29,11 @@ export type Inside = {
|
|
|
29
29
|
instantiate(className: string, key: string): string | null;
|
|
30
30
|
// mint an occupant on a being of this ward, for the being who made her.
|
|
31
31
|
relate(key: string, id: string): Promise<Invitation | null>;
|
|
32
|
+
// an invitation to one of the harbor's own beings, by the harbor's name for
|
|
33
|
+
// it, handed to the taker and never returned. false where this box lends
|
|
34
|
+
// nothing of that name to this ward, or where the taker did not take it, in
|
|
35
|
+
// which case the harbor has already removed what it minted.
|
|
36
|
+
lend(name: string, take: (invitation: Invitation) => Promise<boolean>): Promise<boolean>;
|
|
32
37
|
// take a being of this ward out again, with every relation she holds.
|
|
33
38
|
unmake(key: string): void;
|
|
34
39
|
// the bind table was written: a record put, dropped or moved on.
|
|
@@ -43,9 +48,22 @@ export type SealedInvitation = { ward: string; heir?: string; secret?: string };
|
|
|
43
48
|
// is the call, whatever record sits under that name. One namespace means one
|
|
44
49
|
// list: these are refused at invite and at take, beside the ward's own words.
|
|
45
50
|
const CALLS = ['knock', 'take', 'remove'];
|
|
46
|
-
const reserved = (id: string): boolean => RESERVED_IDS.includes(id) || CALLS.includes(id);
|
|
47
51
|
|
|
48
52
|
export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bind): Stance {
|
|
53
|
+
// A word the ward refuses at every mint and every take. The two the
|
|
54
|
+
// protocol names, and the ones this kit adds because its own spelling took
|
|
55
|
+
// them: the calls a standing already answers to, and a name no partition
|
|
56
|
+
// can hold. That last one is refused here rather than by the cells guard,
|
|
57
|
+
// because the guard throws and this table promises a null.
|
|
58
|
+
// A method is a word or it is nothing at all. The seal writes a payload as
|
|
59
|
+
// JSON, which drops a key whose value it cannot write, so a symbol or a
|
|
60
|
+
// function here would leave as an ask with no method: the empty ask. She
|
|
61
|
+
// would have asked for work, been handed a blueprint, and spent a number on
|
|
62
|
+
// it. Nothing that cannot be sent is sent, and what never left is unreached,
|
|
63
|
+
// which is the one answer that says asking again is safe. This is the same
|
|
64
|
+
// rule args are already held to.
|
|
65
|
+
const named = (method: unknown): boolean => method === undefined || typeof method === 'string';
|
|
66
|
+
const reserved = (id: string): boolean => RESERVED_IDS.includes(id) || CALLS.includes(id) || unkeepable(id);
|
|
49
67
|
// The name of one relation, as her side files it. A relation is a ward and
|
|
50
68
|
// a heir, never a heir alone: the heir pk is outer, it rides in the clear on
|
|
51
69
|
// every lid, and anyone who reads one can quote it back inside an invitation
|
|
@@ -224,6 +242,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
|
|
|
224
242
|
const calls = {
|
|
225
243
|
knock: async (inv: Invitation, method?: string, args: JsonObject = {}, wanted?: Wanted): Promise<Answer> => {
|
|
226
244
|
if (gone()) return word('dropped');
|
|
245
|
+
if (!named(method)) return unreached();
|
|
227
246
|
if (!valid(inv)) return word('invitation'); // S1. nothing is sent
|
|
228
247
|
// She may knock again, and after take that knock is an ask: the relation
|
|
229
248
|
// has one home, so it answers on the standing's lane, under the
|
|
@@ -283,6 +302,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
|
|
|
283
302
|
// the next line. Waiting for the lane is not a reason to lose it.
|
|
284
303
|
ask: (method?: string, args: JsonObject = {}, wanted?: Wanted): Promise<Answer> => {
|
|
285
304
|
if (gone()) return Promise.resolve(word('dropped') as Answer);
|
|
305
|
+
if (!named(method)) return Promise.resolve(unreached() as Answer);
|
|
286
306
|
const keys = at(bind.standings, id);
|
|
287
307
|
if (!keys) return Promise.resolve(word('dropped') as Answer); // S2. she dropped it between one line and the next
|
|
288
308
|
const rec = at(cells.standings, id);
|
|
@@ -349,6 +369,12 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
|
|
|
349
369
|
// it keeps the pk beside the id and nothing else. the invitation IS the key.
|
|
350
370
|
invite: async (id: string, notes?: JsonObject): Promise<Invitation | null> => {
|
|
351
371
|
if (gone() || reserved(id) || at(cells.occupants, id) || at(cells.standings, id)) return null;
|
|
372
|
+
// Notes are values, like everything else the partition keeps, and a
|
|
373
|
+
// spread copies one level: an object inside them would stay the
|
|
374
|
+
// caller's, a handle into her cells that writes past the guard and
|
|
375
|
+
// never says `wrote`, so what a restart brought back would not be
|
|
376
|
+
// what she read. Held to the rule and copied whole, or no invitation.
|
|
377
|
+
if (notes !== undefined && cellFault(notes, 'notes') !== null) return null;
|
|
352
378
|
// The key first, and nothing written until it exists: a record put
|
|
353
379
|
// before the mint would be a record a remove in the meantime drops
|
|
354
380
|
// with no heir to close, and the heir opened after it would name an
|
|
@@ -357,7 +383,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
|
|
|
357
383
|
if (reserved(id) || at(cells.occupants, id) || at(cells.standings, id)) return null; // taken while the key was minted
|
|
358
384
|
// The notes are the terms the inviter minted under, hers to read on her
|
|
359
385
|
// gate. She may write more later; nobody outside ever writes them.
|
|
360
|
-
put(cells.occupants, id, { id, notes: notes ?
|
|
386
|
+
put(cells.occupants, id, { id, notes: notes ? (JSON.parse(JSON.stringify(notes)) as JsonObject) : {} });
|
|
361
387
|
inside.openHeir(k.pk, key, id);
|
|
362
388
|
put(bind.occupants, id, k.pk);
|
|
363
389
|
inside.wrote();
|
|
@@ -373,5 +399,35 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
|
|
|
373
399
|
},
|
|
374
400
|
},
|
|
375
401
|
standings,
|
|
402
|
+
// A standing at one of the things this device can do. The same three
|
|
403
|
+
// moves her boot makes, with the harbor's root doing the inviting instead
|
|
404
|
+
// of a being of this ward: ask the ground for the name, knock with what
|
|
405
|
+
// comes back, take it under the id she gave. She is handed the id.
|
|
406
|
+
//
|
|
407
|
+
// The invitation never reaches her, and that is the difference between
|
|
408
|
+
// this relation and every other she holds. Her own invitations are hers
|
|
409
|
+
// to give away, because giving one away is giving away her own relation.
|
|
410
|
+
// This one is the device's, minted for this ward alone, and a value she
|
|
411
|
+
// could copy is a capability she could hand to anyone. A being who wants
|
|
412
|
+
// to lend her device access to another does it in the open, by an ask of
|
|
413
|
+
// her own that forwards to this standing, where her gate reads who is
|
|
414
|
+
// asking and she can stop.
|
|
415
|
+
lend: async (name: string, id: string): Promise<string | null> => {
|
|
416
|
+
if (gone() || typeof name !== 'string' || typeof id !== 'string') return null;
|
|
417
|
+
if (reserved(id) || at(cells.standings, id) || at(cells.occupants, id)) return null;
|
|
418
|
+
// Her half runs inside the harbor's call, so a knock that was refused
|
|
419
|
+
// and a take that lost the id both end with the harbor removing what it
|
|
420
|
+
// minted. Nothing half-lives here either.
|
|
421
|
+
const took = await inside.lend(name, async (inv) => {
|
|
422
|
+
// Read again: the ground was awaited, and a line of hers in between
|
|
423
|
+
// may have taken the id. Nothing is written for a relation she cannot
|
|
424
|
+
// hold, and the invitation goes back unspent.
|
|
425
|
+
if (gone() || reserved(id) || at(cells.standings, id) || at(cells.occupants, id)) return false;
|
|
426
|
+
const out = await calls.knock(inv);
|
|
427
|
+
if (isSilence(out) || isWord(out)) return false;
|
|
428
|
+
return (await calls.take(id, inv)) === id;
|
|
429
|
+
});
|
|
430
|
+
return took ? id : null;
|
|
431
|
+
},
|
|
376
432
|
};
|
|
377
433
|
}
|
package/src/ward/ward.ts
CHANGED
|
@@ -198,7 +198,7 @@ class Self implements BeingLike {
|
|
|
198
198
|
#boot(key: string, make: (stance: Stance) => BeingLike | null): Resident | null {
|
|
199
199
|
const cells = guardCells(at(this.p.beings, key) ?? emptyCells(), () => this.#wrote());
|
|
200
200
|
const bind = at(this.p.bind, key) ?? emptyBind();
|
|
201
|
-
const stance = buildStance(
|
|
201
|
+
const stance: Stance = buildStance(
|
|
202
202
|
{
|
|
203
203
|
pk: this.pk,
|
|
204
204
|
// This stance, not merely this key: unboot and boot again under the
|
|
@@ -219,6 +219,11 @@ class Self implements BeingLike {
|
|
|
219
219
|
// never came about takes her out again.
|
|
220
220
|
relate: async (k, id) => (await this.doors.get(k)?.stance.occupants.invite(id)) ?? null,
|
|
221
221
|
unmake: (k) => void this.#unboot(k),
|
|
222
|
+
// What this device lends this ward's beings. The ward knocks and takes
|
|
223
|
+
// inside the harbor's own call and reads nothing in the value: which
|
|
224
|
+
// names there are, which ward may ask for one, and what becomes of one
|
|
225
|
+
// she did not take, is the harbor's and no word of the ward.
|
|
226
|
+
lend: async (name, take) => (await this.g.lend?.(name, take)) ?? false,
|
|
222
227
|
wrote: () => this.#wrote(),
|
|
223
228
|
},
|
|
224
229
|
key,
|