nervur 0.19.2 → 0.20.1
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 +156 -104
- package/dist/being/being.d.ts +8 -15
- package/dist/being/being.js +20 -67
- package/dist/being/digest.d.ts +1 -1
- package/dist/being/digest.js +7 -27
- package/dist/being/faculty.d.ts +10 -0
- package/dist/being/faculty.js +74 -0
- package/dist/being/index.d.ts +5 -5
- package/dist/being/index.js +6 -5
- package/dist/being/kind.d.ts +4 -0
- package/dist/being/kind.js +28 -0
- package/dist/being/types.d.ts +35 -63
- package/dist/being/types.js +3 -57
- package/dist/being/words.d.ts +13 -0
- package/dist/being/words.js +14 -0
- package/dist/cli/command.d.ts +13 -0
- package/dist/cli/command.js +194 -0
- package/dist/cli/harbor.d.ts +21 -0
- package/dist/cli/harbor.js +141 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +14 -0
- package/dist/contract/index.d.ts +39 -0
- package/dist/contract/index.js +33 -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 +14 -0
- package/dist/folder/index.js +87 -0
- package/dist/harbor/catalogue.d.ts +25 -0
- package/dist/harbor/catalogue.js +93 -0
- package/dist/harbor/dock.d.ts +29 -0
- package/dist/harbor/dock.js +122 -0
- package/dist/harbor/harbor.d.ts +31 -0
- package/dist/harbor/harbor.js +146 -0
- package/dist/harbor/index.d.ts +7 -10
- package/dist/harbor/index.js +9 -18
- package/dist/harbor/package.d.ts +10 -0
- package/dist/harbor/package.js +115 -0
- package/dist/harbor/registry.d.ts +9 -0
- package/dist/harbor/registry.js +78 -0
- package/dist/harbor/root-line.d.ts +9 -0
- package/dist/harbor/root-line.js +36 -0
- package/dist/harbor/terrain.d.ts +9 -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 +26 -0
- package/dist/pointer/bodies.js +64 -0
- package/dist/pointer/index.d.ts +2 -0
- package/dist/pointer/index.js +4 -0
- package/dist/pointer/world.d.ts +35 -0
- package/dist/pointer/world.js +85 -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/tcp/frame.d.ts +29 -0
- package/dist/tcp/frame.js +74 -0
- package/dist/tcp/index.d.ts +18 -0
- package/dist/tcp/index.js +164 -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 +6 -10
- package/dist/ward/index.js +7 -13
- package/dist/ward/partition.d.ts +44 -60
- package/dist/ward/partition.js +56 -285
- package/dist/ward/pilot.d.ts +8 -0
- package/dist/ward/pilot.js +50 -0
- package/dist/ward/stance.d.ts +27 -20
- package/dist/ward/stance.js +174 -406
- package/dist/ward/ward-being.d.ts +35 -0
- package/dist/ward/ward-being.js +82 -0
- package/dist/ward/ward.d.ts +45 -11
- package/dist/ward/ward.js +239 -357
- package/package.json +24 -31
- package/src/being/being.ts +35 -78
- package/src/being/digest.ts +7 -35
- package/src/being/faculty.ts +73 -0
- package/src/being/index.ts +6 -6
- package/src/being/kind.ts +30 -0
- package/src/being/types.ts +50 -146
- package/src/being/words.ts +31 -0
- package/src/cli/command.ts +188 -0
- package/src/cli/harbor.ts +137 -0
- package/src/cli/main.ts +13 -0
- package/src/contract/index.ts +73 -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 +92 -0
- package/src/harbor/catalogue.ts +110 -0
- package/src/harbor/dock.ts +137 -0
- package/src/harbor/harbor.ts +180 -0
- package/src/harbor/index.ts +9 -19
- package/src/harbor/package.ts +131 -0
- package/src/harbor/registry.ts +73 -0
- package/src/harbor/root-line.ts +36 -0
- package/src/harbor/terrain.ts +15 -0
- package/src/index.ts +20 -0
- package/src/pointer/bodies.ts +65 -0
- package/src/pointer/index.ts +4 -0
- package/src/pointer/world.ts +94 -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 +20 -0
- package/src/stand/stand.ts +220 -0
- package/src/tcp/frame.ts +78 -0
- package/src/tcp/index.ts +173 -0
- package/src/ward/allowance.ts +37 -75
- package/src/ward/cells.ts +63 -176
- package/src/ward/index.ts +7 -17
- package/src/ward/partition.ts +86 -326
- package/src/ward/pilot.ts +50 -0
- package/src/ward/stance.ts +186 -420
- package/src/ward/ward-being.ts +103 -0
- package/src/ward/ward.ts +256 -359
- 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,47 @@
|
|
|
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.1",
|
|
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
|
-
"
|
|
19
|
-
"
|
|
15
|
+
"bin": {
|
|
16
|
+
"nervur": "dist/cli/main.js"
|
|
20
17
|
},
|
|
21
18
|
"exports": {
|
|
22
19
|
".": {
|
|
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"
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
37
22
|
},
|
|
38
|
-
"./
|
|
39
|
-
"types": "./dist/
|
|
40
|
-
"default": "./dist/
|
|
23
|
+
"./folder": {
|
|
24
|
+
"types": "./dist/folder/index.d.ts",
|
|
25
|
+
"default": "./dist/folder/index.js"
|
|
41
26
|
},
|
|
42
|
-
"./
|
|
27
|
+
"./tcp": {
|
|
28
|
+
"types": "./dist/tcp/index.d.ts",
|
|
29
|
+
"default": "./dist/tcp/index.js"
|
|
30
|
+
}
|
|
43
31
|
},
|
|
44
32
|
"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\"",
|
|
33
|
+
"build": "tsc -p tsconfig.build.json",
|
|
34
|
+
"check": "tsc -p tsconfig.json && npm run build && node ../../test.mjs \"test/unit/*.test.ts\" \"test/e2e/*.test.ts\"",
|
|
35
|
+
"deep": "node ../../test.mjs \"test/terrain/*.test.ts\"",
|
|
36
|
+
"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
37
|
"prepublishOnly": "test \"$NERVUR_GATED\" = 1 || { echo 'a publish is /release, from the root, on the human'\"'\"'s word' >&2; exit 1; }"
|
|
51
38
|
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=22.18"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@noble/post-quantum": "0.7.1"
|
|
44
|
+
},
|
|
52
45
|
"publishConfig": {
|
|
53
46
|
"access": "public"
|
|
54
47
|
},
|
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,17 @@ const wrote = (self: object, name: string): boolean => {
|
|
|
30
25
|
return false;
|
|
31
26
|
};
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
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. Her kind is
|
|
32
|
+
// never inherited: see `kind.ts`.
|
|
39
33
|
export class Being {
|
|
40
|
-
//
|
|
41
|
-
//
|
|
34
|
+
// The name her row keeps for this class. Every class that stands declares
|
|
35
|
+
// its own.
|
|
36
|
+
static readonly kind: string = 'org.nervur.being';
|
|
37
|
+
// Her cells' defaults, written at birth only where a key is missing.
|
|
42
38
|
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
39
|
static asks: Record<string, AskSpec> = {};
|
|
46
40
|
|
|
47
41
|
readonly stance: Stance;
|
|
@@ -51,73 +45,36 @@ export class Being {
|
|
|
51
45
|
const C = this.constructor as typeof Being;
|
|
52
46
|
for (const name of Object.keys(C.asks)) {
|
|
53
47
|
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
|
|
48
|
+
if (!wrote(this, name)) throw new Error(`ask '${name}' has no method`);
|
|
55
49
|
}
|
|
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
50
|
for (const [k, v] of Object.entries(C.cells)) if (!Object.hasOwn(stance.cells, k)) stance.cells[k] = structuredClone(v);
|
|
59
51
|
}
|
|
60
52
|
|
|
61
|
-
get cells():
|
|
53
|
+
get cells(): JsonObject {
|
|
62
54
|
return this.stance.cells;
|
|
63
55
|
}
|
|
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;
|
|
56
|
+
|
|
57
|
+
// The notes she invited this asker under.
|
|
58
|
+
notes(asker: Asker): JsonObject | undefined {
|
|
59
|
+
return asker.id === undefined ? undefined : this.stance.occupants.notes(asker.id);
|
|
92
60
|
}
|
|
93
61
|
|
|
94
|
-
// Her blueprint for this asker. Override to shape it by hand.
|
|
95
62
|
describe(asker: Asker): Blueprint {
|
|
96
63
|
const C = this.constructor as typeof Being;
|
|
97
|
-
const
|
|
64
|
+
const notes = this.notes(asker);
|
|
98
65
|
const asks: Ask[] = [];
|
|
99
66
|
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);
|
|
67
|
+
if (spec.for && !spec.for(asker, notes)) continue;
|
|
68
|
+
asks.push({ name, ...(spec.description === undefined ? {} : { description: spec.description }), input: spec.input ?? { type: 'object' } });
|
|
105
69
|
}
|
|
106
70
|
return { asks, notes: {} };
|
|
107
71
|
}
|
|
108
72
|
|
|
109
|
-
|
|
110
|
-
async answer(asker: Asker, method?: string, args: JsonObject = {}): Promise<Reply> {
|
|
73
|
+
async answer(asker: Asker, method: string | undefined, args: JsonObject): Promise<Reply> {
|
|
111
74
|
if (method === undefined) return this.describe(asker);
|
|
112
75
|
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
76
|
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);
|
|
77
|
+
if (!spec || (spec.for && !spec.for(asker, this.notes(asker)))) return { error: 'unknown ask' };
|
|
78
|
+
return ((this as unknown as Record<string, Method>)[method] as Method).call(this, args, asker);
|
|
122
79
|
}
|
|
123
80
|
}
|
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,73 @@
|
|
|
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
|
+
// static override kind = 'com.acme.timer' }
|
|
7
|
+
// class IntervalTimer extends Timer { one class that fulfils it
|
|
8
|
+
// static override kind = 'com.acme.interval-timer' }
|
|
9
|
+
//
|
|
10
|
+
// A being lends by the contract's kind and never learns the class. Every
|
|
11
|
+
// class between the faculty and `Faculty` is a contract and declares its
|
|
12
|
+
// own kind, and a faculty fulfils each of them and her own. Two asks are the
|
|
13
|
+
// faculty's own and answered to the ward-being that opened her alone:
|
|
14
|
+
// `offer`, an invitation on herself for a ward the dock vouches for, and
|
|
15
|
+
// `retract`, an offer that was not taken.
|
|
16
|
+
import { Being } from './being.ts';
|
|
17
|
+
import { kindOf } from './kind.ts';
|
|
18
|
+
import { WARD, type Asker, type Blueprint, type JsonObject, type Reply } from './types.ts';
|
|
19
|
+
|
|
20
|
+
export class Faculty extends Being {
|
|
21
|
+
static override readonly kind: string = 'org.nervur.faculty';
|
|
22
|
+
|
|
23
|
+
// The contracts a class fulfils: itself and every class between it and
|
|
24
|
+
// `Faculty`, by kind. A class among them with no kind of its own throws.
|
|
25
|
+
static contracts(C: abstract new (...args: never[]) => unknown): string[] {
|
|
26
|
+
const kinds: string[] = [];
|
|
27
|
+
for (let c: unknown = C; typeof c === 'function' && c !== Faculty; c = Object.getPrototypeOf(c)) kinds.push(kindOf(c));
|
|
28
|
+
return kinds;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static fulfils(C: unknown): C is typeof Faculty {
|
|
32
|
+
return typeof C === 'function' && C.prototype instanceof Faculty;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#open(): Record<string, string> {
|
|
36
|
+
if (this.cells.offers === undefined) this.cells.offers = {};
|
|
37
|
+
return this.cells.offers as Record<string, string>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async #offer(): Promise<JsonObject> {
|
|
41
|
+
const n = ((this.cells.offered as number | undefined) ?? 0) + 1;
|
|
42
|
+
this.cells.offered = n;
|
|
43
|
+
const id = `lent:${n}`;
|
|
44
|
+
const invitation = await this.stance.occupants.invite(id);
|
|
45
|
+
if (invitation === null) return { error: 'not invited' };
|
|
46
|
+
this.#open()[invitation.heir] = id;
|
|
47
|
+
return { invitation };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#retract(args: JsonObject): JsonObject {
|
|
51
|
+
const open = this.#open();
|
|
52
|
+
const id = typeof args.heir === 'string' && Object.hasOwn(open, args.heir) ? open[args.heir] : undefined;
|
|
53
|
+
if (id === undefined) return { retracted: null };
|
|
54
|
+
this.stance.occupants.remove(id);
|
|
55
|
+
delete open[args.heir as string];
|
|
56
|
+
return { retracted: id };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override describe(asker: Asker): Blueprint {
|
|
60
|
+
const blueprint = super.describe(asker);
|
|
61
|
+
if (asker.id !== WARD) return blueprint;
|
|
62
|
+
return { ...blueprint, asks: [...blueprint.asks, { name: 'offer', input: { type: 'object' } }, { name: 'retract', input: { type: 'object', required: ['heir'] } }] };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// An offer is closed by the first word from the occupant it was made for.
|
|
66
|
+
override answer(asker: Asker, method: string | undefined, args: JsonObject): Promise<Reply> {
|
|
67
|
+
if (asker.id === WARD && method === 'offer') return this.#offer();
|
|
68
|
+
if (asker.id === WARD && method === 'retract') return Promise.resolve(this.#retract(args));
|
|
69
|
+
const open = this.#open();
|
|
70
|
+
for (const [heir, id] of Object.entries(open)) if (id === asker.id) delete open[heir];
|
|
71
|
+
return super.answer(asker, method, args);
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/being/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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 { OWNER,
|
|
8
|
-
export type
|
|
4
|
+
export { canonical, digest } from './digest.ts';
|
|
5
|
+
export { Faculty } from './faculty.ts';
|
|
6
|
+
export { isKind, KIT, kindOf, ownKind } from './kind.ts';
|
|
7
|
+
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';
|
|
8
|
+
export { isSilence, isWord, silence, told, word, type Silence, type Word, type WordName } from './words.ts';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// A kind: the name a row keeps for the class a being is born of, and the
|
|
3
|
+
// name a contract is lent by. It is declared on the class, never taken from
|
|
4
|
+
// the name the language gives it, because a bundler renames classes.
|
|
5
|
+
//
|
|
6
|
+
// static kind = 'com.acme.shop'
|
|
7
|
+
//
|
|
8
|
+
// A kind is a reversed domain its author owns, then a name: at least three
|
|
9
|
+
// segments joined by `.`, each lowercase letters, digits and `-`, starting
|
|
10
|
+
// with a letter or digit. `org.nervur.` is the kit's own. A class declares
|
|
11
|
+
// its own kind: one it inherits names its parent, so it is none.
|
|
12
|
+
export const KIT = 'org.nervur.';
|
|
13
|
+
|
|
14
|
+
const KIND = /^[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*){2,}$/;
|
|
15
|
+
|
|
16
|
+
export const isKind = (text: unknown): text is string => typeof text === 'string' && KIND.test(text);
|
|
17
|
+
|
|
18
|
+
// The kind a class declares on itself, or undefined.
|
|
19
|
+
export const ownKind = (C: unknown): string | undefined => {
|
|
20
|
+
if (typeof C !== 'function' || !Object.hasOwn(C, 'kind')) return undefined;
|
|
21
|
+
const kind = (C as { kind?: unknown }).kind;
|
|
22
|
+
return isKind(kind) ? kind : undefined;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// The kind a class declares on itself, or a throw naming the class.
|
|
26
|
+
export const kindOf = (C: unknown): string => {
|
|
27
|
+
const kind = ownKind(C);
|
|
28
|
+
if (kind === undefined) throw new Error(`class ${typeof C === 'function' ? C.name : String(C)} declares no kind of its own`);
|
|
29
|
+
return kind;
|
|
30
|
+
};
|