@ghostlytools/cli 1.0.0 → 1.0.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.
Files changed (28) hide show
  1. package/README.md +66 -21
  2. package/SKILL.md +99 -18
  3. package/dist/assets/{endpoint-h3cqRyRz.js → endpoint-CaSbSFDw.js} +2 -2
  4. package/dist/assets/{endpoint-h3cqRyRz.js.map → endpoint-CaSbSFDw.js.map} +1 -1
  5. package/dist/assets/{fileBytes-ByS16quP.js → fileBytes-CLv0gryb.js} +2 -2
  6. package/dist/assets/{fileBytes-ByS16quP.js.map → fileBytes-CLv0gryb.js.map} +1 -1
  7. package/dist/assets/{fileBytesIdb-COzoilo9.js → fileBytesIdb-CKCtzMTy.js} +2 -2
  8. package/dist/assets/{fileBytesIdb-COzoilo9.js.map → fileBytesIdb-CKCtzMTy.js.map} +1 -1
  9. package/dist/assets/{fileBytesOpfs-B27wAMUh.js → fileBytesOpfs-By55XVTI.js} +3 -3
  10. package/dist/assets/{fileBytesOpfs-B27wAMUh.js.map → fileBytesOpfs-By55XVTI.js.map} +1 -1
  11. package/dist/assets/{hyperdhtRelay-BKsFpPjr.js → hyperdhtRelay-_rJsllcP.js} +4 -4
  12. package/dist/assets/{hyperdhtRelay-BKsFpPjr.js.map → hyperdhtRelay-_rJsllcP.js.map} +1 -1
  13. package/dist/assets/{mainline-DOncVbJN.js → mainline-B0mUxYPH.js} +12 -5
  14. package/dist/assets/mainline-B0mUxYPH.js.map +1 -0
  15. package/dist/assets/{server-5T5l8hfy.js → server-C-8wbsAX.js} +1833 -319
  16. package/dist/assets/server-C-8wbsAX.js.map +1 -0
  17. package/dist/ghostly.mjs +24088 -564
  18. package/dist/ghostly.mjs.map +1 -1
  19. package/examples/call-echo.mjs +116 -0
  20. package/examples/claude-code-agent.sh +55 -0
  21. package/examples/echo-bot.mjs +21 -0
  22. package/examples/echo-bot.sh +15 -0
  23. package/examples/payment-bot.mjs +59 -0
  24. package/package.json +2 -1
  25. package/dist/assets/mainline-DOncVbJN.js.map +0 -1
  26. package/dist/assets/server-5T5l8hfy.js.map +0 -1
  27. package/dist/assets/src-DptDr2et.js +0 -20971
  28. package/dist/assets/src-DptDr2et.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"fileBytesIdb-COzoilo9.js","names":[],"sources":["../../../browser/src/shared/fileBytesIdb.ts"],"sourcesContent":["import { sha256 } from \"@noble/hashes/sha2.js\";\nimport { STORES, openDb, wrap } from \"./idb\";\nimport { FILE_BYTES_STEP, checkFileId, digestText, type FileBytes } from \"./fileBytes\";\n\n/** One piece of a file. Every piece but the last is exactly `FILE_BYTES_STEP` long. */\ninterface Chunk { id: string; index: number; length: number; data: Blob }\n\n/** The piece being filled, kept in memory until it is full or flushed: at most one step per open file. */\ninterface Tail { index: number; bytes: Uint8Array; length: number; dirty: boolean }\n\nconst STEP = FILE_BYTES_STEP;\n\nasync function chunks(mode: IDBTransactionMode): Promise<IDBObjectStore> {\n return (await openDb()).transaction(STORES.fileChunks, mode).objectStore(STORES.fileChunks);\n}\nconst range = (id: string) => IDBKeyRange.bound([id, 0], [id, Infinity]);\n\n/**\n * Files in IndexedDB, in 1 MiB pieces stored as Blobs (which browsers keep on disk), for platforms without\n * the origin-private file system. Reading one hands out a Blob made of the pieces, so a download never\n * loads the file into memory either.\n */\nexport class IdbFileBytes implements FileBytes {\n readonly kind = \"idb\" as const;\n private readonly tails = new Map<string, Tail>();\n /** Writes to one file run one after another. */\n private readonly queues = new Map<string, Promise<unknown>>();\n\n private serial<T>(id: string, work: () => Promise<T>): Promise<T> {\n const run = (this.queues.get(id) ?? Promise.resolve()).then(work, work);\n const settled = run.catch(() => {});\n this.queues.set(id, settled);\n void settled.then(() => { if (this.queues.get(id) === settled) this.queues.delete(id); });\n return run;\n }\n\n private async list(id: string): Promise<Omit<Chunk, \"data\">[]> {\n const store = await chunks(\"readonly\");\n const keys = (await wrap(store.getAllKeys(range(id)))) as [string, number][];\n if (!keys.length) return [];\n const last = (await wrap(store.get(keys[keys.length - 1]))) as Chunk;\n return keys.map(([, index], i) => ({ id, index, length: i === keys.length - 1 ? last.length : STEP }));\n }\n\n private async put(chunk: Chunk): Promise<void> {\n await wrap((await chunks(\"readwrite\")).put(chunk));\n }\n\n /** The piece being filled, loaded from storage the first time a file is written. */\n private async tail(id: string): Promise<Tail> {\n let tail = this.tails.get(id);\n if (tail) return tail;\n const pieces = await this.list(id);\n const last = pieces[pieces.length - 1];\n tail = { index: 0, bytes: new Uint8Array(STEP), length: 0, dirty: false };\n if (last && last.length < STEP) {\n const stored = (await wrap((await chunks(\"readonly\")).get([id, last.index]))) as Chunk;\n tail.index = last.index;\n tail.bytes.set(new Uint8Array(await stored.data.arrayBuffer()));\n tail.length = last.length;\n } else if (last) tail.index = last.index + 1;\n this.tails.set(id, tail);\n return tail;\n }\n\n private async sizeNow(id: string): Promise<number | null> {\n const tail = this.tails.get(id);\n if (tail) return tail.index * STEP + tail.length;\n const pieces = await this.list(id);\n if (!pieces.length) return null;\n return (pieces.length - 1) * STEP + pieces[pieces.length - 1].length;\n }\n\n private async flushNow(id: string): Promise<void> {\n const tail = this.tails.get(id);\n if (!tail?.dirty) return;\n await this.put({ id, index: tail.index, length: tail.length, data: new Blob([tail.bytes.slice(0, tail.length)]) });\n tail.dirty = false;\n }\n\n async stage(id: string, source: Blob, onProgress?: (copied: number) => void): Promise<string> {\n checkFileId(id);\n await this.remove(id);\n const hash = sha256.create();\n let offset = 0;\n const reader = source.stream().getReader();\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n hash.update(value);\n await this.append(id, offset, value);\n offset += value.length;\n onProgress?.(offset);\n }\n await this.close(id);\n } catch (error) {\n await reader.cancel().catch(() => {});\n await this.remove(id).catch(() => {});\n throw error;\n }\n return digestText(hash.digest());\n }\n\n append(id: string, offset: number, bytes: Uint8Array): Promise<void> {\n checkFileId(id);\n return this.serial(id, async () => {\n const tail = await this.tail(id);\n if (offset !== tail.index * STEP + tail.length) throw new Error(\"File write out of order\");\n let pos = 0;\n while (pos < bytes.length) {\n const take = Math.min(STEP - tail.length, bytes.length - pos);\n tail.bytes.set(bytes.subarray(pos, pos + take), tail.length);\n tail.length += take; pos += take; tail.dirty = true;\n if (tail.length === STEP) {\n await this.flushNow(id);\n tail.index += 1; tail.length = 0;\n }\n }\n });\n }\n\n flush(id: string): Promise<void> {\n return this.serial(id, () => this.flushNow(id));\n }\n\n close(id: string): Promise<void> {\n return this.serial(id, async () => { await this.flushNow(id); this.tails.delete(id); });\n }\n\n size(id: string): Promise<number | null> {\n checkFileId(id);\n return this.serial(id, () => this.sizeNow(id));\n }\n\n truncate(id: string, size: number): Promise<void> {\n checkFileId(id);\n return this.serial(id, async () => {\n await this.flushNow(id);\n this.tails.delete(id);\n const pieces = await this.list(id);\n const keep = Math.floor(size / STEP), rest = size % STEP;\n const store = await chunks(\"readwrite\");\n await Promise.all(pieces.filter((piece) => piece.index > keep || (piece.index === keep && rest === 0))\n .map((piece) => wrap(store.delete([id, piece.index]))));\n const cut = pieces.find((piece) => piece.index === keep);\n if (cut && rest > 0 && cut.length > rest) {\n const stored = (await wrap((await chunks(\"readonly\")).get([id, keep]))) as Chunk;\n await this.put({ id, index: keep, length: rest, data: stored.data.slice(0, rest) });\n }\n });\n }\n\n read(id: string, offset: number, length: number): Promise<Uint8Array> {\n checkFileId(id);\n return this.serial(id, async () => {\n await this.flushNow(id);\n const out: Uint8Array[] = [];\n const store = await chunks(\"readonly\");\n const end = offset + length;\n for (let index = Math.floor(offset / STEP); index * STEP < end; index++) {\n const piece = (await wrap(store.get([id, index]))) as Chunk | undefined;\n if (!piece) break;\n const from = Math.max(0, offset - index * STEP), to = Math.min(piece.length, end - index * STEP);\n if (from < to) out.push(new Uint8Array(await piece.data.slice(from, to).arrayBuffer()));\n if (piece.length < STEP) break;\n }\n if (out.length === 1) return out[0];\n const joined = new Uint8Array(out.reduce((n, part) => n + part.length, 0));\n let pos = 0;\n for (const part of out) { joined.set(part, pos); pos += part.length; }\n return joined;\n });\n }\n\n async digest(id: string): Promise<string> {\n const hash = sha256.create();\n for await (const part of this.pieces(id)) hash.update(new Uint8Array(await part.arrayBuffer()));\n return digestText(hash.digest());\n }\n\n private async *pieces(id: string): AsyncGenerator<Blob> {\n checkFileId(id);\n await this.flush(id);\n const pieces = await this.list(id);\n for (const piece of pieces) {\n const stored = (await wrap((await chunks(\"readonly\")).get([id, piece.index]))) as Chunk | undefined;\n if (stored) yield stored.data;\n }\n }\n\n async blob(id: string, type: string): Promise<Blob | null> {\n const parts: Blob[] = [];\n for await (const part of this.pieces(id)) parts.push(part);\n return parts.length ? new Blob(parts, { type }) : (await this.size(id)) === 0 ? new Blob([], { type }) : null;\n }\n\n remove(id: string): Promise<void> {\n checkFileId(id);\n return this.serial(id, async () => {\n this.tails.delete(id);\n await wrap((await chunks(\"readwrite\")).delete(range(id)));\n });\n }\n\n async removeWhere(prefix: string): Promise<void> {\n for (const id of [...this.tails.keys()]) if (id.startsWith(prefix)) this.tails.delete(id);\n const store = await chunks(\"readwrite\");\n await wrap(prefix ? store.delete(IDBKeyRange.bound([prefix, 0], [`${prefix}￿`, Infinity])) : store.clear());\n }\n\n async room(): Promise<number | null> {\n return storageRoom();\n }\n}\n\n/** What the browser still lets this origin store, or null when it does not say. */\nexport async function storageRoom(): Promise<number | null> {\n try {\n const estimate = await navigator.storage?.estimate?.();\n if (!estimate?.quota) return null;\n return Math.max(0, estimate.quota - (estimate.usage ?? 0));\n } catch { return null; }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fileBytesIdb-CKCtzMTy.js","names":[],"sources":["../../../browser/src/shared/fileBytesIdb.ts"],"sourcesContent":["import { sha256 } from \"@noble/hashes/sha2.js\";\nimport { STORES, openDb, wrap } from \"./idb\";\nimport { FILE_BYTES_STEP, checkFileId, digestText, type FileBytes } from \"./fileBytes\";\n\n/** One piece of a file. Every piece but the last is exactly `FILE_BYTES_STEP` long. */\ninterface Chunk { id: string; index: number; length: number; data: Blob }\n\n/** The piece being filled, kept in memory until it is full or flushed: at most one step per open file. */\ninterface Tail { index: number; bytes: Uint8Array; length: number; dirty: boolean }\n\nconst STEP = FILE_BYTES_STEP;\n\nasync function chunks(mode: IDBTransactionMode): Promise<IDBObjectStore> {\n return (await openDb()).transaction(STORES.fileChunks, mode).objectStore(STORES.fileChunks);\n}\nconst range = (id: string) => IDBKeyRange.bound([id, 0], [id, Infinity]);\n\n/**\n * Files in IndexedDB, in 1 MiB pieces stored as Blobs (which browsers keep on disk), for platforms without\n * the origin-private file system. Reading one hands out a Blob made of the pieces, so a download never\n * loads the file into memory either.\n */\nexport class IdbFileBytes implements FileBytes {\n readonly kind = \"idb\" as const;\n private readonly tails = new Map<string, Tail>();\n /** Writes to one file run one after another. */\n private readonly queues = new Map<string, Promise<unknown>>();\n\n private serial<T>(id: string, work: () => Promise<T>): Promise<T> {\n const run = (this.queues.get(id) ?? Promise.resolve()).then(work, work);\n const settled = run.catch(() => {});\n this.queues.set(id, settled);\n void settled.then(() => { if (this.queues.get(id) === settled) this.queues.delete(id); });\n return run;\n }\n\n private async list(id: string): Promise<Omit<Chunk, \"data\">[]> {\n const store = await chunks(\"readonly\");\n const keys = (await wrap(store.getAllKeys(range(id)))) as [string, number][];\n if (!keys.length) return [];\n const last = (await wrap(store.get(keys[keys.length - 1]))) as Chunk;\n return keys.map(([, index], i) => ({ id, index, length: i === keys.length - 1 ? last.length : STEP }));\n }\n\n private async put(chunk: Chunk): Promise<void> {\n await wrap((await chunks(\"readwrite\")).put(chunk));\n }\n\n /** The piece being filled, loaded from storage the first time a file is written. */\n private async tail(id: string): Promise<Tail> {\n let tail = this.tails.get(id);\n if (tail) return tail;\n const pieces = await this.list(id);\n const last = pieces[pieces.length - 1];\n tail = { index: 0, bytes: new Uint8Array(STEP), length: 0, dirty: false };\n if (last && last.length < STEP) {\n const stored = (await wrap((await chunks(\"readonly\")).get([id, last.index]))) as Chunk;\n tail.index = last.index;\n tail.bytes.set(new Uint8Array(await stored.data.arrayBuffer()));\n tail.length = last.length;\n } else if (last) tail.index = last.index + 1;\n this.tails.set(id, tail);\n return tail;\n }\n\n private async sizeNow(id: string): Promise<number | null> {\n const tail = this.tails.get(id);\n if (tail) return tail.index * STEP + tail.length;\n const pieces = await this.list(id);\n if (!pieces.length) return null;\n return (pieces.length - 1) * STEP + pieces[pieces.length - 1].length;\n }\n\n private async flushNow(id: string): Promise<void> {\n const tail = this.tails.get(id);\n if (!tail?.dirty) return;\n await this.put({ id, index: tail.index, length: tail.length, data: new Blob([tail.bytes.slice(0, tail.length)]) });\n tail.dirty = false;\n }\n\n async stage(id: string, source: Blob, onProgress?: (copied: number) => void): Promise<string> {\n checkFileId(id);\n await this.remove(id);\n const hash = sha256.create();\n let offset = 0;\n const reader = source.stream().getReader();\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n hash.update(value);\n await this.append(id, offset, value);\n offset += value.length;\n onProgress?.(offset);\n }\n await this.close(id);\n } catch (error) {\n await reader.cancel().catch(() => {});\n await this.remove(id).catch(() => {});\n throw error;\n }\n return digestText(hash.digest());\n }\n\n append(id: string, offset: number, bytes: Uint8Array): Promise<void> {\n checkFileId(id);\n return this.serial(id, async () => {\n const tail = await this.tail(id);\n if (offset !== tail.index * STEP + tail.length) throw new Error(\"File write out of order\");\n let pos = 0;\n while (pos < bytes.length) {\n const take = Math.min(STEP - tail.length, bytes.length - pos);\n tail.bytes.set(bytes.subarray(pos, pos + take), tail.length);\n tail.length += take; pos += take; tail.dirty = true;\n if (tail.length === STEP) {\n await this.flushNow(id);\n tail.index += 1; tail.length = 0;\n }\n }\n });\n }\n\n flush(id: string): Promise<void> {\n return this.serial(id, () => this.flushNow(id));\n }\n\n close(id: string): Promise<void> {\n return this.serial(id, async () => { await this.flushNow(id); this.tails.delete(id); });\n }\n\n size(id: string): Promise<number | null> {\n checkFileId(id);\n return this.serial(id, () => this.sizeNow(id));\n }\n\n truncate(id: string, size: number): Promise<void> {\n checkFileId(id);\n return this.serial(id, async () => {\n await this.flushNow(id);\n this.tails.delete(id);\n const pieces = await this.list(id);\n const keep = Math.floor(size / STEP), rest = size % STEP;\n const store = await chunks(\"readwrite\");\n await Promise.all(pieces.filter((piece) => piece.index > keep || (piece.index === keep && rest === 0))\n .map((piece) => wrap(store.delete([id, piece.index]))));\n const cut = pieces.find((piece) => piece.index === keep);\n if (cut && rest > 0 && cut.length > rest) {\n const stored = (await wrap((await chunks(\"readonly\")).get([id, keep]))) as Chunk;\n await this.put({ id, index: keep, length: rest, data: stored.data.slice(0, rest) });\n }\n });\n }\n\n read(id: string, offset: number, length: number): Promise<Uint8Array> {\n checkFileId(id);\n return this.serial(id, async () => {\n await this.flushNow(id);\n const out: Uint8Array[] = [];\n const store = await chunks(\"readonly\");\n const end = offset + length;\n for (let index = Math.floor(offset / STEP); index * STEP < end; index++) {\n const piece = (await wrap(store.get([id, index]))) as Chunk | undefined;\n if (!piece) break;\n const from = Math.max(0, offset - index * STEP), to = Math.min(piece.length, end - index * STEP);\n if (from < to) out.push(new Uint8Array(await piece.data.slice(from, to).arrayBuffer()));\n if (piece.length < STEP) break;\n }\n if (out.length === 1) return out[0];\n const joined = new Uint8Array(out.reduce((n, part) => n + part.length, 0));\n let pos = 0;\n for (const part of out) { joined.set(part, pos); pos += part.length; }\n return joined;\n });\n }\n\n async digest(id: string): Promise<string> {\n const hash = sha256.create();\n for await (const part of this.pieces(id)) hash.update(new Uint8Array(await part.arrayBuffer()));\n return digestText(hash.digest());\n }\n\n private async *pieces(id: string): AsyncGenerator<Blob> {\n checkFileId(id);\n await this.flush(id);\n const pieces = await this.list(id);\n for (const piece of pieces) {\n const stored = (await wrap((await chunks(\"readonly\")).get([id, piece.index]))) as Chunk | undefined;\n if (stored) yield stored.data;\n }\n }\n\n async blob(id: string, type: string): Promise<Blob | null> {\n const parts: Blob[] = [];\n for await (const part of this.pieces(id)) parts.push(part);\n return parts.length ? new Blob(parts, { type }) : (await this.size(id)) === 0 ? new Blob([], { type }) : null;\n }\n\n remove(id: string): Promise<void> {\n checkFileId(id);\n return this.serial(id, async () => {\n this.tails.delete(id);\n await wrap((await chunks(\"readwrite\")).delete(range(id)));\n });\n }\n\n async removeWhere(prefix: string): Promise<void> {\n for (const id of [...this.tails.keys()]) if (id.startsWith(prefix)) this.tails.delete(id);\n const store = await chunks(\"readwrite\");\n await wrap(prefix ? store.delete(IDBKeyRange.bound([prefix, 0], [`${prefix}￿`, Infinity])) : store.clear());\n }\n\n async room(): Promise<number | null> {\n return storageRoom();\n }\n}\n\n/** What the browser still lets this origin store, or null when it does not say. */\nexport async function storageRoom(): Promise<number | null> {\n try {\n const estimate = await navigator.storage?.estimate?.();\n if (!estimate?.quota) return null;\n return Math.max(0, estimate.quota - (estimate.usage ?? 0));\n } catch { return null; }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { _t as fileSpace, mt as checkFileId, yt as FILE_BYTES_STEP } from "../ghostly.mjs";
2
- import { storageRoom } from "./fileBytesIdb-COzoilo9.js";
1
+ import { gt as checkFileId, xt as FILE_BYTES_STEP, yt as fileSpace } from "../ghostly.mjs";
2
+ import { storageRoom } from "./fileBytesIdb-CKCtzMTy.js";
3
3
  //#region ../browser/src/shared/fileBytesOpfs.ts
4
4
  /**
5
5
  * Files in the origin-private file system, written by `fileBytesWorker.ts`. Every page and the peer of one
@@ -142,4 +142,4 @@ var OpfsFileBytes = class OpfsFileBytes {
142
142
  //#endregion
143
143
  export { OpfsFileBytes };
144
144
 
145
- //# sourceMappingURL=fileBytesOpfs-B27wAMUh.js.map
145
+ //# sourceMappingURL=fileBytesOpfs-By55XVTI.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fileBytesOpfs-B27wAMUh.js","names":[],"sources":["../../../browser/src/shared/fileBytesOpfs.ts"],"sourcesContent":["import { FILE_BYTES_STEP, checkFileId, fileSpace, type FileBytes } from \"./fileBytes\";\nimport { storageRoom } from \"./fileBytesIdb\";\nimport type { WorkerReply, WorkerRequest } from \"./fileBytesWorker\";\n\ntype Request = WorkerRequest extends infer R ? R extends unknown ? Omit<R, \"n\" | \"space\"> : never : never;\n\n/**\n * Files in the origin-private file system, written by `fileBytesWorker.ts`. Every page and the peer of one\n * profile see the same folder; a file is written by one of them at a time (the page copies a file it sends,\n * the peer writes one it receives), and read as a Blob only once it is closed.\n */\nexport class OpfsFileBytes implements FileBytes {\n readonly kind = \"opfs\" as const;\n private next = 1;\n private readonly waiting = new Map<number, { resolve(value: unknown): void; reject(error: Error): void; progress?(n: number): void }>();\n\n private constructor(private readonly worker: Worker) {\n worker.onmessage = (event: MessageEvent<WorkerReply>) => {\n const reply = event.data, pending = this.waiting.get(reply.n);\n if (!pending) return;\n if (\"progress\" in reply) return pending.progress?.(reply.progress);\n this.waiting.delete(reply.n);\n if (reply.ok) pending.resolve(reply.value); else pending.reject(new Error(reply.error));\n };\n worker.onerror = (event) => {\n event.preventDefault();\n for (const pending of this.waiting.values()) pending.reject(new Error(`File storage stopped: ${event.message || \"unknown error\"}`));\n this.waiting.clear();\n };\n }\n\n /** The worker, if this browser can run it and gives it sync access handles; null otherwise. */\n static async open(): Promise<OpfsFileBytes | null> {\n if (typeof Worker === \"undefined\" || typeof navigator === \"undefined\" || !navigator.storage?.getDirectory) return null;\n const store = new OpfsFileBytes(new Worker(new URL(\"./fileBytesWorker.ts\", import.meta.url), { type: \"module\" }));\n try {\n if (await store.call<boolean>({ op: \"probe\" })) return store;\n } catch { /* no OPFS here (a private window, an old engine) */ }\n store.worker.terminate();\n return null;\n }\n\n private call<T>(request: Request, transfer: Transferable[] = [], progress?: (n: number) => void): Promise<T> {\n const n = this.next++;\n return new Promise<T>((resolve, reject) => {\n this.waiting.set(n, { resolve: resolve as (value: unknown) => void, reject, progress });\n this.worker.postMessage({ ...request, n, space: fileSpace() } as WorkerRequest, transfer);\n });\n }\n\n stage(id: string, source: Blob, onProgress?: (copied: number) => void): Promise<string> {\n return this.call({ op: \"stage\", id: checkFileId(id), source }, [], onProgress);\n }\n append(id: string, offset: number, bytes: Uint8Array): Promise<void> {\n // A copy goes to the worker: the caller's buffer may be a view into something it still uses.\n const copy = bytes.slice();\n return this.call({ op: \"append\", id: checkFileId(id), offset, bytes: copy }, [copy.buffer]);\n }\n flush(id: string): Promise<void> { return this.call({ op: \"flush\", id: checkFileId(id) }); }\n close(id: string): Promise<void> { return this.call({ op: \"close\", id: checkFileId(id) }); }\n size(id: string): Promise<number | null> { return this.call({ op: \"size\", id: checkFileId(id) }); }\n truncate(id: string, size: number): Promise<void> { return this.call({ op: \"truncate\", id: checkFileId(id), size }); }\n read(id: string, offset: number, length: number): Promise<Uint8Array> {\n return this.call({ op: \"read\", id: checkFileId(id), offset, length: Math.min(length, 16 * FILE_BYTES_STEP) });\n }\n digest(id: string): Promise<string> { return this.call({ op: \"digest\", id: checkFileId(id) }); }\n remove(id: string): Promise<void> { return this.call({ op: \"remove\", id: checkFileId(id) }); }\n removeWhere(prefix: string): Promise<void> { return this.call({ op: \"removeWhere\", prefix }); }\n\n async blob(id: string, type: string): Promise<Blob | null> {\n // Closed first: a file with a sync access handle open cannot be read as a Blob.\n await this.close(id);\n try {\n const root = await (await navigator.storage.getDirectory()).getDirectoryHandle(\"ghostly-files\");\n const file = await (await (await root.getDirectoryHandle(fileSpace())).getFileHandle(checkFileId(id))).getFile();\n // A slice with a type: still the file on disk, never read into memory.\n return file.slice(0, file.size, type);\n } catch { return null; }\n }\n\n room(): Promise<number | null> { return storageRoom(); }\n\n async dropSpace(space: string): Promise<void> {\n try {\n const root = await (await navigator.storage.getDirectory()).getDirectoryHandle(\"ghostly-files\");\n await root.removeEntry(space, { recursive: true });\n } catch (error) { if ((error as { name?: string })?.name !== \"NotFoundError\") throw error; }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fileBytesOpfs-By55XVTI.js","names":[],"sources":["../../../browser/src/shared/fileBytesOpfs.ts"],"sourcesContent":["import { FILE_BYTES_STEP, checkFileId, fileSpace, type FileBytes } from \"./fileBytes\";\nimport { storageRoom } from \"./fileBytesIdb\";\nimport type { WorkerReply, WorkerRequest } from \"./fileBytesWorker\";\n\ntype Request = WorkerRequest extends infer R ? R extends unknown ? Omit<R, \"n\" | \"space\"> : never : never;\n\n/**\n * Files in the origin-private file system, written by `fileBytesWorker.ts`. Every page and the peer of one\n * profile see the same folder; a file is written by one of them at a time (the page copies a file it sends,\n * the peer writes one it receives), and read as a Blob only once it is closed.\n */\nexport class OpfsFileBytes implements FileBytes {\n readonly kind = \"opfs\" as const;\n private next = 1;\n private readonly waiting = new Map<number, { resolve(value: unknown): void; reject(error: Error): void; progress?(n: number): void }>();\n\n private constructor(private readonly worker: Worker) {\n worker.onmessage = (event: MessageEvent<WorkerReply>) => {\n const reply = event.data, pending = this.waiting.get(reply.n);\n if (!pending) return;\n if (\"progress\" in reply) return pending.progress?.(reply.progress);\n this.waiting.delete(reply.n);\n if (reply.ok) pending.resolve(reply.value); else pending.reject(new Error(reply.error));\n };\n worker.onerror = (event) => {\n event.preventDefault();\n for (const pending of this.waiting.values()) pending.reject(new Error(`File storage stopped: ${event.message || \"unknown error\"}`));\n this.waiting.clear();\n };\n }\n\n /** The worker, if this browser can run it and gives it sync access handles; null otherwise. */\n static async open(): Promise<OpfsFileBytes | null> {\n if (typeof Worker === \"undefined\" || typeof navigator === \"undefined\" || !navigator.storage?.getDirectory) return null;\n const store = new OpfsFileBytes(new Worker(new URL(\"./fileBytesWorker.ts\", import.meta.url), { type: \"module\" }));\n try {\n if (await store.call<boolean>({ op: \"probe\" })) return store;\n } catch { /* no OPFS here (a private window, an old engine) */ }\n store.worker.terminate();\n return null;\n }\n\n private call<T>(request: Request, transfer: Transferable[] = [], progress?: (n: number) => void): Promise<T> {\n const n = this.next++;\n return new Promise<T>((resolve, reject) => {\n this.waiting.set(n, { resolve: resolve as (value: unknown) => void, reject, progress });\n this.worker.postMessage({ ...request, n, space: fileSpace() } as WorkerRequest, transfer);\n });\n }\n\n stage(id: string, source: Blob, onProgress?: (copied: number) => void): Promise<string> {\n return this.call({ op: \"stage\", id: checkFileId(id), source }, [], onProgress);\n }\n append(id: string, offset: number, bytes: Uint8Array): Promise<void> {\n // A copy goes to the worker: the caller's buffer may be a view into something it still uses.\n const copy = bytes.slice();\n return this.call({ op: \"append\", id: checkFileId(id), offset, bytes: copy }, [copy.buffer]);\n }\n flush(id: string): Promise<void> { return this.call({ op: \"flush\", id: checkFileId(id) }); }\n close(id: string): Promise<void> { return this.call({ op: \"close\", id: checkFileId(id) }); }\n size(id: string): Promise<number | null> { return this.call({ op: \"size\", id: checkFileId(id) }); }\n truncate(id: string, size: number): Promise<void> { return this.call({ op: \"truncate\", id: checkFileId(id), size }); }\n read(id: string, offset: number, length: number): Promise<Uint8Array> {\n return this.call({ op: \"read\", id: checkFileId(id), offset, length: Math.min(length, 16 * FILE_BYTES_STEP) });\n }\n digest(id: string): Promise<string> { return this.call({ op: \"digest\", id: checkFileId(id) }); }\n remove(id: string): Promise<void> { return this.call({ op: \"remove\", id: checkFileId(id) }); }\n removeWhere(prefix: string): Promise<void> { return this.call({ op: \"removeWhere\", prefix }); }\n\n async blob(id: string, type: string): Promise<Blob | null> {\n // Closed first: a file with a sync access handle open cannot be read as a Blob.\n await this.close(id);\n try {\n const root = await (await navigator.storage.getDirectory()).getDirectoryHandle(\"ghostly-files\");\n const file = await (await (await root.getDirectoryHandle(fileSpace())).getFileHandle(checkFileId(id))).getFile();\n // A slice with a type: still the file on disk, never read into memory.\n return file.slice(0, file.size, type);\n } catch { return null; }\n }\n\n room(): Promise<number | null> { return storageRoom(); }\n\n async dropSpace(space: string): Promise<void> {\n try {\n const root = await (await navigator.storage.getDirectory()).getDirectoryHandle(\"ghostly-files\");\n await root.removeEntry(space, { recursive: true });\n } catch (error) { if ((error as { name?: string })?.name !== \"NotFoundError\") throw error; }\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { Aa as fromBase64Url } from "./src-DptDr2et.js";
2
- import { n as hyperdhtRelayProblem } from "./server-5T5l8hfy.js";
1
+ import { La as fromBase64Url } from "../ghostly.mjs";
2
+ import { n as hyperdhtRelayProblem } from "./server-C-8wbsAX.js";
3
3
  import { ed25519 } from "@noble/curves/ed25519.js";
4
4
  //#region ../browser/src/platform/hyperdhtRelay.ts
5
5
  /**
@@ -11,7 +11,7 @@ import { ed25519 } from "@noble/curves/ed25519.js";
11
11
  * What the relay sees: this browser's address, the HyperDHT keys it listens on and dials, and when and how
12
12
  * much it sends. Not the frames, not the keys' secrets, not the handshake hash.
13
13
  *
14
- * The wire is the Desktop's (native-transports/hyperdht/endpoint.mjs): the same preface, u32 length-prefixed
14
+ * The wire is the Desktop's (native/transports/hyperdht/endpoint.mjs): the same preface, u32 length-prefixed
15
15
  * UTF-8 frames of at most 60 KiB, so a browser and a Desktop talk HyperDHT to each other.
16
16
  */
17
17
  var PREFACE = "ghostly/paired-chat/1";
@@ -339,4 +339,4 @@ async function createRelayedHyperEndpoint(seedB64, url, options = {}) {
339
339
  //#endregion
340
340
  export { createRelayedHyperEndpoint };
341
341
 
342
- //# sourceMappingURL=hyperdhtRelay-BKsFpPjr.js.map
342
+ //# sourceMappingURL=hyperdhtRelay-_rJsllcP.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"hyperdhtRelay-BKsFpPjr.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-_rJsllcP.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,4 +1,4 @@
1
- import { Ea as publicKeyFromZ32, Sa as parseRelayPayload, ba as newerPacket, da as RelayTransport, ha as isDiscoveryBudgetError, ya as createRelayPayload } from "./src-DptDr2et.js";
1
+ import { Aa as parseRelayPayload, Oa as createRelayPayload, Pa as publicKeyFromZ32, Ta as isDiscoveryBudgetError, ka as newerPacket, xa as RelayTransport } from "../ghostly.mjs";
2
2
  import { ed25519 } from "@noble/curves/ed25519.js";
3
3
  import { createHash } from "node:crypto";
4
4
  import DHT from "bittorrent-dht";
@@ -100,14 +100,19 @@ var Mainline = class {
100
100
  * The relays first, as the web app reaches Pkarr (its contacts read only the relays), and the DHT beside them: every
101
101
  * packet also goes to the DHT, and a read goes there when every relay is failing or the read failed for anything but
102
102
  * this app's own request budget (that is a wait, not an outage).
103
+ *
104
+ * `pinned`: the relays given are the only ones (`GHOSTLY_PKARR_RELAYS`, a private network): the profile's `relays`
105
+ * setting does not replace them. With no DHT (`dht` null), the relays alone carry every packet.
103
106
  */
104
107
  var RelaysAndDht = class {
105
108
  dht;
109
+ pinned;
106
110
  relays;
107
111
  lastTimestamp = /* @__PURE__ */ new Map();
108
112
  lastVia = "relay";
109
- constructor(dht, relays = {}) {
113
+ constructor(dht, relays = {}, pinned = false) {
110
114
  this.dht = dht;
115
+ this.pinned = pinned;
111
116
  this.relays = new RelayTransport(relays);
112
117
  }
113
118
  async publish(identity, records, options = {}) {
@@ -117,6 +122,7 @@ var RelaysAndDht = class {
117
122
  await this.publishPayload(identity.pubKeyZ32, createRelayPayload(identity, records, timestamp), options);
118
123
  }
119
124
  async publishPayload(pubKeyZ32, payload, options = {}) {
125
+ if (!this.dht) return this.relays.publishPayload(pubKeyZ32, payload, options);
120
126
  const onDht = this.dht.put(pubKeyZ32, payload);
121
127
  onDht.catch(() => {});
122
128
  try {
@@ -129,6 +135,7 @@ var RelaysAndDht = class {
129
135
  }
130
136
  }
131
137
  async resolve(pubKeyZ32, options = {}) {
138
+ if (!this.dht) return this.relays.resolve(pubKeyZ32, options);
132
139
  let fromRelays = null, failed = null;
133
140
  const down = () => this.relays.discovery().relays.every((relay) => relay.state === "failing");
134
141
  if (!down()) {
@@ -152,7 +159,7 @@ var RelaysAndDht = class {
152
159
  }
153
160
  describe() {
154
161
  return {
155
- protocol: "Pkarr relays (HTTP), and the Mainline DHT (BEP44) directly",
162
+ protocol: this.dht ? "Pkarr relays (HTTP), and the Mainline DHT (BEP44) directly" : "Pkarr relays (HTTP)",
156
163
  relays: this.relays.describe().relays
157
164
  };
158
165
  }
@@ -167,10 +174,10 @@ var RelaysAndDht = class {
167
174
  return this.relays.subscribe(listener);
168
175
  }
169
176
  configure({ relays }) {
170
- this.relays.setRelays(relays);
177
+ if (!this.pinned) this.relays.setRelays(relays);
171
178
  }
172
179
  };
173
180
  //#endregion
174
181
  export { Mainline, RelaysAndDht };
175
182
 
176
- //# sourceMappingURL=mainline-DOncVbJN.js.map
183
+ //# sourceMappingURL=mainline-B0mUxYPH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mainline-B0mUxYPH.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, createRelayPayload, 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 readonly lastTimestamp = new Map<string, bigint>();\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.\n const now = BigInt(Date.now()) * 1000n, last = this.lastTimestamp.get(identity.pubKeyZ32) ?? 0n;\n const timestamp = now > last ? now : last + 1n;\n this.lastTimestamp.set(identity.pubKeyZ32, timestamp);\n await this.publishPayload(identity.pubKeyZ32, createRelayPayload(identity, records, timestamp), options);\n }\n\n async publishPayload(pubKeyZ32: string, payload: Uint8Array, options: PkarrRequestOptions = {}): Promise<void> {\n if (!this.dht) return this.relays.publishPayload(pubKeyZ32, payload, options);\n const onDht = this.dht.put(pubKeyZ32, payload);\n onDht.catch(() => {});\n try {\n await this.relays.publishPayload(pubKeyZ32, payload, options);\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 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 configure({ relays }: { relays: string[]; readRelays: boolean }): void { if (!this.pinned) this.relays.setRelays(relays); }\n}\n"],"mappings":"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"}