@qntx/nostr 0.0.1 → 0.2.0

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 (187) hide show
  1. package/dist/abort-Ba8tDrOI.mjs +38 -0
  2. package/dist/abort-Ba8tDrOI.mjs.map +1 -0
  3. package/dist/{builder-BphJa-Y-.d.mts → builder-BWn55Paf.d.mts} +20 -14
  4. package/dist/{builder-q0VsvnYE.mjs → builder-CyrGKRVP.mjs} +32 -16
  5. package/dist/builder-CyrGKRVP.mjs.map +1 -0
  6. package/dist/{client-Bd8B1mYm.mjs → client-BWsX9Cv1.mjs} +354 -195
  7. package/dist/client-BWsX9Cv1.mjs.map +1 -0
  8. package/dist/client.d.mts +1 -1
  9. package/dist/client.mjs +1 -1
  10. package/dist/core.d.mts +8 -8
  11. package/dist/core.mjs +8 -9
  12. package/dist/error-BW7re1UN.d.mts +9 -0
  13. package/dist/error-B__OMBRY.mjs +14 -0
  14. package/dist/error-B__OMBRY.mjs.map +1 -0
  15. package/dist/{error-WEFS-R-4.d.mts → error-CR8Urc84.d.mts} +20 -8
  16. package/dist/{error-CwBxle6N.mjs → error-DrEIM7FO.mjs} +5 -3
  17. package/dist/error-DrEIM7FO.mjs.map +1 -0
  18. package/dist/{event-CzoQEFMF.mjs → event-D8Nfoh4K.mjs} +40 -32
  19. package/dist/event-D8Nfoh4K.mjs.map +1 -0
  20. package/dist/{event-B5D2jxt_.d.mts → event-fU3Y01PR.d.mts} +23 -20
  21. package/dist/{filter-BxwjEkZp.mjs → filter-BQl3GlHt.mjs} +42 -35
  22. package/dist/filter-BQl3GlHt.mjs.map +1 -0
  23. package/dist/filter-C0IMByE3.d.mts +37 -0
  24. package/dist/{gossip-C5RVB1UB.d.mts → gossip-BRQwIM_q.d.mts} +25 -23
  25. package/dist/{gossip-3xkRRCm_.mjs → gossip-oloDMWEj.mjs} +52 -34
  26. package/dist/gossip-oloDMWEj.mjs.map +1 -0
  27. package/dist/gossip.d.mts +1 -1
  28. package/dist/gossip.mjs +1 -1
  29. package/dist/{http-6vevbBlu.d.mts → http-C4HwIu4B.d.mts} +7 -7
  30. package/dist/{http-D2rhokZo.mjs → http-CuazNRIP.mjs} +10 -6
  31. package/dist/http-CuazNRIP.mjs.map +1 -0
  32. package/dist/index-B425CAcQ.d.mts +328 -0
  33. package/dist/index-BVyfUBuG.d.mts +181 -0
  34. package/dist/{index-BYMnhRIC.d.mts → index-Ba7bJ7DD.d.mts} +68 -50
  35. package/dist/{index-DaRyjo7O.d.mts → index-Boe8FshM.d.mts} +13 -7
  36. package/dist/index-CUIvFQdE.d.mts +73 -0
  37. package/dist/index-JdaS_uce.d.mts +207 -0
  38. package/dist/index.d.mts +22 -20
  39. package/dist/index.mjs +19 -18
  40. package/dist/{key-Dn-aX4x4.mjs → key-Bbtys6o7.mjs} +22 -14
  41. package/dist/key-Bbtys6o7.mjs.map +1 -0
  42. package/dist/{key-BiGxGLhg.d.mts → key-BlsYhN1V.d.mts} +9 -6
  43. package/dist/{kind-BLl4cLOc.mjs → kind-Cxwhhkx8.mjs} +2 -1
  44. package/dist/kind-Cxwhhkx8.mjs.map +1 -0
  45. package/dist/{loaders-BNQszDAn.mjs → loaders-o6YXB_Ri.mjs} +329 -305
  46. package/dist/loaders-o6YXB_Ri.mjs.map +1 -0
  47. package/dist/loaders.d.mts +3 -3
  48. package/dist/loaders.mjs +2 -2
  49. package/dist/memory-C2EVx0_O.mjs +46 -0
  50. package/dist/memory-C2EVx0_O.mjs.map +1 -0
  51. package/dist/{memory-BCmhZPek.mjs → memory-index-Do1mz0WF.mjs} +212 -82
  52. package/dist/memory-index-Do1mz0WF.mjs.map +1 -0
  53. package/dist/{message-C_Uj-Y-c.mjs → message-DcY6Fi9o.mjs} +38 -24
  54. package/dist/message-DcY6Fi9o.mjs.map +1 -0
  55. package/dist/{message-DJC-Jz6T.d.mts → message-iGPIqnn0.d.mts} +15 -8
  56. package/dist/{nip17-BWq-Kfq5.d.mts → nip17-DNEkz8rM.d.mts} +18 -17
  57. package/dist/{nip19-D7905oIv.d.mts → nip19-BevCJZsp.d.mts} +23 -3
  58. package/dist/{nip46-C_DoxiS_.d.mts → nip46-DDV4dC56.d.mts} +13 -12
  59. package/dist/{nip59-DljW_sYh.d.mts → nip59-zQgdLcEr.d.mts} +28 -25
  60. package/dist/{nip65-Di5hZRuV.d.mts → nip65-DBfh2smt.d.mts} +8 -3
  61. package/dist/{nip77-ByxlgoQO.d.mts → nip77-DH4RhS5r.d.mts} +9 -8
  62. package/dist/nips/blossom.d.mts +37 -37
  63. package/dist/nips/blossom.mjs +67 -52
  64. package/dist/nips/blossom.mjs.map +1 -1
  65. package/dist/nips/nip04.d.mts +2 -3
  66. package/dist/nips/nip04.mjs +6 -10
  67. package/dist/nips/nip04.mjs.map +1 -1
  68. package/dist/nips/nip05.d.mts +37 -43
  69. package/dist/nips/nip05.mjs +57 -57
  70. package/dist/nips/nip05.mjs.map +1 -1
  71. package/dist/nips/nip10.d.mts +18 -19
  72. package/dist/nips/nip10.mjs +76 -61
  73. package/dist/nips/nip10.mjs.map +1 -1
  74. package/dist/nips/nip11.d.mts +9 -8
  75. package/dist/nips/nip11.mjs +14 -11
  76. package/dist/nips/nip11.mjs.map +1 -1
  77. package/dist/nips/nip13.d.mts +9 -9
  78. package/dist/nips/nip13.mjs +15 -17
  79. package/dist/nips/nip13.mjs.map +1 -1
  80. package/dist/nips/nip17.d.mts +1 -1
  81. package/dist/nips/nip17.mjs +17 -12
  82. package/dist/nips/nip17.mjs.map +1 -1
  83. package/dist/nips/nip19.d.mts +1 -1
  84. package/dist/nips/nip19.mjs +52 -29
  85. package/dist/nips/nip19.mjs.map +1 -1
  86. package/dist/nips/nip21.d.mts +5 -6
  87. package/dist/nips/nip21.mjs +8 -7
  88. package/dist/nips/nip21.mjs.map +1 -1
  89. package/dist/nips/nip27.d.mts +7 -8
  90. package/dist/nips/nip27.mjs +26 -25
  91. package/dist/nips/nip27.mjs.map +1 -1
  92. package/dist/nips/nip42.d.mts +5 -6
  93. package/dist/nips/nip42.mjs +3 -3
  94. package/dist/nips/nip42.mjs.map +1 -1
  95. package/dist/nips/nip44.d.mts +24 -7
  96. package/dist/nips/nip44.mjs +25 -15
  97. package/dist/nips/nip44.mjs.map +1 -1
  98. package/dist/nips/nip46.d.mts +1 -1
  99. package/dist/nips/nip46.mjs +52 -49
  100. package/dist/nips/nip46.mjs.map +1 -1
  101. package/dist/nips/nip49.d.mts +8 -7
  102. package/dist/nips/nip49.mjs +16 -12
  103. package/dist/nips/nip49.mjs.map +1 -1
  104. package/dist/nips/nip51.d.mts +25 -27
  105. package/dist/nips/nip51.mjs +47 -36
  106. package/dist/nips/nip51.mjs.map +1 -1
  107. package/dist/nips/nip57.d.mts +12 -13
  108. package/dist/nips/nip57.mjs +35 -38
  109. package/dist/nips/nip57.mjs.map +1 -1
  110. package/dist/nips/nip59.d.mts +1 -1
  111. package/dist/nips/nip59.mjs +14 -18
  112. package/dist/nips/nip59.mjs.map +1 -1
  113. package/dist/nips/nip65.d.mts +1 -1
  114. package/dist/nips/nip65.mjs +11 -8
  115. package/dist/nips/nip65.mjs.map +1 -1
  116. package/dist/nips/nip77.d.mts +1 -1
  117. package/dist/nips/nip77.mjs +56 -34
  118. package/dist/nips/nip77.mjs.map +1 -1
  119. package/dist/nips/nip96.d.mts +21 -12
  120. package/dist/nips/nip96.mjs +59 -36
  121. package/dist/nips/nip96.mjs.map +1 -1
  122. package/dist/nips/nip98.d.mts +12 -12
  123. package/dist/nips/nip98.mjs +18 -19
  124. package/dist/nips/nip98.mjs.map +1 -1
  125. package/dist/{nostr_crypto_wasm_bg-hi5aXi5P.mjs → nostr_crypto_wasm_bg-Czrdty7Q.mjs} +1 -1
  126. package/dist/nostr_crypto_wasm_bg.wasm +0 -0
  127. package/dist/{pool-C_j1OmBB.mjs → pool-XmAGaufS.mjs} +611 -487
  128. package/dist/pool-XmAGaufS.mjs.map +1 -0
  129. package/dist/pool-tEr7c5bG.d.mts +302 -0
  130. package/dist/reactive-DDlThDSZ.mjs +324 -0
  131. package/dist/reactive-DDlThDSZ.mjs.map +1 -0
  132. package/dist/reactive-DFpqtIf_.d.mts +66 -0
  133. package/dist/relay.d.mts +5 -4
  134. package/dist/relay.mjs +2 -2
  135. package/dist/report-CHld4uWj.mjs +32 -0
  136. package/dist/report-CHld4uWj.mjs.map +1 -0
  137. package/dist/{signer-CZ6K6FUc.mjs → signer-CKR9EHce.mjs} +107 -72
  138. package/dist/signer-CKR9EHce.mjs.map +1 -0
  139. package/dist/signer.d.mts +3 -3
  140. package/dist/signer.mjs +3 -2
  141. package/dist/storage-xXxLGCVb.mjs +1376 -0
  142. package/dist/storage-xXxLGCVb.mjs.map +1 -0
  143. package/dist/storage.d.mts +4 -4
  144. package/dist/storage.mjs +5 -4
  145. package/dist/store.d.mts +2 -0
  146. package/dist/store.mjs +2 -0
  147. package/dist/testing.d.mts +125 -0
  148. package/dist/testing.mjs +957 -0
  149. package/dist/testing.mjs.map +1 -0
  150. package/dist/types-2pCUprRU.d.mts +47 -0
  151. package/dist/types-CRQEk0Oi.d.mts +19 -0
  152. package/dist/util-LfQirgDk.mjs +113 -0
  153. package/dist/util-LfQirgDk.mjs.map +1 -0
  154. package/dist/wasm.d.mts +4 -4
  155. package/dist/wasm.mjs +55 -56
  156. package/dist/wasm.mjs.map +1 -1
  157. package/dist/websocket-CmeqvAkp.d.mts +27 -0
  158. package/package.json +31 -9
  159. package/dist/builder-q0VsvnYE.mjs.map +0 -1
  160. package/dist/client-Bd8B1mYm.mjs.map +0 -1
  161. package/dist/error-B49rbDzT.d.mts +0 -7
  162. package/dist/error-CwBxle6N.mjs.map +0 -1
  163. package/dist/error-DZEs5OuZ.mjs +0 -24
  164. package/dist/error-DZEs5OuZ.mjs.map +0 -1
  165. package/dist/event-CzoQEFMF.mjs.map +0 -1
  166. package/dist/filter-BxwjEkZp.mjs.map +0 -1
  167. package/dist/filter-DUEKP3q1.d.mts +0 -40
  168. package/dist/gossip-3xkRRCm_.mjs.map +0 -1
  169. package/dist/http-D2rhokZo.mjs.map +0 -1
  170. package/dist/index-C0C4e-Gn.d.mts +0 -55
  171. package/dist/index-DgOugrpF.d.mts +0 -275
  172. package/dist/index-EwL6fHLo.d.mts +0 -267
  173. package/dist/index-k2YhCgsh.d.mts +0 -57
  174. package/dist/key-Dn-aX4x4.mjs.map +0 -1
  175. package/dist/kind-BLl4cLOc.mjs.map +0 -1
  176. package/dist/loaders-BNQszDAn.mjs.map +0 -1
  177. package/dist/memory-BCmhZPek.mjs.map +0 -1
  178. package/dist/message-C_Uj-Y-c.mjs.map +0 -1
  179. package/dist/pool-C_j1OmBB.mjs.map +0 -1
  180. package/dist/pool-DhRi7l-b.d.mts +0 -297
  181. package/dist/signer-CZ6K6FUc.mjs.map +0 -1
  182. package/dist/storage-DOy0iLRE.mjs +0 -870
  183. package/dist/storage-DOy0iLRE.mjs.map +0 -1
  184. package/dist/types-BzyADqnw.d.mts +0 -19
  185. package/dist/types-Dn24TPFs.d.mts +0 -43
  186. package/dist/util-6tfLQ9JF.mjs +0 -73
  187. package/dist/util-6tfLQ9JF.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"nip57.mjs","names":[],"sources":["../../src/nips/nip57.ts"],"sourcesContent":["/**\n * NIP-57 Lightning Zaps — zap request template (kind 9734) and receipt validation (kind 9735).\n * Does not fetch LNURL. Receipt checks never throw.\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/57.md\n */\nimport { sha256 } from \"@noble/hashes/sha2.js\";\nimport { bech32 } from \"@scure/base\";\nimport { EventValidationError } from \"../core/error.ts\";\nimport { validateSignedEvent, type Event, type EventTemplate } from \"../core/event.ts\";\nimport { verifyEvent } from \"../core/key.ts\";\nimport { isAddressableKind, Kind } from \"../core/kind.ts\";\nimport { eventAddress, getDTag, parseEventAddress, Tag } from \"../core/tag.ts\";\nimport { hexToBytes, utf8Encoder } from \"../core/util.ts\";\n\nexport type ProfileZapRequest = {\n pubkey: string;\n /** Amount in millisats. */\n amount: number;\n relays: readonly string[];\n comment?: string;\n lnurl?: string;\n};\n\nexport type EventZapRequest = {\n event: Event;\n /** Amount in millisats. */\n amount: number;\n relays: readonly string[];\n comment?: string;\n lnurl?: string;\n};\n\nexport function makeZapRequest(params: ProfileZapRequest | EventZapRequest): EventTemplate {\n if (params.relays.length === 0) {\n throw new EventValidationError(\"relays tag requires one or more URLs\");\n }\n\n const recipient = \"event\" in params ? params.event.pubkey : params.pubkey;\n const tags: Tag[] = [\n Tag.p(recipient),\n [\"amount\", params.amount.toString()],\n [\"relays\", ...params.relays],\n ];\n\n if (\"event\" in params) {\n const { event } = params;\n tags.push(Tag.e(event.id));\n if (isAddressableKind(event.kind)) {\n const d = getDTag(event.tags);\n if (d === undefined) throw new EventValidationError(\"d tag not found\");\n const addr = eventAddress(event);\n if (addr) tags.push([\"a\", addr]);\n }\n tags.push([\"k\", event.kind.toString()]);\n }\n\n if (params.lnurl) tags.push([\"lnurl\", params.lnurl]);\n\n return {\n kind: Kind.ZapRequest,\n created_at: Math.floor(Date.now() / 1000),\n content: params.comment ?? \"\",\n tags,\n };\n}\n\nexport type ZapReceiptContext = {\n nostrPubkey: string;\n lnurl?: string;\n};\n\nexport type ZapReceiptValidation = {\n valid: boolean;\n reason?: string;\n request?: Event;\n amountMsats?: number;\n};\n\nexport type Bolt11Fields = {\n amountMsats?: number;\n descriptionHash?: Uint8Array;\n paymentHash?: Uint8Array;\n};\n\nfunction firstTagValue(tags: readonly Tag[], name: string): string | undefined {\n for (const tag of tags) {\n if (tag[0] === name && tag[1] !== undefined) return tag[1];\n }\n return undefined;\n}\n\nfunction bytesEq(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n let x = 0;\n for (let i = 0; i < a.length; i++) x |= a[i]! ^ b[i]!;\n return x === 0;\n}\n\nfunction fail(reason: string): ZapReceiptValidation {\n return { valid: false, reason };\n}\n\nconst MSATS_PER_BTC = 100_000_000_000;\nconst MSATS_PER_MILLI = 100_000_000;\nconst MSATS_PER_MICRO = 100_000;\nconst MSATS_PER_NANO = 100;\n\n/** Amount is digits after `ln` + currency letters, optional m/u/n/p. Non-integer pico is omitted. */\nfunction amountMsatsFromHrp(hrp: string): number | undefined {\n if (!hrp.startsWith(\"ln\")) return undefined;\n let i = 2;\n while (i < hrp.length) {\n const c = hrp.charCodeAt(i);\n if (c < 97 || c > 122) break;\n i++;\n }\n if (i === hrp.length) return undefined;\n const rest = hrp.slice(i);\n const m = /^([0-9]+)([munp])?$/.exec(rest);\n if (!m) return undefined;\n const n = Number(m[1]);\n if (!Number.isSafeInteger(n)) return undefined;\n const mul = m[2];\n if (mul === undefined) return n * MSATS_PER_BTC;\n if (mul === \"m\") return n * MSATS_PER_MILLI;\n if (mul === \"u\") return n * MSATS_PER_MICRO;\n if (mul === \"n\") return n * MSATS_PER_NANO;\n // p: 0.1 msat; drop amounts that are not whole millisats\n if (n % 10 !== 0) return undefined;\n return n / 10;\n}\n\nfunction parseMsatsTag(value: string): number | undefined {\n if (!/^[0-9]+$/.test(value)) return undefined;\n const n = Number(value);\n return Number.isSafeInteger(n) ? n : undefined;\n}\n\nfunction countTags(tags: readonly Tag[], name: string): number {\n let n = 0;\n for (const tag of tags) {\n if (tag[0] === name) n++;\n }\n return n;\n}\n\nfunction hasHexTagValue(tags: readonly Tag[], name: string, value: string): boolean {\n const needle = value.toLowerCase();\n for (const tag of tags) {\n if (tag[0] === name && tag[1] !== undefined && tag[1].toLowerCase() === needle) return true;\n }\n return false;\n}\n\nfunction hasAddressTag(tags: readonly Tag[], value: string): boolean {\n const want = parseEventAddress(value);\n if (!want) return false;\n for (const tag of tags) {\n if (tag[0] !== \"a\" || tag[1] === undefined) continue;\n const got = parseEventAddress(tag[1]);\n if (\n got &&\n got.kind === want.kind &&\n got.pubkey === want.pubkey &&\n got.identifier === want.identifier\n ) {\n return true;\n }\n }\n return false;\n}\n\nconst BOLT11_TIMESTAMP_WORDS = 7;\nconst BOLT11_SIGNATURE_WORDS = 104;\nconst BOLT11_HASH_WORDS = 52;\nconst HASH_BYTES = 32;\n\n/** Decode a BOLT11 invoice. Never throws. Requires a 32-byte payment hash (type 1). */\nexport function parseBolt11(pr: string): Bolt11Fields | undefined {\n try {\n // Invoices exceed bech32's default 90-char limit.\n const { prefix, words } = bech32.decode(pr.toLowerCase() as `${string}1${string}`, false);\n if (!prefix.startsWith(\"ln\")) return undefined;\n // timestamp (7) + tagged fields + secp256k1 signature (104)\n if (words.length < BOLT11_TIMESTAMP_WORDS + BOLT11_SIGNATURE_WORDS) return undefined;\n const fields: Bolt11Fields = {};\n const amountMsats = amountMsatsFromHrp(prefix);\n if (amountMsats !== undefined) fields.amountMsats = amountMsats;\n const tlvEnd = words.length - BOLT11_SIGNATURE_WORDS;\n let i = BOLT11_TIMESTAMP_WORDS;\n while (i + 3 <= tlvEnd) {\n const type = words[i]!;\n const dataLen = (words[i + 1]! << 5) | words[i + 2]!;\n i += 3;\n if (i + dataLen > tlvEnd) break;\n const data = words.slice(i, i + dataLen);\n i += dataLen;\n if ((type !== 1 && type !== 23) || dataLen !== BOLT11_HASH_WORDS) continue;\n const bytes = bech32.fromWordsUnsafe(data);\n if (!bytes || bytes.length !== HASH_BYTES) continue;\n if (type === 1) fields.paymentHash ??= bytes;\n else fields.descriptionHash ??= bytes;\n }\n if (!fields.paymentHash) return undefined;\n return fields;\n } catch {\n return undefined;\n }\n}\n\n/** Parse the `description` tag of a kind 9735 receipt as a signed kind 9734. Never throws. */\nexport function parseZapRequestFromReceipt(receipt: Event): Event | undefined {\n try {\n const raw = firstTagValue(receipt.tags, \"description\");\n if (raw === undefined) return undefined;\n const parsed: unknown = JSON.parse(raw);\n if (!validateSignedEvent(parsed) || parsed.kind !== Kind.ZapRequest) return undefined;\n if (!verifyEvent(parsed)) return undefined;\n return parsed;\n } catch {\n return undefined;\n }\n}\n\n/** Validate a kind 9735 zap receipt. Never throws. */\nexport function validateZapReceipt(receipt: Event, ctx: ZapReceiptContext): ZapReceiptValidation {\n try {\n if (receipt.kind !== Kind.Zap || !verifyEvent(receipt)) {\n return fail(\"invalid receipt\");\n }\n if (receipt.pubkey.toLowerCase() !== ctx.nostrPubkey.toLowerCase()) {\n return fail(\"pubkey mismatch\");\n }\n\n const descriptionRaw = firstTagValue(receipt.tags, \"description\");\n const request = parseZapRequestFromReceipt(receipt);\n if (descriptionRaw === undefined || !request) return fail(\"invalid zap request\");\n if (countTags(request.tags, \"p\") !== 1) return fail(\"invalid p count\");\n if (countTags(request.tags, \"e\") > 1) return fail(\"too many e tags\");\n if (!firstTagValue(request.tags, \"relays\")) return fail(\"missing relays\");\n\n for (const tag of request.tags) {\n if (tag[0] !== \"a\") continue;\n if (tag[1] === undefined) return fail(\"invalid a\");\n const parsed = parseEventAddress(tag[1]);\n if (!parsed || !isAddressableKind(parsed.kind)) return fail(\"invalid a\");\n }\n\n // Request P is the LNURL provider (receipt pubkey), not the zap sender.\n const requestPCount = countTags(request.tags, \"P\");\n if (requestPCount > 1) return fail(\"too many P tags\");\n if (requestPCount === 1) {\n const requestP = firstTagValue(request.tags, \"P\");\n if (requestP === undefined || requestP.toLowerCase() !== receipt.pubkey.toLowerCase()) {\n return fail(\"request P mismatch\");\n }\n }\n\n const bolt11Tag = firstTagValue(receipt.tags, \"bolt11\");\n if (bolt11Tag === undefined) return fail(\"missing bolt11\");\n const bolt11 = parseBolt11(bolt11Tag);\n if (!bolt11 || !bolt11.descriptionHash) return fail(\"invalid bolt11\");\n\n const requestAmount = firstTagValue(request.tags, \"amount\");\n if (requestAmount !== undefined) {\n const msats = parseMsatsTag(requestAmount);\n if (msats === undefined || bolt11.amountMsats !== msats) return fail(\"amount mismatch\");\n }\n\n // Hash the tag payload, not JSON.stringify(parsed) (key order may differ).\n const digest = sha256(utf8Encoder.encode(descriptionRaw));\n if (!bytesEq(digest, bolt11.descriptionHash)) return fail(\"description hash mismatch\");\n\n const requestLnurl = firstTagValue(request.tags, \"lnurl\");\n if (requestLnurl !== undefined && ctx.lnurl !== undefined) {\n if (requestLnurl.toLowerCase() !== ctx.lnurl.toLowerCase()) return fail(\"lnurl mismatch\");\n }\n\n const preimageHex = firstTagValue(receipt.tags, \"preimage\");\n if (preimageHex !== undefined && bolt11.paymentHash) {\n let preimage: Uint8Array;\n try {\n preimage = hexToBytes(preimageHex);\n } catch {\n return fail(\"preimage mismatch\");\n }\n if (!bytesEq(sha256(preimage), bolt11.paymentHash)) return fail(\"preimage mismatch\");\n }\n\n const recipient = firstTagValue(request.tags, \"p\");\n if (recipient === undefined || !hasHexTagValue(receipt.tags, \"p\", recipient)) {\n return fail(\"missing p\");\n }\n const requestE = firstTagValue(request.tags, \"e\");\n if (requestE !== undefined && !hasHexTagValue(receipt.tags, \"e\", requestE)) {\n return fail(\"missing e\");\n }\n for (const tag of request.tags) {\n if (tag[0] !== \"a\" || tag[1] === undefined) continue;\n if (!hasAddressTag(receipt.tags, tag[1])) return fail(\"missing a\");\n }\n\n // Receipt P is the zap sender (request pubkey). Do not copy request tag P.\n for (const tag of receipt.tags) {\n if (tag[0] !== \"P\") continue;\n if (tag[1] === undefined || tag[1].toLowerCase() !== request.pubkey.toLowerCase()) {\n return fail(\"receipt P mismatch\");\n }\n }\n\n const amountMsats =\n bolt11.amountMsats ??\n (requestAmount !== undefined ? parseMsatsTag(requestAmount) : undefined);\n return amountMsats === undefined\n ? { valid: true, request }\n : { valid: true, request, amountMsats };\n } catch {\n return fail(\"invalid receipt\");\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"nip57.mjs","names":[],"sources":["../../src/nips/nip57.ts"],"sourcesContent":["/**\n * NIP-57 Lightning Zaps — zap request template (kind 9734) and receipt validation (kind 9735). Does\n * not fetch LNURL. Receipt checks never throw.\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/57.md\n */\nimport { equalBytes } from \"@noble/ciphers/utils.js\";\nimport { sha256 } from \"@noble/hashes/sha2.js\";\nimport { bech32 } from \"@scure/base\";\n\nimport { EventValidationError } from \"../core/error.ts\";\nimport { validateSignedEvent } from \"../core/event.ts\";\nimport type { Event, EventTemplate } from \"../core/event.ts\";\nimport { verifyEvent } from \"../core/key.ts\";\nimport { isAddressableKind, Kind } from \"../core/kind.ts\";\nimport { eventAddress, getDTag, parseEventAddress, Tag } from \"../core/tag.ts\";\nimport { hexToBytes, utf8Encoder } from \"../core/util.ts\";\n\nexport type ProfileZapRequest = {\n pubkey: string;\n /** Amount in millisats. */\n amount: number;\n relays: ReadonlyArray<string>;\n comment?: string;\n lnurl?: string;\n};\n\nexport type EventZapRequest = {\n event: Event;\n /** Amount in millisats. */\n amount: number;\n relays: ReadonlyArray<string>;\n comment?: string;\n lnurl?: string;\n};\n\nexport function makeZapRequest(params: ProfileZapRequest | EventZapRequest): EventTemplate {\n if (params.relays.length === 0) {\n throw new EventValidationError(\"relays tag requires one or more URLs\");\n }\n\n const recipient = \"event\" in params ? params.event.pubkey : params.pubkey;\n const tags: Tag[] = [\n Tag.p(recipient),\n [\"amount\", params.amount.toString()],\n [\"relays\", ...params.relays],\n ];\n\n if (\"event\" in params) {\n const { event } = params;\n tags.push(Tag.e(event.id));\n if (isAddressableKind(event.kind)) {\n const d = getDTag(event.tags);\n if (d === undefined) {\n throw new EventValidationError(\"d tag not found\");\n }\n const addr = eventAddress(event);\n if (addr !== undefined) {\n tags.push([\"a\", addr]);\n }\n }\n tags.push([\"k\", event.kind.toString()]);\n }\n\n if (params.lnurl !== undefined && params.lnurl !== \"\") {\n tags.push([\"lnurl\", params.lnurl]);\n }\n\n return {\n kind: Kind.ZapRequest,\n created_at: Math.floor(Date.now() / 1000),\n content: params.comment ?? \"\",\n tags,\n };\n}\n\nexport type ZapReceiptContext = {\n nostrPubkey: string;\n lnurl?: string;\n};\n\nexport type ZapReceiptValidation = {\n valid: boolean;\n reason?: string;\n request?: Event;\n amountMsats?: number;\n};\n\nexport type Bolt11Fields = {\n amountMsats?: number;\n descriptionHash?: Uint8Array;\n paymentHash?: Uint8Array;\n};\n\nfunction firstTagValue(tags: ReadonlyArray<Tag>, name: string): string | undefined {\n for (const tag of tags) {\n if (tag[0] === name && tag[1] !== undefined) {\n return tag[1];\n }\n }\n return undefined;\n}\n\nfunction fail(reason: string): ZapReceiptValidation {\n return { valid: false, reason };\n}\n\nconst MSATS_PER_BTC = 100_000_000_000;\nconst MSATS_PER_MILLI = 100_000_000;\nconst MSATS_PER_MICRO = 100_000;\nconst MSATS_PER_NANO = 100;\n\n/** Amount is digits after `ln` + currency letters, optional m/u/n/p. Non-integer pico is omitted. */\nfunction amountMsatsFromHrp(hrp: string): number | undefined {\n if (!hrp.startsWith(\"ln\")) {\n return undefined;\n }\n let i = 2;\n while (i < hrp.length) {\n const c = hrp.codePointAt(i);\n if (c === undefined || c < 97 || c > 122) {\n break;\n }\n i++;\n }\n if (i === hrp.length) {\n return undefined;\n }\n const rest = hrp.slice(i);\n const m = /^([0-9]+)([munp])?$/.exec(rest);\n if (!m) {\n return undefined;\n }\n const n = Number(m[1]);\n if (!Number.isSafeInteger(n)) {\n return undefined;\n }\n const mul = m.at(2);\n if (mul === undefined) {\n return n * MSATS_PER_BTC;\n }\n if (mul === \"m\") {\n return n * MSATS_PER_MILLI;\n }\n if (mul === \"u\") {\n return n * MSATS_PER_MICRO;\n }\n if (mul === \"n\") {\n return n * MSATS_PER_NANO;\n }\n // p: 0.1 msat; drop amounts that are not whole millisats\n if (n % 10 !== 0) {\n return undefined;\n }\n return n / 10;\n}\n\nfunction parseMsatsTag(value: string): number | undefined {\n if (!/^[0-9]+$/.test(value)) {\n return undefined;\n }\n const n = Number(value);\n return Number.isSafeInteger(n) ? n : undefined;\n}\n\nfunction countTags(tags: ReadonlyArray<Tag>, name: string): number {\n let n = 0;\n for (const tag of tags) {\n if (tag[0] === name) {\n n++;\n }\n }\n return n;\n}\n\nfunction hasHexTagValue(tags: ReadonlyArray<Tag>, name: string, value: string): boolean {\n const needle = value.toLowerCase();\n for (const tag of tags) {\n if (tag[0] === name && tag[1] !== undefined && tag[1].toLowerCase() === needle) {\n return true;\n }\n }\n return false;\n}\n\nfunction hasAddressTag(tags: ReadonlyArray<Tag>, value: string): boolean {\n const want = parseEventAddress(value);\n if (!want) {\n return false;\n }\n for (const tag of tags) {\n if (tag[0] !== \"a\" || tag[1] === undefined) {\n continue;\n }\n const got = parseEventAddress(tag[1]);\n if (\n got &&\n got.kind === want.kind &&\n got.pubkey === want.pubkey &&\n got.identifier === want.identifier\n ) {\n return true;\n }\n }\n return false;\n}\n\nconst BOLT11_TIMESTAMP_WORDS = 7;\nconst BOLT11_SIGNATURE_WORDS = 104;\nconst BOLT11_HASH_WORDS = 52;\nconst HASH_BYTES = 32;\n\n/** Decode a BOLT11 invoice. Never throws. Requires a 32-byte payment hash (type 1). */\nexport function parseBolt11(pr: string): Bolt11Fields | undefined {\n try {\n // Invoices exceed bech32's default 90-char limit.\n const { prefix, words } = bech32.decode(pr.toLowerCase(), false);\n if (!prefix.startsWith(\"ln\")) {\n return undefined;\n }\n // timestamp (7) + tagged fields + secp256k1 signature (104)\n if (words.length < BOLT11_TIMESTAMP_WORDS + BOLT11_SIGNATURE_WORDS) {\n return undefined;\n }\n const fields: Bolt11Fields = {};\n const amountMsats = amountMsatsFromHrp(prefix);\n if (amountMsats !== undefined) {\n fields.amountMsats = amountMsats;\n }\n const tlvEnd = words.length - BOLT11_SIGNATURE_WORDS;\n let i = BOLT11_TIMESTAMP_WORDS;\n while (i + 3 <= tlvEnd) {\n const type = words[i];\n const lenHigh = words[i + 1];\n const lenLow = words[i + 2];\n if (type === undefined || lenHigh === undefined || lenLow === undefined) {\n break;\n }\n const dataLen = lenHigh * 32 + lenLow;\n i += 3;\n if (i + dataLen > tlvEnd) {\n break;\n }\n const data = words.slice(i, i + dataLen);\n i += dataLen;\n if ((type !== 1 && type !== 23) || dataLen !== BOLT11_HASH_WORDS) {\n continue;\n }\n const bytes = bech32.fromWordsUnsafe(data);\n if (!bytes || bytes.length !== HASH_BYTES) {\n continue;\n }\n if (type === 1) {\n fields.paymentHash ??= bytes;\n } else {\n fields.descriptionHash ??= bytes;\n }\n }\n if (!fields.paymentHash) {\n return undefined;\n }\n return fields;\n } catch {\n return undefined;\n }\n}\n\n/** Parse the `description` tag of a kind 9735 receipt as a signed kind 9734. Never throws. */\nexport function parseZapRequestFromReceipt(receipt: Event): Event | undefined {\n try {\n const raw = firstTagValue(receipt.tags, \"description\");\n if (raw === undefined) {\n return undefined;\n }\n const parsed: unknown = JSON.parse(raw);\n if (!validateSignedEvent(parsed) || parsed.kind !== Kind.ZapRequest) {\n return undefined;\n }\n if (!verifyEvent(parsed)) {\n return undefined;\n }\n return parsed;\n } catch {\n return undefined;\n }\n}\n\n/** Validate a kind 9735 zap receipt. Never throws. */\nexport function validateZapReceipt(receipt: Event, ctx: ZapReceiptContext): ZapReceiptValidation {\n try {\n if (receipt.kind !== Kind.Zap || !verifyEvent(receipt)) {\n return fail(\"invalid receipt\");\n }\n if (receipt.pubkey.toLowerCase() !== ctx.nostrPubkey.toLowerCase()) {\n return fail(\"pubkey mismatch\");\n }\n\n const descriptionRaw = firstTagValue(receipt.tags, \"description\");\n const request = parseZapRequestFromReceipt(receipt);\n if (descriptionRaw === undefined || !request) {\n return fail(\"invalid zap request\");\n }\n if (countTags(request.tags, \"p\") !== 1) {\n return fail(\"invalid p count\");\n }\n if (countTags(request.tags, \"e\") > 1) {\n return fail(\"too many e tags\");\n }\n const relaysValue = firstTagValue(request.tags, \"relays\");\n if (relaysValue === undefined || relaysValue === \"\") {\n return fail(\"missing relays\");\n }\n\n for (const tag of request.tags) {\n if (tag[0] !== \"a\") {\n continue;\n }\n if (tag[1] === undefined) {\n return fail(\"invalid a\");\n }\n const parsed = parseEventAddress(tag[1]);\n if (!parsed || !isAddressableKind(parsed.kind)) {\n return fail(\"invalid a\");\n }\n }\n\n // Request P is the LNURL provider (receipt pubkey), not the zap sender.\n const requestPCount = countTags(request.tags, \"P\");\n if (requestPCount > 1) {\n return fail(\"too many P tags\");\n }\n if (requestPCount === 1) {\n const requestP = firstTagValue(request.tags, \"P\");\n if (requestP === undefined || requestP.toLowerCase() !== receipt.pubkey.toLowerCase()) {\n return fail(\"request P mismatch\");\n }\n }\n\n const bolt11Tag = firstTagValue(receipt.tags, \"bolt11\");\n if (bolt11Tag === undefined) {\n return fail(\"missing bolt11\");\n }\n const bolt11 = parseBolt11(bolt11Tag);\n if (!bolt11 || !bolt11.descriptionHash) {\n return fail(\"invalid bolt11\");\n }\n\n const requestAmount = firstTagValue(request.tags, \"amount\");\n if (requestAmount !== undefined) {\n const msats = parseMsatsTag(requestAmount);\n if (msats === undefined || bolt11.amountMsats !== msats) {\n return fail(\"amount mismatch\");\n }\n }\n\n // Hash the tag payload, not JSON.stringify(parsed) (key order may differ).\n const digest = sha256(utf8Encoder.encode(descriptionRaw));\n if (!equalBytes(digest, bolt11.descriptionHash)) {\n return fail(\"description hash mismatch\");\n }\n\n const requestLnurl = firstTagValue(request.tags, \"lnurl\");\n if (\n requestLnurl !== undefined &&\n ctx.lnurl !== undefined &&\n requestLnurl.toLowerCase() !== ctx.lnurl.toLowerCase()\n ) {\n return fail(\"lnurl mismatch\");\n }\n\n const preimageHex = firstTagValue(receipt.tags, \"preimage\");\n if (preimageHex !== undefined && bolt11.paymentHash) {\n let preimage: Uint8Array;\n try {\n preimage = hexToBytes(preimageHex);\n } catch {\n return fail(\"preimage mismatch\");\n }\n if (!equalBytes(sha256(preimage), bolt11.paymentHash)) {\n return fail(\"preimage mismatch\");\n }\n }\n\n const recipient = firstTagValue(request.tags, \"p\");\n if (recipient === undefined || !hasHexTagValue(receipt.tags, \"p\", recipient)) {\n return fail(\"missing p\");\n }\n const requestE = firstTagValue(request.tags, \"e\");\n if (requestE !== undefined && !hasHexTagValue(receipt.tags, \"e\", requestE)) {\n return fail(\"missing e\");\n }\n for (const tag of request.tags) {\n if (tag[0] !== \"a\" || tag[1] === undefined) {\n continue;\n }\n if (!hasAddressTag(receipt.tags, tag[1])) {\n return fail(\"missing a\");\n }\n }\n\n // Receipt P is the zap sender (request pubkey). Do not copy request tag P.\n for (const tag of receipt.tags) {\n if (tag[0] !== \"P\") {\n continue;\n }\n if (tag[1] === undefined || tag[1].toLowerCase() !== request.pubkey.toLowerCase()) {\n return fail(\"receipt P mismatch\");\n }\n }\n\n const amountMsats =\n bolt11.amountMsats ??\n (requestAmount === undefined ? undefined : parseMsatsTag(requestAmount));\n return amountMsats === undefined\n ? { valid: true, request }\n : { valid: true, request, amountMsats };\n } catch {\n return fail(\"invalid receipt\");\n }\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { _ as requireNip59Crypto, a as Rumor, b as wrap, c as TimestampRandomize, d as createRumor, f as createSeal, g as requireNip44Decryptor, h as randomPastTimestamp, i as Nip59Error, l as WrapOptions, m as isGiftWrapKind, n as Nip44Decryptor, o as SealOptions, p as eventToJson, r as Nip59Crypto, s as TWO_DAYS_SECS, t as GiftWrapTimestamps, u as createGiftWrap, v as rumorToJson, y as unwrap } from "../nip59-DljW_sYh.mjs";
1
+ import { _ as requireNip59Crypto, a as Rumor, b as wrap, c as TimestampRandomize, d as createRumor, f as createSeal, g as requireNip44Decryptor, h as randomPastTimestamp, i as Nip59Error, l as WrapOptions, m as isGiftWrapKind, n as Nip44Decryptor, o as SealOptions, p as eventToJson, r as Nip59Crypto, s as TWO_DAYS_SECS, t as GiftWrapTimestamps, u as createGiftWrap, v as rumorToJson, y as unwrap } from "../nip59-zQgdLcEr.mjs";
2
2
  export { GiftWrapTimestamps, Nip44Decryptor, Nip59Crypto, Nip59Error, Rumor, SealOptions, TWO_DAYS_SECS, TimestampRandomize, WrapOptions, createGiftWrap, createRumor, createSeal, eventToJson, isGiftWrapKind, randomPastTimestamp, requireNip44Decryptor, requireNip59Crypto, rumorToJson, unwrap, wrap };
@@ -1,19 +1,18 @@
1
- import { t as assertHex32 } from "../util-6tfLQ9JF.mjs";
2
- import { a as NostrError } from "../error-DZEs5OuZ.mjs";
3
- import { t as Kind } from "../kind-BLl4cLOc.mjs";
4
- import { f as validateEvent, m as Tag, p as validateSignedEvent, t as getEventHash } from "../event-CzoQEFMF.mjs";
5
- import { r as finalizeEvent, s as verifyEvent, t as Keys } from "../key-Dn-aX4x4.mjs";
1
+ import { c as isRecord, g as NostrError, n as assertHex32 } from "../util-LfQirgDk.mjs";
2
+ import { t as Kind } from "../kind-Cxwhhkx8.mjs";
3
+ import { h as Tag, m as validateSignedEvent, n as getEventHash, p as validateEvent } from "../event-D8Nfoh4K.mjs";
4
+ import { r as finalizeEvent, s as verifyEvent, t as Keys } from "../key-Bbtys6o7.mjs";
6
5
  import { encryptToPubkey } from "./nip44.mjs";
7
6
  import { randomBytes } from "@noble/hashes/utils.js";
8
7
  //#region src/nips/nip59.ts
9
8
  /**
10
- * NIP-59: Gift Wrap.
11
- * Rumor → NIP-44 seal (kind 13) → gift wrap (kind 1059).
12
- * Does not import signer, relay, or client. Crypto is a structural type.
9
+ * NIP-59: Gift Wrap. Rumor → NIP-44 seal (kind 13) → gift wrap (kind 1059). Does not import signer,
10
+ * relay, or client. Crypto is a structural type.
13
11
  *
14
12
  * @see https://github.com/nostr-protocol/nips/blob/master/59.md
15
13
  */
16
14
  var Nip59Error = class extends NostrError {
15
+ name = "Nip59Error";
17
16
  constructor(message, options) {
18
17
  super(message, options);
19
18
  }
@@ -28,16 +27,16 @@ function requireNip59Crypto(crypto) {
28
27
  const decrypt = crypto.nip44Decrypt;
29
28
  if (!encrypt || !decrypt) throw new Nip59Error("NIP-44 is required");
30
29
  return {
31
- getPublicKey: () => crypto.getPublicKey(),
32
- signEvent: (unsigned) => crypto.signEvent(unsigned),
33
- nip44Encrypt: (peer, plaintext) => encrypt.call(crypto, peer, plaintext),
34
- nip44Decrypt: (peer, payload) => decrypt.call(crypto, peer, payload)
30
+ getPublicKey: async () => crypto.getPublicKey(),
31
+ signEvent: async (unsigned) => crypto.signEvent(unsigned),
32
+ nip44Encrypt: async (peer, plaintext) => encrypt.call(crypto, peer, plaintext),
33
+ nip44Decrypt: async (peer, payload) => decrypt.call(crypto, peer, payload)
35
34
  };
36
35
  }
37
36
  function requireNip44Decryptor(crypto) {
38
37
  const decrypt = crypto.nip44Decrypt;
39
38
  if (!decrypt) throw new Nip59Error("NIP-44 is required");
40
- return { nip44Decrypt: (peer, payload) => decrypt.call(crypto, peer, payload) };
39
+ return { nip44Decrypt: async (peer, payload) => decrypt.call(crypto, peer, payload) };
41
40
  }
42
41
  function rumorToJson(rumor) {
43
42
  return JSON.stringify({
@@ -123,13 +122,10 @@ function parseJson(text) {
123
122
  throw new Nip59Error("invalid JSON", { cause: error });
124
123
  }
125
124
  }
126
- function isRecord(value) {
127
- return typeof value === "object" && value !== null && !Array.isArray(value);
128
- }
129
125
  function parseRumor(value) {
130
126
  if (!isRecord(value)) throw new Nip59Error("invalid rumor");
131
127
  if (Object.hasOwn(value, "sig")) throw new Nip59Error("rumor must be unsigned");
132
- const givenId = value.id;
128
+ const givenId = value["id"];
133
129
  if (!validateEvent(value)) throw new Nip59Error("invalid rumor");
134
130
  const unsigned = {
135
131
  kind: value.kind,
@@ -159,7 +155,7 @@ async function unwrap(crypto, giftWrap) {
159
155
  const sealRaw = parseJson(await decryptLayer(decryptor, giftWrap.pubkey, giftWrap.content));
160
156
  if (!validateSignedEvent(sealRaw)) throw new Nip59Error("expected seal");
161
157
  if (sealRaw.kind !== Kind.Seal) throw new Nip59Error("expected seal");
162
- if (sealRaw.tags.length !== 0) throw new Nip59Error("seal tags must be empty");
158
+ for (const tag of sealRaw.tags) if (tag[0] !== "expiration" || typeof tag[1] !== "string" || !/^\d+$/.test(tag[1])) throw new Nip59Error("seal tags must be empty");
163
159
  if (!verifyEvent(sealRaw)) throw new Nip59Error("seal signature");
164
160
  const rumor = parseRumor(parseJson(await decryptLayer(decryptor, sealRaw.pubkey, sealRaw.content)));
165
161
  if (sealRaw.pubkey.toLowerCase() !== rumor.pubkey.toLowerCase()) throw new Nip59Error("seal pubkey does not match rumor pubkey");
@@ -1 +1 @@
1
- {"version":3,"file":"nip59.mjs","names":["TagBuilder"],"sources":["../../src/nips/nip59.ts"],"sourcesContent":["/**\n * NIP-59: Gift Wrap.\n * Rumor → NIP-44 seal (kind 13) → gift wrap (kind 1059).\n * Does not import signer, relay, or client. Crypto is a structural type.\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/59.md\n */\nimport { randomBytes } from \"@noble/hashes/utils.js\";\nimport type { Event, UnsignedEvent } from \"../core/event.ts\";\nimport { getEventHash, validateEvent, validateSignedEvent } from \"../core/event.ts\";\nimport { NostrError } from \"../core/error.ts\";\nimport { Kind } from \"../core/kind.ts\";\nimport { Keys, finalizeEvent, verifyEvent } from \"../core/key.ts\";\nimport type { Tag } from \"../core/tag.ts\";\nimport { Tag as TagBuilder } from \"../core/tag.ts\";\nimport { assertHex32 } from \"../core/util.ts\";\nimport { encryptToPubkey } from \"./nip44.ts\";\n\n/** Unsigned event with a computed id. Never has `sig`. */\nexport type Rumor = UnsignedEvent & {\n readonly id: string;\n};\n\n/**\n * Structural crypto used by NIP-59.\n * Satisfied by NostrSigner when nip44Encrypt/nip44Decrypt are present.\n * This module must not import src/signer/.\n */\nexport type Nip59Crypto = {\n getPublicKey(): Promise<string>;\n signEvent(unsigned: UnsignedEvent): Promise<Event>;\n nip44Encrypt(peer: string, plaintext: string): Promise<string>;\n nip44Decrypt(peer: string, payload: string): Promise<string>;\n};\n\nexport type Nip44Decryptor = {\n nip44Decrypt(peer: string, payload: string): Promise<string>;\n};\n\nexport type GiftWrapTimestamps = {\n readonly seal: number;\n readonly wrap: number;\n};\n\n/** `\"wrap\"` randomizes only the gift wrap; `\"seal+wrap\"` (default) randomizes both. */\nexport type TimestampRandomize = \"wrap\" | \"seal+wrap\";\n\nexport type WrapOptions = {\n /** Unix seconds used as the randomization window end. Default: floor(Date.now()/1000). */\n readonly now?: number;\n /** Uniform integer in [0, maxExclusive). Default: CSPRNG via @noble/hashes randomBytes. */\n readonly randomInt?: (maxExclusive: number) => number;\n /** When set, used as-is. Overrides now/randomInt for this call. */\n readonly timestamps?: GiftWrapTimestamps;\n readonly relayHint?: string;\n /** Appended after the required wrap `p` tag. Never applied to the seal. */\n readonly extraTags?: readonly Tag[];\n /** Default `\"seal+wrap\"` (NIP-59). `\"wrap\"` = only wrap timestamp is randomized; seal uses rumor.created_at. */\n readonly randomize?: TimestampRandomize;\n};\n\nexport type SealOptions = Pick<WrapOptions, \"now\" | \"randomInt\" | \"timestamps\" | \"randomize\">;\n\nexport class Nip59Error extends NostrError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n }\n}\n\nexport const TWO_DAYS_SECS = 2 * 24 * 60 * 60;\n\nexport function isGiftWrapKind(kind: number): boolean {\n return kind === Kind.GiftWrap || kind === Kind.GiftWrapEphemeral;\n}\n\ntype Nip59CryptoInput = {\n getPublicKey(): Promise<string>;\n signEvent(unsigned: UnsignedEvent): Promise<Event>;\n nip44Encrypt?: (peer: string, plaintext: string) => Promise<string>;\n nip44Decrypt?: (peer: string, payload: string) => Promise<string>;\n};\n\n/** Narrow a signer-shaped object to {@link Nip59Crypto}, or throw. */\nexport function requireNip59Crypto(crypto: Nip59CryptoInput): Nip59Crypto {\n const encrypt = crypto.nip44Encrypt;\n const decrypt = crypto.nip44Decrypt;\n if (!encrypt || !decrypt) {\n throw new Nip59Error(\"NIP-44 is required\");\n }\n return {\n getPublicKey: () => crypto.getPublicKey(),\n signEvent: (unsigned) => crypto.signEvent(unsigned),\n nip44Encrypt: (peer, plaintext) => encrypt.call(crypto, peer, plaintext),\n nip44Decrypt: (peer, payload) => decrypt.call(crypto, peer, payload),\n };\n}\n\nexport function requireNip44Decryptor(crypto: {\n nip44Decrypt?: (peer: string, payload: string) => Promise<string>;\n}): Nip44Decryptor {\n const decrypt = crypto.nip44Decrypt;\n if (!decrypt) {\n throw new Nip59Error(\"NIP-44 is required\");\n }\n return {\n nip44Decrypt: (peer, payload) => decrypt.call(crypto, peer, payload),\n };\n}\n\nexport function rumorToJson(rumor: Rumor): string {\n return JSON.stringify({\n id: rumor.id,\n pubkey: rumor.pubkey,\n created_at: rumor.created_at,\n kind: rumor.kind,\n tags: rumor.tags,\n content: rumor.content,\n });\n}\n\nexport function eventToJson(event: Event): string {\n return JSON.stringify({\n id: event.id,\n pubkey: event.pubkey,\n created_at: event.created_at,\n kind: event.kind,\n tags: event.tags,\n content: event.content,\n sig: event.sig,\n });\n}\n\nfunction defaultRandomInt(maxExclusive: number): number {\n if (!Number.isSafeInteger(maxExclusive) || maxExclusive <= 0) {\n throw new Nip59Error(\"randomInt bound must be a positive integer\");\n }\n const bytes = randomBytes(4);\n const n = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0);\n return n % maxExclusive;\n}\n\nexport function randomPastTimestamp(opts?: {\n now?: number;\n randomInt?: (maxExclusive: number) => number;\n}): number {\n const now = opts?.now ?? Math.floor(Date.now() / 1000);\n const offset = (opts?.randomInt ?? defaultRandomInt)(TWO_DAYS_SECS);\n return now - offset;\n}\n\nexport function createRumor(\n pubkey: string,\n template: {\n kind: number;\n content?: string;\n tags?: readonly Tag[];\n created_at?: number;\n },\n): Rumor {\n const unsigned: UnsignedEvent = {\n kind: template.kind,\n content: template.content ?? \"\",\n tags: template.tags ? [...template.tags] : [],\n created_at: template.created_at ?? Math.floor(Date.now() / 1000),\n pubkey: assertHex32(pubkey, \"public key\"),\n };\n return { ...unsigned, id: getEventHash(unsigned) };\n}\n\nexport async function createSeal(\n crypto: Nip59Crypto,\n recipient: string,\n rumor: Rumor,\n opts?: SealOptions,\n): Promise<Event> {\n const sealer = requireNip59Crypto(crypto);\n const recipientPk = assertHex32(recipient, \"public key\");\n let content: string;\n try {\n content = await sealer.nip44Encrypt(recipientPk, rumorToJson(rumor));\n } catch (error) {\n throw new Nip59Error(\"failed to encrypt\", { cause: error });\n }\n const created_at =\n opts?.timestamps?.seal ??\n (opts?.randomize === \"wrap\" ? rumor.created_at : randomPastTimestamp(opts));\n const pubkey = await sealer.getPublicKey();\n return sealer.signEvent({\n kind: Kind.Seal,\n content,\n created_at,\n tags: [],\n pubkey,\n });\n}\n\nexport function createGiftWrap(seal: Event, recipient: string, opts?: WrapOptions): Event {\n const recipientPk = assertHex32(recipient, \"public key\");\n const ephemeral = Keys.generate();\n const content = encryptToPubkey(eventToJson(seal), ephemeral.secretKey.bytes, recipientPk);\n const created_at = opts?.timestamps?.wrap ?? randomPastTimestamp(opts);\n const tags: Tag[] = [TagBuilder.p(recipientPk, opts?.relayHint), ...(opts?.extraTags ?? [])];\n return finalizeEvent(\n {\n kind: Kind.GiftWrap,\n content,\n created_at,\n tags,\n },\n ephemeral.secretKey,\n );\n}\n\nexport async function wrap(\n crypto: Nip59Crypto,\n recipient: string,\n rumor: Rumor,\n opts?: WrapOptions,\n): Promise<Event> {\n const seal = await createSeal(crypto, recipient, rumor, opts);\n return createGiftWrap(seal, recipient, opts);\n}\n\nfunction parseJson(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch (error) {\n throw new Nip59Error(\"invalid JSON\", { cause: error });\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction parseRumor(value: unknown): Rumor {\n if (!isRecord(value)) {\n throw new Nip59Error(\"invalid rumor\");\n }\n if (Object.hasOwn(value, \"sig\")) {\n throw new Nip59Error(\"rumor must be unsigned\");\n }\n const givenId = value.id;\n if (!validateEvent(value)) {\n throw new Nip59Error(\"invalid rumor\");\n }\n const unsigned: UnsignedEvent = {\n kind: value.kind,\n tags: value.tags,\n content: value.content,\n created_at: value.created_at,\n pubkey: value.pubkey.toLowerCase(),\n };\n const expected = getEventHash(unsigned);\n if (typeof givenId === \"string\" && givenId.toLowerCase() !== expected) {\n throw new Nip59Error(\"invalid rumor\");\n }\n return { ...unsigned, id: expected };\n}\n\nasync function decryptLayer(\n crypto: Nip44Decryptor,\n peer: string,\n payload: string,\n): Promise<string> {\n try {\n return await crypto.nip44Decrypt(peer, payload);\n } catch (error) {\n throw new Nip59Error(\"failed to decrypt\", { cause: error });\n }\n}\n\nexport async function unwrap(crypto: Nip44Decryptor, giftWrap: Event): Promise<Rumor> {\n const decryptor = requireNip44Decryptor(crypto);\n if (!validateSignedEvent(giftWrap) || !isGiftWrapKind(giftWrap.kind)) {\n throw new Nip59Error(\"expected gift wrap\");\n }\n if (!verifyEvent(giftWrap)) {\n throw new Nip59Error(\"gift wrap signature\");\n }\n\n const sealJson = await decryptLayer(decryptor, giftWrap.pubkey, giftWrap.content);\n const sealRaw = parseJson(sealJson);\n if (!validateSignedEvent(sealRaw)) {\n throw new Nip59Error(\"expected seal\");\n }\n if (sealRaw.kind !== Kind.Seal) {\n throw new Nip59Error(\"expected seal\");\n }\n if (sealRaw.tags.length !== 0) {\n throw new Nip59Error(\"seal tags must be empty\");\n }\n if (!verifyEvent(sealRaw)) {\n throw new Nip59Error(\"seal signature\");\n }\n\n const rumorJson = await decryptLayer(decryptor, sealRaw.pubkey, sealRaw.content);\n const rumor = parseRumor(parseJson(rumorJson));\n if (sealRaw.pubkey.toLowerCase() !== rumor.pubkey.toLowerCase()) {\n throw new Nip59Error(\"seal pubkey does not match rumor pubkey\");\n }\n return rumor;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"nip59.mjs","names":["TagBuilder"],"sources":["../../src/nips/nip59.ts"],"sourcesContent":["/**\n * NIP-59: Gift Wrap. Rumor → NIP-44 seal (kind 13) → gift wrap (kind 1059). Does not import signer,\n * relay, or client. Crypto is a structural type.\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/59.md\n */\nimport { randomBytes } from \"@noble/hashes/utils.js\";\n\nimport { NostrError } from \"../core/error.ts\";\nimport type { Event, UnsignedEvent } from \"../core/event.ts\";\nimport { getEventHash, validateEvent, validateSignedEvent } from \"../core/event.ts\";\nimport { Keys, finalizeEvent, verifyEvent } from \"../core/key.ts\";\nimport { Kind } from \"../core/kind.ts\";\nimport type { Tag } from \"../core/tag.ts\";\nimport { Tag as TagBuilder } from \"../core/tag.ts\";\nimport { assertHex32, isRecord } from \"../core/util.ts\";\nimport { encryptToPubkey } from \"./nip44.ts\";\n\n/** Unsigned event with a computed id. Never has `sig`. */\nexport type Rumor = UnsignedEvent & {\n readonly id: string;\n};\n\n/**\n * Structural crypto used by NIP-59. Satisfied by NostrSigner when nip44Encrypt/nip44Decrypt are\n * present. This module must not import src/signer/.\n */\nexport type Nip59Crypto = {\n getPublicKey: () => Promise<string>;\n signEvent: (unsigned: UnsignedEvent) => Promise<Event>;\n nip44Encrypt: (peer: string, plaintext: string) => Promise<string>;\n nip44Decrypt: (peer: string, payload: string) => Promise<string>;\n};\n\nexport type Nip44Decryptor = {\n nip44Decrypt: (peer: string, payload: string) => Promise<string>;\n};\n\nexport type GiftWrapTimestamps = {\n readonly seal: number;\n readonly wrap: number;\n};\n\n/** `\"wrap\"` randomizes only the gift wrap; `\"seal+wrap\"` (default) randomizes both. */\nexport type TimestampRandomize = \"wrap\" | \"seal+wrap\";\n\nexport type WrapOptions = {\n /** Unix seconds used as the randomization window end. Default: floor(Date.now()/1000). */\n readonly now?: number | undefined;\n /** Uniform integer in [0, maxExclusive). Default: CSPRNG via @noble/hashes randomBytes. */\n readonly randomInt?: ((maxExclusive: number) => number) | undefined;\n /** When set, used as-is. Overrides now/randomInt for this call. */\n readonly timestamps?: GiftWrapTimestamps | undefined;\n readonly relayHint?: string | undefined;\n /** Appended after the required wrap `p` tag. Never applied to the seal. */\n readonly extraTags?: ReadonlyArray<Tag> | undefined;\n /**\n * Default `\"seal+wrap\"` (NIP-59). `\"wrap\"` = only wrap timestamp is randomized; seal uses\n * rumor.created_at.\n */\n readonly randomize?: TimestampRandomize | undefined;\n};\n\nexport type SealOptions = Pick<WrapOptions, \"now\" | \"randomInt\" | \"timestamps\" | \"randomize\">;\n\nexport class Nip59Error extends NostrError {\n override name = \"Nip59Error\";\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n }\n}\n\nexport const TWO_DAYS_SECS: number = 2 * 24 * 60 * 60;\n\nexport function isGiftWrapKind(kind: number): boolean {\n return kind === Kind.GiftWrap || kind === Kind.GiftWrapEphemeral;\n}\n\ntype Nip59CryptoInput = {\n getPublicKey: () => Promise<string>;\n signEvent: (unsigned: UnsignedEvent) => Promise<Event>;\n nip44Encrypt?: (peer: string, plaintext: string) => Promise<string>;\n nip44Decrypt?: (peer: string, payload: string) => Promise<string>;\n};\n\n/** Narrow a signer-shaped object to {@link Nip59Crypto}, or throw. */\nexport function requireNip59Crypto(crypto: Nip59CryptoInput): Nip59Crypto {\n const encrypt = crypto.nip44Encrypt;\n const decrypt = crypto.nip44Decrypt;\n if (!encrypt || !decrypt) {\n throw new Nip59Error(\"NIP-44 is required\");\n }\n return {\n getPublicKey: async () => crypto.getPublicKey(),\n signEvent: async (unsigned) => crypto.signEvent(unsigned),\n nip44Encrypt: async (peer, plaintext) => encrypt.call(crypto, peer, plaintext),\n nip44Decrypt: async (peer, payload) => decrypt.call(crypto, peer, payload),\n };\n}\n\nexport function requireNip44Decryptor(crypto: {\n nip44Decrypt?: (peer: string, payload: string) => Promise<string>;\n}): Nip44Decryptor {\n const decrypt = crypto.nip44Decrypt;\n if (!decrypt) {\n throw new Nip59Error(\"NIP-44 is required\");\n }\n return {\n nip44Decrypt: async (peer, payload) => decrypt.call(crypto, peer, payload),\n };\n}\n\nexport function rumorToJson(rumor: Rumor): string {\n return JSON.stringify({\n id: rumor.id,\n pubkey: rumor.pubkey,\n created_at: rumor.created_at,\n kind: rumor.kind,\n tags: rumor.tags,\n content: rumor.content,\n });\n}\n\nexport function eventToJson(event: Event): string {\n return JSON.stringify({\n id: event.id,\n pubkey: event.pubkey,\n created_at: event.created_at,\n kind: event.kind,\n tags: event.tags,\n content: event.content,\n sig: event.sig,\n });\n}\n\nfunction defaultRandomInt(maxExclusive: number): number {\n if (!Number.isSafeInteger(maxExclusive) || maxExclusive <= 0) {\n throw new Nip59Error(\"randomInt bound must be a positive integer\");\n }\n const bytes = randomBytes(4);\n const n = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0);\n return n % maxExclusive;\n}\n\nexport function randomPastTimestamp(opts?: {\n now?: number | undefined;\n randomInt?: ((maxExclusive: number) => number) | undefined;\n}): number {\n const now = opts?.now ?? Math.floor(Date.now() / 1000);\n const offset = (opts?.randomInt ?? defaultRandomInt)(TWO_DAYS_SECS);\n return now - offset;\n}\n\nexport function createRumor(\n pubkey: string,\n template: {\n kind: number;\n content?: string | undefined;\n tags?: ReadonlyArray<Tag> | undefined;\n created_at?: number | undefined;\n },\n): Rumor {\n const unsigned: UnsignedEvent = {\n kind: template.kind,\n content: template.content ?? \"\",\n tags: template.tags ? [...template.tags] : [],\n created_at: template.created_at ?? Math.floor(Date.now() / 1000),\n pubkey: assertHex32(pubkey, \"public key\"),\n };\n return { ...unsigned, id: getEventHash(unsigned) };\n}\n\nexport async function createSeal(\n crypto: Nip59Crypto,\n recipient: string,\n rumor: Rumor,\n opts?: SealOptions,\n): Promise<Event> {\n const sealer = requireNip59Crypto(crypto);\n const recipientPk = assertHex32(recipient, \"public key\");\n let content: string;\n try {\n content = await sealer.nip44Encrypt(recipientPk, rumorToJson(rumor));\n } catch (error) {\n throw new Nip59Error(\"failed to encrypt\", { cause: error });\n }\n const created_at =\n opts?.timestamps?.seal ??\n (opts?.randomize === \"wrap\" ? rumor.created_at : randomPastTimestamp(opts));\n const pubkey = await sealer.getPublicKey();\n return sealer.signEvent({\n kind: Kind.Seal,\n content,\n created_at,\n tags: [],\n pubkey,\n });\n}\n\nexport function createGiftWrap(seal: Event, recipient: string, opts?: WrapOptions): Event {\n const recipientPk = assertHex32(recipient, \"public key\");\n const ephemeral = Keys.generate();\n const content = encryptToPubkey(eventToJson(seal), ephemeral.secretKey.bytes, recipientPk);\n const created_at = opts?.timestamps?.wrap ?? randomPastTimestamp(opts);\n const tags: Tag[] = [TagBuilder.p(recipientPk, opts?.relayHint), ...(opts?.extraTags ?? [])];\n return finalizeEvent(\n {\n kind: Kind.GiftWrap,\n content,\n created_at,\n tags,\n },\n ephemeral.secretKey,\n );\n}\n\nexport async function wrap(\n crypto: Nip59Crypto,\n recipient: string,\n rumor: Rumor,\n opts?: WrapOptions,\n): Promise<Event> {\n const seal = await createSeal(crypto, recipient, rumor, opts);\n return createGiftWrap(seal, recipient, opts);\n}\n\nfunction parseJson(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch (error) {\n throw new Nip59Error(\"invalid JSON\", { cause: error });\n }\n}\n\nfunction parseRumor(value: unknown): Rumor {\n if (!isRecord(value)) {\n throw new Nip59Error(\"invalid rumor\");\n }\n if (Object.hasOwn(value, \"sig\")) {\n throw new Nip59Error(\"rumor must be unsigned\");\n }\n const givenId = value[\"id\"];\n if (!validateEvent(value)) {\n throw new Nip59Error(\"invalid rumor\");\n }\n const unsigned: UnsignedEvent = {\n kind: value.kind,\n tags: value.tags,\n content: value.content,\n created_at: value.created_at,\n pubkey: value.pubkey.toLowerCase(),\n };\n const expected = getEventHash(unsigned);\n if (typeof givenId === \"string\" && givenId.toLowerCase() !== expected) {\n throw new Nip59Error(\"invalid rumor\");\n }\n return { ...unsigned, id: expected };\n}\n\nasync function decryptLayer(\n crypto: Nip44Decryptor,\n peer: string,\n payload: string,\n): Promise<string> {\n try {\n return await crypto.nip44Decrypt(peer, payload);\n } catch (error) {\n throw new Nip59Error(\"failed to decrypt\", { cause: error });\n }\n}\n\nexport async function unwrap(crypto: Nip44Decryptor, giftWrap: Event): Promise<Rumor> {\n const decryptor = requireNip44Decryptor(crypto);\n if (!validateSignedEvent(giftWrap) || !isGiftWrapKind(giftWrap.kind)) {\n throw new Nip59Error(\"expected gift wrap\");\n }\n if (!verifyEvent(giftWrap)) {\n throw new Nip59Error(\"gift wrap signature\");\n }\n\n const sealJson = await decryptLayer(decryptor, giftWrap.pubkey, giftWrap.content);\n const sealRaw = parseJson(sealJson);\n if (!validateSignedEvent(sealRaw)) {\n throw new Nip59Error(\"expected seal\");\n }\n if (sealRaw.kind !== Kind.Seal) {\n throw new Nip59Error(\"expected seal\");\n }\n // NIP-17: seals may carry an [\"expiration\", <unix ts>] tag for disappearing messages.\n for (const tag of sealRaw.tags) {\n if (tag[0] !== \"expiration\" || typeof tag[1] !== \"string\" || !/^\\d+$/.test(tag[1])) {\n throw new Nip59Error(\"seal tags must be empty\");\n }\n }\n if (!verifyEvent(sealRaw)) {\n throw new Nip59Error(\"seal signature\");\n }\n\n const rumorJson = await decryptLayer(decryptor, sealRaw.pubkey, sealRaw.content);\n const rumor = parseRumor(parseJson(rumorJson));\n if (sealRaw.pubkey.toLowerCase() !== rumor.pubkey.toLowerCase()) {\n throw new Nip59Error(\"seal pubkey does not match rumor pubkey\");\n }\n return rumor;\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { a as readRelays, c as writeRelays, i as parseRelayList, n as RelayMarker, o as relayListEventBuilder, r as markerOf, s as relayListToTags, t as RelayListItem } from "../nip65-Di5hZRuV.mjs";
1
+ import { a as readRelays, c as writeRelays, i as parseRelayList, n as RelayMarker, o as relayListEventBuilder, r as markerOf, s as relayListToTags, t as RelayListItem } from "../nip65-DBfh2smt.mjs";
2
2
  export { RelayListItem, RelayMarker, markerOf, parseRelayList, readRelays, relayListEventBuilder, relayListToTags, writeRelays };
@@ -1,8 +1,8 @@
1
- import { s as normalizeURL } from "../util-6tfLQ9JF.mjs";
2
- import { n as EventValidationError } from "../error-DZEs5OuZ.mjs";
3
- import { t as Kind } from "../kind-BLl4cLOc.mjs";
4
- import { t as EventBuilder } from "../builder-q0VsvnYE.mjs";
1
+ import { l as normalizeURL, p as EventValidationError } from "../util-LfQirgDk.mjs";
2
+ import { t as Kind } from "../kind-Cxwhhkx8.mjs";
3
+ import { t as EventBuilder } from "../builder-CyrGKRVP.mjs";
5
4
  //#region src/nips/nip65.ts
5
+ /** The NIP-65 marker an item's read/write flags map to. */
6
6
  function markerOf(item) {
7
7
  if (item.read && item.write) return "readwrite";
8
8
  if (item.read) return "read";
@@ -15,16 +15,17 @@ function parseRelayList(event) {
15
15
  const out = [];
16
16
  const seen = /* @__PURE__ */ new Set();
17
17
  for (const tag of event.tags) {
18
- if (tag[0] !== "r" || !tag[1]) continue;
18
+ const value = tag.at(1);
19
+ if (tag[0] !== "r" || value === void 0 || value === "") continue;
19
20
  let url;
20
21
  try {
21
- url = normalizeURL(tag[1]);
22
+ url = normalizeURL(value);
22
23
  } catch {
23
24
  continue;
24
25
  }
25
26
  if (seen.has(url)) continue;
26
27
  seen.add(url);
27
- const marker = tag[2];
28
+ const marker = tag.at(2);
28
29
  if (marker === "read") out.push({
29
30
  url,
30
31
  read: true,
@@ -57,16 +58,18 @@ function relayListToTags(items) {
57
58
  item.url,
58
59
  "write"
59
60
  ];
60
- return ["r", item.url];
61
+ throw new EventValidationError("relay list item must be read, write, or both");
61
62
  });
62
63
  }
63
64
  /** Build an unsigned kind:10002 EventBuilder from relay list items. */
64
65
  function relayListEventBuilder(items) {
65
66
  return new EventBuilder(Kind.RelayList, "").tags(relayListToTags(items));
66
67
  }
68
+ /** URLs of the read-enabled items. */
67
69
  function readRelays(items) {
68
70
  return items.filter((i) => i.read).map((i) => i.url);
69
71
  }
72
+ /** URLs of the write-enabled items. */
70
73
  function writeRelays(items) {
71
74
  return items.filter((i) => i.write).map((i) => i.url);
72
75
  }
@@ -1 +1 @@
1
- {"version":3,"file":"nip65.mjs","names":[],"sources":["../../src/nips/nip65.ts"],"sourcesContent":["import type { Event } from \"../core/event.ts\";\nimport { EventValidationError } from \"../core/error.ts\";\nimport { Kind } from \"../core/kind.ts\";\nimport type { Tag } from \"../core/tag.ts\";\nimport { EventBuilder } from \"../core/builder.ts\";\nimport { normalizeURL } from \"../core/util.ts\";\n\nexport type RelayMarker = \"read\" | \"write\" | \"readwrite\";\n\nexport type RelayListItem = {\n url: string;\n read: boolean;\n write: boolean;\n};\n\nexport function markerOf(item: RelayListItem): RelayMarker {\n if (item.read && item.write) return \"readwrite\";\n if (item.read) return \"read\";\n if (item.write) return \"write\";\n return \"readwrite\";\n}\n\n/** Parse a kind:10002 NIP-65 event into relay list entries. */\nexport function parseRelayList(event: Event): RelayListItem[] {\n if (event.kind !== Kind.RelayList) {\n throw new EventValidationError(`expected kind ${Kind.RelayList}, got ${event.kind}`);\n }\n const out: RelayListItem[] = [];\n const seen = new Set<string>();\n\n for (const tag of event.tags) {\n if (tag[0] !== \"r\" || !tag[1]) continue;\n let url: string;\n try {\n url = normalizeURL(tag[1]);\n } catch {\n continue;\n }\n if (seen.has(url)) continue;\n seen.add(url);\n\n const marker = tag[2];\n if (marker === \"read\") {\n out.push({ url, read: true, write: false });\n } else if (marker === \"write\") {\n out.push({ url, read: false, write: true });\n } else {\n out.push({ url, read: true, write: true });\n }\n }\n return out;\n}\n\n/** Encode relay list items as NIP-65 `r` tags. */\nexport function relayListToTags(items: RelayListItem[]): Tag[] {\n return items.map((item) => {\n if (item.read && item.write) return [\"r\", item.url];\n if (item.read) return [\"r\", item.url, \"read\"];\n if (item.write) return [\"r\", item.url, \"write\"];\n return [\"r\", item.url];\n });\n}\n\n/** Build an unsigned kind:10002 EventBuilder from relay list items. */\nexport function relayListEventBuilder(items: RelayListItem[]): EventBuilder {\n return new EventBuilder(Kind.RelayList, \"\").tags(relayListToTags(items));\n}\n\nexport function readRelays(items: RelayListItem[]): string[] {\n return items.filter((i) => i.read).map((i) => i.url);\n}\n\nexport function writeRelays(items: RelayListItem[]): string[] {\n return items.filter((i) => i.write).map((i) => i.url);\n}\n"],"mappings":";;;;;AAeA,SAAgB,SAAS,MAAkC;CACzD,IAAI,KAAK,QAAQ,KAAK,OAAO,OAAO;CACpC,IAAI,KAAK,MAAM,OAAO;CACtB,IAAI,KAAK,OAAO,OAAO;CACvB,OAAO;AACT;;AAGA,SAAgB,eAAe,OAA+B;CAC5D,IAAI,MAAM,SAAS,KAAK,WACtB,MAAM,IAAI,qBAAqB,iBAAiB,KAAK,UAAU,QAAQ,MAAM,MAAM;CAErF,MAAM,MAAuB,CAAC;CAC9B,MAAM,uBAAO,IAAI,IAAY;CAE7B,KAAK,MAAM,OAAO,MAAM,MAAM;EAC5B,IAAI,IAAI,OAAO,OAAO,CAAC,IAAI,IAAI;EAC/B,IAAI;EACJ,IAAI;GACF,MAAM,aAAa,IAAI,EAAE;EAC3B,QAAQ;GACN;EACF;EACA,IAAI,KAAK,IAAI,GAAG,GAAG;EACnB,KAAK,IAAI,GAAG;EAEZ,MAAM,SAAS,IAAI;EACnB,IAAI,WAAW,QACb,IAAI,KAAK;GAAE;GAAK,MAAM;GAAM,OAAO;EAAM,CAAC;OACrC,IAAI,WAAW,SACpB,IAAI,KAAK;GAAE;GAAK,MAAM;GAAO,OAAO;EAAK,CAAC;OAE1C,IAAI,KAAK;GAAE;GAAK,MAAM;GAAM,OAAO;EAAK,CAAC;CAE7C;CACA,OAAO;AACT;;AAGA,SAAgB,gBAAgB,OAA+B;CAC7D,OAAO,MAAM,KAAK,SAAS;EACzB,IAAI,KAAK,QAAQ,KAAK,OAAO,OAAO,CAAC,KAAK,KAAK,GAAG;EAClD,IAAI,KAAK,MAAM,OAAO;GAAC;GAAK,KAAK;GAAK;EAAM;EAC5C,IAAI,KAAK,OAAO,OAAO;GAAC;GAAK,KAAK;GAAK;EAAO;EAC9C,OAAO,CAAC,KAAK,KAAK,GAAG;CACvB,CAAC;AACH;;AAGA,SAAgB,sBAAsB,OAAsC;CAC1E,OAAO,IAAI,aAAa,KAAK,WAAW,EAAE,CAAC,CAAC,KAAK,gBAAgB,KAAK,CAAC;AACzE;AAEA,SAAgB,WAAW,OAAkC;CAC3D,OAAO,MAAM,QAAQ,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,MAAM,EAAE,GAAG;AACrD;AAEA,SAAgB,YAAY,OAAkC;CAC5D,OAAO,MAAM,QAAQ,MAAM,EAAE,KAAK,CAAC,CAAC,KAAK,MAAM,EAAE,GAAG;AACtD"}
1
+ {"version":3,"file":"nip65.mjs","names":[],"sources":["../../src/nips/nip65.ts"],"sourcesContent":["import { EventBuilder } from \"../core/builder.ts\";\nimport { EventValidationError } from \"../core/error.ts\";\nimport type { Event } from \"../core/event.ts\";\nimport { Kind } from \"../core/kind.ts\";\nimport type { Tag } from \"../core/tag.ts\";\nimport { normalizeURL } from \"../core/util.ts\";\n\n/** NIP-65 `r` tag marker: read-only, write-only, or unmarked (both). */\nexport type RelayMarker = \"read\" | \"write\" | \"readwrite\";\n\n/** One NIP-65 relay-list entry: normalized URL plus read/write flags. */\nexport type RelayListItem = {\n url: string;\n read: boolean;\n write: boolean;\n};\n\n/** The NIP-65 marker an item's read/write flags map to. */\nexport function markerOf(item: RelayListItem): RelayMarker {\n if (item.read && item.write) {\n return \"readwrite\";\n }\n if (item.read) {\n return \"read\";\n }\n if (item.write) {\n return \"write\";\n }\n return \"readwrite\";\n}\n\n/** Parse a kind:10002 NIP-65 event into relay list entries. */\nexport function parseRelayList(event: Event): RelayListItem[] {\n if (event.kind !== Kind.RelayList) {\n throw new EventValidationError(`expected kind ${Kind.RelayList}, got ${event.kind}`);\n }\n const out: RelayListItem[] = [];\n const seen = new Set<string>();\n\n for (const tag of event.tags) {\n const value = tag.at(1);\n if (tag[0] !== \"r\" || value === undefined || value === \"\") {\n continue;\n }\n let url: string;\n try {\n url = normalizeURL(value);\n } catch {\n continue;\n }\n if (seen.has(url)) {\n continue;\n }\n seen.add(url);\n\n const marker = tag.at(2);\n if (marker === \"read\") {\n out.push({ url, read: true, write: false });\n } else if (marker === \"write\") {\n out.push({ url, read: false, write: true });\n } else {\n out.push({ url, read: true, write: true });\n }\n }\n return out;\n}\n\n/** Encode relay list items as NIP-65 `r` tags. */\nexport function relayListToTags(items: RelayListItem[]): Tag[] {\n return items.map((item) => {\n if (item.read && item.write) {\n return [\"r\", item.url];\n }\n if (item.read) {\n return [\"r\", item.url, \"read\"];\n }\n if (item.write) {\n return [\"r\", item.url, \"write\"];\n }\n throw new EventValidationError(\"relay list item must be read, write, or both\");\n });\n}\n\n/** Build an unsigned kind:10002 EventBuilder from relay list items. */\nexport function relayListEventBuilder(items: RelayListItem[]): EventBuilder {\n return new EventBuilder(Kind.RelayList, \"\").tags(relayListToTags(items));\n}\n\n/** URLs of the read-enabled items. */\nexport function readRelays(items: RelayListItem[]): string[] {\n return items.filter((i) => i.read).map((i) => i.url);\n}\n\n/** URLs of the write-enabled items. */\nexport function writeRelays(items: RelayListItem[]): string[] {\n return items.filter((i) => i.write).map((i) => i.url);\n}\n"],"mappings":";;;;;AAkBA,SAAgB,SAAS,MAAkC;CACzD,IAAI,KAAK,QAAQ,KAAK,OACpB,OAAO;CAET,IAAI,KAAK,MACP,OAAO;CAET,IAAI,KAAK,OACP,OAAO;CAET,OAAO;AACT;;AAGA,SAAgB,eAAe,OAA+B;CAC5D,IAAI,MAAM,SAAS,KAAK,WACtB,MAAM,IAAI,qBAAqB,iBAAiB,KAAK,UAAU,QAAQ,MAAM,MAAM;CAErF,MAAM,MAAuB,CAAC;CAC9B,MAAM,uBAAO,IAAI,IAAY;CAE7B,KAAK,MAAM,OAAO,MAAM,MAAM;EAC5B,MAAM,QAAQ,IAAI,GAAG,CAAC;EACtB,IAAI,IAAI,OAAO,OAAO,UAAU,KAAA,KAAa,UAAU,IACrD;EAEF,IAAI;EACJ,IAAI;GACF,MAAM,aAAa,KAAK;EAC1B,QAAQ;GACN;EACF;EACA,IAAI,KAAK,IAAI,GAAG,GACd;EAEF,KAAK,IAAI,GAAG;EAEZ,MAAM,SAAS,IAAI,GAAG,CAAC;EACvB,IAAI,WAAW,QACb,IAAI,KAAK;GAAE;GAAK,MAAM;GAAM,OAAO;EAAM,CAAC;OACrC,IAAI,WAAW,SACpB,IAAI,KAAK;GAAE;GAAK,MAAM;GAAO,OAAO;EAAK,CAAC;OAE1C,IAAI,KAAK;GAAE;GAAK,MAAM;GAAM,OAAO;EAAK,CAAC;CAE7C;CACA,OAAO;AACT;;AAGA,SAAgB,gBAAgB,OAA+B;CAC7D,OAAO,MAAM,KAAK,SAAS;EACzB,IAAI,KAAK,QAAQ,KAAK,OACpB,OAAO,CAAC,KAAK,KAAK,GAAG;EAEvB,IAAI,KAAK,MACP,OAAO;GAAC;GAAK,KAAK;GAAK;EAAM;EAE/B,IAAI,KAAK,OACP,OAAO;GAAC;GAAK,KAAK;GAAK;EAAO;EAEhC,MAAM,IAAI,qBAAqB,8CAA8C;CAC/E,CAAC;AACH;;AAGA,SAAgB,sBAAsB,OAAsC;CAC1E,OAAO,IAAI,aAAa,KAAK,WAAW,EAAE,CAAC,CAAC,KAAK,gBAAgB,KAAK,CAAC;AACzE;;AAGA,SAAgB,WAAW,OAAkC;CAC3D,OAAO,MAAM,QAAQ,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,MAAM,EAAE,GAAG;AACrD;;AAGA,SAAgB,YAAY,OAAkC;CAC5D,OAAO,MAAM,QAAQ,MAAM,EAAE,KAAK,CAAC,CAAC,KAAK,MAAM,EAAE,GAAG;AACtD"}
@@ -1,2 +1,2 @@
1
- import { a as NegentropyStorageVector, c as ReconcileOutcome, d as storageFromItems, i as Negentropy, l as runNegSession, n as MAX_NEG_ROUNDS, o as Nip77Error, r as NegItem, s as PROTOCOL_VERSION, t as DEFAULT_FRAME_SIZE_LIMIT, u as storageFromEvents } from "../nip77-ByxlgoQO.mjs";
1
+ import { a as NegentropyStorageVector, c as ReconcileOutcome, d as storageFromItems, i as Negentropy, l as runNegSession, n as MAX_NEG_ROUNDS, o as Nip77Error, r as NegItem, s as PROTOCOL_VERSION, t as DEFAULT_FRAME_SIZE_LIMIT, u as storageFromEvents } from "../nip77-DH4RhS5r.mjs";
2
2
  export { DEFAULT_FRAME_SIZE_LIMIT, MAX_NEG_ROUNDS, NegItem, Negentropy, NegentropyStorageVector, Nip77Error, PROTOCOL_VERSION, ReconcileOutcome, runNegSession, storageFromEvents, storageFromItems };
@@ -1,10 +1,8 @@
1
- import { i as hexToBytes, r as bytesToHex, t as assertHex32 } from "../util-6tfLQ9JF.mjs";
2
- import { a as NostrError } from "../error-DZEs5OuZ.mjs";
1
+ import { a as hexToBytes, g as NostrError, i as bytesToHex, n as assertHex32 } from "../util-LfQirgDk.mjs";
3
2
  import { sha256 } from "@noble/hashes/sha2.js";
4
3
  //#region src/nips/nip77.ts
5
4
  /**
6
- * NIP-77: Negentropy Syncing.
7
- * Transport-free V1 algorithm. Does not import relay or client.
5
+ * NIP-77: Negentropy Syncing. Transport-free V1 algorithm. Does not import relay or client.
8
6
  *
9
7
  * @see https://github.com/nostr-protocol/nips/blob/master/77.md
10
8
  */
@@ -22,6 +20,7 @@ const Mode = {
22
20
  IdList: 2
23
21
  };
24
22
  var Nip77Error = class extends NostrError {
23
+ name = "Nip77Error";
25
24
  constructor(message, options) {
26
25
  super(message, options);
27
26
  }
@@ -61,7 +60,8 @@ var EncodedBuf = class EncodedBuf {
61
60
  }
62
61
  shift() {
63
62
  if (this.length === 0) throw new Nip77Error("parse ends prematurely");
64
- const first = this.#raw[0];
63
+ const [first] = this.#raw;
64
+ if (first === void 0) throw new Nip77Error("parse ends prematurely");
65
65
  this.#raw = this.#raw.subarray(1);
66
66
  this.length -= 1;
67
67
  return first;
@@ -92,8 +92,8 @@ function encodeVarInt(n) {
92
92
  value >>>= 7;
93
93
  }
94
94
  digits.reverse();
95
- for (let i = 0; i < digits.length - 1; i++) digits[i] |= 128;
96
- return new EncodedBuf(new Uint8Array(digits));
95
+ const encoded = digits.map((digit, i) => i === digits.length - 1 ? digit : digit | 128);
96
+ return new EncodedBuf(new Uint8Array(encoded));
97
97
  }
98
98
  function getBytes(buf, n) {
99
99
  return buf.shiftN(n);
@@ -124,8 +124,11 @@ var Accumulator = class {
124
124
  function compareBytes(a, b) {
125
125
  const n = Math.min(a.byteLength, b.byteLength);
126
126
  for (let i = 0; i < n; i++) {
127
- if (a[i] < b[i]) return -1;
128
- if (a[i] > b[i]) return 1;
127
+ const x = a[i];
128
+ const y = b[i];
129
+ if (x === void 0 || y === void 0) break;
130
+ if (x < y) return -1;
131
+ if (x > y) return 1;
129
132
  }
130
133
  if (a.byteLength > b.byteLength) return 1;
131
134
  if (a.byteLength < b.byteLength) return -1;
@@ -151,7 +154,11 @@ var NegentropyStorageVector = class {
151
154
  if (this.#sealed) throw new Nip77Error("already sealed");
152
155
  this.#sealed = true;
153
156
  this.#items.sort(compareNegItems);
154
- for (let i = 1; i < this.#items.length; i++) if (compareNegItems(this.#items[i - 1], this.#items[i]) === 0) throw new Nip77Error("duplicate item inserted");
157
+ for (let i = 1; i < this.#items.length; i++) {
158
+ const prev = this.#items[i - 1];
159
+ const item = this.#items[i];
160
+ if (prev !== void 0 && item !== void 0 && compareNegItems(prev, item) === 0) throw new Nip77Error("duplicate item inserted");
161
+ }
155
162
  }
156
163
  size() {
157
164
  this.#checkSealed();
@@ -160,7 +167,10 @@ var NegentropyStorageVector = class {
160
167
  iterate(begin, end, cb) {
161
168
  this.#checkSealed();
162
169
  this.#checkBounds(begin, end);
163
- for (let i = begin; i < end; i++) if (!cb(this.#items[i], i)) break;
170
+ for (let i = begin; i < end; i++) {
171
+ const item = this.#items[i];
172
+ if (item === void 0 || !cb(item, i)) break;
173
+ }
164
174
  }
165
175
  findLowerBound(begin, end, bound) {
166
176
  this.#checkSealed();
@@ -170,7 +180,9 @@ var NegentropyStorageVector = class {
170
180
  while (count > 0) {
171
181
  const step = Math.floor(count / 2);
172
182
  const it = first + step;
173
- if (compareNegItems(this.#items[it], bound) < 0) {
183
+ const item = this.#items[it];
184
+ if (item === void 0) break;
185
+ if (compareNegItems(item, bound) < 0) {
174
186
  first = it + 1;
175
187
  count -= step + 1;
176
188
  } else count = step;
@@ -235,29 +247,31 @@ var Negentropy = class {
235
247
  fullOutput.extend(new Uint8Array([97]));
236
248
  const protocolVersion = query.shift();
237
249
  if (protocolVersion < 96 || protocolVersion > 111) throw new Nip77Error("invalid negentropy protocol version byte");
238
- if (protocolVersion !== 97) throw new Nip77Error(`unsupported negentropy protocol version requested: ${protocolVersion - 96}`);
250
+ if (protocolVersion !== 97) return {
251
+ have,
252
+ need,
253
+ nextMessage: bytesToHex(fullOutput.unwrap())
254
+ };
239
255
  const storageSize = this.#storage.size();
240
256
  let prevBound = this.#bound(0);
241
257
  let prevIndex = 0;
242
258
  let skip = false;
243
- while (query.length !== 0) {
259
+ while (query.length > 0) {
244
260
  const o = new EncodedBuf();
245
- const doSkip = () => {
246
- if (!skip) return;
247
- skip = false;
248
- o.extend(this.#encodeBound(prevBound));
249
- o.extend(encodeVarInt(Mode.Skip));
250
- };
251
261
  const currBound = this.#decodeBound(query);
252
262
  const mode = decodeVarInt(query);
253
263
  const lower = prevIndex;
254
264
  const upper = this.#storage.findLowerBound(prevIndex, storageSize, currBound);
255
265
  if (mode === Mode.Skip) skip = true;
256
266
  else if (mode === Mode.Fingerprint) {
257
- if (compareBytes(getBytes(query, FINGERPRINT_SIZE), this.#storage.fingerprint(lower, upper)) !== 0) {
258
- doSkip();
267
+ if (compareBytes(getBytes(query, FINGERPRINT_SIZE), this.#storage.fingerprint(lower, upper)) === 0) skip = true;
268
+ else {
269
+ if (skip) {
270
+ skip = false;
271
+ this.#encodeSkipInto(o, prevBound);
272
+ }
259
273
  this.#splitRange(lower, upper, currBound, o);
260
- } else skip = true;
274
+ }
261
275
  } else if (mode === Mode.IdList) {
262
276
  const numIds = decodeVarInt(query);
263
277
  const theirElems = /* @__PURE__ */ new Map();
@@ -269,13 +283,16 @@ var Negentropy = class {
269
283
  skip = true;
270
284
  this.#storage.iterate(lower, upper, (item) => {
271
285
  const id = bytesToHex(item.id);
272
- if (!theirElems.has(id)) have.push(id);
273
- else theirElems.delete(id);
286
+ if (theirElems.has(id)) theirElems.delete(id);
287
+ else have.push(id);
274
288
  return true;
275
289
  });
276
290
  for (const id of theirElems.keys()) need.push(id);
277
291
  } else {
278
- doSkip();
292
+ if (skip) {
293
+ skip = false;
294
+ this.#encodeSkipInto(o, prevBound);
295
+ }
279
296
  o.extend(this.#encodeBound(currBound));
280
297
  o.extend(encodeVarInt(Mode.IdList));
281
298
  const ourIds = [];
@@ -301,7 +318,7 @@ var Negentropy = class {
301
318
  return {
302
319
  have,
303
320
  need,
304
- nextMessage: fullOutput.length === 1 ? null : bytesToHex(fullOutput.unwrap())
321
+ nextMessage: fullOutput.length === 1 ? void 0 : bytesToHex(fullOutput.unwrap())
305
322
  };
306
323
  }
307
324
  #bound(timestamp, id) {
@@ -332,13 +349,15 @@ var Negentropy = class {
332
349
  let nextBound;
333
350
  if (curr === upper) nextBound = upperBound;
334
351
  else {
352
+ const at = curr - 1;
335
353
  let prevItem;
336
354
  let currItem;
337
- this.#storage.iterate(curr - 1, curr + 1, (item, index) => {
338
- if (index === curr - 1) prevItem = item;
355
+ this.#storage.iterate(at, at + 2, (item, index) => {
356
+ if (index === at) prevItem = item;
339
357
  else currItem = item;
340
358
  return true;
341
359
  });
360
+ if (prevItem === void 0 || currItem === void 0) throw new Nip77Error("missing bound items");
342
361
  nextBound = this.#minimalBound(prevItem, currItem);
343
362
  }
344
363
  o.extend(this.#encodeBound(nextBound));
@@ -375,10 +394,13 @@ var Negentropy = class {
375
394
  this.#lastTimestampOut = INFINITY;
376
395
  return encodeVarInt(0);
377
396
  }
378
- const temp = timestamp;
379
- timestamp -= this.#lastTimestampOut;
380
- this.#lastTimestampOut = temp;
381
- return encodeVarInt(timestamp + 1);
397
+ const delta = timestamp - this.#lastTimestampOut;
398
+ this.#lastTimestampOut = timestamp;
399
+ return encodeVarInt(delta + 1);
400
+ }
401
+ #encodeSkipInto(o, bound) {
402
+ o.extend(this.#encodeBound(bound));
403
+ o.extend(encodeVarInt(Mode.Skip));
382
404
  }
383
405
  #encodeBound(key) {
384
406
  const output = new EncodedBuf();
@@ -407,7 +429,7 @@ async function runNegSession(opts) {
407
429
  const out = neg.reconcile(await opts.next());
408
430
  for (const id of out.have) have.add(id);
409
431
  for (const id of out.need) need.add(id);
410
- if (out.nextMessage === null) return {
432
+ if (out.nextMessage === void 0) return {
411
433
  have: [...have],
412
434
  need: [...need]
413
435
  };