@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/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.
@@ -18,7 +26,7 @@ const WARD_SEAL = new TextEncoder().encode('quo-ward-seal');
18
26
  // designs and no separation at all: one secret would be doing two jobs with
19
27
  // nothing said about it, and a second kit would have to reproduce a
20
28
  // construction nobody named. HKDF-SHA-256 under a label is the separation
21
- // said out loud, and it is what `vectors/framing.json` pins.
29
+ // said out loud, and it is what `protocol/vectors/framing.json` pins.
22
30
  //
23
31
  // Bytes are key material and text is not. A seed handed in as bytes of the
24
32
  // key length is taken as it stands, which is what a harbor mints; anything
@@ -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);
@@ -274,10 +279,14 @@ class Self {
274
279
  return unreached();
275
280
  }
276
281
  const { bytes, ephemeral } = sealed;
277
- // The wait is bounded, and this is the one thing the ward times. A being
278
- // holds three answers and a wait that does not end is none of them: a
279
- // relation that comes back round holds a lane the answer needs, and only a
280
- // bound on the wait can break that. What comes back late is not read.
282
+ // The wait is bounded here, and again around the lane in the stance. Two
283
+ // bounds and not one, because they end two different things: the stance's
284
+ // ends the wait a being is held in, and this one ends the occupancy of the
285
+ // relation's lane. A being holds three answers and a wait that does not
286
+ // end is none of them, and a lane nobody ever leaves is a relation the
287
+ // next ask never reaches. Only a bound on the wire breaks the second, and
288
+ // taking it out would leave one quiet far side holding the lane for good.
289
+ // What comes back late is not read.
281
290
  //
282
291
  // A wait that ran out is `late`, never unreached. Unreached promises
283
292
  // nothing was delivered and is safe to retry; a bound that expired knows
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quo-systems/quo",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
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"
@@ -34,14 +35,14 @@
34
35
  "types": "./dist/conformance/index.d.ts",
35
36
  "default": "./dist/conformance/index.js"
36
37
  },
37
- "./vectors/*": "./vectors/*",
38
+ "./protocol/*": "./protocol/*",
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 protocol/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"
@@ -49,8 +50,8 @@
49
50
  "files": [
50
51
  "dist",
51
52
  "src",
52
- "vectors",
53
- "SPEC.md",
53
+ "protocol",
54
+ "quo-kit.md",
54
55
  "README.md",
55
56
  "LICENSE",
56
57
  "NOTICE"