nervur 0.21.3 → 0.21.5
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/dist/browser/index.d.ts +1 -0
- package/dist/browser/index.js +8 -0
- package/dist/contract/index.d.ts +1 -0
- package/dist/contract/index.js +5 -0
- package/dist/crypto/ed25519.js +22 -14
- package/dist/crypto/subtle.d.ts +0 -7
- package/dist/crypto/subtle.js +0 -18
- package/dist/crypto/x25519.js +12 -9
- package/dist/harbor/catalogue.d.ts +1 -0
- package/dist/harbor/catalogue.js +2 -2
- package/dist/harbor/dock.d.ts +1 -0
- package/dist/harbor/dock.js +8 -3
- package/dist/harbor/harbor.d.ts +2 -0
- package/dist/harbor/harbor.js +6 -0
- package/dist/harbor/terrain.d.ts +1 -0
- package/dist/harbor/terrain.js +6 -0
- package/dist/pointer/bodies.d.ts +1 -0
- package/dist/pointer/bodies.js +7 -1
- package/dist/proof/law.js +1 -1
- package/package.json +2 -1
- package/src/browser/index.ts +9 -0
- package/src/contract/index.ts +5 -0
- package/src/crypto/ed25519.ts +22 -14
- package/src/crypto/subtle.ts +0 -20
- package/src/crypto/x25519.ts +11 -9
- package/src/harbor/catalogue.ts +4 -2
- package/src/harbor/dock.ts +8 -2
- package/src/harbor/harbor.ts +8 -0
- package/src/harbor/terrain.ts +6 -0
- package/src/pointer/bodies.ts +7 -1
- package/src/proof/law.ts +1 -1
package/dist/browser/index.d.ts
CHANGED
package/dist/browser/index.js
CHANGED
|
@@ -156,6 +156,14 @@ export class BrowserTerrain extends PointerTerrain {
|
|
|
156
156
|
super({ entropy, loader: new HeldLoader(parts.modules ?? []), custody: new IdbCustody(where, entropy), memory: new IdbMemory(where), faculties: [webFaculty(), webPush], opens: [WEB_OPENING] });
|
|
157
157
|
this.where = where;
|
|
158
158
|
}
|
|
159
|
+
// A shared or a service worker outlives any one tab, and a service
|
|
160
|
+
// worker is woken for a push with none open. Anywhere else, a page or a
|
|
161
|
+
// worker a tab made, the harbor lives with that tab.
|
|
162
|
+
get foreground() {
|
|
163
|
+
const scope = globalThis;
|
|
164
|
+
const is = (name) => typeof scope[name] === 'function' && globalThis instanceof scope[name];
|
|
165
|
+
return !is('SharedWorkerGlobalScope') && !is('ServiceWorkerGlobalScope');
|
|
166
|
+
}
|
|
159
167
|
async claim() {
|
|
160
168
|
const locks = globalThis.navigator?.locks;
|
|
161
169
|
if (!locks)
|
package/dist/contract/index.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export type Loaded = Module & {
|
|
|
34
34
|
};
|
|
35
35
|
export declare abstract class Loader {
|
|
36
36
|
abstract load(from: string): Promise<Loaded | null>;
|
|
37
|
+
missing(_from: string): string | null;
|
|
37
38
|
}
|
|
38
39
|
export declare abstract class Carrier {
|
|
39
40
|
abstract carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
|
package/dist/contract/index.js
CHANGED
|
@@ -22,6 +22,11 @@ export class Custody {
|
|
|
22
22
|
// crosses a door and never lives in a cell, so it is the
|
|
23
23
|
// one thing beside the seed a terrain hands in: a map, an import, a bundle.
|
|
24
24
|
export class Loader {
|
|
25
|
+
// Why a name it found nothing under holds nothing, where it can say more
|
|
26
|
+
// than that: a bundle names that code comes from it alone.
|
|
27
|
+
missing(_from) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
25
30
|
}
|
|
26
31
|
// Bytes to a ward pk in; bytes, or null for nothing, out. A harbor hands a
|
|
27
32
|
// terrain's carrier what it holds no route for; what it routes goes to its
|
package/dist/crypto/ed25519.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// Ed25519 signs, as RFC 8032 defines it,
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Ed25519 signs, as RFC 8032 defines it, by `@noble/curves` in every
|
|
3
|
+
// engine, since an engine's Web Crypto may refuse a secret the curve takes,
|
|
4
|
+
// and verifies with the cofactorless check the spec names, which the
|
|
5
|
+
// library's own verify does not make. The decoding rules are held here:
|
|
5
6
|
//
|
|
6
7
|
// a signature of any length but sixty-four
|
|
7
8
|
// an `s` at or above the group order
|
|
8
9
|
// an `R` that does not decode, or whose bytes are not its point's encoding
|
|
9
10
|
// a public key that does not decode, or whose y is at or above the prime
|
|
10
11
|
// a public key of small order, in any spelling
|
|
11
|
-
import {
|
|
12
|
+
import { ed25519 } from '@noble/curves/ed25519.js';
|
|
13
|
+
import { concat } from './bytes.js';
|
|
14
|
+
import { own, subtle } from './subtle.js';
|
|
12
15
|
const P = 2n ** 255n - 19n;
|
|
13
16
|
const L = 2n ** 252n + 27742317777372353535851937790883648493n;
|
|
14
17
|
const mod = (a) => ((a % P) + P) % P;
|
|
@@ -63,8 +66,14 @@ const smallOrder = (p) => {
|
|
|
63
66
|
q = add(q, q);
|
|
64
67
|
return q.x === 0n && q.y === q.z;
|
|
65
68
|
};
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
const secretOf = (secret) => {
|
|
70
|
+
if (secret.length !== 32)
|
|
71
|
+
throw new TypeError('a secret is thirty-two bytes');
|
|
72
|
+
return secret;
|
|
73
|
+
};
|
|
74
|
+
export const ed25519Public = async (secret) => ed25519.getPublicKey(secretOf(secret));
|
|
75
|
+
export const sign = async (secret, message) => ed25519.sign(message, secretOf(secret));
|
|
76
|
+
// The cofactorless check, `[s]B = R + [k]A`, on points decoded as above.
|
|
68
77
|
export const verify = async (pk, message, signature) => {
|
|
69
78
|
if (signature.length !== 64)
|
|
70
79
|
return false;
|
|
@@ -73,13 +82,12 @@ export const verify = async (pk, message, signature) => {
|
|
|
73
82
|
return false;
|
|
74
83
|
if (decode(signature.subarray(0, 32)) === null)
|
|
75
84
|
return false;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const imported = await subtle().importKey('raw', own(pk), { name: 'Ed25519' }, false, ['verify']);
|
|
80
|
-
return await subtle().verify('Ed25519', imported, own(signature), own(message));
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
85
|
+
const s = little(signature.subarray(32));
|
|
86
|
+
if (s >= L)
|
|
83
87
|
return false;
|
|
84
|
-
|
|
88
|
+
const R = ed25519.Point.fromBytes(signature.subarray(0, 32));
|
|
89
|
+
const A = ed25519.Point.fromBytes(pk);
|
|
90
|
+
const hashed = new Uint8Array(await subtle().digest('SHA-512', own(concat(signature.subarray(0, 32), pk, message))));
|
|
91
|
+
const k = little(hashed) % L;
|
|
92
|
+
return ed25519.Point.BASE.multiplyUnsafe(s).equals(R.add(A.multiplyUnsafe(k)));
|
|
85
93
|
};
|
package/dist/crypto/subtle.d.ts
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
1
|
export declare const subtle: () => SubtleCrypto;
|
|
2
2
|
export type Bytes = Uint8Array<ArrayBuffer>;
|
|
3
3
|
export declare const own: (bytes: Uint8Array) => Bytes;
|
|
4
|
-
declare const PKCS8: {
|
|
5
|
-
readonly Ed25519: '302e020100300506032b657004220420';
|
|
6
|
-
readonly X25519: '302e020100300506032b656e04220420';
|
|
7
|
-
};
|
|
8
|
-
export declare const importSecret: (curve: keyof typeof PKCS8, secret: Uint8Array, uses: KeyUsage[]) => Promise<CryptoKey>;
|
|
9
|
-
export declare const publicOf: (key: CryptoKey) => Promise<Uint8Array>;
|
|
10
|
-
export {};
|
package/dist/crypto/subtle.js
CHANGED
|
@@ -9,21 +9,3 @@ export const subtle = () => {
|
|
|
9
9
|
};
|
|
10
10
|
// Web Crypto takes a buffer it owns, so every input is copied into one.
|
|
11
11
|
export const own = (bytes) => Uint8Array.from(bytes);
|
|
12
|
-
// The PKCS#8 wrapping of a thirty-two byte secret, which is the whole of it
|
|
13
|
-
// for both curves.
|
|
14
|
-
const PKCS8 = { Ed25519: '302e020100300506032b657004220420', X25519: '302e020100300506032b656e04220420' };
|
|
15
|
-
export const importSecret = (curve, secret, uses) => {
|
|
16
|
-
if (secret.length !== 32)
|
|
17
|
-
throw new TypeError('a secret is thirty-two bytes');
|
|
18
|
-
const prefix = Uint8Array.from(PKCS8[curve].match(/../g), (b) => parseInt(b, 16));
|
|
19
|
-
const wrapped = new Uint8Array(48);
|
|
20
|
-
wrapped.set(prefix);
|
|
21
|
-
wrapped.set(secret, 16);
|
|
22
|
-
return subtle().importKey('pkcs8', wrapped, { name: curve }, true, uses);
|
|
23
|
-
};
|
|
24
|
-
// Web Crypto gives a private key's public half only through a JWK.
|
|
25
|
-
export const publicOf = async (key) => {
|
|
26
|
-
const { x } = await subtle().exportKey('jwk', key);
|
|
27
|
-
const binary = atob(x.replaceAll('-', '+').replaceAll('_', '/'));
|
|
28
|
-
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
29
|
-
};
|
package/dist/crypto/x25519.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// X25519 agrees
|
|
3
|
-
//
|
|
2
|
+
// X25519 agrees, by `@noble/curves` in every engine, since an engine's Web
|
|
3
|
+
// Crypto may refuse a secret the curve takes. A secret is thirty-two bytes,
|
|
4
|
+
// clamped by the function and never before.
|
|
5
|
+
import { x25519 } from '@noble/curves/ed25519.js';
|
|
4
6
|
import { zero } from './bytes.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
export const x25519Public = async (secret) => {
|
|
8
|
+
if (secret.length !== 32)
|
|
9
|
+
throw new TypeError('a secret is thirty-two bytes');
|
|
10
|
+
return x25519.getPublicKey(secret);
|
|
11
|
+
};
|
|
7
12
|
// The agreement, or null for a public key that takes no seal: one whose
|
|
8
|
-
// agreement is thirty-two zero bytes
|
|
13
|
+
// agreement is thirty-two zero bytes.
|
|
9
14
|
export const agree = async (secret, pk) => {
|
|
10
|
-
if (pk.length !== 32)
|
|
15
|
+
if (pk.length !== 32 || secret.length !== 32)
|
|
11
16
|
return null;
|
|
12
17
|
try {
|
|
13
|
-
const
|
|
14
|
-
const theirs = await subtle().importKey('raw', own(pk), { name: 'X25519' }, false, []);
|
|
15
|
-
const shared = new Uint8Array(await subtle().deriveBits({ name: 'X25519', public: theirs }, mine, 256));
|
|
18
|
+
const shared = x25519.getSharedSecret(secret, pk);
|
|
16
19
|
return zero(shared) ? null : shared;
|
|
17
20
|
}
|
|
18
21
|
catch {
|
|
@@ -8,6 +8,7 @@ export type ModuleRow = {
|
|
|
8
8
|
};
|
|
9
9
|
export interface Cataloguing {
|
|
10
10
|
load(from: string): Promise<Loaded | null>;
|
|
11
|
+
missing(from: string): string | null;
|
|
11
12
|
check(modules: readonly Loaded[]): string | null;
|
|
12
13
|
run(modules: readonly Loaded[]): string | null;
|
|
13
14
|
stands(kinds: readonly string[]): boolean;
|
package/dist/harbor/catalogue.js
CHANGED
|
@@ -46,7 +46,7 @@ export class Catalogue extends Faculty {
|
|
|
46
46
|
const refused = loaded instanceof Error
|
|
47
47
|
? loaded.message
|
|
48
48
|
: loaded === null
|
|
49
|
-
? `not found at ${row.from}`
|
|
49
|
+
? (this.#harbor.missing(row.from) ?? `not found at ${row.from}`)
|
|
50
50
|
: loaded.module !== module
|
|
51
51
|
? `${row.from} is module ${loaded.module}`
|
|
52
52
|
: this.#harbor.check([...this.#running.values(), loaded]);
|
|
@@ -64,7 +64,7 @@ export class Catalogue extends Faculty {
|
|
|
64
64
|
if (loaded instanceof Error)
|
|
65
65
|
return { error: loaded.message };
|
|
66
66
|
if (loaded === null)
|
|
67
|
-
return { error: 'no such module' };
|
|
67
|
+
return { error: this.#harbor.missing(args.from) ?? 'no such module' };
|
|
68
68
|
const running = new Map(this.#running);
|
|
69
69
|
running.set(loaded.module, loaded);
|
|
70
70
|
const refused = this.#harbor.run([...running.values()]);
|
package/dist/harbor/dock.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface Hosting {
|
|
|
7
7
|
route(pk: string, at: readonly string[] | null): boolean;
|
|
8
8
|
keep(): Promise<boolean>;
|
|
9
9
|
failures(): Failures;
|
|
10
|
+
readonly foreground: boolean;
|
|
10
11
|
sweep(): Promise<string[]>;
|
|
11
12
|
grounds(kind: string): boolean;
|
|
12
13
|
awaken(key: string): Promise<void>;
|
package/dist/harbor/dock.js
CHANGED
|
@@ -54,11 +54,16 @@ export class Dock extends WardBeing {
|
|
|
54
54
|
super(stance);
|
|
55
55
|
this.#hosting = hosting;
|
|
56
56
|
}
|
|
57
|
-
// The census adds what did not stand, for whoever pilots
|
|
57
|
+
// The census adds what did not stand, for whoever pilots, and whether
|
|
58
|
+
// the harbor lives only as long as a screen.
|
|
58
59
|
describe(asker) {
|
|
59
60
|
const blueprint = super.describe(asker);
|
|
60
|
-
if (pilots(asker, this.notes(asker)))
|
|
61
|
-
blueprint
|
|
61
|
+
if (!pilots(asker, this.notes(asker)))
|
|
62
|
+
return blueprint;
|
|
63
|
+
const notes = blueprint.notes;
|
|
64
|
+
notes.failed = this.#hosting.failures();
|
|
65
|
+
if (this.#hosting.foreground)
|
|
66
|
+
notes.foreground = true;
|
|
62
67
|
return blueprint;
|
|
63
68
|
}
|
|
64
69
|
get hosted() {
|
package/dist/harbor/harbor.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare class Harbor extends Carrier implements Hosting, Cataloguing, Res
|
|
|
15
15
|
sweep(): Promise<string[]>;
|
|
16
16
|
move(name: string, seed: string, from: string | undefined, to: string | undefined): Promise<string | null>;
|
|
17
17
|
release(name: string, store: string | undefined): Promise<void>;
|
|
18
|
+
get foreground(): boolean;
|
|
18
19
|
failures(): Failures;
|
|
19
20
|
get box(): Ward;
|
|
20
21
|
get dock(): Dock;
|
|
@@ -28,6 +29,7 @@ export declare class Harbor extends Carrier implements Hosting, Cataloguing, Res
|
|
|
28
29
|
carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
|
|
29
30
|
classOf(kind: string): BeingClass | undefined;
|
|
30
31
|
load(from: string): Promise<Loaded | null>;
|
|
32
|
+
missing(from: string): string | null;
|
|
31
33
|
check(modules: readonly Loaded[]): string | null;
|
|
32
34
|
run(modules: readonly Loaded[]): string | null;
|
|
33
35
|
stands(kinds: readonly string[]): boolean;
|
package/dist/harbor/harbor.js
CHANGED
|
@@ -153,6 +153,9 @@ export class Harbor extends Carrier {
|
|
|
153
153
|
return being instanceof Store ? [[key, being]] : [];
|
|
154
154
|
});
|
|
155
155
|
}
|
|
156
|
+
get foreground() {
|
|
157
|
+
return this.terrain.foreground;
|
|
158
|
+
}
|
|
156
159
|
// Hosting: what did not stand when the harbor opened, each alone.
|
|
157
160
|
failures() {
|
|
158
161
|
return { wards: { ...this.#failed.wards }, routes: { ...this.#failed.routes } };
|
|
@@ -215,6 +218,9 @@ export class Harbor extends Carrier {
|
|
|
215
218
|
load(from) {
|
|
216
219
|
return this.terrain.loader.load(from);
|
|
217
220
|
}
|
|
221
|
+
missing(from) {
|
|
222
|
+
return this.terrain.loader.missing(from);
|
|
223
|
+
}
|
|
218
224
|
check(modules) {
|
|
219
225
|
return Registry.refusal(this.#kit, modules);
|
|
220
226
|
}
|
package/dist/harbor/terrain.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare abstract class Terrain {
|
|
|
15
15
|
readonly faculties: readonly GroundFaculty[];
|
|
16
16
|
readonly opens: readonly Opening[];
|
|
17
17
|
readonly wakes: boolean;
|
|
18
|
+
get foreground(): boolean;
|
|
18
19
|
claim(): Promise<void>;
|
|
19
20
|
stand(_harbor: Harbor): Promise<void>;
|
|
20
21
|
release(): Promise<void>;
|
package/dist/harbor/terrain.js
CHANGED
|
@@ -6,6 +6,12 @@ export class Terrain {
|
|
|
6
6
|
// Whether its carriers wake and listen where their cells say. A harbor
|
|
7
7
|
// opened for one ask and closed again dials and hears nothing.
|
|
8
8
|
wakes = true;
|
|
9
|
+
// Whether the harbor lives only as long as a screen: a tab's, or a worker
|
|
10
|
+
// one tab owns. It is reached only while that screen is open, and hears
|
|
11
|
+
// no push, so its census says so.
|
|
12
|
+
get foreground() {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
9
15
|
// Before the harbor opens: a terrain whose ground another run holds
|
|
10
16
|
// refuses here. Once it stands: whatever the ground serves beside it.
|
|
11
17
|
// Once its carriers sleep: what that served, gone.
|
package/dist/pointer/bodies.d.ts
CHANGED
package/dist/pointer/bodies.js
CHANGED
|
@@ -28,7 +28,9 @@ export class HeldCustody extends Custody {
|
|
|
28
28
|
return Promise.resolve(this.#seed.slice());
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
// Modules handed to this object, found by the name each declares
|
|
31
|
+
// Modules handed to this object, found by the name each declares: the
|
|
32
|
+
// bundle a ground with no disk to load from carries, an edge's or a
|
|
33
|
+
// browser's. A path, a package or any other name is no module of it.
|
|
32
34
|
export class HeldLoader extends Loader {
|
|
33
35
|
#modules = new Map();
|
|
34
36
|
constructor(modules = []) {
|
|
@@ -40,6 +42,10 @@ export class HeldLoader extends Loader {
|
|
|
40
42
|
const m = this.#modules.get(from);
|
|
41
43
|
return Promise.resolve(m ? { module: m.module, version: m.version, classes: m.classes, from } : null);
|
|
42
44
|
}
|
|
45
|
+
missing(from) {
|
|
46
|
+
const carried = [...this.#modules.keys()];
|
|
47
|
+
return `${from} is no module this bundle carries${carried.length === 0 ? ', and it carries none' : `: it carries ${carried.join(', ')}`}`;
|
|
48
|
+
}
|
|
43
49
|
}
|
|
44
50
|
// Entries that live as long as this object, copied in and out, which is
|
|
45
51
|
// what a restart in one process finds.
|
package/dist/proof/law.js
CHANGED
|
@@ -194,7 +194,7 @@ export const lawScenes = (t, expect) => [
|
|
|
194
194
|
expect.same(lost.names, ['w'], 'the rest of the harbor stands');
|
|
195
195
|
expect.same((await census(lost)).beings.echo?.absent, true);
|
|
196
196
|
expect.same(await lost.root('ask', { being: 'catalogue', method: 'modules' }), {
|
|
197
|
-
answer: { [law.module]: { version: '1', running: null, from: law.module, failed:
|
|
197
|
+
answer: { [law.module]: { version: '1', running: null, from: law.module, failed: `${law.module} is no module this bundle carries, and it carries none` } },
|
|
198
198
|
});
|
|
199
199
|
const again = await world.restart('h', { ...t.again(bodies), loader: new HeldLoader([law]) });
|
|
200
200
|
expect.same((await census(again)).beings.echo?.absent, false);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nervur",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.5",
|
|
4
4
|
"description": "Nervur's kit of Quo: an object asks another object and gets an answer, without knowing where it is.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"quo",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"node": ">=22.18"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
+
"@noble/curves": "2.4.0",
|
|
67
68
|
"@noble/post-quantum": "0.7.1"
|
|
68
69
|
},
|
|
69
70
|
"publishConfig": {
|
package/src/browser/index.ts
CHANGED
|
@@ -175,6 +175,15 @@ export class BrowserTerrain extends PointerTerrain {
|
|
|
175
175
|
this.where = where;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
// A shared or a service worker outlives any one tab, and a service
|
|
179
|
+
// worker is woken for a push with none open. Anywhere else, a page or a
|
|
180
|
+
// worker a tab made, the harbor lives with that tab.
|
|
181
|
+
override get foreground(): boolean {
|
|
182
|
+
const scope = globalThis as unknown as Record<string, unknown>;
|
|
183
|
+
const is = (name: string): boolean => typeof scope[name] === 'function' && globalThis instanceof (scope[name] as new () => unknown);
|
|
184
|
+
return !is('SharedWorkerGlobalScope') && !is('ServiceWorkerGlobalScope');
|
|
185
|
+
}
|
|
186
|
+
|
|
178
187
|
override async claim(): Promise<void> {
|
|
179
188
|
const locks = (globalThis as { navigator?: { locks?: LockManager } }).navigator?.locks;
|
|
180
189
|
if (!locks) return;
|
package/src/contract/index.ts
CHANGED
|
@@ -67,6 +67,11 @@ export type Loaded = Module & { readonly from: string };
|
|
|
67
67
|
// one thing beside the seed a terrain hands in: a map, an import, a bundle.
|
|
68
68
|
export abstract class Loader {
|
|
69
69
|
abstract load(from: string): Promise<Loaded | null>;
|
|
70
|
+
// Why a name it found nothing under holds nothing, where it can say more
|
|
71
|
+
// than that: a bundle names that code comes from it alone.
|
|
72
|
+
missing(_from: string): string | null {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
// Bytes to a ward pk in; bytes, or null for nothing, out. A harbor hands a
|
package/src/crypto/ed25519.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// Ed25519 signs, as RFC 8032 defines it,
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Ed25519 signs, as RFC 8032 defines it, by `@noble/curves` in every
|
|
3
|
+
// engine, since an engine's Web Crypto may refuse a secret the curve takes,
|
|
4
|
+
// and verifies with the cofactorless check the spec names, which the
|
|
5
|
+
// library's own verify does not make. The decoding rules are held here:
|
|
5
6
|
//
|
|
6
7
|
// a signature of any length but sixty-four
|
|
7
8
|
// an `s` at or above the group order
|
|
8
9
|
// an `R` that does not decode, or whose bytes are not its point's encoding
|
|
9
10
|
// a public key that does not decode, or whose y is at or above the prime
|
|
10
11
|
// a public key of small order, in any spelling
|
|
11
|
-
import {
|
|
12
|
+
import { ed25519 } from '@noble/curves/ed25519.js';
|
|
13
|
+
import { concat } from './bytes.ts';
|
|
14
|
+
import { own, subtle } from './subtle.ts';
|
|
12
15
|
|
|
13
16
|
const P = 2n ** 255n - 19n;
|
|
14
17
|
const L = 2n ** 252n + 27742317777372353535851937790883648493n;
|
|
@@ -64,21 +67,26 @@ const smallOrder = (p: Point): boolean => {
|
|
|
64
67
|
return q.x === 0n && q.y === q.z;
|
|
65
68
|
};
|
|
66
69
|
|
|
67
|
-
|
|
70
|
+
const secretOf = (secret: Uint8Array): Uint8Array => {
|
|
71
|
+
if (secret.length !== 32) throw new TypeError('a secret is thirty-two bytes');
|
|
72
|
+
return secret;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const ed25519Public = async (secret: Uint8Array): Promise<Uint8Array> => ed25519.getPublicKey(secretOf(secret));
|
|
68
76
|
|
|
69
|
-
export const sign = async (secret: Uint8Array, message: Uint8Array): Promise<Uint8Array> =>
|
|
70
|
-
new Uint8Array(await subtle().sign('Ed25519', await importSecret('Ed25519', secret, ['sign']), own(message)));
|
|
77
|
+
export const sign = async (secret: Uint8Array, message: Uint8Array): Promise<Uint8Array> => ed25519.sign(message, secretOf(secret));
|
|
71
78
|
|
|
79
|
+
// The cofactorless check, `[s]B = R + [k]A`, on points decoded as above.
|
|
72
80
|
export const verify = async (pk: Uint8Array, message: Uint8Array, signature: Uint8Array): Promise<boolean> => {
|
|
73
81
|
if (signature.length !== 64) return false;
|
|
74
82
|
const key = decode(pk);
|
|
75
83
|
if (key === null || smallOrder(key)) return false;
|
|
76
84
|
if (decode(signature.subarray(0, 32)) === null) return false;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
const s = little(signature.subarray(32));
|
|
86
|
+
if (s >= L) return false;
|
|
87
|
+
const R = ed25519.Point.fromBytes(signature.subarray(0, 32));
|
|
88
|
+
const A = ed25519.Point.fromBytes(pk);
|
|
89
|
+
const hashed = new Uint8Array(await subtle().digest('SHA-512', own(concat(signature.subarray(0, 32), pk, message))));
|
|
90
|
+
const k = little(hashed) % L;
|
|
91
|
+
return ed25519.Point.BASE.multiplyUnsafe(s).equals(R.add(A.multiplyUnsafe(k)));
|
|
84
92
|
};
|
package/src/crypto/subtle.ts
CHANGED
|
@@ -11,23 +11,3 @@ export type Bytes = Uint8Array<ArrayBuffer>;
|
|
|
11
11
|
|
|
12
12
|
// Web Crypto takes a buffer it owns, so every input is copied into one.
|
|
13
13
|
export const own = (bytes: Uint8Array): Bytes => Uint8Array.from(bytes);
|
|
14
|
-
|
|
15
|
-
// The PKCS#8 wrapping of a thirty-two byte secret, which is the whole of it
|
|
16
|
-
// for both curves.
|
|
17
|
-
const PKCS8 = { Ed25519: '302e020100300506032b657004220420', X25519: '302e020100300506032b656e04220420' } as const;
|
|
18
|
-
|
|
19
|
-
export const importSecret = (curve: keyof typeof PKCS8, secret: Uint8Array, uses: KeyUsage[]): Promise<CryptoKey> => {
|
|
20
|
-
if (secret.length !== 32) throw new TypeError('a secret is thirty-two bytes');
|
|
21
|
-
const prefix = Uint8Array.from(PKCS8[curve].match(/../g)!, (b) => parseInt(b, 16));
|
|
22
|
-
const wrapped = new Uint8Array(48);
|
|
23
|
-
wrapped.set(prefix);
|
|
24
|
-
wrapped.set(secret, 16);
|
|
25
|
-
return subtle().importKey('pkcs8', wrapped, { name: curve }, true, uses);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
// Web Crypto gives a private key's public half only through a JWK.
|
|
29
|
-
export const publicOf = async (key: CryptoKey): Promise<Uint8Array> => {
|
|
30
|
-
const { x } = await subtle().exportKey('jwk', key);
|
|
31
|
-
const binary = atob(x!.replaceAll('-', '+').replaceAll('_', '/'));
|
|
32
|
-
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
33
|
-
};
|
package/src/crypto/x25519.ts
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// X25519 agrees
|
|
3
|
-
//
|
|
2
|
+
// X25519 agrees, by `@noble/curves` in every engine, since an engine's Web
|
|
3
|
+
// Crypto may refuse a secret the curve takes. A secret is thirty-two bytes,
|
|
4
|
+
// clamped by the function and never before.
|
|
5
|
+
import { x25519 } from '@noble/curves/ed25519.js';
|
|
4
6
|
import { zero } from './bytes.ts';
|
|
5
|
-
import { importSecret, own, publicOf, subtle } from './subtle.ts';
|
|
6
7
|
|
|
7
|
-
export const x25519Public = async (secret: Uint8Array): Promise<Uint8Array> =>
|
|
8
|
+
export const x25519Public = async (secret: Uint8Array): Promise<Uint8Array> => {
|
|
9
|
+
if (secret.length !== 32) throw new TypeError('a secret is thirty-two bytes');
|
|
10
|
+
return x25519.getPublicKey(secret);
|
|
11
|
+
};
|
|
8
12
|
|
|
9
13
|
// The agreement, or null for a public key that takes no seal: one whose
|
|
10
|
-
// agreement is thirty-two zero bytes
|
|
14
|
+
// agreement is thirty-two zero bytes.
|
|
11
15
|
export const agree = async (secret: Uint8Array, pk: Uint8Array): Promise<Uint8Array | null> => {
|
|
12
|
-
if (pk.length !== 32) return null;
|
|
16
|
+
if (pk.length !== 32 || secret.length !== 32) return null;
|
|
13
17
|
try {
|
|
14
|
-
const
|
|
15
|
-
const theirs = await subtle().importKey('raw', own(pk), { name: 'X25519' }, false, []);
|
|
16
|
-
const shared = new Uint8Array(await subtle().deriveBits({ name: 'X25519', public: theirs }, mine, 256));
|
|
18
|
+
const shared = x25519.getSharedSecret(secret, pk);
|
|
17
19
|
return zero(shared) ? null : shared;
|
|
18
20
|
} catch {
|
|
19
21
|
return null;
|
package/src/harbor/catalogue.ts
CHANGED
|
@@ -29,6 +29,8 @@ export type ModuleRow = { version: string; from: string; kinds: string[] };
|
|
|
29
29
|
// What the catalogue asks of her harbor.
|
|
30
30
|
export interface Cataloguing {
|
|
31
31
|
load(from: string): Promise<Loaded | null>;
|
|
32
|
+
// Why nothing loads from a name, where the loader says more.
|
|
33
|
+
missing(from: string): string | null;
|
|
32
34
|
// Why these modules cannot run together, or null.
|
|
33
35
|
check(modules: readonly Loaded[]): string | null;
|
|
34
36
|
// The modules run from now on, or why they cannot be.
|
|
@@ -72,7 +74,7 @@ export class Catalogue extends Faculty {
|
|
|
72
74
|
loaded instanceof Error
|
|
73
75
|
? loaded.message
|
|
74
76
|
: loaded === null
|
|
75
|
-
? `not found at ${row.from}`
|
|
77
|
+
? (this.#harbor.missing(row.from) ?? `not found at ${row.from}`)
|
|
76
78
|
: loaded.module !== module
|
|
77
79
|
? `${row.from} is module ${loaded.module}`
|
|
78
80
|
: this.#harbor.check([...this.#running.values(), loaded]);
|
|
@@ -86,7 +88,7 @@ export class Catalogue extends Faculty {
|
|
|
86
88
|
if (!text(args.from)) return { error: 'from is text' };
|
|
87
89
|
const loaded = await this.#harbor.load(args.from).catch((e: Error) => e);
|
|
88
90
|
if (loaded instanceof Error) return { error: loaded.message };
|
|
89
|
-
if (loaded === null) return { error: 'no such module' };
|
|
91
|
+
if (loaded === null) return { error: this.#harbor.missing(args.from) ?? 'no such module' };
|
|
90
92
|
const running = new Map(this.#running);
|
|
91
93
|
running.set(loaded.module, loaded);
|
|
92
94
|
const refused = this.#harbor.run([...running.values()]);
|
package/src/harbor/dock.ts
CHANGED
|
@@ -46,6 +46,8 @@ export interface Hosting {
|
|
|
46
46
|
keep(): Promise<boolean>;
|
|
47
47
|
// What did not stand when the harbor opened.
|
|
48
48
|
failures(): Failures;
|
|
49
|
+
// Whether the harbor lives only as long as a screen.
|
|
50
|
+
readonly foreground: boolean;
|
|
49
51
|
// Every place of the memory the dock does not name, gone.
|
|
50
52
|
sweep(): Promise<string[]>;
|
|
51
53
|
// Whether a kind is a carrier this harbor's ground ships.
|
|
@@ -92,10 +94,14 @@ export class Dock extends WardBeing implements Lender {
|
|
|
92
94
|
this.#hosting = hosting;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
// The census adds what did not stand, for whoever pilots
|
|
97
|
+
// The census adds what did not stand, for whoever pilots, and whether
|
|
98
|
+
// the harbor lives only as long as a screen.
|
|
96
99
|
override describe(asker: Asker): Blueprint {
|
|
97
100
|
const blueprint = super.describe(asker);
|
|
98
|
-
if (pilots(asker, this.notes(asker)))
|
|
101
|
+
if (!pilots(asker, this.notes(asker))) return blueprint;
|
|
102
|
+
const notes = blueprint.notes as JsonObject;
|
|
103
|
+
notes.failed = this.#hosting.failures();
|
|
104
|
+
if (this.#hosting.foreground) notes.foreground = true;
|
|
99
105
|
return blueprint;
|
|
100
106
|
}
|
|
101
107
|
|
package/src/harbor/harbor.ts
CHANGED
|
@@ -175,6 +175,10 @@ export class Harbor extends Carrier implements Hosting, Cataloguing, Resolving {
|
|
|
175
175
|
});
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
get foreground(): boolean {
|
|
179
|
+
return this.terrain.foreground;
|
|
180
|
+
}
|
|
181
|
+
|
|
178
182
|
// Hosting: what did not stand when the harbor opened, each alone.
|
|
179
183
|
failures(): Failures {
|
|
180
184
|
return { wards: { ...this.#failed.wards }, routes: { ...this.#failed.routes } };
|
|
@@ -246,6 +250,10 @@ export class Harbor extends Carrier implements Hosting, Cataloguing, Resolving {
|
|
|
246
250
|
return this.terrain.loader.load(from);
|
|
247
251
|
}
|
|
248
252
|
|
|
253
|
+
missing(from: string): string | null {
|
|
254
|
+
return this.terrain.loader.missing(from);
|
|
255
|
+
}
|
|
256
|
+
|
|
249
257
|
check(modules: readonly Loaded[]): string | null {
|
|
250
258
|
return Registry.refusal(this.#kit, modules);
|
|
251
259
|
}
|
package/src/harbor/terrain.ts
CHANGED
|
@@ -25,6 +25,12 @@ export abstract class Terrain {
|
|
|
25
25
|
// Whether its carriers wake and listen where their cells say. A harbor
|
|
26
26
|
// opened for one ask and closed again dials and hears nothing.
|
|
27
27
|
readonly wakes: boolean = true;
|
|
28
|
+
// Whether the harbor lives only as long as a screen: a tab's, or a worker
|
|
29
|
+
// one tab owns. It is reached only while that screen is open, and hears
|
|
30
|
+
// no push, so its census says so.
|
|
31
|
+
get foreground(): boolean {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
28
34
|
|
|
29
35
|
// Before the harbor opens: a terrain whose ground another run holds
|
|
30
36
|
// refuses here. Once it stands: whatever the ground serves beside it.
|
package/src/pointer/bodies.ts
CHANGED
|
@@ -32,7 +32,9 @@ export class HeldCustody extends Custody {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
// Modules handed to this object, found by the name each declares
|
|
35
|
+
// Modules handed to this object, found by the name each declares: the
|
|
36
|
+
// bundle a ground with no disk to load from carries, an edge's or a
|
|
37
|
+
// browser's. A path, a package or any other name is no module of it.
|
|
36
38
|
export class HeldLoader extends Loader {
|
|
37
39
|
readonly #modules = new Map<string, Module>();
|
|
38
40
|
constructor(modules: readonly Module[] = []) {
|
|
@@ -43,6 +45,10 @@ export class HeldLoader extends Loader {
|
|
|
43
45
|
const m = this.#modules.get(from);
|
|
44
46
|
return Promise.resolve(m ? { module: m.module, version: m.version, classes: m.classes, from } : null);
|
|
45
47
|
}
|
|
48
|
+
override missing(from: string): string {
|
|
49
|
+
const carried = [...this.#modules.keys()];
|
|
50
|
+
return `${from} is no module this bundle carries${carried.length === 0 ? ', and it carries none' : `: it carries ${carried.join(', ')}`}`;
|
|
51
|
+
}
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
// Entries that live as long as this object, copied in and out, which is
|
package/src/proof/law.ts
CHANGED
|
@@ -203,7 +203,7 @@ export const lawScenes = (t: Terrain, expect: Expect): [string, () => Promise<vo
|
|
|
203
203
|
expect.same(lost.names, ['w'], 'the rest of the harbor stands');
|
|
204
204
|
expect.same(((await census(lost)) as { beings: Record<string, { absent: boolean }> }).beings.echo?.absent, true);
|
|
205
205
|
expect.same(await lost.root('ask', { being: 'catalogue', method: 'modules' }), {
|
|
206
|
-
answer: { [law.module]: { version: '1', running: null, from: law.module, failed:
|
|
206
|
+
answer: { [law.module]: { version: '1', running: null, from: law.module, failed: `${law.module} is no module this bundle carries, and it carries none` } },
|
|
207
207
|
});
|
|
208
208
|
const again = await world.restart('h', { ...t.again(bodies), loader: new HeldLoader([law]) });
|
|
209
209
|
expect.same(((await census(again)) as { beings: Record<string, { absent: boolean }> }).beings.echo?.absent, false);
|