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/GETTING-STARTED.md +111 -0
- package/README.md +57 -19
- package/SECURITY.md +11 -0
- package/admin.mjs +11 -1
- package/being.mjs +169 -54
- package/blueprint.mjs +137 -34
- package/client.mjs +9 -6
- package/contract.mjs +6 -4
- package/ground.mjs +144 -37
- package/index.mjs +13 -3
- package/modules.mjs +14 -70
- package/package.json +5 -2
- package/tools.mjs +69 -48
- package/voice.mjs +49 -63
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
|
-
|
|
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
|
-
|
|
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 :
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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(
|
|
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
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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',
|
|
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:
|
|
302
|
-
wrapped: Voice.sealTo(voice.boxPk,
|
|
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
|
|
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) =>
|
|
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 (
|
|
524
|
+
if (stranded(address, written).length > 0) {
|
|
416
525
|
return undefined;
|
|
417
526
|
}
|
|
418
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
183
|
-
|
|
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
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
return
|
|
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
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
});
|