nervur 0.13.0 → 0.14.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
@@ -92,7 +92,7 @@ const custodial = (registry, fence) =>
92
92
  upgrade: (one) => (fence.open() ? registry.upgrade(one) : undefined),
93
93
  });
94
94
 
95
- const offered = (modules, program, mine) =>
95
+ const offered = (modules, program, mine, seat) =>
96
96
  Object.freeze({
97
97
  ...Object.fromEntries(
98
98
  (program?.modules ?? [])
@@ -100,19 +100,47 @@ const offered = (modules, program, mine) =>
100
100
  .filter((name) => name !== 'memory' && modules[name] !== undefined)
101
101
  .map((name) => [name, modules[name]]),
102
102
  ),
103
- memory: scoped(mine, program),
103
+ memory: scoped(mine, program, seat),
104
104
  });
105
105
 
106
106
  const grantable = (program) =>
107
107
  (program?.rights ?? [...STRUCTURAL, 'MEMBER']).filter((one) => !STRUCTURAL.includes(one));
108
108
 
109
- const referable = (mine, key, plain, program) => {
109
+ const HORIZON = 7_776_000_000;
110
+
111
+ const whole = (one) => Number.isSafeInteger(one) && one >= 0;
112
+
113
+ const invitation = (plain, rights, program, seat) => {
114
+ const count = plain.count ?? 1;
115
+ const expiresAt = plain.expiresAt ?? 0;
116
+ if (!whole(count) || count < 1 || !whole(expiresAt)) {
117
+ return undefined;
118
+ }
119
+ const now = seat.now();
120
+ if (seat.administration) {
121
+ return count === 1 && expiresAt > 0 && expiresAt < now + HORIZON
122
+ ? { ...plain, count, expiresAt, rights }
123
+ : undefined;
124
+ }
125
+ const ceiling = program?.invites?.(rights);
126
+ if (ceiling !== undefined) {
127
+ if (ceiling.count > 0 && count > ceiling.count) {
128
+ return undefined;
129
+ }
130
+ if (ceiling.within > 0 && (expiresAt === 0 || expiresAt > now + ceiling.within)) {
131
+ return undefined;
132
+ }
133
+ }
134
+ return { ...plain, count, expiresAt, rights };
135
+ };
136
+
137
+ const referable = (mine, key, plain, program, seat) => {
110
138
  const rights = plain.rights ?? 'MEMBER';
111
139
  if (!grantable(program).includes(rights)) {
112
140
  return undefined;
113
141
  }
114
142
  if (key.startsWith('refs/pending/')) {
115
- return { ...plain, rights };
143
+ return invitation(plain, rights, program, seat);
116
144
  }
117
145
  const held = mine.read(key);
118
146
  if (
@@ -131,14 +159,14 @@ const referable = (mine, key, plain, program) => {
131
159
  };
132
160
  };
133
161
 
134
- const scoped = (mine, program) =>
162
+ const scoped = (mine, program, seat) =>
135
163
  Object.freeze({
136
164
  classOf: mine.classOf,
137
165
  list: mine.list,
138
166
  read: mine.read,
139
167
  write: (key, plain) => {
140
168
  if (key.startsWith('refs/') && plain !== undefined) {
141
- const guarded = referable(mine, key, plain, program);
169
+ const guarded = referable(mine, key, plain, program, seat);
142
170
  return guarded === undefined ? undefined : mine.write(key, guarded);
143
171
  }
144
172
  return key.startsWith('refs/') ||
@@ -232,7 +260,18 @@ const successionOf = (memory, address) =>
232
260
  .filter((one) => one !== undefined)
233
261
  .sort((one, other) => one.v - other.v);
234
262
 
235
- const Being = ({ voice, address, cipher, deliver, modules, registry, span, vouched, watch }) => {
263
+ const Being = ({
264
+ voice,
265
+ address,
266
+ cipher,
267
+ deliver,
268
+ modules,
269
+ registry,
270
+ administration,
271
+ span,
272
+ vouched,
273
+ watch,
274
+ }) => {
236
275
  const held = spanOf(span);
237
276
  const memory = modules.memory;
238
277
  const root = rootFor({ memory, voice, address, cipher });
@@ -248,6 +287,7 @@ const Being = ({ voice, address, cipher, deliver, modules, registry, span, vouch
248
287
  });
249
288
  const succession = successionOf(memory, address);
250
289
  const stamp = Stamp();
290
+ const seat = Object.freeze({ administration: administration === true, now: stamp });
251
291
  const lanes = new Map();
252
292
  const answer = (spoken, claim) => {
253
293
  const body = Tools.canon(spoken);
@@ -331,10 +371,15 @@ const Being = ({ voice, address, cipher, deliver, modules, registry, span, vouch
331
371
  if (op?.acceptInvite !== undefined) {
332
372
  const offer = Tools.digest(Bytes.unbase64(op.acceptInvite.proof));
333
373
  const pending = mine.read(`refs/pending/${offer}`);
334
- if (pending === undefined || pending.spent) {
374
+ const now = stamp();
375
+ if (
376
+ pending === undefined ||
377
+ !(pending.count > 0) ||
378
+ (pending.expiresAt > 0 && now > pending.expiresAt)
379
+ ) {
335
380
  return undefined;
336
381
  }
337
- mine.write(`refs/pending/${offer}`, { spent: true });
382
+ mine.write(`refs/pending/${offer}`, { ...pending, count: pending.count - 1 });
338
383
  mine.write(`refs/${from}`, {
339
384
  nextPkHash: op.acceptInvite.nextPkHash ?? null,
340
385
  rights: pending.rights,
@@ -352,7 +397,7 @@ const Being = ({ voice, address, cipher, deliver, modules, registry, span, vouch
352
397
  program.ask({
353
398
  context: {
354
399
  from: program.caller ? from : undefined,
355
- modules: offered(modules, program, turn),
400
+ modules: offered(modules, program, turn, seat),
356
401
  port: Port({ voice, mine: turn, deliver, fence, lanes, rights, stamp }),
357
402
  registry: custodial(registry, fence),
358
403
  rights,
package/blueprint.mjs CHANGED
@@ -85,6 +85,8 @@ const doorHolder = (one) =>
85
85
 
86
86
  const RIGHTS_DIRECTIVE = 'directive @rights(is: [Rights!]!) on FIELD_DEFINITION';
87
87
 
88
+ const INVITES_DIRECTIVE = 'directive @invites(count: Int!, within: Int!) on ENUM_VALUE';
89
+
88
90
  const DEFAULT_RIGHTS = `enum Rights { SELF MEMBER STRANGER }`;
89
91
 
90
92
  const codeOnly = (source) => {
@@ -150,6 +152,30 @@ const rightsOf = (field) => {
150
152
  : listed.value.values.map((one) => one.value ?? one.name?.value);
151
153
  };
152
154
 
155
+ const whole = (one) => Number.isSafeInteger(one) && one >= 0;
156
+
157
+ const ceilingOf = (value) => {
158
+ const marked = value.directives?.find((one) => one.name.value === 'invites');
159
+ if (marked === undefined) {
160
+ return undefined;
161
+ }
162
+ const argOf = (name) => {
163
+ const given = marked.arguments?.find((one) => one.name.value === name)?.value?.value;
164
+ return given === undefined ? undefined : Number(given);
165
+ };
166
+ return { count: argOf('count'), within: argOf('within') };
167
+ };
168
+
169
+ const ceilingsIn = (document) =>
170
+ new Map(
171
+ document.definitions
172
+ .filter((one) => one.kind === Kind.ENUM_TYPE_DEFINITION && one.name.value === 'Rights')
173
+ .flatMap((one) => one.values ?? [])
174
+ .map((value) => [value.name.value, ceilingOf(value)])
175
+ .filter(([, ceiling]) => ceiling !== undefined)
176
+ .map(([name, ceiling]) => [name, Object.freeze(ceiling)]),
177
+ );
178
+
153
179
  const resolverSources = (resolvers) =>
154
180
  Object.keys(resolvers)
155
181
  .sort()
@@ -202,7 +228,9 @@ const faults = (source) => {
202
228
  return ['it settles no blueprint object (VIII)'];
203
229
  }
204
230
  const declares = /enum\s+Rights\b/.test(written.interface) ? '' : DEFAULT_RIGHTS;
205
- const sdl = [RIGHTS_DIRECTIVE, declares, written.interface].filter(Boolean).join('\n');
231
+ const sdl = [RIGHTS_DIRECTIVE, INVITES_DIRECTIVE, declares, written.interface]
232
+ .filter(Boolean)
233
+ .join('\n');
206
234
  let document;
207
235
  let schema;
208
236
  try {
@@ -241,6 +269,17 @@ const faults = (source) => {
241
269
  if (bare.length > 0) {
242
270
  found.push(`the door ${bare.join(', ')} declares no rights, and every door declares them (VI)`);
243
271
  }
272
+ for (const [right, ceiling] of ceilingsIn(document)) {
273
+ if (STRUCTURAL.includes(right)) {
274
+ found.push(
275
+ `${right} carries an @invites ceiling, and a structural right is never invited into (VI)`,
276
+ );
277
+ } else if (!whole(ceiling.count) || !whole(ceiling.within)) {
278
+ found.push(
279
+ `${right}'s @invites ceiling is not two whole numbers of zero or more (VI): count ${ceiling.count}, within ${ceiling.within}`,
280
+ );
281
+ }
282
+ }
244
283
  for (const [held, doors] of outside) {
245
284
  found.push(
246
285
  `the Rights enum does not name ${held}, which ${doors.length} door${doors.length === 1 ? '' : 's'} stand behind: ${doors.join(', ')} (VI)`,
@@ -342,7 +381,9 @@ const Blueprint = (source) => {
342
381
  try {
343
382
  const written = evaluated(source);
344
383
  const declares = /enum\s+Rights\b/.test(written.interface) ? '' : DEFAULT_RIGHTS;
345
- const sdl = [RIGHTS_DIRECTIVE, declares, written.interface].filter(Boolean).join('\n');
384
+ const sdl = [RIGHTS_DIRECTIVE, INVITES_DIRECTIVE, declares, written.interface]
385
+ .filter(Boolean)
386
+ .join('\n');
346
387
  const document = parse(sdl);
347
388
  const schema = buildSchema(sdl);
348
389
  const declaredNames = (schema.getType('Rights')?.getValues() ?? []).map((one) => one.name);
@@ -376,9 +417,18 @@ const Blueprint = (source) => {
376
417
  if (undeclared.length > 0) {
377
418
  return undefined;
378
419
  }
420
+ const ceilings = ceilingsIn(document);
421
+ const misinvited = [...ceilings].filter(
422
+ ([right, ceiling]) =>
423
+ STRUCTURAL.includes(right) || !whole(ceiling.count) || !whole(ceiling.within),
424
+ );
425
+ if (misinvited.length > 0) {
426
+ return undefined;
427
+ }
379
428
 
380
429
  return Object.freeze({
381
430
  admits,
431
+ invites: (held) => ceilings.get(held),
382
432
  bindings: (given) =>
383
433
  modules
384
434
  .filter((one) => one.binding !== undefined)
package/ground.mjs CHANGED
@@ -284,6 +284,7 @@ const Ground = (modules = {}, { span } = {}) => {
284
284
  const buildAdmin = () =>
285
285
  Being({
286
286
  address,
287
+ administration: true,
287
288
  cipher: modules.cipher,
288
289
  deliver: (envelope) => deliver(envelope),
289
290
  modules: modulesFor(adminVoice, adminDeclared()),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nervur",
3
- "version": "0.13.0",
3
+ "version": "0.14.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
@@ -90,6 +90,7 @@ const Program = (source) => {
90
90
  config: blueprint.config,
91
91
  declares: blueprint.declares,
92
92
  digest: blueprint.digest,
93
+ invites: blueprint.invites,
93
94
  introspect: async (held) => {
94
95
  const filtered = seenBy(held);
95
96
  if (filtered === undefined) {