nervur 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -134,6 +134,17 @@ after. Both are custody's own calls on the shelf, never doors — which is
134
134
  what running-is-custody looks like from the outside, stated rather than
135
135
  hidden.
136
136
 
137
+ `unproven()` is custody's third call, and it is a read: the addresses whose
138
+ writes have stopped proving, with when each was first and last seen and how
139
+ many writes have been refused. A write is proven against the key on the
140
+ being's `chain/pk` cell, so whoever holds the shelf can overwrite that cell
141
+ and freeze a being's memory while it answers on, correctly signed and
142
+ indistinguishable to every caller. The ground records that in its own host
143
+ rows under `ground/` — no being reads them, nothing goes over the wire — and
144
+ this is where a host reads them back. Show it to whoever operates the
145
+ machine; a ground that has stopped proving its own writes is a custody
146
+ incident, not a bug.
147
+
137
148
  **`serve` is the client, and it has four faces of one door.** `pointer(at)`
138
149
  is a plain object with one function per field, generated from the schema that
139
150
  caller is allowed to see. `gql(at)` is the raw document. `mcp(at)` is the
@@ -215,7 +226,8 @@ silence, and silence is something local delivery never answers with.
215
226
  activates and destroys, reachable only through the admin being.
216
227
  - [admin.mjs](admin.mjs) — the administrative program every ground installs
217
228
  for itself, aliased `Ground`: census, create, activate, destroy, upgrade,
218
- alias, attest, and behind `SELF` alone invite, drop and extend. Upgrade is
229
+ alias, attest, invite, drop, wear, and behind `SELF` alone extend; `wears`
230
+ answers any stranger with the livery being the ground wears. Upgrade is
219
231
  custody moving the one cell that was always the core's to move: the
220
232
  program is replaced in place — address, cells, refs and chain surviving —
221
233
  refused whole when a cell the being holds would go undeclared, and visible
