@ghostlytools/cli 1.1.3 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +7 -2
  2. package/dist/assets/argon2id-088uQfnH.js.map +1 -1
  3. package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
  4. package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
  5. package/dist/assets/breez_sdk_spark_wasm_bg.wasm +0 -0
  6. package/dist/assets/fedimint_client_wasm_bg-BtHHlFpp.js.map +1 -1
  7. package/dist/assets/{fileBytes-DxiJHmTk.js → fileBytes-3AQ2tJzW.js} +5 -17
  8. package/dist/assets/{fileBytes-DxiJHmTk.js.map → fileBytes-3AQ2tJzW.js.map} +1 -1
  9. package/dist/assets/{fileBytes-BQ84L9Fe.js → fileBytes-CAZDydIe.js} +2 -2
  10. package/dist/assets/{fileBytes-BQ84L9Fe.js.map → fileBytes-CAZDydIe.js.map} +1 -1
  11. package/dist/assets/{fileBytesIdb-DQkQ10PS.js → fileBytesIdb-CmG00qE-.js} +3 -3
  12. package/dist/assets/{fileBytesIdb-DQkQ10PS.js.map → fileBytesIdb-CmG00qE-.js.map} +1 -1
  13. package/dist/assets/{fileBytesOpfs-BFfR_68b.js → fileBytesOpfs-CTgqt-p3.js} +3 -3
  14. package/dist/assets/{fileBytesOpfs-BFfR_68b.js.map → fileBytesOpfs-CTgqt-p3.js.map} +1 -1
  15. package/dist/assets/{hyperdhtRelay-CHMS7_KO.js → hyperdhtRelay-w9cwVqGZ.js} +3 -3
  16. package/dist/assets/{hyperdhtRelay-CHMS7_KO.js.map → hyperdhtRelay-w9cwVqGZ.js.map} +1 -1
  17. package/dist/assets/{idb-Co2lObpU.js → idb-BIdus0WX.js} +32 -8
  18. package/dist/assets/idb-BIdus0WX.js.map +1 -0
  19. package/dist/assets/joinTrace-CO8mlhNO.js +29 -0
  20. package/dist/assets/joinTrace-CO8mlhNO.js.map +1 -0
  21. package/dist/assets/legacy_ciphers-JJOu0JXL.js.map +1 -1
  22. package/dist/assets/{mainline-B0UTU0-4.js → mainline-BabMhm8L.js} +2 -2
  23. package/dist/assets/{mainline-B0UTU0-4.js.map → mainline-BabMhm8L.js.map} +1 -1
  24. package/dist/assets/nacl-fast-CDhNCYHs.js.map +1 -1
  25. package/dist/assets/noble_curves-DDWn5AH0.js.map +1 -1
  26. package/dist/assets/noble_hashes-B7AZyFyS.js.map +1 -1
  27. package/dist/assets/openpgp-BBFr-zHQ.js.map +1 -1
  28. package/dist/assets/{restoredRows-C1O9FfA4.js → restoredRows-lhTKGmCO.js} +2 -2
  29. package/dist/assets/{restoredRows-C1O9FfA4.js.map → restoredRows-lhTKGmCO.js.map} +1 -1
  30. package/dist/assets/rolldown-runtime-8H4AJuhK.js +14 -0
  31. package/dist/assets/{server-9AM0ta6q.js → server-TamKsfmC.js} +1601 -262
  32. package/dist/assets/server-TamKsfmC.js.map +1 -0
  33. package/dist/assets/sha512-BYId-G-T.js.map +1 -1
  34. package/dist/assets/{src-C_E99PGx.js → src-C6qQYrAH.js} +2190 -207
  35. package/dist/assets/src-C6qQYrAH.js.map +1 -0
  36. package/dist/assets/storage-Bya5b2Xt.js.map +1 -1
  37. package/dist/assets/tree-store-N362nuzZ.js.map +1 -1
  38. package/dist/assets/unbzip2-stream-B1C-JX9L.js.map +1 -1
  39. package/dist/assets/{web-OUQl4Q7L.js → web-DwgdhfcY.js} +16 -16
  40. package/dist/assets/{web-OUQl4Q7L.js.map → web-DwgdhfcY.js.map} +1 -1
  41. package/dist/ghostly.mjs +905 -29
  42. package/dist/ghostly.mjs.map +1 -1
  43. package/package.json +7 -7
  44. package/dist/assets/idb-Co2lObpU.js.map +0 -1
  45. package/dist/assets/server-9AM0ta6q.js.map +0 -1
  46. package/dist/assets/src-C_E99PGx.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"fileBytesOpfs-BFfR_68b.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 /** Shared with the views `forSpace` makes: one worker, one run of request numbers. */\n private readonly counter = { next: 1 };\n /** Another profile's folder (`forSpace`); the active profile's when not set. */\n private fixedSpace?: string;\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 // An error keeps the name the worker gave it: \"no room left\" is told from any other failure by its name.\n if (reply.ok) pending.resolve(reply.value); else pending.reject(reply.name ? Object.assign(new Error(reply.error), { name: reply.name }) : 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 space(): string { return this.fixedSpace ?? fileSpace(); }\n\n /** A view on the same worker that reads and writes another profile's folder. */\n forSpace(space: string): OpfsFileBytes {\n const view = Object.create(this) as OpfsFileBytes;\n view.fixedSpace = space;\n return view;\n }\n\n private call<T>(request: Request, transfer: Transferable[] = [], progress?: (n: number) => void): Promise<T> {\n const n = this.counter.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: this.space() } 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(this.space())).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-CTgqt-p3.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 /** Shared with the views `forSpace` makes: one worker, one run of request numbers. */\n private readonly counter = { next: 1 };\n /** Another profile's folder (`forSpace`); the active profile's when not set. */\n private fixedSpace?: string;\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 // An error keeps the name the worker gave it: \"no room left\" is told from any other failure by its name.\n if (reply.ok) pending.resolve(reply.value); else pending.reject(reply.name ? Object.assign(new Error(reply.error), { name: reply.name }) : 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 space(): string { return this.fixedSpace ?? fileSpace(); }\n\n /** A view on the same worker that reads and writes another profile's folder. */\n forSpace(space: string): OpfsFileBytes {\n const view = Object.create(this) as OpfsFileBytes;\n view.fixedSpace = space;\n return view;\n }\n\n private call<T>(request: Request, transfer: Transferable[] = [], progress?: (n: number) => void): Promise<T> {\n const n = this.counter.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: this.space() } 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(this.space())).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 { Qs as fromBase64Url } from "./src-C_E99PGx.js";
2
- import { n as hyperdhtRelayProblem } from "./server-9AM0ta6q.js";
1
+ import { Dc as fromBase64Url } from "./src-C6qQYrAH.js";
2
+ import { n as hyperdhtRelayProblem } from "./server-TamKsfmC.js";
3
3
  import { ed25519 } from "@noble/curves/ed25519.js";
