nervur 0.12.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 +44 -18
- package/SECURITY.md +11 -0
- package/being.mjs +73 -33
- package/blueprint.mjs +13 -13
- package/client.mjs +9 -6
- package/contract.mjs +6 -4
- package/ground.mjs +95 -36
- package/index.mjs +2 -2
- package/modules.mjs +13 -70
- package/package.json +5 -2
- package/tools.mjs +67 -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
|
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/being.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ADMIN } from './admin.mjs';
|
|
2
2
|
import { OPEN, STRUCTURAL, composed, coreClass } from './blueprint.mjs';
|
|
3
3
|
import { Program } from './program.mjs';
|
|
4
|
-
import { MEASURE, SPAN, Stamp, Tools } from './tools.mjs';
|
|
4
|
+
import { Bytes, MEASURE, SPAN, Stamp, Tools } from './tools.mjs';
|
|
5
5
|
import { Voice } from './voice.mjs';
|
|
6
6
|
|
|
7
|
-
const Compartment = ({ memory, voice, address, root, classOf }) => {
|
|
7
|
+
const Compartment = ({ memory, voice, address, root, classOf, cipher }) => {
|
|
8
8
|
const held = (key) => memory.read(address, key);
|
|
9
9
|
const sealed = (key) => {
|
|
10
10
|
const kind = classOf(key);
|
|
@@ -25,7 +25,10 @@ const Compartment = ({ memory, voice, address, root, classOf }) => {
|
|
|
25
25
|
return undefined;
|
|
26
26
|
}
|
|
27
27
|
const secret = sealed(key);
|
|
28
|
-
|
|
28
|
+
if (secret === undefined) {
|
|
29
|
+
return cell.value;
|
|
30
|
+
}
|
|
31
|
+
return cipher === undefined ? undefined : cipher.unseal(secret, cell.value);
|
|
29
32
|
},
|
|
30
33
|
write: (key, plain) => {
|
|
31
34
|
if (classOf(key) === undefined) {
|
|
@@ -35,7 +38,14 @@ const Compartment = ({ memory, voice, address, root, classOf }) => {
|
|
|
35
38
|
return put(key, null);
|
|
36
39
|
}
|
|
37
40
|
const secret = sealed(key);
|
|
38
|
-
|
|
41
|
+
if (secret === undefined) {
|
|
42
|
+
return put(key, plain);
|
|
43
|
+
}
|
|
44
|
+
if (cipher === undefined) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const shut = cipher.seal(secret, plain);
|
|
48
|
+
return shut === undefined ? undefined : put(key, shut);
|
|
39
49
|
},
|
|
40
50
|
});
|
|
41
51
|
};
|
|
@@ -105,6 +115,14 @@ const referable = (mine, key, plain, program) => {
|
|
|
105
115
|
return { ...plain, rights };
|
|
106
116
|
}
|
|
107
117
|
const held = mine.read(key);
|
|
118
|
+
if (
|
|
119
|
+
held?.nextPkHash != null &&
|
|
120
|
+
plain.nextPkHash !== undefined &&
|
|
121
|
+
plain.nextPkHash !== null &&
|
|
122
|
+
plain.nextPkHash !== held.nextPkHash
|
|
123
|
+
) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
108
126
|
return {
|
|
109
127
|
...plain,
|
|
110
128
|
nextPkHash: plain.nextPkHash ?? held?.nextPkHash ?? null,
|
|
@@ -131,10 +149,10 @@ const scoped = (mine, program) =>
|
|
|
131
149
|
},
|
|
132
150
|
});
|
|
133
151
|
|
|
134
|
-
const rootFor = ({ memory, voice, address }) => {
|
|
152
|
+
const rootFor = ({ memory, voice, address, cipher }) => {
|
|
135
153
|
const cell = memory.read(address, `keyring/${Tools.address(voice.pk)}`);
|
|
136
|
-
const opened = cell === undefined ? undefined : voice.open(cell.value.wrapped);
|
|
137
|
-
return opened === undefined ? undefined :
|
|
154
|
+
const opened = cell === undefined ? undefined : voice.open(cell.value.wrapped, cipher);
|
|
155
|
+
return opened === undefined ? undefined : Bytes.unbase64(opened);
|
|
138
156
|
};
|
|
139
157
|
|
|
140
158
|
const Port = ({ voice, mine, deliver, rights, stamp, lanes, fence }) => {
|
|
@@ -142,11 +160,19 @@ const Port = ({ voice, mine, deliver, rights, stamp, lanes, fence }) => {
|
|
|
142
160
|
if (fence !== undefined && !fence.open()) {
|
|
143
161
|
return undefined;
|
|
144
162
|
}
|
|
145
|
-
const turn = (lanes.get(to) ?? Promise.resolve()).then(() => {
|
|
163
|
+
const turn = (lanes.get(to) ?? Promise.resolve()).then(async () => {
|
|
146
164
|
const handle = mine.read(`handles/${to}`);
|
|
147
|
-
|
|
148
|
-
|
|
165
|
+
const envelope = await Tools.envelope(
|
|
166
|
+
handle === undefined ? voice : Voice.revive(handle.face),
|
|
167
|
+
to,
|
|
168
|
+
op,
|
|
169
|
+
stamp(),
|
|
149
170
|
);
|
|
171
|
+
const claim = Tools.digest(
|
|
172
|
+
Tools.canon({ from: envelope.from, op: envelope.op, seq: envelope.seq, to: envelope.to }),
|
|
173
|
+
);
|
|
174
|
+
const reply = await deliver(envelope);
|
|
175
|
+
return reply === undefined ? undefined : { ...reply, claim };
|
|
150
176
|
});
|
|
151
177
|
const settled = turn.then(
|
|
152
178
|
() => undefined,
|
|
@@ -206,10 +232,10 @@ const successionOf = (memory, address) =>
|
|
|
206
232
|
.filter((one) => one !== undefined)
|
|
207
233
|
.sort((one, other) => one.v - other.v);
|
|
208
234
|
|
|
209
|
-
const Being = ({ voice, address, deliver, modules, registry, span, vouched, watch }) => {
|
|
235
|
+
const Being = ({ voice, address, cipher, deliver, modules, registry, span, vouched, watch }) => {
|
|
210
236
|
const held = spanOf(span);
|
|
211
237
|
const memory = modules.memory;
|
|
212
|
-
const root = rootFor({ memory, voice, address });
|
|
238
|
+
const root = rootFor({ memory, voice, address, cipher });
|
|
213
239
|
const source = memory.read(address, 'program')?.value;
|
|
214
240
|
const program = source === undefined ? undefined : Program(source);
|
|
215
241
|
const mine = Compartment({
|
|
@@ -218,16 +244,17 @@ const Being = ({ voice, address, deliver, modules, registry, span, vouched, watc
|
|
|
218
244
|
address,
|
|
219
245
|
root,
|
|
220
246
|
classOf: (key) => program?.classOf(key) ?? coreClass(key),
|
|
247
|
+
cipher,
|
|
221
248
|
});
|
|
222
249
|
const succession = successionOf(memory, address);
|
|
223
250
|
const stamp = Stamp();
|
|
224
251
|
const lanes = new Map();
|
|
225
|
-
const answer = (spoken) => {
|
|
252
|
+
const answer = (spoken, claim) => {
|
|
226
253
|
const body = Tools.canon(spoken);
|
|
227
254
|
if (body.length > MEASURE) {
|
|
228
255
|
return undefined;
|
|
229
256
|
}
|
|
230
|
-
const signed = { body: spoken, sig: voice.sign(body) };
|
|
257
|
+
const signed = { body: spoken, sig: voice.sign(Tools.canon({ body: spoken, claim })) };
|
|
231
258
|
return succession.length === 0 ? signed : { ...signed, succession };
|
|
232
259
|
};
|
|
233
260
|
const note = (envelope, outcome) => {
|
|
@@ -251,6 +278,7 @@ const Being = ({ voice, address, deliver, modules, registry, span, vouched, watc
|
|
|
251
278
|
try {
|
|
252
279
|
const { from, op, seq, sig, to } = envelope;
|
|
253
280
|
const claim = Tools.canon({ from, op, seq, to });
|
|
281
|
+
const claimDigest = Tools.digest(claim);
|
|
254
282
|
const callerPk = Tools.pkOf(from);
|
|
255
283
|
const proven =
|
|
256
284
|
claim.length <= MEASURE &&
|
|
@@ -274,30 +302,34 @@ const Being = ({ voice, address, deliver, modules, registry, span, vouched, watc
|
|
|
274
302
|
if (program !== undefined && !program.visible(rights)) {
|
|
275
303
|
return undefined;
|
|
276
304
|
}
|
|
277
|
-
return answer(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
305
|
+
return answer(
|
|
306
|
+
{
|
|
307
|
+
address,
|
|
308
|
+
box: Bytes.base64(voice.boxPk),
|
|
309
|
+
digest: program?.digest,
|
|
310
|
+
rights,
|
|
311
|
+
sdl: program?.sdlFor(rights),
|
|
312
|
+
},
|
|
313
|
+
claimDigest,
|
|
314
|
+
);
|
|
283
315
|
}
|
|
284
316
|
if (op?.introspect !== undefined) {
|
|
285
317
|
const seen = await program?.introspect(rights);
|
|
286
|
-
return seen === undefined ? undefined : answer({ introspection: seen });
|
|
318
|
+
return seen === undefined ? undefined : answer({ introspection: seen }, claimDigest);
|
|
287
319
|
}
|
|
288
320
|
if (op?.rotate !== undefined) {
|
|
289
321
|
const rotated = rotates({ mine }, from, op.rotate.nextPkHash);
|
|
290
|
-
return rotated === undefined ? undefined : answer({ rotated });
|
|
322
|
+
return rotated === undefined ? undefined : answer({ rotated }, claimDigest);
|
|
291
323
|
}
|
|
292
324
|
if (op?.release !== undefined) {
|
|
293
325
|
if (row === undefined) {
|
|
294
326
|
return undefined;
|
|
295
327
|
}
|
|
296
328
|
mine.write(`refs/${from}`, undefined);
|
|
297
|
-
return answer({ released: from });
|
|
329
|
+
return answer({ released: from }, claimDigest);
|
|
298
330
|
}
|
|
299
331
|
if (op?.acceptInvite !== undefined) {
|
|
300
|
-
const offer = Tools.digest(
|
|
332
|
+
const offer = Tools.digest(Bytes.unbase64(op.acceptInvite.proof));
|
|
301
333
|
const pending = mine.read(`refs/pending/${offer}`);
|
|
302
334
|
if (pending === undefined || pending.spent) {
|
|
303
335
|
return undefined;
|
|
@@ -308,7 +340,7 @@ const Being = ({ voice, address, deliver, modules, registry, span, vouched, watc
|
|
|
308
340
|
rights: pending.rights,
|
|
309
341
|
seen: seq ?? 0,
|
|
310
342
|
});
|
|
311
|
-
return answer({ referred: from, rights: pending.rights });
|
|
343
|
+
return answer({ referred: from, rights: pending.rights }, claimDigest);
|
|
312
344
|
}
|
|
313
345
|
if (op?.ask === undefined || program === undefined || !program.visible(rights)) {
|
|
314
346
|
return undefined;
|
|
@@ -329,7 +361,7 @@ const Being = ({ voice, address, deliver, modules, registry, span, vouched, watc
|
|
|
329
361
|
variables: op.ask.variables,
|
|
330
362
|
}),
|
|
331
363
|
).finally(fence.close);
|
|
332
|
-
return asked === undefined ? undefined : answer(asked);
|
|
364
|
+
return asked === undefined ? undefined : answer(asked, claimDigest);
|
|
333
365
|
} catch {
|
|
334
366
|
return undefined;
|
|
335
367
|
}
|
|
@@ -352,18 +384,18 @@ const spend = ({ memory, voice, address }) => {
|
|
|
352
384
|
return address;
|
|
353
385
|
};
|
|
354
386
|
|
|
355
|
-
const make = ({ memory, voice, address, program }) => {
|
|
387
|
+
const make = ({ memory, voice, address, program, cipher }) => {
|
|
356
388
|
const root = Tools.secret();
|
|
357
389
|
const put = (key, value) => {
|
|
358
390
|
const claim = { address, key, value, version: (memory.read(address, key)?.version ?? 0) + 1 };
|
|
359
391
|
return memory.write({ ...claim, sig: voice.sign(Tools.canon(claim)) });
|
|
360
392
|
};
|
|
361
|
-
put('chain/pk',
|
|
393
|
+
put('chain/pk', Bytes.hex(voice.pk));
|
|
362
394
|
put('chain/nextPkHash', voice.nextPkHash);
|
|
363
395
|
put('program', program ?? '({})');
|
|
364
396
|
put(`keyring/${Tools.address(voice.pk)}`, {
|
|
365
|
-
boxPk:
|
|
366
|
-
wrapped: Voice.sealTo(voice.boxPk,
|
|
397
|
+
boxPk: Bytes.base64(voice.boxPk),
|
|
398
|
+
wrapped: Voice.sealTo(voice.boxPk, Bytes.base64(root), cipher),
|
|
367
399
|
});
|
|
368
400
|
const written = program === undefined ? undefined : Program(program);
|
|
369
401
|
return Compartment({
|
|
@@ -372,6 +404,7 @@ const make = ({ memory, voice, address, program }) => {
|
|
|
372
404
|
address,
|
|
373
405
|
root,
|
|
374
406
|
classOf: (key) => written?.classOf(key) ?? coreClass(key),
|
|
407
|
+
cipher,
|
|
375
408
|
});
|
|
376
409
|
};
|
|
377
410
|
|
|
@@ -379,6 +412,7 @@ const Registry = ({
|
|
|
379
412
|
memory,
|
|
380
413
|
adminMine,
|
|
381
414
|
address,
|
|
415
|
+
cipher,
|
|
382
416
|
deliver,
|
|
383
417
|
modules,
|
|
384
418
|
modulesFor,
|
|
@@ -388,6 +422,7 @@ const Registry = ({
|
|
|
388
422
|
vouched,
|
|
389
423
|
attest,
|
|
390
424
|
watch,
|
|
425
|
+
extended,
|
|
391
426
|
}) => {
|
|
392
427
|
const live = new Map();
|
|
393
428
|
const stranded = (at, written) => {
|
|
@@ -416,6 +451,7 @@ const Registry = ({
|
|
|
416
451
|
);
|
|
417
452
|
const door = Being({
|
|
418
453
|
address: at,
|
|
454
|
+
cipher,
|
|
419
455
|
deliver,
|
|
420
456
|
modules: modulesFor(voice, declared),
|
|
421
457
|
span,
|
|
@@ -452,7 +488,7 @@ const Registry = ({
|
|
|
452
488
|
}
|
|
453
489
|
const voice = Voice();
|
|
454
490
|
const address = Tools.address(voice.pk);
|
|
455
|
-
const mine = make({ memory, voice, address, program });
|
|
491
|
+
const mine = make({ memory, voice, address, program, cipher });
|
|
456
492
|
for (const [key, value] of Object.entries(data)) {
|
|
457
493
|
if (mine.write(key, value) === undefined) {
|
|
458
494
|
forget(address);
|
|
@@ -488,7 +524,11 @@ const Registry = ({
|
|
|
488
524
|
if (stranded(address, written).length > 0) {
|
|
489
525
|
return undefined;
|
|
490
526
|
}
|
|
491
|
-
|
|
527
|
+
if (adminMine.write('program', program) === undefined) {
|
|
528
|
+
return undefined;
|
|
529
|
+
}
|
|
530
|
+
extended?.();
|
|
531
|
+
return address;
|
|
492
532
|
},
|
|
493
533
|
upgrade: ({ address: target, program }) => {
|
|
494
534
|
if (target === address) {
|
|
@@ -551,4 +591,4 @@ const Registry = ({
|
|
|
551
591
|
});
|
|
552
592
|
};
|
|
553
593
|
|
|
554
|
-
export { Being, Compartment, Registry, SUCCESSION, make, rootFor, spend };
|
|
594
|
+
export { Being, Compartment, Registry, SUCCESSION, make, rootFor, spend, successionOf };
|
package/blueprint.mjs
CHANGED
|
@@ -211,13 +211,14 @@ const faults = (source) => {
|
|
|
211
211
|
} catch (error) {
|
|
212
212
|
return [`its schema does not compile: ${error.message} (VIII)`];
|
|
213
213
|
}
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
...STRUCTURAL,
|
|
217
|
-
...(schema.getType('Rights')?.getValues() ?? []).map((one) => one.name),
|
|
218
|
-
]),
|
|
219
|
-
];
|
|
214
|
+
const declaredNames = (schema.getType('Rights')?.getValues() ?? []).map((one) => one.name);
|
|
215
|
+
const rights = [...new Set([...STRUCTURAL, ...declaredNames])];
|
|
220
216
|
const found = [];
|
|
217
|
+
if (!declares && STRUCTURAL.some((one) => !declaredNames.includes(one))) {
|
|
218
|
+
found.push(
|
|
219
|
+
`the Rights enum does not name ${STRUCTURAL.filter((one) => !declaredNames.includes(one)).join(', ')}, and every Rights enum names both SELF and STRANGER (VI)`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
221
222
|
for (const [key, cell] of Object.entries(written.memory ?? {})) {
|
|
222
223
|
if (coreClass(key) !== undefined) {
|
|
223
224
|
found.push(`the cell ${key} is the core's own, and no program declares it (VIII)`);
|
|
@@ -344,12 +345,11 @@ const Blueprint = (source) => {
|
|
|
344
345
|
const sdl = [RIGHTS_DIRECTIVE, declares, written.interface].filter(Boolean).join('\n');
|
|
345
346
|
const document = parse(sdl);
|
|
346
347
|
const schema = buildSchema(sdl);
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
];
|
|
348
|
+
const declaredNames = (schema.getType('Rights')?.getValues() ?? []).map((one) => one.name);
|
|
349
|
+
if (!declares && STRUCTURAL.some((one) => !declaredNames.includes(one))) {
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
const rights = [...new Set([...STRUCTURAL, ...declaredNames])];
|
|
353
353
|
|
|
354
354
|
const admits = (typeName, fieldName, held) => {
|
|
355
355
|
const field = document.definitions
|
|
@@ -395,7 +395,7 @@ const Blueprint = (source) => {
|
|
|
395
395
|
needs.config === undefined ? Object.freeze({}) : inputCheck(schema, needs.config, given),
|
|
396
396
|
digest: Tools.digest(
|
|
397
397
|
Tools.canon({
|
|
398
|
-
interface:
|
|
398
|
+
interface: sdl,
|
|
399
399
|
memory,
|
|
400
400
|
name: written.name,
|
|
401
401
|
needs,
|
package/client.mjs
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { Stamp, Tools } from './tools.mjs';
|
|
2
2
|
|
|
3
|
-
const KEY = /^[0-9a-f]{64}$/;
|
|
4
|
-
|
|
5
3
|
const speaking = (born, at, succession) => {
|
|
6
4
|
if (succession === undefined) {
|
|
7
5
|
return born;
|
|
@@ -11,13 +9,14 @@ const speaking = (born, at, succession) => {
|
|
|
11
9
|
}
|
|
12
10
|
let pk = born;
|
|
13
11
|
for (const step of succession) {
|
|
14
|
-
|
|
12
|
+
const next = typeof step?.next === 'string' ? Tools.pkOf(step.next) : null;
|
|
13
|
+
if (next === null) {
|
|
15
14
|
return undefined;
|
|
16
15
|
}
|
|
17
16
|
if (!Tools.verifies(pk, Tools.canon({ at, next: step.next, v: step.v }), step.sig)) {
|
|
18
17
|
return undefined;
|
|
19
18
|
}
|
|
20
|
-
pk =
|
|
19
|
+
pk = next;
|
|
21
20
|
}
|
|
22
21
|
return pk;
|
|
23
22
|
};
|
|
@@ -32,11 +31,15 @@ const Client = ({ voice, deliver, resolve = (one) => one }) => {
|
|
|
32
31
|
return undefined;
|
|
33
32
|
}
|
|
34
33
|
const turn = (lanes.get(at) ?? Promise.resolve()).then(async () => {
|
|
35
|
-
const
|
|
34
|
+
const envelope = await Tools.envelope(voice, at, op, stamp());
|
|
35
|
+
const claim = Tools.digest(
|
|
36
|
+
Tools.canon({ from: envelope.from, op: envelope.op, seq: envelope.seq, to: envelope.to }),
|
|
37
|
+
);
|
|
38
|
+
const reply = await deliver(envelope);
|
|
36
39
|
const answering = speaking(born, at, reply?.succession);
|
|
37
40
|
return reply?.body === undefined ||
|
|
38
41
|
answering === undefined ||
|
|
39
|
-
!Tools.verifies(answering, Tools.canon(reply.body), reply.sig)
|
|
42
|
+
!Tools.verifies(answering, Tools.canon({ body: reply.body, claim }), reply.sig)
|
|
40
43
|
? undefined
|
|
41
44
|
: reply;
|
|
42
45
|
});
|
package/contract.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildSchema, isEnumType, isInputObjectType,
|
|
1
|
+
import { buildSchema, isEnumType, isInputObjectType, specifiedScalarTypes } from 'graphql';
|
|
2
2
|
import { Tools } from './tools.mjs';
|
|
3
3
|
|
|
4
4
|
const argsAgree = (wanted, given) => {
|
|
@@ -16,9 +16,8 @@ const argsAgree = (wanted, given) => {
|
|
|
16
16
|
const Contract = (name, sdl) => {
|
|
17
17
|
try {
|
|
18
18
|
const schema = buildSchema(sdl);
|
|
19
|
-
const printed = print(parse(sdl));
|
|
20
19
|
return Object.freeze({
|
|
21
|
-
digest: Tools.digest(Tools.canon({ interface:
|
|
20
|
+
digest: Tools.digest(Tools.canon({ interface: sdl, name })),
|
|
22
21
|
name,
|
|
23
22
|
satisfies: (offered) => {
|
|
24
23
|
const theirs = (() => {
|
|
@@ -36,6 +35,9 @@ const Contract = (name, sdl) => {
|
|
|
36
35
|
continue;
|
|
37
36
|
}
|
|
38
37
|
const given = theirs.getTypeMap()[typeName];
|
|
38
|
+
if (given === undefined && !specifiedScalarTypes.some((one) => one.name === typeName)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
39
41
|
if (isEnumType(wanted)) {
|
|
40
42
|
if (
|
|
41
43
|
!isEnumType(given) ||
|
|
@@ -73,7 +75,7 @@ const Contract = (name, sdl) => {
|
|
|
73
75
|
}
|
|
74
76
|
return true;
|
|
75
77
|
},
|
|
76
|
-
sdl
|
|
78
|
+
sdl,
|
|
77
79
|
});
|
|
78
80
|
} catch {
|
|
79
81
|
return undefined;
|