nervur 0.11.0 → 0.13.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 +111 -0
- package/README.md +57 -19
- package/SECURITY.md +11 -0
- package/admin.mjs +11 -1
- package/being.mjs +169 -54
- package/blueprint.mjs +137 -34
- package/client.mjs +9 -6
- package/contract.mjs +6 -4
- package/ground.mjs +144 -37
- package/index.mjs +13 -3
- package/modules.mjs +14 -70
- package/package.json +5 -2
- package/tools.mjs +69 -48
- package/voice.mjs +49 -63
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Getting started — two castles
|
|
2
|
+
|
|
3
|
+
Picture a castle: one gate, its own residents, its own law. A **ground** is
|
|
4
|
+
the castle — one sovereign process, one handler where every letter arrives.
|
|
5
|
+
Its residents are **beings**; its capabilities are **modules**; the ground
|
|
6
|
+
under it is a **floor** — a filesystem, or a browser's IndexedDB — and the
|
|
7
|
+
stance it takes toward the world is its **terrain**. This page raises two
|
|
8
|
+
castles on two terrains and has them speak: a **mountain** (a server, always
|
|
9
|
+
reachable, always remembering) and a **house of cards** (a browser tab,
|
|
10
|
+
raised in seconds, holding what it can afford to lose). Same castle, same
|
|
11
|
+
law, different ground under it.
|
|
12
|
+
|
|
13
|
+
Everything below runs today — the bench drives this exact story in
|
|
14
|
+
`bench/worlds/story/`.
|
|
15
|
+
|
|
16
|
+
## The mountain
|
|
17
|
+
|
|
18
|
+
The machine's raise is one config and one daemon —
|
|
19
|
+
[`nervurd`](https://www.npmjs.com/package/nervurd) is the host, and its
|
|
20
|
+
README is the operator's full page. The five-minute shape:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install -g nervurd
|
|
24
|
+
nervur self mint --self ./hand.json --heir ./heir.json
|
|
25
|
+
nervur init --config ./castle.json --self ./hand.json
|
|
26
|
+
NERVUR_SECRET=... nervurd --config ./castle.json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The config names the shelf, the door, the secret's environment variable,
|
|
30
|
+
and the modules — the castle's capabilities, chosen by you:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"cells": "/var/lib/nervur/castle.json",
|
|
35
|
+
"listen": { "host": "127.0.0.1", "port": 7001 },
|
|
36
|
+
"secret": "NERVUR_SECRET",
|
|
37
|
+
"modules": {}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Keep the heir off the machine. Whoever takes the machine takes what the
|
|
42
|
+
castle holds today; the heir is why they can never become it tomorrow.
|
|
43
|
+
|
|
44
|
+
## The house of cards
|
|
45
|
+
|
|
46
|
+
The browser's raise is two calls from
|
|
47
|
+
[`@nervur-org/floor-web`](https://www.npmjs.com/package/@nervur-org/floor-web),
|
|
48
|
+
because a tab closes constantly and the two acts must not blur:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
import { found, wake } from '@nervur-org/floor-web';
|
|
52
|
+
|
|
53
|
+
// Once, ever: mint the identity, stand the castle, get custody back.
|
|
54
|
+
let kept;
|
|
55
|
+
const castle = found({ keep: (dump) => (kept = dump) });
|
|
56
|
+
// castle.address — the name; castle.custody — the hand, the heir, the
|
|
57
|
+
// secret: yours to keep (IndexedDB, or a mountain you trust), never the
|
|
58
|
+
// ground's.
|
|
59
|
+
|
|
60
|
+
// Every session after: same name, same residents, no identity act.
|
|
61
|
+
// wake hands back the standing ground itself — woken.address is the name.
|
|
62
|
+
const woken = wake({ held: kept, secret: castle.custody.secret });
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Where `kept` and `custody` live is your choice, under this terrain's honest
|
|
66
|
+
limit: a browser may evict what it keeps — a card castle holds what it can
|
|
67
|
+
afford to lose, and keeps what is precious in its relationship with a
|
|
68
|
+
mountain.
|
|
69
|
+
|
|
70
|
+
## They speak
|
|
71
|
+
|
|
72
|
+
A letter is a letter on every terrain: one signed envelope, carried
|
|
73
|
+
however the two castles can reach each other — here, `fetch` to the
|
|
74
|
+
mountain's door. First the mountain's keeper invites the card castle's
|
|
75
|
+
founder (one signed act at the admin door — the CLI, or any client the
|
|
76
|
+
keeper holds), and then the founder asks from the browser:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
import { Client, Tools, Voice } from 'nervur';
|
|
80
|
+
|
|
81
|
+
const me = Voice.revive(castle.custody.hand);
|
|
82
|
+
const talk = Client({
|
|
83
|
+
voice: me,
|
|
84
|
+
deliver: async (envelope) => {
|
|
85
|
+
const res = await fetch('http://127.0.0.1:7001', {
|
|
86
|
+
body: JSON.stringify(envelope),
|
|
87
|
+
headers: { 'content-type': 'application/json' },
|
|
88
|
+
method: 'POST',
|
|
89
|
+
});
|
|
90
|
+
return res.status === 200 ? res.json() : undefined;
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const heard = await talk.ask(mountainAddress, '{ census { active } }');
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The client verifies every answer from the address alone — the mountain's
|
|
98
|
+
word arrives signed, and a broken or missing signature is dropped exactly
|
|
99
|
+
like silence. A stranger who was never invited gets silence too, and cannot
|
|
100
|
+
tell refusal from absence. That is the whole protocol: **say who you are,
|
|
101
|
+
name whom you seek, and the castle answers or keeps quiet.**
|
|
102
|
+
|
|
103
|
+
## Where to go next
|
|
104
|
+
|
|
105
|
+
- [`nervur`'s README](README.md) — the library underneath both castles.
|
|
106
|
+
- [`nervurd`'s README](../nervurd/README.md) — the operator's page: checks,
|
|
107
|
+
rotation, backups, the door's full behaviour.
|
|
108
|
+
- [`@nervur-org/floor-web`](../floor-web/README.md) — the web floor's
|
|
109
|
+
terrain contract, founding and waking in full.
|
|
110
|
+
- [`@nervur-org/species`](../species/README.md) — blueprints your castles
|
|
111
|
+
grow beings from.
|
package/README.md
CHANGED
|
@@ -1,23 +1,40 @@
|
|
|
1
1
|
# Nervur
|
|
2
2
|
|
|
3
|
-
The library.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
The library. Picture a castle: one gate, its own residents, its own law.
|
|
4
|
+
**A ground is the castle** — one sovereign process holding many beings,
|
|
5
|
+
exposing exactly one handler where every message for every resident
|
|
6
|
+
arrives. You arrive at the gate, say _I am X_ with your signature, and
|
|
7
|
+
whom you seek travels inside the letter; the castle answers or keeps
|
|
8
|
+
silence, and a resident that refuses you looks exactly like one that
|
|
9
|
+
never lived there. The castle's capabilities are its **modules** — a
|
|
10
|
+
resident uses only what its blueprint declares — and the terrain under
|
|
11
|
+
the castle is a **floor**: a browser tab, a Linux daemon, a phone, an
|
|
12
|
+
edge worker. The castle is a castle wherever it is built.
|
|
13
|
+
|
|
14
|
+
Beneath the picture, the mechanics: an address is its own verification
|
|
15
|
+
key, authority is a signature, and every relationship is one ref with
|
|
16
|
+
declared rights — so a reference cannot be forged and no ambient
|
|
17
|
+
authority exists anywhere. The door asks by whose authority and refuses
|
|
18
|
+
every other question. The lineage is the object-capability tradition —
|
|
19
|
+
CapTP, the actor model — with one honest departure: the door verifies who
|
|
20
|
+
is calling, and rights are declared per reference. Pure Crockford
|
|
21
|
+
JavaScript — factory functions, closures for privacy, no `this`, no
|
|
22
|
+
classes, frozen surfaces — with no `node:` import anywhere, so the twelve
|
|
23
|
+
files run wherever JavaScript does; three dependencies (`graphql` for the
|
|
24
|
+
one thing that would be madness to hand-roll, `@noble/curves` and
|
|
25
|
+
`@noble/hashes` for the arithmetic every host must agree on).
|
|
16
26
|
|
|
17
27
|
## Quickstart
|
|
18
28
|
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
New to the whole thing? [GETTING-STARTED.md](GETTING-STARTED.md) raises
|
|
30
|
+
two castles on two terrains — a server and a browser tab — and has them
|
|
31
|
+
speak; the bench drives that exact story. What follows here is the
|
|
32
|
+
library alone.
|
|
33
|
+
|
|
34
|
+
`npm install nervur` — what ships is this source, byte for byte. The
|
|
35
|
+
package is a library and carries no command; a ground also needs a floor
|
|
36
|
+
(`@nervur-org/floor-machine` here) — the library is the law and the
|
|
37
|
+
shape, and a host is what it stands on;
|
|
21
38
|
the `nervur` command on an operator's PATH belongs to the
|
|
22
39
|
[`nervurd`](../nervurd/README.md) package, its name twin. Construction is
|
|
23
40
|
synchronous; every call through a door — `pointer`, `mcp`, `gql`, and each
|
|
@@ -25,7 +42,8 @@ function they hand back — is async. This runs today, lifted from the
|
|
|
25
42
|
bench's own passing suites:
|
|
26
43
|
|
|
27
44
|
```js
|
|
28
|
-
import { Cells,
|
|
45
|
+
import { Cells, Cipher, Keychain } from '@nervur-org/floor-machine';
|
|
46
|
+
import { Ground, Tools, Voice } from 'nervur';
|
|
29
47
|
|
|
30
48
|
const DIARY = `({
|
|
31
49
|
name: 'acme.diary',
|
|
@@ -49,7 +67,11 @@ const DIARY = `({
|
|
|
49
67
|
})`;
|
|
50
68
|
|
|
51
69
|
const secret = Buffer.from(process.env.NERVUR_SECRET, 'base64'); // 32 bytes, minted once with Tools.secret()
|
|
52
|
-
const shelf = {
|
|
70
|
+
const shelf = {
|
|
71
|
+
cells: Cells({ file: 'ground.json' }),
|
|
72
|
+
cipher: Cipher(),
|
|
73
|
+
keychain: Keychain(secret),
|
|
74
|
+
};
|
|
53
75
|
|
|
54
76
|
const heir = Voice(); // keep this one somewhere the machine is not
|
|
55
77
|
const self = Voice(Tools.digest(heir.pk)); // and hand the ground only this
|
|
@@ -122,7 +144,11 @@ three things while hiding everything it is made of:
|
|
|
122
144
|
ground arrives here and routes on the envelope's `to`. An address the
|
|
123
145
|
ground does not hold is silence. A host module that listens on a port, or
|
|
124
146
|
an app shell that calls in process, hands its envelopes to this and
|
|
125
|
-
nothing else — the library never opens a socket.
|
|
147
|
+
nothing else — the library never opens a socket. `receive` answers the
|
|
148
|
+
signed `{ body, sig }` or `undefined` for silence; over HTTP, carry the
|
|
149
|
+
answer as the 200 response body and silence however you like (`nervurd`
|
|
150
|
+
uses an empty `204`) — no status code carries meaning, and a caller
|
|
151
|
+
trusts nothing but a verified signed answer.
|
|
126
152
|
- `serve(voice)` — authority established once, and every face inherits it.
|
|
127
153
|
|
|
128
154
|
A fresh ground has no identity, and no door for anyone to reach first: it
|
|
@@ -134,6 +160,17 @@ after. Both are custody's own calls on the shelf, never doors — which is
|
|
|
134
160
|
what running-is-custody looks like from the outside, stated rather than
|
|
135
161
|
hidden.
|
|
136
162
|
|
|
163
|
+
`unproven()` is custody's third call, and it is a read: the addresses whose
|
|
164
|
+
writes have stopped proving, with when each was first and last seen and how
|
|
165
|
+
many writes have been refused. A write is proven against the key on the
|
|
166
|
+
being's `chain/pk` cell, so whoever holds the shelf can overwrite that cell
|
|
167
|
+
and freeze a being's memory while it answers on, correctly signed and
|
|
168
|
+
indistinguishable to every caller. The ground records that in its own host
|
|
169
|
+
rows under `ground/` — no being reads them, nothing goes over the wire — and
|
|
170
|
+
this is where a host reads them back. Show it to whoever operates the
|
|
171
|
+
machine; a ground that has stopped proving its own writes is a custody
|
|
172
|
+
incident, not a bug.
|
|
173
|
+
|
|
137
174
|
**`serve` is the client, and it has four faces of one door.** `pointer(at)`
|
|
138
175
|
is a plain object with one function per field, generated from the schema that
|
|
139
176
|
caller is allowed to see. `gql(at)` is the raw document. `mcp(at)` is the
|
|
@@ -215,7 +252,8 @@ silence, and silence is something local delivery never answers with.
|
|
|
215
252
|
activates and destroys, reachable only through the admin being.
|
|
216
253
|
- [admin.mjs](admin.mjs) — the administrative program every ground installs
|
|
217
254
|
for itself, aliased `Ground`: census, create, activate, destroy, upgrade,
|
|
218
|
-
alias, attest, and behind `SELF` alone
|
|
255
|
+
alias, attest, invite, drop, wear, and behind `SELF` alone extend; `wears`
|
|
256
|
+
answers any stranger with the livery being the ground wears. Upgrade is
|
|
219
257
|
custody moving the one cell that was always the core's to move: the
|
|
220
258
|
program is replaced in place — address, cells, refs and chain surviving —
|
|
221
259
|
refused whole when a cell the being holds would go undeclared, and visible
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
This package answers one question — by whose authority — and a hole in that
|
|
4
|
+
answer is the most serious bug it can have.
|
|
5
|
+
|
|
6
|
+
Report vulnerabilities privately to **<security@nervur.com>**. Do not open a
|
|
7
|
+
public report. State what you ran, what you sent, and what answered; a
|
|
8
|
+
proof of concept is welcome but not required.
|
|
9
|
+
|
|
10
|
+
A person reads every report. Fixes ship as releases on the live 0.x line,
|
|
11
|
+
and the report is credited if you want it to be.
|
package/admin.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
const ADMIN = `({
|
|
2
2
|
name: 'nervur.ground',
|
|
3
3
|
needs: { modules: [{ module: 'memory', contract: 'nervur.memory' }] },
|
|
4
|
-
memory: {
|
|
4
|
+
memory: {
|
|
5
|
+
'aliases/*': { type: 'String', class: 'data' },
|
|
6
|
+
wears: { type: 'String', class: 'data' },
|
|
7
|
+
},
|
|
5
8
|
interface: \`
|
|
6
9
|
input RefInput { address: String!, nextPkHash: String, rights: String }
|
|
7
10
|
type Resident { address: String!, alias: String, digest: String, active: Boolean! }
|
|
@@ -11,6 +14,7 @@ const ADMIN = `({
|
|
|
11
14
|
aliasOf(name: String!): String @rights(is: [SELF, MEMBER])
|
|
12
15
|
attest(module: String!): Boolean @rights(is: [SELF, MEMBER])
|
|
13
16
|
opened: Boolean @rights(is: [SELF, MEMBER, STRANGER])
|
|
17
|
+
wears: String @rights(is: [SELF, MEMBER, STRANGER])
|
|
14
18
|
}
|
|
15
19
|
type Mutation {
|
|
16
20
|
invite(address: String!, nextPkHash: String, rights: String): Boolean @rights(is: [SELF, MEMBER])
|
|
@@ -22,6 +26,7 @@ const ADMIN = `({
|
|
|
22
26
|
destroy(address: String!): String @rights(is: [SELF, MEMBER])
|
|
23
27
|
upgrade(address: String!, program: String!): Boolean @rights(is: [SELF, MEMBER])
|
|
24
28
|
alias(name: String!, address: String!): Boolean @rights(is: [SELF, MEMBER])
|
|
29
|
+
wear(address: String): Boolean @rights(is: [SELF, MEMBER])
|
|
25
30
|
}
|
|
26
31
|
\`,
|
|
27
32
|
resolvers: {
|
|
@@ -37,6 +42,7 @@ const ADMIN = `({
|
|
|
37
42
|
aliasOf: (_, { name }, { modules }) => modules.memory.read('aliases/' + name),
|
|
38
43
|
attest: (_, { module }, { registry }) => registry.attest(module),
|
|
39
44
|
opened: (_, __, { modules }) => modules.memory.list('refs/').length > 0,
|
|
45
|
+
wears: (_, __, { modules }) => modules.memory.read('wears') ?? null,
|
|
40
46
|
},
|
|
41
47
|
Mutation: {
|
|
42
48
|
invite: (_, { address, nextPkHash, rights }, { modules }) =>
|
|
@@ -66,6 +72,10 @@ const ADMIN = `({
|
|
|
66
72
|
registry.upgrade({ address, program }) !== undefined,
|
|
67
73
|
alias: (_, { name, address }, { modules }) =>
|
|
68
74
|
modules.memory.write('aliases/' + name, address) !== undefined,
|
|
75
|
+
wear: (_, { address }, { modules }) => {
|
|
76
|
+
modules.memory.write('wears', address ?? undefined);
|
|
77
|
+
return modules.memory.read('wears') === (address ?? undefined);
|
|
78
|
+
},
|
|
69
79
|
},
|
|
70
80
|
},
|
|
71
81
|
})`;
|