@qntx/nostr 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abort-Ba8tDrOI.mjs +38 -0
- package/dist/abort-Ba8tDrOI.mjs.map +1 -0
- package/dist/{builder-BphJa-Y-.d.mts → builder-BWn55Paf.d.mts} +20 -14
- package/dist/{builder-q0VsvnYE.mjs → builder-CyrGKRVP.mjs} +32 -16
- package/dist/builder-CyrGKRVP.mjs.map +1 -0
- package/dist/{client-Bd8B1mYm.mjs → client-BWsX9Cv1.mjs} +354 -195
- package/dist/client-BWsX9Cv1.mjs.map +1 -0
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -1
- package/dist/core.d.mts +8 -8
- package/dist/core.mjs +8 -9
- package/dist/error-BW7re1UN.d.mts +9 -0
- package/dist/error-B__OMBRY.mjs +14 -0
- package/dist/error-B__OMBRY.mjs.map +1 -0
- package/dist/{error-WEFS-R-4.d.mts → error-CR8Urc84.d.mts} +20 -8
- package/dist/{error-CwBxle6N.mjs → error-DrEIM7FO.mjs} +5 -3
- package/dist/error-DrEIM7FO.mjs.map +1 -0
- package/dist/{event-CzoQEFMF.mjs → event-D8Nfoh4K.mjs} +40 -32
- package/dist/event-D8Nfoh4K.mjs.map +1 -0
- package/dist/{event-B5D2jxt_.d.mts → event-fU3Y01PR.d.mts} +23 -20
- package/dist/{filter-BxwjEkZp.mjs → filter-BQl3GlHt.mjs} +42 -35
- package/dist/filter-BQl3GlHt.mjs.map +1 -0
- package/dist/filter-C0IMByE3.d.mts +37 -0
- package/dist/{gossip-C5RVB1UB.d.mts → gossip-BRQwIM_q.d.mts} +25 -23
- package/dist/{gossip-3xkRRCm_.mjs → gossip-oloDMWEj.mjs} +52 -34
- package/dist/gossip-oloDMWEj.mjs.map +1 -0
- package/dist/gossip.d.mts +1 -1
- package/dist/gossip.mjs +1 -1
- package/dist/{http-6vevbBlu.d.mts → http-C4HwIu4B.d.mts} +7 -7
- package/dist/{http-D2rhokZo.mjs → http-CuazNRIP.mjs} +10 -6
- package/dist/http-CuazNRIP.mjs.map +1 -0
- package/dist/index-B425CAcQ.d.mts +328 -0
- package/dist/index-BVyfUBuG.d.mts +181 -0
- package/dist/{index-BYMnhRIC.d.mts → index-Ba7bJ7DD.d.mts} +68 -50
- package/dist/{index-DaRyjo7O.d.mts → index-Boe8FshM.d.mts} +13 -7
- package/dist/index-CUIvFQdE.d.mts +73 -0
- package/dist/index-JdaS_uce.d.mts +207 -0
- package/dist/index.d.mts +22 -20
- package/dist/index.mjs +19 -18
- package/dist/{key-Dn-aX4x4.mjs → key-Bbtys6o7.mjs} +22 -14
- package/dist/key-Bbtys6o7.mjs.map +1 -0
- package/dist/{key-BiGxGLhg.d.mts → key-BlsYhN1V.d.mts} +9 -6
- package/dist/{kind-BLl4cLOc.mjs → kind-Cxwhhkx8.mjs} +2 -1
- package/dist/kind-Cxwhhkx8.mjs.map +1 -0
- package/dist/{loaders-BNQszDAn.mjs → loaders-o6YXB_Ri.mjs} +329 -305
- package/dist/loaders-o6YXB_Ri.mjs.map +1 -0
- package/dist/loaders.d.mts +3 -3
- package/dist/loaders.mjs +2 -2
- package/dist/memory-C2EVx0_O.mjs +46 -0
- package/dist/memory-C2EVx0_O.mjs.map +1 -0
- package/dist/{memory-BCmhZPek.mjs → memory-index-Do1mz0WF.mjs} +212 -82
- package/dist/memory-index-Do1mz0WF.mjs.map +1 -0
- package/dist/{message-C_Uj-Y-c.mjs → message-DcY6Fi9o.mjs} +38 -24
- package/dist/message-DcY6Fi9o.mjs.map +1 -0
- package/dist/{message-DJC-Jz6T.d.mts → message-iGPIqnn0.d.mts} +15 -8
- package/dist/{nip17-BWq-Kfq5.d.mts → nip17-DNEkz8rM.d.mts} +18 -17
- package/dist/{nip19-D7905oIv.d.mts → nip19-BevCJZsp.d.mts} +23 -3
- package/dist/{nip46-C_DoxiS_.d.mts → nip46-DDV4dC56.d.mts} +13 -12
- package/dist/{nip59-DljW_sYh.d.mts → nip59-zQgdLcEr.d.mts} +28 -25
- package/dist/{nip65-Di5hZRuV.d.mts → nip65-DBfh2smt.d.mts} +8 -3
- package/dist/{nip77-ByxlgoQO.d.mts → nip77-DH4RhS5r.d.mts} +9 -8
- package/dist/nips/blossom.d.mts +37 -37
- package/dist/nips/blossom.mjs +67 -52
- package/dist/nips/blossom.mjs.map +1 -1
- package/dist/nips/nip04.d.mts +2 -3
- package/dist/nips/nip04.mjs +6 -10
- package/dist/nips/nip04.mjs.map +1 -1
- package/dist/nips/nip05.d.mts +37 -43
- package/dist/nips/nip05.mjs +57 -57
- package/dist/nips/nip05.mjs.map +1 -1
- package/dist/nips/nip10.d.mts +18 -19
- package/dist/nips/nip10.mjs +76 -61
- package/dist/nips/nip10.mjs.map +1 -1
- package/dist/nips/nip11.d.mts +9 -8
- package/dist/nips/nip11.mjs +14 -11
- package/dist/nips/nip11.mjs.map +1 -1
- package/dist/nips/nip13.d.mts +9 -9
- package/dist/nips/nip13.mjs +15 -17
- package/dist/nips/nip13.mjs.map +1 -1
- package/dist/nips/nip17.d.mts +1 -1
- package/dist/nips/nip17.mjs +17 -12
- package/dist/nips/nip17.mjs.map +1 -1
- package/dist/nips/nip19.d.mts +1 -1
- package/dist/nips/nip19.mjs +52 -29
- package/dist/nips/nip19.mjs.map +1 -1
- package/dist/nips/nip21.d.mts +5 -6
- package/dist/nips/nip21.mjs +8 -7
- package/dist/nips/nip21.mjs.map +1 -1
- package/dist/nips/nip27.d.mts +7 -8
- package/dist/nips/nip27.mjs +26 -25
- package/dist/nips/nip27.mjs.map +1 -1
- package/dist/nips/nip42.d.mts +5 -6
- package/dist/nips/nip42.mjs +3 -3
- package/dist/nips/nip42.mjs.map +1 -1
- package/dist/nips/nip44.d.mts +24 -7
- package/dist/nips/nip44.mjs +25 -15
- package/dist/nips/nip44.mjs.map +1 -1
- package/dist/nips/nip46.d.mts +1 -1
- package/dist/nips/nip46.mjs +52 -49
- package/dist/nips/nip46.mjs.map +1 -1
- package/dist/nips/nip49.d.mts +8 -7
- package/dist/nips/nip49.mjs +16 -12
- package/dist/nips/nip49.mjs.map +1 -1
- package/dist/nips/nip51.d.mts +25 -27
- package/dist/nips/nip51.mjs +47 -36
- package/dist/nips/nip51.mjs.map +1 -1
- package/dist/nips/nip57.d.mts +12 -13
- package/dist/nips/nip57.mjs +35 -38
- package/dist/nips/nip57.mjs.map +1 -1
- package/dist/nips/nip59.d.mts +1 -1
- package/dist/nips/nip59.mjs +14 -18
- package/dist/nips/nip59.mjs.map +1 -1
- package/dist/nips/nip65.d.mts +1 -1
- package/dist/nips/nip65.mjs +11 -8
- package/dist/nips/nip65.mjs.map +1 -1
- package/dist/nips/nip77.d.mts +1 -1
- package/dist/nips/nip77.mjs +56 -34
- package/dist/nips/nip77.mjs.map +1 -1
- package/dist/nips/nip96.d.mts +21 -12
- package/dist/nips/nip96.mjs +59 -36
- package/dist/nips/nip96.mjs.map +1 -1
- package/dist/nips/nip98.d.mts +12 -12
- package/dist/nips/nip98.mjs +18 -19
- package/dist/nips/nip98.mjs.map +1 -1
- package/dist/{nostr_crypto_wasm_bg-hi5aXi5P.mjs → nostr_crypto_wasm_bg-Czrdty7Q.mjs} +1 -1
- package/dist/nostr_crypto_wasm_bg.wasm +0 -0
- package/dist/{pool-C_j1OmBB.mjs → pool-XmAGaufS.mjs} +611 -487
- package/dist/pool-XmAGaufS.mjs.map +1 -0
- package/dist/pool-tEr7c5bG.d.mts +302 -0
- package/dist/reactive-DDlThDSZ.mjs +324 -0
- package/dist/reactive-DDlThDSZ.mjs.map +1 -0
- package/dist/reactive-DFpqtIf_.d.mts +66 -0
- package/dist/relay.d.mts +5 -4
- package/dist/relay.mjs +2 -2
- package/dist/report-CHld4uWj.mjs +32 -0
- package/dist/report-CHld4uWj.mjs.map +1 -0
- package/dist/{signer-CZ6K6FUc.mjs → signer-CKR9EHce.mjs} +107 -72
- package/dist/signer-CKR9EHce.mjs.map +1 -0
- package/dist/signer.d.mts +3 -3
- package/dist/signer.mjs +3 -2
- package/dist/storage-xXxLGCVb.mjs +1376 -0
- package/dist/storage-xXxLGCVb.mjs.map +1 -0
- package/dist/storage.d.mts +4 -4
- package/dist/storage.mjs +5 -4
- package/dist/store.d.mts +2 -0
- package/dist/store.mjs +2 -0
- package/dist/testing.d.mts +125 -0
- package/dist/testing.mjs +957 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types-2pCUprRU.d.mts +47 -0
- package/dist/types-CRQEk0Oi.d.mts +19 -0
- package/dist/util-LfQirgDk.mjs +113 -0
- package/dist/util-LfQirgDk.mjs.map +1 -0
- package/dist/wasm.d.mts +4 -4
- package/dist/wasm.mjs +55 -56
- package/dist/wasm.mjs.map +1 -1
- package/dist/websocket-CmeqvAkp.d.mts +27 -0
- package/package.json +31 -9
- package/dist/builder-q0VsvnYE.mjs.map +0 -1
- package/dist/client-Bd8B1mYm.mjs.map +0 -1
- package/dist/error-B49rbDzT.d.mts +0 -7
- package/dist/error-CwBxle6N.mjs.map +0 -1
- package/dist/error-DZEs5OuZ.mjs +0 -24
- package/dist/error-DZEs5OuZ.mjs.map +0 -1
- package/dist/event-CzoQEFMF.mjs.map +0 -1
- package/dist/filter-BxwjEkZp.mjs.map +0 -1
- package/dist/filter-DUEKP3q1.d.mts +0 -40
- package/dist/gossip-3xkRRCm_.mjs.map +0 -1
- package/dist/http-D2rhokZo.mjs.map +0 -1
- package/dist/index-C0C4e-Gn.d.mts +0 -55
- package/dist/index-DgOugrpF.d.mts +0 -275
- package/dist/index-EwL6fHLo.d.mts +0 -267
- package/dist/index-k2YhCgsh.d.mts +0 -57
- package/dist/key-Dn-aX4x4.mjs.map +0 -1
- package/dist/kind-BLl4cLOc.mjs.map +0 -1
- package/dist/loaders-BNQszDAn.mjs.map +0 -1
- package/dist/memory-BCmhZPek.mjs.map +0 -1
- package/dist/message-C_Uj-Y-c.mjs.map +0 -1
- package/dist/pool-C_j1OmBB.mjs.map +0 -1
- package/dist/pool-DhRi7l-b.d.mts +0 -297
- package/dist/signer-CZ6K6FUc.mjs.map +0 -1
- package/dist/storage-DOy0iLRE.mjs +0 -870
- package/dist/storage-DOy0iLRE.mjs.map +0 -1
- package/dist/types-BzyADqnw.d.mts +0 -19
- package/dist/types-Dn24TPFs.d.mts +0 -43
- package/dist/util-6tfLQ9JF.mjs +0 -73
- package/dist/util-6tfLQ9JF.mjs.map +0 -1
package/dist/nips/blossom.d.mts
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
import { n as EventTemplate, t as Event } from "../event-
|
|
2
|
-
import { t as EventBuilder } from "../builder-
|
|
3
|
-
import { a as NostrError } from "../error-
|
|
4
|
-
import { t as ManualFetch } from "../http-
|
|
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
|
-
|
|
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).
|
|
25
|
-
declare function getHashFromURL(url: string | URL): string |
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
/**
|
|
66
|
-
|
|
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
|
-
*
|
|
78
|
-
*
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
package/dist/nips/blossom.mjs
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { t as
|
|
4
|
-
import { t as
|
|
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
|
-
*
|
|
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).
|
|
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
|
|
39
|
+
return;
|
|
40
40
|
}
|
|
41
41
|
const matches = path.match(/[0-9a-f]{64}/gi);
|
|
42
|
-
if (
|
|
43
|
-
return matches
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
175
|
-
*
|
|
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 (
|
|
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 (
|
|
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 (
|
|
200
|
-
if (isAbortError(
|
|
201
|
-
last =
|
|
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 (
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
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 (
|
|
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)
|
|
263
|
+
({pathname} = new URL(url));
|
|
253
264
|
} catch {
|
|
254
265
|
return "";
|
|
255
266
|
}
|
|
256
267
|
const i = pathname.toLowerCase().lastIndexOf(hash);
|
|
257
|
-
if (i
|
|
258
|
-
return pathname.slice(i + 64)
|
|
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
|
-
|
|
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
|
-
/**
|
|
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 (
|
|
283
|
-
if (isAbortError(
|
|
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 (
|
|
310
|
+
} catch (error) {
|
|
296
311
|
throw new BlossomError("invalid blossom JSON response", {
|
|
297
|
-
cause:
|
|
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 (!
|
|
318
|
+
if (!isRecord(json)) throw new BlossomError("invalid blob descriptor");
|
|
304
319
|
const raw = json;
|
|
305
|
-
if (typeof raw
|
|
306
|
-
if (typeof raw
|
|
307
|
-
const hash = assertHex32(raw
|
|
308
|
-
if (typeof raw
|
|
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
|
|
325
|
+
url: raw["url"],
|
|
311
326
|
sha256: hash,
|
|
312
|
-
size: raw
|
|
327
|
+
size: raw["size"]
|
|
313
328
|
};
|
|
314
|
-
if (typeof raw
|
|
315
|
-
if (typeof raw
|
|
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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"}
|
package/dist/nips/nip04.d.mts
CHANGED
|
@@ -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
|
package/dist/nips/nip04.mjs
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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(
|
|
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 ||
|
|
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]);
|
package/dist/nips/nip04.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nip04.mjs","names":[],"sources":["../../src/nips/nip04.ts"],"sourcesContent":["
|
|
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"}
|