@ghostlytools/cli 1.1.5 → 1.1.7
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/README.md +1 -0
- package/dist/assets/{endpoint-CaSbSFDw.js → endpoint-D_3bYafG.js} +76 -3
- package/dist/assets/endpoint-D_3bYafG.js.map +1 -0
- package/dist/assets/{hyperdhtRelay-w9cwVqGZ.js → hyperdhtRelay-DvXgebV7.js} +3 -3
- package/dist/assets/{hyperdhtRelay-w9cwVqGZ.js.map → hyperdhtRelay-DvXgebV7.js.map} +1 -1
- package/dist/assets/idb-BIdus0WX.js.map +1 -1
- package/dist/assets/{mainline-BabMhm8L.js → mainline-B7wHcLaW.js} +2 -2
- package/dist/assets/{mainline-BabMhm8L.js.map → mainline-B7wHcLaW.js.map} +1 -1
- package/dist/assets/{server-TamKsfmC.js → server-GJW_5wGI.js} +370 -66
- package/dist/assets/server-GJW_5wGI.js.map +1 -0
- package/dist/assets/{src-C6qQYrAH.js → src-Cp3o_yHN.js} +608 -70
- package/dist/assets/src-Cp3o_yHN.js.map +1 -0
- package/dist/ghostly.mjs +1068 -49
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/endpoint-CaSbSFDw.js.map +0 -1
- package/dist/assets/server-TamKsfmC.js.map +0 -1
- package/dist/assets/src-C6qQYrAH.js.map +0 -1
package/README.md
CHANGED
|
@@ -155,6 +155,7 @@ The rows are in alphabetical order of their first command, and every command of
|
|
|
155
155
|
| `store sign <index> --key <file> [--out <dir>]` | Sign a store index (WISP 1200 · Stores) with the store key file (kept offline; made when missing and the index names no `key`): writes `ghostly-store.json`, canonical, and `ghostly-store.sig` beside the index or in `--out`. Refused, with the reason, when a client would refuse the index (`expires` more than 90 days ahead, a bad field), or when the index already there, signed by the same key, has a higher `sequence` (`rollback`) or the same one with other content (`equivocation`) |
|
|
156
156
|
| `task send <chat\|group> --title <title> [--status s] [--progress n \| --steps a/b] [--step "…"] [--item state:text]… [--pr-url url --pr-number n --additions n --deletions n --files n --pr-state s --pr-checks c] [--branch b] [--tag label]… [--parent task] [--link url]… [--id id] [--json json\|-\|file]`, `task update <chat\|group> <task> [fields…]` | A task card for a bot's work (WISP 405 · Status Cards): a title, a status (queued, running, blocked, done, failed, cancelled), progress, the step it is on, up to 20 steps or log lines (`--item done:Codec`; a lowercase word before the colon must be an item state: pending, running, done, failed or skipped) and its pull request, shown as a small card that apps without cards read as text. Optional, and ignored by apps from before them: where the pull request stands (`--pr-state draft`, `open`, `merged` or `closed`; an open one puts the task in the board's Review column) and its checks (`--pr-checks passing`, `failing` or `pending`), up to 3 tags of 24 characters (`--tag core --tag web`; an update's tags replace the card's), and `--parent <task>`, a task of yours in the same chat that this one is a part of (sent first; its parts stack under it on the board). `send` prints the task's id and message; `update` merges the fields given over the card (an update's `--item` list replaces the card's: give all of them) and edits its message, at most once per 2.5 s (sooner updates merge into the next). `--json` takes the whole card; `null` in it removes a field. Only the CLI sends cards: the app shows them |
|
|
157
157
|
| `typing <chat> [--kind typing\|recording\|thinking] [--status "<text>"] [--for s] [--stop]` | Show the contact you are writing, recording or thinking, or a short status line in its place ("Transcribing your audio…", 40 characters, no links): live chats only, it holds 6 s there; `--for s` keeps it on that long (up to 600 s; a one-shot stays that long); a new kind or status shows at once; a message to the chat or `--stop` ends it |
|
|
158
|
+
| `usage send [<chat\|group>] [--all] --left <percent> [--window "5 h"] [--resets <time>] [--label Claude] [--account <name>] [--also week=80@<time>]… [--used n --limit n] [--id id] [--json json\|-\|file]` | How much of a quota the bot has left (WISP 405 § Usage): the percent (or `--used` of `--limit`), what it is of, the account, the window, when it resets, and up to 3 other windows (`--also week=80@2026-10-10T00:00:00Z`). The app shows a small meter on the chat's row and in its header, amber under 20%, red under 5%, muted once 6 h old or past the reset. One card per chat: the first report sends it, later ones edit it in place (quiet, paced as `task update`'s); each report is whole, so a field it leaves out goes. `--all` reports to every 1:1 chat with a contact. See [docs/AI-AGENTS.md](../../docs/AI-AGENTS.md#reporting-usage) for where a bot gets its numbers |
|
|
158
159
|
| `wallet create <type> [--network testnet] [--provider id] [--value name=value]… [--stdin] [--invite code] [--api-key key]` | A wallet: `cashu`, `lightning` (a card: its source's form in `--value`), `arkade`, `spark` (on Mainnet, `--api-key` is your Breez API key), `bitcoin` (BDK), `fedimint` (`--invite` is the federation's invite code), `usdt`. With `--stdin`, secret fields come as `name=value` lines on stdin (`api-key=…` too), out of `ps` and the shell's history |
|
|
159
160
|
| `wallet faucet <type>`, `wallet add-mint <url> [--primary]` | Test coins; another Cashu mint |
|
|
160
161
|
| `wallet list [--network n]` | Wallets and balances, and what `wallet create` can make on each network |
|
|
@@ -1,4 +1,60 @@
|
|
|
1
1
|
import DHT from "hyperdht";
|
|
2
|
+
//#region ../../native/transports/hyperdht/redial.mjs
|
|
3
|
+
/**
|
|
4
|
+
* How long a HyperDHT dial whose stream opened may go without the contact's preface before it is dialled again.
|
|
5
|
+
*
|
|
6
|
+
* The first connection a freshly started process makes sometimes opens (the handshake went through the DHT and the
|
|
7
|
+
* stream is connected to the right address) and then carries nothing: the contact never sees it, and UDX gives up
|
|
8
|
+
* only after 13 s. A connection made again at once goes through in milliseconds. Found as a chat moved to HyperDHT
|
|
9
|
+
* right after the contact's app restarted taking about 28 s in CI (the move's 8 s, then its 20 s retry), 1 run in 6;
|
|
10
|
+
* reproduced with hyperdht alone under load (bug hunt r11h). On loopback the preface comes within milliseconds of
|
|
11
|
+
* the stream opening, one round trip on the internet.
|
|
12
|
+
*/
|
|
13
|
+
var PREFACE_WAIT_MS = 2e3;
|
|
14
|
+
/**
|
|
15
|
+
* Dials with `dialOnce` until one brings the contact's preface. `dialOnce()` starts a connection and returns
|
|
16
|
+
* `{ opened, ready, close }`: `opened` resolves true once the stream is open (the handshake is done), `ready` with the
|
|
17
|
+
* bound channel once the contact's preface came, and `close()` drops it. A dial that opened and brought no preface
|
|
18
|
+
* within `waitMs` is closed and made again, up to `maxDials` in all. A dial that fails before it opened (the contact
|
|
19
|
+
* not found, refused) fails the whole connect at once, as before: there is nothing to wait for.
|
|
20
|
+
*/
|
|
21
|
+
function redial(dialOnce, { waitMs = PREFACE_WAIT_MS, maxDials = 3 } = {}) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
let settled = false;
|
|
24
|
+
let dials = 0;
|
|
25
|
+
const attempt = () => {
|
|
26
|
+
dials++;
|
|
27
|
+
const dial = dialOnce();
|
|
28
|
+
let timer = null;
|
|
29
|
+
let stalled = false;
|
|
30
|
+
dial.opened.then((open) => {
|
|
31
|
+
if (!open || settled) return;
|
|
32
|
+
timer = setTimeout(() => {
|
|
33
|
+
if (settled || dials >= maxDials) return;
|
|
34
|
+
stalled = true;
|
|
35
|
+
dial.close();
|
|
36
|
+
attempt();
|
|
37
|
+
}, waitMs);
|
|
38
|
+
}, () => {});
|
|
39
|
+
dial.ready.then((bound) => {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
if (settled || stalled) {
|
|
42
|
+
bound.channel.close();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
settled = true;
|
|
46
|
+
resolve(bound);
|
|
47
|
+
}, (error) => {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
if (settled || stalled) return;
|
|
50
|
+
settled = true;
|
|
51
|
+
reject(error);
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
attempt();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
2
58
|
//#region ../../native/transports/hyperdht/endpoint.mjs
|
|
3
59
|
var MAX_FRAME = 61440;
|
|
4
60
|
var PREFACE = "ghostly/paired-chat/1";
|
|
@@ -16,7 +72,8 @@ async function createHyperEndpoint(seed, options = {}) {
|
|
|
16
72
|
let incomingHandler = null;
|
|
17
73
|
const waiting = [];
|
|
18
74
|
const descriptor = { publicKey: keyPair.publicKey.toString("hex") };
|
|
19
|
-
|
|
75
|
+
/** `onChannel`: hears the channel as soon as it counts against the limit (a dial that stalls is dropped from it). */
|
|
76
|
+
async function attach(socket, onChannel) {
|
|
20
77
|
if (stopped || channels.size >= 2) {
|
|
21
78
|
socket.destroy();
|
|
22
79
|
throw new Error("Native connection limit reached");
|
|
@@ -80,6 +137,7 @@ async function createHyperEndpoint(seed, options = {}) {
|
|
|
80
137
|
}
|
|
81
138
|
};
|
|
82
139
|
channels.add(channel);
|
|
140
|
+
onChannel?.(channel);
|
|
83
141
|
socket.on("error", (error) => rejectOpen(error));
|
|
84
142
|
socket.on("close", () => {
|
|
85
143
|
closed = true;
|
|
@@ -168,7 +226,22 @@ async function createHyperEndpoint(seed, options = {}) {
|
|
|
168
226
|
},
|
|
169
227
|
async connect(address) {
|
|
170
228
|
if (stopped || !address || !/^[a-f0-9]{64}$/.test(address.publicKey)) throw new Error("Invalid HyperDHT endpoint");
|
|
171
|
-
|
|
229
|
+
const remote = Buffer.from(address.publicKey, "hex");
|
|
230
|
+
return redial(() => {
|
|
231
|
+
const socket = node.connect(remote, { keyPair });
|
|
232
|
+
let counted = null;
|
|
233
|
+
const ready = attach(socket, (channel) => {
|
|
234
|
+
counted = channel;
|
|
235
|
+
});
|
|
236
|
+
return {
|
|
237
|
+
opened: socket.opened,
|
|
238
|
+
ready,
|
|
239
|
+
close() {
|
|
240
|
+
if (counted) channels.delete(counted);
|
|
241
|
+
socket.destroy();
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
});
|
|
172
245
|
},
|
|
173
246
|
async close() {
|
|
174
247
|
stopped = true;
|
|
@@ -182,4 +255,4 @@ async function createHyperEndpoint(seed, options = {}) {
|
|
|
182
255
|
//#endregion
|
|
183
256
|
export { MAX_FRAME, createHyperEndpoint };
|
|
184
257
|
|
|
185
|
-
//# sourceMappingURL=endpoint-
|
|
258
|
+
//# sourceMappingURL=endpoint-D_3bYafG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"endpoint-D_3bYafG.js","names":[],"sources":["../../../../native/transports/hyperdht/redial.mjs","../../../../native/transports/hyperdht/endpoint.mjs"],"sourcesContent":["/**\n * How long a HyperDHT dial whose stream opened may go without the contact's preface before it is dialled again.\n *\n * The first connection a freshly started process makes sometimes opens (the handshake went through the DHT and the\n * stream is connected to the right address) and then carries nothing: the contact never sees it, and UDX gives up\n * only after 13 s. A connection made again at once goes through in milliseconds. Found as a chat moved to HyperDHT\n * right after the contact's app restarted taking about 28 s in CI (the move's 8 s, then its 20 s retry), 1 run in 6;\n * reproduced with hyperdht alone under load (bug hunt r11h). On loopback the preface comes within milliseconds of\n * the stream opening, one round trip on the internet.\n */\nexport const PREFACE_WAIT_MS = 2000\n\n/**\n * At most this many dials for one `connect`: the first, and two more if they stalled, 2 s apart, all within the 8 s a\n * chat's move gives its dial. Under load a dial made again stalled too about once in 20 (bug hunt r11h).\n */\nexport const MAX_DIALS = 3\n\n/**\n * Dials with `dialOnce` until one brings the contact's preface. `dialOnce()` starts a connection and returns\n * `{ opened, ready, close }`: `opened` resolves true once the stream is open (the handshake is done), `ready` with the\n * bound channel once the contact's preface came, and `close()` drops it. A dial that opened and brought no preface\n * within `waitMs` is closed and made again, up to `maxDials` in all. A dial that fails before it opened (the contact\n * not found, refused) fails the whole connect at once, as before: there is nothing to wait for.\n */\nexport function redial(dialOnce, { waitMs = PREFACE_WAIT_MS, maxDials = MAX_DIALS } = {}) {\n return new Promise((resolve, reject) => {\n let settled = false\n let dials = 0\n const attempt = () => {\n dials++\n const dial = dialOnce()\n let timer = null\n let stalled = false\n dial.opened.then(open => {\n if (!open || settled) return\n timer = setTimeout(() => {\n if (settled || dials >= maxDials) return\n stalled = true\n dial.close()\n attempt()\n }, waitMs)\n }, () => {})\n dial.ready.then(bound => {\n clearTimeout(timer)\n if (settled || stalled) { bound.channel.close(); return }\n settled = true\n resolve(bound)\n }, error => {\n clearTimeout(timer)\n // A dial closed here for stalling ends as closed: the next one carries the connect.\n if (settled || stalled) return\n settled = true\n reject(error)\n })\n }\n attempt()\n })\n}\n","import DHT from 'hyperdht'\nimport { redial } from './redial.mjs'\n\nexport const MAX_FRAME = 60 * 1024\nconst PREFACE = 'ghostly/paired-chat/1'\n\n/** Native UDP + NoiseSecretStream. The binding comes from the authenticated\n * stream itself, never from an application frame supplied by the other peer. */\nexport async function createHyperEndpoint(seed, options = {}) {\n if (!(seed instanceof Uint8Array) || seed.length !== 32) throw new Error('Invalid HyperDHT seed')\n const keyPair = DHT.keyPair(Buffer.from(seed))\n const node = new DHT({ ...options, keyPair })\n const channels = new Set()\n let stopped = false\n let incomingHandler = null\n const waiting = []\n const descriptor = { publicKey: keyPair.publicKey.toString('hex') }\n\n /** `onChannel`: hears the channel as soon as it counts against the limit (a dial that stalls is dropped from it). */\n async function attach(socket, onChannel) {\n if (stopped || channels.size >= 2) { socket.destroy(); throw new Error('Native connection limit reached') }\n let reader = null\n let closeHandler = null\n let closed = false\n let pending = []\n let buffer = Buffer.alloc(0)\n let preface = false\n let resolveOpen, rejectOpen\n const ready = new Promise((resolve, reject) => { resolveOpen = resolve; rejectOpen = reject })\n const timer = setTimeout(() => { socket.destroy(new Error('HyperDHT handshake timed out')) }, 20000)\n const channel = {\n get bufferedAmount() { return socket.writableLength || 0 },\n get onMessage() { return reader },\n set onMessage(value) { reader = value; if (value) for (const text of pending.splice(0)) value(text) },\n get onClose() { return closeHandler },\n set onClose(value) { closeHandler = value; if (closed) value?.() },\n send(text) {\n if (closed || typeof text !== 'string' || !text.length || Buffer.byteLength(text) > MAX_FRAME || this.bufferedAmount > 120 * 1024)\n throw new Error('Native channel is closed or exceeds its frame budget')\n const payload = Buffer.from(text)\n const header = Buffer.alloc(4); header.writeUInt32BE(payload.length)\n socket.write(Buffer.concat([header, payload]))\n },\n drained() {\n if (!this.bufferedAmount) return Promise.resolve()\n return new Promise((resolve, reject) => {\n const done = () => { socket.off('close', gone); resolve() }\n const gone = () => { socket.off('drain', done); reject(new Error('Native channel closed')) }\n socket.once('drain', done); socket.once('close', gone)\n })\n },\n close() { socket.destroy() },\n }\n channels.add(channel)\n onChannel?.(channel)\n socket.on('error', error => rejectOpen(error))\n socket.on('close', () => {\n closed = true; pending = []; clearTimeout(timer); channels.delete(channel)\n rejectOpen(new Error('Native channel closed')); closeHandler?.()\n })\n const opened = () => {\n if (socket.handshakeHash?.length !== 64 || socket.publicKey?.length !== 32 || socket.remotePublicKey?.length !== 32) {\n socket.destroy(new Error('Noise session binding unavailable')); return\n }\n channel.send(PREFACE)\n }\n socket.on('data', chunk => {\n // Secretstream chunks are bounded by its record layer. Process all frames\n // before retaining a partial one; never allocate based on an unchecked size.\n buffer = Buffer.concat([buffer, chunk])\n while (buffer.length >= 4) {\n const length = buffer.readUInt32BE(0)\n if (length === 0 || length > MAX_FRAME) { socket.destroy(new Error('Invalid frame length')); return }\n if (buffer.length < 4 + length) break\n let text\n try { text = new TextDecoder('utf-8', { fatal: true }).decode(buffer.subarray(4, 4 + length)) }\n catch { socket.destroy(new Error('Invalid UTF-8 frame')); return }\n buffer = buffer.subarray(4 + length)\n if (!preface) {\n if (text !== PREFACE) { socket.destroy(new Error('Unexpected application protocol')); return }\n preface = true; clearTimeout(timer)\n resolveOpen({ channel, binding: { transport: 'hyperdht/1', context: socket.handshakeHash.toString('hex'),\n identities: [socket.publicKey.toString('hex'), socket.remotePublicKey.toString('hex')].sort() } })\n } else if (reader) reader(text)\n else if (pending.length < 64) pending.push(text)\n else { socket.destroy(new Error('Receive queue exceeded')); return }\n }\n })\n void socket.opened.then(ok => { if (ok) opened(); else rejectOpen(new Error('Noise handshake failed')) })\n return ready\n }\n\n const server = node.createServer(socket => {\n void attach(socket).then(bound => {\n if (stopped) bound.channel.close()\n else if (incomingHandler) incomingHandler(bound)\n else waiting.push(bound)\n }).catch(() => {})\n })\n try { await server.listen(keyPair) }\n catch (error) { await node.destroy(); throw error }\n return {\n transport: 'hyperdht/1', descriptor, onDescriptor: null,\n get onConnection() { return incomingHandler },\n set onConnection(value) { incomingHandler = value; if (value) for (const bound of waiting.splice(0)) value(bound) },\n async connect(address) {\n if (stopped || !address || !/^[a-f0-9]{64}$/.test(address.publicKey)) throw new Error('Invalid HyperDHT endpoint')\n const remote = Buffer.from(address.publicKey, 'hex')\n // A dial whose stream opened and carried nothing is made again (`redial.mjs`): it is dropped from the limit at\n // once, so the next one has its place even while another channel is live.\n return redial(() => {\n const socket = node.connect(remote, { keyPair })\n let counted = null\n const ready = attach(socket, channel => { counted = channel })\n return { opened: socket.opened, ready, close() { if (counted) channels.delete(counted); socket.destroy() } }\n })\n },\n async close() { stopped = true; for (const channel of channels) channel.close(); waiting.length = 0; await server.close(); await node.destroy() },\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as hyperdhtRelayProblem } from "./server-
|
|
1
|
+
import { Hc as fromBase64Url } from "./src-Cp3o_yHN.js";
|
|
2
|
+
import { n as hyperdhtRelayProblem } from "./server-GJW_5wGI.js";
|
|
3
3
|
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
4
4
|
//#region ../browser/src/platform/hyperdhtRelay.ts
|
|
5
5
|
/**
|
|
@@ -339,4 +339,4 @@ async function createRelayedHyperEndpoint(seedB64, url, options = {}) {
|
|
|
339
339
|
//#endregion
|
|
340
340
|
export { createRelayedHyperEndpoint };
|
|
341
341
|
|
|
342
|
-
//# sourceMappingURL=hyperdhtRelay-
|
|
342
|
+
//# sourceMappingURL=hyperdhtRelay-DvXgebV7.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hyperdhtRelay-w9cwVqGZ.js","names":[],"sources":["../../../browser/src/platform/hyperdhtRelay.ts"],"sourcesContent":["import { ed25519 } from \"@noble/curves/ed25519.js\";\nimport { fromBase64Url, type BoundChannel, type FrameChannel, type NativeEndpoint } from \"@ghostly/core\";\nimport { hyperdhtRelayProblem } from \"../shared/hyperdhtRelay\";\n\nexport { hyperdhtRelayProblem as relayUrlProblem };\n\n/**\n * HyperDHT (WISP 103) for a browser, through a dht-relay: a server that runs the UDP half of HyperDHT and\n * talks to the browser over one WebSocket. It runs non-custodial: the browser keeps its keys, runs the\n * Noise handshake and the encrypted stream itself, and the relay moves handshake messages and ciphertext.\n * The binding (the handshake hash and both keys) is computed here, at the endpoint, as the Desktop does.\n *\n * What the relay sees: this browser's address, the HyperDHT keys it listens on and dials, and when and how\n * much it sends. Not the frames, not the keys' secrets, not the handshake hash.\n *\n * The wire is the Desktop's (native/transports/hyperdht/endpoint.mjs): the same preface, u32 length-prefixed\n * UTF-8 frames of at most 60 KiB, so a browser and a Desktop talk HyperDHT to each other.\n */\n\nconst PREFACE = \"ghostly/paired-chat/1\";\nconst MAX_FRAME = 60 * 1024;\nconst SEND_BUDGET = 120 * 1024;\nconst HANDSHAKE_MS = 20_000;\nconst RELAY_OPEN_MS = 10_000;\nconst LISTEN_MS = 20_000;\nconst MAX_CHANNELS = 2;\n/**\n * How the relay client is always made: never custodial. dht-relay's default is custodial, which sends every\n * secret key to the relay so it can run the handshake and the stream itself. Nothing here takes this from\n * settings or options; test/hyperdhtRelay.test.ts fails if a secret key ever reaches the relay's wire.\n */\nconst NON_CUSTODIAL = Object.freeze({ custodial: false as const });\n\n/**\n * The descriptor a browser's HyperDHT endpoint gives out: its key, that it is reached only through a relay, and\n * which one (the connection panel names its host; a Desktop dialling it ignores both).\n */\nexport interface RelayedHyperDescriptor { publicKey: string; relayed: true; relay: string }\n\n/** The parts of dht-relay and its streams this adapter uses. */\ninterface SecretSocket {\n handshakeHash: Uint8Array | null;\n publicKey: Uint8Array;\n remotePublicKey: Uint8Array;\n opened?: Promise<boolean>;\n destroyed?: boolean;\n write(data: Uint8Array): boolean;\n destroy(error?: Error): void;\n on(event: string, listener: (...args: never[]) => void): unknown;\n once(event: string, listener: (...args: never[]) => void): unknown;\n off(event: string, listener: (...args: never[]) => void): unknown;\n}\ninterface RelayedServer {\n listen(keyPair: KeyPair): Promise<void>;\n close(): Promise<void>;\n}\ninterface RelayedNode {\n ready(): Promise<void>;\n connect(remotePublicKey: Uint8Array, options: { keyPair: KeyPair }): SecretSocket;\n createServer(options: object, onconnection: (socket: SecretSocket) => void): RelayedServer;\n destroy(): Promise<void>;\n}\ninterface KeyPair { publicKey: Uint8Array; secretKey: Uint8Array }\n\nconst hex = (bytes: Uint8Array) => Array.from(bytes, b => b.toString(16).padStart(2, \"0\")).join(\"\");\nconst fromHex = (text: string) => Uint8Array.from(text.match(/../g)!, byte => parseInt(byte, 16));\n\n/** HyperDHT's key pair for a seed: libsodium's layout (the seed, then the public key). */\nexport function hyperKeyPair(seed: Uint8Array): KeyPair {\n if (seed.length !== 32) throw new Error(\"Invalid HyperDHT seed\");\n const publicKey = ed25519.getPublicKey(seed);\n const secretKey = new Uint8Array(64);\n secretKey.set(seed); secretKey.set(publicKey, 32);\n return { publicKey, secretKey };\n}\n\n/**\n * One WebSocket per relay for the whole app, shared by every chat's endpoint (each listens on its own\n * key). It closes when the last endpoint does. When the relay goes away, every endpoint on it says it is\n * unavailable, and the next one to start opens a new connection.\n */\ninterface RelayClient {\n node: RelayedNode;\n endpoints: Set<{ unavailable(): void }>;\n gone: boolean;\n /** Settles when the relay connection closes. */\n closed: Promise<void>;\n}\nconst clients = new Map<string, Promise<RelayClient>>();\n\nasync function relayClient(url: string, createSocket: (url: string) => WebSocket): Promise<RelayClient> {\n const existing = clients.get(url);\n if (existing) {\n const client = await existing.catch(() => null);\n if (client && !client.gone) return client;\n if (clients.get(url) === existing) clients.delete(url);\n }\n const opening = (async () => {\n const [{ default: RelayedDHT }, { default: WebSocketStream }] = await Promise.all([\n // @ts-expect-error dht-relay ships no types: RelayedNode and SecretSocket above describe what this uses.\n import(\"@hyperswarm/dht-relay\"),\n // @ts-expect-error The same package's WebSocket stream, untyped too.\n import(\"@hyperswarm/dht-relay/ws\"),\n ]);\n const socket = createSocket(url);\n socket.binaryType = \"arraybuffer\";\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => { socket.close(); reject(new Error(\"The HyperDHT relay did not answer\")); }, RELAY_OPEN_MS);\n socket.addEventListener(\"open\", () => { clearTimeout(timer); resolve(); }, { once: true });\n socket.addEventListener(\"error\", () => { clearTimeout(timer); reject(new Error(\"Could not reach the HyperDHT relay\")); }, { once: true });\n socket.addEventListener(\"close\", () => { clearTimeout(timer); reject(new Error(\"The HyperDHT relay closed the connection\")); }, { once: true });\n });\n // Non-custodial: the relay is told public keys only, never a secret key (dht-relay's default sends them).\n // The node's own key is never used to listen or dial: a random one says nothing about this app.\n const node = new RelayedDHT(new WebSocketStream(true, socket), { ...NON_CUSTODIAL, keyPair: hyperKeyPair(crypto.getRandomValues(new Uint8Array(32))) }) as RelayedNode;\n let closed!: () => void;\n const client: RelayClient = { node, endpoints: new Set(), gone: false, closed: new Promise(resolve => { closed = resolve; }) };\n socket.addEventListener(\"close\", () => {\n client.gone = true; closed();\n if (clients.get(url) === opening) clients.delete(url);\n for (const endpoint of [...client.endpoints]) endpoint.unavailable();\n });\n return client;\n })();\n clients.set(url, opening);\n opening.catch(() => { if (clients.get(url) === opening) clients.delete(url); });\n return opening;\n}\n\nfunction release(url: string, client: RelayClient, endpoint: { unavailable(): void }): void {\n client.endpoints.delete(endpoint);\n if (client.endpoints.size || client.gone) return;\n client.gone = true;\n void clients.get(url)?.then(current => { if (current === client) clients.delete(url); }).catch(() => {});\n void client.node.destroy().catch(() => {});\n}\n\n/** One authenticated stream as a frame channel, with a bounded queue before the session installs its reader. */\nclass RelayedChannel implements FrameChannel {\n private reader: FrameChannel[\"onMessage\"] = null;\n private buffered: string[] = [];\n private closed = false;\n private pendingBytes = 0;\n onClose: FrameChannel[\"onClose\"] = null;\n constructor(private readonly socket: SecretSocket) {\n socket.on(\"drain\", () => { this.pendingBytes = 0; });\n }\n get bufferedAmount(): number { return this.pendingBytes; }\n readonly sendBudget = SEND_BUDGET;\n get onMessage() { return this.reader; }\n set onMessage(reader: FrameChannel[\"onMessage\"]) {\n this.reader = reader;\n if (reader) for (const text of this.buffered.splice(0)) reader(text);\n }\n receive(text: string): boolean {\n if (this.closed) return true;\n if (this.reader) this.reader(text);\n else if (this.buffered.length < 64) this.buffered.push(text);\n else return false;\n return true;\n }\n send(data: string | Uint8Array): void {\n if (typeof data !== \"string\" || this.closed || !data.length) throw new Error(\"Native channel is closed or exceeds its frame budget\");\n const payload = new TextEncoder().encode(data);\n if (payload.length > MAX_FRAME || this.pendingBytes + payload.length > SEND_BUDGET) throw new Error(\"Native channel is closed or exceeds its frame budget\");\n // The stream takes every write; past its high-water mark it says so until it drains.\n if (!writeFrame(this.socket, payload) || this.pendingBytes) this.pendingBytes += payload.length + 4;\n }\n drained(): Promise<void> {\n if (!this.pendingBytes) return Promise.resolve();\n return new Promise((resolve, reject) => {\n const done = () => { this.socket.off(\"close\", gone); resolve(); };\n const gone = () => { this.socket.off(\"drain\", done); reject(new Error(\"Native channel closed\")); };\n this.socket.once(\"drain\", done); this.socket.once(\"close\", gone);\n });\n }\n closedByPeer(): void {\n if (this.closed) return;\n this.closed = true; this.buffered = [];\n this.onClose?.();\n }\n close(): void {\n if (this.closed) return;\n this.socket.destroy();\n this.closedByPeer();\n }\n}\n\nfunction writeFrame(socket: SecretSocket, payload: Uint8Array): boolean {\n const frame = new Uint8Array(4 + payload.length);\n new DataView(frame.buffer).setUint32(0, payload.length);\n frame.set(payload, 4);\n return socket.write(frame);\n}\n\n/**\n * Waits for the peer's preface on an authenticated stream (sending ours as soon as the handshake is\n * done), then hands the channel over with the binding the stream itself gives. A frame of the wrong size,\n * bytes that are not UTF-8 or another protocol close the stream.\n */\nfunction attach(socket: SecretSocket, channels: Set<RelayedChannel>): Promise<BoundChannel> {\n if (channels.size >= MAX_CHANNELS) { socket.destroy(); return Promise.reject(new Error(\"Native connection limit reached\")); }\n const channel = new RelayedChannel(socket);\n channels.add(channel);\n return new Promise<BoundChannel>((resolve, reject) => {\n let buffer = new Uint8Array(0);\n let prefaced = false;\n const decoder = new TextDecoder(\"utf-8\", { fatal: true });\n const timer = setTimeout(() => socket.destroy(new Error(\"HyperDHT handshake timed out\")), HANDSHAKE_MS);\n const fail = (error: Error) => { socket.destroy(error); };\n socket.on(\"error\", (error: Error) => reject(error));\n socket.on(\"close\", () => {\n clearTimeout(timer); channels.delete(channel);\n reject(new Error(\"Native channel closed\"));\n channel.closedByPeer();\n });\n const opened = () => {\n if (socket.handshakeHash?.length !== 64 || socket.publicKey?.length !== 32 || socket.remotePublicKey?.length !== 32) {\n fail(new Error(\"Noise session binding unavailable\")); return;\n }\n writeFrame(socket, new TextEncoder().encode(PREFACE));\n };\n if (socket.opened) void socket.opened.then(ok => { if (ok) opened(); else reject(new Error(\"Noise handshake failed\")); });\n else socket.once(\"open\", opened);\n socket.on(\"data\", (chunk: Uint8Array) => {\n const joined = new Uint8Array(buffer.length + chunk.length);\n joined.set(buffer); joined.set(chunk, buffer.length);\n buffer = joined;\n while (buffer.length >= 4) {\n const length = new DataView(buffer.buffer, buffer.byteOffset, 4).getUint32(0);\n if (length === 0 || length > MAX_FRAME) { fail(new Error(\"Invalid frame length\")); return; }\n if (buffer.length < 4 + length) break;\n let text: string;\n try { text = decoder.decode(buffer.subarray(4, 4 + length)); }\n catch { fail(new Error(\"Invalid UTF-8 frame\")); return; }\n buffer = buffer.slice(4 + length);\n if (!prefaced) {\n if (text !== PREFACE) { fail(new Error(\"Unexpected application protocol\")); return; }\n prefaced = true; clearTimeout(timer);\n resolve({ channel, binding: { transport: \"hyperdht/1\", context: hex(socket.handshakeHash!),\n identities: [hex(socket.publicKey), hex(socket.remotePublicKey)].sort() as [string, string] } });\n } else if (!channel.receive(text)) { fail(new Error(\"Receive queue exceeded\")); return; }\n }\n });\n });\n}\n\nexport interface RelayedHyperOptions {\n /** How to open the WebSocket (tests pass Node's `ws`). Default: the browser's. */\n createSocket?: (url: string) => WebSocket;\n}\n\n/**\n * A chat's HyperDHT endpoint through the relay at `url`: it listens on the key of `seedB64` (the chat's\n * own transport seed, as on the Desktop) and dials a contact's descriptor. Fails, and so is not offered,\n * when the relay cannot be reached or does not let it listen.\n */\nexport async function createRelayedHyperEndpoint(seedB64: string, url: string, options: RelayedHyperOptions = {}): Promise<NativeEndpoint> {\n const problem = hyperdhtRelayProblem(url);\n if (problem) throw new Error(problem);\n const keyPair = hyperKeyPair(fromBase64Url(seedB64));\n const client = await relayClient(url, options.createSocket ?? (address => new WebSocket(address)));\n const channels = new Set<RelayedChannel>();\n let stopped = false;\n let handler: NativeEndpoint[\"onConnection\"] = null;\n const waiting: BoundChannel[] = [];\n let endpoint: NativeEndpoint | null = null;\n const member = {\n unavailable() {\n if (stopped) return;\n stopped = true;\n for (const channel of channels) channel.close();\n endpoint?.onUnavailable?.();\n },\n };\n client.endpoints.add(member);\n const server = client.node.createServer({}, socket => {\n void attach(socket, channels).then(bound => {\n if (stopped) bound.channel.close();\n else if (handler) handler(bound);\n else if (waiting.length < MAX_CHANNELS) waiting.push(bound);\n else bound.channel.close();\n }).catch(() => {});\n });\n try {\n await Promise.race([\n server.listen(keyPair),\n client.closed.then(() => { throw new Error(\"The HyperDHT relay closed the connection\"); }),\n new Promise((_, reject) => setTimeout(() => reject(new Error(\"The HyperDHT relay did not let this chat listen\")), LISTEN_MS)),\n ]);\n if (client.gone) throw new Error(\"The HyperDHT relay closed the connection\");\n } catch (error) {\n stopped = true;\n void server.close().catch(() => {});\n release(url, client, member);\n throw error;\n }\n const descriptor: RelayedHyperDescriptor = { publicKey: hex(keyPair.publicKey), relayed: true, relay: url };\n endpoint = {\n transport: \"hyperdht/1\", descriptor, onDescriptor: null, onUnavailable: null,\n get onConnection() { return handler; },\n set onConnection(value) { handler = value; if (value) for (const bound of waiting.splice(0)) value(bound); },\n async connect(address) {\n const publicKey = (address as { publicKey?: unknown } | null)?.publicKey;\n if (stopped || typeof publicKey !== \"string\" || !/^[a-f0-9]{64}$/.test(publicKey)) throw new Error(\"Invalid HyperDHT endpoint\");\n return attach(client.node.connect(fromHex(publicKey), { keyPair }), channels);\n },\n async close() {\n stopped = true;\n for (const channel of channels) channel.close();\n waiting.length = 0;\n await server.close().catch(() => {});\n release(url, client, member);\n },\n };\n return endpoint;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"hyperdhtRelay-DvXgebV7.js","names":[],"sources":["../../../browser/src/platform/hyperdhtRelay.ts"],"sourcesContent":["import { ed25519 } from \"@noble/curves/ed25519.js\";\nimport { fromBase64Url, type BoundChannel, type FrameChannel, type NativeEndpoint } from \"@ghostly/core\";\nimport { hyperdhtRelayProblem } from \"../shared/hyperdhtRelay\";\n\nexport { hyperdhtRelayProblem as relayUrlProblem };\n\n/**\n * HyperDHT (WISP 103) for a browser, through a dht-relay: a server that runs the UDP half of HyperDHT and\n * talks to the browser over one WebSocket. It runs non-custodial: the browser keeps its keys, runs the\n * Noise handshake and the encrypted stream itself, and the relay moves handshake messages and ciphertext.\n * The binding (the handshake hash and both keys) is computed here, at the endpoint, as the Desktop does.\n *\n * What the relay sees: this browser's address, the HyperDHT keys it listens on and dials, and when and how\n * much it sends. Not the frames, not the keys' secrets, not the handshake hash.\n *\n * The wire is the Desktop's (native/transports/hyperdht/endpoint.mjs): the same preface, u32 length-prefixed\n * UTF-8 frames of at most 60 KiB, so a browser and a Desktop talk HyperDHT to each other.\n */\n\nconst PREFACE = \"ghostly/paired-chat/1\";\nconst MAX_FRAME = 60 * 1024;\nconst SEND_BUDGET = 120 * 1024;\nconst HANDSHAKE_MS = 20_000;\nconst RELAY_OPEN_MS = 10_000;\nconst LISTEN_MS = 20_000;\nconst MAX_CHANNELS = 2;\n/**\n * How the relay client is always made: never custodial. dht-relay's default is custodial, which sends every\n * secret key to the relay so it can run the handshake and the stream itself. Nothing here takes this from\n * settings or options; test/hyperdhtRelay.test.ts fails if a secret key ever reaches the relay's wire.\n */\nconst NON_CUSTODIAL = Object.freeze({ custodial: false as const });\n\n/**\n * The descriptor a browser's HyperDHT endpoint gives out: its key, that it is reached only through a relay, and\n * which one (the connection panel names its host; a Desktop dialling it ignores both).\n */\nexport interface RelayedHyperDescriptor { publicKey: string; relayed: true; relay: string }\n\n/** The parts of dht-relay and its streams this adapter uses. */\ninterface SecretSocket {\n handshakeHash: Uint8Array | null;\n publicKey: Uint8Array;\n remotePublicKey: Uint8Array;\n opened?: Promise<boolean>;\n destroyed?: boolean;\n write(data: Uint8Array): boolean;\n destroy(error?: Error): void;\n on(event: string, listener: (...args: never[]) => void): unknown;\n once(event: string, listener: (...args: never[]) => void): unknown;\n off(event: string, listener: (...args: never[]) => void): unknown;\n}\ninterface RelayedServer {\n listen(keyPair: KeyPair): Promise<void>;\n close(): Promise<void>;\n}\ninterface RelayedNode {\n ready(): Promise<void>;\n connect(remotePublicKey: Uint8Array, options: { keyPair: KeyPair }): SecretSocket;\n createServer(options: object, onconnection: (socket: SecretSocket) => void): RelayedServer;\n destroy(): Promise<void>;\n}\ninterface KeyPair { publicKey: Uint8Array; secretKey: Uint8Array }\n\nconst hex = (bytes: Uint8Array) => Array.from(bytes, b => b.toString(16).padStart(2, \"0\")).join(\"\");\nconst fromHex = (text: string) => Uint8Array.from(text.match(/../g)!, byte => parseInt(byte, 16));\n\n/** HyperDHT's key pair for a seed: libsodium's layout (the seed, then the public key). */\nexport function hyperKeyPair(seed: Uint8Array): KeyPair {\n if (seed.length !== 32) throw new Error(\"Invalid HyperDHT seed\");\n const publicKey = ed25519.getPublicKey(seed);\n const secretKey = new Uint8Array(64);\n secretKey.set(seed); secretKey.set(publicKey, 32);\n return { publicKey, secretKey };\n}\n\n/**\n * One WebSocket per relay for the whole app, shared by every chat's endpoint (each listens on its own\n * key). It closes when the last endpoint does. When the relay goes away, every endpoint on it says it is\n * unavailable, and the next one to start opens a new connection.\n */\ninterface RelayClient {\n node: RelayedNode;\n endpoints: Set<{ unavailable(): void }>;\n gone: boolean;\n /** Settles when the relay connection closes. */\n closed: Promise<void>;\n}\nconst clients = new Map<string, Promise<RelayClient>>();\n\nasync function relayClient(url: string, createSocket: (url: string) => WebSocket): Promise<RelayClient> {\n const existing = clients.get(url);\n if (existing) {\n const client = await existing.catch(() => null);\n if (client && !client.gone) return client;\n if (clients.get(url) === existing) clients.delete(url);\n }\n const opening = (async () => {\n const [{ default: RelayedDHT }, { default: WebSocketStream }] = await Promise.all([\n // @ts-expect-error dht-relay ships no types: RelayedNode and SecretSocket above describe what this uses.\n import(\"@hyperswarm/dht-relay\"),\n // @ts-expect-error The same package's WebSocket stream, untyped too.\n import(\"@hyperswarm/dht-relay/ws\"),\n ]);\n const socket = createSocket(url);\n socket.binaryType = \"arraybuffer\";\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => { socket.close(); reject(new Error(\"The HyperDHT relay did not answer\")); }, RELAY_OPEN_MS);\n socket.addEventListener(\"open\", () => { clearTimeout(timer); resolve(); }, { once: true });\n socket.addEventListener(\"error\", () => { clearTimeout(timer); reject(new Error(\"Could not reach the HyperDHT relay\")); }, { once: true });\n socket.addEventListener(\"close\", () => { clearTimeout(timer); reject(new Error(\"The HyperDHT relay closed the connection\")); }, { once: true });\n });\n // Non-custodial: the relay is told public keys only, never a secret key (dht-relay's default sends them).\n // The node's own key is never used to listen or dial: a random one says nothing about this app.\n const node = new RelayedDHT(new WebSocketStream(true, socket), { ...NON_CUSTODIAL, keyPair: hyperKeyPair(crypto.getRandomValues(new Uint8Array(32))) }) as RelayedNode;\n let closed!: () => void;\n const client: RelayClient = { node, endpoints: new Set(), gone: false, closed: new Promise(resolve => { closed = resolve; }) };\n socket.addEventListener(\"close\", () => {\n client.gone = true; closed();\n if (clients.get(url) === opening) clients.delete(url);\n for (const endpoint of [...client.endpoints]) endpoint.unavailable();\n });\n return client;\n })();\n clients.set(url, opening);\n opening.catch(() => { if (clients.get(url) === opening) clients.delete(url); });\n return opening;\n}\n\nfunction release(url: string, client: RelayClient, endpoint: { unavailable(): void }): void {\n client.endpoints.delete(endpoint);\n if (client.endpoints.size || client.gone) return;\n client.gone = true;\n void clients.get(url)?.then(current => { if (current === client) clients.delete(url); }).catch(() => {});\n void client.node.destroy().catch(() => {});\n}\n\n/** One authenticated stream as a frame channel, with a bounded queue before the session installs its reader. */\nclass RelayedChannel implements FrameChannel {\n private reader: FrameChannel[\"onMessage\"] = null;\n private buffered: string[] = [];\n private closed = false;\n private pendingBytes = 0;\n onClose: FrameChannel[\"onClose\"] = null;\n constructor(private readonly socket: SecretSocket) {\n socket.on(\"drain\", () => { this.pendingBytes = 0; });\n }\n get bufferedAmount(): number { return this.pendingBytes; }\n readonly sendBudget = SEND_BUDGET;\n get onMessage() { return this.reader; }\n set onMessage(reader: FrameChannel[\"onMessage\"]) {\n this.reader = reader;\n if (reader) for (const text of this.buffered.splice(0)) reader(text);\n }\n receive(text: string): boolean {\n if (this.closed) return true;\n if (this.reader) this.reader(text);\n else if (this.buffered.length < 64) this.buffered.push(text);\n else return false;\n return true;\n }\n send(data: string | Uint8Array): void {\n if (typeof data !== \"string\" || this.closed || !data.length) throw new Error(\"Native channel is closed or exceeds its frame budget\");\n const payload = new TextEncoder().encode(data);\n if (payload.length > MAX_FRAME || this.pendingBytes + payload.length > SEND_BUDGET) throw new Error(\"Native channel is closed or exceeds its frame budget\");\n // The stream takes every write; past its high-water mark it says so until it drains.\n if (!writeFrame(this.socket, payload) || this.pendingBytes) this.pendingBytes += payload.length + 4;\n }\n drained(): Promise<void> {\n if (!this.pendingBytes) return Promise.resolve();\n return new Promise((resolve, reject) => {\n const done = () => { this.socket.off(\"close\", gone); resolve(); };\n const gone = () => { this.socket.off(\"drain\", done); reject(new Error(\"Native channel closed\")); };\n this.socket.once(\"drain\", done); this.socket.once(\"close\", gone);\n });\n }\n closedByPeer(): void {\n if (this.closed) return;\n this.closed = true; this.buffered = [];\n this.onClose?.();\n }\n close(): void {\n if (this.closed) return;\n this.socket.destroy();\n this.closedByPeer();\n }\n}\n\nfunction writeFrame(socket: SecretSocket, payload: Uint8Array): boolean {\n const frame = new Uint8Array(4 + payload.length);\n new DataView(frame.buffer).setUint32(0, payload.length);\n frame.set(payload, 4);\n return socket.write(frame);\n}\n\n/**\n * Waits for the peer's preface on an authenticated stream (sending ours as soon as the handshake is\n * done), then hands the channel over with the binding the stream itself gives. A frame of the wrong size,\n * bytes that are not UTF-8 or another protocol close the stream.\n */\nfunction attach(socket: SecretSocket, channels: Set<RelayedChannel>): Promise<BoundChannel> {\n if (channels.size >= MAX_CHANNELS) { socket.destroy(); return Promise.reject(new Error(\"Native connection limit reached\")); }\n const channel = new RelayedChannel(socket);\n channels.add(channel);\n return new Promise<BoundChannel>((resolve, reject) => {\n let buffer = new Uint8Array(0);\n let prefaced = false;\n const decoder = new TextDecoder(\"utf-8\", { fatal: true });\n const timer = setTimeout(() => socket.destroy(new Error(\"HyperDHT handshake timed out\")), HANDSHAKE_MS);\n const fail = (error: Error) => { socket.destroy(error); };\n socket.on(\"error\", (error: Error) => reject(error));\n socket.on(\"close\", () => {\n clearTimeout(timer); channels.delete(channel);\n reject(new Error(\"Native channel closed\"));\n channel.closedByPeer();\n });\n const opened = () => {\n if (socket.handshakeHash?.length !== 64 || socket.publicKey?.length !== 32 || socket.remotePublicKey?.length !== 32) {\n fail(new Error(\"Noise session binding unavailable\")); return;\n }\n writeFrame(socket, new TextEncoder().encode(PREFACE));\n };\n if (socket.opened) void socket.opened.then(ok => { if (ok) opened(); else reject(new Error(\"Noise handshake failed\")); });\n else socket.once(\"open\", opened);\n socket.on(\"data\", (chunk: Uint8Array) => {\n const joined = new Uint8Array(buffer.length + chunk.length);\n joined.set(buffer); joined.set(chunk, buffer.length);\n buffer = joined;\n while (buffer.length >= 4) {\n const length = new DataView(buffer.buffer, buffer.byteOffset, 4).getUint32(0);\n if (length === 0 || length > MAX_FRAME) { fail(new Error(\"Invalid frame length\")); return; }\n if (buffer.length < 4 + length) break;\n let text: string;\n try { text = decoder.decode(buffer.subarray(4, 4 + length)); }\n catch { fail(new Error(\"Invalid UTF-8 frame\")); return; }\n buffer = buffer.slice(4 + length);\n if (!prefaced) {\n if (text !== PREFACE) { fail(new Error(\"Unexpected application protocol\")); return; }\n prefaced = true; clearTimeout(timer);\n resolve({ channel, binding: { transport: \"hyperdht/1\", context: hex(socket.handshakeHash!),\n identities: [hex(socket.publicKey), hex(socket.remotePublicKey)].sort() as [string, string] } });\n } else if (!channel.receive(text)) { fail(new Error(\"Receive queue exceeded\")); return; }\n }\n });\n });\n}\n\nexport interface RelayedHyperOptions {\n /** How to open the WebSocket (tests pass Node's `ws`). Default: the browser's. */\n createSocket?: (url: string) => WebSocket;\n}\n\n/**\n * A chat's HyperDHT endpoint through the relay at `url`: it listens on the key of `seedB64` (the chat's\n * own transport seed, as on the Desktop) and dials a contact's descriptor. Fails, and so is not offered,\n * when the relay cannot be reached or does not let it listen.\n */\nexport async function createRelayedHyperEndpoint(seedB64: string, url: string, options: RelayedHyperOptions = {}): Promise<NativeEndpoint> {\n const problem = hyperdhtRelayProblem(url);\n if (problem) throw new Error(problem);\n const keyPair = hyperKeyPair(fromBase64Url(seedB64));\n const client = await relayClient(url, options.createSocket ?? (address => new WebSocket(address)));\n const channels = new Set<RelayedChannel>();\n let stopped = false;\n let handler: NativeEndpoint[\"onConnection\"] = null;\n const waiting: BoundChannel[] = [];\n let endpoint: NativeEndpoint | null = null;\n const member = {\n unavailable() {\n if (stopped) return;\n stopped = true;\n for (const channel of channels) channel.close();\n endpoint?.onUnavailable?.();\n },\n };\n client.endpoints.add(member);\n const server = client.node.createServer({}, socket => {\n void attach(socket, channels).then(bound => {\n if (stopped) bound.channel.close();\n else if (handler) handler(bound);\n else if (waiting.length < MAX_CHANNELS) waiting.push(bound);\n else bound.channel.close();\n }).catch(() => {});\n });\n try {\n await Promise.race([\n server.listen(keyPair),\n client.closed.then(() => { throw new Error(\"The HyperDHT relay closed the connection\"); }),\n new Promise((_, reject) => setTimeout(() => reject(new Error(\"The HyperDHT relay did not let this chat listen\")), LISTEN_MS)),\n ]);\n if (client.gone) throw new Error(\"The HyperDHT relay closed the connection\");\n } catch (error) {\n stopped = true;\n void server.close().catch(() => {});\n release(url, client, member);\n throw error;\n }\n const descriptor: RelayedHyperDescriptor = { publicKey: hex(keyPair.publicKey), relayed: true, relay: url };\n endpoint = {\n transport: \"hyperdht/1\", descriptor, onDescriptor: null, onUnavailable: null,\n get onConnection() { return handler; },\n set onConnection(value) { handler = value; if (value) for (const bound of waiting.splice(0)) value(bound); },\n async connect(address) {\n const publicKey = (address as { publicKey?: unknown } | null)?.publicKey;\n if (stopped || typeof publicKey !== \"string\" || !/^[a-f0-9]{64}$/.test(publicKey)) throw new Error(\"Invalid HyperDHT endpoint\");\n return attach(client.node.connect(fromHex(publicKey), { keyPair }), channels);\n },\n async close() {\n stopped = true;\n for (const channel of channels) channel.close();\n waiting.length = 0;\n await server.close().catch(() => {});\n release(url, client, member);\n },\n };\n return endpoint;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"idb-BIdus0WX.js","names":[],"sources":["../../../browser/src/shared/idb.ts"],"sourcesContent":["/**\n * The extension's one IndexedDB database. The peer (offscreen document) and\n * the pages share an origin, so both open it: that is also how file contents\n * move between them without squeezing through runtime messages.\n */\nlet dbName = \"ghostly\";\n\n/** Before first use: keeps profiles that share a browser storage area apart (Desktop's GHOSTLY_PROFILE). */\nexport function setDatabaseName(name: string): void {\n dbName = name;\n}\n/** The peer database this page uses: its profile's (WISP 04). */\nexport function databaseName(): string {\n return dbName;\n}\n/** The schema this build reads and writes. A database stored at a higher one is a newer build's: it is never opened. */\nexport const DB_VERSION = 14;\n\n/**\n * Why the profile's database did not open. `newer`: a newer Ghostly stored it (IndexedDB never opens a database below\n * its stored version). `blocked`: another window still holds it open at an older version. `full`: no storage space.\n * `denied`: the browser gives this page no storage (a private window, storage blocked for the site). `failed`:\n * anything else, a damaged database included (browsers do not tell that one apart).\n */\nexport type ProfileOpenReason = \"newer\" | \"blocked\" | \"full\" | \"denied\" | \"failed\";\n\n/** What the pages are told (a plain object: it crosses the extension's port), and what a bot reads in `details`. */\nexport interface ProfileOpenFailure {\n reason: ProfileOpenReason;\n /** The schema version this build reads (`DB_VERSION`). */\n supportedVersion: number;\n /** The version the database is stored at, when it could be read (`newer`). */\n storedVersion?: number;\n /** What the browser said, in its own words. */\n detail?: string;\n}\n\n/** The profile's database did not open. Nothing was changed, and nothing is ever deleted or reset because of it. */\nexport class ProfileOpenError extends Error {\n constructor(readonly failure: ProfileOpenFailure) {\n super(profileOpenMessage(failure));\n this.name = \"ProfileOpenError\";\n }\n}\n\n/** The failure in plain English, for logs, the CLI and anything that shows an engine error as it came. */\nexport function profileOpenMessage(failure: ProfileOpenFailure): string {\n const could = \"Ghostly could not open this profile's data\";\n switch (failure.reason) {\n case \"newer\":\n return `This profile was last used by a newer version of Ghostly (its data is version ${failure.storedVersion ?? \"above \" + failure.supportedVersion}, this version reads up to ${failure.supportedVersion}). Update Ghostly to open it.`;\n case \"blocked\": return `${could}: another Ghostly window or tab still has it open with an older version. Close it and try again.`;\n case \"full\": return `${could}: this device is out of storage space. Free some space and try again.`;\n case \"denied\": return `${could}: storage is not allowed here (a private window, or storage blocked for this site).`;\n default: return `${could}${failure.detail ? `: ${failure.detail}` : \"\"}`;\n }\n}\n\n/** What the start of the engine failed with, as the failure the pages show: a database that did not open, or anything else. */\nexport function profileOpenFailure(error: unknown): ProfileOpenFailure {\n if (error instanceof ProfileOpenError) return error.failure;\n const detail = error instanceof Error ? error.message : String(error);\n return { reason: \"failed\", supportedVersion: DB_VERSION, ...(detail ? { detail } : {}) };\n}\n\n/** How long an open that another window blocks may wait for that window to let go before it is said to be blocked. */\nexport const OPEN_TIMINGS = { blockedMs: 10_000 };\n\nfunction failureOf(error: unknown): ProfileOpenFailure {\n const name = (error as { name?: unknown } | null)?.name;\n const message = (error as { message?: unknown } | null)?.message;\n const detail = [typeof name === \"string\" ? name : \"\", typeof message === \"string\" ? message : \"\"].filter(Boolean).join(\": \");\n const reason: ProfileOpenReason = name === \"VersionError\" ? \"newer\"\n : name === \"QuotaExceededError\" ? \"full\"\n // Firefox's private windows of old, storage blocked for the site, or no IndexedDB at all.\n : name === \"BlockedError\" ? \"blocked\"\n : name === \"SecurityError\" || name === \"InvalidStateError\" || name === \"ReferenceError\" || name === \"TypeError\" ? \"denied\"\n : \"failed\";\n return { reason, supportedVersion: DB_VERSION, ...(detail ? { detail } : {}) };\n}\n\n/**\n * The version a database is stored at, read without changing it: an open that names no version opens what is there.\n * Only for a database known to exist (an open without a version would make a missing one).\n */\nfunction storedVersion(name: string): Promise<number | undefined> {\n return new Promise((resolve) => {\n try {\n const request = indexedDB.open(name);\n // It did not exist after all: leave nothing behind.\n request.onupgradeneeded = () => { try { request.transaction?.abort(); } catch { /* already over */ } };\n request.onsuccess = () => { const { version } = request.result; request.result.close(); resolve(version); };\n request.onerror = request.onblocked = () => resolve(undefined);\n } catch { resolve(undefined); }\n });\n}\n/** The messages store's index of card messages (`card.kind`): what `db.getCardMessages` reads. */\nexport const CARD_INDEX = \"byCardKind\";\n\nexport const STORES = {\n links: \"links\",\n messages: \"messages\",\n services: \"services\",\n settings: \"settings\",\n files: \"files\",\n proofs: \"proofs\",\n payments: \"payments\",\n quotes: \"quotes\",\n walletTx: \"walletTx\",\n melts: \"melts\",\n /** Swaps at a mint that have not finished (`PendingSwap`): what gets their outputs back when an answer is missed. */\n swaps: \"swaps\",\n intents: \"paymentIntents\",\n /** Private groups (WISP 900): membership chain, epoch secrets and my own recent messages, by group id. */\n groups: \"groups\",\n /** Files kept in 1 MiB pieces where the platform has no file system for them (see `fileBytesIdb.ts`). */\n fileChunks: \"fileChunks\",\n /**\n * What changes about a stored file after it is stored, by file id. A `files` record is written once and never\n * again: WebKit loses the Blob of a record written back with the Blob it read (see `fileStore`).\n */\n fileState: \"fileState\",\n /** Installed mini-apps (WISP 1200 § Updates and rollback), by app reference: `InstalledApp` (`engine/apps.ts`). */\n apps: \"apps\",\n /** The app stores the person added, by store key: `AddedAppStore`. A store is its key; its URL is kept on the record. */\n appStores: \"appStores\",\n /** Each installed app's own storage, by (app reference, scope, key): `AppStorageRow`. Scope: a chat's link id, or `alone`. */\n appStorage: \"appStorage\",\n} as const;\n\n/** The app storage store's index by (app reference, scope): one scope's rows, for its 5 MiB cap and its keys. */\nexport const APP_STORAGE_SCOPE_INDEX = \"byAppScope\";\n/** The app storage store's index by scope: every app's rows of one chat, which go with the chat. */\nexport const APP_STORAGE_CHAT_INDEX = \"byScope\";\n\n/**\n * A file's contents. Metadata travels with the chat message; this is only the bytes: in `blob` for a file\n * small enough to hold whole, or in the platform's file storage (`bytes`, see `fileBytes.ts`) under the same id.\n * Ids are `<link id>-out-<random>` for files we send and `<link id>-in-<random>` for\n * files we receive, both chosen here. Files stored before that are `<link id>-<wire id>`\n * without `direction` and are still read under their old id.\n */\nexport interface StoredFile {\n id: string;\n linkId: string;\n blob?: Blob;\n /** Where the bytes are when there is no `blob`. */\n bytes?: import(\"./fileBytes\").FileBytesKind;\n createdAt: number;\n direction?: \"in\" | \"out\";\n /** The id the file had on the data link. */\n wireId?: string;\n digest?: string;\n metadata?: { name: string; size: number; mime: string; timestamp: number; voice?: import(\"@ghostly/core\").VoiceMeta; video?: import(\"@ghostly/core\").VideoMeta; image?: import(\"@ghostly/core\").ImageMeta };\n transfer?: { state: \"transferring\" | \"done\" | \"failed\"; transferred: number; size: number; error?: string };\n /** files/3: the transfer's own record (`@ghostly/core` `FileTransferRecord`), kept so it resumes after a restart. */\n wire3?: import(\"@ghostly/core\").FileTransferRecord;\n /** Its bytes were left out of the light backup this profile was restored from (WISP 05 § Light backups). */\n leftOut?: boolean;\n /**\n * A file the contact said on the DHT floor (WISP 403 § Files), by its wire id: its bubble waits for its offer on the\n * live session, and no byte is here yet. Gone once the offer came (`wireId` and `wire3` then).\n */\n announced?: string;\n}\n\n/** The fields of a stored file that change after it is stored: kept in `STORES.fileState`, read over the record's own. */\ntype FileStateFields = Partial<Pick<StoredFile, \"bytes\" | \"digest\" | \"transfer\" | \"wire3\">>;\ntype FileState = { id: string } & FileStateFields;\n\nlet dbPromise: Promise<IDBDatabase> | null = null;\n\nexport function openDb(): Promise<IDBDatabase> {\n dbPromise ??= new Promise((resolve, reject) => {\n let settled = false;\n let blocked: ReturnType<typeof setTimeout> | undefined;\n // Said once, as what it is (`ProfileOpenError`): the engine does not start, and the pages show why.\n const fail = (error: unknown) => {\n if (settled) return;\n settled = true;\n clearTimeout(blocked);\n const failure = failureOf(error);\n if (failure.reason !== \"newer\") { reject(new ProfileOpenError(failure)); return; }\n void storedVersion(dbName).then((version) => reject(new ProfileOpenError({ ...failure, ...(version !== undefined ? { storedVersion: version } : {}) })));\n };\n let request: IDBOpenDBRequest;\n try {\n request = indexedDB.open(dbName, DB_VERSION);\n } catch (error) {\n fail(error);\n return;\n }\n request.onupgradeneeded = () => {\n const db = request.result;\n const has = (name: string) => db.objectStoreNames.contains(name);\n if (!has(STORES.links)) db.createObjectStore(STORES.links, { keyPath: \"id\" });\n if (!has(STORES.services)) db.createObjectStore(STORES.services, { keyPath: \"id\" });\n if (!has(STORES.settings)) db.createObjectStore(STORES.settings);\n if (!has(STORES.messages)) {\n db.createObjectStore(STORES.messages, { keyPath: [\"linkId\", \"id\"] }).createIndex(\"byLink\", \"linkId\");\n }\n if (!has(STORES.proofs)) db.createObjectStore(STORES.proofs, { keyPath: \"secret\" });\n if (!has(STORES.payments)) db.createObjectStore(STORES.payments, { keyPath: \"id\" });\n if (!has(STORES.quotes)) db.createObjectStore(STORES.quotes, { keyPath: \"quote\" });\n if (!has(STORES.walletTx)) db.createObjectStore(STORES.walletTx, { keyPath: \"id\" });\n // v5: Lightning payments the mint has not settled yet, and the proofs they hold.\n if (!has(STORES.melts)) db.createObjectStore(STORES.melts, { keyPath: \"quote\" });\n if (!has(STORES.intents)) db.createObjectStore(STORES.intents, { keyPath: \"review.id\" });\n if (!has(STORES.files)) {\n db.createObjectStore(STORES.files, { keyPath: \"id\" }).createIndex(\"byLink\", \"linkId\");\n }\n // v7: private groups.\n if (!has(STORES.groups)) db.createObjectStore(STORES.groups, { keyPath: \"id\" });\n // v8: files in pieces, for platforms without the origin-private file system.\n if (!has(STORES.fileChunks)) db.createObjectStore(STORES.fileChunks, { keyPath: [\"id\", \"index\"] });\n // v9: what changes about a stored file, apart from its record (see `fileStore`).\n if (!has(STORES.fileState)) db.createObjectStore(STORES.fileState, { keyPath: \"id\" });\n // v10: a chat's messages in time order, so its latest page is read without the rest.\n const messages = request.transaction!.objectStore(STORES.messages);\n if (!messages.indexNames.contains(\"byLinkTime\")) messages.createIndex(\"byLinkTime\", [\"linkId\", \"timestamp\"]);\n // v11: the messages that carry a status card, by the card's kind, across every chat and group (the Tasks board).\n // A message without a card has no such key and is not in it, so the index is as small as the cards are few.\n if (!messages.indexNames.contains(CARD_INDEX)) messages.createIndex(CARD_INDEX, \"card.kind\");\n // v12: swaps at a mint, written down before the mint is asked.\n if (!has(STORES.swaps)) db.createObjectStore(STORES.swaps, { keyPath: \"id\" });\n // v13: no change of the schema. A build from here can hold a frozen copy of a profile on a standby device (WISP 06\n // § Compatibility and rollout): the step keeps an older build (1.0.3 reads 12) from opening and starting one.\n // v14: mini-apps (WISP 1200): the installed apps, the stores added and each app's storage per scope. Empty until the\n // apps feature is on; the bundles themselves are in file storage under `app-<digest>`, outside every store here.\n if (!has(STORES.apps)) db.createObjectStore(STORES.apps, { keyPath: \"ref\" });\n if (!has(STORES.appStores)) db.createObjectStore(STORES.appStores, { keyPath: \"key\" });\n if (!has(STORES.appStorage)) {\n const storage = db.createObjectStore(STORES.appStorage, { keyPath: [\"ref\", \"scope\", \"key\"] });\n storage.createIndex(APP_STORAGE_SCOPE_INDEX, [\"ref\", \"scope\"]);\n storage.createIndex(APP_STORAGE_CHAT_INDEX, \"scope\");\n }\n };\n request.onsuccess = () => {\n // Opened after it was said to be blocked: nobody uses this connection, and it must not block the next open.\n if (settled) { request.result.close(); return; }\n settled = true;\n clearTimeout(blocked);\n // Let the other context upgrade the schema instead of blocking it.\n request.result.onversionchange = () => {\n request.result.close();\n dbPromise = null;\n };\n resolve(request.result);\n };\n // The event's default aborts nothing here, but an unhandled one is logged as an uncaught error in some browsers.\n request.onerror = (event) => { event.preventDefault?.(); fail(request.error); };\n // Another window holds the database at an older version and has not let go (ours do, on `versionchange`). The\n // open stays pending for as long as that lasts: before, the engine waited with it, silently and for good.\n request.onblocked = () => {\n blocked ??= setTimeout(() => fail({ name: \"BlockedError\", message: \"another connection holds an older version open\" }), OPEN_TIMINGS.blockedMs);\n };\n });\n return dbPromise;\n}\n\n/** The stores \"Clear all data\" empties: chats and their keys, messages, files, shared apps, groups, mini-apps and their data. */\nexport const PROFILE_STORES = [STORES.links, STORES.messages, STORES.files, STORES.fileState, STORES.fileChunks, STORES.services, STORES.groups, STORES.apps, STORES.appStores, STORES.appStorage] as const;\n\n/**\n * Records of the settings store that belong to the profile, not to a wallet: its DID and its key, its identity proofs\n * with their keys and revocations, its own Nostr data, the public profiles it read. A list of what goes rather than of\n * what stays, on purpose: every other record of that store is a wallet's (seeds, configuration, pending operations),\n * and a wallet record a newer version adds must never be deleted by mistake.\n */\nexport const PROFILE_SETTINGS_KEYS = [\"profileDid\", \"identityProofs\", \"identityRevocations\", \"nostrSocial\", \"publicProfiles\"] as const;\n\n/**\n * The fields of the peer's `settings` record that are a wallet's, kept by \"Clear all data\": the mint list, the first-run\n * wallet setup and the backup reminders. Everything else in it is the profile's (name, picture, network settings,\n * storage credentials for held items, the push subscription and who holds it) and goes.\n */\nexport const WALLET_SETTINGS_FIELDS = [\"mints\", \"mintsInitialized\", \"walletSetup\", \"backupReminders\"] as const;\n\n/**\n * \"Clear all data\" (WISP 04): everything of the profile in its peer database except its wallets. The chats' stores are\n * emptied, the profile's records of the settings store are deleted and the `settings` record keeps only its wallet\n * fields. The wallets (proofs, payments, quotes, history, payment journal, wallet records) stay: ecash is money, and\n * nothing else holds a copy of it. The caller stops the peer first, or it may write back what was just deleted.\n */\nexport async function clearProfileStores(): Promise<void> {\n if (typeof indexedDB === \"undefined\") return;\n const db = await openDb();\n const names = [...PROFILE_STORES, STORES.settings].filter((name) => db.objectStoreNames.contains(name));\n const tx = db.transaction(names, \"readwrite\");\n for (const name of names) if (name !== STORES.settings) tx.objectStore(name).clear();\n if (names.includes(STORES.settings)) {\n const settings = tx.objectStore(STORES.settings);\n for (const key of PROFILE_SETTINGS_KEYS) settings.delete(key);\n const read = settings.get(\"settings\");\n read.onsuccess = () => {\n const record = read.result as Record<string, unknown> | undefined;\n if (!record || typeof record !== \"object\") return;\n const kept = Object.fromEntries(WALLET_SETTINGS_FIELDS.filter((field) => field in record).map((field) => [field, record[field]]));\n settings.put(kept, \"settings\");\n };\n }\n await new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = tx.onabort = () => reject(tx.error);\n });\n}\n\n/** Deletes a database of the origin by name (an SDK's own), as far as the browser lets it: blocked or failed, it gives up quietly. */\nexport function deleteDatabase(name: string): Promise<void> {\n return new Promise<void>((resolve) => {\n try { const request = indexedDB.deleteDatabase(name); request.onsuccess = request.onerror = request.onblocked = () => resolve(); } catch { resolve(); }\n });\n}\n\nexport function wrap<T>(request: IDBRequest<T>): Promise<T> {\n return new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n}\n\nexport async function store(name: string, mode: IDBTransactionMode): Promise<IDBObjectStore> {\n return (await openDb()).transaction(name, mode).objectStore(name);\n}\n\n/**\n * Several writes across stores in one transaction: they all land, or none does.\n * `work` only queues requests; the promise settles when the transaction does.\n */\nexport async function transact(names: string[], work: (stores: Record<string, IDBObjectStore>) => void): Promise<void> {\n const tx = (await openDb()).transaction(names, \"readwrite\");\n const done = new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onabort = () => reject(tx.error ?? new Error(\"The wallet could not save\"));\n tx.onerror = () => reject(tx.error ?? new Error(\"The wallet could not save\"));\n });\n try {\n work(Object.fromEntries(names.map((name) => [name, tx.objectStore(name)])));\n } catch (error) {\n tx.abort();\n await done.catch(() => {});\n throw error;\n }\n return done;\n}\n\n/** A stored file as it stands: its record, with what changed since it was stored read over it. */\nfunction withState(file: StoredFile, state: FileState | undefined): StoredFile {\n if (!state) return file;\n const { id: _id, ...fields } = state;\n return { ...file, ...fields };\n}\n\n/**\n * The files store. A record is written once, by `put`, and never written back: what changes later (`patch`,\n * `updateTransfer`) goes to `STORES.fileState` and is read over it. WebKit (Safari, the macOS app) keeps a stored\n * Blob as a file the record points to, and a record written back with the Blob it read can lose that file: from\n * then on every read of it fails with \"The object can not be found here.\" For the same reason a Blob read from\n * here is never put here again, under any id (a forward copies the bytes into a Blob of its own).\n */\nexport const fileStore = {\n /** Stores a file anew: whatever changed about an earlier file under this id is forgotten. */\n async put(file: StoredFile): Promise<void> {\n await transact([STORES.files, STORES.fileState], stores => {\n stores[STORES.files].put(file);\n stores[STORES.fileState].delete(file.id);\n });\n },\n async updateTransfer(id: string, transfer: NonNullable<StoredFile[\"transfer\"]>): Promise<void> {\n await fileStore.patch(id, { transfer });\n },\n /** Changes some fields of a stored file (never its record, see above); nothing happens to one that was deleted. */\n async patch(id: string, fields: FileStateFields): Promise<void> {\n await transact([STORES.files, STORES.fileState], stores => {\n const exists = stores[STORES.files].getKey(id);\n exists.onsuccess = () => {\n if (exists.result === undefined) return;\n const state = stores[STORES.fileState].get(id);\n state.onsuccess = () => { stores[STORES.fileState].put({ ...(state.result as FileState | undefined), ...fields, id } satisfies FileState); };\n };\n });\n },\n async get(id: string): Promise<StoredFile | undefined> {\n const tx = (await openDb()).transaction([STORES.files, STORES.fileState], \"readonly\");\n const [file, state] = await Promise.all([\n wrap<StoredFile | undefined>(tx.objectStore(STORES.files).get(id)),\n wrap<FileState | undefined>(tx.objectStore(STORES.fileState).get(id)),\n ]);\n return file && withState(file, state);\n },\n async listForLink(linkId: string): Promise<StoredFile[]> {\n const tx = (await openDb()).transaction([STORES.files, STORES.fileState], \"readonly\");\n const files = await wrap<StoredFile[]>(tx.objectStore(STORES.files).index(\"byLink\").getAll(linkId));\n const states = await Promise.all(files.map((file) => wrap<FileState | undefined>(tx.objectStore(STORES.fileState).get(file.id))));\n return files.map((file, i) => withState(file, states[i]));\n },\n async delete(id: string): Promise<void> {\n await transact([STORES.files, STORES.fileState], stores => {\n stores[STORES.files].delete(id);\n stores[STORES.fileState].delete(id);\n });\n },\n async deleteForLink(linkId: string): Promise<void> {\n const tx = (await openDb()).transaction([STORES.files, STORES.fileState], \"readwrite\");\n const files = tx.objectStore(STORES.files), state = tx.objectStore(STORES.fileState);\n const keys = await wrap(files.index(\"byLink\").getAllKeys(linkId));\n await Promise.all(keys.flatMap((key) => [wrap(files.delete(key)), wrap(state.delete(key))]));\n },\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"idb-BIdus0WX.js","names":[],"sources":["../../../browser/src/shared/idb.ts"],"sourcesContent":["/**\n * The extension's one IndexedDB database. The peer (offscreen document) and\n * the pages share an origin, so both open it: that is also how file contents\n * move between them without squeezing through runtime messages.\n */\nlet dbName = \"ghostly\";\n\n/** Before first use: keeps profiles that share a browser storage area apart (Desktop's GHOSTLY_PROFILE). */\nexport function setDatabaseName(name: string): void {\n dbName = name;\n}\n/** The peer database this page uses: its profile's (WISP 04). */\nexport function databaseName(): string {\n return dbName;\n}\n/** The schema this build reads and writes. A database stored at a higher one is a newer build's: it is never opened. */\nexport const DB_VERSION = 14;\n\n/**\n * Why the profile's database did not open. `newer`: a newer Ghostly stored it (IndexedDB never opens a database below\n * its stored version). `blocked`: another window still holds it open at an older version. `full`: no storage space.\n * `denied`: the browser gives this page no storage (a private window, storage blocked for the site). `failed`:\n * anything else, a damaged database included (browsers do not tell that one apart).\n */\nexport type ProfileOpenReason = \"newer\" | \"blocked\" | \"full\" | \"denied\" | \"failed\";\n\n/** What the pages are told (a plain object: it crosses the extension's port), and what a bot reads in `details`. */\nexport interface ProfileOpenFailure {\n reason: ProfileOpenReason;\n /** The schema version this build reads (`DB_VERSION`). */\n supportedVersion: number;\n /** The version the database is stored at, when it could be read (`newer`). */\n storedVersion?: number;\n /** What the browser said, in its own words. */\n detail?: string;\n}\n\n/** The profile's database did not open. Nothing was changed, and nothing is ever deleted or reset because of it. */\nexport class ProfileOpenError extends Error {\n constructor(readonly failure: ProfileOpenFailure) {\n super(profileOpenMessage(failure));\n this.name = \"ProfileOpenError\";\n }\n}\n\n/** The failure in plain English, for logs, the CLI and anything that shows an engine error as it came. */\nexport function profileOpenMessage(failure: ProfileOpenFailure): string {\n const could = \"Ghostly could not open this profile's data\";\n switch (failure.reason) {\n case \"newer\":\n return `This profile was last used by a newer version of Ghostly (its data is version ${failure.storedVersion ?? \"above \" + failure.supportedVersion}, this version reads up to ${failure.supportedVersion}). Update Ghostly to open it.`;\n case \"blocked\": return `${could}: another Ghostly window or tab still has it open with an older version. Close it and try again.`;\n case \"full\": return `${could}: this device is out of storage space. Free some space and try again.`;\n case \"denied\": return `${could}: storage is not allowed here (a private window, or storage blocked for this site).`;\n default: return `${could}${failure.detail ? `: ${failure.detail}` : \"\"}`;\n }\n}\n\n/** What the start of the engine failed with, as the failure the pages show: a database that did not open, or anything else. */\nexport function profileOpenFailure(error: unknown): ProfileOpenFailure {\n if (error instanceof ProfileOpenError) return error.failure;\n const detail = error instanceof Error ? error.message : String(error);\n return { reason: \"failed\", supportedVersion: DB_VERSION, ...(detail ? { detail } : {}) };\n}\n\n/** How long an open that another window blocks may wait for that window to let go before it is said to be blocked. */\nexport const OPEN_TIMINGS = { blockedMs: 10_000 };\n\nfunction failureOf(error: unknown): ProfileOpenFailure {\n const name = (error as { name?: unknown } | null)?.name;\n const message = (error as { message?: unknown } | null)?.message;\n const detail = [typeof name === \"string\" ? name : \"\", typeof message === \"string\" ? message : \"\"].filter(Boolean).join(\": \");\n const reason: ProfileOpenReason = name === \"VersionError\" ? \"newer\"\n : name === \"QuotaExceededError\" ? \"full\"\n // Firefox's private windows of old, storage blocked for the site, or no IndexedDB at all.\n : name === \"BlockedError\" ? \"blocked\"\n : name === \"SecurityError\" || name === \"InvalidStateError\" || name === \"ReferenceError\" || name === \"TypeError\" ? \"denied\"\n : \"failed\";\n return { reason, supportedVersion: DB_VERSION, ...(detail ? { detail } : {}) };\n}\n\n/**\n * The version a database is stored at, read without changing it: an open that names no version opens what is there.\n * Only for a database known to exist (an open without a version would make a missing one).\n */\nfunction storedVersion(name: string): Promise<number | undefined> {\n return new Promise((resolve) => {\n try {\n const request = indexedDB.open(name);\n // It did not exist after all: leave nothing behind.\n request.onupgradeneeded = () => { try { request.transaction?.abort(); } catch { /* already over */ } };\n request.onsuccess = () => { const { version } = request.result; request.result.close(); resolve(version); };\n request.onerror = request.onblocked = () => resolve(undefined);\n } catch { resolve(undefined); }\n });\n}\n/** The messages store's index of card messages (`card.kind`): what `db.getCardMessages` reads. */\nexport const CARD_INDEX = \"byCardKind\";\n\nexport const STORES = {\n links: \"links\",\n messages: \"messages\",\n services: \"services\",\n settings: \"settings\",\n files: \"files\",\n proofs: \"proofs\",\n payments: \"payments\",\n quotes: \"quotes\",\n walletTx: \"walletTx\",\n melts: \"melts\",\n /** Swaps at a mint that have not finished (`PendingSwap`): what gets their outputs back when an answer is missed. */\n swaps: \"swaps\",\n intents: \"paymentIntents\",\n /** Private groups (WISP 900): membership chain, epoch secrets and my own recent messages, by group id. */\n groups: \"groups\",\n /** Files kept in 1 MiB pieces where the platform has no file system for them (see `fileBytesIdb.ts`). */\n fileChunks: \"fileChunks\",\n /**\n * What changes about a stored file after it is stored, by file id. A `files` record is written once and never\n * again: WebKit loses the Blob of a record written back with the Blob it read (see `fileStore`).\n */\n fileState: \"fileState\",\n /** Installed mini-apps (WISP 1200 § Updates and rollback), by app reference: `InstalledApp` (`engine/apps.ts`). */\n apps: \"apps\",\n /** The app stores the person added, by store key: `AddedAppStore`. A store is its key; its URL is kept on the record. */\n appStores: \"appStores\",\n /** Each installed app's own storage, by (app reference, scope, key): `AppStorageRow`. Scope: a chat's link id, or `alone`. */\n appStorage: \"appStorage\",\n} as const;\n\n/** The app storage store's index by (app reference, scope): one scope's rows, for its 5 MiB cap and its keys. */\nexport const APP_STORAGE_SCOPE_INDEX = \"byAppScope\";\n/** The app storage store's index by scope: every app's rows of one chat, which go with the chat. */\nexport const APP_STORAGE_CHAT_INDEX = \"byScope\";\n\n/**\n * A file's contents. Metadata travels with the chat message; this is only the bytes: in `blob` for a file\n * small enough to hold whole, or in the platform's file storage (`bytes`, see `fileBytes.ts`) under the same id.\n * Ids are `<link id>-out-<random>` for files we send and `<link id>-in-<random>` for\n * files we receive, both chosen here. Files stored before that are `<link id>-<wire id>`\n * without `direction` and are still read under their old id.\n */\nexport interface StoredFile {\n id: string;\n linkId: string;\n blob?: Blob;\n /** Where the bytes are when there is no `blob`. */\n bytes?: import(\"./fileBytes\").FileBytesKind;\n createdAt: number;\n direction?: \"in\" | \"out\";\n /** The id the file had on the data link. */\n wireId?: string;\n digest?: string;\n metadata?: { name: string; size: number; mime: string; timestamp: number; voice?: import(\"@ghostly/core\").VoiceMeta; video?: import(\"@ghostly/core\").VideoMeta; image?: import(\"@ghostly/core\").ImageMeta };\n transfer?: { state: \"transferring\" | \"done\" | \"failed\"; transferred: number; size: number; error?: string };\n /** files/3: the transfer's own record (`@ghostly/core` `FileTransferRecord`), kept so it resumes after a restart. */\n wire3?: import(\"@ghostly/core\").FileTransferRecord;\n /** Its bytes were left out of the light backup this profile was restored from (WISP 05 § Light backups). */\n leftOut?: boolean;\n /**\n * A file the contact said on the DHT floor (WISP 403 § Files), by its wire id: its bubble waits for its offer on the\n * live session, and no byte is here yet. Gone once the offer came (`wireId` and `wire3` then).\n */\n announced?: string;\n /**\n * A file of a group (WISP 503), its `linkId` `group:<id>`: the message that announced it and its author, whether this\n * device fetched it without asking (it counts toward the group's automatic limit), and whether its person asked for it.\n * `digest` is the author's, which the bytes are checked against.\n */\n group?: { message: string; author: string; auto?: boolean; asked?: boolean };\n}\n\n/** The fields of a stored file that change after it is stored: kept in `STORES.fileState`, read over the record's own. */\ntype FileStateFields = Partial<Pick<StoredFile, \"bytes\" | \"digest\" | \"transfer\" | \"wire3\" | \"group\">>;\ntype FileState = { id: string } & FileStateFields;\n\nlet dbPromise: Promise<IDBDatabase> | null = null;\n\nexport function openDb(): Promise<IDBDatabase> {\n dbPromise ??= new Promise((resolve, reject) => {\n let settled = false;\n let blocked: ReturnType<typeof setTimeout> | undefined;\n // Said once, as what it is (`ProfileOpenError`): the engine does not start, and the pages show why.\n const fail = (error: unknown) => {\n if (settled) return;\n settled = true;\n clearTimeout(blocked);\n const failure = failureOf(error);\n if (failure.reason !== \"newer\") { reject(new ProfileOpenError(failure)); return; }\n void storedVersion(dbName).then((version) => reject(new ProfileOpenError({ ...failure, ...(version !== undefined ? { storedVersion: version } : {}) })));\n };\n let request: IDBOpenDBRequest;\n try {\n request = indexedDB.open(dbName, DB_VERSION);\n } catch (error) {\n fail(error);\n return;\n }\n request.onupgradeneeded = () => {\n const db = request.result;\n const has = (name: string) => db.objectStoreNames.contains(name);\n if (!has(STORES.links)) db.createObjectStore(STORES.links, { keyPath: \"id\" });\n if (!has(STORES.services)) db.createObjectStore(STORES.services, { keyPath: \"id\" });\n if (!has(STORES.settings)) db.createObjectStore(STORES.settings);\n if (!has(STORES.messages)) {\n db.createObjectStore(STORES.messages, { keyPath: [\"linkId\", \"id\"] }).createIndex(\"byLink\", \"linkId\");\n }\n if (!has(STORES.proofs)) db.createObjectStore(STORES.proofs, { keyPath: \"secret\" });\n if (!has(STORES.payments)) db.createObjectStore(STORES.payments, { keyPath: \"id\" });\n if (!has(STORES.quotes)) db.createObjectStore(STORES.quotes, { keyPath: \"quote\" });\n if (!has(STORES.walletTx)) db.createObjectStore(STORES.walletTx, { keyPath: \"id\" });\n // v5: Lightning payments the mint has not settled yet, and the proofs they hold.\n if (!has(STORES.melts)) db.createObjectStore(STORES.melts, { keyPath: \"quote\" });\n if (!has(STORES.intents)) db.createObjectStore(STORES.intents, { keyPath: \"review.id\" });\n if (!has(STORES.files)) {\n db.createObjectStore(STORES.files, { keyPath: \"id\" }).createIndex(\"byLink\", \"linkId\");\n }\n // v7: private groups.\n if (!has(STORES.groups)) db.createObjectStore(STORES.groups, { keyPath: \"id\" });\n // v8: files in pieces, for platforms without the origin-private file system.\n if (!has(STORES.fileChunks)) db.createObjectStore(STORES.fileChunks, { keyPath: [\"id\", \"index\"] });\n // v9: what changes about a stored file, apart from its record (see `fileStore`).\n if (!has(STORES.fileState)) db.createObjectStore(STORES.fileState, { keyPath: \"id\" });\n // v10: a chat's messages in time order, so its latest page is read without the rest.\n const messages = request.transaction!.objectStore(STORES.messages);\n if (!messages.indexNames.contains(\"byLinkTime\")) messages.createIndex(\"byLinkTime\", [\"linkId\", \"timestamp\"]);\n // v11: the messages that carry a status card, by the card's kind, across every chat and group (the Tasks board).\n // A message without a card has no such key and is not in it, so the index is as small as the cards are few.\n if (!messages.indexNames.contains(CARD_INDEX)) messages.createIndex(CARD_INDEX, \"card.kind\");\n // v12: swaps at a mint, written down before the mint is asked.\n if (!has(STORES.swaps)) db.createObjectStore(STORES.swaps, { keyPath: \"id\" });\n // v13: no change of the schema. A build from here can hold a frozen copy of a profile on a standby device (WISP 06\n // § Compatibility and rollout): the step keeps an older build (1.0.3 reads 12) from opening and starting one.\n // v14: mini-apps (WISP 1200): the installed apps, the stores added and each app's storage per scope. Empty until the\n // apps feature is on; the bundles themselves are in file storage under `app-<digest>`, outside every store here.\n if (!has(STORES.apps)) db.createObjectStore(STORES.apps, { keyPath: \"ref\" });\n if (!has(STORES.appStores)) db.createObjectStore(STORES.appStores, { keyPath: \"key\" });\n if (!has(STORES.appStorage)) {\n const storage = db.createObjectStore(STORES.appStorage, { keyPath: [\"ref\", \"scope\", \"key\"] });\n storage.createIndex(APP_STORAGE_SCOPE_INDEX, [\"ref\", \"scope\"]);\n storage.createIndex(APP_STORAGE_CHAT_INDEX, \"scope\");\n }\n };\n request.onsuccess = () => {\n // Opened after it was said to be blocked: nobody uses this connection, and it must not block the next open.\n if (settled) { request.result.close(); return; }\n settled = true;\n clearTimeout(blocked);\n // Let the other context upgrade the schema instead of blocking it.\n request.result.onversionchange = () => {\n request.result.close();\n dbPromise = null;\n };\n resolve(request.result);\n };\n // The event's default aborts nothing here, but an unhandled one is logged as an uncaught error in some browsers.\n request.onerror = (event) => { event.preventDefault?.(); fail(request.error); };\n // Another window holds the database at an older version and has not let go (ours do, on `versionchange`). The\n // open stays pending for as long as that lasts: before, the engine waited with it, silently and for good.\n request.onblocked = () => {\n blocked ??= setTimeout(() => fail({ name: \"BlockedError\", message: \"another connection holds an older version open\" }), OPEN_TIMINGS.blockedMs);\n };\n });\n return dbPromise;\n}\n\n/** The stores \"Clear all data\" empties: chats and their keys, messages, files, shared apps, groups, mini-apps and their data. */\nexport const PROFILE_STORES = [STORES.links, STORES.messages, STORES.files, STORES.fileState, STORES.fileChunks, STORES.services, STORES.groups, STORES.apps, STORES.appStores, STORES.appStorage] as const;\n\n/**\n * Records of the settings store that belong to the profile, not to a wallet: its DID and its key, its identity proofs\n * with their keys and revocations, its own Nostr data, the public profiles it read. A list of what goes rather than of\n * what stays, on purpose: every other record of that store is a wallet's (seeds, configuration, pending operations),\n * and a wallet record a newer version adds must never be deleted by mistake.\n */\nexport const PROFILE_SETTINGS_KEYS = [\"profileDid\", \"identityProofs\", \"identityRevocations\", \"nostrSocial\", \"publicProfiles\"] as const;\n\n/**\n * The fields of the peer's `settings` record that are a wallet's, kept by \"Clear all data\": the mint list, the first-run\n * wallet setup and the backup reminders. Everything else in it is the profile's (name, picture, network settings,\n * storage credentials for held items, the push subscription and who holds it) and goes.\n */\nexport const WALLET_SETTINGS_FIELDS = [\"mints\", \"mintsInitialized\", \"walletSetup\", \"backupReminders\"] as const;\n\n/**\n * \"Clear all data\" (WISP 04): everything of the profile in its peer database except its wallets. The chats' stores are\n * emptied, the profile's records of the settings store are deleted and the `settings` record keeps only its wallet\n * fields. The wallets (proofs, payments, quotes, history, payment journal, wallet records) stay: ecash is money, and\n * nothing else holds a copy of it. The caller stops the peer first, or it may write back what was just deleted.\n */\nexport async function clearProfileStores(): Promise<void> {\n if (typeof indexedDB === \"undefined\") return;\n const db = await openDb();\n const names = [...PROFILE_STORES, STORES.settings].filter((name) => db.objectStoreNames.contains(name));\n const tx = db.transaction(names, \"readwrite\");\n for (const name of names) if (name !== STORES.settings) tx.objectStore(name).clear();\n if (names.includes(STORES.settings)) {\n const settings = tx.objectStore(STORES.settings);\n for (const key of PROFILE_SETTINGS_KEYS) settings.delete(key);\n const read = settings.get(\"settings\");\n read.onsuccess = () => {\n const record = read.result as Record<string, unknown> | undefined;\n if (!record || typeof record !== \"object\") return;\n const kept = Object.fromEntries(WALLET_SETTINGS_FIELDS.filter((field) => field in record).map((field) => [field, record[field]]));\n settings.put(kept, \"settings\");\n };\n }\n await new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = tx.onabort = () => reject(tx.error);\n });\n}\n\n/** Deletes a database of the origin by name (an SDK's own), as far as the browser lets it: blocked or failed, it gives up quietly. */\nexport function deleteDatabase(name: string): Promise<void> {\n return new Promise<void>((resolve) => {\n try { const request = indexedDB.deleteDatabase(name); request.onsuccess = request.onerror = request.onblocked = () => resolve(); } catch { resolve(); }\n });\n}\n\nexport function wrap<T>(request: IDBRequest<T>): Promise<T> {\n return new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n}\n\nexport async function store(name: string, mode: IDBTransactionMode): Promise<IDBObjectStore> {\n return (await openDb()).transaction(name, mode).objectStore(name);\n}\n\n/**\n * Several writes across stores in one transaction: they all land, or none does.\n * `work` only queues requests; the promise settles when the transaction does.\n */\nexport async function transact(names: string[], work: (stores: Record<string, IDBObjectStore>) => void): Promise<void> {\n const tx = (await openDb()).transaction(names, \"readwrite\");\n const done = new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onabort = () => reject(tx.error ?? new Error(\"The wallet could not save\"));\n tx.onerror = () => reject(tx.error ?? new Error(\"The wallet could not save\"));\n });\n try {\n work(Object.fromEntries(names.map((name) => [name, tx.objectStore(name)])));\n } catch (error) {\n tx.abort();\n await done.catch(() => {});\n throw error;\n }\n return done;\n}\n\n/** A stored file as it stands: its record, with what changed since it was stored read over it. */\nfunction withState(file: StoredFile, state: FileState | undefined): StoredFile {\n if (!state) return file;\n const { id: _id, ...fields } = state;\n return { ...file, ...fields };\n}\n\n/**\n * The files store. A record is written once, by `put`, and never written back: what changes later (`patch`,\n * `updateTransfer`) goes to `STORES.fileState` and is read over it. WebKit (Safari, the macOS app) keeps a stored\n * Blob as a file the record points to, and a record written back with the Blob it read can lose that file: from\n * then on every read of it fails with \"The object can not be found here.\" For the same reason a Blob read from\n * here is never put here again, under any id (a forward copies the bytes into a Blob of its own).\n */\nexport const fileStore = {\n /** Stores a file anew: whatever changed about an earlier file under this id is forgotten. */\n async put(file: StoredFile): Promise<void> {\n await transact([STORES.files, STORES.fileState], stores => {\n stores[STORES.files].put(file);\n stores[STORES.fileState].delete(file.id);\n });\n },\n async updateTransfer(id: string, transfer: NonNullable<StoredFile[\"transfer\"]>): Promise<void> {\n await fileStore.patch(id, { transfer });\n },\n /** Changes some fields of a stored file (never its record, see above); nothing happens to one that was deleted. */\n async patch(id: string, fields: FileStateFields): Promise<void> {\n await transact([STORES.files, STORES.fileState], stores => {\n const exists = stores[STORES.files].getKey(id);\n exists.onsuccess = () => {\n if (exists.result === undefined) return;\n const state = stores[STORES.fileState].get(id);\n state.onsuccess = () => { stores[STORES.fileState].put({ ...(state.result as FileState | undefined), ...fields, id } satisfies FileState); };\n };\n });\n },\n async get(id: string): Promise<StoredFile | undefined> {\n const tx = (await openDb()).transaction([STORES.files, STORES.fileState], \"readonly\");\n const [file, state] = await Promise.all([\n wrap<StoredFile | undefined>(tx.objectStore(STORES.files).get(id)),\n wrap<FileState | undefined>(tx.objectStore(STORES.fileState).get(id)),\n ]);\n return file && withState(file, state);\n },\n async listForLink(linkId: string): Promise<StoredFile[]> {\n const tx = (await openDb()).transaction([STORES.files, STORES.fileState], \"readonly\");\n const files = await wrap<StoredFile[]>(tx.objectStore(STORES.files).index(\"byLink\").getAll(linkId));\n const states = await Promise.all(files.map((file) => wrap<FileState | undefined>(tx.objectStore(STORES.fileState).get(file.id))));\n return files.map((file, i) => withState(file, states[i]));\n },\n async delete(id: string): Promise<void> {\n await transact([STORES.files, STORES.fileState], stores => {\n stores[STORES.files].delete(id);\n stores[STORES.fileState].delete(id);\n });\n },\n async deleteForLink(linkId: string): Promise<void> {\n const tx = (await openDb()).transaction([STORES.files, STORES.fileState], \"readwrite\");\n const files = tx.objectStore(STORES.files), state = tx.objectStore(STORES.fileState);\n const keys = await wrap(files.index(\"byLink\").getAllKeys(linkId));\n await Promise.all(keys.flatMap((key) => [wrap(files.delete(key)), wrap(state.delete(key))]));\n },\n};\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Ec as isDiscoveryBudgetError, Lc as publicKeyFromZ32, Nc as parseRelayPayload, bc as RelayTransport, jc as newerPacket } from "./src-Cp3o_yHN.js";
|
|
2
2
|
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import DHT from "bittorrent-dht";
|
|
@@ -187,4 +187,4 @@ var RelaysAndDht = class {
|
|
|
187
187
|
//#endregion
|
|
188
188
|
export { Mainline, RelaysAndDht };
|
|
189
189
|
|
|
190
|
-
//# sourceMappingURL=mainline-
|
|
190
|
+
//# sourceMappingURL=mainline-B7wHcLaW.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mainline-BabMhm8L.js","names":[],"sources":["../../src/runtime/mainline.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport DHT from \"bittorrent-dht\";\nimport { ed25519 } from \"@noble/curves/ed25519.js\";\nimport {\n RelayTransport, isDiscoveryBudgetError, newerPacket, parseRelayPayload, publicKeyFromZ32,\n type DiscoveryStatus, type GhostRecord, type Identity, type PkarrRequestOptions, type PkarrTransport, type RelayTransportOptions, type SignedPacket,\n} from \"@ghostly/core\";\n\n/**\n * Pkarr over the Mainline DHT (BEP44), from Node, as the Desktop reaches it from Rust: the relays are HTTP bridges to\n * this same DHT, so a headless Ghostly that speaks it directly keeps finding its contacts while the relays fail (they\n * answered HTTP 500 for more than ten minutes on 2026-09-27, and a group's edges stayed down all that while).\n *\n * A Pkarr packet is a BEP44 mutable item: the key is the Ed25519 public key, the value the DNS packet, the sequence\n * number its timestamp in microseconds, and the signature the one the relay payload already carries (over\n * `3:seqi<seq>e1:v<len>:<v>`). So one signed payload goes to the relays and to the DHT unchanged.\n */\n\nexport interface MainlineOptions {\n /** `host:port` nodes to start from; the public routers when absent; none with `false` (a testnet's first node). */\n bootstrap?: string[] | false;\n /** Address to bind (a loopback testnet binds 127.0.0.1). */\n host?: string;\n /** How long a lookup may take before it is given up, in ms. */\n timeoutMs?: number;\n}\n\nconst SIGNATURE = 64, TIMESTAMP = 8;\nconst verify = (sig: Uint8Array, message: Uint8Array, key: Uint8Array) => { try { return ed25519.verify(sig, message, key); } catch { return false; } };\n\n/** A DHT node of this process: puts and gets Pkarr packets. */\nexport class Mainline {\n private readonly dht: DHT;\n /** Its UDP socket is bound. */\n readonly listening: Promise<void>;\n private readonly timeoutMs: number;\n\n constructor(options: MainlineOptions = {}) {\n this.timeoutMs = options.timeoutMs ?? 20_000;\n const bootstrap = options.bootstrap === false ? false : options.bootstrap?.length ? options.bootstrap : undefined;\n this.dht = new DHT({ verify, ...(bootstrap !== undefined ? { bootstrap } : {}), ...(options.host ? { host: options.host } : {}) });\n this.listening = new Promise((resolve) => this.dht.listen(0, options.host, () => resolve()));\n // A node of the table that errors must not end the process.\n this.dht.on(\"error\", () => {});\n }\n\n /** Adds a node (a testnet's peers). */\n addNode(host: string, port: number): void { this.dht.addNode({ host, port }); }\n address(): { port: number } { return this.dht.address() as { port: number }; }\n\n /** Puts a relay payload (`<signature><timestamp><DNS packet>`) as the mutable item of `pubKeyZ32`; resolves with the nodes that took it. */\n put(pubKeyZ32: string, payload: Uint8Array): Promise<number> {\n const k = Buffer.from(publicKeyFromZ32(pubKeyZ32));\n const sig = Buffer.from(payload.subarray(0, SIGNATURE));\n const seq = Number(new DataView(payload.buffer, payload.byteOffset + SIGNATURE, TIMESTAMP).getBigUint64(0));\n const v = Buffer.from(payload.subarray(SIGNATURE + TIMESTAMP));\n return this.bounded(new Promise((resolve, reject) => {\n this.dht.put({ k, v, seq, sig }, (error, _hash, nodes) => {\n if (error) reject(error); else if (!nodes) reject(new Error(\"No DHT node took the packet\")); else resolve(nodes);\n });\n }));\n }\n\n /** The newest packet of `pubKeyZ32` the DHT holds, checked; null when nobody has one. */\n get(pubKeyZ32: string): Promise<SignedPacket | null> {\n const k = Buffer.from(publicKeyFromZ32(pubKeyZ32));\n // A mutable item without salt is found under the SHA-1 of its public key (BEP44).\n const target = createHash(\"sha1\").update(k).digest();\n return this.bounded(new Promise((resolve, reject) => {\n this.dht.get(target, { cache: false }, (error, found) => {\n if (error) return reject(error);\n if (!found?.v || !found.sig) return resolve(null);\n const timestamp = new Uint8Array(TIMESTAMP);\n new DataView(timestamp.buffer).setBigUint64(0, BigInt(found.seq));\n const payload = new Uint8Array(SIGNATURE + TIMESTAMP + found.v.length);\n payload.set(found.sig, 0); payload.set(timestamp, SIGNATURE); payload.set(found.v, SIGNATURE + TIMESTAMP);\n try { resolve(parseRelayPayload(pubKeyZ32, payload)); } catch { resolve(null); }\n });\n }));\n }\n\n private bounded<T>(work: Promise<T>): Promise<T> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(\"DHT lookup timed out\")), this.timeoutMs);\n work.then((value) => { clearTimeout(timer); resolve(value); }, (error) => { clearTimeout(timer); reject(error); });\n });\n }\n\n destroy(): Promise<void> { return new Promise((resolve) => this.dht.destroy(() => resolve())); }\n}\n\n/**\n * The relays first, as the web app reaches Pkarr (its contacts read only the relays), and the DHT beside them: every\n * packet also goes to the DHT, and a read goes there when every relay is failing or the read failed for anything but\n * this app's own request budget (that is a wait, not an outage).\n *\n * `pinned`: the relays given are the only ones (`GHOSTLY_PKARR_RELAYS`, a private network): the profile's `relays`\n * setting does not replace them. With no DHT (`dht` null), the relays alone carry every packet.\n */\nexport class RelaysAndDht implements PkarrTransport {\n readonly relays: RelayTransport;\n private lastVia: \"relay\" | \"dht\" = \"relay\";\n\n constructor(private readonly dht: Mainline | null, relays: RelayTransportOptions = {}, private readonly pinned = false) {\n this.relays = new RelayTransport(relays);\n }\n\n async publish(identity: Identity, records: GhostRecord[], options: PkarrRequestOptions = {}): Promise<void> {\n // BEP44 sequence numbers must strictly increase, on the relays and on the DHT alike: one payload for both, dated\n // past every packet put or read under the key (`RelayTransport.publishDated`), whichever clock dated those.\n await this.relays.publishDated(identity, records, (key, payload, _timestamp, conflict) => this.putBoth(key, payload, options, conflict), options);\n }\n\n publishPayload(pubKeyZ32: string, payload: Uint8Array, options: PkarrRequestOptions = {}): Promise<void> {\n return this.putBoth(pubKeyZ32, payload, options);\n }\n\n private async putBoth(pubKeyZ32: string, payload: Uint8Array, options: PkarrRequestOptions, conflict?: (relay: string) => void): Promise<void> {\n if (!this.dht) return this.relays.publishPayload(pubKeyZ32, payload, options, conflict);\n const onDht = this.dht.put(pubKeyZ32, payload);\n onDht.catch(() => {});\n try {\n await this.relays.publishPayload(pubKeyZ32, payload, options, conflict);\n } catch (error) {\n // Held back by this app's budget: the relays take it when it frees (browser contacts read only them).\n if (isDiscoveryBudgetError(error)) throw error;\n // The relays failed: out on the DHT is out (a Pkarr relay reads the DHT for a key it has not got).\n await onDht.catch(() => { throw error; });\n }\n }\n\n async resolve(pubKeyZ32: string, options: PkarrRequestOptions = {}): Promise<SignedPacket | null> {\n if (!this.dht) return this.relays.resolve(pubKeyZ32, options);\n let fromRelays: SignedPacket | null = null, failed: unknown = null;\n // Every relay left alone for failing: the relays answer from what they last saw, if anything; the DHT is asked.\n const down = () => this.relays.discovery().relays.every((relay) => relay.state === \"failing\");\n if (!down()) {\n try { fromRelays = await this.relays.resolve(pubKeyZ32, options); this.lastVia = \"relay\"; } catch (error) {\n if (isDiscoveryBudgetError(error)) throw error;\n failed = error;\n }\n if (!failed && !down()) return fromRelays;\n }\n try {\n const fromDht = await this.dht.get(pubKeyZ32);\n this.lastVia = \"dht\";\n if (fromDht) this.relays.sawTimestamp(pubKeyZ32, fromDht.timestampMicros);\n return newerPacket(fromRelays, fromDht);\n } catch (error) {\n if (fromRelays) return fromRelays;\n throw failed ?? error;\n }\n }\n\n describe(): { protocol: string; relays: string[] } {\n return { protocol: this.dht ? \"Pkarr relays (HTTP), and the Mainline DHT (BEP44) directly\" : \"Pkarr relays (HTTP)\", relays: this.relays.describe().relays };\n }\n discovery(): DiscoveryStatus {\n const status = this.relays.discovery();\n return this.lastVia === \"dht\" ? { ...status, path: { via: \"dht\" } } : status;\n }\n subscribe(listener: Parameters<RelayTransport[\"subscribe\"]>[0]): () => void { return this.relays.subscribe(listener); }\n /** A relay's answer only: a packet the DHT hands back when the relays are down may be older than one already read. */\n readAnsweredAt(pubKeyZ32: string): number | undefined { return this.relays.readAnsweredAt(pubKeyZ32); }\n onServerTime(listener: Parameters<RelayTransport[\"onServerTime\"]>[0]): () => void { return this.relays.onServerTime(listener); }\n configure({ relays }: { relays: string[]; readRelays: boolean }): void { if (!this.pinned) this.relays.setRelays(relays); }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mainline-B7wHcLaW.js","names":[],"sources":["../../src/runtime/mainline.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport DHT from \"bittorrent-dht\";\nimport { ed25519 } from \"@noble/curves/ed25519.js\";\nimport {\n RelayTransport, isDiscoveryBudgetError, newerPacket, parseRelayPayload, publicKeyFromZ32,\n type DiscoveryStatus, type GhostRecord, type Identity, type PkarrRequestOptions, type PkarrTransport, type RelayTransportOptions, type SignedPacket,\n} from \"@ghostly/core\";\n\n/**\n * Pkarr over the Mainline DHT (BEP44), from Node, as the Desktop reaches it from Rust: the relays are HTTP bridges to\n * this same DHT, so a headless Ghostly that speaks it directly keeps finding its contacts while the relays fail (they\n * answered HTTP 500 for more than ten minutes on 2026-09-27, and a group's edges stayed down all that while).\n *\n * A Pkarr packet is a BEP44 mutable item: the key is the Ed25519 public key, the value the DNS packet, the sequence\n * number its timestamp in microseconds, and the signature the one the relay payload already carries (over\n * `3:seqi<seq>e1:v<len>:<v>`). So one signed payload goes to the relays and to the DHT unchanged.\n */\n\nexport interface MainlineOptions {\n /** `host:port` nodes to start from; the public routers when absent; none with `false` (a testnet's first node). */\n bootstrap?: string[] | false;\n /** Address to bind (a loopback testnet binds 127.0.0.1). */\n host?: string;\n /** How long a lookup may take before it is given up, in ms. */\n timeoutMs?: number;\n}\n\nconst SIGNATURE = 64, TIMESTAMP = 8;\nconst verify = (sig: Uint8Array, message: Uint8Array, key: Uint8Array) => { try { return ed25519.verify(sig, message, key); } catch { return false; } };\n\n/** A DHT node of this process: puts and gets Pkarr packets. */\nexport class Mainline {\n private readonly dht: DHT;\n /** Its UDP socket is bound. */\n readonly listening: Promise<void>;\n private readonly timeoutMs: number;\n\n constructor(options: MainlineOptions = {}) {\n this.timeoutMs = options.timeoutMs ?? 20_000;\n const bootstrap = options.bootstrap === false ? false : options.bootstrap?.length ? options.bootstrap : undefined;\n this.dht = new DHT({ verify, ...(bootstrap !== undefined ? { bootstrap } : {}), ...(options.host ? { host: options.host } : {}) });\n this.listening = new Promise((resolve) => this.dht.listen(0, options.host, () => resolve()));\n // A node of the table that errors must not end the process.\n this.dht.on(\"error\", () => {});\n }\n\n /** Adds a node (a testnet's peers). */\n addNode(host: string, port: number): void { this.dht.addNode({ host, port }); }\n address(): { port: number } { return this.dht.address() as { port: number }; }\n\n /** Puts a relay payload (`<signature><timestamp><DNS packet>`) as the mutable item of `pubKeyZ32`; resolves with the nodes that took it. */\n put(pubKeyZ32: string, payload: Uint8Array): Promise<number> {\n const k = Buffer.from(publicKeyFromZ32(pubKeyZ32));\n const sig = Buffer.from(payload.subarray(0, SIGNATURE));\n const seq = Number(new DataView(payload.buffer, payload.byteOffset + SIGNATURE, TIMESTAMP).getBigUint64(0));\n const v = Buffer.from(payload.subarray(SIGNATURE + TIMESTAMP));\n return this.bounded(new Promise((resolve, reject) => {\n this.dht.put({ k, v, seq, sig }, (error, _hash, nodes) => {\n if (error) reject(error); else if (!nodes) reject(new Error(\"No DHT node took the packet\")); else resolve(nodes);\n });\n }));\n }\n\n /** The newest packet of `pubKeyZ32` the DHT holds, checked; null when nobody has one. */\n get(pubKeyZ32: string): Promise<SignedPacket | null> {\n const k = Buffer.from(publicKeyFromZ32(pubKeyZ32));\n // A mutable item without salt is found under the SHA-1 of its public key (BEP44).\n const target = createHash(\"sha1\").update(k).digest();\n return this.bounded(new Promise((resolve, reject) => {\n this.dht.get(target, { cache: false }, (error, found) => {\n if (error) return reject(error);\n if (!found?.v || !found.sig) return resolve(null);\n const timestamp = new Uint8Array(TIMESTAMP);\n new DataView(timestamp.buffer).setBigUint64(0, BigInt(found.seq));\n const payload = new Uint8Array(SIGNATURE + TIMESTAMP + found.v.length);\n payload.set(found.sig, 0); payload.set(timestamp, SIGNATURE); payload.set(found.v, SIGNATURE + TIMESTAMP);\n try { resolve(parseRelayPayload(pubKeyZ32, payload)); } catch { resolve(null); }\n });\n }));\n }\n\n private bounded<T>(work: Promise<T>): Promise<T> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(\"DHT lookup timed out\")), this.timeoutMs);\n work.then((value) => { clearTimeout(timer); resolve(value); }, (error) => { clearTimeout(timer); reject(error); });\n });\n }\n\n destroy(): Promise<void> { return new Promise((resolve) => this.dht.destroy(() => resolve())); }\n}\n\n/**\n * The relays first, as the web app reaches Pkarr (its contacts read only the relays), and the DHT beside them: every\n * packet also goes to the DHT, and a read goes there when every relay is failing or the read failed for anything but\n * this app's own request budget (that is a wait, not an outage).\n *\n * `pinned`: the relays given are the only ones (`GHOSTLY_PKARR_RELAYS`, a private network): the profile's `relays`\n * setting does not replace them. With no DHT (`dht` null), the relays alone carry every packet.\n */\nexport class RelaysAndDht implements PkarrTransport {\n readonly relays: RelayTransport;\n private lastVia: \"relay\" | \"dht\" = \"relay\";\n\n constructor(private readonly dht: Mainline | null, relays: RelayTransportOptions = {}, private readonly pinned = false) {\n this.relays = new RelayTransport(relays);\n }\n\n async publish(identity: Identity, records: GhostRecord[], options: PkarrRequestOptions = {}): Promise<void> {\n // BEP44 sequence numbers must strictly increase, on the relays and on the DHT alike: one payload for both, dated\n // past every packet put or read under the key (`RelayTransport.publishDated`), whichever clock dated those.\n await this.relays.publishDated(identity, records, (key, payload, _timestamp, conflict) => this.putBoth(key, payload, options, conflict), options);\n }\n\n publishPayload(pubKeyZ32: string, payload: Uint8Array, options: PkarrRequestOptions = {}): Promise<void> {\n return this.putBoth(pubKeyZ32, payload, options);\n }\n\n private async putBoth(pubKeyZ32: string, payload: Uint8Array, options: PkarrRequestOptions, conflict?: (relay: string) => void): Promise<void> {\n if (!this.dht) return this.relays.publishPayload(pubKeyZ32, payload, options, conflict);\n const onDht = this.dht.put(pubKeyZ32, payload);\n onDht.catch(() => {});\n try {\n await this.relays.publishPayload(pubKeyZ32, payload, options, conflict);\n } catch (error) {\n // Held back by this app's budget: the relays take it when it frees (browser contacts read only them).\n if (isDiscoveryBudgetError(error)) throw error;\n // The relays failed: out on the DHT is out (a Pkarr relay reads the DHT for a key it has not got).\n await onDht.catch(() => { throw error; });\n }\n }\n\n async resolve(pubKeyZ32: string, options: PkarrRequestOptions = {}): Promise<SignedPacket | null> {\n if (!this.dht) return this.relays.resolve(pubKeyZ32, options);\n let fromRelays: SignedPacket | null = null, failed: unknown = null;\n // Every relay left alone for failing: the relays answer from what they last saw, if anything; the DHT is asked.\n const down = () => this.relays.discovery().relays.every((relay) => relay.state === \"failing\");\n if (!down()) {\n try { fromRelays = await this.relays.resolve(pubKeyZ32, options); this.lastVia = \"relay\"; } catch (error) {\n if (isDiscoveryBudgetError(error)) throw error;\n failed = error;\n }\n if (!failed && !down()) return fromRelays;\n }\n try {\n const fromDht = await this.dht.get(pubKeyZ32);\n this.lastVia = \"dht\";\n if (fromDht) this.relays.sawTimestamp(pubKeyZ32, fromDht.timestampMicros);\n return newerPacket(fromRelays, fromDht);\n } catch (error) {\n if (fromRelays) return fromRelays;\n throw failed ?? error;\n }\n }\n\n describe(): { protocol: string; relays: string[] } {\n return { protocol: this.dht ? \"Pkarr relays (HTTP), and the Mainline DHT (BEP44) directly\" : \"Pkarr relays (HTTP)\", relays: this.relays.describe().relays };\n }\n discovery(): DiscoveryStatus {\n const status = this.relays.discovery();\n return this.lastVia === \"dht\" ? { ...status, path: { via: \"dht\" } } : status;\n }\n subscribe(listener: Parameters<RelayTransport[\"subscribe\"]>[0]): () => void { return this.relays.subscribe(listener); }\n /** A relay's answer only: a packet the DHT hands back when the relays are down may be older than one already read. */\n readAnsweredAt(pubKeyZ32: string): number | undefined { return this.relays.readAnsweredAt(pubKeyZ32); }\n onServerTime(listener: Parameters<RelayTransport[\"onServerTime\"]>[0]): () => void { return this.relays.onServerTime(listener); }\n configure({ relays }: { relays: string[]; readRelays: boolean }): void { if (!this.pinned) this.relays.setRelays(relays); }\n}\n"],"mappings":";;;;;AA2BA,IAAM,YAAY;AAAI,IAAA,YAAY;AAClC,IAAM,UAAU,KAAiB,SAAqB,QAAoB;CAAE,IAAI;EAAE,OAAO,QAAQ,OAAO,KAAK,SAAS,GAAG;CAAG,QAAQ;EAAE,OAAO;CAAO;AAAE;;AAGtJ,IAAa,WAAb,MAAsB;CACpB;;CAEA;CACA;CAEA,YAAY,UAA2B,CAAC,GAAG;EACzC,KAAK,YAAY,QAAQ,aAAa;EACtC,MAAM,YAAY,QAAQ,cAAc,QAAQ,QAAQ,QAAQ,WAAW,SAAS,QAAQ,YAAY,KAAA;EACxG,KAAK,MAAM,IAAI,IAAI;GAAE;GAAQ,GAAI,cAAc,KAAA,IAAY,EAAE,UAAU,IAAI,CAAC;GAAI,GAAI,QAAQ,OAAO,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;EAAG,CAAC;EACjI,KAAK,YAAY,IAAI,SAAS,YAAY,KAAK,IAAI,OAAO,GAAG,QAAQ,YAAY,QAAQ,CAAC,CAAC;EAE3F,KAAK,IAAI,GAAG,eAAe,CAAC,CAAC;CAC/B;;CAGA,QAAQ,MAAc,MAAoB;EAAE,KAAK,IAAI,QAAQ;GAAE;GAAM;EAAK,CAAC;CAAG;CAC9E,UAA4B;EAAE,OAAO,KAAK,IAAI,QAAQ;CAAuB;;CAG7E,IAAI,WAAmB,SAAsC;EAC3D,MAAM,IAAI,OAAO,KAAK,iBAAiB,SAAS,CAAC;EACjD,MAAM,MAAM,OAAO,KAAK,QAAQ,SAAS,GAAG,SAAS,CAAC;EACtD,MAAM,MAAM,OAAO,IAAI,SAAS,QAAQ,QAAQ,QAAQ,aAAa,WAAW,SAAS,CAAC,CAAC,aAAa,CAAC,CAAC;EAC1G,MAAM,IAAI,OAAO,KAAK,QAAQ,SAAS,EAAqB,CAAC;EAC7D,OAAO,KAAK,QAAQ,IAAI,SAAS,SAAS,WAAW;GACnD,KAAK,IAAI,IAAI;IAAE;IAAG;IAAG;IAAK;GAAI,IAAI,OAAO,OAAO,UAAU;IACxD,IAAI,OAAO,OAAO,KAAK;SAAQ,IAAI,CAAC,OAAO,uBAAO,IAAI,MAAM,6BAA6B,CAAC;SAAQ,QAAQ,KAAK;GACjH,CAAC;EACH,CAAC,CAAC;CACJ;;CAGA,IAAI,WAAiD;EACnD,MAAM,IAAI,OAAO,KAAK,iBAAiB,SAAS,CAAC;EAEjD,MAAM,SAAS,WAAW,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO;EACnD,OAAO,KAAK,QAAQ,IAAI,SAAS,SAAS,WAAW;GACnD,KAAK,IAAI,IAAI,QAAQ,EAAE,OAAO,MAAM,IAAI,OAAO,UAAU;IACvD,IAAI,OAAO,OAAO,OAAO,KAAK;IAC9B,IAAI,CAAC,OAAO,KAAK,CAAC,MAAM,KAAK,OAAO,QAAQ,IAAI;IAChD,MAAM,YAAY,IAAI,WAAW,SAAS;IAC1C,IAAI,SAAS,UAAU,MAAM,CAAC,CAAC,aAAa,GAAG,OAAO,MAAM,GAAG,CAAC;IAChE,MAAM,UAAU,IAAI,WAAW,KAAwB,MAAM,EAAE,MAAM;IACrE,QAAQ,IAAI,MAAM,KAAK,CAAC;IAAG,QAAQ,IAAI,WAAW,SAAS;IAAG,QAAQ,IAAI,MAAM,GAAG,EAAqB;IACxG,IAAI;KAAE,QAAQ,kBAAkB,WAAW,OAAO,CAAC;IAAG,QAAQ;KAAE,QAAQ,IAAI;IAAG;GACjF,CAAC;EACH,CAAC,CAAC;CACJ;CAEA,QAAmB,MAA8B;EAC/C,OAAO,IAAI,SAAS,SAAS,WAAW;GACtC,MAAM,QAAQ,iBAAiB,uBAAO,IAAI,MAAM,sBAAsB,CAAC,GAAG,KAAK,SAAS;GACxF,KAAK,MAAM,UAAU;IAAE,aAAa,KAAK;IAAG,QAAQ,KAAK;GAAG,IAAI,UAAU;IAAE,aAAa,KAAK;IAAG,OAAO,KAAK;GAAG,CAAC;EACnH,CAAC;CACH;CAEA,UAAyB;EAAE,OAAO,IAAI,SAAS,YAAY,KAAK,IAAI,cAAc,QAAQ,CAAC,CAAC;CAAG;AACjG;;;;;;;;;AAUA,IAAa,eAAb,MAAoD;CAIrB;CAA2E;CAHxG;CACA,UAAmC;CAEnC,YAAY,KAAuC,SAAgC,CAAC,GAAG,SAA0B,OAAO;EAA3F,KAAA,MAAA;EAA2E,KAAA,SAAA;EACtG,KAAK,SAAS,IAAI,eAAe,MAAM;CACzC;CAEA,MAAM,QAAQ,UAAoB,SAAwB,UAA+B,CAAC,GAAkB;EAG1G,MAAM,KAAK,OAAO,aAAa,UAAU,UAAU,KAAK,SAAS,YAAY,aAAa,KAAK,QAAQ,KAAK,SAAS,SAAS,QAAQ,GAAG,OAAO;CAClJ;CAEA,eAAe,WAAmB,SAAqB,UAA+B,CAAC,GAAkB;EACvG,OAAO,KAAK,QAAQ,WAAW,SAAS,OAAO;CACjD;CAEA,MAAc,QAAQ,WAAmB,SAAqB,SAA8B,UAAmD;EAC7I,IAAI,CAAC,KAAK,KAAK,OAAO,KAAK,OAAO,eAAe,WAAW,SAAS,SAAS,QAAQ;EACtF,MAAM,QAAQ,KAAK,IAAI,IAAI,WAAW,OAAO;EAC7C,MAAM,YAAY,CAAC,CAAC;EACpB,IAAI;GACF,MAAM,KAAK,OAAO,eAAe,WAAW,SAAS,SAAS,QAAQ;EACxE,SAAS,OAAO;GAEd,IAAI,uBAAuB,KAAK,GAAG,MAAM;GAEzC,MAAM,MAAM,YAAY;IAAE,MAAM;GAAO,CAAC;EAC1C;CACF;CAEA,MAAM,QAAQ,WAAmB,UAA+B,CAAC,GAAiC;EAChG,IAAI,CAAC,KAAK,KAAK,OAAO,KAAK,OAAO,QAAQ,WAAW,OAAO;EAC5D,IAAI,aAAkC,MAAM,SAAkB;EAE9D,MAAM,aAAa,KAAK,OAAO,UAAU,CAAC,CAAC,OAAO,OAAO,UAAU,MAAM,UAAU,SAAS;EAC5F,IAAI,CAAC,KAAK,GAAG;GACX,IAAI;IAAE,aAAa,MAAM,KAAK,OAAO,QAAQ,WAAW,OAAO;IAAG,KAAK,UAAU;GAAS,SAAS,OAAO;IACxG,IAAI,uBAAuB,KAAK,GAAG,MAAM;IACzC,SAAS;GACX;GACA,IAAI,CAAC,UAAU,CAAC,KAAK,GAAG,OAAO;EACjC;EACA,IAAI;GACF,MAAM,UAAU,MAAM,KAAK,IAAI,IAAI,SAAS;GAC5C,KAAK,UAAU;GACf,IAAI,SAAS,KAAK,OAAO,aAAa,WAAW,QAAQ,eAAe;GACxE,OAAO,YAAY,YAAY,OAAO;EACxC,SAAS,OAAO;GACd,IAAI,YAAY,OAAO;GACvB,MAAM,UAAU;EAClB;CACF;CAEA,WAAmD;EACjD,OAAO;GAAE,UAAU,KAAK,MAAM,+DAA+D;GAAuB,QAAQ,KAAK,OAAO,SAAS,CAAC,CAAC;EAAO;CAC5J;CACA,YAA6B;EAC3B,MAAM,SAAS,KAAK,OAAO,UAAU;EACrC,OAAO,KAAK,YAAY,QAAQ;GAAE,GAAG;GAAQ,MAAM,EAAE,KAAK,MAAM;EAAE,IAAI;CACxE;CACA,UAAU,UAAkE;EAAE,OAAO,KAAK,OAAO,UAAU,QAAQ;CAAG;;CAEtH,eAAe,WAAuC;EAAE,OAAO,KAAK,OAAO,eAAe,SAAS;CAAG;CACtG,aAAa,UAAqE;EAAE,OAAO,KAAK,OAAO,aAAa,QAAQ;CAAG;CAC/H,UAAU,EAAE,UAA2D;EAAE,IAAI,CAAC,KAAK,QAAQ,KAAK,OAAO,UAAU,MAAM;CAAG;AAC5H"}
|