nervur 0.19.2 → 0.20.0
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 +66 -101
- package/dist/being/being.d.ts +7 -15
- package/dist/being/being.js +16 -67
- package/dist/being/digest.d.ts +1 -1
- package/dist/being/digest.js +7 -27
- package/dist/being/faculty.d.ts +9 -0
- package/dist/being/faculty.js +68 -0
- package/dist/being/index.d.ts +4 -5
- package/dist/being/index.js +5 -5
- package/dist/being/types.d.ts +35 -62
- package/dist/being/types.js +3 -57
- package/dist/being/words.d.ts +13 -0
- package/dist/being/words.js +14 -0
- package/dist/contract/index.d.ts +26 -0
- package/dist/contract/index.js +19 -0
- package/dist/crypto/aes.d.ts +3 -0
- package/dist/crypto/aes.js +24 -0
- package/dist/crypto/bytes.d.ts +6 -0
- package/dist/crypto/bytes.js +33 -0
- package/dist/crypto/ed25519.d.ts +3 -0
- package/dist/crypto/ed25519.js +85 -0
- package/dist/crypto/hash.d.ts +2 -0
- package/dist/crypto/hash.js +11 -0
- package/dist/crypto/index.d.ts +7 -0
- package/dist/crypto/index.js +10 -0
- package/dist/crypto/json.d.ts +8 -0
- package/dist/crypto/json.js +250 -0
- package/dist/crypto/mlkem.d.ts +12 -0
- package/dist/crypto/mlkem.js +36 -0
- package/dist/crypto/subtle.d.ts +10 -0
- package/dist/crypto/subtle.js +29 -0
- package/dist/crypto/x25519.d.ts +2 -0
- package/dist/crypto/x25519.js +21 -0
- package/dist/folder/index.d.ts +13 -0
- package/dist/folder/index.js +125 -0
- package/dist/harbor/dock.d.ts +19 -0
- package/dist/harbor/dock.js +92 -0
- package/dist/harbor/harbor.d.ts +22 -0
- package/dist/harbor/harbor.js +84 -0
- package/dist/harbor/index.d.ts +4 -10
- package/dist/harbor/index.js +5 -18
- package/dist/harbor/registry.d.ts +7 -0
- package/dist/harbor/registry.js +13 -0
- package/dist/harbor/terrain.d.ts +8 -0
- package/dist/harbor/terrain.js +2 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/pointer/bodies.d.ts +20 -0
- package/dist/pointer/bodies.js +45 -0
- package/dist/pointer/index.d.ts +2 -0
- package/dist/pointer/index.js +4 -0
- package/dist/pointer/world.d.ts +34 -0
- package/dist/pointer/world.js +76 -0
- package/dist/quo/door.d.ts +34 -0
- package/dist/quo/door.js +172 -0
- package/dist/quo/index.d.ts +8 -0
- package/dist/quo/index.js +11 -0
- package/dist/quo/invitation.d.ts +7 -0
- package/dist/quo/invitation.js +15 -0
- package/dist/quo/keys.d.ts +40 -0
- package/dist/quo/keys.js +79 -0
- package/dist/quo/payload.d.ts +15 -0
- package/dist/quo/payload.js +55 -0
- package/dist/quo/relations.d.ts +40 -0
- package/dist/quo/relations.js +33 -0
- package/dist/quo/reply.d.ts +13 -0
- package/dist/quo/reply.js +35 -0
- package/dist/quo/seal.d.ts +42 -0
- package/dist/quo/seal.js +78 -0
- package/dist/quo/standing.d.ts +39 -0
- package/dist/quo/standing.js +90 -0
- package/dist/ward/allowance.d.ts +13 -9
- package/dist/ward/allowance.js +28 -67
- package/dist/ward/cells.d.ts +6 -6
- package/dist/ward/cells.js +67 -179
- package/dist/ward/index.d.ts +5 -10
- package/dist/ward/index.js +6 -13
- package/dist/ward/partition.d.ts +44 -60
- package/dist/ward/partition.js +56 -285
- package/dist/ward/stance.d.ts +29 -20
- package/dist/ward/stance.js +173 -406
- package/dist/ward/ward-being.d.ts +33 -0
- package/dist/ward/ward-being.js +76 -0
- package/dist/ward/ward.d.ts +43 -11
- package/dist/ward/ward.js +215 -363
- package/package.json +19 -33
- package/src/being/being.ts +31 -78
- package/src/being/digest.ts +7 -35
- package/src/being/faculty.ts +66 -0
- package/src/being/index.ts +5 -6
- package/src/being/types.ts +47 -145
- package/src/being/words.ts +31 -0
- package/src/contract/index.ts +44 -0
- package/src/crypto/aes.ts +26 -0
- package/src/crypto/bytes.ts +37 -0
- package/src/crypto/ed25519.ts +84 -0
- package/src/crypto/hash.ts +14 -0
- package/src/crypto/index.ts +10 -0
- package/src/crypto/json.ts +241 -0
- package/src/crypto/mlkem.ts +38 -0
- package/src/crypto/subtle.ts +33 -0
- package/src/crypto/x25519.ts +21 -0
- package/src/folder/index.ts +134 -0
- package/src/harbor/dock.ts +101 -0
- package/src/harbor/harbor.ts +105 -0
- package/src/harbor/index.ts +5 -19
- package/src/harbor/registry.ts +20 -0
- package/src/harbor/terrain.ts +13 -0
- package/src/index.ts +20 -0
- package/src/pointer/bodies.ts +47 -0
- package/src/pointer/index.ts +4 -0
- package/src/pointer/world.ts +91 -0
- package/src/quo/door.ts +178 -0
- package/src/quo/index.ts +11 -0
- package/src/quo/invitation.ts +15 -0
- package/src/quo/keys.ts +91 -0
- package/src/quo/payload.ts +61 -0
- package/src/quo/relations.ts +62 -0
- package/src/quo/reply.ts +38 -0
- package/src/quo/seal.ts +101 -0
- package/src/quo/standing.ts +111 -0
- package/src/stand/main.ts +19 -0
- package/src/stand/stand.ts +178 -0
- package/src/ward/allowance.ts +37 -75
- package/src/ward/cells.ts +63 -176
- package/src/ward/index.ts +6 -17
- package/src/ward/partition.ts +86 -326
- package/src/ward/stance.ts +185 -420
- package/src/ward/ward-being.ts +97 -0
- package/src/ward/ward.ts +229 -363
- package/dist/being/lent.d.ts +0 -32
- package/dist/being/lent.js +0 -72
- package/dist/being/silence.d.ts +0 -12
- package/dist/being/silence.js +0 -41
- package/dist/conformance/assert.d.ts +0 -11
- package/dist/conformance/assert.js +0 -106
- package/dist/conformance/beings.d.ts +0 -199
- package/dist/conformance/beings.js +0 -188
- package/dist/conformance/estate.d.ts +0 -5
- package/dist/conformance/estate.js +0 -388
- package/dist/conformance/index.d.ts +0 -80
- package/dist/conformance/index.js +0 -819
- package/dist/conformance/reach.d.ts +0 -10
- package/dist/conformance/reach.js +0 -72
- package/dist/conformance/store.d.ts +0 -5
- package/dist/conformance/store.js +0 -113
- package/dist/harbor/box.d.ts +0 -121
- package/dist/harbor/box.js +0 -121
- package/dist/harbor/core.d.ts +0 -55
- package/dist/harbor/core.js +0 -662
- package/dist/harbor/dial.d.ts +0 -9
- package/dist/harbor/dial.js +0 -81
- package/dist/harbor/memory.d.ts +0 -29
- package/dist/harbor/memory.js +0 -104
- package/dist/harbor/reach.d.ts +0 -36
- package/dist/harbor/reach.js +0 -199
- package/dist/harbor/store.d.ts +0 -35
- package/dist/harbor/store.js +0 -62
- package/dist/vector/cases.d.ts +0 -42
- package/dist/vector/cases.js +0 -223
- package/dist/vector/index.d.ts +0 -6
- package/dist/vector/index.js +0 -8
- package/dist/vector/stand.d.ts +0 -9
- package/dist/vector/stand.js +0 -77
- package/dist/vector/world.d.ts +0 -144
- package/dist/vector/world.js +0 -209
- package/dist/ward/arithmetic.d.ts +0 -31
- package/dist/ward/arithmetic.js +0 -249
- package/dist/ward/door.d.ts +0 -18
- package/dist/ward/door.js +0 -186
- package/dist/ward/ground.d.ts +0 -29
- package/dist/ward/ground.js +0 -56
- package/dist/ward/heirs.d.ts +0 -13
- package/dist/ward/heirs.js +0 -117
- package/dist/ward/json.d.ts +0 -2
- package/dist/ward/json.js +0 -163
- package/dist/ward/owner.d.ts +0 -13
- package/dist/ward/owner.js +0 -220
- package/dist/ward/seal.d.ts +0 -52
- package/dist/ward/seal.js +0 -150
- package/src/being/lent.ts +0 -72
- package/src/being/silence.ts +0 -46
- package/src/conformance/assert.ts +0 -100
- package/src/conformance/beings.ts +0 -188
- package/src/conformance/estate.ts +0 -412
- package/src/conformance/index.ts +0 -965
- package/src/conformance/reach.ts +0 -83
- package/src/conformance/store.ts +0 -125
- package/src/harbor/box.ts +0 -131
- package/src/harbor/core.ts +0 -699
- package/src/harbor/dial.ts +0 -112
- package/src/harbor/memory.ts +0 -123
- package/src/harbor/reach.ts +0 -221
- package/src/harbor/store.ts +0 -91
- package/src/vector/cases.ts +0 -257
- package/src/vector/index.ts +0 -11
- package/src/vector/stand.ts +0 -76
- package/src/vector/world.ts +0 -232
- package/src/ward/arithmetic.ts +0 -251
- package/src/ward/door.ts +0 -186
- package/src/ward/ground.ts +0 -142
- package/src/ward/heirs.ts +0 -116
- package/src/ward/json.ts +0 -144
- package/src/ward/owner.ts +0 -214
- package/src/ward/seal.ts +0 -178
package/package.json
CHANGED
|
@@ -1,54 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nervur",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Nervur's kit of Quo: an object asks another object and gets an answer, without knowing where it is.
|
|
3
|
+
"version": "0.20.0",
|
|
4
|
+
"description": "Nervur's kit of Quo: an object asks another object and gets an answer, without knowing where it is.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"quo",
|
|
7
7
|
"protocol",
|
|
8
8
|
"capability",
|
|
9
|
-
"object-capability"
|
|
10
|
-
"rpc",
|
|
11
|
-
"ed25519",
|
|
12
|
-
"x25519"
|
|
9
|
+
"object-capability"
|
|
13
10
|
],
|
|
14
11
|
"author": "Razvan Gherghina",
|
|
15
12
|
"license": "Apache-2.0",
|
|
16
13
|
"homepage": "https://nervur.org",
|
|
17
14
|
"type": "module",
|
|
18
|
-
"engines": {
|
|
19
|
-
"node": ">=22.18"
|
|
20
|
-
},
|
|
21
15
|
"exports": {
|
|
22
16
|
".": {
|
|
23
|
-
"types": "./dist/
|
|
24
|
-
"default": "./dist/
|
|
25
|
-
},
|
|
26
|
-
"./ward": {
|
|
27
|
-
"types": "./dist/ward/index.d.ts",
|
|
28
|
-
"default": "./dist/ward/index.js"
|
|
29
|
-
},
|
|
30
|
-
"./harbor": {
|
|
31
|
-
"types": "./dist/harbor/index.d.ts",
|
|
32
|
-
"default": "./dist/harbor/index.js"
|
|
33
|
-
},
|
|
34
|
-
"./conformance": {
|
|
35
|
-
"types": "./dist/conformance/index.d.ts",
|
|
36
|
-
"default": "./dist/conformance/index.js"
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
37
19
|
},
|
|
38
|
-
"./
|
|
39
|
-
"types": "./dist/
|
|
40
|
-
"default": "./dist/
|
|
41
|
-
}
|
|
42
|
-
"./package.json": "./package.json"
|
|
20
|
+
"./folder": {
|
|
21
|
+
"types": "./dist/folder/index.d.ts",
|
|
22
|
+
"default": "./dist/folder/index.js"
|
|
23
|
+
}
|
|
43
24
|
},
|
|
44
25
|
"scripts": {
|
|
45
|
-
"build": "
|
|
46
|
-
"check": "node ../../test.mjs \"test/*.test.ts\"",
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"cover": "node --test --test-timeout=60000 --experimental-test-coverage --test-coverage-exclude=\"**/dist/**\" --test-coverage-exclude=\"**/test/**\" --test-coverage-lines=99 --test-coverage-branches=93 --test-coverage-functions=95 \"test/*.test.ts\"",
|
|
26
|
+
"build": "tsc -p tsconfig.build.json",
|
|
27
|
+
"check": "tsc -p tsconfig.json && npm run build && node ../../test.mjs \"test/unit/*.test.ts\" \"test/e2e/*.test.ts\"",
|
|
28
|
+
"deep": "node ../../test.mjs \"test/terrain/*.test.ts\"",
|
|
29
|
+
"cover": "node --test --test-timeout=60000 --experimental-test-coverage --test-coverage-exclude=\"**/dist/**\" --test-coverage-exclude=\"**/test/**\" --test-coverage-exclude=\"**/src/stand/**\" --test-coverage-exclude=\"../../quo/**\" --test-coverage-lines=99 --test-coverage-branches=93 --test-coverage-functions=95 \"test/unit/*.test.ts\" \"test/e2e/*.test.ts\"",
|
|
50
30
|
"prepublishOnly": "test \"$NERVUR_GATED\" = 1 || { echo 'a publish is /release, from the root, on the human'\"'\"'s word' >&2; exit 1; }"
|
|
51
31
|
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22.18"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@noble/post-quantum": "0.7.1"
|
|
37
|
+
},
|
|
52
38
|
"publishConfig": {
|
|
53
39
|
"access": "public"
|
|
54
40
|
},
|
package/src/being/being.ts
CHANGED
|
@@ -1,28 +1,23 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The base class most beings extend. It writes `answer
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
import type { Ask, Asker, Blueprint,
|
|
2
|
+
// The base class most beings extend. It writes `answer`: the empty ask is
|
|
3
|
+
// her blueprint for this asker, a named ask calls her method of that name,
|
|
4
|
+
// and an ask she did not declare, or hides from this asker, is an error
|
|
5
|
+
// object. A class that writes `answer` by hand is a being just the same.
|
|
6
|
+
// What she acts through is `this.stance`.
|
|
7
|
+
import type { Ask, Asker, Blueprint, JsonObject, Reply, Schema, Stance } from './types.ts';
|
|
8
8
|
|
|
9
|
-
// One declared ask. `for` decides whether this asker sees it, and so
|
|
10
|
-
// this asker may call it
|
|
9
|
+
// One declared ask. `for` decides whether this asker sees it, and so
|
|
10
|
+
// whether this asker may call it.
|
|
11
11
|
export type AskSpec = {
|
|
12
|
-
description?: string;
|
|
13
|
-
input?: Schema;
|
|
14
|
-
|
|
15
|
-
for?: (occupant: OccupantRecord | undefined, asker: Asker) => boolean;
|
|
12
|
+
readonly description?: string;
|
|
13
|
+
readonly input?: Schema;
|
|
14
|
+
readonly for?: (asker: Asker, notes: JsonObject | undefined) => boolean;
|
|
16
15
|
};
|
|
17
16
|
|
|
18
|
-
// Names a subclass may not
|
|
19
|
-
const RESERVED = new Set(['answer', 'describe', 'stance', 'cells', '
|
|
17
|
+
// Names a subclass may not declare as an ask, because they are the base's.
|
|
18
|
+
const RESERVED = new Set(['answer', 'describe', 'stance', 'cells', 'notes', 'constructor']);
|
|
20
19
|
|
|
21
|
-
// Whether she
|
|
22
|
-
// below Object's. A name Object lends every object, `hasOwnProperty` or
|
|
23
|
-
// `toString`, is not a method she wrote; and a field she assigns in her own
|
|
24
|
-
// constructor is not there yet when the base checks, so an ask is a method
|
|
25
|
-
// on the prototype and nothing else.
|
|
20
|
+
// Whether she wrote a method of that name, below Object's prototype.
|
|
26
21
|
const wrote = (self: object, name: string): boolean => {
|
|
27
22
|
for (let p = Object.getPrototypeOf(self); p !== null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
|
|
28
23
|
if (Object.hasOwn(p, name)) return typeof (p as Record<string, unknown>)[name] === 'function';
|
|
@@ -30,18 +25,13 @@ const wrote = (self: object, name: string): boolean => {
|
|
|
30
25
|
return false;
|
|
31
26
|
};
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// asks she may mean to drop and an order she did not write. A subclass that
|
|
38
|
-
// means to extend says so, `static override asks = { ...Parent.asks, mine: {} }`.
|
|
28
|
+
type Method = (args: JsonObject, asker: Asker) => Reply | Promise<Reply>;
|
|
29
|
+
|
|
30
|
+
// A subclass's statics replace its parent's and never merge: her blueprint
|
|
31
|
+
// is what the class in front of you declares, in its order.
|
|
39
32
|
export class Being {
|
|
40
|
-
// Her cells' defaults
|
|
41
|
-
// a restart keeps what she wrote.
|
|
33
|
+
// Her cells' defaults, written at birth only where a key is missing.
|
|
42
34
|
static cells: JsonObject = {};
|
|
43
|
-
// What she can be asked. Declaration order is blueprint order, except a
|
|
44
|
-
// name that reads as an array index, which the language lists first.
|
|
45
35
|
static asks: Record<string, AskSpec> = {};
|
|
46
36
|
|
|
47
37
|
readonly stance: Stance;
|
|
@@ -51,73 +41,36 @@ export class Being {
|
|
|
51
41
|
const C = this.constructor as typeof Being;
|
|
52
42
|
for (const name of Object.keys(C.asks)) {
|
|
53
43
|
if (RESERVED.has(name)) throw new Error(`ask '${name}' is a reserved name`);
|
|
54
|
-
if (!wrote(this, name)) throw new Error(`ask '${name}' has no method
|
|
44
|
+
if (!wrote(this, name)) throw new Error(`ask '${name}' has no method`);
|
|
55
45
|
}
|
|
56
|
-
// Own keys only: a default named after a member of Object's prototype is
|
|
57
|
-
// still hers, and still missing until she writes it.
|
|
58
46
|
for (const [k, v] of Object.entries(C.cells)) if (!Object.hasOwn(stance.cells, k)) stance.cells[k] = structuredClone(v);
|
|
59
47
|
}
|
|
60
48
|
|
|
61
|
-
get cells():
|
|
49
|
+
get cells(): JsonObject {
|
|
62
50
|
return this.stance.cells;
|
|
63
51
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return this.stance.occupants;
|
|
69
|
-
}
|
|
70
|
-
// A standing at one of the things this device can do, under an id of hers.
|
|
71
|
-
// The ward knocks and takes it for her; the invitation never reaches her.
|
|
72
|
-
lend(name: string, id: string): Promise<string | null> {
|
|
73
|
-
return this.stance.lend(name, id);
|
|
74
|
-
}
|
|
75
|
-
invite(id: string, notes?: JsonObject): Promise<Invitation | null> {
|
|
76
|
-
return this.stance.occupants.invite(id, notes);
|
|
77
|
-
}
|
|
78
|
-
knock(invitation: Invitation, method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer> {
|
|
79
|
-
return this.stance.standings.knock(invitation, method, args, wanted);
|
|
80
|
-
}
|
|
81
|
-
take(id: string, invitation: Invitation): Promise<string | null> {
|
|
82
|
-
return this.stance.standings.take(id, invitation);
|
|
83
|
-
}
|
|
84
|
-
// A new being of her ward, by class name, under a key she chooses. With an
|
|
85
|
-
// id, she holds a standing to the being she made, who knows her by her key.
|
|
86
|
-
boot(className: string, key: string, id?: string): Promise<string | null> {
|
|
87
|
-
return this.stance.boot(className, key, id);
|
|
88
|
-
}
|
|
89
|
-
// The occupant record for whoever is at the door. Undefined at a public being.
|
|
90
|
-
occupant(asker: Asker): OccupantRecord | undefined {
|
|
91
|
-
return asker.id !== undefined && Object.hasOwn(this.cells.occupants, asker.id) ? this.cells.occupants[asker.id] : undefined;
|
|
52
|
+
|
|
53
|
+
// The notes she invited this asker under.
|
|
54
|
+
notes(asker: Asker): JsonObject | undefined {
|
|
55
|
+
return asker.id === undefined ? undefined : this.stance.occupants.notes(asker.id);
|
|
92
56
|
}
|
|
93
57
|
|
|
94
|
-
// Her blueprint for this asker. Override to shape it by hand.
|
|
95
58
|
describe(asker: Asker): Blueprint {
|
|
96
59
|
const C = this.constructor as typeof Being;
|
|
97
|
-
const
|
|
60
|
+
const notes = this.notes(asker);
|
|
98
61
|
const asks: Ask[] = [];
|
|
99
62
|
for (const [name, spec] of Object.entries(C.asks)) {
|
|
100
|
-
if (spec.for && !spec.for(
|
|
101
|
-
|
|
102
|
-
if (spec.description !== undefined) ask.description = spec.description;
|
|
103
|
-
if (spec.output !== undefined) ask.output = spec.output;
|
|
104
|
-
asks.push(ask);
|
|
63
|
+
if (spec.for && !spec.for(asker, notes)) continue;
|
|
64
|
+
asks.push({ name, ...(spec.description === undefined ? {} : { description: spec.description }), input: spec.input ?? { type: 'object' } });
|
|
105
65
|
}
|
|
106
66
|
return { asks, notes: {} };
|
|
107
67
|
}
|
|
108
68
|
|
|
109
|
-
|
|
110
|
-
async answer(asker: Asker, method?: string, args: JsonObject = {}): Promise<Reply> {
|
|
69
|
+
async answer(asker: Asker, method: string | undefined, args: JsonObject): Promise<Reply> {
|
|
111
70
|
if (method === undefined) return this.describe(asker);
|
|
112
71
|
const C = this.constructor as typeof Being;
|
|
113
|
-
// Declared, by her, on purpose. `asks` is an ordinary object, so a bare
|
|
114
|
-
// lookup would also find every name on Object's prototype: `valueOf`
|
|
115
|
-
// would answer with her stance, `toString` with a string, and neither is
|
|
116
|
-
// an ask she wrote. Only her own keys are asks, which is what describe
|
|
117
|
-
// shows. What she shows is what she can be asked.
|
|
118
72
|
const spec = Object.hasOwn(C.asks, method) ? C.asks[method] : undefined;
|
|
119
|
-
if (!spec || (spec.for && !spec.for(this.
|
|
120
|
-
|
|
121
|
-
return fn.call(this, args ?? {}, asker);
|
|
73
|
+
if (!spec || (spec.for && !spec.for(asker, this.notes(asker)))) return { error: 'unknown ask' };
|
|
74
|
+
return ((this as unknown as Record<string, Method>)[method] as Method).call(this, args, asker);
|
|
122
75
|
}
|
|
123
76
|
}
|
package/src/being/digest.ts
CHANGED
|
@@ -1,46 +1,18 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
//
|
|
3
|
-
// blueprint.
|
|
4
|
-
//
|
|
5
|
-
import type
|
|
2
|
+
// A being's `seen`: SHA-256, lowercase hex, over the canonical form of her
|
|
3
|
+
// blueprint for one asker. Keys sorted by UTF-16 code units, numbers as
|
|
4
|
+
// ECMAScript writes them, no whitespace.
|
|
5
|
+
import { hex, sha256, utf8, type Json } from '../crypto/index.ts';
|
|
6
6
|
|
|
7
|
-
// A value I-JSON has no room for: a key left empty, a function, a symbol.
|
|
8
|
-
// None of them cross an edge, so none of them may reach a digest. A key
|
|
9
|
-
// carrying one is dropped and a slot carrying one is null, which is what
|
|
10
|
-
// crossing does to them, so the digest names what arrived and not what she
|
|
11
|
-
// happened to be holding.
|
|
12
|
-
const absent = (v: unknown): boolean => v === undefined || typeof v === 'function' || typeof v === 'symbol';
|
|
13
|
-
|
|
14
|
-
// JCS for I-JSON values: sorted keys, no whitespace, JSON escaping. Numbers
|
|
15
|
-
// are serialized as ES does, which is what RFC 8785 specifies. A hole in a
|
|
16
|
-
// list is null, as JSON writes it.
|
|
17
7
|
export const canonical = (v: Json): string => {
|
|
18
|
-
if (Array.isArray(v)) return `[${
|
|
8
|
+
if (Array.isArray(v)) return `[${v.map(canonical).join(',')}]`;
|
|
19
9
|
if (v !== null && typeof v === 'object') {
|
|
20
10
|
return `{${Object.keys(v)
|
|
21
|
-
.filter((k) => !absent(v[k]))
|
|
22
11
|
.sort()
|
|
23
|
-
.map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`)
|
|
12
|
+
.map((k) => `${JSON.stringify(k)}:${canonical(v[k]!)}`)
|
|
24
13
|
.join(',')}}`;
|
|
25
14
|
}
|
|
26
15
|
return JSON.stringify(v);
|
|
27
16
|
};
|
|
28
17
|
|
|
29
|
-
|
|
30
|
-
// which is the price of the boundary: nothing under `src/being` imports
|
|
31
|
-
// anything above it, because this is the whole world a being's own code sees
|
|
32
|
-
// and a being reaching the ward is the thing the shape is against.
|
|
33
|
-
const hex = (bytes: ArrayBuffer): string =>
|
|
34
|
-
Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
35
|
-
|
|
36
|
-
// `crypto.subtle` is read at the call and never captured at load. A browser
|
|
37
|
-
// on a plain http:// origin has `crypto` without `subtle`, and a terrain may
|
|
38
|
-
// install one after this module is first imported; either way the failure is
|
|
39
|
-
// one sentence and not a TypeError from inside a digest nobody can read.
|
|
40
|
-
const subtle = (): SubtleCrypto => {
|
|
41
|
-
const s = globalThis.crypto?.subtle;
|
|
42
|
-
if (!s) throw new Error('this terrain has no crypto.subtle: a being needs a secure context to be described');
|
|
43
|
-
return s;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export const digest = async (blueprint: Json): Promise<string> => hex(await subtle().digest('SHA-256', new TextEncoder().encode(canonical(blueprint))));
|
|
18
|
+
export const digest = async (blueprint: Json): Promise<string> => hex(await sha256(utf8(canonical(blueprint))));
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// A faculty: a being of the box ward that fulfils a contract. A contract is
|
|
3
|
+
// a class between `Faculty` and the class that stands, usually abstract:
|
|
4
|
+
//
|
|
5
|
+
// abstract class Timer extends Faculty { ... } the contract
|
|
6
|
+
// class IntervalTimer extends Timer { ... } one class that fulfils it
|
|
7
|
+
//
|
|
8
|
+
// A being lends by the contract and never learns the class. Two asks are the
|
|
9
|
+
// faculty's own and answered to the ward-being that opened her alone:
|
|
10
|
+
// `offer`, an invitation on herself for a ward the dock vouches for, and
|
|
11
|
+
// `retract`, an offer that was not taken.
|
|
12
|
+
import { Being } from './being.ts';
|
|
13
|
+
import { WARD, type Asker, type Blueprint, type JsonObject, type Reply } from './types.ts';
|
|
14
|
+
|
|
15
|
+
export class Faculty extends Being {
|
|
16
|
+
// The contracts a class fulfils: itself and every class between it and
|
|
17
|
+
// `Faculty`, by name.
|
|
18
|
+
static contracts(C: abstract new (...args: never[]) => unknown): string[] {
|
|
19
|
+
const names: string[] = [];
|
|
20
|
+
for (let c: unknown = C; typeof c === 'function' && c !== Faculty; c = Object.getPrototypeOf(c)) names.push((c as { name: string }).name);
|
|
21
|
+
return names;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static fulfils(C: unknown): C is typeof Faculty {
|
|
25
|
+
return typeof C === 'function' && C.prototype instanceof Faculty;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#open(): Record<string, string> {
|
|
29
|
+
if (this.cells.offers === undefined) this.cells.offers = {};
|
|
30
|
+
return this.cells.offers as Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async #offer(): Promise<JsonObject> {
|
|
34
|
+
const n = ((this.cells.offered as number | undefined) ?? 0) + 1;
|
|
35
|
+
this.cells.offered = n;
|
|
36
|
+
const id = `lent:${n}`;
|
|
37
|
+
const invitation = await this.stance.occupants.invite(id);
|
|
38
|
+
if (invitation === null) return { error: 'not invited' };
|
|
39
|
+
this.#open()[invitation.heir] = id;
|
|
40
|
+
return { invitation };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#retract(args: JsonObject): JsonObject {
|
|
44
|
+
const open = this.#open();
|
|
45
|
+
const id = typeof args.heir === 'string' && Object.hasOwn(open, args.heir) ? open[args.heir] : undefined;
|
|
46
|
+
if (id === undefined) return { retracted: null };
|
|
47
|
+
this.stance.occupants.remove(id);
|
|
48
|
+
delete open[args.heir as string];
|
|
49
|
+
return { retracted: id };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override describe(asker: Asker): Blueprint {
|
|
53
|
+
const blueprint = super.describe(asker);
|
|
54
|
+
if (asker.id !== WARD) return blueprint;
|
|
55
|
+
return { ...blueprint, asks: [...blueprint.asks, { name: 'offer', input: { type: 'object' } }, { name: 'retract', input: { type: 'object', required: ['heir'] } }] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// An offer is closed by the first word from the occupant it was made for.
|
|
59
|
+
override answer(asker: Asker, method: string | undefined, args: JsonObject): Promise<Reply> {
|
|
60
|
+
if (asker.id === WARD && method === 'offer') return this.#offer();
|
|
61
|
+
if (asker.id === WARD && method === 'retract') return Promise.resolve(this.#retract(args));
|
|
62
|
+
const open = this.#open();
|
|
63
|
+
for (const [heir, id] of Object.entries(open)) if (id === asker.id) delete open[heir];
|
|
64
|
+
return super.answer(asker, method, args);
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/being/index.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
//
|
|
2
|
+
// What a being author imports.
|
|
3
3
|
export { Being, type AskSpec } from './being.ts';
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export type * from './types.ts';
|
|
4
|
+
export { canonical, digest } from './digest.ts';
|
|
5
|
+
export { Faculty } from './faculty.ts';
|
|
6
|
+
export { OWNER, WARD, type Answer, type Ask, type Asker, type BeingClass, type BeingLike, type Blueprint, type Invitation, type Json, type JsonObject, type Occupants, type Reply, type Schema, type Stance, type Standing, type Standings, type Wanted } from './types.ts';
|
|
7
|
+
export { isSilence, isWord, silence, told, word, type Silence, type Word, type WordName } from './words.ts';
|
package/src/being/types.ts
CHANGED
|
@@ -1,170 +1,72 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import type {
|
|
2
|
+
// Everything a being author holds, as types.
|
|
3
|
+
import type { Json } from '../crypto/index.ts';
|
|
4
|
+
import type { Invitation } from '../quo/index.ts';
|
|
5
|
+
import type { Silence, Word } from './words.ts';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
export type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
|
|
7
|
+
export type { Invitation, Json };
|
|
8
8
|
export type JsonObject = { [key: string]: Json };
|
|
9
9
|
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// The ids the ward speaks under, and no being may mint. An asker id is hers:
|
|
14
|
-
// she chooses the strings, and the ward has no business in her namespace. But
|
|
15
|
-
// the ward must sometimes stand at her door itself — its describe asks every
|
|
16
|
-
// being under OWNER — and an id it shares with one of her occupants is two
|
|
17
|
-
// different parties wearing one name. She could not tell them apart, and the
|
|
18
|
-
// ward would shape its describe through an occupant's gate.
|
|
19
|
-
//
|
|
20
|
-
// So the ward's words are shouted, and refused at the mint. A word can only
|
|
21
|
-
// be reserved before anyone has used it: a partition already holding an
|
|
22
|
-
// occupant of that name could never have it taken back. PUBLIC guards nothing
|
|
23
|
-
// today — a public asker is {} and carries no id at all — and that is exactly
|
|
24
|
-
// why it is claimed now, while claiming it is free.
|
|
10
|
+
// Who is at the door: the id she filed the occupant under, `{}` for nobody,
|
|
11
|
+
// and `{ id: OWNER }` for whoever holds the ward's unsealed ask.
|
|
12
|
+
export type Asker = { readonly id: string } | { readonly id?: undefined };
|
|
25
13
|
export const OWNER = 'OWNER';
|
|
26
|
-
export const PUBLIC = 'PUBLIC';
|
|
27
|
-
export const RESERVED_IDS: readonly string[] = [OWNER, PUBLIC];
|
|
28
14
|
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
export
|
|
15
|
+
// The ward-being's key in her ward, and so the id every being she makes
|
|
16
|
+
// knows her by.
|
|
17
|
+
export const WARD = 'ward';
|
|
32
18
|
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// an invitation to a public being carries no heir and no secret, and a key
|
|
37
|
-
// present and undefined is not the same object once JSON has been through it.
|
|
38
|
-
export const invitationArgs = (inv: Invitation): JsonObject => ({
|
|
39
|
-
ward: inv.ward,
|
|
40
|
-
...(inv.heir !== undefined ? { heir: inv.heir } : {}),
|
|
41
|
-
...(inv.secret !== undefined ? { secret: inv.secret } : {}),
|
|
42
|
-
});
|
|
19
|
+
// What comes back from an ask, and what a being answers.
|
|
20
|
+
export type Answer = Json | Silence | Word;
|
|
21
|
+
export type Reply = Json | Silence | undefined;
|
|
43
22
|
|
|
44
|
-
|
|
45
|
-
// the reading of it is too, and it is one reading: a form that
|
|
46
|
-
// asks a guest for one, a lent being handed one to wake her maker by, and a
|
|
47
|
-
// shell that finds one in a link all ask the same question, and a kinder
|
|
48
|
-
// answer in one of them is a value that fails at a door instead of at the
|
|
49
|
-
// edge it came in by. A ward pk is a hundred and twenty-eight lowercase hex,
|
|
50
|
-
// and a heir comes with its secret or neither comes: an invitation to a
|
|
51
|
-
// public being carries no heir, and a heir with no secret opens nothing.
|
|
52
|
-
export const isInvitation = (v: unknown): v is Invitation => {
|
|
53
|
-
if (v === null || typeof v !== 'object' || Array.isArray(v)) return false;
|
|
54
|
-
const o = v as Record<string, unknown>;
|
|
55
|
-
if (typeof o.ward !== 'string' || !/^[0-9a-f]{128}$/.test(o.ward)) return false;
|
|
56
|
-
const heir = typeof o.heir === 'string',
|
|
57
|
-
secret = typeof o.secret === 'string';
|
|
58
|
-
return (heir && secret) || (!heir && !secret && !('heir' in o) && !('secret' in o));
|
|
59
|
-
};
|
|
23
|
+
export type Wanted = { readonly time?: number };
|
|
60
24
|
|
|
61
|
-
// A blueprint
|
|
25
|
+
// A blueprint: the asks a being can be asked, as an MCP tool list, and notes.
|
|
62
26
|
export type Schema = JsonObject;
|
|
63
|
-
export type Ask = { name: string; description?: string; input: Schema
|
|
27
|
+
export type Ask = { name: string; description?: string; input: Schema };
|
|
64
28
|
export type Blueprint = { asks: Ask[]; notes: Json };
|
|
65
29
|
|
|
66
|
-
|
|
67
|
-
// the far side of a door is somebody else's code, so nothing may be written
|
|
68
|
-
// into a standing's record as a blueprint without being read as one first: a
|
|
69
|
-
// side walks `asks` by name, and a list of anything else, or no list at all,
|
|
70
|
-
// is a side that breaks on a far ward's answer. The asks are checked to the
|
|
71
|
-
// depth a side reads them, a name and an input each, and no further: what a
|
|
72
|
-
// far being puts beside those is hers.
|
|
73
|
-
export const isBlueprint = (v: unknown): v is Blueprint => {
|
|
74
|
-
if (v === null || typeof v !== 'object' || Array.isArray(v)) return false;
|
|
75
|
-
const { asks, notes } = v as { asks?: unknown; notes?: unknown };
|
|
76
|
-
if (!Array.isArray(asks) || notes === undefined) return false;
|
|
77
|
-
return asks.every((a) => a !== null && typeof a === 'object' && !Array.isArray(a) && typeof (a as Ask).name === 'string' && (a as Ask).input !== null && typeof (a as Ask).input === 'object' && !Array.isArray((a as Ask).input));
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export type StandingRecord = { id: string; digest: string | null; blueprint: Blueprint | null; seen: string | null };
|
|
81
|
-
export type OccupantRecord = { id: string; notes: JsonObject };
|
|
82
|
-
export type Cells = {
|
|
83
|
-
standings: Record<string, StandingRecord>;
|
|
84
|
-
occupants: Record<string, OccupantRecord>;
|
|
85
|
-
[hers: string]: Json;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export type Silence = typeof silence;
|
|
89
|
-
|
|
90
|
-
// The ward's words: what her ward says when no object came back and it knows
|
|
91
|
-
// why. A door says the first five to a key it has bound, and rides them on
|
|
92
|
-
// the wire as `{ quo: word }`; her own ward says the last four to her and
|
|
93
|
-
// they never leave the ward. Each is one frozen object under one symbol key,
|
|
94
|
-
// so no being makes one by accident and none crosses an edge as a value. A
|
|
95
|
-
// being who reaches for the symbol on purpose makes an object her ward reads
|
|
96
|
-
// as her having thrown, which is what a being who returns nonsense gets: the
|
|
97
|
-
// key is a guard against collision, never against her.
|
|
98
|
-
export type DoorWord = 'removed' | 'absent' | 'unannounced' | 'repeated' | 'threw';
|
|
99
|
-
export type WardWord = 'unreached' | 'late' | 'invitation' | 'dropped';
|
|
100
|
-
export type WordName = DoorWord | WardWord;
|
|
101
|
-
export const WORD_KEY: unique symbol = Symbol.for('quo.word');
|
|
102
|
-
export type Word<W extends WordName = WordName> = { readonly [K in typeof WORD_KEY]: W };
|
|
103
|
-
export type Unreached = Word<'unreached'>;
|
|
104
|
-
|
|
105
|
-
// What comes back from an ask or a knock.
|
|
106
|
-
export type Answer = Json | Silence | Word;
|
|
107
|
-
// What a being answers.
|
|
108
|
-
export type Reply = Json | Silence;
|
|
109
|
-
|
|
110
|
-
// What a being may ask for when she wants to say so herself: the time this
|
|
111
|
-
// one ask may spend. Optional, and so is saying anything at all: an ask that
|
|
112
|
-
// says nothing gets her ward's default, which is the ordinary way to ask.
|
|
113
|
-
export type Wanted = { time?: number };
|
|
114
|
-
|
|
115
|
-
export type Standing = {
|
|
30
|
+
export interface Standing {
|
|
116
31
|
readonly id: string;
|
|
117
32
|
ask(method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
|
|
118
|
-
}
|
|
33
|
+
}
|
|
119
34
|
|
|
120
|
-
|
|
121
|
-
|
|
35
|
+
// Her side of the relations she holds.
|
|
36
|
+
export interface Standings {
|
|
37
|
+
// Knocks with the empty ask and holds the relation under `id`: the id, or
|
|
38
|
+
// null where the id is taken or reserved, or no object came back.
|
|
122
39
|
take(id: string, invitation: Invitation): Promise<string | null>;
|
|
123
|
-
|
|
124
|
-
|
|
40
|
+
get(id: string): Standing | undefined;
|
|
41
|
+
ids(): string[];
|
|
42
|
+
remove(id: string): boolean;
|
|
43
|
+
}
|
|
125
44
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// written on the occupant for her gate to read, and hers alone after that.
|
|
45
|
+
// The door's side: the ids she invited.
|
|
46
|
+
export interface Occupants {
|
|
129
47
|
invite(id: string, notes?: JsonObject): Promise<Invitation | null>;
|
|
48
|
+
notes(id: string): JsonObject | undefined;
|
|
49
|
+
ids(): string[];
|
|
50
|
+
remove(id: string): boolean;
|
|
51
|
+
}
|
|
130
52
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
export type Stance = {
|
|
136
|
-
readonly cells: Cells;
|
|
53
|
+
// What every being is handed at birth.
|
|
54
|
+
export interface Stance {
|
|
55
|
+
readonly key: string;
|
|
56
|
+
readonly cells: JsonObject;
|
|
137
57
|
readonly occupants: Occupants;
|
|
138
58
|
readonly standings: Standings;
|
|
139
|
-
// A
|
|
140
|
-
//
|
|
141
|
-
// name, the id already names a record or is a reserved word, or the
|
|
142
|
-
// relation was refused.
|
|
143
|
-
//
|
|
144
|
-
// Her ward asks the ground, then knocks and takes in her name, as it does
|
|
145
|
-
// for a being she boots. So the invitation never reaches her, because it is
|
|
146
|
-
// the device's capability and not her own relation to give away; what she
|
|
147
|
-
// holds afterwards is an ordinary standing that counts, rotates, can be
|
|
148
|
-
// removed and hears `removed`.
|
|
149
|
-
//
|
|
150
|
-
// The ground is the booting harbor's, so this reaches the box she is
|
|
151
|
-
// running on and no other. A standing she wakes up holding names a being on
|
|
152
|
-
// the box she was on when it was made, and a migration is a restart she
|
|
153
|
-
// cannot tell from any other, so a being who wants the box she is on asks
|
|
154
|
-
// again at every birth.
|
|
155
|
-
lend(name: string, id: string): Promise<string | null>;
|
|
156
|
-
// A new being of her ward, by class name, under a key she chooses. The key
|
|
157
|
-
// back, or null: the key is taken, the harbor holds no such class, or the
|
|
158
|
-
// class threw at birth. A being may make; only the owner reaches into
|
|
159
|
-
// another. Naming an id asks for a way back too: the being made holds an
|
|
160
|
-
// occupant for her maker under the maker's own key, and the maker holds
|
|
161
|
-
// the standing under that id. A relation that could not be made is a boot
|
|
162
|
-
// that made nobody, and the being made goes with it.
|
|
59
|
+
// A new being of her ward, by class name, under a key she chooses. With an
|
|
60
|
+
// id, the new being invites her under her key, and she takes it under id.
|
|
163
61
|
boot(className: string, key: string, id?: string): Promise<string | null>;
|
|
164
|
-
|
|
62
|
+
// A standing under `id` at the faculty this harbor opened for a contract:
|
|
63
|
+
// the id, or null where none is open or the relation was refused.
|
|
64
|
+
lend(contract: { readonly name: string }, id: string): Promise<string | null>;
|
|
65
|
+
}
|
|
165
66
|
|
|
166
|
-
//
|
|
67
|
+
// Anything with `answer` is a being.
|
|
167
68
|
export interface BeingLike {
|
|
168
|
-
answer(asker: Asker, method
|
|
69
|
+
answer(asker: Asker, method: string | undefined, args: JsonObject): Reply | Promise<Reply>;
|
|
70
|
+
describe?(asker: Asker): Blueprint;
|
|
169
71
|
}
|
|
170
|
-
export type BeingClass = new (stance: Stance) => BeingLike;
|
|
72
|
+
export type BeingClass = (new (stance: Stance) => BeingLike) & { readonly name: string };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// What a being is told when no object came back. Silence is one symbol.
|
|
3
|
+
// A word is one frozen object per name, so a being never makes one by
|
|
4
|
+
// accident, and two copies of the package in one bundle still agree.
|
|
5
|
+
//
|
|
6
|
+
// removed, unannounced, repeated a far door's words, as Quo says them
|
|
7
|
+
// unreached nothing came back: asking again is safe
|
|
8
|
+
// late the allowance ran out
|
|
9
|
+
// dropped the standing was removed while asking
|
|
10
|
+
// invitation what was handed is no invitation
|
|
11
|
+
import type { Word as DoorWord } from '../quo/index.ts';
|
|
12
|
+
|
|
13
|
+
export const silence: unique symbol = Symbol.for('nervur.silence');
|
|
14
|
+
export type Silence = typeof silence;
|
|
15
|
+
export const isSilence = (x: unknown): x is Silence => x === silence;
|
|
16
|
+
|
|
17
|
+
export type WordName = DoorWord | 'unreached' | 'late' | 'dropped' | 'invitation';
|
|
18
|
+
const KEY: unique symbol = Symbol.for('nervur.word');
|
|
19
|
+
export type Word<W extends WordName = WordName> = { readonly [KEY]: W };
|
|
20
|
+
|
|
21
|
+
const made = new Map<WordName, Word>();
|
|
22
|
+
export const word = <W extends WordName>(name: W): Word<W> => {
|
|
23
|
+
let w = made.get(name);
|
|
24
|
+
if (!w) made.set(name, (w = Object.freeze({ [KEY]: name })));
|
|
25
|
+
return w as Word<W>;
|
|
26
|
+
};
|
|
27
|
+
export const isWord = (x: unknown): x is Word => typeof x === 'object' && x !== null && typeof (x as Record<symbol, unknown>)[KEY] === 'string';
|
|
28
|
+
|
|
29
|
+
// A word by its name, and anything else as it is, so one comparison says
|
|
30
|
+
// which answer came back.
|
|
31
|
+
export const told = (x: unknown): unknown => (isWord(x) ? x[KEY] : x);
|