nervur 0.8.2 → 0.10.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/README.md CHANGED
@@ -49,13 +49,20 @@ const DIARY = `({
49
49
  })`;
50
50
 
51
51
  const secret = Buffer.from(process.env.NERVUR_SECRET, 'base64'); // 32 bytes, minted once with Tools.secret()
52
- const ground = Ground({ cells: Cells({ file: 'ground.json' }), keychain: Keychain(secret) });
52
+ const shelf = { cells: Cells({ file: 'ground.json' }), keychain: Keychain(secret) };
53
53
 
54
+ const heir = Voice(); // keep this one somewhere the machine is not
55
+ const self = Voice(Tools.digest(heir.pk)); // and hand the ground only this
56
+ Ground(shelf).init(self.hand()); // once, ever — a fresh ground offers nothing else
57
+
58
+ const ground = Ground(shelf); // now it stands, and answers
54
59
  const me = Voice(); // keep it: writeFileSync of me.pack(), or it dies with the process
55
- const serve = ground.serve(me);
56
- const stranger = await serve.pointer('Ground');
57
- await stranger.init({ address: Tools.address(me.pk) }); // first boot only — once, ever
60
+ await ground
61
+ .serve(self)
62
+ .pointer('Ground')
63
+ .then((it) => it.invite({ address: Tools.address(me.pk) }));
58
64
 
65
+ const serve = ground.serve(me);
59
66
  const admin = await serve.pointer('Ground'); // a pointer is your rights at mint — re-mint after they change
