@quo-systems/quo 0.2.14 → 0.2.16

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 (62) hide show
  1. package/README.md +37 -20
  2. package/dist/being/being.d.ts +1 -0
  3. package/dist/being/being.js +8 -3
  4. package/dist/being/index.d.ts +1 -1
  5. package/dist/being/index.js +1 -1
  6. package/dist/being/types.d.ts +2 -0
  7. package/dist/being/types.js +17 -0
  8. package/dist/conformance/beings.d.ts +41 -0
  9. package/dist/conformance/beings.js +28 -2
  10. package/dist/conformance/index.d.ts +10 -1
  11. package/dist/conformance/index.js +162 -6
  12. package/dist/harbor/core.d.ts +4 -2
  13. package/dist/harbor/core.js +25 -4
  14. package/dist/harbor/index.d.ts +1 -1
  15. package/dist/harbor/memory.d.ts +4 -3
  16. package/dist/harbor/memory.js +13 -4
  17. package/dist/harbor/reach.js +1 -1
  18. package/dist/ward/arithmetic.d.ts +4 -0
  19. package/dist/ward/arithmetic.js +82 -12
  20. package/dist/ward/cells.d.ts +2 -0
  21. package/dist/ward/cells.js +60 -11
  22. package/dist/ward/door.d.ts +1 -0
  23. package/dist/ward/door.js +29 -10
  24. package/dist/ward/ground.d.ts +3 -1
  25. package/dist/ward/ground.js +1 -1
  26. package/dist/ward/heirs.js +8 -1
  27. package/dist/ward/index.d.ts +2 -2
  28. package/dist/ward/index.js +3 -3
  29. package/dist/ward/owner.js +49 -8
  30. package/dist/ward/seal.d.ts +1 -0
  31. package/dist/ward/seal.js +11 -3
  32. package/dist/ward/stance.d.ts +1 -0
  33. package/dist/ward/stance.js +62 -4
  34. package/dist/ward/ward.js +13 -4
  35. package/package.json +7 -6
  36. package/{SPEC.md → protocol/SPEC.md} +367 -537
  37. package/protocol/vectors/door.json +345 -0
  38. package/quo-kit.md +595 -0
  39. package/src/being/being.ts +8 -3
  40. package/src/being/index.ts +1 -1
  41. package/src/being/types.ts +34 -0
  42. package/src/conformance/beings.ts +25 -2
  43. package/src/conformance/estate.ts +9 -9
  44. package/src/conformance/index.ts +204 -7
  45. package/src/conformance/reach.ts +1 -1
  46. package/src/harbor/core.ts +26 -5
  47. package/src/harbor/index.ts +1 -1
  48. package/src/harbor/memory.ts +14 -5
  49. package/src/harbor/reach.ts +1 -1
  50. package/src/ward/arithmetic.ts +83 -14
  51. package/src/ward/cells.ts +59 -10
  52. package/src/ward/door.ts +27 -9
  53. package/src/ward/ground.ts +39 -11
  54. package/src/ward/heirs.ts +7 -1
  55. package/src/ward/index.ts +4 -4
  56. package/src/ward/owner.ts +45 -10
  57. package/src/ward/seal.ts +12 -3
  58. package/src/ward/stance.ts +60 -4
  59. package/src/ward/ward.ts +14 -5
  60. /package/{vectors → protocol/vectors}/arithmetic.json +0 -0
  61. /package/{vectors → protocol/vectors}/framing.json +0 -0
  62. /package/{vectors → protocol/vectors}/wire.json +0 -0
package/README.md CHANGED
@@ -13,8 +13,12 @@ Nothing else is Quo.
13
13
  beings and judges its door.
14
14
  - **Being.** One ordinary object, one voice.
15
15
 
16
- [`SPEC.md`](SPEC.md) is the truth. It is self-contained and
16
+ [`SPEC.md`](SPEC.md) is the truth. It is the protocol alone: what any ward in
17
+ any language must do for its bytes to be Quo. It is self-contained and
17
18
  assumes nothing from any other document, this README included. Read it first.
19
+ [`quo-kit.md`](quo-kit.md) is this kit: what one TypeScript
20
+ implementation chose and another kit may refuse. It assumes the spec, and
21
+ where the two disagree the spec wins.
18
22
 
19
23
  ## This package
20
24
 
@@ -24,12 +28,13 @@ no dependencies. The package ships JavaScript with declarations, emitted by
24
28
  types nowhere under `node_modules`.
25
29
 
26
30
  ```
31
+ protocol/ the shelf a kit in any language reads: SPEC.md and the vectors, no code
32
+ protocol/vectors/ fixed inputs and outputs, so another language proves its bytes
27
33
  src/being/ the Being side: what a being author imports, if anything
28
34
  src/ward/ the ward: the Ground contract, door, seal, arithmetic, heirs, stance, allowance
29
35
  src/harbor/ MemoryHarbor, and the harbor core with its store, reach and dialer
30
36
  src/conformance/ behaviours any ward must show, written against the truth
31
37
  test/ the suites
32
- vectors/ fixed inputs and outputs, so another language proves its bytes
33
38
  ```
