nervur 0.12.0 → 0.14.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 +127 -42
- package/blueprint.mjs +65 -15
- package/client.mjs +9 -6
- package/contract.mjs +6 -4
- package/ground.mjs +96 -36
- package/index.mjs +2 -2
- package/modules.mjs +13 -70
- package/package.json +5 -2
- package/program.mjs +1 -0
- 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
|
};
|
|
@@ -82,7 +92,7 @@ const custodial = (registry, fence) =>
|
|
|
82
92
|
upgrade: (one) => (fence.open() ? registry.upgrade(one) : undefined),
|
|
83
93
|
});
|
|
84
94
|
|
|
85
|
-
const offered = (modules, program, mine) =>
|
|
95
|
+
const offered = (modules, program, mine, seat) =>
|
|
86
96
|
Object.freeze({
|
|
87
97
|
...Object.fromEntries(
|
|
88
98
|
(program?.modules ?? [])
|
|
@@ -90,21 +100,57 @@ const offered = (modules, program, mine) =>
|
|
|
90
100
|
.filter((name) => name !== 'memory' && modules[name] !== undefined)
|
|
91
101
|
.map((name) => [name, modules[name]]),
|
|
92
102
|
),
|
|
93
|
-
memory: scoped(mine, program),
|
|
103
|
+
memory: scoped(mine, program, seat),
|
|
94
104
|
});
|
|
95
105
|
|
|
96
106
|
const grantable = (program) =>
|
|
97
107
|
(program?.rights ?? [...STRUCTURAL, 'MEMBER']).filter((one) => !STRUCTURAL.includes(one));
|
|
98
108
|
|
|
99
|
-
const
|
|
109
|
+
const HORIZON = 7_776_000_000;
|
|
110
|
+
|
|
111
|
+
const whole = (one) => Number.isSafeInteger(one) && one >= 0;
|
|
112
|
+
|
|
113
|
+
const invitation = (plain, rights, program, seat) => {
|
|
114
|
+
const count = plain.count ?? 1;
|
|
115
|
+
const expiresAt = plain.expiresAt ?? 0;
|
|
116
|
+
if (!whole(count) || count < 1 || !whole(expiresAt)) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
const now = seat.now();
|
|
120
|
+
if (seat.administration) {
|
|
121
|
+
return count === 1 && expiresAt > 0 && expiresAt < now + HORIZON
|
|
122
|
+
? { ...plain, count, expiresAt, rights }
|
|
123
|
+
: undefined;
|
|
124
|
+
}
|
|
125
|
+
const ceiling = program?.invites?.(rights);
|
|
126
|
+
if (ceiling !== undefined) {
|
|
127
|
+
if (ceiling.count > 0 && count > ceiling.count) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
if (ceiling.within > 0 && (expiresAt === 0 || expiresAt > now + ceiling.within)) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { ...plain, count, expiresAt, rights };
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const referable = (mine, key, plain, program, seat) => {
|
|
100
138
|
const rights = plain.rights ?? 'MEMBER';
|
|
101
139
|
if (!grantable(program).includes(rights)) {
|
|
102
140
|
return undefined;
|
|
103
141
|
}
|
|
104
142
|
if (key.startsWith('refs/pending/')) {
|
|
105
|
-
return
|
|
143
|
+
return invitation(plain, rights, program, seat);
|
|
106
144
|
}
|
|
107
145
|
const held = mine.read(key);
|
|
146
|
+
if (
|
|
147
|
+
held?.nextPkHash != null &&
|
|
148
|
+
plain.nextPkHash !== undefined &&
|
|
149
|
+
plain.nextPkHash !== null &&
|
|
150
|
+
plain.nextPkHash !== held.nextPkHash
|
|
151
|
+
) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
108
154
|
return {
|
|
109
155
|
...plain,
|
|
110
156
|
nextPkHash: plain.nextPkHash ?? held?.nextPkHash ?? null,
|
|
@@ -113,14 +159,14 @@ const referable = (mine, key, plain, program) => {
|
|
|
113
159
|
};
|
|
114
160
|
};
|
|
115
161
|
|
|
116
|
-
const scoped = (mine, program) =>
|
|
162
|
+
const scoped = (mine, program, seat) =>
|
|
117
163
|
Object.freeze({
|
|
118
164
|
classOf: mine.classOf,
|
|
119
165
|
list: mine.list,
|
|
120
166
|
read: mine.read,
|
|
121
167
|
write: (key, plain) => {
|
|
122
168
|
if (key.startsWith('refs/') && plain !== undefined) {
|
|
123
|
-
const guarded = referable(mine, key, plain, program);
|
|
169
|
+
const guarded = referable(mine, key, plain, program, seat);
|
|
124
170
|
return guarded === undefined ? undefined : mine.write(key, guarded);
|
|
125
171
|
}
|
|
126
172
|
return key.startsWith('refs/') ||
|
|
@@ -131,10 +177,10 @@ const scoped = (mine, program) =>
|
|
|
131
177
|
},
|
|
132
178
|
});
|
|
133
179
|
|
|
134
|
-
const rootFor = ({ memory, voice, address }) => {
|
|
180
|
+
const rootFor = ({ memory, voice, address, cipher }) => {
|
|
135
181
|
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 :
|
|
182
|
+
const opened = cell === undefined ? undefined : voice.open(cell.value.wrapped, cipher);
|
|
183
|
+
return opened === undefined ? undefined : Bytes.unbase64(opened);
|
|
138
184
|
};
|
|
139
185
|
|
|
140
186
|
const Port = ({ voice, mine, deliver, rights, stamp, lanes, fence }) => {
|
|
@@ -142,11 +188,19 @@ const Port = ({ voice, mine, deliver, rights, stamp, lanes, fence }) => {
|
|
|
142
188
|
if (fence !== undefined && !fence.open()) {
|
|
143
189
|
return undefined;
|
|
144
190
|
}
|
|
145
|
-
const turn = (lanes.get(to) ?? Promise.resolve()).then(() => {
|
|
191
|
+
const turn = (lanes.get(to) ?? Promise.resolve()).then(async () => {
|
|
146
192
|
const handle = mine.read(`handles/${to}`);
|
|
147
|
-
|
|
148
|
-
|
|
193
|
+
const envelope = await Tools.envelope(
|
|
194
|
+
handle === undefined ? voice : Voice.revive(handle.face),
|
|
195
|
+
to,
|
|
196
|
+
op,
|
|
197
|
+
stamp(),
|
|
149
198
|
);
|
|
199
|
+
const claim = Tools.digest(
|
|
200
|
+
Tools.canon({ from: envelope.from, op: envelope.op, seq: envelope.seq, to: envelope.to }),
|
|
201
|
+
);
|
|
202
|
+
const reply = await deliver(envelope);
|
|
203
|
+
return reply === undefined ? undefined : { ...reply, claim };
|
|
150
204
|
});
|
|
151
205
|
const settled = turn.then(
|
|
152
206
|
() => undefined,
|
|
@@ -206,10 +260,21 @@ const successionOf = (memory, address) =>
|
|
|
206
260
|
.filter((one) => one !== undefined)
|
|
207
261
|
.sort((one, other) => one.v - other.v);
|
|
208
262
|
|
|
209
|
-
const Being = ({
|
|
263
|
+
const Being = ({
|
|
264
|
+
voice,
|
|
265
|
+
address,
|
|
266
|
+
cipher,
|
|
267
|
+
deliver,
|
|
268
|
+
modules,
|
|
269
|
+
registry,
|
|
270
|
+
administration,
|
|
271
|
+
span,
|
|
272
|
+
vouched,
|
|
273
|
+
watch,
|
|
274
|
+
}) => {
|
|
210
275
|
const held = spanOf(span);
|
|
211
276
|
const memory = modules.memory;
|
|
212
|
-
const root = rootFor({ memory, voice, address });
|
|
277
|
+
const root = rootFor({ memory, voice, address, cipher });
|
|
213
278
|
const source = memory.read(address, 'program')?.value;
|
|
214
279
|
const program = source === undefined ? undefined : Program(source);
|
|
215
280
|
const mine = Compartment({
|
|
@@ -218,16 +283,18 @@ const Being = ({ voice, address, deliver, modules, registry, span, vouched, watc
|
|
|
218
283
|
address,
|
|
219
284
|
root,
|
|
220
285
|
classOf: (key) => program?.classOf(key) ?? coreClass(key),
|
|
286
|
+
cipher,
|
|
221
287
|
});
|
|
222
288
|
const succession = successionOf(memory, address);
|
|
223
289
|
const stamp = Stamp();
|
|
290
|
+
const seat = Object.freeze({ administration: administration === true, now: stamp });
|
|
224
291
|
const lanes = new Map();
|
|
225
|
-
const answer = (spoken) => {
|
|
292
|
+
const answer = (spoken, claim) => {
|
|
226
293
|
const body = Tools.canon(spoken);
|
|
227
294
|
if (body.length > MEASURE) {
|
|
228
295
|
return undefined;
|
|
229
296
|
}
|
|
230
|
-
const signed = { body: spoken, sig: voice.sign(body) };
|
|
297
|
+
const signed = { body: spoken, sig: voice.sign(Tools.canon({ body: spoken, claim })) };
|
|
231
298
|
return succession.length === 0 ? signed : { ...signed, succession };
|
|
232
299
|
};
|
|
233
300
|
const note = (envelope, outcome) => {
|
|
@@ -251,6 +318,7 @@ const Being = ({ voice, address, deliver, modules, registry, span, vouched, watc
|
|
|
251
318
|
try {
|
|
252
319
|
const { from, op, seq, sig, to } = envelope;
|
|
253
320
|
const claim = Tools.canon({ from, op, seq, to });
|
|
321
|
+
const claimDigest = Tools.digest(claim);
|
|
254
322
|
const callerPk = Tools.pkOf(from);
|
|
255
323
|
const proven =
|
|
256
324
|
claim.length <= MEASURE &&
|
|
@@ -274,41 +342,50 @@ const Being = ({ voice, address, deliver, modules, registry, span, vouched, watc
|
|
|
274
342
|
if (program !== undefined && !program.visible(rights)) {
|
|
275
343
|
return undefined;
|
|
276
344
|
}
|
|
277
|
-
return answer(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
345
|
+
return answer(
|
|
346
|
+
{
|
|
347
|
+
address,
|
|
348
|
+
box: Bytes.base64(voice.boxPk),
|
|
349
|
+
digest: program?.digest,
|
|
350
|
+
rights,
|
|
351
|
+
sdl: program?.sdlFor(rights),
|
|
352
|
+
},
|
|
353
|
+
claimDigest,
|
|
354
|
+
);
|
|
283
355
|
}
|
|
284
356
|
if (op?.introspect !== undefined) {
|
|
285
357
|
const seen = await program?.introspect(rights);
|
|
286
|
-
return seen === undefined ? undefined : answer({ introspection: seen });
|
|
358
|
+
return seen === undefined ? undefined : answer({ introspection: seen }, claimDigest);
|
|
287
359
|
}
|
|
288
360
|
if (op?.rotate !== undefined) {
|
|
289
361
|
const rotated = rotates({ mine }, from, op.rotate.nextPkHash);
|
|
290
|
-
return rotated === undefined ? undefined : answer({ rotated });
|
|
362
|
+
return rotated === undefined ? undefined : answer({ rotated }, claimDigest);
|
|
291
363
|
}
|
|
292
364
|
if (op?.release !== undefined) {
|
|
293
365
|
if (row === undefined) {
|
|
294
366
|
return undefined;
|
|
295
367
|
}
|
|
296
368
|
mine.write(`refs/${from}`, undefined);
|
|
297
|
-
return answer({ released: from });
|
|
369
|
+
return answer({ released: from }, claimDigest);
|
|
298
370
|
}
|
|
299
371
|
if (op?.acceptInvite !== undefined) {
|
|
300
|
-
const offer = Tools.digest(
|
|
372
|
+
const offer = Tools.digest(Bytes.unbase64(op.acceptInvite.proof));
|
|
301
373
|
const pending = mine.read(`refs/pending/${offer}`);
|
|
302
|
-
|
|
374
|
+
const now = stamp();
|
|
375
|
+
if (
|
|
376
|
+
pending === undefined ||
|
|
377
|
+
!(pending.count > 0) ||
|
|
378
|
+
(pending.expiresAt > 0 && now > pending.expiresAt)
|
|
379
|
+
) {
|
|
303
380
|
return undefined;
|
|
304
381
|
}
|
|
305
|
-
mine.write(`refs/pending/${offer}`, {
|
|
382
|
+
mine.write(`refs/pending/${offer}`, { ...pending, count: pending.count - 1 });
|
|
306
383
|
mine.write(`refs/${from}`, {
|
|
307
384
|
nextPkHash: op.acceptInvite.nextPkHash ?? null,
|
|
308
385
|
rights: pending.rights,
|
|
309
386
|
seen: seq ?? 0,
|
|
310
387
|
});
|
|
311
|
-
return answer({ referred: from, rights: pending.rights });
|
|
388
|
+
return answer({ referred: from, rights: pending.rights }, claimDigest);
|
|
312
389
|
}
|
|
313
390
|
if (op?.ask === undefined || program === undefined || !program.visible(rights)) {
|
|
314
391
|
return undefined;
|
|
@@ -320,7 +397,7 @@ const Being = ({ voice, address, deliver, modules, registry, span, vouched, watc
|
|
|
320
397
|
program.ask({
|
|
321
398
|
context: {
|
|
322
399
|
from: program.caller ? from : undefined,
|
|
323
|
-
modules: offered(modules, program, turn),
|
|
400
|
+
modules: offered(modules, program, turn, seat),
|
|
324
401
|
port: Port({ voice, mine: turn, deliver, fence, lanes, rights, stamp }),
|
|
325
402
|
registry: custodial(registry, fence),
|
|
326
403
|
rights,
|
|
@@ -329,7 +406,7 @@ const Being = ({ voice, address, deliver, modules, registry, span, vouched, watc
|
|
|
329
406
|
variables: op.ask.variables,
|
|
330
407
|
}),
|
|
331
408
|
).finally(fence.close);
|
|
332
|
-
return asked === undefined ? undefined : answer(asked);
|
|
409
|
+
return asked === undefined ? undefined : answer(asked, claimDigest);
|
|
333
410
|
} catch {
|
|
334
411
|
return undefined;
|
|
335
412
|
}
|
|
@@ -352,18 +429,18 @@ const spend = ({ memory, voice, address }) => {
|
|
|
352
429
|
return address;
|
|
353
430
|
};
|
|
354
431
|
|
|
355
|
-
const make = ({ memory, voice, address, program }) => {
|
|
432
|
+
const make = ({ memory, voice, address, program, cipher }) => {
|
|
356
433
|
const root = Tools.secret();
|
|
357
434
|
const put = (key, value) => {
|
|
358
435
|
const claim = { address, key, value, version: (memory.read(address, key)?.version ?? 0) + 1 };
|
|
359
436
|
return memory.write({ ...claim, sig: voice.sign(Tools.canon(claim)) });
|
|
360
437
|
};
|
|
361
|
-
put('chain/pk',
|
|
438
|
+
put('chain/pk', Bytes.hex(voice.pk));
|
|
362
439
|
put('chain/nextPkHash', voice.nextPkHash);
|
|
363
440
|
put('program', program ?? '({})');
|
|
364
441
|
put(`keyring/${Tools.address(voice.pk)}`, {
|
|
365
|
-
boxPk:
|
|
366
|
-
wrapped: Voice.sealTo(voice.boxPk,
|
|
442
|
+
boxPk: Bytes.base64(voice.boxPk),
|
|
443
|
+
wrapped: Voice.sealTo(voice.boxPk, Bytes.base64(root), cipher),
|
|
367
444
|
});
|
|
368
445
|
const written = program === undefined ? undefined : Program(program);
|
|
369
446
|
return Compartment({
|
|
@@ -372,6 +449,7 @@ const make = ({ memory, voice, address, program }) => {
|
|
|
372
449
|
address,
|
|
373
450
|
root,
|
|
374
451
|
classOf: (key) => written?.classOf(key) ?? coreClass(key),
|
|
452
|
+
cipher,
|
|
375
453
|
});
|
|
376
454
|
};
|
|
377
455
|
|
|
@@ -379,6 +457,7 @@ const Registry = ({
|
|
|
379
457
|
memory,
|
|
380
458
|
adminMine,
|
|
381
459
|
address,
|
|
460
|
+
cipher,
|
|
382
461
|
deliver,
|
|
383
462
|
modules,
|
|
384
463
|
modulesFor,
|
|
@@ -388,6 +467,7 @@ const Registry = ({
|
|
|
388
467
|
vouched,
|
|
389
468
|
attest,
|
|
390
469
|
watch,
|
|
470
|
+
extended,
|
|
391
471
|
}) => {
|
|
392
472
|
const live = new Map();
|
|
393
473
|
const stranded = (at, written) => {
|
|
@@ -416,6 +496,7 @@ const Registry = ({
|
|
|
416
496
|
);
|
|
417
497
|
const door = Being({
|
|
418
498
|
address: at,
|
|
499
|
+
cipher,
|
|
419
500
|
deliver,
|
|
420
501
|
modules: modulesFor(voice, declared),
|
|
421
502
|
span,
|
|
@@ -452,7 +533,7 @@ const Registry = ({
|
|
|
452
533
|
}
|
|
453
534
|
const voice = Voice();
|
|
454
535
|
const address = Tools.address(voice.pk);
|
|
455
|
-
const mine = make({ memory, voice, address, program });
|
|
536
|
+
const mine = make({ memory, voice, address, program, cipher });
|
|
456
537
|
for (const [key, value] of Object.entries(data)) {
|
|
457
538
|
if (mine.write(key, value) === undefined) {
|
|
458
539
|
forget(address);
|
|
@@ -488,7 +569,11 @@ const Registry = ({
|
|
|
488
569
|
if (stranded(address, written).length > 0) {
|
|
489
570
|
return undefined;
|
|
490
571
|
}
|
|
491
|
-
|
|
572
|
+
if (adminMine.write('program', program) === undefined) {
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
extended?.();
|
|
576
|
+
return address;
|
|
492
577
|
},
|
|
493
578
|
upgrade: ({ address: target, program }) => {
|
|
494
579
|
if (target === address) {
|
|
@@ -551,4 +636,4 @@ const Registry = ({
|
|
|
551
636
|
});
|
|
552
637
|
};
|
|
553
638
|
|
|
554
|
-
export { Being, Compartment, Registry, SUCCESSION, make, rootFor, spend };
|
|
639
|
+
export { Being, Compartment, Registry, SUCCESSION, make, rootFor, spend, successionOf };
|