nervur 0.8.0 → 0.8.2

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
@@ -313,6 +313,62 @@ MEMBER — once, and never again. `Clock` and `Entropy` are not floor: they
313
313
  are ordinary modules a program declares, pinnable so a program never
314
314
  reaches for the wall.
315
315
 
316
+ ## Extending the administration
317
+
318
+ A ground's administration is a being like any other, and the only one whose
319
+ context holds the registry — so a door that must create, upgrade or destroy
320
+ beings belongs on it and nowhere else. Adopters extend it; nobody replaces
321
+ it.
322
+
323
+ Pass an extension through the `blueprints` module and the ground composes it
324
+ with the standard administration. Write it as an ordinary blueprint whose
325
+ schema **extends** the roots:
326
+
327
+ ```js
328
+ const cases = `({
329
+ name: 'procese.cases',
330
+ needs: { caller: true, modules: [{ module: 'memory', contract: 'nervur.memory' }] },
331
+ memory: { 'case/*': { type: 'String', class: 'data' } },
332
+ interface: \`
333
+ extend type Mutation {
334
+ openCase(title: String!): String @rights(is: [SELF, MEMBER])
335
+ }
336
+ \`,
337
+ resolvers: {
338
+ Mutation: {
339
+ openCase: (_, { title }, { from, modules, registry }) => {
340
+ const at = registry.create({ program: CASE, refs: [{ address: from }] });
341
+ modules.memory.write('case/' + at, title);
342
+ return at ?? null;
343
+ },
344
+ },
345
+ },
346
+ })`;
347
+
348
+ const ground = Ground({ cells, keychain, blueprints: { ground: () => cases } });
349
+ ```
350
+
351
+ The composed program is one blueprint with one digest, so `describe` answers
352
+ what this ground's administration actually is, and an outsider can pin it.
353
+
354
+ Three rules hold, and each refuses the whole ground rather than bending:
355
+
356
+ 1. **Standard door names are reserved.** Declare `create`, `census`,
357
+ `upgrade`, `destroy`, `alias`, `attest`, `opened` or `init` and no ground
358
+ rises. You cannot change what a standard door means — which is why a
359
+ stranger may trust that if `create` answers, it created.
360
+ 2. **Add only.** Use `extend type Query` / `extend type Mutation`; cell
361
+ names, module slots and `needs.config` may not collide with the
362
+ administration's own.
363
+ 3. **Close a door the way anything is closed here** — fence it behind a
364
+ right the ground never grants. The door stands, means what the
365
+ constitution says, and answers nobody: that is how an appliance ground
366
+ stops creating beings, without redefining a thing.
367
+
368
+ The acts stay the core's own: an extension reaches them through the same
369
+ registry face the standard doors use, so it can invent no power the standard
370
+ administration lacked.
371
+
316
372
  ## Writing a module
317
373
 
318
374
  Five rules, each load-bearing:
package/blueprint.mjs CHANGED
@@ -80,6 +80,9 @@ const RESERVED = ['eval', 'import', '__proto__', 'prototype', 'Math.random'];
80
80
 
81
81
  const SHADOWED = FORBIDDEN.map(([name]) => name).filter((name) => !RESERVED.includes(name));
82
82
 
83
+ const doorHolder = (one) =>
84
+ one.kind === Kind.OBJECT_TYPE_DEFINITION || one.kind === Kind.OBJECT_TYPE_EXTENSION;
85
+
83
86
  const RIGHTS_DIRECTIVE = 'directive @rights(is: [Rights!]!) on FIELD_DEFINITION';
84
87
 
85
88
  const DEFAULT_RIGHTS = `enum Rights { SELF MEMBER STRANGER }`;
@@ -156,6 +159,67 @@ const resolverSources = (resolvers) =>
156
159
  .map((field) => `${type}.${field}=${resolvers[type][field].toString()}`),
157
160
  );
158
161
 
162
+ const merged = (base, added) =>
163
+ Object.fromEntries(
164
+ [...new Set([...Object.keys(base ?? {}), ...Object.keys(added ?? {})])].map((key) => [
165
+ key,
166
+ { ...(base ?? {})[key], ...(added ?? {})[key] },
167
+ ]),
168
+ );
169
+
170
+ const doorsIn = (sdl) =>
171
+ 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}`));
176
+
177
+ const composed = (baseSource, addedSource) => {
178
+ try {
179
+ const base = evaluated(baseSource);
180
+ const added = evaluated(addedSource);
181
+ const standard = doorsIn(base.interface);
182
+ if (doorsIn(added.interface).some((one) => standard.includes(one))) {
183
+ return undefined;
184
+ }
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;
191
+ }
192
+ const slots = [...(base.needs?.modules ?? []), ...(added.needs?.modules ?? [])];
193
+ if (new Set(slots.map((one) => one.module)).size !== slots.length) {
194
+ return undefined;
195
+ }
196
+ const needs = {
197
+ ...(base.needs ?? {}),
198
+ ...(added.needs ?? {}),
199
+ caller: base.needs?.caller === true || added.needs?.caller === true,
200
+ modules: slots,
201
+ };
202
+ const doors = merged(base.resolvers, added.resolvers);
203
+ const written = Object.entries(doors)
204
+ .map(
205
+ ([type, fields]) =>
206
+ `${type}: { ${Object.entries(fields)
207
+ .map(([field, one]) => `${field}: ${one.toString()}`)
208
+ .join(', ')} }`,
209
+ )
210
+ .join(', ');
211
+ return `({
212
+ name: ${JSON.stringify(added.name)},
213
+ needs: ${JSON.stringify(needs)},
214
+ memory: ${JSON.stringify(cells)},
215
+ interface: ${JSON.stringify(`${base.interface}\n${added.interface}`)},
216
+ resolvers: { ${written} },
217
+ })`;
218
+ } catch {
219
+ return undefined;
220
+ }
221
+ };
222
+
159
223
  const inputCheck = (schema, typeName, given) => {
160
224
  const type = schema.getType(typeName);
161
225
  if (type?.getFields === undefined) {
@@ -189,10 +253,10 @@ const Blueprint = (source) => {
189
253
  ];
190
254
 
191
255
  const admits = (typeName, fieldName, held) => {
192
- const type = document.definitions.find(
193
- (one) => one.kind === Kind.OBJECT_TYPE_DEFINITION && one.name.value === typeName,
194
- );
195
- const field = type?.fields?.find((one) => one.name.value === fieldName);
256
+ const field = document.definitions
257
+ .filter((one) => doorHolder(one) && one.name.value === typeName)
258
+ .flatMap((one) => one.fields ?? [])
259
+ .find((one) => one.name.value === fieldName);
196
260
  const allowed = field === undefined ? undefined : rightsOf(field);
197
261
  return allowed === undefined ? true : allowed.includes(held);
198
262
  };
@@ -212,7 +276,7 @@ const Blueprint = (source) => {
212
276
  schema.getSubscriptionType()?.name,
213
277
  ].filter((one) => one !== undefined);
214
278
  const undeclared = document.definitions
215
- .filter((one) => one.kind === Kind.OBJECT_TYPE_DEFINITION && roots.includes(one.name.value))
279
+ .filter((one) => doorHolder(one) && roots.includes(one.name.value))
216
280
  .flatMap((one) => one.fields ?? [])
217
281
  .filter((one) => {
218
282
  const allowed = rightsOf(one);
@@ -260,17 +324,17 @@ const Blueprint = (source) => {
260
324
  ...document,
261
325
  definitions: document.definitions
262
326
  .map((one) =>
263
- one.kind === Kind.OBJECT_TYPE_DEFINITION
327
+ doorHolder(one)
264
328
  ? {
265
329
  ...one,
266
- fields: one.fields.filter((field) => {
330
+ fields: (one.fields ?? []).filter((field) => {
267
331
  const allowed = rightsOf(field);
268
332
  return allowed === undefined || allowed.includes(held);
269
333
  }),
270
334
  }
271
335
  : one,
272
336
  )
273
- .filter((one) => one.kind !== Kind.OBJECT_TYPE_DEFINITION || one.fields.length > 0),
337
+ .filter((one) => !doorHolder(one) || one.fields.length > 0),
274
338
  }),
275
339
  trespass: [],
276
340
  });
@@ -279,4 +343,4 @@ const Blueprint = (source) => {
279
343
  }
280
344
  };
281
345
 
282
- export { Blueprint, CLASSES, CORE_ONLY, FORBIDDEN, OPEN, STRUCTURAL, coreClass, reads };
346
+ export { Blueprint, CLASSES, CORE_ONLY, FORBIDDEN, OPEN, STRUCTURAL, composed, coreClass, reads };
package/ground.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ADMIN } from './admin.mjs';
2
2
  import { Being, Compartment, Registry, make, rootFor } from './being.mjs';
3
- import { Blueprint, coreClass } from './blueprint.mjs';
3
+ import { Blueprint, composed, coreClass } from './blueprint.mjs';
4
4
  import { Client } from './client.mjs';
5
5
  import { Contracts } from './contract.mjs';
6
6
  import { Memory, Paths, Wire } from './modules.mjs';
@@ -13,22 +13,6 @@ const GROUND = 'Ground';
13
13
 
14
14
  const FLOOR = ['blueprints', 'cells', 'contracts', 'keychain', 'paths', 'watch', 'wire'];
15
15
 
16
- const doorsOf = (source) => {
17
- const written = Blueprint(source);
18
- if (written === undefined) {
19
- return undefined;
20
- }
21
- return [written.schema.getQueryType(), written.schema.getMutationType()]
22
- .filter((one) => one !== undefined && one !== null)
23
- .flatMap((one) => Object.keys(one.getFields()));
24
- };
25
-
26
- const redefines = (source) => {
27
- const custom = doorsOf(source);
28
- const standard = doorsOf(ADMIN);
29
- return custom === undefined || custom.some((one) => standard.includes(one));
30
- };
31
-
32
16
  const install = ({ cells, keychain, program }) => {
33
17
  const memory = Memory(cells);
34
18
  const voice = Voice();
@@ -58,10 +42,10 @@ const Ground = (modules = {}) => {
58
42
  }
59
43
 
60
44
  const extension = modules.blueprints?.ground();
61
- if (extension !== undefined && redefines(extension)) {
45
+ const program = extension === undefined ? ADMIN : composed(ADMIN, extension);
46
+ if (program === undefined || Blueprint(program)?.digest === undefined) {
62
47
  return undefined;
63
48
  }
64
- const program = extension ?? ADMIN;
65
49
  if (cells.peek('ground/admin') === undefined) {
66
50
  install({ cells, keychain, program });
67
51
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nervur",
3
- "version": "0.8.0",
4
- "description": "A distributed system of sovereign beings — one door, one question: by whose authority.",
3
+ "version": "0.8.2",
4
+ "description": "A distributed system of sovereign beings \u2014 one door, one question: by whose authority.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Razvan Gherghina",
7
7
  "homepage": "https://nervur.org",