@qntx/nostr 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/{builder-GBddxj8i.d.mts → builder-BrmLOVj_.d.mts} +2 -2
  2. package/dist/{builder-BQbFDO6S.mjs → builder-Cf2N12CG.mjs} +2 -2
  3. package/dist/{builder-BQbFDO6S.mjs.map → builder-Cf2N12CG.mjs.map} +1 -1
  4. package/dist/{client-CTmp5w84.mjs → client-B3_vToDX.mjs} +5 -5
  5. package/dist/{client-CTmp5w84.mjs.map → client-B3_vToDX.mjs.map} +1 -1
  6. package/dist/client.d.mts +1 -1
  7. package/dist/client.mjs +1 -1
  8. package/dist/core.d.mts +2 -2
  9. package/dist/core.mjs +2 -2
  10. package/dist/{error-UHkqzWL7.mjs → error-D3VNRVhL.mjs} +6 -2
  11. package/dist/error-D3VNRVhL.mjs.map +1 -0
  12. package/dist/{gossip-BTMdlUI7.d.mts → gossip-u0iBlZ1U.d.mts} +2 -2
  13. package/dist/gossip.d.mts +1 -1
  14. package/dist/{index-CSsnE32i.d.mts → index-DxQO1eWA.d.mts} +6 -6
  15. package/dist/{index-N7HUGgwo.d.mts → index-SwMknboJ.d.mts} +53 -3
  16. package/dist/{index-BP94lABY.d.mts → index-yq9xkONo.d.mts} +5 -5
  17. package/dist/index.d.mts +8 -8
  18. package/dist/index.mjs +9 -9
  19. package/dist/{key-k8LDEPZh.mjs → key-B218_SoL.mjs} +32 -7
  20. package/dist/key-B218_SoL.mjs.map +1 -0
  21. package/dist/{key-CyKN1RTi.d.mts → key-CwMVo14x.d.mts} +16 -4
  22. package/dist/{loaders-Dp2nXEFk.mjs → loaders-Xdbx-K61.mjs} +2 -2
  23. package/dist/{loaders-Dp2nXEFk.mjs.map → loaders-Xdbx-K61.mjs.map} +1 -1
  24. package/dist/loaders.d.mts +1 -1
  25. package/dist/loaders.mjs +1 -1
  26. package/dist/{nip17-B7MI82Iq.d.mts → nip17-BM4hJYBq.d.mts} +4 -4
  27. package/dist/{nip51-O_vhOiK2.d.mts → nip51-D_bu8Hph.d.mts} +2 -2
  28. package/dist/{nip59-CdDoJptA.d.mts → nip59-CGWfoItR.d.mts} +14 -6
  29. package/dist/{nip65-BL79HI8N.d.mts → nip65-DaWMg943.d.mts} +2 -2
  30. package/dist/nips/blossom.d.mts +1 -1
  31. package/dist/nips/blossom.mjs +1 -1
  32. package/dist/nips/nip04.d.mts +10 -0
  33. package/dist/nips/nip04.mjs +38 -8
  34. package/dist/nips/nip04.mjs.map +1 -1
  35. package/dist/nips/nip10.d.mts +1 -1
  36. package/dist/nips/nip10.mjs +1 -1
  37. package/dist/nips/nip17.d.mts +1 -1
  38. package/dist/nips/nip17.mjs +2 -2
  39. package/dist/nips/nip17.mjs.map +1 -1
  40. package/dist/nips/nip44.d.mts +5 -0
  41. package/dist/nips/nip44.mjs +9 -2
  42. package/dist/nips/nip44.mjs.map +1 -1
  43. package/dist/nips/nip49.d.mts +7 -1
  44. package/dist/nips/nip49.mjs +16 -2
  45. package/dist/nips/nip49.mjs.map +1 -1
  46. package/dist/nips/nip51.d.mts +1 -1
  47. package/dist/nips/nip51.mjs +1 -1
  48. package/dist/nips/nip59.d.mts +1 -1
  49. package/dist/nips/nip59.mjs +47 -19
  50. package/dist/nips/nip59.mjs.map +1 -1
  51. package/dist/nips/nip65.d.mts +1 -1
  52. package/dist/nips/nip65.mjs +1 -1
  53. package/dist/nips/nip98.mjs +9 -1
  54. package/dist/nips/nip98.mjs.map +1 -1
  55. package/dist/{pool-BMUsw2un.mjs → pool-THuuOW4S.mjs} +2 -2
  56. package/dist/{pool-BMUsw2un.mjs.map → pool-THuuOW4S.mjs.map} +1 -1
  57. package/dist/{relay-DXtwUEdX.mjs → relay-BGPqiGKF.mjs} +2 -2
  58. package/dist/{relay-DXtwUEdX.mjs.map → relay-BGPqiGKF.mjs.map} +1 -1
  59. package/dist/relay.mjs +2 -2
  60. package/dist/{signer-CWtxpOxc.mjs → signer-CNTIeYH4.mjs} +169 -10
  61. package/dist/signer-CNTIeYH4.mjs.map +1 -0
  62. package/dist/signer.d.mts +2 -2
  63. package/dist/signer.mjs +3 -3
  64. package/dist/testing.mjs +2 -2
  65. package/package.json +1 -1
  66. package/dist/error-UHkqzWL7.mjs.map +0 -1
  67. package/dist/key-k8LDEPZh.mjs.map +0 -1
  68. package/dist/signer-CWtxpOxc.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"nip17.mjs","names":["TagBuilder"],"sources":["../../src/nips/nip17.ts"],"sourcesContent":["/**\n * NIP-17: Private Direct Messages. Kind 10050 advertises where gift-wraps should be delivered. Kind\n * 14 rumor construction and per-recipient wrap live here. Envelope primitives live in nip59.ts.\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/17.md\n */\nimport { EventBuilder } from \"../core/builder.ts\";\nimport { EventValidationError, NostrError } 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 { Tag as TagBuilder } from \"../core/tag.ts\";\nimport { assertHex32, normalizeRelayUrls } from \"../core/util.ts\";\nimport { createGiftWrap, createRumor, createSeal } from \"./nip59.ts\";\nimport type { Nip59Crypto, Rumor, WrapOptions } from \"./nip59.ts\";\n\nexport class Nip17Error extends NostrError {\n override name = \"Nip17Error\";\n}\n\nexport type Recipient = {\n readonly pubkey: string;\n readonly relayHint?: string | undefined;\n};\n\nexport type ReplyTo = {\n readonly id: string;\n readonly relayHint?: string | undefined;\n};\n\nexport type ChatMessageOptions = {\n readonly created_at?: number | undefined;\n readonly subject?: string | undefined;\n readonly replyTo?: ReplyTo | undefined;\n};\n\n/** Parse kind:10050 DM relay list (`[\"relay\", url]` tags). */\nexport function parseDmRelayList(event: Pick<Event, \"kind\" | \"tags\">): string[] {\n if (event.kind !== Kind.DirectMessageRelaysList) {\n throw new EventValidationError(\n `expected kind ${Kind.DirectMessageRelaysList}, got ${event.kind}`,\n );\n }\n const urls: string[] = [];\n for (const tag of event.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\n/** Encode DM relay URLs as NIP-17 `relay` tags. */\nexport function dmRelayListToTags(relays: ReadonlyArray<string>): Tag[] {\n return normalizeRelayUrls(relays).map((url) => [\"relay\", url]);\n}\n\n/** Build an unsigned kind:10050 EventBuilder. NIP-17 requires ≥1 relay tag. */\nexport function dmRelayListEventBuilder(relays: ReadonlyArray<string>): EventBuilder {\n const tags = dmRelayListToTags(relays);\n if (tags.length === 0) {\n throw new Nip17Error(\"DM relay list requires at least one relay\");\n }\n return new EventBuilder(Kind.DirectMessageRelaysList, \"\").tags(tags);\n}\n\nfunction isRecipient(value: unknown): value is Recipient {\n return typeof value === \"object\" && value !== null && \"pubkey\" in value;\n}\n\nfunction asRecipientList(\n input: string | Recipient | ReadonlyArray<string | Recipient>,\n): ReadonlyArray<string | Recipient> {\n if (typeof input === \"string\" || isRecipient(input)) {\n return [input];\n }\n return input;\n}\n\n/** Accept a hex pubkey, a Recipient, or a readonly array of either. Dedup by pubkey. */\nexport function normalizeRecipients(\n input: string | Recipient | ReadonlyArray<string | Recipient>,\n): Recipient[] {\n const out: Recipient[] = [];\n const seen = new Set<string>();\n for (const item of asRecipientList(input)) {\n const rec: Recipient = typeof item === \"string\" ? { pubkey: item } : item;\n const pubkey = assertHex32(rec.pubkey, \"public key\");\n if (seen.has(pubkey)) {\n continue;\n }\n seen.add(pubkey);\n out.push({ pubkey, relayHint: rec.relayHint });\n }\n return out;\n}\n\nexport function buildChatMessageRumor(\n senderPubkey: string,\n recipients: ReadonlyArray<Recipient>,\n content: string,\n opts?: ChatMessageOptions,\n): Rumor {\n if (recipients.length === 0) {\n throw new Nip17Error(\"recipients must not be empty\");\n }\n const tags: Tag[] = [];\n for (const recipient of recipients) {\n const pk = assertHex32(recipient.pubkey, \"public key\");\n tags.push(TagBuilder.p(pk, recipient.relayHint));\n }\n if (opts?.replyTo) {\n // Kind 14 reply e-tag is unmarked: [\"e\", <id>, <relay-url>]\n tags.push(TagBuilder.e(assertHex32(opts.replyTo.id, \"event id\"), opts.replyTo.relayHint ?? \"\"));\n }\n if (opts?.subject !== undefined) {\n tags.push([\"subject\", opts.subject]);\n }\n return createRumor(senderPubkey, {\n kind: Kind.PrivateDirectMessage,\n content,\n tags,\n created_at: opts?.created_at,\n });\n}\n\nfunction wrapTargets(sender: string, recipients: ReadonlyArray<Recipient>): Recipient[] {\n const senderPk = sender.toLowerCase();\n const self = recipients.find((r) => r.pubkey.toLowerCase() === senderPk);\n const out: Recipient[] = [{ pubkey: senderPk, relayHint: self?.relayHint }];\n const seen = new Set<string>([senderPk]);\n for (const recipient of recipients) {\n const pk = recipient.pubkey.toLowerCase();\n if (seen.has(pk)) {\n continue;\n }\n seen.add(pk);\n out.push({ pubkey: pk, relayHint: recipient.relayHint });\n }\n return out;\n}\n\nexport async function wrapDirectMessage(\n crypto: Nip59Crypto,\n recipients: ReadonlyArray<Recipient>,\n rumor: Rumor,\n opts?: Pick<\n WrapOptions,\n \"now\" | \"randomInt\" | \"timestamps\" | \"expiration\" | \"ephemeral\" | \"randomize\"\n >,\n): Promise<ReadonlyArray<{ recipient: string; wrap: Event }>> {\n if (recipients.length === 0) {\n throw new Nip17Error(\"recipients must not be empty\");\n }\n const targets = wrapTargets(rumor.pubkey, recipients);\n const out: Array<{ recipient: string; wrap: Event }> = [];\n const timeOpts = opts\n ? {\n now: opts.now,\n randomInt: opts.randomInt,\n timestamps: opts.timestamps,\n expiration: opts.expiration,\n randomize: opts.randomize,\n }\n : undefined;\n for (const target of targets) {\n // oxlint-disable-next-line no-await-in-loop -- signer calls stay ordered, one recipient at a time\n const seal = await createSeal(crypto, target.pubkey, rumor, timeOpts);\n const wrap = createGiftWrap(seal, target.pubkey, {\n ...timeOpts,\n relayHint: target.relayHint,\n ephemeral: opts?.ephemeral,\n });\n out.push({ recipient: target.pubkey, wrap });\n }\n return out;\n}\n\nexport function requireDmRelays(pubkey: string, relays: ReadonlyArray<string>): string[] {\n if (relays.length === 0) {\n throw new Nip17Error(`pubkey ${pubkey} is not ready to receive DMs (no kind 10050)`);\n }\n return [...relays];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"nip17.mjs","names":["TagBuilder"],"sources":["../../src/nips/nip17.ts"],"sourcesContent":["/**\n * NIP-17: Private Direct Messages. Kind 10050 advertises where gift-wraps should be delivered. Kind\n * 14 rumor construction and per-recipient wrap live here. Envelope primitives live in nip59.ts.\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/17.md\n */\nimport { EventBuilder } from \"../core/builder.ts\";\nimport { EventValidationError, NostrError } 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 { Tag as TagBuilder } from \"../core/tag.ts\";\nimport { assertHex32, normalizeRelayUrls } from \"../core/util.ts\";\nimport { createGiftWrap, createRumor, createSeal } from \"./nip59.ts\";\nimport type { Nip59Crypto, Rumor, WrapOptions } from \"./nip59.ts\";\n\nexport class Nip17Error extends NostrError {\n override name = \"Nip17Error\";\n}\n\nexport type Recipient = {\n readonly pubkey: string;\n readonly relayHint?: string | undefined;\n};\n\nexport type ReplyTo = {\n readonly id: string;\n readonly relayHint?: string | undefined;\n};\n\nexport type ChatMessageOptions = {\n readonly created_at?: number | undefined;\n readonly subject?: string | undefined;\n readonly replyTo?: ReplyTo | undefined;\n};\n\n/** Parse kind:10050 DM relay list (`[\"relay\", url]` tags). */\nexport function parseDmRelayList(event: Pick<Event, \"kind\" | \"tags\">): string[] {\n if (event.kind !== Kind.DirectMessageRelaysList) {\n throw new EventValidationError(\n `expected kind ${Kind.DirectMessageRelaysList}, got ${event.kind}`,\n );\n }\n const urls: string[] = [];\n for (const tag of event.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\n/** Encode DM relay URLs as NIP-17 `relay` tags. */\nexport function dmRelayListToTags(relays: ReadonlyArray<string>): Tag[] {\n return normalizeRelayUrls(relays).map((url) => [\"relay\", url]);\n}\n\n/** Build an unsigned kind:10050 EventBuilder. NIP-17 requires ≥1 relay tag. */\nexport function dmRelayListEventBuilder(relays: ReadonlyArray<string>): EventBuilder {\n const tags = dmRelayListToTags(relays);\n if (tags.length === 0) {\n throw new Nip17Error(\"DM relay list requires at least one relay\");\n }\n return new EventBuilder(Kind.DirectMessageRelaysList, \"\").tags(tags);\n}\n\nfunction isRecipient(value: unknown): value is Recipient {\n return typeof value === \"object\" && value !== null && \"pubkey\" in value;\n}\n\nfunction asRecipientList(\n input: string | Recipient | ReadonlyArray<string | Recipient>,\n): ReadonlyArray<string | Recipient> {\n if (typeof input === \"string\" || isRecipient(input)) {\n return [input];\n }\n return input;\n}\n\n/** Accept a hex pubkey, a Recipient, or a readonly array of either. Dedup by pubkey. */\nexport function normalizeRecipients(\n input: string | Recipient | ReadonlyArray<string | Recipient>,\n): Recipient[] {\n const out: Recipient[] = [];\n const seen = new Set<string>();\n for (const item of asRecipientList(input)) {\n const rec: Recipient = typeof item === \"string\" ? { pubkey: item } : item;\n const pubkey = assertHex32(rec.pubkey, \"public key\");\n if (seen.has(pubkey)) {\n continue;\n }\n seen.add(pubkey);\n out.push({ pubkey, relayHint: rec.relayHint });\n }\n return out;\n}\n\nexport function buildChatMessageRumor(\n senderPubkey: string,\n recipients: ReadonlyArray<Recipient>,\n content: string,\n opts?: ChatMessageOptions,\n): Rumor {\n if (recipients.length === 0) {\n throw new Nip17Error(\"recipients must not be empty\");\n }\n const tags: Tag[] = [];\n for (const recipient of recipients) {\n const pk = assertHex32(recipient.pubkey, \"public key\");\n tags.push(TagBuilder.p(pk, recipient.relayHint));\n }\n if (opts?.replyTo) {\n // Kind 14 reply e-tag is unmarked: [\"e\", <id>, <relay-url>]\n tags.push(TagBuilder.e(assertHex32(opts.replyTo.id, \"event id\"), opts.replyTo.relayHint ?? \"\"));\n }\n if (opts?.subject !== undefined) {\n tags.push([\"subject\", opts.subject]);\n }\n return createRumor(senderPubkey, {\n kind: Kind.PrivateDirectMessage,\n content,\n tags,\n created_at: opts?.created_at,\n });\n}\n\nfunction wrapTargets(sender: string, recipients: ReadonlyArray<Recipient>): Recipient[] {\n const senderPk = sender.toLowerCase();\n const self = recipients.find((r) => r.pubkey.toLowerCase() === senderPk);\n const out: Recipient[] = [{ pubkey: senderPk, relayHint: self?.relayHint }];\n const seen = new Set<string>([senderPk]);\n for (const recipient of recipients) {\n const pk = recipient.pubkey.toLowerCase();\n if (seen.has(pk)) {\n continue;\n }\n seen.add(pk);\n out.push({ pubkey: pk, relayHint: recipient.relayHint });\n }\n return out;\n}\n\nexport async function wrapDirectMessage(\n crypto: Nip59Crypto,\n recipients: ReadonlyArray<Recipient>,\n rumor: Rumor,\n opts?: Pick<\n WrapOptions,\n \"now\" | \"randomBytes\" | \"timestamps\" | \"expiration\" | \"ephemeral\" | \"randomize\"\n >,\n): Promise<ReadonlyArray<{ recipient: string; wrap: Event }>> {\n if (recipients.length === 0) {\n throw new Nip17Error(\"recipients must not be empty\");\n }\n const targets = wrapTargets(rumor.pubkey, recipients);\n const out: Array<{ recipient: string; wrap: Event }> = [];\n const timeOpts = opts\n ? {\n now: opts.now,\n randomBytes: opts.randomBytes,\n timestamps: opts.timestamps,\n expiration: opts.expiration,\n randomize: opts.randomize,\n }\n : undefined;\n for (const target of targets) {\n // oxlint-disable-next-line no-await-in-loop -- signer calls stay ordered, one recipient at a time\n const seal = await createSeal(crypto, target.pubkey, rumor, timeOpts);\n const wrap = createGiftWrap(seal, target.pubkey, {\n ...timeOpts,\n relayHint: target.relayHint,\n ephemeral: opts?.ephemeral,\n });\n out.push({ recipient: target.pubkey, wrap });\n }\n return out;\n}\n\nexport function requireDmRelays(pubkey: string, relays: ReadonlyArray<string>): string[] {\n if (relays.length === 0) {\n throw new Nip17Error(`pubkey ${pubkey} is not ready to receive DMs (no kind 10050)`);\n }\n return [...relays];\n}\n"],"mappings":"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"}
