nervur 0.16.0 → 0.17.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.
Files changed (111) hide show
  1. package/GETTING_STARTED.md +138 -0
  2. package/NOTICE +1 -1
  3. package/README.md +108 -20
  4. package/dist/being/being.d.ts +24 -0
  5. package/dist/being/being.js +109 -0
  6. package/dist/being/digest.d.ts +3 -0
  7. package/dist/being/digest.js +37 -0
  8. package/dist/being/index.d.ts +5 -0
  9. package/dist/being/index.js +6 -0
  10. package/dist/being/silence.d.ts +12 -0
  11. package/dist/being/silence.js +41 -0
  12. package/dist/being/types.d.ts +89 -0
  13. package/dist/being/types.js +58 -0
  14. package/dist/conformance/assert.d.ts +11 -0
  15. package/dist/conformance/assert.js +106 -0
  16. package/dist/conformance/beings.d.ts +198 -0
  17. package/dist/conformance/beings.js +183 -0
  18. package/dist/conformance/estate.d.ts +5 -0
  19. package/dist/conformance/estate.js +388 -0
  20. package/dist/conformance/index.d.ts +81 -0
  21. package/dist/conformance/index.js +816 -0
  22. package/dist/conformance/reach.d.ts +10 -0
  23. package/dist/conformance/reach.js +72 -0
  24. package/dist/conformance/store.d.ts +5 -0
  25. package/dist/conformance/store.js +123 -0
  26. package/dist/harbor/core.d.ts +53 -0
  27. package/dist/harbor/core.js +621 -0
  28. package/dist/harbor/dial.d.ts +9 -0
  29. package/dist/harbor/dial.js +81 -0
  30. package/dist/harbor/index.d.ts +8 -0
  31. package/dist/harbor/index.js +16 -0
  32. package/dist/harbor/memory.d.ts +25 -0
  33. package/dist/harbor/memory.js +72 -0
  34. package/dist/harbor/reach.d.ts +36 -0
  35. package/dist/harbor/reach.js +191 -0
  36. package/dist/harbor/store.d.ts +40 -0
  37. package/dist/harbor/store.js +70 -0
  38. package/dist/vector/cases.d.ts +41 -0
  39. package/dist/vector/cases.js +195 -0
  40. package/dist/vector/index.d.ts +6 -0
  41. package/dist/vector/index.js +8 -0
  42. package/dist/vector/stand.d.ts +9 -0
  43. package/dist/vector/stand.js +76 -0
  44. package/dist/vector/world.d.ts +143 -0
  45. package/dist/vector/world.js +198 -0
  46. package/dist/ward/allowance.d.ts +10 -0
  47. package/dist/ward/allowance.js +71 -0
  48. package/dist/ward/arithmetic.d.ts +31 -0
  49. package/dist/ward/arithmetic.js +245 -0
  50. package/dist/ward/cells.d.ts +7 -0
  51. package/dist/ward/cells.js +186 -0
  52. package/dist/ward/door.d.ts +18 -0
  53. package/dist/ward/door.js +186 -0
  54. package/dist/ward/ground.d.ts +23 -0
  55. package/dist/ward/ground.js +38 -0
  56. package/dist/ward/heirs.d.ts +13 -0
  57. package/dist/ward/heirs.js +115 -0
  58. package/dist/ward/index.d.ts +9 -0
  59. package/dist/ward/index.js +13 -0
  60. package/dist/ward/owner.d.ts +13 -0
  61. package/dist/ward/owner.js +220 -0
  62. package/dist/ward/partition.d.ts +65 -0
  63. package/dist/ward/partition.js +295 -0
  64. package/dist/ward/seal.d.ts +52 -0
  65. package/dist/ward/seal.js +145 -0
  66. package/dist/ward/stance.d.ts +25 -0
  67. package/dist/ward/stance.js +413 -0
  68. package/dist/ward/ward.d.ts +12 -0
  69. package/dist/ward/ward.js +361 -0
  70. package/package.json +37 -15
  71. package/protocol/SPEC.md +1843 -0
  72. package/protocol/vectors/arithmetic.json +117 -0
  73. package/protocol/vectors/door.json +345 -0
  74. package/protocol/vectors/framing.json +92 -0
  75. package/protocol/vectors/wire.json +48 -0
  76. package/quo-kit.md +652 -0
  77. package/src/being/being.ts +123 -0
  78. package/src/being/digest.ts +46 -0
  79. package/src/being/index.ts +7 -0
  80. package/src/being/silence.ts +46 -0
  81. package/src/being/types.ts +170 -0
  82. package/src/conformance/assert.ts +100 -0
  83. package/src/conformance/beings.ts +183 -0
  84. package/src/conformance/estate.ts +412 -0
  85. package/src/conformance/index.ts +962 -0
  86. package/src/conformance/reach.ts +83 -0
  87. package/src/conformance/store.ts +136 -0
  88. package/src/harbor/core.ts +660 -0
  89. package/src/harbor/dial.ts +112 -0
  90. package/src/harbor/index.ts +17 -0
  91. package/src/harbor/memory.ts +83 -0
  92. package/src/harbor/reach.ts +215 -0
  93. package/src/harbor/store.ts +101 -0
  94. package/src/vector/cases.ts +229 -0
  95. package/src/vector/index.ts +11 -0
  96. package/src/vector/stand.ts +75 -0
  97. package/src/vector/world.ts +220 -0
  98. package/src/ward/allowance.ts +85 -0
  99. package/src/ward/arithmetic.ts +247 -0
  100. package/src/ward/cells.ts +190 -0
  101. package/src/ward/door.ts +186 -0
  102. package/src/ward/ground.ts +160 -0
  103. package/src/ward/heirs.ts +114 -0
  104. package/src/ward/index.ts +17 -0
  105. package/src/ward/owner.ts +214 -0
  106. package/src/ward/partition.ts +353 -0
  107. package/src/ward/seal.ts +174 -0
  108. package/src/ward/stance.ts +433 -0
  109. package/src/ward/ward.ts +378 -0
  110. package/index.js +0 -17
  111. package/nervur.js +0 -5