34
39
 
35
40
  ## Requirements
@@ -73,29 +78,41 @@ import { conform } from '@quo-systems/quo/conformance';
73
78
 
74
79
  ## Another language
75
80
 
76
- The hand to a kit in another language is two things of two kinds. The
77
- vectors are the byte-level hand, everything a stranger can observe:
78
- `vectors/arithmetic.json`, the primitives the seal rests on, SHA-256,
79
- Ed25519, X25519, HKDF and AES-256-GCM; `vectors/framing.json`, Quo's own,
80
- the ward pk, the digest, the signed ask body, the sealed shapes, the
81
- invitation and the knock; `vectors/wire.json`, the frames on a socket and
82
- the one request a door takes. They import by name,
83
- `@quo-systems/quo/vectors/framing.json`, so a kit's own suite can read them
84
- from the package. A kit reproduces them or it is not this
85
- protocol. `src/conformance/` is the behavioural hand, and it is a checklist
86
- a kit ports, not a harness it runs: the beings it is shown with run only in
87
- a TypeScript ward. `SPEC.md` "The tree" says the whole of it.
81
+ The hand to a kit in another language is one folder, `protocol/`, and it
82
+ holds two things of two kinds. `SPEC.md` is the protocol, and it assumes
83
+ nothing: a kit is written against it and against nothing else here. The
84
+ vectors beside it are the byte-level hand, everything a stranger can
85
+ observe: `protocol/vectors/arithmetic.json`, the primitives the seal rests
86
+ on, SHA-256, Ed25519, X25519, HKDF and AES-256-GCM;
87
+ `protocol/vectors/framing.json`, Quo's own, the ward pk, the digest, the
88
+ signed ask body, the sealed shapes, the invitation and the knock;
89
+ `protocol/vectors/wire.json`, the frames on a socket and the one request a
90
+ door takes; and `protocol/vectors/door.json`, the door's thirteen cases,
91
+ each one an arrival a ward will not answer, with the bytes that arrive, the
92
+ bytes that leave and the partition's digest on both sides of the judgement.
93
+ They import by name,
94
+ `@quo-systems/quo/protocol/vectors/framing.json`, so a kit's own suite can
95
+ read them from the package. A kit reproduces them or it is not this
96
+ protocol.
97
+
98
+ Nothing outside that folder is the protocol. `src/` is this kit's
99
+ interpretation, and `src/conformance/` is a checklist a kit ports rather
100
+ than a harness it runs: it imports the base class, the silence spelling and
101
+ this kit's store and reach, so the beings it is shown with run only in a
102
+ TypeScript ward. `quo-kit.md` "The three shelves" says which is which.
88
103
 
89
104
  ## Publishing
90
105
 
91
106
  Published from 0.1.0, the first version under this name, with no
