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.
@@ -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. **A distributed system of sovereign beings**: an address is its
4
- own verification key, authority is a signature, and every relationship is
5
- one ref with declared rights — so a reference cannot be forged and no
6
- ambient authority exists anywhere. A ground is one sovereign process holding
7
- many beings — it creates them and it destroys them, and it exposes exactly
8
- one handler where every message for every being arrives. A being is one
9
- function closed over its own cells; the door asks by whose authority and
10
- refuses every other question. The lineage is the object-capability
11
- tradition — CapTP, the actor model — with one honest departure: the door
12
- verifies who is calling, and rights are declared per reference. Pure
13
- Crockford JavaScript — factory functions, closures for privacy, no `this`,
14
- no classes, frozen surfaces — on `node:` built-ins, plus `graphql` for the
15
- one thing that would be madness to hand-roll: the schema and its execution.
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
- `npm install nervur` — what ships is this source, byte for byte, one
20
- dependency (`graphql`). The package is a library and carries no command;
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, Ground, Keychain, Tools, Voice } from 'nervur';
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 = { cells: Cells({ file: 'ground.json' }), keychain: Keychain(secret) };
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
- return secret === undefined ? cell.value : Tools.unseal(secret, cell.value);
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
- return put(key, secret === undefined ? plain : Tools.seal(secret, plain));
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 referable = (mine, key, plain, program) => {
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 { ...plain, rights };
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 : Buffer.from(opened, 'base64');
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
- return deliver(
148
- Tools.envelope(handle === undefined ? voice : Voice.revive(handle.face), to, op, stamp()),
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 = ({ voice, address, deliver, modules, registry, span, vouched, watch }) => {
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
- address,
279
- digest: program?.digest,
280
- rights,
281
- sdl: program?.sdlFor(rights),
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(Buffer.from(op.acceptInvite.proof, 'base64'));
372
+ const offer = Tools.digest(Bytes.unbase64(op.acceptInvite.proof));
301
373
  const pending = mine.read(`refs/pending/${offer}`);
302
- if (pending === undefined || pending.spent) {
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}`, { spent: true });
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', Buffer.from(voice.pk).toString('hex'));
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: Buffer.from(voice.boxPk).toString('base64'),
366
- wrapped: Voice.sealTo(voice.boxPk, root.toString('base64')),
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
- return adminMine.write('program', program) === undefined ? undefined : address;
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 };