@ghostlytools/cli 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +9 -3
  2. package/dist/assets/{api-CrxVxtY1.js → api-Dq2QXQDk.js} +141 -40
  3. package/dist/assets/api-Dq2QXQDk.js.map +1 -0
  4. package/dist/assets/{api-BiaX6l6f.js → api-TiB5QSG9.js} +2 -2
  5. package/dist/assets/{apiKit-CajiDxdK.js → apiKit-CA2x5GJF.js} +7 -3
  6. package/dist/assets/apiKit-CA2x5GJF.js.map +1 -0
  7. package/dist/assets/{apps-CbfiCSIw.js → apps-BxV9i_Y4.js} +3 -4
  8. package/dist/assets/{apps-CbfiCSIw.js.map → apps-BxV9i_Y4.js.map} +1 -1
  9. package/dist/assets/{apps-t23gkc28.js → apps-C-T8jeji.js} +1 -1
  10. package/dist/assets/{arkade-D4BVJFEQ.js → arkade-C_qDmHnm.js} +2 -2
  11. package/dist/assets/{arkade-D4BVJFEQ.js.map → arkade-C_qDmHnm.js.map} +1 -1
  12. package/dist/assets/{backup-Cg52WeQi.js → backup-Bugnn3Ue.js} +1 -1
  13. package/dist/assets/{backup-BAMi2lAq.js → backup-CnMN2oUK.js} +8 -8
  14. package/dist/assets/backup-CnMN2oUK.js.map +1 -0
  15. package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
  16. package/dist/assets/bitcoind-CZchoh43.js +349 -0
  17. package/dist/assets/bitcoind-CZchoh43.js.map +1 -0
  18. package/dist/assets/bitcoindProvider-DGw3xBSa.js +97 -0
  19. package/dist/assets/bitcoindProvider-DGw3xBSa.js.map +1 -0
  20. package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
  21. package/dist/assets/buttons-Bj7Vf7Pn.js +2 -0
  22. package/dist/assets/{buttons-CX1pKB0j.js → buttons-CvNBzh2S.js} +4 -5
  23. package/dist/assets/{buttons-CX1pKB0j.js.map → buttons-CvNBzh2S.js.map} +1 -1
  24. package/dist/assets/{endpoint-Dn7H8qE1.js → endpoint-BOTFMpfS.js} +44 -12
  25. package/dist/assets/endpoint-BOTFMpfS.js.map +1 -0
  26. package/dist/assets/{envelope-BvU0fLEt.js → envelope-WjingYSc.js} +2 -2
  27. package/dist/assets/{envelope-BvU0fLEt.js.map → envelope-WjingYSc.js.map} +1 -1
  28. package/dist/assets/{errors-my6VJR_0.js → errors-DO5z6h_l.js} +29 -1
  29. package/dist/assets/errors-DO5z6h_l.js.map +1 -0
  30. package/dist/assets/groupFiles-IEUW3DQz.js +3202 -0
  31. package/dist/assets/groupFiles-IEUW3DQz.js.map +1 -0
  32. package/dist/assets/{host-Cmd205Dc.js → host-CWt-jtqz.js} +41 -16
  33. package/dist/assets/host-CWt-jtqz.js.map +1 -0
  34. package/dist/assets/{hyperdhtRelay-CEmBpT0Q.js → hyperdhtRelay-CPYwSLLS.js} +2 -3
  35. package/dist/assets/{hyperdhtRelay-CEmBpT0Q.js.map → hyperdhtRelay-CPYwSLLS.js.map} +1 -1
  36. package/dist/assets/{irohWeb--YyEA-hX.js → irohWeb-4oChIOIp.js} +2 -3
  37. package/dist/assets/{irohWeb--YyEA-hX.js.map → irohWeb-4oChIOIp.js.map} +1 -1
  38. package/dist/assets/{main-BzNN6bkx.js → main-DbqcdShN.js} +142 -35
  39. package/dist/assets/main-DbqcdShN.js.map +1 -0
  40. package/dist/assets/{mainline-D5D5C-jT.js → mainline-ZkiOnevs.js} +2 -2
  41. package/dist/assets/{mainline-D5D5C-jT.js.map → mainline-ZkiOnevs.js.map} +1 -1
  42. package/dist/assets/{networkReason-DxZWHw6v.js → networkReason-CetKkTPo.js} +2 -2
  43. package/dist/assets/{networkReason-DxZWHw6v.js.map → networkReason-CetKkTPo.js.map} +1 -1
  44. package/dist/assets/nwc-CgkqyvsE.js +612 -0
  45. package/dist/assets/nwc-CgkqyvsE.js.map +1 -0
  46. package/dist/assets/nwcProvider-4eVz_8HP.js +77 -0
  47. package/dist/assets/nwcProvider-4eVz_8HP.js.map +1 -0
  48. package/dist/assets/{profiles-B6F4oa9K.js → profiles-BHc9pEYg.js} +2 -2
  49. package/dist/assets/{profiles-B6F4oa9K.js.map → profiles-BHc9pEYg.js.map} +1 -1
  50. package/dist/assets/registry-DlAKO0Fy.js +3793 -0
  51. package/dist/assets/registry-DlAKO0Fy.js.map +1 -0
  52. package/dist/assets/registry-OBJBnHVQ.js +2 -0
  53. package/dist/assets/resolve-CouHhCpJ.js +591 -0
  54. package/dist/assets/resolve-CouHhCpJ.js.map +1 -0
  55. package/dist/assets/{server-CBCEYKMN.js → server-Ss0gJ-nw.js} +184 -1181
  56. package/dist/assets/server-Ss0gJ-nw.js.map +1 -0
  57. package/dist/assets/{src-DhfGgAUu.js → src-DDzSERyc.js} +2249 -72
  58. package/dist/assets/src-DDzSERyc.js.map +1 -0
  59. package/dist/assets/{turnRefs-ChEfgbSX.js → turnRefs-B87dtrHM.js} +2 -2
  60. package/dist/assets/{turnRefs-ChEfgbSX.js.map → turnRefs-B87dtrHM.js.map} +1 -1
  61. package/dist/assets/types-C4_KafL_.js +122 -0
  62. package/dist/assets/types-C4_KafL_.js.map +1 -0
  63. package/dist/assets/{usdt-C1Xao2xH.js → usdt-Fj8CQO2z.js} +3 -3
  64. package/dist/assets/{usdt-C1Xao2xH.js.map → usdt-Fj8CQO2z.js.map} +1 -1
  65. package/dist/ghostly.mjs +1 -1
  66. package/package.json +1 -1
  67. package/dist/assets/api-CrxVxtY1.js.map +0 -1
  68. package/dist/assets/apiKit-CajiDxdK.js.map +0 -1
  69. package/dist/assets/backup-BAMi2lAq.js.map +0 -1
  70. package/dist/assets/buttons-CWl2yNJu.js +0 -2
  71. package/dist/assets/endpoint-Dn7H8qE1.js.map +0 -1
  72. package/dist/assets/errors-my6VJR_0.js.map +0 -1
  73. package/dist/assets/groupFiles-BNTSDaoq.js +0 -7685
  74. package/dist/assets/groupFiles-BNTSDaoq.js.map +0 -1
  75. package/dist/assets/groupFiles-DBbjOLuD.js +0 -2088
  76. package/dist/assets/groupFiles-DBbjOLuD.js.map +0 -1
  77. package/dist/assets/host-Cmd205Dc.js.map +0 -1
  78. package/dist/assets/main-BzNN6bkx.js.map +0 -1
  79. package/dist/assets/server-CBCEYKMN.js.map +0 -1
  80. package/dist/assets/src-DhfGgAUu.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bitcoindProvider-DGw3xBSa.js","names":[],"sources":["../../../browser/src/engine/paymentAdapters/providers/bitcoindProvider.ts"],"sourcesContent":["import type { OnchainProviderDescriptor } from \"./onchain\";\nimport type { ProviderSettings } from \"./types\";\n\n/**\n * Bitcoin Core as the source picker lists it, and its settings. The client (`./bitcoind`) and btc-signer's\n * transaction code load when a wallet connects, not with the app.\n */\n\nconst WALLET_NAME = /^[A-Za-z0-9._-]{0,64}$/;\n\n/** `user:password` in the password field (the .cookie file's contents) when no user is given. */\nexport function credentials({ config, secrets }: ProviderSettings) {\n const password = secrets.password ?? \"\";\n const user = config.user?.trim() ?? \"\";\n if (!user && password.includes(\":\")) {\n const at = password.indexOf(\":\");\n return { user: password.slice(0, at), password: password.slice(at + 1) };\n }\n return { user, password };\n}\n\nexport const bitcoindRpc: OnchainProviderDescriptor = {\n id: \"bitcoind\",\n label: \"Bitcoin Core\",\n kind: \"onchain\",\n description: \"A wallet on your own Bitcoin Core node, over its RPC. Your node holds the keys and signs. Needs the desktop app.\",\n networks: [\"bitcoin\", \"testnet\", \"signet\", \"regtest\"],\n platforms: [\"desktop\"],\n experimental: true,\n fields: [\n { name: \"url\", label: \"RPC address\", kind: \"url\", placeholder: \"http://127.0.0.1:8332\", defaults: { mainnet: \"http://127.0.0.1:8332\", testnet: \"http://127.0.0.1:38332\" },\n help: \"Your node's rpcbind and rpcport: 8332 mainnet, 38332 signet, 48332 testnet4, 18443 regtest.\" },\n { name: \"wallet\", label: \"Wallet name\", kind: \"text\", optional: true, placeholder: \"ghostly\", help: \"A wallet loaded in the node (bitcoin-cli listwallets). Empty: its default wallet.\" },\n { name: \"user\", label: \"RPC user\", kind: \"text\", optional: true, help: \"Leave empty to paste the .cookie file below.\" },\n { name: \"password\", label: \"RPC password or cookie\", kind: \"secret\", help: \"rpcpassword, or the contents of the node's .cookie file (it changes when bitcoind restarts).\" },\n ],\n validate(settings) {\n const url = (() => { try { return new URL(settings.config.url?.trim() ?? \"\"); } catch { return undefined; } })();\n if (!url || ![\"http:\", \"https:\"].includes(url.protocol)) throw new Error(\"Enter the node's RPC address, like http://127.0.0.1:8332\");\n if (url.username || url.password) throw new Error(\"Put the RPC user and password in their own fields, not in the address\");\n if (url.search || url.hash) throw new Error(\"The RPC address cannot have a query or fragment\");\n if (!WALLET_NAME.test(settings.config.wallet?.trim() ?? \"\") || [\".\", \"..\"].includes(settings.config.wallet?.trim() ?? \"\")) throw new Error(\"A wallet name has only letters, digits, '-', '_' and '.'\");\n const { user, password } = credentials(settings);\n if (!user || !password) throw new Error(\"Enter the RPC user and password, or paste the .cookie file\");\n },\n async create(settings, host) {\n const { BitcoindOnchain, tauriTransport } = await import(\"./bitcoind\");\n return BitcoindOnchain.connect({ url: settings.config.url.trim(), wallet: settings.config.wallet?.trim() ?? \"\", ...credentials(settings) }, host.mode, tauriTransport(host.invoke), host.signal);\n },\n};\n"],"mappings":";;;;;AAQA,IAAM,cAAc;;AAGpB,SAAgB,YAAY,EAAE,QAAQ,WAA6B;CACjE,MAAM,WAAW,QAAQ,YAAY;CACrC,MAAM,OAAO,OAAO,MAAM,KAAK,KAAK;CACpC,IAAI,CAAC,QAAQ,SAAS,SAAS,GAAG,GAAG;EACnC,MAAM,KAAK,SAAS,QAAQ,GAAG;EAC/B,OAAO;GAAE,MAAM,SAAS,MAAM,GAAG,EAAE;GAAG,UAAU,SAAS,MAAM,KAAK,CAAC;EAAE;CACzE;CACA,OAAO;EAAE;EAAM;CAAS;AAC1B;AAEA,IAAa,cAAyC;CACpD,IAAI;CACJ,OAAO;CACP,MAAM;CACN,aAAa;CACb,UAAU;EAAC;EAAW;EAAW;EAAU;CAAS;CACpD,WAAW,CAAC,SAAS;CACrB,cAAc;CACd,QAAQ;EACN;GAAE,MAAM;GAAO,OAAO;GAAe,MAAM;GAAO,aAAa;GAAyB,UAAU;IAAE,SAAS;IAAyB,SAAS;GAAyB;GACtK,MAAM;EAA8F;EACtG;GAAE,MAAM;GAAU,OAAO;GAAe,MAAM;GAAQ,UAAU;GAAM,aAAa;GAAW,MAAM;EAAoF;EACxL;GAAE,MAAM;GAAQ,OAAO;GAAY,MAAM;GAAQ,UAAU;GAAM,MAAM;EAA+C;EACtH;GAAE,MAAM;GAAY,OAAO;GAA0B,MAAM;GAAU,MAAM;EAA+F;CAC5K;CACA,SAAS,UAAU;EACjB,MAAM,aAAa;GAAE,IAAI;IAAE,OAAO,IAAI,IAAI,SAAS,OAAO,KAAK,KAAK,KAAK,EAAE;GAAG,QAAQ;IAAE;GAAkB;EAAE,EAAA,CAAG;EAC/G,IAAI,CAAC,OAAO,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,SAAS,IAAI,QAAQ,GAAG,MAAM,IAAI,MAAM,0DAA0D;EACnI,IAAI,IAAI,YAAY,IAAI,UAAU,MAAM,IAAI,MAAM,uEAAuE;EACzH,IAAI,IAAI,UAAU,IAAI,MAAM,MAAM,IAAI,MAAM,iDAAiD;EAC7F,IAAI,CAAC,YAAY,KAAK,SAAS,OAAO,QAAQ,KAAK,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC,SAAS,SAAS,OAAO,QAAQ,KAAK,KAAK,EAAE,GAAG,MAAM,IAAI,MAAM,0DAA0D;EACrM,MAAM,EAAE,MAAM,aAAa,YAAY,QAAQ;EAC/C,IAAI,CAAC,QAAQ,CAAC,UAAU,MAAM,IAAI,MAAM,4DAA4D;CACtG;CACA,MAAM,OAAO,UAAU,MAAM;EAC3B,MAAM,EAAE,iBAAiB,mBAAmB,MAAM,OAAO;EACzD,OAAO,gBAAgB,QAAQ;GAAE,KAAK,SAAS,OAAO,IAAI,KAAK;GAAG,QAAQ,SAAS,OAAO,QAAQ,KAAK,KAAK;GAAI,GAAG,YAAY,QAAQ;EAAE,GAAG,KAAK,MAAM,eAAe,KAAK,MAAM,GAAG,KAAK,MAAM;CACjM;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"bitcoindevkit_bg-B-iLWrmX.js","names":["import.meta.ROLLDOWN_FILE_URL_WPGYjwIlzWVNM1bYhOc83w"],"sources":["../../../../node_modules/@bitcoindevkit/bdk-wallet-web/bitcoindevkit_bg.wasm?url"],"sourcesContent":["export default import.meta.ROLLDOWN_FILE_URL_WPGYjwIlzWVNM1bYhOc83w"],"ignoreList":[0],"mappings":";AAAA,IAAA,2BAAe,IAAA,IAAA,oCAAA,YAAA,GAAmD,CAAA,CAAnDA"}
1
+ {"version":3,"file":"bitcoindevkit_bg-B-iLWrmX.js","names":["import.meta.ROLLDOWN_FILE_URL_FKNdLzKnlftW3t_gzo81yw"],"sources":["../../../../node_modules/@bitcoindevkit/bdk-wallet-web/bitcoindevkit_bg.wasm?url"],"sourcesContent":["export default import.meta.ROLLDOWN_FILE_URL_FKNdLzKnlftW3t_gzo81yw"],"ignoreList":[0],"mappings":";AAAA,IAAA,2BAAe,IAAA,IAAA,oCAAA,YAAA,GAAmD,CAAA,CAAnDA"}
@@ -0,0 +1,2 @@
1
+ import { r as buttonsOf } from "./buttons-CvNBzh2S.js";
2
+ export { buttonsOf };
@@ -1,7 +1,6 @@
1
- import { F as buttonLabelClash, Ft as toBase64Url, N as STATUS_CARD_LIMITS, Nt as randomBytes, z as checkStatusCard } from "./groupFiles-DBbjOLuD.js";
2
- import { Zr as BUTTONS_CAPABILITY } from "./src-DhfGgAUu.js";
3
- import { t as CliError } from "./errors-my6VJR_0.js";
4
- import { _ as str, d as messageOf, f as node, i as chatOrGroup, m as oneOf, n as bool, p as num } from "./apiKit-CajiDxdK.js";
1
+ import { Ya as STATUS_CARD_LIMITS, Za as buttonLabelClash, _l as randomBytes, eo as checkStatusCard, ui as BUTTONS_CAPABILITY, vl as toBase64Url } from "./src-DDzSERyc.js";
2
+ import { t as CliError } from "./errors-DO5z6h_l.js";
3
+ import { _ as str, d as messageOf, f as node, i as chatOrGroup, m as oneOf, n as bool, p as num } from "./apiKit-CA2x5GJF.js";
5
4
  import { sha256 } from "@noble/hashes/sha2.js";
6
5
  import { createBase58check } from "@scure/base";
7
6
  import { wordlist } from "@scure/bip39/wordlists/english.js";
