nervur 0.9.0 → 0.11.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/admin.mjs CHANGED
@@ -13,8 +13,8 @@ const ADMIN = `({
13
13
  opened: Boolean @rights(is: [SELF, MEMBER, STRANGER])
14
14
  }
15
15
  type Mutation {
16
- invite(address: String!, nextPkHash: String, rights: String): Boolean @rights(is: [SELF])
17
- drop(address: String!): Boolean @rights(is: [SELF])
16
+ invite(address: String!, nextPkHash: String, rights: String): Boolean @rights(is: [SELF, MEMBER])
17
+ drop(address: String!): Boolean @rights(is: [SELF, MEMBER])
18
18
  extend(program: String): Boolean @rights(is: [SELF])
19
19
  create(program: String!, refs: [RefInput!], data: String, config: String): String
20
20
  @rights(is: [SELF, MEMBER])
package/being.mjs CHANGED
@@ -143,7 +143,10 @@ const rotates = ({ mine }, from, nextPkHash) => {
143
143
  .list('refs/')
144
144
  .map((key) => ({ key, referred: mine.read(key) }))
145
145
  .find(({ referred }) => referred?.nextPkHash === claim);
146
- if (held === undefined) {
146
+ if (
147
+ held === undefined ||
148
+ (held.key !== `refs/${from}` && mine.read(`refs/${from}`) !== undefined)
149
+ ) {
147
150
  return undefined;
148
151
  }
149
152
  mine.write(`refs/${from}`, { nextPkHash, rights: held.referred.rights });
@@ -261,7 +264,7 @@ const Being = ({ voice, address, deliver, modules, registry, vouched, watch }) =
261
264
  });
262
265
  return answer({ referred: from, rights: pending.rights });
263
266
  }
264
- if (op?.ask === undefined || program === undefined) {
267
+ if (op?.ask === undefined || program === undefined || !program.visible(rights)) {
265
268
  return undefined;
266
269
  }
267
270
  const asked = await program.ask({
@@ -336,10 +339,13 @@ const Registry = ({
336
339
  if (voice === undefined) {
337
340
  return undefined;
338
341
  }
342
+ const declared = (Program(memory.read(at, 'program')?.value)?.modules ?? []).map(
343
+ (one) => one.module,
344
+ );
339
345
  const door = Being({
340
346
  address: at,
341
347
  deliver,
342
- modules: modulesFor(voice),
348
+ modules: modulesFor(voice, declared),
343
349
  vouched,
344
350
  voice,
345
351
  watch,
@@ -355,7 +361,7 @@ const Registry = ({
355
361
  if (program !== undefined && written === undefined) {
356
362
  return undefined;
357
363
  }
358
- const missing = (written?.modules ?? []).filter((one) => modules[one.module] === undefined);
364
+ const missing = (written?.modules ?? []).filter((one) => !modules.has(one.module));
359
365
  if (missing.length > 0) {
360
366
  return undefined;
361
367
  }
@@ -403,7 +409,7 @@ const Registry = ({
403
409
  if (written === undefined) {
404
410
  return undefined;
405
411
  }
406
- if (written.modules.some((one) => modules[one.module] === undefined)) {
412
+ if (written.modules.some((one) => !modules.has(one.module))) {
407
413
  return undefined;
408
414
  }
409
415
  if (orphans(address, written).length > 0) {
package/blueprint.mjs CHANGED
@@ -167,12 +167,12 @@ 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
177
  const composed = (baseSource, addedSource) => {
178
178
  try {
@@ -319,23 +319,36 @@ const Blueprint = (source) => {
319
319
  rights,
320
320
  schema,
321
321
  // The directive definition stays: pointer.mjs runs buildSchema on this output.
322
- sdlFor: (held) =>
323
- print({
322
+ sdlFor: (held) => {
323
+ const kept = document.definitions
324
+ .map((one) =>
325
+ doorHolder(one)
326
+ ? {
327
+ ...one,
328
+ fields: (one.fields ?? []).filter((field) => {
329
+ const allowed = rightsOf(field);
330
+ return allowed === undefined || allowed.includes(held);
331
+ }),
332
+ }
333
+ : one,
334
+ )
335
+ .filter((one) => !doorHolder(one) || one.fields.length > 0);
336
+ const defined = new Set(
337
+ kept
338
+ .filter((one) => one.kind === Kind.OBJECT_TYPE_DEFINITION)
339
+ .map((one) => one.name.value),
340
+ );
341
+ // A filtered view where only a part's extension survives must still
342
+ // stand alone as a schema, or the right it serves opens nothing.
343
+ return print({
324
344
  ...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
- }),
345
+ definitions: kept.map((one) =>
346
+ one.kind === Kind.OBJECT_TYPE_EXTENSION && !defined.has(one.name.value)
347
+ ? { ...one, kind: Kind.OBJECT_TYPE_DEFINITION }
348
+ : one,
349
+ ),
350
+ });
351
+ },
339
352
  trespass: [],
340
353
  });
341
354
  } catch {
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
@@ -93,7 +93,8 @@ const Ground = (modules = {}) => {
93
93
 
94
94
  const extension = modules.blueprints?.ground();
95
95
  const program = extension === undefined ? ADMIN : composed(ADMIN, extension);
96
- if (program === undefined || Blueprint(program)?.digest === undefined) {
96
+ const blueprint = program === undefined ? undefined : Blueprint(program);
97
+ if (blueprint?.digest === undefined) {
97
98
  return undefined;
98
99
  }
99
100
  const installed = cells.peek('ground/admin');
@@ -141,18 +142,24 @@ const Ground = (modules = {}) => {
141
142
  typeof one?.scope === 'function' ? one.scope(address) : one,
142
143
  ]),
143
144
  );
144
- const modulesFor = (voice) =>
145
+ const declaredOnly = (obj, declared) =>
146
+ Object.fromEntries(Object.entries(obj).filter(([name]) => declared.includes(name)));
147
+ const modulesFor = (voice, declared = []) =>
145
148
  Object.freeze(
146
149
  introduced(
147
150
  {
148
- ...extra,
149
- ...boundFor(voice),
151
+ ...declaredOnly(extra, declared),
152
+ ...declaredOnly(boundFor(voice), declared),
150
153
  memory,
151
154
  },
152
155
  Tools.address(voice.pk),
153
156
  ),
154
157
  );
155
- const adminModules = modulesFor(adminVoice);
158
+ const offeredNames = new Set([...Object.keys(extra), ...bound.map(([name]) => name), 'memory']);
159
+ const adminDeclared = () =>
160
+ (Program(memory.read(address, 'program')?.value)?.modules ?? blueprint.modules).map(
161
+ (one) => one.module,
162
+ );
156
163
 
157
164
  const adminMine = Compartment({
158
165
  memory,
@@ -185,8 +192,11 @@ const Ground = (modules = {}) => {
185
192
 
186
193
  const attest = async (name) => {
187
194
  const one = extra[name];
188
- if (one?.at === undefined) {
189
- return adminModules[name] !== undefined;
195
+ if (one === undefined) {
196
+ return false;
197
+ }
198
+ if (one.at === undefined) {
199
+ return null;
190
200
  }
191
201
  const seen = await deliver(Tools.envelope(adminVoice, one.at, { describe: {} }, stamp()));
192
202
  return seen?.body?.sdl === undefined
@@ -199,7 +209,7 @@ const Ground = (modules = {}) => {
199
209
  adminMine,
200
210
  address,
201
211
  deliver: (envelope) => deliver(envelope),
202
- modules: adminModules,
212
+ modules: offeredNames,
203
213
  modulesFor,
204
214
  announce,
205
215
  forget,
@@ -211,7 +221,7 @@ const Ground = (modules = {}) => {
211
221
  Being({
212
222
  address,
213
223
  deliver: (envelope) => deliver(envelope),
214
- modules: adminModules,
224
+ modules: modulesFor(adminVoice, adminDeclared()),
215
225
  registry,
216
226
  vouched,
217
227
  voice: adminVoice,
package/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  export { ADMIN } from './admin.mjs';
2
2
  export { Blueprint, CLASSES, CORE_ONLY, OPEN, STRUCTURAL, coreClass, reads } from './blueprint.mjs';
3
- export { Client } from './client.mjs';
3
+ export { Client, speaking } from './client.mjs';
4
4
  export { Contract, Contracts } from './contract.mjs';
5
- export { GROUND, Ground } from './ground.mjs';
5
+ export { FLOOR, GROUND, Ground } from './ground.mjs';
6
6
  export { Cells, Clock, Dns, Entropy, Keychain, Memory, Paths, Wire } from './modules.mjs';
7
7
  export { Mcp, Pointer } from './pointer.mjs';
8
8
  export { Program } from './program.mjs';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nervur",
3
- "version": "0.9.0",
4
- "description": "A distributed system of sovereign beings \u2014 one door, one question: by whose authority.",
3
+ "version": "0.11.0",
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",
7
7
  "homepage": "https://nervur.org",
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,