nervur 0.22.0 → 0.22.2-3
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/AUTHORING.md +741 -0
- package/KIT-SPEC.md +286 -0
- package/LICENSE +1 -1
- package/NOTICE +6 -2
- package/README.md +97 -424
- package/dist/app/app-ground.d.ts +18 -0
- package/dist/app/app-ground.js +25 -0
- package/dist/app/index.d.ts +2 -0
- package/dist/app/index.js +5 -0
- package/dist/app/native.d.ts +46 -0
- package/dist/app/native.js +101 -0
- package/dist/being/being.d.ts +229 -0
- package/dist/being/being.js +20 -0
- package/dist/being/covers.d.ts +9 -0
- package/dist/being/covers.js +21 -0
- package/dist/being/index.d.ts +3 -0
- package/dist/being/index.js +5 -0
- package/dist/being/need.d.ts +49 -0
- package/dist/being/need.js +33 -0
- package/dist/being/schema.d.ts +100 -0
- package/dist/being/schema.js +132 -0
- package/dist/being/table.d.ts +42 -0
- package/dist/being/table.js +288 -0
- package/dist/bench/bench-ground.d.ts +55 -0
- package/dist/bench/bench-ground.js +101 -0
- package/dist/bench/bench.d.ts +87 -0
- package/dist/bench/bench.js +260 -0
- package/dist/bench/fake-carry.d.ts +32 -0
- package/dist/bench/fake-carry.js +56 -0
- package/dist/bench/fake-clock.d.ts +17 -0
- package/dist/bench/fake-clock.js +34 -0
- package/dist/bench/fake-custody.d.ts +9 -0
- package/dist/bench/fake-custody.js +10 -0
- package/dist/bench/fake-faculty.d.ts +25 -0
- package/dist/bench/fake-faculty.js +48 -0
- package/dist/bench/fake-keys.d.ts +5 -0
- package/dist/bench/fake-keys.js +9 -0
- package/dist/bench/fake-memory.d.ts +14 -0
- package/dist/bench/fake-memory.js +46 -0
- package/dist/bench/fake-network.d.ts +57 -0
- package/dist/bench/fake-network.js +169 -0
- package/dist/bench/index.d.ts +11 -0
- package/dist/bench/index.js +13 -0
- package/dist/bench/seeded.d.ts +11 -0
- package/dist/bench/seeded.js +37 -0
- package/dist/bench/settle.d.ts +2 -0
- package/dist/bench/settle.js +49 -0
- package/dist/bench/stewards.d.ts +17 -0
- package/dist/bench/stewards.js +20 -0
- package/dist/bodies/class-list.d.ts +14 -0
- package/dist/bodies/class-list.js +33 -0
- package/dist/bodies/joined-carry.d.ts +23 -0
- package/dist/bodies/joined-carry.js +42 -0
- package/dist/bodies/noble-crypto.d.ts +22 -0
- package/dist/bodies/noble-crypto.js +88 -0
- package/dist/bodies/seed-keys.d.ts +10 -0
- package/dist/bodies/seed-keys.js +42 -0
- package/dist/bodies/strict-tools.d.ts +10 -0
- package/dist/bodies/strict-tools.js +306 -0
- package/dist/bodies/web-carry.d.ts +65 -0
- package/dist/bodies/web-carry.js +224 -0
- package/dist/bodies/web-clock.d.ts +12 -0
- package/dist/bodies/web-clock.js +33 -0
- package/dist/browser/browser-ground.d.ts +69 -0
- package/dist/browser/browser-ground.js +219 -0
- package/dist/browser/index.d.ts +4 -0
- package/dist/browser/index.js +7 -0
- package/dist/browser/indexeddb-memory.d.ts +17 -0
- package/dist/browser/indexeddb-memory.js +97 -0
- package/dist/browser/locked-custody.d.ts +25 -0
- package/dist/browser/locked-custody.js +89 -0
- package/dist/browser/origin-classes.d.ts +7 -0
- package/dist/browser/origin-classes.js +31 -0
- package/dist/foundation.d.ts +150 -0
- package/dist/foundation.js +4 -0
- package/dist/ground/ground.d.ts +177 -0
- package/dist/ground/ground.js +363 -0
- package/dist/house/crossing.d.ts +37 -0
- package/dist/house/crossing.js +131 -0
- package/dist/house/house.d.ts +80 -0
- package/dist/house/house.js +1753 -0
- package/dist/house/rows-shape.d.ts +103 -0
- package/dist/house/rows-shape.js +12 -0
- package/dist/house/rows.d.ts +27 -0
- package/dist/house/rows.js +168 -0
- package/dist/index.d.ts +29 -1
- package/dist/index.js +25 -14
- package/dist/node/bridge.d.ts +17 -0
- package/dist/node/bridge.js +188 -0
- package/dist/node/cli.js +170 -0
- package/dist/node/custody.d.ts +20 -0
- package/dist/node/custody.js +34 -0
- package/dist/node/file-keys.d.ts +7 -0
- package/dist/node/file-keys.js +30 -0
- package/dist/node/file-memory.d.ts +13 -0
- package/dist/node/file-memory.js +91 -0
- package/dist/node/folder-classes.d.ts +5 -0
- package/dist/node/folder-classes.js +40 -0
- package/dist/node/gone.d.ts +2 -0
- package/dist/node/gone.js +7 -0
- package/dist/node/hand.d.ts +29 -0
- package/dist/node/hand.js +85 -0
- package/dist/node/held-keys.d.ts +14 -0
- package/dist/node/held-keys.js +36 -0
- package/dist/node/http.d.ts +11 -0
- package/dist/node/http.js +90 -0
- package/dist/node/index.d.ts +13 -0
- package/dist/node/index.js +15 -0
- package/dist/node/keychain-keys.d.ts +10 -0
- package/dist/node/keychain-keys.js +51 -0
- package/dist/node/ledger-memory.d.ts +17 -0
- package/dist/node/ledger-memory.js +151 -0
- package/dist/node/lock.d.ts +10 -0
- package/dist/node/lock.js +78 -0
- package/dist/node/node-ground.d.ts +33 -0
- package/dist/node/node-ground.js +147 -0
- package/dist/node/notify.d.ts +1 -0
- package/dist/node/notify.js +18 -0
- package/dist/node/tcp-carry.d.ts +39 -0
- package/dist/node/tcp-carry.js +216 -0
- package/dist/node/websocket.d.ts +6 -0
- package/dist/node/websocket.js +118 -0
- package/dist/quo/frame.d.ts +20 -0
- package/dist/quo/frame.js +48 -0
- package/dist/quo/room.d.ts +154 -0
- package/dist/quo/room.js +362 -0
- package/dist/serve/index.d.ts +27 -0
- package/dist/serve/index.js +55 -0
- package/package.json +49 -47
- package/dist/browser.d.ts +0 -1
- package/dist/browser.js +0 -8
- package/dist/cli.js +0 -16
- package/dist/core/being/being.d.ts +0 -17
- package/dist/core/being/being.js +0 -72
- package/dist/core/being/digest.d.ts +0 -3
- package/dist/core/being/digest.js +0 -17
- package/dist/core/being/faculty.d.ts +0 -10
- package/dist/core/being/faculty.js +0 -77
- package/dist/core/being/index.d.ts +0 -6
- package/dist/core/being/index.js +0 -8
- package/dist/core/being/kind.d.ts +0 -6
- package/dist/core/being/kind.js +0 -34
- package/dist/core/being/types.d.ts +0 -61
- package/dist/core/being/types.js +0 -4
- package/dist/core/being/words.d.ts +0 -13
- package/dist/core/being/words.js +0 -14
- package/dist/core/browser/index.d.ts +0 -37
- package/dist/core/browser/index.js +0 -196
- package/dist/core/browser/worker.d.ts +0 -11
- package/dist/core/browser/worker.js +0 -37
- package/dist/core/cli/command.d.ts +0 -13
- package/dist/core/cli/command.js +0 -330
- package/dist/core/cli/edge.d.ts +0 -7
- package/dist/core/cli/edge.js +0 -79
- package/dist/core/cli/harbor.d.ts +0 -25
- package/dist/core/cli/harbor.js +0 -115
- package/dist/core/contract/index.d.ts +0 -42
- package/dist/core/contract/index.js +0 -34
- package/dist/core/contract/link.d.ts +0 -4
- package/dist/core/contract/link.js +0 -45
- package/dist/core/crypto/aes.d.ts +0 -3
- package/dist/core/crypto/aes.js +0 -24
- package/dist/core/crypto/bytes.d.ts +0 -6
- package/dist/core/crypto/bytes.js +0 -33
- package/dist/core/crypto/ed25519.d.ts +0 -3
- package/dist/core/crypto/ed25519.js +0 -93
- package/dist/core/crypto/hash.d.ts +0 -2
- package/dist/core/crypto/hash.js +0 -11
- package/dist/core/crypto/index.d.ts +0 -7
- package/dist/core/crypto/index.js +0 -10
- package/dist/core/crypto/json.d.ts +0 -8
- package/dist/core/crypto/json.js +0 -250
- package/dist/core/crypto/mlkem.d.ts +0 -12
- package/dist/core/crypto/mlkem.js +0 -36
- package/dist/core/crypto/subtle.d.ts +0 -3
- package/dist/core/crypto/subtle.js +0 -11
- package/dist/core/crypto/x25519.d.ts +0 -2
- package/dist/core/crypto/x25519.js +0 -24
- package/dist/core/edge/index.d.ts +0 -164
- package/dist/core/edge/index.js +0 -739
- package/dist/core/edge/kit.d.ts +0 -2
- package/dist/core/edge/kit.js +0 -2
- package/dist/core/folder/index.d.ts +0 -18
- package/dist/core/folder/index.js +0 -130
- package/dist/core/git/http.d.ts +0 -6
- package/dist/core/git/http.js +0 -102
- package/dist/core/git/index.d.ts +0 -5
- package/dist/core/git/index.js +0 -10
- package/dist/core/git/object.d.ts +0 -40
- package/dist/core/git/object.js +0 -103
- package/dist/core/git/pack.d.ts +0 -9
- package/dist/core/git/pack.js +0 -171
- package/dist/core/git/store.d.ts +0 -23
- package/dist/core/git/store.js +0 -120
- package/dist/core/git/zlib.d.ts +0 -6
- package/dist/core/git/zlib.js +0 -210
- package/dist/core/harbor/carrying.d.ts +0 -51
- package/dist/core/harbor/carrying.js +0 -69
- package/dist/core/harbor/catalogue.d.ts +0 -33
- package/dist/core/harbor/catalogue.js +0 -365
- package/dist/core/harbor/dna.d.ts +0 -13
- package/dist/core/harbor/dna.js +0 -120
- package/dist/core/harbor/dock.d.ts +0 -62
- package/dist/core/harbor/dock.js +0 -381
- package/dist/core/harbor/harbor.d.ts +0 -16
- package/dist/core/harbor/harbor.js +0 -438
- package/dist/core/harbor/index.d.ts +0 -11
- package/dist/core/harbor/index.js +0 -14
- package/dist/core/harbor/memory.d.ts +0 -10
- package/dist/core/harbor/memory.js +0 -25
- package/dist/core/harbor/package.d.ts +0 -20
- package/dist/core/harbor/package.js +0 -172
- package/dist/core/harbor/probe.d.ts +0 -22
- package/dist/core/harbor/probe.js +0 -15
- package/dist/core/harbor/registry.d.ts +0 -15
- package/dist/core/harbor/registry.js +0 -110
- package/dist/core/harbor/root-line.d.ts +0 -7
- package/dist/core/harbor/root-line.js +0 -9
- package/dist/core/harbor/terrain.d.ts +0 -25
- package/dist/core/harbor/terrain.js +0 -28
- package/dist/core/http/index.d.ts +0 -2
- package/dist/core/http/index.js +0 -154
- package/dist/core/http/websocket.d.ts +0 -29
- package/dist/core/http/websocket.js +0 -133
- package/dist/core/line/answer.d.ts +0 -8
- package/dist/core/line/answer.js +0 -60
- package/dist/core/line/frame.d.ts +0 -29
- package/dist/core/line/frame.js +0 -77
- package/dist/core/line/ground.d.ts +0 -3
- package/dist/core/line/ground.js +0 -7
- package/dist/core/line/index.d.ts +0 -4
- package/dist/core/line/index.js +0 -8
- package/dist/core/line/web.d.ts +0 -12
- package/dist/core/line/web.js +0 -117
- package/dist/core/node/index.d.ts +0 -23
- package/dist/core/node/index.js +0 -124
- package/dist/core/pointer/bodies.d.ts +0 -31
- package/dist/core/pointer/bodies.js +0 -118
- package/dist/core/pointer/index.d.ts +0 -2
- package/dist/core/pointer/index.js +0 -4
- package/dist/core/pointer/world.d.ts +0 -45
- package/dist/core/pointer/world.js +0 -106
- package/dist/core/proof/contracts.d.ts +0 -19
- package/dist/core/proof/contracts.js +0 -116
- package/dist/core/proof/expect.d.ts +0 -6
- package/dist/core/proof/expect.js +0 -18
- package/dist/core/proof/index.d.ts +0 -3
- package/dist/core/proof/index.js +0 -14
- package/dist/core/proof/law.d.ts +0 -25
- package/dist/core/proof/law.js +0 -715
- package/dist/core/quo/address.d.ts +0 -19
- package/dist/core/quo/address.js +0 -84
- package/dist/core/quo/door.d.ts +0 -34
- package/dist/core/quo/door.js +0 -174
- package/dist/core/quo/index.d.ts +0 -9
- package/dist/core/quo/index.js +0 -12
- package/dist/core/quo/invitation.d.ts +0 -8
- package/dist/core/quo/invitation.js +0 -19
- package/dist/core/quo/keys.d.ts +0 -40
- package/dist/core/quo/keys.js +0 -79
- package/dist/core/quo/payload.d.ts +0 -15
- package/dist/core/quo/payload.js +0 -55
- package/dist/core/quo/relations.d.ts +0 -40
- package/dist/core/quo/relations.js +0 -33
- package/dist/core/quo/reply.d.ts +0 -13
- package/dist/core/quo/reply.js +0 -35
- package/dist/core/quo/seal.d.ts +0 -42
- package/dist/core/quo/seal.js +0 -78
- package/dist/core/quo/standing.d.ts +0 -39
- package/dist/core/quo/standing.js +0 -90
- package/dist/core/tcp/index.d.ts +0 -27
- package/dist/core/tcp/index.js +0 -209
- package/dist/core/ward/allowance.d.ts +0 -14
- package/dist/core/ward/allowance.js +0 -32
- package/dist/core/ward/cells.d.ts +0 -7
- package/dist/core/ward/cells.js +0 -74
- package/dist/core/ward/house.d.ts +0 -67
- package/dist/core/ward/house.js +0 -473
- package/dist/core/ward/index.d.ts +0 -5
- package/dist/core/ward/index.js +0 -7
- package/dist/core/ward/partition.d.ts +0 -50
- package/dist/core/ward/partition.js +0 -66
- package/dist/core/ward/stance.d.ts +0 -29
- package/dist/core/ward/stance.js +0 -186
- package/dist/core/ward/ward.d.ts +0 -44
- package/dist/core/ward/ward.js +0 -140
- package/dist/core.d.ts +0 -10
- package/dist/core.js +0 -19
- package/dist/defaults/dock/index.d.ts +0 -4
- package/dist/defaults/dock/index.js +0 -11
- package/dist/defaults/index.d.ts +0 -5
- package/dist/defaults/index.js +0 -10
- package/dist/defaults/memory/index.d.ts +0 -12
- package/dist/defaults/memory/index.js +0 -39
- package/dist/defaults/tcp/index.d.ts +0 -16
- package/dist/defaults/tcp/index.js +0 -82
- package/dist/defaults/ward/index.d.ts +0 -4
- package/dist/defaults/ward/index.js +0 -11
- package/dist/defaults/web/index.d.ts +0 -15
- package/dist/defaults/web/index.js +0 -90
- package/dist/edge.d.ts +0 -1
- package/dist/edge.js +0 -9
- package/dist/kit.d.ts +0 -4
- package/dist/kit.js +0 -12
- package/dist/node.d.ts +0 -1
- package/dist/node.js +0 -8
- package/src/browser.ts +0 -10
- package/src/cli.ts +0 -15
- package/src/core/being/being.ts +0 -84
- package/src/core/being/digest.ts +0 -18
- package/src/core/being/faculty.ts +0 -76
- package/src/core/being/index.ts +0 -8
- package/src/core/being/kind.ts +0 -37
- package/src/core/being/types.ts +0 -74
- package/src/core/being/words.ts +0 -31
- package/src/core/browser/index.ts +0 -219
- package/src/core/browser/worker.ts +0 -63
- package/src/core/cli/command.ts +0 -311
- package/src/core/cli/edge.ts +0 -86
- package/src/core/cli/harbor.ts +0 -120
- package/src/core/contract/index.ts +0 -82
- package/src/core/contract/link.ts +0 -58
- package/src/core/crypto/aes.ts +0 -26
- package/src/core/crypto/bytes.ts +0 -37
- package/src/core/crypto/ed25519.ts +0 -92
- package/src/core/crypto/hash.ts +0 -14
- package/src/core/crypto/index.ts +0 -10
- package/src/core/crypto/json.ts +0 -241
- package/src/core/crypto/mlkem.ts +0 -38
- package/src/core/crypto/subtle.ts +0 -13
- package/src/core/crypto/x25519.ts +0 -23
- package/src/core/edge/index.ts +0 -865
- package/src/core/folder/index.ts +0 -137
- package/src/core/git/http.ts +0 -103
- package/src/core/git/index.ts +0 -10
- package/src/core/git/object.ts +0 -116
- package/src/core/git/pack.ts +0 -147
- package/src/core/git/store.ts +0 -122
- package/src/core/git/zlib.ts +0 -197
- package/src/core/harbor/carrying.ts +0 -124
- package/src/core/harbor/catalogue.ts +0 -386
- package/src/core/harbor/dna.ts +0 -116
- package/src/core/harbor/dock.ts +0 -420
- package/src/core/harbor/harbor.ts +0 -433
- package/src/core/harbor/index.ts +0 -14
- package/src/core/harbor/memory.ts +0 -31
- package/src/core/harbor/package.ts +0 -195
- package/src/core/harbor/probe.ts +0 -39
- package/src/core/harbor/registry.ts +0 -107
- package/src/core/harbor/root-line.ts +0 -19
- package/src/core/harbor/terrain.ts +0 -54
- package/src/core/http/index.ts +0 -147
- package/src/core/http/websocket.ts +0 -124
- package/src/core/line/answer.ts +0 -60
- package/src/core/line/frame.ts +0 -83
- package/src/core/line/ground.ts +0 -18
- package/src/core/line/index.ts +0 -8
- package/src/core/line/web.ts +0 -123
- package/src/core/node/index.ts +0 -139
- package/src/core/pointer/bodies.ts +0 -122
- package/src/core/pointer/index.ts +0 -4
- package/src/core/pointer/world.ts +0 -134
- package/src/core/proof/contracts.ts +0 -134
- package/src/core/proof/expect.ts +0 -25
- package/src/core/proof/index.ts +0 -14
- package/src/core/proof/law.ts +0 -742
- package/src/core/quo/address.ts +0 -87
- package/src/core/quo/door.ts +0 -180
- package/src/core/quo/index.ts +0 -12
- package/src/core/quo/invitation.ts +0 -19
- package/src/core/quo/keys.ts +0 -91
- package/src/core/quo/payload.ts +0 -61
- package/src/core/quo/relations.ts +0 -62
- package/src/core/quo/reply.ts +0 -38
- package/src/core/quo/seal.ts +0 -101
- package/src/core/quo/standing.ts +0 -111
- package/src/core/stand/main.ts +0 -20
- package/src/core/stand/stand.ts +0 -259
- package/src/core/tcp/index.ts +0 -227
- package/src/core/ward/allowance.ts +0 -47
- package/src/core/ward/cells.ts +0 -77
- package/src/core/ward/house.ts +0 -524
- package/src/core/ward/index.ts +0 -7
- package/src/core/ward/partition.ts +0 -117
- package/src/core/ward/stance.ts +0 -207
- package/src/core/ward/ward.ts +0 -166
- package/src/core.ts +0 -55
- package/src/defaults/dock/index.ts +0 -12
- package/src/defaults/index.ts +0 -10
- package/src/defaults/memory/index.ts +0 -50
- package/src/defaults/tcp/index.ts +0 -89
- package/src/defaults/ward/index.ts +0 -12
- package/src/defaults/web/index.ts +0 -97
- package/src/edge.ts +0 -11
- package/src/index.ts +0 -18
- package/src/kit.ts +0 -15
- package/src/node.ts +0 -10
- /package/dist/{cli.d.ts → node/cli.d.ts} +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { NobleCrypto } from './noble-crypto.js';
|
|
2
|
+
const SEED = /^[0-9a-f]{64}$/;
|
|
3
|
+
export class SeedKeys {
|
|
4
|
+
#seed;
|
|
5
|
+
#crypto;
|
|
6
|
+
#held;
|
|
7
|
+
constructor(seed, crypto = new NobleCrypto()) {
|
|
8
|
+
if (typeof seed !== 'string' || !SEED.test(seed))
|
|
9
|
+
throw new TypeError('a seed is sixty-four lowercase hex digits');
|
|
10
|
+
this.#seed = Uint8Array.from(seed.match(/../g), (pair) => Number.parseInt(pair, 16));
|
|
11
|
+
this.#crypto = crypto;
|
|
12
|
+
}
|
|
13
|
+
#keys() {
|
|
14
|
+
this.#held ??= (async () => {
|
|
15
|
+
const crypto = this.#crypto;
|
|
16
|
+
const signing = await crypto.hkdf(this.#seed, 'quo-ward-sign', 32);
|
|
17
|
+
const scalar = await crypto.hkdf(this.#seed, 'quo-ward-seal', 32);
|
|
18
|
+
// The lock is the kit's: one per ward, drawn from the seed under a label of Nervur's.
|
|
19
|
+
const lock = await crypto.lockPair(await crypto.hkdf(this.#seed, 'nervur-lock', 64));
|
|
20
|
+
const ward = Object.freeze({ signing: await crypto.signingPublic(signing), padlock: await crypto.agreePublic(scalar), lock: lock.encapsulation });
|
|
21
|
+
return { signing, scalar, decapsulation: lock.decapsulation, ward };
|
|
22
|
+
})();
|
|
23
|
+
return this.#held;
|
|
24
|
+
}
|
|
25
|
+
async ward() {
|
|
26
|
+
return (await this.#keys()).ward;
|
|
27
|
+
}
|
|
28
|
+
async sign(message) {
|
|
29
|
+
return this.#crypto.sign((await this.#keys()).signing, message);
|
|
30
|
+
}
|
|
31
|
+
async agree(pk) {
|
|
32
|
+
return this.#crypto.agree((await this.#keys()).scalar, pk);
|
|
33
|
+
}
|
|
34
|
+
async decapsulate(ciphertext) {
|
|
35
|
+
return this.#crypto.decapsulate((await this.#keys()).decapsulation, ciphertext);
|
|
36
|
+
}
|
|
37
|
+
async derive(label, length) {
|
|
38
|
+
if (label.startsWith('quo-'))
|
|
39
|
+
throw new TypeError('a label beginning quo- is Quo\'s');
|
|
40
|
+
return this.#crypto.hkdf(this.#seed, `nervur-derive:${label}`, length);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Parsed, Tools } from '../foundation.ts';
|
|
2
|
+
export declare class StrictTools implements Tools {
|
|
3
|
+
utf8(text: string): Uint8Array;
|
|
4
|
+
text(bytes: Uint8Array): string | null;
|
|
5
|
+
hex(bytes: Uint8Array): string;
|
|
6
|
+
bytes(hex: string): Uint8Array | null;
|
|
7
|
+
parse(text: string): Parsed | null;
|
|
8
|
+
canonical(value: unknown): string;
|
|
9
|
+
check(schema: unknown, value: unknown): string | null;
|
|
10
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
const encoder = new TextEncoder();
|
|
2
|
+
const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
3
|
+
const HEX = /^(?:[0-9a-f]{2})*$/;
|
|
4
|
+
const NUMBER = /-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/y;
|
|
5
|
+
const ESCAPES = { '"': '"', '\\': '\\', '/': '/', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t' };
|
|
6
|
+
const define = (target, key, value) => Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
7
|
+
// An iterative reader, so no nesting a sender writes can exhaust the stack.
|
|
8
|
+
const read = (text) => {
|
|
9
|
+
const length = text.length;
|
|
10
|
+
let at = 0;
|
|
11
|
+
let duplicate = false;
|
|
12
|
+
let nestedDuplicate = false;
|
|
13
|
+
let fields = null;
|
|
14
|
+
const stack = [];
|
|
15
|
+
const space = () => {
|
|
16
|
+
while (at < length) {
|
|
17
|
+
const code = text.charCodeAt(at);
|
|
18
|
+
if (code !== 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d)
|
|
19
|
+
return;
|
|
20
|
+
at++;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const string = () => {
|
|
24
|
+
if (text[at] !== '"')
|
|
25
|
+
return null;
|
|
26
|
+
at++;
|
|
27
|
+
let out = '';
|
|
28
|
+
let from = at;
|
|
29
|
+
while (at < length) {
|
|
30
|
+
const code = text.charCodeAt(at);
|
|
31
|
+
if (code === 0x22) {
|
|
32
|
+
out += text.slice(from, at);
|
|
33
|
+
at++;
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
if (code < 0x20)
|
|
37
|
+
return null;
|
|
38
|
+
if (code === 0x5c) {
|
|
39
|
+
out += text.slice(from, at);
|
|
40
|
+
const escape = text[at + 1];
|
|
41
|
+
if (escape === 'u') {
|
|
42
|
+
const digits = text.slice(at + 2, at + 6);
|
|
43
|
+
if (!/^[0-9a-fA-F]{4}$/.test(digits))
|
|
44
|
+
return null;
|
|
45
|
+
out += String.fromCharCode(Number.parseInt(digits, 16));
|
|
46
|
+
at += 6;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const plain = escape === undefined ? undefined : ESCAPES[escape];
|
|
50
|
+
if (plain === undefined)
|
|
51
|
+
return null;
|
|
52
|
+
out += plain;
|
|
53
|
+
at += 2;
|
|
54
|
+
}
|
|
55
|
+
from = at;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
at++;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
};
|
|
62
|
+
const key = (frame) => {
|
|
63
|
+
const name = string();
|
|
64
|
+
if (name === null)
|
|
65
|
+
return false;
|
|
66
|
+
space();
|
|
67
|
+
if (text[at] !== ':')
|
|
68
|
+
return false;
|
|
69
|
+
at++;
|
|
70
|
+
space();
|
|
71
|
+
if (frame.keys.has(name)) {
|
|
72
|
+
if (stack[0] === frame)
|
|
73
|
+
duplicate = true;
|
|
74
|
+
else
|
|
75
|
+
nestedDuplicate = true;
|
|
76
|
+
}
|
|
77
|
+
frame.keys.add(name);
|
|
78
|
+
frame.key = name;
|
|
79
|
+
frame.start = at;
|
|
80
|
+
return true;
|
|
81
|
+
};
|
|
82
|
+
for (;;) {
|
|
83
|
+
space();
|
|
84
|
+
let value;
|
|
85
|
+
const char = text[at];
|
|
86
|
+
if (char === '{') {
|
|
87
|
+
at++;
|
|
88
|
+
space();
|
|
89
|
+
const frame = { object: true, value: {}, keys: new Set(), key: '', start: 0 };
|
|
90
|
+
if (text[at] === '}') {
|
|
91
|
+
at++;
|
|
92
|
+
value = frame.value;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
stack.push(frame);
|
|
96
|
+
if (stack.length === 1)
|
|
97
|
+
fields = Object.create(null);
|
|
98
|
+
if (!key(frame))
|
|
99
|
+
return null;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else if (char === '[') {
|
|
104
|
+
at++;
|
|
105
|
+
space();
|
|
106
|
+
if (text[at] === ']') {
|
|
107
|
+
at++;
|
|
108
|
+
value = [];
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
stack.push({ object: false, value: [] });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else if (char === '"') {
|
|
116
|
+
value = string();
|
|
117
|
+
if (value === null)
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
else if (text.startsWith('true', at)) {
|
|
121
|
+
at += 4;
|
|
122
|
+
value = true;
|
|
123
|
+
}
|
|
124
|
+
else if (text.startsWith('false', at)) {
|
|
125
|
+
at += 5;
|
|
126
|
+
value = false;
|
|
127
|
+
}
|
|
128
|
+
else if (text.startsWith('null', at)) {
|
|
129
|
+
at += 4;
|
|
130
|
+
value = null;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
NUMBER.lastIndex = at;
|
|
134
|
+
const number = NUMBER.exec(text);
|
|
135
|
+
if (number === null)
|
|
136
|
+
return null;
|
|
137
|
+
at += number[0].length;
|
|
138
|
+
value = Number(number[0]);
|
|
139
|
+
}
|
|
140
|
+
// The value is whole: set it in its container, and close what it ends.
|
|
141
|
+
for (;;) {
|
|
142
|
+
const top = stack.at(-1);
|
|
143
|
+
if (top === undefined) {
|
|
144
|
+
space();
|
|
145
|
+
if (at !== length)
|
|
146
|
+
return null;
|
|
147
|
+
if (!(typeof value === 'object' && value !== null && !Array.isArray(value)))
|
|
148
|
+
fields = null;
|
|
149
|
+
return { value, fields, duplicate, nestedDuplicate };
|
|
150
|
+
}
|
|
151
|
+
if (top.object) {
|
|
152
|
+
if (stack.length === 1 && fields !== null)
|
|
153
|
+
fields[top.key] = text.slice(top.start, at);
|
|
154
|
+
define(top.value, top.key, value);
|
|
155
|
+
space();
|
|
156
|
+
if (text[at] === ',') {
|
|
157
|
+
at++;
|
|
158
|
+
space();
|
|
159
|
+
if (!key(top))
|
|
160
|
+
return null;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
if (text[at] !== '}')
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
top.value.push(value);
|
|
168
|
+
space();
|
|
169
|
+
if (text[at] === ',') {
|
|
170
|
+
at++;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
if (text[at] !== ']')
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
at++;
|
|
177
|
+
stack.pop();
|
|
178
|
+
value = top.value;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const canonical = (value) => {
|
|
183
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string')
|
|
184
|
+
return JSON.stringify(value);
|
|
185
|
+
if (typeof value === 'number') {
|
|
186
|
+
if (!Number.isFinite(value))
|
|
187
|
+
throw new TypeError('a number that is not finite has no JSON');
|
|
188
|
+
return JSON.stringify(value);
|
|
189
|
+
}
|
|
190
|
+
if (Array.isArray(value))
|
|
191
|
+
return `[${value.map(canonical).join(',')}]`;
|
|
192
|
+
if (typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
193
|
+
const keys = Object.keys(value).sort();
|
|
194
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
|
|
195
|
+
}
|
|
196
|
+
throw new TypeError('the value is not JSON');
|
|
197
|
+
};
|
|
198
|
+
export class StrictTools {
|
|
199
|
+
utf8(text) {
|
|
200
|
+
return encoder.encode(text);
|
|
201
|
+
}
|
|
202
|
+
text(bytes) {
|
|
203
|
+
try {
|
|
204
|
+
return decoder.decode(bytes);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
hex(bytes) {
|
|
211
|
+
let out = '';
|
|
212
|
+
for (const byte of bytes)
|
|
213
|
+
out += byte.toString(16).padStart(2, '0');
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
bytes(hex) {
|
|
217
|
+
if (typeof hex !== 'string' || !HEX.test(hex))
|
|
218
|
+
return null;
|
|
219
|
+
const out = new Uint8Array(hex.length / 2);
|
|
220
|
+
for (let at = 0; at < out.length; at++)
|
|
221
|
+
out[at] = Number.parseInt(hex.slice(at * 2, at * 2 + 2), 16);
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
parse(text) {
|
|
225
|
+
return read(text);
|
|
226
|
+
}
|
|
227
|
+
canonical(value) {
|
|
228
|
+
return canonical(value);
|
|
229
|
+
}
|
|
230
|
+
check(schema, value) {
|
|
231
|
+
return check(schema, value, 'the value');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const typeOf = (value) => {
|
|
235
|
+
if (value === null)
|
|
236
|
+
return 'null';
|
|
237
|
+
if (Array.isArray(value))
|
|
238
|
+
return 'array';
|
|
239
|
+
if (typeof value === 'number')
|
|
240
|
+
return Number.isInteger(value) ? 'integer' : 'number';
|
|
241
|
+
return typeof value;
|
|
242
|
+
};
|
|
243
|
+
// The closed subset, read as JSON Schema 2020-12 reads it.
|
|
244
|
+
const check = (schema, value, at) => {
|
|
245
|
+
if (typeof schema !== 'object' || schema === null)
|
|
246
|
+
return `${at} meets no schema`;
|
|
247
|
+
const type = schema.type;
|
|
248
|
+
if (type !== undefined) {
|
|
249
|
+
const actual = typeOf(value);
|
|
250
|
+
const fits = actual === type || (type === 'number' && actual === 'integer');
|
|
251
|
+
if (!fits)
|
|
252
|
+
return `${at} is not ${type === 'integer' || type === 'array' || type === 'object' ? 'an' : 'a'} ${type}`;
|
|
253
|
+
}
|
|
254
|
+
if ('const' in schema && schema.const !== value)
|
|
255
|
+
return `${at} is not ${JSON.stringify(schema.const)}`;
|
|
256
|
+
if (Array.isArray(schema.enum) && !schema.enum.includes(value))
|
|
257
|
+
return `${at} is none of ${JSON.stringify(schema.enum)}`;
|
|
258
|
+
if (typeof value === 'string') {
|
|
259
|
+
const length = [...value].length;
|
|
260
|
+
if (typeof schema.minLength === 'number' && length < schema.minLength)
|
|
261
|
+
return `${at} is shorter than ${schema.minLength}`;
|
|
262
|
+
if (typeof schema.maxLength === 'number' && length > schema.maxLength)
|
|
263
|
+
return `${at} is longer than ${schema.maxLength}`;
|
|
264
|
+
if (typeof schema.pattern === 'string' && !new RegExp(schema.pattern, 'u').test(value))
|
|
265
|
+
return `${at} does not match ${schema.pattern}`;
|
|
266
|
+
if (schema.contentEncoding === 'base16' && !HEX.test(value))
|
|
267
|
+
return `${at} is not lowercase hex`;
|
|
268
|
+
}
|
|
269
|
+
if (typeof value === 'number') {
|
|
270
|
+
if (typeof schema.minimum === 'number' && value < schema.minimum)
|
|
271
|
+
return `${at} is below ${schema.minimum}`;
|
|
272
|
+
if (typeof schema.maximum === 'number' && value > schema.maximum)
|
|
273
|
+
return `${at} is above ${schema.maximum}`;
|
|
274
|
+
}
|
|
275
|
+
if (Array.isArray(value) && schema.items !== undefined) {
|
|
276
|
+
for (let index = 0; index < value.length; index++) {
|
|
277
|
+
const found = check(schema.items, value[index], `${at}[${index}]`);
|
|
278
|
+
if (found !== null)
|
|
279
|
+
return found;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (typeOf(value) === 'object') {
|
|
283
|
+
const object = value;
|
|
284
|
+
const properties = (schema.properties ?? {});
|
|
285
|
+
for (const key of (schema.required ?? []))
|
|
286
|
+
if (!Object.hasOwn(object, key))
|
|
287
|
+
return `${at}.${key} is missing`;
|
|
288
|
+
for (const [key, field] of Object.entries(object)) {
|
|
289
|
+
const inner = Object.hasOwn(properties, key) ? properties[key] : undefined;
|
|
290
|
+
if (inner === undefined) {
|
|
291
|
+
if (schema.additionalProperties === false)
|
|
292
|
+
return `${at}.${key} is not allowed`;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const found = check(inner, field, `${at}.${key}`);
|
|
296
|
+
if (found !== null)
|
|
297
|
+
return found;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (Array.isArray(schema.anyOf)) {
|
|
301
|
+
const reasons = schema.anyOf.map((branch) => check(branch, value, at));
|
|
302
|
+
if (!reasons.includes(null))
|
|
303
|
+
return reasons[0] ?? `${at} meets no branch`;
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Carry, Sent } from '../foundation.ts';
|
|
2
|
+
/** A WebSocket the ground's listener opened: bytes out, and closing. */
|
|
3
|
+
export interface HeldSocket {
|
|
4
|
+
send(data: Uint8Array): void;
|
|
5
|
+
close(): void;
|
|
6
|
+
}
|
|
7
|
+
/** What a handler does with a WebSocket once it is open. */
|
|
8
|
+
export interface Held {
|
|
9
|
+
message(data: Uint8Array | string): void;
|
|
10
|
+
close(): void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Whatever speaks HTTP on a ground. `fetch` answers a request, or `null`
|
|
14
|
+
* where it is not this handler's. `upgrade` takes a WebSocket: the
|
|
15
|
+
* subprotocol it selects, and what it does once the socket is open.
|
|
16
|
+
*/
|
|
17
|
+
export interface Handler {
|
|
18
|
+
fetch(request: Request): Promise<Response | null>;
|
|
19
|
+
upgrade?(request: Request): {
|
|
20
|
+
protocol: string;
|
|
21
|
+
open(socket: HeldSocket): Held;
|
|
22
|
+
} | null;
|
|
23
|
+
}
|
|
24
|
+
type Door = (box: Uint8Array) => Promise<Uint8Array | null>;
|
|
25
|
+
export declare class WebCarry implements Carry, Handler {
|
|
26
|
+
#private;
|
|
27
|
+
/**
|
|
28
|
+
* `path` is where it listens under the ground's listener, `/quo` where
|
|
29
|
+
* omitted. `addresses` are written into invitations, or read each time
|
|
30
|
+
* where the listener's port is known only once it listens. `origins` are
|
|
31
|
+
* the pages of other origins it answers, by the CORS protocol.
|
|
32
|
+
*/
|
|
33
|
+
constructor({ path, addresses, origins, allowPrivate, wait, }?: {
|
|
34
|
+
path?: string;
|
|
35
|
+
addresses?: readonly string[] | (() => readonly string[]);
|
|
36
|
+
origins?: readonly string[];
|
|
37
|
+
allowPrivate?: boolean;
|
|
38
|
+
wait?: number;
|
|
39
|
+
});
|
|
40
|
+
/** A ward's door, answered on the post and on held lines alike. */
|
|
41
|
+
listen({ ward, door }: {
|
|
42
|
+
ward: string;
|
|
43
|
+
door: Door;
|
|
44
|
+
}): void;
|
|
45
|
+
/** A ward this handler answers no more. */
|
|
46
|
+
unlisten({ ward }: {
|
|
47
|
+
ward: string;
|
|
48
|
+
}): void;
|
|
49
|
+
at(_options?: {
|
|
50
|
+
toward?: string;
|
|
51
|
+
}): readonly string[];
|
|
52
|
+
fetch(request: Request): Promise<Response | null>;
|
|
53
|
+
upgrade(request: Request): {
|
|
54
|
+
protocol: string;
|
|
55
|
+
open(socket: HeldSocket): Held;
|
|
56
|
+
} | null;
|
|
57
|
+
send({ ward, at, box }: {
|
|
58
|
+
ward: string;
|
|
59
|
+
at: readonly string[];
|
|
60
|
+
box: Uint8Array;
|
|
61
|
+
}): Promise<Sent>;
|
|
62
|
+
/** Closes every held line it dialled. */
|
|
63
|
+
close(): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { ASK, NOTHING, PK, REPLY, body, privateHost, read } from '../quo/frame.js';
|
|
2
|
+
/** The longest post: a ward pk and the longest box. */
|
|
3
|
+
const LONGEST_POST = 1_048_640;
|
|
4
|
+
const PROTOCOL = 'quo';
|
|
5
|
+
// Errors of a connection never made: the box went nowhere.
|
|
6
|
+
const UNREACHED = new Set(['ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN', 'EHOSTUNREACH', 'ENETUNREACH', 'UND_ERR_CONNECT_TIMEOUT']);
|
|
7
|
+
const hex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
8
|
+
const unhex = (text) => Uint8Array.from(text.match(/../g) ?? [], (pair) => Number.parseInt(pair, 16));
|
|
9
|
+
const joined = (a, b) => {
|
|
10
|
+
const out = new Uint8Array(a.length + b.length);
|
|
11
|
+
out.set(a);
|
|
12
|
+
out.set(b, a.length);
|
|
13
|
+
return out;
|
|
14
|
+
};
|
|
15
|
+
export class WebCarry {
|
|
16
|
+
#doors = new Map();
|
|
17
|
+
#lines = new Map();
|
|
18
|
+
#path;
|
|
19
|
+
#addresses;
|
|
20
|
+
#origins;
|
|
21
|
+
#allowPrivate;
|
|
22
|
+
#wait;
|
|
23
|
+
/**
|
|
24
|
+
* `path` is where it listens under the ground's listener, `/quo` where
|
|
25
|
+
* omitted. `addresses` are written into invitations, or read each time
|
|
26
|
+
* where the listener's port is known only once it listens. `origins` are
|
|
27
|
+
* the pages of other origins it answers, by the CORS protocol.
|
|
28
|
+
*/
|
|
29
|
+
constructor({ path = '/quo', addresses = [], origins = [], allowPrivate = false, wait = 30_000, } = {}) {
|
|
30
|
+
this.#path = path;
|
|
31
|
+
this.#addresses = addresses;
|
|
32
|
+
this.#origins = new Set(origins);
|
|
33
|
+
this.#allowPrivate = allowPrivate;
|
|
34
|
+
this.#wait = wait;
|
|
35
|
+
}
|
|
36
|
+
/** A ward's door, answered on the post and on held lines alike. */
|
|
37
|
+
listen({ ward, door }) {
|
|
38
|
+
this.#doors.set(ward, door);
|
|
39
|
+
}
|
|
40
|
+
/** A ward this handler answers no more. */
|
|
41
|
+
unlisten({ ward }) {
|
|
42
|
+
this.#doors.delete(ward);
|
|
43
|
+
}
|
|
44
|
+
at(_options = {}) {
|
|
45
|
+
return typeof this.#addresses === 'function' ? this.#addresses() : this.#addresses;
|
|
46
|
+
}
|
|
47
|
+
// ---- the listener ----
|
|
48
|
+
#cors(request) {
|
|
49
|
+
const origin = request.headers.get('origin');
|
|
50
|
+
return origin !== null && (this.#origins.has(origin) || this.#origins.has('*')) ? { 'access-control-allow-origin': origin, vary: 'origin' } : {};
|
|
51
|
+
}
|
|
52
|
+
async fetch(request) {
|
|
53
|
+
if (new URL(request.url).pathname !== this.#path)
|
|
54
|
+
return null;
|
|
55
|
+
const cors = this.#cors(request);
|
|
56
|
+
if (request.method === 'OPTIONS' && 'access-control-allow-origin' in cors) {
|
|
57
|
+
return new Response(null, { status: 204, headers: { ...cors, 'access-control-allow-methods': 'POST', 'access-control-allow-headers': 'content-type' } });
|
|
58
|
+
}
|
|
59
|
+
if (request.method !== 'POST')
|
|
60
|
+
return new Response(null, { status: 405, headers: cors });
|
|
61
|
+
const bytes = new Uint8Array(await request.arrayBuffer());
|
|
62
|
+
if (bytes.length <= PK || bytes.length > LONGEST_POST)
|
|
63
|
+
return new Response(null, { status: 400, headers: cors });
|
|
64
|
+
const door = this.#doors.get(hex(bytes.subarray(0, PK)));
|
|
65
|
+
if (door === undefined)
|
|
66
|
+
return new Response(null, { status: 404, headers: cors });
|
|
67
|
+
const reply = await door(bytes.slice(PK)).catch(() => null);
|
|
68
|
+
if (reply === null)
|
|
69
|
+
return new Response(null, { status: 204, headers: cors });
|
|
70
|
+
return new Response(new Uint8Array(reply), { status: 200, headers: { ...cors, 'content-type': 'application/octet-stream' } });
|
|
71
|
+
}
|
|
72
|
+
upgrade(request) {
|
|
73
|
+
if (new URL(request.url).pathname !== this.#path)
|
|
74
|
+
return null;
|
|
75
|
+
const offered = (request.headers.get('sec-websocket-protocol') ?? '').split(',').map((protocol) => protocol.trim());
|
|
76
|
+
if (!offered.includes(PROTOCOL))
|
|
77
|
+
return null;
|
|
78
|
+
return {
|
|
79
|
+
protocol: PROTOCOL,
|
|
80
|
+
open: (socket) => ({
|
|
81
|
+
message: (data) => {
|
|
82
|
+
const frame = typeof data === 'string' ? null : read(data);
|
|
83
|
+
if (frame === null)
|
|
84
|
+
return socket.close();
|
|
85
|
+
// A reply or a nothing frame is not a listener's to read.
|
|
86
|
+
if (frame.kind !== ASK)
|
|
87
|
+
return;
|
|
88
|
+
const door = this.#doors.get(hex(frame.rest.subarray(0, PK)));
|
|
89
|
+
void (door === undefined ? Promise.resolve(null) : door(frame.rest.slice(PK)).catch(() => null)).then((reply) => {
|
|
90
|
+
try {
|
|
91
|
+
socket.send(reply === null ? body(NOTHING, frame.id, new Uint8Array()) : body(REPLY, frame.id, reply));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// The line went while the door answered.
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
close: () => undefined,
|
|
99
|
+
}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// ---- the dialer ----
|
|
103
|
+
async send({ ward, at, box }) {
|
|
104
|
+
// A ward this carry listens for is on this ground: its door takes the box by pointer.
|
|
105
|
+
const door = this.#doors.get(ward);
|
|
106
|
+
if (door !== undefined) {
|
|
107
|
+
const reply = await door(box).catch(() => null);
|
|
108
|
+
if (reply !== null)
|
|
109
|
+
return { reply };
|
|
110
|
+
}
|
|
111
|
+
for (const address of at) {
|
|
112
|
+
let url;
|
|
113
|
+
try {
|
|
114
|
+
url = new URL(address);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!this.#allowPrivate && privateHost(url.hostname))
|
|
120
|
+
continue;
|
|
121
|
+
const scheme = url.protocol.slice(0, -1);
|
|
122
|
+
const answer = scheme === 'http' || scheme === 'https' ? await this.#post(address, ward, box) : scheme === 'ws' || scheme === 'wss' ? await this.#held(address, ward, box) : 'unheard';
|
|
123
|
+
// A reply ends it; an ask that may have been heard is not carried again.
|
|
124
|
+
if (answer === 'heard')
|
|
125
|
+
return { reply: null, heard: true };
|
|
126
|
+
if (answer !== 'unheard')
|
|
127
|
+
return { reply: answer, via: address };
|
|
128
|
+
}
|
|
129
|
+
return { reply: null, heard: false };
|
|
130
|
+
}
|
|
131
|
+
async #post(address, ward, box) {
|
|
132
|
+
const abort = new AbortController();
|
|
133
|
+
const timer = setTimeout(() => abort.abort(), this.#wait);
|
|
134
|
+
try {
|
|
135
|
+
const response = await fetch(address, { method: 'POST', body: joined(unhex(ward), box), signal: abort.signal });
|
|
136
|
+
if (response.status !== 200) {
|
|
137
|
+
await response.body?.cancel();
|
|
138
|
+
return 'unheard';
|
|
139
|
+
}
|
|
140
|
+
const reply = new Uint8Array(await response.arrayBuffer());
|
|
141
|
+
return reply.length > 0 ? reply : 'unheard';
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
// A connection never made, or a port the fetch standard blocks, sent nothing.
|
|
145
|
+
const cause = error.cause;
|
|
146
|
+
const unsent = (typeof cause?.code === 'string' && UNREACHED.has(cause.code)) || cause?.message === 'bad port';
|
|
147
|
+
return !abort.signal.aborted && unsent ? 'unheard' : 'heard';
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async #held(address, ward, box) {
|
|
154
|
+
let line;
|
|
155
|
+
try {
|
|
156
|
+
line = await this.#line(address);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return 'unheard';
|
|
160
|
+
}
|
|
161
|
+
const id = line.next;
|
|
162
|
+
line.next = (line.next + 1) >>> 0;
|
|
163
|
+
const answered = new Promise((resolve) => line.pending.set(id, resolve));
|
|
164
|
+
try {
|
|
165
|
+
line.socket.send(body(ASK, id, joined(unhex(ward), box)));
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
line.pending.delete(id);
|
|
169
|
+
return 'unheard';
|
|
170
|
+
}
|
|
171
|
+
let timer;
|
|
172
|
+
const late = new Promise((resolve) => (timer = setTimeout(() => resolve(null), this.#wait)));
|
|
173
|
+
const frame = await Promise.race([answered, late]);
|
|
174
|
+
clearTimeout(timer);
|
|
175
|
+
line.pending.delete(id);
|
|
176
|
+
if (frame === null)
|
|
177
|
+
return 'heard';
|
|
178
|
+
return frame.kind === REPLY ? frame.rest : 'unheard';
|
|
179
|
+
}
|
|
180
|
+
#line(address) {
|
|
181
|
+
let held = this.#lines.get(address);
|
|
182
|
+
if (held === undefined) {
|
|
183
|
+
held = new Promise((resolve, reject) => {
|
|
184
|
+
const socket = new WebSocket(address, [PROTOCOL]);
|
|
185
|
+
socket.binaryType = 'arraybuffer';
|
|
186
|
+
const line = { socket, pending: new Map(), next: 1 };
|
|
187
|
+
let open = false;
|
|
188
|
+
socket.onopen = () => {
|
|
189
|
+
if (socket.protocol !== PROTOCOL)
|
|
190
|
+
return socket.close();
|
|
191
|
+
open = true;
|
|
192
|
+
resolve(line);
|
|
193
|
+
};
|
|
194
|
+
socket.onmessage = (event) => {
|
|
195
|
+
const frame = typeof event.data === 'string' ? null : read(new Uint8Array(event.data));
|
|
196
|
+
if (frame === null)
|
|
197
|
+
return socket.close();
|
|
198
|
+
// An ask is not a dialer's to read: nothing is said to it, and the line stands.
|
|
199
|
+
if (frame.kind === ASK)
|
|
200
|
+
return;
|
|
201
|
+
line.pending.get(frame.id)?.(frame);
|
|
202
|
+
};
|
|
203
|
+
socket.onclose = () => {
|
|
204
|
+
this.#lines.delete(address);
|
|
205
|
+
for (const settle of line.pending.values())
|
|
206
|
+
settle(null);
|
|
207
|
+
line.pending.clear();
|
|
208
|
+
if (!open)
|
|
209
|
+
reject(new Error(`no held line to ${address}`));
|
|
210
|
+
};
|
|
211
|
+
socket.onerror = () => undefined;
|
|
212
|
+
});
|
|
213
|
+
this.#lines.set(address, held);
|
|
214
|
+
held.catch(() => this.#lines.delete(address));
|
|
215
|
+
}
|
|
216
|
+
return held;
|
|
217
|
+
}
|
|
218
|
+
/** Closes every held line it dialled. */
|
|
219
|
+
async close() {
|
|
220
|
+
for (const line of this.#lines.values())
|
|
221
|
+
void line.then((held) => held.socket.close(), () => undefined);
|
|
222
|
+
this.#lines.clear();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// A timer holds at most this long; a longer wait is a chain of them.
|
|
2
|
+
const LONGEST = 2 ** 31 - 1;
|
|
3
|
+
export class WebClock {
|
|
4
|
+
#waits = new Map();
|
|
5
|
+
now() {
|
|
6
|
+
return Date.now();
|
|
7
|
+
}
|
|
8
|
+
wait({ id, ms }) {
|
|
9
|
+
this.cancel({ id });
|
|
10
|
+
const at = Date.now() + Math.max(0, ms);
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const arm = () => {
|
|
13
|
+
const left = at - Date.now();
|
|
14
|
+
const timer = setTimeout(() => {
|
|
15
|
+
if (at > Date.now())
|
|
16
|
+
return arm();
|
|
17
|
+
this.#waits.delete(id);
|
|
18
|
+
resolve(true);
|
|
19
|
+
}, Math.min(Math.max(0, left), LONGEST));
|
|
20
|
+
this.#waits.set(id, { timer, resolve });
|
|
21
|
+
};
|
|
22
|
+
arm();
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
cancel({ id }) {
|
|
26
|
+
const held = this.#waits.get(id);
|
|
27
|
+
if (held === undefined)
|
|
28
|
+
return;
|
|
29
|
+
clearTimeout(held.timer);
|
|
30
|
+
this.#waits.delete(id);
|
|
31
|
+
held.resolve(false);
|
|
32
|
+
}
|
|
33
|
+
}
|