nervur 0.11.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.
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, 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,11 +38,60 @@ 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
  };
42
52
 
53
+ const spanOf = (given) =>
54
+ typeof given === 'number' && Number.isFinite(given) && given > 0 && given < SPAN ? given : SPAN;
55
+
56
+ const within = (span, work) => {
57
+ let timer;
58
+ const lapsed = new Promise((settle) => {
59
+ timer = setTimeout(() => settle(undefined), span);
60
+ timer.unref?.();
61
+ });
62
+ return Promise.race([work, lapsed]).finally(() => clearTimeout(timer));
63
+ };
64
+
65
+ const Fence = () => {
66
+ let standing = true;
67
+ return Object.freeze({
68
+ close: () => {
69
+ standing = false;
70
+ return undefined;
71
+ },
72
+ open: () => standing,
73
+ });
74
+ };
75
+
76
+ const fenced = (mine, fence) =>
77
+ Object.freeze({
78
+ classOf: mine.classOf,
79
+ list: mine.list,
80
+ read: mine.read,
81
+ write: (key, plain) => (fence.open() ? mine.write(key, plain) : undefined),
82
+ });
83
+
84
+ const custodial = (registry, fence) =>
85
+ registry === undefined
86
+ ? undefined
87
+ : Object.freeze({
88
+ ...registry,
89
+ create: (one) => (fence.open() ? registry.create(one) : undefined),
90
+ destroy: (at) => (fence.open() ? registry.destroy(at) : undefined),
91
+ extend: (source) => (fence.open() ? registry.extend(source) : undefined),
92
+ upgrade: (one) => (fence.open() ? registry.upgrade(one) : undefined),
93
+ });
94
+
43
95
  const offered = (modules, program, mine) =>
44
96
  Object.freeze({
45
97
  ...Object.fromEntries(
@@ -63,6 +115,14 @@ const referable = (mine, key, plain, program) => {
63
115
  return { ...plain, rights };
64
116
  }
65
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
+ }
66
126
  return {
67
127
  ...plain,
68
128
  nextPkHash: plain.nextPkHash ?? held?.nextPkHash ?? null,
@@ -89,21 +149,30 @@ const scoped = (mine, program) =>
89
149
  },
90
150
  });
91
151
 
92
- const rootFor = ({ memory, voice, address }) => {
152
+ const rootFor = ({ memory, voice, address, cipher }) => {
93
153
  const cell = memory.read(address, `keyring/${Tools.address(voice.pk)}`);
94
- const opened = cell === undefined ? undefined : voice.open(cell.value.wrapped);
95
- 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);
96
156
  };
97
157
 
