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