@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.
- package/README.md +37 -20
- 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 +4 -3
- package/dist/harbor/memory.js +13 -4
- package/dist/harbor/reach.js +1 -1
- package/dist/ward/arithmetic.d.ts +4 -0
- package/dist/ward/arithmetic.js +82 -12
- 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 +11 -3
- package/dist/ward/stance.d.ts +1 -0
- package/dist/ward/stance.js +62 -4
- package/dist/ward/ward.js +13 -4
- package/package.json +7 -6
- package/{SPEC.md → protocol/SPEC.md} +367 -537
- package/protocol/vectors/door.json +345 -0
- package/quo-kit.md +595 -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 +14 -5
- package/src/harbor/reach.ts +1 -1
- package/src/ward/arithmetic.ts +83 -14
- 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 +12 -3
- package/src/ward/stance.ts +60 -4
- package/src/ward/ward.ts +14 -5
- /package/{vectors → protocol/vectors}/arithmetic.json +0 -0
- /package/{vectors → protocol/vectors}/framing.json +0 -0
- /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)
|
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);
|
|
@@ -274,10 +279,14 @@ class Self {
|
|
|
274
279
|
return unreached();
|
|
275
280
|
}
|
|
276
281
|
const { bytes, ephemeral } = sealed;
|
|
277
|
-
// The wait is bounded, and
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
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.
|
|
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
|
-
"./
|
|
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
|
-
"
|
|
53
|
-
"
|
|
53
|
+
"protocol",
|
|
54
|
+
"quo-kit.md",
|
|
54
55
|
"README.md",
|
|
55
56
|
"LICENSE",
|
|
56
57
|
"NOTICE"
|