98
- const Port = ({ voice, mine, deliver, rights }) => {
99
- const stamp = Stamp();
100
- const lanes = new Map();
158
+ const Port = ({ voice, mine, deliver, rights, stamp, lanes, fence }) => {
101
159
  const send = async (to, op) => {
102
- const turn = (lanes.get(to) ?? Promise.resolve()).then(() => {
160
+ if (fence !== undefined && !fence.open()) {
161
+ return undefined;
162
+ }
163
+ const turn = (lanes.get(to) ?? Promise.resolve()).then(async () => {
103
164
  const handle = mine.read(`handles/${to}`);
104
- return deliver(
105
- 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(),
170
+ );
171
+ const claim = Tools.digest(
172
+ Tools.canon({ from: envelope.from, op: envelope.op, seq: envelope.seq, to: envelope.to }),
106
173
  );
174
+ const reply = await deliver(envelope);
175
+ return reply === undefined ? undefined : { ...reply, claim };
107
176
  });
108
177
  const settled = turn.then(
109
178
  () => undefined,
@@ -163,9 +232,10 @@ const successionOf = (memory, address) =>
163
232
  .filter((one) => one !== undefined)
164
233
  .sort((one, other) => one.v - other.v);
165
234
 
166
- const Being = ({ voice, address, deliver, modules, registry, vouched, watch }) => {
235
+ const Being = ({ voice, address, cipher, deliver, modules, registry, span, vouched, watch }) => {
236
+ const held = spanOf(span);
167
237
  const memory = modules.memory;
168
- const root = rootFor({ memory, voice, address });
238
+ const root = rootFor({ memory, voice, address, cipher });
169
239
  const source = memory.read(address, 'program')?.value;
170
240
  const program = source === undefined ? undefined : Program(source);
171
241
  const mine = Compartment({
@@ -174,14 +244,17 @@ const Being = ({ voice, address, deliver, modules, registry, vouched, watch }) =
174
244
  address,
175
245
  root,
176
246
  classOf: (key) => program?.classOf(key) ?? coreClass(key),
247
+ cipher,
177
248
  });
178
249
  const succession = successionOf(memory, address);
179
- const answer = (spoken) => {
250
+ const stamp = Stamp();
251
+ const lanes = new Map();
252
+ const answer = (spoken, claim) => {
180
253
  const body = Tools.canon(spoken);
181
254
  if (body.length > MEASURE) {
182
255
  return undefined;
183
256
  }
184
- const signed = { body: spoken, sig: voice.sign(body) };
257
+ const signed = { body: spoken, sig: voice.sign(Tools.canon({ body: spoken, claim })) };
185
258
  return succession.length === 0 ? signed : { ...signed, succession };
186
259
  };
187
260
  const note = (envelope, outcome) => {
@@ -205,6 +278,7 @@ const Being = ({ voice, address, deliver, modules, registry, vouched, watch }) =
205
278
  try {
206
279
  const { from, op, seq, sig, to } = envelope;
207
280
  const claim = Tools.canon({ from, op, seq, to });
281
+ const claimDigest = Tools.digest(claim);
208
282
  const callerPk = Tools.pkOf(from);
209
283
  const proven =
210
284
  claim.length <= MEASURE &&
@@ -228,30 +302,34 @@ const Being = ({ voice, address, deliver, modules, registry, vouched, watch }) =
228
302
  if (program !== undefined && !program.visible(rights)) {
229
303
  return undefined;
230
304
  }
231
- return answer({
232
- address,
233
- digest: program?.digest,
234
- rights,
235
- sdl: program?.sdlFor(rights),
236
- });
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
+ );
237
315
  }
238
316
  if (op?.introspect !== undefined) {
239
317
  const seen = await program?.introspect(rights);
240
- return seen === undefined ? undefined : answer({ introspection: seen });
318
+ return seen === undefined ? undefined : answer({ introspection: seen }, claimDigest);
241
319
  }
242
320
  if (op?.rotate !== undefined) {
243
321
  const rotated = rotates({ mine }, from, op.rotate.nextPkHash);
244
- return rotated === undefined ? undefined : answer({ rotated });
322
+ return rotated === undefined ? undefined : answer({ rotated }, claimDigest);
245
323
  }
246
324
  if (op?.release !== undefined) {
247
325
  if (row === undefined) {
248
326
  return undefined;
249
327
  }
250
328
  mine.write(`refs/${from}`, undefined);
251
- return answer({ released: from });
329
+ return answer({ released: from }, claimDigest);
252
330
  }
253
331
  if (op?.acceptInvite !== undefined) {
254
- const offer = Tools.digest(Buffer.from(op.acceptInvite.proof, 'base64'));
332
+ const offer = Tools.digest(Bytes.unbase64(op.acceptInvite.proof));
255
333
  const pending = mine.read(`refs/pending/${offer}`);
256
334
  if (pending === undefined || pending.spent) {
257
335
  return undefined;
@@ -262,23 +340,28 @@ const Being = ({ voice, address, deliver, modules, registry, vouched, watch }) =
262
340
  rights: pending.rights,
263
341
  seen: seq ?? 0,
264
342
  });
265
- return answer({ referred: from, rights: pending.rights });
343
+ return answer({ referred: from, rights: pending.rights }, claimDigest);
266
344
  }
267
345
  if (op?.ask === undefined || program === undefined || !program.visible(rights)) {
268
346
  return undefined;
269
347
  }
270
- const asked = await program.ask({
271
- context: {
272
- from: program.caller ? from : undefined,
273
- modules: offered(modules, program, mine),
274
- port: Port({ voice, mine, deliver, rights }),
275
- registry,
276
- rights,
277
- },
278
- source: op.ask.source,
279
- variables: op.ask.variables,
280
- });
281
- return asked === undefined ? undefined : answer(asked);
348
+ const fence = Fence();
349
+ const turn = fenced(mine, fence);
350
+ const asked = await within(
351
+ held,
352
+ program.ask({
353
+ context: {
354
+ from: program.caller ? from : undefined,
355
+ modules: offered(modules, program, turn),
356
+ port: Port({ voice, mine: turn, deliver, fence, lanes, rights, stamp }),
357
+ registry: custodial(registry, fence),
358
+ rights,
359
+ },
360
+ source: op.ask.source,
361
+ variables: op.ask.variables,
362
+ }),
363
+ ).finally(fence.close);
364
+ return asked === undefined ? undefined : answer(asked, claimDigest);
282
365
  } catch {
283
366
  return undefined;
284
367
  }
@@ -288,18 +371,31 @@ const Being = ({ voice, address, deliver, modules, registry, vouched, watch }) =
288
371
  });
289
372
  };
290
373
 
291
- const make = ({ memory, voice, address, program }) => {
374
+ // VII: the chain takes no plain write above version 1, so its rows stand.
375
+ const spend = ({ memory, voice, address }) => {
376
+ for (const key of memory.list(address, '')) {
377
+ const cell = key.startsWith('chain/') ? undefined : memory.read(address, key);
378
+ if (cell === undefined || cell.value === null) {
379
+ continue;
380
+ }
381
+ const claim = { address, key, value: null, version: cell.version + 1 };
382
+ memory.write({ ...claim, sig: voice.sign(Tools.canon(claim)) });
383
+ }
384
+ return address;
385
+ };
386
+
387
+ const make = ({ memory, voice, address, program, cipher }) => {
292
388
  const root = Tools.secret();
293
389
  const put = (key, value) => {
294
390
  const claim = { address, key, value, version: (memory.read(address, key)?.version ?? 0) + 1 };
295
391
  return memory.write({ ...claim, sig: voice.sign(Tools.canon(claim)) });
296
392
  };
297
- put('chain/pk', Buffer.from(voice.pk).toString('hex'));
393
+ put('chain/pk', Bytes.hex(voice.pk));
298
394
  put('chain/nextPkHash', voice.nextPkHash);
299
395
  put('program', program ?? '({})');
300
396
  put(`keyring/${Tools.address(voice.pk)}`, {
301
- boxPk: Buffer.from(voice.boxPk).toString('base64'),
302
- wrapped: Voice.sealTo(voice.boxPk, root.toString('base64')),
397
+ boxPk: Bytes.base64(voice.boxPk),
398
+ wrapped: Voice.sealTo(voice.boxPk, Bytes.base64(root), cipher),
303
399
  });
304
400
  const written = program === undefined ? undefined : Program(program);
305
401
  return Compartment({
@@ -308,6 +404,7 @@ const make = ({ memory, voice, address, program }) => {
308
404
  address,
309
405
  root,
310
406
  classOf: (key) => written?.classOf(key) ?? coreClass(key),
407
+ cipher,
311
408
  });
312
409
  };
313
410
 
@@ -315,21 +412,31 @@ const Registry = ({
315
412
  memory,
316
413
  adminMine,
317
414
  address,
415
+ cipher,
318
416
  deliver,
319
417
  modules,
320
418
  modulesFor,
321
419
  announce,
322
420
  forget,
421
+ span,
323
422
  vouched,
324
423
  attest,
325
424
  watch,
425
+ extended,
326
426
  }) => {
327
427
  const live = new Map();
328
- const orphans = (at, written) =>
329
- memory
428
+ const stranded = (at, written) => {
429
+ const source = memory.read(at, 'program')?.value;
430
+ const standing = source === undefined ? undefined : Program(source);
431
+ return memory
330
432
  .list(at, '')
331
433
  .filter((key) => memory.read(at, key)?.value !== null)
332
- .filter((key) => written.classOf(key) === undefined);
434
+ .filter((key) => {
435
+ const under = written.classOf(key);
436
+ const was = standing?.classOf(key) ?? coreClass(key);
437
+ return under === undefined || (was !== undefined && was !== under);
438
+ });
439
+ };
333
440
  const activate = (at) => {
334
441
  if (live.has(at)) {
335
442
  return live.get(at);
@@ -344,8 +451,10 @@ const Registry = ({
344
451
  );
345
452
  const door = Being({
346
453
  address: at,
454
+ cipher,
347
455
  deliver,
348
456
  modules: modulesFor(voice, declared),
457
+ span,
349
458
  vouched,
350
459
  voice,
351
460
  watch,
@@ -379,7 +488,7 @@ const Registry = ({
379
488
  }
380
489
  const voice = Voice();
381
490
  const address = Tools.address(voice.pk);
382
- const mine = make({ memory, voice, address, program });
491
+ const mine = make({ memory, voice, address, program, cipher });
383
492
  for (const [key, value] of Object.entries(data)) {
384
493
  if (mine.write(key, value) === undefined) {
385
494
  forget(address);
@@ -412,10 +521,14 @@ const Registry = ({
412
521
  if (written.modules.some((one) => !modules.has(one.module))) {
413
522
  return undefined;
414
523
  }
415
- if (orphans(address, written).length > 0) {
524
+ if (stranded(address, written).length > 0) {
416
525
  return undefined;
417
526
  }
418
- return adminMine.write('program', program) === undefined ? undefined : address;
527
+ if (adminMine.write('program', program) === undefined) {
528
+ return undefined;
529
+ }
530
+ extended?.();
531
+ return address;
419
532
  },
420
533
  upgrade: ({ address: target, program }) => {
421
534
  if (target === address) {
@@ -425,7 +538,7 @@ const Registry = ({
425
538
  if (written === undefined) {
426
539
  return undefined;
427
540
  }
428
- if (orphans(target, written).length > 0) {
541
+ if (stranded(target, written).length > 0) {
429
542
  return undefined;
430
543
  }
431
544
  const held = adminMine.read(`identity/custody/${target}`);
@@ -446,12 +559,14 @@ const Registry = ({
446
559
  return target;
447
560
  },
448
561
  destroy: (at) => {
449
- if (adminMine.read(`identity/custody/${at}`) === undefined) {
562
+ const held = adminMine.read(`identity/custody/${at}`);
563
+ const voice = held === undefined ? undefined : Voice.revive(held);
564
+ if (voice === undefined) {
450
565
  return undefined;
451
566
  }
452
567
  adminMine.write(`identity/custody/${at}`, undefined);
453
568
  live.delete(at);
454
- forget(at);
569
+ forget(at, voice);
455
570
  return at;
456
571
  },
457
572
  residents: () => {
@@ -476,4 +591,4 @@ const Registry = ({
476
591
  });
477
592
  };
478
593
 
479
- export { Being, Compartment, Registry, SUCCESSION, make, rootFor };
594
+ export { Being, Compartment, Registry, SUCCESSION, make, rootFor, spend, successionOf };
package/blueprint.mjs CHANGED
@@ -174,25 +174,121 @@ const doorsIn = (sdl) =>
174
174
  .definitions.filter((one) => doorHolder(one) && ['Mutation', 'Query'].includes(one.name.value))
175
175
  .flatMap((one) => (one.fields ?? []).map((field) => field.name.value));
176
176
 
177
- const composed = (baseSource, addedSource) => {
177
+ const rootsIn = (document, schema) => {
178
+ const roots = [
179
+ schema.getQueryType()?.name,
180
+ schema.getMutationType()?.name,
181
+ schema.getSubscriptionType()?.name,
182
+ ].filter((one) => one !== undefined);
183
+ return document.definitions
184
+ .filter((one) => doorHolder(one) && roots.includes(one.name.value))
185
+ .flatMap((one) => one.fields ?? []);
186
+ };
187
+
188
+ // Names why Blueprint would refuse; Blueprint itself rules, and never reads
189
+ // this. The bench binds the two: every cause named here is one it refuses.
190
+ const faults = (source) => {
191
+ const trespass = reads(source);
192
+ if (trespass.length > 0) {
193
+ return [`it reads ambient power no blueprint may hold: ${trespass.join(', ')} (VIII)`];
194
+ }
195
+ let written;
196
+ try {
197
+ written = evaluated(source);
198
+ } catch (error) {
199
+ return [`it is not an expression settling a blueprint object: ${error.message} (VIII)`];
200
+ }
201
+ if (written === null || typeof written !== 'object') {
202
+ return ['it settles no blueprint object (VIII)'];
203
+ }
204
+ const declares = /enum\s+Rights\b/.test(written.interface) ? '' : DEFAULT_RIGHTS;
205
+ const sdl = [RIGHTS_DIRECTIVE, declares, written.interface].filter(Boolean).join('\n');
206
+ let document;
207
+ let schema;
208
+ try {
209
+ document = parse(sdl);
210
+ schema = buildSchema(sdl);
211
+ } catch (error) {
212
+ return [`its schema does not compile: ${error.message} (VIII)`];
213
+ }
214
+ const declaredNames = (schema.getType('Rights')?.getValues() ?? []).map((one) => one.name);
215
+ const rights = [...new Set([...STRUCTURAL, ...declaredNames])];
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
+ }
222
+ for (const [key, cell] of Object.entries(written.memory ?? {})) {
223
+ if (coreClass(key) !== undefined) {
224
+ found.push(`the cell ${key} is the core's own, and no program declares it (VIII)`);
225
+ } else if (!CLASSES.includes(cell?.class)) {
226
+ found.push(`the cell ${key} names no class the memory law holds: ${cell?.class} (VIII)`);
227
+ }
228
+ }
229
+ const bare = [];
230
+ const outside = new Map();
231
+ for (const field of rootsIn(document, schema)) {
232
+ const allowed = rightsOf(field);
233
+ if (allowed === undefined) {
234
+ bare.push(field.name.value);
235
+ continue;
236
+ }
237
+ for (const held of allowed.filter((one) => !rights.includes(one))) {
238
+ outside.set(held, [...(outside.get(held) ?? []), field.name.value]);
239
+ }
240
+ }
241
+ if (bare.length > 0) {
242
+ found.push(`the door ${bare.join(', ')} declares no rights, and every door declares them (VI)`);
243
+ }
244
+ for (const [held, doors] of outside) {
245
+ found.push(
246
+ `the Rights enum does not name ${held}, which ${doors.length} door${doors.length === 1 ? '' : 's'} stand behind: ${doors.join(', ')} (VI)`,
247
+ );
248
+ }
249
+ return found;
250
+ };
251
+
252
+ const composition = (baseSource, addedSource) => {
253
+ const refused = (reason) => ({ refused: reason });
178
254
  try {
179
255
  const base = evaluated(baseSource);
180
256
  const added = evaluated(addedSource);
257
+ if (added === null || typeof added !== 'object') {
258
+ return refused('the part settles no blueprint object (VIII)');
259
+ }
181
260
  const standard = doorsIn(base.interface);
182
- if (doorsIn(added.interface).some((one) => standard.includes(one))) {
183
- return undefined;
261
+ const collides = [...new Set(doorsIn(added.interface).filter((one) => standard.includes(one)))];
262
+ if (collides.length > 0) {
263
+ return refused(
264
+ `the part declares the door ${collides.join(', ')}, which the administration already names — a standard door name is reserved on both roots (X)`,
265
+ );
184
266
  }
185
- const cells = { ...(base.memory ?? {}), ...(added.memory ?? {}) };
186
- if (
187
- Object.keys(base.memory ?? {}).some((key) => (added.memory ?? {})[key] !== undefined) ||
188
- (added.needs?.config !== undefined && base.needs?.config !== undefined)
189
- ) {
190
- return undefined;
267
+ const kept = Object.keys(base.memory ?? {}).filter(
268
+ (key) => (added.memory ?? {})[key] !== undefined,
269
+ );
270
+ if (kept.length > 0) {
271
+ return refused(
272
+ `the part declares the cell ${kept.join(', ')}, which the administration already keeps (X)`,
273
+ );
274
+ }
275
+ if (added.needs?.config !== undefined && base.needs?.config !== undefined) {
276
+ return refused(
277
+ 'the part declares needs.config, and the program it composes with declares one too — a program holds one config (X)',
278
+ );
191
279
  }
192
280
  const slots = [...(base.needs?.modules ?? []), ...(added.needs?.modules ?? [])];
193
- if (new Set(slots.map((one) => one.module)).size !== slots.length) {
194
- return undefined;
281
+ const twice = [
282
+ ...new Set(
283
+ slots.map((one) => one.module).filter((name, at, all) => all.indexOf(name) !== at),
284
+ ),
285
+ ];
286
+ if (twice.length > 0) {
287
+ return refused(
288
+ `the part declares the module slot ${twice.join(', ')}, which the administration already holds (X)`,
289
+ );
195
290
  }
291
+ const cells = { ...(base.memory ?? {}), ...(added.memory ?? {}) };
196
292
  const needs = {
197
293
  ...(base.needs ?? {}),
198
294
  ...(added.needs ?? {}),
@@ -208,18 +304,22 @@ const composed = (baseSource, addedSource) => {
208
304
  .join(', ')} }`,
209
305
  )
210
306
  .join(', ');
211
- return `({
307
+ const source = `({
212
308
  name: ${JSON.stringify(added.name)},
213
309
  needs: ${JSON.stringify(needs)},
214
310
  memory: ${JSON.stringify(cells)},
215
311
  interface: ${JSON.stringify(`${base.interface}\n${added.interface}`)},
216
312
  resolvers: { ${written} },
217
313
  })`;
218
- } catch {
219
- return undefined;
314
+ const found = faults(source);
315
+ return found.length > 0 ? refused(found.join('; ')) : { source };
316
+ } catch (error) {
317
+ return refused(`the part does not compose: ${error.message} (VIII)`);
220
318
  }
221
319
  };
222
320
 
321
+ const composed = (baseSource, addedSource) => composition(baseSource, addedSource).source;
322
+
223
323
  const inputCheck = (schema, typeName, given) => {
224
324
  const type = schema.getType(typeName);
225
325
  if (type?.getFields === undefined) {
@@ -245,12 +345,11 @@ const Blueprint = (source) => {
245
345
  const sdl = [RIGHTS_DIRECTIVE, declares, written.interface].filter(Boolean).join('\n');
246
346
  const document = parse(sdl);
247
347
  const schema = buildSchema(sdl);
248
- const rights = [
249
- ...new Set([
250
- ...STRUCTURAL,
251
- ...(schema.getType('Rights')?.getValues() ?? []).map((one) => one.name),
252
- ]),
253
- ];
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])];
254
353
 
255
354
  const admits = (typeName, fieldName, held) => {
256
355
  const field = document.definitions
@@ -270,18 +369,10 @@ const Blueprint = (source) => {
270
369
  if (misdeclared.length > 0) {
271
370
  return undefined;
272
371
  }
273
- const roots = [
274
- schema.getQueryType()?.name,
275
- schema.getMutationType()?.name,
276
- schema.getSubscriptionType()?.name,
277
- ].filter((one) => one !== undefined);
278
- const undeclared = document.definitions
279
- .filter((one) => doorHolder(one) && roots.includes(one.name.value))
280
- .flatMap((one) => one.fields ?? [])
281
- .filter((one) => {
282
- const allowed = rightsOf(one);
283
- return allowed === undefined || allowed.some((held) => !rights.includes(held));
284
- });
372
+ const undeclared = rootsIn(document, schema).filter((one) => {
373
+ const allowed = rightsOf(one);
374
+ return allowed === undefined || allowed.some((held) => !rights.includes(held));
375
+ });
285
376
  if (undeclared.length > 0) {
286
377
  return undefined;
287
378
  }
@@ -304,7 +395,7 @@ const Blueprint = (source) => {
304
395
  needs.config === undefined ? Object.freeze({}) : inputCheck(schema, needs.config, given),
305
396
  digest: Tools.digest(
306
397
  Tools.canon({
307
- interface: print(document),
398
+ interface: sdl,
308
399
  memory,
309
400
  name: written.name,
310
401
  needs,
@@ -356,4 +447,16 @@ const Blueprint = (source) => {
356
447
  }
357
448
  };
358
449
 
359
- export { Blueprint, CLASSES, CORE_ONLY, FORBIDDEN, OPEN, STRUCTURAL, composed, coreClass, reads };
450
+ export {
451
+ Blueprint,
452
+ CLASSES,
453
+ CORE_ONLY,
454
+ FORBIDDEN,
455
+ OPEN,
456
+ STRUCTURAL,
457
+ composed,
458
+ composition,
459
+ coreClass,
460
+ faults,
461
+ reads,
462
+ };
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
  });