nervur 0.8.1 → 0.9.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 +60 -35
- package/admin.mjs +16 -8
- package/being.mjs +40 -10
- package/blueprint.mjs +11 -8
- package/client.mjs +26 -2
- package/ground.mjs +59 -8
- package/package.json +2 -2
- package/voice.mjs +45 -14
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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
72
|
-
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
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
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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()`
|
|
297
|
-
|
|
298
|
-
|
|
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:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
revives, every being in custody is announced, the ground answers.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
324
|
-
with the standard administration.
|
|
325
|
-
|
|
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
|
-
|
|
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
|
|
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`
|
|
358
|
-
|
|
359
|
-
stranger may trust that if `create` answers, it
|
|
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
|
-
|
|
16
|
+
invite(address: String!, nextPkHash: String, rights: String): Boolean @rights(is: [SELF])
|
|
17
|
+
drop(address: String!): Boolean @rights(is: [SELF])
|
|
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
|
-
|
|
41
|
-
modules.memory.
|
|
42
|
-
|
|
43
|
-
:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 {
|
|
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
|
-
|
|
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);
|
|
@@ -377,20 +397,30 @@ const Registry = ({
|
|
|
377
397
|
return address;
|
|
378
398
|
},
|
|
379
399
|
deactivate: (at) => live.delete(at),
|
|
380
|
-
|
|
400
|
+
extend: (source) => {
|
|
401
|
+
const program = source === undefined || source === null ? ADMIN : composed(ADMIN, source);
|
|
381
402
|
const written = program === undefined ? undefined : Program(program);
|
|
382
403
|
if (written === undefined) {
|
|
383
404
|
return undefined;
|
|
384
405
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (orphaned.length > 0) {
|
|
406
|
+
if (written.modules.some((one) => modules[one.module] === undefined)) {
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
if (orphans(address, written).length > 0) {
|
|
390
410
|
return undefined;
|
|
391
411
|
}
|
|
412
|
+
return adminMine.write('program', program) === undefined ? undefined : address;
|
|
413
|
+
},
|
|
414
|
+
upgrade: ({ address: target, program }) => {
|
|
392
415
|
if (target === address) {
|
|
393
|
-
return
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
const written = program === undefined ? undefined : Program(program);
|
|
419
|
+
if (written === undefined) {
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
if (orphans(target, written).length > 0) {
|
|
423
|
+
return undefined;
|
|
394
424
|
}
|
|
395
425
|
const held = adminMine.read(`identity/custody/${target}`);
|
|
396
426
|
const voice = held === undefined ? undefined : Voice.revive(held);
|
|
@@ -440,4 +470,4 @@ const Registry = ({
|
|
|
440
470
|
});
|
|
441
471
|
};
|
|
442
472
|
|
|
443
|
-
export { Being, Compartment, Registry, make, rootFor };
|
|
473
|
+
export { Being, Compartment, Registry, SUCCESSION, make, rootFor };
|
package/blueprint.mjs
CHANGED
|
@@ -80,6 +80,9 @@ const RESERVED = ['eval', 'import', '__proto__', 'prototype', 'Math.random'];
|
|
|
80
80
|
|
|
81
81
|
const SHADOWED = FORBIDDEN.map(([name]) => name).filter((name) => !RESERVED.includes(name));
|
|
82
82
|
|
|
83
|
+
const doorHolder = (one) =>
|
|
84
|
+
one.kind === Kind.OBJECT_TYPE_DEFINITION || one.kind === Kind.OBJECT_TYPE_EXTENSION;
|
|
85
|
+
|
|
83
86
|
const RIGHTS_DIRECTIVE = 'directive @rights(is: [Rights!]!) on FIELD_DEFINITION';
|
|
84
87
|
|
|
85
88
|
const DEFAULT_RIGHTS = `enum Rights { SELF MEMBER STRANGER }`;
|
|
@@ -250,10 +253,10 @@ const Blueprint = (source) => {
|
|
|
250
253
|
];
|
|
251
254
|
|
|
252
255
|
const admits = (typeName, fieldName, held) => {
|
|
253
|
-
const
|
|
254
|
-
(one) => one
|
|
255
|
-
|
|
256
|
-
|
|
256
|
+
const field = document.definitions
|
|
257
|
+
.filter((one) => doorHolder(one) && one.name.value === typeName)
|
|
258
|
+
.flatMap((one) => one.fields ?? [])
|
|
259
|
+
.find((one) => one.name.value === fieldName);
|
|
257
260
|
const allowed = field === undefined ? undefined : rightsOf(field);
|
|
258
261
|
return allowed === undefined ? true : allowed.includes(held);
|
|
259
262
|
};
|
|
@@ -273,7 +276,7 @@ const Blueprint = (source) => {
|
|
|
273
276
|
schema.getSubscriptionType()?.name,
|
|
274
277
|
].filter((one) => one !== undefined);
|
|
275
278
|
const undeclared = document.definitions
|
|
276
|
-
.filter((one) => one
|
|
279
|
+
.filter((one) => doorHolder(one) && roots.includes(one.name.value))
|
|
277
280
|
.flatMap((one) => one.fields ?? [])
|
|
278
281
|
.filter((one) => {
|
|
279
282
|
const allowed = rightsOf(one);
|
|
@@ -321,17 +324,17 @@ const Blueprint = (source) => {
|
|
|
321
324
|
...document,
|
|
322
325
|
definitions: document.definitions
|
|
323
326
|
.map((one) =>
|
|
324
|
-
one
|
|
327
|
+
doorHolder(one)
|
|
325
328
|
? {
|
|
326
329
|
...one,
|
|
327
|
-
fields: one.fields.filter((field) => {
|
|
330
|
+
fields: (one.fields ?? []).filter((field) => {
|
|
328
331
|
const allowed = rightsOf(field);
|
|
329
332
|
return allowed === undefined || allowed.includes(held);
|
|
330
333
|
}),
|
|
331
334
|
}
|
|
332
335
|
: one,
|
|
333
336
|
)
|
|
334
|
-
.filter((one) => one
|
|
337
|
+
.filter((one) => !doorHolder(one) || one.fields.length > 0),
|
|
335
338
|
}),
|
|
336
339
|
trespass: [],
|
|
337
340
|
});
|
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
|
|
9
|
-
if (
|
|
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 = {}) => {
|
|
@@ -46,12 +96,11 @@ const Ground = (modules = {}) => {
|
|
|
46
96
|
if (program === undefined || Blueprint(program)?.digest === undefined) {
|
|
47
97
|
return undefined;
|
|
48
98
|
}
|
|
49
|
-
if (cells.peek('ground/admin') === undefined) {
|
|
50
|
-
install({ cells, keychain, program });
|
|
51
|
-
}
|
|
52
99
|
const installed = cells.peek('ground/admin');
|
|
53
100
|
if (installed === undefined) {
|
|
54
|
-
return
|
|
101
|
+
return Object.freeze({
|
|
102
|
+
init: (bundle) => install({ cells, keychain, program, bundle }),
|
|
103
|
+
});
|
|
55
104
|
}
|
|
56
105
|
const bundle = keychain.unseal(installed.sealed);
|
|
57
106
|
const adminVoice = bundle === undefined ? undefined : Voice.revive(bundle);
|
|
@@ -184,6 +233,8 @@ const Ground = (modules = {}) => {
|
|
|
184
233
|
return Object.freeze({
|
|
185
234
|
address,
|
|
186
235
|
receive,
|
|
236
|
+
rotate: (bundle) =>
|
|
237
|
+
succeeds({ adminMine, adminVoice, address, bundle, cells, keychain, memory }),
|
|
187
238
|
serve: (voice) => {
|
|
188
239
|
const client = Client({ voice, deliver, resolve });
|
|
189
240
|
const sdlOf = async (at) => (await client.describe(at))?.body?.sdl;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nervur",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A distributed system of sovereign beings
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "A distributed system of sovereign beings \u2014 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
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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),
|