4
4
  //#region ../browser/src/platform/hyperdhtRelay.ts
5
5
  /**
@@ -339,4 +339,4 @@ async function createRelayedHyperEndpoint(seedB64, url, options = {}) {
339
339
  //#endregion
340
340
  export { createRelayedHyperEndpoint };
341
341
 
342
- //# sourceMappingURL=hyperdhtRelay-CHMS7_KO.js.map
342
+ //# sourceMappingURL=hyperdhtRelay-w9cwVqGZ.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"hyperdhtRelay-CHMS7_KO.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-w9cwVqGZ.js","names":[],"sources":["../../../browser/src/platform/hyperdhtRelay.ts"],"sourcesContent":["import { ed25519 } from \"@noble/curves/ed25519.js\";\nimport { fromBase64Url, type BoundChannel, type FrameChannel, type NativeEndpoint } from \"@ghostly/core\";\nimport { hyperdhtRelayProblem } from \"../shared/hyperdhtRelay\";\n\nexport { hyperdhtRelayProblem as relayUrlProblem };\n\n/**\n * HyperDHT (WISP 103) for a browser, through a dht-relay: a server that runs the UDP half of HyperDHT and\n * talks to the browser over one WebSocket. It runs non-custodial: the browser keeps its keys, runs the\n * Noise handshake and the encrypted stream itself, and the relay moves handshake messages and ciphertext.\n * The binding (the handshake hash and both keys) is computed here, at the endpoint, as the Desktop does.\n *\n * What the relay sees: this browser's address, the HyperDHT keys it listens on and dials, and when and how\n * much it sends. Not the frames, not the keys' secrets, not the handshake hash.\n *\n * The wire is the Desktop's (native/transports/hyperdht/endpoint.mjs): the same preface, u32 length-prefixed\n * UTF-8 frames of at most 60 KiB, so a browser and a Desktop talk HyperDHT to each other.\n */\n\nconst PREFACE = \"ghostly/paired-chat/1\";\nconst MAX_FRAME = 60 * 1024;\nconst SEND_BUDGET = 120 * 1024;\nconst HANDSHAKE_MS = 20_000;\nconst RELAY_OPEN_MS = 10_000;\nconst LISTEN_MS = 20_000;\nconst MAX_CHANNELS = 2;\n/**\n * How the relay client is always made: never custodial. dht-relay's default is custodial, which sends every\n * secret key to the relay so it can run the handshake and the stream itself. Nothing here takes this from\n * settings or options; test/hyperdhtRelay.test.ts fails if a secret key ever reaches the relay's wire.\n */\nconst NON_CUSTODIAL = Object.freeze({ custodial: false as const });\n\n/**\n * The descriptor a browser's HyperDHT endpoint gives out: its key, that it is reached only through a relay, and\n * which one (the connection panel names its host; a Desktop dialling it ignores both).\n */\nexport interface RelayedHyperDescriptor { publicKey: string; relayed: true; relay: string }\n\n/** The parts of dht-relay and its streams this adapter uses. */\ninterface SecretSocket {\n handshakeHash: Uint8Array | null;\n publicKey: Uint8Array;\n remotePublicKey: Uint8Array;\n opened?: Promise<boolean>;\n destroyed?: boolean;\n write(data: Uint8Array): boolean;\n destroy(error?: Error): void;\n on(event: string, listener: (...args: never[]) => void): unknown;\n once(event: string, listener: (...args: never[]) => void): unknown;\n off(event: string, listener: (...args: never[]) => void): unknown;\n}\ninterface RelayedServer {\n listen(keyPair: KeyPair): Promise<void>;\n close(): Promise<void>;\n}\ninterface RelayedNode {\n ready(): Promise<void>;\n connect(remotePublicKey: Uint8Array, options: { keyPair: KeyPair }): SecretSocket;\n createServer(options: object, onconnection: (socket: SecretSocket) => void): RelayedServer;\n destroy(): Promise<void>;\n}\ninterface KeyPair { publicKey: Uint8Array; secretKey: Uint8Array }\n\nconst hex = (bytes: Uint8Array) => Array.from(bytes, b => b.toString(16).padStart(2, \"0\")).join(\"\");\nconst fromHex = (text: string) => Uint8Array.from(text.match(/../g)!, byte => parseInt(byte, 16));\n\n/** HyperDHT's key pair for a seed: libsodium's layout (the seed, then the public key). */\nexport function hyperKeyPair(seed: Uint8Array): KeyPair {\n if (seed.length !== 32) throw new Error(\"Invalid HyperDHT seed\");\n const publicKey = ed25519.getPublicKey(seed);\n const secretKey = new Uint8Array(64);\n secretKey.set(seed); secretKey.set(publicKey, 32);\n return { publicKey, secretKey };\n}\n\n/**\n * One WebSocket per relay for the whole app, shared by every chat's endpoint (each listens on its own\n * key). It closes when the last endpoint does. When the relay goes away, every endpoint on it says it is\n * unavailable, and the next one to start opens a new connection.\n */\ninterface RelayClient {\n node: RelayedNode;\n endpoints: Set<{ unavailable(): void }>;\n gone: boolean;\n /** Settles when the relay connection closes. */\n closed: Promise<void>;\n}\nconst clients = new Map<string, Promise<RelayClient>>();\n\nasync function relayClient(url: string, createSocket: (url: string) => WebSocket): Promise<RelayClient> {\n const existing = clients.get(url);\n if (existing) {\n const client = await existing.catch(() => null);\n if (client && !client.gone) return client;\n if (clients.get(url) === existing) clients.delete(url);\n }\n const opening = (async () => {\n const [{ default: RelayedDHT }, { default: WebSocketStream }] = await Promise.all([\n // @ts-expect-error dht-relay ships no types: RelayedNode and SecretSocket above describe what this uses.\n import(\"@hyperswarm/dht-relay\"),\n // @ts-expect-error The same package's WebSocket stream, untyped too.\n import(\"@hyperswarm/dht-relay/ws\"),\n ]);\n const socket = createSocket(url);\n socket.binaryType = \"arraybuffer\";\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => { socket.close(); reject(new Error(\"The HyperDHT relay did not answer\")); }, RELAY_OPEN_MS);\n socket.addEventListener(\"open\", () => { clearTimeout(timer); resolve(); }, { once: true });\n socket.addEventListener(\"error\", () => { clearTimeout(timer); reject(new Error(\"Could not reach the HyperDHT relay\")); }, { once: true });\n socket.addEventListener(\"close\", () => { clearTimeout(timer); reject(new Error(\"The HyperDHT relay closed the connection\")); }, { once: true });\n });\n // Non-custodial: the relay is told public keys only, never a secret key (dht-relay's default sends them).\n // The node's own key is never used to listen or dial: a random one says nothing about this app.\n const node = new RelayedDHT(new WebSocketStream(true, socket), { ...NON_CUSTODIAL, keyPair: hyperKeyPair(crypto.getRandomValues(new Uint8Array(32))) }) as RelayedNode;\n let closed!: () => void;\n const client: RelayClient = { node, endpoints: new Set(), gone: false, closed: new Promise(resolve => { closed = resolve; }) };\n socket.addEventListener(\"close\", () => {\n client.gone = true; closed();\n if (clients.get(url) === opening) clients.delete(url);\n for (const endpoint of [...client.endpoints]) endpoint.unavailable();\n });\n return client;\n })();\n clients.set(url, opening);\n opening.catch(() => { if (clients.get(url) === opening) clients.delete(url); });\n return opening;\n}\n\nfunction release(url: string, client: RelayClient, endpoint: { unavailable(): void }): void {\n client.endpoints.delete(endpoint);\n if (client.endpoints.size || client.gone) return;\n client.gone = true;\n void clients.get(url)?.then(current => { if (current === client) clients.delete(url); }).catch(() => {});\n void client.node.destroy().catch(() => {});\n}\n\n/** One authenticated stream as a frame channel, with a bounded queue before the session installs its reader. */\nclass RelayedChannel implements FrameChannel {\n private reader: FrameChannel[\"onMessage\"] = null;\n private buffered: string[] = [];\n private closed = false;\n private pendingBytes = 0;\n onClose: FrameChannel[\"onClose\"] = null;\n constructor(private readonly socket: SecretSocket) {\n socket.on(\"drain\", () => { this.pendingBytes = 0; });\n }\n get bufferedAmount(): number { return this.pendingBytes; }\n readonly sendBudget = SEND_BUDGET;\n get onMessage() { return this.reader; }\n set onMessage(reader: FrameChannel[\"onMessage\"]) {\n this.reader = reader;\n if (reader) for (const text of this.buffered.splice(0)) reader(text);\n }\n receive(text: string): boolean {\n if (this.closed) return true;\n if (this.reader) this.reader(text);\n else if (this.buffered.length < 64) this.buffered.push(text);\n else return false;\n return true;\n }\n send(data: string | Uint8Array): void {\n if (typeof data !== \"string\" || this.closed || !data.length) throw new Error(\"Native channel is closed or exceeds its frame budget\");\n const payload = new TextEncoder().encode(data);\n if (payload.length > MAX_FRAME || this.pendingBytes + payload.length > SEND_BUDGET) throw new Error(\"Native channel is closed or exceeds its frame budget\");\n // The stream takes every write; past its high-water mark it says so until it drains.\n if (!writeFrame(this.socket, payload) || this.pendingBytes) this.pendingBytes += payload.length + 4;\n }\n drained(): Promise<void> {\n if (!this.pendingBytes) return Promise.resolve();\n return new Promise((resolve, reject) => {\n const done = () => { this.socket.off(\"close\", gone); resolve(); };\n const gone = () => { this.socket.off(\"drain\", done); reject(new Error(\"Native channel closed\")); };\n this.socket.once(\"drain\", done); this.socket.once(\"close\", gone);\n });\n }\n closedByPeer(): void {\n if (this.closed) return;\n this.closed = true; this.buffered = [];\n this.onClose?.();\n }\n close(): void {\n if (this.closed) return;\n this.socket.destroy();\n this.closedByPeer();\n }\n}\n\nfunction writeFrame(socket: SecretSocket, payload: Uint8Array): boolean {\n const frame = new Uint8Array(4 + payload.length);\n new DataView(frame.buffer).setUint32(0, payload.length);\n frame.set(payload, 4);\n return socket.write(frame);\n}\n\n/**\n * Waits for the peer's preface on an authenticated stream (sending ours as soon as the handshake is\n * done), then hands the channel over with the binding the stream itself gives. A frame of the wrong size,\n * bytes that are not UTF-8 or another protocol close the stream.\n */\nfunction attach(socket: SecretSocket, channels: Set<RelayedChannel>): Promise<BoundChannel> {\n if (channels.size >= MAX_CHANNELS) { socket.destroy(); return Promise.reject(new Error(\"Native connection limit reached\")); }\n const channel = new RelayedChannel(socket);\n channels.add(channel);\n return new Promise<BoundChannel>((resolve, reject) => {\n let buffer = new Uint8Array(0);\n let prefaced = false;\n const decoder = new TextDecoder(\"utf-8\", { fatal: true });\n const timer = setTimeout(() => socket.destroy(new Error(\"HyperDHT handshake timed out\")), HANDSHAKE_MS);\n const fail = (error: Error) => { socket.destroy(error); };\n socket.on(\"error\", (error: Error) => reject(error));\n socket.on(\"close\", () => {\n clearTimeout(timer); channels.delete(channel);\n reject(new Error(\"Native channel closed\"));\n channel.closedByPeer();\n });\n const opened = () => {\n if (socket.handshakeHash?.length !== 64 || socket.publicKey?.length !== 32 || socket.remotePublicKey?.length !== 32) {\n fail(new Error(\"Noise session binding unavailable\")); return;\n }\n writeFrame(socket, new TextEncoder().encode(PREFACE));\n };\n if (socket.opened) void socket.opened.then(ok => { if (ok) opened(); else reject(new Error(\"Noise handshake failed\")); });\n else socket.once(\"open\", opened);\n socket.on(\"data\", (chunk: Uint8Array) => {\n const joined = new Uint8Array(buffer.length + chunk.length);\n joined.set(buffer); joined.set(chunk, buffer.length);\n buffer = joined;\n while (buffer.length >= 4) {\n const length = new DataView(buffer.buffer, buffer.byteOffset, 4).getUint32(0);\n if (length === 0 || length > MAX_FRAME) { fail(new Error(\"Invalid frame length\")); return; }\n if (buffer.length < 4 + length) break;\n let text: string;\n try { text = decoder.decode(buffer.subarray(4, 4 + length)); }\n catch { fail(new Error(\"Invalid UTF-8 frame\")); return; }\n buffer = buffer.slice(4 + length);\n if (!prefaced) {\n if (text !== PREFACE) { fail(new Error(\"Unexpected application protocol\")); return; }\n prefaced = true; clearTimeout(timer);\n resolve({ channel, binding: { transport: \"hyperdht/1\", context: hex(socket.handshakeHash!),\n identities: [hex(socket.publicKey), hex(socket.remotePublicKey)].sort() as [string, string] } });\n } else if (!channel.receive(text)) { fail(new Error(\"Receive queue exceeded\")); return; }\n }\n });\n });\n}\n\nexport interface RelayedHyperOptions {\n /** How to open the WebSocket (tests pass Node's `ws`). Default: the browser's. */\n createSocket?: (url: string) => WebSocket;\n}\n\n/**\n * A chat's HyperDHT endpoint through the relay at `url`: it listens on the key of `seedB64` (the chat's\n * own transport seed, as on the Desktop) and dials a contact's descriptor. Fails, and so is not offered,\n * when the relay cannot be reached or does not let it listen.\n */\nexport async function createRelayedHyperEndpoint(seedB64: string, url: string, options: RelayedHyperOptions = {}): Promise<NativeEndpoint> {\n const problem = hyperdhtRelayProblem(url);\n if (problem) throw new Error(problem);\n const keyPair = hyperKeyPair(fromBase64Url(seedB64));\n const client = await relayClient(url, options.createSocket ?? (address => new WebSocket(address)));\n const channels = new Set<RelayedChannel>();\n let stopped = false;\n let handler: NativeEndpoint[\"onConnection\"] = null;\n const waiting: BoundChannel[] = [];\n let endpoint: NativeEndpoint | null = null;\n const member = {\n unavailable() {\n if (stopped) return;\n stopped = true;\n for (const channel of channels) channel.close();\n endpoint?.onUnavailable?.();\n },\n };\n client.endpoints.add(member);\n const server = client.node.createServer({}, socket => {\n void attach(socket, channels).then(bound => {\n if (stopped) bound.channel.close();\n else if (handler) handler(bound);\n else if (waiting.length < MAX_CHANNELS) waiting.push(bound);\n else bound.channel.close();\n }).catch(() => {});\n });\n try {\n await Promise.race([\n server.listen(keyPair),\n client.closed.then(() => { throw new Error(\"The HyperDHT relay closed the connection\"); }),\n new Promise((_, reject) => setTimeout(() => reject(new Error(\"The HyperDHT relay did not let this chat listen\")), LISTEN_MS)),\n ]);\n if (client.gone) throw new Error(\"The HyperDHT relay closed the connection\");\n } catch (error) {\n stopped = true;\n void server.close().catch(() => {});\n release(url, client, member);\n throw error;\n }\n const descriptor: RelayedHyperDescriptor = { publicKey: hex(keyPair.publicKey), relayed: true, relay: url };\n endpoint = {\n transport: \"hyperdht/1\", descriptor, onDescriptor: null, onUnavailable: null,\n get onConnection() { return handler; },\n set onConnection(value) { handler = value; if (value) for (const bound of waiting.splice(0)) value(bound); },\n async connect(address) {\n const publicKey = (address as { publicKey?: unknown } | null)?.publicKey;\n if (stopped || typeof publicKey !== \"string\" || !/^[a-f0-9]{64}$/.test(publicKey)) throw new Error(\"Invalid HyperDHT endpoint\");\n return attach(client.node.connect(fromHex(publicKey), { keyPair }), channels);\n },\n async close() {\n stopped = true;\n for (const channel of channels) channel.close();\n waiting.length = 0;\n await server.close().catch(() => {});\n release(url, client, member);\n },\n };\n return endpoint;\n}\n"],"mappings":"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"}
@@ -35,7 +35,7 @@ function profileOpenFailure(error) {
35
35
  const detail = error instanceof Error ? error.message : String(error);
36
36
  return {
37
37
  reason: "failed",
38
- supportedVersion: 13,
38
+ supportedVersion: 14,
39
39
  ...detail ? { detail } : {}
40
40
  };
41
41
  }
