@qntx/nostr 0.5.0 → 0.7.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 (178) hide show
  1. package/dist/{abort-Ba8tDrOI.mjs → abort-DBq2STPj.mjs} +13 -6
  2. package/dist/abort-DBq2STPj.mjs.map +1 -0
  3. package/dist/{builder-9tjI6WQ3.mjs → builder-CdLE4J6O.mjs} +11 -17
  4. package/dist/builder-CdLE4J6O.mjs.map +1 -0
  5. package/dist/{builder-sSflu6nB.d.mts → builder-DgkqDylK.d.mts} +5 -6
  6. package/dist/{client-BtIAtxrL.mjs → client-CILhdfsF.mjs} +104 -314
  7. package/dist/client-CILhdfsF.mjs.map +1 -0
  8. package/dist/client.d.mts +2 -2
  9. package/dist/client.mjs +2 -2
  10. package/dist/collections-DtwMc1NU.mjs +33 -0
  11. package/dist/collections-DtwMc1NU.mjs.map +1 -0
  12. package/dist/core.d.mts +9 -8
  13. package/dist/core.mjs +7 -7
  14. package/dist/{error-BW7re1UN.d.mts → error-BD_0QFKT.d.mts} +2 -2
  15. package/dist/{error-CR8Urc84.d.mts → error-BquXDmIN.d.mts} +4 -4
  16. package/dist/{error-DrEIM7FO.mjs → error-L_QdmSnV.mjs} +2 -2
  17. package/dist/{error-DrEIM7FO.mjs.map → error-L_QdmSnV.mjs.map} +1 -1
  18. package/dist/{error-B__OMBRY.mjs → error-gtJvV6YS.mjs} +2 -2
  19. package/dist/{error-B__OMBRY.mjs.map → error-gtJvV6YS.mjs.map} +1 -1
  20. package/dist/{event-fU3Y01PR.d.mts → event-DJeb_DIL.d.mts} +4 -2
  21. package/dist/{event-D8Nfoh4K.mjs → event-DinQMmJ1.mjs} +16 -10
  22. package/dist/event-DinQMmJ1.mjs.map +1 -0
  23. package/dist/{filter-C0IMByE3.d.mts → filter-jI_9htF7.d.mts} +2 -2
  24. package/dist/{gossip-DpIvkBuj.d.mts → gossip-C7HLW5kI.d.mts} +35 -26
  25. package/dist/{gossip-oloDMWEj.mjs → gossip-CcTp9VpU.mjs} +23 -33
  26. package/dist/gossip-CcTp9VpU.mjs.map +1 -0
  27. package/dist/gossip.d.mts +1 -1
  28. package/dist/gossip.mjs +1 -1
  29. package/dist/{index-CvkvqB16.d.mts → index-DU9tRFTZ.d.mts} +7 -20
  30. package/dist/{index-q4MDce_r.d.mts → index-DiXwNn2M.d.mts} +6 -8
  31. package/dist/{index-QxvrpMfF.d.mts → index-DocgD9K0.d.mts} +92 -167
  32. package/dist/{index-D1bqFHKB.d.mts → index-InQZpHyQ.d.mts} +18 -18
  33. package/dist/{index-MxC28S4P.d.mts → index-Tx15HGAD.d.mts} +26 -28
  34. package/dist/{index-Dmc8FJgO.d.mts → index-iLT8UzY3.d.mts} +19 -37
  35. package/dist/index.d.mts +23 -21
  36. package/dist/index.mjs +21 -19
  37. package/dist/instance-BcKzxHIP.mjs +220 -0
  38. package/dist/instance-BcKzxHIP.mjs.map +1 -0
  39. package/dist/instance-DjsL8h84.d.mts +15 -0
  40. package/dist/{key-Cv4_pN3x.d.mts → key-D_5OgGT7.d.mts} +11 -7
  41. package/dist/{key-Bps-LqX7.mjs → key-bshqfmZL.mjs} +13 -9
  42. package/dist/key-bshqfmZL.mjs.map +1 -0
  43. package/dist/{loaders-o6YXB_Ri.mjs → loaders-DD0XAGzw.mjs} +73 -82
  44. package/dist/loaders-DD0XAGzw.mjs.map +1 -0
  45. package/dist/loaders.d.mts +3 -3
  46. package/dist/loaders.mjs +2 -2
  47. package/dist/{memory-C2EVx0_O.mjs → memory-D_-5-T31.mjs} +2 -2
  48. package/dist/memory-D_-5-T31.mjs.map +1 -0
  49. package/dist/{memory-index-Do1mz0WF.mjs → memory-index-C6rujtoc.mjs} +30 -247
  50. package/dist/memory-index-C6rujtoc.mjs.map +1 -0
  51. package/dist/message-CWLcFTQ1.mjs +194 -0
  52. package/dist/message-CWLcFTQ1.mjs.map +1 -0
  53. package/dist/{message-iGPIqnn0.d.mts → message-CbBZOhq3.d.mts} +3 -3
  54. package/dist/{nip17-CKIufV98.d.mts → nip17-cTDFfFVt.d.mts} +5 -6
  55. package/dist/{nip19-BevCJZsp.d.mts → nip19-D1OnJ192.d.mts} +3 -9
  56. package/dist/{nip46-DDV4dC56.d.mts → nip46-DrsPI8EX.d.mts} +3 -5
  57. package/dist/nip51-DoPUvQ--.d.mts +87 -0
  58. package/dist/{nip59-zQgdLcEr.d.mts → nip59-BFIihLYY.d.mts} +3 -4
  59. package/dist/nip65-52HiwiyD.d.mts +23 -0
  60. package/dist/{nip77-DH4RhS5r.d.mts → nip77-C9QcZn7b.d.mts} +3 -4
  61. package/dist/nips/blossom.d.mts +12 -8
  62. package/dist/nips/blossom.mjs +30 -9
  63. package/dist/nips/blossom.mjs.map +1 -1
  64. package/dist/nips/nip04.mjs +17 -7
  65. package/dist/nips/nip04.mjs.map +1 -1
  66. package/dist/nips/nip05.d.mts +2 -3
  67. package/dist/nips/nip05.mjs +1 -4
  68. package/dist/nips/nip05.mjs.map +1 -1
  69. package/dist/nips/nip10.d.mts +3 -3
  70. package/dist/nips/nip10.mjs +3 -3
  71. package/dist/nips/nip11.d.mts +1 -1
  72. package/dist/nips/nip11.mjs +4 -4
  73. package/dist/nips/nip11.mjs.map +1 -1
  74. package/dist/nips/nip13.d.mts +7 -7
  75. package/dist/nips/nip13.mjs +32 -10
  76. package/dist/nips/nip13.mjs.map +1 -1
  77. package/dist/nips/nip17.d.mts +1 -1
  78. package/dist/nips/nip17.mjs +13 -33
  79. package/dist/nips/nip17.mjs.map +1 -1
  80. package/dist/nips/nip19.d.mts +2 -2
  81. package/dist/nips/nip19.mjs +16 -28
  82. package/dist/nips/nip19.mjs.map +1 -1
  83. package/dist/nips/nip21.d.mts +6 -1
  84. package/dist/nips/nip21.mjs +15 -5
  85. package/dist/nips/nip21.mjs.map +1 -1
  86. package/dist/nips/nip27.d.mts +4 -4
  87. package/dist/nips/nip27.mjs.map +1 -1
  88. package/dist/nips/nip42.d.mts +1 -1
  89. package/dist/nips/nip42.mjs +2 -1
  90. package/dist/nips/nip42.mjs.map +1 -1
  91. package/dist/nips/nip44.mjs +1 -1
  92. package/dist/nips/nip46.d.mts +2 -2
  93. package/dist/nips/nip46.mjs +15 -19
  94. package/dist/nips/nip46.mjs.map +1 -1
  95. package/dist/nips/nip49.d.mts +5 -5
  96. package/dist/nips/nip49.mjs +4 -4
  97. package/dist/nips/nip49.mjs.map +1 -1
  98. package/dist/nips/nip51.d.mts +2 -86
  99. package/dist/nips/nip51.mjs +7 -23
  100. package/dist/nips/nip51.mjs.map +1 -1
  101. package/dist/nips/nip57.d.mts +7 -5
  102. package/dist/nips/nip57.mjs +12 -17
  103. package/dist/nips/nip57.mjs.map +1 -1
  104. package/dist/nips/nip59.d.mts +1 -1
  105. package/dist/nips/nip59.mjs +16 -16
  106. package/dist/nips/nip59.mjs.map +1 -1
  107. package/dist/nips/nip65.d.mts +2 -2
  108. package/dist/nips/nip65.mjs +14 -37
  109. package/dist/nips/nip65.mjs.map +1 -1
  110. package/dist/nips/nip77.d.mts +1 -1
  111. package/dist/nips/nip77.mjs +33 -32
  112. package/dist/nips/nip77.mjs.map +1 -1
  113. package/dist/nips/nip98.d.mts +5 -5
  114. package/dist/nips/nip98.mjs +8 -8
  115. package/dist/nips/nip98.mjs.map +1 -1
  116. package/dist/nostr_crypto_wasm_bg.wasm +0 -0
  117. package/dist/{pool-DBIYMtwy.d.mts → pool-SzJKPIa3.d.mts} +96 -91
  118. package/dist/pool-YHRZgcQ3.mjs +596 -0
  119. package/dist/pool-YHRZgcQ3.mjs.map +1 -0
  120. package/dist/put-DS0yzVJx.mjs +187 -0
  121. package/dist/put-DS0yzVJx.mjs.map +1 -0
  122. package/dist/{reactive-DFpqtIf_.d.mts → reactive-BJ1ggn4d.d.mts} +9 -22
  123. package/dist/{reactive-DDlThDSZ.mjs → reactive-BJRoVHxU.mjs} +113 -64
  124. package/dist/reactive-BJRoVHxU.mjs.map +1 -0
  125. package/dist/{pool-As20OGxB.mjs → relay-CvxPiQ_m.mjs} +178 -721
  126. package/dist/relay-CvxPiQ_m.mjs.map +1 -0
  127. package/dist/relay.d.mts +4 -4
  128. package/dist/relay.mjs +3 -2
  129. package/dist/serial-Bzpw-xz3.mjs +21 -0
  130. package/dist/serial-Bzpw-xz3.mjs.map +1 -0
  131. package/dist/{signer-CYQutDzD.mjs → signer-yW3slDC2.mjs} +12 -14
  132. package/dist/signer-yW3slDC2.mjs.map +1 -0
  133. package/dist/signer.d.mts +2 -2
  134. package/dist/signer.mjs +2 -2
  135. package/dist/{storage-Dky3wuoH.mjs → storage-CZ52ifcQ.mjs} +17 -42
  136. package/dist/storage-CZ52ifcQ.mjs.map +1 -0
  137. package/dist/storage.d.mts +3 -3
  138. package/dist/storage.mjs +4 -4
  139. package/dist/store.d.mts +1 -1
  140. package/dist/store.mjs +1 -1
  141. package/dist/testing.d.mts +18 -18
  142. package/dist/testing.mjs +9 -9
  143. package/dist/testing.mjs.map +1 -1
  144. package/dist/{types-2pCUprRU.d.mts → types-Cm-XMgtH.d.mts} +6 -6
  145. package/dist/{types-CRQEk0Oi.d.mts → types-DNmiscfd.d.mts} +2 -2
  146. package/dist/{util-LfQirgDk.mjs → util-yFYzlbrm.mjs} +29 -7
  147. package/dist/util-yFYzlbrm.mjs.map +1 -0
  148. package/dist/{verifier-t5WoWbtY.mjs → verifier-BTLOpo1n.mjs} +3 -3
  149. package/dist/verifier-BTLOpo1n.mjs.map +1 -0
  150. package/dist/verifier-CgTFpgsC.d.mts +22 -0
  151. package/dist/wasm.browser.d.mts +8 -0
  152. package/dist/wasm.browser.mjs +19 -0
  153. package/dist/wasm.browser.mjs.map +1 -0
  154. package/dist/wasm.d.mts +3 -13
  155. package/dist/wasm.mjs +7 -208
  156. package/dist/wasm.mjs.map +1 -1
  157. package/package.json +13 -10
  158. package/dist/abort-Ba8tDrOI.mjs.map +0 -1
  159. package/dist/builder-9tjI6WQ3.mjs.map +0 -1
  160. package/dist/client-BtIAtxrL.mjs.map +0 -1
  161. package/dist/event-D8Nfoh4K.mjs.map +0 -1
  162. package/dist/gossip-oloDMWEj.mjs.map +0 -1
  163. package/dist/key-Bps-LqX7.mjs.map +0 -1
  164. package/dist/loaders-o6YXB_Ri.mjs.map +0 -1
  165. package/dist/memory-C2EVx0_O.mjs.map +0 -1
  166. package/dist/memory-index-Do1mz0WF.mjs.map +0 -1
  167. package/dist/message-DcY6Fi9o.mjs +0 -193
  168. package/dist/message-DcY6Fi9o.mjs.map +0 -1
  169. package/dist/nip65-CjgmdpPB.d.mts +0 -26
  170. package/dist/nips/nip96.d.mts +0 -40
  171. package/dist/nips/nip96.mjs +0 -118
  172. package/dist/nips/nip96.mjs.map +0 -1
  173. package/dist/pool-As20OGxB.mjs.map +0 -1
  174. package/dist/reactive-DDlThDSZ.mjs.map +0 -1
  175. package/dist/signer-CYQutDzD.mjs.map +0 -1
  176. package/dist/storage-Dky3wuoH.mjs.map +0 -1
  177. package/dist/util-LfQirgDk.mjs.map +0 -1
  178. package/dist/verifier-t5WoWbtY.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"nip51.mjs","names":[],"sources":["../../src/nips/nip51.ts"],"sourcesContent":["/**\n * NIP-51: Lists Public tags plus NIP-44 private `.content` (author encrypts to self). Crypto is\n * injected; this module does not import signer. No NIP-04 sniff. Kind 10063 Blossom servers live in\n * blossom.ts.\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/51.md\n */\nimport { 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 { getDTag, isTag, Tag } from \"../core/tag.ts\";\nimport { assertHex32, isHex32, normalizeURL } from \"../core/util.ts\";\n\n/**\n * Structural crypto used by NIP-51 private tags. Satisfied by NostrSigner when\n * nip44Encrypt/nip44Decrypt are present. This module must not import src/signer/.\n */\nexport type Nip51Crypto = {\n getPublicKey: () => Promise<string>;\n nip44Encrypt: (peer: string, plaintext: string) => Promise<string>;\n nip44Decrypt: (peer: string, payload: string) => Promise<string>;\n};\n\nexport type MuteItem =\n | { type: \"pubkey\"; value: string }\n | { type: \"event\"; value: string }\n | { type: \"hashtag\"; value: string }\n | { type: \"word\"; value: string };\n\nfunction requireKind(event: Pick<Event, \"kind\">, kind: number): void {\n if (event.kind !== kind) {\n throw new EventValidationError(`expected kind ${kind}, got ${event.kind}`);\n }\n}\n\nfunction collectRelays(tags: Event[\"tags\"]): string[] {\n const out: string[] = [];\n const seen = new Set<string>();\n for (const tag of tags) {\n const value = tag.at(1);\n if (tag[0] !== \"relay\" || 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 out.push(url);\n }\n return out;\n}\n\nfunction collectEmoji(tags: Event[\"tags\"]): Array<{ shortcode: string; url: string }> {\n const out: Array<{ shortcode: string; url: string }> = [];\n for (const tag of tags) {\n if (tag[0] !== \"emoji\") {\n continue;\n }\n const shortcode = tag.at(1);\n const url = tag.at(2);\n if (shortcode === undefined || shortcode === \"\" || url === undefined || url === \"\") {\n continue;\n }\n out.push({ shortcode, url });\n }\n return out;\n}\n\nfunction firstTagValue(tags: Event[\"tags\"], name: string): string | undefined {\n for (const tag of tags) {\n const value = tag.at(1);\n if (tag[0] === name && value !== undefined && value !== \"\") {\n return value;\n }\n }\n return undefined;\n}\n\nfunction identifier(tags: Event[\"tags\"]): string {\n return getDTag(tags) ?? \"\";\n}\n\n/** NIP-51 a-tag: `30002:<64-hex-pubkey>:<d>` with `d` possibly containing `:`. */\nfunction isRelaySetAddress(value: string): boolean {\n const kindSep = value.indexOf(\":\");\n if (kindSep === -1) {\n return false;\n }\n if (value.slice(0, kindSep) !== String(Kind.RelaySets)) {\n return false;\n }\n const pkSep = value.indexOf(\":\", kindSep + 1);\n if (pkSep === -1) {\n return false;\n }\n const pubkey = value.slice(kindSep + 1, pkSep);\n const d = value.slice(pkSep + 1);\n return isHex32(pubkey.toLowerCase()) && d.length > 0;\n}\n\n/** Parse kind:10000 mute list public tags (`p` / `e` / `t` / `word`). */\nexport function parseMuteList(event: Pick<Event, \"kind\" | \"tags\">): MuteItem[] {\n requireKind(event, Kind.MuteList);\n const items: MuteItem[] = [];\n for (const tag of event.tags) {\n const value = tag.at(1);\n if (value === undefined || value === \"\") {\n continue;\n }\n switch (tag[0]) {\n case \"p\":\n if (isHex32(value.toLowerCase())) {\n items.push({ type: \"pubkey\", value: value.toLowerCase() });\n }\n break;\n case \"e\":\n if (isHex32(value.toLowerCase())) {\n items.push({ type: \"event\", value: value.toLowerCase() });\n }\n break;\n case \"t\":\n // NIP-51 does not require hashtag case-folding.\n items.push({ type: \"hashtag\", value });\n break;\n case \"word\":\n items.push({ type: \"word\", value: value.toLowerCase() });\n break;\n default:\n break;\n }\n }\n return items;\n}\n\n/** Build an unsigned kind:10000 EventBuilder from public mute items. */\nexport function muteListEventBuilder(items: ReadonlyArray<MuteItem>): EventBuilder {\n const b = new EventBuilder(Kind.MuteList, \"\");\n for (const item of items) {\n switch (item.type) {\n case \"pubkey\":\n b.tag(Tag.p(assertHex32(item.value, \"public key\")));\n break;\n case \"event\":\n b.tag(Tag.e(assertHex32(item.value, \"event id\")));\n break;\n case \"hashtag\":\n if (item.value) {\n b.tag(Tag.t(item.value));\n }\n break;\n case \"word\":\n if (item.value) {\n b.tag([\"word\", item.value.toLowerCase()]);\n }\n break;\n }\n }\n return b;\n}\n\n/** NIP-44 ciphertext of tags, conversation peer = author's own pubkey. */\nexport async function encryptPrivateTags(\n crypto: Nip51Crypto,\n tags: Event[\"tags\"],\n): Promise<string> {\n const self = await crypto.getPublicKey();\n return crypto.nip44Encrypt(self, JSON.stringify(tags));\n}\n\n/** Decrypt NIP-51 `.content`. Empty content is no private tags, not ciphertext. */\nexport async function decryptPrivateTags(\n crypto: Nip51Crypto,\n event: Pick<Event, \"pubkey\" | \"content\">,\n): Promise<Event[\"tags\"]> {\n // NIP-44 rejects empty plaintext; empty `.content` means no private items.\n if (event.content === \"\") {\n return [];\n }\n const self = await crypto.getPublicKey();\n if (self.toLowerCase() !== event.pubkey.toLowerCase()) {\n throw new EventValidationError(\"NIP-51 private content is only for the author\");\n }\n const plaintext = await crypto.nip44Decrypt(self, event.content);\n let parsed: unknown;\n try {\n parsed = JSON.parse(plaintext);\n } catch (error) {\n throw new EventValidationError(\"invalid NIP-51 private tags\", { cause: error });\n }\n if (!Array.isArray(parsed) || !parsed.every(isTag)) {\n throw new EventValidationError(\"invalid NIP-51 private tags\");\n }\n return parsed;\n}\n\n/** Public mute items from tags; private mute items from NIP-44 `.content`. */\nexport async function parseMuteListPrivate(\n crypto: Nip51Crypto,\n event: Pick<Event, \"kind\" | \"tags\" | \"pubkey\" | \"content\">,\n): Promise<{ public: MuteItem[]; private: MuteItem[] }> {\n const pub = parseMuteList(event);\n const decrypted = await decryptPrivateTags(crypto, event);\n return {\n public: pub,\n private: parseMuteList({ kind: event.kind, tags: decrypted }),\n };\n}\n\n/** Parse kind:10001 pin list public `e` tags. */\nexport function parsePinList(event: Pick<Event, \"kind\" | \"tags\">): string[] {\n requireKind(event, Kind.PinList);\n const ids: string[] = [];\n for (const tag of event.tags) {\n const value = tag.at(1);\n if (tag[0] !== \"e\" || value === undefined || !isHex32(value.toLowerCase())) {\n continue;\n }\n ids.push(value.toLowerCase());\n }\n return ids;\n}\n\n/** Build an unsigned kind:10001 EventBuilder from event ids. */\nexport function pinListEventBuilder(ids: ReadonlyArray<string>): EventBuilder {\n const b = new EventBuilder(Kind.PinList, \"\");\n for (const id of ids) {\n b.tag(Tag.e(assertHex32(id, \"event id\")));\n }\n return b;\n}\n\n/** Parse kind:10003 bookmark list public `e` and `a` tags. */\nexport function parseBookmarkList(event: Pick<Event, \"kind\" | \"tags\">): {\n e: string[];\n a: string[];\n} {\n requireKind(event, Kind.BookmarkList);\n const e: string[] = [];\n const a: string[] = [];\n for (const tag of event.tags) {\n const value = tag.at(1);\n if (value === undefined || value === \"\") {\n continue;\n }\n if (tag[0] === \"e\") {\n if (isHex32(value.toLowerCase())) {\n e.push(value.toLowerCase());\n }\n } else if (tag[0] === \"a\") {\n a.push(value);\n }\n }\n return { e, a };\n}\n\n/** Build an unsigned kind:10003 EventBuilder from public bookmark pointers. */\nexport function bookmarkListEventBuilder(items: {\n e?: ReadonlyArray<string>;\n a?: ReadonlyArray<string>;\n}): EventBuilder {\n const b = new EventBuilder(Kind.BookmarkList, \"\");\n if (items.e) {\n for (const id of items.e) {\n b.tag(Tag.e(assertHex32(id, \"event id\")));\n }\n }\n if (items.a) {\n for (const coord of items.a) {\n if (coord) {\n b.tag(Tag.a(coord));\n }\n }\n }\n return b;\n}\n\n/** Parse kind:10030 user emoji list public `emoji` and `a` tags. */\nexport function parseUserEmojiList(event: Pick<Event, \"kind\" | \"tags\">): {\n emoji: Array<{ shortcode: string; url: string }>;\n sets: string[];\n} {\n requireKind(event, Kind.UserEmojiList);\n const sets: string[] = [];\n for (const tag of event.tags) {\n const value = tag.at(1);\n if (tag[0] === \"a\" && value !== undefined && value !== \"\") {\n sets.push(value);\n }\n }\n return { emoji: collectEmoji(event.tags), sets };\n}\n\n/** Parse kind:30002 relay set public `d` and `relay` tags. */\nexport function parseRelaySet(event: Pick<Event, \"kind\" | \"tags\">): {\n d: string;\n relays: string[];\n} {\n requireKind(event, Kind.RelaySets);\n return { d: identifier(event.tags), relays: collectRelays(event.tags) };\n}\n\n/** Parse kind:10012 favorite relays public `relay` tags and kind:30002 `a` tags. */\nexport function parseFavoriteRelays(event: Pick<Event, \"kind\" | \"tags\">): {\n relays: string[];\n sets: string[];\n} {\n requireKind(event, Kind.FavoriteRelays);\n const sets: string[] = [];\n for (const tag of event.tags) {\n const value = tag.at(1);\n if (tag[0] !== \"a\" || value === undefined || !isRelaySetAddress(value)) {\n continue;\n }\n sets.push(value);\n }\n return { relays: collectRelays(event.tags), sets };\n}\n\n/** Parse kind:30030 emoji set public `d` / `title` / `emoji` tags. */\nexport function parseEmojiSet(event: Pick<Event, \"kind\" | \"tags\">): {\n d: string;\n title?: string;\n emoji: Array<{ shortcode: string; url: string }>;\n} {\n requireKind(event, Kind.EmojiSet);\n const result: {\n d: string;\n title?: string;\n emoji: Array<{ shortcode: string; url: string }>;\n } = { d: identifier(event.tags), emoji: collectEmoji(event.tags) };\n const title = firstTagValue(event.tags, \"title\");\n if (title !== undefined) {\n result.title = title;\n }\n return result;\n}\n\n/** Parse kind:39089 starter pack (follow pack) public `d` and `p` tags. */\nexport function parseFollowPack(event: Pick<Event, \"kind\" | \"tags\">): {\n d: string;\n pubkeys: string[];\n} {\n requireKind(event, Kind.StarterPack);\n const pubkeys: string[] = [];\n for (const tag of event.tags) {\n const value = tag.at(1);\n if (tag[0] !== \"p\" || value === undefined || !isHex32(value.toLowerCase())) {\n continue;\n }\n pubkeys.push(value.toLowerCase());\n }\n return { d: identifier(event.tags), pubkeys };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"nip51.mjs","names":[],"sources":["../../src/nips/nip51.ts"],"sourcesContent":["/**\n * NIP-51: Lists Public tags plus NIP-44 private `.content` (author encrypts to self). Crypto is\n * injected; this module does not import signer. No NIP-04 sniff. Kind 10063 Blossom servers live in\n * blossom.ts.\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/51.md\n */\nimport { 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 { firstTagValue, getDTag, isTag, Tag } from \"../core/tag.ts\";\nimport { assertHex32, isHex32, normalizeRelayUrls } from \"../core/util.ts\";\n\n/**\n * Structural crypto used by NIP-51 private tags. Satisfied by NostrSigner when\n * nip44Encrypt/nip44Decrypt are present. This module must not import src/signer/.\n */\nexport type Nip51Crypto = {\n getPublicKey: () => Promise<string>;\n nip44Encrypt: (peer: string, plaintext: string) => Promise<string>;\n nip44Decrypt: (peer: string, payload: string) => Promise<string>;\n};\n\nexport type MuteItem =\n | { type: \"pubkey\"; value: string }\n | { type: \"event\"; value: string }\n | { type: \"hashtag\"; value: string }\n | { type: \"word\"; value: string };\n\nfunction requireKind(event: Pick<Event, \"kind\">, kind: number): void {\n if (event.kind !== kind) {\n throw new EventValidationError(`expected kind ${kind}, got ${event.kind}`);\n }\n}\n\nfunction collectRelays(tags: Event[\"tags\"]): string[] {\n const urls: string[] = [];\n for (const tag of tags) {\n const value = tag.at(1);\n if (tag[0] === \"relay\" && value !== undefined) {\n urls.push(value);\n }\n }\n return normalizeRelayUrls(urls);\n}\n\nfunction collectEmoji(tags: Event[\"tags\"]): Array<{ shortcode: string; url: string }> {\n const out: Array<{ shortcode: string; url: string }> = [];\n for (const tag of tags) {\n if (tag[0] !== \"emoji\") {\n continue;\n }\n const shortcode = tag.at(1);\n const url = tag.at(2);\n if (shortcode === undefined || shortcode === \"\" || url === undefined || url === \"\") {\n continue;\n }\n out.push({ shortcode, url });\n }\n return out;\n}\n\nfunction identifier(tags: Event[\"tags\"]): string {\n return getDTag(tags) ?? \"\";\n}\n\n/** NIP-51 a-tag: `30002:<64-hex-pubkey>:<d>` with `d` possibly containing `:`. */\nfunction isRelaySetAddress(value: string): boolean {\n const kindSep = value.indexOf(\":\");\n if (kindSep === -1) {\n return false;\n }\n if (value.slice(0, kindSep) !== String(Kind.RelaySets)) {\n return false;\n }\n const pkSep = value.indexOf(\":\", kindSep + 1);\n if (pkSep === -1) {\n return false;\n }\n const pubkey = value.slice(kindSep + 1, pkSep);\n const d = value.slice(pkSep + 1);\n return isHex32(pubkey.toLowerCase()) && d.length > 0;\n}\n\n/** Parse kind:10000 mute list public tags (`p` / `e` / `t` / `word`). */\nexport function parseMuteList(event: Pick<Event, \"kind\" | \"tags\">): MuteItem[] {\n requireKind(event, Kind.MuteList);\n const items: MuteItem[] = [];\n for (const tag of event.tags) {\n const value = tag.at(1);\n if (value === undefined || value === \"\") {\n continue;\n }\n switch (tag[0]) {\n case \"p\":\n if (isHex32(value.toLowerCase())) {\n items.push({ type: \"pubkey\", value: value.toLowerCase() });\n }\n break;\n case \"e\":\n if (isHex32(value.toLowerCase())) {\n items.push({ type: \"event\", value: value.toLowerCase() });\n }\n break;\n case \"t\":\n // NIP-51 does not require hashtag case-folding.\n items.push({ type: \"hashtag\", value });\n break;\n case \"word\":\n items.push({ type: \"word\", value: value.toLowerCase() });\n break;\n default:\n break;\n }\n }\n return items;\n}\n\n/** Build an unsigned kind:10000 EventBuilder from public mute items. */\nexport function muteListEventBuilder(items: ReadonlyArray<MuteItem>): EventBuilder {\n const b = new EventBuilder(Kind.MuteList, \"\");\n for (const item of items) {\n switch (item.type) {\n case \"pubkey\":\n b.tag(Tag.p(assertHex32(item.value, \"public key\")));\n break;\n case \"event\":\n b.tag(Tag.e(assertHex32(item.value, \"event id\")));\n break;\n case \"hashtag\":\n if (item.value) {\n b.tag(Tag.t(item.value));\n }\n break;\n case \"word\":\n if (item.value) {\n b.tag([\"word\", item.value.toLowerCase()]);\n }\n break;\n }\n }\n return b;\n}\n\n/** NIP-44 ciphertext of tags, conversation peer = author's own pubkey. */\nexport async function encryptPrivateTags(\n crypto: Nip51Crypto,\n tags: Event[\"tags\"],\n): Promise<string> {\n const self = await crypto.getPublicKey();\n return crypto.nip44Encrypt(self, JSON.stringify(tags));\n}\n\n/** Decrypt NIP-51 `.content`. Empty content is no private tags, not ciphertext. */\nexport async function decryptPrivateTags(\n crypto: Nip51Crypto,\n event: Pick<Event, \"pubkey\" | \"content\">,\n): Promise<Event[\"tags\"]> {\n // NIP-44 rejects empty plaintext; empty `.content` means no private items.\n if (event.content === \"\") {\n return [];\n }\n const self = await crypto.getPublicKey();\n if (self.toLowerCase() !== event.pubkey.toLowerCase()) {\n throw new EventValidationError(\"NIP-51 private content is only for the author\");\n }\n const plaintext = await crypto.nip44Decrypt(self, event.content);\n let parsed: unknown;\n try {\n parsed = JSON.parse(plaintext);\n } catch (error) {\n throw new EventValidationError(\"invalid NIP-51 private tags\", { cause: error });\n }\n if (!Array.isArray(parsed) || !parsed.every(isTag)) {\n throw new EventValidationError(\"invalid NIP-51 private tags\");\n }\n return parsed;\n}\n\n/** Public mute items from tags; private mute items from NIP-44 `.content`. */\nexport async function parseMuteListPrivate(\n crypto: Nip51Crypto,\n event: Pick<Event, \"kind\" | \"tags\" | \"pubkey\" | \"content\">,\n): Promise<{ public: MuteItem[]; private: MuteItem[] }> {\n const pub = parseMuteList(event);\n const decrypted = await decryptPrivateTags(crypto, event);\n return {\n public: pub,\n private: parseMuteList({ kind: event.kind, tags: decrypted }),\n };\n}\n\n/** Parse kind:10001 pin list public `e` tags. */\nexport function parsePinList(event: Pick<Event, \"kind\" | \"tags\">): string[] {\n requireKind(event, Kind.PinList);\n const ids: string[] = [];\n for (const tag of event.tags) {\n const value = tag.at(1);\n if (tag[0] !== \"e\" || value === undefined || !isHex32(value.toLowerCase())) {\n continue;\n }\n ids.push(value.toLowerCase());\n }\n return ids;\n}\n\n/** Build an unsigned kind:10001 EventBuilder from event ids. */\nexport function pinListEventBuilder(ids: ReadonlyArray<string>): EventBuilder {\n const b = new EventBuilder(Kind.PinList, \"\");\n for (const id of ids) {\n b.tag(Tag.e(assertHex32(id, \"event id\")));\n }\n return b;\n}\n\n/** Parse kind:10003 bookmark list public `e` and `a` tags. */\nexport function parseBookmarkList(event: Pick<Event, \"kind\" | \"tags\">): {\n e: string[];\n a: string[];\n} {\n requireKind(event, Kind.BookmarkList);\n const e: string[] = [];\n const a: string[] = [];\n for (const tag of event.tags) {\n const value = tag.at(1);\n if (value === undefined || value === \"\") {\n continue;\n }\n if (tag[0] === \"e\") {\n if (isHex32(value.toLowerCase())) {\n e.push(value.toLowerCase());\n }\n } else if (tag[0] === \"a\") {\n a.push(value);\n }\n }\n return { e, a };\n}\n\n/** Build an unsigned kind:10003 EventBuilder from public bookmark pointers. */\nexport function bookmarkListEventBuilder(items: {\n e?: ReadonlyArray<string>;\n a?: ReadonlyArray<string>;\n}): EventBuilder {\n const b = new EventBuilder(Kind.BookmarkList, \"\");\n if (items.e) {\n for (const id of items.e) {\n b.tag(Tag.e(assertHex32(id, \"event id\")));\n }\n }\n if (items.a) {\n for (const coord of items.a) {\n if (coord) {\n b.tag(Tag.a(coord));\n }\n }\n }\n return b;\n}\n\n/** Parse kind:10030 user emoji list public `emoji` and `a` tags. */\nexport function parseUserEmojiList(event: Pick<Event, \"kind\" | \"tags\">): {\n emoji: Array<{ shortcode: string; url: string }>;\n sets: string[];\n} {\n requireKind(event, Kind.UserEmojiList);\n const sets: string[] = [];\n for (const tag of event.tags) {\n const value = tag.at(1);\n if (tag[0] === \"a\" && value !== undefined && value !== \"\") {\n sets.push(value);\n }\n }\n return { emoji: collectEmoji(event.tags), sets };\n}\n\n/** Parse kind:30002 relay set public `d` and `relay` tags. */\nexport function parseRelaySet(event: Pick<Event, \"kind\" | \"tags\">): {\n d: string;\n relays: string[];\n} {\n requireKind(event, Kind.RelaySets);\n return { d: identifier(event.tags), relays: collectRelays(event.tags) };\n}\n\n/** Parse kind:10012 favorite relays public `relay` tags and kind:30002 `a` tags. */\nexport function parseFavoriteRelays(event: Pick<Event, \"kind\" | \"tags\">): {\n relays: string[];\n sets: string[];\n} {\n requireKind(event, Kind.FavoriteRelays);\n const sets: string[] = [];\n for (const tag of event.tags) {\n const value = tag.at(1);\n if (tag[0] !== \"a\" || value === undefined || !isRelaySetAddress(value)) {\n continue;\n }\n sets.push(value);\n }\n return { relays: collectRelays(event.tags), sets };\n}\n\n/** Parse kind:30030 emoji set public `d` / `title` / `emoji` tags. */\nexport function parseEmojiSet(event: Pick<Event, \"kind\" | \"tags\">): {\n d: string;\n title?: string;\n emoji: Array<{ shortcode: string; url: string }>;\n} {\n requireKind(event, Kind.EmojiSet);\n const result: {\n d: string;\n title?: string;\n emoji: Array<{ shortcode: string; url: string }>;\n } = { d: identifier(event.tags), emoji: collectEmoji(event.tags) };\n const title = firstTagValue(event.tags, \"title\");\n // empty title is treated as absent\n if (title !== undefined && title !== \"\") {\n result.title = title;\n }\n return result;\n}\n\n/** Parse kind:39089 starter pack (follow pack) public `d` and `p` tags. */\nexport function parseFollowPack(event: Pick<Event, \"kind\" | \"tags\">): {\n d: string;\n pubkeys: string[];\n} {\n requireKind(event, Kind.StarterPack);\n const pubkeys: string[] = [];\n for (const tag of event.tags) {\n const value = tag.at(1);\n if (tag[0] !== \"p\" || value === undefined || !isHex32(value.toLowerCase())) {\n continue;\n }\n pubkeys.push(value.toLowerCase());\n }\n return { d: identifier(event.tags), pubkeys };\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { n as EventTemplate, t as Event } from "../event-fU3Y01PR.mjs";
1
+ import { n as EventTemplate, t as Event } from "../event-DJeb_DIL.mjs";
2
2
  //#region src/nips/nip57.d.ts
3
3
  export type ProfileZapRequest = {
4
4
  pubkey: string;
@@ -22,10 +22,12 @@ export type ZapReceiptContext = {
22
22
  lnurl?: string;
23
23
  };
24
24
  export type ZapReceiptValidation = {
25
- valid: boolean;
26
- reason?: string;
27
- request?: Event;
28
- amountMsats?: number;
25
+ readonly valid: true;
26
+ readonly request: Event;
27
+ readonly amountMsats: number | undefined;
28
+ } | {
29
+ readonly valid: false;
30
+ readonly reason: string;
29
31
  };
30
32
  export type Bolt11Fields = {
31
33
  amountMsats?: number;
@@ -1,7 +1,7 @@
1
- import { a as hexToBytes, d as utf8Encoder, p as EventValidationError } from "../util-LfQirgDk.mjs";
1
+ import { a as hexToBytes, d as nowSeconds, h as EventValidationError, p as utf8Encoder } from "../util-yFYzlbrm.mjs";
2
2
  import { r as isAddressableKind, t as Kind } from "../kind-Cxwhhkx8.mjs";
3
- import { b as parseEventAddress, g as eventAddress, h as Tag, m as validateSignedEvent, v as getDTag } from "../event-D8Nfoh4K.mjs";
4
- import { n as verifyEvent } from "../verifier-t5WoWbtY.mjs";
3
+ import { _ as firstTagValue, g as eventAddress, h as Tag, m as validateSignedEvent, x as parseEventAddress, y as getDTag } from "../event-DinQMmJ1.mjs";
4
+ import { n as verifyEvent } from "../verifier-BTLOpo1n.mjs";
5
5
  import { sha256 } from "@noble/hashes/sha2.js";
6
6
  import { bech32 } from "@scure/base";
7
7
  import { equalBytes } from "@noble/ciphers/utils.js";
@@ -13,6 +13,7 @@ import { equalBytes } from "@noble/ciphers/utils.js";
13
13
  * @see https://github.com/nostr-protocol/nips/blob/master/57.md
14
14
  */
15
15
  function makeZapRequest(params) {
16
+ if (!Number.isSafeInteger(params.amount) || params.amount <= 0) throw new EventValidationError("zap amount must be a positive integer (msats)");
16
17
  if (params.relays.length === 0) throw new EventValidationError("relays tag requires one or more URLs");
17
18
  const recipient = "event" in params ? params.event.pubkey : params.pubkey;
18
19
  const tags = [
@@ -33,14 +34,11 @@ function makeZapRequest(params) {
33
34
  if (params.lnurl !== void 0 && params.lnurl !== "") tags.push(["lnurl", params.lnurl]);
34
35
  return {
35
36
  kind: Kind.ZapRequest,
36
- created_at: Math.floor(Date.now() / 1e3),
37
+ created_at: nowSeconds(),
37
38
  content: params.comment ?? "",
38
39
  tags
39
40
  };
40
41
  }
41
- function firstTagValue(tags, name) {
42
- for (const tag of tags) if (tag[0] === name && tag[1] !== void 0) return tag[1];
43
- }
44
42
  function fail(reason) {
45
43
  return {
46
44
  valid: false,
@@ -67,10 +65,11 @@ function amountMsatsFromHrp(hrp) {
67
65
  const n = Number(m[1]);
68
66
  if (!Number.isSafeInteger(n)) return;
69
67
  const mul = m.at(2);
70
- if (mul === void 0) return n * MSATS_PER_BTC;
71
- if (mul === "m") return n * MSATS_PER_MILLI;
72
- if (mul === "u") return n * MSATS_PER_MICRO;
73
- if (mul === "n") return n * MSATS_PER_NANO;
68
+ const factor = mul === void 0 ? MSATS_PER_BTC : mul === "m" ? MSATS_PER_MILLI : mul === "u" ? MSATS_PER_MICRO : mul === "n" ? MSATS_PER_NANO : 0;
69
+ if (factor !== 0) {
70
+ const msats = n * factor;
71
+ return Number.isSafeInteger(msats) ? msats : void 0;
72
+ }
74
73
  if (n % 10 !== 0) return;
75
74
  return n / 10;
76
75
  }
@@ -238,14 +237,10 @@ function validateZapReceipt(receipt, ctx) {
238
237
  if (tag[0] !== "P") continue;
239
238
  if (tag[1] === void 0 || tag[1].toLowerCase() !== request.pubkey.toLowerCase()) return fail("receipt P mismatch");
240
239
  }
241
- const amountMsats = bolt11.amountMsats ?? (requestAmount === void 0 ? void 0 : parseMsatsTag(requestAmount));
242
- return amountMsats === void 0 ? {
243
- valid: true,
244
- request
245
- } : {
240
+ return {
246
241
  valid: true,
247
242
  request,
248
- amountMsats
243
+ amountMsats: bolt11.amountMsats ?? (requestAmount === void 0 ? void 0 : parseMsatsTag(requestAmount))
249
244
  };
250
245
  } catch {
251
246
  return fail("invalid receipt");
@@ -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). 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 { isAddressableKind, Kind } from \"../core/kind.ts\";\nimport { eventAddress, getDTag, parseEventAddress, Tag } from \"../core/tag.ts\";\nimport { hexToBytes, utf8Encoder } from \"../core/util.ts\";\nimport { verifyEvent } from \"../core/verifier.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 /** The `d` tagged field, UTF-8 decoded; omitted on invalid UTF-8. */\n description?: string;\n descriptionHash?: Uint8Array;\n paymentHash?: Uint8Array;\n /** Invoice creation time (unix seconds). */\n timestamp: number;\n /** `x` tagged field — seconds after `timestamp` until expiry. Defaults to 3600 when absent. */\n expiry: number;\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;\nconst BOLT11_TAG_PAYMENT_HASH = 1;\nconst BOLT11_TAG_EXPIRY = 6;\nconst BOLT11_TAG_DESCRIPTION = 13;\nconst BOLT11_TAG_DESCRIPTION_HASH = 23;\nconst DEFAULT_EXPIRY_SECONDS = 3600;\n\nconst utf8DecoderFatal = new TextDecoder(\"utf-8\", { fatal: true });\n\n/** Big-endian 5-bit words as a JS number (safe below 2^53). */\nfunction wordsToInt(words: ReadonlyArray<number>): number {\n let value = 0;\n for (const word of words) {\n value = value * 32 + word;\n }\n return value;\n}\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 timestamp: wordsToInt(words.slice(0, BOLT11_TIMESTAMP_WORDS)),\n expiry: DEFAULT_EXPIRY_SECONDS,\n };\n let sawExpiry = false;\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 === BOLT11_TAG_EXPIRY) {\n if (!sawExpiry) {\n sawExpiry = true;\n fields.expiry = wordsToInt(data);\n }\n continue;\n }\n if (type === BOLT11_TAG_DESCRIPTION) {\n const bytes = bech32.fromWordsUnsafe(data);\n if (bytes !== undefined) {\n try {\n fields.description ??= utf8DecoderFatal.decode(bytes);\n } catch {\n // Invalid UTF-8 — drop the field, not the invoice.\n }\n }\n continue;\n }\n if (\n (type !== BOLT11_TAG_PAYMENT_HASH && type !== BOLT11_TAG_DESCRIPTION_HASH) ||\n dataLen !== BOLT11_HASH_WORDS\n ) {\n continue;\n }\n const bytes = bech32.fromWordsUnsafe(data);\n if (!bytes || bytes.length !== HASH_BYTES) {\n continue;\n }\n if (type === BOLT11_TAG_PAYMENT_HASH) {\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
+ {"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 { isAddressableKind, Kind } from \"../core/kind.ts\";\nimport { eventAddress, firstTagValue, getDTag, parseEventAddress, Tag } from \"../core/tag.ts\";\nimport { hexToBytes, nowSeconds, utf8Encoder } from \"../core/util.ts\";\nimport { verifyEvent } from \"../core/verifier.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 (!Number.isSafeInteger(params.amount) || params.amount <= 0) {\n throw new EventValidationError(\"zap amount must be a positive integer (msats)\");\n }\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: nowSeconds(),\n content: params.comment ?? \"\",\n tags,\n };\n}\n\nexport type ZapReceiptContext = {\n nostrPubkey: string;\n lnurl?: string;\n};\n\nexport type ZapReceiptValidation =\n | {\n readonly valid: true;\n readonly request: Event;\n readonly amountMsats: number | undefined;\n }\n | { readonly valid: false; readonly reason: string };\n\nexport type Bolt11Fields = {\n amountMsats?: number;\n /** The `d` tagged field, UTF-8 decoded; omitted on invalid UTF-8. */\n description?: string;\n descriptionHash?: Uint8Array;\n paymentHash?: Uint8Array;\n /** Invoice creation time (unix seconds). */\n timestamp: number;\n /** `x` tagged field — seconds after `timestamp` until expiry. Defaults to 3600 when absent. */\n expiry: number;\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 const factor =\n mul === undefined\n ? MSATS_PER_BTC\n : mul === \"m\"\n ? MSATS_PER_MILLI\n : mul === \"u\"\n ? MSATS_PER_MICRO\n : mul === \"n\"\n ? MSATS_PER_NANO\n : 0;\n if (factor !== 0) {\n const msats = n * factor;\n return Number.isSafeInteger(msats) ? msats : undefined;\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;\nconst BOLT11_TAG_PAYMENT_HASH = 1;\nconst BOLT11_TAG_EXPIRY = 6;\nconst BOLT11_TAG_DESCRIPTION = 13;\nconst BOLT11_TAG_DESCRIPTION_HASH = 23;\nconst DEFAULT_EXPIRY_SECONDS = 3600;\n\nconst utf8DecoderFatal = new TextDecoder(\"utf-8\", { fatal: true });\n\n/** Big-endian 5-bit words as a JS number (safe below 2^53). */\nfunction wordsToInt(words: ReadonlyArray<number>): number {\n let value = 0;\n for (const word of words) {\n value = value * 32 + word;\n }\n return value;\n}\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 timestamp: wordsToInt(words.slice(0, BOLT11_TIMESTAMP_WORDS)),\n expiry: DEFAULT_EXPIRY_SECONDS,\n };\n let sawExpiry = false;\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 === BOLT11_TAG_EXPIRY) {\n if (!sawExpiry) {\n sawExpiry = true;\n fields.expiry = wordsToInt(data);\n }\n continue;\n }\n if (type === BOLT11_TAG_DESCRIPTION) {\n const bytes = bech32.fromWordsUnsafe(data);\n if (bytes !== undefined) {\n try {\n fields.description ??= utf8DecoderFatal.decode(bytes);\n } catch {\n // Invalid UTF-8 — drop the field, not the invoice.\n }\n }\n continue;\n }\n if (\n (type !== BOLT11_TAG_PAYMENT_HASH && type !== BOLT11_TAG_DESCRIPTION_HASH) ||\n dataLen !== BOLT11_HASH_WORDS\n ) {\n continue;\n }\n const bytes = bech32.fromWordsUnsafe(data);\n if (!bytes || bytes.length !== HASH_BYTES) {\n continue;\n }\n if (type === BOLT11_TAG_PAYMENT_HASH) {\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 { 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-zQgdLcEr.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-BFIihLYY.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,8 +1,8 @@
1
- import { c as isRecord, g as NostrError, n as assertHex32 } from "../util-LfQirgDk.mjs";
1
+ import { c as isRecord, d as nowSeconds, n as assertHex32, v as NostrError } from "../util-yFYzlbrm.mjs";
2
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, t as Keys } from "../key-Bps-LqX7.mjs";
5
- import { n as verifyEvent } from "../verifier-t5WoWbtY.mjs";
3
+ import { h as Tag, m as validateSignedEvent, n as getEventHash, p as validateEvent } from "../event-DinQMmJ1.mjs";
4
+ import { r as finalizeEvent, t as Keys } from "../key-bshqfmZL.mjs";
5
+ import { n as verifyEvent } from "../verifier-BTLOpo1n.mjs";
6
6
  import { encryptToPubkey } from "./nip44.mjs";
7
7
  import { randomBytes } from "@noble/hashes/utils.js";
8
8
  //#region src/nips/nip59.ts
@@ -14,9 +14,6 @@ import { randomBytes } from "@noble/hashes/utils.js";
14
14
  */
15
15
  var Nip59Error = class extends NostrError {
16
16
  name = "Nip59Error";
17
- constructor(message, options) {
18
- super(message, options);
19
- }
20
17
  };
21
18
  const TWO_DAYS_SECS = 172800;
22
19
  function isGiftWrapKind(kind) {
@@ -61,19 +58,23 @@ function eventToJson(event) {
61
58
  });
62
59
  }
63
60
  function defaultRandomInt(maxExclusive) {
64
- if (!Number.isSafeInteger(maxExclusive) || maxExclusive <= 0) throw new Nip59Error("randomInt bound must be a positive integer");
65
- const bytes = randomBytes(4);
66
- return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0) % maxExclusive;
61
+ if (!Number.isSafeInteger(maxExclusive) || maxExclusive <= 0 || maxExclusive > 4294967296) throw new Nip59Error("randomInt bound must be a positive integer within uint32 range");
62
+ const limit = Math.floor(4294967296 / maxExclusive) * maxExclusive;
63
+ for (;;) {
64
+ const bytes = randomBytes(4);
65
+ const n = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0);
66
+ if (n < limit) return n % maxExclusive;
67
+ }
67
68
  }
68
69
  function randomPastTimestamp(opts) {
69
- return (opts?.now ?? Math.floor(Date.now() / 1e3)) - (opts?.randomInt ?? defaultRandomInt)(TWO_DAYS_SECS);
70
+ return (opts?.now ?? nowSeconds()) - (opts?.randomInt ?? defaultRandomInt)(TWO_DAYS_SECS);
70
71
  }
71
72
  function createRumor(pubkey, template) {
72
73
  const unsigned = {
73
74
  kind: template.kind,
74
75
  content: template.content ?? "",
75
76
  tags: template.tags ? [...template.tags] : [],
76
- created_at: template.created_at ?? Math.floor(Date.now() / 1e3),
77
+ created_at: template.created_at ?? nowSeconds(),
77
78
  pubkey: assertHex32(pubkey, "public key")
78
79
  };
79
80
  return {
@@ -82,17 +83,16 @@ function createRumor(pubkey, template) {
82
83
  };
83
84
  }
84
85
  async function createSeal(crypto, recipient, rumor, opts) {
85
- const sealer = requireNip59Crypto(crypto);
86
86
  const recipientPk = assertHex32(recipient, "public key");
87
87
  let content;
88
88
  try {
89
- content = await sealer.nip44Encrypt(recipientPk, rumorToJson(rumor));
89
+ content = await crypto.nip44Encrypt(recipientPk, rumorToJson(rumor));
90
90
  } catch (error) {
91
91
  throw new Nip59Error("failed to encrypt", { cause: error });
92
92
  }
93
93
  const created_at = opts?.timestamps?.seal ?? (opts?.randomize === "wrap" ? rumor.created_at : randomPastTimestamp(opts));
94
- const pubkey = await sealer.getPublicKey();
95
- return sealer.signEvent({
94
+ const pubkey = await crypto.getPublicKey();
95
+ return crypto.signEvent({
96
96
  kind: Kind.Seal,
97
97
  content,
98
98
  created_at,
@@ -1 +1 @@
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 } 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 { verifyEvent } from \"../core/verifier.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
+ {"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 } 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, nowSeconds } from \"../core/util.ts\";\nimport { verifyEvent } from \"../core/verifier.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}\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 || maxExclusive > 0x100000000) {\n throw new Nip59Error(\"randomInt bound must be a positive integer within uint32 range\");\n }\n // rejection sampling: discard draws ≥ the largest multiple of maxExclusive that fits uint32\n const limit = Math.floor(0x100000000 / maxExclusive) * maxExclusive;\n for (;;) {\n const bytes = randomBytes(4);\n const n = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0);\n if (n < limit) {\n return n % maxExclusive;\n }\n }\n}\n\nexport function randomPastTimestamp(opts?: {\n now?: number | undefined;\n randomInt?: ((maxExclusive: number) => number) | undefined;\n}): number {\n const now = opts?.now ?? nowSeconds();\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 ?? nowSeconds(),\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 recipientPk = assertHex32(recipient, \"public key\");\n let content: string;\n try {\n content = await crypto.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 crypto.getPublicKey();\n return crypto.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-CjgmdpPB.mjs";
2
- export { RelayListItem, RelayMarker, markerOf, parseRelayList, readRelays, relayListEventBuilder, relayListToTags, writeRelays };
1
+ import { a as relayListEventBuilder, i as readRelays, n as RelayMarker, o as relayListToTags, r as parseRelayList, s as writeRelays, t as RelayListItem } from "../nip65-52HiwiyD.mjs";
2
+ export { RelayListItem, RelayMarker, parseRelayList, readRelays, relayListEventBuilder, relayListToTags, writeRelays };
@@ -1,14 +1,7 @@
1
- import { l as normalizeURL, p as EventValidationError } from "../util-LfQirgDk.mjs";
1
+ import { h as EventValidationError, u as normalizeURL } from "../util-yFYzlbrm.mjs";
2
2
  import { t as Kind } from "../kind-Cxwhhkx8.mjs";
3
- import { t as EventBuilder } from "../builder-9tjI6WQ3.mjs";
3
+ import { t as EventBuilder } from "../builder-CdLE4J6O.mjs";
4
4
  //#region src/nips/nip65.ts
5
- /** The NIP-65 marker an item's read/write flags map to. */
6
- function markerOf(item) {
7
- if (item.read && item.write) return "readwrite";
8
- if (item.read) return "read";
9
- if (item.write) return "write";
10
- return "readwrite";
11
- }
12
5
  /** Parse a kind:10002 NIP-65 event into relay list entries. */
13
6
  function parseRelayList(event) {
14
7
  if (event.kind !== Kind.RelayList) throw new EventValidationError(`expected kind ${Kind.RelayList}, got ${event.kind}`);
@@ -26,40 +19,24 @@ function parseRelayList(event) {
26
19
  if (seen.has(url)) continue;
27
20
  seen.add(url);
28
21
  const marker = tag.at(2);
29
- if (marker === "read") out.push({
30
- url,
31
- read: true,
32
- write: false
33
- });
34
- else if (marker === "write") out.push({
22
+ if (marker === "read" || marker === "write") out.push({
35
23
  url,
36
- read: false,
37
- write: true
24
+ marker
38
25
  });
39
26
  else out.push({
40
27
  url,
41
- read: true,
42
- write: true
28
+ marker: "both"
43
29
  });
44
30
  }
45
31
  return out;
46
32
  }
47
- /** Encode relay list items as NIP-65 `r` tags. */
33
+ /** Encode relay list items as NIP-65 `r` tags (`both` is unmarked). */
48
34
  function relayListToTags(items) {
49
- return items.map((item) => {
50
- if (item.read && item.write) return ["r", item.url];
51
- if (item.read) return [
52
- "r",
53
- item.url,
54
- "read"
55
- ];
56
- if (item.write) return [
57
- "r",
58
- item.url,
59
- "write"
60
- ];
61
- throw new EventValidationError("relay list item must be read, write, or both");
62
- });
35
+ return items.map((item) => item.marker === "both" ? ["r", item.url] : [
36
+ "r",
37
+ item.url,
38
+ item.marker
39
+ ]);
63
40
  }
64
41
  /** Build an unsigned kind:10002 EventBuilder from relay list items. */
65
42
  function relayListEventBuilder(items) {
@@ -67,13 +44,13 @@ function relayListEventBuilder(items) {
67
44
  }
68
45
  /** URLs of the read-enabled items. */
69
46
  function readRelays(items) {
70
- return items.filter((i) => i.read).map((i) => i.url);
47
+ return items.filter((i) => i.marker !== "write").map((i) => i.url);
71
48
  }
72
49
  /** URLs of the write-enabled items. */
73
50
  function writeRelays(items) {
74
- return items.filter((i) => i.write).map((i) => i.url);
51
+ return items.filter((i) => i.marker !== "read").map((i) => i.url);
75
52
  }
76
53
  //#endregion
77
- export { markerOf, parseRelayList, readRelays, relayListEventBuilder, relayListToTags, writeRelays };
54
+ export { parseRelayList, readRelays, relayListEventBuilder, relayListToTags, writeRelays };
78
55
 
79
56
  //# sourceMappingURL=nip65.mjs.map
@@ -1 +1 @@
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
+ {"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\" | \"both\";\n\n/** One NIP-65 relay-list entry: normalized URL plus its marker. */\nexport type RelayListItem = {\n readonly url: string;\n readonly marker: RelayMarker;\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\" || marker === \"write\") {\n out.push({ url, marker });\n } else {\n out.push({ url, marker: \"both\" });\n }\n }\n return out;\n}\n\n/** Encode relay list items as NIP-65 `r` tags (`both` is unmarked). */\nexport function relayListToTags(items: ReadonlyArray<RelayListItem>): Tag[] {\n return items.map((item) =>\n item.marker === \"both\" ? [\"r\", item.url] : [\"r\", item.url, item.marker],\n );\n}\n\n/** Build an unsigned kind:10002 EventBuilder from relay list items. */\nexport function relayListEventBuilder(items: ReadonlyArray<RelayListItem>): EventBuilder {\n return new EventBuilder(Kind.RelayList, \"\").tags(relayListToTags(items));\n}\n\n/** URLs of the read-enabled items. */\nexport function readRelays(items: ReadonlyArray<RelayListItem>): string[] {\n return items.filter((i) => i.marker !== \"write\").map((i) => i.url);\n}\n\n/** URLs of the write-enabled items. */\nexport function writeRelays(items: ReadonlyArray<RelayListItem>): string[] {\n return items.filter((i) => i.marker !== \"read\").map((i) => i.url);\n}\n"],"mappings":";;;;;AAiBA,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,UAAU,WAAW,SAClC,IAAI,KAAK;GAAE;GAAK;EAAO,CAAC;OAExB,IAAI,KAAK;GAAE;GAAK,QAAQ;EAAO,CAAC;CAEpC;CACA,OAAO;AACT;;AAGA,SAAgB,gBAAgB,OAA4C;CAC1E,OAAO,MAAM,KAAK,SAChB,KAAK,WAAW,SAAS,CAAC,KAAK,KAAK,GAAG,IAAI;EAAC;EAAK,KAAK;EAAK,KAAK;CAAM,CACxE;AACF;;AAGA,SAAgB,sBAAsB,OAAmD;CACvF,OAAO,IAAI,aAAa,KAAK,WAAW,EAAE,CAAC,CAAC,KAAK,gBAAgB,KAAK,CAAC;AACzE;;AAGA,SAAgB,WAAW,OAA+C;CACxE,OAAO,MAAM,QAAQ,MAAM,EAAE,WAAW,OAAO,CAAC,CAAC,KAAK,MAAM,EAAE,GAAG;AACnE;;AAGA,SAAgB,YAAY,OAA+C;CACzE,OAAO,MAAM,QAAQ,MAAM,EAAE,WAAW,MAAM,CAAC,CAAC,KAAK,MAAM,EAAE,GAAG;AAClE"}
@@ -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-DH4RhS5r.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-C9QcZn7b.mjs";
2
2
  export { DEFAULT_FRAME_SIZE_LIMIT, MAX_NEG_ROUNDS, NegItem, Negentropy, NegentropyStorageVector, Nip77Error, PROTOCOL_VERSION, ReconcileOutcome, runNegSession, storageFromEvents, storageFromItems };