@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/dist/ward/cells.js
CHANGED
|
@@ -2,32 +2,71 @@
|
|
|
2
2
|
// and a peer chooses the depth of what she answers; a bound keeps a hostile
|
|
3
3
|
// reply from ending a walk in a stack overflow instead of a refusal.
|
|
4
4
|
export const DEPTH = 64;
|
|
5
|
+
// How many values one written out may come to. Depth alone does not bound
|
|
6
|
+
// what a value costs to keep: one subvalue may sit under two keys, and
|
|
7
|
+
// nesting that forty levels deep is inside the depth bound and is a trillion
|
|
8
|
+
// values once written down. What is walked here is the object graph, which is
|
|
9
|
+
// small; what a harbor keeps is the tree it expands to, which is not. So the
|
|
10
|
+
// walk counts the tree while it proves the graph, and refuses a value nobody
|
|
11
|
+
// could write down. This is a bound on what a being holds, so it is this
|
|
12
|
+
// kit's number and no word of the spec.
|
|
13
|
+
export const BREADTH = 1 << 20;
|
|
14
|
+
// Text a harbor can write down: every surrogate in a pair. A lone one is a
|
|
15
|
+
// code unit with no character, which JSON escapes and UTF-8 cannot encode at
|
|
16
|
+
// all, so a harbor that keeps strings as bytes loses it and one that keeps
|
|
17
|
+
// the object does not. I-JSON says a string is text, and this is that rule
|
|
18
|
+
// where the value is written rather than where it is read.
|
|
19
|
+
const WELL_FORMED = /^(?:[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])*$/;
|
|
5
20
|
// I-JSON, all the way down. A number that JSON cannot write is not a number
|
|
6
21
|
// a harbor can keep, a key on the prototype is not a key she wrote, and a
|
|
7
22
|
// hole in a list, or a key named `__proto__`, is a thing JSON writes one way
|
|
8
23
|
// and a runtime reads another.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
24
|
+
//
|
|
25
|
+
// A fault is a sentence; anything else is the count of values under this one,
|
|
26
|
+
// itself included. A value already counted is a value already proven, so a
|
|
27
|
+
// graph that shares is walked once per node rather than once per path.
|
|
28
|
+
function walk(v, path, seen, sized, depth) {
|
|
29
|
+
if (v === null || typeof v === 'boolean')
|
|
30
|
+
return 1;
|
|
31
|
+
if (typeof v === 'string')
|
|
32
|
+
return WELL_FORMED.test(v) ? 1 : `${path} carries a lone surrogate, which is no text a harbor can write down`;
|
|
33
|
+
if (typeof v === 'number') {
|
|
34
|
+
if (!Number.isFinite(v))
|
|
35
|
+
return `${path} is ${String(v)}, which no harbor can write down`;
|
|
36
|
+
// Minus zero is a number JSON writes as `0` and reads back as `0`. A
|
|
37
|
+
// harbor that keeps values would hand her the sign she wrote and one that
|
|
38
|
+
// writes JSON would not, and she is never told which she is standing in.
|
|
39
|
+
return Object.is(v, -0) ? `${path} is minus zero, which comes back as zero from every harbor that writes JSON` : 1;
|
|
40
|
+
}
|
|
14
41
|
if (typeof v !== 'object')
|
|
15
42
|
return `${path} is a ${typeof v}, which is not a value`;
|
|
16
43
|
if (seen.has(v))
|
|
17
44
|
return `${path} refers back to itself`;
|
|
45
|
+
const counted = sized.get(v);
|
|
46
|
+
if (counted !== undefined)
|
|
47
|
+
return counted;
|
|
18
48
|
if (depth >= DEPTH)
|
|
19
49
|
return `${path} is nested past ${DEPTH} levels, which no harbor can keep`;
|
|
20
50
|
seen.add(v);
|
|
21
51
|
try {
|
|
52
|
+
let size = 1;
|
|
53
|
+
const under = (where, x) => {
|
|
54
|
+
const r = walk(x, where, seen, sized, depth + 1);
|
|
55
|
+
if (typeof r === 'string')
|
|
56
|
+
return r;
|
|
57
|
+
size += r;
|
|
58
|
+
return size > BREADTH ? `${path} writes out to more than ${BREADTH} values, which no harbor can keep` : null;
|
|
59
|
+
};
|
|
22
60
|
if (Array.isArray(v)) {
|
|
23
61
|
for (let i = 0; i < v.length; i += 1) {
|
|
24
62
|
if (!(i in v))
|
|
25
63
|
return `${path}[${i}] is a hole, which no harbor can write down`;
|
|
26
|
-
const f =
|
|
64
|
+
const f = under(`${path}[${i}]`, v[i]);
|
|
27
65
|
if (f)
|
|
28
66
|
return f;
|
|
29
67
|
}
|
|
30
|
-
|
|
68
|
+
sized.set(v, size);
|
|
69
|
+
return size;
|
|
31
70
|
}
|
|
32
71
|
const proto = Object.getPrototypeOf(v);
|
|
33
72
|
if (proto !== Object.prototype && proto !== null)
|
|
@@ -35,17 +74,21 @@ function fault(v, path, seen, depth) {
|
|
|
35
74
|
for (const k of Object.keys(v)) {
|
|
36
75
|
if (k === '__proto__')
|
|
37
76
|
return `${path}.__proto__ is a key no harbor can keep`;
|
|
38
|
-
const f =
|
|
77
|
+
const f = under(`${path}.${k}`, v[k]);
|
|
39
78
|
if (f)
|
|
40
79
|
return f;
|
|
41
80
|
}
|
|
42
|
-
|
|
81
|
+
sized.set(v, size);
|
|
82
|
+
return size;
|
|
43
83
|
}
|
|
44
84
|
finally {
|
|
45
85
|
seen.delete(v);
|
|
46
86
|
}
|
|
47
87
|
}
|
|
48
|
-
export const cellFault = (v, path) =>
|
|
88
|
+
export const cellFault = (v, path) => {
|
|
89
|
+
const out = walk(v, path, new Set(), new Map(), 0);
|
|
90
|
+
return typeof out === 'string' ? out : null;
|
|
91
|
+
};
|
|
49
92
|
// The three keys at the root of her cells that are the ward's: it writes
|
|
50
93
|
// them, she reads them, and a write of hers there is refused like a non-value.
|
|
51
94
|
const WARDS = new Set(['standings', 'occupants', 'class']);
|
|
@@ -56,6 +99,12 @@ const WARDS = new Set(['standings', 'occupants', 'class']);
|
|
|
56
99
|
const wrapped = new WeakMap();
|
|
57
100
|
const targets = new WeakMap();
|
|
58
101
|
const guards = new WeakSet();
|
|
102
|
+
// A name no partition can hold, wherever it appears. It is here beside the
|
|
103
|
+
// guard that refuses it, because the ward refuses it too: a relation named
|
|
104
|
+
// this one would be written into the bind table and then throw on her cells,
|
|
105
|
+
// which is half a relation and a throw where a null is promised. One rule,
|
|
106
|
+
// one place, read by the guard and by the stance.
|
|
107
|
+
export const unkeepable = (k) => k === '__proto__';
|
|
59
108
|
// A key a write may land on: a string that is not `__proto__`, and not one
|
|
60
109
|
// of the ward's at the root. A symbol key is a thing JSON never writes.
|
|
61
110
|
const refuse = (why) => {
|
|
@@ -64,7 +113,7 @@ const refuse = (why) => {
|
|
|
64
113
|
function keyFault(t, k, v, path, root) {
|
|
65
114
|
if (typeof k !== 'string')
|
|
66
115
|
refuse(`${path} takes no symbol key`);
|
|
67
|
-
if (k
|
|
116
|
+
if (unkeepable(k))
|
|
68
117
|
refuse(`${path}.__proto__ is a key no harbor can keep`);
|
|
69
118
|
if (root && WARDS.has(k))
|
|
70
119
|
refuse(`${path}.${k} is the ward's to write`);
|
package/dist/ward/door.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export type Judged = {
|
|
|
12
12
|
bytes: Uint8Array;
|
|
13
13
|
heard: boolean;
|
|
14
14
|
};
|
|
15
|
+
export declare function answered(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload>;
|
|
15
16
|
export declare function arrive(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload>;
|
|
16
17
|
export declare function seen(door: Door, asker: Asker): Promise<string | null>;
|
|
17
18
|
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
|
@@ -16,10 +16,13 @@ import { KEY, sealingPair } from './arithmetic.js';
|
|
|
16
16
|
import { cellFault } from './cells.js';
|
|
17
17
|
const SILENCE = { silence: true };
|
|
18
18
|
const said = (quo) => ({ quo });
|
|
19
|
-
// One arrival at one being, already named
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
19
|
+
// One arrival at one being, already named: the three choices, D11, D12 and
|
|
20
|
+
// D13, and nothing else. Catches every throw. `bound` says whether the asker
|
|
21
|
+
// is a key this door holds, or the ward's own owner: she hears `threw`; a
|
|
22
|
+
// stranger at the public being hears silence, because her insides are hers.
|
|
23
|
+
// Every caller that reaches a being's answer goes through here, the owner's
|
|
24
|
+
// `ask` included, so the three choices are written once.
|
|
25
|
+
export async function answered(door, asker, method, args, bound) {
|
|
23
26
|
const threw = bound ? said('threw') : SILENCE;
|
|
24
27
|
let out;
|
|
25
28
|
try {
|
|
@@ -42,18 +45,34 @@ export async function arrive(door, asker, method, args, bound) {
|
|
|
42
45
|
// was spent. It is threw, like a word out of her.
|
|
43
46
|
if (cellFault(out, 'answer') !== null)
|
|
44
47
|
return threw;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
return { object: out, seen: null };
|
|
49
|
+
}
|
|
50
|
+
// What crosses the wire: her answer, with her digest for this asker beside
|
|
51
|
+
// it. The digest rides along, it is not the answer. She has already
|
|
52
|
+
// answered: a describe that will not run costs the digest, and nothing else.
|
|
53
|
+
// The owner does not take one, because the owner asks for a describe when it
|
|
54
|
+
// wants one and nothing is sealed on its behalf.
|
|
55
|
+
export async function arrive(door, asker, method, args, bound) {
|
|
56
|
+
const reply = await answered(door, asker, method, args, bound);
|
|
57
|
+
if (method === undefined || !('object' in reply))
|
|
58
|
+
return reply;
|
|
59
|
+
return { object: reply.object, seen: await seen(door, asker) };
|
|
50
60
|
}
|
|
51
61
|
// Her digest for this asker, or null when her describe threw or fell silent.
|
|
52
62
|
// The owner's describe reads it the same way for every being of the ward.
|
|
53
63
|
export async function seen(door, asker) {
|
|
54
64
|
try {
|
|
55
65
|
const bp = await door.being.answer(asker);
|
|
56
|
-
|
|
66
|
+
if (isSilence(bp) || isWord(bp))
|
|
67
|
+
return null;
|
|
68
|
+
// Held to the same rule as her answer, and for the same reason plus one:
|
|
69
|
+
// the digest is a walk, and a walk over a graph that shares or turns back
|
|
70
|
+
// on itself is not a digest but a hang or a throw. A blueprint that could
|
|
71
|
+
// not cross is a blueprint with no digest, which is what she has when her
|
|
72
|
+
// describe says nothing.
|
|
73
|
+
if (cellFault(bp, 'blueprint') !== null)
|
|
74
|
+
return null;
|
|
75
|
+
return await digest(bp);
|
|
57
76
|
}
|
|
58
77
|
catch {
|
|
59
78
|
return null;
|
package/dist/ward/ground.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Stance, BeingLike, BeingClass } from '../being/types.ts';
|
|
1
|
+
import type { Stance, BeingLike, BeingClass, Invitation } from '../being/types.ts';
|
|
2
|
+
export type Lend = (name: string, take: (invitation: Invitation) => Promise<boolean>) => Promise<boolean>;
|
|
2
3
|
export type Ground = {
|
|
3
4
|
seed: string | Uint8Array;
|
|
4
5
|
memory: Record<string, unknown>;
|
|
@@ -6,6 +7,7 @@ export type Ground = {
|
|
|
6
7
|
wrote?: () => void;
|
|
7
8
|
carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
|
|
8
9
|
random(n: number): Uint8Array;
|
|
10
|
+
lend?: Lend;
|
|
9
11
|
};
|
|
10
12
|
export type WardPointers = {
|
|
11
13
|
door(bytes: Uint8Array): Promise<{
|
package/dist/ward/ground.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
|
|
2
|
+
// harbor in this tree writes the same three and the protocol keeps the harbors
|
|
3
3
|
// themselves apart: what a memory harbor and a real one differ on is the
|
|
4
4
|
// route and the store, and nothing here. A second kit writes its own harbor
|
|
5
5
|
// and may write these again; they are convenience, never contract.
|
package/dist/ward/heirs.js
CHANGED
|
@@ -103,6 +103,13 @@ export class Heirs {
|
|
|
103
103
|
}
|
|
104
104
|
if (by === h.announced)
|
|
105
105
|
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)
|
|
113
|
+
h.announced = next;
|
|
107
114
|
}
|
|
108
115
|
}
|
package/dist/ward/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { Ward } from './ward.ts';
|
|
2
|
-
export type { Ground, WardPointers } from './ground.ts';
|
|
2
|
+
export type { Ground, WardPointers, Lend } from './ground.ts';
|
|
3
3
|
export type { Partition, Heir, Bind, StandingKeys } from './partition.ts';
|
|
4
|
-
export { GONE, MINTED } from './partition.ts';
|
|
4
|
+
export { GONE, MINTED, KNOCKS } from './partition.ts';
|
|
5
5
|
export type { AskPayload, ReplyPayload } from './seal.ts';
|
|
6
6
|
export * as seal from './seal.ts';
|
|
7
7
|
export * as arithmetic from './arithmetic.ts';
|
package/dist/ward/index.js
CHANGED
|
@@ -1,9 +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
|
|
5
|
-
// same ones and for a test that pins them.
|
|
6
|
-
export { GONE, MINTED } from './partition.js';
|
|
4
|
+
// The three bounds on what a partition may hold, for a kit that must keep
|
|
5
|
+
// the same ones and for a test that pins them.
|
|
6
|
+
export { GONE, MINTED, KNOCKS } from './partition.js';
|
|
7
7
|
// The seal and the arithmetic, for a kit in another language to check its
|
|
8
8
|
// bytes against, and for tests that speak to a door directly.
|
|
9
9
|
export * as seal from './seal.js';
|
package/dist/ward/owner.js
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
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.js';
|
|
12
12
|
import { OWNER } from '../being/types.js';
|
|
13
13
|
import { put } from './partition.js';
|
|
14
|
-
import { seen } from './door.js';
|
|
15
|
-
import { within, DEFAULT, LATE } from './allowance.js';
|
|
14
|
+
import { seen, answered } from './door.js';
|
|
15
|
+
import { within, allow, DEFAULT, LATE } from './allowance.js';
|
|
16
16
|
const str = { type: 'string' };
|
|
17
17
|
export const OWNER_ASKS = [
|
|
18
18
|
{ name: 'boot', description: 'boot a being by class name, under a key the owner chooses', input: { type: 'object', properties: { key: str, class: str }, required: ['key', 'class'] } },
|
|
@@ -25,6 +25,11 @@ export const OWNER_ASKS = [
|
|
|
25
25
|
},
|
|
26
26
|
{ 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'] } },
|
|
27
27
|
{ 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'] } },
|
|
28
|
+
{
|
|
29
|
+
name: 'ask',
|
|
30
|
+
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',
|
|
31
|
+
input: { type: 'object', properties: { being: str, method: str, args: { type: 'object' }, wanted: { type: 'object', properties: { time: { type: 'number' } } } } },
|
|
32
|
+
},
|
|
28
33
|
];
|
|
29
34
|
export async function ownerAnswer(w, asker, method, args) {
|
|
30
35
|
// Every name the owner gives is a string, or it is nothing. Coerced instead
|
|
@@ -82,7 +87,7 @@ export async function ownerAnswer(w, asker, method, args) {
|
|
|
82
87
|
const door = w.doors.get(being);
|
|
83
88
|
if (!door)
|
|
84
89
|
return { error: 'no such being' };
|
|
85
|
-
return await door.stance.occupants.invite(id, notes !== null && typeof notes === 'object' && !Array.isArray(notes) ? notes : undefined);
|
|
90
|
+
return await door.stance.occupants.invite(id, notes !== null && typeof notes === 'object' && !Array.isArray(notes) ? (notes) : undefined);
|
|
86
91
|
}
|
|
87
92
|
if (method === 'knock') {
|
|
88
93
|
const b = args.being;
|
|
@@ -152,6 +157,42 @@ export async function ownerAnswer(w, asker, method, args) {
|
|
|
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)
|
|
169
|
+
return { error: 'no such being' };
|
|
170
|
+
// A method is a name or it is the empty ask. Coerced instead of checked,
|
|
171
|
+
// a shape passed where a name belongs becomes a string she never declared,
|
|
172
|
+
// and she would answer `unknown ask` to something nobody asked.
|
|
173
|
+
if (args.method !== undefined && typeof args.method !== 'string')
|
|
174
|
+
return { error: 'an ask is named by a word' };
|
|
175
|
+
// No being named is the in-process twin of bytes for nobody: the public
|
|
176
|
+
// being, asked as nobody, unbound, so a throw of hers is silence. Her
|
|
177
|
+
// insides are not a stranger's to read, and a device serving her to
|
|
178
|
+
// strangers must hear what a stranger hears and not one word more.
|
|
179
|
+
const key = being ?? w.publicKey();
|
|
180
|
+
const door = key === null ? undefined : w.doors.get(key);
|
|
181
|
+
if (!door || door.key === w.pk)
|
|
182
|
+
return { error: 'no such being' };
|
|
183
|
+
const bound = being !== null;
|
|
184
|
+
// The owner is a caller like any other: wanted says what this ask may
|
|
185
|
+
// spend, and saying nothing is the ward's default. A wait that ran out is
|
|
186
|
+
// late, the same word a being would hear, said as the object an owner hears.
|
|
187
|
+
const out = await within(allow(args.wanted).time, answered(door, bound ? { id: OWNER } : {}, args.method, args.args ?? {}, bound));
|
|
188
|
+
if (out === LATE)
|
|
189
|
+
return { error: 'late' };
|
|
190
|
+
if ('quo' in out)
|
|
191
|
+
return { error: out.quo };
|
|
192
|
+
if (!('object' in out))
|
|
193
|
+
return { error: 'silence' };
|
|
194
|
+
return out.object;
|
|
195
|
+
}
|
|
155
196
|
return { error: 'unknown ask' };
|
|
156
197
|
}
|
|
157
198
|
// The ward's describe for its owner: its beings, their classes, a digest each,
|
package/dist/ward/seal.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type WardKey = {
|
|
|
6
6
|
padlockPk: Uint8Array;
|
|
7
7
|
pk: string;
|
|
8
8
|
};
|
|
9
|
+
export declare const SIZE: number;
|
|
9
10
|
export declare function wardKey(seed: string | Uint8Array): Promise<WardKey>;
|
|
10
11
|
export declare const wardSignPk: (pk: string) => Uint8Array;
|
|
11
12
|
export declare const wardPadlock: (pk: string) => Uint8Array;
|
package/dist/ward/seal.js
CHANGED
|
@@ -10,6 +10,14 @@ const text = new TextDecoder();
|
|
|
10
10
|
// answering to one name is how a kit is read wrong.
|
|
11
11
|
const WARD_SIGN = new TextEncoder().encode('quo-ward-sign');
|
|
12
12
|
const WARD_SEAL = new TextEncoder().encode('quo-ward-seal');
|
|
13
|
+
// The one size in Quo: one mebibyte of bytes each way. It is read before
|
|
14
|
+
// anything is opened, so bytes above it are never decrypted, never parsed and
|
|
15
|
+
// never allocated against. Refusing them is bytes that said nothing, which is
|
|
16
|
+
// already silence, so there is no tenth word and no new door case. Breadth
|
|
17
|
+
// costs bytes, so this one number bounds every other breadth on the wire, and
|
|
18
|
+
// what a being holds is not the protocol's business. An ask is a message and
|
|
19
|
+
// not a file; what is larger is asked for in pieces.
|
|
20
|
+
export const SIZE = 1024 * 1024;
|
|
13
21
|
// The ward's key from its seed. Its pk on the wire is the signing pk then the padlock, 128 hex.
|
|
14
22
|
//
|
|
15
23
|
// One seed, two curves, and each secret derived from it under its own label.
|
|
@@ -55,7 +63,7 @@ export async function sealAsk(to, payload, signer, padlock, seed) {
|
|
|
55
63
|
// Verification is the door's own step, because only the door knows which key may speak.
|
|
56
64
|
export async function openAsk(bytes, padlockSecret) {
|
|
57
65
|
try {
|
|
58
|
-
if (!(bytes instanceof Uint8Array) || bytes.length < 1)
|
|
66
|
+
if (!(bytes instanceof Uint8Array) || bytes.length < 1 || bytes.length > SIZE)
|
|
59
67
|
return null;
|
|
60
68
|
const inside = await unbox(bytes, padlockSecret);
|
|
61
69
|
if (inside.length <= SIGNATURE)
|
|
@@ -109,7 +117,7 @@ export async function sealReply(reply, ephemeralPk, wardSign, seed) {
|
|
|
109
117
|
// throw or an absent value for what a stranger wrote.
|
|
110
118
|
export async function openReply(bytes, ephemeralSecret, signPk) {
|
|
111
119
|
try {
|
|
112
|
-
if (!(bytes instanceof Uint8Array))
|
|
120
|
+
if (!(bytes instanceof Uint8Array) || bytes.length > SIZE)
|
|
113
121
|
return null;
|
|
114
122
|
const inside = await unbox(bytes, ephemeralSecret);
|
|
115
123
|
if (inside.length <= SIGNATURE)
|
package/dist/ward/stance.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type Inside = {
|
|
|
13
13
|
send(bind: Bind, keys: StandingKeys, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<ReplyPayload | Silence | Word>;
|
|
14
14
|
instantiate(className: string, key: string): string | null;
|
|
15
15
|
relate(key: string, id: string): Promise<Invitation | null>;
|
|
16
|
+
lend(name: string, take: (invitation: Invitation) => Promise<boolean>): Promise<boolean>;
|
|
16
17
|
unmake(key: string): void;
|
|
17
18
|
wrote(): void;
|
|
18
19
|
};
|
package/dist/ward/stance.js
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
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.js';
|
|
4
|
+
import { silence, isSilence, isWord, unreached, word, wordOf } from '../being/silence.js';
|
|
5
5
|
import { digest } from '../being/digest.js';
|
|
6
6
|
import { RESERVED_IDS, isBlueprint } from '../being/types.js';
|
|
7
7
|
import { at, put, drop, prune } from './partition.js';
|
|
8
8
|
import { allow, within, LATE } from './allowance.js';
|
|
9
9
|
import { isHex, isWardPk } from './seal.js';
|
|
10
|
-
import { cellFault } from './cells.js';
|
|
10
|
+
import { cellFault, unkeepable } from './cells.js';
|
|
11
11
|
// The three calls on `standings` share the object with the ids she takes, so
|
|
12
12
|
// a standing named after one of them would be unreachable: `standings.knock`
|
|
13
13
|
// is the call, whatever record sits under that name. One namespace means one
|
|
14
14
|
// list: these are refused at invite and at take, beside the ward's own words.
|
|
15
15
|
const CALLS = ['knock', 'take', 'remove'];
|
|
16
|
-
const reserved = (id) => RESERVED_IDS.includes(id) || CALLS.includes(id);
|
|
17
16
|
export function buildStance(inside, key, cells, bind) {
|
|
17
|
+
// A word the ward refuses at every mint and every take. The two the
|
|
18
|
+
// protocol names, and the ones this kit adds because its own spelling took
|
|
19
|
+
// them: the calls a standing already answers to, and a name no partition
|
|
20
|
+
// can hold. That last one is refused here rather than by the cells guard,
|
|
21
|
+
// because the guard throws and this table promises a null.
|
|
22
|
+
// A method is a word or it is nothing at all. The seal writes a payload as
|
|
23
|
+
// JSON, which drops a key whose value it cannot write, so a symbol or a
|
|
24
|
+
// function here would leave as an ask with no method: the empty ask. She
|
|
25
|
+
// would have asked for work, been handed a blueprint, and spent a number on
|
|
26
|
+
// it. Nothing that cannot be sent is sent, and what never left is unreached,
|
|
27
|
+
// which is the one answer that says asking again is safe. This is the same
|
|
28
|
+
// rule args are already held to.
|
|
29
|
+
const named = (method) => method === undefined || typeof method === 'string';
|
|
30
|
+
const reserved = (id) => RESERVED_IDS.includes(id) || CALLS.includes(id) || unkeepable(id);
|
|
18
31
|
// The name of one relation, as her side files it. A relation is a ward and
|
|
19
32
|
// a heir, never a heir alone: the heir pk is outer, it rides in the clear on
|
|
20
33
|
// every lid, and anyone who reads one can quote it back inside an invitation
|
|
@@ -189,6 +202,8 @@ export function buildStance(inside, key, cells, bind) {
|
|
|
189
202
|
knock: async (inv, method, args = {}, wanted) => {
|
|
190
203
|
if (gone())
|
|
191
204
|
return word('dropped');
|
|
205
|
+
if (!named(method))
|
|
206
|
+
return unreached();
|
|
192
207
|
if (!valid(inv))
|
|
193
208
|
return word('invitation'); // S1. nothing is sent
|
|
194
209
|
// She may knock again, and after take that knock is an ask: the relation
|
|
@@ -254,6 +269,8 @@ export function buildStance(inside, key, cells, bind) {
|
|
|
254
269
|
ask: (method, args = {}, wanted) => {
|
|
255
270
|
if (gone())
|
|
256
271
|
return Promise.resolve(word('dropped'));
|
|
272
|
+
if (!named(method))
|
|
273
|
+
return Promise.resolve(unreached());
|
|
257
274
|
const keys = at(bind.standings, id);
|
|
258
275
|
if (!keys)
|
|
259
276
|
return Promise.resolve(word('dropped')); // S2. she dropped it between one line and the next
|
|
@@ -324,6 +341,13 @@ export function buildStance(inside, key, cells, bind) {
|
|
|
324
341
|
invite: async (id, notes) => {
|
|
325
342
|
if (gone() || reserved(id) || at(cells.occupants, id) || at(cells.standings, id))
|
|
326
343
|
return null;
|
|
344
|
+
// Notes are values, like everything else the partition keeps, and a
|
|
345
|
+
// spread copies one level: an object inside them would stay the
|
|
346
|
+
// caller's, a handle into her cells that writes past the guard and
|
|
347
|
+
// never says `wrote`, so what a restart brought back would not be
|
|
348
|
+
// what she read. Held to the rule and copied whole, or no invitation.
|
|
349
|
+
if (notes !== undefined && cellFault(notes, 'notes') !== null)
|
|
350
|
+
return null;
|
|
327
351
|
// The key first, and nothing written until it exists: a record put
|
|
328
352
|
// before the mint would be a record a remove in the meantime drops
|
|
329
353
|
// with no heir to close, and the heir opened after it would name an
|
|
@@ -333,7 +357,7 @@ export function buildStance(inside, key, cells, bind) {
|
|
|
333
357
|
return null; // taken while the key was minted
|
|
334
358
|
// The notes are the terms the inviter minted under, hers to read on her
|
|
335
359
|
// gate. She may write more later; nobody outside ever writes them.
|
|
336
|
-
put(cells.occupants, id, { id, notes: notes ?
|
|
360
|
+
put(cells.occupants, id, { id, notes: notes ? JSON.parse(JSON.stringify(notes)) : {} });
|
|
337
361
|
inside.openHeir(k.pk, key, id);
|
|
338
362
|
put(bind.occupants, id, k.pk);
|
|
339
363
|
inside.wrote();
|
|
@@ -351,5 +375,39 @@ export function buildStance(inside, key, cells, bind) {
|
|
|
351
375
|
},
|
|
352
376
|
},
|
|
353
377
|
standings,
|
|
378
|
+
// A standing at one of the things this device can do. The same three
|
|
379
|
+
// moves her boot makes, with the harbor's root doing the inviting instead
|
|
380
|
+
// of a being of this ward: ask the ground for the name, knock with what
|
|
381
|
+
// comes back, take it under the id she gave. She is handed the id.
|
|
382
|
+
//
|
|
383
|
+
// The invitation never reaches her, and that is the difference between
|
|
384
|
+
// this relation and every other she holds. Her own invitations are hers
|
|
385
|
+
// to give away, because giving one away is giving away her own relation.
|
|
386
|
+
// This one is the device's, minted for this ward alone, and a value she
|
|
387
|
+
// could copy is a capability she could hand to anyone. A being who wants
|
|
388
|
+
// to lend her device access to another does it in the open, by an ask of
|
|
389
|
+
// her own that forwards to this standing, where her gate reads who is
|
|
390
|
+
// asking and she can stop.
|
|
391
|
+
lend: async (name, id) => {
|
|
392
|
+
if (gone() || typeof name !== 'string' || typeof id !== 'string')
|
|
393
|
+
return null;
|
|
394
|
+
if (reserved(id) || at(cells.standings, id) || at(cells.occupants, id))
|
|
395
|
+
return null;
|
|
396
|
+
// Her half runs inside the harbor's call, so a knock that was refused
|
|
397
|
+
// and a take that lost the id both end with the harbor removing what it
|
|
398
|
+
// minted. Nothing half-lives here either.
|
|
399
|
+
const took = await inside.lend(name, async (inv) => {
|
|
400
|
+
// Read again: the ground was awaited, and a line of hers in between
|
|
401
|
+
// may have taken the id. Nothing is written for a relation she cannot
|
|
402
|
+
// hold, and the invitation goes back unspent.
|
|
403
|
+
if (gone() || reserved(id) || at(cells.standings, id) || at(cells.occupants, id))
|
|
404
|
+
return false;
|
|
405
|
+
const out = await calls.knock(inv);
|
|
406
|
+
if (isSilence(out) || isWord(out))
|
|
407
|
+
return false;
|
|
408
|
+
return (await calls.take(id, inv)) === id;
|
|
409
|
+
});
|
|
410
|
+
return took ? id : null;
|
|
411
|
+
},
|
|
354
412
|
};
|
|
355
413
|
}
|
package/dist/ward/ward.js
CHANGED
|
@@ -210,6 +210,11 @@ class Self {
|
|
|
210
210
|
// never came about takes her out again.
|
|
211
211
|
relate: async (k, id) => (await this.doors.get(k)?.stance.occupants.invite(id)) ?? null,
|
|
212
212
|
unmake: (k) => void this.#unboot(k),
|
|
213
|
+
// What this device lends this ward's beings. The ward knocks and takes
|
|
214
|
+
// inside the harbor's own call and reads nothing in the value: which
|
|
215
|
+
// names there are, which ward may ask for one, and what becomes of one
|
|
216
|
+
// she did not take, is the harbor's and no word of the ward.
|
|
217
|
+
lend: async (name, take) => (await this.g.lend?.(name, take)) ?? false,
|
|
213
218
|
wrote: () => this.#wrote(),
|
|
214
219
|
}, key, cells, bind);
|
|
215
220
|
const being = make(stance);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quo-systems/quo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"description": "Quo: an object asks another object and gets an answer, without knowing where it is. Being, Ward, Harbor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"quo",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"author": "Razvan Gherghina",
|
|
15
15
|
"license": "Apache-2.0",
|
|
16
|
+
"homepage": "https://quo.systems",
|
|
16
17
|
"type": "module",
|
|
17
18
|
"engines": {
|
|
18
19
|
"node": ">=22.18"
|
|
@@ -38,10 +39,10 @@
|
|
|
38
39
|
"./package.json": "./package.json"
|
|
39
40
|
},
|
|
40
41
|
"scripts": {
|
|
41
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json && cp ../../papers/SPEC.md SPEC.md",
|
|
42
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && cp ../../papers/SPEC.md SPEC.md && cp ../../papers/quo-kit.md quo-kit.md",
|
|
42
43
|
"test": "node --test \"test/*.test.ts\"",
|
|
43
44
|
"check:terrain": "node --test \"test/terrain/*.test.ts\"",
|
|
44
|
-
"prepublishOnly": "test \"$QUO_GATED\" = 1 || { echo 'publish from the root, gated once: npm run release' >&2; exit 1; }"
|
|
45
|
+
"prepublishOnly": "test \"$QUO_GATED\" = 1 || { echo 'publish from the root, gated once: npm run release:quo' >&2; exit 1; }"
|
|
45
46
|
},
|
|
46
47
|
"publishConfig": {
|
|
47
48
|
"access": "public"
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
"src",
|
|
52
53
|
"vectors",
|
|
53
54
|
"SPEC.md",
|
|
55
|
+
"quo-kit.md",
|
|
54
56
|
"README.md",
|
|
55
57
|
"LICENSE",
|
|
56
58
|
"NOTICE"
|