@qntx/nostr 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{builder-CGgM_QHT.mjs → builder-70CC2EBq.mjs} +4 -4
- package/dist/{builder-CGgM_QHT.mjs.map → builder-70CC2EBq.mjs.map} +1 -1
- package/dist/{builder-C72nJITG.d.mts → builder-Dk00TdNf.d.mts} +2 -2
- package/dist/{client-COkEihj_.mjs → client-BXmvZ7vU.mjs} +45 -30
- package/dist/client-BXmvZ7vU.mjs.map +1 -0
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -2
- package/dist/core.d.mts +8 -7
- package/dist/core.mjs +8 -8
- package/dist/{error-Dyb89ahb.mjs → error-CoP2myne.mjs} +2 -2
- package/dist/{error-Dyb89ahb.mjs.map → error-CoP2myne.mjs.map} +1 -1
- package/dist/{error-BPqAU5RB.d.mts → error-DE1UCZ2J.d.mts} +6 -2
- package/dist/{error-BjSUsx6M.d.mts → error-IHMcLy8-.d.mts} +2 -2
- package/dist/{error-QyECUDIp.mjs → error-VDn7Nv-6.mjs} +2 -2
- package/dist/{error-QyECUDIp.mjs.map → error-VDn7Nv-6.mjs.map} +1 -1
- package/dist/{event-DvM1f03b.mjs → event-DHtcm0jM.mjs} +17 -6
- package/dist/event-DHtcm0jM.mjs.map +1 -0
- package/dist/{filter-3jOh4uLN.mjs → filter-BbShkmc4.mjs} +34 -28
- package/dist/filter-BbShkmc4.mjs.map +1 -0
- package/dist/{filter-oYG2zjJ3.d.mts → filter-w1R-mU63.d.mts} +8 -6
- package/dist/{gossip-BvCItMVZ.d.mts → gossip-C-7WhJ7v.d.mts} +3 -3
- package/dist/{gossip-CRyhK6E6.mjs → gossip-CejMm_QM.mjs} +13 -11
- package/dist/gossip-CejMm_QM.mjs.map +1 -0
- package/dist/gossip.d.mts +1 -1
- package/dist/gossip.mjs +1 -2
- package/dist/{index-B2JlP2Jf.d.mts → index-11znTLM0.d.mts} +4 -4
- package/dist/{index-DzHmhHqD.d.mts → index-B7nMjbyt.d.mts} +3 -3
- package/dist/{index-muydfuYD.d.mts → index-BuNI8JH_.d.mts} +13 -13
- package/dist/{index-Dpj_MUxb.d.mts → index-C5QomO2D.d.mts} +10 -3
- package/dist/{index-DCobP2Jj.d.mts → index-DDZjqV48.d.mts} +10 -10
- package/dist/{index-CcgmG3F6.d.mts → index-DYk24u9o.d.mts} +12 -7
- package/dist/index.d.mts +21 -20
- package/dist/index.mjs +19 -19
- package/dist/{key-D_oEutQ6.mjs → key-Bt0ow7mI.mjs} +33 -11
- package/dist/key-Bt0ow7mI.mjs.map +1 -0
- package/dist/{key-DojW2QZl.d.mts → key-E0AJKMRB.d.mts} +19 -11
- package/dist/{loaders-BRh2riV9.mjs → loaders-COGGrZY4.mjs} +7 -7
- package/dist/loaders-COGGrZY4.mjs.map +1 -0
- package/dist/loaders.d.mts +2 -2
- package/dist/loaders.mjs +1 -2
- package/dist/{memory-D_GdEQYG.mjs → memory-Dq4dg-U4.mjs} +2 -2
- package/dist/{memory-D_GdEQYG.mjs.map → memory-Dq4dg-U4.mjs.map} +1 -1
- package/dist/{memory-index-3GSrPIWj.mjs → memory-index-DhlmbqWx.mjs} +19 -18
- package/dist/memory-index-DhlmbqWx.mjs.map +1 -0
- package/dist/{message-BMfreCQc.mjs → message-D6k16gtg.mjs} +20 -20
- package/dist/message-D6k16gtg.mjs.map +1 -0
- package/dist/{verifier-DhAbqVJF.d.mts → message-u_7KOq6Q.d.mts} +3 -22
- package/dist/{nip17-BZvFQLcs.d.mts → nip17--YwFSuph.d.mts} +4 -4
- package/dist/{nip19-D4RKn6GC.d.mts → nip19-qtyc5sCl.d.mts} +2 -2
- package/dist/{nip46-i8L8_nIm.d.mts → nip46-C_ZFQprM.d.mts} +2 -2
- package/dist/{nip51-C_kTYcud.d.mts → nip51-yi9w8FsM.d.mts} +2 -2
- package/dist/{nip59-DfJ6Moeu.d.mts → nip59-B6Ey8L9z.d.mts} +12 -3
- package/dist/{nip65-CPNE0SV0.d.mts → nip65-XrBjV9WM.d.mts} +2 -2
- package/dist/{nip77-DhpoL7d8.d.mts → nip77-BAXijIt_.d.mts} +2 -2
- package/dist/nips/blossom.d.mts +3 -2
- package/dist/nips/blossom.mjs +11 -11
- package/dist/nips/blossom.mjs.map +1 -1
- package/dist/nips/nip04.d.mts +3 -2
- package/dist/nips/nip04.mjs +10 -12
- package/dist/nips/nip04.mjs.map +1 -1
- package/dist/nips/nip05.d.mts +2 -2
- package/dist/nips/nip05.mjs +9 -8
- package/dist/nips/nip05.mjs.map +1 -1
- package/dist/nips/nip10.d.mts +3 -2
- package/dist/nips/nip10.mjs +19 -19
- package/dist/nips/nip10.mjs.map +1 -1
- package/dist/nips/nip11.d.mts +2 -1
- package/dist/nips/nip11.mjs +6 -4
- package/dist/nips/nip11.mjs.map +1 -1
- package/dist/nips/nip13.d.mts +1 -1
- package/dist/nips/nip13.mjs +2 -2
- package/dist/nips/nip13.mjs.map +1 -1
- package/dist/nips/nip17.d.mts +1 -1
- package/dist/nips/nip17.mjs +6 -6
- package/dist/nips/nip17.mjs.map +1 -1
- package/dist/nips/nip19.d.mts +1 -1
- package/dist/nips/nip19.mjs +1 -1
- package/dist/nips/nip21.d.mts +2 -2
- package/dist/nips/nip21.mjs +3 -3
- package/dist/nips/nip21.mjs.map +1 -1
- package/dist/nips/nip27.d.mts +3 -3
- package/dist/nips/nip27.mjs +308 -85
- package/dist/nips/nip27.mjs.map +1 -1
- package/dist/nips/nip42.mjs +1 -1
- package/dist/nips/nip44.d.mts +2 -1
- package/dist/nips/nip44.mjs +4 -4
- package/dist/nips/nip44.mjs.map +1 -1
- package/dist/nips/nip46.d.mts +1 -1
- package/dist/nips/nip46.mjs +9 -8
- package/dist/nips/nip46.mjs.map +1 -1
- package/dist/nips/nip49.d.mts +1 -1
- package/dist/nips/nip49.mjs +1 -1
- package/dist/nips/nip51.d.mts +1 -1
- package/dist/nips/nip51.mjs +14 -14
- package/dist/nips/nip51.mjs.map +1 -1
- package/dist/nips/nip57.mjs +10 -10
- package/dist/nips/nip57.mjs.map +1 -1
- package/dist/nips/nip59.d.mts +1 -1
- package/dist/nips/nip59.mjs +14 -11
- package/dist/nips/nip59.mjs.map +1 -1
- package/dist/nips/nip65.d.mts +1 -1
- package/dist/nips/nip65.mjs +2 -2
- package/dist/nips/nip77.d.mts +1 -1
- package/dist/nips/nip77.mjs +2 -2
- package/dist/nips/nip77.mjs.map +1 -1
- package/dist/nips/nip98.d.mts +1 -1
- package/dist/nips/nip98.mjs +5 -5
- package/dist/nips/nip98.mjs.map +1 -1
- package/dist/{pool-MehgIIzm.d.mts → pool-BMfTQZjM.d.mts} +5 -4
- package/dist/{pool-lMT_mvzn.mjs → pool-D13eVCqc.mjs} +5 -5
- package/dist/{pool-lMT_mvzn.mjs.map → pool-D13eVCqc.mjs.map} +1 -1
- package/dist/{put-BEM5j8xK.mjs → put-DuymazkT.mjs} +5 -5
- package/dist/put-DuymazkT.mjs.map +1 -0
- package/dist/{reactive-B7lPFgTS.d.mts → reactive-BkzOsSwU.d.mts} +3 -3
- package/dist/{reactive-DFHY-f00.mjs → reactive-C0-rTqN9.mjs} +10 -9
- package/dist/reactive-C0-rTqN9.mjs.map +1 -0
- package/dist/{relay-DIR77xQ2.mjs → relay-CbgToreY.mjs} +6 -6
- package/dist/{relay-DIR77xQ2.mjs.map → relay-CbgToreY.mjs.map} +1 -1
- package/dist/relay.d.mts +3 -3
- package/dist/relay.mjs +2 -2
- package/dist/{signer-Bha9gQy8.mjs → signer-DQzjm3pt.mjs} +28 -14
- package/dist/signer-DQzjm3pt.mjs.map +1 -0
- package/dist/signer.d.mts +2 -2
- package/dist/signer.mjs +2 -2
- package/dist/{storage-n2BE1bdY.mjs → storage-DvDAuxzC.mjs} +26 -41
- package/dist/storage-DvDAuxzC.mjs.map +1 -0
- package/dist/storage.d.mts +3 -3
- package/dist/storage.mjs +4 -4
- package/dist/store.d.mts +1 -1
- package/dist/store.mjs +1 -2
- package/dist/testing.d.mts +2 -2
- package/dist/testing.mjs +7 -7
- package/dist/{types-DiVVyf1e.d.mts → types-COupAqtN.d.mts} +2 -2
- package/dist/{types-BJ4wZ7JJ.d.mts → types-DF8yT6EG.d.mts} +7 -1
- package/dist/{util-DfsyJ9nz.mjs → util-BcRBO6o-.mjs} +18 -3
- package/dist/util-BcRBO6o-.mjs.map +1 -0
- package/dist/{verifier-DEHUy4AD.mjs → verifier-DBSPHzGg.mjs} +6 -15
- package/dist/verifier-DBSPHzGg.mjs.map +1 -0
- package/dist/verifier-Or2nDvHb.d.mts +16 -0
- package/package.json +3 -3
- package/dist/client-COkEihj_.mjs.map +0 -1
- package/dist/event-DvM1f03b.mjs.map +0 -1
- package/dist/filter-3jOh4uLN.mjs.map +0 -1
- package/dist/gossip-CRyhK6E6.mjs.map +0 -1
- package/dist/key-D_oEutQ6.mjs.map +0 -1
- package/dist/loaders-BRh2riV9.mjs.map +0 -1
- package/dist/memory-index-3GSrPIWj.mjs.map +0 -1
- package/dist/message-BMfreCQc.mjs.map +0 -1
- package/dist/put-BEM5j8xK.mjs.map +0 -1
- package/dist/reactive-DFHY-f00.mjs.map +0 -1
- package/dist/signer-Bha9gQy8.mjs.map +0 -1
- package/dist/storage-n2BE1bdY.mjs.map +0 -1
- package/dist/util-DfsyJ9nz.mjs.map +0 -1
- package/dist/verifier-DEHUy4AD.mjs.map +0 -1
|
@@ -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 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 eslint/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 eslint/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":"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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 {\n assertHex32,\n bytesToHex,\n isHex32,\n isRecord,\n nowSeconds,\n utf8Encoder,\n trimAscii,\n} 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}/g);\n if (matches === null || matches.length === 0) {\n return undefined;\n }\n return matches.at(-1);\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;\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 && trimAscii(opts.message) === \"\") {\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 eslint/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 eslint/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.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 === undefined ? undefined : trimAscii(reason);\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\nexport function 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\" || !isHex32(raw[\"sha256\"])) {\n throw new BlossomError(\"blob descriptor missing sha256\");\n }\n const hash = raw[\"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":"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"}
|
package/dist/nips/nip04.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as NostrBackend } from "../key-E0AJKMRB.mjs";
|
|
1
2
|
//#region src/nips/nip04.d.ts
|
|
2
3
|
/**
|
|
3
4
|
* Encrypt plaintext with the raw 32-byte ECDH x-coordinate (NIP-04's key material, like NIP-44's
|
|
@@ -7,8 +8,8 @@ export declare function encrypt(plaintext: string, sharedSecret: Uint8Array): st
|
|
|
7
8
|
/** Decrypt a NIP-04 payload with the raw 32-byte ECDH x-coordinate. */
|
|
8
9
|
export declare function decrypt(payload: string, sharedSecret: Uint8Array): string;
|
|
9
10
|
/** Encrypt plaintext to a peer pubkey, deriving the shared secret from `secretKey`. */
|
|
10
|
-
export declare function encryptToPubkey(plaintext: string, secretKey: Uint8Array, peerPubkey: string): string;
|
|
11
|
+
export declare function encryptToPubkey(plaintext: string, secretKey: Uint8Array, peerPubkey: string, backend?: NostrBackend): string;
|
|
11
12
|
/** Decrypt a NIP-04 payload from a peer pubkey, deriving the shared secret from `secretKey`. */
|
|
12
|
-
export declare function decryptFromPubkey(payload: string, secretKey: Uint8Array, peerPubkey: string): string;
|
|
13
|
+
export declare function decryptFromPubkey(payload: string, secretKey: Uint8Array, peerPubkey: string, backend?: NostrBackend): string;
|
|
13
14
|
//#endregion
|
|
14
15
|
//# sourceMappingURL=nip04.d.mts.map
|
package/dist/nips/nip04.mjs
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import { _ as
|
|
1
|
+
import { _ as utf8Decoder, d as lowerAscii, n as assertHex32, r as assertSecretKeyBytes, s as hexToBytes$1, t as assertByteLength, v as utf8Encoder, y as CryptoError } from "../util-BcRBO6o-.mjs";
|
|
2
|
+
import { a as nobleBackend } from "../key-Bt0ow7mI.mjs";
|
|
2
3
|
import { randomBytes } from "@noble/hashes/utils.js";
|
|
3
|
-
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
4
4
|
import { cbc } from "@noble/ciphers/aes.js";
|
|
5
5
|
import { base64 } from "@scure/base";
|
|
6
6
|
//#region src/nips/nip04.ts
|
|
7
7
|
/** NIP-04 legacy encrypted direct messages. Prefer NIP-44 for new applications. */
|
|
8
|
-
function normalizeSharedSecret(privkey, pubkey) {
|
|
8
|
+
function normalizeSharedSecret(privkey, pubkey, backend) {
|
|
9
9
|
assertHex32(pubkey, "public key");
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
throw new CryptoError("invalid NIP-04 public key", { cause: error });
|
|
14
|
-
}
|
|
10
|
+
const sharedX = backend.ecdhX(privkey, hexToBytes$1(lowerAscii(pubkey)));
|
|
11
|
+
if (sharedX.length !== 32) throw new CryptoError("invalid NIP-04 shared secret");
|
|
12
|
+
return sharedX;
|
|
15
13
|
}
|
|
16
14
|
/**
|
|
17
15
|
* Encrypt plaintext with the raw 32-byte ECDH x-coordinate (NIP-04's key material, like NIP-44's
|
|
@@ -38,9 +36,9 @@ function decrypt(payload, sharedSecret) {
|
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
38
|
/** Encrypt plaintext to a peer pubkey, deriving the shared secret from `secretKey`. */
|
|
41
|
-
function encryptToPubkey(plaintext, secretKey, peerPubkey) {
|
|
39
|
+
function encryptToPubkey(plaintext, secretKey, peerPubkey, backend = nobleBackend) {
|
|
42
40
|
assertSecretKeyBytes(secretKey);
|
|
43
|
-
const sharedSecret = normalizeSharedSecret(secretKey, peerPubkey);
|
|
41
|
+
const sharedSecret = normalizeSharedSecret(secretKey, peerPubkey, backend);
|
|
44
42
|
try {
|
|
45
43
|
return encrypt(plaintext, sharedSecret);
|
|
46
44
|
} finally {
|
|
@@ -48,10 +46,10 @@ function encryptToPubkey(plaintext, secretKey, peerPubkey) {
|
|
|
48
46
|
}
|
|
49
47
|
}
|
|
50
48
|
/** Decrypt a NIP-04 payload from a peer pubkey, deriving the shared secret from `secretKey`. */
|
|
51
|
-
function decryptFromPubkey(payload, secretKey, peerPubkey) {
|
|
49
|
+
function decryptFromPubkey(payload, secretKey, peerPubkey, backend = nobleBackend) {
|
|
52
50
|
assertSecretKeyBytes(secretKey);
|
|
53
51
|
splitPayload(payload);
|
|
54
|
-
const sharedSecret = normalizeSharedSecret(secretKey, peerPubkey);
|
|
52
|
+
const sharedSecret = normalizeSharedSecret(secretKey, peerPubkey, backend);
|
|
55
53
|
try {
|
|
56
54
|
return decrypt(payload, sharedSecret);
|
|
57
55
|
} finally {
|
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 {
|
|
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 { randomBytes } from \"@noble/hashes/utils.js\";\nimport { base64 } from \"@scure/base\";\n\nimport { CryptoError } from \"../core/error.ts\";\nimport { nobleBackend } from \"../core/key.ts\";\nimport type { NostrBackend } from \"../core/key.ts\";\nimport {\n assertByteLength,\n assertHex32,\n assertSecretKeyBytes,\n hexToBytes,\n utf8Decoder,\n utf8Encoder,\n lowerAscii,\n} from \"../core/util.ts\";\n\nfunction normalizeSharedSecret(\n privkey: Uint8Array,\n pubkey: string,\n backend: NostrBackend,\n): Uint8Array {\n assertHex32(pubkey, \"public key\");\n const sharedX = backend.ecdhX(privkey, hexToBytes(lowerAscii(pubkey)));\n if (sharedX.length !== 32) {\n throw new CryptoError(\"invalid NIP-04 shared secret\");\n }\n return sharedX;\n}\n\n/**\n * Encrypt plaintext with the raw 32-byte ECDH x-coordinate (NIP-04's key material, like NIP-44's\n * conversation key), for key holders that only expose ECDH ({@link NostrKeyOperations}).\n */\nexport function encrypt(plaintext: string, sharedSecret: Uint8Array): string {\n assertByteLength(sharedSecret, 32, \"shared secret\");\n const iv = randomBytes(16);\n const plaintextBytes = utf8Encoder.encode(plaintext);\n const ciphertext = cbc(sharedSecret, iv).encrypt(plaintextBytes);\n return `${base64.encode(ciphertext)}?iv=${base64.encode(iv)}`;\n}\n\n/** Decrypt a NIP-04 payload with the raw 32-byte ECDH x-coordinate. */\nexport function decrypt(payload: string, sharedSecret: Uint8Array): string {\n assertByteLength(sharedSecret, 32, \"shared secret\");\n const [ciphertextPart, ivPart] = splitPayload(payload);\n try {\n const iv = base64.decode(ivPart);\n const ciphertext = base64.decode(ciphertextPart);\n const plaintext = cbc(sharedSecret, iv).decrypt(ciphertext);\n return utf8Decoder.decode(plaintext);\n } catch (error) {\n throw new CryptoError(\"invalid NIP-04 payload\", { cause: error });\n }\n}\n\n/** Encrypt plaintext to a peer pubkey, deriving the shared secret from `secretKey`. */\nexport function encryptToPubkey(\n plaintext: string,\n secretKey: Uint8Array,\n peerPubkey: string,\n backend: NostrBackend = nobleBackend,\n): string {\n assertSecretKeyBytes(secretKey);\n const sharedSecret = normalizeSharedSecret(secretKey, peerPubkey, backend);\n try {\n return encrypt(plaintext, sharedSecret);\n } finally {\n sharedSecret.fill(0);\n }\n}\n\n/** Decrypt a NIP-04 payload from a peer pubkey, deriving the shared secret from `secretKey`. */\nexport function decryptFromPubkey(\n payload: string,\n secretKey: Uint8Array,\n peerPubkey: string,\n backend: NostrBackend = nobleBackend,\n): string {\n assertSecretKeyBytes(secretKey);\n splitPayload(payload);\n const sharedSecret = normalizeSharedSecret(secretKey, peerPubkey, backend);\n try {\n return decrypt(payload, sharedSecret);\n } finally {\n sharedSecret.fill(0);\n }\n}\n\nfunction splitPayload(data: string): [ciphertext: string, iv: string] {\n const parts = data.split(\"?iv=\");\n const [ciphertext, iv] = parts;\n if (\n parts.length !== 2 ||\n ciphertext === undefined ||\n ciphertext === \"\" ||\n iv === undefined ||\n iv === \"\"\n ) {\n throw new CryptoError(\"invalid NIP-04 payload: missing iv\");\n }\n return [ciphertext, iv];\n}\n"],"mappings":";;;;;;;AAkBA,SAAS,sBACP,SACA,QACA,SACY;CACZ,YAAY,QAAQ,YAAY;CAChC,MAAM,UAAU,QAAQ,MAAM,SAASA,aAAW,WAAW,MAAM,CAAC,CAAC;CACrE,IAAI,QAAQ,WAAW,IACrB,MAAM,IAAI,YAAY,8BAA8B;CAEtD,OAAO;AACT;;;;;AAMA,SAAgB,QAAQ,WAAmB,cAAkC;CAC3E,iBAAiB,cAAc,IAAI,eAAe;CAClD,MAAM,KAAK,YAAY,EAAE;CACzB,MAAM,iBAAiB,YAAY,OAAO,SAAS;CACnD,MAAM,aAAa,IAAI,cAAc,EAAE,CAAC,CAAC,QAAQ,cAAc;CAC/D,OAAO,GAAG,OAAO,OAAO,UAAU,EAAE,MAAM,OAAO,OAAO,EAAE;AAC5D;;AAGA,SAAgB,QAAQ,SAAiB,cAAkC;CACzE,iBAAiB,cAAc,IAAI,eAAe;CAClD,MAAM,CAAC,gBAAgB,UAAU,aAAa,OAAO;CACrD,IAAI;EACF,MAAM,KAAK,OAAO,OAAO,MAAM;EAC/B,MAAM,aAAa,OAAO,OAAO,cAAc;EAC/C,MAAM,YAAY,IAAI,cAAc,EAAE,CAAC,CAAC,QAAQ,UAAU;EAC1D,OAAO,YAAY,OAAO,SAAS;CACrC,SAAS,OAAO;EACd,MAAM,IAAI,YAAY,0BAA0B,EAAE,OAAO,MAAM,CAAC;CAClE;AACF;;AAGA,SAAgB,gBACd,WACA,WACA,YACA,UAAwB,cAChB;CACR,qBAAqB,SAAS;CAC9B,MAAM,eAAe,sBAAsB,WAAW,YAAY,OAAO;CACzE,IAAI;EACF,OAAO,QAAQ,WAAW,YAAY;CACxC,UAAU;EACR,aAAa,KAAK,CAAC;CACrB;AACF;;AAGA,SAAgB,kBACd,SACA,WACA,YACA,UAAwB,cAChB;CACR,qBAAqB,SAAS;CAC9B,aAAa,OAAO;CACpB,MAAM,eAAe,sBAAsB,WAAW,YAAY,OAAO;CACzE,IAAI;EACF,OAAO,QAAQ,SAAS,YAAY;CACtC,UAAU;EACR,aAAa,KAAK,CAAC;CACrB;AACF;AAEA,SAAS,aAAa,MAAgD;CACpE,MAAM,QAAQ,KAAK,MAAM,MAAM;CAC/B,MAAM,CAAC,YAAY,MAAM;CACzB,IACE,MAAM,WAAW,KACjB,eAAe,KAAA,KACf,eAAe,MACf,OAAO,KAAA,KACP,OAAO,IAEP,MAAM,IAAI,YAAY,oCAAoC;CAE5D,OAAO,CAAC,YAAY,EAAE;AACxB"}
|
package/dist/nips/nip05.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { f as ProfilePointer } from "../nip19-
|
|
1
|
+
import { o as NostrError } from "../error-DE1UCZ2J.mjs";
|
|
2
|
+
import { f as ProfilePointer } from "../nip19-qtyc5sCl.mjs";
|
|
3
3
|
import { t as ManualFetch } from "../http-kwWG-V1X.mjs";
|
|
4
4
|
//#region src/nips/nip05.d.ts
|
|
5
5
|
/** Root local-part (`_@domain` rendered as just the domain). */
|
package/dist/nips/nip05.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as isHex32, u as isRecord,
|
|
1
|
+
import { c as isHex32, d as lowerAscii, g as trimAscii, u as isRecord, w as NostrError } from "../util-BcRBO6o-.mjs";
|
|
2
2
|
import { n as requireGlobalFetch, t as fetchManual } from "../http-CuazNRIP.mjs";
|
|
3
3
|
//#region src/nips/nip05.ts
|
|
4
4
|
/**
|
|
@@ -31,12 +31,13 @@ function isNip05(value) {
|
|
|
31
31
|
* (local becomes `_`).
|
|
32
32
|
*/
|
|
33
33
|
function parseNip05(input) {
|
|
34
|
-
const match = NIP05_REGEX.exec(input
|
|
34
|
+
const match = NIP05_REGEX.exec(trimAscii(input));
|
|
35
35
|
const domain = match?.at(2);
|
|
36
36
|
if (match === null || domain === void 0 || domain === "") throw new Nip05Error(`invalid NIP-05 identifier: ${input}`);
|
|
37
|
+
const localRaw = match.at(1) ?? "_";
|
|
37
38
|
return {
|
|
38
|
-
local: (
|
|
39
|
-
domain: domain
|
|
39
|
+
local: lowerAscii(localRaw),
|
|
40
|
+
domain: lowerAscii(domain)
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
/** Build the well-known HTTPS URL for an address. */
|
|
@@ -69,17 +70,17 @@ function parseNip05Document(json) {
|
|
|
69
70
|
if (!isRecord(raw["names"])) throw new Nip05Error("NIP-05 document missing names map");
|
|
70
71
|
const names = {};
|
|
71
72
|
for (const [k, v] of Object.entries(raw["names"])) {
|
|
72
|
-
if (typeof v !== "string" || !isHex32(v
|
|
73
|
-
|
|
73
|
+
if (typeof v !== "string" || !isHex32(v)) continue;
|
|
74
|
+
if (/^[a-z0-9._-]+$/.test(k)) names[k] = v;
|
|
74
75
|
}
|
|
75
76
|
const rawRelays = raw["relays"];
|
|
76
77
|
let relays;
|
|
77
78
|
if (isRecord(rawRelays)) {
|
|
78
79
|
relays = {};
|
|
79
80
|
for (const [pk, list] of Object.entries(rawRelays)) {
|
|
80
|
-
if (!isHex32(pk
|
|
81
|
+
if (!isHex32(pk)) continue;
|
|
81
82
|
const urls = stringUrls(list);
|
|
82
|
-
if (urls !== void 0 && urls.length > 0) relays[pk
|
|
83
|
+
if (urls !== void 0 && urls.length > 0) relays[pk] = urls;
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
const doc = { names };
|
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 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/**\n * Matches optional local@domain. Groups: 1=local (optional), 2=domain. Case classes are written out\n * so matching is ASCII-only: the `i` flag would also fold U+212A (K) into `k`.\n */\n// oxlint-disable-next-line typescript/no-inferrable-types -- isolatedDeclarations requires the annotation for dts emit\nexport const NIP05_REGEX: RegExp = /^(?:([A-Za-z0-9._-]+)@)?([A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)+)$/;\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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"}
|
|
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, lowerAscii, trimAscii } 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/**\n * Matches optional local@domain. Groups: 1=local (optional), 2=domain. Case classes are written out\n * so matching is ASCII-only: the `i` flag would also fold U+212A (K) into `k`.\n */\n// oxlint-disable-next-line typescript/no-inferrable-types -- isolatedDeclarations requires the annotation for dts emit\nexport const NIP05_REGEX: RegExp = /^(?:([A-Za-z0-9._-]+)@)?([A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)+)$/;\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(trimAscii(input));\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: lowerAscii(localRaw), domain: lowerAscii(domain) };\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)) {\n continue;\n }\n if (/^[a-z0-9._-]+$/.test(k)) {\n names[k] = v;\n }\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)) {\n continue;\n }\n const urls = stringUrls(list);\n if (urls !== undefined && urls.length > 0) {\n relays[pk] = 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,6 +1,6 @@
|
|
|
1
1
|
import { h as Tag, t as Event } from "../event-j3BwI5dF.mjs";
|
|
2
|
-
import { t as EventBuilder } from "../builder-
|
|
3
|
-
import { f as ProfilePointer, i as EventPointer, t as AddressPointer } from "../nip19-
|
|
2
|
+
import { t as EventBuilder } from "../builder-Dk00TdNf.mjs";
|
|
3
|
+
import { f as ProfilePointer, i as EventPointer, t as AddressPointer } from "../nip19-qtyc5sCl.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 = {
|
|
@@ -39,4 +39,5 @@ export declare function buildReplyTags(opts: ReplyTagsOptions): Tag[];
|
|
|
39
39
|
*/
|
|
40
40
|
export declare function replyTo(parent: ReplyParent, content: string, opts?: Pick<ReplyTagsOptions, "relayHint" | "quotes">): EventBuilder;
|
|
41
41
|
//#endregion
|
|
42
|
+
export {};
|
|
42
43
|
//# sourceMappingURL=nip10.d.mts.map
|
package/dist/nips/nip10.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { c as isHex32,
|
|
1
|
+
import { b as EventValidationError, c as isHex32, d as lowerAscii } from "../util-BcRBO6o-.mjs";
|
|
2
2
|
import { t as Kind } from "../kind-I2Tvbe82.mjs";
|
|
3
|
-
import { S as parseEventAddress, g as Tag, y as formatEventAddress } from "../event-
|
|
4
|
-
import { t as EventBuilder } from "../builder-
|
|
3
|
+
import { C as parseEventAddressInput, S as parseEventAddress, g as Tag, y as formatEventAddress } from "../event-DHtcm0jM.mjs";
|
|
4
|
+
import { t as EventBuilder } from "../builder-70CC2EBq.mjs";
|
|
5
5
|
//#region src/nips/nip10.ts
|
|
6
6
|
/**
|
|
7
7
|
* NIP-10: Text Notes and Threads
|
|
@@ -9,13 +9,13 @@ import { t as EventBuilder } from "../builder-CGgM_QHT.mjs";
|
|
|
9
9
|
* @see https://github.com/nostr-protocol/nips/blob/master/10.md
|
|
10
10
|
*/
|
|
11
11
|
function eTagAuthor(tag) {
|
|
12
|
-
for (const author of [tag[4], tag[3]]) if (author !== void 0 && isHex32(author
|
|
12
|
+
for (const author of [tag[4], tag[3]]) if (author !== void 0 && isHex32(author)) return author;
|
|
13
13
|
}
|
|
14
14
|
function eventPointerFromETag(tag) {
|
|
15
15
|
const id = tag.at(1);
|
|
16
|
-
if (tag[0] !== "e" || id === void 0 || !isHex32(id
|
|
16
|
+
if (tag[0] !== "e" || id === void 0 || !isHex32(id)) return;
|
|
17
17
|
const pointer = {
|
|
18
|
-
id
|
|
18
|
+
id,
|
|
19
19
|
relays: tag[2] !== void 0 && tag[2] !== "" ? [tag[2]] : []
|
|
20
20
|
};
|
|
21
21
|
const author = eTagAuthor(tag);
|
|
@@ -26,13 +26,13 @@ function quoteFromQTag(tag) {
|
|
|
26
26
|
const value = tag.at(1);
|
|
27
27
|
if (tag[0] !== "q" || value === void 0 || value === "") return;
|
|
28
28
|
const relays = tag[2] !== void 0 && tag[2] !== "" ? [tag[2]] : [];
|
|
29
|
-
if (isHex32(value
|
|
29
|
+
if (isHex32(value)) {
|
|
30
30
|
const pointer = {
|
|
31
|
-
id: value
|
|
31
|
+
id: value,
|
|
32
32
|
relays
|
|
33
33
|
};
|
|
34
34
|
const author = tag.at(3);
|
|
35
|
-
if (author !== void 0 && isHex32(author
|
|
35
|
+
if (author !== void 0 && isHex32(author)) pointer.author = author;
|
|
36
36
|
return pointer;
|
|
37
37
|
}
|
|
38
38
|
const addr = parseEventAddress(value);
|
|
@@ -46,15 +46,15 @@ function quoteFromQTag(tag) {
|
|
|
46
46
|
}
|
|
47
47
|
function quoteToTag(quote) {
|
|
48
48
|
if (typeof quote === "string") {
|
|
49
|
-
if (isHex32(quote
|
|
50
|
-
const addr =
|
|
49
|
+
if (isHex32(lowerAscii(quote))) return { tag: ["q", lowerAscii(quote)] };
|
|
50
|
+
const addr = parseEventAddressInput(quote);
|
|
51
51
|
if (!addr) return;
|
|
52
52
|
return { tag: ["q", formatEventAddress(addr.kind, addr.pubkey, addr.identifier)] };
|
|
53
53
|
}
|
|
54
54
|
if ("id" in quote) {
|
|
55
|
-
const id = quote.id
|
|
55
|
+
const id = lowerAscii(quote.id);
|
|
56
56
|
const relay = quote.relays?.[0];
|
|
57
|
-
const author = quote.author !== void 0 && isHex32(quote.author
|
|
57
|
+
const author = quote.author !== void 0 && isHex32(lowerAscii(quote.author)) ? lowerAscii(quote.author) : void 0;
|
|
58
58
|
let tag;
|
|
59
59
|
if (author === void 0) tag = relay !== void 0 && relay !== "" ? [
|
|
60
60
|
"q",
|
|
@@ -101,7 +101,7 @@ function parseThreadTags(event) {
|
|
|
101
101
|
const tag = event.tags[i];
|
|
102
102
|
if (tag === void 0) continue;
|
|
103
103
|
const eValue = tag.at(1);
|
|
104
|
-
if (tag[0] === "e" && eValue !== void 0 && isHex32(eValue
|
|
104
|
+
if (tag[0] === "e" && eValue !== void 0 && isHex32(eValue)) {
|
|
105
105
|
const pointer = eventPointerFromETag(tag);
|
|
106
106
|
if (pointer === void 0) continue;
|
|
107
107
|
const marker = tag.at(3);
|
|
@@ -113,7 +113,7 @@ function parseThreadTags(event) {
|
|
|
113
113
|
reply = pointer;
|
|
114
114
|
continue;
|
|
115
115
|
}
|
|
116
|
-
if (marker !== void 0 && marker !== "" && !isHex32(marker
|
|
116
|
+
if (marker !== void 0 && marker !== "" && !isHex32(marker)) {
|
|
117
117
|
mentions.push(pointer);
|
|
118
118
|
continue;
|
|
119
119
|
}
|
|
@@ -128,8 +128,8 @@ function parseThreadTags(event) {
|
|
|
128
128
|
continue;
|
|
129
129
|
}
|
|
130
130
|
const pValue = tag.at(1);
|
|
131
|
-
if (tag[0] === "p" && pValue !== void 0 && isHex32(pValue
|
|
132
|
-
pubkey: pValue
|
|
131
|
+
if (tag[0] === "p" && pValue !== void 0 && isHex32(pValue)) profiles.push({
|
|
132
|
+
pubkey: pValue,
|
|
133
133
|
relays: tag[2] !== void 0 && tag[2] !== "" ? [tag[2]] : []
|
|
134
134
|
});
|
|
135
135
|
}
|
|
@@ -169,14 +169,14 @@ function buildReplyTags(opts) {
|
|
|
169
169
|
relays: [],
|
|
170
170
|
author: opts.parent.pubkey
|
|
171
171
|
};
|
|
172
|
-
const parentIsRoot = root.id
|
|
172
|
+
const parentIsRoot = lowerAscii(root.id) === lowerAscii(opts.parent.id);
|
|
173
173
|
const tags = [];
|
|
174
174
|
const rootRelay = root.relays?.[0] ?? opts.relayHint ?? "";
|
|
175
175
|
tags.push(Tag.e(root.id, rootRelay, "root", root.author));
|
|
176
176
|
if (!parentIsRoot) tags.push(Tag.e(opts.parent.id, opts.relayHint ?? "", "reply", opts.parent.pubkey));
|
|
177
177
|
const pSeen = /* @__PURE__ */ new Set();
|
|
178
178
|
const addP = (pk, relay) => {
|
|
179
|
-
const key = pk
|
|
179
|
+
const key = lowerAscii(pk);
|
|
180
180
|
if (pSeen.has(key)) return;
|
|
181
181
|
pSeen.add(key);
|
|
182
182
|
tags.push(Tag.p(pk, relay ?? void 0));
|
package/dist/nips/nip10.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nip10.mjs","names":[],"sources":["../../src/nips/nip10.ts"],"sourcesContent":["/**\n * NIP-10: Text Notes and Threads\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/10.md\n */\nimport { EventBuilder } from \"../core/builder.ts\";\nimport { EventValidationError } from \"../core/error.ts\";\nimport type { Event } from \"../core/event.ts\";\nimport { Kind } from \"../core/kind.ts\";\nimport { formatEventAddress, parseEventAddress, Tag } from \"../core/tag.ts\";\nimport type { Mutable } from \"../core/util.ts\";\nimport { isHex32 } from \"../core/util.ts\";\nimport type { AddressPointer, EventPointer, ProfilePointer } from \"./nip19.ts\";\n\n/** Parsed NIP-10 thread references from an event's `e`/`q`/`p` tags. */\nexport type ThreadReferences = {\n /** Pointer to the root of the thread. */\n readonly root: EventPointer | undefined;\n /** Pointer to the parent event this note replies to. */\n readonly reply: EventPointer | undefined;\n /** Other e-tagged events (not root/reply). */\n readonly mentions: ReadonlyArray<EventPointer>;\n /** Quoted events (`q` tags): event ids or addresses. Discriminate with `\"id\" in q`. */\n readonly quotes: ReadonlyArray<EventPointer | AddressPointer>;\n /** P-tagged profiles involved in the thread. */\n readonly profiles: ReadonlyArray<ProfilePointer>;\n};\n\ntype ReplyParent = Pick<Event, \"id\" | \"pubkey\" | \"tags\" | \"kind\">;\ntype QuoteInput = string | EventPointer | AddressPointer;\n\nfunction eTagAuthor(tag: ReadonlyArray<string>): string | undefined {\n // NIP-10 5-tuple pubkey is index 4; NIP-01 4-tuple pubkey is index 3.\n for (const author of [tag[4], tag[3]]) {\n if (author !== undefined && isHex32(author.toLowerCase())) {\n return author.toLowerCase();\n }\n }\n return undefined;\n}\n\nfunction eventPointerFromETag(tag: ReadonlyArray<string>): EventPointer | undefined {\n const id = tag.at(1);\n if (tag[0] !== \"e\" || id === undefined || !isHex32(id.toLowerCase())) {\n return undefined;\n }\n const pointer: Mutable<EventPointer> = {\n id: id.toLowerCase(),\n relays: tag[2] !== undefined && tag[2] !== \"\" ? [tag[2]] : [],\n };\n const author = eTagAuthor(tag);\n if (author !== undefined) {\n pointer.author = author;\n }\n return pointer;\n}\n\nfunction quoteFromQTag(tag: ReadonlyArray<string>): EventPointer | AddressPointer | undefined {\n const value = tag.at(1);\n if (tag[0] !== \"q\" || value === undefined || value === \"\") {\n return undefined;\n }\n const relays = tag[2] !== undefined && tag[2] !== \"\" ? [tag[2]] : [];\n if (isHex32(value.toLowerCase())) {\n const pointer: Mutable<EventPointer> = { id: value.toLowerCase(), relays };\n const author = tag.at(3);\n if (author !== undefined && isHex32(author.toLowerCase())) {\n pointer.author = author.toLowerCase();\n }\n return pointer;\n }\n const addr = parseEventAddress(value);\n if (!addr) {\n return undefined;\n }\n // Address q tags do not use the event-id pubkey slot (index 3).\n return {\n identifier: addr.identifier,\n pubkey: addr.pubkey,\n kind: addr.kind,\n relays,\n };\n}\n\ntype BuiltQuote = { tag: Tag; author?: string | undefined; relay?: string | undefined };\n\nfunction quoteToTag(quote: QuoteInput): BuiltQuote | undefined {\n if (typeof quote === \"string\") {\n if (isHex32(quote.toLowerCase())) {\n return { tag: [\"q\", quote.toLowerCase()] };\n }\n const addr = parseEventAddress(quote);\n if (!addr) {\n return undefined;\n }\n return { tag: [\"q\", formatEventAddress(addr.kind, addr.pubkey, addr.identifier)] };\n }\n if (\"id\" in quote) {\n const id = quote.id.toLowerCase();\n const relay = quote.relays?.[0];\n const author =\n quote.author !== undefined && isHex32(quote.author.toLowerCase())\n ? quote.author.toLowerCase()\n : undefined;\n let tag: Tag;\n if (author === undefined) {\n tag = relay !== undefined && relay !== \"\" ? [\"q\", id, relay] : [\"q\", id];\n } else {\n tag = [\"q\", id, relay ?? \"\", author];\n }\n return { tag, author, relay };\n }\n const coord = formatEventAddress(quote.kind, quote.pubkey, quote.identifier);\n const relay = quote.relays?.[0];\n const tag: Tag = relay !== undefined && relay !== \"\" ? [\"q\", coord, relay] : [\"q\", coord];\n return { tag, author: quote.pubkey, relay };\n}\n\nfunction assertKind1Parent(parent: ReplyParent): void {\n // NIP-10 is kind 1 only; comments are NIP-22.\n if (parent.kind !== Kind.TextNote) {\n throw new EventValidationError(\"NIP-10 replyTo is for kind 1\");\n }\n}\n\n/** Parse NIP-10 thread markers and legacy positional e-tags from an event. */\nexport function parseThreadTags(event: Pick<Event, \"tags\">): ThreadReferences {\n const mentions: EventPointer[] = [];\n const quotes: Array<EventPointer | AddressPointer> = [];\n const profiles: ProfilePointer[] = [];\n let root: EventPointer | undefined;\n let reply: EventPointer | undefined;\n\n let maybeParent: EventPointer | undefined;\n let maybeRoot: EventPointer | undefined;\n\n for (let i = event.tags.length - 1; i >= 0; i--) {\n const tag = event.tags[i];\n if (tag === undefined) {\n continue;\n }\n\n const eValue = tag.at(1);\n if (tag[0] === \"e\" && eValue !== undefined && isHex32(eValue.toLowerCase())) {\n const pointer = eventPointerFromETag(tag);\n if (pointer === undefined) {\n continue;\n }\n const marker = tag.at(3);\n\n if (marker === \"root\") {\n root = pointer;\n continue;\n }\n if (marker === \"reply\") {\n reply = pointer;\n continue;\n }\n // Preferred markers are root/reply only. A hex32 at index 3 is NIP-01 pubkey, not a marker.\n if (marker !== undefined && marker !== \"\" && !isHex32(marker.toLowerCase())) {\n mentions.push(pointer);\n continue;\n }\n\n // Legacy positional: last unmarked is parent, second-to-last is root.\n if (maybeParent) {\n maybeRoot = pointer;\n } else {\n maybeParent = pointer;\n }\n mentions.push(pointer);\n continue;\n }\n\n if (tag[0] === \"q\") {\n const quote = quoteFromQTag(tag);\n if (quote) {\n quotes.push(quote);\n }\n continue;\n }\n\n const pValue = tag.at(1);\n if (tag[0] === \"p\" && pValue !== undefined && isHex32(pValue.toLowerCase())) {\n profiles.push({\n pubkey: pValue.toLowerCase(),\n relays: tag[2] !== undefined && tag[2] !== \"\" ? [tag[2]] : [],\n });\n }\n }\n\n root ??= maybeRoot ?? maybeParent ?? reply;\n reply ??= maybeParent ?? root;\n\n // Drop root/reply from mentions (by id).\n const drop = new Set([root?.id, reply?.id].filter((id): id is string => Boolean(id)));\n const keptMentions = mentions.filter((m) => !drop.has(m.id));\n\n // Inherit relay hints from matching p-tags.\n const inheritHints = (ref: EventPointer): EventPointer => {\n const refAuthor = ref.author;\n if (refAuthor === undefined || refAuthor === \"\") {\n return ref;\n }\n const profileRelays = profiles.find((p) => p.pubkey === refAuthor)?.relays;\n if (profileRelays === undefined || profileRelays.length === 0) {\n return ref;\n }\n const relays = [...(ref.relays ?? [])];\n for (const url of profileRelays) {\n if (!relays.includes(url)) {\n relays.push(url);\n }\n }\n return { ...ref, relays };\n };\n\n return {\n root: root === undefined ? undefined : inheritHints(root),\n reply: reply === undefined ? undefined : inheritHints(reply),\n mentions: keptMentions.map(inheritHints),\n quotes,\n profiles,\n };\n}\n\n/** Options for {@link buildReplyTags}. */\nexport type ReplyTagsOptions = {\n /** Parent event being replied to. */\n parent: ReplyParent;\n /** Optional relay hint for the parent e-tag. */\n relayHint?: string | undefined;\n /** Quoted events (`q` tags): hex ids, `kind:pubkey:d` coords, or pointers. */\n quotes?: QuoteInput[] | undefined;\n};\n\n/**\n * Build NIP-10 e/p tags for a reply to `parent`. Uses marked tags (`root` / `reply`) per preferred\n * modern style.\n */\nexport function buildReplyTags(opts: ReplyTagsOptions): Tag[] {\n assertKind1Parent(opts.parent);\n const thread = parseThreadTags(opts.parent);\n const root = thread.root ?? { id: opts.parent.id, relays: [], author: opts.parent.pubkey };\n const parentIsRoot = root.id.toLowerCase() === opts.parent.id.toLowerCase();\n\n const tags: Tag[] = [];\n const rootRelay = root.relays?.[0] ?? opts.relayHint ?? \"\";\n tags.push(Tag.e(root.id, rootRelay, \"root\", root.author));\n\n if (!parentIsRoot) {\n tags.push(Tag.e(opts.parent.id, opts.relayHint ?? \"\", \"reply\", opts.parent.pubkey));\n }\n\n // Ensure root + parent authors are p-tagged.\n const pSeen = new Set<string>();\n const addP = (pk: string, relay?: string) => {\n const key = pk.toLowerCase();\n if (pSeen.has(key)) {\n return;\n }\n pSeen.add(key);\n tags.push(Tag.p(pk, relay ?? undefined));\n };\n if (root.author !== undefined && root.author !== \"\") {\n addP(root.author, root.relays?.[0]);\n }\n addP(opts.parent.pubkey, opts.relayHint);\n for (const p of thread.profiles) {\n addP(p.pubkey, p.relays?.[0]);\n }\n\n const qTags: Tag[] = [];\n for (const quote of opts.quotes ?? []) {\n const built = quoteToTag(quote);\n if (built === undefined) {\n continue;\n }\n if (built.author !== undefined && built.author !== \"\") {\n addP(built.author, built.relay);\n }\n qTags.push(built.tag);\n }\n tags.push(...qTags);\n\n return tags;\n}\n\n/**\n * Build an {@link EventBuilder} reply with NIP-10 tags. Lives here (not on EventBuilder) so core\n * does not depend on nips.\n */\nexport function replyTo(\n parent: ReplyParent,\n content: string,\n opts?: Pick<ReplyTagsOptions, \"relayHint\" | \"quotes\">,\n): EventBuilder {\n return EventBuilder.textNote(content).tags(\n buildReplyTags({\n parent,\n relayHint: opts?.relayHint,\n quotes: opts?.quotes,\n }),\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"nip10.mjs","names":[],"sources":["../../src/nips/nip10.ts"],"sourcesContent":["/**\n * NIP-10: Text Notes and Threads\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/10.md\n */\nimport { EventBuilder } from \"../core/builder.ts\";\nimport { EventValidationError } from \"../core/error.ts\";\nimport type { Event } from \"../core/event.ts\";\nimport { Kind } from \"../core/kind.ts\";\nimport { formatEventAddress, parseEventAddress, parseEventAddressInput, Tag } from \"../core/tag.ts\";\nimport type { Mutable } from \"../core/util.ts\";\nimport { isHex32, lowerAscii } from \"../core/util.ts\";\nimport type { AddressPointer, EventPointer, ProfilePointer } from \"./nip19.ts\";\n\n/** Parsed NIP-10 thread references from an event's `e`/`q`/`p` tags. */\nexport type ThreadReferences = {\n /** Pointer to the root of the thread. */\n readonly root: EventPointer | undefined;\n /** Pointer to the parent event this note replies to. */\n readonly reply: EventPointer | undefined;\n /** Other e-tagged events (not root/reply). */\n readonly mentions: ReadonlyArray<EventPointer>;\n /** Quoted events (`q` tags): event ids or addresses. Discriminate with `\"id\" in q`. */\n readonly quotes: ReadonlyArray<EventPointer | AddressPointer>;\n /** P-tagged profiles involved in the thread. */\n readonly profiles: ReadonlyArray<ProfilePointer>;\n};\n\ntype ReplyParent = Pick<Event, \"id\" | \"pubkey\" | \"tags\" | \"kind\">;\ntype QuoteInput = string | EventPointer | AddressPointer;\n\nfunction eTagAuthor(tag: ReadonlyArray<string>): string | undefined {\n // NIP-10 5-tuple pubkey is index 4; NIP-01 4-tuple pubkey is index 3.\n for (const author of [tag[4], tag[3]]) {\n if (author !== undefined && isHex32(author)) {\n return author;\n }\n }\n return undefined;\n}\n\nfunction eventPointerFromETag(tag: ReadonlyArray<string>): EventPointer | undefined {\n const id = tag.at(1);\n if (tag[0] !== \"e\" || id === undefined || !isHex32(id)) {\n return undefined;\n }\n const pointer: Mutable<EventPointer> = {\n id,\n relays: tag[2] !== undefined && tag[2] !== \"\" ? [tag[2]] : [],\n };\n const author = eTagAuthor(tag);\n if (author !== undefined) {\n pointer.author = author;\n }\n return pointer;\n}\n\nfunction quoteFromQTag(tag: ReadonlyArray<string>): EventPointer | AddressPointer | undefined {\n const value = tag.at(1);\n if (tag[0] !== \"q\" || value === undefined || value === \"\") {\n return undefined;\n }\n const relays = tag[2] !== undefined && tag[2] !== \"\" ? [tag[2]] : [];\n if (isHex32(value)) {\n const pointer: Mutable<EventPointer> = { id: value, relays };\n const author = tag.at(3);\n if (author !== undefined && isHex32(author)) {\n pointer.author = author;\n }\n return pointer;\n }\n const addr = parseEventAddress(value);\n if (!addr) {\n return undefined;\n }\n // Address q tags do not use the event-id pubkey slot (index 3).\n return {\n identifier: addr.identifier,\n pubkey: addr.pubkey,\n kind: addr.kind,\n relays,\n };\n}\n\ntype BuiltQuote = { tag: Tag; author?: string | undefined; relay?: string | undefined };\n\nfunction quoteToTag(quote: QuoteInput): BuiltQuote | undefined {\n if (typeof quote === \"string\") {\n if (isHex32(lowerAscii(quote))) {\n return { tag: [\"q\", lowerAscii(quote)] };\n }\n const addr = parseEventAddressInput(quote);\n if (!addr) {\n return undefined;\n }\n return { tag: [\"q\", formatEventAddress(addr.kind, addr.pubkey, addr.identifier)] };\n }\n if (\"id\" in quote) {\n const id = lowerAscii(quote.id);\n const relay = quote.relays?.[0];\n const author =\n quote.author !== undefined && isHex32(lowerAscii(quote.author))\n ? lowerAscii(quote.author)\n : undefined;\n let tag: Tag;\n if (author === undefined) {\n tag = relay !== undefined && relay !== \"\" ? [\"q\", id, relay] : [\"q\", id];\n } else {\n tag = [\"q\", id, relay ?? \"\", author];\n }\n return { tag, author, relay };\n }\n const coord = formatEventAddress(quote.kind, quote.pubkey, quote.identifier);\n const relay = quote.relays?.[0];\n const tag: Tag = relay !== undefined && relay !== \"\" ? [\"q\", coord, relay] : [\"q\", coord];\n return { tag, author: quote.pubkey, relay };\n}\n\nfunction assertKind1Parent(parent: ReplyParent): void {\n // NIP-10 is kind 1 only; comments are NIP-22.\n if (parent.kind !== Kind.TextNote) {\n throw new EventValidationError(\"NIP-10 replyTo is for kind 1\");\n }\n}\n\n/** Parse NIP-10 thread markers and legacy positional e-tags from an event. */\nexport function parseThreadTags(event: Pick<Event, \"tags\">): ThreadReferences {\n const mentions: EventPointer[] = [];\n const quotes: Array<EventPointer | AddressPointer> = [];\n const profiles: ProfilePointer[] = [];\n let root: EventPointer | undefined;\n let reply: EventPointer | undefined;\n\n let maybeParent: EventPointer | undefined;\n let maybeRoot: EventPointer | undefined;\n\n for (let i = event.tags.length - 1; i >= 0; i--) {\n const tag = event.tags[i];\n if (tag === undefined) {\n continue;\n }\n\n const eValue = tag.at(1);\n if (tag[0] === \"e\" && eValue !== undefined && isHex32(eValue)) {\n const pointer = eventPointerFromETag(tag);\n if (pointer === undefined) {\n continue;\n }\n const marker = tag.at(3);\n\n if (marker === \"root\") {\n root = pointer;\n continue;\n }\n if (marker === \"reply\") {\n reply = pointer;\n continue;\n }\n // Preferred markers are root/reply only. A hex32 at index 3 is NIP-01 pubkey, not a marker.\n if (marker !== undefined && marker !== \"\" && !isHex32(marker)) {\n mentions.push(pointer);\n continue;\n }\n\n // Legacy positional: last unmarked is parent, second-to-last is root.\n if (maybeParent) {\n maybeRoot = pointer;\n } else {\n maybeParent = pointer;\n }\n mentions.push(pointer);\n continue;\n }\n\n if (tag[0] === \"q\") {\n const quote = quoteFromQTag(tag);\n if (quote) {\n quotes.push(quote);\n }\n continue;\n }\n\n const pValue = tag.at(1);\n if (tag[0] === \"p\" && pValue !== undefined && isHex32(pValue)) {\n profiles.push({\n pubkey: pValue,\n relays: tag[2] !== undefined && tag[2] !== \"\" ? [tag[2]] : [],\n });\n }\n }\n\n root ??= maybeRoot ?? maybeParent ?? reply;\n reply ??= maybeParent ?? root;\n\n // Drop root/reply from mentions (by id).\n const drop = new Set([root?.id, reply?.id].filter((id): id is string => Boolean(id)));\n const keptMentions = mentions.filter((m) => !drop.has(m.id));\n\n // Inherit relay hints from matching p-tags.\n const inheritHints = (ref: EventPointer): EventPointer => {\n const refAuthor = ref.author;\n if (refAuthor === undefined || refAuthor === \"\") {\n return ref;\n }\n const profileRelays = profiles.find((p) => p.pubkey === refAuthor)?.relays;\n if (profileRelays === undefined || profileRelays.length === 0) {\n return ref;\n }\n const relays = [...(ref.relays ?? [])];\n for (const url of profileRelays) {\n if (!relays.includes(url)) {\n relays.push(url);\n }\n }\n return { ...ref, relays };\n };\n\n return {\n root: root === undefined ? undefined : inheritHints(root),\n reply: reply === undefined ? undefined : inheritHints(reply),\n mentions: keptMentions.map(inheritHints),\n quotes,\n profiles,\n };\n}\n\n/** Options for {@link buildReplyTags}. */\nexport type ReplyTagsOptions = {\n /** Parent event being replied to. */\n parent: ReplyParent;\n /** Optional relay hint for the parent e-tag. */\n relayHint?: string | undefined;\n /** Quoted events (`q` tags): hex ids, `kind:pubkey:d` coords, or pointers. */\n quotes?: QuoteInput[] | undefined;\n};\n\n/**\n * Build NIP-10 e/p tags for a reply to `parent`. Uses marked tags (`root` / `reply`) per preferred\n * modern style.\n */\nexport function buildReplyTags(opts: ReplyTagsOptions): Tag[] {\n assertKind1Parent(opts.parent);\n const thread = parseThreadTags(opts.parent);\n const root = thread.root ?? { id: opts.parent.id, relays: [], author: opts.parent.pubkey };\n const parentIsRoot = lowerAscii(root.id) === lowerAscii(opts.parent.id);\n\n const tags: Tag[] = [];\n const rootRelay = root.relays?.[0] ?? opts.relayHint ?? \"\";\n tags.push(Tag.e(root.id, rootRelay, \"root\", root.author));\n\n if (!parentIsRoot) {\n tags.push(Tag.e(opts.parent.id, opts.relayHint ?? \"\", \"reply\", opts.parent.pubkey));\n }\n\n // Ensure root + parent authors are p-tagged.\n const pSeen = new Set<string>();\n const addP = (pk: string, relay?: string) => {\n const key = lowerAscii(pk);\n if (pSeen.has(key)) {\n return;\n }\n pSeen.add(key);\n tags.push(Tag.p(pk, relay ?? undefined));\n };\n if (root.author !== undefined && root.author !== \"\") {\n addP(root.author, root.relays?.[0]);\n }\n addP(opts.parent.pubkey, opts.relayHint);\n for (const p of thread.profiles) {\n addP(p.pubkey, p.relays?.[0]);\n }\n\n const qTags: Tag[] = [];\n for (const quote of opts.quotes ?? []) {\n const built = quoteToTag(quote);\n if (built === undefined) {\n continue;\n }\n if (built.author !== undefined && built.author !== \"\") {\n addP(built.author, built.relay);\n }\n qTags.push(built.tag);\n }\n tags.push(...qTags);\n\n return tags;\n}\n\n/**\n * Build an {@link EventBuilder} reply with NIP-10 tags. Lives here (not on EventBuilder) so core\n * does not depend on nips.\n */\nexport function replyTo(\n parent: ReplyParent,\n content: string,\n opts?: Pick<ReplyTagsOptions, \"relayHint\" | \"quotes\">,\n): EventBuilder {\n return EventBuilder.textNote(content).tags(\n buildReplyTags({\n parent,\n relayHint: opts?.relayHint,\n quotes: opts?.quotes,\n }),\n );\n}\n"],"mappings":"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"}
|
package/dist/nips/nip11.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { o as NostrError } from "../error-DE1UCZ2J.mjs";
|
|
2
2
|
import { t as ManualFetch } from "../http-kwWG-V1X.mjs";
|
|
3
3
|
//#region src/nips/nip11.d.ts
|
|
4
4
|
export type Nip11Fetch = ManualFetch;
|
|
@@ -36,6 +36,7 @@ export declare class Nip11Error extends NostrError {
|
|
|
36
36
|
}
|
|
37
37
|
/** Convert a relay websocket URL to the HTTP URL that serves the NIP-11 document. */
|
|
38
38
|
export declare function relayInfoHttpUrl(wsUrl: string): string;
|
|
39
|
+
export declare function parseRelayInformation(json: unknown): RelayInformation;
|
|
39
40
|
export declare function fetchRelayInformation(wsUrl: string, opts?: {
|
|
40
41
|
fetch?: Nip11Fetch;
|
|
41
42
|
signal?: AbortSignal;
|
package/dist/nips/nip11.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { u as isRecord,
|
|
1
|
+
import { c as isHex32, u as isRecord, w as NostrError } from "../util-BcRBO6o-.mjs";
|
|
2
2
|
import { n as requireGlobalFetch, t as fetchManual } from "../http-CuazNRIP.mjs";
|
|
3
3
|
//#region src/nips/nip11.ts
|
|
4
4
|
/**
|
|
@@ -12,8 +12,6 @@ const STRING_FIELDS = [
|
|
|
12
12
|
"description",
|
|
13
13
|
"banner",
|
|
14
14
|
"icon",
|
|
15
|
-
"pubkey",
|
|
16
|
-
"self",
|
|
17
15
|
"contact",
|
|
18
16
|
"software",
|
|
19
17
|
"version",
|
|
@@ -64,6 +62,10 @@ function parseRelayInformation(json) {
|
|
|
64
62
|
const value = raw[key];
|
|
65
63
|
if (typeof value === "string") info[key] = value;
|
|
66
64
|
}
|
|
65
|
+
for (const key of ["pubkey", "self"]) {
|
|
66
|
+
const value = raw[key];
|
|
67
|
+
if (typeof value === "string" && isHex32(value)) info[key] = value;
|
|
68
|
+
}
|
|
67
69
|
if (Array.isArray(raw["supported_nips"])) info.supported_nips = raw["supported_nips"].filter(isNonNegativeInteger);
|
|
68
70
|
const rawLim = raw["limitation"];
|
|
69
71
|
if (isRecord(rawLim)) {
|
|
@@ -97,6 +99,6 @@ async function fetchRelayInformation(wsUrl, opts) {
|
|
|
97
99
|
return parseRelayInformation(json);
|
|
98
100
|
}
|
|
99
101
|
//#endregion
|
|
100
|
-
export { Nip11Error, fetchRelayInformation, relayInfoHttpUrl };
|
|
102
|
+
export { Nip11Error, fetchRelayInformation, parseRelayInformation, relayInfoHttpUrl };
|
|
101
103
|
|
|
102
104
|
//# sourceMappingURL=nip11.mjs.map
|