nervur 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GETTING_STARTED.md +138 -0
- package/LICENSE +2 -1
- package/NOTICE +6 -0
- package/README.md +119 -151
- 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 +51 -11
- 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/src/being.js +0 -125
- package/src/contract.js +0 -161
- package/src/ground.js +0 -273
- package/src/index.js +0 -5
- package/src/program.js +0 -74
- package/src/projection.js +0 -102
|
@@ -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 razvan --domain acme.com --default --show
|
|
92
|
+
npx nervur serve --dir ~/.nervur --http 8787
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`init` mints the ward `acme`, boots razvan'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":"razvan","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/LICENSE
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
Apache License
|
|
2
3
|
Version 2.0, January 2004
|
|
3
4
|
http://www.apache.org/licenses/
|
|
@@ -178,7 +179,7 @@
|
|
|
178
179
|
APPENDIX: How to apply the Apache License to your work.
|
|
179
180
|
|
|
180
181
|
To apply the Apache License to your work, attach the following
|
|
181
|
-
boilerplate notice, with the fields enclosed by brackets "
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
183
|
replaced with your own identifying information. (Don't include
|
|
183
184
|
the brackets!) The text should be enclosed in the appropriate
|
|
184
185
|
comment syntax for the file format. We also recommend that a
|
package/NOTICE
ADDED
package/README.md
CHANGED
|
@@ -1,152 +1,120 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
Nervur
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
**`ground()`** — one closure over one warden, holding beings and judging what
|
|
36
|
-
arrives. `bench/ground.test.js` walks it end to end: a real sealed ask, a real
|
|
37
|
-
answer, and silence where a standing does not reach.
|
|
38
|
-
|
|
39
|
-
**The outbound relation** — `remember` records an invitation this ground was
|
|
40
|
-
handed and names which of its beings spends it; `ask` seals down that relation,
|
|
41
|
-
posts it at the hints the invitation carried, and reads the answer back. Every
|
|
42
|
-
one of those acts is the kit's. What is Nervur's is the ergonomics: a relation
|
|
43
|
-
is found by whose it is, so one being cannot spend another's; the number is the
|
|
44
|
-
ground's to pick, so no caller counts by hand; and the first ask carries the
|
|
45
|
-
rotation an invitation's heir keys oblige, so nobody meets that trap twice.
|
|
46
|
-
`bench/estate.test.js` stands two houses on two real loopback doors and crosses
|
|
47
|
-
between them.
|
|
48
|
-
|
|
49
|
-
Silence, unreachable and a relation that is not yours are three different
|
|
50
|
-
things and stay three: silence is `null`, a road that does not answer throws
|
|
51
|
-
out of the carriage, and spending what a being does not hold throws before a
|
|
52
|
-
byte moves.
|
|
53
|
-
|
|
54
|
-
**`contract(sdl)` and `program(contract, resolvers)`** — the compiler, and
|
|
55
|
-
there is exactly one of it. A being and an organ are both a contract plus
|
|
56
|
-
resolvers, and `program` cannot tell which it is holding; that is not a
|
|
57
|
-
convenience, it is the architecture. A field is read with `hasOwn` and never
|
|
58
|
-
through the prototype chain, exactly the declared arguments reach a resolver
|
|
59
|
-
and no others, a resolver short of a required argument is not called at all,
|
|
60
|
-
and a resolver that throws is answered as silence — nothing here promises that
|
|
61
|
-
an ask is fulfilled, so a refusal a caller must tell apart is carried in the
|
|
62
|
-
answer type instead. `bench/program.test.js` asserts every one of those, and
|
|
63
|
-
asserts the first on a being and an organ side by side.
|
|
64
|
-
|
|
65
|
-
**The contract is a GraphQL schema** — schema, resolvers and a context, the
|
|
66
|
-
shape every engineer already knows. It is authoring and nothing else: never at
|
|
67
|
-
the wire, never an answer, never a describe. The organs a being reaches outward
|
|
68
|
-
with are declared in the schema itself, with `schema @needs(organs: [...])`,
|
|
69
|
-
and the being's own name is the root type's — `schema { query: Clerk }`.
|
|
70
|
-
|
|
71
|
-
**`project(contract)`** — the boundary, and there must be exactly one of it.
|
|
72
|
-
Every implementer of Quo speaks the notation and has never heard of Nervur, so
|
|
73
|
-
a contract becomes a Quo blueprint in the kit's own notation before anything
|
|
74
|
-
holds it, and what a stranger verifies is that blueprint's digest. GraphQL is
|
|
75
|
-
nullable by default and the notation is required by default, so `String!`
|
|
76
|
-
becomes `text` and `String` becomes `text?`; the schema's other object and
|
|
77
|
-
input types become record blocks, and their ordering is the notation's own law
|
|
78
|
-
and therefore the kit's `print` to judge. Where GraphQL says something the
|
|
79
|
-
notation has no word for — `Float`, `ID`, an enum, a union, an interface, a
|
|
80
|
-
record field that takes arguments — the contract is refused at build rather
|
|
81
|
-
than guessed at. One word runs the other way: the notation has a field that
|
|
82
|
-
answers nothing and GraphQL insists every field has a type, so `Nothing` in
|
|
83
|
-
answer position is that field's spelling. `bench/projection.test.js` holds it.
|
|
84
|
-
|
|
85
|
-
**`being(sdl, resolvers)`** — a schema and ordinary functions under it. It is
|
|
86
|
-
compiled by `program` like anything else; what it adds is the wire skin the kit
|
|
87
|
-
leaves open, and the two promises the kit leaves to Nervur: a resolver meets
|
|
88
|
-
its arguments as named values and never bytes, and a being answers **exactly**
|
|
89
|
-
the fields its contract declares. That second one is the gate the digest's
|
|
90
|
-
meaning rests on — the law says what a blueprint does not declare does not
|
|
91
|
-
exist, and the kit dispatches on whatever methods the held object happens to
|
|
92
|
-
have.
|
|
93
|
-
|
|
94
|
-
**Organs** — the only way a being _acts_ outward. That is not a style choice:
|
|
95
|
-
a being has no warden and no caller, so it cannot mint standing and a field
|
|
96
|
-
answering another being's name hands over a label rather than reach. What it
|
|
97
|
-
can still do is pass on standing it was already given — the constitution says
|
|
98
|
-
data can carry an `invitation`, and an invitation carries the heir keys — so
|
|
99
|
-
"a being cannot hand over reach" is false as a general claim and only the
|
|
100
|
-
minting half is true. A being names the organ aliases it needs in its own
|
|
101
|
-
schema, the ground supplies them at `hold`, and the resolvers are closed over
|
|
102
|
-
exactly those. An organ the schema never asked for is not absent from a check —
|
|
103
|
-
it is absent from the object.
|
|
104
|
-
|
|
105
|
-
**The organ that crosses** — `errand` mints the reach a being uses to speak to
|
|
106
|
-
another house. It is shut over which being spends the relation and which house
|
|
107
|
-
it stands at, so a being holds one road on its own behalf and nothing it could
|
|
108
|
-
point elsewhere; it never holds the warden and never holds the relation. Which
|
|
109
|
-
being at the far house is still the caller's to name, because the standing
|
|
110
|
-
decides that and the standing is judged over there. The **leash** is handed in
|
|
111
|
-
rather than chosen — the resolver passes on the one the door gave it, whole and
|
|
112
|
-
never a number read off it, because what may go onward is this door's dwell
|
|
113
|
-
subtracted at the moment of sealing, which is later than any moment the being
|
|
114
|
-
could have read. So the remainder reaches the far house without the being
|
|
115
|
-
having chosen it, and a being cannot hand its organ more than it received.
|
|
116
|
-
`bench/errand.test.js` serves one sealed ask at the near house by crossing to
|
|
117
|
-
the far one and answering with what came back.
|
|
118
|
-
|
|
119
|
-
**Contacts** — who reaches this being, and the two acts that change it:
|
|
120
|
-
`list()`, `admit(keys)` and `remove(voicePk)`. Every one of them goes to the
|
|
121
|
-
warden's own inbound record — `grant`, `amend`, `standing` — read live at the
|
|
122
|
-
moment it is called and never snapshotted, so a being that admitted someone a
|
|
123
|
-
moment ago sees them. Quo keeps that record and refuses to have an opinion
|
|
124
|
-
about it, having no word for member, owner or guest; the opinion is Nervur's,
|
|
125
|
-
and this is the whole of it. The inverse — voices by being, where the warden
|
|
126
|
-
keeps beings by voice — is derived rather than indexed, because a second index
|
|
127
|
-
is a second truth.
|
|
128
|
-
|
|
129
|
-
The scoping is structural, the same shape `errand` uses: the context is minted
|
|
130
|
-
per `hold` with the being shut inside the closure, so there is no address it
|
|
131
|
-
could name to reach another being's row. It reaches a resolver as the resolver
|
|
132
|
-
factory's second argument, closed over at raise beside the organs, rather than
|
|
133
|
-
as a third argument to every resolver. `bench/contacts.test.js` proves the
|
|
134
|
-
admission and the removal at the door — a real sealed ask that gets in, and
|
|
135
|
-
then meets silence.
|
|
136
|
-
|
|
137
|
-
Organs are declared inside the contract, with `schema @needs(organs: [...])`.
|
|
138
|
-
That directive is authoring and never reaches the wire: the projection drops
|
|
139
|
-
it, so the blueprint a stranger reads carries no trace of what a being reaches
|
|
140
|
-
outward with, which is inside-the-ground business the wire never sees.
|
|
141
|
-
|
|
142
|
-
## What is not here yet
|
|
143
|
-
|
|
144
|
-
`own` and the floor, memory, standings and the graph walked across them, the
|
|
145
|
-
per-call caller, ground-as-a-folder, persistence, register-and-invoke,
|
|
146
|
-
collections-as-beings, per-caller beings.
|
|
147
|
-
|
|
148
|
-
## The bench
|
|
149
|
-
|
|
150
|
-
```bash
|
|
151
|
-
npm run gate-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.
|
|
5
|
+
|
|
6
|
+
Quo is a protocol that lets an object ask another object and get an answer,
|
|
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
|
+
|
|
152
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
|
+
```
|
|
43
|
+
|
|
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.
|
|
48
|
+
|
|
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';
|
|
73
|
+
```
|
|
74
|
+
|
|
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
|
|
105
|
+
|
|
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.
|
|
110
|
+
|
|
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.
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
Apache-2.0. Copyright 2026 Razvan Gherghina. See [LICENSE](LICENSE) and
|
|
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;
|