@qntx/nostr 0.10.1 → 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 (123) hide show
  1. package/dist/{builder-GBddxj8i.d.mts → builder-BrmLOVj_.d.mts} +2 -2
  2. package/dist/{builder-CVRmUeNm.mjs → builder-Cf2N12CG.mjs} +22 -5
  3. package/dist/builder-Cf2N12CG.mjs.map +1 -0
  4. package/dist/{client-CeyDLoyO.mjs → client-B3_vToDX.mjs} +14 -14
  5. package/dist/{client-CeyDLoyO.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 +4 -4
  9. package/dist/core.mjs +7 -7
  10. package/dist/{error-DlhGW4mJ.mjs → error-D3VNRVhL.mjs} +7 -3
  11. package/dist/error-D3VNRVhL.mjs.map +1 -0
  12. package/dist/{error-BILMe3Gr.mjs → error-DH4guike.mjs} +2 -2
  13. package/dist/{error-BILMe3Gr.mjs.map → error-DH4guike.mjs.map} +1 -1
  14. package/dist/{event-C2eLiPpV.mjs → event-B3ua2_oL.mjs} +24 -11
  15. package/dist/event-B3ua2_oL.mjs.map +1 -0
  16. package/dist/{filter-TrF2LEoy.mjs → filter-B16P33dz.mjs} +31 -10
  17. package/dist/filter-B16P33dz.mjs.map +1 -0
  18. package/dist/{filter-CfEgQBXt.d.mts → filter-PG7r79rt.d.mts} +6 -2
  19. package/dist/{gossip-5ezg0Quo.mjs → gossip-K_JIexQs.mjs} +3 -3
  20. package/dist/{gossip-5ezg0Quo.mjs.map → gossip-K_JIexQs.mjs.map} +1 -1
  21. package/dist/{gossip-BWb9HmfK.d.mts → gossip-u0iBlZ1U.d.mts} +3 -3
  22. package/dist/gossip.d.mts +1 -1
  23. package/dist/gossip.mjs +1 -1
  24. package/dist/{index-CLJEKFGd.d.mts → index-DD0Ywpay.d.mts} +3 -3
  25. package/dist/{index-uCYwXaaP.d.mts → index-DxQO1eWA.d.mts} +10 -10
  26. package/dist/{index-C0mFpelb.d.mts → index-SwMknboJ.d.mts} +54 -4
  27. package/dist/{index-CoX7SnO7.d.mts → index-rKcmI6ex.d.mts} +3 -3
  28. package/dist/{index-B1fLejg4.d.mts → index-yq9xkONo.d.mts} +8 -8
  29. package/dist/index.d.mts +15 -15
  30. package/dist/index.mjs +19 -19
  31. package/dist/{key-DY_HEIpY.mjs → key-B218_SoL.mjs} +34 -9
  32. package/dist/key-B218_SoL.mjs.map +1 -0
  33. package/dist/{key-CyKN1RTi.d.mts → key-CwMVo14x.d.mts} +16 -4
  34. package/dist/{loaders-DifimjJ7.mjs → loaders-Xdbx-K61.mjs} +6 -6
  35. package/dist/{loaders-DifimjJ7.mjs.map → loaders-Xdbx-K61.mjs.map} +1 -1
  36. package/dist/loaders.d.mts +2 -2
  37. package/dist/loaders.mjs +1 -1
  38. package/dist/{memory-DENGQ_f6.mjs → memory-VyPnOBU3.mjs} +2 -2
  39. package/dist/{memory-DENGQ_f6.mjs.map → memory-VyPnOBU3.mjs.map} +1 -1
  40. package/dist/{memory-index-D_lvlYAy.mjs → memory-index-Bhwe0ZdG.mjs} +4 -4
  41. package/dist/{memory-index-D_lvlYAy.mjs.map → memory-index-Bhwe0ZdG.mjs.map} +1 -1
  42. package/dist/{message-Rd5Z_17h.mjs → message-pYfVKxPn.mjs} +74 -29
  43. package/dist/message-pYfVKxPn.mjs.map +1 -0
  44. package/dist/{nip17-B7MI82Iq.d.mts → nip17-BM4hJYBq.d.mts} +4 -4
  45. package/dist/{nip51-O_vhOiK2.d.mts → nip51-D_bu8Hph.d.mts} +2 -2
  46. package/dist/{nip59-CdDoJptA.d.mts → nip59-CGWfoItR.d.mts} +14 -6
  47. package/dist/{nip65-BL79HI8N.d.mts → nip65-DaWMg943.d.mts} +2 -2
  48. package/dist/nips/blossom.d.mts +1 -1
  49. package/dist/nips/blossom.mjs +2 -2
  50. package/dist/nips/nip04.d.mts +10 -0
  51. package/dist/nips/nip04.mjs +38 -8
  52. package/dist/nips/nip04.mjs.map +1 -1
  53. package/dist/nips/nip05.mjs +1 -1
  54. package/dist/nips/nip10.d.mts +1 -1
  55. package/dist/nips/nip10.mjs +3 -3
  56. package/dist/nips/nip11.mjs +1 -1
  57. package/dist/nips/nip13.mjs +1 -1
  58. package/dist/nips/nip17.d.mts +1 -1
  59. package/dist/nips/nip17.mjs +4 -4
  60. package/dist/nips/nip17.mjs.map +1 -1
  61. package/dist/nips/nip19.mjs +11 -4
  62. package/dist/nips/nip19.mjs.map +1 -1
  63. package/dist/nips/nip21.mjs +1 -1
  64. package/dist/nips/nip42.mjs +1 -1
  65. package/dist/nips/nip44.d.mts +5 -0
  66. package/dist/nips/nip44.mjs +10 -3
  67. package/dist/nips/nip44.mjs.map +1 -1
  68. package/dist/nips/nip46.mjs +1 -1
  69. package/dist/nips/nip49.d.mts +7 -1
  70. package/dist/nips/nip49.mjs +17 -3
  71. package/dist/nips/nip49.mjs.map +1 -1
  72. package/dist/nips/nip51.d.mts +1 -1
  73. package/dist/nips/nip51.mjs +3 -3
  74. package/dist/nips/nip57.mjs +3 -3
  75. package/dist/nips/nip59.d.mts +1 -1
  76. package/dist/nips/nip59.mjs +50 -22
  77. package/dist/nips/nip59.mjs.map +1 -1
  78. package/dist/nips/nip65.d.mts +1 -1
  79. package/dist/nips/nip65.mjs +2 -2
  80. package/dist/nips/nip77.mjs +1 -1
  81. package/dist/nips/nip98.mjs +12 -4
  82. package/dist/nips/nip98.mjs.map +1 -1
  83. package/dist/{pool-Bfe3Tu40.d.mts → pool-DUuYJ0x4.d.mts} +3 -3
  84. package/dist/{pool-jok2-iBJ.mjs → pool-THuuOW4S.mjs} +5 -5
  85. package/dist/{pool-jok2-iBJ.mjs.map → pool-THuuOW4S.mjs.map} +1 -1
  86. package/dist/{put-JsiCKMyZ.mjs → put-DRmV1wHD.mjs} +3 -3
  87. package/dist/{put-JsiCKMyZ.mjs.map → put-DRmV1wHD.mjs.map} +1 -1
  88. package/dist/{reactive-6Wn9qf_d.mjs → reactive--pDpLt2H.mjs} +5 -5
  89. package/dist/{reactive-6Wn9qf_d.mjs.map → reactive--pDpLt2H.mjs.map} +1 -1
  90. package/dist/{reactive-X4p6nABy.d.mts → reactive-jyKrA9ej.d.mts} +3 -3
  91. package/dist/{relay-__QtO33h.mjs → relay-BGPqiGKF.mjs} +6 -6
  92. package/dist/{relay-__QtO33h.mjs.map → relay-BGPqiGKF.mjs.map} +1 -1
  93. package/dist/relay.d.mts +3 -3
  94. package/dist/relay.mjs +2 -2
  95. package/dist/{signer-D7PtVvQn.mjs → signer-CNTIeYH4.mjs} +170 -11
  96. package/dist/signer-CNTIeYH4.mjs.map +1 -0
  97. package/dist/signer.d.mts +2 -2
  98. package/dist/signer.mjs +3 -3
  99. package/dist/{storage-oihjE0_x.mjs → storage-B5Wdt_Mn.mjs} +12 -29
  100. package/dist/storage-B5Wdt_Mn.mjs.map +1 -0
  101. package/dist/storage.d.mts +2 -2
  102. package/dist/storage.mjs +4 -4
  103. package/dist/store.d.mts +1 -1
  104. package/dist/store.mjs +1 -1
  105. package/dist/testing.d.mts +1 -1
  106. package/dist/testing.mjs +18 -7
  107. package/dist/testing.mjs.map +1 -1
  108. package/dist/{types-Ccox7-5v.d.mts → types-CqhnT-Mx.d.mts} +2 -2
  109. package/dist/{util-DMF9AIO0.mjs → util-DDpPkiED.mjs} +14 -2
  110. package/dist/util-DDpPkiED.mjs.map +1 -0
  111. package/dist/{verifier-uC_jd-Nw.mjs → verifier-CjH1vnP2.mjs} +3 -3
  112. package/dist/{verifier-uC_jd-Nw.mjs.map → verifier-CjH1vnP2.mjs.map} +1 -1
  113. package/dist/{verifier-9c-bINrv.d.mts → verifier-Cl24c9ph.d.mts} +3 -3
  114. package/package.json +1 -1
  115. package/dist/builder-CVRmUeNm.mjs.map +0 -1
  116. package/dist/error-DlhGW4mJ.mjs.map +0 -1
  117. package/dist/event-C2eLiPpV.mjs.map +0 -1
  118. package/dist/filter-TrF2LEoy.mjs.map +0 -1
  119. package/dist/key-DY_HEIpY.mjs.map +0 -1
  120. package/dist/message-Rd5Z_17h.mjs.map +0 -1
  121. package/dist/signer-D7PtVvQn.mjs.map +0 -1
  122. package/dist/storage-oihjE0_x.mjs.map +0 -1
  123. package/dist/util-DMF9AIO0.mjs.map +0 -1
@@ -1,10 +1,11 @@
1
- import { c as isRecord, d as nowSeconds, n as assertHex32, v as NostrError } from "../util-DMF9AIO0.mjs";
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
- import { g as Tag, h as validateSignedEvent, m as validateEvent, n as getEventHash } from "../event-C2eLiPpV.mjs";
4
- import { r as finalizeEvent, t as Keys } from "../key-DY_HEIpY.mjs";
5
- import { n as verifyEvent } from "../verifier-uC_jd-Nw.mjs";
6
- import { encryptToPubkey } from "./nip44.mjs";
3
+ import { g as Tag, h as validateSignedEvent, m as validateEvent, n as getEventHash } from "../event-B3ua2_oL.mjs";
4
+ import { n as SecretKey, o as signEvent, t as Keys } from "../key-B218_SoL.mjs";
5
+ import { n as verifyEvent } from "../verifier-CjH1vnP2.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
- import { h as EventValidationError, u as normalizeURL } from "../util-DMF9AIO0.mjs";
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-CVRmUeNm.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) {
@@ -1,4 +1,4 @@
1
- import { a as hexToBytes, i as bytesToHex, n as assertHex32, v as NostrError } from "../util-DMF9AIO0.mjs";
1
+ import { i as bytesToHex, n as assertHex32, o as hexToBytes, y as NostrError } from "../util-DDpPkiED.mjs";
2
2
  import { sha256 } from "@noble/hashes/sha2.js";
3
3
  //#region src/nips/nip77.ts
4
4
  /**
@@ -1,7 +1,7 @@
1
- import { d as nowSeconds, f as utf8Decoder, i as bytesToHex, p as utf8Encoder, v as NostrError } from "../util-DMF9AIO0.mjs";
1
+ import { f as nowSeconds, i as bytesToHex, m as utf8Encoder, p as utf8Decoder, y as NostrError } from "../util-DDpPkiED.mjs";
2
2
  import { t as Kind } from "../kind-D8fENiiL.mjs";
3
- import { h as validateSignedEvent, v as firstTagValue } from "../event-C2eLiPpV.mjs";
4
- import { n as verifyEvent } from "../verifier-uC_jd-Nw.mjs";
3
+ import { h as validateSignedEvent, v as firstTagValue } from "../event-B3ua2_oL.mjs";
4
+ import { n as verifyEvent } from "../verifier-CjH1vnP2.mjs";
5
5
  import { sha256 } from "@noble/hashes/sha2.js";
6
6
  import { base64 } from "@scure/base";
7
7
  //#region src/nips/nip98.ts
@@ -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"}
@@ -1,6 +1,6 @@
1
1
  import { n as EventTemplate, t as Event } from "./event-j3BwI5dF.mjs";
2
- import { t as Filter } from "./filter-CfEgQBXt.mjs";
3
- import { o as CountResult, t as EventVerifier } from "./verifier-9c-bINrv.mjs";
2
+ import { t as Filter } from "./filter-PG7r79rt.mjs";
3
+ import { o as CountResult, t as EventVerifier } from "./verifier-Cl24c9ph.mjs";
4
4
  import { a as NegentropyStorageVector } from "./nip77-wHA_nEV5.mjs";
5
5
  import { t as WebSocketConstructor } from "./websocket-CmeqvAkp.mjs";
6
6
  //#region src/relay/subscription.d.ts
@@ -392,4 +392,4 @@ declare class Pool {
392
392
  }
393
393
  //#endregion
394
394
  export { Closer as _, PoolFetchResult as a, SubscriptionHandlers as b, PoolSubscribeOptions as c, RelayOptions as d, RelayStatus as f, RelayFetchResult as g, RelayFetchEnd as h, PoolEventMap as i, Relay as l, PublishResult as m, Pool as n, PoolOptions as o, RelayStatusName as p, PoolCountResult as r, PoolPublishResult as s, InvalidEventPolicy as t, RelayEventMap as u, RelaySubscription as v, subscriptionToAsyncIterable as x, SubscribeOptions as y };
395
- //# sourceMappingURL=pool-Bfe3Tu40.d.mts.map
395
+ //# sourceMappingURL=pool-DUuYJ0x4.d.mts.map
@@ -1,9 +1,9 @@
1
- import { S as toError, _ as MessageError, u as normalizeURL } from "./util-DMF9AIO0.mjs";
2
- import { n as canonicalizeFilters } from "./filter-TrF2LEoy.mjs";
3
- import { t as assertSubscriptionId } from "./message-Rd5Z_17h.mjs";
1
+ import { C as toError, d as normalizeURL, v as MessageError } from "./util-DDpPkiED.mjs";
2
+ import { n as canonicalizeFilters } from "./filter-B16P33dz.mjs";
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-__QtO33h.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-jok2-iBJ.mjs.map
613
+ //# sourceMappingURL=pool-THuuOW4S.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"pool-jok2-iBJ.mjs","names":[],"sources":["../src/relay/url.ts","../src/relay/fan-in.ts","../src/relay/pool.ts"],"sourcesContent":["import { normalizeURL } from \"../core/util.ts\";\n\n/**\n * {@link normalizeURL}, or the raw string when it is not a valid relay URL (ensureRelay then fails\n * it like a dead relay).\n */\nexport function relayUrlKey(url: string): string {\n try {\n return normalizeURL(url);\n } catch {\n return url;\n }\n}\n\n/** Unique relay keys in first-seen order. */\nexport function uniqueRelayUrls(urls: Iterable<string>): string[] {\n const seen = new Set<string>();\n const unique: string[] = [];\n for (const url of urls) {\n const key = relayUrlKey(url);\n if (seen.has(key)) {\n continue;\n }\n seen.add(key);\n unique.push(key);\n }\n return unique;\n}\n\n/**\n * True for `ws:` / `http:` URLs that are not `.onion`. Local-network `ws://` is still insecure;\n * allow those via a trusted set.\n */\nexport function isInsecureRelayUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n if (parsed.hostname.endsWith(\".onion\")) {\n return false;\n }\n return parsed.protocol === \"ws:\" || parsed.protocol === \"http:\";\n } catch {\n return false;\n }\n}\n","import { abortReason, onAbort, throwIfAborted } from \"../core/abort.ts\";\nimport type { Event } from \"../core/event.ts\";\nimport type { Filter } from \"../core/filter.ts\";\nimport { invokeSafely } from \"../core/report.ts\";\nimport type { Pool } from \"./pool.ts\";\nimport type { Relay } from \"./relay.ts\";\nimport type { Closer } from \"./subscription.ts\";\nimport { uniqueRelayUrls } from \"./url.ts\";\n\nexport type RoutedJob = {\n urls: ReadonlyArray<string>;\n filters: ReadonlyArray<Filter>;\n id?: string | undefined;\n};\n\nexport type FanInOptions = {\n /** First receipt only (deduped across jobs). `relayUrl` is the normalized relay URL. */\n onevent?: ((event: Event, relayUrl: string) => void) | undefined;\n oneose?: (() => void) | undefined;\n onclose?: ((reason: string) => void) | undefined;\n signal?: AbortSignal | undefined;\n /** Armed at fan-in only. Never forwarded to relay.subscribe. */\n eoseTimeoutMs?: number | undefined;\n alreadyHaveEvent?: ((id: string) => boolean) | undefined;\n /** Every receipt from every relay, including skipped duplicates. */\n receivedEvent?: ((id: string, relayUrl: string) => void) | undefined;\n closeOnEose?: boolean | undefined;\n /** Optional. ensureRelay uses Pool.#opts.connectTimeoutMs when omitted. */\n connectTimeoutMs?: number | undefined;\n};\n\n/**\n * Aggregate EOSE/close across routed jobs. One `seen` set. `pending` counts URL list entries\n * (duplicates included). `pendingEose` counts unique `jobIndex:url` keys.\n */\nexport function fanIn(pool: Pool, jobs: ReadonlyArray<RoutedJob>, opts: FanInOptions = {}): Closer {\n const seen = new Set<string>();\n const closers: Closer[] = [];\n let closed = false;\n let eoseFired = false;\n let sawEose = false;\n let eoseTimer: ReturnType<typeof setTimeout> | undefined;\n const eoseDone = new Set<string>();\n const eoseAttempted = new Set<string>();\n let pendingEose = 0;\n let pending = 0;\n /** Close reason of the last attached subscription that ended; drives the aggregate reason. */\n let lastReason: string | undefined;\n\n const fireEose = () => {\n if (closed || eoseFired) {\n return;\n }\n eoseFired = true;\n if (eoseTimer !== undefined) {\n clearTimeout(eoseTimer);\n eoseTimer = undefined;\n }\n invokeSafely(() => opts.oneose?.());\n };\n\n /**\n * Remove one relay from the pending-EOSE set. `eosed` credits a real EOSE; a CLOSED or a connect\n * failure only drops the wait — the aggregate `oneose` fires once every remaining relay ended AND\n * at least one actually EOSE'd.\n */\n const settleEose = (jobIndex: number, url: string, eosed: boolean) => {\n const key = `${jobIndex}:${url}`;\n if (eoseDone.has(key)) {\n return;\n }\n eoseDone.add(key);\n if (eosed) {\n sawEose = true;\n }\n pendingEose -= 1;\n if (pendingEose === 0 && sawEose) {\n fireEose();\n }\n };\n\n const settleClose = () => {\n closed = true;\n disposeAbort();\n if (eoseTimer !== undefined) {\n clearTimeout(eoseTimer);\n eoseTimer = undefined;\n }\n };\n\n const closeAll = (reason?: string) => {\n if (closed) {\n return;\n }\n settleClose();\n for (const c of closers) {\n c.close(reason);\n }\n invokeSafely(() => opts.onclose?.(reason ?? \"closed by client\"));\n };\n\n if (opts.signal?.aborted === true) {\n closed = true;\n invokeSafely(() => opts.onclose?.(\"aborted\"));\n return { close: closeAll };\n }\n\n const disposeAbort = onAbort(opts.signal, () => closeAll(\"aborted\"));\n\n /**\n * Single completion rule for both endings: `pending` reaches zero when every URL entry either\n * failed to attach or its subscription ended — at that point no attached closer can still be\n * live. The reason is the last subscription's close reason, or \"all relays failed\" when no relay\n * ever attached.\n */\n const finish = (): void => {\n if (pending <= 0 && !closed) {\n settleClose();\n invokeSafely(() => opts.onclose?.(lastReason ?? \"all relays failed\"));\n }\n };\n\n const failUrl = (jobIndex: number, key: string): void => {\n settleEose(jobIndex, key, false);\n pending -= 1;\n finish();\n };\n\n const attach = (relay: Relay, job: RoutedJob, jobIndex: number): void => {\n if (closed) {\n return;\n }\n const received = opts.receivedEvent;\n const sub = relay.subscribe([...job.filters], {\n id: jobs.length === 1 ? job.id : undefined,\n closeOnEose: opts.closeOnEose,\n alreadyHaveEvent: (id) => {\n let have = seen.has(id);\n invokeSafely(() => {\n have = have || Boolean(opts.alreadyHaveEvent?.(id));\n });\n return have;\n },\n receivedEvent:\n received === undefined ? undefined : (id) => invokeSafely(() => received(id, relay.url)),\n onevent: (event) => {\n seen.add(event.id);\n invokeSafely(() => opts.onevent?.(event, relay.url));\n },\n oneose: () => settleEose(jobIndex, relay.url, true),\n onclose: (reason) => {\n settleEose(jobIndex, relay.url, false);\n lastReason = reason;\n pending -= 1;\n finish();\n },\n });\n closers.push(sub);\n };\n\n for (const [jobIndex, job] of jobs.entries()) {\n // Equivalent spellings of one relay URL attach exactly once per job.\n for (const key of uniqueRelayUrls(job.urls)) {\n pending += 1;\n const eoseKey = `${jobIndex}:${key}`;\n if (!eoseAttempted.has(eoseKey)) {\n eoseAttempted.add(eoseKey);\n pendingEose += 1;\n }\n\n const tryAttach = (relay: Relay): void => {\n try {\n attach(relay, job, jobIndex);\n } catch {\n // Any attach failure retires this URL: RelayClosedError is the expected\n // dropped-between-connect-and-subscribe race; anything else must not escape the\n // `void` wrapper as an unhandled rejection, so it fails the URL as well.\n failUrl(jobIndex, key);\n }\n };\n\n void (async (): Promise<void> => {\n let relay: Relay;\n try {\n relay = await pool.ensureRelay(key, {\n signal: opts.signal,\n timeoutMs: opts.connectTimeoutMs,\n });\n } catch {\n // ensureRelay rejected only — not tryAttach throws (those must not look like connect failure).\n const existing = pool.getRelay(key);\n if (existing !== undefined) {\n tryAttach(existing);\n return;\n }\n failUrl(jobIndex, key);\n return;\n }\n tryAttach(relay);\n })();\n }\n }\n\n if (opts.eoseTimeoutMs !== undefined && pending > 0) {\n eoseTimer = setTimeout(() => {\n eoseTimer = undefined;\n fireEose();\n }, opts.eoseTimeoutMs);\n }\n\n if (pending === 0) {\n queueMicrotask(() => {\n if (closed) {\n return;\n }\n settleClose();\n invokeSafely(() => opts.onclose?.(\"no relays\"));\n });\n }\n\n return { close: closeAll };\n}\n\nexport async function fetchRouted(\n pool: Pool,\n jobs: ReadonlyArray<RoutedJob>,\n opts: {\n timeoutMs?: number | undefined;\n signal?: AbortSignal | undefined;\n connectTimeoutMs?: number | undefined;\n /** Every event of every relay batch, including cross-relay duplicates. */\n onevent?: ((event: Event, relayUrl: string) => void) | undefined;\n } = {},\n): Promise<Event[]> {\n throwIfAborted(opts.signal);\n const byId = new Map<string, Event>();\n await Promise.all(\n jobs.flatMap((job) => {\n return uniqueRelayUrls(job.urls).map(async (url) => {\n let batch: ReadonlyArray<Event>;\n let relayUrl: string;\n try {\n const relay = await pool.ensureRelay(url, {\n signal: opts.signal,\n timeoutMs: opts.connectTimeoutMs,\n });\n relayUrl = relay.url;\n const result = await relay.fetch([...job.filters], {\n timeoutMs: opts.timeoutMs,\n signal: opts.signal,\n });\n batch = result.events;\n } catch {\n // An abort rejects the whole call; per-relay failures are skipped.\n if (opts.signal?.aborted === true) {\n throw abortReason(opts.signal);\n }\n return;\n }\n // The whole batch lands before callbacks so a throwing onevent cannot\n // drop events; listener errors are reported, never propagated.\n for (const event of batch) {\n if (!byId.has(event.id)) {\n byId.set(event.id, event);\n }\n }\n for (const event of batch) {\n invokeSafely(() => opts.onevent?.(event, relayUrl));\n }\n });\n }),\n );\n return [...byId.values()];\n}\n","import { abortReason, throwIfAborted } from \"../core/abort.ts\";\nimport { Emitter } from \"../core/emitter.ts\";\nimport { MessageError, toError } from \"../core/error.ts\";\nimport type { Event, EventTemplate } from \"../core/event.ts\";\nimport { canonicalizeFilters } from \"../core/filter.ts\";\nimport type { Filter } from \"../core/filter.ts\";\nimport { assertSubscriptionId } from \"../core/message.ts\";\nimport type { CountResult } from \"../core/message.ts\";\nimport { normalizeURL } from \"../core/util.ts\";\nimport { RelayConnectionError, RelayPublishError, RelaySuspendedError } from \"./error.ts\";\nimport { fanIn, fetchRouted } from \"./fan-in.ts\";\nimport { Relay, RelayStatus } from \"./relay.ts\";\nimport type { RelayFetchEnd, RelayOptions, SubscribeOptions } from \"./relay.ts\";\nimport type { Closer } from \"./subscription.ts\";\nimport { isInsecureRelayUrl, uniqueRelayUrls } from \"./url.ts\";\n\n/** Per-relay tolerance for events that fail id/signature verification. */\nexport type InvalidEventPolicy = {\n /** Invalid events tolerated within `windowMs`; exceeding it suspends the relay. */\n limit: number;\n windowMs: number;\n /** How long a suspended relay stays closed before it may reconnect. */\n cooldownMs: number;\n};\n\n/** Pool-wide options applied to every managed relay. */\nexport type PoolOptions = Omit<RelayOptions, \"authSigner\"> & {\n /**\n * When set, automatically answer NIP-42 AUTH challenges for relays that send them. Return\n * undefined to skip a given relay URL.\n */\n automaticallyAuth?:\n | ((relayURL: string) => ((event: EventTemplate) => Promise<Event>) | undefined)\n | undefined;\n /** When false (default), ensureRelay rejects isInsecureRelayUrl unless trusted. */\n allowInsecure?: boolean | undefined;\n trustedInsecureUrls?: ReadonlyArray<string> | undefined;\n /** Close unused relays. Unset/0 = disabled. */\n idleTimeoutMs?: number | undefined;\n idleCleanupIntervalMs?: number | undefined;\n /**\n * Soft cap on connected non-pinned relays. When `ensureRelay` would create a new non-pinned relay\n * at the cap, the least-recently-used idle one is closed first (idle = no subscriptions and no\n * in-flight requests). If none is idle the connect proceeds anyway — the cap never fails a\n * request.\n */\n maxRelays?: number | undefined;\n /** Normalized URLs never closed by idle cleanup or `maxRelays` eviction. */\n pinnedUrls?: ReadonlyArray<string> | undefined;\n /**\n * When set, a relay whose `limit`-th-plus-one EVENT fails id/signature verification inside a\n * `windowMs` sliding window is disconnected and suspended for `cooldownMs`. During suspension\n * `ensureRelay` rejects with {@link RelaySuspendedError}; once it lifts, live subscriptions resume\n * through the normal reconnect path. Unset = events are dropped without counting, as before.\n */\n invalidEventPolicy?: InvalidEventPolicy | undefined;\n};\n\n/** Typed event payloads for {@link Pool.on}. */\nexport type PoolEventMap = {\n /** Normalized URLs closed by an idle-cleanup pass. */\n idle: ReadonlyArray<string>;\n /**\n * A relay suspended for exceeding `invalidEventPolicy`: `url` is the normalized relay URL and\n * `until` the epoch-ms time the suspension lifts.\n */\n suspend: { readonly url: string; readonly until: number };\n};\n\n/** Per-relay publish outcome: the relay's OK verdict or a transport-level failure. */\nexport type PoolPublishResult =\n | { readonly url: string; readonly status: \"ok\"; readonly message: string }\n | { readonly url: string; readonly status: \"rejected\"; readonly message: string }\n | { readonly url: string; readonly status: \"failed\"; readonly error: Error };\n\n/** Multi-relay subscribe options: callbacks also receive the normalized relay URL. */\nexport type PoolSubscribeOptions = Omit<SubscribeOptions, \"onevent\" | \"receivedEvent\"> & {\n onevent?: ((event: Event, relayUrl: string) => void) | undefined;\n receivedEvent?: ((id: string, relayUrl: string) => void) | undefined;\n};\n\n/** Per-relay one-shot fetch outcome: collected events plus how its REQ ended. */\nexport type PoolFetchResult = {\n readonly url: string;\n readonly events: ReadonlyArray<Event>;\n readonly end: RelayFetchEnd | { readonly type: \"failed\"; readonly error: Error };\n};\n\n/** Per-relay NIP-45 COUNT result (or a transport-level failure). */\nexport type PoolCountResult =\n | ({ readonly url: string; readonly status: \"ok\" } & CountResult)\n | { readonly url: string; readonly status: \"failed\"; readonly error: Error };\n\nconst countOk = (url: string, payload: CountResult): PoolCountResult => ({\n url,\n status: \"ok\",\n ...payload,\n});\n\nfunction isIdle(relay: Relay): boolean {\n return (\n relay.status !== RelayStatus.Connecting &&\n relay.subscriptionCount === 0 &&\n relay.inFlightCount === 0\n );\n}\n\n/** Multi-relay coordinator: connection reuse, cross-relay event dedup, fan-out publish. */\nexport class Pool {\n readonly #relays = new Map<string, Relay>();\n readonly #relayOptions: Omit<RelayOptions, \"authSigner\">;\n readonly #automaticallyAuth: PoolOptions[\"automaticallyAuth\"];\n readonly #maxRelays: number | undefined;\n readonly #invalidEventPolicy: InvalidEventPolicy | undefined;\n readonly #events = new Emitter<PoolEventMap>();\n readonly #lastActivity = new Map<string, number>();\n readonly #idleTimeoutMs: number;\n #idleTimer: ReturnType<typeof setInterval> | undefined;\n #allowInsecure: boolean;\n #trustedInsecure: Set<string>;\n #pinned: Set<string>;\n /** Per-relay timestamps of verification failures inside the sliding window. */\n readonly #invalidEvents = new Map<string, number[]>();\n /** Normalized URL → epoch ms when its suspension lifts. */\n readonly #suspendedUntil = new Map<string, number>();\n readonly #resumeTimers = new Map<string, ReturnType<typeof setTimeout>>();\n\n constructor(opts: PoolOptions = {}) {\n const {\n automaticallyAuth,\n allowInsecure,\n trustedInsecureUrls,\n idleTimeoutMs,\n idleCleanupIntervalMs,\n maxRelays,\n pinnedUrls,\n invalidEventPolicy,\n ...relayOptions\n } = opts;\n this.#relayOptions = relayOptions;\n this.#automaticallyAuth = automaticallyAuth;\n this.#maxRelays = maxRelays;\n this.#invalidEventPolicy = invalidEventPolicy;\n this.#allowInsecure = allowInsecure ?? false;\n this.#trustedInsecure = new Set((trustedInsecureUrls ?? []).map(normalizeURL));\n this.#pinned = new Set((pinnedUrls ?? []).map(normalizeURL));\n this.#idleTimeoutMs = idleTimeoutMs ?? 0;\n if (this.#idleTimeoutMs > 0) {\n this.#idleTimer = setInterval(() => this.cleanIdleRelays(), idleCleanupIntervalMs ?? 30_000);\n }\n }\n\n /**\n * Listen for a pool event. Multiple listeners per type are allowed and fire in registration\n * order; a throwing listener is reported and does not break the dispatch. Returns an unsubscribe\n * function.\n */\n on<K extends keyof PoolEventMap>(\n type: K,\n listener: (payload: PoolEventMap[K]) => void,\n ): () => void {\n return this.#events.on(type, listener);\n }\n\n setAllowInsecure(allow: boolean): void {\n this.#allowInsecure = allow;\n }\n\n setTrustedInsecureUrls(urls: ReadonlyArray<string>): void {\n this.#trustedInsecure = new Set(urls.map(normalizeURL));\n }\n\n setPinnedUrls(urls: ReadonlyArray<string>): void {\n this.#pinned = new Set(urls.map(normalizeURL));\n }\n\n /**\n * Drop cached AUTH rejections and re-fire pending challenges on every pooled relay, so a later\n * `setSigner` takes effect without reconnect.\n */\n resetAuth(): void {\n for (const relay of this.#relays.values()) {\n relay.resetAuth();\n }\n }\n\n cleanIdleRelays(): void {\n if (this.#idleTimeoutMs <= 0) {\n return;\n }\n const now = Date.now();\n const idle: string[] = [];\n for (const [url, relay] of this.#relays) {\n if (this.#pinned.has(url) || !isIdle(relay)) {\n continue;\n }\n const last = this.#lastActivity.get(url) ?? 0;\n if (!relay.connected || now - last >= this.#idleTimeoutMs) {\n idle.push(url);\n }\n }\n for (const url of this.#lastActivity.keys()) {\n if (!this.#relays.has(url)) {\n this.#lastActivity.delete(url);\n }\n }\n if (idle.length === 0) {\n return;\n }\n this.closeRelays(idle);\n this.#events.emit(\"idle\", idle);\n }\n\n #touch(url: string): void {\n this.#lastActivity.set(url, Date.now());\n }\n\n /**\n * Count one verification failure for `norm`; on the `limit + 1`-th inside `windowMs` suspend the\n * relay: drop its connection (subscriptions kept), record `until`, and emit `suspend`.\n */\n #noteInvalidEvent(norm: string): void {\n const policy = this.#invalidEventPolicy;\n if (policy === undefined) {\n return;\n }\n const now = Date.now();\n const window = (this.#invalidEvents.get(norm) ?? []).filter((at) => now - at < policy.windowMs);\n window.push(now);\n if (window.length <= policy.limit) {\n this.#invalidEvents.set(norm, window);\n return;\n }\n this.#invalidEvents.delete(norm);\n if (this.#suspendedUntil.has(norm)) {\n return;\n }\n const until = now + policy.cooldownMs;\n this.#suspendedUntil.set(norm, until);\n this.#relays.get(norm)?.disconnect();\n const resume = setTimeout(() => {\n this.#resumeTimers.delete(norm);\n this.#suspendedUntil.delete(norm);\n const relay = this.#relays.get(norm);\n if (relay === undefined || relay.connected || relay.subscriptionCount === 0) {\n return;\n }\n void (async (): Promise<void> => {\n try {\n await relay.connect();\n } catch {\n // With enableReconnect the relay's own backoff retries; otherwise the\n // next ensureRelay attempt reconnects on demand.\n }\n })();\n }, policy.cooldownMs);\n this.#resumeTimers.set(norm, resume);\n this.#events.emit(\"suspend\", { url: norm, until });\n }\n\n #rejectInsecure(url: string, norm: string): void {\n if (this.#allowInsecure) {\n return;\n }\n if (!isInsecureRelayUrl(url)) {\n return;\n }\n if (this.#trustedInsecure.has(norm)) {\n return;\n }\n throw new RelayConnectionError(\"insecure relay connection blocked\", norm);\n }\n\n #stopIdleCleanup(): void {\n if (this.#idleTimer === undefined) {\n return;\n }\n clearInterval(this.#idleTimer);\n this.#idleTimer = undefined;\n }\n\n /**\n * Soft cap: closing the least-recently-used idle non-pinned relay when a new non-pinned relay\n * would push the count past `maxRelays`. Busy relays are never evicted; with none idle the cap is\n * exceeded rather than failing.\n */\n #enforceMaxRelays(incoming: string): void {\n const cap = this.#maxRelays;\n if (cap === undefined || this.#pinned.has(incoming)) {\n return;\n }\n let count = 0;\n let oldestUrl: string | undefined;\n let oldestAt = Number.POSITIVE_INFINITY;\n for (const [url, relay] of this.#relays) {\n if (this.#pinned.has(url)) {\n continue;\n }\n count += 1;\n if (!isIdle(relay)) {\n continue;\n }\n const at = this.#lastActivity.get(url) ?? 0;\n if (at < oldestAt) {\n oldestAt = at;\n oldestUrl = url;\n }\n }\n if (count >= cap && oldestUrl !== undefined) {\n this.closeRelays([oldestUrl]);\n }\n }\n\n async ensureRelay(\n url: string,\n opts?: { signal?: AbortSignal | undefined; timeoutMs?: number | undefined },\n ): Promise<Relay> {\n const norm = normalizeURL(url);\n this.#rejectInsecure(url, norm);\n const suspendedUntil = this.#suspendedUntil.get(norm);\n if (suspendedUntil !== undefined) {\n if (Date.now() < suspendedUntil) {\n throw new RelaySuspendedError(norm, suspendedUntil);\n }\n this.#suspendedUntil.delete(norm);\n }\n let relay = this.#relays.get(norm);\n if (!relay) {\n this.#enforceMaxRelays(norm);\n const signFn = this.#automaticallyAuth?.(norm) ?? undefined;\n const created = new Relay(norm, { ...this.#relayOptions, authSigner: signFn });\n // Only drop from the pool on terminal close (reconnect keeps the entry). Guard on\n // identity: a stale relay's delayed close must not delete a newer relay's entry.\n created.on(\"close\", () => {\n if (this.#relays.get(norm) === created) {\n this.#relays.delete(norm);\n this.#lastActivity.delete(norm);\n }\n });\n created.on(\"invalidevent\", () => {\n this.#noteInvalidEvent(norm);\n });\n if (signFn) {\n created.on(\"auth\", () => {\n void (async (): Promise<void> => {\n try {\n await created.auth(signFn);\n } catch {\n // auth failure surfaces on subsequent CLOSED/OK; avoid unhandled rejection\n }\n })();\n });\n }\n this.#relays.set(norm, created);\n relay = created;\n }\n this.#touch(norm);\n if (!relay.connected) {\n try {\n await relay.connect({\n signal: opts?.signal,\n timeoutMs: opts?.timeoutMs ?? this.#relayOptions.connectTimeoutMs,\n });\n } catch (error) {\n // Keep the entry when reconnect is enabled so open subscriptions can recover.\n if (this.#relayOptions.enableReconnect !== true) {\n this.#relays.delete(norm);\n this.#lastActivity.delete(norm);\n }\n throw error;\n }\n }\n return relay;\n }\n\n /** Close every pooled relay and clear all pool state. */\n close(): void {\n this.#stopIdleCleanup();\n for (const timer of this.#resumeTimers.values()) {\n clearTimeout(timer);\n }\n this.#resumeTimers.clear();\n this.#suspendedUntil.clear();\n this.#invalidEvents.clear();\n for (const relay of this.#relays.values()) {\n relay.close();\n }\n this.#relays.clear();\n this.#lastActivity.clear();\n }\n\n /** Close and forget the listed relays; other pooled relays are untouched. */\n closeRelays(urls: ReadonlyArray<string>): void {\n for (const url of urls) {\n const norm = normalizeURL(url);\n const timer = this.#resumeTimers.get(norm);\n if (timer !== undefined) {\n clearTimeout(timer);\n this.#resumeTimers.delete(norm);\n }\n this.#relays.get(norm)?.close();\n this.#relays.delete(norm);\n this.#lastActivity.delete(norm);\n }\n }\n\n /**\n * Subscribe across relays. Deduplicates by event id. Returns a closer; callbacks receive every\n * new event once.\n */\n subscribe(\n relays: ReadonlyArray<string>,\n filters: ReadonlyArray<Filter>,\n opts: PoolSubscribeOptions = {},\n ): Closer {\n if (filters.length === 0) {\n throw new MessageError(\"REQ requires at least one filter\");\n }\n if (opts.id !== undefined) {\n assertSubscriptionId(opts.id);\n }\n const canonical = canonicalizeFilters(filters);\n return fanIn(this, [{ urls: relays, filters: canonical, id: opts.id }], {\n onevent: opts.onevent,\n oneose: opts.oneose,\n onclose: opts.onclose,\n signal: opts.signal,\n eoseTimeoutMs: opts.eoseTimeoutMs,\n alreadyHaveEvent: opts.alreadyHaveEvent,\n receivedEvent: opts.receivedEvent,\n closeOnEose: opts.closeOnEose,\n connectTimeoutMs: this.#relayOptions.connectTimeoutMs,\n });\n }\n\n /**\n * One-shot fetch per relay: each entry reports its own events and end reason (`eose`, `closed`,\n * `timeout`), or `failed` when the connect or REQ itself errored. An abort rejects the whole\n * call. Events are not deduped across relays — use {@link fetch} for that.\n */\n async fetchEach(\n relays: ReadonlyArray<string>,\n filters: ReadonlyArray<Filter>,\n opts?: { timeoutMs?: number | undefined; signal?: AbortSignal | undefined },\n ): Promise<PoolFetchResult[]> {\n if (filters.length === 0) {\n throw new MessageError(\"REQ requires at least one filter\");\n }\n throwIfAborted(opts?.signal);\n const canonical = canonicalizeFilters(filters);\n return Promise.all(\n uniqueRelayUrls(relays).map(async (url): Promise<PoolFetchResult> => {\n try {\n const relay = await this.ensureRelay(url, {\n signal: opts?.signal,\n timeoutMs: this.#relayOptions.connectTimeoutMs,\n });\n this.#touch(relay.url);\n const result = await relay.fetch([...canonical], {\n timeoutMs: opts?.timeoutMs,\n signal: opts?.signal,\n });\n return { url, events: result.events, end: result.end };\n } catch (error) {\n // An abort rejects the whole call; per-relay failures are reported.\n if (opts?.signal?.aborted === true) {\n throw abortReason(opts.signal);\n }\n return {\n url,\n events: [],\n end: { type: \"failed\", error: toError(error) },\n };\n }\n }),\n );\n }\n\n /** Fetch events until each connected relay EOSE or timeout; dedupe by id. */\n async fetch(\n relays: ReadonlyArray<string>,\n filters: ReadonlyArray<Filter>,\n opts?: {\n timeoutMs?: number | undefined;\n signal?: AbortSignal | undefined;\n /** Every event of every relay batch, including cross-relay duplicates. */\n onevent?: ((event: Event, relayUrl: string) => void) | undefined;\n },\n ): Promise<Event[]> {\n if (filters.length === 0) {\n throw new MessageError(\"REQ requires at least one filter\");\n }\n return fetchRouted(this, [{ urls: relays, filters: canonicalizeFilters(filters) }], {\n timeoutMs: opts?.timeoutMs,\n signal: opts?.signal,\n connectTimeoutMs: this.#relayOptions.connectTimeoutMs,\n onevent: opts?.onevent,\n });\n }\n\n /** Publish to all listed relays; returns per-relay outcomes. */\n async publish(\n relays: ReadonlyArray<string>,\n event: Event,\n opts?: { timeoutMs?: number | undefined },\n ): Promise<PoolPublishResult[]> {\n return Promise.all(\n uniqueRelayUrls(relays).map(async (url): Promise<PoolPublishResult> => {\n try {\n const relay = await this.ensureRelay(url, {\n timeoutMs: this.#relayOptions.connectTimeoutMs,\n });\n this.#touch(relay.url);\n const result = await relay.publish(event, { timeoutMs: opts?.timeoutMs });\n return result.ok\n ? { url, status: \"ok\", message: result.message }\n : { url, status: \"rejected\", message: result.message };\n } catch (error) {\n return { url, status: \"failed\", error: toError(error) };\n }\n }),\n );\n }\n\n /** First successful publish (Promise.any semantics). */\n async publishAny(\n relays: ReadonlyArray<string>,\n event: Event,\n opts?: { timeoutMs?: number | undefined },\n ): Promise<Extract<PoolPublishResult, { status: \"ok\" }>> {\n return Promise.any(\n uniqueRelayUrls(relays).map(async (url) => {\n const relay = await this.ensureRelay(url, {\n timeoutMs: this.#relayOptions.connectTimeoutMs,\n });\n this.#touch(relay.url);\n const result = await relay.publish(event, { timeoutMs: opts?.timeoutMs });\n if (!result.ok) {\n throw new RelayPublishError(result.message || \"rejected\", relay.url);\n }\n return { url, status: \"ok\" as const, message: result.message };\n }),\n );\n }\n\n /**\n * NIP-45 COUNT across relays. Per-relay outcomes; failures do not throw. Counts are not summed —\n * each relay reports independently (may overlap).\n */\n async count(\n relays: ReadonlyArray<string>,\n filters: ReadonlyArray<Filter>,\n opts?: { timeoutMs?: number | undefined; signal?: AbortSignal | undefined },\n ): Promise<PoolCountResult[]> {\n throwIfAborted(opts?.signal);\n const canonical = canonicalizeFilters(filters);\n return Promise.all(\n uniqueRelayUrls(relays).map(async (url): Promise<PoolCountResult> => {\n try {\n const relay = await this.ensureRelay(url, {\n signal: opts?.signal,\n timeoutMs: this.#relayOptions.connectTimeoutMs,\n });\n this.#touch(relay.url);\n const payload: CountResult = await relay.count(canonical, {\n timeoutMs: opts?.timeoutMs,\n signal: opts?.signal,\n });\n return countOk(url, payload);\n } catch (error) {\n // An abort rejects the whole call; per-relay failures are reported.\n if (opts?.signal?.aborted === true) {\n throw abortReason(opts.signal);\n }\n return { url, status: \"failed\", error: toError(error) };\n }\n }),\n );\n }\n\n listRelays(): string[] {\n return [...this.#relays.keys()];\n }\n\n /** Lookup a pooled relay, including disconnected reconnecting entries. */\n getRelay(url: string): Relay | undefined {\n try {\n return this.#relays.get(normalizeURL(url));\n } catch {\n return this.#relays.get(url);\n }\n }\n\n /** Currently connected URLs. Unlike listRelays(), excludes reconnecting/disconnected entries. */\n connectedUrls(): string[] {\n const urls: string[] = [];\n for (const [url, relay] of this.#relays) {\n if (relay.connected) {\n urls.push(url);\n }\n }\n return urls;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pool-THuuOW4S.mjs","names":[],"sources":["../src/relay/url.ts","../src/relay/fan-in.ts","../src/relay/pool.ts"],"sourcesContent":["import { normalizeURL } from \"../core/util.ts\";\n\n/**\n * {@link normalizeURL}, or the raw string when it is not a valid relay URL (ensureRelay then fails\n * it like a dead relay).\n */\nexport function relayUrlKey(url: string): string {\n try {\n return normalizeURL(url);\n } catch {\n return url;\n }\n}\n\n/** Unique relay keys in first-seen order. */\nexport function uniqueRelayUrls(urls: Iterable<string>): string[] {\n const seen = new Set<string>();\n const unique: string[] = [];\n for (const url of urls) {\n const key = relayUrlKey(url);\n if (seen.has(key)) {\n continue;\n }\n seen.add(key);\n unique.push(key);\n }\n return unique;\n}\n\n/**\n * True for `ws:` / `http:` URLs that are not `.onion`. Local-network `ws://` is still insecure;\n * allow those via a trusted set.\n */\nexport function isInsecureRelayUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n if (parsed.hostname.endsWith(\".onion\")) {\n return false;\n }\n return parsed.protocol === \"ws:\" || parsed.protocol === \"http:\";\n } catch {\n return false;\n }\n}\n","import { abortReason, onAbort, throwIfAborted } from \"../core/abort.ts\";\nimport type { Event } from \"../core/event.ts\";\nimport type { Filter } from \"../core/filter.ts\";\nimport { invokeSafely } from \"../core/report.ts\";\nimport type { Pool } from \"./pool.ts\";\nimport type { Relay } from \"./relay.ts\";\nimport type { Closer } from \"./subscription.ts\";\nimport { uniqueRelayUrls } from \"./url.ts\";\n\nexport type RoutedJob = {\n urls: ReadonlyArray<string>;\n filters: ReadonlyArray<Filter>;\n id?: string | undefined;\n};\n\nexport type FanInOptions = {\n /** First receipt only (deduped across jobs). `relayUrl` is the normalized relay URL. */\n onevent?: ((event: Event, relayUrl: string) => void) | undefined;\n oneose?: (() => void) | undefined;\n onclose?: ((reason: string) => void) | undefined;\n signal?: AbortSignal | undefined;\n /** Armed at fan-in only. Never forwarded to relay.subscribe. */\n eoseTimeoutMs?: number | undefined;\n alreadyHaveEvent?: ((id: string) => boolean) | undefined;\n /** Every receipt from every relay, including skipped duplicates. */\n receivedEvent?: ((id: string, relayUrl: string) => void) | undefined;\n closeOnEose?: boolean | undefined;\n /** Optional. ensureRelay uses Pool.#opts.connectTimeoutMs when omitted. */\n connectTimeoutMs?: number | undefined;\n};\n\n/**\n * Aggregate EOSE/close across routed jobs. One `seen` set. `pending` counts URL list entries\n * (duplicates included). `pendingEose` counts unique `jobIndex:url` keys.\n */\nexport function fanIn(pool: Pool, jobs: ReadonlyArray<RoutedJob>, opts: FanInOptions = {}): Closer {\n const seen = new Set<string>();\n const closers: Closer[] = [];\n let closed = false;\n let eoseFired = false;\n let sawEose = false;\n let eoseTimer: ReturnType<typeof setTimeout> | undefined;\n const eoseDone = new Set<string>();\n const eoseAttempted = new Set<string>();\n let pendingEose = 0;\n let pending = 0;\n /** Close reason of the last attached subscription that ended; drives the aggregate reason. */\n let lastReason: string | undefined;\n\n const fireEose = () => {\n if (closed || eoseFired) {\n return;\n }\n eoseFired = true;\n if (eoseTimer !== undefined) {\n clearTimeout(eoseTimer);\n eoseTimer = undefined;\n }\n invokeSafely(() => opts.oneose?.());\n };\n\n /**\n * Remove one relay from the pending-EOSE set. `eosed` credits a real EOSE; a CLOSED or a connect\n * failure only drops the wait — the aggregate `oneose` fires once every remaining relay ended AND\n * at least one actually EOSE'd.\n */\n const settleEose = (jobIndex: number, url: string, eosed: boolean) => {\n const key = `${jobIndex}:${url}`;\n if (eoseDone.has(key)) {\n return;\n }\n eoseDone.add(key);\n if (eosed) {\n sawEose = true;\n }\n pendingEose -= 1;\n if (pendingEose === 0 && sawEose) {\n fireEose();\n }\n };\n\n const settleClose = () => {\n closed = true;\n disposeAbort();\n if (eoseTimer !== undefined) {\n clearTimeout(eoseTimer);\n eoseTimer = undefined;\n }\n };\n\n const closeAll = (reason?: string) => {\n if (closed) {\n return;\n }\n settleClose();\n for (const c of closers) {\n c.close(reason);\n }\n invokeSafely(() => opts.onclose?.(reason ?? \"closed by client\"));\n };\n\n if (opts.signal?.aborted === true) {\n closed = true;\n invokeSafely(() => opts.onclose?.(\"aborted\"));\n return { close: closeAll };\n }\n\n const disposeAbort = onAbort(opts.signal, () => closeAll(\"aborted\"));\n\n /**\n * Single completion rule for both endings: `pending` reaches zero when every URL entry either\n * failed to attach or its subscription ended — at that point no attached closer can still be\n * live. The reason is the last subscription's close reason, or \"all relays failed\" when no relay\n * ever attached.\n */\n const finish = (): void => {\n if (pending <= 0 && !closed) {\n settleClose();\n invokeSafely(() => opts.onclose?.(lastReason ?? \"all relays failed\"));\n }\n };\n\n const failUrl = (jobIndex: number, key: string): void => {\n settleEose(jobIndex, key, false);\n pending -= 1;\n finish();\n };\n\n const attach = (relay: Relay, job: RoutedJob, jobIndex: number): void => {\n if (closed) {\n return;\n }\n const received = opts.receivedEvent;\n const sub = relay.subscribe([...job.filters], {\n id: jobs.length === 1 ? job.id : undefined,\n closeOnEose: opts.closeOnEose,\n alreadyHaveEvent: (id) => {\n let have = seen.has(id);\n invokeSafely(() => {\n have = have || Boolean(opts.alreadyHaveEvent?.(id));\n });\n return have;\n },\n receivedEvent:\n received === undefined ? undefined : (id) => invokeSafely(() => received(id, relay.url)),\n onevent: (event) => {\n seen.add(event.id);\n invokeSafely(() => opts.onevent?.(event, relay.url));\n },\n oneose: () => settleEose(jobIndex, relay.url, true),\n onclose: (reason) => {\n settleEose(jobIndex, relay.url, false);\n lastReason = reason;\n pending -= 1;\n finish();\n },\n });\n closers.push(sub);\n };\n\n for (const [jobIndex, job] of jobs.entries()) {\n // Equivalent spellings of one relay URL attach exactly once per job.\n for (const key of uniqueRelayUrls(job.urls)) {\n pending += 1;\n const eoseKey = `${jobIndex}:${key}`;\n if (!eoseAttempted.has(eoseKey)) {\n eoseAttempted.add(eoseKey);\n pendingEose += 1;\n }\n\n const tryAttach = (relay: Relay): void => {\n try {\n attach(relay, job, jobIndex);\n } catch {\n // Any attach failure retires this URL: RelayClosedError is the expected\n // dropped-between-connect-and-subscribe race; anything else must not escape the\n // `void` wrapper as an unhandled rejection, so it fails the URL as well.\n failUrl(jobIndex, key);\n }\n };\n\n void (async (): Promise<void> => {\n let relay: Relay;\n try {\n relay = await pool.ensureRelay(key, {\n signal: opts.signal,\n timeoutMs: opts.connectTimeoutMs,\n });\n } catch {\n // ensureRelay rejected only — not tryAttach throws (those must not look like connect failure).\n const existing = pool.getRelay(key);\n if (existing !== undefined) {\n tryAttach(existing);\n return;\n }\n failUrl(jobIndex, key);\n return;\n }\n tryAttach(relay);\n })();\n }\n }\n\n if (opts.eoseTimeoutMs !== undefined && pending > 0) {\n eoseTimer = setTimeout(() => {\n eoseTimer = undefined;\n fireEose();\n }, opts.eoseTimeoutMs);\n }\n\n if (pending === 0) {\n queueMicrotask(() => {\n if (closed) {\n return;\n }\n settleClose();\n invokeSafely(() => opts.onclose?.(\"no relays\"));\n });\n }\n\n return { close: closeAll };\n}\n\nexport async function fetchRouted(\n pool: Pool,\n jobs: ReadonlyArray<RoutedJob>,\n opts: {\n timeoutMs?: number | undefined;\n signal?: AbortSignal | undefined;\n connectTimeoutMs?: number | undefined;\n /** Every event of every relay batch, including cross-relay duplicates. */\n onevent?: ((event: Event, relayUrl: string) => void) | undefined;\n } = {},\n): Promise<Event[]> {\n throwIfAborted(opts.signal);\n const byId = new Map<string, Event>();\n await Promise.all(\n jobs.flatMap((job) => {\n return uniqueRelayUrls(job.urls).map(async (url) => {\n let batch: ReadonlyArray<Event>;\n let relayUrl: string;\n try {\n const relay = await pool.ensureRelay(url, {\n signal: opts.signal,\n timeoutMs: opts.connectTimeoutMs,\n });\n relayUrl = relay.url;\n const result = await relay.fetch([...job.filters], {\n timeoutMs: opts.timeoutMs,\n signal: opts.signal,\n });\n batch = result.events;\n } catch {\n // An abort rejects the whole call; per-relay failures are skipped.\n if (opts.signal?.aborted === true) {\n throw abortReason(opts.signal);\n }\n return;\n }\n // The whole batch lands before callbacks so a throwing onevent cannot\n // drop events; listener errors are reported, never propagated.\n for (const event of batch) {\n if (!byId.has(event.id)) {\n byId.set(event.id, event);\n }\n }\n for (const event of batch) {\n invokeSafely(() => opts.onevent?.(event, relayUrl));\n }\n });\n }),\n );\n return [...byId.values()];\n}\n","import { abortReason, throwIfAborted } from \"../core/abort.ts\";\nimport { Emitter } from \"../core/emitter.ts\";\nimport { MessageError, toError } from \"../core/error.ts\";\nimport type { Event, EventTemplate } from \"../core/event.ts\";\nimport { canonicalizeFilters } from \"../core/filter.ts\";\nimport type { Filter } from \"../core/filter.ts\";\nimport { assertSubscriptionId } from \"../core/message.ts\";\nimport type { CountResult } from \"../core/message.ts\";\nimport { normalizeURL } from \"../core/util.ts\";\nimport { RelayConnectionError, RelayPublishError, RelaySuspendedError } from \"./error.ts\";\nimport { fanIn, fetchRouted } from \"./fan-in.ts\";\nimport { Relay, RelayStatus } from \"./relay.ts\";\nimport type { RelayFetchEnd, RelayOptions, SubscribeOptions } from \"./relay.ts\";\nimport type { Closer } from \"./subscription.ts\";\nimport { isInsecureRelayUrl, uniqueRelayUrls } from \"./url.ts\";\n\n/** Per-relay tolerance for events that fail id/signature verification. */\nexport type InvalidEventPolicy = {\n /** Invalid events tolerated within `windowMs`; exceeding it suspends the relay. */\n limit: number;\n windowMs: number;\n /** How long a suspended relay stays closed before it may reconnect. */\n cooldownMs: number;\n};\n\n/** Pool-wide options applied to every managed relay. */\nexport type PoolOptions = Omit<RelayOptions, \"authSigner\"> & {\n /**\n * When set, automatically answer NIP-42 AUTH challenges for relays that send them. Return\n * undefined to skip a given relay URL.\n */\n automaticallyAuth?:\n | ((relayURL: string) => ((event: EventTemplate) => Promise<Event>) | undefined)\n | undefined;\n /** When false (default), ensureRelay rejects isInsecureRelayUrl unless trusted. */\n allowInsecure?: boolean | undefined;\n trustedInsecureUrls?: ReadonlyArray<string> | undefined;\n /** Close unused relays. Unset/0 = disabled. */\n idleTimeoutMs?: number | undefined;\n idleCleanupIntervalMs?: number | undefined;\n /**\n * Soft cap on connected non-pinned relays. When `ensureRelay` would create a new non-pinned relay\n * at the cap, the least-recently-used idle one is closed first (idle = no subscriptions and no\n * in-flight requests). If none is idle the connect proceeds anyway — the cap never fails a\n * request.\n */\n maxRelays?: number | undefined;\n /** Normalized URLs never closed by idle cleanup or `maxRelays` eviction. */\n pinnedUrls?: ReadonlyArray<string> | undefined;\n /**\n * When set, a relay whose `limit`-th-plus-one EVENT fails id/signature verification inside a\n * `windowMs` sliding window is disconnected and suspended for `cooldownMs`. During suspension\n * `ensureRelay` rejects with {@link RelaySuspendedError}; once it lifts, live subscriptions resume\n * through the normal reconnect path. Unset = events are dropped without counting, as before.\n */\n invalidEventPolicy?: InvalidEventPolicy | undefined;\n};\n\n/** Typed event payloads for {@link Pool.on}. */\nexport type PoolEventMap = {\n /** Normalized URLs closed by an idle-cleanup pass. */\n idle: ReadonlyArray<string>;\n /**\n * A relay suspended for exceeding `invalidEventPolicy`: `url` is the normalized relay URL and\n * `until` the epoch-ms time the suspension lifts.\n */\n suspend: { readonly url: string; readonly until: number };\n};\n\n/** Per-relay publish outcome: the relay's OK verdict or a transport-level failure. */\nexport type PoolPublishResult =\n | { readonly url: string; readonly status: \"ok\"; readonly message: string }\n | { readonly url: string; readonly status: \"rejected\"; readonly message: string }\n | { readonly url: string; readonly status: \"failed\"; readonly error: Error };\n\n/** Multi-relay subscribe options: callbacks also receive the normalized relay URL. */\nexport type PoolSubscribeOptions = Omit<SubscribeOptions, \"onevent\" | \"receivedEvent\"> & {\n onevent?: ((event: Event, relayUrl: string) => void) | undefined;\n receivedEvent?: ((id: string, relayUrl: string) => void) | undefined;\n};\n\n/** Per-relay one-shot fetch outcome: collected events plus how its REQ ended. */\nexport type PoolFetchResult = {\n readonly url: string;\n readonly events: ReadonlyArray<Event>;\n readonly end: RelayFetchEnd | { readonly type: \"failed\"; readonly error: Error };\n};\n\n/** Per-relay NIP-45 COUNT result (or a transport-level failure). */\nexport type PoolCountResult =\n | ({ readonly url: string; readonly status: \"ok\" } & CountResult)\n | { readonly url: string; readonly status: \"failed\"; readonly error: Error };\n\nconst countOk = (url: string, payload: CountResult): PoolCountResult => ({\n url,\n status: \"ok\",\n ...payload,\n});\n\nfunction isIdle(relay: Relay): boolean {\n return (\n relay.status !== RelayStatus.Connecting &&\n relay.subscriptionCount === 0 &&\n relay.inFlightCount === 0\n );\n}\n\n/** Multi-relay coordinator: connection reuse, cross-relay event dedup, fan-out publish. */\nexport class Pool {\n readonly #relays = new Map<string, Relay>();\n readonly #relayOptions: Omit<RelayOptions, \"authSigner\">;\n readonly #automaticallyAuth: PoolOptions[\"automaticallyAuth\"];\n readonly #maxRelays: number | undefined;\n readonly #invalidEventPolicy: InvalidEventPolicy | undefined;\n readonly #events = new Emitter<PoolEventMap>();\n readonly #lastActivity = new Map<string, number>();\n readonly #idleTimeoutMs: number;\n #idleTimer: ReturnType<typeof setInterval> | undefined;\n #allowInsecure: boolean;\n #trustedInsecure: Set<string>;\n #pinned: Set<string>;\n /** Per-relay timestamps of verification failures inside the sliding window. */\n readonly #invalidEvents = new Map<string, number[]>();\n /** Normalized URL → epoch ms when its suspension lifts. */\n readonly #suspendedUntil = new Map<string, number>();\n readonly #resumeTimers = new Map<string, ReturnType<typeof setTimeout>>();\n\n constructor(opts: PoolOptions = {}) {\n const {\n automaticallyAuth,\n allowInsecure,\n trustedInsecureUrls,\n idleTimeoutMs,\n idleCleanupIntervalMs,\n maxRelays,\n pinnedUrls,\n invalidEventPolicy,\n ...relayOptions\n } = opts;\n this.#relayOptions = relayOptions;\n this.#automaticallyAuth = automaticallyAuth;\n this.#maxRelays = maxRelays;\n this.#invalidEventPolicy = invalidEventPolicy;\n this.#allowInsecure = allowInsecure ?? false;\n this.#trustedInsecure = new Set((trustedInsecureUrls ?? []).map(normalizeURL));\n this.#pinned = new Set((pinnedUrls ?? []).map(normalizeURL));\n this.#idleTimeoutMs = idleTimeoutMs ?? 0;\n if (this.#idleTimeoutMs > 0) {\n this.#idleTimer = setInterval(() => this.cleanIdleRelays(), idleCleanupIntervalMs ?? 30_000);\n }\n }\n\n /**\n * Listen for a pool event. Multiple listeners per type are allowed and fire in registration\n * order; a throwing listener is reported and does not break the dispatch. Returns an unsubscribe\n * function.\n */\n on<K extends keyof PoolEventMap>(\n type: K,\n listener: (payload: PoolEventMap[K]) => void,\n ): () => void {\n return this.#events.on(type, listener);\n }\n\n setAllowInsecure(allow: boolean): void {\n this.#allowInsecure = allow;\n }\n\n setTrustedInsecureUrls(urls: ReadonlyArray<string>): void {\n this.#trustedInsecure = new Set(urls.map(normalizeURL));\n }\n\n setPinnedUrls(urls: ReadonlyArray<string>): void {\n this.#pinned = new Set(urls.map(normalizeURL));\n }\n\n /**\n * Drop cached AUTH rejections and re-fire pending challenges on every pooled relay, so a later\n * `setSigner` takes effect without reconnect.\n */\n resetAuth(): void {\n for (const relay of this.#relays.values()) {\n relay.resetAuth();\n }\n }\n\n cleanIdleRelays(): void {\n if (this.#idleTimeoutMs <= 0) {\n return;\n }\n const now = Date.now();\n const idle: string[] = [];\n for (const [url, relay] of this.#relays) {\n if (this.#pinned.has(url) || !isIdle(relay)) {\n continue;\n }\n const last = this.#lastActivity.get(url) ?? 0;\n if (!relay.connected || now - last >= this.#idleTimeoutMs) {\n idle.push(url);\n }\n }\n for (const url of this.#lastActivity.keys()) {\n if (!this.#relays.has(url)) {\n this.#lastActivity.delete(url);\n }\n }\n if (idle.length === 0) {\n return;\n }\n this.closeRelays(idle);\n this.#events.emit(\"idle\", idle);\n }\n\n #touch(url: string): void {\n this.#lastActivity.set(url, Date.now());\n }\n\n /**\n * Count one verification failure for `norm`; on the `limit + 1`-th inside `windowMs` suspend the\n * relay: drop its connection (subscriptions kept), record `until`, and emit `suspend`.\n */\n #noteInvalidEvent(norm: string): void {\n const policy = this.#invalidEventPolicy;\n if (policy === undefined) {\n return;\n }\n const now = Date.now();\n const window = (this.#invalidEvents.get(norm) ?? []).filter((at) => now - at < policy.windowMs);\n window.push(now);\n if (window.length <= policy.limit) {\n this.#invalidEvents.set(norm, window);\n return;\n }\n this.#invalidEvents.delete(norm);\n if (this.#suspendedUntil.has(norm)) {\n return;\n }\n const until = now + policy.cooldownMs;\n this.#suspendedUntil.set(norm, until);\n this.#relays.get(norm)?.disconnect();\n const resume = setTimeout(() => {\n this.#resumeTimers.delete(norm);\n this.#suspendedUntil.delete(norm);\n const relay = this.#relays.get(norm);\n if (relay === undefined || relay.connected || relay.subscriptionCount === 0) {\n return;\n }\n void (async (): Promise<void> => {\n try {\n await relay.connect();\n } catch {\n // With enableReconnect the relay's own backoff retries; otherwise the\n // next ensureRelay attempt reconnects on demand.\n }\n })();\n }, policy.cooldownMs);\n this.#resumeTimers.set(norm, resume);\n this.#events.emit(\"suspend\", { url: norm, until });\n }\n\n #rejectInsecure(url: string, norm: string): void {\n if (this.#allowInsecure) {\n return;\n }\n if (!isInsecureRelayUrl(url)) {\n return;\n }\n if (this.#trustedInsecure.has(norm)) {\n return;\n }\n throw new RelayConnectionError(\"insecure relay connection blocked\", norm);\n }\n\n #stopIdleCleanup(): void {\n if (this.#idleTimer === undefined) {\n return;\n }\n clearInterval(this.#idleTimer);\n this.#idleTimer = undefined;\n }\n\n /**\n * Soft cap: closing the least-recently-used idle non-pinned relay when a new non-pinned relay\n * would push the count past `maxRelays`. Busy relays are never evicted; with none idle the cap is\n * exceeded rather than failing.\n */\n #enforceMaxRelays(incoming: string): void {\n const cap = this.#maxRelays;\n if (cap === undefined || this.#pinned.has(incoming)) {\n return;\n }\n let count = 0;\n let oldestUrl: string | undefined;\n let oldestAt = Number.POSITIVE_INFINITY;\n for (const [url, relay] of this.#relays) {\n if (this.#pinned.has(url)) {\n continue;\n }\n count += 1;\n if (!isIdle(relay)) {\n continue;\n }\n const at = this.#lastActivity.get(url) ?? 0;\n if (at < oldestAt) {\n oldestAt = at;\n oldestUrl = url;\n }\n }\n if (count >= cap && oldestUrl !== undefined) {\n this.closeRelays([oldestUrl]);\n }\n }\n\n async ensureRelay(\n url: string,\n opts?: { signal?: AbortSignal | undefined; timeoutMs?: number | undefined },\n ): Promise<Relay> {\n const norm = normalizeURL(url);\n this.#rejectInsecure(url, norm);\n const suspendedUntil = this.#suspendedUntil.get(norm);\n if (suspendedUntil !== undefined) {\n if (Date.now() < suspendedUntil) {\n throw new RelaySuspendedError(norm, suspendedUntil);\n }\n this.#suspendedUntil.delete(norm);\n }\n let relay = this.#relays.get(norm);\n if (!relay) {\n this.#enforceMaxRelays(norm);\n const signFn = this.#automaticallyAuth?.(norm) ?? undefined;\n const created = new Relay(norm, { ...this.#relayOptions, authSigner: signFn });\n // Only drop from the pool on terminal close (reconnect keeps the entry). Guard on\n // identity: a stale relay's delayed close must not delete a newer relay's entry.\n created.on(\"close\", () => {\n if (this.#relays.get(norm) === created) {\n this.#relays.delete(norm);\n this.#lastActivity.delete(norm);\n }\n });\n created.on(\"invalidevent\", () => {\n this.#noteInvalidEvent(norm);\n });\n if (signFn) {\n created.on(\"auth\", () => {\n void (async (): Promise<void> => {\n try {\n await created.auth(signFn);\n } catch {\n // auth failure surfaces on subsequent CLOSED/OK; avoid unhandled rejection\n }\n })();\n });\n }\n this.#relays.set(norm, created);\n relay = created;\n }\n this.#touch(norm);\n if (!relay.connected) {\n try {\n await relay.connect({\n signal: opts?.signal,\n timeoutMs: opts?.timeoutMs ?? this.#relayOptions.connectTimeoutMs,\n });\n } catch (error) {\n // Keep the entry when reconnect is enabled so open subscriptions can recover.\n if (this.#relayOptions.enableReconnect !== true) {\n this.#relays.delete(norm);\n this.#lastActivity.delete(norm);\n }\n throw error;\n }\n }\n return relay;\n }\n\n /** Close every pooled relay and clear all pool state. */\n close(): void {\n this.#stopIdleCleanup();\n for (const timer of this.#resumeTimers.values()) {\n clearTimeout(timer);\n }\n this.#resumeTimers.clear();\n this.#suspendedUntil.clear();\n this.#invalidEvents.clear();\n for (const relay of this.#relays.values()) {\n relay.close();\n }\n this.#relays.clear();\n this.#lastActivity.clear();\n }\n\n /** Close and forget the listed relays; other pooled relays are untouched. */\n closeRelays(urls: ReadonlyArray<string>): void {\n for (const url of urls) {\n const norm = normalizeURL(url);\n const timer = this.#resumeTimers.get(norm);\n if (timer !== undefined) {\n clearTimeout(timer);\n this.#resumeTimers.delete(norm);\n }\n this.#relays.get(norm)?.close();\n this.#relays.delete(norm);\n this.#lastActivity.delete(norm);\n }\n }\n\n /**\n * Subscribe across relays. Deduplicates by event id. Returns a closer; callbacks receive every\n * new event once.\n */\n subscribe(\n relays: ReadonlyArray<string>,\n filters: ReadonlyArray<Filter>,\n opts: PoolSubscribeOptions = {},\n ): Closer {\n if (filters.length === 0) {\n throw new MessageError(\"REQ requires at least one filter\");\n }\n if (opts.id !== undefined) {\n assertSubscriptionId(opts.id);\n }\n const canonical = canonicalizeFilters(filters);\n return fanIn(this, [{ urls: relays, filters: canonical, id: opts.id }], {\n onevent: opts.onevent,\n oneose: opts.oneose,\n onclose: opts.onclose,\n signal: opts.signal,\n eoseTimeoutMs: opts.eoseTimeoutMs,\n alreadyHaveEvent: opts.alreadyHaveEvent,\n receivedEvent: opts.receivedEvent,\n closeOnEose: opts.closeOnEose,\n connectTimeoutMs: this.#relayOptions.connectTimeoutMs,\n });\n }\n\n /**\n * One-shot fetch per relay: each entry reports its own events and end reason (`eose`, `closed`,\n * `timeout`), or `failed` when the connect or REQ itself errored. An abort rejects the whole\n * call. Events are not deduped across relays — use {@link fetch} for that.\n */\n async fetchEach(\n relays: ReadonlyArray<string>,\n filters: ReadonlyArray<Filter>,\n opts?: { timeoutMs?: number | undefined; signal?: AbortSignal | undefined },\n ): Promise<PoolFetchResult[]> {\n if (filters.length === 0) {\n throw new MessageError(\"REQ requires at least one filter\");\n }\n throwIfAborted(opts?.signal);\n const canonical = canonicalizeFilters(filters);\n return Promise.all(\n uniqueRelayUrls(relays).map(async (url): Promise<PoolFetchResult> => {\n try {\n const relay = await this.ensureRelay(url, {\n signal: opts?.signal,\n timeoutMs: this.#relayOptions.connectTimeoutMs,\n });\n this.#touch(relay.url);\n const result = await relay.fetch([...canonical], {\n timeoutMs: opts?.timeoutMs,\n signal: opts?.signal,\n });\n return { url, events: result.events, end: result.end };\n } catch (error) {\n // An abort rejects the whole call; per-relay failures are reported.\n if (opts?.signal?.aborted === true) {\n throw abortReason(opts.signal);\n }\n return {\n url,\n events: [],\n end: { type: \"failed\", error: toError(error) },\n };\n }\n }),\n );\n }\n\n /** Fetch events until each connected relay EOSE or timeout; dedupe by id. */\n async fetch(\n relays: ReadonlyArray<string>,\n filters: ReadonlyArray<Filter>,\n opts?: {\n timeoutMs?: number | undefined;\n signal?: AbortSignal | undefined;\n /** Every event of every relay batch, including cross-relay duplicates. */\n onevent?: ((event: Event, relayUrl: string) => void) | undefined;\n },\n ): Promise<Event[]> {\n if (filters.length === 0) {\n throw new MessageError(\"REQ requires at least one filter\");\n }\n return fetchRouted(this, [{ urls: relays, filters: canonicalizeFilters(filters) }], {\n timeoutMs: opts?.timeoutMs,\n signal: opts?.signal,\n connectTimeoutMs: this.#relayOptions.connectTimeoutMs,\n onevent: opts?.onevent,\n });\n }\n\n /** Publish to all listed relays; returns per-relay outcomes. */\n async publish(\n relays: ReadonlyArray<string>,\n event: Event,\n opts?: { timeoutMs?: number | undefined },\n ): Promise<PoolPublishResult[]> {\n return Promise.all(\n uniqueRelayUrls(relays).map(async (url): Promise<PoolPublishResult> => {\n try {\n const relay = await this.ensureRelay(url, {\n timeoutMs: this.#relayOptions.connectTimeoutMs,\n });\n this.#touch(relay.url);\n const result = await relay.publish(event, { timeoutMs: opts?.timeoutMs });\n return result.ok\n ? { url, status: \"ok\", message: result.message }\n : { url, status: \"rejected\", message: result.message };\n } catch (error) {\n return { url, status: \"failed\", error: toError(error) };\n }\n }),\n );\n }\n\n /** First successful publish (Promise.any semantics). */\n async publishAny(\n relays: ReadonlyArray<string>,\n event: Event,\n opts?: { timeoutMs?: number | undefined },\n ): Promise<Extract<PoolPublishResult, { status: \"ok\" }>> {\n return Promise.any(\n uniqueRelayUrls(relays).map(async (url) => {\n const relay = await this.ensureRelay(url, {\n timeoutMs: this.#relayOptions.connectTimeoutMs,\n });\n this.#touch(relay.url);\n const result = await relay.publish(event, { timeoutMs: opts?.timeoutMs });\n if (!result.ok) {\n throw new RelayPublishError(result.message || \"rejected\", relay.url);\n }\n return { url, status: \"ok\" as const, message: result.message };\n }),\n );\n }\n\n /**\n * NIP-45 COUNT across relays. Per-relay outcomes; failures do not throw. Counts are not summed —\n * each relay reports independently (may overlap).\n */\n async count(\n relays: ReadonlyArray<string>,\n filters: ReadonlyArray<Filter>,\n opts?: { timeoutMs?: number | undefined; signal?: AbortSignal | undefined },\n ): Promise<PoolCountResult[]> {\n throwIfAborted(opts?.signal);\n const canonical = canonicalizeFilters(filters);\n return Promise.all(\n uniqueRelayUrls(relays).map(async (url): Promise<PoolCountResult> => {\n try {\n const relay = await this.ensureRelay(url, {\n signal: opts?.signal,\n timeoutMs: this.#relayOptions.connectTimeoutMs,\n });\n this.#touch(relay.url);\n const payload: CountResult = await relay.count(canonical, {\n timeoutMs: opts?.timeoutMs,\n signal: opts?.signal,\n });\n return countOk(url, payload);\n } catch (error) {\n // An abort rejects the whole call; per-relay failures are reported.\n if (opts?.signal?.aborted === true) {\n throw abortReason(opts.signal);\n }\n return { url, status: \"failed\", error: toError(error) };\n }\n }),\n );\n }\n\n listRelays(): string[] {\n return [...this.#relays.keys()];\n }\n\n /** Lookup a pooled relay, including disconnected reconnecting entries. */\n getRelay(url: string): Relay | undefined {\n try {\n return this.#relays.get(normalizeURL(url));\n } catch {\n return this.#relays.get(url);\n }\n }\n\n /** Currently connected URLs. Unlike listRelays(), excludes reconnecting/disconnected entries. */\n connectedUrls(): string[] {\n const urls: string[] = [];\n for (const [url, relay] of this.#relays) {\n if (relay.connected) {\n urls.push(url);\n }\n }\n return urls;\n }\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
- import { o as isHex32 } from "./util-DMF9AIO0.mjs";
1
+ import { s as isHex32 } from "./util-DDpPkiED.mjs";
2
2
  import { i as isEphemeralKind, t as Kind } from "./kind-D8fENiiL.mjs";
3
- import { S as parseEventAddress, _ as eventAddress, a as isReplaceableWinner, h as validateSignedEvent, i as isMarkedVerified } from "./event-C2eLiPpV.mjs";
3
+ import { S as parseEventAddress, _ as eventAddress, a as isReplaceableWinner, h as validateSignedEvent, i as isMarkedVerified } from "./event-B3ua2_oL.mjs";
4
4
  //#region src/storage/deletion.ts
5
5
  /**
6
6
  * NIP-09 application plan for a kind 5 event.
@@ -184,4 +184,4 @@ function applyPutMemory(s, d) {
184
184
  //#endregion
185
185
  export { planDeletion as a, DeletionState as i, decidePut as n, outboxBoundKey as r, applyPutMemory as t };
186
186
 
187
- //# sourceMappingURL=put-JsiCKMyZ.mjs.map
187
+ //# sourceMappingURL=put-DRmV1wHD.mjs.map