@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.
Files changed (55) hide show
  1. package/README.md +10 -4
  2. package/SPEC.md +354 -536
  3. package/dist/being/being.d.ts +1 -0
  4. package/dist/being/being.js +8 -3
  5. package/dist/being/index.d.ts +1 -1
  6. package/dist/being/index.js +1 -1
  7. package/dist/being/types.d.ts +2 -0
  8. package/dist/being/types.js +17 -0
  9. package/dist/conformance/beings.d.ts +41 -0
  10. package/dist/conformance/beings.js +28 -2
  11. package/dist/conformance/index.d.ts +10 -1
  12. package/dist/conformance/index.js +162 -6
  13. package/dist/harbor/core.d.ts +4 -2
  14. package/dist/harbor/core.js +25 -4
  15. package/dist/harbor/index.d.ts +1 -1
  16. package/dist/harbor/memory.d.ts +3 -3
  17. package/dist/harbor/memory.js +6 -3
  18. package/dist/harbor/reach.js +1 -1
  19. package/dist/ward/cells.d.ts +2 -0
  20. package/dist/ward/cells.js +60 -11
  21. package/dist/ward/door.d.ts +1 -0
  22. package/dist/ward/door.js +29 -10
  23. package/dist/ward/ground.d.ts +3 -1
  24. package/dist/ward/ground.js +1 -1
  25. package/dist/ward/heirs.js +8 -1
  26. package/dist/ward/index.d.ts +2 -2
  27. package/dist/ward/index.js +3 -3
  28. package/dist/ward/owner.js +49 -8
  29. package/dist/ward/seal.d.ts +1 -0
  30. package/dist/ward/seal.js +10 -2
  31. package/dist/ward/stance.d.ts +1 -0
  32. package/dist/ward/stance.js +62 -4
  33. package/dist/ward/ward.js +5 -0
  34. package/package.json +5 -3
  35. package/quo-kit.md +523 -0
  36. package/src/being/being.ts +8 -3
  37. package/src/being/index.ts +1 -1
  38. package/src/being/types.ts +34 -0
  39. package/src/conformance/beings.ts +25 -2
  40. package/src/conformance/estate.ts +9 -9
  41. package/src/conformance/index.ts +204 -7
  42. package/src/conformance/reach.ts +1 -1
  43. package/src/harbor/core.ts +26 -5
  44. package/src/harbor/index.ts +1 -1
  45. package/src/harbor/memory.ts +7 -4
  46. package/src/harbor/reach.ts +1 -1
  47. package/src/ward/cells.ts +59 -10
  48. package/src/ward/door.ts +27 -9
  49. package/src/ward/ground.ts +39 -11
  50. package/src/ward/heirs.ts +7 -1
  51. package/src/ward/index.ts +4 -4
  52. package/src/ward/owner.ts +45 -10
  53. package/src/ward/seal.ts +11 -2
  54. package/src/ward/stance.ts +60 -4
  55. package/src/ward/ward.ts +6 -1
@@ -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
- function fault(v, path, seen, depth) {
10
- if (v === null || typeof v === 'boolean' || typeof v === 'string')
11
- return null;
12
- if (typeof v === 'number')
13
- return Number.isFinite(v) ? null : `${path} is ${String(v)}, which no harbor can write down`;
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 = fault(v[i], `${path}[${i}]`, seen, depth + 1);
64
+ const f = under(`${path}[${i}]`, v[i]);
27
65
  if (f)
28
66
  return f;
29
67
  }
30
- return null;
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 = fault(v[k], `${path}.${k}`, seen, depth + 1);
77
+ const f = under(`${path}.${k}`, v[k]);
39
78
  if (f)
40
79
  return f;
41
80
  }
42
- return null;
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) => fault(v, path, new Set(), 0);
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 === '__proto__')
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`);
@@ -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. Catches every throw. `bound` says
20
- // whether the asker is a key this door holds: she hears `threw`; a stranger
21
- // at the public being hears silence, because her insides are hers.
22
- export async function arrive(door, asker, method, args, bound) {
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
- if (method === undefined)
46
- return { object: out, seen: null };
47
- // The digest rides along, it is not the answer. She has already answered:
48
- // a describe that will not run costs the digest, and nothing else.
49
- return { object: out, seen: await seen(door, asker) };
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
- return isSilence(bp) || isWord(bp) ? null : await digest(bp);
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;
@@ -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<{
@@ -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 spec keeps the harbors
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.
@@ -103,6 +103,13 @@ export class Heirs {
103
103
  }
104
104
  if (by === h.announced)
105
105
  h.current = by;
106
- h.announced = next;
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
  }
@@ -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';
@@ -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 two bounds on what a partition may hold, for a kit that must keep the
5
- // same ones and for a test that pins them.
6
- export { GONE, MINTED } from './partition.js';
4
+ // 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';
@@ -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. Six asks: boot, public, invite, knock, remove and
7
- // unboot, which is boot's inverse and takes her relations with her. The
8
- // knock is a being's knock made for her, and the standing is written into
9
- // her cells under the id the owner gave; remove takes a relation out of 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,
@@ -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)
@@ -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
  };
@@ -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 ? { ...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.13",
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"