@qntx/nostr 0.6.0 → 0.8.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 → abort-DBq2STPj.mjs} +13 -6
- package/dist/abort-DBq2STPj.mjs.map +1 -0
- package/dist/{builder-sSflu6nB.d.mts → builder-CTwkB2zM.d.mts} +14 -15
- package/dist/{builder-9tjI6WQ3.mjs → builder-aNU2gGFP.mjs} +11 -17
- package/dist/builder-aNU2gGFP.mjs.map +1 -0
- package/dist/{client-CadO5fAX.mjs → client-Cj2lUaVh.mjs} +141 -353
- package/dist/client-Cj2lUaVh.mjs.map +1 -0
- package/dist/client.d.mts +2 -2
- package/dist/client.mjs +2 -2
- package/dist/collections-DtwMc1NU.mjs +33 -0
- package/dist/collections-DtwMc1NU.mjs.map +1 -0
- package/dist/core.d.mts +9 -8
- package/dist/core.mjs +7 -7
- package/dist/emitter-DkCY6NkU.mjs +32 -0
- package/dist/emitter-DkCY6NkU.mjs.map +1 -0
- package/dist/{error-BW7re1UN.d.mts → error-BD_0QFKT.d.mts} +2 -2
- package/dist/{error-DrEIM7FO.mjs → error-BILMe3Gr.mjs} +2 -2
- package/dist/{error-DrEIM7FO.mjs.map → error-BILMe3Gr.mjs.map} +1 -1
- package/dist/{error-CR8Urc84.d.mts → error-BquXDmIN.d.mts} +4 -4
- package/dist/{error-B__OMBRY.mjs → error-DlhGW4mJ.mjs} +2 -2
- package/dist/{error-B__OMBRY.mjs.map → error-DlhGW4mJ.mjs.map} +1 -1
- package/dist/{event-D8Nfoh4K.mjs → event-CHf47dPM.mjs} +16 -10
- package/dist/event-CHf47dPM.mjs.map +1 -0
- package/dist/{event-fU3Y01PR.d.mts → event-DJeb_DIL.d.mts} +4 -2
- package/dist/{filter-C0IMByE3.d.mts → filter-jI_9htF7.d.mts} +2 -2
- package/dist/{gossip-oloDMWEj.mjs → gossip-DFxIBlsr.mjs} +23 -33
- package/dist/gossip-DFxIBlsr.mjs.map +1 -0
- package/dist/{gossip-DpIvkBuj.d.mts → gossip-OOeeFzG5.d.mts} +37 -28
- package/dist/gossip.d.mts +1 -1
- package/dist/gossip.mjs +1 -1
- package/dist/{index-D1bqFHKB.d.mts → index-BxCBX-8R.d.mts} +18 -18
- package/dist/{index-lvXeER5t.d.mts → index-CPVEyzNX.d.mts} +32 -50
- package/dist/{index-MxC28S4P.d.mts → index-C_w2WfVz.d.mts} +27 -29
- package/dist/{index-CNl2q67H.d.mts → index-Cl_1XdYP.d.mts} +111 -180
- package/dist/{index-CvkvqB16.d.mts → index-DU9tRFTZ.d.mts} +7 -20
- package/dist/{index-q4MDce_r.d.mts → index-DyKwQnbt.d.mts} +6 -8
- package/dist/index.d.mts +23 -21
- package/dist/index.mjs +21 -19
- package/dist/{instance-DQE8BoWt.mjs → instance-C62w3cEt.mjs} +26 -27
- package/dist/instance-C62w3cEt.mjs.map +1 -0
- package/dist/{instance-CIca-ynn.d.mts → instance-DjsL8h84.d.mts} +5 -5
- package/dist/{key-Cv4_pN3x.d.mts → key-D_5OgGT7.d.mts} +11 -7
- package/dist/{key-Bps-LqX7.mjs → key-DnVpchl6.mjs} +13 -9
- package/dist/key-DnVpchl6.mjs.map +1 -0
- package/dist/{loaders-o6YXB_Ri.mjs → loaders-BmPkQfzI.mjs} +73 -82
- package/dist/loaders-BmPkQfzI.mjs.map +1 -0
- package/dist/loaders.d.mts +3 -3
- package/dist/loaders.mjs +2 -2
- package/dist/{memory-C2EVx0_O.mjs → memory-CdJ07kub.mjs} +2 -2
- package/dist/memory-CdJ07kub.mjs.map +1 -0
- package/dist/{memory-index-Do1mz0WF.mjs → memory-index-Cbc83vOZ.mjs} +30 -247
- package/dist/memory-index-Cbc83vOZ.mjs.map +1 -0
- package/dist/message-oNr7t0SJ.mjs +194 -0
- package/dist/message-oNr7t0SJ.mjs.map +1 -0
- package/dist/{message-iGPIqnn0.d.mts → message-tclYCVNc.d.mts} +6 -6
- package/dist/{nip17-CKIufV98.d.mts → nip17-B5WgW4C4.d.mts} +6 -7
- package/dist/{nip19-BevCJZsp.d.mts → nip19-DpPN17bY.d.mts} +25 -31
- package/dist/{nip46-DDV4dC56.d.mts → nip46-BHaxC9TZ.d.mts} +19 -21
- package/dist/nip51-CR0Q8eym.d.mts +87 -0
- package/dist/{nip59-zQgdLcEr.d.mts → nip59-C-RWFQIv.d.mts} +11 -5
- package/dist/nip65-D1fbmhOg.d.mts +23 -0
- package/dist/{nip77-DH4RhS5r.d.mts → nip77-DzUwUxHq.d.mts} +8 -9
- package/dist/nips/blossom.d.mts +17 -13
- package/dist/nips/blossom.mjs +30 -9
- package/dist/nips/blossom.mjs.map +1 -1
- package/dist/nips/nip04.mjs +17 -7
- package/dist/nips/nip04.mjs.map +1 -1
- package/dist/nips/nip05.d.mts +9 -10
- package/dist/nips/nip05.mjs +1 -4
- package/dist/nips/nip05.mjs.map +1 -1
- package/dist/nips/nip10.d.mts +8 -8
- package/dist/nips/nip10.mjs +34 -32
- package/dist/nips/nip10.mjs.map +1 -1
- package/dist/nips/nip11.d.mts +28 -28
- package/dist/nips/nip11.mjs +4 -4
- package/dist/nips/nip11.mjs.map +1 -1
- package/dist/nips/nip13.d.mts +7 -7
- package/dist/nips/nip13.mjs +32 -10
- package/dist/nips/nip13.mjs.map +1 -1
- package/dist/nips/nip17.d.mts +1 -1
- package/dist/nips/nip17.mjs +16 -34
- package/dist/nips/nip17.mjs.map +1 -1
- package/dist/nips/nip19.d.mts +2 -2
- package/dist/nips/nip19.mjs +16 -28
- package/dist/nips/nip19.mjs.map +1 -1
- package/dist/nips/nip21.d.mts +9 -4
- package/dist/nips/nip21.mjs +15 -5
- package/dist/nips/nip21.mjs.map +1 -1
- package/dist/nips/nip27.d.mts +22 -22
- package/dist/nips/nip27.mjs +2 -2
- package/dist/nips/nip27.mjs.map +1 -1
- package/dist/nips/nip42.d.mts +1 -1
- package/dist/nips/nip42.mjs +2 -1
- package/dist/nips/nip42.mjs.map +1 -1
- package/dist/nips/nip44.mjs +2 -2
- package/dist/nips/nip44.mjs.map +1 -1
- package/dist/nips/nip46.d.mts +2 -2
- package/dist/nips/nip46.mjs +15 -19
- package/dist/nips/nip46.mjs.map +1 -1
- package/dist/nips/nip49.d.mts +9 -9
- package/dist/nips/nip49.mjs +4 -4
- package/dist/nips/nip49.mjs.map +1 -1
- package/dist/nips/nip51.d.mts +2 -86
- package/dist/nips/nip51.mjs +7 -23
- package/dist/nips/nip51.mjs.map +1 -1
- package/dist/nips/nip57.d.mts +25 -23
- package/dist/nips/nip57.mjs +12 -17
- package/dist/nips/nip57.mjs.map +1 -1
- package/dist/nips/nip59.d.mts +1 -1
- package/dist/nips/nip59.mjs +27 -19
- package/dist/nips/nip59.mjs.map +1 -1
- package/dist/nips/nip65.d.mts +2 -2
- package/dist/nips/nip65.mjs +14 -37
- package/dist/nips/nip65.mjs.map +1 -1
- package/dist/nips/nip77.d.mts +1 -1
- package/dist/nips/nip77.mjs +33 -32
- package/dist/nips/nip77.mjs.map +1 -1
- package/dist/nips/nip98.d.mts +5 -5
- package/dist/nips/nip98.mjs +8 -8
- package/dist/nips/nip98.mjs.map +1 -1
- package/dist/{pool-DBIYMtwy.d.mts → pool-CqQvUnsq.d.mts} +132 -108
- package/dist/pool-vlw74C4t.mjs +606 -0
- package/dist/pool-vlw74C4t.mjs.map +1 -0
- package/dist/put-w13ci3sC.mjs +187 -0
- package/dist/put-w13ci3sC.mjs.map +1 -0
- package/dist/{reactive-BfhPp9a3.mjs → reactive-Tz0otDIP.mjs} +125 -88
- package/dist/reactive-Tz0otDIP.mjs.map +1 -0
- package/dist/{reactive-7aqeip4A.d.mts → reactive-gGqUrzW8.d.mts} +20 -26
- package/dist/relay-quCs7MHD.mjs +1811 -0
- package/dist/relay-quCs7MHD.mjs.map +1 -0
- package/dist/relay.d.mts +4 -4
- package/dist/relay.mjs +3 -2
- package/dist/serial-Bzpw-xz3.mjs +21 -0
- package/dist/serial-Bzpw-xz3.mjs.map +1 -0
- package/dist/{signer-CYQutDzD.mjs → signer-poTGe6Er.mjs} +12 -14
- package/dist/signer-poTGe6Er.mjs.map +1 -0
- package/dist/signer.d.mts +2 -2
- package/dist/signer.mjs +2 -2
- package/dist/{storage-Dky3wuoH.mjs → storage-B5IXTVuS.mjs} +28 -51
- package/dist/storage-B5IXTVuS.mjs.map +1 -0
- package/dist/storage.d.mts +3 -3
- package/dist/storage.mjs +4 -4
- package/dist/store.d.mts +2 -2
- package/dist/store.mjs +1 -1
- package/dist/testing.d.mts +19 -19
- package/dist/testing.mjs +9 -9
- package/dist/testing.mjs.map +1 -1
- package/dist/{types-2pCUprRU.d.mts → types-BacwnDtU.d.mts} +10 -10
- package/dist/{types-CRQEk0Oi.d.mts → types-DNmiscfd.d.mts} +2 -2
- package/dist/{util-LfQirgDk.mjs → util-DMF9AIO0.mjs} +33 -7
- package/dist/util-DMF9AIO0.mjs.map +1 -0
- package/dist/{verifier-t5WoWbtY.mjs → verifier-B51Wkt5H.mjs} +3 -3
- package/dist/verifier-B51Wkt5H.mjs.map +1 -0
- package/dist/verifier-CgTFpgsC.d.mts +22 -0
- package/dist/wasm.browser.d.mts +3 -3
- package/dist/wasm.browser.mjs +3 -3
- package/dist/wasm.d.mts +3 -3
- package/dist/wasm.mjs +3 -3
- package/dist/wasm.mjs.map +1 -1
- package/package.json +5 -9
- package/dist/abort-Ba8tDrOI.mjs.map +0 -1
- package/dist/builder-9tjI6WQ3.mjs.map +0 -1
- package/dist/client-CadO5fAX.mjs.map +0 -1
- package/dist/event-D8Nfoh4K.mjs.map +0 -1
- package/dist/gossip-oloDMWEj.mjs.map +0 -1
- package/dist/instance-DQE8BoWt.mjs.map +0 -1
- package/dist/key-Bps-LqX7.mjs.map +0 -1
- package/dist/loaders-o6YXB_Ri.mjs.map +0 -1
- package/dist/memory-C2EVx0_O.mjs.map +0 -1
- package/dist/memory-index-Do1mz0WF.mjs.map +0 -1
- package/dist/message-DcY6Fi9o.mjs +0 -193
- package/dist/message-DcY6Fi9o.mjs.map +0 -1
- package/dist/nip65-CjgmdpPB.d.mts +0 -26
- package/dist/nips/nip96.d.mts +0 -40
- package/dist/nips/nip96.mjs +0 -118
- package/dist/nips/nip96.mjs.map +0 -1
- package/dist/pool-As20OGxB.mjs +0 -2166
- package/dist/pool-As20OGxB.mjs.map +0 -1
- package/dist/reactive-BfhPp9a3.mjs.map +0 -1
- package/dist/signer-CYQutDzD.mjs.map +0 -1
- package/dist/storage-Dky3wuoH.mjs.map +0 -1
- package/dist/util-LfQirgDk.mjs.map +0 -1
- package/dist/verifier-t5WoWbtY.mjs.map +0 -1
package/dist/nips/blossom.d.mts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { n as EventTemplate, t as Event } from "../event-
|
|
2
|
-
import { t as EventBuilder } from "../builder-
|
|
3
|
-
import { a as NostrError } from "../error-
|
|
1
|
+
import { n as EventTemplate, t as Event } from "../event-DJeb_DIL.mjs";
|
|
2
|
+
import { t as EventBuilder } from "../builder-CTwkB2zM.mjs";
|
|
3
|
+
import { a as NostrError } from "../error-BquXDmIN.mjs";
|
|
4
4
|
import { t as ManualFetch } from "../http-C4HwIu4B.mjs";
|
|
5
5
|
//#region src/nips/blossom.d.ts
|
|
6
6
|
export type BlobDescriptor = {
|
|
7
|
-
url: string;
|
|
8
|
-
sha256: string;
|
|
9
|
-
size: number;
|
|
10
|
-
type?: string;
|
|
11
|
-
uploaded?: number;
|
|
7
|
+
readonly url: string;
|
|
8
|
+
readonly sha256: string;
|
|
9
|
+
readonly size: number;
|
|
10
|
+
readonly type?: string | undefined;
|
|
11
|
+
readonly uploaded?: number | undefined;
|
|
12
12
|
};
|
|
13
13
|
export type BlossomFetch = ManualFetch;
|
|
14
14
|
/** Signs a kind 24242 auth template (draft; pubkey is filled by the signer). */
|
|
@@ -30,17 +30,20 @@ export declare function verifyBlob(data: Blob | ArrayBuffer | Uint8Array, sha256
|
|
|
30
30
|
/** `Nostr ` + base64url(utf8 JSON), padding stripped. NIP-98 uses standard base64; mixing them is a 401. */
|
|
31
31
|
export declare function encodeAuthorizationHeader(event: Event): string;
|
|
32
32
|
export declare function createAuthTemplate(verb: BlossomAuthVerb, opts?: {
|
|
33
|
-
sha256?: string;
|
|
34
|
-
expiration?: number;
|
|
35
|
-
message?: string;
|
|
33
|
+
sha256?: string | undefined;
|
|
34
|
+
expiration?: number | undefined;
|
|
35
|
+
message?: string | undefined;
|
|
36
|
+
servers?: ReadonlyArray<string> | undefined;
|
|
36
37
|
}): EventTemplate;
|
|
37
38
|
export declare function createUploadAuth(sign: BlossomSign, file: Blob, opts?: {
|
|
38
|
-
message?: string;
|
|
39
|
-
expiration?: number;
|
|
39
|
+
message?: string | undefined;
|
|
40
|
+
expiration?: number | undefined;
|
|
41
|
+
servers?: ReadonlyArray<string> | undefined;
|
|
40
42
|
}): Promise<Event>;
|
|
41
43
|
export declare function upload(server: string, file: Blob, auth: Event, opts?: {
|
|
42
44
|
fetch?: BlossomFetch;
|
|
43
45
|
signal?: AbortSignal;
|
|
46
|
+
sha256?: string | undefined;
|
|
44
47
|
}): Promise<BlobDescriptor>;
|
|
45
48
|
export declare function listBlobs(server: string, pubkey: string, auth: Event | undefined, opts?: {
|
|
46
49
|
fetch?: BlossomFetch;
|
|
@@ -59,6 +62,7 @@ export declare function mirrorBlob(server: string, blob: BlobDescriptor, opts: {
|
|
|
59
62
|
export declare function checkUpload(server: string, file: Blob, auth: Event, opts?: {
|
|
60
63
|
fetch?: BlossomFetch;
|
|
61
64
|
signal?: AbortSignal;
|
|
65
|
+
sha256?: string | undefined;
|
|
62
66
|
}): Promise<void>;
|
|
63
67
|
/**
|
|
64
68
|
* Strict HEAD probe. 2xx → true. 404 → false. Other HTTP (including 405) / network → throw
|
package/dist/nips/blossom.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { c as isRecord, d as
|
|
1
|
+
import { c as isRecord, d as nowSeconds, h as EventValidationError, i as bytesToHex, n as assertHex32, p as utf8Encoder, v as NostrError } from "../util-DMF9AIO0.mjs";
|
|
2
2
|
import { t as Kind } from "../kind-Cxwhhkx8.mjs";
|
|
3
|
-
import { t as EventBuilder } from "../builder-
|
|
3
|
+
import { t as EventBuilder } from "../builder-aNU2gGFP.mjs";
|
|
4
4
|
import { n as requireGlobalFetch, r as sendManual, t as fetchManual } from "../http-CuazNRIP.mjs";
|
|
5
5
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
6
6
|
import { base64urlnopad } from "@scure/base";
|
|
@@ -66,15 +66,36 @@ const AUTH_VERB_MESSAGES = {
|
|
|
66
66
|
get: "Get Blob",
|
|
67
67
|
media: "Upload Media"
|
|
68
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* Hostname for a BUD-11 `server` tag: accepts a full URL or a bare domain; lowercased, undefined
|
|
71
|
+
* when unparseable.
|
|
72
|
+
*/
|
|
73
|
+
function serverHostname(raw) {
|
|
74
|
+
try {
|
|
75
|
+
const url = new URL(raw.includes("://") ? raw : `https://${raw}`);
|
|
76
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return;
|
|
77
|
+
const hostname = url.hostname.toLowerCase();
|
|
78
|
+
return hostname === "" ? void 0 : hostname;
|
|
79
|
+
} catch {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
69
83
|
function createAuthTemplate(verb, opts) {
|
|
70
84
|
if (!AUTH_VERBS.has(verb)) throw new BlossomError(`invalid blossom auth verb: ${verb}`);
|
|
71
85
|
if (opts?.message !== void 0 && opts.message.trim() === "") throw new BlossomError("blossom auth message must not be empty");
|
|
72
|
-
const expiration = opts?.expiration ??
|
|
86
|
+
const expiration = opts?.expiration ?? nowSeconds() + AUTH_EXPIRATION_SECS;
|
|
73
87
|
const tags = [["t", verb], ["expiration", String(expiration)]];
|
|
74
88
|
if (opts?.sha256 !== void 0) tags.push(["x", assertHex32(opts.sha256, "blob sha256")]);
|
|
89
|
+
const seenServers = /* @__PURE__ */ new Set();
|
|
90
|
+
for (const server of opts?.servers ?? []) {
|
|
91
|
+
const hostname = serverHostname(server);
|
|
92
|
+
if (hostname === void 0 || seenServers.has(hostname)) continue;
|
|
93
|
+
seenServers.add(hostname);
|
|
94
|
+
tags.push(["server", hostname]);
|
|
95
|
+
}
|
|
75
96
|
return {
|
|
76
97
|
kind: Kind.BlobsAuth,
|
|
77
|
-
created_at:
|
|
98
|
+
created_at: nowSeconds(),
|
|
78
99
|
tags,
|
|
79
100
|
content: opts?.message ?? AUTH_VERB_MESSAGES[verb]
|
|
80
101
|
};
|
|
@@ -86,7 +107,7 @@ async function createUploadAuth(sign, file, opts) {
|
|
|
86
107
|
}));
|
|
87
108
|
}
|
|
88
109
|
async function upload(server, file, auth, opts) {
|
|
89
|
-
const hash = await sha256Blob(file);
|
|
110
|
+
const hash = opts?.sha256 === void 0 ? await sha256Blob(file) : assertHex32(opts.sha256, "blob sha256");
|
|
90
111
|
const headers = {
|
|
91
112
|
Authorization: encodeAuthorizationHeader(auth),
|
|
92
113
|
"X-SHA-256": hash
|
|
@@ -138,7 +159,7 @@ async function mirrorBlob(server, blob, opts) {
|
|
|
138
159
|
}
|
|
139
160
|
/** BUD-06 preflight. Does not PUT the body. */
|
|
140
161
|
async function checkUpload(server, file, auth, opts) {
|
|
141
|
-
const hash = await sha256Blob(file);
|
|
162
|
+
const hash = opts?.sha256 === void 0 ? await sha256Blob(file) : assertHex32(opts.sha256, "blob sha256");
|
|
142
163
|
const headers = {
|
|
143
164
|
Authorization: encodeAuthorizationHeader(auth),
|
|
144
165
|
"X-SHA-256": hash,
|
|
@@ -207,7 +228,7 @@ async function uploadToServers(servers, file, auth, opts) {
|
|
|
207
228
|
return await upload(server, file, auth, opts);
|
|
208
229
|
} catch (error) {
|
|
209
230
|
if (isAbortError(error)) throw error;
|
|
210
|
-
last = error instanceof BlossomError ? error : new BlossomError("blossom upload failed", { cause: error
|
|
231
|
+
last = error instanceof BlossomError ? error : new BlossomError("blossom upload failed", { cause: error });
|
|
211
232
|
}
|
|
212
233
|
throw last ?? new BlossomError("no servers");
|
|
213
234
|
}
|
|
@@ -282,7 +303,7 @@ function blossomHttpError(method, url, res) {
|
|
|
282
303
|
return new BlossomError(detail === void 0 || detail === "" ? `blossom ${method} ${url} failed (${res.status})` : detail, { status: res.status });
|
|
283
304
|
}
|
|
284
305
|
async function blossomFetch(fetchImpl, url, init) {
|
|
285
|
-
return fetchManual(fetchImpl ?? requireGlobalFetch(missingBlossomFetch), url, init, (err) => new BlossomError(`blossom ${init.method} ${url} failed`, { cause: err
|
|
306
|
+
return fetchManual(fetchImpl ?? requireGlobalFetch(missingBlossomFetch), url, init, (err) => new BlossomError(`blossom ${init.method} ${url} failed`, { cause: err }));
|
|
286
307
|
}
|
|
287
308
|
/**
|
|
288
309
|
* HEAD with redirect:manual. Network → undefined. AbortError propagates. Never throws on HTTP
|
|
@@ -309,7 +330,7 @@ async function readJson(res) {
|
|
|
309
330
|
return await res.json();
|
|
310
331
|
} catch (error) {
|
|
311
332
|
throw new BlossomError("invalid blossom JSON response", {
|
|
312
|
-
cause: error
|
|
333
|
+
cause: error,
|
|
313
334
|
status: res.status
|
|
314
335
|
});
|
|
315
336
|
}
|
|
@@ -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. 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
|
+
{"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 type { Mutable } from \"../core/util.ts\";\nimport { assertHex32, bytesToHex, isRecord, nowSeconds, utf8Encoder } from \"../core/util.ts\";\nimport { fetchManual, requireGlobalFetch, sendManual } from \"./http.ts\";\nimport type { ManualFetch } from \"./http.ts\";\n\nexport type BlobDescriptor = {\n readonly url: string;\n readonly sha256: string;\n readonly size: number;\n readonly type?: string | undefined;\n readonly uploaded?: number | undefined;\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\n/**\n * Hostname for a BUD-11 `server` tag: accepts a full URL or a bare domain; lowercased, undefined\n * when unparseable.\n */\nfunction serverHostname(raw: string): string | undefined {\n try {\n const url = new URL(raw.includes(\"://\") ? raw : `https://${raw}`);\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n return undefined;\n }\n const hostname = url.hostname.toLowerCase();\n return hostname === \"\" ? undefined : hostname;\n } catch {\n return undefined;\n }\n}\n\nexport function createAuthTemplate(\n verb: BlossomAuthVerb,\n opts?: {\n sha256?: string | undefined;\n expiration?: number | undefined;\n message?: string | undefined;\n servers?: ReadonlyArray<string> | undefined;\n },\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 ?? nowSeconds() + 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 const seenServers = new Set<string>();\n for (const server of opts?.servers ?? []) {\n const hostname = serverHostname(server);\n if (hostname === undefined || seenServers.has(hostname)) {\n continue;\n }\n seenServers.add(hostname);\n tags.push([\"server\", hostname]);\n }\n return {\n kind: Kind.BlobsAuth,\n created_at: nowSeconds(),\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?: {\n message?: string | undefined;\n expiration?: number | undefined;\n servers?: ReadonlyArray<string> | undefined;\n },\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; sha256?: string | undefined },\n): Promise<BlobDescriptor> {\n const hash =\n opts?.sha256 === undefined ? await sha256Blob(file) : assertHex32(opts.sha256, \"blob sha256\");\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; sha256?: string | undefined },\n): Promise<void> {\n const hash =\n opts?.sha256 === undefined ? await sha256Blob(file) : assertHex32(opts.sha256, \"blob sha256\");\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,\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,\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,\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: Mutable<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":";;;;;;;;;;;;;;AAoCA,MAAM,6BAAa,IAAI,IAAqB;CAAC;CAAU;CAAU;CAAQ;CAAO;AAAO,CAAC;;AAGxF,MAAM,uBAAuB;AAE7B,IAAa,eAAb,cAAkC,WAAW;CAC3C,OAAgB;CAChB;CAEA,YAAY,SAAiB,SAA8C;EACzE,MAAM,EAAE,QAAQ,GAAG,SAAS,WAAW,CAAC;EACxC,MAAM,SAAS,IAAI;EACnB,KAAK,SAAS;CAChB;AACF;;AAGA,SAAgB,eAAe,KAAuC;CACpE,IAAI;CACJ,IAAI;EACF,QAAQ,eAAe,MAAM,MAAM,IAAI,IAAI,GAAG,EAAA,CAAG;CACnD,QAAQ;EACN;CACF;CACA,MAAM,UAAU,KAAK,MAAM,gBAAgB;CAC3C,IAAI,YAAY,QAAQ,QAAQ,WAAW,GACzC;CAEF,OAAO,QAAQ,GAAG,EAAE,CAAC,EAAE,YAAY;AACrC;AAEA,eAAsB,WAAW,MAAwD;CACvF,IAAI;CACJ,IAAI,gBAAgB,YAClB,QAAQ;MACH,IAAI,gBAAgB,aACzB,QAAQ,IAAI,WAAW,IAAI;MAE3B,QAAQ,IAAI,WAAW,MAAM,KAAK,YAAY,CAAC;CAEjD,OAAO,WAAW,OAAO,KAAK,CAAC;AACjC;;AAGA,eAAsB,WACpB,MACA,WACkB;CAClB,MAAM,WAAW,YAAY,WAAW,aAAa;CACrD,OAAQ,MAAM,WAAW,IAAI,MAAO;AACtC;;AAGA,SAAgB,0BAA0B,OAAsB;CAC9D,OAAO,SAAS,eAAe,OAAO,YAAY,OAAO,KAAK,UAAU,KAAK,CAAC,CAAC;AACjF;;AAGA,MAAM,qBAAsD;CAC1D,QAAQ;CACR,QAAQ;CACR,MAAM;CACN,KAAK;CACL,OAAO;AACT;;;;;AAMA,SAAS,eAAe,KAAiC;CACvD,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,IAAI,SAAS,KAAK,IAAI,MAAM,WAAW,KAAK;EAChE,IAAI,IAAI,aAAa,WAAW,IAAI,aAAa,UAC/C;EAEF,MAAM,WAAW,IAAI,SAAS,YAAY;EAC1C,OAAO,aAAa,KAAK,KAAA,IAAY;CACvC,QAAQ;EACN;CACF;AACF;AAEA,SAAgB,mBACd,MACA,MAMe;CACf,IAAI,CAAC,WAAW,IAAI,IAAI,GACtB,MAAM,IAAI,aAAa,8BAA8B,MAAM;CAE7D,IAAI,MAAM,YAAY,KAAA,KAAa,KAAK,QAAQ,KAAK,MAAM,IACzD,MAAM,IAAI,aAAa,wCAAwC;CAEjE,MAAM,aAAa,MAAM,cAAc,WAAW,IAAI;CACtD,MAAM,OAAc,CAClB,CAAC,KAAK,IAAI,GACV,CAAC,cAAc,OAAO,UAAU,CAAC,CACnC;CACA,IAAI,MAAM,WAAW,KAAA,GACnB,KAAK,KAAK,CAAC,KAAK,YAAY,KAAK,QAAQ,aAAa,CAAC,CAAC;CAE1D,MAAM,8BAAc,IAAI,IAAY;CACpC,KAAK,MAAM,UAAU,MAAM,WAAW,CAAC,GAAG;EACxC,MAAM,WAAW,eAAe,MAAM;EACtC,IAAI,aAAa,KAAA,KAAa,YAAY,IAAI,QAAQ,GACpD;EAEF,YAAY,IAAI,QAAQ;EACxB,KAAK,KAAK,CAAC,UAAU,QAAQ,CAAC;CAChC;CACA,OAAO;EACL,MAAM,KAAK;EACX,YAAY,WAAW;EACvB;EACA,SAAS,MAAM,WAAW,mBAAmB;CAC/C;AACF;AAEA,eAAsB,iBACpB,MACA,MACA,MAKgB;CAEhB,OAAO,KAAK,mBAAmB,UAAU;EAAE,QAAQ,MADhC,WAAW,IAAI;EACuB,GAAG;CAAK,CAAC,CAAC;AACrE;AAEA,eAAsB,OACpB,QACA,MACA,MACA,MACyB;CACzB,MAAM,OACJ,MAAM,WAAW,KAAA,IAAY,MAAM,WAAW,IAAI,IAAI,YAAY,KAAK,QAAQ,aAAa;CAC9F,MAAM,UAAkC;EACtC,eAAe,0BAA0B,IAAI;EAC7C,aAAa;CACf;CACA,IAAI,KAAK,SAAS,IAChB,QAAQ,kBAAkB,KAAK;CAQjC,OAAO,sBAAsB,oBAAoB,MAAM,SAAS,MAN9C,eAAe,MAAM,OAAO,WAAW,QAAQ,SAAS,GAAG;EAC3E,QAAQ;EACR;EACA,MAAM;EACN,QAAQ,MAAM;CAChB,CAAC,CACkE,CAAC,GAAG,IAAI;AAC7E;AAEA,eAAsB,UACpB,QACA,QACA,MACA,MAC2B;CAC3B,MAAM,KAAK,YAAY,QAAQ,QAAQ;CACvC,MAAM,UAAkC,CAAC;CACzC,IAAI,MACF,QAAQ,mBAAmB,0BAA0B,IAAI;CAE3D,MAAM,MAAM,MAAM,eAAe,MAAM,OAAO,WAAW,QAAQ,SAAS,IAAI,GAAG;EAC/E,QAAQ;EACR;EACA,QAAQ,MAAM;CAChB,CAAC;CACD,MAAM,OAAO,MAAM,SAAS,GAAG;CAC/B,IAAI,CAAC,MAAM,QAAQ,IAAI,GACrB,MAAM,IAAI,aAAa,8CAA8C,EAAE,QAAQ,IAAI,OAAO,CAAC;CAE7F,OAAO,KAAK,IAAI,mBAAmB;AACrC;AAEA,eAAsB,WACpB,QACA,WACA,MACA,MACe;CACf,MAAM,OAAO,YAAY,WAAW,aAAa;CACjD,MAAM,eAAe,MAAM,OAAO,WAAW,QAAQ,IAAI,MAAM,GAAG;EAChE,QAAQ;EACR,SAAS,EAAE,eAAe,0BAA0B,IAAI,EAAE;EAC1D,QAAQ,MAAM;CAChB,CAAC;AACH;AAEA,eAAsB,WACpB,QACA,MACA,MACyB;CACzB,MAAM,OAAO,YAAY,KAAK,QAAQ,aAAa;CACnD,MAAM,UAAkC;EACtC,eAAe,0BAA0B,KAAK,IAAI;EAClD,gBAAgB;EAChB,aAAa;EACb,oBAAoB,OAAO,KAAK,IAAI;CACtC;CACA,IAAI,KAAK,SAAS,KAAA,KAAa,KAAK,SAAS,IAC3C,QAAQ,oBAAoB,KAAK;CAQnC,OAAO,sBAAsB,oBAAoB,MAAM,SAAS,MAN9C,eAAe,KAAK,OAAO,WAAW,QAAQ,SAAS,GAAG;EAC1E,QAAQ;EACR;EACA,MAAM,KAAK,UAAU,EAAE,KAAK,KAAK,IAAI,CAAC;EACtC,QAAQ,KAAK;CACf,CAAC,CACkE,CAAC,GAAG,IAAI;AAC7E;;AAGA,eAAsB,YACpB,QACA,MACA,MACA,MACe;CACf,MAAM,OACJ,MAAM,WAAW,KAAA,IAAY,MAAM,WAAW,IAAI,IAAI,YAAY,KAAK,QAAQ,aAAa;CAC9F,MAAM,UAAkC;EACtC,eAAe,0BAA0B,IAAI;EAC7C,aAAa;EACb,oBAAoB,OAAO,KAAK,IAAI;CACtC;CACA,IAAI,KAAK,MACP,QAAQ,oBAAoB,KAAK;CAEnC,MAAM,eAAe,MAAM,OAAO,WAAW,QAAQ,SAAS,GAAG;EAC/D,QAAQ;EACR;EACA,QAAQ,MAAM;CAChB,CAAC;AACH;;;;;AAMA,eAAsB,WACpB,QACA,WACA,MACkB;CAElB,MAAM,MAAM,WAAW,QAAQ,IADlB,YAAY,WAAW,aACE,GAAG;CACzC,MAAM,MAAM,MAAM,aAAa,MAAM,OAAO,KAAK;EAAE,QAAQ;EAAQ,QAAQ,MAAM;CAAO,CAAC;CACzF,IAAI,IAAI,WAAW,KACjB,OAAO;CAET,IAAI,IAAI,IACN,OAAO;CAET,MAAM,iBAAiB,QAAQ,KAAK,GAAG;AACzC;;AAGA,eAAsB,QACpB,QACA,WACA,MACqB;CACrB,MAAM,OAAO,YAAY,WAAW,aAAa;CACjD,MAAM,MAAM,WAAW,QAAQ,IAAI,MAAM;CACzC,MAAM,MAAM,MAAM,aAAa,MAAM,OAAO,KAAK;EAAE,QAAQ;EAAO,QAAQ,MAAM;CAAO,CAAC;CACxF,IAAI,CAAC,IAAI,IACP,MAAM,iBAAiB,OAAO,KAAK,GAAG;CAExC,MAAM,QAAQ,IAAI,WAAW,MAAM,IAAI,YAAY,CAAC;CACpD,IAAI,CAAE,MAAM,WAAW,OAAO,IAAI,GAChC,MAAM,IAAI,aAAa,sBAAsB;CAE/C,OAAO;AACT;;;;;;AAOA,eAAsB,YACpB,KACA,SACA,MACiB;CACjB,MAAM,OAAO,eAAe,GAAG;CAC/B,IAAI,SAAS,KAAA,GACX,OAAO;CAGT,MAAM,MAAM,qBAAqB,KAAK,IAAI;CAC1C,MAAM,YAAY,MAAM,SAAS,mBAAmB,mBAAmB;CAEvE,MAAM,iBAAiB,MAAM,WAAW,WAAW,KAAK,MAAM,MAAM;CAEpE,IAAI,mBAAmB,KAAA,KAAa,kBAAkB,OAAO,iBAAiB,KAC5E,OAAO;CAGT,KAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,OAAO,aAAa,MAAM;EAChC,IAAI,SAAS,KAAA,GACX;EAEF,MAAM,YAAY,GAAG,KAAK,GAAG,OAAO;EAEpC,MAAM,SAAS,MAAM,WAAW,WAAW,WAAW,MAAM,MAAM;EAClE,IAAI,WAAW,KAAA,KAAa,UAAU,OAAO,SAAS,KACpD,OAAO;CAEX;CACA,OAAO;AACT;;AAGA,eAAsB,gBACpB,SACA,MACA,MACA,MACyB;CACzB,IAAI,QAAQ,WAAW,GACrB,MAAM,IAAI,aAAa,YAAY;CAErC,IAAI;CACJ,KAAK,MAAM,UAAU,SACnB,IAAI;EAEF,OAAO,MAAM,OAAO,QAAQ,MAAM,MAAM,IAAI;CAC9C,SAAS,OAAO;EACd,IAAI,aAAa,KAAK,GACpB,MAAM;EAER,OACE,iBAAiB,eACb,QACA,IAAI,aAAa,yBAAyB,EACxC,OAAO,MACT,CAAC;CACT;CAEF,MAAM,QAAQ,IAAI,aAAa,YAAY;AAC7C;AAEA,SAAgB,8BAA8B,SAA8C;CAC1F,MAAM,OAAc,CAAC;CACrB,MAAM,uBAAO,IAAI,IAAY;CAC7B,KAAK,MAAM,OAAO,SAAS;EACzB,MAAM,MAAM,aAAa,GAAG;EAC5B,IAAI,QAAQ,KAAA,KAAa,KAAK,IAAI,GAAG,GACnC;EAEF,KAAK,IAAI,GAAG;EACZ,KAAK,KAAK,CAAC,UAAU,GAAG,CAAC;CAC3B;CACA,IAAI,KAAK,WAAW,GAClB,MAAM,IAAI,aAAa,qDAAqD;CAE9E,OAAO,IAAI,aAAa,KAAK,mBAAmB,EAAE,CAAC,CAAC,KAAK,IAAI;AAC/D;AAEA,SAAgB,uBAAuB,OAA+C;CACpF,IAAI,MAAM,SAAS,KAAK,mBACtB,MAAM,IAAI,qBAAqB,iBAAiB,KAAK,kBAAkB,QAAQ,MAAM,MAAM;CAE7F,MAAM,MAAgB,CAAC;CACvB,MAAM,uBAAO,IAAI,IAAY;CAC7B,KAAK,MAAM,OAAO,MAAM,MAAM;EAC5B,MAAM,WAAW,IAAI,GAAG,CAAC;EACzB,IAAI,IAAI,OAAO,YAAY,aAAa,KAAA,KAAa,aAAa,IAChE;EAEF,MAAM,MAAM,aAAa,QAAQ;EACjC,IAAI,QAAQ,KAAA,KAAa,KAAK,IAAI,GAAG,GACnC;EAEF,KAAK,IAAI,GAAG;EACZ,IAAI,KAAK,GAAG;CACd;CACA,OAAO;AACT;AAEA,SAAS,aAAa,KAAiC;CACrD,IAAI;EACF,MAAM,IAAI,IAAI,IAAI,GAAG;EACrB,IAAI,EAAE,aAAa,WAAW,EAAE,aAAa,UAC3C;EAEF,MAAM,OAAO,EAAE,SAAS,QAAQ,QAAQ,EAAE;EAC1C,OAAO,GAAG,EAAE,SAAS;CACvB,QAAQ;EACN;CACF;AACF;;AAGA,SAAS,WAAW,QAAgB,MAAsB;CACxD,MAAM,OAAO,aAAa,MAAM;CAChC,IAAI,SAAS,KAAA,GACX,MAAM,IAAI,aAAa,+BAA+B,QAAQ;CAGhE,OAAO,GAAG,OADK,KAAK,WAAW,GAAG,IAAI,OAAO,IAAI;AAEnD;AAEA,SAAS,aAAa,KAAuB;CAC3C,OAAO,eAAe,SAAS,IAAI,SAAS;AAC9C;;AAGA,SAAS,qBAAqB,KAAa,MAAsB;CAC/D,IAAI;CACJ,IAAI;EACF,CAAC,CAAE,YAAa,IAAI,IAAI,GAAG;CAC7B,QAAQ;EACN,OAAO;CACT;CACA,MAAM,IAAI,SAAS,YAAY,CAAC,CAAC,YAAY,IAAI;CACjD,IAAI,MAAM,IACR,OAAO;CAGT,OADU,WAAW,KAAK,SAAS,MAAM,IAAI,EAAE,CACxC,CAAC,GAAG,MAAM;AACnB;AAEA,SAAS,sBAAoC;CAC3C,OAAO,IAAI,aAAa,oDAAoD;AAC9E;AAIA,SAAS,iBAAiB,QAAgB,KAAa,KAAmC;CACxF,IAAI;CACJ,IAAI;EACF,SAAS,IAAI,QAAQ,IAAI,UAAU,KAAK,KAAA;CAC1C,QAAQ;EACN,SAAS,KAAA;CACX;CACA,MAAM,SAAS,QAAQ,KAAK;CAC5B,OAAO,IAAI,aACT,WAAW,KAAA,KAAa,WAAW,KAC/B,WAAW,OAAO,GAAG,IAAI,WAAW,IAAI,OAAO,KAC/C,QACJ,EAAE,QAAQ,IAAI,OAAO,CACvB;AACF;AAEA,eAAe,aACb,WACA,KACA,MAMyB;CACzB,OAAO,YACL,aAAa,mBAAmB,mBAAmB,GACnD,KACA,OACC,QACC,IAAI,aAAa,WAAW,KAAK,OAAO,GAAG,IAAI,UAAU,EACvD,OAAO,IACT,CAAC,CACL;AACF;;;;;AAMA,eAAe,WACb,WACA,KACA,QAC6B;CAC7B,IAAI;EAEF,QAAO,MADW,WAAW,WAAW,KAAK;GAAE,QAAQ;GAAQ;EAAO,CAAC,EAAA,CAC5D;CACb,SAAS,OAAO;EACd,IAAI,aAAa,KAAK,GACpB,MAAM;EAER;CACF;AACF;AAEA,eAAe,eACb,WACA,KACA,MAMyB;CACzB,MAAM,MAAM,MAAM,aAAa,WAAW,KAAK,IAAI;CACnD,IAAI,IAAI,IACN,OAAO;CAET,MAAM,iBAAiB,KAAK,QAAQ,KAAK,GAAG;AAC9C;AAEA,eAAe,SAAS,KAAuC;CAC7D,IAAI;EACF,OAAO,MAAM,IAAI,KAAK;CACxB,SAAS,OAAO;EACd,MAAM,IAAI,aAAa,iCAAiC;GACtD,OAAO;GACP,QAAQ,IAAI;EACd,CAAC;CACH;AACF;AAEA,SAAS,oBAAoB,MAA+B;CAC1D,IAAI,CAAC,SAAS,IAAI,GAChB,MAAM,IAAI,aAAa,yBAAyB;CAElD,MAAM,MAAM;CACZ,IAAI,OAAO,IAAI,WAAW,YAAY,CAAC,IAAI,QACzC,MAAM,IAAI,aAAa,6BAA6B;CAEtD,IAAI,OAAO,IAAI,cAAc,UAC3B,MAAM,IAAI,aAAa,gCAAgC;CAEzD,MAAM,OAAO,YAAY,IAAI,WAAW,aAAa;CACrD,IAAI,OAAO,IAAI,YAAY,YAAY,CAAC,OAAO,UAAU,IAAI,OAAO,KAAK,IAAI,UAAU,GACrF,MAAM,IAAI,aAAa,8BAA8B;CAEvD,MAAM,OAAgC;EAAE,KAAK,IAAI;EAAQ,QAAQ;EAAM,MAAM,IAAI;CAAQ;CACzF,IAAI,OAAO,IAAI,YAAY,UACzB,KAAK,OAAO,IAAI;CAElB,IACE,OAAO,IAAI,gBAAgB,YAC3B,OAAO,UAAU,IAAI,WAAW,KAChC,IAAI,eAAe,GAEnB,KAAK,WAAW,IAAI;CAEtB,OAAO;AACT;AAEA,SAAS,sBAAsB,MAAsB,UAAkC;CACrF,IAAI,KAAK,WAAW,UAClB,MAAM,IAAI,aAAa,iCAAiC;CAE1D,OAAO;AACT"}
|
package/dist/nips/nip04.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as hexToBytes$1,
|
|
1
|
+
import { a as hexToBytes$1, f as utf8Decoder, m as CryptoError, n as assertHex32, p as utf8Encoder, r as assertSecretKeyBytes } from "../util-DMF9AIO0.mjs";
|
|
2
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";
|
|
@@ -7,10 +7,16 @@ import { base64 } from "@scure/base";
|
|
|
7
7
|
/** NIP-04 legacy encrypted direct messages. Prefer NIP-44 for new applications. */
|
|
8
8
|
function normalizeSharedSecret(privkey, pubkey) {
|
|
9
9
|
assertHex32(pubkey, "public key");
|
|
10
|
-
|
|
10
|
+
try {
|
|
11
|
+
return secp256k1.getSharedSecret(privkey, hexToBytes$1(`02${pubkey.toLowerCase()}`)).slice(1, 33);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
throw new CryptoError("invalid NIP-04 public key", { cause: error });
|
|
14
|
+
}
|
|
11
15
|
}
|
|
12
16
|
function resolveSecret(secretKey) {
|
|
13
|
-
|
|
17
|
+
if (typeof secretKey === "string") return hexToBytes$1(assertHex32(secretKey, "secret key"));
|
|
18
|
+
assertSecretKeyBytes(secretKey);
|
|
19
|
+
return secretKey;
|
|
14
20
|
}
|
|
15
21
|
/** Encrypt plaintext to a peer pubkey (NIP-04). */
|
|
16
22
|
function encrypt(secretKey, pubkey, text) {
|
|
@@ -26,10 +32,14 @@ function decrypt(secretKey, pubkey, data) {
|
|
|
26
32
|
const parts = data.split("?iv=");
|
|
27
33
|
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");
|
|
28
34
|
const normalizedKey = normalizeSharedSecret(privkey, pubkey);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
try {
|
|
36
|
+
const iv = base64.decode(parts[1]);
|
|
37
|
+
const ciphertext = base64.decode(parts[0]);
|
|
38
|
+
const plaintext = cbc(normalizedKey, iv).decrypt(ciphertext);
|
|
39
|
+
return utf8Decoder.decode(plaintext);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new CryptoError("invalid NIP-04 payload", { cause: error });
|
|
42
|
+
}
|
|
33
43
|
}
|
|
34
44
|
//#endregion
|
|
35
45
|
export { decrypt, encrypt };
|
package/dist/nips/nip04.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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 {
|
|
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 {\n assertHex32,\n assertSecretKeyBytes,\n hexToBytes,\n utf8Decoder,\n utf8Encoder,\n} from \"../core/util.ts\";\n\nfunction normalizeSharedSecret(privkey: Uint8Array, pubkey: string): Uint8Array {\n assertHex32(pubkey, \"public key\");\n try {\n const key = secp256k1.getSharedSecret(privkey, hexToBytes(`02${pubkey.toLowerCase()}`));\n return key.slice(1, 33);\n } catch (error) {\n throw new CryptoError(\"invalid NIP-04 public key\", { cause: error });\n }\n}\n\nfunction resolveSecret(secretKey: string | Uint8Array): Uint8Array {\n if (typeof secretKey === \"string\") {\n return hexToBytes(assertHex32(secretKey, \"secret key\"));\n }\n assertSecretKeyBytes(secretKey);\n return 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 try {\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 } catch (error) {\n throw new CryptoError(\"invalid NIP-04 payload\", { cause: error });\n }\n}\n"],"mappings":";;;;;;;AAeA,SAAS,sBAAsB,SAAqB,QAA4B;CAC9E,YAAY,QAAQ,YAAY;CAChC,IAAI;EAEF,OADY,UAAU,gBAAgB,SAASA,aAAW,KAAK,OAAO,YAAY,GAAG,CAC5E,CAAC,CAAC,MAAM,GAAG,EAAE;CACxB,SAAS,OAAO;EACd,MAAM,IAAI,YAAY,6BAA6B,EAAE,OAAO,MAAM,CAAC;CACrE;AACF;AAEA,SAAS,cAAc,WAA4C;CACjE,IAAI,OAAO,cAAc,UACvB,OAAOA,aAAW,YAAY,WAAW,YAAY,CAAC;CAExD,qBAAqB,SAAS;CAC9B,OAAO;AACT;;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,IAAI;EACF,MAAM,KAAK,OAAO,OAAO,MAAM,EAAE;EACjC,MAAM,aAAa,OAAO,OAAO,MAAM,EAAE;EACzC,MAAM,YAAY,IAAI,eAAe,EAAE,CAAC,CAAC,QAAQ,UAAU;EAC3D,OAAO,YAAY,OAAO,SAAS;CACrC,SAAS,OAAO;EACd,MAAM,IAAI,YAAY,0BAA0B,EAAE,OAAO,MAAM,CAAC;CAClE;AACF"}
|
package/dist/nips/nip05.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as NostrError } from "../error-
|
|
2
|
-
import { f as ProfilePointer } from "../nip19-
|
|
1
|
+
import { a as NostrError } from "../error-BquXDmIN.mjs";
|
|
2
|
+
import { f as ProfilePointer } from "../nip19-DpPN17bY.mjs";
|
|
3
3
|
import { t as ManualFetch } from "../http-C4HwIu4B.mjs";
|
|
4
4
|
//#region src/nips/nip05.d.ts
|
|
5
5
|
/** Root local-part (`_@domain` rendered as just the domain). */
|
|
@@ -14,25 +14,24 @@ export declare const WELL_KNOWN_PATH = "/.well-known/nostr.json";
|
|
|
14
14
|
export type Nip05 = string;
|
|
15
15
|
export type Nip05Address = {
|
|
16
16
|
/** Local part (lowercased). `_` for domain-only identifiers. */
|
|
17
|
-
local: string;
|
|
17
|
+
readonly local: string;
|
|
18
18
|
/** Domain (lowercased). */
|
|
19
|
-
domain: string;
|
|
19
|
+
readonly domain: string;
|
|
20
20
|
};
|
|
21
21
|
/** 46.md appendix discovery metadata (`{relays, nostrconnect_url}`). Not a bunker pointer. */
|
|
22
22
|
export type Nip05Nip46 = {
|
|
23
|
-
relays?: string
|
|
24
|
-
nostrconnectUrl?: string;
|
|
23
|
+
readonly relays?: ReadonlyArray<string> | undefined;
|
|
24
|
+
readonly nostrconnectUrl?: string | undefined;
|
|
25
25
|
};
|
|
26
26
|
export type Nip05Document = {
|
|
27
|
-
names: Record<string, string
|
|
27
|
+
readonly names: Readonly<Record<string, string>>;
|
|
28
28
|
/** NIP-05 profile relay hints */
|
|
29
|
-
relays?: Record<string, string
|
|
30
|
-
nip46?: Nip05Nip46;
|
|
29
|
+
readonly relays?: Readonly<Record<string, ReadonlyArray<string>>> | undefined;
|
|
30
|
+
readonly nip46?: Nip05Nip46 | undefined;
|
|
31
31
|
};
|
|
32
32
|
export type Nip05Fetch = ManualFetch;
|
|
33
33
|
export declare class Nip05Error extends NostrError {
|
|
34
34
|
name: string;
|
|
35
|
-
constructor(message: string, options?: ErrorOptions);
|
|
36
35
|
}
|
|
37
36
|
/** Matches optional local@domain. Groups: 1=local (optional), 2=domain. */
|
|
38
37
|
export declare const NIP05_REGEX: RegExp;
|
package/dist/nips/nip05.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as isRecord,
|
|
1
|
+
import { c as isRecord, o as isHex32, v as NostrError } from "../util-DMF9AIO0.mjs";
|
|
2
2
|
import { n as requireGlobalFetch, t as fetchManual } from "../http-CuazNRIP.mjs";
|
|
3
3
|
//#region src/nips/nip05.ts
|
|
4
4
|
/**
|
|
@@ -11,9 +11,6 @@ const NIP05_ROOT_LOCAL = "_";
|
|
|
11
11
|
const WELL_KNOWN_PATH = "/.well-known/nostr.json";
|
|
12
12
|
var Nip05Error = class extends NostrError {
|
|
13
13
|
name = "Nip05Error";
|
|
14
|
-
constructor(message, options) {
|
|
15
|
-
super(message, options);
|
|
16
|
-
}
|
|
17
14
|
};
|
|
18
15
|
/** Matches optional local@domain. Groups: 1=local (optional), 2=domain. */
|
|
19
16
|
const NIP05_REGEX = /^(?:([a-z0-9._-]+)@)?([a-z0-9-]+(?:\.[a-z0-9-]+)+)$/i;
|
package/dist/nips/nip05.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nip05.mjs","names":[],"sources":["../../src/nips/nip05.ts"],"sourcesContent":["/**\n * NIP-05: Mapping Nostr keys to DNS-based internet identifiers\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/05.md\n */\nimport { NostrError } from \"../core/error.ts\";\nimport { isHex32, isRecord } from \"../core/util.ts\";\nimport { fetchManual, requireGlobalFetch } from \"./http.ts\";\nimport type { ManualFetch } from \"./http.ts\";\nimport type { ProfilePointer } from \"./nip19.ts\";\n\n/** Root local-part (`_@domain` rendered as just the domain). */\nexport const NIP05_ROOT_LOCAL = \"_\";\n\nexport const WELL_KNOWN_PATH = \"/.well-known/nostr.json\";\n\n/**\n * NIP-05 identifier string.\n *\n * - Full: `name@domain`\n * - Root: `domain` or `_@domain`\n */\nexport type Nip05 = string;\n\nexport type Nip05Address = {\n /** Local part (lowercased). `_` for domain-only identifiers. */\n local: string;\n /** Domain (lowercased). */\n domain: string;\n};\n\n/** 46.md appendix discovery metadata (`{relays, nostrconnect_url}`). Not a bunker pointer. */\nexport type Nip05Nip46 = {\n relays?: string[];\n nostrconnectUrl?: string;\n};\n\nexport type Nip05Document = {\n names: Record<string, string>;\n /** NIP-05 profile relay hints */\n relays?: Record<string, string[]>;\n nip46?: Nip05Nip46;\n};\n\nexport type Nip05Fetch = ManualFetch;\n\nexport class Nip05Error extends NostrError {\n override name = \"Nip05Error\";\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n }\n}\n\n/** Matches optional local@domain. Groups: 1=local (optional), 2=domain. */\n// oxlint-disable-next-line no-inferrable-types -- isolatedDeclarations requires the annotation for dts emit\nexport const NIP05_REGEX: RegExp = /^(?:([a-z0-9._-]+)@)?([a-z0-9-]+(?:\\.[a-z0-9-]+)+)$/i;\n\nexport function isNip05(value: unknown): value is string {\n if (typeof value !== \"string\") {\n return false;\n }\n try {\n parseNip05(value);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Parse an identifier into local + domain. Accepts `name@domain`, `_@domain`, or bare `domain`\n * (local becomes `_`).\n */\nexport function parseNip05(input: string): Nip05Address {\n const match = NIP05_REGEX.exec(input.trim());\n const domain = match?.at(2);\n if (match === null || domain === undefined || domain === \"\") {\n throw new Nip05Error(`invalid NIP-05 identifier: ${input}`);\n }\n const localRaw = match.at(1) ?? NIP05_ROOT_LOCAL;\n return { local: localRaw.toLowerCase(), domain: domain.toLowerCase() };\n}\n\n/** Build the well-known HTTPS URL for an address. */\nexport function wellKnownUrl(address: Nip05Address): string {\n const url = new URL(`https://${address.domain}${WELL_KNOWN_PATH}`);\n url.searchParams.set(\"name\", address.local);\n return url.toString();\n}\n\nfunction stringUrls(list: unknown): string[] | undefined {\n if (!Array.isArray(list)) {\n return undefined;\n }\n return list.filter((u): u is string => typeof u === \"string\" && u.length > 0);\n}\n\n/**\n * Parse nostr.json `nip46`: 46.md appendix `{relays, nostrconnect_url}`. Hex-pubkey maps and other\n * keys are ignored.\n */\nexport function parseNip05Nip46(raw: unknown): Nip05Nip46 | undefined {\n if (!isRecord(raw)) {\n return undefined;\n }\n const obj = raw;\n const result: Nip05Nip46 = {};\n\n if (Array.isArray(obj[\"relays\"])) {\n result.relays = stringUrls(obj[\"relays\"]) ?? [];\n }\n\n if (typeof obj[\"nostrconnect_url\"] === \"string\" && obj[\"nostrconnect_url\"].length > 0) {\n result.nostrconnectUrl = obj[\"nostrconnect_url\"];\n }\n\n if (result.relays === undefined && result.nostrconnectUrl === undefined) {\n return undefined;\n }\n return result;\n}\n\n/** Parse and validate a nostr.json document body. */\nexport function parseNip05Document(json: unknown): Nip05Document {\n if (!isRecord(json)) {\n throw new Nip05Error(\"NIP-05 document must be a JSON object\");\n }\n const raw = json;\n if (!isRecord(raw[\"names\"])) {\n throw new Nip05Error(\"NIP-05 document missing names map\");\n }\n\n const names: Record<string, string> = {};\n for (const [k, v] of Object.entries(raw[\"names\"])) {\n if (typeof v !== \"string\" || !isHex32(v.toLowerCase())) {\n continue;\n }\n names[k.toLowerCase()] = v.toLowerCase();\n }\n\n const rawRelays = raw[\"relays\"];\n let relays: Record<string, string[]> | undefined;\n if (isRecord(rawRelays)) {\n relays = {};\n for (const [pk, list] of Object.entries(rawRelays)) {\n if (!isHex32(pk.toLowerCase())) {\n continue;\n }\n const urls = stringUrls(list);\n if (urls !== undefined && urls.length > 0) {\n relays[pk.toLowerCase()] = urls;\n }\n }\n }\n\n const doc: Nip05Document = { names };\n if (relays !== undefined) {\n doc.relays = relays;\n }\n const nip46 = parseNip05Nip46(raw[\"nip46\"]);\n if (nip46 !== undefined) {\n doc.nip46 = nip46;\n }\n return doc;\n}\n\n/** Resolve local name from an already-parsed document. */\nexport function lookupFromDocument(\n doc: Nip05Document,\n address: Nip05Address,\n): ProfilePointer | undefined {\n const pubkey = doc.names[address.local];\n if (pubkey === undefined) {\n return undefined;\n }\n const relays = doc.relays?.[pubkey];\n return relays !== undefined && relays.length > 0 ? { pubkey, relays: [...relays] } : { pubkey };\n}\n\n/**\n * Fetch and parse `/.well-known/nostr.json`. Returns `undefined` on network/parse failure (does not\n * throw for those). Rejects HTTP redirects (non-200) per NIP-05 security constraints. AbortError\n * rethrows; it is not mapped to `undefined`.\n */\nexport async function queryNip05Document(\n identifier: string,\n opts?: { fetch?: Nip05Fetch; signal?: AbortSignal },\n): Promise<{ address: Nip05Address; doc: Nip05Document } | undefined> {\n let address: Nip05Address;\n try {\n address = parseNip05(identifier);\n } catch {\n return undefined;\n }\n\n const url = wellKnownUrl(address);\n const fetchImpl =\n opts?.fetch ??\n requireGlobalFetch(() => new Nip05Error(\"no fetch implementation available; pass opts.fetch\"));\n\n try {\n const res = await fetchManual(fetchImpl, url, { signal: opts?.signal }, (err) =>\n err instanceof Error ? err : new Error(\"NIP-05 request failed\"),\n );\n // Redirects and errors must not be trusted (NIP-05 security).\n if (res.status !== 200) {\n return undefined;\n }\n const json = await res.json();\n return { address, doc: parseNip05Document(json) };\n } catch (error) {\n if (error instanceof Error && error.name === \"AbortError\") {\n throw error;\n }\n return undefined;\n }\n}\n\n/**\n * Query `/.well-known/nostr.json` for an identifier. Returns `undefined` on network/parse/lookup\n * failure (does not throw for those). Profile `relays` only. `nip46` is discovery metadata, not a\n * bunker pointer.\n */\nexport async function queryProfile(\n identifier: string,\n opts?: { fetch?: Nip05Fetch; signal?: AbortSignal },\n): Promise<ProfilePointer | undefined> {\n const fetched = await queryNip05Document(identifier, opts);\n if (fetched === undefined) {\n return undefined;\n }\n return lookupFromDocument(fetched.doc, fetched.address);\n}\n\n/** True when the identifier resolves to exactly `pubkey`. */\nexport async function verifyNip05(\n pubkey: string,\n identifier: string,\n opts?: { fetch?: Nip05Fetch; signal?: AbortSignal },\n): Promise<boolean> {\n if (!isHex32(pubkey.toLowerCase())) {\n return false;\n }\n const profile = await queryProfile(identifier, opts);\n return profile !== undefined && profile.pubkey === pubkey.toLowerCase();\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"nip05.mjs","names":[],"sources":["../../src/nips/nip05.ts"],"sourcesContent":["/**\n * NIP-05: Mapping Nostr keys to DNS-based internet identifiers\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/05.md\n */\nimport { NostrError } from \"../core/error.ts\";\nimport type { Mutable } from \"../core/util.ts\";\nimport { isHex32, isRecord } from \"../core/util.ts\";\nimport { fetchManual, requireGlobalFetch } from \"./http.ts\";\nimport type { ManualFetch } from \"./http.ts\";\nimport type { ProfilePointer } from \"./nip19.ts\";\n\n/** Root local-part (`_@domain` rendered as just the domain). */\nexport const NIP05_ROOT_LOCAL = \"_\";\n\nexport const WELL_KNOWN_PATH = \"/.well-known/nostr.json\";\n\n/**\n * NIP-05 identifier string.\n *\n * - Full: `name@domain`\n * - Root: `domain` or `_@domain`\n */\nexport type Nip05 = string;\n\nexport type Nip05Address = {\n /** Local part (lowercased). `_` for domain-only identifiers. */\n readonly local: string;\n /** Domain (lowercased). */\n readonly domain: string;\n};\n\n/** 46.md appendix discovery metadata (`{relays, nostrconnect_url}`). Not a bunker pointer. */\nexport type Nip05Nip46 = {\n readonly relays?: ReadonlyArray<string> | undefined;\n readonly nostrconnectUrl?: string | undefined;\n};\n\nexport type Nip05Document = {\n readonly names: Readonly<Record<string, string>>;\n /** NIP-05 profile relay hints */\n readonly relays?: Readonly<Record<string, ReadonlyArray<string>>> | undefined;\n readonly nip46?: Nip05Nip46 | undefined;\n};\n\nexport type Nip05Fetch = ManualFetch;\n\nexport class Nip05Error extends NostrError {\n override name = \"Nip05Error\";\n}\n\n/** Matches optional local@domain. Groups: 1=local (optional), 2=domain. */\n// oxlint-disable-next-line no-inferrable-types -- isolatedDeclarations requires the annotation for dts emit\nexport const NIP05_REGEX: RegExp = /^(?:([a-z0-9._-]+)@)?([a-z0-9-]+(?:\\.[a-z0-9-]+)+)$/i;\n\nexport function isNip05(value: unknown): value is string {\n if (typeof value !== \"string\") {\n return false;\n }\n try {\n parseNip05(value);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Parse an identifier into local + domain. Accepts `name@domain`, `_@domain`, or bare `domain`\n * (local becomes `_`).\n */\nexport function parseNip05(input: string): Nip05Address {\n const match = NIP05_REGEX.exec(input.trim());\n const domain = match?.at(2);\n if (match === null || domain === undefined || domain === \"\") {\n throw new Nip05Error(`invalid NIP-05 identifier: ${input}`);\n }\n const localRaw = match.at(1) ?? NIP05_ROOT_LOCAL;\n return { local: localRaw.toLowerCase(), domain: domain.toLowerCase() };\n}\n\n/** Build the well-known HTTPS URL for an address. */\nexport function wellKnownUrl(address: Nip05Address): string {\n const url = new URL(`https://${address.domain}${WELL_KNOWN_PATH}`);\n url.searchParams.set(\"name\", address.local);\n return url.toString();\n}\n\nfunction stringUrls(list: unknown): string[] | undefined {\n if (!Array.isArray(list)) {\n return undefined;\n }\n return list.filter((u): u is string => typeof u === \"string\" && u.length > 0);\n}\n\n/**\n * Parse nostr.json `nip46`: 46.md appendix `{relays, nostrconnect_url}`. Hex-pubkey maps and other\n * keys are ignored.\n */\nexport function parseNip05Nip46(raw: unknown): Nip05Nip46 | undefined {\n if (!isRecord(raw)) {\n return undefined;\n }\n const obj = raw;\n const result: Mutable<Nip05Nip46> = {};\n\n if (Array.isArray(obj[\"relays\"])) {\n result.relays = stringUrls(obj[\"relays\"]) ?? [];\n }\n\n if (typeof obj[\"nostrconnect_url\"] === \"string\" && obj[\"nostrconnect_url\"].length > 0) {\n result.nostrconnectUrl = obj[\"nostrconnect_url\"];\n }\n\n if (result.relays === undefined && result.nostrconnectUrl === undefined) {\n return undefined;\n }\n return result;\n}\n\n/** Parse and validate a nostr.json document body. */\nexport function parseNip05Document(json: unknown): Nip05Document {\n if (!isRecord(json)) {\n throw new Nip05Error(\"NIP-05 document must be a JSON object\");\n }\n const raw = json;\n if (!isRecord(raw[\"names\"])) {\n throw new Nip05Error(\"NIP-05 document missing names map\");\n }\n\n const names: Record<string, string> = {};\n for (const [k, v] of Object.entries(raw[\"names\"])) {\n if (typeof v !== \"string\" || !isHex32(v.toLowerCase())) {\n continue;\n }\n names[k.toLowerCase()] = v.toLowerCase();\n }\n\n const rawRelays = raw[\"relays\"];\n let relays: Record<string, string[]> | undefined;\n if (isRecord(rawRelays)) {\n relays = {};\n for (const [pk, list] of Object.entries(rawRelays)) {\n if (!isHex32(pk.toLowerCase())) {\n continue;\n }\n const urls = stringUrls(list);\n if (urls !== undefined && urls.length > 0) {\n relays[pk.toLowerCase()] = urls;\n }\n }\n }\n\n const doc: Mutable<Nip05Document> = { names };\n if (relays !== undefined) {\n doc.relays = relays;\n }\n const nip46 = parseNip05Nip46(raw[\"nip46\"]);\n if (nip46 !== undefined) {\n doc.nip46 = nip46;\n }\n return doc;\n}\n\n/** Resolve local name from an already-parsed document. */\nexport function lookupFromDocument(\n doc: Nip05Document,\n address: Nip05Address,\n): ProfilePointer | undefined {\n const pubkey = doc.names[address.local];\n if (pubkey === undefined) {\n return undefined;\n }\n const relays = doc.relays?.[pubkey];\n return relays !== undefined && relays.length > 0 ? { pubkey, relays: [...relays] } : { pubkey };\n}\n\n/**\n * Fetch and parse `/.well-known/nostr.json`. Returns `undefined` on network/parse failure (does not\n * throw for those). Rejects HTTP redirects (non-200) per NIP-05 security constraints. AbortError\n * rethrows; it is not mapped to `undefined`.\n */\nexport async function queryNip05Document(\n identifier: string,\n opts?: { fetch?: Nip05Fetch; signal?: AbortSignal },\n): Promise<{ address: Nip05Address; doc: Nip05Document } | undefined> {\n let address: Nip05Address;\n try {\n address = parseNip05(identifier);\n } catch {\n return undefined;\n }\n\n const url = wellKnownUrl(address);\n const fetchImpl =\n opts?.fetch ??\n requireGlobalFetch(() => new Nip05Error(\"no fetch implementation available; pass opts.fetch\"));\n\n try {\n const res = await fetchManual(fetchImpl, url, { signal: opts?.signal }, (err) =>\n err instanceof Error ? err : new Error(\"NIP-05 request failed\"),\n );\n // Redirects and errors must not be trusted (NIP-05 security).\n if (res.status !== 200) {\n return undefined;\n }\n const json = await res.json();\n return { address, doc: parseNip05Document(json) };\n } catch (error) {\n if (error instanceof Error && error.name === \"AbortError\") {\n throw error;\n }\n return undefined;\n }\n}\n\n/**\n * Query `/.well-known/nostr.json` for an identifier. Returns `undefined` on network/parse/lookup\n * failure (does not throw for those). Profile `relays` only. `nip46` is discovery metadata, not a\n * bunker pointer.\n */\nexport async function queryProfile(\n identifier: string,\n opts?: { fetch?: Nip05Fetch; signal?: AbortSignal },\n): Promise<ProfilePointer | undefined> {\n const fetched = await queryNip05Document(identifier, opts);\n if (fetched === undefined) {\n return undefined;\n }\n return lookupFromDocument(fetched.doc, fetched.address);\n}\n\n/** True when the identifier resolves to exactly `pubkey`. */\nexport async function verifyNip05(\n pubkey: string,\n identifier: string,\n opts?: { fetch?: Nip05Fetch; signal?: AbortSignal },\n): Promise<boolean> {\n if (!isHex32(pubkey.toLowerCase())) {\n return false;\n }\n const profile = await queryProfile(identifier, opts);\n return profile !== undefined && profile.pubkey === pubkey.toLowerCase();\n}\n"],"mappings":"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"}
|
package/dist/nips/nip10.d.mts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { _ as Tag, t as Event } from "../event-
|
|
2
|
-
import { t as EventBuilder } from "../builder-
|
|
3
|
-
import { f as ProfilePointer, i as EventPointer, t as AddressPointer } from "../nip19-
|
|
1
|
+
import { _ as Tag, t as Event } from "../event-DJeb_DIL.mjs";
|
|
2
|
+
import { t as EventBuilder } from "../builder-CTwkB2zM.mjs";
|
|
3
|
+
import { f as ProfilePointer, i as EventPointer, t as AddressPointer } from "../nip19-DpPN17bY.mjs";
|
|
4
4
|
//#region src/nips/nip10.d.ts
|
|
5
5
|
/** Parsed NIP-10 thread references from an event's `e`/`q`/`p` tags. */
|
|
6
6
|
export type ThreadReferences = {
|
|
7
7
|
/** Pointer to the root of the thread. */
|
|
8
|
-
root: EventPointer | undefined;
|
|
8
|
+
readonly root: EventPointer | undefined;
|
|
9
9
|
/** Pointer to the parent event this note replies to. */
|
|
10
|
-
reply: EventPointer | undefined;
|
|
10
|
+
readonly reply: EventPointer | undefined;
|
|
11
11
|
/** Other e-tagged events (not root/reply). */
|
|
12
|
-
mentions: EventPointer
|
|
12
|
+
readonly mentions: ReadonlyArray<EventPointer>;
|
|
13
13
|
/** Quoted events (`q` tags): event ids or addresses. Discriminate with `"id" in q`. */
|
|
14
|
-
quotes:
|
|
14
|
+
readonly quotes: ReadonlyArray<EventPointer | AddressPointer>;
|
|
15
15
|
/** P-tagged profiles involved in the thread. */
|
|
16
|
-
profiles: ProfilePointer
|
|
16
|
+
readonly profiles: ReadonlyArray<ProfilePointer>;
|
|
17
17
|
};
|
|
18
18
|
type ReplyParent = Pick<Event, "id" | "pubkey" | "tags" | "kind">;
|
|
19
19
|
type QuoteInput = string | EventPointer | AddressPointer;
|
package/dist/nips/nip10.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { h as EventValidationError, o as isHex32 } from "../util-DMF9AIO0.mjs";
|
|
2
2
|
import { t as Kind } from "../kind-Cxwhhkx8.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { t as EventBuilder } from "../builder-
|
|
3
|
+
import { h as Tag, v as formatEventAddress, x as parseEventAddress } from "../event-CHf47dPM.mjs";
|
|
4
|
+
import { t as EventBuilder } from "../builder-aNU2gGFP.mjs";
|
|
5
5
|
//#region src/nips/nip10.ts
|
|
6
6
|
/**
|
|
7
7
|
* NIP-10: Text Notes and Threads
|
|
@@ -90,13 +90,11 @@ function assertKind1Parent(parent) {
|
|
|
90
90
|
}
|
|
91
91
|
/** Parse NIP-10 thread markers and legacy positional e-tags from an event. */
|
|
92
92
|
function parseThreadTags(event) {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
profiles: []
|
|
99
|
-
};
|
|
93
|
+
const mentions = [];
|
|
94
|
+
const quotes = [];
|
|
95
|
+
const profiles = [];
|
|
96
|
+
let root;
|
|
97
|
+
let reply;
|
|
100
98
|
let maybeParent;
|
|
101
99
|
let maybeRoot;
|
|
102
100
|
for (let i = event.tags.length - 1; i >= 0; i--) {
|
|
@@ -108,52 +106,56 @@ function parseThreadTags(event) {
|
|
|
108
106
|
if (pointer === void 0) continue;
|
|
109
107
|
const marker = tag.at(3);
|
|
110
108
|
if (marker === "root") {
|
|
111
|
-
|
|
109
|
+
root = pointer;
|
|
112
110
|
continue;
|
|
113
111
|
}
|
|
114
112
|
if (marker === "reply") {
|
|
115
|
-
|
|
113
|
+
reply = pointer;
|
|
116
114
|
continue;
|
|
117
115
|
}
|
|
118
116
|
if (marker !== void 0 && marker !== "" && !isHex32(marker.toLowerCase())) {
|
|
119
|
-
|
|
117
|
+
mentions.push(pointer);
|
|
120
118
|
continue;
|
|
121
119
|
}
|
|
122
120
|
if (maybeParent) maybeRoot = pointer;
|
|
123
121
|
else maybeParent = pointer;
|
|
124
|
-
|
|
122
|
+
mentions.push(pointer);
|
|
125
123
|
continue;
|
|
126
124
|
}
|
|
127
125
|
if (tag[0] === "q") {
|
|
128
126
|
const quote = quoteFromQTag(tag);
|
|
129
|
-
if (quote)
|
|
127
|
+
if (quote) quotes.push(quote);
|
|
130
128
|
continue;
|
|
131
129
|
}
|
|
132
130
|
const pValue = tag.at(1);
|
|
133
|
-
if (tag[0] === "p" && pValue !== void 0 && isHex32(pValue.toLowerCase()))
|
|
131
|
+
if (tag[0] === "p" && pValue !== void 0 && isHex32(pValue.toLowerCase())) profiles.push({
|
|
134
132
|
pubkey: pValue.toLowerCase(),
|
|
135
133
|
relays: tag[2] !== void 0 && tag[2] !== "" ? [tag[2]] : []
|
|
136
134
|
});
|
|
137
135
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const drop = new Set([
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
result.reply,
|
|
144
|
-
result.root,
|
|
145
|
-
...result.mentions
|
|
146
|
-
]) {
|
|
147
|
-
if (ref === void 0) continue;
|
|
136
|
+
root ??= maybeRoot ?? maybeParent ?? reply;
|
|
137
|
+
reply ??= maybeParent ?? root;
|
|
138
|
+
const drop = new Set([root?.id, reply?.id].filter((id) => Boolean(id)));
|
|
139
|
+
const keptMentions = mentions.filter((m) => !drop.has(m.id));
|
|
140
|
+
const inheritHints = (ref) => {
|
|
148
141
|
const refAuthor = ref.author;
|
|
149
|
-
if (refAuthor === void 0 || refAuthor === "")
|
|
150
|
-
const profileRelays =
|
|
151
|
-
if (profileRelays === void 0 || profileRelays.length === 0)
|
|
142
|
+
if (refAuthor === void 0 || refAuthor === "") return ref;
|
|
143
|
+
const profileRelays = profiles.find((p) => p.pubkey === refAuthor)?.relays;
|
|
144
|
+
if (profileRelays === void 0 || profileRelays.length === 0) return ref;
|
|
152
145
|
const relays = [...ref.relays ?? []];
|
|
153
146
|
for (const url of profileRelays) if (!relays.includes(url)) relays.push(url);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
147
|
+
return {
|
|
148
|
+
...ref,
|
|
149
|
+
relays
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
root: root === void 0 ? void 0 : inheritHints(root),
|
|
154
|
+
reply: reply === void 0 ? void 0 : inheritHints(reply),
|
|
155
|
+
mentions: keptMentions.map(inheritHints),
|
|
156
|
+
quotes,
|
|
157
|
+
profiles
|
|
158
|
+
};
|
|
157
159
|
}
|
|
158
160
|
/**
|
|
159
161
|
* Build NIP-10 e/p tags for a reply to `parent`. Uses marked tags (`root` / `reply`) per preferred
|