@qntx/nostr 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/dist/abort-Ba8tDrOI.mjs +38 -0
  2. package/dist/abort-Ba8tDrOI.mjs.map +1 -0
  3. package/dist/{builder-BphJa-Y-.d.mts → builder-BWn55Paf.d.mts} +20 -14
  4. package/dist/{builder-q0VsvnYE.mjs → builder-CyrGKRVP.mjs} +32 -16
  5. package/dist/builder-CyrGKRVP.mjs.map +1 -0
  6. package/dist/{client-Bd8B1mYm.mjs → client-BWsX9Cv1.mjs} +354 -195
  7. package/dist/client-BWsX9Cv1.mjs.map +1 -0
  8. package/dist/client.d.mts +1 -1
  9. package/dist/client.mjs +1 -1
  10. package/dist/core.d.mts +8 -8
  11. package/dist/core.mjs +8 -9
  12. package/dist/error-BW7re1UN.d.mts +9 -0
  13. package/dist/error-B__OMBRY.mjs +14 -0
  14. package/dist/error-B__OMBRY.mjs.map +1 -0
  15. package/dist/{error-WEFS-R-4.d.mts → error-CR8Urc84.d.mts} +20 -8
  16. package/dist/{error-CwBxle6N.mjs → error-DrEIM7FO.mjs} +5 -3
  17. package/dist/error-DrEIM7FO.mjs.map +1 -0
  18. package/dist/{event-CzoQEFMF.mjs → event-D8Nfoh4K.mjs} +40 -32
  19. package/dist/event-D8Nfoh4K.mjs.map +1 -0
  20. package/dist/{event-B5D2jxt_.d.mts → event-fU3Y01PR.d.mts} +23 -20
  21. package/dist/{filter-BxwjEkZp.mjs → filter-BQl3GlHt.mjs} +42 -35
  22. package/dist/filter-BQl3GlHt.mjs.map +1 -0
  23. package/dist/filter-C0IMByE3.d.mts +37 -0
  24. package/dist/{gossip-C5RVB1UB.d.mts → gossip-BRQwIM_q.d.mts} +25 -23
  25. package/dist/{gossip-3xkRRCm_.mjs → gossip-oloDMWEj.mjs} +52 -34
  26. package/dist/gossip-oloDMWEj.mjs.map +1 -0
  27. package/dist/gossip.d.mts +1 -1
  28. package/dist/gossip.mjs +1 -1
  29. package/dist/{http-6vevbBlu.d.mts → http-C4HwIu4B.d.mts} +7 -7
  30. package/dist/{http-D2rhokZo.mjs → http-CuazNRIP.mjs} +10 -6
  31. package/dist/http-CuazNRIP.mjs.map +1 -0
  32. package/dist/index-B425CAcQ.d.mts +328 -0
  33. package/dist/index-BVyfUBuG.d.mts +181 -0
  34. package/dist/{index-BYMnhRIC.d.mts → index-Ba7bJ7DD.d.mts} +68 -50
  35. package/dist/{index-DaRyjo7O.d.mts → index-Boe8FshM.d.mts} +13 -7
  36. package/dist/index-CUIvFQdE.d.mts +73 -0
  37. package/dist/index-JdaS_uce.d.mts +207 -0
  38. package/dist/index.d.mts +22 -20
  39. package/dist/index.mjs +19 -18
  40. package/dist/{key-Dn-aX4x4.mjs → key-Bbtys6o7.mjs} +22 -14
  41. package/dist/key-Bbtys6o7.mjs.map +1 -0
  42. package/dist/{key-BiGxGLhg.d.mts → key-BlsYhN1V.d.mts} +9 -6
  43. package/dist/{kind-BLl4cLOc.mjs → kind-Cxwhhkx8.mjs} +2 -1
  44. package/dist/kind-Cxwhhkx8.mjs.map +1 -0
  45. package/dist/{loaders-BNQszDAn.mjs → loaders-o6YXB_Ri.mjs} +329 -305
  46. package/dist/loaders-o6YXB_Ri.mjs.map +1 -0
  47. package/dist/loaders.d.mts +3 -3
  48. package/dist/loaders.mjs +2 -2
  49. package/dist/memory-C2EVx0_O.mjs +46 -0
  50. package/dist/memory-C2EVx0_O.mjs.map +1 -0
  51. package/dist/{memory-BCmhZPek.mjs → memory-index-Do1mz0WF.mjs} +212 -82
  52. package/dist/memory-index-Do1mz0WF.mjs.map +1 -0
  53. package/dist/{message-C_Uj-Y-c.mjs → message-DcY6Fi9o.mjs} +38 -24
  54. package/dist/message-DcY6Fi9o.mjs.map +1 -0
  55. package/dist/{message-DJC-Jz6T.d.mts → message-iGPIqnn0.d.mts} +15 -8
  56. package/dist/{nip17-BWq-Kfq5.d.mts → nip17-DNEkz8rM.d.mts} +18 -17
  57. package/dist/{nip19-D7905oIv.d.mts → nip19-BevCJZsp.d.mts} +23 -3
  58. package/dist/{nip46-C_DoxiS_.d.mts → nip46-DDV4dC56.d.mts} +13 -12
  59. package/dist/{nip59-DljW_sYh.d.mts → nip59-zQgdLcEr.d.mts} +28 -25
  60. package/dist/{nip65-Di5hZRuV.d.mts → nip65-DBfh2smt.d.mts} +8 -3
  61. package/dist/{nip77-ByxlgoQO.d.mts → nip77-DH4RhS5r.d.mts} +9 -8
  62. package/dist/nips/blossom.d.mts +37 -37
  63. package/dist/nips/blossom.mjs +67 -52
  64. package/dist/nips/blossom.mjs.map +1 -1
  65. package/dist/nips/nip04.d.mts +2 -3
  66. package/dist/nips/nip04.mjs +6 -10
  67. package/dist/nips/nip04.mjs.map +1 -1
  68. package/dist/nips/nip05.d.mts +37 -43
  69. package/dist/nips/nip05.mjs +57 -57
  70. package/dist/nips/nip05.mjs.map +1 -1
  71. package/dist/nips/nip10.d.mts +18 -19
  72. package/dist/nips/nip10.mjs +76 -61
  73. package/dist/nips/nip10.mjs.map +1 -1
  74. package/dist/nips/nip11.d.mts +9 -8
  75. package/dist/nips/nip11.mjs +14 -11
  76. package/dist/nips/nip11.mjs.map +1 -1
  77. package/dist/nips/nip13.d.mts +9 -9
  78. package/dist/nips/nip13.mjs +15 -17
  79. package/dist/nips/nip13.mjs.map +1 -1
  80. package/dist/nips/nip17.d.mts +1 -1
  81. package/dist/nips/nip17.mjs +17 -12
  82. package/dist/nips/nip17.mjs.map +1 -1
  83. package/dist/nips/nip19.d.mts +1 -1
  84. package/dist/nips/nip19.mjs +52 -29
  85. package/dist/nips/nip19.mjs.map +1 -1
  86. package/dist/nips/nip21.d.mts +5 -6
  87. package/dist/nips/nip21.mjs +8 -7
  88. package/dist/nips/nip21.mjs.map +1 -1
  89. package/dist/nips/nip27.d.mts +7 -8
  90. package/dist/nips/nip27.mjs +26 -25
  91. package/dist/nips/nip27.mjs.map +1 -1
  92. package/dist/nips/nip42.d.mts +5 -6
  93. package/dist/nips/nip42.mjs +3 -3
  94. package/dist/nips/nip42.mjs.map +1 -1
  95. package/dist/nips/nip44.d.mts +24 -7
  96. package/dist/nips/nip44.mjs +25 -15
  97. package/dist/nips/nip44.mjs.map +1 -1
  98. package/dist/nips/nip46.d.mts +1 -1
  99. package/dist/nips/nip46.mjs +52 -49
  100. package/dist/nips/nip46.mjs.map +1 -1
  101. package/dist/nips/nip49.d.mts +8 -7
  102. package/dist/nips/nip49.mjs +16 -12
  103. package/dist/nips/nip49.mjs.map +1 -1
  104. package/dist/nips/nip51.d.mts +25 -27
  105. package/dist/nips/nip51.mjs +47 -36
  106. package/dist/nips/nip51.mjs.map +1 -1
  107. package/dist/nips/nip57.d.mts +12 -13
  108. package/dist/nips/nip57.mjs +35 -38
  109. package/dist/nips/nip57.mjs.map +1 -1
  110. package/dist/nips/nip59.d.mts +1 -1
  111. package/dist/nips/nip59.mjs +14 -18
  112. package/dist/nips/nip59.mjs.map +1 -1
  113. package/dist/nips/nip65.d.mts +1 -1
  114. package/dist/nips/nip65.mjs +11 -8
  115. package/dist/nips/nip65.mjs.map +1 -1
  116. package/dist/nips/nip77.d.mts +1 -1
  117. package/dist/nips/nip77.mjs +56 -34
  118. package/dist/nips/nip77.mjs.map +1 -1
  119. package/dist/nips/nip96.d.mts +21 -12
  120. package/dist/nips/nip96.mjs +59 -36
  121. package/dist/nips/nip96.mjs.map +1 -1
  122. package/dist/nips/nip98.d.mts +12 -12
  123. package/dist/nips/nip98.mjs +18 -19
  124. package/dist/nips/nip98.mjs.map +1 -1
  125. package/dist/{pool-C_j1OmBB.mjs → pool-XmAGaufS.mjs} +611 -487
  126. package/dist/pool-XmAGaufS.mjs.map +1 -0
  127. package/dist/pool-tEr7c5bG.d.mts +302 -0
  128. package/dist/reactive-DDlThDSZ.mjs +324 -0
  129. package/dist/reactive-DDlThDSZ.mjs.map +1 -0
  130. package/dist/reactive-DFpqtIf_.d.mts +66 -0
  131. package/dist/relay.d.mts +5 -4
  132. package/dist/relay.mjs +2 -2
  133. package/dist/report-CHld4uWj.mjs +32 -0
  134. package/dist/report-CHld4uWj.mjs.map +1 -0
  135. package/dist/{signer-CZ6K6FUc.mjs → signer-CKR9EHce.mjs} +107 -72
  136. package/dist/signer-CKR9EHce.mjs.map +1 -0
  137. package/dist/signer.d.mts +3 -3
  138. package/dist/signer.mjs +3 -2
  139. package/dist/storage-xXxLGCVb.mjs +1376 -0
  140. package/dist/storage-xXxLGCVb.mjs.map +1 -0
  141. package/dist/storage.d.mts +4 -4
  142. package/dist/storage.mjs +5 -4
  143. package/dist/store.d.mts +2 -0
  144. package/dist/store.mjs +2 -0
  145. package/dist/testing.d.mts +125 -0
  146. package/dist/testing.mjs +957 -0
  147. package/dist/testing.mjs.map +1 -0
  148. package/dist/types-2pCUprRU.d.mts +47 -0
  149. package/dist/types-CRQEk0Oi.d.mts +19 -0
  150. package/dist/util-LfQirgDk.mjs +113 -0
  151. package/dist/util-LfQirgDk.mjs.map +1 -0
  152. package/dist/wasm.d.mts +4 -4
  153. package/dist/wasm.mjs +54 -55
  154. package/dist/wasm.mjs.map +1 -1
  155. package/dist/websocket-CmeqvAkp.d.mts +27 -0
  156. package/package.json +31 -9
  157. package/dist/builder-q0VsvnYE.mjs.map +0 -1
  158. package/dist/client-Bd8B1mYm.mjs.map +0 -1
  159. package/dist/error-B49rbDzT.d.mts +0 -7
  160. package/dist/error-CwBxle6N.mjs.map +0 -1
  161. package/dist/error-DZEs5OuZ.mjs +0 -24
  162. package/dist/error-DZEs5OuZ.mjs.map +0 -1
  163. package/dist/event-CzoQEFMF.mjs.map +0 -1
  164. package/dist/filter-BxwjEkZp.mjs.map +0 -1
  165. package/dist/filter-DUEKP3q1.d.mts +0 -40
  166. package/dist/gossip-3xkRRCm_.mjs.map +0 -1
  167. package/dist/http-D2rhokZo.mjs.map +0 -1
  168. package/dist/index-C0C4e-Gn.d.mts +0 -55
  169. package/dist/index-DgOugrpF.d.mts +0 -275
  170. package/dist/index-EwL6fHLo.d.mts +0 -267
  171. package/dist/index-k2YhCgsh.d.mts +0 -57
  172. package/dist/key-Dn-aX4x4.mjs.map +0 -1
  173. package/dist/kind-BLl4cLOc.mjs.map +0 -1
  174. package/dist/loaders-BNQszDAn.mjs.map +0 -1
  175. package/dist/memory-BCmhZPek.mjs.map +0 -1
  176. package/dist/message-C_Uj-Y-c.mjs.map +0 -1
  177. package/dist/pool-C_j1OmBB.mjs.map +0 -1
  178. package/dist/pool-DhRi7l-b.d.mts +0 -297
  179. package/dist/signer-CZ6K6FUc.mjs.map +0 -1
  180. package/dist/storage-DOy0iLRE.mjs +0 -870
  181. package/dist/storage-DOy0iLRE.mjs.map +0 -1
  182. package/dist/types-BzyADqnw.d.mts +0 -19
  183. package/dist/types-Dn24TPFs.d.mts +0 -43
  184. package/dist/util-6tfLQ9JF.mjs +0 -73
  185. package/dist/util-6tfLQ9JF.mjs.map +0 -1