@@ -47,7 +47,7 @@ function failureOf(error) {
47
47
  const detail = [typeof name === "string" ? name : "", typeof message === "string" ? message : ""].filter(Boolean).join(": ");
48
48
  return {
49
49
  reason: name === "VersionError" ? "newer" : name === "QuotaExceededError" ? "full" : name === "BlockedError" ? "blocked" : name === "SecurityError" || name === "InvalidStateError" || name === "ReferenceError" || name === "TypeError" ? "denied" : "failed",
50
- supportedVersion: 13,
50
+ supportedVersion: 14,
51
51
  ...detail ? { detail } : {}
52
52
  };
53
53
  }
@@ -99,8 +99,18 @@ var STORES = {
99
99
  * What changes about a stored file after it is stored, by file id. A `files` record is written once and never
100
100
  * again: WebKit loses the Blob of a record written back with the Blob it read (see `fileStore`).
101
101
  */
102
- fileState: "fileState"
102
+ fileState: "fileState",
103
+ /** Installed mini-apps (WISP 1200 § Updates and rollback), by app reference: `InstalledApp` (`engine/apps.ts`). */
104
+ apps: "apps",
105
+ /** The app stores the person added, by store key: `AddedAppStore`. A store is its key; its URL is kept on the record. */
106
+ appStores: "appStores",
107
+ /** Each installed app's own storage, by (app reference, scope, key): `AppStorageRow`. Scope: a chat's link id, or `alone`. */
108
+ appStorage: "appStorage"
103
109
  };
