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.
- package/GETTING_STARTED.md +138 -0
- package/NOTICE +1 -1
- package/README.md +108 -20
- package/dist/being/being.d.ts +24 -0
- package/dist/being/being.js +109 -0
- package/dist/being/digest.d.ts +3 -0
- package/dist/being/digest.js +37 -0
- package/dist/being/index.d.ts +5 -0
- package/dist/being/index.js +6 -0
- package/dist/being/silence.d.ts +12 -0
- package/dist/being/silence.js +41 -0
- package/dist/being/types.d.ts +89 -0
- package/dist/being/types.js +58 -0
- package/dist/conformance/assert.d.ts +11 -0
- package/dist/conformance/assert.js +106 -0
- package/dist/conformance/beings.d.ts +198 -0
- package/dist/conformance/beings.js +183 -0
- package/dist/conformance/estate.d.ts +5 -0
- package/dist/conformance/estate.js +388 -0
- package/dist/conformance/index.d.ts +81 -0
- package/dist/conformance/index.js +816 -0
- package/dist/conformance/reach.d.ts +10 -0
- package/dist/conformance/reach.js +72 -0
- package/dist/conformance/store.d.ts +5 -0
- package/dist/conformance/store.js +123 -0
- package/dist/harbor/core.d.ts +53 -0
- package/dist/harbor/core.js +621 -0
- package/dist/harbor/dial.d.ts +9 -0
- package/dist/harbor/dial.js +81 -0
- package/dist/harbor/index.d.ts +8 -0
- package/dist/harbor/index.js +16 -0
- package/dist/harbor/memory.d.ts +25 -0
- package/dist/harbor/memory.js +72 -0
- package/dist/harbor/reach.d.ts +36 -0
- package/dist/harbor/reach.js +191 -0
- package/dist/harbor/store.d.ts +40 -0
- package/dist/harbor/store.js +70 -0
- package/dist/vector/cases.d.ts +41 -0
- package/dist/vector/cases.js +195 -0
- package/dist/vector/index.d.ts +6 -0
- package/dist/vector/index.js +8 -0
- package/dist/vector/stand.d.ts +9 -0
- package/dist/vector/stand.js +76 -0
- package/dist/vector/world.d.ts +143 -0
- package/dist/vector/world.js +198 -0
- package/dist/ward/allowance.d.ts +10 -0
- package/dist/ward/allowance.js +71 -0
- package/dist/ward/arithmetic.d.ts +31 -0
- package/dist/ward/arithmetic.js +245 -0
- package/dist/ward/cells.d.ts +7 -0
- package/dist/ward/cells.js +186 -0
- package/dist/ward/door.d.ts +18 -0
- package/dist/ward/door.js +186 -0
- package/dist/ward/ground.d.ts +23 -0
- package/dist/ward/ground.js +38 -0
- package/dist/ward/heirs.d.ts +13 -0
- package/dist/ward/heirs.js +115 -0
- package/dist/ward/index.d.ts +9 -0
- package/dist/ward/index.js +13 -0
- package/dist/ward/owner.d.ts +13 -0
- package/dist/ward/owner.js +220 -0
- package/dist/ward/partition.d.ts +65 -0
- package/dist/ward/partition.js +295 -0
- package/dist/ward/seal.d.ts +52 -0
- package/dist/ward/seal.js +145 -0
- package/dist/ward/stance.d.ts +25 -0
- package/dist/ward/stance.js +413 -0
- package/dist/ward/ward.d.ts +12 -0
- package/dist/ward/ward.js +361 -0
- package/package.json +37 -15
- package/protocol/SPEC.md +1843 -0
- package/protocol/vectors/arithmetic.json +117 -0
- package/protocol/vectors/door.json +345 -0
- package/protocol/vectors/framing.json +92 -0
- package/protocol/vectors/wire.json +48 -0
- package/quo-kit.md +652 -0
- package/src/being/being.ts +123 -0
- package/src/being/digest.ts +46 -0
- package/src/being/index.ts +7 -0
- package/src/being/silence.ts +46 -0
- package/src/being/types.ts +170 -0
- package/src/conformance/assert.ts +100 -0
- package/src/conformance/beings.ts +183 -0
- package/src/conformance/estate.ts +412 -0
- package/src/conformance/index.ts +962 -0
- package/src/conformance/reach.ts +83 -0
- package/src/conformance/store.ts +136 -0
- package/src/harbor/core.ts +660 -0
- package/src/harbor/dial.ts +112 -0
- package/src/harbor/index.ts +17 -0
- package/src/harbor/memory.ts +83 -0
- package/src/harbor/reach.ts +215 -0
- package/src/harbor/store.ts +101 -0
- package/src/vector/cases.ts +229 -0
- package/src/vector/index.ts +11 -0
- package/src/vector/stand.ts +75 -0
- package/src/vector/world.ts +220 -0
- package/src/ward/allowance.ts +85 -0
- package/src/ward/arithmetic.ts +247 -0
- package/src/ward/cells.ts +190 -0
- package/src/ward/door.ts +186 -0
- package/src/ward/ground.ts +160 -0
- package/src/ward/heirs.ts +114 -0
- package/src/ward/index.ts +17 -0
- package/src/ward/owner.ts +214 -0
- package/src/ward/partition.ts +353 -0
- package/src/ward/seal.ts +174 -0
- package/src/ward/stance.ts +433 -0
- package/src/ward/ward.ts +378 -0
- package/index.js +0 -17
- 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
package/README.md
CHANGED
|
@@ -1,32 +1,120 @@
|
|
|
1
|
-
#
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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,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;
|