@@ -494,4 +493,4 @@ var BUTTON_METHODS = {
494
493
  //#endregion
495
494
  export { waitForEdit as a, findSecret as c, pressable as i, buttonsCard as n, waitForGroupFrame as o, buttonsOf as r, waitForMessage as s, BUTTON_METHODS as t };
496
495
 
497
- //# sourceMappingURL=buttons-CX1pKB0j.js.map
496
+ //# sourceMappingURL=buttons-CvNBzh2S.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"buttons-CX1pKB0j.js","names":[],"sources":["../../../../apps/ui/src/lib/parse/secrets.ts","../../src/waits.ts","../../src/buttons.ts"],"sourcesContent":["import { sha256 } from \"@noble/hashes/sha2.js\";\nimport { createBase58check } from \"@scure/base\";\nimport { wordlist } from \"@scure/bip39/wordlists/english.js\";\n\n/**\n * What the composer asks about before a message goes: text that looks like a secret someone could take funds or an\n * identity with, or a Cashu token (bearer money: the text box would hand it to whoever reads the chat).\n *\n * Detection is local and synchronous, and a finding never carries the text it found: only what kind it is and, for\n * ecash, the amount decoded here. Ark, Spark and Fedimint wallets are BIP39 seeds or raw keys, caught by the rules\n * below; Fedimint ecash notes are plain base64 with no prefix, so they are not recognised.\n */\nexport type SecretFinding =\n | { kind: \"mnemonic\" | \"nsec\" | \"bitcoin-key\" | \"hex-key\" }\n | { kind: \"cashu\"; amount: number | null; unit: string };\n\nexport type SecretKind = SecretFinding[\"kind\"];\n\nconst WORD_INDEX = new Map(wordlist.map((word, index) => [word, index]));\nconst MNEMONIC_LENGTHS = [12, 15, 18, 21, 24];\n/** Checksums tried per message at most: a 16 KiB run of seed words stays a few milliseconds. */\nconst CHECKSUM_BUDGET = 4096;\n\nconst NSEC = /\\bnsec1[02-9ac-hj-np-z]{58}(?![02-9ac-hj-np-z])/i;\nconst EXTENDED_PRIVATE = /\\b[xtyzuvYZUV]prv[1-9A-HJ-NP-Za-km-z]{100,}/;\nconst WIF = /\\b[59KLc][1-9A-HJ-NP-Za-km-z]{50,51}\\b/g;\nconst HEX_SECRET = /\\b(?:0x)?(?:[0-9a-fA-F]{128}|[0-9a-fA-F]{64})\\b/g;\nconst SECRET_CONTEXT = /priv(?:ate)?[\\s_-]*key|secret|seed|mnemonic|nsec|xprv|\\bwif\\b|recovery|passphrase/i;\n/** How far from a 64-hex string a word like \"seed\" still counts as saying what it is. */\nconst CONTEXT_REACH = 48;\nconst CASHU_TOKEN = /\\bcashu([AB])([A-Za-z0-9_\\-+/]{20,}={0,2})/g;\n\n/**\n * Characters that take no room on screen: format characters (zero-width space, soft hyphen, word joiner, the joiners,\n * direction marks), control characters that are not white space, variation selectors, the grapheme joiner and the\n * Hangul fillers. A page, a PDF or a terminal puts some of them into long strings as places to break a line.\n */\nconst INVISIBLE = /(?![\\t\\n\\v\\f\\r\\x85])[\\p{Cc}\\p{Cf}]|\\u034f|[\\u115f\\u1160\\u3164\\uffa0]|[\\ufe00-\\ufe0f]|[\\u{e0100}-\\u{e01ef}]/gu;\n\nconst base58check = createBase58check(sha256);\n\n/**\n * The first secret in `text`, keys and seeds before ecash; null for anything else. The text is read as it is and as\n * it reads without its invisible characters: a key with one inside is whole again once the reader takes it out.\n */\nexport function findSecret(text: string): SecretFinding | null {\n const visible = text.replace(INVISIBLE, \"\");\n const readings = visible === text ? [text] : [text, visible];\n if (readings.some(hasMnemonic)) return { kind: \"mnemonic\" };\n if (readings.some((reading) => NSEC.test(reading))) return { kind: \"nsec\" };\n if (readings.some((reading) => EXTENDED_PRIVATE.test(reading) || hasWif(reading))) return { kind: \"bitcoin-key\" };\n if (readings.some(hasHexSecret)) return { kind: \"hex-key\" };\n const tokens = readings.map(findCashu);\n return tokens.find((found) => found?.kind === \"cashu\" && found.amount !== null) ?? tokens.find((found) => found) ?? null;\n}\n\n/**\n * 12 to 24 words in a row from the English BIP39 list whose checksum holds, anywhere in the text; or a message that\n * is nothing but 12, 15, 18, 21 or 24 of those words (a seed with one word mistyped is still most of a seed). Prose\n * breaks a run at once: \"the\", \"a\", \"is\", \"to\", \"and\", \"of\" and \"I\" are not on the list.\n *\n * One word said over and over is not a seed, though one in 16 such runs passes a 12-word checksum (\"word\" does): a\n * window of a single word never counts, and a window mostly of one word (\"abandon … about\") counts only when that\n * word does not go on past it on either side.\n */\nfunction hasMnemonic(text: string): boolean {\n const tokens = text.toLowerCase().match(/[a-z]+/g);\n if (!tokens || tokens.length < 12) return false;\n let budget = CHECKSUM_BUDGET;\n let run: number[] = [];\n const seedLike = (start: number, length: number) => {\n const window = run.slice(start, start + length);\n const counts = new Map<number, number>();\n for (const word of window) counts.set(word, (counts.get(word) ?? 0) + 1);\n if (counts.size === 1) return false;\n const [most, times] = [...counts].reduce((a, b) => (b[1] > a[1] ? b : a));\n if (times > length / 2 && (run[start - 1] === most || run[start + length] === most)) return false;\n return checksumHolds(window);\n };\n const check = () => {\n if (run.length >= 12) {\n for (const length of MNEMONIC_LENGTHS) {\n for (let start = 0; start + length <= run.length && budget > 0; start++, budget--) {\n if (seedLike(start, length)) return true;\n }\n }\n if (run.length === tokens.length && MNEMONIC_LENGTHS.includes(run.length) && new Set(run).size > 1) return true;\n }\n run = [];\n return false;\n };\n for (const token of tokens) {\n const index = WORD_INDEX.get(token);\n if (index !== undefined) run.push(index);\n else if (check()) return true;\n }\n return check();\n}\n\n/** BIP39: the words' 11-bit indices are the entropy followed by the first bits of its SHA-256. */\nfunction checksumHolds(indices: number[]): boolean {\n const bits = indices.length * 11, checksumBits = bits / 33, entropyBytes = (bits - checksumBits) / 8;\n const bytes = new Uint8Array(Math.ceil(bits / 8));\n indices.forEach((index, i) => {\n for (let b = 0; b < 11; b++) if (index & (1 << (10 - b))) { const at = i * 11 + b; bytes[at >> 3] |= 0x80 >> (at & 7); }\n });\n const expected = sha256(bytes.subarray(0, entropyBytes))[0] >> (8 - checksumBits);\n const got = bytes[entropyBytes] >> (8 - checksumBits);\n return expected === got;\n}\n\n/** A WIF private key: base58check with version 0x80 (mainnet) or 0xef (testnet), 32 bytes and an optional 0x01. */\nfunction hasWif(text: string): boolean {\n for (const [candidate] of text.matchAll(WIF)) {\n try {\n const payload = base58check.decode(candidate);\n if ((payload[0] === 0x80 || payload[0] === 0xef) && (payload.length === 33 || (payload.length === 34 && payload[33] === 1))) return true;\n } catch { /* not base58check: an ordinary word */ }\n }\n return false;\n}\n\n/** 64 or 128 hex digits with \"private key\", \"secret\", \"seed\" or the like close by. A bare hash is not asked about. */\nfunction hasHexSecret(text: string): boolean {\n for (const match of text.matchAll(HEX_SECRET)) {\n const before = text.slice(Math.max(0, match.index - CONTEXT_REACH), match.index);\n const after = text.slice(match.index + match[0].length, match.index + match[0].length + CONTEXT_REACH);\n if (SECRET_CONTEXT.test(before) || SECRET_CONTEXT.test(after)) return true;\n }\n return false;\n}\n\n/** Every Cashu token in the text, with their amounts added up when all of them decode and share one unit. */\nfunction findCashu(text: string): SecretFinding | null {\n const tokens = [...text.matchAll(CASHU_TOKEN)].map(([, version, body]) => decodeCashu(version as \"A\" | \"B\", body));\n if (!tokens.length) return null;\n const unit = tokens.find((token) => token)?.unit ?? \"sat\";\n const readable = tokens.every((token) => token && token.unit === unit);\n return { kind: \"cashu\", amount: readable ? tokens.reduce((sum, token) => sum + token!.amount, 0) : null, unit };\n}\n\n/** What a Cashu token is worth, read from the token itself (NUT-00): nothing is asked of the mint. */\nexport function decodeCashu(version: \"A\" | \"B\", body: string): { amount: number; unit: string } | null {\n try {\n const bytes = fromBase64(body);\n if (version === \"A\") {\n const token = JSON.parse(new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes)) as { token?: { proofs?: { amount?: unknown }[] }[]; unit?: unknown };\n return total(token.token?.flatMap((entry) => entry.proofs ?? []).map((proof) => proof.amount), token.unit);\n }\n const token = decodeCbor(bytes) as { t?: { p?: { a?: unknown }[] }[]; u?: unknown } | null;\n return total(token?.t?.flatMap((entry) => entry.p ?? []).map((proof) => proof.a), token?.u);\n } catch {\n return null;\n }\n}\n\nfunction total(amounts: unknown[] | undefined, unit: unknown): { amount: number; unit: string } | null {\n if (!amounts?.length || !amounts.every((a): a is number => Number.isSafeInteger(a) && (a as number) >= 0)) return null;\n const amount = amounts.reduce((sum, a) => sum + a, 0);\n return Number.isSafeInteger(amount) ? { amount, unit: typeof unit === \"string\" && unit ? unit : \"sat\" } : null;\n}\n\nfunction fromBase64(text: string): Uint8Array {\n const standard = text.replace(/-/g, \"+\").replace(/_/g, \"/\").replace(/=+$/, \"\");\n const binary = atob(standard + \"=\".repeat((4 - (standard.length % 4)) % 4));\n return Uint8Array.from(binary, (c) => c.charCodeAt(0));\n}\n\n/** Enough CBOR (RFC 8949) for a cashuB token: definite lengths, integers, bytes, text, arrays, maps, tags, simple values. */\nfunction decodeCbor(bytes: Uint8Array): unknown {\n let at = 0, depth = 0;\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n const length = (info: number): number => {\n if (info < 24) return info;\n const size = info === 24 ? 1 : info === 25 ? 2 : info === 26 ? 4 : info === 27 ? 8 : 0;\n if (!size || at + size > bytes.length) throw new Error(\"cbor\");\n let value = 0;\n for (let i = 0; i < size; i++) value = value * 256 + bytes[at + i];\n at += size;\n return value;\n };\n const item = (): unknown => {\n if (at >= bytes.length || ++depth > 32) throw new Error(\"cbor\");\n const head = bytes[at++], major = head >> 5, info = head & 31;\n try {\n if (major === 7) {\n if (info === 20) return false;\n if (info === 21) return true;\n if (info === 22 || info === 23) return null;\n if (info === 25) { at += 2; return NaN; } // a half float: never in a token\n if (info === 26) { at += 4; return view.getFloat32(at - 4); }\n if (info === 27) { at += 8; return view.getFloat64(at - 8); }\n throw new Error(\"cbor\");\n }\n const n = length(info);\n switch (major) {\n case 0: return n;\n case 1: return -1 - n;\n case 2: case 3: {\n if (at + n > bytes.length) throw new Error(\"cbor\");\n const slice = bytes.subarray(at, (at += n));\n return major === 2 ? slice : new TextDecoder(\"utf-8\", { fatal: true }).decode(slice);\n }\n case 4: {\n if (n > bytes.length - at) throw new Error(\"cbor\");\n return Array.from({ length: n }, item);\n }\n case 5: {\n if (n > bytes.length - at) throw new Error(\"cbor\");\n const map: Record<string, unknown> = Object.create(null);\n for (let i = 0; i < n; i++) { const key = item(); map[String(key)] = item(); }\n return map;\n }\n default: return item(); // a tag: the value it tags\n }\n } finally { depth--; }\n };\n return item();\n}\n","import type { StoredMessage } from \"@ghostly/browser/shared/types\";\nimport { node, type ApiContext } from \"./apiKit\";\nimport { CliError } from \"./errors\";\nimport type { GhostlyEvent } from \"./events\";\n\n/*\n * Waiting on what a command sent (WISP 1100): a message's delivery, an edit's confirmation, a group frame taken by an\n * edge. Shared by the chat, group and status card methods.\n */\n\nconst DELIVERY_RANK: Record<string, number> = { sending: 0, waiting: 1, queued: 1, held: 2, sent: 2, delivered: 3 };\n\n/** Waits until the contact confirmed the latest edit of one of my messages (it is no longer pending). */\nexport async function waitForEdit(ctx: ApiContext, chat: string, messageId: string, ms: number): Promise<StoredMessage> {\n const until = Date.now() + ms;\n for (;;) {\n const message = await node(ctx).getMessage(chat, messageId);\n if (!message) throw new CliError(\"not_found\", `No message ${messageId} in this chat`);\n if (!message.edit?.pending) return message;\n if (Date.now() >= until) throw new CliError(\"timeout\", `Timed out after ${Math.round(ms / 1000)} s: the contact has not confirmed the edit yet. It goes by itself once the chat is live and the contact's app shows edits, while this profile is online`, { messageId, edits: message.edit.seq });\n await new Promise((resolve) => setTimeout(resolve, 250));\n }\n}\n\n/**\n * Waits until an edge took my group message, or its edit number `edit` (WISP 902: a group has no receipts, so this is\n * as far as the author sees): a member's edge in a private group, an edge to one of my hubs in a community.\n */\nexport async function waitForGroupFrame(ctx: ApiContext, groupId: string, messageId: string, edit: number | undefined, ms: number): Promise<number> {\n const until = Date.now() + ms;\n for (;;) {\n const taken = node(ctx).groupTaken({ groupId, messageId, ...(edit ? { edit } : {}) });\n if (taken > 0) return taken;\n if (Date.now() >= until) throw new CliError(\"timeout\", `Timed out after ${Math.round(ms / 1000)} s: no member's edge took it yet. It stays in the group and goes when one opens, while this profile is online`, { messageId, ...(edit ? { edits: edit } : {}) });\n await new Promise((resolve) => setTimeout(resolve, 250));\n }\n}\n\n/** Waits until a message of mine reached `target` (`sent`: on its way to the contact; `delivered`: acknowledged). */\nexport async function waitForMessage(ctx: ApiContext, chat: string, messageId: string, target: \"sent\" | \"delivered\", ms: number): Promise<StoredMessage> {\n const want = DELIVERY_RANK[target];\n const look = (message: StoredMessage | undefined) => {\n if (!message) return undefined;\n const delivery = message.delivery ?? \"sent\";\n if (delivery === \"failed\") throw new CliError(\"engine\", message.deliveryError ?? \"The message could not be sent\", { messageId, delivery });\n return (DELIVERY_RANK[delivery] ?? 0) >= want ? message : undefined;\n };\n return new Promise<StoredMessage>((resolve, reject) => {\n let settled = false;\n const finish = (error: unknown, message?: StoredMessage) => {\n if (settled) return;\n settled = true; clearTimeout(timer); off();\n if (error) reject(error); else resolve(message!);\n };\n const check = (last = false) => void node(ctx).getMessage(chat, messageId).then((message) => {\n try {\n const done = look(message);\n if (done) finish(null, done);\n else if (last) finish(new CliError(\"timeout\", `Timed out after ${Math.round(ms / 1000)} s: the message is ${message?.delivery ?? \"not sent yet\"} and still goes by itself while this profile is online`, { messageId, delivery: message?.delivery ?? null }));\n } catch (error) { finish(error); }\n }, finish);\n const timer = setTimeout(() => check(true), ms);\n // Any event naming the message: a message first seen already delivered comes as `message.sent`, never as\n // `message.delivery`. Listening before the first read leaves no gap for a change to slip through.\n const off = ctx.hub.onEvent((event: GhostlyEvent) => {\n if (event.chat === chat && (event.messageId === messageId || (event.message as { id?: string } | undefined)?.id === messageId)) check();\n });\n check();\n });\n}\n","import { BUTTONS_CAPABILITY, STATUS_CARD_LIMITS, buttonLabelClash, checkStatusCard, randomBytes, toBase64Url, type ButtonsCard } from \"@ghostly/core\";\nimport type { LinkView, StoredMessage } from \"@ghostly/browser/shared/types\";\nimport type { Parsed } from \"./args\";\nimport { bool, chatOrGroup, messageOf, node, num, oneOf, str, type Method, type Params } from \"./apiKit\";\nimport { CliError } from \"./errors\";\nimport { findSecret } from \"../../../apps/ui/src/lib/parse/secrets\";\nimport { waitForEdit, waitForGroupFrame, waitForMessage } from \"./waits\";\n\n/*\n * Message buttons (WISP 406 · Message Buttons), the bot's side: a message whose text is the question, with up to six\n * buttons under it (`send --button yes:Yes`). A press comes back as a reply whose text is the button's label; the\n * engine marks it (`press`) and `listen` says `button.pressed`. `button update` then shows the answer (`chosen`) and\n * closes the buttons, by editing the message with the same buttons.\n */\n\n/**\n * `--button id:Label` (again for each, 1 to 6), `--style id=primary|neutral|danger`, `--once` and `--id`, as the\n * method's parameters: `buttons` and `buttonsId`. Nothing when no `--button` is given. The label may hold colons: the\n * id ends at the first one. Two labels that read the same (ignoring case and spaces at the ends), or a label that is\n * another button's id, are a usage error here; other bounds (ids, labels, how many) are the card's, checked when it is sent.\n */\nexport function buttonsOf(options: Parsed[\"options\"]): { buttons: Record<string, unknown>[]; buttonsId?: string } | undefined {\n const given = Array.isArray(options.button) ? options.button as string[] : [];\n if (!given.length) {\n for (const flag of [\"style\", \"once\", \"id\"] as const)\n if (options[flag] !== undefined) throw new CliError(\"usage\", `--${flag} goes with --button id:Label`);\n return undefined;\n }\n const buttons: Record<string, unknown>[] = given.map((value) => {\n const colon = value.indexOf(\":\");\n if (colon < 1 || colon === value.length - 1) throw new CliError(\"usage\", `--button takes id:Label, like yes:Yes, not ${JSON.stringify(value)}`);\n return { id: value.slice(0, colon), label: value.slice(colon + 1).trim() };\n });\n // Two buttons a typed answer could not tell apart: the same label, or a label that is another's id (ignoring case).\n const clash = buttonLabelClash(buttons);\n if (clash) {\n const [at, other] = [JSON.stringify(given[clash.at]), JSON.stringify(given[clash.with])];\n throw new CliError(\"usage\", clash.kind === \"label\"\n ? `--button ${at} repeats the label of --button ${other} (ignoring case and spaces): a typed answer could not tell them apart`\n : `--button ${at} has a label that is the id of --button ${other}: a typed answer could not tell them apart`);\n }\n for (const value of Array.isArray(options.style) ? options.style as string[] : []) {\n const eq = value.indexOf(\"=\");\n const button = eq > 0 ? buttons.find((b) => b.id === value.slice(0, eq)) : undefined;\n if (!button) throw new CliError(\"usage\", `--style takes id=primary|neutral|danger for one of the buttons, not ${JSON.stringify(value)}`);\n button.style = value.slice(eq + 1);\n }\n if (options.once === true) for (const button of buttons) button.once = true;\n return { buttons, ...(options.id !== undefined && { buttonsId: String(options.id) }) };\n}\n\n/** A buttons card checked by the sender's rule, or a `bad_request` that says what is wrong. */\nfunction checked(card: Record<string, unknown>): ButtonsCard {\n const result = checkStatusCard(card);\n if (\"error\" in result) throw new CliError(\"bad_request\", `Buttons: ${result.error}`, { card });\n return result.card as ButtonsCard;\n}\n\n/**\n * The card a send's `buttons` (and `buttonsId`) make, or undefined when it names none. `buttons` is a list of\n * `{id, label, style?, once?}`; the id of the question is made up (`ask-…`) unless given.\n */\nexport function buttonsCard(params: Params): ButtonsCard | undefined {\n const buttons = params.buttons;\n if (buttons === undefined || buttons === null) {\n if (params.buttonsId !== undefined) throw new CliError(\"bad_request\", \"buttonsId goes with buttons\");\n return undefined;\n }\n if (!Array.isArray(buttons)) throw new CliError(\"bad_request\", \"buttons must be a list of {id, label, style?, once?}\");\n const id = str(params, \"buttonsId\") ?? `ask-${toBase64Url(randomBytes(6))}`;\n return checked({ kind: \"buttons\", id, buttons });\n}\n\n/** Whether a 1:1 contact's app shows and presses buttons: null until its app said what it offers. */\nexport function pressable(link: LinkView): boolean | null {\n const peer = link.sessionOffers?.peer;\n return peer ? peer.includes(BUTTONS_CAPABILITY) : null;\n}\n\n/** The message of mine with buttons that `button update` names, by its id here (or the id both sides know). */\nfunction buttonsMessage(message: StoredMessage | undefined, ref: string, group: boolean): StoredMessage & { card: ButtonsCard } {\n if (!message) throw new CliError(\"not_found\", `No message ${JSON.stringify(ref)} of yours in this ${group ? \"group\" : \"chat\"}: the messageId send gave`);\n if (message.card?.kind !== \"buttons\") throw new CliError(\"bad_request\", \"That message has no buttons: send one with send --button id:Label\");\n return message as StoredMessage & { card: ButtonsCard };\n}\n\n/**\n * `button update`: the answer shown on a question's buttons (`chosen`), the buttons closed (`close`), or a new text\n * (`text`), by editing the message with the same buttons. What is not given stays as it was.\n */\nasync function updateButtons(ctx: Parameters<Method>[0], params: Params): Promise<Record<string, unknown>> {\n const target = chatOrGroup(ctx, str(params, \"chat\", true));\n const ref = str(params, \"message\", true);\n const chosen = str(params, \"chosen\");\n const close = bool(params, \"close\");\n const newText = str(params, \"text\");\n if (chosen === undefined && !close && newText === undefined) throw new CliError(\"bad_request\", \"Nothing to update: give chosen, close or text\");\n // A new text is a message like any other: the same guard as `send` and `edit`.\n if (newText !== undefined && !bool(params, \"force\")) {\n const secret = findSecret(newText);\n if (secret) throw new CliError(\"confirm\", `The text looks like ${secret.kind === \"cashu\" ? \"a Cashu token (money anyone who reads it can take)\" : \"a secret (a seed or a private key)\"}; update with --force if you mean to`, { kind: secret.kind });\n }\n const message = buttonsMessage(await messageOf(ctx, target.linkId, ref, (m) => m.sender === \"me\"), ref, target.group);\n if ((message.edit?.seq ?? 0) >= STATUS_CARD_LIMITS.edits)\n throw new CliError(\"refused\", `This message took ${STATUS_CARD_LIMITS.edits} updates, the most one takes: ask again with a new send`, { messageId: message.id, edits: message.edit!.seq });\n const before = message.card;\n // Said here, with the buttons it has, as a press says it: the card's own check names only the field.\n if (chosen !== undefined && !before.buttons.some((b) => b.id === chosen))\n throw new CliError(\"bad_request\", `That message has no button ${JSON.stringify(chosen)}; its buttons: ${before.buttons.map((b) => b.id).join(\", \")}`, { card: before });\n const card = checked({ ...before, ...(chosen !== undefined && { chosen }), ...((close || before.closed) && { closed: true }) });\n const text = newText ?? message.text;\n const wait = oneOf(params, \"wait\", target.group ? [\"none\", \"sent\"] as const : [\"none\", \"confirmed\"] as const, \"none\");\n const ms = num(params, \"timeout\", 30, { min: 1, max: 3600 }) * 1000;\n const result = await node(ctx).editMessage({ linkId: target.linkId, messageId: message.id, text, card });\n if (result.error) throw new CliError(result.refused ? \"refused\" : \"unavailable\", result.error);\n let edited = await node(ctx).getMessage(target.linkId, message.id);\n if (wait === \"confirmed\" && edited?.edit?.pending) edited = await waitForEdit(ctx, target.linkId, message.id, ms);\n const edges = wait === \"sent\" && edited?.edit ? await waitForGroupFrame(ctx, target.id, message.id, edited.edit.seq, ms) : undefined;\n return {\n [target.group ? \"group\" : \"chat\"]: target.id, buttons: card.id, messageId: message.id, card, edits: edited?.edit?.seq ?? 0,\n ...(target.group ? { ...(edges !== undefined && { edges }) } : { confirmed: !!edited && !edited.edit?.pending }),\n };\n}\n\n/**\n * `button press`: the other side of a question, as the app's tap: a press on a button of someone else's message, by\n * its id here (or the id both sides know). The engine sends a reply whose text is the button's label, and refuses what\n * an app would: a button the message does not have, closed buttons, a second answer past `once`, a second press\n * within a second. A one-shot in a chat waits until the press went out, as `send` does.\n */\nasync function pressButton(ctx: Parameters<Method>[0], params: Params): Promise<Record<string, unknown>> {\n const target = chatOrGroup(ctx, str(params, \"chat\", true));\n const ref = str(params, \"message\", true);\n const buttonId = str(params, \"button\", true);\n const where = target.group ? \"group\" : \"chat\";\n const message = await messageOf(ctx, target.linkId, ref, (m) => m.sender !== \"me\");\n if (!message) throw new CliError(\"not_found\", `No message ${JSON.stringify(ref)} from someone else in this ${where}: the id its event or history gave`);\n if (message.card?.kind !== \"buttons\") throw new CliError(\"bad_request\", \"That message has no buttons\");\n const button = message.card.buttons.find((b) => b.id === buttonId);\n if (!button) throw new CliError(\"not_found\", `That message has no button ${JSON.stringify(buttonId)}; its buttons: ${message.card.buttons.map((b) => b.id).join(\", \")}`);\n const wait = target.group\n ? oneOf(params, \"wait\", [\"none\", \"sent\"] as const, \"none\")\n : oneOf(params, \"wait\", [\"none\", \"sent\", \"delivered\"] as const, ctx.mode === \"one-shot\" ? \"sent\" : \"none\");\n const ms = num(params, \"timeout\", 30, { min: 1, max: 3600 }) * 1000;\n const result = await node(ctx).pressButton({ linkId: target.linkId, messageId: message.id, buttonId });\n if (result.error) throw new CliError(result.refused ? \"refused\" : \"unavailable\", result.error, result.paced ? { paced: true } : undefined);\n const replyId = result.messageId ?? null;\n const answer = { [where]: target.id, messageId: message.id, button: button.id, label: button.label, replyId };\n if (!replyId) return answer;\n if (target.group) {\n const edges = wait === \"sent\" ? await waitForGroupFrame(ctx, target.id, replyId, undefined, ms) : node(ctx).groupTaken({ groupId: target.id, messageId: replyId });\n return { ...answer, edges };\n }\n const reply = wait === \"none\" ? await node(ctx).getMessage(target.linkId, replyId) : await waitForMessage(ctx, target.linkId, replyId, wait, ms);\n return { ...answer, delivery: reply?.delivery ?? null };\n}\n\nexport const BUTTON_METHODS: Record<string, Method> = {\n /** WISP 406 · Message Buttons: a press on a button of someone else's message, as the app's tap. */\n \"button.press\": pressButton,\n /** WISP 406 · Message Buttons: the answer chosen on a question of mine, its buttons closed, or its text changed. */\n \"button.update\": updateButtons,\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"buttons-CvNBzh2S.js","names":[],"sources":["../../../../apps/ui/src/lib/parse/secrets.ts","../../src/waits.ts","../../src/buttons.ts"],"sourcesContent":["import { sha256 } from \"@noble/hashes/sha2.js\";\nimport { createBase58check } from \"@scure/base\";\nimport { wordlist } from \"@scure/bip39/wordlists/english.js\";\n\n/**\n * What the composer asks about before a message goes: text that looks like a secret someone could take funds or an\n * identity with, or a Cashu token (bearer money: the text box would hand it to whoever reads the chat).\n *\n * Detection is local and synchronous, and a finding never carries the text it found: only what kind it is and, for\n * ecash, the amount decoded here. Ark, Spark and Fedimint wallets are BIP39 seeds or raw keys, caught by the rules\n * below; Fedimint ecash notes are plain base64 with no prefix, so they are not recognised.\n */\nexport type SecretFinding =\n | { kind: \"mnemonic\" | \"nsec\" | \"bitcoin-key\" | \"hex-key\" }\n | { kind: \"cashu\"; amount: number | null; unit: string };\n\nexport type SecretKind = SecretFinding[\"kind\"];\n\nconst WORD_INDEX = new Map(wordlist.map((word, index) => [word, index]));\nconst MNEMONIC_LENGTHS = [12, 15, 18, 21, 24];\n/** Checksums tried per message at most: a 16 KiB run of seed words stays a few milliseconds. */\nconst CHECKSUM_BUDGET = 4096;\n\nconst NSEC = /\\bnsec1[02-9ac-hj-np-z]{58}(?![02-9ac-hj-np-z])/i;\nconst EXTENDED_PRIVATE = /\\b[xtyzuvYZUV]prv[1-9A-HJ-NP-Za-km-z]{100,}/;\nconst WIF = /\\b[59KLc][1-9A-HJ-NP-Za-km-z]{50,51}\\b/g;\nconst HEX_SECRET = /\\b(?:0x)?(?:[0-9a-fA-F]{128}|[0-9a-fA-F]{64})\\b/g;\nconst SECRET_CONTEXT = /priv(?:ate)?[\\s_-]*key|secret|seed|mnemonic|nsec|xprv|\\bwif\\b|recovery|passphrase/i;\n/** How far from a 64-hex string a word like \"seed\" still counts as saying what it is. */\nconst CONTEXT_REACH = 48;\nconst CASHU_TOKEN = /\\bcashu([AB])([A-Za-z0-9_\\-+/]{20,}={0,2})/g;\n\n/**\n * Characters that take no room on screen: format characters (zero-width space, soft hyphen, word joiner, the joiners,\n * direction marks), control characters that are not white space, variation selectors, the grapheme joiner and the\n * Hangul fillers. A page, a PDF or a terminal puts some of them into long strings as places to break a line.\n */\nconst INVISIBLE = /(?![\\t\\n\\v\\f\\r\\x85])[\\p{Cc}\\p{Cf}]|\\u034f|[\\u115f\\u1160\\u3164\\uffa0]|[\\ufe00-\\ufe0f]|[\\u{e0100}-\\u{e01ef}]/gu;\n\nconst base58check = createBase58check(sha256);\n\n/**\n * The first secret in `text`, keys and seeds before ecash; null for anything else. The text is read as it is and as\n * it reads without its invisible characters: a key with one inside is whole again once the reader takes it out.\n */\nexport function findSecret(text: string): SecretFinding | null {\n const visible = text.replace(INVISIBLE, \"\");\n const readings = visible === text ? [text] : [text, visible];\n if (readings.some(hasMnemonic)) return { kind: \"mnemonic\" };\n if (readings.some((reading) => NSEC.test(reading))) return { kind: \"nsec\" };\n if (readings.some((reading) => EXTENDED_PRIVATE.test(reading) || hasWif(reading))) return { kind: \"bitcoin-key\" };\n if (readings.some(hasHexSecret)) return { kind: \"hex-key\" };\n const tokens = readings.map(findCashu);\n return tokens.find((found) => found?.kind === \"cashu\" && found.amount !== null) ?? tokens.find((found) => found) ?? null;\n}\n\n/**\n * 12 to 24 words in a row from the English BIP39 list whose checksum holds, anywhere in the text; or a message that\n * is nothing but 12, 15, 18, 21 or 24 of those words (a seed with one word mistyped is still most of a seed). Prose\n * breaks a run at once: \"the\", \"a\", \"is\", \"to\", \"and\", \"of\" and \"I\" are not on the list.\n *\n * One word said over and over is not a seed, though one in 16 such runs passes a 12-word checksum (\"word\" does): a\n * window of a single word never counts, and a window mostly of one word (\"abandon … about\") counts only when that\n * word does not go on past it on either side.\n */\nfunction hasMnemonic(text: string): boolean {\n const tokens = text.toLowerCase().match(/[a-z]+/g);\n if (!tokens || tokens.length < 12) return false;\n let budget = CHECKSUM_BUDGET;\n let run: number[] = [];\n const seedLike = (start: number, length: number) => {\n const window = run.slice(start, start + length);\n const counts = new Map<number, number>();\n for (const word of window) counts.set(word, (counts.get(word) ?? 0) + 1);\n if (counts.size === 1) return false;\n const [most, times] = [...counts].reduce((a, b) => (b[1] > a[1] ? b : a));\n if (times > length / 2 && (run[start - 1] === most || run[start + length] === most)) return false;\n return checksumHolds(window);\n };\n const check = () => {\n if (run.length >= 12) {\n for (const length of MNEMONIC_LENGTHS) {\n for (let start = 0; start + length <= run.length && budget > 0; start++, budget--) {\n if (seedLike(start, length)) return true;\n }\n }\n if (run.length === tokens.length && MNEMONIC_LENGTHS.includes(run.length) && new Set(run).size > 1) return true;\n }\n run = [];\n return false;\n };\n for (const token of tokens) {\n const index = WORD_INDEX.get(token);\n if (index !== undefined) run.push(index);\n else if (check()) return true;\n }\n return check();\n}\n\n/** BIP39: the words' 11-bit indices are the entropy followed by the first bits of its SHA-256. */\nfunction checksumHolds(indices: number[]): boolean {\n const bits = indices.length * 11, checksumBits = bits / 33, entropyBytes = (bits - checksumBits) / 8;\n const bytes = new Uint8Array(Math.ceil(bits / 8));\n indices.forEach((index, i) => {\n for (let b = 0; b < 11; b++) if (index & (1 << (10 - b))) { const at = i * 11 + b; bytes[at >> 3] |= 0x80 >> (at & 7); }\n });\n const expected = sha256(bytes.subarray(0, entropyBytes))[0] >> (8 - checksumBits);\n const got = bytes[entropyBytes] >> (8 - checksumBits);\n return expected === got;\n}\n\n/** A WIF private key: base58check with version 0x80 (mainnet) or 0xef (testnet), 32 bytes and an optional 0x01. */\nfunction hasWif(text: string): boolean {\n for (const [candidate] of text.matchAll(WIF)) {\n try {\n const payload = base58check.decode(candidate);\n if ((payload[0] === 0x80 || payload[0] === 0xef) && (payload.length === 33 || (payload.length === 34 && payload[33] === 1))) return true;\n } catch { /* not base58check: an ordinary word */ }\n }\n return false;\n}\n\n/** 64 or 128 hex digits with \"private key\", \"secret\", \"seed\" or the like close by. A bare hash is not asked about. */\nfunction hasHexSecret(text: string): boolean {\n for (const match of text.matchAll(HEX_SECRET)) {\n const before = text.slice(Math.max(0, match.index - CONTEXT_REACH), match.index);\n const after = text.slice(match.index + match[0].length, match.index + match[0].length + CONTEXT_REACH);\n if (SECRET_CONTEXT.test(before) || SECRET_CONTEXT.test(after)) return true;\n }\n return false;\n}\n\n/** Every Cashu token in the text, with their amounts added up when all of them decode and share one unit. */\nfunction findCashu(text: string): SecretFinding | null {\n const tokens = [...text.matchAll(CASHU_TOKEN)].map(([, version, body]) => decodeCashu(version as \"A\" | \"B\", body));\n if (!tokens.length) return null;\n const unit = tokens.find((token) => token)?.unit ?? \"sat\";\n const readable = tokens.every((token) => token && token.unit === unit);\n return { kind: \"cashu\", amount: readable ? tokens.reduce((sum, token) => sum + token!.amount, 0) : null, unit };\n}\n\n/** What a Cashu token is worth, read from the token itself (NUT-00): nothing is asked of the mint. */\nexport function decodeCashu(version: \"A\" | \"B\", body: string): { amount: number; unit: string } | null {\n try {\n const bytes = fromBase64(body);\n if (version === \"A\") {\n const token = JSON.parse(new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes)) as { token?: { proofs?: { amount?: unknown }[] }[]; unit?: unknown };\n return total(token.token?.flatMap((entry) => entry.proofs ?? []).map((proof) => proof.amount), token.unit);\n }\n const token = decodeCbor(bytes) as { t?: { p?: { a?: unknown }[] }[]; u?: unknown } | null;\n return total(token?.t?.flatMap((entry) => entry.p ?? []).map((proof) => proof.a), token?.u);\n } catch {\n return null;\n }\n}\n\nfunction total(amounts: unknown[] | undefined, unit: unknown): { amount: number; unit: string } | null {\n if (!amounts?.length || !amounts.every((a): a is number => Number.isSafeInteger(a) && (a as number) >= 0)) return null;\n const amount = amounts.reduce((sum, a) => sum + a, 0);\n return Number.isSafeInteger(amount) ? { amount, unit: typeof unit === \"string\" && unit ? unit : \"sat\" } : null;\n}\n\nfunction fromBase64(text: string): Uint8Array {\n const standard = text.replace(/-/g, \"+\").replace(/_/g, \"/\").replace(/=+$/, \"\");\n const binary = atob(standard + \"=\".repeat((4 - (standard.length % 4)) % 4));\n return Uint8Array.from(binary, (c) => c.charCodeAt(0));\n}\n\n/** Enough CBOR (RFC 8949) for a cashuB token: definite lengths, integers, bytes, text, arrays, maps, tags, simple values. */\nfunction decodeCbor(bytes: Uint8Array): unknown {\n let at = 0, depth = 0;\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n const length = (info: number): number => {\n if (info < 24) return info;\n const size = info === 24 ? 1 : info === 25 ? 2 : info === 26 ? 4 : info === 27 ? 8 : 0;\n if (!size || at + size > bytes.length) throw new Error(\"cbor\");\n let value = 0;\n for (let i = 0; i < size; i++) value = value * 256 + bytes[at + i];\n at += size;\n return value;\n };\n const item = (): unknown => {\n if (at >= bytes.length || ++depth > 32) throw new Error(\"cbor\");\n const head = bytes[at++], major = head >> 5, info = head & 31;\n try {\n if (major === 7) {\n if (info === 20) return false;\n if (info === 21) return true;\n if (info === 22 || info === 23) return null;\n if (info === 25) { at += 2; return NaN; } // a half float: never in a token\n if (info === 26) { at += 4; return view.getFloat32(at - 4); }\n if (info === 27) { at += 8; return view.getFloat64(at - 8); }\n throw new Error(\"cbor\");\n }\n const n = length(info);\n switch (major) {\n case 0: return n;\n case 1: return -1 - n;\n case 2: case 3: {\n if (at + n > bytes.length) throw new Error(\"cbor\");\n const slice = bytes.subarray(at, (at += n));\n return major === 2 ? slice : new TextDecoder(\"utf-8\", { fatal: true }).decode(slice);\n }\n case 4: {\n if (n > bytes.length - at) throw new Error(\"cbor\");\n return Array.from({ length: n }, item);\n }\n case 5: {\n if (n > bytes.length - at) throw new Error(\"cbor\");\n const map: Record<string, unknown> = Object.create(null);\n for (let i = 0; i < n; i++) { const key = item(); map[String(key)] = item(); }\n return map;\n }\n default: return item(); // a tag: the value it tags\n }\n } finally { depth--; }\n };\n return item();\n}\n","import type { StoredMessage } from \"@ghostly/browser/shared/types\";\nimport { node, type ApiContext } from \"./apiKit\";\nimport { CliError } from \"./errors\";\nimport type { GhostlyEvent } from \"./events\";\n\n/*\n * Waiting on what a command sent (WISP 1100): a message's delivery, an edit's confirmation, a group frame taken by an\n * edge. Shared by the chat, group and status card methods.\n */\n\nconst DELIVERY_RANK: Record<string, number> = { sending: 0, waiting: 1, queued: 1, held: 2, sent: 2, delivered: 3 };\n\n/** Waits until the contact confirmed the latest edit of one of my messages (it is no longer pending). */\nexport async function waitForEdit(ctx: ApiContext, chat: string, messageId: string, ms: number): Promise<StoredMessage> {\n const until = Date.now() + ms;\n for (;;) {\n const message = await node(ctx).getMessage(chat, messageId);\n if (!message) throw new CliError(\"not_found\", `No message ${messageId} in this chat`);\n if (!message.edit?.pending) return message;\n if (Date.now() >= until) throw new CliError(\"timeout\", `Timed out after ${Math.round(ms / 1000)} s: the contact has not confirmed the edit yet. It goes by itself once the chat is live and the contact's app shows edits, while this profile is online`, { messageId, edits: message.edit.seq });\n await new Promise((resolve) => setTimeout(resolve, 250));\n }\n}\n\n/**\n * Waits until an edge took my group message, or its edit number `edit` (WISP 902: a group has no receipts, so this is\n * as far as the author sees): a member's edge in a private group, an edge to one of my hubs in a community.\n */\nexport async function waitForGroupFrame(ctx: ApiContext, groupId: string, messageId: string, edit: number | undefined, ms: number): Promise<number> {\n const until = Date.now() + ms;\n for (;;) {\n const taken = node(ctx).groupTaken({ groupId, messageId, ...(edit ? { edit } : {}) });\n if (taken > 0) return taken;\n if (Date.now() >= until) throw new CliError(\"timeout\", `Timed out after ${Math.round(ms / 1000)} s: no member's edge took it yet. It stays in the group and goes when one opens, while this profile is online`, { messageId, ...(edit ? { edits: edit } : {}) });\n await new Promise((resolve) => setTimeout(resolve, 250));\n }\n}\n\n/** Waits until a message of mine reached `target` (`sent`: on its way to the contact; `delivered`: acknowledged). */\nexport async function waitForMessage(ctx: ApiContext, chat: string, messageId: string, target: \"sent\" | \"delivered\", ms: number): Promise<StoredMessage> {\n const want = DELIVERY_RANK[target];\n const look = (message: StoredMessage | undefined) => {\n if (!message) return undefined;\n const delivery = message.delivery ?? \"sent\";\n if (delivery === \"failed\") throw new CliError(\"engine\", message.deliveryError ?? \"The message could not be sent\", { messageId, delivery });\n return (DELIVERY_RANK[delivery] ?? 0) >= want ? message : undefined;\n };\n return new Promise<StoredMessage>((resolve, reject) => {\n let settled = false;\n const finish = (error: unknown, message?: StoredMessage) => {\n if (settled) return;\n settled = true; clearTimeout(timer); off();\n if (error) reject(error); else resolve(message!);\n };\n const check = (last = false) => void node(ctx).getMessage(chat, messageId).then((message) => {\n try {\n const done = look(message);\n if (done) finish(null, done);\n else if (last) finish(new CliError(\"timeout\", `Timed out after ${Math.round(ms / 1000)} s: the message is ${message?.delivery ?? \"not sent yet\"} and still goes by itself while this profile is online`, { messageId, delivery: message?.delivery ?? null }));\n } catch (error) { finish(error); }\n }, finish);\n const timer = setTimeout(() => check(true), ms);\n // Any event naming the message: a message first seen already delivered comes as `message.sent`, never as\n // `message.delivery`. Listening before the first read leaves no gap for a change to slip through.\n const off = ctx.hub.onEvent((event: GhostlyEvent) => {\n if (event.chat === chat && (event.messageId === messageId || (event.message as { id?: string } | undefined)?.id === messageId)) check();\n });\n check();\n });\n}\n","import { BUTTONS_CAPABILITY, STATUS_CARD_LIMITS, buttonLabelClash, checkStatusCard, randomBytes, toBase64Url, type ButtonsCard } from \"@ghostly/core\";\nimport type { LinkView, StoredMessage } from \"@ghostly/browser/shared/types\";\nimport type { Parsed } from \"./args\";\nimport { bool, chatOrGroup, messageOf, node, num, oneOf, str, type Method, type Params } from \"./apiKit\";\nimport { CliError } from \"./errors\";\nimport { findSecret } from \"../../../apps/ui/src/lib/parse/secrets\";\nimport { waitForEdit, waitForGroupFrame, waitForMessage } from \"./waits\";\n\n/*\n * Message buttons (WISP 406 · Message Buttons), the bot's side: a message whose text is the question, with up to six\n * buttons under it (`send --button yes:Yes`). A press comes back as a reply whose text is the button's label; the\n * engine marks it (`press`) and `listen` says `button.pressed`. `button update` then shows the answer (`chosen`) and\n * closes the buttons, by editing the message with the same buttons.\n */\n\n/**\n * `--button id:Label` (again for each, 1 to 6), `--style id=primary|neutral|danger`, `--once` and `--id`, as the\n * method's parameters: `buttons` and `buttonsId`. Nothing when no `--button` is given. The label may hold colons: the\n * id ends at the first one. Two labels that read the same (ignoring case and spaces at the ends), or a label that is\n * another button's id, are a usage error here; other bounds (ids, labels, how many) are the card's, checked when it is sent.\n */\nexport function buttonsOf(options: Parsed[\"options\"]): { buttons: Record<string, unknown>[]; buttonsId?: string } | undefined {\n const given = Array.isArray(options.button) ? options.button as string[] : [];\n if (!given.length) {\n for (const flag of [\"style\", \"once\", \"id\"] as const)\n if (options[flag] !== undefined) throw new CliError(\"usage\", `--${flag} goes with --button id:Label`);\n return undefined;\n }\n const buttons: Record<string, unknown>[] = given.map((value) => {\n const colon = value.indexOf(\":\");\n if (colon < 1 || colon === value.length - 1) throw new CliError(\"usage\", `--button takes id:Label, like yes:Yes, not ${JSON.stringify(value)}`);\n return { id: value.slice(0, colon), label: value.slice(colon + 1).trim() };\n });\n // Two buttons a typed answer could not tell apart: the same label, or a label that is another's id (ignoring case).\n const clash = buttonLabelClash(buttons);\n if (clash) {\n const [at, other] = [JSON.stringify(given[clash.at]), JSON.stringify(given[clash.with])];\n throw new CliError(\"usage\", clash.kind === \"label\"\n ? `--button ${at} repeats the label of --button ${other} (ignoring case and spaces): a typed answer could not tell them apart`\n : `--button ${at} has a label that is the id of --button ${other}: a typed answer could not tell them apart`);\n }\n for (const value of Array.isArray(options.style) ? options.style as string[] : []) {\n const eq = value.indexOf(\"=\");\n const button = eq > 0 ? buttons.find((b) => b.id === value.slice(0, eq)) : undefined;\n if (!button) throw new CliError(\"usage\", `--style takes id=primary|neutral|danger for one of the buttons, not ${JSON.stringify(value)}`);\n button.style = value.slice(eq + 1);\n }\n if (options.once === true) for (const button of buttons) button.once = true;\n return { buttons, ...(options.id !== undefined && { buttonsId: String(options.id) }) };\n}\n\n/** A buttons card checked by the sender's rule, or a `bad_request` that says what is wrong. */\nfunction checked(card: Record<string, unknown>): ButtonsCard {\n const result = checkStatusCard(card);\n if (\"error\" in result) throw new CliError(\"bad_request\", `Buttons: ${result.error}`, { card });\n return result.card as ButtonsCard;\n}\n\n/**\n * The card a send's `buttons` (and `buttonsId`) make, or undefined when it names none. `buttons` is a list of\n * `{id, label, style?, once?}`; the id of the question is made up (`ask-…`) unless given.\n */\nexport function buttonsCard(params: Params): ButtonsCard | undefined {\n const buttons = params.buttons;\n if (buttons === undefined || buttons === null) {\n if (params.buttonsId !== undefined) throw new CliError(\"bad_request\", \"buttonsId goes with buttons\");\n return undefined;\n }\n if (!Array.isArray(buttons)) throw new CliError(\"bad_request\", \"buttons must be a list of {id, label, style?, once?}\");\n const id = str(params, \"buttonsId\") ?? `ask-${toBase64Url(randomBytes(6))}`;\n return checked({ kind: \"buttons\", id, buttons });\n}\n\n/** Whether a 1:1 contact's app shows and presses buttons: null until its app said what it offers. */\nexport function pressable(link: LinkView): boolean | null {\n const peer = link.sessionOffers?.peer;\n return peer ? peer.includes(BUTTONS_CAPABILITY) : null;\n}\n\n/** The message of mine with buttons that `button update` names, by its id here (or the id both sides know). */\nfunction buttonsMessage(message: StoredMessage | undefined, ref: string, group: boolean): StoredMessage & { card: ButtonsCard } {\n if (!message) throw new CliError(\"not_found\", `No message ${JSON.stringify(ref)} of yours in this ${group ? \"group\" : \"chat\"}: the messageId send gave`);\n if (message.card?.kind !== \"buttons\") throw new CliError(\"bad_request\", \"That message has no buttons: send one with send --button id:Label\");\n return message as StoredMessage & { card: ButtonsCard };\n}\n\n/**\n * `button update`: the answer shown on a question's buttons (`chosen`), the buttons closed (`close`), or a new text\n * (`text`), by editing the message with the same buttons. What is not given stays as it was.\n */\nasync function updateButtons(ctx: Parameters<Method>[0], params: Params): Promise<Record<string, unknown>> {\n const target = chatOrGroup(ctx, str(params, \"chat\", true));\n const ref = str(params, \"message\", true);\n const chosen = str(params, \"chosen\");\n const close = bool(params, \"close\");\n const newText = str(params, \"text\");\n if (chosen === undefined && !close && newText === undefined) throw new CliError(\"bad_request\", \"Nothing to update: give chosen, close or text\");\n // A new text is a message like any other: the same guard as `send` and `edit`.\n if (newText !== undefined && !bool(params, \"force\")) {\n const secret = findSecret(newText);\n if (secret) throw new CliError(\"confirm\", `The text looks like ${secret.kind === \"cashu\" ? \"a Cashu token (money anyone who reads it can take)\" : \"a secret (a seed or a private key)\"}; update with --force if you mean to`, { kind: secret.kind });\n }\n const message = buttonsMessage(await messageOf(ctx, target.linkId, ref, (m) => m.sender === \"me\"), ref, target.group);\n if ((message.edit?.seq ?? 0) >= STATUS_CARD_LIMITS.edits)\n throw new CliError(\"refused\", `This message took ${STATUS_CARD_LIMITS.edits} updates, the most one takes: ask again with a new send`, { messageId: message.id, edits: message.edit!.seq });\n const before = message.card;\n // Said here, with the buttons it has, as a press says it: the card's own check names only the field.\n if (chosen !== undefined && !before.buttons.some((b) => b.id === chosen))\n throw new CliError(\"bad_request\", `That message has no button ${JSON.stringify(chosen)}; its buttons: ${before.buttons.map((b) => b.id).join(\", \")}`, { card: before });\n const card = checked({ ...before, ...(chosen !== undefined && { chosen }), ...((close || before.closed) && { closed: true }) });\n const text = newText ?? message.text;\n const wait = oneOf(params, \"wait\", target.group ? [\"none\", \"sent\"] as const : [\"none\", \"confirmed\"] as const, \"none\");\n const ms = num(params, \"timeout\", 30, { min: 1, max: 3600 }) * 1000;\n const result = await node(ctx).editMessage({ linkId: target.linkId, messageId: message.id, text, card });\n if (result.error) throw new CliError(result.refused ? \"refused\" : \"unavailable\", result.error);\n let edited = await node(ctx).getMessage(target.linkId, message.id);\n if (wait === \"confirmed\" && edited?.edit?.pending) edited = await waitForEdit(ctx, target.linkId, message.id, ms);\n const edges = wait === \"sent\" && edited?.edit ? await waitForGroupFrame(ctx, target.id, message.id, edited.edit.seq, ms) : undefined;\n return {\n [target.group ? \"group\" : \"chat\"]: target.id, buttons: card.id, messageId: message.id, card, edits: edited?.edit?.seq ?? 0,\n ...(target.group ? { ...(edges !== undefined && { edges }) } : { confirmed: !!edited && !edited.edit?.pending }),\n };\n}\n\n/**\n * `button press`: the other side of a question, as the app's tap: a press on a button of someone else's message, by\n * its id here (or the id both sides know). The engine sends a reply whose text is the button's label, and refuses what\n * an app would: a button the message does not have, closed buttons, a second answer past `once`, a second press\n * within a second. A one-shot in a chat waits until the press went out, as `send` does.\n */\nasync function pressButton(ctx: Parameters<Method>[0], params: Params): Promise<Record<string, unknown>> {\n const target = chatOrGroup(ctx, str(params, \"chat\", true));\n const ref = str(params, \"message\", true);\n const buttonId = str(params, \"button\", true);\n const where = target.group ? \"group\" : \"chat\";\n const message = await messageOf(ctx, target.linkId, ref, (m) => m.sender !== \"me\");\n if (!message) throw new CliError(\"not_found\", `No message ${JSON.stringify(ref)} from someone else in this ${where}: the id its event or history gave`);\n if (message.card?.kind !== \"buttons\") throw new CliError(\"bad_request\", \"That message has no buttons\");\n const button = message.card.buttons.find((b) => b.id === buttonId);\n if (!button) throw new CliError(\"not_found\", `That message has no button ${JSON.stringify(buttonId)}; its buttons: ${message.card.buttons.map((b) => b.id).join(\", \")}`);\n const wait = target.group\n ? oneOf(params, \"wait\", [\"none\", \"sent\"] as const, \"none\")\n : oneOf(params, \"wait\", [\"none\", \"sent\", \"delivered\"] as const, ctx.mode === \"one-shot\" ? \"sent\" : \"none\");\n const ms = num(params, \"timeout\", 30, { min: 1, max: 3600 }) * 1000;\n const result = await node(ctx).pressButton({ linkId: target.linkId, messageId: message.id, buttonId });\n if (result.error) throw new CliError(result.refused ? \"refused\" : \"unavailable\", result.error, result.paced ? { paced: true } : undefined);\n const replyId = result.messageId ?? null;\n const answer = { [where]: target.id, messageId: message.id, button: button.id, label: button.label, replyId };\n if (!replyId) return answer;\n if (target.group) {\n const edges = wait === \"sent\" ? await waitForGroupFrame(ctx, target.id, replyId, undefined, ms) : node(ctx).groupTaken({ groupId: target.id, messageId: replyId });\n return { ...answer, edges };\n }\n const reply = wait === \"none\" ? await node(ctx).getMessage(target.linkId, replyId) : await waitForMessage(ctx, target.linkId, replyId, wait, ms);\n return { ...answer, delivery: reply?.delivery ?? null };\n}\n\nexport const BUTTON_METHODS: Record<string, Method> = {\n /** WISP 406 · Message Buttons: a press on a button of someone else's message, as the app's tap. */\n \"button.press\": pressButton,\n /** WISP 406 · Message Buttons: the answer chosen on a question of mine, its buttons closed, or its text changed. */\n \"button.update\": updateButtons,\n};\n"],"mappings":";;;;;;;AAkBA,IAAM,aAAa,IAAI,IAAI,SAAS,KAAK,MAAM,UAAU,CAAC,MAAM,KAAK,CAAC,CAAC;AACvE,IAAM,mBAAmB;CAAC;CAAI;CAAI;CAAI;CAAI;AAAE;;AAE5C,IAAM,kBAAkB;AAExB,IAAM,OAAO;AACb,IAAM,mBAAmB;AACzB,IAAM,MAAM;AACZ,IAAM,aAAa;AACnB,IAAM,iBAAiB;;AAEvB,IAAM,gBAAgB;AACtB,IAAM,cAAc;;;;;;AAOpB,IAAM,YAAY;AAElB,IAAM,cAAc,kBAAkB,MAAM;;;;;AAM5C,SAAgB,WAAW,MAAoC;CAC7D,MAAM,UAAU,KAAK,QAAQ,WAAW,EAAE;CAC1C,MAAM,WAAW,YAAY,OAAO,CAAC,IAAI,IAAI,CAAC,MAAM,OAAO;CAC3D,IAAI,SAAS,KAAK,WAAW,GAAG,OAAO,EAAE,MAAM,WAAW;CAC1D,IAAI,SAAS,MAAM,YAAY,KAAK,KAAK,OAAO,CAAC,GAAG,OAAO,EAAE,MAAM,OAAO;CAC1E,IAAI,SAAS,MAAM,YAAY,iBAAiB,KAAK,OAAO,KAAK,OAAO,OAAO,CAAC,GAAG,OAAO,EAAE,MAAM,cAAc;CAChH,IAAI,SAAS,KAAK,YAAY,GAAG,OAAO,EAAE,MAAM,UAAU;CAC1D,MAAM,SAAS,SAAS,IAAI,SAAS;CACrC,OAAO,OAAO,MAAM,UAAU,OAAO,SAAS,WAAW,MAAM,WAAW,IAAI,KAAK,OAAO,MAAM,UAAU,KAAK,KAAK;AACtH;;;;;;;;;;AAWA,SAAS,YAAY,MAAuB;CAC1C,MAAM,SAAS,KAAK,YAAY,CAAC,CAAC,MAAM,SAAS;CACjD,IAAI,CAAC,UAAU,OAAO,SAAS,IAAI,OAAO;CAC1C,IAAI,SAAS;CACb,IAAI,MAAgB,CAAC;CACrB,MAAM,YAAY,OAAe,WAAmB;EAClD,MAAM,SAAS,IAAI,MAAM,OAAO,QAAQ,MAAM;EAC9C,MAAM,yBAAS,IAAI,IAAoB;EACvC,KAAK,MAAM,QAAQ,QAAQ,OAAO,IAAI,OAAO,OAAO,IAAI,IAAI,KAAK,KAAK,CAAC;EACvE,IAAI,OAAO,SAAS,GAAG,OAAO;EAC9B,MAAM,CAAC,MAAM,SAAS,CAAC,GAAG,MAAM,CAAC,CAAC,QAAQ,GAAG,MAAO,EAAE,KAAK,EAAE,KAAK,IAAI,CAAE;EACxE,IAAI,QAAQ,SAAS,MAAM,IAAI,QAAQ,OAAO,QAAQ,IAAI,QAAQ,YAAY,OAAO,OAAO;EAC5F,OAAO,cAAc,MAAM;CAC7B;CACA,MAAM,cAAc;EAClB,IAAI,IAAI,UAAU,IAAI;GACpB,KAAK,MAAM,UAAU,kBACnB,KAAK,IAAI,QAAQ,GAAG,QAAQ,UAAU,IAAI,UAAU,SAAS,GAAG,SAAS,UACvE,IAAI,SAAS,OAAO,MAAM,GAAG,OAAO;GAGxC,IAAI,IAAI,WAAW,OAAO,UAAU,iBAAiB,SAAS,IAAI,MAAM,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,OAAO,GAAG,OAAO;EAC7G;EACA,MAAM,CAAC;EACP,OAAO;CACT;CACA,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,QAAQ,WAAW,IAAI,KAAK;EAClC,IAAI,UAAU,KAAA,GAAW,IAAI,KAAK,KAAK;OAClC,IAAI,MAAM,GAAG,OAAO;CAC3B;CACA,OAAO,MAAM;AACf;;AAGA,SAAS,cAAc,SAA4B;CACjD,MAAM,OAAO,QAAQ,SAAS,IAAI,eAAe,OAAO,IAAI,gBAAgB,OAAO,gBAAgB;CACnG,MAAM,QAAQ,IAAI,WAAW,KAAK,KAAK,OAAO,CAAC,CAAC;CAChD,QAAQ,SAAS,OAAO,MAAM;EAC5B,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,KAAK,IAAI,QAAS,KAAM,KAAK,GAAK;GAAE,MAAM,KAAK,IAAI,KAAK;GAAG,MAAM,MAAM,MAAM,QAAS,KAAK;EAAI;CACzH,CAAC;CAGD,OAFiB,OAAO,MAAM,SAAS,GAAG,YAAY,CAAC,CAAC,CAAC,MAAO,IAAI,iBACxD,MAAM,iBAAkB,IAAI;AAE1C;;AAGA,SAAS,OAAO,MAAuB;CACrC,KAAK,MAAM,CAAC,cAAc,KAAK,SAAS,GAAG,GACzC,IAAI;EACF,MAAM,UAAU,YAAY,OAAO,SAAS;EAC5C,KAAK,QAAQ,OAAO,OAAQ,QAAQ,OAAO,SAAU,QAAQ,WAAW,MAAO,QAAQ,WAAW,MAAM,QAAQ,QAAQ,IAAK,OAAO;CACtI,QAAQ,CAA0C;CAEpD,OAAO;AACT;;AAGA,SAAS,aAAa,MAAuB;CAC3C,KAAK,MAAM,SAAS,KAAK,SAAS,UAAU,GAAG;EAC7C,MAAM,SAAS,KAAK,MAAM,KAAK,IAAI,GAAG,MAAM,QAAQ,aAAa,GAAG,MAAM,KAAK;EAC/E,MAAM,QAAQ,KAAK,MAAM,MAAM,QAAQ,MAAM,EAAE,CAAC,QAAQ,MAAM,QAAQ,MAAM,EAAE,CAAC,SAAS,aAAa;EACrG,IAAI,eAAe,KAAK,MAAM,KAAK,eAAe,KAAK,KAAK,GAAG,OAAO;CACxE;CACA,OAAO;AACT;;AAGA,SAAS,UAAU,MAAoC;CACrD,MAAM,SAAS,CAAC,GAAG,KAAK,SAAS,WAAW,CAAC,CAAC,CAAC,KAAK,GAAG,SAAS,UAAU,YAAY,SAAsB,IAAI,CAAC;CACjH,IAAI,CAAC,OAAO,QAAQ,OAAO;CAC3B,MAAM,OAAO,OAAO,MAAM,UAAU,KAAK,CAAC,EAAE,QAAQ;CAEpD,OAAO;EAAE,MAAM;EAAS,QADP,OAAO,OAAO,UAAU,SAAS,MAAM,SAAS,IACjC,IAAW,OAAO,QAAQ,KAAK,UAAU,MAAM,MAAO,QAAQ,CAAC,IAAI;EAAM;CAAK;AAChH;;AAGA,SAAgB,YAAY,SAAoB,MAAuD;CACrG,IAAI;EACF,MAAM,QAAQ,WAAW,IAAI;EAC7B,IAAI,YAAY,KAAK;GACnB,MAAM,QAAQ,KAAK,MAAM,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC,CAAC,CAAC,OAAO,KAAK,CAAC;GAChF,OAAO,MAAM,MAAM,OAAO,SAAS,UAAU,MAAM,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,UAAU,MAAM,MAAM,GAAG,MAAM,IAAI;EAC3G;EACA,MAAM,QAAQ,WAAW,KAAK;EAC9B,OAAO,MAAM,OAAO,GAAG,SAAS,UAAU,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,UAAU,MAAM,CAAC,GAAG,OAAO,CAAC;CAC5F,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,MAAM,SAAgC,MAAwD;CACrG,IAAI,CAAC,SAAS,UAAU,CAAC,QAAQ,OAAO,MAAmB,OAAO,cAAc,CAAC,KAAM,KAAgB,CAAC,GAAG,OAAO;CAClH,MAAM,SAAS,QAAQ,QAAQ,KAAK,MAAM,MAAM,GAAG,CAAC;CACpD,OAAO,OAAO,cAAc,MAAM,IAAI;EAAE;EAAQ,MAAM,OAAO,SAAS,YAAY,OAAO,OAAO;CAAM,IAAI;AAC5G;AAEA,SAAS,WAAW,MAA0B;CAC5C,MAAM,WAAW,KAAK,QAAQ,MAAM,GAAG,CAAC,CAAC,QAAQ,MAAM,GAAG,CAAC,CAAC,QAAQ,OAAO,EAAE;CAC7E,MAAM,SAAS,KAAK,WAAW,IAAI,QAAQ,IAAK,SAAS,SAAS,KAAM,CAAC,CAAC;CAC1E,OAAO,WAAW,KAAK,SAAS,MAAM,EAAE,WAAW,CAAC,CAAC;AACvD;;AAGA,SAAS,WAAW,OAA4B;CAC9C,IAAI,KAAK,GAAG,QAAQ;CACpB,MAAM,OAAO,IAAI,SAAS,MAAM,QAAQ,MAAM,YAAY,MAAM,UAAU;CAC1E,MAAM,UAAU,SAAyB;EACvC,IAAI,OAAO,IAAI,OAAO;EACtB,MAAM,OAAO,SAAS,KAAK,IAAI,SAAS,KAAK,IAAI,SAAS,KAAK,IAAI,SAAS,KAAK,IAAI;EACrF,IAAI,CAAC,QAAQ,KAAK,OAAO,MAAM,QAAQ,MAAM,IAAI,MAAM,MAAM;EAC7D,IAAI,QAAQ;EACZ,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,KAAK,QAAQ,QAAQ,MAAM,MAAM,KAAK;EAChE,MAAM;EACN,OAAO;CACT;CACA,MAAM,aAAsB;EAC1B,IAAI,MAAM,MAAM,UAAU,EAAE,QAAQ,IAAI,MAAM,IAAI,MAAM,MAAM;EAC9D,MAAM,OAAO,MAAM,OAAO,QAAQ,QAAQ,GAAG,OAAO,OAAO;EAC3D,IAAI;GACF,IAAI,UAAU,GAAG;IACf,IAAI,SAAS,IAAI,OAAO;IACxB,IAAI,SAAS,IAAI,OAAO;IACxB,IAAI,SAAS,MAAM,SAAS,IAAI,OAAO;IACvC,IAAI,SAAS,IAAI;KAAE,MAAM;KAAG,OAAO;IAAK;IACxC,IAAI,SAAS,IAAI;KAAE,MAAM;KAAG,OAAO,KAAK,WAAW,KAAK,CAAC;IAAG;IAC5D,IAAI,SAAS,IAAI;KAAE,MAAM;KAAG,OAAO,KAAK,WAAW,KAAK,CAAC;IAAG;IAC5D,MAAM,IAAI,MAAM,MAAM;GACxB;GACA,MAAM,IAAI,OAAO,IAAI;GACrB,QAAQ,OAAR;IACE,KAAK,GAAG,OAAO;IACf,KAAK,GAAG,OAAO,KAAK;IACpB,KAAK;IAAG,KAAK,GAAG;KACd,IAAI,KAAK,IAAI,MAAM,QAAQ,MAAM,IAAI,MAAM,MAAM;KACjD,MAAM,QAAQ,MAAM,SAAS,IAAK,MAAM,CAAE;KAC1C,OAAO,UAAU,IAAI,QAAQ,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC,CAAC,CAAC,OAAO,KAAK;IACrF;IACA,KAAK;KACH,IAAI,IAAI,MAAM,SAAS,IAAI,MAAM,IAAI,MAAM,MAAM;KACjD,OAAO,MAAM,KAAK,EAAE,QAAQ,EAAE,GAAG,IAAI;IAEvC,KAAK,GAAG;KACN,IAAI,IAAI,MAAM,SAAS,IAAI,MAAM,IAAI,MAAM,MAAM;KACjD,MAAM,MAA+B,OAAO,OAAO,IAAI;KACvD,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK;MAAE,MAAM,MAAM,KAAK;MAAG,IAAI,OAAO,GAAG,KAAK,KAAK;KAAG;KAC7E,OAAO;IACT;IACA,SAAS,OAAO,KAAK;GACvB;EACF,UAAU;GAAE;EAAS;CACvB;CACA,OAAO,KAAK;AACd;;;AChNA,IAAM,gBAAwC;CAAE,SAAS;CAAG,SAAS;CAAG,QAAQ;CAAG,MAAM;CAAG,MAAM;CAAG,WAAW;AAAE;;AAGlH,eAAsB,YAAY,KAAiB,MAAc,WAAmB,IAAoC;CACtH,MAAM,QAAQ,KAAK,IAAI,IAAI;CAC3B,SAAS;EACP,MAAM,UAAU,MAAM,KAAK,GAAG,CAAC,CAAC,WAAW,MAAM,SAAS;EAC1D,IAAI,CAAC,SAAS,MAAM,IAAI,SAAS,aAAa,cAAc,UAAU,cAAc;EACpF,IAAI,CAAC,QAAQ,MAAM,SAAS,OAAO;EACnC,IAAI,KAAK,IAAI,KAAK,OAAO,MAAM,IAAI,SAAS,WAAW,mBAAmB,KAAK,MAAM,KAAK,GAAI,EAAE,0JAA0J;GAAE;GAAW,OAAO,QAAQ,KAAK;EAAI,CAAC;EAChS,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,GAAG,CAAC;CACzD;AACF;;;;;AAMA,eAAsB,kBAAkB,KAAiB,SAAiB,WAAmB,MAA0B,IAA6B;CAClJ,MAAM,QAAQ,KAAK,IAAI,IAAI;CAC3B,SAAS;EACP,MAAM,QAAQ,KAAK,GAAG,CAAC,CAAC,WAAW;GAAE;GAAS;GAAW,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;EAAG,CAAC;EACpF,IAAI,QAAQ,GAAG,OAAO;EACtB,IAAI,KAAK,IAAI,KAAK,OAAO,MAAM,IAAI,SAAS,WAAW,mBAAmB,KAAK,MAAM,KAAK,GAAI,EAAE,gHAAgH;GAAE;GAAW,GAAI,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;EAAG,CAAC;EAC/P,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,GAAG,CAAC;CACzD;AACF;;AAGA,eAAsB,eAAe,KAAiB,MAAc,WAAmB,QAA8B,IAAoC;CACvJ,MAAM,OAAO,cAAc;CAC3B,MAAM,QAAQ,YAAuC;EACnD,IAAI,CAAC,SAAS,OAAO,KAAA;EACrB,MAAM,WAAW,QAAQ,YAAY;EACrC,IAAI,aAAa,UAAU,MAAM,IAAI,SAAS,UAAU,QAAQ,iBAAiB,iCAAiC;GAAE;GAAW;EAAS,CAAC;EACzI,QAAQ,cAAc,aAAa,MAAM,OAAO,UAAU,KAAA;CAC5D;CACA,OAAO,IAAI,SAAwB,SAAS,WAAW;EACrD,IAAI,UAAU;EACd,MAAM,UAAU,OAAgB,YAA4B;GAC1D,IAAI,SAAS;GACb,UAAU;GAAM,aAAa,KAAK;GAAG,IAAI;GACzC,IAAI,OAAO,OAAO,KAAK;QAAQ,QAAQ,OAAQ;EACjD;EACA,MAAM,SAAS,OAAO,UAAU,KAAK,KAAK,GAAG,CAAC,CAAC,WAAW,MAAM,SAAS,CAAC,CAAC,MAAM,YAAY;GAC3F,IAAI;IACF,MAAM,OAAO,KAAK,OAAO;IACzB,IAAI,MAAM,OAAO,MAAM,IAAI;SACtB,IAAI,MAAM,OAAO,IAAI,SAAS,WAAW,mBAAmB,KAAK,MAAM,KAAK,GAAI,EAAE,qBAAqB,SAAS,YAAY,eAAe,yDAAyD;KAAE;KAAW,UAAU,SAAS,YAAY;IAAK,CAAC,CAAC;GAC9P,SAAS,OAAO;IAAE,OAAO,KAAK;GAAG;EACnC,GAAG,MAAM;EACT,MAAM,QAAQ,iBAAiB,MAAM,IAAI,GAAG,EAAE;EAG9C,MAAM,MAAM,IAAI,IAAI,SAAS,UAAwB;GACnD,IAAI,MAAM,SAAS,SAAS,MAAM,cAAc,aAAc,MAAM,SAAyC,OAAO,YAAY,MAAM;EACxI,CAAC;EACD,MAAM;CACR,CAAC;AACH;;;;;;;;;AChDA,SAAgB,UAAU,SAAoG;CAC5H,MAAM,QAAQ,MAAM,QAAQ,QAAQ,MAAM,IAAI,QAAQ,SAAqB,CAAC;CAC5E,IAAI,CAAC,MAAM,QAAQ;EACjB,KAAK,MAAM,QAAQ;GAAC;GAAS;GAAQ;EAAI,GACvC,IAAI,QAAQ,UAAU,KAAA,GAAW,MAAM,IAAI,SAAS,SAAS,KAAK,KAAK,6BAA6B;EACtG;CACF;CACA,MAAM,UAAqC,MAAM,KAAK,UAAU;EAC9D,MAAM,QAAQ,MAAM,QAAQ,GAAG;EAC/B,IAAI,QAAQ,KAAK,UAAU,MAAM,SAAS,GAAG,MAAM,IAAI,SAAS,SAAS,8CAA8C,KAAK,UAAU,KAAK,GAAG;EAC9I,OAAO;GAAE,IAAI,MAAM,MAAM,GAAG,KAAK;GAAG,OAAO,MAAM,MAAM,QAAQ,CAAC,CAAC,CAAC,KAAK;EAAE;CAC3E,CAAC;CAED,MAAM,QAAQ,iBAAiB,OAAO;CACtC,IAAI,OAAO;EACT,MAAM,CAAC,IAAI,SAAS,CAAC,KAAK,UAAU,MAAM,MAAM,GAAG,GAAG,KAAK,UAAU,MAAM,MAAM,KAAK,CAAC;EACvF,MAAM,IAAI,SAAS,SAAS,MAAM,SAAS,UACvC,YAAY,GAAG,iCAAiC,MAAM,yEACtD,YAAY,GAAG,0CAA0C,MAAM,2CAA2C;CAChH;CACA,KAAK,MAAM,SAAS,MAAM,QAAQ,QAAQ,KAAK,IAAI,QAAQ,QAAoB,CAAC,GAAG;EACjF,MAAM,KAAK,MAAM,QAAQ,GAAG;EAC5B,MAAM,SAAS,KAAK,IAAI,QAAQ,MAAM,MAAM,EAAE,OAAO,MAAM,MAAM,GAAG,EAAE,CAAC,IAAI,KAAA;EAC3E,IAAI,CAAC,QAAQ,MAAM,IAAI,SAAS,SAAS,uEAAuE,KAAK,UAAU,KAAK,GAAG;EACvI,OAAO,QAAQ,MAAM,MAAM,KAAK,CAAC;CACnC;CACA,IAAI,QAAQ,SAAS,MAAM,KAAK,MAAM,UAAU,SAAS,OAAO,OAAO;CACvE,OAAO;EAAE;EAAS,GAAI,QAAQ,OAAO,KAAA,KAAa,EAAE,WAAW,OAAO,QAAQ,EAAE,EAAE;CAAG;AACvF;;AAGA,SAAS,QAAQ,MAA4C;CAC3D,MAAM,SAAS,gBAAgB,IAAI;CACnC,IAAI,WAAW,QAAQ,MAAM,IAAI,SAAS,eAAe,YAAY,OAAO,SAAS,EAAE,KAAK,CAAC;CAC7F,OAAO,OAAO;AAChB;;;;;AAMA,SAAgB,YAAY,QAAyC;CACnE,MAAM,UAAU,OAAO;CACvB,IAAI,YAAY,KAAA,KAAa,YAAY,MAAM;EAC7C,IAAI,OAAO,cAAc,KAAA,GAAW,MAAM,IAAI,SAAS,eAAe,6BAA6B;EACnG;CACF;CACA,IAAI,CAAC,MAAM,QAAQ,OAAO,GAAG,MAAM,IAAI,SAAS,eAAe,sDAAsD;CAErH,OAAO,QAAQ;EAAE,MAAM;EAAW,IADvB,IAAI,QAAQ,WAAW,KAAK,OAAO,YAAY,YAAY,CAAC,CAAC;EAClC;CAAQ,CAAC;AACjD;;AAGA,SAAgB,UAAU,MAAgC;CACxD,MAAM,OAAO,KAAK,eAAe;CACjC,OAAO,OAAO,KAAK,SAAS,kBAAkB,IAAI;AACpD;;AAGA,SAAS,eAAe,SAAoC,KAAa,OAAuD;CAC9H,IAAI,CAAC,SAAS,MAAM,IAAI,SAAS,aAAa,cAAc,KAAK,UAAU,GAAG,EAAE,oBAAoB,QAAQ,UAAU,OAAO,0BAA0B;CACvJ,IAAI,QAAQ,MAAM,SAAS,WAAW,MAAM,IAAI,SAAS,eAAe,mEAAmE;CAC3I,OAAO;AACT;;;;;AAMA,eAAe,cAAc,KAA4B,QAAkD;CACzG,MAAM,SAAS,YAAY,KAAK,IAAI,QAAQ,QAAQ,IAAI,CAAC;CACzD,MAAM,MAAM,IAAI,QAAQ,WAAW,IAAI;CACvC,MAAM,SAAS,IAAI,QAAQ,QAAQ;CACnC,MAAM,QAAQ,KAAK,QAAQ,OAAO;CAClC,MAAM,UAAU,IAAI,QAAQ,MAAM;CAClC,IAAI,WAAW,KAAA,KAAa,CAAC,SAAS,YAAY,KAAA,GAAW,MAAM,IAAI,SAAS,eAAe,+CAA+C;CAE9I,IAAI,YAAY,KAAA,KAAa,CAAC,KAAK,QAAQ,OAAO,GAAG;EACnD,MAAM,SAAS,WAAW,OAAO;EACjC,IAAI,QAAQ,MAAM,IAAI,SAAS,WAAW,uBAAuB,OAAO,SAAS,UAAU,uDAAuD,qCAAqC,uCAAuC,EAAE,MAAM,OAAO,KAAK,CAAC;CACrP;CACA,MAAM,UAAU,eAAe,MAAM,UAAU,KAAK,OAAO,QAAQ,MAAM,MAAM,EAAE,WAAW,IAAI,GAAG,KAAK,OAAO,KAAK;CACpH,KAAK,QAAQ,MAAM,OAAO,MAAM,mBAAmB,OACjD,MAAM,IAAI,SAAS,WAAW,qBAAqB,mBAAmB,MAAM,0DAA0D;EAAE,WAAW,QAAQ;EAAI,OAAO,QAAQ,KAAM;CAAI,CAAC;CAC3L,MAAM,SAAS,QAAQ;CAEvB,IAAI,WAAW,KAAA,KAAa,CAAC,OAAO,QAAQ,MAAM,MAAM,EAAE,OAAO,MAAM,GACrE,MAAM,IAAI,SAAS,eAAe,8BAA8B,KAAK,UAAU,MAAM,EAAE,iBAAiB,OAAO,QAAQ,KAAK,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;CACxK,MAAM,OAAO,QAAQ;EAAE,GAAG;EAAQ,GAAI,WAAW,KAAA,KAAa,EAAE,OAAO;EAAI,IAAK,SAAS,OAAO,WAAW,EAAE,QAAQ,KAAK;CAAG,CAAC;CAC9H,MAAM,OAAO,WAAW,QAAQ;CAChC,MAAM,OAAO,MAAM,QAAQ,QAAQ,OAAO,QAAQ,CAAC,QAAQ,MAAM,IAAa,CAAC,QAAQ,WAAW,GAAY,MAAM;CACpH,MAAM,KAAK,IAAI,QAAQ,WAAW,IAAI;EAAE,KAAK;EAAG,KAAK;CAAK,CAAC,IAAI;CAC/D,MAAM,SAAS,MAAM,KAAK,GAAG,CAAC,CAAC,YAAY;EAAE,QAAQ,OAAO;EAAQ,WAAW,QAAQ;EAAI;EAAM;CAAK,CAAC;CACvG,IAAI,OAAO,OAAO,MAAM,IAAI,SAAS,OAAO,UAAU,YAAY,eAAe,OAAO,KAAK;CAC7F,IAAI,SAAS,MAAM,KAAK,GAAG,CAAC,CAAC,WAAW,OAAO,QAAQ,QAAQ,EAAE;CACjE,IAAI,SAAS,eAAe,QAAQ,MAAM,SAAS,SAAS,MAAM,YAAY,KAAK,OAAO,QAAQ,QAAQ,IAAI,EAAE;CAChH,MAAM,QAAQ,SAAS,UAAU,QAAQ,OAAO,MAAM,kBAAkB,KAAK,OAAO,IAAI,QAAQ,IAAI,OAAO,KAAK,KAAK,EAAE,IAAI,KAAA;CAC3H,OAAO;GACJ,OAAO,QAAQ,UAAU,SAAS,OAAO;EAAI,SAAS,KAAK;EAAI,WAAW,QAAQ;EAAI;EAAM,OAAO,QAAQ,MAAM,OAAO;EACzH,GAAI,OAAO,QAAQ,EAAE,GAAI,UAAU,KAAA,KAAa,EAAE,MAAM,EAAG,IAAI,EAAE,WAAW,CAAC,CAAC,UAAU,CAAC,OAAO,MAAM,QAAQ;CAChH;AACF;;;;;;;AAQA,eAAe,YAAY,KAA4B,QAAkD;CACvG,MAAM,SAAS,YAAY,KAAK,IAAI,QAAQ,QAAQ,IAAI,CAAC;CACzD,MAAM,MAAM,IAAI,QAAQ,WAAW,IAAI;CACvC,MAAM,WAAW,IAAI,QAAQ,UAAU,IAAI;CAC3C,MAAM,QAAQ,OAAO,QAAQ,UAAU;CACvC,MAAM,UAAU,MAAM,UAAU,KAAK,OAAO,QAAQ,MAAM,MAAM,EAAE,WAAW,IAAI;CACjF,IAAI,CAAC,SAAS,MAAM,IAAI,SAAS,aAAa,cAAc,KAAK,UAAU,GAAG,EAAE,6BAA6B,MAAM,mCAAmC;CACtJ,IAAI,QAAQ,MAAM,SAAS,WAAW,MAAM,IAAI,SAAS,eAAe,6BAA6B;CACrG,MAAM,SAAS,QAAQ,KAAK,QAAQ,MAAM,MAAM,EAAE,OAAO,QAAQ;CACjE,IAAI,CAAC,QAAQ,MAAM,IAAI,SAAS,aAAa,8BAA8B,KAAK,UAAU,QAAQ,EAAE,iBAAiB,QAAQ,KAAK,QAAQ,KAAK,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,GAAG;CACvK,MAAM,OAAO,OAAO,QAChB,MAAM,QAAQ,QAAQ,CAAC,QAAQ,MAAM,GAAY,MAAM,IACvD,MAAM,QAAQ,QAAQ;EAAC;EAAQ;EAAQ;CAAW,GAAY,IAAI,SAAS,aAAa,SAAS,MAAM;CAC3G,MAAM,KAAK,IAAI,QAAQ,WAAW,IAAI;EAAE,KAAK;EAAG,KAAK;CAAK,CAAC,IAAI;CAC/D,MAAM,SAAS,MAAM,KAAK,GAAG,CAAC,CAAC,YAAY;EAAE,QAAQ,OAAO;EAAQ,WAAW,QAAQ;EAAI;CAAS,CAAC;CACrG,IAAI,OAAO,OAAO,MAAM,IAAI,SAAS,OAAO,UAAU,YAAY,eAAe,OAAO,OAAO,OAAO,QAAQ,EAAE,OAAO,KAAK,IAAI,KAAA,CAAS;CACzI,MAAM,UAAU,OAAO,aAAa;CACpC,MAAM,SAAS;GAAG,QAAQ,OAAO;EAAI,WAAW,QAAQ;EAAI,QAAQ,OAAO;EAAI,OAAO,OAAO;EAAO;CAAQ;CAC5G,IAAI,CAAC,SAAS,OAAO;CACrB,IAAI,OAAO,OAAO;EAChB,MAAM,QAAQ,SAAS,SAAS,MAAM,kBAAkB,KAAK,OAAO,IAAI,SAAS,KAAA,GAAW,EAAE,IAAI,KAAK,GAAG,CAAC,CAAC,WAAW;GAAE,SAAS,OAAO;GAAI,WAAW;EAAQ,CAAC;EACjK,OAAO;GAAE,GAAG;GAAQ;EAAM;CAC5B;CACA,MAAM,QAAQ,SAAS,SAAS,MAAM,KAAK,GAAG,CAAC,CAAC,WAAW,OAAO,QAAQ,OAAO,IAAI,MAAM,eAAe,KAAK,OAAO,QAAQ,SAAS,MAAM,EAAE;CAC/I,OAAO;EAAE,GAAG;EAAQ,UAAU,OAAO,YAAY;CAAK;AACxD;AAEA,IAAa,iBAAyC;;CAEpD,gBAAgB;;CAEhB,iBAAiB;AACnB"}
@@ -87,6 +87,30 @@ var BINARY = 2147483648;
87
87
  var PREFACE = "ghostly/paired-chat/1";
88
88
  /** How long a closing channel waits for what it sent last to go out before the stream is destroyed anyway. */
89
89
  var CLOSE_FLUSH_MS = 300;
90
+ /**
91
+ * Bytes a channel may hold before `send` throws: above what every sender here lets itself queue (the CLI's 1 MiB high
92
+ * water mark and an app's quarter above it; the Desktop page's send budget over the sidecar's `SEND_ROOM`), so it
93
+ * trips only for one that ignores its own line.
94
+ */
95
+ var MAX_BUFFERED = 2097152;
96
+ /**
97
+ * `drained()` resolves once a channel holds no more than this: no more than the sidecar's `SEND_ROOM`, so its wait for
98
+ * room never spins on a `drained()` that resolves at once.
99
+ */
100
+ var DRAINED_AT = 65536;
101
+ /**
102
+ * How often a wait in `drained()` looks again. Each stream says `drain` only once its own queue is empty, which on a fast
103
+ * link comes first; on a slow one this finds the queue at `DRAINED_AT` before it runs dry.
104
+ */
105
+ var DRAIN_POLL_MS = 10;
106
+ /**
107
+ * What a connection's stream still holds: secret-stream's queue and the UDX stream's under it. Both are streamx
108
+ * streams, which count it in `_writableState.buffered` (hyperdht is pinned; hyperdhtEndpointBuffered.test.ts reads it).
109
+ * secret-stream has no `writableLength`: read from there, it was always 0.
110
+ */
111
+ function held(socket) {
112
+ return (socket._writableState?.buffered ?? 0) + (socket.rawStream?._writableState?.buffered ?? 0);
113
+ }
90
114
  /** Native UDP + NoiseSecretStream. The binding comes from the authenticated
91
115
  * stream itself, never from an application frame supplied by the other peer. */
92
116
  async function createHyperEndpoint(seed, options = {}) {
@@ -125,7 +149,7 @@ async function createHyperEndpoint(seed, options = {}) {
125
149
  const channel = {
126
150
  binary: true,
127
151
  get bufferedAmount() {
128
- return socket.writableLength || 0;
152
+ return held(socket);
129
153
  },
130
154
  get onMessage() {
131
155
  return reader;
@@ -143,24 +167,32 @@ async function createHyperEndpoint(seed, options = {}) {
143
167
  },
144
168
  send(data) {
145
169
  const binary = data instanceof Uint8Array;
146
- if (closed || !(binary || typeof data === "string") || !data.length || (binary ? data.length : Buffer.byteLength(data)) > 61440 || this.bufferedAmount > 122880) throw new Error("Native channel is closed or exceeds its frame budget");
170
+ if (closed || !(binary || typeof data === "string") || !data.length || (binary ? data.length : Buffer.byteLength(data)) > 61440 || this.bufferedAmount > MAX_BUFFERED) throw new Error("Native channel is closed or exceeds its frame budget");
147
171
  const payload = binary ? Buffer.from(data.buffer, data.byteOffset, data.byteLength) : Buffer.from(data);
148
172
  const header = Buffer.alloc(4);
149
173
  header.writeUInt32BE((binary ? BINARY : 0) + payload.length);
150
174
  socket.write(Buffer.concat([header, payload]));
151
175
  },
152
176
  drained() {
153
- if (!this.bufferedAmount) return Promise.resolve();
177
+ if (this.bufferedAmount <= DRAINED_AT) return Promise.resolve();
178
+ if (closed) return Promise.reject(/* @__PURE__ */ new Error("Native channel closed"));
154
179
  return new Promise((resolve, reject) => {
155
- const done = () => {
156
- socket.off("close", gone);
157
- resolve();
180
+ const raw = socket.rawStream;
181
+ const check = () => {
182
+ if (this.bufferedAmount <= DRAINED_AT) finish(resolve);
158
183
  };
159
- const gone = () => {
160
- socket.off("drain", done);
161
- reject(/* @__PURE__ */ new Error("Native channel closed"));
162
- };
163
- socket.once("drain", done);
184
+ const gone = () => finish(() => reject(/* @__PURE__ */ new Error("Native channel closed")));
185
+ const poll = setInterval(check, DRAIN_POLL_MS);
186
+ poll.unref?.();
187
+ function finish(settle) {
188
+ clearInterval(poll);
189
+ socket.off("drain", check);
190
+ raw?.off("drain", check);
191
+ socket.off("close", gone);
192
+ settle();
193
+ }
194
+ socket.on("drain", check);
195
+ raw?.on("drain", check);
164
196
  socket.once("close", gone);
165
197
  });
166
198
  },
@@ -298,4 +330,4 @@ async function createHyperEndpoint(seed, options = {}) {
298
330
  //#endregion
299
331
  export { MAX_FRAME, createHyperEndpoint };
300
332
 
301
- //# sourceMappingURL=endpoint-Dn7H8qE1.js.map
333
+ //# sourceMappingURL=endpoint-BOTFMpfS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint-BOTFMpfS.js","names":[],"sources":["../../../../native/transports/hyperdht/redial.mjs","../../../../native/transports/hyperdht/endpoint.mjs"],"sourcesContent":["/**\n * How long a HyperDHT dial whose stream opened may go without the contact's preface before it is dialled again.\n *\n * The first connection a freshly started process makes sometimes opens (the handshake went through the DHT and the\n * stream is connected to the right address) and then carries nothing: the contact never sees it, and UDX gives up\n * only after 13 s. A connection made again at once goes through in milliseconds. Found as a chat moved to HyperDHT\n * right after the contact's app restarted taking about 28 s in CI (the move's 8 s, then its 20 s retry), 1 run in 6;\n * reproduced with hyperdht alone under load (bug hunt r11h). On loopback the preface comes within milliseconds of\n * the stream opening, one round trip on the internet.\n */\nexport const PREFACE_WAIT_MS = 2000\n\n/**\n * The longest a stream that opened waits for the preface on a slow path: the 8 s a chat's move gives its dial. Past\n * it nobody is waiting for the dial made again, and UDX gives a silent stream up at 13 s anyway.\n */\nexport const PREFACE_MAX_WAIT_MS = 8000\n\n/**\n * How many of the stream's smoothed round trips the preface may take. A contact that waits for our first packet\n * before its side of the stream connects (we are behind a firewall) writes its preface one round trip after our\n * stream opened; a lost packet adds a retransmission timeout (at least one round trip more). Three keep a slow path's\n * preface from being dialled again for nothing (a fixed 2 s on a 3 s path made three dials) and leave every path\n * under 667 ms on the 2 s.\n */\nexport const RTT_FACTOR = 3\n\n/**\n * Before the stream has its first RTT sample, how many times the dial's own opening (from the dial to the stream\n * open) the preface may take. The opening is the DHT lookup plus the handshake through a DHT node, so it took at\n * least two round trips of the path, and the preface comes one round trip after the stream opened: once covers it\n * (on a 3 s path the first sample comes only 3 s after the stream opened, past the 2 s). It is not more because a\n * stalled stream never gets a sample, and its opening is mostly the lookup (1-3 s on the public DHT even for a near\n * contact): twice a 2.5 s opening would dial again only 7.5 s after the dial, past the move's 8 s once the second\n * dial's own opening is added. A dial that opened in milliseconds (loopback, a near contact) stays on the 2 s.\n */\nexport const OPENING_FACTOR = 1\n\n/**\n * The preface wait for a stream whose smoothed RTT is `rtt` ms and whose dial took `opening` ms to open: three round\n * trips once there is a sample, the opening itself before, between `floor` and `ceiling`.\n */\nexport function prefaceWait(rtt, opening = 0, floor = PREFACE_WAIT_MS, ceiling = PREFACE_MAX_WAIT_MS) {\n const wait = rtt > 0 ? RTT_FACTOR * rtt : OPENING_FACTOR * (opening || 0)\n return Math.min(ceiling, Math.max(floor, wait))\n}\n\n/**\n * At most this many dials for one `connect`: the first, and two more if they stalled, 2 s apart, all within the 8 s a\n * chat's move gives its dial. Under load a dial made again stalled too about once in 20 (bug hunt r11h).\n */\nexport const MAX_DIALS = 3\n\n/**\n * Dials with `dialOnce` until one brings the contact's preface. `dialOnce()` starts a connection and returns\n * `{ opened, ready, close }`: `opened` resolves true once the stream is open (the handshake is done), `ready` with the\n * bound channel once the contact's preface came, and `close()` drops it; `rtt()`, if given, is the stream's smoothed\n * RTT in ms so far, 0 before its first sample (UDX's `rawStream.rtt`). A dial that opened and brought no preface\n * within `waitMs`, or longer on a slow path (`prefaceWait`, at most `maxWaitMs`), is closed and made again, up to\n * `maxDials` in all. The RTT is read again whenever the wait would end, so a sample taken after the stream opened\n * counts. A dial that fails before it opened (the contact not found, refused) fails the whole connect at once, as\n * before: there is nothing to wait for.\n */\nexport function redial(dialOnce, { waitMs = PREFACE_WAIT_MS, maxWaitMs = PREFACE_MAX_WAIT_MS, maxDials = MAX_DIALS } = {}) {\n return new Promise((resolve, reject) => {\n let settled = false\n let dials = 0\n const attempt = () => {\n dials++\n const dialledAt = Date.now()\n const dial = dialOnce()\n let timer = null\n let stalled = false\n dial.opened.then(open => {\n if (!open || settled) return\n const openedAt = Date.now()\n const check = () => {\n if (settled) return\n const left = openedAt + prefaceWait(dial.rtt?.(), openedAt - dialledAt, waitMs, maxWaitMs) - Date.now()\n if (left > 0) { timer = setTimeout(check, left); return }\n if (dials >= maxDials) return\n stalled = true\n dial.close()\n attempt()\n }\n timer = setTimeout(check, waitMs)\n }, () => {})\n dial.ready.then(bound => {\n clearTimeout(timer)\n if (settled || stalled) { bound.channel.close(); return }\n settled = true\n resolve(bound)\n }, error => {\n clearTimeout(timer)\n // A dial closed here for stalling ends as closed: the next one carries the connect.\n if (settled || stalled) return\n settled = true\n reject(error)\n })\n }\n attempt()\n })\n}\n","import DHT from 'hyperdht'\nimport { redial } from './redial.mjs'\n\nexport const MAX_FRAME = 60 * 1024\n/** The top bit of a frame's length word: the frame is a binary message, not UTF-8 text (WISP 103). */\nconst BINARY = 0x80000000\nconst PREFACE = 'ghostly/paired-chat/1'\n/** How long a closing channel waits for what it sent last to go out before the stream is destroyed anyway. */\nconst CLOSE_FLUSH_MS = 300\n/**\n * Bytes a channel may hold before `send` throws: above what every sender here lets itself queue (the CLI's 1 MiB high\n * water mark and an app's quarter above it; the Desktop page's send budget over the sidecar's `SEND_ROOM`), so it\n * trips only for one that ignores its own line.\n */\nconst MAX_BUFFERED = 2 * 1024 * 1024\n/**\n * `drained()` resolves once a channel holds no more than this: no more than the sidecar's `SEND_ROOM`, so its wait for\n * room never spins on a `drained()` that resolves at once.\n */\nconst DRAINED_AT = 64 * 1024\n/**\n * How often a wait in `drained()` looks again. Each stream says `drain` only once its own queue is empty, which on a fast\n * link comes first; on a slow one this finds the queue at `DRAINED_AT` before it runs dry.\n */\nconst DRAIN_POLL_MS = 10\n\n/**\n * What a connection's stream still holds: secret-stream's queue and the UDX stream's under it. Both are streamx\n * streams, which count it in `_writableState.buffered` (hyperdht is pinned; hyperdhtEndpointBuffered.test.ts reads it).\n * secret-stream has no `writableLength`: read from there, it was always 0.\n */\nfunction held(socket) {\n return (socket._writableState?.buffered ?? 0) + (socket.rawStream?._writableState?.buffered ?? 0)\n}\n\n/** Native UDP + NoiseSecretStream. The binding comes from the authenticated\n * stream itself, never from an application frame supplied by the other peer. */\nexport async function createHyperEndpoint(seed, options = {}) {\n if (!(seed instanceof Uint8Array) || seed.length !== 32) throw new Error('Invalid HyperDHT seed')\n const keyPair = DHT.keyPair(Buffer.from(seed))\n const node = new DHT({ ...options, keyPair })\n const channels = new Set()\n let stopped = false\n let incomingHandler = null\n const waiting = []\n const descriptor = { publicKey: keyPair.publicKey.toString('hex') }\n\n /** `onChannel`: hears the channel as soon as it counts against the limit (a dial that stalls is dropped from it). */\n async function attach(socket, onChannel) {\n if (stopped || channels.size >= 2) { socket.destroy(); throw new Error('Native connection limit reached') }\n let reader = null\n let closeHandler = null\n let closed = false\n let ending = null\n let pending = []\n let buffer = Buffer.alloc(0)\n let preface = false\n let resolveOpen, rejectOpen\n const ready = new Promise((resolve, reject) => { resolveOpen = resolve; rejectOpen = reject })\n const timer = setTimeout(() => { socket.destroy(new Error('HyperDHT handshake timed out')) }, 20000)\n const channel = {\n // It carries binary messages (WISP 100 § Text and binary messages): a capability that needs them may be offered.\n binary: true,\n get bufferedAmount() { return held(socket) },\n get onMessage() { return reader },\n set onMessage(value) { reader = value; if (value) for (const text of pending.splice(0)) value(text) },\n get onClose() { return closeHandler },\n set onClose(value) { closeHandler = value; if (closed) value?.() },\n send(data) {\n const binary = data instanceof Uint8Array\n if (closed || !(binary || typeof data === 'string') || !data.length || (binary ? data.length : Buffer.byteLength(data)) > MAX_FRAME || this.bufferedAmount > MAX_BUFFERED)\n throw new Error('Native channel is closed or exceeds its frame budget')\n const payload = binary ? Buffer.from(data.buffer, data.byteOffset, data.byteLength) : Buffer.from(data)\n const header = Buffer.alloc(4); header.writeUInt32BE((binary ? BINARY : 0) + payload.length)\n socket.write(Buffer.concat([header, payload]))\n },\n drained() {\n if (this.bufferedAmount <= DRAINED_AT) return Promise.resolve()\n if (closed) return Promise.reject(new Error('Native channel closed'))\n return new Promise((resolve, reject) => {\n const raw = socket.rawStream\n const check = () => { if (this.bufferedAmount <= DRAINED_AT) finish(resolve) }\n const gone = () => finish(() => reject(new Error('Native channel closed')))\n const poll = setInterval(check, DRAIN_POLL_MS)\n poll.unref?.()\n function finish(settle) {\n clearInterval(poll); socket.off('drain', check); raw?.off('drain', check); socket.off('close', gone); settle()\n }\n socket.on('drain', check); raw?.on('drain', check); socket.once('close', gone)\n })\n },\n // What was sent last (a goodbye, as an app stops) goes out before the stream ends: destroyed at once, it was\n // dropped unsent. A contact that never finishes the stream has CLOSE_FLUSH_MS.\n close() {\n if (closed || ending) return ending ?? Promise.resolve()\n ending = new Promise(resolve => { socket.once('close', resolve) })\n const timer = setTimeout(() => socket.destroy(), CLOSE_FLUSH_MS)\n timer.unref?.()\n socket.end()\n return ending\n },\n }\n channels.add(channel)\n onChannel?.(channel)\n socket.on('error', error => rejectOpen(error))\n // The contact ended its side (its channel closed): this one ends too, so the stream closes now, not on its timer.\n socket.on('end', () => socket.end())\n socket.on('close', () => {\n closed = true; pending = []; clearTimeout(timer); channels.delete(channel)\n rejectOpen(new Error('Native channel closed')); closeHandler?.()\n })\n const opened = () => {\n if (socket.handshakeHash?.length !== 64 || socket.publicKey?.length !== 32 || socket.remotePublicKey?.length !== 32) {\n socket.destroy(new Error('Noise session binding unavailable')); return\n }\n channel.send(PREFACE)\n }\n socket.on('data', chunk => {\n // Secretstream chunks are bounded by its record layer. Process all frames\n // before retaining a partial one; never allocate based on an unchecked size.\n buffer = Buffer.concat([buffer, chunk])\n while (buffer.length >= 4) {\n const word = buffer.readUInt32BE(0)\n const binary = word >= BINARY, length = word % BINARY\n // A binary frame before the preface is another protocol.\n if (length === 0 || length > MAX_FRAME || (binary && !preface)) { socket.destroy(new Error('Invalid frame length')); return }\n if (buffer.length < 4 + length) break\n let text\n if (binary) text = new Uint8Array(buffer.subarray(4, 4 + length))\n else {\n try { text = new TextDecoder('utf-8', { fatal: true }).decode(buffer.subarray(4, 4 + length)) }\n catch { socket.destroy(new Error('Invalid UTF-8 frame')); return }\n }\n buffer = buffer.subarray(4 + length)\n if (!preface) {\n if (text !== PREFACE) { socket.destroy(new Error('Unexpected application protocol')); return }\n preface = true; clearTimeout(timer)\n resolveOpen({ channel, binding: { transport: 'hyperdht/1', context: socket.handshakeHash.toString('hex'),\n identities: [socket.publicKey.toString('hex'), socket.remotePublicKey.toString('hex')].sort() } })\n } else if (reader) reader(text)\n else if (pending.length < 64) pending.push(text)\n else { socket.destroy(new Error('Receive queue exceeded')); return }\n }\n })\n void socket.opened.then(ok => { if (ok) opened(); else rejectOpen(new Error('Noise handshake failed')) })\n return ready\n }\n\n const server = node.createServer(socket => {\n void attach(socket).then(bound => {\n if (stopped) bound.channel.close()\n else if (incomingHandler) incomingHandler(bound)\n else waiting.push(bound)\n }).catch(() => {})\n })\n try { await server.listen(keyPair) }\n catch (error) { await node.destroy(); throw error }\n return {\n transport: 'hyperdht/1', descriptor, onDescriptor: null,\n get onConnection() { return incomingHandler },\n set onConnection(value) { incomingHandler = value; if (value) for (const bound of waiting.splice(0)) value(bound) },\n async connect(address) {\n if (stopped || !address || !/^[a-f0-9]{64}$/.test(address.publicKey)) throw new Error('Invalid HyperDHT endpoint')\n const remote = Buffer.from(address.publicKey, 'hex')\n // A dial whose stream opened and carried nothing is made again (`redial.mjs`): it is dropped from the limit at\n // once, so the next one has its place even while another channel is live. The wait follows the stream's RTT.\n return redial(() => {\n const socket = node.connect(remote, { keyPair })\n let counted = null\n const ready = attach(socket, channel => { counted = channel })\n return { opened: socket.opened, ready, rtt: () => socket.rawStream?.rtt ?? 0,\n close() { if (counted) channels.delete(counted); socket.destroy() } }\n })\n },\n // The channels end first (their last frames out, CLOSE_FLUSH_MS at most): the node's socket goes with it.\n async close() { stopped = true; const ended = [...channels].map(channel => channel.close()); waiting.length = 0; await Promise.all(ended); await server.close(); await node.destroy() },\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import "./src-DhfGgAUu.js";
1
+ import "./src-DDzSERyc.js";
2
2
  //#region ../browser/src/backup/codec.ts
3
3
  function fromBase64Url(text) {
4
4
  if (!/^[A-Za-z0-9_-]*$/.test(text)) throw new Error("Invalid base64url");
@@ -87,4 +87,4 @@ async function open(text, passphrase) {
87
87
  //#endregion
88
88
  export { fromBase64Url as i, MAX_BACKUP_BYTES as n, open as r, BACKUP_MEDIA_TYPE as t };
89
89
 
90
- //# sourceMappingURL=envelope-BvU0fLEt.js.map
90
+ //# sourceMappingURL=envelope-WjingYSc.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"envelope-BvU0fLEt.js","names":[],"sources":["../../../browser/src/backup/codec.ts","../../../browser/src/backup/envelope.ts"],"sourcesContent":["import { toBase64Url } from \"@ghostly/core\";\n\n/**\n * Base64url without padding. Core's encoder, not `btoa`: every picture, voice note and kept file (up to 16 MiB each)\n * goes through it on the page's thread, where `btoa` spends over a second per 16 MiB.\n */\nexport { toBase64Url };\nexport function fromBase64Url(text: string): Uint8Array {\n if (!/^[A-Za-z0-9_-]*$/.test(text)) throw new Error(\"Invalid base64url\");\n const binary = atob(text.replace(/-/g, \"+\").replace(/_/g, \"/\") + \"=\".repeat((4 - (text.length % 4)) % 4));\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return bytes;\n}\n\ntype Tagged = { $ghostly: \"bigint\" | \"bytes\" | \"blob\"; value: string; type?: string };\n/** Data that happens to look like a tag (a contact can send anything) travels as its entries instead. */\ntype Escaped = { $ghostly: \"object\"; entries: [string, unknown][] };\n\n/**\n * Blobs cannot be read synchronously: turn them into tagged values first, everywhere in the tree. An\n * object of the data's own with a `$ghostly` key is escaped, so it comes back as it was and not as a tag.\n */\nasync function inlineBlobs(value: unknown): Promise<unknown> {\n if (typeof Blob !== \"undefined\" && value instanceof Blob) return { $ghostly: \"blob\", type: value.type, value: toBase64Url(new Uint8Array(await value.arrayBuffer())) } satisfies Tagged;\n if (Array.isArray(value)) return Promise.all(value.map(inlineBlobs));\n if (value && typeof value === \"object\" && !(value instanceof Uint8Array) && !(value instanceof ArrayBuffer) && !ArrayBuffer.isView(value)) {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) out[k] = await inlineBlobs(v);\n return Object.prototype.hasOwnProperty.call(out, \"$ghostly\") ? ({ $ghostly: \"object\", entries: Object.entries(out) } satisfies Escaped) : out;\n }\n return value;\n}\n\n/** JSON that also carries what IndexedDB holds: bigints, bytes and blobs (WISP 05). */\nexport async function encode(value: unknown): Promise<string> {\n return JSON.stringify(await inlineBlobs(value), (_key, v) => {\n if (typeof v === \"bigint\") return { $ghostly: \"bigint\", value: String(v) } satisfies Tagged;\n if (v instanceof Uint8Array) return { $ghostly: \"bytes\", value: toBase64Url(v) } satisfies Tagged;\n if (v instanceof ArrayBuffer) return { $ghostly: \"bytes\", value: toBase64Url(new Uint8Array(v)) } satisfies Tagged;\n return v;\n });\n}\n/** Never throws on a tag it cannot read: that value stays as it is, and the rest of the backup restores. */\nexport function decode(text: string): unknown {\n return JSON.parse(text, (_key, v) => {\n const escaped = v as Partial<Escaped> | null;\n if (escaped && typeof escaped === \"object\" && escaped.$ghostly === \"object\" && Array.isArray(escaped.entries)) {\n const out: Record<string, unknown> = {};\n for (const entry of escaped.entries) if (Array.isArray(entry) && typeof entry[0] === \"string\") Object.defineProperty(out, entry[0], { value: entry[1], enumerable: true, writable: true, configurable: true });\n return out;\n }\n const tag = v as Partial<Tagged> | null;\n if (!tag || typeof tag !== \"object\" || typeof tag.$ghostly !== \"string\" || typeof tag.value !== \"string\") return v;\n if (tag.$ghostly === \"bigint\") return /^-?\\d+$/.test(tag.value) ? BigInt(tag.value) : v;\n if (!/^[A-Za-z0-9_-]*$/.test(tag.value)) return v;\n if (tag.$ghostly === \"bytes\") return fromBase64Url(tag.value);\n if (tag.$ghostly === \"blob\") return new Blob([fromBase64Url(tag.value) as BlobPart], { type: typeof tag.type === \"string\" ? tag.type : \"\" });\n return v;\n });\n}\n","import { fromBase64Url, toBase64Url } from \"./codec\";\n\n/** The clear part of a bundle (WISP 05): only what decryption needs. */\nexport interface Envelope {\n format: \"ghostly-backup\";\n version: 1;\n kdf: { name: \"PBKDF2-SHA256\"; iterations: number; salt: string };\n cipher: { name: \"AES-256-GCM\"; iv: string };\n compression: \"gzip\" | \"none\";\n ciphertext: string;\n}\nexport const BACKUP_MEDIA_TYPE = \"application/vnd.ghostly.backup+json\";\nconst ITERATIONS = 600_000;\n\nasync function keyFrom(passphrase: string, salt: Uint8Array, iterations: number): Promise<CryptoKey> {\n const material = await crypto.subtle.importKey(\"raw\", new TextEncoder().encode(passphrase), \"PBKDF2\", false, [\"deriveKey\"]);\n return crypto.subtle.deriveKey({ name: \"PBKDF2\", hash: \"SHA-256\", salt: salt as BufferSource, iterations }, material, { name: \"AES-GCM\", length: 256 }, false, [\"encrypt\", \"decrypt\"]);\n}\n/** A profile with years of files is large, but not this large: past it, a bundle is refused, not read. */\nexport const MAX_BACKUP_BYTES = 1024 * 1024 * 1024;\n\nasync function through(bytes: Uint8Array, stream: CompressionStream | DecompressionStream): Promise<Uint8Array> {\n const reader = new Blob([bytes as BlobPart]).stream().pipeThrough(stream).getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n for (let next = await reader.read(); !next.done; next = await reader.read()) {\n size += next.value.length;\n if (size > MAX_BACKUP_BYTES) { await reader.cancel(); throw new Error(\"This backup is too large to restore\"); }\n chunks.push(next.value);\n }\n const out = new Uint8Array(size);\n let at = 0;\n for (const chunk of chunks) { out.set(chunk, at); at += chunk.length; }\n return out;\n}\n\n/**\n * The clear header is authenticated with the ciphertext (AES-GCM associated data): changing any of it,\n * the compression included, makes the bundle fail to open instead of being read differently.\n */\nconst header = (e: Pick<Envelope, \"format\" | \"version\" | \"kdf\" | \"cipher\" | \"compression\">) =>\n new TextEncoder().encode([e.format, e.version, e.kdf.name, e.kdf.iterations, e.kdf.salt, e.cipher.name, e.cipher.iv, e.compression].join(\"\\n\"));\nconst canCompress = () => typeof CompressionStream !== \"undefined\";\n\n/** Compresses and encrypts a payload under a passphrase the person chose. */\nexport async function seal(payload: string, passphrase: string): Promise<string> {\n if (passphrase.length < 12) throw new Error(\"Use at least 12 characters for the backup passphrase\");\n const salt = crypto.getRandomValues(new Uint8Array(16)), iv = crypto.getRandomValues(new Uint8Array(12));\n const plain = new TextEncoder().encode(payload);\n const compression = canCompress() ? \"gzip\" : \"none\";\n const body = compression === \"gzip\" ? await through(plain, new CompressionStream(\"gzip\")) : plain;\n const clear = { format: \"ghostly-backup\", version: 1, kdf: { name: \"PBKDF2-SHA256\", iterations: ITERATIONS, salt: toBase64Url(salt) }, cipher: { name: \"AES-256-GCM\", iv: toBase64Url(iv) }, compression } as const;\n const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: \"AES-GCM\", iv, additionalData: header(clear) as BufferSource }, await keyFrom(passphrase, salt, ITERATIONS), body as BufferSource));\n const envelope: Envelope = { ...clear, ciphertext: toBase64Url(ciphertext) };\n return JSON.stringify(envelope);\n}\n\n/** Checks, decrypts and decompresses a bundle. Nothing from a bundle is used before this succeeds. */\nexport async function open(text: string, passphrase: string): Promise<string> {\n if (text.length > MAX_BACKUP_BYTES * 1.4) throw new Error(\"This backup is too large to restore\");\n let envelope: Envelope;\n try { envelope = JSON.parse(text) as Envelope; } catch { throw new Error(\"This is not a Ghostly backup\"); }\n if (envelope?.format !== \"ghostly-backup\") throw new Error(\"This is not a Ghostly backup\");\n if (envelope.version !== 1) throw new Error(\"This backup comes from a newer Ghostly; update to restore it\");\n if (envelope.kdf?.name !== \"PBKDF2-SHA256\" || !(envelope.kdf.iterations >= ITERATIONS) || envelope.kdf.iterations > 10_000_000 || envelope.cipher?.name !== \"AES-256-GCM\" || ![\"gzip\", \"none\"].includes(envelope.compression)) throw new Error(\"Unsupported backup encryption\");\n let body: Uint8Array;\n try {\n body = new Uint8Array(await crypto.subtle.decrypt({ name: \"AES-GCM\", iv: fromBase64Url(envelope.cipher.iv) as BufferSource, additionalData: header(envelope) as BufferSource }, await keyFrom(passphrase, fromBase64Url(envelope.kdf.salt), envelope.kdf.iterations), fromBase64Url(envelope.ciphertext) as BufferSource));\n } catch { throw new Error(\"Wrong passphrase, or the backup was changed\"); }\n const plain = envelope.compression === \"gzip\" ? await through(body, new DecompressionStream(\"gzip\")) : body;\n return new TextDecoder().decode(plain);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"envelope-WjingYSc.js","names":[],"sources":["../../../browser/src/backup/codec.ts","../../../browser/src/backup/envelope.ts"],"sourcesContent":["import { toBase64Url } from \"@ghostly/core\";\n\n/**\n * Base64url without padding. Core's encoder, not `btoa`: every picture, voice note and kept file (up to 16 MiB each)\n * goes through it on the page's thread, where `btoa` spends over a second per 16 MiB.\n */\nexport { toBase64Url };\nexport function fromBase64Url(text: string): Uint8Array {\n if (!/^[A-Za-z0-9_-]*$/.test(text)) throw new Error(\"Invalid base64url\");\n const binary = atob(text.replace(/-/g, \"+\").replace(/_/g, \"/\") + \"=\".repeat((4 - (text.length % 4)) % 4));\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return bytes;\n}\n\ntype Tagged = { $ghostly: \"bigint\" | \"bytes\" | \"blob\"; value: string; type?: string };\n/** Data that happens to look like a tag (a contact can send anything) travels as its entries instead. */\ntype Escaped = { $ghostly: \"object\"; entries: [string, unknown][] };\n\n/**\n * Blobs cannot be read synchronously: turn them into tagged values first, everywhere in the tree. An\n * object of the data's own with a `$ghostly` key is escaped, so it comes back as it was and not as a tag.\n */\nasync function inlineBlobs(value: unknown): Promise<unknown> {\n if (typeof Blob !== \"undefined\" && value instanceof Blob) return { $ghostly: \"blob\", type: value.type, value: toBase64Url(new Uint8Array(await value.arrayBuffer())) } satisfies Tagged;\n if (Array.isArray(value)) return Promise.all(value.map(inlineBlobs));\n if (value && typeof value === \"object\" && !(value instanceof Uint8Array) && !(value instanceof ArrayBuffer) && !ArrayBuffer.isView(value)) {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) out[k] = await inlineBlobs(v);\n return Object.prototype.hasOwnProperty.call(out, \"$ghostly\") ? ({ $ghostly: \"object\", entries: Object.entries(out) } satisfies Escaped) : out;\n }\n return value;\n}\n\n/** JSON that also carries what IndexedDB holds: bigints, bytes and blobs (WISP 05). */\nexport async function encode(value: unknown): Promise<string> {\n return JSON.stringify(await inlineBlobs(value), (_key, v) => {\n if (typeof v === \"bigint\") return { $ghostly: \"bigint\", value: String(v) } satisfies Tagged;\n if (v instanceof Uint8Array) return { $ghostly: \"bytes\", value: toBase64Url(v) } satisfies Tagged;\n if (v instanceof ArrayBuffer) return { $ghostly: \"bytes\", value: toBase64Url(new Uint8Array(v)) } satisfies Tagged;\n return v;\n });\n}\n/** Never throws on a tag it cannot read: that value stays as it is, and the rest of the backup restores. */\nexport function decode(text: string): unknown {\n return JSON.parse(text, (_key, v) => {\n const escaped = v as Partial<Escaped> | null;\n if (escaped && typeof escaped === \"object\" && escaped.$ghostly === \"object\" && Array.isArray(escaped.entries)) {\n const out: Record<string, unknown> = {};\n for (const entry of escaped.entries) if (Array.isArray(entry) && typeof entry[0] === \"string\") Object.defineProperty(out, entry[0], { value: entry[1], enumerable: true, writable: true, configurable: true });\n return out;\n }\n const tag = v as Partial<Tagged> | null;\n if (!tag || typeof tag !== \"object\" || typeof tag.$ghostly !== \"string\" || typeof tag.value !== \"string\") return v;\n if (tag.$ghostly === \"bigint\") return /^-?\\d+$/.test(tag.value) ? BigInt(tag.value) : v;\n if (!/^[A-Za-z0-9_-]*$/.test(tag.value)) return v;\n if (tag.$ghostly === \"bytes\") return fromBase64Url(tag.value);\n if (tag.$ghostly === \"blob\") return new Blob([fromBase64Url(tag.value) as BlobPart], { type: typeof tag.type === \"string\" ? tag.type : \"\" });\n return v;\n });\n}\n","import { fromBase64Url, toBase64Url } from \"./codec\";\n\n/** The clear part of a bundle (WISP 05): only what decryption needs. */\nexport interface Envelope {\n format: \"ghostly-backup\";\n version: 1;\n kdf: { name: \"PBKDF2-SHA256\"; iterations: number; salt: string };\n cipher: { name: \"AES-256-GCM\"; iv: string };\n compression: \"gzip\" | \"none\";\n ciphertext: string;\n}\nexport const BACKUP_MEDIA_TYPE = \"application/vnd.ghostly.backup+json\";\nconst ITERATIONS = 600_000;\n\nasync function keyFrom(passphrase: string, salt: Uint8Array, iterations: number): Promise<CryptoKey> {\n const material = await crypto.subtle.importKey(\"raw\", new TextEncoder().encode(passphrase), \"PBKDF2\", false, [\"deriveKey\"]);\n return crypto.subtle.deriveKey({ name: \"PBKDF2\", hash: \"SHA-256\", salt: salt as BufferSource, iterations }, material, { name: \"AES-GCM\", length: 256 }, false, [\"encrypt\", \"decrypt\"]);\n}\n/** A profile with years of files is large, but not this large: past it, a bundle is refused, not read. */\nexport const MAX_BACKUP_BYTES = 1024 * 1024 * 1024;\n\nasync function through(bytes: Uint8Array, stream: CompressionStream | DecompressionStream): Promise<Uint8Array> {\n const reader = new Blob([bytes as BlobPart]).stream().pipeThrough(stream).getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n for (let next = await reader.read(); !next.done; next = await reader.read()) {\n size += next.value.length;\n if (size > MAX_BACKUP_BYTES) { await reader.cancel(); throw new Error(\"This backup is too large to restore\"); }\n chunks.push(next.value);\n }\n const out = new Uint8Array(size);\n let at = 0;\n for (const chunk of chunks) { out.set(chunk, at); at += chunk.length; }\n return out;\n}\n\n/**\n * The clear header is authenticated with the ciphertext (AES-GCM associated data): changing any of it,\n * the compression included, makes the bundle fail to open instead of being read differently.\n */\nconst header = (e: Pick<Envelope, \"format\" | \"version\" | \"kdf\" | \"cipher\" | \"compression\">) =>\n new TextEncoder().encode([e.format, e.version, e.kdf.name, e.kdf.iterations, e.kdf.salt, e.cipher.name, e.cipher.iv, e.compression].join(\"\\n\"));\nconst canCompress = () => typeof CompressionStream !== \"undefined\";\n\n/** Compresses and encrypts a payload under a passphrase the person chose. */\nexport async function seal(payload: string, passphrase: string): Promise<string> {\n if (passphrase.length < 12) throw new Error(\"Use at least 12 characters for the backup passphrase\");\n const salt = crypto.getRandomValues(new Uint8Array(16)), iv = crypto.getRandomValues(new Uint8Array(12));\n const plain = new TextEncoder().encode(payload);\n const compression = canCompress() ? \"gzip\" : \"none\";\n const body = compression === \"gzip\" ? await through(plain, new CompressionStream(\"gzip\")) : plain;\n const clear = { format: \"ghostly-backup\", version: 1, kdf: { name: \"PBKDF2-SHA256\", iterations: ITERATIONS, salt: toBase64Url(salt) }, cipher: { name: \"AES-256-GCM\", iv: toBase64Url(iv) }, compression } as const;\n const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: \"AES-GCM\", iv, additionalData: header(clear) as BufferSource }, await keyFrom(passphrase, salt, ITERATIONS), body as BufferSource));\n const envelope: Envelope = { ...clear, ciphertext: toBase64Url(ciphertext) };\n return JSON.stringify(envelope);\n}\n\n/** Checks, decrypts and decompresses a bundle. Nothing from a bundle is used before this succeeds. */\nexport async function open(text: string, passphrase: string): Promise<string> {\n if (text.length > MAX_BACKUP_BYTES * 1.4) throw new Error(\"This backup is too large to restore\");\n let envelope: Envelope;\n try { envelope = JSON.parse(text) as Envelope; } catch { throw new Error(\"This is not a Ghostly backup\"); }\n if (envelope?.format !== \"ghostly-backup\") throw new Error(\"This is not a Ghostly backup\");\n if (envelope.version !== 1) throw new Error(\"This backup comes from a newer Ghostly; update to restore it\");\n if (envelope.kdf?.name !== \"PBKDF2-SHA256\" || !(envelope.kdf.iterations >= ITERATIONS) || envelope.kdf.iterations > 10_000_000 || envelope.cipher?.name !== \"AES-256-GCM\" || ![\"gzip\", \"none\"].includes(envelope.compression)) throw new Error(\"Unsupported backup encryption\");\n let body: Uint8Array;\n try {\n body = new Uint8Array(await crypto.subtle.decrypt({ name: \"AES-GCM\", iv: fromBase64Url(envelope.cipher.iv) as BufferSource, additionalData: header(envelope) as BufferSource }, await keyFrom(passphrase, fromBase64Url(envelope.kdf.salt), envelope.kdf.iterations), fromBase64Url(envelope.ciphertext) as BufferSource));\n } catch { throw new Error(\"Wrong passphrase, or the backup was changed\"); }\n const plain = envelope.compression === \"gzip\" ? await through(body, new DecompressionStream(\"gzip\")) : body;\n return new TextDecoder().decode(plain);\n}\n"],"mappings":"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"}
@@ -33,6 +33,8 @@ function asCliError(error) {
33
33
  if (error instanceof CliError) return error;
34
34
  if (error && typeof error === "object" && "code" in error && "message" in error && typeof error.code === "string" && Object.hasOwn(EXIT, error.code)) return new CliError(error.code, String(error.message));
35
35
  if (error instanceof Error && error.code === "unreachable") return new CliError("unavailable", error.message);
36
+ const known = engineKnown(error);
37
+ if (known) return known;
36
38
  const local = localFileError(error);
37
39
  if (local) return local;
38
40
  const unopened = profileOpenError(error);
@@ -43,6 +45,32 @@ function asCliError(error) {
43
45
  return new CliError("engine", message);
44
46
  }
45
47
  /**
48
+ * The engine's known errors (`EngineError`, packages/core engineErrors.ts) a bot branches on, by their `engineCode`: a
49
+ * host or the network not answering is `unavailable` (try again later), one too slow `timeout`, a value the person gave
50
+ * `bad_request`, money that is not there `refused`. Any other keeps `engine`. Read by its shape, as `profileOpenError`.
51
+ */
52
+ var ENGINE_CODES = {
53
+ mintUnreachable: "unavailable",
54
+ lnurlUnreachable: "unavailable",
55
+ hostUnreachable: "unavailable",
56
+ networkUnreachable: "unavailable",
57
+ usdtRpcUnavailable: "unavailable",
58
+ hostTimedOut: "timeout",
59
+ networkTimedOut: "timeout",
60
+ invalidMintUrl: "bad_request",
61
+ mintNotHttps: "bad_request",
62
+ invalidMaxFee: "bad_request",
63
+ notEnoughSats: "refused",
64
+ ecashAlreadySpent: "refused"
65
+ };
66
+ function engineKnown(error) {
67
+ if (!(error instanceof Error)) return void 0;
68
+ const { engineCode, values } = error;
69
+ if (typeof engineCode !== "string" || !Object.hasOwn(ENGINE_CODES, engineCode)) return void 0;
70
+ if (engineCode === "lnurlUnreachable" && typeof values?.host === "string") return new CliError("unavailable", `Could not reach ${values.host}`);
71
+ return new CliError(ENGINE_CODES[engineCode], error.message);
72
+ }
73
+ /**
46
74
  * The profile's database did not open (the engine's `ProfileOpenError`, packages/browser/src/shared/idb.ts): `engine`
47
75
  * (exit 1) with the engine's own words, which name the versions when a newer ghostly stored it, and the reason and
48
76
  * the versions in `details` for a bot. Read by its shape: this module is also the thin client's, which loads no engine.
@@ -74,4 +102,4 @@ function localFileError(error) {
74
102
  //#endregion
75
103
  export { EXIT as n, asCliError as r, CliError as t };
76
104
 
77
- //# sourceMappingURL=errors-my6VJR_0.js.map
105
+ //# sourceMappingURL=errors-DO5z6h_l.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-DO5z6h_l.js","names":[],"sources":["../../src/errors.ts"],"sourcesContent":["/**\n * Errors that reach a bot: a stable `code` to branch on, a message for people, and the exit status the CLI\n * leaves with (WISP 1100 § Exit codes). The daemon sends `{code, message}` over the socket; the CLI prints\n * `{\"error\": {code, message}}` on stdout and exits with `exit`.\n */\nexport type ErrorCode =\n | \"bad_request\" | \"usage\" | \"not_found\" | \"refused\" | \"unavailable\" | \"confirm\" | \"timeout\"\n | \"engine\" | \"busy\" | \"internal\";\n\nexport const EXIT: Record<ErrorCode, number> = {\n engine: 1, refused: 1, unavailable: 1, bad_request: 1, busy: 1, internal: 1,\n usage: 2, not_found: 3, timeout: 4, confirm: 5,\n};\n\nexport class CliError extends Error {\n constructor(readonly code: ErrorCode, message: string, readonly details?: Record<string, unknown>) {\n super(message);\n this.name = \"CliError\";\n }\n toJSON(): { code: ErrorCode; message: string; details?: Record<string, unknown> } {\n return { code: this.code, message: this.message, ...(this.details ? { details: this.details } : {}) };\n }\n}\n\n/** Anything thrown, as the error a bot sees: the engine's own refusals keep their words. */\nexport function asCliError(error: unknown): CliError {\n if (error instanceof CliError) return error;\n if (error && typeof error === \"object\" && \"code\" in error && \"message\" in error && typeof (error as { code: unknown }).code === \"string\"\n && Object.hasOwn(EXIT, (error as { code: string }).code)) return new CliError((error as { code: ErrorCode }).code, String((error as { message: unknown }).message));\n // A contact the chat's link could not reach (its `GhostlyHttpError` \"unreachable\"): not there now, not the engine failing.\n if (error instanceof Error && (error as { code?: unknown }).code === \"unreachable\") return new CliError(\"unavailable\", error.message);\n const known = engineKnown(error);\n if (known) return known;\n const local = localFileError(error);\n if (local) return local;\n const unopened = profileOpenError(error);\n if (unopened) return unopened;\n const message = error instanceof Error ? error.message : String(error);\n if (/real money|confirmedReal|Mainnet/i.test(message) && /confirm/i.test(message)) return new CliError(\"confirm\", message);\n if (/other network|cross-network|network does not match/i.test(message)) return new CliError(\"refused\", message);\n return new CliError(\"engine\", message);\n}\n\n/**\n * The engine's known errors (`EngineError`, packages/core engineErrors.ts) a bot branches on, by their `engineCode`: a\n * host or the network not answering is `unavailable` (try again later), one too slow `timeout`, a value the person gave\n * `bad_request`, money that is not there `refused`. Any other keeps `engine`. Read by its shape, as `profileOpenError`.\n */\nconst ENGINE_CODES: Record<string, ErrorCode> = {\n mintUnreachable: \"unavailable\", lnurlUnreachable: \"unavailable\", hostUnreachable: \"unavailable\", networkUnreachable: \"unavailable\", usdtRpcUnavailable: \"unavailable\",\n hostTimedOut: \"timeout\", networkTimedOut: \"timeout\",\n invalidMintUrl: \"bad_request\", mintNotHttps: \"bad_request\", invalidMaxFee: \"bad_request\",\n notEnoughSats: \"refused\", ecashAlreadySpent: \"refused\",\n};\n\nfunction engineKnown(error: unknown): CliError | undefined {\n if (!(error instanceof Error)) return undefined;\n const { engineCode, values } = error as { engineCode?: unknown; values?: { host?: unknown } };\n if (typeof engineCode !== \"string\" || !Object.hasOwn(ENGINE_CODES, engineCode)) return undefined;\n // The app's reason for a Lightning address it could not read (a browser's CORS) is no reason in a terminal.\n if (engineCode === \"lnurlUnreachable\" && typeof values?.host === \"string\") return new CliError(\"unavailable\", `Could not reach ${values.host}`);\n return new CliError(ENGINE_CODES[engineCode], error.message);\n}\n\n/**\n * The profile's database did not open (the engine's `ProfileOpenError`, packages/browser/src/shared/idb.ts): `engine`\n * (exit 1) with the engine's own words, which name the versions when a newer ghostly stored it, and the reason and\n * the versions in `details` for a bot. Read by its shape: this module is also the thin client's, which loads no engine.\n */\nfunction profileOpenError(error: unknown): CliError | undefined {\n if (!(error instanceof Error) || error.name !== \"ProfileOpenError\") return undefined;\n const failure = (error as { failure?: { reason?: unknown; storedVersion?: unknown; supportedVersion?: unknown } }).failure;\n if (!failure || typeof failure.reason !== \"string\") return undefined;\n return new CliError(\"engine\", error.message, {\n reason: failure.reason === \"newer\" ? \"newer_profile\" : `profile_${failure.reason}`,\n ...(typeof failure.storedVersion === \"number\" ? { storedVersion: failure.storedVersion } : {}),\n ...(typeof failure.supportedVersion === \"number\" ? { supportedVersion: failure.supportedVersion } : {}),\n });\n}\n\n/**\n * A file or folder on this machine the command could not open (`file save --dir`, `profile backup --out`): the\n * person's path, not the engine refusing, so it says so with the code a bot branches on. Before, a folder that did\n * not exist was `engine` (exit 1) with Node's `ENOENT: no such file or directory, open '…'`.\n */\nfunction localFileError(error: unknown): CliError | undefined {\n if (!error || typeof error !== \"object\" || typeof (error as { syscall?: unknown }).syscall !== \"string\") return undefined;\n const { code, path } = error as { code?: unknown; path?: unknown };\n const where = typeof path === \"string\" ? path : \"the path\";\n if (code === \"ENOENT\" || code === \"ENOTDIR\") return new CliError(\"not_found\", `No such file or folder: ${where}`);\n if (code === \"EACCES\" || code === \"EPERM\" || code === \"EROFS\") return new CliError(\"refused\", `Not allowed to write or read ${where}`);\n if (code === \"EISDIR\") return new CliError(\"bad_request\", `${where} is a folder: name a file`);\n if (code === \"ENAMETOOLONG\") return new CliError(\"bad_request\", `A name in ${where} is too long for this file system (at most 255 bytes)`);\n return undefined;\n}\n"],"mappings":"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"}