110
+ /** The app storage store's index by (app reference, scope): one scope's rows, for its 5 MiB cap and its keys. */
111
+ var APP_STORAGE_SCOPE_INDEX = "byAppScope";
112
+ /** The app storage store's index by scope: every app's rows of one chat, which go with the chat. */
113
+ var APP_STORAGE_CHAT_INDEX = "byScope";
104
114
  var dbPromise = null;
105
115
  function openDb() {
106
116
  dbPromise ??= new Promise((resolve, reject) => {
@@ -122,7 +132,7 @@ function openDb() {
122
132
  };
123
133
  let request;
124
134
  try {
125
- request = indexedDB.open(dbName, 13);
135
+ request = indexedDB.open(dbName, 14);
126
136
  } catch (error) {
127
137
  fail(error);
128
138
  return;
@@ -148,6 +158,17 @@ function openDb() {
148
158
  if (!messages.indexNames.contains("byLinkTime")) messages.createIndex("byLinkTime", ["linkId", "timestamp"]);
149
159
  if (!messages.indexNames.contains("byCardKind")) messages.createIndex(CARD_INDEX, "card.kind");
150
160
  if (!has(STORES.swaps)) db.createObjectStore(STORES.swaps, { keyPath: "id" });
161
+ if (!has(STORES.apps)) db.createObjectStore(STORES.apps, { keyPath: "ref" });
162
+ if (!has(STORES.appStores)) db.createObjectStore(STORES.appStores, { keyPath: "key" });
163
+ if (!has(STORES.appStorage)) {
164
+ const storage = db.createObjectStore(STORES.appStorage, { keyPath: [
165
+ "ref",
166
+ "scope",
167
+ "key"
168
+ ] });
169
+ storage.createIndex(APP_STORAGE_SCOPE_INDEX, ["ref", "scope"]);
170
+ storage.createIndex(APP_STORAGE_CHAT_INDEX, "scope");
171
+ }
151
172
  };
152
173
  request.onsuccess = () => {
153
174
  if (settled) {
@@ -175,7 +196,7 @@ function openDb() {
175
196
  });
176
197
  return dbPromise;
177
198
  }
178
- /** The stores "Clear all data" empties: chats and their keys, messages, files, shared apps, groups. */
199
+ /** The stores "Clear all data" empties: chats and their keys, messages, files, shared apps, groups, mini-apps and their data. */
179
200
  var PROFILE_STORES = [
180
201
  STORES.links,
181
202
  STORES.messages,
@@ -183,7 +204,10 @@ var PROFILE_STORES = [
183
204
  STORES.fileState,
184
205
  STORES.fileChunks,
185
206
  STORES.services,
186
- STORES.groups
207
+ STORES.groups,
208
+ STORES.apps,
209
+ STORES.appStores,
210
+ STORES.appStorage
187
211
  ];
188
212
  /**
189
213
  * Records of the settings store that belong to the profile, not to a wallet: its DID and its key, its identity proofs
@@ -346,6 +370,6 @@ var fileStore = {
346
370
  }
347
371
  };
348
372
  //#endregion
349
- export { deleteDatabase as a, profileOpenFailure as c, wrap as d, databaseName as i, store as l, STORES as n, fileStore as o, clearProfileStores as r, openDb as s, CARD_INDEX as t, transact as u };
373
+ export { clearProfileStores as a, fileStore as c, store as d, transact as f, STORES as i, openDb as l, APP_STORAGE_SCOPE_INDEX as n, databaseName as o, wrap as p, CARD_INDEX as r, deleteDatabase as s, APP_STORAGE_CHAT_INDEX as t, profileOpenFailure as u };
350
374
 
351
- //# sourceMappingURL=idb-Co2lObpU.js.map
375
+ //# sourceMappingURL=idb-BIdus0WX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idb-BIdus0WX.js","names":[],"sources":["../../../browser/src/shared/idb.ts"],"sourcesContent":["/**\n * The extension's one IndexedDB database. The peer (offscreen document) and\n * the pages share an origin, so both open it: that is also how file contents\n * move between them without squeezing through runtime messages.\n */\nlet dbName = \"ghostly\";\n\n/** Before first use: keeps profiles that share a browser storage area apart (Desktop's GHOSTLY_PROFILE). */\nexport function setDatabaseName(name: string): void {\n dbName = name;\n}\n/** The peer database this page uses: its profile's (WISP 04). */\nexport function databaseName(): string {\n return dbName;\n}\n/** The schema this build reads and writes. A database stored at a higher one is a newer build's: it is never opened. */\nexport const DB_VERSION = 14;\n\n/**\n * Why the profile's database did not open. `newer`: a newer Ghostly stored it (IndexedDB never opens a database below\n * its stored version). `blocked`: another window still holds it open at an older version. `full`: no storage space.\n * `denied`: the browser gives this page no storage (a private window, storage blocked for the site). `failed`:\n * anything else, a damaged database included (browsers do not tell that one apart).\n */\nexport type ProfileOpenReason = \"newer\" | \"blocked\" | \"full\" | \"denied\" | \"failed\";\n\n/** What the pages are told (a plain object: it crosses the extension's port), and what a bot reads in `details`. */\nexport interface ProfileOpenFailure {\n reason: ProfileOpenReason;\n /** The schema version this build reads (`DB_VERSION`). */\n supportedVersion: number;\n /** The version the database is stored at, when it could be read (`newer`). */\n storedVersion?: number;\n /** What the browser said, in its own words. */\n detail?: string;\n}\n\n/** The profile's database did not open. Nothing was changed, and nothing is ever deleted or reset because of it. */\nexport class ProfileOpenError extends Error {\n constructor(readonly failure: ProfileOpenFailure) {\n super(profileOpenMessage(failure));\n this.name = \"ProfileOpenError\";\n }\n}\n\n/** The failure in plain English, for logs, the CLI and anything that shows an engine error as it came. */\nexport function profileOpenMessage(failure: ProfileOpenFailure): string {\n const could = \"Ghostly could not open this profile's data\";\n switch (failure.reason) {\n case \"newer\":\n return `This profile was last used by a newer version of Ghostly (its data is version ${failure.storedVersion ?? \"above \" + failure.supportedVersion}, this version reads up to ${failure.supportedVersion}). Update Ghostly to open it.`;\n case \"blocked\": return `${could}: another Ghostly window or tab still has it open with an older version. Close it and try again.`;\n case \"full\": return `${could}: this device is out of storage space. Free some space and try again.`;\n case \"denied\": return `${could}: storage is not allowed here (a private window, or storage blocked for this site).`;\n default: return `${could}${failure.detail ? `: ${failure.detail}` : \"\"}`;\n }\n}\n\n/** What the start of the engine failed with, as the failure the pages show: a database that did not open, or anything else. */\nexport function profileOpenFailure(error: unknown): ProfileOpenFailure {\n if (error instanceof ProfileOpenError) return error.failure;\n const detail = error instanceof Error ? error.message : String(error);\n return { reason: \"failed\", supportedVersion: DB_VERSION, ...(detail ? { detail } : {}) };\n}\n\n/** How long an open that another window blocks may wait for that window to let go before it is said to be blocked. */\nexport const OPEN_TIMINGS = { blockedMs: 10_000 };\n\nfunction failureOf(error: unknown): ProfileOpenFailure {\n const name = (error as { name?: unknown } | null)?.name;\n const message = (error as { message?: unknown } | null)?.message;\n const detail = [typeof name === \"string\" ? name : \"\", typeof message === \"string\" ? message : \"\"].filter(Boolean).join(\": \");\n const reason: ProfileOpenReason = name === \"VersionError\" ? \"newer\"\n : name === \"QuotaExceededError\" ? \"full\"\n // Firefox's private windows of old, storage blocked for the site, or no IndexedDB at all.\n : name === \"BlockedError\" ? \"blocked\"\n : name === \"SecurityError\" || name === \"InvalidStateError\" || name === \"ReferenceError\" || name === \"TypeError\" ? \"denied\"\n : \"failed\";\n return { reason, supportedVersion: DB_VERSION, ...(detail ? { detail } : {}) };\n}\n\n/**\n * The version a database is stored at, read without changing it: an open that names no version opens what is there.\n * Only for a database known to exist (an open without a version would make a missing one).\n */\nfunction storedVersion(name: string): Promise<number | undefined> {\n return new Promise((resolve) => {\n try {\n const request = indexedDB.open(name);\n // It did not exist after all: leave nothing behind.\n request.onupgradeneeded = () => { try { request.transaction?.abort(); } catch { /* already over */ } };\n request.onsuccess = () => { const { version } = request.result; request.result.close(); resolve(version); };\n request.onerror = request.onblocked = () => resolve(undefined);\n } catch { resolve(undefined); }\n });\n}\n/** The messages store's index of card messages (`card.kind`): what `db.getCardMessages` reads. */\nexport const CARD_INDEX = \"byCardKind\";\n\nexport const STORES = {\n links: \"links\",\n messages: \"messages\",\n services: \"services\",\n settings: \"settings\",\n files: \"files\",\n proofs: \"proofs\",\n payments: \"payments\",\n quotes: \"quotes\",\n walletTx: \"walletTx\",\n melts: \"melts\",\n /** Swaps at a mint that have not finished (`PendingSwap`): what gets their outputs back when an answer is missed. */\n swaps: \"swaps\",\n intents: \"paymentIntents\",\n /** Private groups (WISP 900): membership chain, epoch secrets and my own recent messages, by group id. */\n groups: \"groups\",\n /** Files kept in 1 MiB pieces where the platform has no file system for them (see `fileBytesIdb.ts`). */\n fileChunks: \"fileChunks\",\n /**\n * What changes about a stored file after it is stored, by file id. A `files` record is written once and never\n * again: WebKit loses the Blob of a record written back with the Blob it read (see `fileStore`).\n */\n fileState: \"fileState\",\n /** Installed mini-apps (WISP 1200 § Updates and rollback), by app reference: `InstalledApp` (`engine/apps.ts`). */\n apps: \"apps\",\n /** The app stores the person added, by store key: `AddedAppStore`. A store is its key; its URL is kept on the record. */\n appStores: \"appStores\",\n /** Each installed app's own storage, by (app reference, scope, key): `AppStorageRow`. Scope: a chat's link id, or `alone`. */\n appStorage: \"appStorage\",\n} as const;\n\n/** The app storage store's index by (app reference, scope): one scope's rows, for its 5 MiB cap and its keys. */\nexport const APP_STORAGE_SCOPE_INDEX = \"byAppScope\";\n/** The app storage store's index by scope: every app's rows of one chat, which go with the chat. */\nexport const APP_STORAGE_CHAT_INDEX = \"byScope\";\n\n/**\n * A file's contents. Metadata travels with the chat message; this is only the bytes: in `blob` for a file\n * small enough to hold whole, or in the platform's file storage (`bytes`, see `fileBytes.ts`) under the same id.\n * Ids are `<link id>-out-<random>` for files we send and `<link id>-in-<random>` for\n * files we receive, both chosen here. Files stored before that are `<link id>-<wire id>`\n * without `direction` and are still read under their old id.\n */\nexport interface StoredFile {\n id: string;\n linkId: string;\n blob?: Blob;\n /** Where the bytes are when there is no `blob`. */\n bytes?: import(\"./fileBytes\").FileBytesKind;\n createdAt: number;\n direction?: \"in\" | \"out\";\n /** The id the file had on the data link. */\n wireId?: string;\n digest?: string;\n metadata?: { name: string; size: number; mime: string; timestamp: number; voice?: import(\"@ghostly/core\").VoiceMeta; video?: import(\"@ghostly/core\").VideoMeta; image?: import(\"@ghostly/core\").ImageMeta };\n transfer?: { state: \"transferring\" | \"done\" | \"failed\"; transferred: number; size: number; error?: string };\n /** files/3: the transfer's own record (`@ghostly/core` `FileTransferRecord`), kept so it resumes after a restart. */\n wire3?: import(\"@ghostly/core\").FileTransferRecord;\n /** Its bytes were left out of the light backup this profile was restored from (WISP 05 § Light backups). */\n leftOut?: boolean;\n /**\n * A file the contact said on the DHT floor (WISP 403 § Files), by its wire id: its bubble waits for its offer on the\n * live session, and no byte is here yet. Gone once the offer came (`wireId` and `wire3` then).\n */\n announced?: string;\n}\n\n/** The fields of a stored file that change after it is stored: kept in `STORES.fileState`, read over the record's own. */\ntype FileStateFields = Partial<Pick<StoredFile, \"bytes\" | \"digest\" | \"transfer\" | \"wire3\">>;\ntype FileState = { id: string } & FileStateFields;\n\nlet dbPromise: Promise<IDBDatabase> | null = null;\n\nexport function openDb(): Promise<IDBDatabase> {\n dbPromise ??= new Promise((resolve, reject) => {\n let settled = false;\n let blocked: ReturnType<typeof setTimeout> | undefined;\n // Said once, as what it is (`ProfileOpenError`): the engine does not start, and the pages show why.\n const fail = (error: unknown) => {\n if (settled) return;\n settled = true;\n clearTimeout(blocked);\n const failure = failureOf(error);\n if (failure.reason !== \"newer\") { reject(new ProfileOpenError(failure)); return; }\n void storedVersion(dbName).then((version) => reject(new ProfileOpenError({ ...failure, ...(version !== undefined ? { storedVersion: version } : {}) })));\n };\n let request: IDBOpenDBRequest;\n try {\n request = indexedDB.open(dbName, DB_VERSION);\n } catch (error) {\n fail(error);\n return;\n }\n request.onupgradeneeded = () => {\n const db = request.result;\n const has = (name: string) => db.objectStoreNames.contains(name);\n if (!has(STORES.links)) db.createObjectStore(STORES.links, { keyPath: \"id\" });\n if (!has(STORES.services)) db.createObjectStore(STORES.services, { keyPath: \"id\" });\n if (!has(STORES.settings)) db.createObjectStore(STORES.settings);\n if (!has(STORES.messages)) {\n db.createObjectStore(STORES.messages, { keyPath: [\"linkId\", \"id\"] }).createIndex(\"byLink\", \"linkId\");\n }\n if (!has(STORES.proofs)) db.createObjectStore(STORES.proofs, { keyPath: \"secret\" });\n if (!has(STORES.payments)) db.createObjectStore(STORES.payments, { keyPath: \"id\" });\n if (!has(STORES.quotes)) db.createObjectStore(STORES.quotes, { keyPath: \"quote\" });\n if (!has(STORES.walletTx)) db.createObjectStore(STORES.walletTx, { keyPath: \"id\" });\n // v5: Lightning payments the mint has not settled yet, and the proofs they hold.\n if (!has(STORES.melts)) db.createObjectStore(STORES.melts, { keyPath: \"quote\" });\n if (!has(STORES.intents)) db.createObjectStore(STORES.intents, { keyPath: \"review.id\" });\n if (!has(STORES.files)) {\n db.createObjectStore(STORES.files, { keyPath: \"id\" }).createIndex(\"byLink\", \"linkId\");\n }\n // v7: private groups.\n if (!has(STORES.groups)) db.createObjectStore(STORES.groups, { keyPath: \"id\" });\n // v8: files in pieces, for platforms without the origin-private file system.\n if (!has(STORES.fileChunks)) db.createObjectStore(STORES.fileChunks, { keyPath: [\"id\", \"index\"] });\n // v9: what changes about a stored file, apart from its record (see `fileStore`).\n if (!has(STORES.fileState)) db.createObjectStore(STORES.fileState, { keyPath: \"id\" });\n // v10: a chat's messages in time order, so its latest page is read without the rest.\n const messages = request.transaction!.objectStore(STORES.messages);\n if (!messages.indexNames.contains(\"byLinkTime\")) messages.createIndex(\"byLinkTime\", [\"linkId\", \"timestamp\"]);\n // v11: the messages that carry a status card, by the card's kind, across every chat and group (the Tasks board).\n // A message without a card has no such key and is not in it, so the index is as small as the cards are few.\n if (!messages.indexNames.contains(CARD_INDEX)) messages.createIndex(CARD_INDEX, \"card.kind\");\n // v12: swaps at a mint, written down before the mint is asked.\n if (!has(STORES.swaps)) db.createObjectStore(STORES.swaps, { keyPath: \"id\" });\n // v13: no change of the schema. A build from here can hold a frozen copy of a profile on a standby device (WISP 06\n // § Compatibility and rollout): the step keeps an older build (1.0.3 reads 12) from opening and starting one.\n // v14: mini-apps (WISP 1200): the installed apps, the stores added and each app's storage per scope. Empty until the\n // apps feature is on; the bundles themselves are in file storage under `app-<digest>`, outside every store here.\n if (!has(STORES.apps)) db.createObjectStore(STORES.apps, { keyPath: \"ref\" });\n if (!has(STORES.appStores)) db.createObjectStore(STORES.appStores, { keyPath: \"key\" });\n if (!has(STORES.appStorage)) {\n const storage = db.createObjectStore(STORES.appStorage, { keyPath: [\"ref\", \"scope\", \"key\"] });\n storage.createIndex(APP_STORAGE_SCOPE_INDEX, [\"ref\", \"scope\"]);\n storage.createIndex(APP_STORAGE_CHAT_INDEX, \"scope\");\n }\n };\n request.onsuccess = () => {\n // Opened after it was said to be blocked: nobody uses this connection, and it must not block the next open.\n if (settled) { request.result.close(); return; }\n settled = true;\n clearTimeout(blocked);\n // Let the other context upgrade the schema instead of blocking it.\n request.result.onversionchange = () => {\n request.result.close();\n dbPromise = null;\n };\n resolve(request.result);\n };\n // The event's default aborts nothing here, but an unhandled one is logged as an uncaught error in some browsers.\n request.onerror = (event) => { event.preventDefault?.(); fail(request.error); };\n // Another window holds the database at an older version and has not let go (ours do, on `versionchange`). The\n // open stays pending for as long as that lasts: before, the engine waited with it, silently and for good.\n request.onblocked = () => {\n blocked ??= setTimeout(() => fail({ name: \"BlockedError\", message: \"another connection holds an older version open\" }), OPEN_TIMINGS.blockedMs);\n };\n });\n return dbPromise;\n}\n\n/** The stores \"Clear all data\" empties: chats and their keys, messages, files, shared apps, groups, mini-apps and their data. */\nexport const PROFILE_STORES = [STORES.links, STORES.messages, STORES.files, STORES.fileState, STORES.fileChunks, STORES.services, STORES.groups, STORES.apps, STORES.appStores, STORES.appStorage] as const;\n\n/**\n * Records of the settings store that belong to the profile, not to a wallet: its DID and its key, its identity proofs\n * with their keys and revocations, its own Nostr data, the public profiles it read. A list of what goes rather than of\n * what stays, on purpose: every other record of that store is a wallet's (seeds, configuration, pending operations),\n * and a wallet record a newer version adds must never be deleted by mistake.\n */\nexport const PROFILE_SETTINGS_KEYS = [\"profileDid\", \"identityProofs\", \"identityRevocations\", \"nostrSocial\", \"publicProfiles\"] as const;\n\n/**\n * The fields of the peer's `settings` record that are a wallet's, kept by \"Clear all data\": the mint list, the first-run\n * wallet setup and the backup reminders. Everything else in it is the profile's (name, picture, network settings,\n * storage credentials for held items, the push subscription and who holds it) and goes.\n */\nexport const WALLET_SETTINGS_FIELDS = [\"mints\", \"mintsInitialized\", \"walletSetup\", \"backupReminders\"] as const;\n\n/**\n * \"Clear all data\" (WISP 04): everything of the profile in its peer database except its wallets. The chats' stores are\n * emptied, the profile's records of the settings store are deleted and the `settings` record keeps only its wallet\n * fields. The wallets (proofs, payments, quotes, history, payment journal, wallet records) stay: ecash is money, and\n * nothing else holds a copy of it. The caller stops the peer first, or it may write back what was just deleted.\n */\nexport async function clearProfileStores(): Promise<void> {\n if (typeof indexedDB === \"undefined\") return;\n const db = await openDb();\n const names = [...PROFILE_STORES, STORES.settings].filter((name) => db.objectStoreNames.contains(name));\n const tx = db.transaction(names, \"readwrite\");\n for (const name of names) if (name !== STORES.settings) tx.objectStore(name).clear();\n if (names.includes(STORES.settings)) {\n const settings = tx.objectStore(STORES.settings);\n for (const key of PROFILE_SETTINGS_KEYS) settings.delete(key);\n const read = settings.get(\"settings\");\n read.onsuccess = () => {\n const record = read.result as Record<string, unknown> | undefined;\n if (!record || typeof record !== \"object\") return;\n const kept = Object.fromEntries(WALLET_SETTINGS_FIELDS.filter((field) => field in record).map((field) => [field, record[field]]));\n settings.put(kept, \"settings\");\n };\n }\n await new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = tx.onabort = () => reject(tx.error);\n });\n}\n\n/** Deletes a database of the origin by name (an SDK's own), as far as the browser lets it: blocked or failed, it gives up quietly. */\nexport function deleteDatabase(name: string): Promise<void> {\n return new Promise<void>((resolve) => {\n try { const request = indexedDB.deleteDatabase(name); request.onsuccess = request.onerror = request.onblocked = () => resolve(); } catch { resolve(); }\n });\n}\n\nexport function wrap<T>(request: IDBRequest<T>): Promise<T> {\n return new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n}\n\nexport async function store(name: string, mode: IDBTransactionMode): Promise<IDBObjectStore> {\n return (await openDb()).transaction(name, mode).objectStore(name);\n}\n\n/**\n * Several writes across stores in one transaction: they all land, or none does.\n * `work` only queues requests; the promise settles when the transaction does.\n */\nexport async function transact(names: string[], work: (stores: Record<string, IDBObjectStore>) => void): Promise<void> {\n const tx = (await openDb()).transaction(names, \"readwrite\");\n const done = new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onabort = () => reject(tx.error ?? new Error(\"The wallet could not save\"));\n tx.onerror = () => reject(tx.error ?? new Error(\"The wallet could not save\"));\n });\n try {\n work(Object.fromEntries(names.map((name) => [name, tx.objectStore(name)])));\n } catch (error) {\n tx.abort();\n await done.catch(() => {});\n throw error;\n }\n return done;\n}\n\n/** A stored file as it stands: its record, with what changed since it was stored read over it. */\nfunction withState(file: StoredFile, state: FileState | undefined): StoredFile {\n if (!state) return file;\n const { id: _id, ...fields } = state;\n return { ...file, ...fields };\n}\n\n/**\n * The files store. A record is written once, by `put`, and never written back: what changes later (`patch`,\n * `updateTransfer`) goes to `STORES.fileState` and is read over it. WebKit (Safari, the macOS app) keeps a stored\n * Blob as a file the record points to, and a record written back with the Blob it read can lose that file: from\n * then on every read of it fails with \"The object can not be found here.\" For the same reason a Blob read from\n * here is never put here again, under any id (a forward copies the bytes into a Blob of its own).\n */\nexport const fileStore = {\n /** Stores a file anew: whatever changed about an earlier file under this id is forgotten. */\n async put(file: StoredFile): Promise<void> {\n await transact([STORES.files, STORES.fileState], stores => {\n stores[STORES.files].put(file);\n stores[STORES.fileState].delete(file.id);\n });\n },\n async updateTransfer(id: string, transfer: NonNullable<StoredFile[\"transfer\"]>): Promise<void> {\n await fileStore.patch(id, { transfer });\n },\n /** Changes some fields of a stored file (never its record, see above); nothing happens to one that was deleted. */\n async patch(id: string, fields: FileStateFields): Promise<void> {\n await transact([STORES.files, STORES.fileState], stores => {\n const exists = stores[STORES.files].getKey(id);\n exists.onsuccess = () => {\n if (exists.result === undefined) return;\n const state = stores[STORES.fileState].get(id);\n state.onsuccess = () => { stores[STORES.fileState].put({ ...(state.result as FileState | undefined), ...fields, id } satisfies FileState); };\n };\n });\n },\n async get(id: string): Promise<StoredFile | undefined> {\n const tx = (await openDb()).transaction([STORES.files, STORES.fileState], \"readonly\");\n const [file, state] = await Promise.all([\n wrap<StoredFile | undefined>(tx.objectStore(STORES.files).get(id)),\n wrap<FileState | undefined>(tx.objectStore(STORES.fileState).get(id)),\n ]);\n return file && withState(file, state);\n },\n async listForLink(linkId: string): Promise<StoredFile[]> {\n const tx = (await openDb()).transaction([STORES.files, STORES.fileState], \"readonly\");\n const files = await wrap<StoredFile[]>(tx.objectStore(STORES.files).index(\"byLink\").getAll(linkId));\n const states = await Promise.all(files.map((file) => wrap<FileState | undefined>(tx.objectStore(STORES.fileState).get(file.id))));\n return files.map((file, i) => withState(file, states[i]));\n },\n async delete(id: string): Promise<void> {\n await transact([STORES.files, STORES.fileState], stores => {\n stores[STORES.files].delete(id);\n stores[STORES.fileState].delete(id);\n });\n },\n async deleteForLink(linkId: string): Promise<void> {\n const tx = (await openDb()).transaction([STORES.files, STORES.fileState], \"readwrite\");\n const files = tx.objectStore(STORES.files), state = tx.objectStore(STORES.fileState);\n const keys = await wrap(files.index(\"byLink\").getAllKeys(linkId));\n await Promise.all(keys.flatMap((key) => [wrap(files.delete(key)), wrap(state.delete(key))]));\n },\n};\n"],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
2
+ //#region ../browser/src/engine/joinTrace.ts
3
+ var joinTrace_exports = /* @__PURE__ */ __exportAll({
4
+ setJoinTraceSink: () => setJoinTraceSink,
5
+ traceJoin: () => traceJoin
6
+ });
7
+ var sink = null;
8
+ /** Where the lines go besides the console; `null` removes it. */
9
+ function setJoinTraceSink(next) {
10
+ sink = next;
11
+ }
12
+ function traceJoin(group, step, detail) {
13
+ const console_ = globalThis.__ghostlyJoinTrace;
14
+ if (!console_ && !sink) return;
15
+ const line = JSON.stringify({
16
+ t: Date.now(),
17
+ g: group,
18
+ step,
19
+ ...detail
20
+ });
21
+ if (console_) console.debug(`[ghostly:join] ${line}`);
22
+ try {
23
+ sink?.(line);
24
+ } catch {}
25
+ }
26
+ //#endregion
27
+ export { traceJoin as n, joinTrace_exports as t };
28
+
29
+ //# sourceMappingURL=joinTrace-CO8mlhNO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"joinTrace-CO8mlhNO.js","names":[],"sources":["../../../browser/src/engine/joinTrace.ts"],"sourcesContent":["/**\n * Timestamps along a join through a group's link, for measuring where the time goes. Silent unless\n * the page sets `globalThis.__ghostlyJoinTrace` (the e2e measurement does): each step is then one\n * `console.debug` line, `[ghostly:join] {\"t\":…,\"g\":…,\"step\":…}`, read back by the test. A host may also\n * install a sink (the CLI writes the lines beside its link trace), which gets every line whether or not\n * the global is set.\n */\ntype JoinTraceSink = (line: string) => void;\nlet sink: JoinTraceSink | null = null;\n\n/** Where the lines go besides the console; `null` removes it. */\nexport function setJoinTraceSink(next: JoinTraceSink | null): void {\n sink = next;\n}\n\nexport function traceJoin(group: string, step: string, detail?: Record<string, unknown>): void {\n const console_ = (globalThis as { __ghostlyJoinTrace?: boolean }).__ghostlyJoinTrace;\n if (!console_ && !sink) return;\n const line = JSON.stringify({ t: Date.now(), g: group, step, ...detail });\n if (console_) console.debug(`[ghostly:join] ${line}`);\n try { sink?.(line); } catch { /* a sink that fails must not fail the join */ }\n}\n"],"mappings":";;;;;;AAQA,IAAI,OAA6B;;AAGjC,SAAgB,iBAAiB,MAAkC;CACjE,OAAO;AACT;AAEA,SAAgB,UAAU,OAAe,MAAc,QAAwC;CAC7F,MAAM,WAAY,WAAgD;CAClE,IAAI,CAAC,YAAY,CAAC,MAAM;CACxB,MAAM,OAAO,KAAK,UAAU;EAAE,GAAG,KAAK,IAAI;EAAG,GAAG;EAAO;EAAM,GAAG;CAAO,CAAC;CACxE,IAAI,UAAU,QAAQ,MAAM,kBAAkB,MAAM;CACpD,IAAI;EAAE,OAAO,IAAI;CAAG,QAAQ,CAAiD;AAC/E"}