nervur 0.12.0 → 0.13.0

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