@@ -0,0 +1,138 @@
1
+ # Getting started
2
+
3
+ Quo lets an object ask another object and get an answer, without knowing
4
+ whether that other object is in the same process, on the same device, or
5
+ on another planet. Three words: a harbor boots wards, a ward keeps beings
6
+ and judges its door, and a being is one ordinary object with one voice.
7
+ This is the shortest road from nothing to each of the three, for a
8
+ stranger with a terminal. `SPEC.md` is the truth behind every sentence
9
+ here and assumes nothing; `quo-dock.md` is the dock, the part a box runs.
10
+
11
+ Quo is the protocol and Nervur is this kit of it. Every name you install
12
+ and every command you type is Nervur's; `quo` stays on the wire, as the
13
+ `quo.` route a ward's door answers at.
14
+
15
+ Two packages, and you start with the one that fits what you have:
16
+
17
+ - `nervur`, the library. A harbor, a ward and a being in one
18
+ process, no wire, no files. For a program that wants Quo inside it.
19
+ - `@nervur-org/dock`, the dock. A daemon and one command, `nervur`, that
20
+ stand a box up: a person's world with a page, a model's side, an api, a
21
+ door on the wire. For a box that receives people and models.
22
+
23
+ The command is in the dock and not in the library, so `npm install nervur`
24
+ gives you something to import and no command, and the command arrives with
25
+ `npm install -g @nervur-org/dock`. That is how a kit of this shape is
26
+ always named: one unscoped headline name for the library, the parts under
27
+ the scope, and the CLI in the package that owns it.
28
+
29
+ ## A being, in one process
30
+
31
+ ```bash
32
+ npm install nervur
33
+ ```
34
+
35
+ A being is a class with `asks`, the methods anyone may reach, each with
36
+ the JSON schema of its input. Everything else on the class is hers alone.
37
+
38
+ ```js
39
+ import { Being } from 'nervur';
40
+ import { Ward } from 'nervur/ward';
41
+ import { MemoryHarbor } from 'nervur/harbor';
42
+
43
+ class Shop extends Being {
44
+ static asks = { price: { input: { type: 'object', properties: { item: { type: 'string' } } } } };
45
+ price({ item }) {
46
+ return { item, eur: 12 };
47
+ }
48
+ }
49
+ class Customer extends Being {
50
+ static asks = {};
51
+ }
52
+
53
+ const harbor = new MemoryHarbor();
54
+ const ward = await harbor.boot('acme', Ward, { Shop, Customer });
55
+ await ward.ask('boot', { key: 'shop', class: 'Shop' });
56
+ await ward.ask('boot', { key: 'ana', class: 'Customer' });
57
+ ```
58
+
59
+ The harbor booted a ward named `acme` with two classes it may make beings
60
+ of, and the ward's owner, the process itself, booted one of each by key.
61
+ The ward's own asks are seven, `boot`, `public`, `invite`, `knock`,
62
+ `remove`, `unboot` and `ask`, and `ward.ask()` with no method is her
63
+ describe: those asks and her `notes`, the ward's `pk` and her beings.
64
+
65
+ Nobody reaches a being she has not invited. An invitation is minted on a
66
+ being for one id, the shop's for `ana`, and the customer knocks with it;
67
+ from then she holds a standing at the shop, under the name she took, and
68
+ the shop holds her as an occupant.
69
+
70
+ ```js
71
+ const invitation = await ward.ask('invite', { being: 'shop', id: 'ana' });
72
+ const ana = harbor.objects.get(harbor.partitions.get('acme').beings.ana);
73
+ await ana.knock(invitation);
74
+ await ana.take('shop', invitation);
75
+ console.log(await ana.standings.shop.ask('price', { item: 'bread' }));
76
+ // { item: 'bread', eur: 12 }
77
+ ```
78
+
79
+ That is the whole protocol: a standing on one side, an occupant on the
80
+ other, an ask that rides the relation and an answer that rides it back.
81
+ The two beings here share a process; the same lines hold when the shop is
82
+ on a box across the sea, because a standing is an address and a key, and
83
+ the harbor owns the wire. `harbor.objects` is the memory harbor's hand for
84
+ a test and a first program; a being on a real box is reached through her
85
+ ward, never held.
86
+
87
+ ## A box
88
+
89
+ ```bash
90
+ npm install @nervur-org/dock
91
+ npx nervur init --dir ~/.nervur --ward acme --user ana --domain acme.com --default --show
92
+ npx nervur serve --dir ~/.nervur --http 8787
93
+ ```
94
+
95
+ `init` mints the ward `acme`, boots ana's user being, her doorbell and
96
+ the desk in it, marks it the default ward and shows it at the web route,
97
+ and writes `routes.json`, the four routes of a box, `mcp.`, `web.`, `quo.`
98
+ and `api.` under the domain, for a proxy to map onto the one loopback
99
+ port. Without `--domain`, write it yourself; on a Mac the four are paths
100
+ at one loopback address:
101
+
102
+ ```json
103
+ { "mcp": "http://127.0.0.1:8787/mcp", "web": "http://127.0.0.1:8787/web", "quo": "http://127.0.0.1:8787/quo", "api": "http://127.0.0.1:8787/api" }
104
+ ```
105
+
106
+ `serve` is the daemon, the one process over that folder, and every other
107
+ command is its client. From a second terminal, a phone:
108
+
109
+ ```bash
110
+ npx nervur invite --dir ~/.nervur '{"being":"ana","id":"phone"}'
111
+ ```
112
+
113
+ The answer carries `link`, the ward's page with the invitation in its
114
+ fragment. Opened on the phone, the tab boots a harbor of its own, joins as
115
+ that device, and is in: no account, nothing typed. What a model gets is
116
+ the same world as tools at `mcp.`, what a program gets is the same asks as
117
+ JSON at `api.`, and what another box gets is the door at `quo.`; a being
118
+ answers each the same, because each is an ask at her door.
119
+
120
+ The box's own doings are rows on the faculties of its dock ward, placed by
121
+ the root with `nervur ask --ward dock`: a socket held to another box, an
122
+ agent run for a ward, a schedule on the clock. A second box owns this one
123
+ across the wire with an invitation on the ward's own pk, knocked with
124
+ from there, and every owner command with `--via` from then on. Each of
125
+ those, the edge, and what an estate stands beyond the init, is the
126
+ "Getting started" chapter of `quo-dock.md`, proven cold on a Mac by
127
+ somebody with nothing else to read.
128
+
129
+ ## Your own beings on a box
130
+
131
+ A box holds the dock's classes and yours. `classes/index.ts` beside the
132
+ wards exports each of yours by name, and `nervur boot '{"key":"shop",
133
+ "class":"Shop"}'` boots one; `nervur init --class Shop` makes the ward's home
134
+ being one of yours, an organisation's ward with the org as its being. A
135
+ being's asks may name who may reach them, `for`, over the record of the
136
+ occupant asking, and her `cells` are what she keeps between boots: the
137
+ whole of what a being is, in `SPEC.md`, and the words above the spec, a
138
+ world, a home, a membership, in `WORLDS.md` and `GLOSSARY.md`.
package/NOTICE CHANGED
@@ -1,4 +1,4 @@
1
- Quo
1
+ Nervur
2
2
  Copyright 2026 Razvan Gherghina