package/admin.mjs CHANGED
@@ -1,7 +1,10 @@
1
1
  const ADMIN = `({
2
2
  name: 'nervur.ground',
3
3
  needs: { modules: [{ module: 'memory', contract: 'nervur.memory' }] },
4
- memory: { 'aliases/*': { type: 'String', class: 'data' } },
4
+ memory: {
5
+ 'aliases/*': { type: 'String', class: 'data' },
6
+ wears: { type: 'String', class: 'data' },
7
+ },
5
8
  interface: \`
6
9
  input RefInput { address: String!, nextPkHash: String, rights: String }
7
10
  type Resident { address: String!, alias: String, digest: String, active: Boolean! }
@@ -11,6 +14,7 @@ const ADMIN = `({
11
14
  aliasOf(name: String!): String @rights(is: [SELF, MEMBER])
12
15
  attest(module: String!): Boolean @rights(is: [SELF, MEMBER])
13
16
  opened: Boolean @rights(is: [SELF, MEMBER, STRANGER])
17
+ wears: String @rights(is: [SELF, MEMBER, STRANGER])
14
18
  }
15
19
  type Mutation {
16
20
  invite(address: String!, nextPkHash: String, rights: String): Boolean @rights(is: [SELF, MEMBER])
@@ -22,6 +26,7 @@ const ADMIN = `({
22
26
  destroy(address: String!): String @rights(is: [SELF, MEMBER])
23
27
  upgrade(address: String!, program: String!): Boolean @rights(is: [SELF, MEMBER])
24
28
  alias(name: String!, address: String!): Boolean @rights(is: [SELF, MEMBER])
29
+ wear(address: String): Boolean @rights(is: [SELF, MEMBER])
25
30
  }
26
31
  \`,
27
32
  resolvers: {
@@ -37,6 +42,7 @@ const ADMIN = `({
37
42
  aliasOf: (_, { name }, { modules }) => modules.memory.read('aliases/' + name),
38
43
  attest: (_, { module }, { registry }) => registry.attest(module),
39
44
  opened: (_, __, { modules }) => modules.memory.list('refs/').length > 0,
45
+ wears: (_, __, { modules }) => modules.memory.read('wears') ?? null,
40
46
  },
41
47
  Mutation: {
42
48
  invite: (_, { address, nextPkHash, rights }, { modules }) =>
@@ -66,6 +72,10 @@ const ADMIN = `({
66
72
  registry.upgrade({ address, program }) !== undefined,
67
73
  alias: (_, { name, address }, { modules }) =>
68
74
  modules.memory.write('aliases/' + name, address) !== undefined,
75
+ wear: (_, { address }, { modules }) => {
76
+ modules.memory.write('wears', address ?? undefined);
77
+ return modules.memory.read('wears') === (address ?? undefined);
78
+ },
69
79
  },
70
80
  },
71
81
  })`;
package/being.mjs CHANGED
@@ -1,7 +1,7 @@
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 { MEASURE, SPAN, Stamp, Tools } from './tools.mjs';
5
5
  import { Voice } from './voice.mjs';
6
6
 
7
7
  const Compartment = ({ memory, voice, address, root, classOf }) => {
@@ -40,6 +40,48 @@ const Compartment = ({ memory, voice, address, root, classOf }) => {
40
40
  });
41
41
  };
42
42
 
43
+ const spanOf = (given) =>
44
+ typeof given === 'number' && Number.isFinite(given) && given > 0 && given < SPAN ? given : SPAN;
45
+
46
+ const within = (span, work) => {
47
+ let timer;
48
+ const lapsed = new Promise((settle) => {
49
+ timer = setTimeout(() => settle(undefined), span);
50
+ timer.unref?.();
51
+ });
52
+ return Promise.race([work, lapsed]).finally(() => clearTimeout(timer));
53
+ };
54
+
55
+ const Fence = () => {
56
+ let standing = true;
57
+ return Object.freeze({
58
+ close: () => {
59
+ standing = false;
60
+ return undefined;
61
+ },
62
+ open: () => standing,
63
+ });
64
+ };
65
+
66
+ const fenced = (mine, fence) =>
67
+ Object.freeze({
68
+ classOf: mine.classOf,
69
+ list: mine.list,
70
+ read: mine.read,
71
+ write: (key, plain) => (fence.open() ? mine.write(key, plain) : undefined),
72
+ });
73
+
74
+ const custodial = (registry, fence) =>
75
+ registry === undefined
76
+ ? undefined
77
+ : Object.freeze({
78
+ ...registry,
79
+ create: (one) => (fence.open() ? registry.create(one) : undefined),
80
+ destroy: (at) => (fence.open() ? registry.destroy(at) : undefined),
81
+ extend: (source) => (fence.open() ? registry.extend(source) : undefined),
82
+ upgrade: (one) => (fence.open() ? registry.upgrade(one) : undefined),
83
+ });
84
+
43
85
  const offered = (modules, program, mine) =>
44
86
  Object.freeze({
45
87
  ...Object.fromEntries(
@@ -95,10 +137,11 @@ const rootFor = ({ memory, voice, address }) => {
95
137
  return opened === undefined ? undefined : Buffer.from(opened, 'base64');
96
138
  };
97
139
 
98
- const Port = ({ voice, mine, deliver, rights }) => {
99
- const stamp = Stamp();
100
- const lanes = new Map();
140
+ const Port = ({ voice, mine, deliver, rights, stamp, lanes, fence }) => {
101
141
  const send = async (to, op) => {
142
+ if (fence !== undefined && !fence.open()) {
143
+ return undefined;
144
+ }
102
145
  const turn = (lanes.get(to) ?? Promise.resolve()).then(() => {
103
146
  const handle = mine.read(`handles/${to}`);
104
147
  return deliver(
@@ -143,7 +186,10 @@ const rotates = ({ mine }, from, nextPkHash) => {
143
186
  .list('refs/')
144
187
  .map((key) => ({ key, referred: mine.read(key) }))
145
188
  .find(({ referred }) => referred?.nextPkHash === claim);
146
- if (held === undefined) {
189
+ if (
190
+ held === undefined ||
191
+ (held.key !== `refs/${from}` && mine.read(`refs/${from}`) !== undefined)
192
+ ) {
147
193
  return undefined;
148
194
  }
149
195
  mine.write(`refs/${from}`, { nextPkHash, rights: held.referred.rights });
@@ -160,7 +206,8 @@ const successionOf = (memory, address) =>
160
206
  .filter((one) => one !== undefined)
161
207
  .sort((one, other) => one.v - other.v);
162
208
 
163
- const Being = ({ voice, address, deliver, modules, registry, vouched, watch }) => {
209
+ const Being = ({ voice, address, deliver, modules, registry, span, vouched, watch }) => {
210
+ const held = spanOf(span);
164
211
  const memory = modules.memory;
165
212
  const root = rootFor({ memory, voice, address });
166
213
  const source = memory.read(address, 'program')?.value;
@@ -173,6 +220,8 @@ const Being = ({ voice, address, deliver, modules, registry, vouched, watch }) =
173
220
  classOf: (key) => program?.classOf(key) ?? coreClass(key),
174
221
  });
175
222
  const succession = successionOf(memory, address);
223
+ const stamp = Stamp();
224
+ const lanes = new Map();
176
225
  const answer = (spoken) => {
177
226
  const body = Tools.canon(spoken);
178
227
  if (body.length > MEASURE) {
@@ -261,20 +310,25 @@ const Being = ({ voice, address, deliver, modules, registry, vouched, watch }) =
261
310
  });
262
311
  return answer({ referred: from, rights: pending.rights });
263
312
  }
264
- if (op?.ask === undefined || program === undefined) {
313
+ if (op?.ask === undefined || program === undefined || !program.visible(rights)) {
265
314
  return undefined;
266
315
  }
267
- const asked = await program.ask({
268
- context: {
269
- from: program.caller ? from : undefined,
270
- modules: offered(modules, program, mine),
271
- port: Port({ voice, mine, deliver, rights }),
272
- registry,
273
- rights,
274
- },
275
- source: op.ask.source,
276
- variables: op.ask.variables,
277
- });
316
+ const fence = Fence();
317
+ const turn = fenced(mine, fence);
318
+ const asked = await within(
319
+ held,
320
+ program.ask({
321
+ context: {
322
+ from: program.caller ? from : undefined,
323
+ modules: offered(modules, program, turn),
324
+ port: Port({ voice, mine: turn, deliver, fence, lanes, rights, stamp }),
325
+ registry: custodial(registry, fence),
326
+ rights,
327
+ },
328
+ source: op.ask.source,
329
+ variables: op.ask.variables,
330
+ }),
331
+ ).finally(fence.close);
278
332
  return asked === undefined ? undefined : answer(asked);
279
333
  } catch {
280
334
  return undefined;
@@ -285,6 +339,19 @@ const Being = ({ voice, address, deliver, modules, registry, vouched, watch }) =
285
339
  });
286
340
  };
287
341
 
342
+ // VII: the chain takes no plain write above version 1, so its rows stand.
343
+ const spend = ({ memory, voice, address }) => {
344
+ for (const key of memory.list(address, '')) {
345
+ const cell = key.startsWith('chain/') ? undefined : memory.read(address, key);
346
+ if (cell === undefined || cell.value === null) {
347
+ continue;
348
+ }
349
+ const claim = { address, key, value: null, version: cell.version + 1 };
350
+ memory.write({ ...claim, sig: voice.sign(Tools.canon(claim)) });
351
+ }
352
+ return address;
353
+ };
354
+
288
355
  const make = ({ memory, voice, address, program }) => {
289
356
  const root = Tools.secret();
290
357
  const put = (key, value) => {
@@ -317,16 +384,24 @@ const Registry = ({
317
384
  modulesFor,
318
385
  announce,
319
386
  forget,
387
+ span,
320
388
  vouched,
321
389
  attest,
322
390
  watch,
323
391
  }) => {
324
392
  const live = new Map();
325
- const orphans = (at, written) =>
326
- memory
393
+ const stranded = (at, written) => {
394
+ const source = memory.read(at, 'program')?.value;
395
+ const standing = source === undefined ? undefined : Program(source);
396
+ return memory
327
397
  .list(at, '')
328
398
  .filter((key) => memory.read(at, key)?.value !== null)
329
- .filter((key) => written.classOf(key) === undefined);
399
+ .filter((key) => {
400
+ const under = written.classOf(key);
401
+ const was = standing?.classOf(key) ?? coreClass(key);
402
+ return under === undefined || (was !== undefined && was !== under);
403
+ });
404
+ };
330
405
  const activate = (at) => {
331
406
  if (live.has(at)) {
332
407
  return live.get(at);
@@ -343,6 +418,7 @@ const Registry = ({
343
418
  address: at,
344
419
  deliver,
345
420
  modules: modulesFor(voice, declared),
421
+ span,
346
422
  vouched,
347
423
  voice,
348
424
  watch,
@@ -409,7 +485,7 @@ const Registry = ({
409
485
  if (written.modules.some((one) => !modules.has(one.module))) {
410
486
  return undefined;
411
487
  }
412
- if (orphans(address, written).length > 0) {
488
+ if (stranded(address, written).length > 0) {
413
489
  return undefined;
414
490
  }
415
491
  return adminMine.write('program', program) === undefined ? undefined : address;
@@ -422,7 +498,7 @@ const Registry = ({
422
498
  if (written === undefined) {
423
499
  return undefined;
424
500
  }
425
- if (orphans(target, written).length > 0) {
501
+ if (stranded(target, written).length > 0) {
426
502
  return undefined;
427
503
  }
428
504
  const held = adminMine.read(`identity/custody/${target}`);
@@ -443,12 +519,14 @@ const Registry = ({
443
519
  return target;
444
520
  },
445
521
  destroy: (at) => {
446
- if (adminMine.read(`identity/custody/${at}`) === undefined) {
522
+ const held = adminMine.read(`identity/custody/${at}`);
523
+ const voice = held === undefined ? undefined : Voice.revive(held);
524
+ if (voice === undefined) {
447
525
  return undefined;
448
526
  }
449
527
  adminMine.write(`identity/custody/${at}`, undefined);
450
528
  live.delete(at);
451
- forget(at);
529
+ forget(at, voice);
452
530
  return at;
453
531
  },
454
532
  residents: () => {
@@ -473,4 +551,4 @@ const Registry = ({
473
551
  });
474
552
  };
475
553
 
476
- export { Being, Compartment, Registry, SUCCESSION, make, rootFor };
554
+ export { Being, Compartment, Registry, SUCCESSION, make, rootFor, spend };
package/blueprint.mjs CHANGED
@@ -167,32 +167,127 @@ const merged = (base, added) =>
167
167
  ]),
168
168
  );
169
169
 
170
+ // Bare names, not Type.field pairs: X's "if create answers, it created"
171
+ // makes Query.create and Mutation.create one reserved word.
170
172
  const doorsIn = (sdl) =>
171
173
  parse(sdl)
172
- .definitions.filter(
173
- (one) => one.kind === Kind.OBJECT_TYPE_DEFINITION || one.kind === Kind.OBJECT_TYPE_EXTENSION,
174
- )
175
- .flatMap((one) => (one.fields ?? []).map((field) => `${one.name.value}.${field.name.value}`));
174
+ .definitions.filter((one) => doorHolder(one) && ['Mutation', 'Query'].includes(one.name.value))
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 rights = [
215
+ ...new Set([
216
+ ...STRUCTURAL,
217
+ ...(schema.getType('Rights')?.getValues() ?? []).map((one) => one.name),
218
+ ]),
219
+ ];
220
+ const found = [];
221
+ for (const [key, cell] of Object.entries(written.memory ?? {})) {
222
+ if (coreClass(key) !== undefined) {
223
+ found.push(`the cell ${key} is the core's own, and no program declares it (VIII)`);
224
+ } else if (!CLASSES.includes(cell?.class)) {
225
+ found.push(`the cell ${key} names no class the memory law holds: ${cell?.class} (VIII)`);
226
+ }
227
+ }
228
+ const bare = [];
229
+ const outside = new Map();
230
+ for (const field of rootsIn(document, schema)) {
231
+ const allowed = rightsOf(field);
232
+ if (allowed === undefined) {
233
+ bare.push(field.name.value);
234
+ continue;
235
+ }
236
+ for (const held of allowed.filter((one) => !rights.includes(one))) {
237
+ outside.set(held, [...(outside.get(held) ?? []), field.name.value]);
238
+ }
239
+ }
240
+ if (bare.length > 0) {
241
+ found.push(`the door ${bare.join(', ')} declares no rights, and every door declares them (VI)`);
242
+ }
243
+ for (const [held, doors] of outside) {
244
+ found.push(
245
+ `the Rights enum does not name ${held}, which ${doors.length} door${doors.length === 1 ? '' : 's'} stand behind: ${doors.join(', ')} (VI)`,
246
+ );
247
+ }
248
+ return found;
249
+ };
250
+
251
+ const composition = (baseSource, addedSource) => {
252
+ const refused = (reason) => ({ refused: reason });
178
253
  try {
179
254
  const base = evaluated(baseSource);
180
255
  const added = evaluated(addedSource);
256
+ if (added === null || typeof added !== 'object') {
257
+ return refused('the part settles no blueprint object (VIII)');
258
+ }
181
259
  const standard = doorsIn(base.interface);
182
- if (doorsIn(added.interface).some((one) => standard.includes(one))) {
183
- return undefined;
260
+ const collides = [...new Set(doorsIn(added.interface).filter((one) => standard.includes(one)))];
261
+ if (collides.length > 0) {
262
+ return refused(
263
+ `the part declares the door ${collides.join(', ')}, which the administration already names — a standard door name is reserved on both roots (X)`,
264
+ );
184
265
  }
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;
266
+ const kept = Object.keys(base.memory ?? {}).filter(
267
+ (key) => (added.memory ?? {})[key] !== undefined,
268
+ );
269
+ if (kept.length > 0) {
270
+ return refused(
271
+ `the part declares the cell ${kept.join(', ')}, which the administration already keeps (X)`,
272
+ );
273
+ }
274
+ if (added.needs?.config !== undefined && base.needs?.config !== undefined) {
275
+ return refused(
276
+ 'the part declares needs.config, and the program it composes with declares one too — a program holds one config (X)',
277
+ );
191
278
  }
192
279
  const slots = [...(base.needs?.modules ?? []), ...(added.needs?.modules ?? [])];
193
- if (new Set(slots.map((one) => one.module)).size !== slots.length) {
194
- return undefined;
280
+ const twice = [
281
+ ...new Set(
282
+ slots.map((one) => one.module).filter((name, at, all) => all.indexOf(name) !== at),
283
+ ),
284
+ ];
285
+ if (twice.length > 0) {
286
+ return refused(
287
+ `the part declares the module slot ${twice.join(', ')}, which the administration already holds (X)`,
288
+ );
195
289
  }
290
+ const cells = { ...(base.memory ?? {}), ...(added.memory ?? {}) };
196
291
  const needs = {
197
292
  ...(base.needs ?? {}),
198
293
  ...(added.needs ?? {}),
@@ -208,18 +303,22 @@ const composed = (baseSource, addedSource) => {
208
303
  .join(', ')} }`,
209
304
  )
210
305
  .join(', ');
211
- return `({
306
+ const source = `({
212
307
  name: ${JSON.stringify(added.name)},
213
308
  needs: ${JSON.stringify(needs)},
214
309
  memory: ${JSON.stringify(cells)},
215
310
  interface: ${JSON.stringify(`${base.interface}\n${added.interface}`)},
216
311
  resolvers: { ${written} },
217
312
  })`;
218
- } catch {
219
- return undefined;
313
+ const found = faults(source);
314
+ return found.length > 0 ? refused(found.join('; ')) : { source };
315
+ } catch (error) {
316
+ return refused(`the part does not compose: ${error.message} (VIII)`);
220
317
  }
221
318
  };
222
319
 
320
+ const composed = (baseSource, addedSource) => composition(baseSource, addedSource).source;
321
+
223
322
  const inputCheck = (schema, typeName, given) => {
224
323
  const type = schema.getType(typeName);
225
324
  if (type?.getFields === undefined) {
@@ -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
  }
@@ -319,23 +410,36 @@ const Blueprint = (source) => {
319
410
  rights,
320
411
  schema,
321
412
  // The directive definition stays: pointer.mjs runs buildSchema on this output.
322
- sdlFor: (held) =>
323
- print({
413
+ sdlFor: (held) => {
414
+ const kept = document.definitions
415
+ .map((one) =>
416
+ doorHolder(one)
417
+ ? {
418
+ ...one,
419
+ fields: (one.fields ?? []).filter((field) => {
420
+ const allowed = rightsOf(field);
421
+ return allowed === undefined || allowed.includes(held);
422
+ }),
423
+ }
424
+ : one,
425
+ )
426
+ .filter((one) => !doorHolder(one) || one.fields.length > 0);
427
+ const defined = new Set(
428
+ kept
429
+ .filter((one) => one.kind === Kind.OBJECT_TYPE_DEFINITION)
430
+ .map((one) => one.name.value),
431
+ );
432
+ // A filtered view where only a part's extension survives must still
433
+ // stand alone as a schema, or the right it serves opens nothing.
434
+ return print({
324
435
  ...document,
325
- definitions: document.definitions
326
- .map((one) =>
327
- doorHolder(one)
328
- ? {
329
- ...one,
330
- fields: (one.fields ?? []).filter((field) => {
331
- const allowed = rightsOf(field);
332
- return allowed === undefined || allowed.includes(held);
333
- }),
334
- }
335
- : one,
336
- )
337
- .filter((one) => !doorHolder(one) || one.fields.length > 0),
338
- }),
436
+ definitions: kept.map((one) =>
437
+ one.kind === Kind.OBJECT_TYPE_EXTENSION && !defined.has(one.name.value)
438
+ ? { ...one, kind: Kind.OBJECT_TYPE_DEFINITION }
439
+ : one,
440
+ ),
441
+ });
442
+ },
339
443
  trespass: [],
340
444
  });
341
445
  } catch {
@@ -343,4 +447,16 @@ const Blueprint = (source) => {
343
447
  }
344
448
  };
345
449
 
346
- 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
@@ -64,4 +64,4 @@ const Client = ({ voice, deliver, resolve = (one) => one }) => {
64
64
  });
65
65
  };
66
66
 
67
- export { Client };
67
+ export { Client, speaking };
package/ground.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ADMIN } from './admin.mjs';
2
- import { Being, Compartment, Registry, SUCCESSION, make, rootFor } from './being.mjs';
2
+ import { Being, Compartment, Registry, SUCCESSION, make, rootFor, spend } 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,6 +13,36 @@ const GROUND = 'Ground';
13
13
 
14
14
  const FLOOR = ['blueprints', 'cells', 'contracts', 'keychain', 'paths', 'watch', 'wire'];
15
15
 
16
+ const UNPROVEN = 'ground/unproven/';
17
+
18
+ const HEIR = 'ground/heir';
19
+
20
+ const commits = (cells, bundle) => {
21
+ const boxPk = bundle?.heirBoxPk;
22
+ if (typeof boxPk !== 'string' || Buffer.from(boxPk, 'base64').length !== 32) {
23
+ return undefined;
24
+ }
25
+ cells.put(HEIR, { boxPk, nextPkHash: bundle.nextPkHash });
26
+ return undefined;
27
+ };
28
+
29
+ const unprovenAt = (cells) => (at) => {
30
+ try {
31
+ const stood = cells.peek(`${UNPROVEN}${at}`);
32
+ const seen = Date.now();
33
+ cells.put(`${UNPROVEN}${at}`, {
34
+ at,
35
+ last: seen,
36
+ seen: (stood?.seen ?? 0) + 1,
37
+ since: stood?.since ?? seen,
38
+ });
39
+ } catch {
40
+ return undefined;
41
+ }
42
+ };
43
+
44
+ const unprovenRows = (cells) => cells.keys(UNPROVEN).map((row) => cells.peek(row));
45
+
16
46
  const install = ({ cells, keychain, program, bundle }) => {
17
47
  if (bundle?.next !== undefined) {
18
48
  return undefined;
@@ -29,6 +59,7 @@ const install = ({ cells, keychain, program, bundle }) => {
29
59
  }
30
60
  make({ memory, voice, address, program });
31
61
  cells.put('ground/admin', { address, sealed });
62
+ commits(cells, bundle);
32
63
  return address;
33
64
  };
34
65
 
@@ -72,10 +103,13 @@ const succeeds = ({ adminMine, adminVoice, address, bundle, cells, keychain, mem
72
103
  return undefined;
73
104
  }
74
105
  cells.put('ground/admin', { address, sealed });
106
+ commits(cells, bundle);
75
107
  return Tools.address(heir.pk);
76
108
  };
77
109
 
78
- const Ground = (modules = {}) => {
110
+ // Never a key in the modules object: everything there that is not in FLOOR
111
+ // becomes a module handed to programs under that name.
112
+ const Ground = (modules = {}, { span } = {}) => {
79
113
  const cells = modules.cells;
80
114
  const keychain = modules.keychain;
81
115
  if (cells === undefined || keychain?.seal === undefined || keychain?.unseal === undefined) {
@@ -121,7 +155,7 @@ const Ground = (modules = {}) => {
121
155
  };
122
156
 
123
157
  const address = installed.address;
124
- const memory = Memory(cells);
158
+ const memory = Memory(cells, unprovenAt(cells));
125
159
  const paths = modules.paths ?? Paths(new Map());
126
160
  const wire = modules.wire ?? Wire();
127
161
 
@@ -185,9 +219,13 @@ const Ground = (modules = {}) => {
185
219
  return door(envelope, token);
186
220
  };
187
221
  const announce = (at) => paths.publish(at, receive);
188
- const forget = (at) => {
222
+ const forget = (at, voice) => {
189
223
  paths.publish(at, undefined);
190
- cells.drop(`${at}/`);
224
+ if (voice === undefined) {
225
+ cells.drop(`${at}/`);
226
+ return undefined;
227
+ }
228
+ return spend({ memory, voice, address: at });
191
229
  };
192
230
 
193
231
  const attest = async (name) => {
@@ -213,6 +251,7 @@ const Ground = (modules = {}) => {
213
251
  modulesFor,
214
252
  announce,
215
253
  forget,
254
+ span,
216
255
  vouched,
217
256
  attest,
218
257
  watch,
@@ -223,6 +262,7 @@ const Ground = (modules = {}) => {
223
262
  deliver: (envelope) => deliver(envelope),
224
263
  modules: modulesFor(adminVoice, adminDeclared()),
225
264
  registry,
265
+ span,
226
266
  vouched,
227
267
  voice: adminVoice,
228
268
  watch,
@@ -268,6 +308,14 @@ const Ground = (modules = {}) => {
268
308
  },
269
309
  });
270
310
  },
311
+ seated: (at, from) => {
312
+ if (at === from) {
313
+ return true;
314
+ }
315
+ const row = held(at) ? memory.read(at, `refs/${from}`)?.value : undefined;
316
+ return row !== undefined && row !== null;
317
+ },
318
+ unproven: () => unprovenRows(cells),
271
319
  });
272
320
  };
273
321
 
package/index.mjs CHANGED
@@ -1,10 +1,20 @@
1
1
  export { ADMIN } from './admin.mjs';
2
- export { Blueprint, CLASSES, CORE_ONLY, OPEN, STRUCTURAL, coreClass, reads } from './blueprint.mjs';
3
- export { Client } from './client.mjs';
2
+ export {
3
+ Blueprint,
4
+ CLASSES,
5
+ CORE_ONLY,
6
+ OPEN,
7
+ STRUCTURAL,
8
+ composition,
9
+ coreClass,
10
+ faults,
11
+ reads,
12
+ } from './blueprint.mjs';
13
+ export { Client, speaking } from './client.mjs';
4
14
  export { Contract, Contracts } from './contract.mjs';
5
15
  export { FLOOR, GROUND, Ground } from './ground.mjs';
6
16
  export { Cells, Clock, Dns, Entropy, Keychain, Memory, Paths, Wire } from './modules.mjs';
7
17
  export { Mcp, Pointer } from './pointer.mjs';
8
18
  export { Program } from './program.mjs';
9
- export { MEASURE, Stamp, Tools } from './tools.mjs';
19
+ export { MEASURE, SPAN, Stamp, Tools } from './tools.mjs';
10
20
  export { Voice } from './voice.mjs';
package/modules.mjs CHANGED
@@ -28,7 +28,7 @@ const Cells = ({ file } = {}) => {
28
28
  });
29
29
  };
30
30
 
31
- const Memory = (cells) => {
31
+ const Memory = (cells, unproven) => {
32
32
  const held = (address, key) => cells.peek(`${address}/${key}`);
33
33
  const writerAt = (address) => {
34
34
  const chain = held(address, 'chain/pk');
@@ -64,6 +64,7 @@ const Memory = (cells) => {
64
64
  return undefined;
65
65
  }
66
66
  if (!Tools.verifies(writer, Tools.canon({ address, key, value, version }), sig)) {
67
+ unproven?.(address);
67
68
  return undefined;
68
69
  }
69
70
  cells.put(`${address}/${key}`, { value, version });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nervur",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "A distributed system of sovereign beings — one door, one question: by whose authority.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Razvan Gherghina",
package/program.mjs CHANGED
@@ -4,6 +4,8 @@ import {
4
4
  getIntrospectionQuery,
5
5
  graphql,
6
6
  isObjectType,
7
+ parse,
8
+ validate,
7
9
  } from 'graphql';
8
10
  import { Blueprint } from './blueprint.mjs';
9
11
 
@@ -41,8 +43,39 @@ const Program = (source) => {
41
43
  }
42
44
  return curtained.get(held);
43
45
  };
46
+ // graphql's validator demands a Query root even to judge a mutation, so an
47
+ // all-mutation filtered world validates against a copy wearing an empty one
48
+ // — never printed, never executed against.
49
+ const askable = new Map();
50
+ const askableBy = (held) => {
51
+ if (!askable.has(held)) {
52
+ const seen = seenBy(held);
53
+ try {
54
+ askable.set(
55
+ held,
56
+ seen === undefined || seen.getQueryType() != null
57
+ ? seen
58
+ : buildSchema(`${blueprint.sdlFor(held)}\ntype Query { _stands: Boolean }`),
59
+ );
60
+ } catch {
61
+ askable.set(held, undefined);
62
+ }
63
+ }
64
+ return askable.get(held);
65
+ };
44
66
  return Object.freeze({
45
67
  ask: async ({ source: asked, variables, context }) => {
68
+ const seen = askableBy(context.rights);
69
+ if (seen === undefined) {
70
+ return undefined;
71
+ }
72
+ try {
73
+ if (validate(seen, parse(asked)).length > 0) {
74
+ return undefined;
75
+ }
76
+ } catch {
77
+ return undefined;
78
+ }
46
79
  const answered = await graphql({
47
80
  contextValue: context,
48
81
  schema,
package/tools.mjs CHANGED
@@ -15,6 +15,8 @@ const VERSION_G = 6 << 3;
15
15
 
16
16
  const MEASURE = 8_388_608;
17
17
 
18
+ const SPAN = 60_000;
19
+
18
20
  const ordered = (value) =>
19
21
  value === null || typeof value !== 'object'
20
22
  ? value
@@ -138,4 +140,4 @@ const Stamp = () => {
138
140
  };
139
141
  };
140
142
 
141
- export { MEASURE, Stamp, Tools };
143
+ export { MEASURE, SPAN, Stamp, Tools };