60
67
  const diary = await admin.create({
61
68
  program: DIARY,
@@ -68,8 +75,8 @@ console.log(await mine.pages()); // [ 'first entry' ]
68
75
 
69
76
  The rights enum here is the default (`SELF MEMBER STRANGER`); declare your
70
77
  own `enum Rights` in the interface to replace it. Reboot: construct the
71
- same `Ground` from the same two arguments — it wakes remembering; `init`
72
- refuses, because the ref survived in the cells.
78
+ same `Ground` from the same two arguments — it wakes remembering, and
79
+ offers no `init`, because the identity survived in the cells.
73
80
 
74
81
  **The wire, when a caller is remote.** An envelope is plain JSON:
75
82
 
@@ -118,11 +125,14 @@ three things while hiding everything it is made of:
118
125
  nothing else — the library never opens a socket.
119
126
  - `serve(voice)` — authority established once, and every face inherits it.
120
127
 
121
- A fresh ground holds no reference, so whoever reaches it first takes the
122
- first one: `pointer('Ground').init({ address })` writes the first MEMBER ref
123
- on the admin being — the same ref row as any other — exactly once, refusing
124
- everyone after. That window is what running-is-custody looks like from the
125
- outside, and it is stated rather than hidden.
128
+ A fresh ground has no identity, and no door for anyone to reach first: it
129
+ offers `init(hand)` and nothing else — no `address`, no `receive`, no
130
+ `serve`. Custody mints the voice off the machine, keeps the heir, and hands
131
+ over `hand()` alone; a pack carrying its own successor is refused. The act
132
+ is spent the moment it lands, and only `rotate(hand)` moves that voice
133
+ after. Both are custody's own calls on the shelf, never doors — which is
134
+ what running-is-custody looks like from the outside, stated rather than
135
+ hidden.
126
136
 
127
137
  **`serve` is the client, and it has four faces of one door.** `pointer(at)`
128
138
  is a plain object with one function per field, generated from the schema that
@@ -194,18 +204,25 @@ silence, and silence is something local delivery never answers with.
194
204
  - [pointer.mjs](pointer.mjs) — `Pointer` and `Mcp`, both read off an SDL, so
195
205
  no client is ever written by hand for a program.
196
206
  - [client.mjs](client.mjs) — the bare envelope client, usable on its own.
197
- Signs as its voice and verifies every reply against `pkOf(to)`.
207
+ Signs as its voice, and verifies every reply from the address alone:
208
+ `pkOf(to)` is the key the being was born with, and each handover the reply
209
+ carries in `succession` is checked against the key before it, so a being
210
+ that has rotated is still provably itself to a caller that has never met
211
+ it. A chain that breaks anywhere is a dropped answer.
198
212
  - [being.mjs](being.mjs) — the being. `Compartment` seals a cell by its
199
213
  class, `Port` carries authority both ways, `Being` is the door — prove,
200
214
  read rights, execute, sign — and `Registry` creates, takes custody of,
201
215
  activates and destroys, reachable only through the admin being.
202
216
  - [admin.mjs](admin.mjs) — the administrative program every ground installs
203
217
  for itself, aliased `Ground`: census, create, activate, destroy, upgrade,
204
- alias, attest. Upgrade is custody moving the one cell that was always the
205
- core's to move: the program is replaced in place — address, cells, refs
206
- and chain surviving — refused whole when a cell the being holds would go
207
- undeclared, and visible to every peer because `describe` answers the
208
- program's digest.
218
+ alias, attest, and behind `SELF` alone invite, drop and extend. Upgrade is
219
+ custody moving the one cell that was always the core's to move: the
220
+ program is replaced in place — address, cells, refs and chain surviving —
221
+ refused whole when a cell the being holds would go undeclared, and visible
222
+ to every peer because `describe` answers the program's digest. It refuses
223
+ the ground's own address; that program moves only through `extend`, which
224
+ takes the custom part alone and lets the core compose it with the fixed
225
+ administration.
209
226
  - [ground.mjs](ground.mjs) — the sovereign ground, its one handler and
210
227
  `serve`.
211
228
  - [index.mjs](index.mjs) — the surface an adopter imports.
@@ -293,9 +310,10 @@ other name passes through untouched as a module for programs.
293
310
  by address, so that `memory` offers the same interface under the same
294
311
  digest on every ground and a pretender is refused. A digest pins shape;
295
312
  what the words mean is the contract author's prose to state.
296
- - **`blueprints` — optional.** `blueprints.ground()` replaces the default
297
- admin blueprint, which is how a host customises what administering this
298
- ground even means.
313
+ - **`blueprints` — optional.** `blueprints.ground()` seeds the
314
+ administration's custom part at install. After that the part moves only
315
+ through the `extend` door, so a host that keeps its blueprint in a file
316
+ and hands it over with the CLI never needs this slot at all.
299
317
  - **`watch` — optional.** The custodian's eye: a function called at every
300
318
  door decision with one frozen fact, `{ to, from, kind, outcome }`, and
301
319
  the outcomes are a closed set — `no-door`, `unproven`, `replayed`,
@@ -305,13 +323,14 @@ other name passes through untouched as a module for programs.
305
323
  costs nothing. Tracing, metrics and logs are a host package built on
306
324
  this, never the library's.
307
325
 
308
- Boot, in order: on first boot the ground installs itself — mints its admin
309
- voice, writes the admin being, hands the packed voice to the keychain to
310
- seal into one cell. Every boot after: the keychain unseals, the voice
311
- revives, every being in custody is announced, the ground answers. The first voice to call `init` names the first
312
- MEMBER — once, and never again. `Clock` and `Entropy` are not floor: they
313
- are ordinary modules a program declares, pinnable so a program never
314
- reaches for the wall.
326
+ Boot, in order: a ground over an empty shelf offers `init(hand)` alone.
327
+ Given the hand, it writes the admin being and seals that voice into one cell
328
+ under the keychain. Every boot after: the keychain unseals, the voice
329
+ revives, every being in custody is announced, the ground answers.
330
+ `rotate(hand)` is the only thing that moves that voice, and it certifies the
331
+ handover so every caller follows the ground from its address. `Clock` and
332
+ `Entropy` are not floor: they are ordinary modules a program declares,
333
+ pinnable so a program never reaches for the wall.
315
334
 
316
335
  ## Extending the administration
317
336
 
@@ -320,9 +339,11 @@ context holds the registry — so a door that must create, upgrade or destroy
320
339
  beings belongs on it and nowhere else. Adopters extend it; nobody replaces
321
340
  it.
322
341
 
323
- Pass an extension through the `blueprints` module and the ground composes it
324
- with the standard administration. Write it as an ordinary blueprint whose
325
- schema **extends** the roots:
342
+ Hand the custom part to the `extend` door — `SELF`'s alone — and the core
343
+ composes it with the standard administration. What you supply is the part by
344
+ itself, never the whole program, so the acts this ground owes can never go
345
+ missing. The same door replaces the part and removes it. Write it as an
346
+ ordinary blueprint whose schema **extends** the roots:
326
347
 
327
348
  ```js
328
349
  const cases = `({
@@ -345,18 +366,22 @@ const cases = `({
345
366
  },
346
367
  })`;
347
368
 
348
- const ground = Ground({ cells, keychain, blueprints: { ground: () => cases } });
369
+ await ground
370
+ .serve(self)
371
+ .pointer('Ground')
372
+ .then((it) => it.extend({ program: cases }));
349
373
  ```
350
374
 
351
375
  The composed program is one blueprint with one digest, so `describe` answers
352
376
  what this ground's administration actually is, and an outsider can pin it.
353
377
 
354
- Three rules hold, and each refuses the whole ground rather than bending:
378
+ Three rules hold, and each refuses the whole extension rather than bending:
355
379
 
356
380
  1. **Standard door names are reserved.** Declare `create`, `census`,
357
- `upgrade`, `destroy`, `alias`, `attest`, `opened` or `init` and no ground
358
- rises. You cannot change what a standard door means — which is why a
359
- stranger may trust that if `create` answers, it created.
381
+ `upgrade`, `destroy`, `alias`, `attest`, `opened`, `invite`, `drop` or
382
+ `extend` and the part is refused. You cannot change what a standard door
383
+ means — which is why a stranger may trust that if `create` answers, it
384
+ created.
360
385
  2. **Add only.** Use `extend type Query` / `extend type Mutation`; cell
361
386
  names, module slots and `needs.config` may not collide with the
362
387
  administration's own.
package/admin.mjs CHANGED
@@ -13,7 +13,9 @@ const ADMIN = `({
13
13
  opened: Boolean @rights(is: [SELF, MEMBER, STRANGER])
14
14
  }
15
15
  type Mutation {
16
- init(address: String!, nextPkHash: String): Boolean @rights(is: [SELF, MEMBER, STRANGER])
16
+ invite(address: String!, nextPkHash: String, rights: String): Boolean @rights(is: [SELF, MEMBER])
17
+ drop(address: String!): Boolean @rights(is: [SELF, MEMBER])
18
+ extend(program: String): Boolean @rights(is: [SELF])
17
19
  create(program: String!, refs: [RefInput!], data: String, config: String): String
18
20
  @rights(is: [SELF, MEMBER])
19
21
  activate(address: String!): String @rights(is: [SELF, MEMBER])
@@ -37,13 +39,19 @@ const ADMIN = `({
37
39
  opened: (_, __, { modules }) => modules.memory.list('refs/').length > 0,
38
40
  },
39
41
  Mutation: {
40
- init: (_, { address, nextPkHash }, { modules }) =>
41
- modules.memory.list('refs/').length > 0
42
- ? false
43
- : modules.memory.write('refs/' + address, {
44
- nextPkHash: nextPkHash ?? null,
45
- rights: 'MEMBER',
46
- }) !== undefined,
42
+ invite: (_, { address, nextPkHash, rights }, { modules }) =>
43
+ modules.memory.write('refs/' + address, {
44
+ nextPkHash: nextPkHash ?? null,
45
+ rights: rights ?? 'MEMBER',
46
+ }) !== undefined,
47
+ drop: (_, { address }, { modules }) => {
48
+ if (modules.memory.read('refs/' + address) === undefined) {
49
+ return false;
50
+ }
51
+ modules.memory.write('refs/' + address, undefined);
52
+ return modules.memory.read('refs/' + address) === undefined;
53
+ },
54
+ extend: (_, { program }, { registry }) => registry.extend(program) !== undefined,
47
55
  create: (_, { program, refs, data, config }, { registry }) =>
48
56
  registry.create({
49
57
  config: config === undefined ? undefined : JSON.parse(config),
package/being.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { OPEN, STRUCTURAL, coreClass } from './blueprint.mjs';
1
+ import { ADMIN } from './admin.mjs';
2
+ import { OPEN, STRUCTURAL, composed, coreClass } from './blueprint.mjs';
2
3
  import { Program } from './program.mjs';
3
4
  import { MEASURE, Stamp, Tools } from './tools.mjs';
4
5
  import { Voice } from './voice.mjs';
@@ -150,6 +151,15 @@ const rotates = ({ mine }, from, nextPkHash) => {
150
151
  return from;
151
152
  };
152
153
 
154
+ const SUCCESSION = 'chain/succession/';
155
+
156
+ const successionOf = (memory, address) =>
157
+ memory
158
+ .list(address, SUCCESSION)
159
+ .map((key) => memory.read(address, key)?.value)
160
+ .filter((one) => one !== undefined)
161
+ .sort((one, other) => one.v - other.v);
162
+
153
163
  const Being = ({ voice, address, deliver, modules, registry, vouched, watch }) => {
154
164
  const memory = modules.memory;
155
165
  const root = rootFor({ memory, voice, address });
@@ -162,9 +172,14 @@ const Being = ({ voice, address, deliver, modules, registry, vouched, watch }) =
162
172
  root,
163
173
  classOf: (key) => program?.classOf(key) ?? coreClass(key),
164
174
  });
175
+ const succession = successionOf(memory, address);
165
176
  const answer = (spoken) => {
166
177
  const body = Tools.canon(spoken);
167
- return body.length > MEASURE ? undefined : { body: spoken, sig: voice.sign(body) };
178
+ if (body.length > MEASURE) {
179
+ return undefined;
180
+ }
181
+ const signed = { body: spoken, sig: voice.sign(body) };
182
+ return succession.length === 0 ? signed : { ...signed, succession };
168
183
  };
169
184
  const note = (envelope, outcome) => {
170
185
  try {
@@ -307,6 +322,11 @@ const Registry = ({
307
322
  watch,
308
323
  }) => {
309
324
  const live = new Map();
325
+ const orphans = (at, written) =>
326
+ memory
327
+ .list(at, '')
328
+ .filter((key) => memory.read(at, key)?.value !== null)
329
+ .filter((key) => written.classOf(key) === undefined);
310
330
  const activate = (at) => {
311
331
  if (live.has(at)) {
312
332
  return live.get(at);
@@ -316,10 +336,13 @@ const Registry = ({
316
336
  if (voice === undefined) {
317
337
  return undefined;
318
338
  }
339
+ const declared = (Program(memory.read(at, 'program')?.value)?.modules ?? []).map(
340
+ (one) => one.module,
341
+ );
319
342
  const door = Being({
320
343
  address: at,
321
344
  deliver,
322
- modules: modulesFor(voice),
345
+ modules: modulesFor(voice, declared),
323
346
  vouched,
324
347
  voice,
325
348
  watch,
@@ -335,7 +358,7 @@ const Registry = ({
335
358
  if (program !== undefined && written === undefined) {
336
359
  return undefined;
337
360
  }
338
- const missing = (written?.modules ?? []).filter((one) => modules[one.module] === undefined);
361
+ const missing = (written?.modules ?? []).filter((one) => !modules.has(one.module));
339
362
  if (missing.length > 0) {
340
363
  return undefined;
341
364
  }
@@ -377,20 +400,30 @@ const Registry = ({
377
400
  return address;
378
401
  },
379
402
  deactivate: (at) => live.delete(at),
380
- upgrade: ({ address: target, program }) => {
403
+ extend: (source) => {
404
+ const program = source === undefined || source === null ? ADMIN : composed(ADMIN, source);
381
405
  const written = program === undefined ? undefined : Program(program);
382
406
  if (written === undefined) {
383
407
  return undefined;
384
408
  }
385
- const orphaned = memory
386
- .list(target, '')
387
- .filter((key) => memory.read(target, key)?.value !== null)
388
- .filter((key) => written.classOf(key) === undefined);
389
- if (orphaned.length > 0) {
409
+ if (written.modules.some((one) => !modules.has(one.module))) {
410
+ return undefined;
411
+ }
412
+ if (orphans(address, written).length > 0) {
390
413
  return undefined;
391
414
  }
415
+ return adminMine.write('program', program) === undefined ? undefined : address;
416
+ },
417
+ upgrade: ({ address: target, program }) => {
392
418
  if (target === address) {
393
- return adminMine.write('program', program) === undefined ? undefined : target;
419
+ return undefined;
420
+ }
421
+ const written = program === undefined ? undefined : Program(program);
422
+ if (written === undefined) {
423
+ return undefined;
424
+ }
425
+ if (orphans(target, written).length > 0) {
426
+ return undefined;
394
427
  }
395
428
  const held = adminMine.read(`identity/custody/${target}`);
396
429
  const voice = held === undefined ? undefined : Voice.revive(held);
@@ -440,4 +473,4 @@ const Registry = ({
440
473
  });
441
474
  };
442
475
 
443
- export { Being, Compartment, Registry, make, rootFor };
476
+ export { Being, Compartment, Registry, SUCCESSION, make, rootFor };
package/client.mjs CHANGED
@@ -1,17 +1,41 @@
1
1
  import { Stamp, Tools } from './tools.mjs';
2
2
 
3
+ const KEY = /^[0-9a-f]{64}$/;
4
+
5
+ const speaking = (born, at, succession) => {
6
+ if (succession === undefined) {
7
+ return born;
8
+ }
9
+ if (!Array.isArray(succession)) {
10
+ return undefined;
11
+ }
12
+ let pk = born;
13
+ for (const step of succession) {
14
+ if (!KEY.test(step?.next ?? '')) {
15
+ return undefined;
16
+ }
17
+ if (!Tools.verifies(pk, Tools.canon({ at, next: step.next, v: step.v }), step.sig)) {
18
+ return undefined;
19
+ }
20
+ pk = Buffer.from(step.next, 'hex');
21
+ }
22
+ return pk;
23
+ };
24
+
3
25
  const Client = ({ voice, deliver, resolve = (one) => one }) => {
4
26
  const stamp = Stamp();
5
27
  const lanes = new Map();
6
28
  const send = async (to, op) => {
7
29
  const at = resolve(to);
8
- const answering = at === undefined ? null : Tools.pkOf(at);
9
- if (answering === null) {
30
+ const born = at === undefined ? null : Tools.pkOf(at);
31
+ if (born === null) {
10
32
  return undefined;
11
33
  }
12
34
  const turn = (lanes.get(at) ?? Promise.resolve()).then(async () => {
13
35
  const reply = await deliver(Tools.envelope(voice, at, op, stamp()));
36
+ const answering = speaking(born, at, reply?.succession);
14
37
  return reply?.body === undefined ||
38
+ answering === undefined ||
15
39
  !Tools.verifies(answering, Tools.canon(reply.body), reply.sig)
16
40
  ? undefined
17
41
  : reply;
package/ground.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ADMIN } from './admin.mjs';
2
- import { Being, Compartment, Registry, make, rootFor } from './being.mjs';
2
+ import { Being, Compartment, Registry, SUCCESSION, make, rootFor } from './being.mjs';
3
3
  import { Blueprint, composed, coreClass } from './blueprint.mjs';
4
4
  import { Client } from './client.mjs';
5
5
  import { Contracts } from './contract.mjs';
@@ -13,16 +13,66 @@ const GROUND = 'Ground';
13
13
 
14
14
  const FLOOR = ['blueprints', 'cells', 'contracts', 'keychain', 'paths', 'watch', 'wire'];
15
15
 
16
- const install = ({ cells, keychain, program }) => {
16
+ const install = ({ cells, keychain, program, bundle }) => {
17
+ if (bundle?.next !== undefined) {
18
+ return undefined;
19
+ }
20
+ const voice = Voice.revive(bundle ?? {});
21
+ if (voice === undefined) {
22
+ return undefined;
23
+ }
17
24
  const memory = Memory(cells);
18
- const voice = Voice();
19
25
  const address = Tools.address(voice.pk);
20
26
  const sealed = keychain.seal(voice.pack());
21
27
  if (sealed === undefined) {
22
- return;
28
+ return undefined;
23
29
  }
24
30
  make({ memory, voice, address, program });
25
31
  cells.put('ground/admin', { address, sealed });
32
+ return address;
33
+ };
34
+
35
+ const succeeds = ({ adminMine, adminVoice, address, bundle, cells, keychain, memory }) => {
36
+ if (bundle?.next !== undefined) {
37
+ return undefined;
38
+ }
39
+ const heir = Voice.revive(bundle ?? {});
40
+ const committed = memory.read(address, 'chain/nextPkHash');
41
+ if (heir === undefined || committed?.value !== Tools.digest(heir.pk)) {
42
+ return undefined;
43
+ }
44
+ const root = rootFor({ memory, voice: adminVoice, address });
45
+ const sealed = root === undefined ? undefined : keychain.seal(heir.pack());
46
+ if (sealed === undefined) {
47
+ return undefined;
48
+ }
49
+ const rewrapped = adminMine.write(`keyring/${Tools.address(heir.pk)}`, {
50
+ boxPk: Buffer.from(heir.boxPk).toString('base64'),
51
+ wrapped: Voice.sealTo(heir.boxPk, root.toString('base64')),
52
+ });
53
+ if (rewrapped === undefined) {
54
+ return undefined;
55
+ }
56
+ const version = committed.version + 1;
57
+ const handover = { at: address, next: Buffer.from(heir.pk).toString('hex'), v: version };
58
+ const certified = adminMine.write(`${SUCCESSION}${version}`, {
59
+ ...handover,
60
+ sig: adminVoice.sign(Tools.canon(handover)),
61
+ });
62
+ if (certified === undefined) {
63
+ return undefined;
64
+ }
65
+ const claim = {
66
+ address,
67
+ nextPkHash: heir.nextPkHash,
68
+ pk: Buffer.from(heir.pk).toString('hex'),
69
+ version,
70
+ };
71
+ if (memory.replace({ ...claim, sig: heir.sign(Tools.canon(claim)) }) === undefined) {
72
+ return undefined;
73
+ }
74
+ cells.put('ground/admin', { address, sealed });
75
+ return Tools.address(heir.pk);
26
76
  };
27
77
 
28
78
  const Ground = (modules = {}) => {
@@ -43,15 +93,15 @@ const Ground = (modules = {}) => {
43
93
 
44
94
  const extension = modules.blueprints?.ground();
45
95
  const program = extension === undefined ? ADMIN : composed(ADMIN, extension);
46
- if (program === undefined || Blueprint(program)?.digest === undefined) {
96
+ const blueprint = program === undefined ? undefined : Blueprint(program);
97
+ if (blueprint?.digest === undefined) {
47
98
  return undefined;
48
99
  }
49
- if (cells.peek('ground/admin') === undefined) {
50
- install({ cells, keychain, program });
51
- }
52
100
  const installed = cells.peek('ground/admin');
53
101
  if (installed === undefined) {
54
- return undefined;
102
+ return Object.freeze({
103
+ init: (bundle) => install({ cells, keychain, program, bundle }),
104
+ });
55
105
  }
56
106
  const bundle = keychain.unseal(installed.sealed);
57
107
  const adminVoice = bundle === undefined ? undefined : Voice.revive(bundle);
@@ -92,18 +142,24 @@ const Ground = (modules = {}) => {
92
142
  typeof one?.scope === 'function' ? one.scope(address) : one,
93
143
  ]),
94
144
  );
95
- const modulesFor = (voice) =>
145
+ const declaredOnly = (obj, declared) =>
146
+ Object.fromEntries(Object.entries(obj).filter(([name]) => declared.includes(name)));
147
+ const modulesFor = (voice, declared = []) =>
96
148
  Object.freeze(
97
149
  introduced(
98
150
  {
99
- ...extra,
100
- ...boundFor(voice),
151
+ ...declaredOnly(extra, declared),
152
+ ...declaredOnly(boundFor(voice), declared),
101
153
  memory,
102
154
  },
103
155
  Tools.address(voice.pk),
104
156
  ),
105
157
  );
106
- const adminModules = modulesFor(adminVoice);
158
+ const offeredNames = new Set([...Object.keys(extra), ...bound.map(([name]) => name), 'memory']);
159
+ const adminDeclared = () =>
160
+ (Program(memory.read(address, 'program')?.value)?.modules ?? blueprint.modules).map(
161
+ (one) => one.module,
162
+ );
107
163
 
108
164
  const adminMine = Compartment({
109
165
  memory,
@@ -136,8 +192,11 @@ const Ground = (modules = {}) => {
136
192
 
137
193
  const attest = async (name) => {
138
194
  const one = extra[name];
139
- if (one?.at === undefined) {
140
- return adminModules[name] !== undefined;
195
+ if (one === undefined) {
196
+ return false;
197
+ }
198
+ if (one.at === undefined) {
199
+ return null;
141
200
  }
142
201
  const seen = await deliver(Tools.envelope(adminVoice, one.at, { describe: {} }, stamp()));
143
202
  return seen?.body?.sdl === undefined
@@ -150,7 +209,7 @@ const Ground = (modules = {}) => {
150
209
  adminMine,
151
210
  address,
152
211
  deliver: (envelope) => deliver(envelope),
153
- modules: adminModules,
212
+ modules: offeredNames,
154
213
  modulesFor,
155
214
  announce,
156
215
  forget,
@@ -162,7 +221,7 @@ const Ground = (modules = {}) => {
162
221
  Being({
163
222
  address,
164
223
  deliver: (envelope) => deliver(envelope),
165
- modules: adminModules,
224
+ modules: modulesFor(adminVoice, adminDeclared()),
166
225
  registry,
167
226
  vouched,
168
227
  voice: adminVoice,
@@ -184,6 +243,8 @@ const Ground = (modules = {}) => {
184
243
  return Object.freeze({
185
244
  address,
186
245
  receive,
246
+ rotate: (bundle) =>
247
+ succeeds({ adminMine, adminVoice, address, bundle, cells, keychain, memory }),
187
248
  serve: (voice) => {
188
249
  const client = Client({ voice, deliver, resolve });
189
250
  const sdlOf = async (at) => (await client.describe(at))?.body?.sdl;
package/index.mjs CHANGED
@@ -2,7 +2,7 @@ export { ADMIN } from './admin.mjs';
2
2
  export { Blueprint, CLASSES, CORE_ONLY, OPEN, STRUCTURAL, coreClass, reads } from './blueprint.mjs';
3
3
  export { Client } from './client.mjs';
4
4
  export { Contract, Contracts } from './contract.mjs';
5
- export { GROUND, Ground } from './ground.mjs';
5
+ export { FLOOR, GROUND, Ground } from './ground.mjs';
6
6
  export { Cells, Clock, Dns, Entropy, Keychain, Memory, Paths, Wire } from './modules.mjs';
7
7
  export { Mcp, Pointer } from './pointer.mjs';
8
8
  export { Program } from './program.mjs';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nervur",
3
- "version": "0.8.2",
4
- "description": "A distributed system of sovereign beings \u2014 one door, one question: by whose authority.",
3
+ "version": "0.10.0",
4
+ "description": "A distributed system of sovereign beings — one door, one question: by whose authority.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Razvan Gherghina",
7
7
  "homepage": "https://nervur.org",
package/voice.mjs CHANGED
@@ -33,10 +33,12 @@ const agreed = (privateKey, rawPub) =>
33
33
  )
34
34
  .digest();
35
35
 
36
- const voiced = ({ present, next, box }) =>
36
+ const committed = /^[0-9a-f]{64}$/;
37
+
38
+ const voiced = ({ present, next, box, nextPkHash }) =>
37
39
  Object.freeze({
38
40
  boxPk: rawPk(box),
39
- nextPkHash: Tools.digest(rawPk(next)),
41
+ nextPkHash: next === undefined ? nextPkHash : Tools.digest(rawPk(next)),
40
42
  open: (wrapped) => {
41
43
  try {
42
44
  const [ephemeral, sealed] = wrapped.split('.');
@@ -45,28 +47,57 @@ const voiced = ({ present, next, box }) =>
45
47
  return undefined;
46
48
  }
47
49
  },
48
- pack: () => ({ box: packed(box), next: packed(next), present: packed(present) }),
50
+ hand: () => ({
51
+ box: packed(box),
52
+ nextPkHash: next === undefined ? nextPkHash : Tools.digest(rawPk(next)),
53
+ present: packed(present),
54
+ }),
55
+ pack: () =>
56
+ next === undefined
57
+ ? { box: packed(box), nextPkHash, present: packed(present) }
58
+ : { box: packed(box), next: packed(next), present: packed(present) },
49
59
  pk: rawPk(present),
50
60
  sign: (bytes) => sign(null, bytes, present).toString('base64'),
51
61
  succeed: () =>
52
- voiced({
53
- box: generateKeyPairSync('x25519').privateKey,
54
- next: generateKeyPairSync('ed25519').privateKey,
55
- present: next,
56
- }),
62
+ next === undefined
63
+ ? undefined
64
+ : voiced({
65
+ box: generateKeyPairSync('x25519').privateKey,
66
+ next: generateKeyPairSync('ed25519').privateKey,
67
+ present: next,
68
+ }),
57
69
  });
58
70
 
59
71
  const Voice = Object.freeze(
60
72
  Object.assign(
61
- () =>
62
- voiced({
63
- box: generateKeyPairSync('x25519').privateKey,
64
- next: generateKeyPairSync('ed25519').privateKey,
65
- present: generateKeyPairSync('ed25519').privateKey,
66
- }),
73
+ (nextPkHash) => {
74
+ if (nextPkHash === undefined) {
75
+ return voiced({
76
+ box: generateKeyPairSync('x25519').privateKey,
77
+ next: generateKeyPairSync('ed25519').privateKey,
78
+ present: generateKeyPairSync('ed25519').privateKey,
79
+ });
80
+ }
81
+ return committed.test(nextPkHash)
82
+ ? voiced({
83
+ box: generateKeyPairSync('x25519').privateKey,
84
+ nextPkHash,
85
+ present: generateKeyPairSync('ed25519').privateKey,
86
+ })
87
+ : undefined;
88
+ },
67
89
  {
68
90
  revive: (bundle) => {
69
91
  try {
92
+ if (bundle.next === undefined) {
93
+ return committed.test(bundle.nextPkHash)
94
+ ? voiced({
95
+ box: unpacked(bundle.box),
96
+ nextPkHash: bundle.nextPkHash,
97
+ present: unpacked(bundle.present),
98
+ })
99
+ : undefined;
100
+ }
70
101
  return voiced({
71
102
  box: unpacked(bundle.box),
72
103
  next: unpacked(bundle.next),