3
3
 
4
4
  This product includes software developed by Razvan Gherghina.
package/README.md CHANGED
@@ -1,32 +1,120 @@
1
- # nervur
1
+ # Nervur
2
+
3
+ Nervur is the first open source kit of Quo, the library itself: harbor, ward
4
+ and being, with the spec and the vectors inside, and no dependencies.
2
5
 
3
6
  Quo is a protocol that lets an object ask another object and get an answer,
4
- without knowing whether that other object is in the same process, on the
5
- same device, or on another planet. Nervur's kit is its first implementation,
6
- open source, three packages under the `@nervur-org` scope.
7
+ without knowing whether that other object is in the same process, on the same
8
+ device, or on another planet. Quo is the protocol and this is one
9
+ implementation of it; the two names never stand for the same thing.
10
+
11
+ It is three words, two of which are beings, and two edges.
12
+ Nothing else is Quo.
13
+
14
+ - **Harbor.** The program a device runs to boot wards. Owns the wire and the
15
+ operating system. Not a being.
16
+ - **Ward.** One process of its harbor. A being plus ward functions. Keeps
17
+ beings and judges its door.
18
+ - **Being.** One ordinary object, one voice.
19
+
20
+ [`protocol/SPEC.md`](protocol/SPEC.md) is the truth. It is the protocol
21
+ alone: what any ward in any language must do for its bytes to be Quo. It is
22
+ self-contained and assumes nothing from any other document, this README
23
+ included. Read it first.
24
+ [`quo-kit.md`](quo-kit.md) is this kit: what one TypeScript
25
+ implementation chose and another kit may refuse. It assumes the spec, and
26
+ where the two disagree the spec wins.
27
+
28
+ ## This package
29
+
30
+ A TypeScript implementation, written against Node's own type stripping, with
31
+ no dependencies. The package ships JavaScript with declarations beside the
32
+ source it was emitted from, because Node strips types nowhere under
33
+ `node_modules`.
34
+
35
+ ```
36
+ protocol/ the shelf a kit in any language reads: SPEC.md and the vectors, no code
37
+ protocol/vectors/ fixed inputs and outputs, so another language proves its bytes
38
+ src/being/ the Being side: what a being author imports, if anything
39
+ src/ward/ the ward: the Ground contract, door, seal, arithmetic, heirs, stance, allowance
40
+ src/harbor/ MemoryHarbor, and the harbor core with its store, reach and dialer
41
+ src/conformance/ behaviours any ward must show, written against the truth
42
+ ```
7
43
 
