nervur 0.3.0 → 0.6.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/LICENSE +0 -1
- package/NOTICE +2 -0
- package/README.md +328 -43
- package/admin.mjs +60 -0
- package/being.mjs +369 -0
- package/blueprint.mjs +281 -0
- package/client.mjs +43 -0
- package/contract.mjs +96 -0
- package/ground.mjs +196 -0
- package/index.mjs +10 -0
- package/modules.mjs +111 -0
- package/package.json +14 -12
- package/pointer.mjs +79 -0
- package/program.mjs +74 -0
- package/tools.mjs +141 -0
- package/voice.mjs +90 -0
- package/completion.js +0 -122
- package/nervur.js +0 -672
- package/preflight.js +0 -162
- package/scaffold.js +0 -50
- package/style.js +0 -82
package/being.mjs
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { OPEN, STRUCTURAL, coreClass } from './blueprint.mjs';
|
|
2
|
+
import { Program } from './program.mjs';
|
|
3
|
+
import { MEASURE, Stamp, Tools } from './tools.mjs';
|
|
4
|
+
import { Voice } from './voice.mjs';
|
|
5
|
+
|
|
6
|
+
const Compartment = ({ memory, voice, address, root, classOf }) => {
|
|
7
|
+
const held = (key) => memory.read(address, key);
|
|
8
|
+
const sealed = (key) => {
|
|
9
|
+
const kind = classOf(key);
|
|
10
|
+
return kind === undefined || OPEN.includes(kind) ? undefined : Tools.derive(root, kind);
|
|
11
|
+
};
|
|
12
|
+
const put = (key, value) => {
|
|
13
|
+
const claim = { address, key, value, version: (held(key)?.version ?? 0) + 1 };
|
|
14
|
+
return memory.write({ ...claim, sig: voice.sign(Tools.canon(claim)) });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
return Object.freeze({
|
|
18
|
+
classOf,
|
|
19
|
+
list: (prefix) =>
|
|
20
|
+
memory.list(address, prefix).filter((key) => memory.read(address, key)?.value !== null),
|
|
21
|
+
read: (key) => {
|
|
22
|
+
const cell = held(key);
|
|
23
|
+
if (cell === undefined || cell.value === null || classOf(key) === undefined) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const secret = sealed(key);
|
|
27
|
+
return secret === undefined ? cell.value : Tools.unseal(secret, cell.value);
|
|
28
|
+
},
|
|
29
|
+
write: (key, plain) => {
|
|
30
|
+
if (classOf(key) === undefined) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (plain === undefined) {
|
|
34
|
+
return put(key, null);
|
|
35
|
+
}
|
|
36
|
+
const secret = sealed(key);
|
|
37
|
+
return put(key, secret === undefined ? plain : Tools.seal(secret, plain));
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const scoped = (mine, program) =>
|
|
43
|
+
Object.freeze({
|
|
44
|
+
classOf: mine.classOf,
|
|
45
|
+
list: mine.list,
|
|
46
|
+
read: mine.read,
|
|
47
|
+
write: (key, plain) =>
|
|
48
|
+
key.startsWith('refs/') || key.startsWith('handles/') || program?.declares(key) !== undefined
|
|
49
|
+
? mine.write(key, plain)
|
|
50
|
+
: undefined,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const rootFor = ({ memory, voice, address }) => {
|
|
54
|
+
const cell = memory.read(address, `keyring/${Tools.address(voice.pk)}`);
|
|
55
|
+
const opened = cell === undefined ? undefined : voice.open(cell.value.wrapped);
|
|
56
|
+
return opened === undefined ? undefined : Buffer.from(opened, 'base64');
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const Port = ({ voice, mine, modules, rights }) => {
|
|
60
|
+
const stamp = Stamp();
|
|
61
|
+
const lanes = new Map();
|
|
62
|
+
const send = async (to, op) => {
|
|
63
|
+
const turn = (lanes.get(to) ?? Promise.resolve()).then(() => {
|
|
64
|
+
const handle = mine.read(`handles/${to}`);
|
|
65
|
+
return modules.deliver(
|
|
66
|
+
Tools.envelope(handle === undefined ? voice : Voice.revive(handle.face), to, op, stamp()),
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
const settled = turn.then(
|
|
70
|
+
() => undefined,
|
|
71
|
+
() => undefined,
|
|
72
|
+
);
|
|
73
|
+
lanes.set(to, settled);
|
|
74
|
+
settled.then(() => {
|
|
75
|
+
if (lanes.get(to) === settled) {
|
|
76
|
+
lanes.delete(to);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return turn;
|
|
80
|
+
};
|
|
81
|
+
return Object.freeze({
|
|
82
|
+
face: (to) => {
|
|
83
|
+
const handle = mine.read(`handles/${to}`);
|
|
84
|
+
return Tools.address(handle === undefined ? voice.pk : Voice.revive(handle.face).pk);
|
|
85
|
+
},
|
|
86
|
+
mint: (to) => {
|
|
87
|
+
const minted = Voice();
|
|
88
|
+
mine.write(`handles/${to}`, { face: minted.pack() });
|
|
89
|
+
return { address: Tools.address(minted.pk), nextPkHash: minted.nextPkHash };
|
|
90
|
+
},
|
|
91
|
+
release: async (to) => {
|
|
92
|
+
const gone = await send(to, { release: {} });
|
|
93
|
+
mine.write(`handles/${to}`, undefined);
|
|
94
|
+
return gone;
|
|
95
|
+
},
|
|
96
|
+
rights,
|
|
97
|
+
send,
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const rotates = ({ mine }, from, nextPkHash) => {
|
|
102
|
+
const claim = Tools.digest(Tools.pkOf(from));
|
|
103
|
+
const held = mine
|
|
104
|
+
.list('refs/')
|
|
105
|
+
.map((key) => ({ key, referred: mine.read(key) }))
|
|
106
|
+
.find(({ referred }) => referred?.nextPkHash === claim);
|
|
107
|
+
if (held === undefined) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
mine.write(`refs/${from}`, { nextPkHash, rights: held.referred.rights });
|
|
111
|
+
mine.write(held.key, undefined);
|
|
112
|
+
return from;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const Being = ({ voice, address, modules, registry, vouched, watch }) => {
|
|
116
|
+
const memory = modules.memory;
|
|
117
|
+
const root = rootFor({ memory, voice, address });
|
|
118
|
+
const source = memory.read(address, 'program')?.value;
|
|
119
|
+
const program = source === undefined ? undefined : Program(source);
|
|
120
|
+
const mine = Compartment({
|
|
121
|
+
memory,
|
|
122
|
+
voice,
|
|
123
|
+
address,
|
|
124
|
+
root,
|
|
125
|
+
classOf: (key) => program?.classOf(key) ?? coreClass(key),
|
|
126
|
+
});
|
|
127
|
+
const answer = (spoken) => {
|
|
128
|
+
const body = Tools.canon(spoken);
|
|
129
|
+
return body.length > MEASURE ? undefined : { body: spoken, sig: voice.sign(body) };
|
|
130
|
+
};
|
|
131
|
+
const note = (envelope, outcome) => {
|
|
132
|
+
try {
|
|
133
|
+
watch?.(
|
|
134
|
+
Object.freeze({
|
|
135
|
+
from: envelope?.from,
|
|
136
|
+
kind: envelope?.op === undefined ? undefined : Object.keys(envelope.op)[0],
|
|
137
|
+
outcome,
|
|
138
|
+
to: envelope?.to,
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
} catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return Object.freeze(async (envelope, token) => {
|
|
147
|
+
let gated;
|
|
148
|
+
const spoken = await (async () => {
|
|
149
|
+
try {
|
|
150
|
+
const { from, op, seq, sig, to } = envelope;
|
|
151
|
+
const claim = Tools.canon({ from, op, seq, to });
|
|
152
|
+
const callerPk = Tools.pkOf(from);
|
|
153
|
+
const proven =
|
|
154
|
+
claim.length <= MEASURE &&
|
|
155
|
+
callerPk !== null &&
|
|
156
|
+
((vouched !== undefined && token === vouched) || Tools.verifies(callerPk, claim, sig));
|
|
157
|
+
if (to !== address || !proven) {
|
|
158
|
+
gated = 'unproven';
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
const self = from === Tools.address(voice.pk);
|
|
162
|
+
const row = self ? undefined : mine.read(`refs/${from}`);
|
|
163
|
+
if (row !== undefined && !(vouched !== undefined && token === vouched)) {
|
|
164
|
+
if (!(seq > (row.seen ?? 0))) {
|
|
165
|
+
gated = 'replayed';
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
mine.write(`refs/${from}`, { ...row, seen: seq });
|
|
169
|
+
}
|
|
170
|
+
const rights = self ? 'SELF' : (row?.rights ?? 'STRANGER');
|
|
171
|
+
if (op?.describe !== undefined) {
|
|
172
|
+
return answer({
|
|
173
|
+
address,
|
|
174
|
+
digest: program?.digest,
|
|
175
|
+
rights,
|
|
176
|
+
sdl: program?.sdlFor(rights),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (op?.introspect !== undefined) {
|
|
180
|
+
const seen = await program?.introspect(rights);
|
|
181
|
+
return seen === undefined ? undefined : answer({ introspection: seen });
|
|
182
|
+
}
|
|
183
|
+
if (op?.rotate !== undefined) {
|
|
184
|
+
const rotated = rotates({ mine }, from, op.rotate.nextPkHash);
|
|
185
|
+
return rotated === undefined ? undefined : answer({ rotated });
|
|
186
|
+
}
|
|
187
|
+
if (op?.release !== undefined) {
|
|
188
|
+
if (row === undefined) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
mine.write(`refs/${from}`, undefined);
|
|
192
|
+
return answer({ released: from });
|
|
193
|
+
}
|
|
194
|
+
if (op?.acceptInvite !== undefined) {
|
|
195
|
+
const offer = Tools.digest(Buffer.from(op.acceptInvite.proof, 'base64'));
|
|
196
|
+
const pending = mine.read(`refs/pending/${offer}`);
|
|
197
|
+
if (pending === undefined || pending.spent) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
mine.write(`refs/pending/${offer}`, { spent: true });
|
|
201
|
+
mine.write(`refs/${from}`, {
|
|
202
|
+
nextPkHash: op.acceptInvite.nextPkHash ?? null,
|
|
203
|
+
rights: pending.rights,
|
|
204
|
+
seen: seq ?? 0,
|
|
205
|
+
});
|
|
206
|
+
return answer({ referred: from, rights: pending.rights });
|
|
207
|
+
}
|
|
208
|
+
if (op?.ask === undefined || program === undefined) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
const asked = await program.ask({
|
|
212
|
+
context: {
|
|
213
|
+
modules: Object.freeze({ ...modules, memory: scoped(mine, program) }),
|
|
214
|
+
port: Port({ voice, mine, modules, rights }),
|
|
215
|
+
registry,
|
|
216
|
+
rights,
|
|
217
|
+
},
|
|
218
|
+
source: op.ask.source,
|
|
219
|
+
variables: op.ask.variables,
|
|
220
|
+
});
|
|
221
|
+
return asked === undefined ? undefined : answer(asked);
|
|
222
|
+
} catch {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
})();
|
|
226
|
+
note(envelope, gated ?? (spoken === undefined ? 'mute' : 'answered'));
|
|
227
|
+
return spoken;
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const make = ({ memory, voice, address, program }) => {
|
|
232
|
+
const root = Tools.secret();
|
|
233
|
+
const put = (key, value) => {
|
|
234
|
+
const claim = { address, key, value, version: (memory.read(address, key)?.version ?? 0) + 1 };
|
|
235
|
+
return memory.write({ ...claim, sig: voice.sign(Tools.canon(claim)) });
|
|
236
|
+
};
|
|
237
|
+
put('chain/pk', Buffer.from(voice.pk).toString('hex'));
|
|
238
|
+
put('chain/nextPkHash', voice.nextPkHash);
|
|
239
|
+
put('program', program ?? '({})');
|
|
240
|
+
put(`keyring/${Tools.address(voice.pk)}`, {
|
|
241
|
+
boxPk: Buffer.from(voice.boxPk).toString('base64'),
|
|
242
|
+
wrapped: Voice.sealTo(voice.boxPk, root.toString('base64')),
|
|
243
|
+
});
|
|
244
|
+
const written = program === undefined ? undefined : Program(program);
|
|
245
|
+
return Compartment({
|
|
246
|
+
memory,
|
|
247
|
+
voice,
|
|
248
|
+
address,
|
|
249
|
+
root,
|
|
250
|
+
classOf: (key) => written?.classOf(key) ?? coreClass(key),
|
|
251
|
+
});
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const Registry = ({
|
|
255
|
+
memory,
|
|
256
|
+
adminMine,
|
|
257
|
+
modules,
|
|
258
|
+
modulesFor,
|
|
259
|
+
announce,
|
|
260
|
+
forget,
|
|
261
|
+
vouched,
|
|
262
|
+
attest,
|
|
263
|
+
watch,
|
|
264
|
+
}) => {
|
|
265
|
+
const live = new Map();
|
|
266
|
+
const activate = (address) => {
|
|
267
|
+
if (live.has(address)) {
|
|
268
|
+
return live.get(address);
|
|
269
|
+
}
|
|
270
|
+
const held = adminMine.read(`identity/custody/${address}`);
|
|
271
|
+
const voice = held === undefined ? undefined : Voice.revive(held);
|
|
272
|
+
if (voice === undefined) {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
const door = Being({ voice, address, modules: modulesFor(voice), vouched, watch });
|
|
276
|
+
live.set(address, door);
|
|
277
|
+
return door;
|
|
278
|
+
};
|
|
279
|
+
return Object.freeze({
|
|
280
|
+
activate,
|
|
281
|
+
active: () => live.size,
|
|
282
|
+
attest,
|
|
283
|
+
create: ({ program, refs = [], data = {}, bindings, config } = {}) => {
|
|
284
|
+
const written = program === undefined ? undefined : Program(program);
|
|
285
|
+
if (program !== undefined && written === undefined) {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
const missing = (written?.modules ?? []).filter((one) => modules[one.module] === undefined);
|
|
289
|
+
if (missing.length > 0) {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
const brought = written?.bindings(bindings);
|
|
293
|
+
const settings = written?.config(config);
|
|
294
|
+
if (written !== undefined && (brought === undefined || settings === undefined)) {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
const allowed = written?.rights ?? [...STRUCTURAL, 'MEMBER'];
|
|
298
|
+
if (refs.some((ref) => !allowed.includes(ref.rights ?? 'MEMBER'))) {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
if (refs.some((ref) => typeof ref.address !== 'string' || Tools.pkOf(ref.address) === null)) {
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
const voice = Voice();
|
|
305
|
+
const address = Tools.address(voice.pk);
|
|
306
|
+
const mine = make({ memory, voice, address, program });
|
|
307
|
+
for (const [key, value] of Object.entries(data)) {
|
|
308
|
+
if (mine.write(key, value) === undefined) {
|
|
309
|
+
forget(address);
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (written !== undefined) {
|
|
314
|
+
mine.write('config', settings);
|
|
315
|
+
for (const [module, bound] of Object.entries(brought)) {
|
|
316
|
+
mine.write(`bindings/${module}`, bound);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
for (const ref of refs) {
|
|
320
|
+
mine.write(`refs/${ref.address}`, {
|
|
321
|
+
nextPkHash: ref.nextPkHash ?? null,
|
|
322
|
+
rights: ref.rights ?? 'MEMBER',
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
adminMine.write(`identity/custody/${address}`, voice.pack());
|
|
326
|
+
announce(address);
|
|
327
|
+
return address;
|
|
328
|
+
},
|
|
329
|
+
deactivate: (address) => live.delete(address),
|
|
330
|
+
upgrade: ({ address, program }) => {
|
|
331
|
+
const held = adminMine.read(`identity/custody/${address}`);
|
|
332
|
+
const voice = held === undefined ? undefined : Voice.revive(held);
|
|
333
|
+
const written = program === undefined ? undefined : Program(program);
|
|
334
|
+
if (voice === undefined || written === undefined) {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
const orphaned = memory
|
|
338
|
+
.list(address, '')
|
|
339
|
+
.filter((key) => memory.read(address, key)?.value !== null)
|
|
340
|
+
.filter((key) => written.classOf(key) === undefined);
|
|
341
|
+
if (orphaned.length > 0) {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
const claim = {
|
|
345
|
+
address,
|
|
346
|
+
key: 'program',
|
|
347
|
+
value: program,
|
|
348
|
+
version: (memory.read(address, 'program')?.version ?? 0) + 1,
|
|
349
|
+
};
|
|
350
|
+
if (memory.write({ ...claim, sig: voice.sign(Tools.canon(claim)) }) === undefined) {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
live.delete(address);
|
|
354
|
+
return address;
|
|
355
|
+
},
|
|
356
|
+
destroy: (address) => {
|
|
357
|
+
if (adminMine.read(`identity/custody/${address}`) === undefined) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
adminMine.write(`identity/custody/${address}`, undefined);
|
|
361
|
+
live.delete(address);
|
|
362
|
+
forget(address);
|
|
363
|
+
return address;
|
|
364
|
+
},
|
|
365
|
+
stored: () => adminMine.list('identity/custody/').length,
|
|
366
|
+
});
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
export { Being, Compartment, Registry, make, rootFor };
|
package/blueprint.mjs
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { Kind, buildSchema, coerceInputValue, parse, print } from 'graphql';
|
|
2
|
+
import { Tools } from './tools.mjs';
|
|
3
|
+
|
|
4
|
+
const STRUCTURAL = ['SELF', 'STRANGER'];
|
|
5
|
+
|
|
6
|
+
const CLASSES = [
|
|
7
|
+
'bindings',
|
|
8
|
+
'chain',
|
|
9
|
+
'config',
|
|
10
|
+
'data',
|
|
11
|
+
'handles',
|
|
12
|
+
'identity',
|
|
13
|
+
'keyring',
|
|
14
|
+
'program',
|
|
15
|
+
'refs',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const OPEN = ['chain', 'keyring', 'program'];
|
|
19
|
+
|
|
20
|
+
const CORE_ONLY = ['chain', 'identity', 'keyring', 'program'];
|
|
21
|
+
|
|
22
|
+
const CORE = [
|
|
23
|
+
['chain/', 'chain'],
|
|
24
|
+
['keyring/', 'keyring'],
|
|
25
|
+
['refs/', 'refs'],
|
|
26
|
+
['handles/', 'handles'],
|
|
27
|
+
['bindings/', 'bindings'],
|
|
28
|
+
['identity/', 'identity'],
|
|
29
|
+
['config', 'config'],
|
|
30
|
+
['program', 'program'],
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const coreClass = (key) =>
|
|
34
|
+
CORE.find(([mark]) => (mark.endsWith('/') ? key.startsWith(mark) : key === mark))?.[1];
|
|
35
|
+
|
|
36
|
+
const declaredClass = (memory, key) => {
|
|
37
|
+
const held = memory[key];
|
|
38
|
+
if (held !== undefined) {
|
|
39
|
+
return held.class;
|
|
40
|
+
}
|
|
41
|
+
const under = Object.keys(memory)
|
|
42
|
+
.filter((one) => one.endsWith('/*'))
|
|
43
|
+
.find((one) => key.startsWith(one.slice(0, -1)));
|
|
44
|
+
return under === undefined ? undefined : memory[under].class;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const AMBIENT = [
|
|
48
|
+
'globalThis',
|
|
49
|
+
'process',
|
|
50
|
+
'require',
|
|
51
|
+
'import',
|
|
52
|
+
'eval',
|
|
53
|
+
'Function',
|
|
54
|
+
'fetch',
|
|
55
|
+
'XMLHttpRequest',
|
|
56
|
+
'WebAssembly',
|
|
57
|
+
'SharedArrayBuffer',
|
|
58
|
+
'Atomics',
|
|
59
|
+
'Buffer',
|
|
60
|
+
'crypto',
|
|
61
|
+
'Date',
|
|
62
|
+
'setTimeout',
|
|
63
|
+
'setInterval',
|
|
64
|
+
'queueMicrotask',
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const REACHES = [
|
|
68
|
+
['constructor', /\bconstructor\b/],
|
|
69
|
+
['__proto__', /\b__proto__\b/],
|
|
70
|
+
['prototype', /\bprototype\b/],
|
|
71
|
+
['Math.random', /\bMath\s*\.\s*random\b/],
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const FORBIDDEN = [
|
|
75
|
+
...AMBIENT.map((name) => [name, new RegExp(`(?<![.$\\w])${name}\\b(?!\\s*:)`)]),
|
|
76
|
+
...REACHES,
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const RESERVED = ['eval', 'import', '__proto__', 'prototype', 'Math.random'];
|
|
80
|
+
|
|
81
|
+
const SHADOWED = FORBIDDEN.map(([name]) => name).filter((name) => !RESERVED.includes(name));
|
|
82
|
+
|
|
83
|
+
const RIGHTS_DIRECTIVE = 'directive @rights(is: [Rights!]!) on FIELD_DEFINITION';
|
|
84
|
+
|
|
85
|
+
const DEFAULT_RIGHTS = `enum Rights { SELF MEMBER STRANGER }`;
|
|
86
|
+
|
|
87
|
+
const codeOnly = (source) => {
|
|
88
|
+
const out = [];
|
|
89
|
+
const modes = [{ depth: 0, kind: 'code' }];
|
|
90
|
+
let at = 0;
|
|
91
|
+
while (at < source.length) {
|
|
92
|
+
const mode = modes[modes.length - 1];
|
|
93
|
+
const one = source[at];
|
|
94
|
+
if (mode.kind !== 'code') {
|
|
95
|
+
if (one === '\\') {
|
|
96
|
+
out.push(' ');
|
|
97
|
+
at += 2;
|
|
98
|
+
} else if (one === mode.mark) {
|
|
99
|
+
modes.pop();
|
|
100
|
+
out.push(' ');
|
|
101
|
+
at += 1;
|
|
102
|
+
} else if (mode.kind === 'template' && one === '$' && source[at + 1] === '{') {
|
|
103
|
+
modes.push({ depth: 0, kind: 'code' });
|
|
104
|
+
out.push(' ');
|
|
105
|
+
at += 2;
|
|
106
|
+
} else {
|
|
107
|
+
out.push(one === '\n' ? '\n' : ' ');
|
|
108
|
+
at += 1;
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (one === "'" || one === '"') {
|
|
113
|
+
modes.push({ kind: 'quote', mark: one });
|
|
114
|
+
} else if (one === '`') {
|
|
115
|
+
modes.push({ kind: 'template', mark: '`' });
|
|
116
|
+
} else if (one === '{') {
|
|
117
|
+
mode.depth += 1;
|
|
118
|
+
} else if (one === '}' && mode.depth === 0 && modes.length > 1) {
|
|
119
|
+
modes.pop();
|
|
120
|
+
out.push(' ');
|
|
121
|
+
at += 1;
|
|
122
|
+
continue;
|
|
123
|
+
} else if (one === '}') {
|
|
124
|
+
mode.depth -= 1;
|
|
125
|
+
}
|
|
126
|
+
out.push(one === "'" || one === '"' || one === '`' ? ' ' : one);
|
|
127
|
+
at += 1;
|
|
128
|
+
}
|
|
129
|
+
return out.join('');
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const reads = (source) => {
|
|
133
|
+
const code = codeOnly(source);
|
|
134
|
+
return FORBIDDEN.filter(([, mark]) => mark.test(code)).map(([name]) => name);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const evaluated = (source) => {
|
|
138
|
+
const body = `"use strict"; return (${source});`;
|
|
139
|
+
return new Function(...SHADOWED, body)(...SHADOWED.map(() => undefined));
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const rightsOf = (field) => {
|
|
143
|
+
const marked = field.directives?.find((one) => one.name.value === 'rights');
|
|
144
|
+
const listed = marked?.arguments?.find((one) => one.name.value === 'is');
|
|
145
|
+
return listed === undefined
|
|
146
|
+
? undefined
|
|
147
|
+
: listed.value.values.map((one) => one.value ?? one.name?.value);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const resolverSources = (resolvers) =>
|
|
151
|
+
Object.keys(resolvers)
|
|
152
|
+
.sort()
|
|
153
|
+
.flatMap((type) =>
|
|
154
|
+
Object.keys(resolvers[type])
|
|
155
|
+
.sort()
|
|
156
|
+
.map((field) => `${type}.${field}=${resolvers[type][field].toString()}`),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const inputCheck = (schema, typeName, given) => {
|
|
160
|
+
const type = schema.getType(typeName);
|
|
161
|
+
if (type?.getFields === undefined) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
let failed = false;
|
|
165
|
+
const taken = coerceInputValue(given ?? {}, type, () => {
|
|
166
|
+
failed = true;
|
|
167
|
+
});
|
|
168
|
+
return failed || taken === undefined
|
|
169
|
+
? undefined
|
|
170
|
+
: Object.freeze(JSON.parse(JSON.stringify(taken)));
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const Blueprint = (source) => {
|
|
174
|
+
const trespass = reads(source);
|
|
175
|
+
if (trespass.length > 0) {
|
|
176
|
+
return { trespass };
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const written = evaluated(source);
|
|
180
|
+
const declares = /enum\s+Rights\b/.test(written.interface) ? '' : DEFAULT_RIGHTS;
|
|
181
|
+
const sdl = [RIGHTS_DIRECTIVE, declares, written.interface].filter(Boolean).join('\n');
|
|
182
|
+
const document = parse(sdl);
|
|
183
|
+
const schema = buildSchema(sdl);
|
|
184
|
+
const rights = [
|
|
185
|
+
...new Set([
|
|
186
|
+
...STRUCTURAL,
|
|
187
|
+
...(schema.getType('Rights')?.getValues() ?? []).map((one) => one.name),
|
|
188
|
+
]),
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
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);
|
|
196
|
+
const allowed = field === undefined ? undefined : rightsOf(field);
|
|
197
|
+
return allowed === undefined ? true : allowed.includes(held);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const needs = written.needs ?? {};
|
|
201
|
+
const modules = needs.modules ?? [];
|
|
202
|
+
const memory = written.memory ?? {};
|
|
203
|
+
const misdeclared = Object.entries(memory).filter(
|
|
204
|
+
([key, cell]) => !CLASSES.includes(cell?.class) || coreClass(key) !== undefined,
|
|
205
|
+
);
|
|
206
|
+
if (misdeclared.length > 0) {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
const roots = [
|
|
210
|
+
schema.getQueryType()?.name,
|
|
211
|
+
schema.getMutationType()?.name,
|
|
212
|
+
schema.getSubscriptionType()?.name,
|
|
213
|
+
].filter((one) => one !== undefined);
|
|
214
|
+
const undeclared = document.definitions
|
|
215
|
+
.filter((one) => one.kind === Kind.OBJECT_TYPE_DEFINITION && roots.includes(one.name.value))
|
|
216
|
+
.flatMap((one) => one.fields ?? [])
|
|
217
|
+
.filter((one) => {
|
|
218
|
+
const allowed = rightsOf(one);
|
|
219
|
+
return allowed === undefined || allowed.some((held) => !rights.includes(held));
|
|
220
|
+
});
|
|
221
|
+
if (undeclared.length > 0) {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return Object.freeze({
|
|
226
|
+
admits,
|
|
227
|
+
bindings: (given) =>
|
|
228
|
+
modules
|
|
229
|
+
.filter((one) => one.binding !== undefined)
|
|
230
|
+
.reduce((taken, one) => {
|
|
231
|
+
if (taken === undefined) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
const checked = inputCheck(schema, one.binding, given?.[one.module]);
|
|
235
|
+
return checked === undefined ? undefined : { ...taken, [one.module]: checked };
|
|
236
|
+
}, {}),
|
|
237
|
+
classOf: (key) => coreClass(key) ?? declaredClass(memory, key),
|
|
238
|
+
declares: (key) => declaredClass(memory, key),
|
|
239
|
+
config: (given) =>
|
|
240
|
+
needs.config === undefined ? Object.freeze({}) : inputCheck(schema, needs.config, given),
|
|
241
|
+
digest: Tools.digest(
|
|
242
|
+
Tools.canon({
|
|
243
|
+
interface: print(document),
|
|
244
|
+
memory,
|
|
245
|
+
name: written.name,
|
|
246
|
+
needs,
|
|
247
|
+
resolvers: resolverSources(written.resolvers ?? {}),
|
|
248
|
+
}),
|
|
249
|
+
),
|
|
250
|
+
memory: Object.freeze(memory),
|
|
251
|
+
modules,
|
|
252
|
+
name: written.name,
|
|
253
|
+
resolvers: written.resolvers ?? {},
|
|
254
|
+
rights,
|
|
255
|
+
schema,
|
|
256
|
+
// The directive definition stays: pointer.mjs runs buildSchema on this output.
|
|
257
|
+
sdlFor: (held) =>
|
|
258
|
+
print({
|
|
259
|
+
...document,
|
|
260
|
+
definitions: document.definitions
|
|
261
|
+
.map((one) =>
|
|
262
|
+
one.kind === Kind.OBJECT_TYPE_DEFINITION
|
|
263
|
+
? {
|
|
264
|
+
...one,
|
|
265
|
+
fields: one.fields.filter((field) => {
|
|
266
|
+
const allowed = rightsOf(field);
|
|
267
|
+
return allowed === undefined || allowed.includes(held);
|
|
268
|
+
}),
|
|
269
|
+
}
|
|
270
|
+
: one,
|
|
271
|
+
)
|
|
272
|
+
.filter((one) => one.kind !== Kind.OBJECT_TYPE_DEFINITION || one.fields.length > 0),
|
|
273
|
+
}),
|
|
274
|
+
trespass: [],
|
|
275
|
+
});
|
|
276
|
+
} catch {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export { Blueprint, CLASSES, CORE_ONLY, FORBIDDEN, OPEN, STRUCTURAL, coreClass, reads };
|
package/client.mjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Stamp, Tools } from './tools.mjs';
|
|
2
|
+
|
|
3
|
+
const Client = ({ voice, deliver, resolve = (one) => one }) => {
|
|
4
|
+
const stamp = Stamp();
|
|
5
|
+
const lanes = new Map();
|
|
6
|
+
const send = async (to, op) => {
|
|
7
|
+
const at = resolve(to);
|
|
8
|
+
const answering = at === undefined ? null : Tools.pkOf(at);
|
|
9
|
+
if (answering === null) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const turn = (lanes.get(at) ?? Promise.resolve()).then(async () => {
|
|
13
|
+
const reply = await deliver(Tools.envelope(voice, at, op, stamp()));
|
|
14
|
+
return reply?.body === undefined ||
|
|
15
|
+
!Tools.verifies(answering, Tools.canon(reply.body), reply.sig)
|
|
16
|
+
? undefined
|
|
17
|
+
: reply;
|
|
18
|
+
});
|
|
19
|
+
const settled = turn.then(
|
|
20
|
+
() => undefined,
|
|
21
|
+
() => undefined,
|
|
22
|
+
);
|
|
23
|
+
lanes.set(at, settled);
|
|
24
|
+
settled.then(() => {
|
|
25
|
+
if (lanes.get(at) === settled) {
|
|
26
|
+
lanes.delete(at);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
return turn;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
address: Tools.address(voice.pk),
|
|
34
|
+
ask: (to, source, variables) => send(to, { ask: { source, variables } }),
|
|
35
|
+
describe: (to) => send(to, { describe: {} }),
|
|
36
|
+
introspect: (to) => send(to, { introspect: {} }),
|
|
37
|
+
pk: voice.pk,
|
|
38
|
+
release: (to) => send(to, { release: {} }),
|
|
39
|
+
send,
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export { Client };
|