@@ -1,93 +1,93 @@
1
- import { n as EventTemplate, t as Event } from "../event-B5D2jxt_.mjs";
2
- import { t as EventBuilder } from "../builder-BphJa-Y-.mjs";
3
- import { a as NostrError } from "../error-WEFS-R-4.mjs";
4
- import { t as ManualFetch } from "../http-6vevbBlu.mjs";
1
+ import { n as EventTemplate, t as Event } from "../event-fU3Y01PR.mjs";
2
+ import { t as EventBuilder } from "../builder-BWn55Paf.mjs";
3
+ import { a as NostrError } from "../error-CR8Urc84.mjs";
4
+ import { t as ManualFetch } from "../http-C4HwIu4B.mjs";
5
5
  //#region src/nips/blossom.d.ts
6
- type BlobDescriptor = {
6
+ export type BlobDescriptor = {
7
7
  url: string;
8
8
  sha256: string;
9
9
  size: number;
10
10
  type?: string;
11
11
  uploaded?: number;
12
12
  };
13
- type BlossomFetch = ManualFetch;
13
+ export type BlossomFetch = ManualFetch;
14
14
  /** Signs a kind 24242 auth template (draft; pubkey is filled by the signer). */
15
- type BlossomSign = (template: EventTemplate) => Promise<Event>;
15
+ export type BlossomSign = (template: EventTemplate) => Promise<Event>;
16
16
  /** BUD-11 `t` verbs. Mirror reuses `upload`; there is no `t=mirror`. */
17
- type BlossomAuthVerb = "upload" | "delete" | "list" | "get" | "media";
18
- declare class BlossomError extends NostrError {
19
- readonly status?: number;
17
+ export type BlossomAuthVerb = "upload" | "delete" | "list" | "get" | "media";
18
+ export declare class BlossomError extends NostrError {
19
+ name: string;
20
+ readonly status?: number | undefined;
20
21
  constructor(message: string, options?: ErrorOptions & {
21
22
  status?: number;
22
23
  });
23
24
  }
24
- /** Last 64-char hex in the URL path (extension ignored). Null if none or the URL is invalid. */
25
- declare function getHashFromURL(url: string | URL): string | null;
26
- declare function sha256Blob(data: Blob | ArrayBuffer | Uint8Array): Promise<string>;
25
+ /** Last 64-char hex in the URL path (extension ignored). Undefined if none or the URL is invalid. */
26
+ export declare function getHashFromURL(url: string | URL): string | undefined;
27
+ export declare function sha256Blob(data: Blob | ArrayBuffer | Uint8Array): Promise<string>;
27
28
  /** True when `sha256Blob(data)` equals the expected hex (any case). */
28
- declare function verifyBlob(data: Blob | ArrayBuffer | Uint8Array, sha256Hex: string): Promise<boolean>;
29
- /**
30
- * `Nostr ` + base64url(utf8 JSON), padding stripped.
31
- * NIP-98 uses standard base64; mixing them is a 401.
32
- */
33
- declare function encodeAuthorizationHeader(event: Event): string;
34
- declare function createAuthTemplate(verb: BlossomAuthVerb, opts?: {
29
+ export declare function verifyBlob(data: Blob | ArrayBuffer | Uint8Array, sha256Hex: string): Promise<boolean>;
30
+ /** `Nostr ` + base64url(utf8 JSON), padding stripped. NIP-98 uses standard base64; mixing them is a 401. */
31
+ export declare function encodeAuthorizationHeader(event: Event): string;
32
+ export declare function createAuthTemplate(verb: BlossomAuthVerb, opts?: {
35
33
  sha256?: string;
36
34
  expiration?: number;
37
35
  message?: string;
38
36
  }): EventTemplate;
39
- declare function createUploadAuth(sign: BlossomSign, file: Blob, opts?: {
37
+ export declare function createUploadAuth(sign: BlossomSign, file: Blob, opts?: {
40
38
  message?: string;
41
39
  expiration?: number;
42
40
  }): Promise<Event>;
43
- declare function upload(server: string, file: Blob, auth: Event, opts?: {
41
+ export declare function upload(server: string, file: Blob, auth: Event, opts?: {
44
42
  fetch?: BlossomFetch;
45
43
  signal?: AbortSignal;
46
44
  }): Promise<BlobDescriptor>;
47
- declare function listBlobs(server: string, pubkey: string, auth: Event | undefined, opts?: {
45
+ export declare function listBlobs(server: string, pubkey: string, auth: Event | undefined, opts?: {
48
46
  fetch?: BlossomFetch;
49
47
  signal?: AbortSignal;
50
48
  }): Promise<BlobDescriptor[]>;
51
- declare function deleteBlob(server: string, sha256Hex: string, auth: Event, opts?: {
49
+ export declare function deleteBlob(server: string, sha256Hex: string, auth: Event, opts?: {
52
50
  fetch?: BlossomFetch;
53
51
  signal?: AbortSignal;
54
52
  }): Promise<void>;
55
- declare function mirrorBlob(server: string, blob: BlobDescriptor, opts: {
53
+ export declare function mirrorBlob(server: string, blob: BlobDescriptor, opts: {
56
54
  auth: Event;
57
55
  fetch?: BlossomFetch;
58
56
  signal?: AbortSignal;
59
57
  }): Promise<BlobDescriptor>;
60
58
  /** BUD-06 preflight. Does not PUT the body. */
61
- declare function checkUpload(server: string, file: Blob, auth: Event, opts?: {
59
+ export declare function checkUpload(server: string, file: Blob, auth: Event, opts?: {
62
60
  fetch?: BlossomFetch;
63
61
  signal?: AbortSignal;
64
62
  }): Promise<void>;
65
- /** Strict HEAD probe. 2xx → true. 404 → false. Other HTTP (including 405) / network → throw BlossomError. AbortError propagates. No GET fallback. */
66
- declare function blobExists(server: string, sha256Hex: string, opts?: {
63
+ /**
64
+ * Strict HEAD probe. 2xx → true. 404 → false. Other HTTP (including 405) / network → throw
65
+ * BlossomError. AbortError propagates. No GET fallback.
66
+ */
67
+ export declare function blobExists(server: string, sha256Hex: string, opts?: {
67
68
  fetch?: BlossomFetch;
68
69
  signal?: AbortSignal;
69
70
  }): Promise<boolean>;
70
71
  /** GET body, then verifyBlob. Non-2xx or sha256 mismatch → throw BlossomError. */
71
- declare function getBlob(server: string, sha256Hex: string, opts?: {
72
+ export declare function getBlob(server: string, sha256Hex: string, opts?: {
72
73
  fetch?: BlossomFetch;
73
74
  signal?: AbortSignal;
74
75
  }): Promise<Uint8Array>;
75
76
  /**
76
- * If `url` is not available, try `servers` for the same hash (NIP-B7 SHOULD).
77
- * Never throws on HTTP status. AbortError still throws.
78
- * Returns the original URL when no hash, original still available, or nothing else responds.
77
+ * If `url` is not available, try `servers` for the same hash (NIP-B7 SHOULD). Never throws on HTTP
78
+ * status. AbortError still throws. Returns the original URL when no hash, original still available,
79
+ * or nothing else responds.
79
80
  */
80
- declare function healBlobUrl(url: string, servers: readonly string[], opts?: {
81
+ export declare function healBlobUrl(url: string, servers: ReadonlyArray<string>, opts?: {
81
82
  fetch?: BlossomFetch;
82
83
  signal?: AbortSignal;
83
84
  }): Promise<string>;
84
85
  /** PUT /upload to each server in order until one descriptor succeeds. */
85
- declare function uploadToServers(servers: readonly string[], file: Blob, auth: Event, opts?: {
86
+ export declare function uploadToServers(servers: ReadonlyArray<string>, file: Blob, auth: Event, opts?: {
86
87
  fetch?: BlossomFetch;
87
88
  signal?: AbortSignal;
88
89
  }): Promise<BlobDescriptor>;
89
- declare function blossomServerListEventBuilder(servers: readonly string[]): EventBuilder;
90
- declare function parseBlossomServerList(event: Pick<Event, "kind" | "tags">): string[];
90
+ export declare function blossomServerListEventBuilder(servers: ReadonlyArray<string>): EventBuilder;
91
+ export declare function parseBlossomServerList(event: Pick<Event, "kind" | "tags">): string[];
91
92
  //#endregion
92
- export { BlobDescriptor, BlossomAuthVerb, BlossomError, BlossomFetch, BlossomSign, blobExists, blossomServerListEventBuilder, checkUpload, createAuthTemplate, createUploadAuth, deleteBlob, encodeAuthorizationHeader, getBlob, getHashFromURL, healBlobUrl, listBlobs, mirrorBlob, parseBlossomServerList, sha256Blob, upload, uploadToServers, verifyBlob };
93
93
  //# sourceMappingURL=blossom.d.mts.map
@@ -1,14 +1,13 @@
1
- import { l as utf8Encoder, r as bytesToHex, t as assertHex32 } from "../util-6tfLQ9JF.mjs";
2
- import { a as NostrError, n as EventValidationError } from "../error-DZEs5OuZ.mjs";
3
- import { t as Kind } from "../kind-BLl4cLOc.mjs";
4
- import { t as EventBuilder } from "../builder-q0VsvnYE.mjs";
5
- import { n as requireGlobalFetch, r as sendManual, t as fetchManual } from "../http-D2rhokZo.mjs";
1
+ import { c as isRecord, d as utf8Encoder, g as NostrError, i as bytesToHex, n as assertHex32, p as EventValidationError } from "../util-LfQirgDk.mjs";
2
+ import { t as Kind } from "../kind-Cxwhhkx8.mjs";
3
+ import { t as EventBuilder } from "../builder-CyrGKRVP.mjs";
4
+ import { n as requireGlobalFetch, r as sendManual, t as fetchManual } from "../http-CuazNRIP.mjs";
6
5
  import { sha256 } from "@noble/hashes/sha2.js";
7
6
  import { base64urlnopad } from "@scure/base";
8
7
  //#region src/nips/blossom.ts
9
8
  /**
10
- * Blossom BUD-01/02/04/06 HTTP helpers, NIP-B7 kind 10063 server lists, and URL healing.
11
- * Auth is kind 24242, not NIP-98; the Authorization value is base64url.
9
+ * Blossom BUD-01/02/04/06 HTTP helpers, NIP-B7 kind 10063 server lists, and URL healing. Auth is
10
+ * kind 24242, not NIP-98; the Authorization value is base64url.
12
11
  *
13
12
  * @see https://github.com/hzrd149/blossom
14
13
  * @see https://github.com/nostr-protocol/nips/blob/master/B7.md
@@ -23,6 +22,7 @@ const AUTH_VERBS = /* @__PURE__ */ new Set([
23
22
  /** Default BUD-11 expiration: now + 1h. Servers 401 when the tag is missing. */
24
23
  const AUTH_EXPIRATION_SECS = 3600;
25
24
  var BlossomError = class extends NostrError {
25
+ name = "BlossomError";
26
26
  status;
27
27
  constructor(message, options) {
28
28
  const { status, ...rest } = options ?? {};
@@ -30,17 +30,17 @@ var BlossomError = class extends NostrError {
30
30
  this.status = status;
31
31
  }
32
32
  };
33
- /** Last 64-char hex in the URL path (extension ignored). Null if none or the URL is invalid. */
33
+ /** Last 64-char hex in the URL path (extension ignored). Undefined if none or the URL is invalid. */
34
34
  function getHashFromURL(url) {
35
35
  let path;
36
36
  try {
37
37
  path = (url instanceof URL ? url : new URL(url)).pathname;
38
38
  } catch {
39
- return null;
39
+ return;
40
40
  }
41
41
  const matches = path.match(/[0-9a-f]{64}/gi);
42
- if (!matches?.length) return null;
43
- return matches[matches.length - 1].toLowerCase();
42
+ if (matches === null || matches.length === 0) return;
43
+ return matches.at(-1)?.toLowerCase();
44
44
  }
45
45
  async function sha256Blob(data) {
46
46
  let bytes;
@@ -54,15 +54,21 @@ async function verifyBlob(data, sha256Hex) {
54
54
  const expected = assertHex32(sha256Hex, "blob sha256");
55
55
  return await sha256Blob(data) === expected;
56
56
  }
57
- /**
58
- * `Nostr ` + base64url(utf8 JSON), padding stripped.
59
- * NIP-98 uses standard base64; mixing them is a 401.
60
- */
57
+ /** `Nostr ` + base64url(utf8 JSON), padding stripped. NIP-98 uses standard base64; mixing them is a 401. */
61
58
  function encodeAuthorizationHeader(event) {
62
59
  return `Nostr ${base64urlnopad.encode(utf8Encoder.encode(JSON.stringify(event)))}`;
63
60
  }
61
+ /** Default BUD-11 human-readable content per verb. */
62
+ const AUTH_VERB_MESSAGES = {
63
+ upload: "Upload Blob",
64
+ delete: "Delete Blob",
65
+ list: "List Blobs",
66
+ get: "Get Blob",
67
+ media: "Upload Media"
68
+ };
64
69
  function createAuthTemplate(verb, opts) {
65
70
  if (!AUTH_VERBS.has(verb)) throw new BlossomError(`invalid blossom auth verb: ${verb}`);
71
+ if (opts?.message !== void 0 && opts.message.trim() === "") throw new BlossomError("blossom auth message must not be empty");
66
72
  const expiration = opts?.expiration ?? Math.floor(Date.now() / 1e3) + AUTH_EXPIRATION_SECS;
67
73
  const tags = [["t", verb], ["expiration", String(expiration)]];
68
74
  if (opts?.sha256 !== void 0) tags.push(["x", assertHex32(opts.sha256, "blob sha256")]);
@@ -70,7 +76,7 @@ function createAuthTemplate(verb, opts) {
70
76
  kind: Kind.BlobsAuth,
71
77
  created_at: Math.floor(Date.now() / 1e3),
72
78
  tags,
73
- content: opts?.message ?? ""
79
+ content: opts?.message ?? AUTH_VERB_MESSAGES[verb]
74
80
  };
75
81
  }
76
82
  async function createUploadAuth(sign, file, opts) {
@@ -85,7 +91,7 @@ async function upload(server, file, auth, opts) {
85
91
  Authorization: encodeAuthorizationHeader(auth),
86
92
  "X-SHA-256": hash
87
93
  };
88
- if (file.type) headers["Content-Type"] = file.type;
94
+ if (file.type !== "") headers["Content-Type"] = file.type;
89
95
  return requireDescriptorHash(parseBlobDescriptor(await readJson(await blossomRequest(opts?.fetch, blossomUrl(server, "/upload"), {
90
96
  method: "PUT",
91
97
  headers,
@@ -96,7 +102,7 @@ async function upload(server, file, auth, opts) {
96
102
  async function listBlobs(server, pubkey, auth, opts) {
97
103
  const pk = assertHex32(pubkey, "pubkey");
98
104
  const headers = {};
99
- if (auth) headers.Authorization = encodeAuthorizationHeader(auth);
105
+ if (auth) headers["Authorization"] = encodeAuthorizationHeader(auth);
100
106
  const res = await blossomRequest(opts?.fetch, blossomUrl(server, `/list/${pk}`), {
101
107
  method: "GET",
102
108
  headers,
@@ -122,7 +128,7 @@ async function mirrorBlob(server, blob, opts) {
122
128
  "X-SHA-256": hash,
123
129
  "X-Content-Length": String(blob.size)
124
130
  };
125
- if (blob.type) headers["X-Content-Type"] = blob.type;
131
+ if (blob.type !== void 0 && blob.type !== "") headers["X-Content-Type"] = blob.type;
126
132
  return requireDescriptorHash(parseBlobDescriptor(await readJson(await blossomRequest(opts.fetch, blossomUrl(server, "/mirror"), {
127
133
  method: "PUT",
128
134
  headers,
@@ -145,7 +151,10 @@ async function checkUpload(server, file, auth, opts) {
145
151
  signal: opts?.signal
146
152
  });
147
153
  }
148
- /** Strict HEAD probe. 2xx → true. 404 → false. Other HTTP (including 405) / network → throw BlossomError. AbortError propagates. No GET fallback. */
154
+ /**
155
+ * Strict HEAD probe. 2xx → true. 404 → false. Other HTTP (including 405) / network → throw
156
+ * BlossomError. AbortError propagates. No GET fallback.
157
+ */
149
158
  async function blobExists(server, sha256Hex, opts) {
150
159
  const url = blossomUrl(server, `/${assertHex32(sha256Hex, "blob sha256")}`);
151
160
  const res = await blossomFetch(opts?.fetch, url, {
@@ -170,20 +179,20 @@ async function getBlob(server, sha256Hex, opts) {
170
179
  return bytes;
171
180
  }
172
181
  /**
173
- * If `url` is not available, try `servers` for the same hash (NIP-B7 SHOULD).
174
- * Never throws on HTTP status. AbortError still throws.
175
- * Returns the original URL when no hash, original still available, or nothing else responds.
182
+ * If `url` is not available, try `servers` for the same hash (NIP-B7 SHOULD). Never throws on HTTP
183
+ * status. AbortError still throws. Returns the original URL when no hash, original still available,
184
+ * or nothing else responds.
176
185
  */
177
186
  async function healBlobUrl(url, servers, opts) {
178
187
  const hash = getHashFromURL(url);
179
- if (!hash) return url;
188
+ if (hash === void 0) return url;
180
189
  const ext = extensionFromHashUrl(url, hash);
181
190
  const fetchImpl = opts?.fetch ?? requireGlobalFetch(missingBlossomFetch);
182
191
  const originalStatus = await headStatus(fetchImpl, url, opts?.signal);
183
192
  if (originalStatus !== void 0 && originalStatus >= 200 && originalStatus < 400) return url;
184
193
  for (const server of servers) {
185
194
  const base = parseHttpUrl(server);
186
- if (!base) continue;
195
+ if (base === void 0) continue;
187
196
  const candidate = `${base}/${hash}${ext}`;
188
197
  const status = await headStatus(fetchImpl, candidate, opts?.signal);
189
198
  if (status !== void 0 && status >= 200 && status < 300) return candidate;
@@ -196,9 +205,9 @@ async function uploadToServers(servers, file, auth, opts) {
196
205
  let last;
197
206
  for (const server of servers) try {
198
207
  return await upload(server, file, auth, opts);
199
- } catch (err) {
200
- if (isAbortError(err)) throw err;
201
- last = err instanceof BlossomError ? err : new BlossomError("blossom upload failed", { cause: err instanceof Error ? err : void 0 });
208
+ } catch (error) {
209
+ if (isAbortError(error)) throw error;
210
+ last = error instanceof BlossomError ? error : new BlossomError("blossom upload failed", { cause: error instanceof Error ? error : void 0 });
202
211
  }
203
212
  throw last ?? new BlossomError("no servers");
204
213
  }
@@ -207,10 +216,11 @@ function blossomServerListEventBuilder(servers) {
207
216
  const seen = /* @__PURE__ */ new Set();
208
217
  for (const raw of servers) {
209
218
  const url = parseHttpUrl(raw);
210
- if (!url || seen.has(url)) continue;
219
+ if (url === void 0 || seen.has(url)) continue;
211
220
  seen.add(url);
212
221
  tags.push(["server", url]);
213
222
  }
223
+ if (tags.length === 0) throw new BlossomError("server list requires at least one valid http(s) URL");
214
224
  return new EventBuilder(Kind.BlossomServerList, "").tags(tags);
215
225
  }
216
226
  function parseBlossomServerList(event) {
@@ -218,9 +228,10 @@ function parseBlossomServerList(event) {
218
228
  const out = [];
219
229
  const seen = /* @__PURE__ */ new Set();
220
230
  for (const tag of event.tags) {
221
- if (tag[0] !== "server" || !tag[1]) continue;
222
- const url = parseHttpUrl(tag[1]);
223
- if (!url || seen.has(url)) continue;
231
+ const tagValue = tag.at(1);
232
+ if (tag[0] !== "server" || tagValue === void 0 || tagValue === "") continue;
233
+ const url = parseHttpUrl(tagValue);
234
+ if (url === void 0 || seen.has(url)) continue;
224
235
  seen.add(url);
225
236
  out.push(url);
226
237
  }
@@ -229,7 +240,7 @@ function parseBlossomServerList(event) {
229
240
  function parseHttpUrl(raw) {
230
241
  try {
231
242
  const u = new URL(raw);
232
- if (u.protocol !== "http:" && u.protocol !== "https:") return void 0;
243
+ if (u.protocol !== "http:" && u.protocol !== "https:") return;
233
244
  const path = u.pathname.replace(/\/+$/, "");
234
245
  return `${u.origin}${path}`;
235
246
  } catch {
@@ -239,7 +250,7 @@ function parseHttpUrl(raw) {
239
250
  /** Join `/upload` etc. onto the stored server, keeping a path prefix (`/v1/upload`). */
240
251
  function blossomUrl(server, path) {
241
252
  const base = parseHttpUrl(server);
242
- if (!base) throw new BlossomError(`invalid blossom server URL: ${server}`);
253
+ if (base === void 0) throw new BlossomError(`invalid blossom server URL: ${server}`);
243
254
  return `${base}${path.startsWith("/") ? path : `/${path}`}`;
244
255
  }
245
256
  function isAbortError(err) {
@@ -249,13 +260,13 @@ function isAbortError(err) {
249
260
  function extensionFromHashUrl(url, hash) {
250
261
  let pathname;
251
262
  try {
252
- pathname = new URL(url).pathname;
263
+ ({pathname} = new URL(url));
253
264
  } catch {
254
265
  return "";
255
266
  }
256
267
  const i = pathname.toLowerCase().lastIndexOf(hash);
257
- if (i < 0) return "";
258
- return pathname.slice(i + 64).match(/^\.[^/]+/)?.[0] ?? "";
268
+ if (i === -1) return "";
269
+ return /^\.[^/]+/.exec(pathname.slice(i + 64))?.[0] ?? "";
259
270
  }
260
271
  function missingBlossomFetch() {
261
272
  return new BlossomError("no fetch implementation available; pass opts.fetch");
@@ -267,20 +278,24 @@ function blossomHttpError(method, url, res) {
267
278
  } catch {
268
279
  reason = void 0;
269
280
  }
270
- return new BlossomError(reason?.trim() || `blossom ${method} ${url} failed (${res.status})`, { status: res.status });
281
+ const detail = reason?.trim();
282
+ return new BlossomError(detail === void 0 || detail === "" ? `blossom ${method} ${url} failed (${res.status})` : detail, { status: res.status });
271
283
  }
272
284
  async function blossomFetch(fetchImpl, url, init) {
273
285
  return fetchManual(fetchImpl ?? requireGlobalFetch(missingBlossomFetch), url, init, (err) => new BlossomError(`blossom ${init.method} ${url} failed`, { cause: err instanceof Error ? err : void 0 }));
274
286
  }
275
- /** HEAD with redirect:manual. Network → undefined. AbortError propagates. Never throws on HTTP status. */
287
+ /**
288
+ * HEAD with redirect:manual. Network → undefined. AbortError propagates. Never throws on HTTP
289
+ * status.
290
+ */
276
291
  async function headStatus(fetchImpl, url, signal) {
277
292
  try {
278
293
  return (await sendManual(fetchImpl, url, {
279
294
  method: "HEAD",
280
295
  signal
281
296
  })).status;
282
- } catch (err) {
283
- if (isAbortError(err)) throw err;
297
+ } catch (error) {
298
+ if (isAbortError(error)) throw error;
284
299
  return;
285
300
  }
286
301
  }
@@ -292,27 +307,27 @@ async function blossomRequest(fetchImpl, url, init) {
292
307
  async function readJson(res) {
293
308
  try {
294
309
  return await res.json();
295
- } catch (cause) {
310
+ } catch (error) {
296
311
  throw new BlossomError("invalid blossom JSON response", {
297
- cause: cause instanceof Error ? cause : void 0,
312
+ cause: error instanceof Error ? error : void 0,
298
313
  status: res.status
299
314
  });
300
315
  }
301
316
  }
302
317
  function parseBlobDescriptor(json) {
303
- if (!json || typeof json !== "object" || Array.isArray(json)) throw new BlossomError("invalid blob descriptor");
318
+ if (!isRecord(json)) throw new BlossomError("invalid blob descriptor");
304
319
  const raw = json;
305
- if (typeof raw.url !== "string" || !raw.url) throw new BlossomError("blob descriptor missing url");
306
- if (typeof raw.sha256 !== "string") throw new BlossomError("blob descriptor missing sha256");
307
- const hash = assertHex32(raw.sha256, "blob sha256");
308
- if (typeof raw.size !== "number" || !Number.isInteger(raw.size) || raw.size < 0) throw new BlossomError("blob descriptor missing size");
320
+ if (typeof raw["url"] !== "string" || !raw["url"]) throw new BlossomError("blob descriptor missing url");
321
+ if (typeof raw["sha256"] !== "string") throw new BlossomError("blob descriptor missing sha256");
322
+ const hash = assertHex32(raw["sha256"], "blob sha256");
323
+ if (typeof raw["size"] !== "number" || !Number.isInteger(raw["size"]) || raw["size"] < 0) throw new BlossomError("blob descriptor missing size");
309
324
  const desc = {
310
- url: raw.url,
325
+ url: raw["url"],
311
326
  sha256: hash,
312
- size: raw.size
327
+ size: raw["size"]
313
328
  };
314
- if (typeof raw.type === "string") desc.type = raw.type;
315
- if (typeof raw.uploaded === "number" && Number.isInteger(raw.uploaded) && raw.uploaded >= 0) desc.uploaded = raw.uploaded;
329
+ if (typeof raw["type"] === "string") desc.type = raw["type"];
330
+ if (typeof raw["uploaded"] === "number" && Number.isInteger(raw["uploaded"]) && raw["uploaded"] >= 0) desc.uploaded = raw["uploaded"];
316
331
  return desc;
317
332
  }
318
333
  function requireDescriptorHash(desc, expected) {
@@ -1 +1 @@
1
- {"version":3,"file":"blossom.mjs","names":[],"sources":["../../src/nips/blossom.ts"],"sourcesContent":["/**\n * Blossom BUD-01/02/04/06 HTTP helpers, NIP-B7 kind 10063 server lists, and URL healing.\n * Auth is kind 24242, not NIP-98; the Authorization value is base64url.\n *\n * @see https://github.com/hzrd149/blossom\n * @see https://github.com/nostr-protocol/nips/blob/master/B7.md\n */\nimport { sha256 } from \"@noble/hashes/sha2.js\";\nimport { base64urlnopad } from \"@scure/base\";\nimport { EventBuilder } from \"../core/builder.ts\";\nimport { EventValidationError, NostrError } from \"../core/error.ts\";\nimport type { Event, EventTemplate } from \"../core/event.ts\";\nimport { Kind } from \"../core/kind.ts\";\nimport type { Tag } from \"../core/tag.ts\";\nimport { assertHex32, bytesToHex, utf8Encoder } from \"../core/util.ts\";\nimport { fetchManual, requireGlobalFetch, sendManual, type ManualFetch } from \"./http.ts\";\n\nexport type BlobDescriptor = {\n url: string;\n sha256: string;\n size: number;\n type?: string;\n uploaded?: number;\n};\n\nexport type BlossomFetch = ManualFetch;\n\n/** Signs a kind 24242 auth template (draft; pubkey is filled by the signer). */\nexport type BlossomSign = (template: EventTemplate) => Promise<Event>;\n\n/** BUD-11 `t` verbs. Mirror reuses `upload`; there is no `t=mirror`. */\nexport type BlossomAuthVerb = \"upload\" | \"delete\" | \"list\" | \"get\" | \"media\";\n\nconst AUTH_VERBS = new Set<BlossomAuthVerb>([\"upload\", \"delete\", \"list\", \"get\", \"media\"]);\n\n/** Default BUD-11 expiration: now + 1h. Servers 401 when the tag is missing. */\nconst AUTH_EXPIRATION_SECS = 3600;\n\nexport class BlossomError extends NostrError {\n readonly status?: number;\n\n constructor(message: string, options?: ErrorOptions & { status?: number }) {\n const { status, ...rest } = options ?? {};\n super(message, rest);\n this.status = status;\n }\n}\n\n/** Last 64-char hex in the URL path (extension ignored). Null if none or the URL is invalid. */\nexport function getHashFromURL(url: string | URL): string | null {\n let path: string;\n try {\n path = (url instanceof URL ? url : new URL(url)).pathname;\n } catch {\n return null;\n }\n const matches = path.match(/[0-9a-f]{64}/gi);\n if (!matches?.length) return null;\n return matches[matches.length - 1]!.toLowerCase();\n}\n\nexport async function sha256Blob(data: Blob | ArrayBuffer | Uint8Array): Promise<string> {\n let bytes: Uint8Array;\n if (data instanceof Uint8Array) {\n bytes = data;\n } else if (data instanceof ArrayBuffer) {\n bytes = new Uint8Array(data);\n } else {\n bytes = new Uint8Array(await data.arrayBuffer());\n }\n return bytesToHex(sha256(bytes));\n}\n\n/** True when `sha256Blob(data)` equals the expected hex (any case). */\nexport async function verifyBlob(\n data: Blob | ArrayBuffer | Uint8Array,\n sha256Hex: string,\n): Promise<boolean> {\n const expected = assertHex32(sha256Hex, \"blob sha256\");\n return (await sha256Blob(data)) === expected;\n}\n\n/**\n * `Nostr ` + base64url(utf8 JSON), padding stripped.\n * NIP-98 uses standard base64; mixing them is a 401.\n */\nexport function encodeAuthorizationHeader(event: Event): string {\n return `Nostr ${base64urlnopad.encode(utf8Encoder.encode(JSON.stringify(event)))}`;\n}\n\nexport function createAuthTemplate(\n verb: BlossomAuthVerb,\n opts?: { sha256?: string; expiration?: number; message?: string },\n): EventTemplate {\n if (!AUTH_VERBS.has(verb)) {\n throw new BlossomError(`invalid blossom auth verb: ${verb}`);\n }\n const expiration = opts?.expiration ?? Math.floor(Date.now() / 1000) + AUTH_EXPIRATION_SECS;\n const tags: Tag[] = [\n [\"t\", verb],\n [\"expiration\", String(expiration)],\n ];\n if (opts?.sha256 !== undefined) {\n tags.push([\"x\", assertHex32(opts.sha256, \"blob sha256\")]);\n }\n return {\n kind: Kind.BlobsAuth,\n created_at: Math.floor(Date.now() / 1000),\n tags,\n content: opts?.message ?? \"\",\n };\n}\n\nexport async function createUploadAuth(\n sign: BlossomSign,\n file: Blob,\n opts?: { message?: string; expiration?: number },\n): Promise<Event> {\n const hash = await sha256Blob(file);\n return sign(createAuthTemplate(\"upload\", { sha256: hash, ...opts }));\n}\n\nexport async function upload(\n server: string,\n file: Blob,\n auth: Event,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<BlobDescriptor> {\n const hash = await sha256Blob(file);\n const headers: Record<string, string> = {\n Authorization: encodeAuthorizationHeader(auth),\n \"X-SHA-256\": hash,\n };\n if (file.type) headers[\"Content-Type\"] = file.type;\n const res = await blossomRequest(opts?.fetch, blossomUrl(server, \"/upload\"), {\n method: \"PUT\",\n headers,\n body: file,\n signal: opts?.signal,\n });\n return requireDescriptorHash(parseBlobDescriptor(await readJson(res)), hash);\n}\n\nexport async function listBlobs(\n server: string,\n pubkey: string,\n auth: Event | undefined,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<BlobDescriptor[]> {\n const pk = assertHex32(pubkey, \"pubkey\");\n const headers: Record<string, string> = {};\n if (auth) headers.Authorization = encodeAuthorizationHeader(auth);\n const res = await blossomRequest(opts?.fetch, blossomUrl(server, `/list/${pk}`), {\n method: \"GET\",\n headers,\n signal: opts?.signal,\n });\n const json = await readJson(res);\n if (!Array.isArray(json)) {\n throw new BlossomError(\"blossom list response must be a JSON array\", { status: res.status });\n }\n return json.map(parseBlobDescriptor);\n}\n\nexport async function deleteBlob(\n server: string,\n sha256Hex: string,\n auth: Event,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<void> {\n const hash = assertHex32(sha256Hex, \"blob sha256\");\n await blossomRequest(opts?.fetch, blossomUrl(server, `/${hash}`), {\n method: \"DELETE\",\n headers: { Authorization: encodeAuthorizationHeader(auth) },\n signal: opts?.signal,\n });\n}\n\nexport async function mirrorBlob(\n server: string,\n blob: BlobDescriptor,\n opts: { auth: Event; fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<BlobDescriptor> {\n const hash = assertHex32(blob.sha256, \"blob sha256\");\n const headers: Record<string, string> = {\n Authorization: encodeAuthorizationHeader(opts.auth),\n \"Content-Type\": \"application/json\",\n \"X-SHA-256\": hash,\n \"X-Content-Length\": String(blob.size),\n };\n if (blob.type) headers[\"X-Content-Type\"] = blob.type;\n const res = await blossomRequest(opts.fetch, blossomUrl(server, \"/mirror\"), {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ url: blob.url }),\n signal: opts.signal,\n });\n return requireDescriptorHash(parseBlobDescriptor(await readJson(res)), hash);\n}\n\n/** BUD-06 preflight. Does not PUT the body. */\nexport async function checkUpload(\n server: string,\n file: Blob,\n auth: Event,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<void> {\n const hash = await sha256Blob(file);\n const headers: Record<string, string> = {\n Authorization: encodeAuthorizationHeader(auth),\n \"X-SHA-256\": hash,\n \"X-Content-Length\": String(file.size),\n };\n if (file.type) headers[\"X-Content-Type\"] = file.type;\n await blossomRequest(opts?.fetch, blossomUrl(server, \"/upload\"), {\n method: \"HEAD\",\n headers,\n signal: opts?.signal,\n });\n}\n\n/** Strict HEAD probe. 2xx → true. 404 → false. Other HTTP (including 405) / network → throw BlossomError. AbortError propagates. No GET fallback. */\nexport async function blobExists(\n server: string,\n sha256Hex: string,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<boolean> {\n const hash = assertHex32(sha256Hex, \"blob sha256\");\n const url = blossomUrl(server, `/${hash}`);\n const res = await blossomFetch(opts?.fetch, url, { method: \"HEAD\", signal: opts?.signal });\n if (res.status === 404) return false;\n if (res.ok) return true;\n throw blossomHttpError(\"HEAD\", url, res);\n}\n\n/** GET body, then verifyBlob. Non-2xx or sha256 mismatch → throw BlossomError. */\nexport async function getBlob(\n server: string,\n sha256Hex: string,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<Uint8Array> {\n const hash = assertHex32(sha256Hex, \"blob sha256\");\n const url = blossomUrl(server, `/${hash}`);\n const res = await blossomFetch(opts?.fetch, url, { method: \"GET\", signal: opts?.signal });\n if (!res.ok) throw blossomHttpError(\"GET\", url, res);\n const bytes = new Uint8Array(await res.arrayBuffer());\n if (!(await verifyBlob(bytes, hash))) {\n throw new BlossomError(\"blob sha256 mismatch\");\n }\n return bytes;\n}\n\n/**\n * If `url` is not available, try `servers` for the same hash (NIP-B7 SHOULD).\n * Never throws on HTTP status. AbortError still throws.\n * Returns the original URL when no hash, original still available, or nothing else responds.\n */\nexport async function healBlobUrl(\n url: string,\n servers: readonly string[],\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<string> {\n const hash = getHashFromURL(url);\n if (!hash) return url;\n\n const ext = extensionFromHashUrl(url, hash);\n const fetchImpl = opts?.fetch ?? requireGlobalFetch(missingBlossomFetch);\n\n const originalStatus = await headStatus(fetchImpl, url, opts?.signal);\n // 2xx/3xx: still available. NIP-B7 only heals URLs that are not.\n if (originalStatus !== undefined && originalStatus >= 200 && originalStatus < 400) {\n return url;\n }\n\n for (const server of servers) {\n const base = parseHttpUrl(server);\n if (!base) continue;\n const candidate = `${base}/${hash}${ext}`;\n const status = await headStatus(fetchImpl, candidate, opts?.signal);\n if (status !== undefined && status >= 200 && status < 300) return candidate;\n }\n return url;\n}\n\n/** PUT /upload to each server in order until one descriptor succeeds. */\nexport async function uploadToServers(\n servers: readonly string[],\n file: Blob,\n auth: Event,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<BlobDescriptor> {\n if (servers.length === 0) {\n throw new BlossomError(\"no servers\");\n }\n let last: BlossomError | undefined;\n for (const server of servers) {\n try {\n return await upload(server, file, auth, opts);\n } catch (err) {\n if (isAbortError(err)) throw err;\n last =\n err instanceof BlossomError\n ? err\n : new BlossomError(\"blossom upload failed\", {\n cause: err instanceof Error ? err : undefined,\n });\n }\n }\n throw last ?? new BlossomError(\"no servers\");\n}\n\nexport function blossomServerListEventBuilder(servers: readonly string[]): EventBuilder {\n const tags: Tag[] = [];\n const seen = new Set<string>();\n for (const raw of servers) {\n const url = parseHttpUrl(raw);\n if (!url || seen.has(url)) continue;\n seen.add(url);\n tags.push([\"server\", url]);\n }\n return new EventBuilder(Kind.BlossomServerList, \"\").tags(tags);\n}\n\nexport function parseBlossomServerList(event: Pick<Event, \"kind\" | \"tags\">): string[] {\n if (event.kind !== Kind.BlossomServerList) {\n throw new EventValidationError(`expected kind ${Kind.BlossomServerList}, got ${event.kind}`);\n }\n const out: string[] = [];\n const seen = new Set<string>();\n for (const tag of event.tags) {\n if (tag[0] !== \"server\" || !tag[1]) continue;\n const url = parseHttpUrl(tag[1]);\n if (!url || seen.has(url)) continue;\n seen.add(url);\n out.push(url);\n }\n return out;\n}\n\nfunction parseHttpUrl(raw: string): string | undefined {\n try {\n const u = new URL(raw);\n if (u.protocol !== \"http:\" && u.protocol !== \"https:\") return undefined;\n const path = u.pathname.replace(/\\/+$/, \"\");\n return `${u.origin}${path}`;\n } catch {\n return undefined;\n }\n}\n\n/** Join `/upload` etc. onto the stored server, keeping a path prefix (`/v1/upload`). */\nfunction blossomUrl(server: string, path: string): string {\n const base = parseHttpUrl(server);\n if (!base) {\n throw new BlossomError(`invalid blossom server URL: ${server}`);\n }\n const suffix = path.startsWith(\"/\") ? path : `/${path}`;\n return `${base}${suffix}`;\n}\n\nfunction isAbortError(err: unknown): boolean {\n return err instanceof Error && err.name === \"AbortError\";\n}\n\n/** Path suffix after the last 64-hex segment (`HASH.png` → `.png`). */\nfunction extensionFromHashUrl(url: string, hash: string): string {\n let pathname: string;\n try {\n pathname = new URL(url).pathname;\n } catch {\n return \"\";\n }\n const i = pathname.toLowerCase().lastIndexOf(hash);\n if (i < 0) return \"\";\n const m = pathname.slice(i + 64).match(/^\\.[^/]+/);\n return m?.[0] ?? \"\";\n}\n\nfunction missingBlossomFetch(): BlossomError {\n return new BlossomError(\"no fetch implementation available; pass opts.fetch\");\n}\n\ntype ManualResponse = Awaited<ReturnType<ManualFetch>>;\n\nfunction blossomHttpError(method: string, url: string, res: ManualResponse): BlossomError {\n let reason: string | undefined;\n try {\n reason = res.headers.get(\"x-reason\") ?? undefined;\n } catch {\n reason = undefined;\n }\n return new BlossomError(reason?.trim() || `blossom ${method} ${url} failed (${res.status})`, {\n status: res.status,\n });\n}\n\nasync function blossomFetch(\n fetchImpl: BlossomFetch | undefined,\n url: string,\n init: {\n method: string;\n headers?: Record<string, string>;\n body?: Blob | string | null;\n signal?: AbortSignal;\n },\n): Promise<ManualResponse> {\n return fetchManual(\n fetchImpl ?? requireGlobalFetch(missingBlossomFetch),\n url,\n init,\n (err) =>\n new BlossomError(`blossom ${init.method} ${url} failed`, {\n cause: err instanceof Error ? err : undefined,\n }),\n );\n}\n\n/** HEAD with redirect:manual. Network → undefined. AbortError propagates. Never throws on HTTP status. */\nasync function headStatus(\n fetchImpl: ManualFetch,\n url: string,\n signal?: AbortSignal,\n): Promise<number | undefined> {\n try {\n const res = await sendManual(fetchImpl, url, { method: \"HEAD\", signal });\n return res.status;\n } catch (err) {\n if (isAbortError(err)) throw err;\n return undefined;\n }\n}\n\nasync function blossomRequest(\n fetchImpl: BlossomFetch | undefined,\n url: string,\n init: {\n method: string;\n headers?: Record<string, string>;\n body?: Blob | string | null;\n signal?: AbortSignal;\n },\n): Promise<ManualResponse> {\n const res = await blossomFetch(fetchImpl, url, init);\n if (res.ok) return res;\n throw blossomHttpError(init.method, url, res);\n}\n\nasync function readJson(res: ManualResponse): Promise<unknown> {\n try {\n return await res.json();\n } catch (cause) {\n throw new BlossomError(\"invalid blossom JSON response\", {\n cause: cause instanceof Error ? cause : undefined,\n status: res.status,\n });\n }\n}\n\nfunction parseBlobDescriptor(json: unknown): BlobDescriptor {\n if (!json || typeof json !== \"object\" || Array.isArray(json)) {\n throw new BlossomError(\"invalid blob descriptor\");\n }\n const raw = json as Record<string, unknown>;\n if (typeof raw.url !== \"string\" || !raw.url) {\n throw new BlossomError(\"blob descriptor missing url\");\n }\n if (typeof raw.sha256 !== \"string\") {\n throw new BlossomError(\"blob descriptor missing sha256\");\n }\n const hash = assertHex32(raw.sha256, \"blob sha256\");\n if (typeof raw.size !== \"number\" || !Number.isInteger(raw.size) || raw.size < 0) {\n throw new BlossomError(\"blob descriptor missing size\");\n }\n const desc: BlobDescriptor = { url: raw.url, sha256: hash, size: raw.size };\n if (typeof raw.type === \"string\") desc.type = raw.type;\n if (typeof raw.uploaded === \"number\" && Number.isInteger(raw.uploaded) && raw.uploaded >= 0) {\n desc.uploaded = raw.uploaded;\n }\n return desc;\n}\n\nfunction requireDescriptorHash(desc: BlobDescriptor, expected: string): BlobDescriptor {\n if (desc.sha256 !== expected) {\n throw new BlossomError(\"blob descriptor sha256 mismatch\");\n }\n return desc;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"blossom.mjs","names":[],"sources":["../../src/nips/blossom.ts"],"sourcesContent":["/**\n * Blossom BUD-01/02/04/06 HTTP helpers, NIP-B7 kind 10063 server lists, and URL healing. Auth is\n * kind 24242, not NIP-98; the Authorization value is base64url.\n *\n * @see https://github.com/hzrd149/blossom\n * @see https://github.com/nostr-protocol/nips/blob/master/B7.md\n */\nimport { sha256 } from \"@noble/hashes/sha2.js\";\nimport { base64urlnopad } from \"@scure/base\";\n\nimport { EventBuilder } from \"../core/builder.ts\";\nimport { EventValidationError, NostrError } from \"../core/error.ts\";\nimport type { Event, EventTemplate } from \"../core/event.ts\";\nimport { Kind } from \"../core/kind.ts\";\nimport type { Tag } from \"../core/tag.ts\";\nimport { assertHex32, bytesToHex, isRecord, utf8Encoder } from \"../core/util.ts\";\nimport { fetchManual, requireGlobalFetch, sendManual } from \"./http.ts\";\nimport type { ManualFetch } from \"./http.ts\";\n\nexport type BlobDescriptor = {\n url: string;\n sha256: string;\n size: number;\n type?: string;\n uploaded?: number;\n};\n\nexport type BlossomFetch = ManualFetch;\n\n/** Signs a kind 24242 auth template (draft; pubkey is filled by the signer). */\nexport type BlossomSign = (template: EventTemplate) => Promise<Event>;\n\n/** BUD-11 `t` verbs. Mirror reuses `upload`; there is no `t=mirror`. */\nexport type BlossomAuthVerb = \"upload\" | \"delete\" | \"list\" | \"get\" | \"media\";\n\nconst AUTH_VERBS = new Set<BlossomAuthVerb>([\"upload\", \"delete\", \"list\", \"get\", \"media\"]);\n\n/** Default BUD-11 expiration: now + 1h. Servers 401 when the tag is missing. */\nconst AUTH_EXPIRATION_SECS = 3600;\n\nexport class BlossomError extends NostrError {\n override name = \"BlossomError\";\n readonly status?: number | undefined;\n\n constructor(message: string, options?: ErrorOptions & { status?: number }) {\n const { status, ...rest } = options ?? {};\n super(message, rest);\n this.status = status;\n }\n}\n\n/** Last 64-char hex in the URL path (extension ignored). Undefined if none or the URL is invalid. */\nexport function getHashFromURL(url: string | URL): string | undefined {\n let path: string;\n try {\n path = (url instanceof URL ? url : new URL(url)).pathname;\n } catch {\n return undefined;\n }\n const matches = path.match(/[0-9a-f]{64}/gi);\n if (matches === null || matches.length === 0) {\n return undefined;\n }\n return matches.at(-1)?.toLowerCase();\n}\n\nexport async function sha256Blob(data: Blob | ArrayBuffer | Uint8Array): Promise<string> {\n let bytes: Uint8Array;\n if (data instanceof Uint8Array) {\n bytes = data;\n } else if (data instanceof ArrayBuffer) {\n bytes = new Uint8Array(data);\n } else {\n bytes = new Uint8Array(await data.arrayBuffer());\n }\n return bytesToHex(sha256(bytes));\n}\n\n/** True when `sha256Blob(data)` equals the expected hex (any case). */\nexport async function verifyBlob(\n data: Blob | ArrayBuffer | Uint8Array,\n sha256Hex: string,\n): Promise<boolean> {\n const expected = assertHex32(sha256Hex, \"blob sha256\");\n return (await sha256Blob(data)) === expected;\n}\n\n/** `Nostr ` + base64url(utf8 JSON), padding stripped. NIP-98 uses standard base64; mixing them is a 401. */\nexport function encodeAuthorizationHeader(event: Event): string {\n return `Nostr ${base64urlnopad.encode(utf8Encoder.encode(JSON.stringify(event)))}`;\n}\n\n/** Default BUD-11 human-readable content per verb. */\nconst AUTH_VERB_MESSAGES: Record<BlossomAuthVerb, string> = {\n upload: \"Upload Blob\",\n delete: \"Delete Blob\",\n list: \"List Blobs\",\n get: \"Get Blob\",\n media: \"Upload Media\",\n};\n\nexport function createAuthTemplate(\n verb: BlossomAuthVerb,\n opts?: { sha256?: string; expiration?: number; message?: string },\n): EventTemplate {\n if (!AUTH_VERBS.has(verb)) {\n throw new BlossomError(`invalid blossom auth verb: ${verb}`);\n }\n if (opts?.message !== undefined && opts.message.trim() === \"\") {\n throw new BlossomError(\"blossom auth message must not be empty\");\n }\n const expiration = opts?.expiration ?? Math.floor(Date.now() / 1000) + AUTH_EXPIRATION_SECS;\n const tags: Tag[] = [\n [\"t\", verb],\n [\"expiration\", String(expiration)],\n ];\n if (opts?.sha256 !== undefined) {\n tags.push([\"x\", assertHex32(opts.sha256, \"blob sha256\")]);\n }\n return {\n kind: Kind.BlobsAuth,\n created_at: Math.floor(Date.now() / 1000),\n tags,\n content: opts?.message ?? AUTH_VERB_MESSAGES[verb],\n };\n}\n\nexport async function createUploadAuth(\n sign: BlossomSign,\n file: Blob,\n opts?: { message?: string; expiration?: number },\n): Promise<Event> {\n const hash = await sha256Blob(file);\n return sign(createAuthTemplate(\"upload\", { sha256: hash, ...opts }));\n}\n\nexport async function upload(\n server: string,\n file: Blob,\n auth: Event,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<BlobDescriptor> {\n const hash = await sha256Blob(file);\n const headers: Record<string, string> = {\n Authorization: encodeAuthorizationHeader(auth),\n \"X-SHA-256\": hash,\n };\n if (file.type !== \"\") {\n headers[\"Content-Type\"] = file.type;\n }\n const res = await blossomRequest(opts?.fetch, blossomUrl(server, \"/upload\"), {\n method: \"PUT\",\n headers,\n body: file,\n signal: opts?.signal,\n });\n return requireDescriptorHash(parseBlobDescriptor(await readJson(res)), hash);\n}\n\nexport async function listBlobs(\n server: string,\n pubkey: string,\n auth: Event | undefined,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<BlobDescriptor[]> {\n const pk = assertHex32(pubkey, \"pubkey\");\n const headers: Record<string, string> = {};\n if (auth) {\n headers[\"Authorization\"] = encodeAuthorizationHeader(auth);\n }\n const res = await blossomRequest(opts?.fetch, blossomUrl(server, `/list/${pk}`), {\n method: \"GET\",\n headers,\n signal: opts?.signal,\n });\n const json = await readJson(res);\n if (!Array.isArray(json)) {\n throw new BlossomError(\"blossom list response must be a JSON array\", { status: res.status });\n }\n return json.map(parseBlobDescriptor);\n}\n\nexport async function deleteBlob(\n server: string,\n sha256Hex: string,\n auth: Event,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<void> {\n const hash = assertHex32(sha256Hex, \"blob sha256\");\n await blossomRequest(opts?.fetch, blossomUrl(server, `/${hash}`), {\n method: \"DELETE\",\n headers: { Authorization: encodeAuthorizationHeader(auth) },\n signal: opts?.signal,\n });\n}\n\nexport async function mirrorBlob(\n server: string,\n blob: BlobDescriptor,\n opts: { auth: Event; fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<BlobDescriptor> {\n const hash = assertHex32(blob.sha256, \"blob sha256\");\n const headers: Record<string, string> = {\n Authorization: encodeAuthorizationHeader(opts.auth),\n \"Content-Type\": \"application/json\",\n \"X-SHA-256\": hash,\n \"X-Content-Length\": String(blob.size),\n };\n if (blob.type !== undefined && blob.type !== \"\") {\n headers[\"X-Content-Type\"] = blob.type;\n }\n const res = await blossomRequest(opts.fetch, blossomUrl(server, \"/mirror\"), {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ url: blob.url }),\n signal: opts.signal,\n });\n return requireDescriptorHash(parseBlobDescriptor(await readJson(res)), hash);\n}\n\n/** BUD-06 preflight. Does not PUT the body. */\nexport async function checkUpload(\n server: string,\n file: Blob,\n auth: Event,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<void> {\n const hash = await sha256Blob(file);\n const headers: Record<string, string> = {\n Authorization: encodeAuthorizationHeader(auth),\n \"X-SHA-256\": hash,\n \"X-Content-Length\": String(file.size),\n };\n if (file.type) {\n headers[\"X-Content-Type\"] = file.type;\n }\n await blossomRequest(opts?.fetch, blossomUrl(server, \"/upload\"), {\n method: \"HEAD\",\n headers,\n signal: opts?.signal,\n });\n}\n\n/**\n * Strict HEAD probe. 2xx → true. 404 → false. Other HTTP (including 405) / network → throw\n * BlossomError. AbortError propagates. No GET fallback.\n */\nexport async function blobExists(\n server: string,\n sha256Hex: string,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<boolean> {\n const hash = assertHex32(sha256Hex, \"blob sha256\");\n const url = blossomUrl(server, `/${hash}`);\n const res = await blossomFetch(opts?.fetch, url, { method: \"HEAD\", signal: opts?.signal });\n if (res.status === 404) {\n return false;\n }\n if (res.ok) {\n return true;\n }\n throw blossomHttpError(\"HEAD\", url, res);\n}\n\n/** GET body, then verifyBlob. Non-2xx or sha256 mismatch → throw BlossomError. */\nexport async function getBlob(\n server: string,\n sha256Hex: string,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<Uint8Array> {\n const hash = assertHex32(sha256Hex, \"blob sha256\");\n const url = blossomUrl(server, `/${hash}`);\n const res = await blossomFetch(opts?.fetch, url, { method: \"GET\", signal: opts?.signal });\n if (!res.ok) {\n throw blossomHttpError(\"GET\", url, res);\n }\n const bytes = new Uint8Array(await res.arrayBuffer());\n if (!(await verifyBlob(bytes, hash))) {\n throw new BlossomError(\"blob sha256 mismatch\");\n }\n return bytes;\n}\n\n/**\n * If `url` is not available, try `servers` for the same hash (NIP-B7 SHOULD). Never throws on HTTP\n * status. AbortError still throws. Returns the original URL when no hash, original still available,\n * or nothing else responds.\n */\nexport async function healBlobUrl(\n url: string,\n servers: ReadonlyArray<string>,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<string> {\n const hash = getHashFromURL(url);\n if (hash === undefined) {\n return url;\n }\n\n const ext = extensionFromHashUrl(url, hash);\n const fetchImpl = opts?.fetch ?? requireGlobalFetch(missingBlossomFetch);\n\n const originalStatus = await headStatus(fetchImpl, url, opts?.signal);\n // 2xx/3xx: still available. NIP-B7 only heals URLs that are not.\n if (originalStatus !== undefined && originalStatus >= 200 && originalStatus < 400) {\n return url;\n }\n\n for (const server of servers) {\n const base = parseHttpUrl(server);\n if (base === undefined) {\n continue;\n }\n const candidate = `${base}/${hash}${ext}`;\n // oxlint-disable-next-line no-await-in-loop -- probes servers in order, returning on the first hit\n const status = await headStatus(fetchImpl, candidate, opts?.signal);\n if (status !== undefined && status >= 200 && status < 300) {\n return candidate;\n }\n }\n return url;\n}\n\n/** PUT /upload to each server in order until one descriptor succeeds. */\nexport async function uploadToServers(\n servers: ReadonlyArray<string>,\n file: Blob,\n auth: Event,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<BlobDescriptor> {\n if (servers.length === 0) {\n throw new BlossomError(\"no servers\");\n }\n let last: BlossomError | undefined;\n for (const server of servers) {\n try {\n // oxlint-disable-next-line no-await-in-loop -- tries servers sequentially until one succeeds\n return await upload(server, file, auth, opts);\n } catch (error) {\n if (isAbortError(error)) {\n throw error;\n }\n last =\n error instanceof BlossomError\n ? error\n : new BlossomError(\"blossom upload failed\", {\n cause: error instanceof Error ? error : undefined,\n });\n }\n }\n throw last ?? new BlossomError(\"no servers\");\n}\n\nexport function blossomServerListEventBuilder(servers: ReadonlyArray<string>): EventBuilder {\n const tags: Tag[] = [];\n const seen = new Set<string>();\n for (const raw of servers) {\n const url = parseHttpUrl(raw);\n if (url === undefined || seen.has(url)) {\n continue;\n }\n seen.add(url);\n tags.push([\"server\", url]);\n }\n if (tags.length === 0) {\n throw new BlossomError(\"server list requires at least one valid http(s) URL\");\n }\n return new EventBuilder(Kind.BlossomServerList, \"\").tags(tags);\n}\n\nexport function parseBlossomServerList(event: Pick<Event, \"kind\" | \"tags\">): string[] {\n if (event.kind !== Kind.BlossomServerList) {\n throw new EventValidationError(`expected kind ${Kind.BlossomServerList}, got ${event.kind}`);\n }\n const out: string[] = [];\n const seen = new Set<string>();\n for (const tag of event.tags) {\n const tagValue = tag.at(1);\n if (tag[0] !== \"server\" || tagValue === undefined || tagValue === \"\") {\n continue;\n }\n const url = parseHttpUrl(tagValue);\n if (url === undefined || seen.has(url)) {\n continue;\n }\n seen.add(url);\n out.push(url);\n }\n return out;\n}\n\nfunction parseHttpUrl(raw: string): string | undefined {\n try {\n const u = new URL(raw);\n if (u.protocol !== \"http:\" && u.protocol !== \"https:\") {\n return undefined;\n }\n const path = u.pathname.replace(/\\/+$/, \"\");\n return `${u.origin}${path}`;\n } catch {\n return undefined;\n }\n}\n\n/** Join `/upload` etc. onto the stored server, keeping a path prefix (`/v1/upload`). */\nfunction blossomUrl(server: string, path: string): string {\n const base = parseHttpUrl(server);\n if (base === undefined) {\n throw new BlossomError(`invalid blossom server URL: ${server}`);\n }\n const suffix = path.startsWith(\"/\") ? path : `/${path}`;\n return `${base}${suffix}`;\n}\n\nfunction isAbortError(err: unknown): boolean {\n return err instanceof Error && err.name === \"AbortError\";\n}\n\n/** Path suffix after the last 64-hex segment (`HASH.png` → `.png`). */\nfunction extensionFromHashUrl(url: string, hash: string): string {\n let pathname: string;\n try {\n ({ pathname } = new URL(url));\n } catch {\n return \"\";\n }\n const i = pathname.toLowerCase().lastIndexOf(hash);\n if (i === -1) {\n return \"\";\n }\n const m = /^\\.[^/]+/.exec(pathname.slice(i + 64));\n return m?.[0] ?? \"\";\n}\n\nfunction missingBlossomFetch(): BlossomError {\n return new BlossomError(\"no fetch implementation available; pass opts.fetch\");\n}\n\ntype ManualResponse = Awaited<ReturnType<ManualFetch>>;\n\nfunction blossomHttpError(method: string, url: string, res: ManualResponse): BlossomError {\n let reason: string | undefined;\n try {\n reason = res.headers.get(\"x-reason\") ?? undefined;\n } catch {\n reason = undefined;\n }\n const detail = reason?.trim();\n return new BlossomError(\n detail === undefined || detail === \"\"\n ? `blossom ${method} ${url} failed (${res.status})`\n : detail,\n { status: res.status },\n );\n}\n\nasync function blossomFetch(\n fetchImpl: BlossomFetch | undefined,\n url: string,\n init: {\n method: string;\n headers?: Record<string, string>;\n body?: Blob | string;\n signal?: AbortSignal | undefined;\n },\n): Promise<ManualResponse> {\n return fetchManual(\n fetchImpl ?? requireGlobalFetch(missingBlossomFetch),\n url,\n init,\n (err) =>\n new BlossomError(`blossom ${init.method} ${url} failed`, {\n cause: err instanceof Error ? err : undefined,\n }),\n );\n}\n\n/**\n * HEAD with redirect:manual. Network → undefined. AbortError propagates. Never throws on HTTP\n * status.\n */\nasync function headStatus(\n fetchImpl: ManualFetch,\n url: string,\n signal?: AbortSignal,\n): Promise<number | undefined> {\n try {\n const res = await sendManual(fetchImpl, url, { method: \"HEAD\", signal });\n return res.status;\n } catch (error) {\n if (isAbortError(error)) {\n throw error;\n }\n return undefined;\n }\n}\n\nasync function blossomRequest(\n fetchImpl: BlossomFetch | undefined,\n url: string,\n init: {\n method: string;\n headers?: Record<string, string>;\n body?: Blob | string;\n signal?: AbortSignal | undefined;\n },\n): Promise<ManualResponse> {\n const res = await blossomFetch(fetchImpl, url, init);\n if (res.ok) {\n return res;\n }\n throw blossomHttpError(init.method, url, res);\n}\n\nasync function readJson(res: ManualResponse): Promise<unknown> {\n try {\n return await res.json();\n } catch (error) {\n throw new BlossomError(\"invalid blossom JSON response\", {\n cause: error instanceof Error ? error : undefined,\n status: res.status,\n });\n }\n}\n\nfunction parseBlobDescriptor(json: unknown): BlobDescriptor {\n if (!isRecord(json)) {\n throw new BlossomError(\"invalid blob descriptor\");\n }\n const raw = json;\n if (typeof raw[\"url\"] !== \"string\" || !raw[\"url\"]) {\n throw new BlossomError(\"blob descriptor missing url\");\n }\n if (typeof raw[\"sha256\"] !== \"string\") {\n throw new BlossomError(\"blob descriptor missing sha256\");\n }\n const hash = assertHex32(raw[\"sha256\"], \"blob sha256\");\n if (typeof raw[\"size\"] !== \"number\" || !Number.isInteger(raw[\"size\"]) || raw[\"size\"] < 0) {\n throw new BlossomError(\"blob descriptor missing size\");\n }\n const desc: BlobDescriptor = { url: raw[\"url\"], sha256: hash, size: raw[\"size\"] };\n if (typeof raw[\"type\"] === \"string\") {\n desc.type = raw[\"type\"];\n }\n if (\n typeof raw[\"uploaded\"] === \"number\" &&\n Number.isInteger(raw[\"uploaded\"]) &&\n raw[\"uploaded\"] >= 0\n ) {\n desc.uploaded = raw[\"uploaded\"];\n }\n return desc;\n}\n\nfunction requireDescriptorHash(desc: BlobDescriptor, expected: string): BlobDescriptor {\n if (desc.sha256 !== expected) {\n throw new BlossomError(\"blob descriptor sha256 mismatch\");\n }\n return desc;\n}\n"],"mappings":"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"}
@@ -1,8 +1,7 @@
1
1
  //#region src/nips/nip04.d.ts
2
2
  /** Encrypt plaintext to a peer pubkey (NIP-04). */
3
- declare function encrypt(secretKey: string | Uint8Array, pubkey: string, text: string): string;
3
+ export declare function encrypt(secretKey: string | Uint8Array, pubkey: string, text: string): string;
4
4
  /** Decrypt a NIP-04 payload from a peer pubkey. */
5
- declare function decrypt(secretKey: string | Uint8Array, pubkey: string, data: string): string;
5
+ export declare function decrypt(secretKey: string | Uint8Array, pubkey: string, data: string): string;
6
6
  //#endregion
7
- export { decrypt, encrypt };
8
7
  //# sourceMappingURL=nip04.d.mts.map
@@ -1,20 +1,16 @@
1
- import { c as utf8Decoder, i as hexToBytes, l as utf8Encoder, t as assertHex32 } from "../util-6tfLQ9JF.mjs";
2
- import { t as CryptoError } from "../error-DZEs5OuZ.mjs";
1
+ import { a as hexToBytes$1, d as utf8Encoder, f as CryptoError, n as assertHex32, u as utf8Decoder } from "../util-LfQirgDk.mjs";
2
+ import { randomBytes } from "@noble/hashes/utils.js";
3
3
  import { secp256k1 } from "@noble/curves/secp256k1.js";
4
4
  import { cbc } from "@noble/ciphers/aes.js";
5
- import { randomBytes } from "@noble/hashes/utils.js";
6
5
  import { base64 } from "@scure/base";
7
6
  //#region src/nips/nip04.ts
8
- /**
9
- * NIP-04 legacy encrypted direct messages.
10
- * Prefer NIP-44 for new applications.
11
- */
7
+ /** NIP-04 legacy encrypted direct messages. Prefer NIP-44 for new applications. */
12
8
  function normalizeSharedSecret(privkey, pubkey) {
13
9
  assertHex32(pubkey, "public key");
14
- return secp256k1.getSharedSecret(privkey, hexToBytes("02" + pubkey.toLowerCase())).slice(1, 33);
10
+ return secp256k1.getSharedSecret(privkey, hexToBytes$1(`02${pubkey.toLowerCase()}`)).slice(1, 33);
15
11
  }
16
12
  function resolveSecret(secretKey) {
17
- return typeof secretKey === "string" ? hexToBytes(assertHex32(secretKey, "secret key")) : secretKey;
13
+ return typeof secretKey === "string" ? hexToBytes$1(assertHex32(secretKey, "secret key")) : secretKey;
18
14
  }
19
15
  /** Encrypt plaintext to a peer pubkey (NIP-04). */
20
16
  function encrypt(secretKey, pubkey, text) {
@@ -28,7 +24,7 @@ function encrypt(secretKey, pubkey, text) {
28
24
  function decrypt(secretKey, pubkey, data) {
29
25
  const privkey = resolveSecret(secretKey);
30
26
  const parts = data.split("?iv=");
31
- if (parts.length !== 2 || !parts[0] || !parts[1]) throw new CryptoError("invalid NIP-04 payload: missing iv");
27
+ if (parts.length !== 2 || parts[0] === void 0 || parts[0] === "" || parts[1] === void 0 || parts[1] === "") throw new CryptoError("invalid NIP-04 payload: missing iv");
32
28
  const normalizedKey = normalizeSharedSecret(privkey, pubkey);
33
29
  const iv = base64.decode(parts[1]);
34
30
  const ciphertext = base64.decode(parts[0]);
@@ -1 +1 @@
1
- {"version":3,"file":"nip04.mjs","names":[],"sources":["../../src/nips/nip04.ts"],"sourcesContent":["/**\n * NIP-04 legacy encrypted direct messages.\n * Prefer NIP-44 for new applications.\n */\nimport { cbc } from \"@noble/ciphers/aes.js\";\nimport { secp256k1 } from \"@noble/curves/secp256k1.js\";\nimport { randomBytes } from \"@noble/hashes/utils.js\";\nimport { base64 } from \"@scure/base\";\nimport { CryptoError } from \"../core/error.ts\";\nimport { assertHex32, hexToBytes, utf8Decoder, utf8Encoder } from \"../core/util.ts\";\n\nfunction normalizeSharedSecret(privkey: Uint8Array, pubkey: string): Uint8Array {\n assertHex32(pubkey, \"public key\");\n const key = secp256k1.getSharedSecret(privkey, hexToBytes(\"02\" + pubkey.toLowerCase()));\n return key.slice(1, 33);\n}\n\nfunction resolveSecret(secretKey: string | Uint8Array): Uint8Array {\n return typeof secretKey === \"string\"\n ? hexToBytes(assertHex32(secretKey, \"secret key\"))\n : secretKey;\n}\n\n/** Encrypt plaintext to a peer pubkey (NIP-04). */\nexport function encrypt(secretKey: string | Uint8Array, pubkey: string, text: string): string {\n const privkey = resolveSecret(secretKey);\n const normalizedKey = normalizeSharedSecret(privkey, pubkey);\n const iv = randomBytes(16);\n const plaintext = utf8Encoder.encode(text);\n const ciphertext = cbc(normalizedKey, iv).encrypt(plaintext);\n return `${base64.encode(ciphertext)}?iv=${base64.encode(iv)}`;\n}\n\n/** Decrypt a NIP-04 payload from a peer pubkey. */\nexport function decrypt(secretKey: string | Uint8Array, pubkey: string, data: string): string {\n const privkey = resolveSecret(secretKey);\n const parts = data.split(\"?iv=\");\n if (parts.length !== 2 || !parts[0] || !parts[1]) {\n throw new CryptoError(\"invalid NIP-04 payload: missing iv\");\n }\n const normalizedKey = normalizeSharedSecret(privkey, pubkey);\n const iv = base64.decode(parts[1]);\n const ciphertext = base64.decode(parts[0]);\n const plaintext = cbc(normalizedKey, iv).decrypt(ciphertext);\n return utf8Decoder.decode(plaintext);\n}\n"],"mappings":";;;;;;;;;;;AAWA,SAAS,sBAAsB,SAAqB,QAA4B;CAC9E,YAAY,QAAQ,YAAY;CAEhC,OADY,UAAU,gBAAgB,SAAS,WAAW,OAAO,OAAO,YAAY,CAAC,CAC5E,CAAC,CAAC,MAAM,GAAG,EAAE;AACxB;AAEA,SAAS,cAAc,WAA4C;CACjE,OAAO,OAAO,cAAc,WACxB,WAAW,YAAY,WAAW,YAAY,CAAC,IAC/C;AACN;;AAGA,SAAgB,QAAQ,WAAgC,QAAgB,MAAsB;CAE5F,MAAM,gBAAgB,sBADN,cAAc,SACoB,GAAG,MAAM;CAC3D,MAAM,KAAK,YAAY,EAAE;CACzB,MAAM,YAAY,YAAY,OAAO,IAAI;CACzC,MAAM,aAAa,IAAI,eAAe,EAAE,CAAC,CAAC,QAAQ,SAAS;CAC3D,OAAO,GAAG,OAAO,OAAO,UAAU,EAAE,MAAM,OAAO,OAAO,EAAE;AAC5D;;AAGA,SAAgB,QAAQ,WAAgC,QAAgB,MAAsB;CAC5F,MAAM,UAAU,cAAc,SAAS;CACvC,MAAM,QAAQ,KAAK,MAAM,MAAM;CAC/B,IAAI,MAAM,WAAW,KAAK,CAAC,MAAM,MAAM,CAAC,MAAM,IAC5C,MAAM,IAAI,YAAY,oCAAoC;CAE5D,MAAM,gBAAgB,sBAAsB,SAAS,MAAM;CAC3D,MAAM,KAAK,OAAO,OAAO,MAAM,EAAE;CACjC,MAAM,aAAa,OAAO,OAAO,MAAM,EAAE;CACzC,MAAM,YAAY,IAAI,eAAe,EAAE,CAAC,CAAC,QAAQ,UAAU;CAC3D,OAAO,YAAY,OAAO,SAAS;AACrC"}
1
+ {"version":3,"file":"nip04.mjs","names":["hexToBytes"],"sources":["../../src/nips/nip04.ts"],"sourcesContent":["/** NIP-04 legacy encrypted direct messages. Prefer NIP-44 for new applications. */\nimport { cbc } from \"@noble/ciphers/aes.js\";\nimport { secp256k1 } from \"@noble/curves/secp256k1.js\";\nimport { randomBytes } from \"@noble/hashes/utils.js\";\nimport { base64 } from \"@scure/base\";\n\nimport { CryptoError } from \"../core/error.ts\";\nimport { assertHex32, hexToBytes, utf8Decoder, utf8Encoder } from \"../core/util.ts\";\n\nfunction normalizeSharedSecret(privkey: Uint8Array, pubkey: string): Uint8Array {\n assertHex32(pubkey, \"public key\");\n const key = secp256k1.getSharedSecret(privkey, hexToBytes(`02${pubkey.toLowerCase()}`));\n return key.slice(1, 33);\n}\n\nfunction resolveSecret(secretKey: string | Uint8Array): Uint8Array {\n return typeof secretKey === \"string\"\n ? hexToBytes(assertHex32(secretKey, \"secret key\"))\n : secretKey;\n}\n\n/** Encrypt plaintext to a peer pubkey (NIP-04). */\nexport function encrypt(secretKey: string | Uint8Array, pubkey: string, text: string): string {\n const privkey = resolveSecret(secretKey);\n const normalizedKey = normalizeSharedSecret(privkey, pubkey);\n const iv = randomBytes(16);\n const plaintext = utf8Encoder.encode(text);\n const ciphertext = cbc(normalizedKey, iv).encrypt(plaintext);\n return `${base64.encode(ciphertext)}?iv=${base64.encode(iv)}`;\n}\n\n/** Decrypt a NIP-04 payload from a peer pubkey. */\nexport function decrypt(secretKey: string | Uint8Array, pubkey: string, data: string): string {\n const privkey = resolveSecret(secretKey);\n const parts = data.split(\"?iv=\");\n if (\n parts.length !== 2 ||\n parts[0] === undefined ||\n parts[0] === \"\" ||\n parts[1] === undefined ||\n parts[1] === \"\"\n ) {\n throw new CryptoError(\"invalid NIP-04 payload: missing iv\");\n }\n const normalizedKey = normalizeSharedSecret(privkey, pubkey);\n const iv = base64.decode(parts[1]);\n const ciphertext = base64.decode(parts[0]);\n const plaintext = cbc(normalizedKey, iv).decrypt(ciphertext);\n return utf8Decoder.decode(plaintext);\n}\n"],"mappings":";;;;;;;AASA,SAAS,sBAAsB,SAAqB,QAA4B;CAC9E,YAAY,QAAQ,YAAY;CAEhC,OADY,UAAU,gBAAgB,SAASA,aAAW,KAAK,OAAO,YAAY,GAAG,CAC5E,CAAC,CAAC,MAAM,GAAG,EAAE;AACxB;AAEA,SAAS,cAAc,WAA4C;CACjE,OAAO,OAAO,cAAc,WACxBA,aAAW,YAAY,WAAW,YAAY,CAAC,IAC/C;AACN;;AAGA,SAAgB,QAAQ,WAAgC,QAAgB,MAAsB;CAE5F,MAAM,gBAAgB,sBADN,cAAc,SACoB,GAAG,MAAM;CAC3D,MAAM,KAAK,YAAY,EAAE;CACzB,MAAM,YAAY,YAAY,OAAO,IAAI;CACzC,MAAM,aAAa,IAAI,eAAe,EAAE,CAAC,CAAC,QAAQ,SAAS;CAC3D,OAAO,GAAG,OAAO,OAAO,UAAU,EAAE,MAAM,OAAO,OAAO,EAAE;AAC5D;;AAGA,SAAgB,QAAQ,WAAgC,QAAgB,MAAsB;CAC5F,MAAM,UAAU,cAAc,SAAS;CACvC,MAAM,QAAQ,KAAK,MAAM,MAAM;CAC/B,IACE,MAAM,WAAW,KACjB,MAAM,OAAO,KAAA,KACb,MAAM,OAAO,MACb,MAAM,OAAO,KAAA,KACb,MAAM,OAAO,IAEb,MAAM,IAAI,YAAY,oCAAoC;CAE5D,MAAM,gBAAgB,sBAAsB,SAAS,MAAM;CAC3D,MAAM,KAAK,OAAO,OAAO,MAAM,EAAE;CACjC,MAAM,aAAa,OAAO,OAAO,MAAM,EAAE;CACzC,MAAM,YAAY,IAAI,eAAe,EAAE,CAAC,CAAC,QAAQ,UAAU;CAC3D,OAAO,YAAY,OAAO,SAAS;AACrC"}