8
- This package is the unscoped twin of that scope. `import` from it and you
9
- have the library; run it and you have the dock's command:
44
+ The suites live beside the source in the tree this is developed in and are
45
+ not in the package: what proves the kit is not what a consumer installs.
46
+ What does ship of them is `src/conformance/`, which is the part written for
47
+ somebody else's ward rather than for this one.
10
48
 
11
- ```bash
12
- npm install nervur
13
- npx nervur init
49
+ ## Requirements
50
+
51
+ Node 22.18 or later, and nothing else. The package has no dependencies, and
52
+ it ships JavaScript with declarations, so nothing is compiled on the way in.
53
+
54
+ ## Where it is proven
55
+
56
+ The same conformance suite this package exports runs against several worlds:
57
+ one ward, one harbor with a ward per being, two harbors, and the harbor core
58
+ over a memory store. A world declares what it can express and a chapter it
59
+ cannot reach is skipped by name rather than failed, which is how a kit in
60
+ another language reports the same suite honestly.
61
+
62
+ It is run again out of a bundle in a real Chromium, in workerd, in Deno and
63
+ in Bun, because a ward's truth must hold wherever a ward runs.
64
+
65
+ ## Entry points
66
+
67
+ ```js
68
+ import { Being, silence, digest } from 'nervur';
69
+ import { Ward } from 'nervur/ward';
70
+ import { MemoryHarbor, Harbor, request, dial } from 'nervur/harbor';
71
+ import { conform } from 'nervur/conformance';
72
+ import { Stand } from 'nervur/vector';
14
73
  ```
15
74
 
16
- | Package | What it is |
17
- | --- | --- |
18
- | [`@nervur-org/nervur`](https://www.npmjs.com/package/@nervur-org/nervur) | the library: harbor, ward and being, the protocol itself, with the spec inside |
19
- | [`@nervur-org/dock`](https://www.npmjs.com/package/@nervur-org/dock) | what every estate on Quo needs and nobody writes twice: the daemon, the command, the model sides and the screen |
20
- | [`@nervur-org/ui`](https://www.npmjs.com/package/@nervur-org/ui) | the kit: one token contract, one baseline, and the primitives a screen and a website both need. Knows nothing of Quo |
75
+ ## Another language
76
+
77
+ The hand to a kit in another language is one folder, `protocol/`, and it
78
+ holds two things of two kinds. `SPEC.md` is the protocol, and it assumes
79
+ nothing: a kit is written against it and against nothing else here. The
80
+ vectors beside it are the byte-level hand, everything a stranger can
81
+ observe: `protocol/vectors/arithmetic.json`, the primitives the seal rests
82
+ on, SHA-256, Ed25519, X25519, HKDF and AES-256-GCM;
83
+ `protocol/vectors/framing.json`, Quo's own, the ward pk, the digest, the
84
+ signed ask body, the sealed shapes, the invitation and the knock;
85
+ `protocol/vectors/wire.json`, the frames on a socket and the one request a
86
+ door takes; and `protocol/vectors/door.json`, the door's thirteen cases,
87
+ each one an arrival a ward will not answer, with the bytes that arrive, the
88
+ bytes that leave and the partition's digest on both sides of the judgement.
89
+ They import by name,
90
+ `nervur/protocol/vectors/framing.json`, so a kit's own suite can
91
+ read them from the package. A kit reproduces them or it is not this
92
+ protocol. `door.json` is also replayed from outside, by a verifier holding
93
+ no key, against a kit standing in vector mode, which `SPEC.md` describes
94
+ under that name. This kit stands in it with `Stand` from `nervur/vector`,
95
+ which is vector mode and names no runtime: put it behind any listener that
96
+ hands it a request body and returns what it answers.
97
+
98
+ Nothing outside that folder is the protocol. `src/` is this kit's
99
+ interpretation, and `src/conformance/` is a checklist a kit ports rather
100
+ than a harness it runs: it imports the base class, the silence spelling and
101
+ this kit's store and reach, so the beings it is shown with run only in a
102
+ TypeScript ward. `quo-kit.md` "The three shelves" says which is which.
103
+
104
+ ## Versions
21
105
 
22
- It pins the library and the dock exactly, so that the name stands for one
23
- kit. Its own number counts up on its own, because a name and what it
24
- stands for are two things. An estate that wants the pieces apart installs
25
- them apart.
106
+ No compatibility promise before 1.0.0: the words may still move, and there
107
+ is no migration to write because there is nothing yet to migrate from. This
108
+ package carries the version of its own work and is bound to no other's, so a
109
+ number equal to `@nervur-org/dock`'s or `@nervur-org/ui`'s is a coincidence.
26
110
 
27
- <https://nervur.org> is the kit. <https://quo.systems> is the protocol.
111
+ What ships is the emitted `dist/`, the source it came from, the protocol
112
+ shelf with the spec and the vectors inside it, this file, the licence and
113
+ the notice. No tests and no configs. The spec ships because it is the truth
114
+ the source and the vectors are read against, and a package a stranger reads
115
+ with no truth beside it is a pile of names.
28
116
 
29
117
  ## License
30
118
 
31
119
  Apache-2.0. Copyright 2026 Razvan Gherghina. See [LICENSE](LICENSE) and
32
- [NOTICE](NOTICE).
120
+ [NOTICE](NOTICE); every source file carries an SPDX line.
@@ -0,0 +1,24 @@
1
+ import type { Asker, Blueprint, Cells, Invitation, JsonObject, Wanted, OccupantRecord, Occupants, Reply, Schema, Stance, Standings, Answer } from './types.ts';
2
+ export type AskSpec = {
3
+ description?: string;
4
+ input?: Schema;
5
+ output?: Schema;
6
+ for?: (occupant: OccupantRecord | undefined, asker: Asker) => boolean;
7
+ };
8
+ export declare class Being {
9
+ static cells: JsonObject;
10
+ static asks: Record<string, AskSpec>;
11
+ readonly stance: Stance;
12
+ constructor(stance: Stance);
13
+ get cells(): Cells;
14
+ get standings(): Standings;
15
+ get occupants(): Occupants;
16
+ lend(name: string, id: string): Promise<string | null>;
17
+ invite(id: string, notes?: JsonObject): Promise<Invitation | null>;
18
+ knock(invitation: Invitation, method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
19
+ take(id: string, invitation: Invitation): Promise<string | null>;
20
+ boot(className: string, key: string, id?: string): Promise<string | null>;
21
+ occupant(asker: Asker): OccupantRecord | undefined;
22
+ describe(asker: Asker): Blueprint;
23
+ answer(asker: Asker, method?: string, args?: JsonObject): Promise<Reply>;
24
+ }
@@ -0,0 +1,109 @@
1
+ // Names a subclass may not use for an ask, because they are the base's own.
2
+ const RESERVED = new Set(['answer', 'describe', 'stance', 'cells', 'standings', 'occupants', 'occupant', 'invite', 'knock', 'take', 'boot', 'lend', 'constructor']);
3
+ // Whether she has a method of that name, written on her own prototype chain
4
+ // below Object's. A name Object lends every object, `hasOwnProperty` or
5
+ // `toString`, is not a method she wrote; and a field she assigns in her own
6
+ // constructor is not there yet when the base checks, so an ask is a method
7
+ // on the prototype and nothing else.
8
+ const wrote = (self, name) => {
9
+ for (let p = Object.getPrototypeOf(self); p !== null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
10
+ if (Object.hasOwn(p, name))
11
+ return typeof p[name] === 'function';
12
+ }
13
+ return false;
14
+ };
15
+ // Both statics below are read off the class the object was made from, so a
16
+ // subclass declaring either replaces its parent's rather than adding to it.
17
+ // That is the rule: her blueprint is exactly what the class in front of you
18
+ // declares, in the order she chose, and merging down a chain would hand her
19
+ // asks she may mean to drop and an order she did not write. A subclass that
20
+ // means to extend says so, `static override asks = { ...Parent.asks, mine: {} }`.
21
+ export class Being {
22
+ // Her cells' defaults. Merged in at birth, only where a key is missing, so
23
+ // a restart keeps what she wrote.
24
+ static cells = {};
25
+ // What she can be asked. Declaration order is blueprint order, except a
26
+ // name that reads as an array index, which the language lists first.
27
+ static asks = {};
28
+ stance;
29
+ constructor(stance) {
30
+ this.stance = stance;
31
+ const C = this.constructor;
32
+ for (const name of Object.keys(C.asks)) {
33
+ if (RESERVED.has(name))
34
+ throw new Error(`ask '${name}' is a reserved name`);
35
+ if (!wrote(this, name))
36
+ throw new Error(`ask '${name}' has no method on the prototype`);
37
+ }
38
+ // Own keys only: a default named after a member of Object's prototype is
39
+ // still hers, and still missing until she writes it.
40
+ for (const [k, v] of Object.entries(C.cells))
41
+ if (!Object.hasOwn(stance.cells, k))
42
+ stance.cells[k] = structuredClone(v);
43
+ }
44
+ get cells() {
45
+ return this.stance.cells;
46
+ }
47
+ get standings() {
48
+ return this.stance.standings;
49
+ }
50
+ get occupants() {
51
+ return this.stance.occupants;
52
+ }
53
+ // A standing at one of the things this device can do, under an id of hers.
54
+ // The ward knocks and takes it for her; the invitation never reaches her.
55
+ lend(name, id) {
56
+ return this.stance.lend(name, id);
57
+ }
58
+ invite(id, notes) {
59
+ return this.stance.occupants.invite(id, notes);
60
+ }
61
+ knock(invitation, method, args, wanted) {
62
+ return this.stance.standings.knock(invitation, method, args, wanted);
63
+ }
64
+ take(id, invitation) {
65
+ return this.stance.standings.take(id, invitation);
66
+ }
67
+ // A new being of her ward, by class name, under a key she chooses. With an
68
+ // id, she holds a standing to the being she made, who knows her by her key.
69
+ boot(className, key, id) {
70
+ return this.stance.boot(className, key, id);
71
+ }
72
+ // The occupant record for whoever is at the door. Undefined at a public being.
73
+ occupant(asker) {
74
+ return asker.id !== undefined && Object.hasOwn(this.cells.occupants, asker.id) ? this.cells.occupants[asker.id] : undefined;
75
+ }
76
+ // Her blueprint for this asker. Override to shape it by hand.
77
+ describe(asker) {
78
+ const C = this.constructor;
79
+ const rec = this.occupant(asker);
80
+ const asks = [];
81
+ for (const [name, spec] of Object.entries(C.asks)) {
82
+ if (spec.for && !spec.for(rec, asker))
83
+ continue;
84
+ const ask = { name, input: spec.input ?? { type: 'object' } };
85
+ if (spec.description !== undefined)
86
+ ask.description = spec.description;
87
+ if (spec.output !== undefined)
88
+ ask.output = spec.output;
89
+ asks.push(ask);
90
+ }
91
+ return { asks, notes: {} };
92
+ }
93
+ // The one function. Override to wrap it; call super to keep the dispatch.
94
+ async answer(asker, method, args = {}) {
95
+ if (method === undefined)
96
+ return this.describe(asker);
97
+ const C = this.constructor;
98
+ // Declared, by her, on purpose. `asks` is an ordinary object, so a bare
99
+ // lookup would also find every name on Object's prototype: `valueOf`
100
+ // would answer with her stance, `toString` with a string, and neither is
101
+ // an ask she wrote. Only her own keys are asks, which is what describe
102
+ // shows. What she shows is what she can be asked.
103
+ const spec = Object.hasOwn(C.asks, method) ? C.asks[method] : undefined;
104
+ if (!spec || (spec.for && !spec.for(this.occupant(asker), asker)))
105
+ return { error: 'unknown ask' };
106
+ const fn = this[method];
107
+ return fn.call(this, args ?? {}, asker);
108
+ }
109
+ }
@@ -0,0 +1,3 @@
1
+ import type { Json } from './types.ts';
2
+ export declare const canonical: (v: Json) => string;
3
+ export declare const digest: (blueprint: Json) => Promise<string>;
@@ -0,0 +1,37 @@
1
+ // A value I-JSON has no room for: a key left empty, a function, a symbol.
2
+ // None of them cross an edge, so none of them may reach a digest. A key
3
+ // carrying one is dropped and a slot carrying one is null, which is what
4
+ // crossing does to them, so the digest names what arrived and not what she
5
+ // happened to be holding.
6
+ const absent = (v) => v === undefined || typeof v === 'function' || typeof v === 'symbol';
7
+ // JCS for I-JSON values: sorted keys, no whitespace, JSON escaping. Numbers
8
+ // are serialized as ES does, which is what RFC 8785 specifies. A hole in a
9
+ // list is null, as JSON writes it.
10
+ export const canonical = (v) => {
11
+ if (Array.isArray(v))
12
+ return `[${Array.from(v, (slot) => (absent(slot) ? 'null' : canonical(slot))).join(',')}]`;
13
+ if (v !== null && typeof v === 'object') {
14
+ return `{${Object.keys(v)
15
+ .filter((k) => !absent(v[k]))
16
+ .sort()
17
+ .map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`)
18
+ .join(',')}}`;
19
+ }
20
+ return JSON.stringify(v);
21
+ };
22
+ // Hex and the guard are spelled here and again in `src/ward/arithmetic.ts`,
23
+ // which is the price of the boundary: nothing under `src/being` imports
24
+ // anything above it, because this is the whole world a being's own code sees
25
+ // and a being reaching the ward is the thing the shape is against.
26
+ const hex = (bytes) => Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, '0')).join('');
27
+ // `crypto.subtle` is read at the call and never captured at load. A browser
28
+ // on a plain http:// origin has `crypto` without `subtle`, and a terrain may
29
+ // install one after this module is first imported; either way the failure is
30
+ // one sentence and not a TypeError from inside a digest nobody can read.
31
+ const subtle = () => {
32
+ const s = globalThis.crypto?.subtle;
33
+ if (!s)
34
+ throw new Error('this terrain has no crypto.subtle: a being needs a secure context to be described');
35
+ return s;
36
+ };
37
+ export const digest = async (blueprint) => hex(await subtle().digest('SHA-256', new TextEncoder().encode(canonical(blueprint))));
@@ -0,0 +1,5 @@
1
+ export { Being, type AskSpec } from './being.ts';
2
+ export { silence, isSilence, answered, unreached, isUnreached, word, isWord, wordOf, told, DOOR_WORDS, isDoorWord } from './silence.ts';
3
+ export { digest, canonical } from './digest.ts';
4
+ export { OWNER, PUBLIC, RESERVED_IDS, isBlueprint, invitationArgs, isInvitation } from './types.ts';
5
+ export type * from './types.ts';
@@ -0,0 +1,6 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // nervur — the Being side. What a being author imports, if anything.
3
+ export { Being } from './being.js';
4
+ export { silence, isSilence, answered, unreached, isUnreached, word, isWord, wordOf, told, DOOR_WORDS, isDoorWord } from './silence.js';
5
+ export { digest, canonical } from './digest.js';
6
+ export { OWNER, PUBLIC, RESERVED_IDS, isBlueprint, invitationArgs, isInvitation } from './types.js';
@@ -0,0 +1,12 @@
1
+ import { type DoorWord, type Unreached, type Word, type WordName } from './types.ts';
2
+ export declare const silence: unique symbol;
3
+ export declare const isSilence: (x: unknown) => x is typeof silence;
4
+ export declare const DOOR_WORDS: readonly DoorWord[];
5
+ export declare const isDoorWord: (s: unknown) => s is DoorWord;
6
+ export declare const word: <W extends WordName>(name: W) => Word<W>;
7
+ export declare const isWord: (x: unknown) => x is Word;
8
+ export declare const wordOf: (x: Word) => WordName;
9
+ export declare const answered: <T>(x: T) => x is Exclude<T, typeof silence | Word>;
10
+ export declare const told: (x: unknown) => unknown;
11
+ export declare const unreached: () => Unreached;
12
+ export declare const isUnreached: (x: unknown) => x is Unreached;
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Silence is one distinguished value. Null is an answer. Silence is no answer.
3
+ //
4
+ // The ward's words are the other distinguished values: what her ward tells
5
+ // her when no object came back and it knows why. Each is one frozen object
6
+ // under one symbol key, so a being cannot make one by accident, none of them
7
+ // crosses an edge as a value, and a being who compares against them needs
8
+ // no fourth shape. `unreached` is one of them.
9
+ import { WORD_KEY } from './types.js';
10
+ export const silence = Symbol.for('quo.silence');
11
+ export const isSilence = (x) => x === silence;
12
+ // The words a door says to a key it has bound, and no other. They ride on
13
+ // the wire as `{ quo: word }`, so a reply is checked against this list.
14
+ export const DOOR_WORDS = ['removed', 'absent', 'unannounced', 'repeated', 'threw'];
15
+ export const isDoorWord = (s) => typeof s === 'string' && DOOR_WORDS.includes(s);
16
+ const WORDS = new Map();
17
+ export const word = (name) => {
18
+ let w = WORDS.get(name);
19
+ if (!w)
20
+ WORDS.set(name, (w = Object.freeze({ [WORD_KEY]: name })));
21
+ return w;
22
+ };
23
+ export const isWord = (x) => x !== null && typeof x === 'object' && typeof x[WORD_KEY] === 'string';
24
+ export const wordOf = (x) => x[WORD_KEY];
25
+ // What came back is an answer and not one of the two things that are not
26
+ // answers. A being who only wants the object writes one test instead of
27
+ // three, and the three shapes stay three shapes: silence is no answer, a word
28
+ // is her ward telling her why there is none.
29
+ export const answered = (x) => !isSilence(x) && !isWord(x);
30
+ // What she was told, with a word given its name and everything else left
31
+ // exactly as it came. One value to compare against, so asking which word came
32
+ // back is one test and not two joined by an and: `told(out) === 'late'` says
33
+ // what `isWord(out) && wordOf(out) === 'late'` says, and says which answer
34
+ // arrived instead when it is wrong, where the pair collapses to false and
35
+ // names nothing. Silence stays the symbol it is, and an object stays itself.
36
+ export const told = (x) => (isWord(x) ? wordOf(x) : x);
37
+ // Unreached: no far door was reached. Nothing is known to have been
38
+ // delivered, so asking again is safe. A being cannot produce it: a ward that
39
+ // sees a word come out of a being reads it as her having thrown.
40
+ export const unreached = () => word('unreached');
41
+ export const isUnreached = (x) => isWord(x) && wordOf(x) === 'unreached';
@@ -0,0 +1,89 @@
1
+ import type { silence } from './silence.ts';
2
+ export type Json = null | boolean | number | string | Json[] | {
3
+ [key: string]: Json;
4
+ };
5
+ export type JsonObject = {
6
+ [key: string]: Json;
7
+ };
8
+ export type Asker = {
9
+ id: string;
10
+ } | {
11
+ id?: undefined;
12
+ };
13
+ export declare const OWNER = "OWNER";
14
+ export declare const PUBLIC = "PUBLIC";
15
+ export declare const RESERVED_IDS: readonly string[];
16
+ export type Invitation = {
17
+ ward: string;
18
+ heir?: string;
19
+ secret?: string;
20
+ };
21
+ export declare const invitationArgs: (inv: Invitation) => JsonObject;
22
+ export declare const isInvitation: (v: unknown) => v is Invitation;
23
+ export type Schema = JsonObject;
24
+ export type Ask = {
25
+ name: string;
26
+ description?: string;
27
+ input: Schema;
28
+ output?: Schema;
29
+ };
30
+ export type Blueprint = {
31
+ asks: Ask[];
32
+ notes: Json;
33
+ };
34
+ export declare const isBlueprint: (v: unknown) => v is Blueprint;
35
+ export type StandingRecord = {
36
+ id: string;
37
+ digest: string | null;
38
+ blueprint: Blueprint | null;
39
+ seen: string | null;
40
+ };
41
+ export type OccupantRecord = {
42
+ id: string;
43
+ notes: JsonObject;
44
+ };
45
+ export type Cells = {
46
+ standings: Record<string, StandingRecord>;
47
+ occupants: Record<string, OccupantRecord>;
48
+ [hers: string]: Json;
49
+ };
50
+ export type Silence = typeof silence;
51
+ export type DoorWord = 'removed' | 'absent' | 'unannounced' | 'repeated' | 'threw';
52
+ export type WardWord = 'unreached' | 'late' | 'invitation' | 'dropped';
53
+ export type WordName = DoorWord | WardWord;
54
+ export declare const WORD_KEY: unique symbol;
55
+ export type Word<W extends WordName = WordName> = {
56
+ readonly [K in typeof WORD_KEY]: W;
57
+ };
58
+ export type Unreached = Word<'unreached'>;
59
+ export type Answer = Json | Silence | Word;
60
+ export type Reply = Json | Silence;
61
+ export type Wanted = {
62
+ time?: number;
63
+ };
64
+ export type Standing = {
65
+ readonly id: string;
66
+ ask(method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
67
+ };
68
+ export type Standings = {
69
+ knock(invitation: Invitation, method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
70
+ take(id: string, invitation: Invitation): Promise<string | null>;
71
+ remove(id: string): void;
72
+ } & {
73
+ readonly [id: string]: Standing | undefined;
74
+ };
75
+ export type Occupants = {
76
+ invite(id: string, notes?: JsonObject): Promise<Invitation | null>;
77
+ remove(id: string): void;
78
+ };
79
+ export type Stance = {
80
+ readonly cells: Cells;
81
+ readonly occupants: Occupants;
82
+ readonly standings: Standings;
83
+ lend(name: string, id: string): Promise<string | null>;
84
+ boot(className: string, key: string, id?: string): Promise<string | null>;
85
+ };
86
+ export interface BeingLike {
87
+ answer(asker: Asker, method?: string, args?: JsonObject): Reply | Promise<Reply>;
88
+ }
89
+ export type BeingClass = new (stance: Stance) => BeingLike;