@@ -8,6 +8,11 @@
8
8
  */
9
9
  export declare const DEFAULT_MAX_PAYLOAD_CHARS: number;
10
10
  export declare function getConversationKey(privkeyA: Uint8Array, pubkeyB: string): Uint8Array;
11
+ /**
12
+ * NIP-44 conversation key from the raw 32-byte ECDH x-coordinate instead of a secret key, for key
13
+ * holders that only expose ECDH ({@link NostrKeyOperations}).
14
+ */
15
+ export declare function getConversationKeyFromSharedSecret(sharedX: Uint8Array): Uint8Array;
11
16
  /** NIP-44 `get_message_keys`: HKDF-expand the 76-byte key material. */
12
17
  export declare function getMessageKeys(conversationKey: Uint8Array, nonce: Uint8Array): {
13
18
  chacha_key: Uint8Array;
@@ -25,7 +25,14 @@ function assert32(bytes, label) {
25
25
  function getConversationKey(privkeyA, pubkeyB) {
26
26
  assertSecretKeyBytes(privkeyA);
27
27
  assertHex32(pubkeyB, "public key");
28
- const sharedX = secp256k1.getSharedSecret(privkeyA, hexToBytes$1(`02${pubkeyB.toLowerCase()}`)).subarray(1, 33);
28
+ return getConversationKeyFromSharedSecret(secp256k1.getSharedSecret(privkeyA, hexToBytes$1(`02${pubkeyB.toLowerCase()}`)).subarray(1, 33));
29
+ }
30
+ /**
31
+ * NIP-44 conversation key from the raw 32-byte ECDH x-coordinate instead of a secret key, for key
32
+ * holders that only expose ECDH ({@link NostrKeyOperations}).
33
+ */
34
+ function getConversationKeyFromSharedSecret(sharedX) {
35
+ assert32(sharedX, "shared secret");
29
36
  return extract(sha256, sharedX, utf8Encoder.encode("nip44-v2"));
30
37
  }
31
38
  /** NIP-44 `get_message_keys`: HKDF-expand the 76-byte key material. */
@@ -130,6 +137,6 @@ function decryptFromPubkey(payload, secretKey, peerPubkey, opts) {
130
137
  return decrypt(payload, getConversationKey(secretKey, peerPubkey), opts);
131
138
  }
132
139
  //#endregion
133
- export { DEFAULT_MAX_PAYLOAD_CHARS, calcPaddedLen, decrypt, decryptFromPubkey, encrypt, encryptToPubkey, getConversationKey, getMessageKeys };
140
+ export { DEFAULT_MAX_PAYLOAD_CHARS, calcPaddedLen, decrypt, decryptFromPubkey, encrypt, encryptToPubkey, getConversationKey, getConversationKeyFromSharedSecret, getMessageKeys };
134
141
 
135
142
  //# sourceMappingURL=nip44.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"nip44.mjs","names":["hexToBytes","hkdf_extract","hkdf_expand"],"sources":["../../src/nips/nip44.ts"],"sourcesContent":["import { chacha20 } from \"@noble/ciphers/chacha.js\";\nimport { equalBytes } from \"@noble/ciphers/utils.js\";\nimport { secp256k1 } from \"@noble/curves/secp256k1.js\";\nimport { extract as hkdf_extract, expand as hkdf_expand } from \"@noble/hashes/hkdf.js\";\nimport { hmac } from \"@noble/hashes/hmac.js\";\nimport { sha256 } from \"@noble/hashes/sha2.js\";\nimport { concatBytes, randomBytes } from \"@noble/hashes/utils.js\";\nimport { base64 } from \"@scure/base\";\n\nimport { CryptoError } from \"../core/error.ts\";\nimport {\n assertHex32,\n assertSecretKeyBytes,\n hexToBytes,\n utf8Decoder,\n utf8Encoder,\n} from \"../core/util.ts\";\n\nconst minPlaintextSize = 0x0001;\nconst maxPlaintextSize = 0xffffffff;\nconst extendedPrefixThreshold = 0x10000;\n\n/**\n * Default decode-side payload cap: the base64 length of a payload carrying exactly 1 MiB (0x100000\n * bytes) of plaintext — version(1) + nonce(32) + extended prefix(6) + calcPaddedLen(1 MiB) +\n * mac(32) bytes. NIP-44 asks implementations to enforce their own maximum payload size; relays cap\n * events far below this. Raise it via `opts.maxPayloadChars` in `decrypt`/ `decryptFromPubkey` for\n * large local payloads.\n */\nexport const DEFAULT_MAX_PAYLOAD_CHARS: number = Math.ceil((71 + calcPaddedLen(0x100000)) / 3) * 4;\n\nfunction assert32(bytes: Uint8Array, label: string): void {\n if (bytes.length !== 32) {\n throw new CryptoError(`${label} must be 32 bytes`);\n }\n}\n\nexport function getConversationKey(privkeyA: Uint8Array, pubkeyB: string): Uint8Array {\n assertSecretKeyBytes(privkeyA);\n assertHex32(pubkeyB, \"public key\");\n const sharedX = secp256k1\n .getSharedSecret(privkeyA, hexToBytes(`02${pubkeyB.toLowerCase()}`))\n .subarray(1, 33);\n return hkdf_extract(sha256, sharedX, utf8Encoder.encode(\"nip44-v2\"));\n}\n\n/** NIP-44 `get_message_keys`: HKDF-expand the 76-byte key material. */\nexport function getMessageKeys(\n conversationKey: Uint8Array,\n nonce: Uint8Array,\n): { chacha_key: Uint8Array; chacha_nonce: Uint8Array; hmac_key: Uint8Array } {\n assert32(conversationKey, \"conversation_key\");\n assert32(nonce, \"nonce\");\n const keys = hkdf_expand(sha256, conversationKey, nonce, 76);\n return {\n chacha_key: keys.subarray(0, 32),\n chacha_nonce: keys.subarray(32, 44),\n hmac_key: keys.subarray(44, 76),\n };\n}\n\nexport function calcPaddedLen(len: number): number {\n if (!Number.isSafeInteger(len) || len < 1) {\n throw new CryptoError(\"expected positive integer\");\n }\n if (len <= 32) {\n return 32;\n }\n const nextPower = 2 ** (Math.floor(Math.log2(len - 1)) + 1);\n const chunk = nextPower <= 256 ? 32 : nextPower / 8;\n return chunk * (Math.floor((len - 1) / chunk) + 1);\n}\n\nfunction writeU16BE(num: number): Uint8Array {\n if (!Number.isSafeInteger(num) || num < minPlaintextSize || num > 0xffff) {\n throw new CryptoError(\"invalid plaintext size: must be between 1 and 65535 bytes\");\n }\n const arr = new Uint8Array(2);\n new DataView(arr.buffer).setUint16(0, num, false);\n return arr;\n}\n\nfunction writeU32BE(num: number): Uint8Array {\n if (!Number.isSafeInteger(num) || num < extendedPrefixThreshold || num > maxPlaintextSize) {\n throw new CryptoError(\"invalid plaintext size: must be between 65536 and 4294967295 bytes\");\n }\n const arr = new Uint8Array(4);\n new DataView(arr.buffer).setUint32(0, num, false);\n return arr;\n}\n\nfunction pad(plaintext: string): Uint8Array {\n const unpadded = utf8Encoder.encode(plaintext);\n const unpaddedLen = unpadded.length;\n if (unpaddedLen < minPlaintextSize || unpaddedLen > maxPlaintextSize) {\n throw new CryptoError(\"invalid plaintext size: must be between 1 and 4294967295 bytes\");\n }\n const prefix =\n unpaddedLen >= extendedPrefixThreshold\n ? concatBytes(new Uint8Array([0, 0]), writeU32BE(unpaddedLen))\n : writeU16BE(unpaddedLen);\n const suffix = new Uint8Array(calcPaddedLen(unpaddedLen) - unpaddedLen);\n return concatBytes(prefix, unpadded, suffix);\n}\n\nfunction unpad(padded: Uint8Array): string {\n const dv = new DataView(padded.buffer, padded.byteOffset, padded.byteLength);\n const firstTwo = dv.getUint16(0);\n let unpaddedLen: number;\n let prefixLen: number;\n if (firstTwo === 0) {\n unpaddedLen = dv.getUint32(2);\n if (unpaddedLen < extendedPrefixThreshold) {\n throw new CryptoError(\"invalid padding\");\n }\n prefixLen = 6;\n } else {\n unpaddedLen = firstTwo;\n prefixLen = 2;\n }\n const unpadded = padded.subarray(prefixLen, prefixLen + unpaddedLen);\n if (\n unpaddedLen < minPlaintextSize ||\n unpaddedLen > maxPlaintextSize ||\n unpadded.length !== unpaddedLen ||\n padded.length !== prefixLen + calcPaddedLen(unpaddedLen)\n ) {\n throw new CryptoError(\"invalid padding\");\n }\n return utf8Decoder.decode(unpadded);\n}\n\nfunction hmacAad(key: Uint8Array, message: Uint8Array, aad: Uint8Array): Uint8Array {\n if (aad.length !== 32) {\n throw new CryptoError(\"AAD associated data must be 32 bytes\");\n }\n return hmac(sha256, key, concatBytes(aad, message));\n}\n\nfunction decodePayload(\n payload: string,\n maxPayloadChars: number,\n): {\n nonce: Uint8Array;\n ciphertext: Uint8Array;\n mac: Uint8Array;\n} {\n if (typeof payload !== \"string\") {\n throw new CryptoError(\"payload must be a valid string\");\n }\n if (payload.startsWith(\"#\")) {\n throw new CryptoError(\"unknown encryption version\");\n }\n if (payload.length < 132 || payload.length > maxPayloadChars) {\n throw new CryptoError(`invalid payload length: ${payload.length}`);\n }\n let data: Uint8Array;\n try {\n data = base64.decode(payload);\n } catch (error) {\n throw new CryptoError(\"invalid base64\", { cause: error });\n }\n if (data.length < 99) {\n throw new CryptoError(`invalid data length: ${data.length}`);\n }\n const version = data.at(0);\n if (version === undefined) {\n throw new CryptoError(\"invalid data length\");\n }\n if (version !== 2) {\n throw new CryptoError(`unknown encryption version ${version}`);\n }\n return {\n nonce: data.subarray(1, 33),\n ciphertext: data.subarray(33, -32),\n mac: data.subarray(-32),\n };\n}\n\nexport function encrypt(\n plaintext: string,\n conversationKey: Uint8Array,\n nonce: Uint8Array = randomBytes(32),\n): string {\n const { chacha_key, chacha_nonce, hmac_key } = getMessageKeys(conversationKey, nonce);\n const padded = pad(plaintext);\n const ciphertext = chacha20(chacha_key, chacha_nonce, padded);\n const mac = hmacAad(hmac_key, ciphertext, nonce);\n return base64.encode(concatBytes(new Uint8Array([2]), nonce, ciphertext, mac));\n}\n\nexport function decrypt(\n payload: string,\n conversationKey: Uint8Array,\n opts?: { maxPayloadChars?: number },\n): string {\n const { nonce, ciphertext, mac } = decodePayload(\n payload,\n opts?.maxPayloadChars ?? DEFAULT_MAX_PAYLOAD_CHARS,\n );\n const { chacha_key, chacha_nonce, hmac_key } = getMessageKeys(conversationKey, nonce);\n const calculatedMac = hmacAad(hmac_key, ciphertext, nonce);\n if (!equalBytes(calculatedMac, mac)) {\n throw new CryptoError(\"invalid MAC\");\n }\n const padded = chacha20(chacha_key, chacha_nonce, ciphertext);\n return unpad(padded);\n}\n\n/** Convenience: derive conversation key from secret/public pair and encrypt. */\nexport function encryptToPubkey(\n plaintext: string,\n secretKey: Uint8Array,\n peerPubkey: string,\n): string {\n return encrypt(plaintext, getConversationKey(secretKey, peerPubkey));\n}\n\n/** Convenience: decrypt a payload from a peer pubkey. */\nexport function decryptFromPubkey(\n payload: string,\n secretKey: Uint8Array,\n peerPubkey: string,\n opts?: { maxPayloadChars?: number },\n): string {\n return decrypt(payload, getConversationKey(secretKey, peerPubkey), opts);\n}\n"],"mappings":";;;;;;;;;;AAkBA,MAAM,mBAAmB;AACzB,MAAM,mBAAmB;AACzB,MAAM,0BAA0B;;;;;;;;AAShC,MAAa,4BAAoC,KAAK,MAAM,KAAK,cAAc,OAAQ,KAAK,CAAC,IAAI;AAEjG,SAAS,SAAS,OAAmB,OAAqB;CACxD,IAAI,MAAM,WAAW,IACnB,MAAM,IAAI,YAAY,GAAG,MAAM,kBAAkB;AAErD;AAEA,SAAgB,mBAAmB,UAAsB,SAA6B;CACpF,qBAAqB,QAAQ;CAC7B,YAAY,SAAS,YAAY;CACjC,MAAM,UAAU,UACb,gBAAgB,UAAUA,aAAW,KAAK,QAAQ,YAAY,GAAG,CAAC,CAAC,CACnE,SAAS,GAAG,EAAE;CACjB,OAAOC,QAAa,QAAQ,SAAS,YAAY,OAAO,UAAU,CAAC;AACrE;;AAGA,SAAgB,eACd,iBACA,OAC4E;CAC5E,SAAS,iBAAiB,kBAAkB;CAC5C,SAAS,OAAO,OAAO;CACvB,MAAM,OAAOC,OAAY,QAAQ,iBAAiB,OAAO,EAAE;CAC3D,OAAO;EACL,YAAY,KAAK,SAAS,GAAG,EAAE;EAC/B,cAAc,KAAK,SAAS,IAAI,EAAE;EAClC,UAAU,KAAK,SAAS,IAAI,EAAE;CAChC;AACF;AAEA,SAAgB,cAAc,KAAqB;CACjD,IAAI,CAAC,OAAO,cAAc,GAAG,KAAK,MAAM,GACtC,MAAM,IAAI,YAAY,2BAA2B;CAEnD,IAAI,OAAO,IACT,OAAO;CAET,MAAM,YAAY,MAAM,KAAK,MAAM,KAAK,KAAK,MAAM,CAAC,CAAC,IAAI;CACzD,MAAM,QAAQ,aAAa,MAAM,KAAK,YAAY;CAClD,OAAO,SAAS,KAAK,OAAO,MAAM,KAAK,KAAK,IAAI;AAClD;AAEA,SAAS,WAAW,KAAyB;CAC3C,IAAI,CAAC,OAAO,cAAc,GAAG,KAAK,MAAM,oBAAoB,MAAM,OAChE,MAAM,IAAI,YAAY,2DAA2D;CAEnF,MAAM,sBAAM,IAAI,WAAW,CAAC;CAC5B,IAAI,SAAS,IAAI,MAAM,CAAC,CAAC,UAAU,GAAG,KAAK,KAAK;CAChD,OAAO;AACT;AAEA,SAAS,WAAW,KAAyB;CAC3C,IAAI,CAAC,OAAO,cAAc,GAAG,KAAK,MAAM,2BAA2B,MAAM,kBACvE,MAAM,IAAI,YAAY,oEAAoE;CAE5F,MAAM,sBAAM,IAAI,WAAW,CAAC;CAC5B,IAAI,SAAS,IAAI,MAAM,CAAC,CAAC,UAAU,GAAG,KAAK,KAAK;CAChD,OAAO;AACT;AAEA,SAAS,IAAI,WAA+B;CAC1C,MAAM,WAAW,YAAY,OAAO,SAAS;CAC7C,MAAM,cAAc,SAAS;CAC7B,IAAI,cAAc,oBAAoB,cAAc,kBAClD,MAAM,IAAI,YAAY,gEAAgE;CAExF,MAAM,SACJ,eAAe,0BACX,YAAY,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC,GAAG,WAAW,WAAW,CAAC,IAC3D,WAAW,WAAW;CAC5B,MAAM,SAAS,IAAI,WAAW,cAAc,WAAW,IAAI,WAAW;CACtE,OAAO,YAAY,QAAQ,UAAU,MAAM;AAC7C;AAEA,SAAS,MAAM,QAA4B;CACzC,MAAM,KAAK,IAAI,SAAS,OAAO,QAAQ,OAAO,YAAY,OAAO,UAAU;CAC3E,MAAM,WAAW,GAAG,UAAU,CAAC;CAC/B,IAAI;CACJ,IAAI;CACJ,IAAI,aAAa,GAAG;EAClB,cAAc,GAAG,UAAU,CAAC;EAC5B,IAAI,cAAc,yBAChB,MAAM,IAAI,YAAY,iBAAiB;EAEzC,YAAY;CACd,OAAO;EACL,cAAc;EACd,YAAY;CACd;CACA,MAAM,WAAW,OAAO,SAAS,WAAW,YAAY,WAAW;CACnE,IACE,cAAc,oBACd,cAAc,oBACd,SAAS,WAAW,eACpB,OAAO,WAAW,YAAY,cAAc,WAAW,GAEvD,MAAM,IAAI,YAAY,iBAAiB;CAEzC,OAAO,YAAY,OAAO,QAAQ;AACpC;AAEA,SAAS,QAAQ,KAAiB,SAAqB,KAA6B;CAClF,IAAI,IAAI,WAAW,IACjB,MAAM,IAAI,YAAY,sCAAsC;CAE9D,OAAO,KAAK,QAAQ,KAAK,YAAY,KAAK,OAAO,CAAC;AACpD;AAEA,SAAS,cACP,SACA,iBAKA;CACA,IAAI,OAAO,YAAY,UACrB,MAAM,IAAI,YAAY,gCAAgC;CAExD,IAAI,QAAQ,WAAW,GAAG,GACxB,MAAM,IAAI,YAAY,4BAA4B;CAEpD,IAAI,QAAQ,SAAS,OAAO,QAAQ,SAAS,iBAC3C,MAAM,IAAI,YAAY,2BAA2B,QAAQ,QAAQ;CAEnE,IAAI;CACJ,IAAI;EACF,OAAO,OAAO,OAAO,OAAO;CAC9B,SAAS,OAAO;EACd,MAAM,IAAI,YAAY,kBAAkB,EAAE,OAAO,MAAM,CAAC;CAC1D;CACA,IAAI,KAAK,SAAS,IAChB,MAAM,IAAI,YAAY,wBAAwB,KAAK,QAAQ;CAE7D,MAAM,UAAU,KAAK,GAAG,CAAC;CACzB,IAAI,YAAY,KAAA,GACd,MAAM,IAAI,YAAY,qBAAqB;CAE7C,IAAI,YAAY,GACd,MAAM,IAAI,YAAY,8BAA8B,SAAS;CAE/D,OAAO;EACL,OAAO,KAAK,SAAS,GAAG,EAAE;EAC1B,YAAY,KAAK,SAAS,IAAI,GAAG;EACjC,KAAK,KAAK,SAAS,GAAG;CACxB;AACF;AAEA,SAAgB,QACd,WACA,iBACA,QAAoB,YAAY,EAAE,GAC1B;CACR,MAAM,EAAE,YAAY,cAAc,aAAa,eAAe,iBAAiB,KAAK;CACpF,MAAM,SAAS,IAAI,SAAS;CAC5B,MAAM,aAAa,SAAS,YAAY,cAAc,MAAM;CAC5D,MAAM,MAAM,QAAQ,UAAU,YAAY,KAAK;CAC/C,OAAO,OAAO,OAAO,YAAY,IAAI,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO,YAAY,GAAG,CAAC;AAC/E;AAEA,SAAgB,QACd,SACA,iBACA,MACQ;CACR,MAAM,EAAE,OAAO,YAAY,QAAQ,cACjC,SACA,MAAM,mBAAmB,yBAC3B;CACA,MAAM,EAAE,YAAY,cAAc,aAAa,eAAe,iBAAiB,KAAK;CACpF,MAAM,gBAAgB,QAAQ,UAAU,YAAY,KAAK;CACzD,IAAI,CAAC,WAAW,eAAe,GAAG,GAChC,MAAM,IAAI,YAAY,aAAa;CAGrC,OAAO,MADQ,SAAS,YAAY,cAAc,UAChC,CAAC;AACrB;;AAGA,SAAgB,gBACd,WACA,WACA,YACQ;CACR,OAAO,QAAQ,WAAW,mBAAmB,WAAW,UAAU,CAAC;AACrE;;AAGA,SAAgB,kBACd,SACA,WACA,YACA,MACQ;CACR,OAAO,QAAQ,SAAS,mBAAmB,WAAW,UAAU,GAAG,IAAI;AACzE"}
1
+ {"version":3,"file":"nip44.mjs","names":["hexToBytes","hkdf_extract","hkdf_expand"],"sources":["../../src/nips/nip44.ts"],"sourcesContent":["import { chacha20 } from \"@noble/ciphers/chacha.js\";\nimport { equalBytes } from \"@noble/ciphers/utils.js\";\nimport { secp256k1 } from \"@noble/curves/secp256k1.js\";\nimport { extract as hkdf_extract, expand as hkdf_expand } from \"@noble/hashes/hkdf.js\";\nimport { hmac } from \"@noble/hashes/hmac.js\";\nimport { sha256 } from \"@noble/hashes/sha2.js\";\nimport { concatBytes, randomBytes } from \"@noble/hashes/utils.js\";\nimport { base64 } from \"@scure/base\";\n\nimport { CryptoError } from \"../core/error.ts\";\nimport {\n assertHex32,\n assertSecretKeyBytes,\n hexToBytes,\n utf8Decoder,\n utf8Encoder,\n} from \"../core/util.ts\";\n\nconst minPlaintextSize = 0x0001;\nconst maxPlaintextSize = 0xffffffff;\nconst extendedPrefixThreshold = 0x10000;\n\n/**\n * Default decode-side payload cap: the base64 length of a payload carrying exactly 1 MiB (0x100000\n * bytes) of plaintext — version(1) + nonce(32) + extended prefix(6) + calcPaddedLen(1 MiB) +\n * mac(32) bytes. NIP-44 asks implementations to enforce their own maximum payload size; relays cap\n * events far below this. Raise it via `opts.maxPayloadChars` in `decrypt`/ `decryptFromPubkey` for\n * large local payloads.\n */\nexport const DEFAULT_MAX_PAYLOAD_CHARS: number = Math.ceil((71 + calcPaddedLen(0x100000)) / 3) * 4;\n\nfunction assert32(bytes: Uint8Array, label: string): void {\n if (bytes.length !== 32) {\n throw new CryptoError(`${label} must be 32 bytes`);\n }\n}\n\nexport function getConversationKey(privkeyA: Uint8Array, pubkeyB: string): Uint8Array {\n assertSecretKeyBytes(privkeyA);\n assertHex32(pubkeyB, \"public key\");\n const sharedX = secp256k1\n .getSharedSecret(privkeyA, hexToBytes(`02${pubkeyB.toLowerCase()}`))\n .subarray(1, 33);\n return getConversationKeyFromSharedSecret(sharedX);\n}\n\n/**\n * NIP-44 conversation key from the raw 32-byte ECDH x-coordinate instead of a secret key, for key\n * holders that only expose ECDH ({@link NostrKeyOperations}).\n */\nexport function getConversationKeyFromSharedSecret(sharedX: Uint8Array): Uint8Array {\n assert32(sharedX, \"shared secret\");\n return hkdf_extract(sha256, sharedX, utf8Encoder.encode(\"nip44-v2\"));\n}\n\n/** NIP-44 `get_message_keys`: HKDF-expand the 76-byte key material. */\nexport function getMessageKeys(\n conversationKey: Uint8Array,\n nonce: Uint8Array,\n): { chacha_key: Uint8Array; chacha_nonce: Uint8Array; hmac_key: Uint8Array } {\n assert32(conversationKey, \"conversation_key\");\n assert32(nonce, \"nonce\");\n const keys = hkdf_expand(sha256, conversationKey, nonce, 76);\n return {\n chacha_key: keys.subarray(0, 32),\n chacha_nonce: keys.subarray(32, 44),\n hmac_key: keys.subarray(44, 76),\n };\n}\n\nexport function calcPaddedLen(len: number): number {\n if (!Number.isSafeInteger(len) || len < 1) {\n throw new CryptoError(\"expected positive integer\");\n }\n if (len <= 32) {\n return 32;\n }\n const nextPower = 2 ** (Math.floor(Math.log2(len - 1)) + 1);\n const chunk = nextPower <= 256 ? 32 : nextPower / 8;\n return chunk * (Math.floor((len - 1) / chunk) + 1);\n}\n\nfunction writeU16BE(num: number): Uint8Array {\n if (!Number.isSafeInteger(num) || num < minPlaintextSize || num > 0xffff) {\n throw new CryptoError(\"invalid plaintext size: must be between 1 and 65535 bytes\");\n }\n const arr = new Uint8Array(2);\n new DataView(arr.buffer).setUint16(0, num, false);\n return arr;\n}\n\nfunction writeU32BE(num: number): Uint8Array {\n if (!Number.isSafeInteger(num) || num < extendedPrefixThreshold || num > maxPlaintextSize) {\n throw new CryptoError(\"invalid plaintext size: must be between 65536 and 4294967295 bytes\");\n }\n const arr = new Uint8Array(4);\n new DataView(arr.buffer).setUint32(0, num, false);\n return arr;\n}\n\nfunction pad(plaintext: string): Uint8Array {\n const unpadded = utf8Encoder.encode(plaintext);\n const unpaddedLen = unpadded.length;\n if (unpaddedLen < minPlaintextSize || unpaddedLen > maxPlaintextSize) {\n throw new CryptoError(\"invalid plaintext size: must be between 1 and 4294967295 bytes\");\n }\n const prefix =\n unpaddedLen >= extendedPrefixThreshold\n ? concatBytes(new Uint8Array([0, 0]), writeU32BE(unpaddedLen))\n : writeU16BE(unpaddedLen);\n const suffix = new Uint8Array(calcPaddedLen(unpaddedLen) - unpaddedLen);\n return concatBytes(prefix, unpadded, suffix);\n}\n\nfunction unpad(padded: Uint8Array): string {\n const dv = new DataView(padded.buffer, padded.byteOffset, padded.byteLength);\n const firstTwo = dv.getUint16(0);\n let unpaddedLen: number;\n let prefixLen: number;\n if (firstTwo === 0) {\n unpaddedLen = dv.getUint32(2);\n if (unpaddedLen < extendedPrefixThreshold) {\n throw new CryptoError(\"invalid padding\");\n }\n prefixLen = 6;\n } else {\n unpaddedLen = firstTwo;\n prefixLen = 2;\n }\n const unpadded = padded.subarray(prefixLen, prefixLen + unpaddedLen);\n if (\n unpaddedLen < minPlaintextSize ||\n unpaddedLen > maxPlaintextSize ||\n unpadded.length !== unpaddedLen ||\n padded.length !== prefixLen + calcPaddedLen(unpaddedLen)\n ) {\n throw new CryptoError(\"invalid padding\");\n }\n return utf8Decoder.decode(unpadded);\n}\n\nfunction hmacAad(key: Uint8Array, message: Uint8Array, aad: Uint8Array): Uint8Array {\n if (aad.length !== 32) {\n throw new CryptoError(\"AAD associated data must be 32 bytes\");\n }\n return hmac(sha256, key, concatBytes(aad, message));\n}\n\nfunction decodePayload(\n payload: string,\n maxPayloadChars: number,\n): {\n nonce: Uint8Array;\n ciphertext: Uint8Array;\n mac: Uint8Array;\n} {\n if (typeof payload !== \"string\") {\n throw new CryptoError(\"payload must be a valid string\");\n }\n if (payload.startsWith(\"#\")) {\n throw new CryptoError(\"unknown encryption version\");\n }\n if (payload.length < 132 || payload.length > maxPayloadChars) {\n throw new CryptoError(`invalid payload length: ${payload.length}`);\n }\n let data: Uint8Array;\n try {\n data = base64.decode(payload);\n } catch (error) {\n throw new CryptoError(\"invalid base64\", { cause: error });\n }\n if (data.length < 99) {\n throw new CryptoError(`invalid data length: ${data.length}`);\n }\n const version = data.at(0);\n if (version === undefined) {\n throw new CryptoError(\"invalid data length\");\n }\n if (version !== 2) {\n throw new CryptoError(`unknown encryption version ${version}`);\n }\n return {\n nonce: data.subarray(1, 33),\n ciphertext: data.subarray(33, -32),\n mac: data.subarray(-32),\n };\n}\n\nexport function encrypt(\n plaintext: string,\n conversationKey: Uint8Array,\n nonce: Uint8Array = randomBytes(32),\n): string {\n const { chacha_key, chacha_nonce, hmac_key } = getMessageKeys(conversationKey, nonce);\n const padded = pad(plaintext);\n const ciphertext = chacha20(chacha_key, chacha_nonce, padded);\n const mac = hmacAad(hmac_key, ciphertext, nonce);\n return base64.encode(concatBytes(new Uint8Array([2]), nonce, ciphertext, mac));\n}\n\nexport function decrypt(\n payload: string,\n conversationKey: Uint8Array,\n opts?: { maxPayloadChars?: number },\n): string {\n const { nonce, ciphertext, mac } = decodePayload(\n payload,\n opts?.maxPayloadChars ?? DEFAULT_MAX_PAYLOAD_CHARS,\n );\n const { chacha_key, chacha_nonce, hmac_key } = getMessageKeys(conversationKey, nonce);\n const calculatedMac = hmacAad(hmac_key, ciphertext, nonce);\n if (!equalBytes(calculatedMac, mac)) {\n throw new CryptoError(\"invalid MAC\");\n }\n const padded = chacha20(chacha_key, chacha_nonce, ciphertext);\n return unpad(padded);\n}\n\n/** Convenience: derive conversation key from secret/public pair and encrypt. */\nexport function encryptToPubkey(\n plaintext: string,\n secretKey: Uint8Array,\n peerPubkey: string,\n): string {\n return encrypt(plaintext, getConversationKey(secretKey, peerPubkey));\n}\n\n/** Convenience: decrypt a payload from a peer pubkey. */\nexport function decryptFromPubkey(\n payload: string,\n secretKey: Uint8Array,\n peerPubkey: string,\n opts?: { maxPayloadChars?: number },\n): string {\n return decrypt(payload, getConversationKey(secretKey, peerPubkey), opts);\n}\n"],"mappings":"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"}
@@ -19,6 +19,12 @@ export type Nip49EncryptOptions = {
19
19
  scrypt?: Scrypt | undefined;
20
20
  };
21
21
  export type Nip49DecryptOptions = {
22
+ /**
23
+ * Scrypt log2(N) ceiling, an integer in 1..22. Required: scrypt needs `128 * r * 2^logn` bytes of
24
+ * memory (4 GiB at logn 22), so a crafted `ncryptsec` could exhaust the caller's memory. Payloads
25
+ * above the ceiling are rejected with {@link Nip49Error} before scrypt runs.
26
+ */
27
+ maxLogN: number;
22
28
  /** Scrypt implementation. Defaults to noble `scryptAsync`. */
23
29
  scrypt?: Scrypt | undefined;
24
30
  };
@@ -28,6 +34,6 @@ export declare class Nip49Error extends NostrError {
28
34
  /** Encrypt a 32-byte secret key to an `ncryptsec` bech32 string. */
29
35
  export declare function encrypt(secretKey: Uint8Array, password: string, opts?: Nip49EncryptOptions): Promise<Ncryptsec>;
30
36
  /** Decrypt an `ncryptsec` bech32 string to a 32-byte secret key. */
31
- export declare function decrypt(ncryptsec: string, password: string, opts?: Nip49DecryptOptions): Promise<Uint8Array>;
37
+ export declare function decrypt(ncryptsec: string, password: string, opts: Nip49DecryptOptions): Promise<Uint8Array>;
32
38
  //#endregion
33
39
  //# sourceMappingURL=nip49.d.mts.map
@@ -1,6 +1,7 @@
1
1
  import { m as utf8Encoder, r as assertSecretKeyBytes, y as NostrError } from "../util-DDpPkiED.mjs";
2
2
  import { Bech32MaxSize, encodeBytes } from "./nip19.mjs";
3
3
  import { concatBytes, randomBytes } from "@noble/hashes/utils.js";
4
+ import { secp256k1 } from "@noble/curves/secp256k1.js";
4
5
  import { bech32 } from "@scure/base";
5
6
  import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
6
7
  import { scryptAsync } from "@noble/hashes/scrypt.js";
@@ -58,6 +59,8 @@ async function encrypt(secretKey, password, opts) {
58
59
  }
59
60
  /** Decrypt an `ncryptsec` bech32 string to a 32-byte secret key. */
60
61
  async function decrypt(ncryptsec, password, opts) {
62
+ const { maxLogN } = opts;
63
+ if (!Number.isInteger(maxLogN) || maxLogN < LOGN_MIN || maxLogN > LOGN_MAX) throw new Nip49Error(`invalid maxLogN ${maxLogN}, expected integer ${LOGN_MIN}..${LOGN_MAX}`);
61
64
  let prefix;
62
65
  let b;
63
66
  try {
@@ -74,15 +77,26 @@ async function decrypt(ncryptsec, password, opts) {
74
77
  if (version !== VERSION) throw new Nip49Error(`invalid version ${version}, expected 0x02`);
75
78
  const logn = b.at(1);
76
79
  if (logn === void 0) throw new Nip49Error("invalid ncryptsec length");
80
+ if (logn > maxLogN) throw new Nip49Error(`logn ${logn} exceeds maxLogN ${maxLogN}`);
77
81
  const salt = b.subarray(2, 18);
78
82
  const nonce = b.subarray(18, 42);
79
83
  const ksb = b.at(42);
80
84
  if (ksb === void 0) throw new Nip49Error("invalid ncryptsec length");
85
+ if (ksb !== 0 && ksb !== 1 && ksb !== 2) throw new Nip49Error(`invalid key security byte ${ksb}, expected 0x00, 0x01, or 0x02`);
81
86
  const aad = Uint8Array.from([ksb]);
82
87
  const ciphertext = b.subarray(43);
83
88
  try {
84
- const key = await deriveKey(password, salt, logn, opts?.scrypt ?? nobleScrypt);
85
- return xchacha20poly1305(key, nonce, aad).decrypt(ciphertext);
89
+ const key = await deriveKey(password, salt, logn, opts.scrypt ?? nobleScrypt);
90
+ try {
91
+ const secret = xchacha20poly1305(key, nonce, aad).decrypt(ciphertext);
92
+ if (!secp256k1.utils.isValidSecretKey(secret)) {
93
+ secret.fill(0);
94
+ throw new Nip49Error("invalid secret key");
95
+ }
96
+ return secret;
97
+ } finally {
98
+ key.fill(0);
99
+ }
86
100
  } catch (error) {
87
101
  if (error instanceof Nip49Error) throw error;
88
102
  throw new Nip49Error("failed to decrypt", { cause: error });
@@ -1 +1 @@
1
- {"version":3,"file":"nip49.mjs","names":[],"sources":["../../src/nips/nip49.ts"],"sourcesContent":["/**\n * NIP-49: Private Key Encryption (`ncryptsec`).\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/49.md\n */\nimport { xchacha20poly1305 } from \"@noble/ciphers/chacha.js\";\nimport { scryptAsync } from \"@noble/hashes/scrypt.js\";\nimport { concatBytes, randomBytes } from \"@noble/hashes/utils.js\";\nimport { bech32 } from \"@scure/base\";\n\nimport { NostrError } from \"../core/error.ts\";\nimport { assertSecretKeyBytes, utf8Encoder } from \"../core/util.ts\";\nimport { Bech32MaxSize, encodeBytes } from \"./nip19.ts\";\n\nexport type Ncryptsec = `ncryptsec1${string}`;\nexport type KeySecurityByte = 0x00 | 0x01 | 0x02;\n\nexport type ScryptParams = {\n readonly N: number;\n readonly r: number;\n readonly p: number;\n readonly dkLen: number;\n};\n\n/** Derive `params.dkLen` bytes from the NFKC-normalized UTF-8 password and salt (RFC 7914). */\nexport type Scrypt = (\n password: Uint8Array,\n salt: Uint8Array,\n params: ScryptParams,\n) => Promise<Uint8Array>;\n\nexport type Nip49EncryptOptions = {\n /** Scrypt log2(N) work factor, 1..22. Defaults to 16. */\n logn?: number | undefined;\n /** Key security byte recorded in the payload. Defaults to 0x02. */\n ksb?: KeySecurityByte | undefined;\n /** Scrypt implementation. Defaults to noble `scryptAsync`. */\n scrypt?: Scrypt | undefined;\n};\n\nexport type Nip49DecryptOptions = {\n /** Scrypt implementation. Defaults to noble `scryptAsync`. */\n scrypt?: Scrypt | undefined;\n};\n\nconst VERSION = 0x02;\nconst SALT_LEN = 16;\nconst NONCE_LEN = 24;\nconst PAYLOAD_LEN = 91;\nconst LOGN_MIN = 1;\nconst LOGN_MAX = 22;\nconst SCRYPT_R = 8;\nconst SCRYPT_P = 1;\n\nexport class Nip49Error extends NostrError {\n override name = \"Nip49Error\";\n}\n\nfunction assertLogn(logn: number): void {\n if (!Number.isInteger(logn) || logn < LOGN_MIN || logn > LOGN_MAX) {\n throw new Nip49Error(`invalid logn ${logn}, expected integer ${LOGN_MIN}..${LOGN_MAX}`);\n }\n}\n\n// noble 2.3: V (N blocks) + p B blocks + one tmp scratch block.\nconst nobleScrypt: Scrypt = async (password, salt, params) =>\n scryptAsync(password, salt, {\n ...params,\n maxmem: 128 * params.r * (params.N + params.p + 1),\n });\n\nasync function deriveKey(\n password: string,\n salt: Uint8Array,\n logn: number,\n kdf: Scrypt,\n): Promise<Uint8Array> {\n assertLogn(logn);\n let key: Uint8Array;\n try {\n key = await kdf(utf8Encoder.encode(password.normalize(\"NFKC\")), salt, {\n N: 2 ** logn,\n r: SCRYPT_R,\n p: SCRYPT_P,\n dkLen: 32,\n });\n } catch (error) {\n throw new Nip49Error(\"scrypt failed\", {\n cause: error,\n });\n }\n if (key.length !== 32) {\n throw new Nip49Error(`scrypt returned ${key.length} bytes, expected 32`);\n }\n return key;\n}\n\n/** Encrypt a 32-byte secret key to an `ncryptsec` bech32 string. */\nexport async function encrypt(\n secretKey: Uint8Array,\n password: string,\n opts?: Nip49EncryptOptions,\n): Promise<Ncryptsec> {\n assertSecretKeyBytes(secretKey);\n const logn = opts?.logn ?? 16;\n const salt = randomBytes(SALT_LEN);\n const key = await deriveKey(password, salt, logn, opts?.scrypt ?? nobleScrypt);\n const nonce = randomBytes(NONCE_LEN);\n const aad = Uint8Array.from([opts?.ksb ?? 0x02]);\n const ciphertext = xchacha20poly1305(key, nonce, aad).encrypt(secretKey);\n const bytes = concatBytes(\n Uint8Array.from([VERSION]),\n Uint8Array.from([logn]),\n salt,\n nonce,\n aad,\n ciphertext,\n );\n return encodeBytes(\"ncryptsec\", bytes);\n}\n\n/** Decrypt an `ncryptsec` bech32 string to a 32-byte secret key. */\nexport async function decrypt(\n ncryptsec: string,\n password: string,\n opts?: Nip49DecryptOptions,\n): Promise<Uint8Array> {\n let prefix: string;\n let b: Uint8Array;\n try {\n const decoded = bech32.decode(ncryptsec, Bech32MaxSize);\n ({ prefix } = decoded);\n b = new Uint8Array(bech32.fromWords(decoded.words));\n } catch (error) {\n throw new Nip49Error(\"invalid ncryptsec\", {\n cause: error,\n });\n }\n if (prefix !== \"ncryptsec\") {\n throw new Nip49Error(`invalid prefix ${prefix}, expected 'ncryptsec'`);\n }\n if (b.length !== PAYLOAD_LEN) {\n throw new Nip49Error(\"invalid ncryptsec length\");\n }\n const version = b.at(0);\n if (version === undefined) {\n throw new Nip49Error(\"invalid ncryptsec length\");\n }\n if (version !== VERSION) {\n throw new Nip49Error(`invalid version ${version}, expected 0x02`);\n }\n const logn = b.at(1);\n if (logn === undefined) {\n throw new Nip49Error(\"invalid ncryptsec length\");\n }\n const salt = b.subarray(2, 2 + SALT_LEN);\n const nonce = b.subarray(2 + SALT_LEN, 2 + SALT_LEN + NONCE_LEN);\n const ksb = b.at(2 + SALT_LEN + NONCE_LEN);\n if (ksb === undefined) {\n throw new Nip49Error(\"invalid ncryptsec length\");\n }\n const aad = Uint8Array.from([ksb]);\n const ciphertext = b.subarray(2 + SALT_LEN + NONCE_LEN + 1);\n try {\n const key = await deriveKey(password, salt, logn, opts?.scrypt ?? nobleScrypt);\n return xchacha20poly1305(key, nonce, aad).decrypt(ciphertext);\n } catch (error) {\n if (error instanceof Nip49Error) {\n throw error;\n }\n throw new Nip49Error(\"failed to decrypt\", {\n cause: error,\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"nip49.mjs","names":[],"sources":["../../src/nips/nip49.ts"],"sourcesContent":["/**\n * NIP-49: Private Key Encryption (`ncryptsec`).\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/49.md\n */\nimport { xchacha20poly1305 } from \"@noble/ciphers/chacha.js\";\nimport { secp256k1 } from \"@noble/curves/secp256k1.js\";\nimport { scryptAsync } from \"@noble/hashes/scrypt.js\";\nimport { concatBytes, randomBytes } from \"@noble/hashes/utils.js\";\nimport { bech32 } from \"@scure/base\";\n\nimport { NostrError } from \"../core/error.ts\";\nimport { assertSecretKeyBytes, utf8Encoder } from \"../core/util.ts\";\nimport { Bech32MaxSize, encodeBytes } from \"./nip19.ts\";\n\nexport type Ncryptsec = `ncryptsec1${string}`;\nexport type KeySecurityByte = 0x00 | 0x01 | 0x02;\n\nexport type ScryptParams = {\n readonly N: number;\n readonly r: number;\n readonly p: number;\n readonly dkLen: number;\n};\n\n/** Derive `params.dkLen` bytes from the NFKC-normalized UTF-8 password and salt (RFC 7914). */\nexport type Scrypt = (\n password: Uint8Array,\n salt: Uint8Array,\n params: ScryptParams,\n) => Promise<Uint8Array>;\n\nexport type Nip49EncryptOptions = {\n /** Scrypt log2(N) work factor, 1..22. Defaults to 16. */\n logn?: number | undefined;\n /** Key security byte recorded in the payload. Defaults to 0x02. */\n ksb?: KeySecurityByte | undefined;\n /** Scrypt implementation. Defaults to noble `scryptAsync`. */\n scrypt?: Scrypt | undefined;\n};\n\nexport type Nip49DecryptOptions = {\n /**\n * Scrypt log2(N) ceiling, an integer in 1..22. Required: scrypt needs `128 * r * 2^logn` bytes of\n * memory (4 GiB at logn 22), so a crafted `ncryptsec` could exhaust the caller's memory. Payloads\n * above the ceiling are rejected with {@link Nip49Error} before scrypt runs.\n */\n maxLogN: number;\n /** Scrypt implementation. Defaults to noble `scryptAsync`. */\n scrypt?: Scrypt | undefined;\n};\n\nconst VERSION = 0x02;\nconst SALT_LEN = 16;\nconst NONCE_LEN = 24;\nconst PAYLOAD_LEN = 91;\nconst LOGN_MIN = 1;\nconst LOGN_MAX = 22;\nconst SCRYPT_R = 8;\nconst SCRYPT_P = 1;\n\nexport class Nip49Error extends NostrError {\n override name = \"Nip49Error\";\n}\n\nfunction assertLogn(logn: number): void {\n if (!Number.isInteger(logn) || logn < LOGN_MIN || logn > LOGN_MAX) {\n throw new Nip49Error(`invalid logn ${logn}, expected integer ${LOGN_MIN}..${LOGN_MAX}`);\n }\n}\n\n// noble 2.3: V (N blocks) + p B blocks + one tmp scratch block.\nconst nobleScrypt: Scrypt = async (password, salt, params) =>\n scryptAsync(password, salt, {\n ...params,\n maxmem: 128 * params.r * (params.N + params.p + 1),\n });\n\nasync function deriveKey(\n password: string,\n salt: Uint8Array,\n logn: number,\n kdf: Scrypt,\n): Promise<Uint8Array> {\n assertLogn(logn);\n let key: Uint8Array;\n try {\n key = await kdf(utf8Encoder.encode(password.normalize(\"NFKC\")), salt, {\n N: 2 ** logn,\n r: SCRYPT_R,\n p: SCRYPT_P,\n dkLen: 32,\n });\n } catch (error) {\n throw new Nip49Error(\"scrypt failed\", {\n cause: error,\n });\n }\n if (key.length !== 32) {\n throw new Nip49Error(`scrypt returned ${key.length} bytes, expected 32`);\n }\n return key;\n}\n\n/** Encrypt a 32-byte secret key to an `ncryptsec` bech32 string. */\nexport async function encrypt(\n secretKey: Uint8Array,\n password: string,\n opts?: Nip49EncryptOptions,\n): Promise<Ncryptsec> {\n assertSecretKeyBytes(secretKey);\n const logn = opts?.logn ?? 16;\n const salt = randomBytes(SALT_LEN);\n const key = await deriveKey(password, salt, logn, opts?.scrypt ?? nobleScrypt);\n const nonce = randomBytes(NONCE_LEN);\n const aad = Uint8Array.from([opts?.ksb ?? 0x02]);\n const ciphertext = xchacha20poly1305(key, nonce, aad).encrypt(secretKey);\n const bytes = concatBytes(\n Uint8Array.from([VERSION]),\n Uint8Array.from([logn]),\n salt,\n nonce,\n aad,\n ciphertext,\n );\n return encodeBytes(\"ncryptsec\", bytes);\n}\n\n/** Decrypt an `ncryptsec` bech32 string to a 32-byte secret key. */\nexport async function decrypt(\n ncryptsec: string,\n password: string,\n opts: Nip49DecryptOptions,\n): Promise<Uint8Array> {\n const { maxLogN } = opts;\n if (!Number.isInteger(maxLogN) || maxLogN < LOGN_MIN || maxLogN > LOGN_MAX) {\n throw new Nip49Error(`invalid maxLogN ${maxLogN}, expected integer ${LOGN_MIN}..${LOGN_MAX}`);\n }\n let prefix: string;\n let b: Uint8Array;\n try {\n const decoded = bech32.decode(ncryptsec, Bech32MaxSize);\n ({ prefix } = decoded);\n b = new Uint8Array(bech32.fromWords(decoded.words));\n } catch (error) {\n throw new Nip49Error(\"invalid ncryptsec\", {\n cause: error,\n });\n }\n if (prefix !== \"ncryptsec\") {\n throw new Nip49Error(`invalid prefix ${prefix}, expected 'ncryptsec'`);\n }\n if (b.length !== PAYLOAD_LEN) {\n throw new Nip49Error(\"invalid ncryptsec length\");\n }\n const version = b.at(0);\n if (version === undefined) {\n throw new Nip49Error(\"invalid ncryptsec length\");\n }\n if (version !== VERSION) {\n throw new Nip49Error(`invalid version ${version}, expected 0x02`);\n }\n const logn = b.at(1);\n if (logn === undefined) {\n throw new Nip49Error(\"invalid ncryptsec length\");\n }\n if (logn > maxLogN) {\n throw new Nip49Error(`logn ${logn} exceeds maxLogN ${maxLogN}`);\n }\n const salt = b.subarray(2, 2 + SALT_LEN);\n const nonce = b.subarray(2 + SALT_LEN, 2 + SALT_LEN + NONCE_LEN);\n const ksb = b.at(2 + SALT_LEN + NONCE_LEN);\n if (ksb === undefined) {\n throw new Nip49Error(\"invalid ncryptsec length\");\n }\n // NIP-49 defines 0x00, 0x01, and 0x02 as the key security byte.\n if (ksb !== 0x00 && ksb !== 0x01 && ksb !== 0x02) {\n throw new Nip49Error(`invalid key security byte ${ksb}, expected 0x00, 0x01, or 0x02`);\n }\n const aad = Uint8Array.from([ksb]);\n const ciphertext = b.subarray(2 + SALT_LEN + NONCE_LEN + 1);\n try {\n const key = await deriveKey(password, salt, logn, opts.scrypt ?? nobleScrypt);\n try {\n const secret = xchacha20poly1305(key, nonce, aad).decrypt(ciphertext);\n // Ruling N3: the decrypted bytes must form a valid secp256k1 scalar.\n if (!secp256k1.utils.isValidSecretKey(secret)) {\n secret.fill(0);\n throw new Nip49Error(\"invalid secret key\");\n }\n return secret;\n } finally {\n key.fill(0);\n }\n } catch (error) {\n if (error instanceof Nip49Error) {\n throw error;\n }\n throw new Nip49Error(\"failed to decrypt\", {\n cause: error,\n });\n }\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { a as encryptPrivateTags, c as parseEmojiSet, d as parseMuteList, f as parseMuteListPrivate, g as pinListEventBuilder, h as parseUserEmojiList, i as decryptPrivateTags, l as parseFavoriteRelays, m as parseRelaySet, n as Nip51Crypto, o as muteListEventBuilder, p as parsePinList, r as bookmarkListEventBuilder, s as parseBookmarkList, t as MuteItem, u as parseFollowPack } from "../nip51-O_vhOiK2.mjs";
1
+ import { a as encryptPrivateTags, c as parseEmojiSet, d as parseMuteList, f as parseMuteListPrivate, g as pinListEventBuilder, h as parseUserEmojiList, i as decryptPrivateTags, l as parseFavoriteRelays, m as parseRelaySet, n as Nip51Crypto, o as muteListEventBuilder, p as parsePinList, r as bookmarkListEventBuilder, s as parseBookmarkList, t as MuteItem, u as parseFollowPack } from "../nip51-D_bu8Hph.mjs";
2
2
  export { MuteItem, Nip51Crypto, bookmarkListEventBuilder, decryptPrivateTags, encryptPrivateTags, muteListEventBuilder, parseBookmarkList, parseEmojiSet, parseFavoriteRelays, parseFollowPack, parseMuteList, parseMuteListPrivate, parsePinList, parseRelaySet, parseUserEmojiList, pinListEventBuilder };
@@ -1,7 +1,7 @@
1
1
  import { g as EventValidationError, n as assertHex32, s as isHex32, u as normalizeRelayUrls } from "../util-DDpPkiED.mjs";
2
2
  import { t as Kind } from "../kind-D8fENiiL.mjs";
3
3
  import { b as getDTag, g as Tag, v as firstTagValue, x as isTag } from "../event-B3ua2_oL.mjs";
4
- import { t as EventBuilder } from "../builder-BQbFDO6S.mjs";
4
+ import { t as EventBuilder } from "../builder-Cf2N12CG.mjs";
5
5
  //#region src/nips/nip51.ts
6
6
  /**
7
7
  * NIP-51: Lists Public tags plus NIP-44 private `.content` (author encrypts to self). Crypto is
@@ -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-CdDoJptA.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-CGWfoItR.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,10 +1,11 @@
1
1
  import { f as nowSeconds, l as isRecord, n as assertHex32, y as NostrError } from "../util-DDpPkiED.mjs";
2
2
  import { t as Kind } from "../kind-D8fENiiL.mjs";
3
3
  import { g as Tag, h as validateSignedEvent, m as validateEvent, n as getEventHash } from "../event-B3ua2_oL.mjs";
4
- import { r as finalizeEvent, t as Keys } from "../key-k8LDEPZh.mjs";
4
+ import { n as SecretKey, o as signEvent, t as Keys } from "../key-B218_SoL.mjs";
5
5
  import { n as verifyEvent } from "../verifier-CjH1vnP2.mjs";
6
- import { encryptToPubkey } from "./nip44.mjs";
6
+ import { encrypt, getConversationKey } from "./nip44.mjs";
7
7
  import { randomBytes } from "@noble/hashes/utils.js";
8
+ import { secp256k1 } from "@noble/curves/secp256k1.js";
8
9
  //#region src/nips/nip59.ts
9
10
  /**
10
11
  * NIP-59: Gift Wrap. Rumor → NIP-44 seal (kind 13) → gift wrap (kind 1059). Does not import signer,
@@ -61,17 +62,17 @@ function assertExpiration(ts) {
61
62
  if (!Number.isSafeInteger(ts) || ts < 0) throw new Nip59Error("expiration must be a non-negative safe integer");
62
63
  return ts;
63
64
  }
64
- function defaultRandomInt(maxExclusive) {
65
- if (!Number.isSafeInteger(maxExclusive) || maxExclusive <= 0 || maxExclusive > 4294967296) throw new Nip59Error("randomInt bound must be a positive integer within uint32 range");
65
+ function defaultRandomInt(maxExclusive, drawBytes) {
66
+ if (!Number.isSafeInteger(maxExclusive) || maxExclusive <= 0 || maxExclusive > 4294967296) throw new Nip59Error("random bound must be a positive integer within uint32 range");
66
67
  const limit = Math.floor(4294967296 / maxExclusive) * maxExclusive;
67
68
  for (;;) {
68
- const bytes = randomBytes(4);
69
+ const bytes = drawBytes(4);
69
70
  const n = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0);
70
71
  if (n < limit) return n % maxExclusive;
71
72
  }
72
73
  }
73
74
  function randomPastTimestamp(opts) {
74
- return (opts?.now ?? nowSeconds()) - (opts?.randomInt ?? defaultRandomInt)(TWO_DAYS_SECS);
75
+ return (opts?.now ?? nowSeconds()) - defaultRandomInt(TWO_DAYS_SECS, opts?.randomBytes ?? randomBytes);
75
76
  }
76
77
  function createRumor(pubkey, template) {
77
78
  const unsigned = {
@@ -88,13 +89,13 @@ function createRumor(pubkey, template) {
88
89
  }
89
90
  async function createSeal(crypto, recipient, rumor, opts) {
90
91
  const recipientPk = assertHex32(recipient, "public key");
92
+ const created_at = opts?.timestamps?.seal ?? (opts?.randomize === "wrap" ? rumor.created_at : randomPastTimestamp(opts));
91
93
  let content;
92
94
  try {
93
95
  content = await crypto.nip44Encrypt(recipientPk, rumorToJson(rumor));
94
96
  } catch (error) {
95
97
  throw new Nip59Error("failed to encrypt", { cause: error });
96
98
  }
97
- const created_at = opts?.timestamps?.seal ?? (opts?.randomize === "wrap" ? rumor.created_at : randomPastTimestamp(opts));
98
99
  const tags = [];
99
100
  if (opts?.expiration !== void 0) tags.push(["expiration", String(assertExpiration(opts.expiration))]);
100
101
  const pubkey = await crypto.getPublicKey();
@@ -108,18 +109,45 @@ async function createSeal(crypto, recipient, rumor, opts) {
108
109
  }
109
110
  function createGiftWrap(seal, recipient, opts) {
110
111
  const recipientPk = assertHex32(recipient, "public key");
111
- const ephemeral = Keys.generate();
112
- const content = encryptToPubkey(eventToJson(seal), ephemeral.secretKey.bytes, recipientPk);
113
- const created_at = opts?.timestamps?.wrap ?? randomPastTimestamp(opts);
114
- const tags = [Tag.p(recipientPk, opts?.relayHint)];
115
- if (opts?.expiration !== void 0) tags.push(["expiration", String(assertExpiration(opts.expiration))]);
116
- tags.push(...opts?.extraTags ?? []);
117
- return finalizeEvent({
118
- kind: opts?.ephemeral === true ? Kind.GiftWrapEphemeral : Kind.GiftWrap,
119
- content,
120
- created_at,
121
- tags
122
- }, ephemeral.secretKey);
112
+ const drawBytes = opts?.randomBytes ?? randomBytes;
113
+ let secretKey;
114
+ for (;;) {
115
+ const candidate = drawBytes(32);
116
+ if (secp256k1.utils.isValidSecretKey(candidate)) {
117
+ secretKey = SecretKey.fromBytes(candidate);
118
+ candidate.fill(0);
119
+ break;
120
+ }
121
+ candidate.fill(0);
122
+ }
123
+ try {
124
+ const ephemeral = Keys.fromSecretKey(secretKey);
125
+ const created_at = opts?.timestamps?.wrap ?? randomPastTimestamp({
126
+ ...opts,
127
+ randomBytes: drawBytes
128
+ });
129
+ const secretBytes = secretKey.bytes;
130
+ let conversationKey;
131
+ try {
132
+ conversationKey = getConversationKey(secretBytes, recipientPk);
133
+ const content = encrypt(eventToJson(seal), conversationKey, drawBytes(32));
134
+ const tags = [Tag.p(recipientPk, opts?.relayHint)];
135
+ if (opts?.expiration !== void 0) tags.push(["expiration", String(assertExpiration(opts.expiration))]);
136
+ tags.push(...opts?.extraTags ?? []);
137
+ return signEvent({
138
+ kind: opts?.ephemeral === true ? Kind.GiftWrapEphemeral : Kind.GiftWrap,
139
+ content,
140
+ created_at,
141
+ tags,
142
+ pubkey: ephemeral.publicKey
143
+ }, ephemeral, drawBytes(32));
144
+ } finally {
145
+ secretBytes.fill(0);
146
+ conversationKey?.fill(0);
147
+ }
148
+ } finally {
149
+ secretKey.zeroize();
150
+ }
123
151
  }
124
152
  async function wrap(crypto, recipient, rumor, opts) {
125
153
  return createGiftWrap(await createSeal(crypto, recipient, rumor, opts), recipient, opts);
@@ -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, 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 * NIP-17 disappearing-message expiry (unix seconds): sets `[\"expiration\", ts]` on the seal and on\n * the gift wrap, signalling relays to discard the wrap after that time.\n */\n readonly expiration?: number | undefined;\n /** Use ephemeral gift wrap kind 21059 instead of 1059 (NIP-59; relays MUST NOT store). */\n readonly ephemeral?: boolean | 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<\n WrapOptions,\n \"now\" | \"randomInt\" | \"timestamps\" | \"expiration\" | \"randomize\"\n>;\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 assertExpiration(ts: number): number {\n if (!Number.isSafeInteger(ts) || ts < 0) {\n throw new Nip59Error(\"expiration must be a non-negative safe integer\");\n }\n return ts;\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 tags: Tag[] = [];\n if (opts?.expiration !== undefined) {\n tags.push([\"expiration\", String(assertExpiration(opts.expiration))]);\n }\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)];\n if (opts?.expiration !== undefined) {\n tags.push([\"expiration\", String(assertExpiration(opts.expiration))]);\n }\n tags.push(...(opts?.extraTags ?? []));\n return finalizeEvent(\n {\n kind: opts?.ephemeral === true ? Kind.GiftWrapEphemeral : 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 { secp256k1 } from \"@noble/curves/secp256k1.js\";\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, SecretKey, signEvent } 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 { encrypt, getConversationKey } 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 /**\n * Entropy source replacing @noble/hashes `randomBytes` for everything `wrap`/`createGiftWrap`\n * draws — in stream order: the ephemeral secret key (32-byte draws, redrawn while not a valid\n * scalar), the timestamp offset (4-byte big-endian u32 draws under the [0, 2 days) rejection\n * bound), the NIP-44 nonce (32 bytes), and the BIP-340 auxiliary randomness (32 bytes). In\n * `createSeal` only the timestamp offset is drawn here; the seal's nonce and aux belong to the\n * injected {@link Nip59Crypto}. `wrap` = seal draws then gift-wrap draws. Default: noble\n * `randomBytes`.\n */\n readonly randomBytes?: ((n: number) => Uint8Array) | undefined;\n /** When set, used as-is. Overrides now/randomBytes 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 * NIP-17 disappearing-message expiry (unix seconds): sets `[\"expiration\", ts]` on the seal and on\n * the gift wrap, signalling relays to discard the wrap after that time.\n */\n readonly expiration?: number | undefined;\n /** Use ephemeral gift wrap kind 21059 instead of 1059 (NIP-59; relays MUST NOT store). */\n readonly ephemeral?: boolean | 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<\n WrapOptions,\n \"now\" | \"randomBytes\" | \"timestamps\" | \"expiration\" | \"randomize\"\n>;\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 assertExpiration(ts: number): number {\n if (!Number.isSafeInteger(ts) || ts < 0) {\n throw new Nip59Error(\"expiration must be a non-negative safe integer\");\n }\n return ts;\n}\n\nfunction defaultRandomInt(maxExclusive: number, drawBytes: (n: number) => Uint8Array): number {\n if (!Number.isSafeInteger(maxExclusive) || maxExclusive <= 0 || maxExclusive > 0x100000000) {\n throw new Nip59Error(\"random 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 = drawBytes(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 randomBytes?: ((n: number) => Uint8Array) | undefined;\n}): number {\n const now = opts?.now ?? nowSeconds();\n const offset = defaultRandomInt(TWO_DAYS_SECS, opts?.randomBytes ?? randomBytes);\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 // The timestamp offset is drawn from `opts.randomBytes` before the crypto's nonce and aux so\n // the shared stream matches nk-nips: offset → nonce → aux.\n const created_at =\n opts?.timestamps?.seal ??\n (opts?.randomize === \"wrap\" ? rumor.created_at : randomPastTimestamp(opts));\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 tags: Tag[] = [];\n if (opts?.expiration !== undefined) {\n tags.push([\"expiration\", String(assertExpiration(opts.expiration))]);\n }\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 drawBytes = opts?.randomBytes ?? randomBytes;\n // Ephemeral key: redraw 32-byte candidates until a valid scalar, like\n // nk-core's `SecretKey::generate_with_rng`. Stream order: key → offset → nonce → aux. The\n // candidates, the ephemeral secret key, and the conversation key are wiped after use.\n let secretKey: SecretKey;\n for (;;) {\n const candidate = drawBytes(32);\n if (secp256k1.utils.isValidSecretKey(candidate)) {\n secretKey = SecretKey.fromBytes(candidate);\n candidate.fill(0);\n break;\n }\n candidate.fill(0);\n }\n try {\n const ephemeral = Keys.fromSecretKey(secretKey);\n const created_at =\n opts?.timestamps?.wrap ?? randomPastTimestamp({ ...opts, randomBytes: drawBytes });\n const secretBytes = secretKey.bytes;\n let conversationKey: Uint8Array | undefined;\n try {\n conversationKey = getConversationKey(secretBytes, recipientPk);\n const content = encrypt(eventToJson(seal), conversationKey, drawBytes(32));\n const tags: Tag[] = [TagBuilder.p(recipientPk, opts?.relayHint)];\n if (opts?.expiration !== undefined) {\n tags.push([\"expiration\", String(assertExpiration(opts.expiration))]);\n }\n tags.push(...(opts?.extraTags ?? []));\n return signEvent(\n {\n kind: opts?.ephemeral === true ? Kind.GiftWrapEphemeral : Kind.GiftWrap,\n content,\n created_at,\n tags,\n pubkey: ephemeral.publicKey,\n },\n ephemeral,\n drawBytes(32),\n );\n } finally {\n secretBytes.fill(0);\n conversationKey?.fill(0);\n }\n } finally {\n secretKey.zeroize();\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 relayListEventBuilder, i as readRelays, n as RelayMarker, o as relayListToTags, r as parseRelayList, s as writeRelays, t as RelayListItem } from "../nip65-BL79HI8N.mjs";
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-DaWMg943.mjs";
2
2
  export { RelayListItem, RelayMarker, parseRelayList, readRelays, relayListEventBuilder, relayListToTags, writeRelays };
@@ -1,6 +1,6 @@
1
1
  import { d as normalizeURL, g as EventValidationError } from "../util-DDpPkiED.mjs";
2
2
  import { t as Kind } from "../kind-D8fENiiL.mjs";
3
- import { t as EventBuilder } from "../builder-BQbFDO6S.mjs";
3
+ import { t as EventBuilder } from "../builder-Cf2N12CG.mjs";
4
4
  //#region src/nips/nip65.ts
5
5
  /** Parse a kind:10002 NIP-65 event into relay list entries. */
6
6
  function parseRelayList(event) {
@@ -43,7 +43,15 @@ async function getToken(url, method, sign, opts) {
43
43
  tags,
44
44
  content: opts?.content ?? ""
45
45
  });
46
- const encoded = base64.encode(utf8Encoder.encode(JSON.stringify(signed)));
46
+ const encoded = base64.encode(utf8Encoder.encode(JSON.stringify({
47
+ id: signed.id,
48
+ pubkey: signed.pubkey,
49
+ created_at: signed.created_at,
50
+ kind: signed.kind,
51
+ tags: signed.tags,
52
+ content: signed.content,
53
+ sig: signed.sig
54
+ })));
47
55
  return opts?.includeAuthorizationScheme === true ? AUTHORIZATION_SCHEME + encoded : encoded;
48
56
  }
49
57
  /** Decode a NIP-98 token (with or without the `Nostr ` scheme) into an event. */
@@ -1 +1 @@
1
- {"version":3,"file":"nip98.mjs","names":[],"sources":["../../src/nips/nip98.ts"],"sourcesContent":["/**\n * NIP-98: HTTP Auth (kind 27235). Token is standard base64 of the signed event JSON, not base64url.\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/98.md\n */\nimport { sha256 } from \"@noble/hashes/sha2.js\";\nimport { base64 } from \"@scure/base\";\n\nimport { NostrError } from \"../core/error.ts\";\nimport type { Event, EventTemplate } from \"../core/event.ts\";\nimport { validateSignedEvent } from \"../core/event.ts\";\nimport { Kind } from \"../core/kind.ts\";\nimport { firstTagValue } from \"../core/tag.ts\";\nimport type { Tag } from \"../core/tag.ts\";\nimport { bytesToHex, nowSeconds, utf8Decoder, utf8Encoder } from \"../core/util.ts\";\nimport { verifyEvent } from \"../core/verifier.ts\";\n\nconst AUTHORIZATION_SCHEME = \"Nostr \";\nconst DEFAULT_MAX_SKEW_SEC = 60;\n\nexport class Nip98Error extends NostrError {\n override name = \"Nip98Error\";\n}\n\n/** SHA-256 of the request body: raw bytes for string/Uint8Array, else JSON.stringify. */\nfunction hashPayload(payload: unknown): string {\n const bytes =\n typeof payload === \"string\"\n ? utf8Encoder.encode(payload)\n : payload instanceof Uint8Array\n ? payload\n : utf8Encoder.encode(JSON.stringify(payload));\n return bytesToHex(sha256(bytes));\n}\n\n/** RFC 4648 standard base64; accept missing `=` (NIP-98 spec example is unpadded). */\nfunction decodeStandardBase64(encoded: string): Uint8Array {\n const rem = encoded.length % 4;\n const padded = rem === 0 ? encoded : encoded + \"=\".repeat(4 - rem);\n return base64.decode(padded);\n}\n\nfunction stripAuthorizationScheme(token: string): string {\n const prefix = /^nostr\\s+/i.exec(token);\n return prefix ? token.slice(prefix[0].length) : token;\n}\n\n/**\n * Sign a kind 27235 auth event and encode it as a NIP-98 token. Uses standard base64\n * (`@scure/base`), not base64url.\n */\nexport async function getToken(\n url: string,\n method: string,\n sign: (template: EventTemplate) => Promise<Event> | Event,\n opts?: {\n includeAuthorizationScheme?: boolean | undefined;\n content?: string | undefined;\n payload?: unknown;\n now?: number | undefined;\n },\n): Promise<string> {\n const tags: Tag[] = [\n [\"u\", url],\n [\"method\", method],\n ];\n if (opts?.payload !== undefined) {\n tags.push([\"payload\", hashPayload(opts.payload)]);\n }\n\n const signed = await sign({\n kind: Kind.HttpAuth,\n created_at: opts?.now ?? nowSeconds(),\n tags,\n content: opts?.content ?? \"\",\n });\n\n const encoded = base64.encode(utf8Encoder.encode(JSON.stringify(signed)));\n return opts?.includeAuthorizationScheme === true ? AUTHORIZATION_SCHEME + encoded : encoded;\n}\n\n/** Decode a NIP-98 token (with or without the `Nostr ` scheme) into an event. */\nexport function unpackEventFromToken(token: string): Event {\n if (token === \"\") {\n throw new Nip98Error(\"missing token\");\n }\n\n const encoded = stripAuthorizationScheme(token);\n\n let json: string;\n try {\n json = utf8Decoder.decode(decodeStandardBase64(encoded));\n } catch (error) {\n throw new Nip98Error(\"invalid token encoding\", { cause: error });\n }\n if (!json.startsWith(\"{\")) {\n throw new Nip98Error(\"invalid token\");\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch (error) {\n throw new Nip98Error(\"invalid token JSON\", { cause: error });\n }\n if (!validateSignedEvent(parsed)) {\n throw new Nip98Error(\"token is not a signed event\");\n }\n return parsed;\n}\n\n/**\n * True when `event` is a valid NIP-98 auth event for `url` + `method`. Verifies signature, kind\n * 27235, timestamp window, `u`, `method`, and optional `payload` tag.\n */\nexport function validateAuthEvent(\n event: Event,\n url: string,\n method: string,\n opts?: { payload?: unknown; maxSkewSec?: number },\n): boolean {\n if (!verifyEvent(event)) {\n return false;\n }\n if (event.kind !== Kind.HttpAuth) {\n return false;\n }\n\n const maxSkewSec = opts?.maxSkewSec ?? DEFAULT_MAX_SKEW_SEC;\n const now = nowSeconds();\n if (Math.abs(now - event.created_at) > maxSkewSec) {\n return false;\n }\n\n const u = firstTagValue(event.tags, \"u\");\n if (u !== url) {\n return false;\n }\n\n const m = firstTagValue(event.tags, \"method\");\n if (m === undefined || m.toLowerCase() !== method.toLowerCase()) {\n return false;\n }\n\n if (opts?.payload !== undefined) {\n const payloadTag = firstTagValue(event.tags, \"payload\");\n if (payloadTag !== hashPayload(opts.payload)) {\n return false;\n }\n }\n\n return true;\n}\n"],"mappings":";;;;;;;;;;;;AAiBA,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAE7B,IAAa,aAAb,cAAgC,WAAW;CACzC,OAAgB;AAClB;;AAGA,SAAS,YAAY,SAA0B;CAC7C,MAAM,QACJ,OAAO,YAAY,WACf,YAAY,OAAO,OAAO,IAC1B,mBAAmB,aACjB,UACA,YAAY,OAAO,KAAK,UAAU,OAAO,CAAC;CAClD,OAAO,WAAW,OAAO,KAAK,CAAC;AACjC;;AAGA,SAAS,qBAAqB,SAA6B;CACzD,MAAM,MAAM,QAAQ,SAAS;CAC7B,MAAM,SAAS,QAAQ,IAAI,UAAU,UAAU,IAAI,OAAO,IAAI,GAAG;CACjE,OAAO,OAAO,OAAO,MAAM;AAC7B;AAEA,SAAS,yBAAyB,OAAuB;CACvD,MAAM,SAAS,aAAa,KAAK,KAAK;CACtC,OAAO,SAAS,MAAM,MAAM,OAAO,EAAE,CAAC,MAAM,IAAI;AAClD;;;;;AAMA,eAAsB,SACpB,KACA,QACA,MACA,MAMiB;CACjB,MAAM,OAAc,CAClB,CAAC,KAAK,GAAG,GACT,CAAC,UAAU,MAAM,CACnB;CACA,IAAI,MAAM,YAAY,KAAA,GACpB,KAAK,KAAK,CAAC,WAAW,YAAY,KAAK,OAAO,CAAC,CAAC;CAGlD,MAAM,SAAS,MAAM,KAAK;EACxB,MAAM,KAAK;EACX,YAAY,MAAM,OAAO,WAAW;EACpC;EACA,SAAS,MAAM,WAAW;CAC5B,CAAC;CAED,MAAM,UAAU,OAAO,OAAO,YAAY,OAAO,KAAK,UAAU,MAAM,CAAC,CAAC;CACxE,OAAO,MAAM,+BAA+B,OAAO,uBAAuB,UAAU;AACtF;;AAGA,SAAgB,qBAAqB,OAAsB;CACzD,IAAI,UAAU,IACZ,MAAM,IAAI,WAAW,eAAe;CAGtC,MAAM,UAAU,yBAAyB,KAAK;CAE9C,IAAI;CACJ,IAAI;EACF,OAAO,YAAY,OAAO,qBAAqB,OAAO,CAAC;CACzD,SAAS,OAAO;EACd,MAAM,IAAI,WAAW,0BAA0B,EAAE,OAAO,MAAM,CAAC;CACjE;CACA,IAAI,CAAC,KAAK,WAAW,GAAG,GACtB,MAAM,IAAI,WAAW,eAAe;CAGtC,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,IAAI;CAC1B,SAAS,OAAO;EACd,MAAM,IAAI,WAAW,sBAAsB,EAAE,OAAO,MAAM,CAAC;CAC7D;CACA,IAAI,CAAC,oBAAoB,MAAM,GAC7B,MAAM,IAAI,WAAW,6BAA6B;CAEpD,OAAO;AACT;;;;;AAMA,SAAgB,kBACd,OACA,KACA,QACA,MACS;CACT,IAAI,CAAC,YAAY,KAAK,GACpB,OAAO;CAET,IAAI,MAAM,SAAS,KAAK,UACtB,OAAO;CAGT,MAAM,aAAa,MAAM,cAAc;CACvC,MAAM,MAAM,WAAW;CACvB,IAAI,KAAK,IAAI,MAAM,MAAM,UAAU,IAAI,YACrC,OAAO;CAIT,IADU,cAAc,MAAM,MAAM,GAChC,MAAM,KACR,OAAO;CAGT,MAAM,IAAI,cAAc,MAAM,MAAM,QAAQ;CAC5C,IAAI,MAAM,KAAA,KAAa,EAAE,YAAY,MAAM,OAAO,YAAY,GAC5D,OAAO;CAGT,IAAI,MAAM,YAAY,KAAA,GACD;MAAA,cAAc,MAAM,MAAM,SAChC,MAAM,YAAY,KAAK,OAAO,GACzC,OAAO;CAAA;CAIX,OAAO;AACT"}
1
+ {"version":3,"file":"nip98.mjs","names":[],"sources":["../../src/nips/nip98.ts"],"sourcesContent":["/**\n * NIP-98: HTTP Auth (kind 27235). Token is standard base64 of the signed event JSON, not base64url.\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/98.md\n */\nimport { sha256 } from \"@noble/hashes/sha2.js\";\nimport { base64 } from \"@scure/base\";\n\nimport { NostrError } from \"../core/error.ts\";\nimport type { Event, EventTemplate } from \"../core/event.ts\";\nimport { validateSignedEvent } from \"../core/event.ts\";\nimport { Kind } from \"../core/kind.ts\";\nimport { firstTagValue } from \"../core/tag.ts\";\nimport type { Tag } from \"../core/tag.ts\";\nimport { bytesToHex, nowSeconds, utf8Decoder, utf8Encoder } from \"../core/util.ts\";\nimport { verifyEvent } from \"../core/verifier.ts\";\n\nconst AUTHORIZATION_SCHEME = \"Nostr \";\nconst DEFAULT_MAX_SKEW_SEC = 60;\n\nexport class Nip98Error extends NostrError {\n override name = \"Nip98Error\";\n}\n\n/** SHA-256 of the request body: raw bytes for string/Uint8Array, else JSON.stringify. */\nfunction hashPayload(payload: unknown): string {\n const bytes =\n typeof payload === \"string\"\n ? utf8Encoder.encode(payload)\n : payload instanceof Uint8Array\n ? payload\n : utf8Encoder.encode(JSON.stringify(payload));\n return bytesToHex(sha256(bytes));\n}\n\n/** RFC 4648 standard base64; accept missing `=` (NIP-98 spec example is unpadded). */\nfunction decodeStandardBase64(encoded: string): Uint8Array {\n const rem = encoded.length % 4;\n const padded = rem === 0 ? encoded : encoded + \"=\".repeat(4 - rem);\n return base64.decode(padded);\n}\n\nfunction stripAuthorizationScheme(token: string): string {\n const prefix = /^nostr\\s+/i.exec(token);\n return prefix ? token.slice(prefix[0].length) : token;\n}\n\n/**\n * Sign a kind 27235 auth event and encode it as a NIP-98 token. Uses standard base64\n * (`@scure/base`), not base64url.\n */\nexport async function getToken(\n url: string,\n method: string,\n sign: (template: EventTemplate) => Promise<Event> | Event,\n opts?: {\n includeAuthorizationScheme?: boolean | undefined;\n content?: string | undefined;\n payload?: unknown;\n now?: number | undefined;\n },\n): Promise<string> {\n const tags: Tag[] = [\n [\"u\", url],\n [\"method\", method],\n ];\n if (opts?.payload !== undefined) {\n tags.push([\"payload\", hashPayload(opts.payload)]);\n }\n\n const signed = await sign({\n kind: Kind.HttpAuth,\n created_at: opts?.now ?? nowSeconds(),\n tags,\n content: opts?.content ?? \"\",\n });\n\n // Canonical NIP-01 wire field order: the token is deterministic regardless\n // of the signer's object key order, and byte-identical to nk-nips'\n // `token(&Event)`. Extra signer-provided properties are dropped.\n const encoded = base64.encode(\n utf8Encoder.encode(\n JSON.stringify({\n id: signed.id,\n pubkey: signed.pubkey,\n created_at: signed.created_at,\n kind: signed.kind,\n tags: signed.tags,\n content: signed.content,\n sig: signed.sig,\n }),\n ),\n );\n return opts?.includeAuthorizationScheme === true ? AUTHORIZATION_SCHEME + encoded : encoded;\n}\n\n/** Decode a NIP-98 token (with or without the `Nostr ` scheme) into an event. */\nexport function unpackEventFromToken(token: string): Event {\n if (token === \"\") {\n throw new Nip98Error(\"missing token\");\n }\n\n const encoded = stripAuthorizationScheme(token);\n\n let json: string;\n try {\n json = utf8Decoder.decode(decodeStandardBase64(encoded));\n } catch (error) {\n throw new Nip98Error(\"invalid token encoding\", { cause: error });\n }\n if (!json.startsWith(\"{\")) {\n throw new Nip98Error(\"invalid token\");\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch (error) {\n throw new Nip98Error(\"invalid token JSON\", { cause: error });\n }\n if (!validateSignedEvent(parsed)) {\n throw new Nip98Error(\"token is not a signed event\");\n }\n return parsed;\n}\n\n/**\n * True when `event` is a valid NIP-98 auth event for `url` + `method`. Verifies signature, kind\n * 27235, timestamp window, `u`, `method`, and optional `payload` tag.\n */\nexport function validateAuthEvent(\n event: Event,\n url: string,\n method: string,\n opts?: { payload?: unknown; maxSkewSec?: number },\n): boolean {\n if (!verifyEvent(event)) {\n return false;\n }\n if (event.kind !== Kind.HttpAuth) {\n return false;\n }\n\n const maxSkewSec = opts?.maxSkewSec ?? DEFAULT_MAX_SKEW_SEC;\n const now = nowSeconds();\n if (Math.abs(now - event.created_at) > maxSkewSec) {\n return false;\n }\n\n const u = firstTagValue(event.tags, \"u\");\n if (u !== url) {\n return false;\n }\n\n const m = firstTagValue(event.tags, \"method\");\n if (m === undefined || m.toLowerCase() !== method.toLowerCase()) {\n return false;\n }\n\n if (opts?.payload !== undefined) {\n const payloadTag = firstTagValue(event.tags, \"payload\");\n if (payloadTag !== hashPayload(opts.payload)) {\n return false;\n }\n }\n\n return true;\n}\n"],"mappings":";;;;;;;;;;;;AAiBA,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAE7B,IAAa,aAAb,cAAgC,WAAW;CACzC,OAAgB;AAClB;;AAGA,SAAS,YAAY,SAA0B;CAC7C,MAAM,QACJ,OAAO,YAAY,WACf,YAAY,OAAO,OAAO,IAC1B,mBAAmB,aACjB,UACA,YAAY,OAAO,KAAK,UAAU,OAAO,CAAC;CAClD,OAAO,WAAW,OAAO,KAAK,CAAC;AACjC;;AAGA,SAAS,qBAAqB,SAA6B;CACzD,MAAM,MAAM,QAAQ,SAAS;CAC7B,MAAM,SAAS,QAAQ,IAAI,UAAU,UAAU,IAAI,OAAO,IAAI,GAAG;CACjE,OAAO,OAAO,OAAO,MAAM;AAC7B;AAEA,SAAS,yBAAyB,OAAuB;CACvD,MAAM,SAAS,aAAa,KAAK,KAAK;CACtC,OAAO,SAAS,MAAM,MAAM,OAAO,EAAE,CAAC,MAAM,IAAI;AAClD;;;;;AAMA,eAAsB,SACpB,KACA,QACA,MACA,MAMiB;CACjB,MAAM,OAAc,CAClB,CAAC,KAAK,GAAG,GACT,CAAC,UAAU,MAAM,CACnB;CACA,IAAI,MAAM,YAAY,KAAA,GACpB,KAAK,KAAK,CAAC,WAAW,YAAY,KAAK,OAAO,CAAC,CAAC;CAGlD,MAAM,SAAS,MAAM,KAAK;EACxB,MAAM,KAAK;EACX,YAAY,MAAM,OAAO,WAAW;EACpC;EACA,SAAS,MAAM,WAAW;CAC5B,CAAC;CAKD,MAAM,UAAU,OAAO,OACrB,YAAY,OACV,KAAK,UAAU;EACb,IAAI,OAAO;EACX,QAAQ,OAAO;EACf,YAAY,OAAO;EACnB,MAAM,OAAO;EACb,MAAM,OAAO;EACb,SAAS,OAAO;EAChB,KAAK,OAAO;CACd,CAAC,CACH,CACF;CACA,OAAO,MAAM,+BAA+B,OAAO,uBAAuB,UAAU;AACtF;;AAGA,SAAgB,qBAAqB,OAAsB;CACzD,IAAI,UAAU,IACZ,MAAM,IAAI,WAAW,eAAe;CAGtC,MAAM,UAAU,yBAAyB,KAAK;CAE9C,IAAI;CACJ,IAAI;EACF,OAAO,YAAY,OAAO,qBAAqB,OAAO,CAAC;CACzD,SAAS,OAAO;EACd,MAAM,IAAI,WAAW,0BAA0B,EAAE,OAAO,MAAM,CAAC;CACjE;CACA,IAAI,CAAC,KAAK,WAAW,GAAG,GACtB,MAAM,IAAI,WAAW,eAAe;CAGtC,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,IAAI;CAC1B,SAAS,OAAO;EACd,MAAM,IAAI,WAAW,sBAAsB,EAAE,OAAO,MAAM,CAAC;CAC7D;CACA,IAAI,CAAC,oBAAoB,MAAM,GAC7B,MAAM,IAAI,WAAW,6BAA6B;CAEpD,OAAO;AACT;;;;;AAMA,SAAgB,kBACd,OACA,KACA,QACA,MACS;CACT,IAAI,CAAC,YAAY,KAAK,GACpB,OAAO;CAET,IAAI,MAAM,SAAS,KAAK,UACtB,OAAO;CAGT,MAAM,aAAa,MAAM,cAAc;CACvC,MAAM,MAAM,WAAW;CACvB,IAAI,KAAK,IAAI,MAAM,MAAM,UAAU,IAAI,YACrC,OAAO;CAIT,IADU,cAAc,MAAM,MAAM,GAChC,MAAM,KACR,OAAO;CAGT,MAAM,IAAI,cAAc,MAAM,MAAM,QAAQ;CAC5C,IAAI,MAAM,KAAA,KAAa,EAAE,YAAY,MAAM,OAAO,YAAY,GAC5D,OAAO;CAGT,IAAI,MAAM,YAAY,KAAA,GACD;MAAA,cAAc,MAAM,MAAM,SAChC,MAAM,YAAY,KAAK,OAAO,GACzC,OAAO;CAAA;CAIX,OAAO;AACT"}
@@ -3,7 +3,7 @@ import { n as canonicalizeFilters } from "./filter-B16P33dz.mjs";
3
3
  import { t as assertSubscriptionId } from "./message-pYfVKxPn.mjs";
4
4
  import { i as throwIfAborted, n as onAbort, t as abortReason } from "./abort-DBq2STPj.mjs";
5
5
  import { t as invokeSafely } from "./report-CHld4uWj.mjs";
6
- import { c as RelayConnectionError, d as RelaySuspendedError, n as Relay, r as RelayStatus, u as RelayPublishError } from "./relay-DXtwUEdX.mjs";
6
+ import { c as RelayConnectionError, d as RelaySuspendedError, n as Relay, r as RelayStatus, u as RelayPublishError } from "./relay-BGPqiGKF.mjs";
7
7
  import { t as Emitter } from "./emitter-DkCY6NkU.mjs";
8
8
  //#region src/relay/url.ts
9
9
  /**
@@ -610,4 +610,4 @@ var Pool = class {
610
610
  //#endregion
611
611
  export { isInsecureRelayUrl as i, fanIn as n, fetchRouted as r, Pool as t };
612
612
 
613
- //# sourceMappingURL=pool-BMUsw2un.mjs.map
613
+ //# sourceMappingURL=pool-THuuOW4S.mjs.map