@ghostlytools/cli 1.2.4 → 1.3.1
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 +25 -9
- package/dist/assets/api-Dq2QXQDk.js +4172 -0
- package/dist/assets/api-Dq2QXQDk.js.map +1 -0
- package/dist/assets/api-TiB5QSG9.js +3 -0
- package/dist/assets/apiKit-CA2x5GJF.js +200 -0
- package/dist/assets/apiKit-CA2x5GJF.js.map +1 -0
- package/dist/assets/apps-BxV9i_Y4.js +786 -0
- package/dist/assets/apps-BxV9i_Y4.js.map +1 -0
- package/dist/assets/apps-C-T8jeji.js +2 -0
- package/dist/assets/{arkade-CdnKGDbg.js → arkade-C_qDmHnm.js} +2 -2
- package/dist/assets/{arkade-CdnKGDbg.js.map → arkade-C_qDmHnm.js.map} +1 -1
- package/dist/assets/backup-Bugnn3Ue.js +2 -0
- package/dist/assets/backup-CnMN2oUK.js +731 -0
- package/dist/assets/backup-CnMN2oUK.js.map +1 -0
- package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
- package/dist/assets/bitcoind-CZchoh43.js +349 -0
- package/dist/assets/bitcoind-CZchoh43.js.map +1 -0
- package/dist/assets/bitcoindProvider-DGw3xBSa.js +97 -0
- package/dist/assets/bitcoindProvider-DGw3xBSa.js.map +1 -0
- package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
- package/dist/assets/buttons-Bj7Vf7Pn.js +2 -0
- package/dist/assets/buttons-CvNBzh2S.js +496 -0
- package/dist/assets/buttons-CvNBzh2S.js.map +1 -0
- package/dist/assets/{endpoint-pgFkR2AS.js → endpoint-BOTFMpfS.js} +56 -18
- package/dist/assets/endpoint-BOTFMpfS.js.map +1 -0
- package/dist/assets/envelope-WjingYSc.js +90 -0
- package/dist/assets/envelope-WjingYSc.js.map +1 -0
- package/dist/assets/errors-DO5z6h_l.js +105 -0
- package/dist/assets/errors-DO5z6h_l.js.map +1 -0
- package/dist/assets/fedimint_client_wasm_bg-BtHHlFpp.js.map +1 -1
- package/dist/assets/{fileBytes-Dbifu5I5.js → fileBytes-CfSXjTFa.js} +2 -2
- package/dist/assets/{fileBytes-Dbifu5I5.js.map → fileBytes-CfSXjTFa.js.map} +1 -1
- package/dist/assets/fileBytes-CgbibpbV.js +2 -0
- package/dist/assets/{fileBytes-BcYZhNgF.js → fileBytes-Di7VZtmU.js} +6 -19
- package/dist/assets/fileBytes-Di7VZtmU.js.map +1 -0
- package/dist/assets/fileBytesIdb-CFP-DSkZ.js +2 -0
- package/dist/assets/{fileBytesIdb-DzSaqI0A.js → fileBytesIdb-D5u6H8X_.js} +4 -4
- package/dist/assets/{fileBytesIdb-DzSaqI0A.js.map → fileBytesIdb-D5u6H8X_.js.map} +1 -1
- package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js → fileBytesOpfs-Dd1N4ERH.js} +3 -3
- package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js.map → fileBytesOpfs-Dd1N4ERH.js.map} +1 -1
- package/dist/assets/groupFiles-IEUW3DQz.js +3202 -0
- package/dist/assets/groupFiles-IEUW3DQz.js.map +1 -0
- package/dist/assets/host-CWt-jtqz.js +4673 -0
- package/dist/assets/host-CWt-jtqz.js.map +1 -0
- package/dist/assets/hyperdhtRelay-BT_wRxab.js +21 -0
- package/dist/assets/hyperdhtRelay-BT_wRxab.js.map +1 -0
- package/dist/assets/{hyperdhtRelay-C1p0nkle.js → hyperdhtRelay-CPYwSLLS.js} +25 -10
- package/dist/assets/hyperdhtRelay-CPYwSLLS.js.map +1 -0
- package/dist/assets/{idb-CTF6LF5B.js → idb-B5L8wuOm.js} +7 -5
- package/dist/assets/idb-B5L8wuOm.js.map +1 -0
- package/dist/assets/irohWeb-4oChIOIp.js +544 -0
- package/dist/assets/irohWeb-4oChIOIp.js.map +1 -0
- package/dist/assets/{joinTrace-CO8mlhNO.js → joinTrace-CjRDFy85.js} +2 -7
- package/dist/assets/{joinTrace-CO8mlhNO.js.map → joinTrace-CjRDFy85.js.map} +1 -1
- package/dist/assets/joinTrace-D-fnt2p3.js +2 -0
- package/dist/assets/main-DbqcdShN.js +4409 -0
- package/dist/assets/main-DbqcdShN.js.map +1 -0
- package/dist/assets/{mainline-BH6s8HVi.js → mainline-ZkiOnevs.js} +11 -6
- package/dist/assets/mainline-ZkiOnevs.js.map +1 -0
- package/dist/assets/mints-BrlY773j.js +74 -0
- package/dist/assets/mints-BrlY773j.js.map +1 -0
- package/dist/assets/mints-CqTdA4Gk.js +2 -0
- package/dist/assets/networkReason-CetKkTPo.js +154 -0
- package/dist/assets/networkReason-CetKkTPo.js.map +1 -0
- package/dist/assets/nwc-CgkqyvsE.js +612 -0
- package/dist/assets/nwc-CgkqyvsE.js.map +1 -0
- package/dist/assets/nwcProvider-4eVz_8HP.js +77 -0
- package/dist/assets/nwcProvider-4eVz_8HP.js.map +1 -0
- package/dist/assets/privateFolder-Bk9C9oFv.js +38 -0
- package/dist/assets/privateFolder-Bk9C9oFv.js.map +1 -0
- package/dist/assets/profiles-BHc9pEYg.js +139 -0
- package/dist/assets/profiles-BHc9pEYg.js.map +1 -0
- package/dist/assets/registry-DlAKO0Fy.js +3793 -0
- package/dist/assets/registry-DlAKO0Fy.js.map +1 -0
- package/dist/assets/registry-OBJBnHVQ.js +2 -0
- package/dist/assets/resolve-CouHhCpJ.js +591 -0
- package/dist/assets/resolve-CouHhCpJ.js.map +1 -0
- package/dist/assets/{restoredRows-C_84q0Pd.js → restoredRows-Db3oUTko.js} +2 -2
- package/dist/assets/{restoredRows-C_84q0Pd.js.map → restoredRows-Db3oUTko.js.map} +1 -1
- package/dist/assets/{server-0zfNpABQ.js → server-Ss0gJ-nw.js} +1275 -1404
- package/dist/assets/server-Ss0gJ-nw.js.map +1 -0
- package/dist/assets/{src-CbLkHfz8.js → src-DDzSERyc.js} +882 -134
- package/dist/assets/src-DDzSERyc.js.map +1 -0
- package/dist/assets/turnRefs-B87dtrHM.js +323 -0
- package/dist/assets/turnRefs-B87dtrHM.js.map +1 -0
- package/dist/assets/types-C4_KafL_.js +122 -0
- package/dist/assets/types-C4_KafL_.js.map +1 -0
- package/dist/assets/{usdt-Qf4UuygS.js → usdt-Fj8CQO2z.js} +3 -3
- package/dist/assets/{usdt-Qf4UuygS.js.map → usdt-Fj8CQO2z.js.map} +1 -1
- package/dist/ghostly.mjs +4 -22077
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/endpoint-pgFkR2AS.js.map +0 -1
- package/dist/assets/fileBytes-BcYZhNgF.js.map +0 -1
- package/dist/assets/hyperdhtRelay-C1p0nkle.js.map +0 -1
- package/dist/assets/idb-CTF6LF5B.js.map +0 -1
- package/dist/assets/mainline-BH6s8HVi.js.map +0 -1
- package/dist/assets/rolldown-runtime-8H4AJuhK.js +0 -14
- package/dist/assets/server-0zfNpABQ.js.map +0 -1
- package/dist/assets/src-CbLkHfz8.js.map +0 -1
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"endpoint-pgFkR2AS.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 * The longest a stream that opened waits for the preface on a slow path: the 8 s a chat's move gives its dial. Past\n * it nobody is waiting for the dial made again, and UDX gives a silent stream up at 13 s anyway.\n */\nexport const PREFACE_MAX_WAIT_MS = 8000\n\n/**\n * How many of the stream's smoothed round trips the preface may take. A contact that waits for our first packet\n * before its side of the stream connects (we are behind a firewall) writes its preface one round trip after our\n * stream opened; a lost packet adds a retransmission timeout (at least one round trip more). Three keep a slow path's\n * preface from being dialled again for nothing (a fixed 2 s on a 3 s path made three dials) and leave every path\n * under 667 ms on the 2 s.\n */\nexport const RTT_FACTOR = 3\n\n/**\n * Before the stream has its first RTT sample, how many times the dial's own opening (from the dial to the stream\n * open) the preface may take. The opening is the DHT lookup plus the handshake through a DHT node, so it took at\n * least two round trips of the path, and the preface comes one round trip after the stream opened: once covers it\n * (on a 3 s path the first sample comes only 3 s after the stream opened, past the 2 s). It is not more because a\n * stalled stream never gets a sample, and its opening is mostly the lookup (1-3 s on the public DHT even for a near\n * contact): twice a 2.5 s opening would dial again only 7.5 s after the dial, past the move's 8 s once the second\n * dial's own opening is added. A dial that opened in milliseconds (loopback, a near contact) stays on the 2 s.\n */\nexport const OPENING_FACTOR = 1\n\n/**\n * The preface wait for a stream whose smoothed RTT is `rtt` ms and whose dial took `opening` ms to open: three round\n * trips once there is a sample, the opening itself before, between `floor` and `ceiling`.\n */\nexport function prefaceWait(rtt, opening = 0, floor = PREFACE_WAIT_MS, ceiling = PREFACE_MAX_WAIT_MS) {\n const wait = rtt > 0 ? RTT_FACTOR * rtt : OPENING_FACTOR * (opening || 0)\n return Math.min(ceiling, Math.max(floor, wait))\n}\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; `rtt()`, if given, is the stream's smoothed\n * RTT in ms so far, 0 before its first sample (UDX's `rawStream.rtt`). A dial that opened and brought no preface\n * within `waitMs`, or longer on a slow path (`prefaceWait`, at most `maxWaitMs`), is closed and made again, up to\n * `maxDials` in all. The RTT is read again whenever the wait would end, so a sample taken after the stream opened\n * counts. A dial that fails before it opened (the contact not found, refused) fails the whole connect at once, as\n * before: there is nothing to wait for.\n */\nexport function redial(dialOnce, { waitMs = PREFACE_WAIT_MS, maxWaitMs = PREFACE_MAX_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 dialledAt = Date.now()\n const dial = dialOnce()\n let timer = null\n let stalled = false\n dial.opened.then(open => {\n if (!open || settled) return\n const openedAt = Date.now()\n const check = () => {\n if (settled) return\n const left = openedAt + prefaceWait(dial.rtt?.(), openedAt - dialledAt, waitMs, maxWaitMs) - Date.now()\n if (left > 0) { timer = setTimeout(check, left); return }\n if (dials >= maxDials) return\n stalled = true\n dial.close()\n attempt()\n }\n timer = setTimeout(check, 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/** How long a closing channel waits for what it sent last to go out before the stream is destroyed anyway. */\nconst CLOSE_FLUSH_MS = 300\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 ending = null\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 // What was sent last (a goodbye, as an app stops) goes out before the stream ends: destroyed at once, it was\n // dropped unsent. A contact that never finishes the stream has CLOSE_FLUSH_MS.\n close() {\n if (closed || ending) return ending ?? Promise.resolve()\n ending = new Promise(resolve => { socket.once('close', resolve) })\n const timer = setTimeout(() => socket.destroy(), CLOSE_FLUSH_MS)\n timer.unref?.()\n socket.end()\n return ending\n },\n }\n channels.add(channel)\n onChannel?.(channel)\n socket.on('error', error => rejectOpen(error))\n // The contact ended its side (its channel closed): this one ends too, so the stream closes now, not on its timer.\n socket.on('end', () => socket.end())\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. The wait follows the stream's RTT.\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, rtt: () => socket.rawStream?.rtt ?? 0,\n close() { if (counted) channels.delete(counted); socket.destroy() } }\n })\n },\n // The channels end first (their last frames out, CLOSE_FLUSH_MS at most): the node's socket goes with it.\n async close() { stopped = true; const ended = [...channels].map(channel => channel.close()); waiting.length = 0; await Promise.all(ended); await server.close(); await node.destroy() },\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fileBytes-BcYZhNgF.js","names":[],"sources":["../../../browser/src/shared/fileBytesCommon.ts","../../../browser/src/shared/fileBytes.ts"],"sourcesContent":["/** What `fileBytes.ts` and its worker share: the worker is built alone, so this imports nothing. */\n\n/** Reads and copies move this much at a time. */\nexport const FILE_BYTES_STEP = 1024 * 1024;\n\nconst BASE64URL = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\n\n/** base64url without padding (the worker has no @ghostly/core). */\nexport function digestText(bytes: Uint8Array): string {\n let out = \"\";\n for (let i = 0; i < bytes.length; i += 3) {\n const n = (bytes[i] << 16) | ((bytes[i + 1] ?? 0) << 8) | (bytes[i + 2] ?? 0);\n out += BASE64URL[(n >> 18) & 63] + BASE64URL[(n >> 12) & 63];\n if (i + 1 < bytes.length) out += BASE64URL[(n >> 6) & 63];\n if (i + 2 < bytes.length) out += BASE64URL[n & 63];\n }\n return out;\n}\n","import { sha256 } from \"@noble/hashes/sha2.js\";\nimport { databaseName } from \"./idb\";\nimport { digestText, FILE_BYTES_STEP } from \"./fileBytesCommon\";\n\n/**\n * Where the bytes of a file live when they may be more than memory holds: a file sent or received in a chat.\n * Every implementation keeps one file per id under the profile's space (`databaseName()`), is written in\n * order (`append` at the stored size, `truncate` to go back), and is read in ranges, so no caller ever needs\n * the whole file at once.\n *\n * - `opfs`: the origin-private file system, written by a worker with sync access handles (web, extension).\n * - `native`: a real file in the app's data folder, through Rust (Desktop).\n * - `idb`: 1 MiB records in IndexedDB, where neither of those exists (and in tests).\n */\nexport interface FileBytes {\n readonly kind: FileBytesKind;\n /** Copies a picked file in, hashing it on the way. Resolves to its SHA-256, base64url. */\n stage(id: string, source: Blob, onProgress?: (copied: number) => void): Promise<string>;\n /** Writes at `offset`, which must be the stored size. Resolves once written; `flush` makes it durable. */\n append(id: string, offset: number, bytes: Uint8Array): Promise<void>;\n /** What was appended so far survives a crash. */\n flush(id: string): Promise<void>;\n /** Writing is over for now: flushed, and the file let go so it can be read elsewhere. */\n close(id: string): Promise<void>;\n /** Bytes stored, or null when there is no such file. */\n size(id: string): Promise<number | null>;\n /** Cuts the file to `size` bytes (a transfer going back to its last durable point). */\n truncate(id: string, size: number): Promise<void>;\n /** At most `length` bytes from `offset`; fewer only at the end of the file. */\n read(id: string, offset: number, length: number): Promise<Uint8Array>;\n /** SHA-256 of what is stored, base64url: read back from storage, so it checks what was written. */\n digest(id: string): Promise<string>;\n /**\n * The file as a Blob to show or save, backed by storage rather than memory, or null where this platform\n * cannot hand one out (Desktop, above `NATIVE_BLOB_MAX`: saved with `save` instead).\n */\n blob(id: string, type: string): Promise<Blob | null>;\n remove(id: string): Promise<void>;\n /** Removes every file whose id starts with `prefix` (\"\" for all of this space). */\n removeWhere(prefix: string): Promise<void>;\n /** Bytes this device can still take for files, or null when the platform does not say. */\n room(): Promise<number | null>;\n /** Saves a copy where the person chooses, where the platform has a dialog for it. False when they cancelled. */\n save?(id: string, name: string): Promise<boolean>;\n /**\n * A URL a `<video>` or `<audio>` plays the stored file from, served in ranges by the platform (Desktop, for a file\n * too large for `blob`), or null when there is no such file. The URL works until `release`.\n */\n stream?(id: string, type: string): Promise<FileStream | null>;\n /**\n * What this space's files take on disk, where they are real files (Desktop): the files, and the copies staged for a\n * save (a backup, a file on its way through the save dialog) apart. Absent elsewhere: there the browser's own\n * estimate counts them.\n */\n usage?(): Promise<FileUsage>;\n /** Removes another profile's folder (a deleted profile). IndexedDB pieces go with the profile's database. */\n dropSpace?(space: string): Promise<void>;\n /**\n * The same storage for another profile's space: a backup of a profile that is not the active one reads its files\n * through it, and a restore writes a new profile's files through it before that profile exists. Absent where files\n * are pieces in the profile's own database (`idb`): those are read and written with that database.\n */\n forSpace?(space: string): FileBytes;\n}\n\nexport type FileBytesKind = \"opfs\" | \"native\" | \"idb\";\n\n/** Bytes a space's files take (`FileBytes.usage`): the files and how many, and the copies staged for a save. */\nexport interface FileUsage { files: number; count: number; staged: number }\n\n/** A stored file being served to a media element: its URL, and how to stop serving it. */\nexport interface FileStream {\n url: string;\n release(): void;\n}\n\n/** Files sent from memory-sized sources stay whole in IndexedDB; anything larger goes through a `FileBytes`. */\nexport const SMALL_FILE_BYTES = 16 * 1024 * 1024;\n\nexport { FILE_BYTES_STEP, digestText } from \"./fileBytesCommon\";\n\nconst ID = /^[A-Za-z0-9_-]{1,200}$/;\nconst SPACE = /^[A-Za-z0-9_.-]{1,100}$/;\n\n/** A file id every backend accepts as a file name as it is: never a path. */\nexport function checkFileId(id: string): string {\n if (!ID.test(id)) throw new Error(\"Invalid file id\");\n return id;\n}\n\n/** This profile's folder name. */\nexport function fileSpace(): string {\n const space = databaseName();\n if (!SPACE.test(space)) throw new Error(\"Invalid profile space\");\n return space;\n}\n\n/**\n * SHA-256 of a Blob, read a step at a time: memory stays at one step. Read with `slice().arrayBuffer()`, not\n * `stream()`: in WebKit a Blob kept in IndexedDB that reads fine in slices failed as a stream (\"The object can not be\n * found here\"), which failed a voice note after all its bytes had arrived.\n */\nexport async function blobDigest(blob: Blob, onProgress?: (done: number) => void): Promise<string> {\n const hash = sha256.create();\n for (let done = 0; done < blob.size;) {\n const part = new Uint8Array(await blob.slice(done, Math.min(blob.size, done + FILE_BYTES_STEP)).arrayBuffer());\n if (!part.length) throw new Error(\"The file is shorter than it says\");\n hash.update(part);\n done += part.length;\n onProgress?.(done);\n }\n return digestText(hash.digest());\n}\n\ntype Maker = () => Promise<FileBytes | null>;\nconst makers = new Map<FileBytesKind, Maker>([\n [\"opfs\", () => import(\"./fileBytesOpfs\").then((m) => m.OpfsFileBytes.open())],\n [\"idb\", () => import(\"./fileBytesIdb\").then((m) => new m.IdbFileBytes())],\n]);\nconst made = new Map<FileBytesKind, Promise<FileBytes | null>>();\nlet preferred: FileBytesKind[] = [\"opfs\", \"idb\"];\n\n/** How a backend is made. The platform registers its own (Desktop: `native`) before anything uses it. */\nexport function registerFileBytes(kind: FileBytesKind, make: Maker, prefer = false): void {\n makers.set(kind, make);\n made.delete(kind);\n if (prefer) preferred = [kind, ...preferred.filter((k) => k !== kind)];\n}\n\n/** The backend of this kind, or null where this platform has none. */\nexport function fileBytesOf(kind: FileBytesKind): Promise<FileBytes | null> {\n let backend = made.get(kind);\n if (!backend) {\n const make = makers.get(kind);\n backend = make ? make().catch(() => null) : Promise.resolve(null);\n made.set(kind, backend);\n }\n return backend;\n}\n\n/** Where new files go: the first backend this platform has. */\nexport async function fileBytes(): Promise<FileBytes> {\n for (const kind of preferred) {\n const backend = await fileBytesOf(kind);\n if (backend) return backend;\n }\n throw new Error(\"This device has no storage for files\");\n}\n\n/** \"Clear all data\" and a deleted chat: the bytes of its files go with it, wherever they are. */\nexport async function removeFileBytes(prefix = \"\"): Promise<void> {\n await Promise.all([...makers.keys()].map(async (kind) => (await fileBytesOf(kind))?.removeWhere(prefix)));\n}\n\n/** A deleted profile's files, wherever they are. */\nexport async function dropFileSpace(space: string): Promise<void> {\n if (!SPACE.test(space)) throw new Error(\"Invalid profile space\");\n await Promise.all([...makers.keys()].map(async (kind) => (await fileBytesOf(kind))?.dropSpace?.(space)));\n}\n\n/** Tests: forget what was made and which backend comes first. */\nexport function resetFileBytes(order: FileBytesKind[] = [\"opfs\", \"idb\"]): void {\n made.clear();\n preferred = order;\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hyperdhtRelay-C1p0nkle.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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"idb-CTF6LF5B.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 = 15;\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/** The wallet history's index by (mint, time): what `newestWalletTx` reads. */\nexport const WALLET_TX_INDEX = \"byMintTime\";\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 // v15: the wallet history by mint and time, so a refresh reads each network's newest movements without the rest.\n const walletTx = request.transaction!.objectStore(STORES.walletTx);\n if (!walletTx.indexNames.contains(WALLET_TX_INDEX)) walletTx.createIndex(WALLET_TX_INDEX, [\"mint\", \"timestamp\"]);\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 services, 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/**\n * The `count` newest wallet movements of the mints `wanted` picks, newest first. Read backwards on the history index, a\n * mint at a time: a mint not wanted, or one whose `count` newest are read, is stepped over at once. What it reads grows\n * with the number of mints, never with the history's length.\n */\nexport async function newestWalletTx<T extends { timestamp: number }>(wanted: (mint: string) => boolean, count: number): Promise<T[]> {\n const index = (await store(STORES.walletTx, \"readonly\")).index(WALLET_TX_INDEX);\n const rows: T[] = [];\n await new Promise<void>((resolve, reject) => {\n let mint: string | undefined, taken = 0;\n const read = (below: IDBKeyRange | null) => {\n const request = index.openCursor(below, \"prev\");\n request.onerror = () => reject(request.error);\n request.onsuccess = () => {\n const cursor = request.result;\n if (!cursor) { resolve(); return; }\n const [at] = cursor.key as [string, number];\n if (at !== mint) { mint = at; taken = 0; }\n // A new cursor below every key of this mint starts at the next mint's newest movement. Not `continue(key)`:\n // fake-indexeddb (the CLI's and Desktop-headless' IndexedDB) walks every key that skips.\n if (taken >= count || !wanted(at)) { read(IDBKeyRange.upperBound([at, -Infinity], true)); return; }\n rows.push(cursor.value as T);\n taken++;\n cursor.continue();\n };\n };\n read(null);\n });\n return rows.sort((a, b) => b.timestamp - a.timestamp).slice(0, count);\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 /** The id of every group a stored file is of (`linkId` `group:<id>`), read off the index: no record is loaded. */\n async groupIds(): Promise<string[]> {\n const index = (await store(STORES.files, \"readonly\")).index(\"byLink\"), ids: string[] = [];\n await new Promise<void>((resolve, reject) => {\n const request = index.openKeyCursor(IDBKeyRange.bound(\"group:\", \"group:\\uffff\"), \"nextunique\");\n request.onerror = () => reject(request.error);\n request.onsuccess = () => {\n const cursor = request.result;\n if (!cursor) return resolve();\n ids.push(String(cursor.key).slice(\"group:\".length));\n cursor.continue();\n };\n });\n return ids;\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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"mainline-BH6s8HVi.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 readHeldBackFor(pubKeyZ32: string): number | undefined { return this.relays.readHeldBackFor(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,14 +0,0 @@
|
|
|
1
|
-
import "node:module";
|
|
2
|
-
//#region \0rolldown/runtime.js
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __exportAll = (all, no_symbols) => {
|
|
5
|
-
let target = {};
|
|
6
|
-
for (var name in all) __defProp(target, name, {
|
|
7
|
-
get: all[name],
|
|
8
|
-
enumerable: true
|
|
9
|
-
});
|
|
10
|
-
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
11
|
-
return target;
|
|
12
|
-
};
|
|
13
|
-
//#endregion
|
|
14
|
-
export { __exportAll as t };
|