@ghostlytools/cli 1.1.2 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  `ghostly` runs **the Ghostly app's own engine** without a screen: the same chats, groups and protocol as the web app,
4
4
  the extension and the Desktop, on Node. A bot keeps a profile online with `ghostly daemon`, reads what happens with
5
5
  `ghostly listen` (JSON lines), and acts with the other commands (JSON answers). The contract behind it, and why it
6
- is built this way, is [WISP 11xx](../../docs/wisps/11xx-headless.md). A guided tour: [docs/CLI.md](../../docs/CLI.md).
6
+ is built this way, is [WISP 1100](../../docs/wisps/1100-headless.md). A guided tour: [docs/CLI.md](../../docs/CLI.md).
7
7
 
8
8
  > Status: phases 1 to 5 (profiles, pictures and backups, invites, one chat, private and community groups with their
9
9
  > admin tools and hubs, the event stream, hooks and agent turns, wallets and payments, files and voice notes, identity
@@ -90,8 +90,8 @@ The rows are in alphabetical order of their first command, and every command of
90
90
 
91
91
  | Command | What it does |
92
92
  |---|---|
93
- | `button press <chat\|group> <message> <button> [--wait none\|sent\|delivered] [--timeout s]` | Press a button of someone else's question, as a tap in the app does (WISP 4xx · Message Buttons): the reply carries the button's label, and the asker hears `button.pressed`. `<message>` is the question's id (its event or `chat history`), `<button>` a button's id from its `card`. Refused (exit 1) like a tap: closed buttons, a second answer past `--once`, a second press within a second. A one-shot in a chat waits until it went out, as `send`. Answers `{chat \| group, messageId, button, label, replyId, delivery \| edges}` |
94
- | `button update <chat\|group> <message> [--chosen <button>] [--close] [--text <text>] [--force] [--wait none\|confirmed\|sent] [--timeout s]` | Answer a question you sent with `send --button` (WISP 4xx · Message Buttons): `--chosen` shows which button won, `--close` takes presses no more, `--text` changes the text (shown as edited; refused if it looks like a secret, unless `--force`); the buttons and, unless given, the text stay. `<message>` is the `messageId` send gave. Answers `{chat \| group, buttons, messageId, card, edits}` |
93
+ | `button press <chat\|group> <message> <button> [--wait none\|sent\|delivered] [--timeout s]` | Press a button of someone else's question, as a tap in the app does (WISP 406 · Message Buttons): the reply carries the button's label, and the asker hears `button.pressed`. `<message>` is the question's id (its event or `chat history`), `<button>` a button's id from its `card`. Refused (exit 1) like a tap: closed buttons, a second answer past `--once`, a second press within a second. A one-shot in a chat waits until it went out, as `send`. Answers `{chat \| group, messageId, button, label, replyId, delivery \| edges}` |
94
+ | `button update <chat\|group> <message> [--chosen <button>] [--close] [--text <text>] [--force] [--wait none\|confirmed\|sent] [--timeout s]` | Answer a question you sent with `send --button` (WISP 406 · Message Buttons): `--chosen` shows which button won, `--close` takes presses no more, `--text` changes the text (shown as edited; refused if it looks like a secret, unless `--force`); the buttons and, unless given, the text stay. `<message>` is the `messageId` send gave. Answers `{chat \| group, buttons, messageId, card, edits}` |
95
95
  | `call auto [on\|off] [--from <chat>]… [--rate n]` | Answer calls by themselves, from anyone or the chats named (kept in the profile) |
96
96
  | `call hangup [<chat\|call>]`, `call list`, `call flush [<chat\|call>]` | Hang up (or decline); calls on now; drop the audio queued and not played yet |
97
97
  | `call pipe [<chat\|call>]` | A call's audio on stdin and stdout, for shell pipelines (sox, ffmpeg) |
@@ -144,13 +144,13 @@ The rows are in alphabetical order of their first command, and every command of
144
144
  | `profile picture <jpeg> \| --clear` | The picture contacts see (a JPEG within 512 px; 128 px is what the app sends) |
145
145
  | `profile show`, `profile set [--name <name>] [--share-profile \| --no-share-profile]` | The name contacts see |
146
146
  | `react <chat> <message> <emoji> [--remove]` | React to a message with one emoji; a new one replaces yours, `--remove` takes it back |
147
- | `routine send <chat\|group> --name <name> --schedule "every day 01:00" [--cron "0 1 * * *"] [--state active\|paused] [--next <time>] [--run ok\|failed\|skipped[:summary]] [--link url]… [--id id] [--json json\|-\|file]`, `routine update <chat\|group> <routine> [fields…]` | A routine card for something a bot runs on a schedule (WISP 4xx · Status Cards): its name, schedule (and cron line, shown only), state, last run and result, next run and up to 10 recent runs. `--run` records a run now as the last one; `--next` takes milliseconds or a date. Updates merge and are paced as `task update`'s. The app shows it, and runs nothing |
147
+ | `routine send <chat\|group> --name <name> --schedule "every day 01:00" [--cron "0 1 * * *"] [--state active\|paused] [--next <time>] [--run ok\|failed\|skipped[:summary]] [--link url]… [--id id] [--json json\|-\|file]`, `routine update <chat\|group> <routine> [fields…]` | A routine card for something a bot runs on a schedule (WISP 405 · Status Cards): its name, schedule (and cron line, shown only), state, last run and result, next run and up to 10 recent runs. `--run` records a run now as the last one; `--next` takes milliseconds or a date. Updates merge and are paced as `task update`'s. The app shows it, and runs nothing |
148
148
  | `send <chat> [text…] [--reply <message>] [--button id:Label]… [--style id=primary\|neutral\|danger]… [--once] [--id id] [--stdin] [--force] [--wait none\|sent\|delivered] [--timeout s]` | Send text (arguments, or stdin); `--reply` quotes a message of the chat. `--button yes:Yes` (1 to 6; the id ends at the first colon; no two labels alike ignoring case and spaces, and no label that is another button's id, else exit 2) puts buttons under the text, which is the question and what older apps show, so say how to answer in words. `--once`: one answer a person. A question that goes on the DHT floor or into a hold (the contact's app closed) reaches them as text alone; the engine sends its buttons again once the chat is live, as an edit of the buttons alone (no `message.edited` for it). The answer adds `buttons` (the question's id, `ask-…` unless `--id`), `card`, and `pressable`: whether the contact's app shows buttons (null until it says). A press comes back as `button.pressed` ([Events](#events)) |
149
149
  | `service add <name> <http://127.0.0.1:port>`, `service share <service> <chat> [--off]`, `service list`, `service remove\|enable` | Share a web app on this machine, per contact |
150
150
  | `service peer <chat>`, `service open <chat> <service> [--port p]`, `service close <chat> <service>` | A contact's app on a loopback port here (daemon). `url` is a link for your browser, `http://<random>.localhost:<port>/.ghostly-open/<token>`: it sets the service's cookie and shows the app on a host name of its own; any other host name, or a request without that cookie, gets 404. Chrome, Edge and curl reach `*.localhost` names by themselves; Safari and Node's `fetch` on macOS may not (add the name to `/etc/hosts` for them) |
151
151
  | `settings get [--show-secret]`, `settings set <key> <json>` | Relays, Iroh relays, the HyperDHT relay, ICE servers, `sendTyping` (false: contacts are never told you type), … |
152
152
  | `status` | The profile, its chats, whether WebRTC and calls run, the last event seq |
153
- | `task send <chat\|group> --title <title> [--status s] [--progress n \| --steps a/b] [--step "…"] [--item state:text]… [--pr-url url --pr-number n --additions n --deletions n --files n --pr-state s --pr-checks c] [--branch b] [--tag label]… [--parent task] [--link url]… [--id id] [--json json\|-\|file]`, `task update <chat\|group> <task> [fields…]` | A task card for a bot's work (WISP 4xx · Status Cards): a title, a status (queued, running, blocked, done, failed, cancelled), progress, the step it is on, up to 20 steps or log lines (`--item done:Codec`; a lowercase word before the colon must be an item state: pending, running, done, failed or skipped) and its pull request, shown as a small card that apps without cards read as text. Optional, and ignored by apps from before them: where the pull request stands (`--pr-state draft`, `open`, `merged` or `closed`; an open one puts the task in the board's Review column) and its checks (`--pr-checks passing`, `failing` or `pending`), up to 3 tags of 24 characters (`--tag core --tag web`; an update's tags replace the card's), and `--parent <task>`, a task of yours in the same chat that this one is a part of (sent first; its parts stack under it on the board). `send` prints the task's id and message; `update` merges the fields given over the card (an update's `--item` list replaces the card's: give all of them) and edits its message, at most once per 2.5 s (sooner updates merge into the next). `--json` takes the whole card; `null` in it removes a field. Only the CLI sends cards: the app shows them |
153
+ | `task send <chat\|group> --title <title> [--status s] [--progress n \| --steps a/b] [--step "…"] [--item state:text]… [--pr-url url --pr-number n --additions n --deletions n --files n --pr-state s --pr-checks c] [--branch b] [--tag label]… [--parent task] [--link url]… [--id id] [--json json\|-\|file]`, `task update <chat\|group> <task> [fields…]` | A task card for a bot's work (WISP 405 · Status Cards): a title, a status (queued, running, blocked, done, failed, cancelled), progress, the step it is on, up to 20 steps or log lines (`--item done:Codec`; a lowercase word before the colon must be an item state: pending, running, done, failed or skipped) and its pull request, shown as a small card that apps without cards read as text. Optional, and ignored by apps from before them: where the pull request stands (`--pr-state draft`, `open`, `merged` or `closed`; an open one puts the task in the board's Review column) and its checks (`--pr-checks passing`, `failing` or `pending`), up to 3 tags of 24 characters (`--tag core --tag web`; an update's tags replace the card's), and `--parent <task>`, a task of yours in the same chat that this one is a part of (sent first; its parts stack under it on the board). `send` prints the task's id and message; `update` merges the fields given over the card (an update's `--item` list replaces the card's: give all of them) and edits its message, at most once per 2.5 s (sooner updates merge into the next). `--json` takes the whole card; `null` in it removes a field. Only the CLI sends cards: the app shows them |
154
154
  | `typing <chat> [--kind typing\|recording\|thinking] [--status "<text>"] [--for s] [--stop]` | Show the contact you are writing, recording or thinking, or a short status line in its place ("Transcribing your audio…", 40 characters, no links): live chats only, it holds 6 s there; `--for s` keeps it on that long (up to 600 s; a one-shot stays that long); a new kind or status shows at once; a message to the chat or `--stop` ends it |
155
155
  | `wallet create <type> [--network testnet] [--provider id] [--value name=value]… [--stdin] [--invite code] [--api-key key]` | A wallet: `cashu`, `lightning` (a card: its source's form in `--value`), `arkade`, `spark` (on Mainnet, `--api-key` is your Breez API key), `bitcoin` (BDK), `fedimint` (`--invite` is the federation's invite code), `usdt`. With `--stdin`, secret fields come as `name=value` lines on stdin (`api-key=…` too), out of `ps` and the shell's history |
156
156
  | `wallet faucet <type>`, `wallet add-mint <url> [--primary]` | Test coins; another Cashu mint |
@@ -290,7 +290,7 @@ commands, `group.created` events and `engine getState` print it as `<hidden>`; `
290
290
  - Reactions (WISP 400 § Reactions): `message.reaction` (a chat) and `group.reaction` (a group) say each change once:
291
291
  `{messageId, by, emoji, removed, mine}`, with `by` `me`, `peer` or a member key, `emoji` "" when taken back, `mine`
292
292
  when the message is this profile's. `chat history` lists each message's `reactions`: `[{by, emoji, at}]`.
293
- - Buttons (WISP 4xx · Message Buttons): a press on a message you sent with `--button` is a reply whose text is the
293
+ - Buttons (WISP 406 · Message Buttons): a press on a message you sent with `--button` is a reply whose text is the
294
294
  button's label (its `message.received` or `group.message`, with `message.press` and `message.replyTo.button`), then
295
295
  `button.pressed`: `{chat | group, messageId, button, label, by, name, replyId, inferred?, untrusted: {name}}`.
296
296
  `messageId` is the question's, `replyId` the press's, `by` the chat (or a group member's key). `name` is the chat's
package/SKILL.md CHANGED
@@ -360,4 +360,4 @@ Every call of the app's engine is reachable: `ghostly engine --list`, then `ghos
360
360
  Not on the CLI yet: Bark wallets, OpenID Connect proofs, and video in calls (voice only). More:
361
361
  [AI agents on Ghostly](https://ghostly.tools/developers/agents), the
362
362
  [CLI guide](https://github.com/MiguelMedeiros/ghostly/blob/main/docs/CLI.md), the
363
- [package README](https://github.com/MiguelMedeiros/ghostly/blob/main/packages/cli/README.md) and WISP 11xx.
363
+ [package README](https://github.com/MiguelMedeiros/ghostly/blob/main/packages/cli/README.md) and WISP 1100.
@@ -1,5 +1,5 @@
1
- import { Qs as fromBase64Url } from "./src-CJdPTl7Q.js";
2
- import { n as hyperdhtRelayProblem } from "./server-vGoe9MET.js";
1
+ import { Qs as fromBase64Url } from "./src-C_E99PGx.js";
2
+ import { n as hyperdhtRelayProblem } from "./server-9AM0ta6q.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-CVLCuqVr.js.map
342
+ //# sourceMappingURL=hyperdhtRelay-CHMS7_KO.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"hyperdhtRelay-CVLCuqVr.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-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,4 +1,4 @@
1
- import { As as RelayTransport, Is as isDiscoveryBudgetError, Us as parseRelayPayload, Vs as newerPacket, qs as publicKeyFromZ32 } from "./src-CJdPTl7Q.js";
1
+ import { As as RelayTransport, Is as isDiscoveryBudgetError, Us as parseRelayPayload, Vs as newerPacket, qs as publicKeyFromZ32 } from "./src-C_E99PGx.js";
2
2
  import { ed25519 } from "@noble/curves/ed25519.js";
3
3
  import { createHash } from "node:crypto";
4
4
  import DHT from "bittorrent-dht";
@@ -187,4 +187,4 @@ var RelaysAndDht = class {
187
187
  //#endregion
188
188
  export { Mainline, RelaysAndDht };
189
189
 
190
- //# sourceMappingURL=mainline-CKFpwXcB.js.map
190
+ //# sourceMappingURL=mainline-B0UTU0-4.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"mainline-CKFpwXcB.js","names":[],"sources":["../../src/runtime/mainline.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport DHT from \"bittorrent-dht\";\nimport { ed25519 } from \"@noble/curves/ed25519.js\";\nimport {\n RelayTransport, isDiscoveryBudgetError, newerPacket, parseRelayPayload, publicKeyFromZ32,\n type DiscoveryStatus, type GhostRecord, type Identity, type PkarrRequestOptions, type PkarrTransport, type RelayTransportOptions, type SignedPacket,\n} from \"@ghostly/core\";\n\n/**\n * Pkarr over the Mainline DHT (BEP44), from Node, as the Desktop reaches it from Rust: the relays are HTTP bridges to\n * this same DHT, so a headless Ghostly that speaks it directly keeps finding its contacts while the relays fail (they\n * answered HTTP 500 for more than ten minutes on 2026-09-27, and a group's edges stayed down all that while).\n *\n * A Pkarr packet is a BEP44 mutable item: the key is the Ed25519 public key, the value the DNS packet, the sequence\n * number its timestamp in microseconds, and the signature the one the relay payload already carries (over\n * `3:seqi<seq>e1:v<len>:<v>`). So one signed payload goes to the relays and to the DHT unchanged.\n */\n\nexport interface MainlineOptions {\n /** `host:port` nodes to start from; the public routers when absent; none with `false` (a testnet's first node). */\n bootstrap?: string[] | false;\n /** Address to bind (a loopback testnet binds 127.0.0.1). */\n host?: string;\n /** How long a lookup may take before it is given up, in ms. */\n timeoutMs?: number;\n}\n\nconst SIGNATURE = 64, TIMESTAMP = 8;\nconst verify = (sig: Uint8Array, message: Uint8Array, key: Uint8Array) => { try { return ed25519.verify(sig, message, key); } catch { return false; } };\n\n/** A DHT node of this process: puts and gets Pkarr packets. */\nexport class Mainline {\n private readonly dht: DHT;\n /** Its UDP socket is bound. */\n readonly listening: Promise<void>;\n private readonly timeoutMs: number;\n\n constructor(options: MainlineOptions = {}) {\n this.timeoutMs = options.timeoutMs ?? 20_000;\n const bootstrap = options.bootstrap === false ? false : options.bootstrap?.length ? options.bootstrap : undefined;\n this.dht = new DHT({ verify, ...(bootstrap !== undefined ? { bootstrap } : {}), ...(options.host ? { host: options.host } : {}) });\n this.listening = new Promise((resolve) => this.dht.listen(0, options.host, () => resolve()));\n // A node of the table that errors must not end the process.\n this.dht.on(\"error\", () => {});\n }\n\n /** Adds a node (a testnet's peers). */\n addNode(host: string, port: number): void { this.dht.addNode({ host, port }); }\n address(): { port: number } { return this.dht.address() as { port: number }; }\n\n /** Puts a relay payload (`<signature><timestamp><DNS packet>`) as the mutable item of `pubKeyZ32`; resolves with the nodes that took it. */\n put(pubKeyZ32: string, payload: Uint8Array): Promise<number> {\n const k = Buffer.from(publicKeyFromZ32(pubKeyZ32));\n const sig = Buffer.from(payload.subarray(0, SIGNATURE));\n const seq = Number(new DataView(payload.buffer, payload.byteOffset + SIGNATURE, TIMESTAMP).getBigUint64(0));\n const v = Buffer.from(payload.subarray(SIGNATURE + TIMESTAMP));\n return this.bounded(new Promise((resolve, reject) => {\n this.dht.put({ k, v, seq, sig }, (error, _hash, nodes) => {\n if (error) reject(error); else if (!nodes) reject(new Error(\"No DHT node took the packet\")); else resolve(nodes);\n });\n }));\n }\n\n /** The newest packet of `pubKeyZ32` the DHT holds, checked; null when nobody has one. */\n get(pubKeyZ32: string): Promise<SignedPacket | null> {\n const k = Buffer.from(publicKeyFromZ32(pubKeyZ32));\n // A mutable item without salt is found under the SHA-1 of its public key (BEP44).\n const target = createHash(\"sha1\").update(k).digest();\n return this.bounded(new Promise((resolve, reject) => {\n this.dht.get(target, { cache: false }, (error, found) => {\n if (error) return reject(error);\n if (!found?.v || !found.sig) return resolve(null);\n const timestamp = new Uint8Array(TIMESTAMP);\n new DataView(timestamp.buffer).setBigUint64(0, BigInt(found.seq));\n const payload = new Uint8Array(SIGNATURE + TIMESTAMP + found.v.length);\n payload.set(found.sig, 0); payload.set(timestamp, SIGNATURE); payload.set(found.v, SIGNATURE + TIMESTAMP);\n try { resolve(parseRelayPayload(pubKeyZ32, payload)); } catch { resolve(null); }\n });\n }));\n }\n\n private bounded<T>(work: Promise<T>): Promise<T> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(\"DHT lookup timed out\")), this.timeoutMs);\n work.then((value) => { clearTimeout(timer); resolve(value); }, (error) => { clearTimeout(timer); reject(error); });\n });\n }\n\n destroy(): Promise<void> { return new Promise((resolve) => this.dht.destroy(() => resolve())); }\n}\n\n/**\n * The relays first, as the web app reaches Pkarr (its contacts read only the relays), and the DHT beside them: every\n * packet also goes to the DHT, and a read goes there when every relay is failing or the read failed for anything but\n * this app's own request budget (that is a wait, not an outage).\n *\n * `pinned`: the relays given are the only ones (`GHOSTLY_PKARR_RELAYS`, a private network): the profile's `relays`\n * setting does not replace them. With no DHT (`dht` null), the relays alone carry every packet.\n */\nexport class RelaysAndDht implements PkarrTransport {\n readonly relays: RelayTransport;\n private lastVia: \"relay\" | \"dht\" = \"relay\";\n\n constructor(private readonly dht: Mainline | null, relays: RelayTransportOptions = {}, private readonly pinned = false) {\n this.relays = new RelayTransport(relays);\n }\n\n async publish(identity: Identity, records: GhostRecord[], options: PkarrRequestOptions = {}): Promise<void> {\n // BEP44 sequence numbers must strictly increase, on the relays and on the DHT alike: one payload for both, dated\n // past every packet put or read under the key (`RelayTransport.publishDated`), whichever clock dated those.\n await this.relays.publishDated(identity, records, (key, payload, _timestamp, conflict) => this.putBoth(key, payload, options, conflict), options);\n }\n\n publishPayload(pubKeyZ32: string, payload: Uint8Array, options: PkarrRequestOptions = {}): Promise<void> {\n return this.putBoth(pubKeyZ32, payload, options);\n }\n\n private async putBoth(pubKeyZ32: string, payload: Uint8Array, options: PkarrRequestOptions, conflict?: (relay: string) => void): Promise<void> {\n if (!this.dht) return this.relays.publishPayload(pubKeyZ32, payload, options, conflict);\n const onDht = this.dht.put(pubKeyZ32, payload);\n onDht.catch(() => {});\n try {\n await this.relays.publishPayload(pubKeyZ32, payload, options, conflict);\n } catch (error) {\n // Held back by this app's budget: the relays take it when it frees (browser contacts read only them).\n if (isDiscoveryBudgetError(error)) throw error;\n // The relays failed: out on the DHT is out (a Pkarr relay reads the DHT for a key it has not got).\n await onDht.catch(() => { throw error; });\n }\n }\n\n async resolve(pubKeyZ32: string, options: PkarrRequestOptions = {}): Promise<SignedPacket | null> {\n if (!this.dht) return this.relays.resolve(pubKeyZ32, options);\n let fromRelays: SignedPacket | null = null, failed: unknown = null;\n // Every relay left alone for failing: the relays answer from what they last saw, if anything; the DHT is asked.\n const down = () => this.relays.discovery().relays.every((relay) => relay.state === \"failing\");\n if (!down()) {\n try { fromRelays = await this.relays.resolve(pubKeyZ32, options); this.lastVia = \"relay\"; } catch (error) {\n if (isDiscoveryBudgetError(error)) throw error;\n failed = error;\n }\n if (!failed && !down()) return fromRelays;\n }\n try {\n const fromDht = await this.dht.get(pubKeyZ32);\n this.lastVia = \"dht\";\n if (fromDht) this.relays.sawTimestamp(pubKeyZ32, fromDht.timestampMicros);\n return newerPacket(fromRelays, fromDht);\n } catch (error) {\n if (fromRelays) return fromRelays;\n throw failed ?? error;\n }\n }\n\n describe(): { protocol: string; relays: string[] } {\n return { protocol: this.dht ? \"Pkarr relays (HTTP), and the Mainline DHT (BEP44) directly\" : \"Pkarr relays (HTTP)\", relays: this.relays.describe().relays };\n }\n discovery(): DiscoveryStatus {\n const status = this.relays.discovery();\n return this.lastVia === \"dht\" ? { ...status, path: { via: \"dht\" } } : status;\n }\n subscribe(listener: Parameters<RelayTransport[\"subscribe\"]>[0]): () => void { return this.relays.subscribe(listener); }\n /** A relay's answer only: a packet the DHT hands back when the relays are down may be older than one already read. */\n readAnsweredAt(pubKeyZ32: string): number | undefined { return this.relays.readAnsweredAt(pubKeyZ32); }\n onServerTime(listener: Parameters<RelayTransport[\"onServerTime\"]>[0]): () => void { return this.relays.onServerTime(listener); }\n configure({ relays }: { relays: string[]; readRelays: boolean }): void { if (!this.pinned) this.relays.setRelays(relays); }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mainline-B0UTU0-4.js","names":[],"sources":["../../src/runtime/mainline.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport DHT from \"bittorrent-dht\";\nimport { ed25519 } from \"@noble/curves/ed25519.js\";\nimport {\n RelayTransport, isDiscoveryBudgetError, newerPacket, parseRelayPayload, publicKeyFromZ32,\n type DiscoveryStatus, type GhostRecord, type Identity, type PkarrRequestOptions, type PkarrTransport, type RelayTransportOptions, type SignedPacket,\n} from \"@ghostly/core\";\n\n/**\n * Pkarr over the Mainline DHT (BEP44), from Node, as the Desktop reaches it from Rust: the relays are HTTP bridges to\n * this same DHT, so a headless Ghostly that speaks it directly keeps finding its contacts while the relays fail (they\n * answered HTTP 500 for more than ten minutes on 2026-09-27, and a group's edges stayed down all that while).\n *\n * A Pkarr packet is a BEP44 mutable item: the key is the Ed25519 public key, the value the DNS packet, the sequence\n * number its timestamp in microseconds, and the signature the one the relay payload already carries (over\n * `3:seqi<seq>e1:v<len>:<v>`). So one signed payload goes to the relays and to the DHT unchanged.\n */\n\nexport interface MainlineOptions {\n /** `host:port` nodes to start from; the public routers when absent; none with `false` (a testnet's first node). */\n bootstrap?: string[] | false;\n /** Address to bind (a loopback testnet binds 127.0.0.1). */\n host?: string;\n /** How long a lookup may take before it is given up, in ms. */\n timeoutMs?: number;\n}\n\nconst SIGNATURE = 64, TIMESTAMP = 8;\nconst verify = (sig: Uint8Array, message: Uint8Array, key: Uint8Array) => { try { return ed25519.verify(sig, message, key); } catch { return false; } };\n\n/** A DHT node of this process: puts and gets Pkarr packets. */\nexport class Mainline {\n private readonly dht: DHT;\n /** Its UDP socket is bound. */\n readonly listening: Promise<void>;\n private readonly timeoutMs: number;\n\n constructor(options: MainlineOptions = {}) {\n this.timeoutMs = options.timeoutMs ?? 20_000;\n const bootstrap = options.bootstrap === false ? false : options.bootstrap?.length ? options.bootstrap : undefined;\n this.dht = new DHT({ verify, ...(bootstrap !== undefined ? { bootstrap } : {}), ...(options.host ? { host: options.host } : {}) });\n this.listening = new Promise((resolve) => this.dht.listen(0, options.host, () => resolve()));\n // A node of the table that errors must not end the process.\n this.dht.on(\"error\", () => {});\n }\n\n /** Adds a node (a testnet's peers). */\n addNode(host: string, port: number): void { this.dht.addNode({ host, port }); }\n address(): { port: number } { return this.dht.address() as { port: number }; }\n\n /** Puts a relay payload (`<signature><timestamp><DNS packet>`) as the mutable item of `pubKeyZ32`; resolves with the nodes that took it. */\n put(pubKeyZ32: string, payload: Uint8Array): Promise<number> {\n const k = Buffer.from(publicKeyFromZ32(pubKeyZ32));\n const sig = Buffer.from(payload.subarray(0, SIGNATURE));\n const seq = Number(new DataView(payload.buffer, payload.byteOffset + SIGNATURE, TIMESTAMP).getBigUint64(0));\n const v = Buffer.from(payload.subarray(SIGNATURE + TIMESTAMP));\n return this.bounded(new Promise((resolve, reject) => {\n this.dht.put({ k, v, seq, sig }, (error, _hash, nodes) => {\n if (error) reject(error); else if (!nodes) reject(new Error(\"No DHT node took the packet\")); else resolve(nodes);\n });\n }));\n }\n\n /** The newest packet of `pubKeyZ32` the DHT holds, checked; null when nobody has one. */\n get(pubKeyZ32: string): Promise<SignedPacket | null> {\n const k = Buffer.from(publicKeyFromZ32(pubKeyZ32));\n // A mutable item without salt is found under the SHA-1 of its public key (BEP44).\n const target = createHash(\"sha1\").update(k).digest();\n return this.bounded(new Promise((resolve, reject) => {\n this.dht.get(target, { cache: false }, (error, found) => {\n if (error) return reject(error);\n if (!found?.v || !found.sig) return resolve(null);\n const timestamp = new Uint8Array(TIMESTAMP);\n new DataView(timestamp.buffer).setBigUint64(0, BigInt(found.seq));\n const payload = new Uint8Array(SIGNATURE + TIMESTAMP + found.v.length);\n payload.set(found.sig, 0); payload.set(timestamp, SIGNATURE); payload.set(found.v, SIGNATURE + TIMESTAMP);\n try { resolve(parseRelayPayload(pubKeyZ32, payload)); } catch { resolve(null); }\n });\n }));\n }\n\n private bounded<T>(work: Promise<T>): Promise<T> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(\"DHT lookup timed out\")), this.timeoutMs);\n work.then((value) => { clearTimeout(timer); resolve(value); }, (error) => { clearTimeout(timer); reject(error); });\n });\n }\n\n destroy(): Promise<void> { return new Promise((resolve) => this.dht.destroy(() => resolve())); }\n}\n\n/**\n * The relays first, as the web app reaches Pkarr (its contacts read only the relays), and the DHT beside them: every\n * packet also goes to the DHT, and a read goes there when every relay is failing or the read failed for anything but\n * this app's own request budget (that is a wait, not an outage).\n *\n * `pinned`: the relays given are the only ones (`GHOSTLY_PKARR_RELAYS`, a private network): the profile's `relays`\n * setting does not replace them. With no DHT (`dht` null), the relays alone carry every packet.\n */\nexport class RelaysAndDht implements PkarrTransport {\n readonly relays: RelayTransport;\n private lastVia: \"relay\" | \"dht\" = \"relay\";\n\n constructor(private readonly dht: Mainline | null, relays: RelayTransportOptions = {}, private readonly pinned = false) {\n this.relays = new RelayTransport(relays);\n }\n\n async publish(identity: Identity, records: GhostRecord[], options: PkarrRequestOptions = {}): Promise<void> {\n // BEP44 sequence numbers must strictly increase, on the relays and on the DHT alike: one payload for both, dated\n // past every packet put or read under the key (`RelayTransport.publishDated`), whichever clock dated those.\n await this.relays.publishDated(identity, records, (key, payload, _timestamp, conflict) => this.putBoth(key, payload, options, conflict), options);\n }\n\n publishPayload(pubKeyZ32: string, payload: Uint8Array, options: PkarrRequestOptions = {}): Promise<void> {\n return this.putBoth(pubKeyZ32, payload, options);\n }\n\n private async putBoth(pubKeyZ32: string, payload: Uint8Array, options: PkarrRequestOptions, conflict?: (relay: string) => void): Promise<void> {\n if (!this.dht) return this.relays.publishPayload(pubKeyZ32, payload, options, conflict);\n const onDht = this.dht.put(pubKeyZ32, payload);\n onDht.catch(() => {});\n try {\n await this.relays.publishPayload(pubKeyZ32, payload, options, conflict);\n } catch (error) {\n // Held back by this app's budget: the relays take it when it frees (browser contacts read only them).\n if (isDiscoveryBudgetError(error)) throw error;\n // The relays failed: out on the DHT is out (a Pkarr relay reads the DHT for a key it has not got).\n await onDht.catch(() => { throw error; });\n }\n }\n\n async resolve(pubKeyZ32: string, options: PkarrRequestOptions = {}): Promise<SignedPacket | null> {\n if (!this.dht) return this.relays.resolve(pubKeyZ32, options);\n let fromRelays: SignedPacket | null = null, failed: unknown = null;\n // Every relay left alone for failing: the relays answer from what they last saw, if anything; the DHT is asked.\n const down = () => this.relays.discovery().relays.every((relay) => relay.state === \"failing\");\n if (!down()) {\n try { fromRelays = await this.relays.resolve(pubKeyZ32, options); this.lastVia = \"relay\"; } catch (error) {\n if (isDiscoveryBudgetError(error)) throw error;\n failed = error;\n }\n if (!failed && !down()) return fromRelays;\n }\n try {\n const fromDht = await this.dht.get(pubKeyZ32);\n this.lastVia = \"dht\";\n if (fromDht) this.relays.sawTimestamp(pubKeyZ32, fromDht.timestampMicros);\n return newerPacket(fromRelays, fromDht);\n } catch (error) {\n if (fromRelays) return fromRelays;\n throw failed ?? error;\n }\n }\n\n describe(): { protocol: string; relays: string[] } {\n return { protocol: this.dht ? \"Pkarr relays (HTTP), and the Mainline DHT (BEP44) directly\" : \"Pkarr relays (HTTP)\", relays: this.relays.describe().relays };\n }\n discovery(): DiscoveryStatus {\n const status = this.relays.discovery();\n return this.lastVia === \"dht\" ? { ...status, path: { via: \"dht\" } } : status;\n }\n subscribe(listener: Parameters<RelayTransport[\"subscribe\"]>[0]): () => void { return this.relays.subscribe(listener); }\n /** A relay's answer only: a packet the DHT hands back when the relays are down may be older than one already read. */\n readAnsweredAt(pubKeyZ32: string): number | undefined { return this.relays.readAnsweredAt(pubKeyZ32); }\n onServerTime(listener: Parameters<RelayTransport[\"onServerTime\"]>[0]): () => void { return this.relays.onServerTime(listener); }\n configure({ relays }: { relays: string[]; readRelays: boolean }): void { if (!this.pinned) this.relays.setRelays(relays); }\n}\n"],"mappings":"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"}