nervur 0.11.0 → 0.13.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/GETTING-STARTED.md +111 -0
- package/README.md +57 -19
- package/SECURITY.md +11 -0
- package/admin.mjs +11 -1
- package/being.mjs +169 -54
- package/blueprint.mjs +137 -34
- package/client.mjs +9 -6
- package/contract.mjs +6 -4
- package/ground.mjs +144 -37
- package/index.mjs +13 -3
- package/modules.mjs +14 -70
- package/package.json +5 -2
- package/tools.mjs +69 -48
- package/voice.mjs +49 -63
package/contract.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildSchema, isEnumType, isInputObjectType,
|
|
1
|
+
import { buildSchema, isEnumType, isInputObjectType, specifiedScalarTypes } from 'graphql';
|
|
2
2
|
import { Tools } from './tools.mjs';
|
|
3
3
|
|
|
4
4
|
const argsAgree = (wanted, given) => {
|
|
@@ -16,9 +16,8 @@ const argsAgree = (wanted, given) => {
|
|
|
16
16
|
const Contract = (name, sdl) => {
|
|
17
17
|
try {
|
|
18
18
|
const schema = buildSchema(sdl);
|
|
19
|
-
const printed = print(parse(sdl));
|
|
20
19
|
return Object.freeze({
|
|
21
|
-
digest: Tools.digest(Tools.canon({ interface:
|
|
20
|
+
digest: Tools.digest(Tools.canon({ interface: sdl, name })),
|
|
22
21
|
name,
|
|
23
22
|
satisfies: (offered) => {
|
|
24
23
|
const theirs = (() => {
|
|
@@ -36,6 +35,9 @@ const Contract = (name, sdl) => {
|
|
|
36
35
|
continue;
|
|
37
36
|
}
|
|
38
37
|
const given = theirs.getTypeMap()[typeName];
|
|
38
|
+
if (given === undefined && !specifiedScalarTypes.some((one) => one.name === typeName)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
39
41
|
if (isEnumType(wanted)) {
|
|
40
42
|
if (
|
|
41
43
|
!isEnumType(given) ||
|
|
@@ -73,7 +75,7 @@ const Contract = (name, sdl) => {
|
|
|
73
75
|
}
|
|
74
76
|
return true;
|
|
75
77
|
},
|
|
76
|
-
sdl
|
|
78
|
+
sdl,
|
|
77
79
|
});
|
|
78
80
|
} catch {
|
|
79
81
|
return undefined;
|
package/ground.mjs
CHANGED
|
@@ -1,20 +1,73 @@
|
|
|
1
1
|
import { ADMIN } from './admin.mjs';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Being,
|
|
4
|
+
Compartment,
|
|
5
|
+
Registry,
|
|
6
|
+
SUCCESSION,
|
|
7
|
+
make,
|
|
8
|
+
rootFor,
|
|
9
|
+
spend,
|
|
10
|
+
successionOf,
|
|
11
|
+
} from './being.mjs';
|
|
3
12
|
import { Blueprint, composed, coreClass } from './blueprint.mjs';
|
|
4
13
|
import { Client } from './client.mjs';
|
|
5
14
|
import { Contracts } from './contract.mjs';
|
|
6
|
-
import { Memory
|
|
15
|
+
import { Memory } from './modules.mjs';
|
|
7
16
|
import { Mcp, Pointer } from './pointer.mjs';
|
|
8
17
|
import { Program } from './program.mjs';
|
|
9
|
-
import { Stamp, Tools } from './tools.mjs';
|
|
18
|
+
import { Bytes, Stamp, Tools } from './tools.mjs';
|
|
10
19
|
import { Voice } from './voice.mjs';
|
|
11
20
|
|
|
12
21
|
const GROUND = 'Ground';
|
|
13
22
|
|
|
14
|
-
const FLOOR = [
|
|
23
|
+
const FLOOR = [
|
|
24
|
+
'blueprints',
|
|
25
|
+
'cells',
|
|
26
|
+
'cipher',
|
|
27
|
+
'contracts',
|
|
28
|
+
'keychain',
|
|
29
|
+
'moved',
|
|
30
|
+
'paths',
|
|
31
|
+
'watch',
|
|
32
|
+
'wire',
|
|
33
|
+
];
|
|
15
34
|
|
|
16
|
-
const
|
|
17
|
-
|
|
35
|
+
const UNPROVEN = 'ground/unproven/';
|
|
36
|
+
|
|
37
|
+
const HEIR = 'ground/heir';
|
|
38
|
+
|
|
39
|
+
const commits = (cells, bundle) => {
|
|
40
|
+
const boxPk = bundle?.heirBoxPk;
|
|
41
|
+
try {
|
|
42
|
+
if (typeof boxPk !== 'string' || Bytes.unbase64(boxPk).length !== 32) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
cells.put(HEIR, { boxPk, nextPkHash: bundle.nextPkHash });
|
|
49
|
+
return undefined;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const unprovenAt = (cells) => (at) => {
|
|
53
|
+
try {
|
|
54
|
+
const stood = cells.peek(`${UNPROVEN}${at}`);
|
|
55
|
+
const seen = Date.now();
|
|
56
|
+
cells.put(`${UNPROVEN}${at}`, {
|
|
57
|
+
at,
|
|
58
|
+
last: seen,
|
|
59
|
+
seen: (stood?.seen ?? 0) + 1,
|
|
60
|
+
since: stood?.since ?? seen,
|
|
61
|
+
});
|
|
62
|
+
} catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const unprovenRows = (cells) => cells.keys(UNPROVEN).map((row) => cells.peek(row));
|
|
68
|
+
|
|
69
|
+
const install = ({ cells, keychain, program, bundle, cipher }) => {
|
|
70
|
+
if (cells.peek('ground/admin') !== undefined || bundle?.next !== undefined) {
|
|
18
71
|
return undefined;
|
|
19
72
|
}
|
|
20
73
|
const voice = Voice.revive(bundle ?? {});
|
|
@@ -27,12 +80,13 @@ const install = ({ cells, keychain, program, bundle }) => {
|
|
|
27
80
|
if (sealed === undefined) {
|
|
28
81
|
return undefined;
|
|
29
82
|
}
|
|
30
|
-
make({ memory, voice, address, program });
|
|
83
|
+
make({ memory, voice, address, program, cipher });
|
|
31
84
|
cells.put('ground/admin', { address, sealed });
|
|
85
|
+
commits(cells, bundle);
|
|
32
86
|
return address;
|
|
33
87
|
};
|
|
34
88
|
|
|
35
|
-
const succeeds = ({ adminMine, adminVoice, address, bundle, cells, keychain, memory }) => {
|
|
89
|
+
const succeeds = ({ adminMine, adminVoice, address, bundle, cells, cipher, keychain, memory }) => {
|
|
36
90
|
if (bundle?.next !== undefined) {
|
|
37
91
|
return undefined;
|
|
38
92
|
}
|
|
@@ -41,21 +95,22 @@ const succeeds = ({ adminMine, adminVoice, address, bundle, cells, keychain, mem
|
|
|
41
95
|
if (heir === undefined || committed?.value !== Tools.digest(heir.pk)) {
|
|
42
96
|
return undefined;
|
|
43
97
|
}
|
|
44
|
-
const root = rootFor({ memory, voice: adminVoice, address });
|
|
98
|
+
const root = rootFor({ memory, voice: adminVoice, address, cipher });
|
|
45
99
|
const sealed = root === undefined ? undefined : keychain.seal(heir.pack());
|
|
46
100
|
if (sealed === undefined) {
|
|
47
101
|
return undefined;
|
|
48
102
|
}
|
|
49
103
|
const rewrapped = adminMine.write(`keyring/${Tools.address(heir.pk)}`, {
|
|
50
|
-
boxPk:
|
|
51
|
-
wrapped: Voice.sealTo(heir.boxPk,
|
|
104
|
+
boxPk: Bytes.base64(heir.boxPk),
|
|
105
|
+
wrapped: Voice.sealTo(heir.boxPk, Bytes.base64(root), cipher),
|
|
52
106
|
});
|
|
53
107
|
if (rewrapped === undefined) {
|
|
54
108
|
return undefined;
|
|
55
109
|
}
|
|
56
110
|
const version = committed.version + 1;
|
|
57
|
-
const
|
|
58
|
-
const
|
|
111
|
+
const certV = successionOf(memory, address).length + 1;
|
|
112
|
+
const handover = { at: address, next: Tools.address(heir.pk), v: certV };
|
|
113
|
+
const certified = adminMine.write(`${SUCCESSION}${certV}`, {
|
|
59
114
|
...handover,
|
|
60
115
|
sig: adminVoice.sign(Tools.canon(handover)),
|
|
61
116
|
});
|
|
@@ -65,20 +120,30 @@ const succeeds = ({ adminMine, adminVoice, address, bundle, cells, keychain, mem
|
|
|
65
120
|
const claim = {
|
|
66
121
|
address,
|
|
67
122
|
nextPkHash: heir.nextPkHash,
|
|
68
|
-
pk:
|
|
123
|
+
pk: Bytes.hex(heir.pk),
|
|
69
124
|
version,
|
|
70
125
|
};
|
|
71
126
|
if (memory.replace({ ...claim, sig: heir.sign(Tools.canon(claim)) }) === undefined) {
|
|
72
127
|
return undefined;
|
|
73
128
|
}
|
|
74
129
|
cells.put('ground/admin', { address, sealed });
|
|
130
|
+
commits(cells, bundle);
|
|
75
131
|
return Tools.address(heir.pk);
|
|
76
132
|
};
|
|
77
133
|
|
|
78
|
-
|
|
134
|
+
// Never a key in the modules object: everything there that is not in FLOOR
|
|
135
|
+
// becomes a module handed to programs under that name.
|
|
136
|
+
const Ground = (modules = {}, { span } = {}) => {
|
|
79
137
|
const cells = modules.cells;
|
|
80
138
|
const keychain = modules.keychain;
|
|
81
|
-
|
|
139
|
+
const cipher = modules.cipher;
|
|
140
|
+
if (
|
|
141
|
+
cells === undefined ||
|
|
142
|
+
keychain?.seal === undefined ||
|
|
143
|
+
keychain?.unseal === undefined ||
|
|
144
|
+
cipher?.seal === undefined ||
|
|
145
|
+
cipher?.unseal === undefined
|
|
146
|
+
) {
|
|
82
147
|
return undefined;
|
|
83
148
|
}
|
|
84
149
|
|
|
@@ -100,7 +165,7 @@ const Ground = (modules = {}) => {
|
|
|
100
165
|
const installed = cells.peek('ground/admin');
|
|
101
166
|
if (installed === undefined) {
|
|
102
167
|
return Object.freeze({
|
|
103
|
-
init: (bundle) => install({ cells, keychain, program, bundle }),
|
|
168
|
+
init: (bundle) => install({ cells, keychain, program, bundle, cipher: modules.cipher }),
|
|
104
169
|
});
|
|
105
170
|
}
|
|
106
171
|
const bundle = keychain.unseal(installed.sealed);
|
|
@@ -121,17 +186,22 @@ const Ground = (modules = {}) => {
|
|
|
121
186
|
};
|
|
122
187
|
|
|
123
188
|
const address = installed.address;
|
|
124
|
-
const memory = Memory(cells);
|
|
125
|
-
const paths = modules.paths ??
|
|
126
|
-
const wire =
|
|
189
|
+
const memory = Memory(cells, unprovenAt(cells), modules.moved);
|
|
190
|
+
const paths = modules.paths ?? Object.freeze({ find: () => undefined, publish: () => undefined });
|
|
191
|
+
const wire =
|
|
192
|
+
modules.wire ??
|
|
193
|
+
Object.freeze({
|
|
194
|
+
carry: async (endpoint, envelope) =>
|
|
195
|
+
typeof endpoint === 'function' ? endpoint(envelope) : undefined,
|
|
196
|
+
});
|
|
127
197
|
|
|
128
198
|
const stamp = Stamp();
|
|
129
199
|
const boundFor = (voice) =>
|
|
130
200
|
Object.fromEntries(
|
|
131
201
|
bound.map(([name, one]) => [
|
|
132
202
|
name,
|
|
133
|
-
Pointer(written.get(one.contract).sdl, (source, variables) =>
|
|
134
|
-
deliver(Tools.envelope(voice, one.at, { ask: { source, variables } }, stamp())),
|
|
203
|
+
Pointer(written.get(one.contract).sdl, async (source, variables) =>
|
|
204
|
+
deliver(await Tools.envelope(voice, one.at, { ask: { source, variables } }, stamp())),
|
|
135
205
|
),
|
|
136
206
|
]),
|
|
137
207
|
);
|
|
@@ -165,9 +235,10 @@ const Ground = (modules = {}) => {
|
|
|
165
235
|
memory,
|
|
166
236
|
voice: adminVoice,
|
|
167
237
|
address,
|
|
168
|
-
root: rootFor({ memory, voice: adminVoice, address }),
|
|
238
|
+
root: rootFor({ memory, voice: adminVoice, address, cipher: modules.cipher }),
|
|
169
239
|
classOf: (key) =>
|
|
170
240
|
Program(memory.read(address, 'program')?.value)?.classOf(key) ?? coreClass(key),
|
|
241
|
+
cipher: modules.cipher,
|
|
171
242
|
});
|
|
172
243
|
|
|
173
244
|
const receive = async (envelope, token) => {
|
|
@@ -185,9 +256,14 @@ const Ground = (modules = {}) => {
|
|
|
185
256
|
return door(envelope, token);
|
|
186
257
|
};
|
|
187
258
|
const announce = (at) => paths.publish(at, receive);
|
|
188
|
-
const forget = (at) => {
|
|
259
|
+
const forget = (at, voice) => {
|
|
189
260
|
paths.publish(at, undefined);
|
|
190
|
-
|
|
261
|
+
if (voice === undefined) {
|
|
262
|
+
cells.drop(`${at}/`);
|
|
263
|
+
modules.moved?.(at);
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
return spend({ memory, voice, address: at });
|
|
191
267
|
};
|
|
192
268
|
|
|
193
269
|
const attest = async (name) => {
|
|
@@ -198,35 +274,49 @@ const Ground = (modules = {}) => {
|
|
|
198
274
|
if (one.at === undefined) {
|
|
199
275
|
return null;
|
|
200
276
|
}
|
|
201
|
-
const seen = await deliver(Tools.envelope(adminVoice, one.at, { describe: {} }, stamp()));
|
|
277
|
+
const seen = await deliver(await Tools.envelope(adminVoice, one.at, { describe: {} }, stamp()));
|
|
202
278
|
return seen?.body?.sdl === undefined
|
|
203
279
|
? false
|
|
204
280
|
: written.get(one.contract).satisfies(seen.body.sdl);
|
|
205
281
|
};
|
|
206
282
|
|
|
283
|
+
let adminDoor;
|
|
284
|
+
const buildAdmin = () =>
|
|
285
|
+
Being({
|
|
286
|
+
address,
|
|
287
|
+
cipher: modules.cipher,
|
|
288
|
+
deliver: (envelope) => deliver(envelope),
|
|
289
|
+
modules: modulesFor(adminVoice, adminDeclared()),
|
|
290
|
+
registry,
|
|
291
|
+
span,
|
|
292
|
+
vouched,
|
|
293
|
+
voice: adminVoice,
|
|
294
|
+
watch,
|
|
295
|
+
});
|
|
207
296
|
const registry = Registry({
|
|
208
297
|
memory,
|
|
209
298
|
adminMine,
|
|
210
299
|
address,
|
|
300
|
+
cipher: modules.cipher,
|
|
211
301
|
deliver: (envelope) => deliver(envelope),
|
|
212
302
|
modules: offeredNames,
|
|
213
303
|
modulesFor,
|
|
214
304
|
announce,
|
|
215
305
|
forget,
|
|
306
|
+
span,
|
|
216
307
|
vouched,
|
|
217
308
|
attest,
|
|
218
309
|
watch,
|
|
310
|
+
extended: () => {
|
|
311
|
+
adminDoor = undefined;
|
|
312
|
+
},
|
|
219
313
|
});
|
|
220
|
-
const admin = (envelope, token) =>
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
vouched,
|
|
227
|
-
voice: adminVoice,
|
|
228
|
-
watch,
|
|
229
|
-
})(envelope, token);
|
|
314
|
+
const admin = (envelope, token) => {
|
|
315
|
+
if (adminDoor === undefined) {
|
|
316
|
+
adminDoor = buildAdmin();
|
|
317
|
+
}
|
|
318
|
+
return adminDoor(envelope, token);
|
|
319
|
+
};
|
|
230
320
|
|
|
231
321
|
announce(address);
|
|
232
322
|
for (const key of adminMine.list('identity/custody/')) {
|
|
@@ -244,7 +334,16 @@ const Ground = (modules = {}) => {
|
|
|
244
334
|
address,
|
|
245
335
|
receive,
|
|
246
336
|
rotate: (bundle) =>
|
|
247
|
-
succeeds({
|
|
337
|
+
succeeds({
|
|
338
|
+
adminMine,
|
|
339
|
+
adminVoice,
|
|
340
|
+
address,
|
|
341
|
+
bundle,
|
|
342
|
+
cells,
|
|
343
|
+
cipher: modules.cipher,
|
|
344
|
+
keychain,
|
|
345
|
+
memory,
|
|
346
|
+
}),
|
|
248
347
|
serve: (voice) => {
|
|
249
348
|
const client = Client({ voice, deliver, resolve });
|
|
250
349
|
const sdlOf = async (at) => (await client.describe(at))?.body?.sdl;
|
|
@@ -268,6 +367,14 @@ const Ground = (modules = {}) => {
|
|
|
268
367
|
},
|
|
269
368
|
});
|
|
270
369
|
},
|
|
370
|
+
seated: (at, from) => {
|
|
371
|
+
if (at === from) {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
const row = held(at) ? memory.read(at, `refs/${from}`)?.value : undefined;
|
|
375
|
+
return row !== undefined && row !== null;
|
|
376
|
+
},
|
|
377
|
+
unproven: () => unprovenRows(cells),
|
|
271
378
|
});
|
|
272
379
|
};
|
|
273
380
|
|
package/index.mjs
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
export { ADMIN } from './admin.mjs';
|
|
2
|
-
export {
|
|
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';
|
|
3
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
|
-
export {
|
|
16
|
+
export { Memory } 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 { Bytes, MEASURE, SPAN, Stamp, Tools } from './tools.mjs';
|
|
10
20
|
export { Voice } from './voice.mjs';
|
package/modules.mjs
CHANGED
|
@@ -1,38 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Tools } from './tools.mjs';
|
|
1
|
+
import { Bytes, Tools } from './tools.mjs';
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const flush = () => {
|
|
9
|
-
if (file) {
|
|
10
|
-
writeFileSync(`${file}.next`, JSON.stringify(Object.fromEntries(cells)));
|
|
11
|
-
renameSync(`${file}.next`, file);
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
return Object.freeze({
|
|
15
|
-
drop: (prefix) => {
|
|
16
|
-
for (const cell of [...cells.keys()].filter((one) => one.startsWith(prefix))) {
|
|
17
|
-
cells.delete(cell);
|
|
18
|
-
}
|
|
19
|
-
flush();
|
|
20
|
-
},
|
|
21
|
-
dump: () => JSON.stringify(Object.fromEntries(cells)),
|
|
22
|
-
keys: (prefix) => [...cells.keys()].filter((cell) => cell.startsWith(prefix)),
|
|
23
|
-
peek: (cell) => cells.get(cell),
|
|
24
|
-
put: (cell, held) => {
|
|
25
|
-
cells.set(cell, held);
|
|
26
|
-
flush();
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const Memory = (cells) => {
|
|
3
|
+
// The ground's own machinery over its cells. Every implementation a host
|
|
4
|
+
// supplies — cells, keychain, cipher, paths, wire — belongs to that host's
|
|
5
|
+
// floor and never to the core (X, Everything is a module).
|
|
6
|
+
const Memory = (cells, unproven, moved) => {
|
|
32
7
|
const held = (address, key) => cells.peek(`${address}/${key}`);
|
|
33
8
|
const writerAt = (address) => {
|
|
34
9
|
const chain = held(address, 'chain/pk');
|
|
35
|
-
return chain === undefined ? Tools.pkOf(address) :
|
|
10
|
+
return chain === undefined ? Tools.pkOf(address) : Bytes.unhex(chain.value);
|
|
36
11
|
};
|
|
37
12
|
return Object.freeze({
|
|
38
13
|
list: (address, prefix) =>
|
|
@@ -42,16 +17,17 @@ const Memory = (cells) => {
|
|
|
42
17
|
|
|
43
18
|
replace: ({ address, pk, nextPkHash, sig }) => {
|
|
44
19
|
const committed = held(address, 'chain/nextPkHash');
|
|
45
|
-
if (committed === undefined || Tools.digest(
|
|
20
|
+
if (committed === undefined || Tools.digest(Bytes.unhex(pk)) !== committed.value) {
|
|
46
21
|
return undefined;
|
|
47
22
|
}
|
|
48
23
|
const version = committed.version + 1;
|
|
49
24
|
const claim = { address, nextPkHash, pk, version };
|
|
50
|
-
if (!Tools.verifies(
|
|
25
|
+
if (!Tools.verifies(Bytes.unhex(pk), Tools.canon(claim), sig)) {
|
|
51
26
|
return undefined;
|
|
52
27
|
}
|
|
53
28
|
cells.put(`${address}/chain/pk`, { value: pk, version });
|
|
54
29
|
cells.put(`${address}/chain/nextPkHash`, { value: nextPkHash, version });
|
|
30
|
+
moved?.(address);
|
|
55
31
|
return version;
|
|
56
32
|
},
|
|
57
33
|
|
|
@@ -64,48 +40,16 @@ const Memory = (cells) => {
|
|
|
64
40
|
return undefined;
|
|
65
41
|
}
|
|
66
42
|
if (!Tools.verifies(writer, Tools.canon({ address, key, value, version }), sig)) {
|
|
43
|
+
unproven?.(address);
|
|
67
44
|
return undefined;
|
|
68
45
|
}
|
|
69
46
|
cells.put(`${address}/${key}`, { value, version });
|
|
47
|
+
if (!key.startsWith('refs/')) {
|
|
48
|
+
moved?.(address);
|
|
49
|
+
}
|
|
70
50
|
return version;
|
|
71
51
|
},
|
|
72
52
|
});
|
|
73
53
|
};
|
|
74
54
|
|
|
75
|
-
|
|
76
|
-
let at = 0;
|
|
77
|
-
return Object.freeze({ now: () => ticks[Math.min(at++, ticks.length - 1)] });
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const Entropy = (grains) => {
|
|
81
|
-
let at = 0;
|
|
82
|
-
return Object.freeze({ grain: () => grains[Math.min(at++, grains.length - 1)] });
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const Keychain = (secret) =>
|
|
86
|
-
Object.freeze({
|
|
87
|
-
seal: (value) => {
|
|
88
|
-
try {
|
|
89
|
-
return Tools.seal(secret, value);
|
|
90
|
-
} catch {
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
unseal: (sealed) => Tools.unseal(secret, sealed),
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const Dns = () => new Map();
|
|
98
|
-
|
|
99
|
-
const Paths = (dns) =>
|
|
100
|
-
Object.freeze({
|
|
101
|
-
find: (address) => dns.get(address),
|
|
102
|
-
publish: (address, endpoint) =>
|
|
103
|
-
endpoint === undefined ? dns.delete(address) : dns.set(address, endpoint),
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
const Wire = () =>
|
|
107
|
-
Object.freeze({
|
|
108
|
-
carry: async (endpoint, envelope) => (endpoint === undefined ? undefined : endpoint(envelope)),
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
export { Cells, Clock, Dns, Entropy, Keychain, Memory, Paths, Wire };
|
|
55
|
+
export { Memory };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nervur",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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",
|
|
@@ -11,12 +11,15 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"*.mjs",
|
|
14
|
-
"
|
|
14
|
+
"GETTING-STARTED.md",
|
|
15
|
+
"NOTICE",
|
|
16
|
+
"SECURITY.md"
|
|
15
17
|
],
|
|
16
18
|
"engines": {
|
|
17
19
|
"node": ">=22"
|
|
18
20
|
},
|
|
19
21
|
"dependencies": {
|
|
22
|
+
"@noble/curves": "2.3.0",
|
|
20
23
|
"graphql": "^17.0.2"
|
|
21
24
|
}
|
|
22
25
|
}
|
package/tools.mjs
CHANGED
|
@@ -1,13 +1,57 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
createDecipheriv,
|
|
4
|
-
createHash,
|
|
5
|
-
createPublicKey,
|
|
6
|
-
randomBytes,
|
|
7
|
-
verify,
|
|
8
|
-
} from 'node:crypto';
|
|
1
|
+
import { ed25519 } from '@noble/curves/ed25519.js';
|
|
2
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
9
3
|
|
|
10
|
-
const
|
|
4
|
+
const HEX = '0123456789abcdef';
|
|
5
|
+
|
|
6
|
+
const utf8 = new TextEncoder();
|
|
7
|
+
const text8 = new TextDecoder();
|
|
8
|
+
|
|
9
|
+
// Buffer is Node's alone. Every byte string here is a Uint8Array, and these
|
|
10
|
+
// are the encodings the substrate names: hex on a digest, base64 on a
|
|
11
|
+
// signature, utf8 under canon.
|
|
12
|
+
const Bytes = Object.freeze({
|
|
13
|
+
base64: (bytes) => {
|
|
14
|
+
let binary = '';
|
|
15
|
+
for (const byte of bytes) {
|
|
16
|
+
binary += String.fromCharCode(byte);
|
|
17
|
+
}
|
|
18
|
+
return btoa(binary);
|
|
19
|
+
},
|
|
20
|
+
concat: (parts) => {
|
|
21
|
+
const whole = new Uint8Array(parts.reduce((run, one) => run + one.length, 0));
|
|
22
|
+
let at = 0;
|
|
23
|
+
for (const one of parts) {
|
|
24
|
+
whole.set(one, at);
|
|
25
|
+
at += one.length;
|
|
26
|
+
}
|
|
27
|
+
return whole;
|
|
28
|
+
},
|
|
29
|
+
equal: (one, other) => one.length === other.length && one.every((byte, at) => byte === other[at]),
|
|
30
|
+
hex: (bytes) => {
|
|
31
|
+
let out = '';
|
|
32
|
+
for (const byte of bytes) {
|
|
33
|
+
out += HEX[byte >> 4] + HEX[byte & 15];
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
},
|
|
37
|
+
of: (value) => utf8.encode(value),
|
|
38
|
+
read: (bytes) => text8.decode(bytes),
|
|
39
|
+
unbase64: (value) => {
|
|
40
|
+
const binary = atob(value);
|
|
41
|
+
const bytes = new Uint8Array(binary.length);
|
|
42
|
+
for (let at = 0; at < binary.length; at += 1) {
|
|
43
|
+
bytes[at] = binary.charCodeAt(at);
|
|
44
|
+
}
|
|
45
|
+
return bytes;
|
|
46
|
+
},
|
|
47
|
+
unhex: (value) => {
|
|
48
|
+
const bytes = new Uint8Array(value.length >> 1);
|
|
49
|
+
for (let at = 0; at < bytes.length; at += 1) {
|
|
50
|
+
bytes[at] = Number.parseInt(value.substr(at << 1, 2), 16);
|
|
51
|
+
}
|
|
52
|
+
return bytes;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
11
55
|
|
|
12
56
|
const B32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
13
57
|
|
|
@@ -15,6 +59,8 @@ const VERSION_G = 6 << 3;
|
|
|
15
59
|
|
|
16
60
|
const MEASURE = 8_388_608;
|
|
17
61
|
|
|
62
|
+
const SPAN = 60_000;
|
|
63
|
+
|
|
18
64
|
const ordered = (value) =>
|
|
19
65
|
value === null || typeof value !== 'object'
|
|
20
66
|
? value
|
|
@@ -68,21 +114,23 @@ const unbase32 = (text) => {
|
|
|
68
114
|
bits -= 8;
|
|
69
115
|
}
|
|
70
116
|
}
|
|
71
|
-
return
|
|
117
|
+
return Uint8Array.from(out);
|
|
72
118
|
};
|
|
73
119
|
|
|
74
120
|
const Tools = Object.freeze({
|
|
75
121
|
address: (pk) => {
|
|
76
|
-
const payload =
|
|
122
|
+
const payload = Bytes.concat([Uint8Array.of(VERSION_G), pk]);
|
|
77
123
|
const crc = crc16(payload);
|
|
78
|
-
return base32(
|
|
124
|
+
return base32(Bytes.concat([payload, Uint8Array.of(crc & 0xff, crc >> 8)]));
|
|
79
125
|
},
|
|
80
|
-
canon: (claim) =>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
126
|
+
canon: (claim) => Bytes.of(JSON.stringify(ordered(claim))),
|
|
127
|
+
// The label arrives as text and the digest is over its utf8 bytes, which is
|
|
128
|
+
// the byte string the substrate has always hashed.
|
|
129
|
+
derive: (root, label) => sha256.create().update(root).update(Bytes.of(label)).digest(),
|
|
130
|
+
digest: (bytes) => Bytes.hex(sha256(bytes)),
|
|
131
|
+
envelope: async (voice, to, op, seq) => {
|
|
84
132
|
const from = Tools.address(voice.pk);
|
|
85
|
-
return { from, op, seq, sig: voice.sign(Tools.canon({ from, op, seq, to })), to };
|
|
133
|
+
return { from, op, seq, sig: await voice.sign(Tools.canon({ from, op, seq, to })), to };
|
|
86
134
|
},
|
|
87
135
|
pkOf: (address) => {
|
|
88
136
|
const bytes = typeof address === 'string' && address.length === 56 ? unbase32(address) : null;
|
|
@@ -91,39 +139,12 @@ const Tools = Object.freeze({
|
|
|
91
139
|
}
|
|
92
140
|
const payload = bytes.subarray(0, 33);
|
|
93
141
|
const checksum = bytes[33] | (bytes[34] << 8);
|
|
94
|
-
return crc16(payload) === checksum ?
|
|
95
|
-
},
|
|
96
|
-
seal: (secret, value) => {
|
|
97
|
-
const iv = randomBytes(12);
|
|
98
|
-
const cipher = createCipheriv('aes-256-gcm', secret, iv);
|
|
99
|
-
const sealed = Buffer.concat([cipher.update(JSON.stringify(value), 'utf8'), cipher.final()]);
|
|
100
|
-
return Buffer.concat([iv, cipher.getAuthTag(), sealed]).toString('base64');
|
|
101
|
-
},
|
|
102
|
-
secret: () => randomBytes(32),
|
|
103
|
-
unseal: (secret, sealed) => {
|
|
104
|
-
try {
|
|
105
|
-
const bytes = Buffer.from(sealed, 'base64');
|
|
106
|
-
const decipher = createDecipheriv('aes-256-gcm', secret, bytes.subarray(0, 12));
|
|
107
|
-
decipher.setAuthTag(bytes.subarray(12, 28));
|
|
108
|
-
return JSON.parse(
|
|
109
|
-
Buffer.concat([decipher.update(bytes.subarray(28)), decipher.final()]).toString('utf8'),
|
|
110
|
-
);
|
|
111
|
-
} catch {
|
|
112
|
-
return undefined;
|
|
113
|
-
}
|
|
142
|
+
return crc16(payload) === checksum ? payload.slice(1) : null;
|
|
114
143
|
},
|
|
144
|
+
secret: () => crypto.getRandomValues(new Uint8Array(32)),
|
|
115
145
|
verifies: (pk, bytes, sig) => {
|
|
116
146
|
try {
|
|
117
|
-
return verify(
|
|
118
|
-
null,
|
|
119
|
-
bytes,
|
|
120
|
-
createPublicKey({
|
|
121
|
-
key: Buffer.concat([ED25519_SPKI_PREFIX, pk]),
|
|
122
|
-
format: 'der',
|
|
123
|
-
type: 'spki',
|
|
124
|
-
}),
|
|
125
|
-
Buffer.from(sig, 'base64'),
|
|
126
|
-
);
|
|
147
|
+
return ed25519.verify(Bytes.unbase64(sig), bytes, pk);
|
|
127
148
|
} catch {
|
|
128
149
|
return false;
|
|
129
150
|
}
|
|
@@ -138,4 +159,4 @@ const Stamp = () => {
|
|
|
138
159
|
};
|
|
139
160
|
};
|
|
140
161
|
|
|
141
|
-
export { MEASURE, Stamp, Tools };
|
|
162
|
+
export { Bytes, MEASURE, SPAN, Stamp, Tools };
|