92
107
  compatibility promise before 1.0.0: the words may still move. `npm pack
93
- --dry-run` shows what ships: the emitted `dist/`, the source, the vectors,
94
- the spec, this file, the licence and the notice, and no tests and no
95
- configs. The spec ships because it is the truth the source and the vectors
96
- are read against. Publishing is gated from the root of the repository:
97
- `npm run release` there runs both gates, `check` and `check:terrain`, and
98
- publishes on green; `npm publish` inside this package refuses and says so.
108
+ --dry-run` shows what ships: the emitted `dist/`, the source, the protocol
109
+ shelf with the spec inside it, this file, the licence and the notice, and no
110
+ tests and no configs. The spec ships because it is the truth the source and
111
+ the vectors are read against. Publishing is gated from the root of the repository:
112
+ `npm run release:quo` there runs both gates, `check` and `check:terrain`,
113
+ and publishes on green; `npm publish` inside this package refuses and says
114
+ so. This package carries the version of its own work and is bound to no
115
+ other's: a number equal to another package's is a coincidence.
99
116
 
100
117
  ## License
101
118
 
@@ -13,6 +13,7 @@ export declare class Being {
13
13
  get cells(): Cells;
14
14
  get standings(): Standings;
15
15
  get occupants(): Occupants;
16
+ lend(name: string, id: string): Promise<string | null>;
16
17
  invite(id: string, notes?: JsonObject): Promise<Invitation | null>;
17
18
  knock(invitation: Invitation, method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
18
19
  take(id: string, invitation: Invitation): Promise<string | null>;
@@ -1,11 +1,11 @@
1
1
  // Names a subclass may not use for an ask, because they are the base's own.
2
- const RESERVED = new Set(['answer', 'describe', 'stance', 'cells', 'standings', 'occupants', 'occupant', 'invite', 'knock', 'take', 'boot', 'constructor']);
2
+ const RESERVED = new Set(['answer', 'describe', 'stance', 'cells', 'standings', 'occupants', 'occupant', 'invite', 'knock', 'take', 'boot', 'lend', 'constructor']);
3
3
  // Whether she has a method of that name, written on her own prototype chain
4
4
  // below Object's. A name Object lends every object, `hasOwnProperty` or
5
5
  // `toString`, is not a method she wrote; and a field she assigns in her own
6
6
  // constructor is not there yet when the base checks, so an ask is a method
7
7
  // on the prototype and nothing else.
8
- const method = (self, name) => {
8
+ const wrote = (self, name) => {
9
9
  for (let p = Object.getPrototypeOf(self); p !== null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
10
10
  if (Object.hasOwn(p, name))
11
11
  return typeof p[name] === 'function';
@@ -32,7 +32,7 @@ export class Being {
32
32
  for (const name of Object.keys(C.asks)) {
33
33
  if (RESERVED.has(name))
34
34
  throw new Error(`ask '${name}' is a reserved name`);
35
- if (!method(this, name))
35
+ if (!wrote(this, name))
36
36
  throw new Error(`ask '${name}' has no method on the prototype`);
37
37
  }
38
38
  // Own keys only: a default named after a member of Object's prototype is
@@ -50,6 +50,11 @@ export class Being {
50
50
  get occupants() {
51
51
  return this.stance.occupants;
52
52
  }
53
+ // A standing at one of the things this device can do, under an id of hers.
54
+ // The ward knocks and takes it for her; the invitation never reaches her.
55
+ lend(name, id) {
56
+ return this.stance.lend(name, id);
57
+ }
53
58
  invite(id, notes) {
54
59
  return this.stance.occupants.invite(id, notes);
55
60
  }
@@ -1,5 +1,5 @@
1
1
  export { Being, type AskSpec } from './being.ts';
2
2
  export { silence, isSilence, answered, unreached, isUnreached, word, isWord, wordOf, told, DOOR_WORDS, isDoorWord } from './silence.ts';
3
3
  export { digest, canonical } from './digest.ts';
4
- export { OWNER, PUBLIC, RESERVED_IDS, isBlueprint, invitationArgs } from './types.ts';
4
+ export { OWNER, PUBLIC, RESERVED_IDS, isBlueprint, invitationArgs, isInvitation } from './types.ts';
5
5
  export type * from './types.ts';
@@ -3,4 +3,4 @@
3
3
  export { Being } from './being.js';
4
4
  export { silence, isSilence, answered, unreached, isUnreached, word, isWord, wordOf, told, DOOR_WORDS, isDoorWord } from './silence.js';
5
5
  export { digest, canonical } from './digest.js';
6
- export { OWNER, PUBLIC, RESERVED_IDS, isBlueprint, invitationArgs } from './types.js';
6
+ export { OWNER, PUBLIC, RESERVED_IDS, isBlueprint, invitationArgs, isInvitation } from './types.js';
@@ -19,6 +19,7 @@ export type Invitation = {
19
19
  secret?: string;
20
20
  };
21
21
  export declare const invitationArgs: (inv: Invitation) => JsonObject;
22
+ export declare const isInvitation: (v: unknown) => v is Invitation;
22
23
  export type Schema = JsonObject;
23
24
  export type Ask = {
24
25
  name: string;
@@ -79,6 +80,7 @@ export type Stance = {
79
80
  readonly cells: Cells;
80
81
  readonly occupants: Occupants;
81
82
  readonly standings: Standings;
83
+ lend(name: string, id: string): Promise<string | null>;
82
84
  boot(className: string, key: string, id?: string): Promise<string | null>;
83
85
  };
84
86
  export interface BeingLike {
@@ -23,6 +23,23 @@ export const invitationArgs = (inv) => ({
23
23
  ...(inv.heir !== undefined ? { heir: inv.heir } : {}),
24
24
  ...(inv.secret !== undefined ? { secret: inv.secret } : {}),
25
25
  });
26
+ // Whether a value that arrived is an invitation. The shape is the spec's, so
27
+ // the reading of it is too, and it is one reading: a form that
28
+ // asks a guest for one, a faculty handed one to wake her maker by, and a
29
+ // shell that finds one in a link all ask the same question, and a kinder
30
+ // answer in one of them is a value that fails at a door instead of at the
31
+ // edge it came in by. A ward pk is a hundred and twenty-eight lowercase hex,
32
+ // and a heir comes with its secret or neither comes: an invitation to a
33
+ // public being carries no heir, and a heir with no secret opens nothing.
34
+ export const isInvitation = (v) => {
35
+ if (v === null || typeof v !== 'object' || Array.isArray(v))
36
+ return false;
37
+ const o = v;
38
+ if (typeof o.ward !== 'string' || !/^[0-9a-f]{128}$/.test(o.ward))
39
+ return false;
40
+ const heir = typeof o.heir === 'string', secret = typeof o.secret === 'string';
41
+ return (heir && secret) || (!heir && !secret && !('heir' in o) && !('secret' in o));
42
+ };
26
43
  // Whether what came back from an empty ask is a blueprint. Every describe on
27
44
  // the far side of a door is somebody else's code, so nothing may be written
28
45
  // into a standing's record as a blueprint without being read as one first: a
@@ -132,6 +132,20 @@ export declare class Maker extends Being {
132
132
  required: string[];
133
133
  };
134
134
  };
135
+ borrow: {
136
+ input: {
137
+ type: string;
138
+ properties: {
139
+ name: {
140
+ type: string;
141
+ };
142
+ id: {
143
+ type: string;
144
+ };
145
+ };
146
+ required: string[];
147
+ };
148
+ };
135
149
  };
136
150
  open({ class: className, key, id }: JsonObject): Promise<{
137
151
  made: string | null;
@@ -139,6 +153,9 @@ export declare class Maker extends Being {
139
153
  say({ id }: JsonObject): Promise<{
140
154
  said: Json;
141
155
  }>;
156
+ borrow({ name, id }: JsonObject): Promise<{
157
+ lent: string | null;
158
+ }>;
142
159
  }
143
160
  export declare class Member extends Being {
144
161
  static cells: {
@@ -150,8 +167,32 @@ export declare class Member extends Being {
150
167
  type: string;
151
168
  };
152
169
  };
170
+ relay: {
171
+ input: {
172
+ type: string;
173
+ properties: {
174
+ id: {
175
+ type: string;
176
+ };
177
+ method: {
178
+ type: string;
179
+ };
180
+ time: {
181
+ type: string;
182
+ };
183
+ };
184
+ required: string[];
185
+ };
186
+ };
153
187
  };
154
188
  ping(_args: JsonObject, asker: Asker): {
155
189
  pong: string | null;
156
190
  };
191
+ relay({ id, method, args, time }: JsonObject): Promise<{
192
+ said: string;
193
+ out: null;
194
+ } | {
195
+ said: null;
196
+ out: Json;
197
+ }>;
157
198
  }
@@ -3,7 +3,7 @@
3
3
  // shop, a customer on the base class, a maker, a member, and one raw being
4
4
  // with no base at all.
5
5
  import { Being } from '../being/being.js';
6
- import { answered, isUnreached } from '../being/silence.js';
6
+ import { answered, isSilence, isUnreached, isWord, wordOf } from '../being/silence.js';
7
7
  import { invitationArgs } from '../being/types.js';
8
8
  // A printer. Invites whom she is told to, prints for her occupants. Her
9
9
  // blueprint lives in her cells so a test can change her shape.
@@ -135,6 +135,7 @@ export class Maker extends Being {
135
135
  static asks = {
136
136
  open: { input: { type: 'object', properties: { class: { type: 'string' }, key: { type: 'string' }, id: { type: 'string' } }, required: ['class', 'key'] } },
137
137
  say: { input: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
138
+ borrow: { input: { type: 'object', properties: { name: { type: 'string' }, id: { type: 'string' } }, required: ['name', 'id'] } },
138
139
  };
139
140
  async open({ class: className, key, id }) {
140
141
  const made = await this.boot(className, key, id);
@@ -146,12 +147,37 @@ export class Maker extends Being {
146
147
  const standing = this.standings[id];
147
148
  return { said: standing === undefined ? null : (await standing.ask('ping')) };
148
149
  }
150
+ // Making's other half: a standing at something the box already has, by the
151
+ // name the box knows it under. She is handed the id and never the value.
152
+ async borrow({ name, id }) {
153
+ return { lent: await this.lend(name, id) };
154
+ }
149
155
  }
150
156
  export class Member extends Being {
151
157
  static cells = { heard: [] };
152
- static asks = { ping: { input: { type: 'object' } } };
158
+ static asks = {
159
+ ping: { input: { type: 'object' } },
160
+ relay: { input: { type: 'object', properties: { id: { type: 'string' }, method: { type: 'string' }, time: { type: 'number' } }, required: ['id'] } },
161
+ };
153
162
  ping(_args, asker) {
154
163
  this.cells.heard.push({ from: asker.id ?? null });
155
164
  return { pong: asker.id ?? null };
156
165
  }
166
+ // She asks on one of her standings from inside her own answer, and says
167
+ // what came back as a word rather than as the word itself, since a word is
168
+ // her ward's value and never a thing that crosses. `time` is what she is
169
+ // willing to wait, which is the whole of what the third argument is for.
170
+ // Two members holding each other and relaying make a cycle, and a cycle is
171
+ // how a suite that owns no clock can see a wait end.
172
+ async relay({ id, method, args, time }) {
173
+ const standing = this.standings[id];
174
+ if (standing === undefined)
175
+ return { said: 'no standing', out: null };
176
+ const out = await standing.ask(method, (args ?? {}), time === undefined ? undefined : { time: time });
177
+ if (isSilence(out))
178
+ return { said: 'silence', out: null };
179
+ if (isWord(out))
180
+ return { said: wordOf(out), out: null };
181
+ return { said: null, out: out };
182
+ }
157
183
  }
@@ -53,6 +53,10 @@ export type World = {
53
53
  census(): Census;
54
54
  migrate(key: string): Promise<void>;
55
55
  owner(key: string): (method?: string, args?: JsonObject) => Promise<unknown>;
56
+ } & Partial<Standing>;
57
+ export type Standing = {
58
+ partition(key: string): JsonObject;
59
+ restart(key: string, partition?: JsonObject): Promise<void>;
56
60
  };
57
61
  export { Printer, Shop, Customer, Echo, Maker, Member };
58
62
  export declare const HEX64: RegExp;
@@ -60,8 +64,13 @@ export declare const HEX128: RegExp;
60
64
  export type Runner = (name: string, opts: {
61
65
  skip?: string | false;
62
66
  }, fn: () => Promise<void> | void) => void;
63
- export declare function conform(label: string, make: () => Promise<World>, { canDown, test }: {
67
+ export type Made = {
68
+ lends?: Record<string, string>;
69
+ };
70
+ export declare function conform(label: string, make: (made?: Made) => Promise<World>, { canDown, canLend, canRestart, test }: {
64
71
  canDown?: boolean;
72
+ canLend?: boolean;
73
+ canRestart?: boolean;
65
74
  test: Runner;
66
75
  }): void;
67
76
  export { conformStore } from './store.ts';
@@ -3,7 +3,10 @@
3
3
  //
4
4
  // A ward is handed in as a probe:
5
5
  //
6
- // make() -> world
6
+ // make({ lends }) -> world. `lends` maps a name the box lends to the
7
+ // class of the being it lends under it; the probe
8
+ // stands one of each in a ward its harbor roots,
9
+ // and every ward it boots is grounded to lend them
7
10
  // world.boot(key, Class, cells?, { isPublic }) -> handle
8
11
  // world.heir(invitation) -> { current, announced, fresh } | undefined
9
12
  // what the inviting ward holds for that heir
@@ -19,7 +22,7 @@
19
22
  //
20
23
  // Every assertion here is the truth's. Nothing here knows how a ward is built.
21
24
  import { assert } from './assert.js';
22
- import { silence, isSilence, isUnreached, told } from '../being/silence.js';
25
+ import { silence, answered, isSilence, isUnreached, told } from '../being/silence.js';
23
26
  import { digest } from '../being/digest.js';
24
27
  import { Printer, Shop, Customer, Echo, Maker, Member } from './beings.js';
25
28
  import { estate } from './estate.js';
@@ -33,9 +36,15 @@ export const HEX64 = /^[0-9a-f]{64}$/; // one key, one secret, one digest
33
36
  export const HEX128 = /^[0-9a-f]{128}$/; // a ward pk: the signing pk then the padlock
34
37
  // Her ward's word, as she receives it.
35
38
  const said = (x, w) => told(x) === w;
36
- export function conform(label, make, { canDown = true, test }) {
39
+ // A world whose harness stands a ward up again. The two members are optional
40
+ // on the probe and required of a harness that says it can, so a chapter that
41
+ // needs them says so here rather than reaching past a maybe at every line.
42
+ const able = (w) => w;
43
+ export function conform(label, make, { canDown = true, canLend = true, canRestart = true, test }) {
37
44
  const t = (name, fn) => test(`[${label}] ${name}`, {}, fn);
38
45
  const down = (name, fn) => test(`[${label}] ${name}`, { skip: !canDown && 'nothing to cut inside one ward' }, fn);
46
+ const lends = (name, fn) => test(`[${label}] ${name}`, { skip: !canLend && 'this harness stands no ward of its harbor to lend from' }, fn);
47
+ const kept = (name, fn) => test(`[${label}] ${name}`, { skip: !canRestart && 'this harness cannot hand a ward a partition and stand it up again' }, fn);
39
48
  async function world({ printerInvitesShop = true } = {}) {
40
49
  const w = await make();
41
50
  const printer = await w.boot('P', Printer);
@@ -166,8 +175,12 @@ export function conform(label, make, { canDown = true, test }) {
166
175
  // alice hosts here rather than the shop, because the shop's own `invite`
167
176
  // takes a tier and mints the id itself. This one is the stance's, which
168
177
  // is where the rule lives.
169
- const { alice, bob } = await world();
170
- for (const id of ['OWNER', 'PUBLIC', 'knock', 'take', 'remove']) {
178
+ const { w, alice, bob } = await world();
179
+ // `__proto__` is in the list because a partition cannot hold it. A kit
180
+ // that refuses it only where it writes her cells writes the ward's half
181
+ // first and then throws, which leaves one relation in two standings and a
182
+ // throw where this table promises a null.
183
+ for (const id of ['OWNER', 'PUBLIC', 'knock', 'take', 'remove', '__proto__']) {
171
184
  assert.equal(await alice.being.invite(id), null, `${id} is not hers to mint`);
172
185
  assert.equal(Object.hasOwn(alice.cells.occupants, id), false, `and nothing was written for ${id}`);
173
186
  // The other end: a guest is answered and then refused the name.
@@ -175,6 +188,9 @@ export function conform(label, make, { canDown = true, test }) {
175
188
  assert.deepEqual(await bob.being.knock(inv, 'hi'), { heard: 'hi' }, `the knock for ${id} was answered`);
176
189
  assert.equal(await bob.being.take(id, inv), null, `take refuses ${id} for the same reason`);
177
190
  assert.equal(Object.hasOwn(bob.cells.standings, id), false, `and nothing was written for ${id}`);
191
+ // The ward's half too, and not only hers: a standing filed here with no
192
+ // record of it in her cells is a lane nobody can see and nobody removes.
193
+ assert.equal(Object.hasOwn(w.census().binds.B.standings, id), false, `and the ward wrote no keys for ${id}`);
178
194
  assert.equal(await bob.being.take(`as-${id}`, inv), `as-${id}`, 'and an ordinary id takes the same invitation');
179
195
  }
180
196
  // And the three calls are still the calls, not records that shadowed them.
@@ -190,7 +206,7 @@ export function conform(label, make, { canDown = true, test }) {
190
206
  const { w, shop, alice } = await world();
191
207
  const ask = w.owner('S');
192
208
  const bp = (await ask());
193
- assert.deepEqual(bp.asks.map((a) => a.name), ['boot', 'public', 'invite', 'knock', 'remove', 'unboot'], 'six asks, in this order');
209
+ assert.deepEqual(bp.asks.map((a) => a.name), ['boot', 'public', 'invite', 'knock', 'remove', 'unboot', 'ask'], 'seven asks, in this order');
194
210
  for (const a of bp.asks) {
195
211
  assert.equal(typeof a.description, 'string', `${a.name} says what it is for`);
196
212
  assert.equal(a.input.type, 'object', `${a.name} takes one object`);
@@ -228,6 +244,32 @@ export function conform(label, make, { canDown = true, test }) {
228
244
  assert.ok(gone.removed.includes('for-alice'), 'her relations went with her, and the owner is told which');
229
245
  assert.ok(said(await alice.being.standings['via-owner'].ask('hello'), 'removed'), 'and her occupant hears removed at the door');
230
246
  });
247
+ // The owner's seventh ask: reaching into a being and asking her. It is the
248
+ // third asker of the ward-to-being edge, and the ward is the only one who
249
+ // fills it in: nothing outside a ward names OWNER, and no side holds a
250
+ // being's answer to judge it again. Strictly less than the unboot beside
251
+ // it, which takes her out with every relation she holds.
252
+ t('the owner asks a being of the ward, as owner or, with no being named, as nobody at the public being', async () => {
253
+ const { w } = await world();
254
+ const ask = w.owner('S');
255
+ const echo = w.owner('E');
256
+ await w.boot('E', Echo);
257
+ // Named: she is asked as the owner, and hears the owner's name.
258
+ assert.deepEqual(await echo('ask', { being: 'E', method: 'echo', args: { x: 1 } }), { from: 'OWNER', x: 1 }, 'the ward names the asker, and the name is the owner');
259
+ assert.deepEqual(bp(await ask('ask', { being: 'S' })).asks.map((a) => a.name), ['hello', 'buy'], 'no method is her describe, and a gate that reads an occupant record shows the owner nothing: refund is hers to nobody here');
260
+ assert.deepEqual(await ask('ask', { being: 'S', method: 'hello' }), { welcome: true });
261
+ assert.deepEqual(await ask('ask', { being: 'S', method: 'nosuch' }), { error: 'unknown ask' }, 'her own answer, as any asker gets it');
262
+ // Nobody named, and nobody home: one refusal, and never a fourth word.
263
+ assert.deepEqual(await echo('ask', {}), { error: 'no such being' }, 'a ward with no public being has nobody to ask as nobody');
264
+ assert.deepEqual(await echo('public', { key: 'E' }), { public: 'E' });
265
+ assert.deepEqual(await echo('ask', { method: 'echo', args: { x: 2 } }), { from: null, x: 2 }, 'with no being named it is the public being, asked as nobody');
266
+ // What it refuses, each in an object, because the owner hears objects.
267
+ const pk = bp(await ask()).notes.pk;
268
+ assert.deepEqual(await ask('ask', { being: 'nobody' }), { error: 'no such being' });
269
+ assert.deepEqual(await ask('ask', { being: pk }), { error: 'no such being' }, 'the ward is a being to nobody outside, here as at invite and unboot');
270
+ assert.deepEqual(await ask('ask', { being: 42 }), { error: 'no such being' }, 'a name is a string or it is nothing');
271
+ assert.deepEqual(await ask('ask', { being: 'S', method: 42 }), { error: 'an ask is named by a word' }, 'and so is an ask');
272
+ });
231
273
  // The one public being, marked and unmarked through the owner. A second
232
274
  // one would leave the first holding every relation she had, reachable by
233
275
  // nobody at the bare pk and told by nobody that she had been replaced.
@@ -277,6 +319,120 @@ export function conform(label, make, { canDown = true, test }) {
277
319
  assert.deepEqual(await maker.being.say({ id: 'mine' }), { said: { pong: 'MK' } }, 'she asks through the relation, and arrives under her own key');
278
320
  assert.deepEqual(await maker.being.say({ id: 'nothing' }), { said: null });
279
321
  });
322
+ // The count, reached the one way a suite that owns no bytes can reach it:
323
+ // a caller's numbers are in her own partition, so winding them back and
324
+ // standing her ward up again makes her next ask carry a number the far
325
+ // door has already honoured. A kit with no once-only delivery answers it
326
+ // and passes every other chapter here clean.
327
+ kept('a number already honoured is repeated: the far being is not asked twice, and nothing is written for it', async () => {
328
+ const { w, shop, alice } = await world();
329
+ const inv = await shop.being.invite();
330
+ assert.deepEqual(await alice.being.join(inv), { welcome: true });
331
+ assert.deepEqual(await alice.being.buy('a hat'), { ok: true, receipt: 'receipt for a hat' });
332
+ const sold = shop.cells.sales.length;
333
+ // Her keys for the shop, wound back to the number she has just spent.
334
+ const kept = able(w);
335
+ const p = kept.partition('A');
336
+ const keys = p.bind.A.standings;
337
+ const mine = keys.shop;
338
+ const back = mine.seq - 1;
339
+ mine.seq = back;
340
+ await kept.restart('A', p);
341
+ const again = await alice.being.buy('a second hat');
342
+ assert.ok(said(again, 'repeated'), `a number already honoured is repeated, not ${JSON.stringify(again)}`);
343
+ assert.equal(shop.cells.sales.length, sold, 'and she was never asked');
344
+ // The count carries on from where it was: the next number is fresh.
345
+ assert.deepEqual(await alice.being.buy('a third hat'), { ok: true, receipt: 'receipt for a third hat' });
346
+ });
347
+ // A restart is silent by construction: the ward comes back on the same
348
+ // seed with the same pk, and every relation it holds still points at it.
349
+ // What must survive with it is what the door has honoured, or a restart is
350
+ // a stretch of time in which every number can be replayed.
351
+ kept('a restart is silent: same pk, every relation still stands, and the far door still refuses what it spent', async () => {
352
+ const { w, shop, alice } = await world();
353
+ const inv = await shop.being.invite();
354
+ assert.deepEqual(await alice.being.join(inv), { welcome: true });
355
+ assert.deepEqual(await alice.being.buy('a hat'), { ok: true, receipt: 'receipt for a hat' });
356
+ const pk = shop.pk;
357
+ const able2 = able(w);
358
+ const mark = JSON.stringify(able2.partition('S').heirs);
359
+ await able2.restart('S');
360
+ assert.equal(shop.pk, pk, 'a ward that came back changed its pk');
361
+ assert.equal(JSON.stringify(able2.partition('S').heirs), mark, 'and it came back holding what it had honoured');
362
+ assert.deepEqual(await alice.being.buy('a second hat'), { ok: true, receipt: 'receipt for a second hat' }, 'the relation goes on, and she was never told');
363
+ assert.deepEqual(alice.cells.standings.shop.id, 'shop', 'her side was not told either');
364
+ });
365
+ // Notes are the one thing on a record that a being seeds and a ward keeps
366
+ // without ever reading. A kit that files what it was handed rather than a
367
+ // copy of it leaves the seeder a hand inside her cells: it writes without
368
+ // the ward being told, so what a restart brings back is not what she read.
369
+ t('notes are values and are kept as a copy: what the seeder still holds is not what the record holds', async () => {
370
+ const { alice } = await world();
371
+ const seeded = { tier: { deep: 'gold' } };
372
+ assert.ok((await alice.being.invite('noted', seeded)) !== null, 'she minted with notes');
373
+ seeded.tier.deep = 'moved';
374
+ assert.deepEqual(alice.cells.occupants.noted.notes, { tier: { deep: 'gold' } }, 'the record is the copy, all the way down');
375
+ // And notes no harbor could write back are no invitation, for the reason
376
+ // every other write is refused: a record it could not keep.
377
+ assert.equal(await alice.being.invite('bad-notes', { n: 0 / 0 }), null);
378
+ assert.equal(Object.hasOwn(alice.cells.occupants, 'bad-notes'), false, 'and nothing was written for it');
379
+ });
380
+ // The wait, and the one thing a kit can get wrong that nothing else here
381
+ // would notice. A ward bounds every ask itself, so a relation cycle ends:
382
+ // A asks B, B asking back holds A's lane, and the third ask can never be
383
+ // answered. A kit with no bound waits for ever and hangs, and every other
384
+ // chapter in this suite would still pass it. The word is `late`, which
385
+ // promises nothing either way, and the relation lives afterwards.
386
+ t('the wait is bounded, so a cycle ends: the ask that cannot be answered is late, and the relation lives', async () => {
387
+ const { w } = await world();
388
+ const a = await w.boot('MA', Member);
389
+ const b = await w.boot('MB', Member);
390
+ const toB = (await b.being.invite('from-a'));
391
+ const toA = (await a.being.invite('from-b'));
392
+ assert.ok(answered(await a.being.knock(toB, 'ping')), 'a knocks b');
393
+ assert.equal(await a.being.take('b', toB), 'b');
394
+ assert.ok(answered(await b.being.knock(toA, 'ping')), 'b knocks a');
395
+ assert.equal(await b.being.take('a', toA), 'a');
396
+ // A asks B; B, answering, asks A back; A, answering that, asks B on the
397
+ // lane the first ask still holds. Every link is a legal ask.
398
+ const out = (await a.being.relay({ id: 'b', method: 'relay', args: { id: 'a', method: 'relay', args: { id: 'b', method: 'ping' }, time: 200 } }));
399
+ assert.equal(out.said, null, 'the outer ask was answered');
400
+ assert.equal(out.out?.said, 'late', 'and what it carries is the ask that could not be, said as late');
401
+ // The relation is not the worse for it: the lane is free again and the
402
+ // count moved on, which is what a bound is for.
403
+ const after = (await a.being.relay({ id: 'b', method: 'ping' }));
404
+ assert.equal(after.said, null, 'and she asks again on the same standing');
405
+ });
406
+ // Lend is boot's other half: what the device already has, reached the one
407
+ // way anything is reached. The being lent stands in a ward the harbor
408
+ // roots, so nothing here can look at her from the outside; what the ward
409
+ // owes is what is asserted. She is handed an id, the standing is an
410
+ // ordinary one, and the invitation is nowhere she can read.
411
+ lends('what the device lends is a being: she is handed an id, asks on an ordinary standing, and never sees the invitation', async () => {
412
+ const w = await make({ lends: { pen: 'Member' } });
413
+ const maker = await w.boot('MK', Maker);
414
+ assert.deepEqual(await maker.being.borrow({ name: 'pen', id: 'pen' }), { lent: 'pen' }, 'the id she gave is the id she gets');
415
+ // An ordinary standing from there on: she asks on it, and arrives at that
416
+ // being's door as somebody the far side named.
417
+ const said = (await maker.being.say({ id: 'pen' }));
418
+ assert.equal(typeof said.said?.pong, 'string', 'she arrived under an id the far being minted');
419
+ // The keys are the ward's. Nothing of the relation is in her cells, which
420
+ // is the whole difference between this standing and one she gave away.
421
+ const c = w.census();
422
+ const heir = c.binds.MK.standings.pen.heir;
423
+ assert.equal(typeof heir, 'string', 'the ward bound a heir for it');
424
+ assert.equal(JSON.stringify(c.cells.MK).includes(heir), false, 'and the invitation reached her nowhere');
425
+ // Null is every kind of no, and she cannot tell them apart.
426
+ assert.deepEqual(await maker.being.borrow({ name: 'gps', id: 'x' }), { lent: null }, 'a name this box does not lend');
427
+ assert.deepEqual(await maker.being.borrow({ name: 'pen', id: 'pen' }), { lent: null }, 'an id she already holds');
428
+ assert.deepEqual(await maker.being.borrow({ name: 'pen', id: 'OWNER' }), { lent: null }, 'a word the ward reserves');
429
+ });
430
+ lends('a harbor with nothing to lend lends nothing, and says so the same way', async () => {
431
+ const w = await make();
432
+ const maker = await w.boot('MK', Maker);
433
+ assert.deepEqual(await maker.being.borrow({ name: 'pen', id: 'pen' }), { lent: null });
434
+ assert.equal(w.census().binds.MK.standings.pen, undefined, 'and nothing was written for it');
435
+ });
280
436
  // The case the estate's walk reaches by weight and this reaches by hand: a
281
437
  // seat kicked after it answered and before it was taken. She holds a key
282
438
  // the door bound for her, so she is the one person who hears why, and she
@@ -1,4 +1,4 @@
1
- import { type WardPointers } from '../ward/ground.ts';
1
+ import { type WardPointers, type Lend } from '../ward/ground.ts';
2
2
  import type { BeingClass, BeingLike } from '../being/types.ts';
3
3
  import { type Reach } from './reach.ts';
4
4
  import type { Kept, Store, WardRecord } from './store.ts';
@@ -33,7 +33,9 @@ export declare class Harbor {
33
33
  readonly classes: Record<string, BeingClass>;
34
34
  readonly fallbacks: Reach[];
35
35
  readonly announcers: Set<() => void>;
36
- constructor(store: Store, loader?: Loader);
36
+ lend: Lend | undefined;
37
+ constructor(store: Store, loader?: Loader, lend?: Lend);
38
+ protected lendFor(_name: string, _record: WardRecord): Lend | undefined;
37
39
  boot(): Promise<void>;
38
40
  partitionOf(name: string): Record<string, unknown> | undefined;
39
41
  carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;