@qntx/nostr 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{builder-GBddxj8i.d.mts → builder-C72nJITG.d.mts} +2 -2
- package/dist/{builder-BQbFDO6S.mjs → builder-CGgM_QHT.mjs} +5 -5
- package/dist/{builder-BQbFDO6S.mjs.map → builder-CGgM_QHT.mjs.map} +1 -1
- package/dist/{client-CTmp5w84.mjs → client-COkEihj_.mjs} +87 -79
- package/dist/client-COkEihj_.mjs.map +1 -0
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -1
- package/dist/core.d.mts +7 -7
- package/dist/core.mjs +9 -9
- package/dist/{emitter-DkCY6NkU.mjs → emitter-u4kJqQDk.mjs} +2 -2
- package/dist/{emitter-DkCY6NkU.mjs.map → emitter-u4kJqQDk.mjs.map} +1 -1
- package/dist/{error-BquXDmIN.d.mts → error-BPqAU5RB.d.mts} +2 -6
- package/dist/{error-BD_0QFKT.d.mts → error-BjSUsx6M.d.mts} +2 -2
- package/dist/{error-UHkqzWL7.mjs → error-Dyb89ahb.mjs} +7 -3
- package/dist/error-Dyb89ahb.mjs.map +1 -0
- package/dist/{error-DH4guike.mjs → error-QyECUDIp.mjs} +2 -2
- package/dist/{error-DH4guike.mjs.map → error-QyECUDIp.mjs.map} +1 -1
- package/dist/{event-B3ua2_oL.mjs → event-DvM1f03b.mjs} +7 -3
- package/dist/event-DvM1f03b.mjs.map +1 -0
- package/dist/{filter-B16P33dz.mjs → filter-3jOh4uLN.mjs} +50 -20
- package/dist/filter-3jOh4uLN.mjs.map +1 -0
- package/dist/{filter-PG7r79rt.d.mts → filter-oYG2zjJ3.d.mts} +6 -2
- package/dist/{gossip-BTMdlUI7.d.mts → gossip-BvCItMVZ.d.mts} +3 -3
- package/dist/{gossip-K_JIexQs.mjs → gossip-CRyhK6E6.mjs} +4 -4
- package/dist/{gossip-K_JIexQs.mjs.map → gossip-CRyhK6E6.mjs.map} +1 -1
- package/dist/gossip.d.mts +1 -1
- package/dist/gossip.mjs +1 -1
- package/dist/http-CuazNRIP.mjs.map +1 -1
- package/dist/{index-DD0Ywpay.d.mts → index-B2JlP2Jf.d.mts} +9 -9
- package/dist/{index-BP94lABY.d.mts → index-DCobP2Jj.d.mts} +10 -10
- package/dist/{index-Hp26bgYG.d.mts → index-Dpj_MUxb.d.mts} +8 -5
- package/dist/{index-rKcmI6ex.d.mts → index-DzHmhHqD.d.mts} +3 -3
- package/dist/{index-N7HUGgwo.d.mts → index-YID-zPDF.d.mts} +58 -8
- package/dist/{index-CSsnE32i.d.mts → index-muydfuYD.d.mts} +21 -21
- package/dist/index.d.mts +19 -20
- package/dist/index.mjs +20 -20
- package/dist/{key-k8LDEPZh.mjs → key-D_oEutQ6.mjs} +32 -9
- package/dist/key-D_oEutQ6.mjs.map +1 -0
- package/dist/{key-CyKN1RTi.d.mts → key-DojW2QZl.d.mts} +14 -4
- package/dist/{kind-D8fENiiL.mjs → kind-I2Tvbe82.mjs} +6 -5
- package/dist/kind-I2Tvbe82.mjs.map +1 -0
- package/dist/{loaders-Dp2nXEFk.mjs → loaders-BRh2riV9.mjs} +17 -16
- package/dist/loaders-BRh2riV9.mjs.map +1 -0
- package/dist/loaders.d.mts +2 -2
- package/dist/loaders.mjs +1 -1
- package/dist/{memory-VyPnOBU3.mjs → memory-D_GdEQYG.mjs} +2 -2
- package/dist/memory-D_GdEQYG.mjs.map +1 -0
- package/dist/memory-index-3GSrPIWj.mjs +671 -0
- package/dist/memory-index-3GSrPIWj.mjs.map +1 -0
- package/dist/{message-pYfVKxPn.mjs → message-BMfreCQc.mjs} +6 -6
- package/dist/message-BMfreCQc.mjs.map +1 -0
- package/dist/{nip17-B7MI82Iq.d.mts → nip17-BZvFQLcs.d.mts} +5 -5
- package/dist/{nip19-DpPN17bY.d.mts → nip19-D4RKn6GC.d.mts} +2 -2
- package/dist/{nip46-BHaxC9TZ.d.mts → nip46-i8L8_nIm.d.mts} +2 -2
- package/dist/{nip51-O_vhOiK2.d.mts → nip51-C_kTYcud.d.mts} +2 -2
- package/dist/{nip59-CdDoJptA.d.mts → nip59-DfJ6Moeu.d.mts} +15 -7
- package/dist/{nip65-BL79HI8N.d.mts → nip65-CPNE0SV0.d.mts} +2 -2
- package/dist/{nip77-wHA_nEV5.d.mts → nip77-DhpoL7d8.d.mts} +2 -2
- package/dist/nips/blossom.d.mts +2 -2
- package/dist/nips/blossom.mjs +3 -3
- package/dist/nips/blossom.mjs.map +1 -1
- package/dist/nips/nip04.d.mts +11 -4
- package/dist/nips/nip04.mjs +44 -21
- package/dist/nips/nip04.mjs.map +1 -1
- package/dist/nips/nip05.d.mts +6 -3
- package/dist/nips/nip05.mjs +6 -3
- package/dist/nips/nip05.mjs.map +1 -1
- package/dist/nips/nip10.d.mts +2 -2
- package/dist/nips/nip10.mjs +4 -4
- package/dist/nips/nip11.d.mts +1 -1
- package/dist/nips/nip11.mjs +1 -1
- package/dist/nips/nip13.d.mts +1 -1
- package/dist/nips/nip13.mjs +1 -1
- package/dist/nips/nip13.mjs.map +1 -1
- package/dist/nips/nip17.d.mts +1 -1
- package/dist/nips/nip17.mjs +5 -5
- package/dist/nips/nip17.mjs.map +1 -1
- package/dist/nips/nip19.d.mts +1 -1
- package/dist/nips/nip19.mjs +2 -2
- package/dist/nips/nip19.mjs.map +1 -1
- package/dist/nips/nip21.d.mts +6 -3
- package/dist/nips/nip21.mjs +8 -5
- package/dist/nips/nip21.mjs.map +1 -1
- package/dist/nips/nip27.d.mts +1 -1
- package/dist/nips/nip27.mjs +15 -9
- package/dist/nips/nip27.mjs.map +1 -1
- package/dist/nips/nip42.mjs +2 -2
- package/dist/nips/nip44.d.mts +5 -0
- package/dist/nips/nip44.mjs +10 -3
- package/dist/nips/nip44.mjs.map +1 -1
- package/dist/nips/nip46.d.mts +1 -1
- package/dist/nips/nip46.mjs +9 -7
- package/dist/nips/nip46.mjs.map +1 -1
- package/dist/nips/nip49.d.mts +8 -2
- package/dist/nips/nip49.mjs +17 -3
- package/dist/nips/nip49.mjs.map +1 -1
- package/dist/nips/nip51.d.mts +1 -1
- package/dist/nips/nip51.mjs +5 -5
- package/dist/nips/nip51.mjs.map +1 -1
- package/dist/nips/nip57.mjs +6 -5
- package/dist/nips/nip57.mjs.map +1 -1
- package/dist/nips/nip59.d.mts +1 -1
- package/dist/nips/nip59.mjs +51 -23
- package/dist/nips/nip59.mjs.map +1 -1
- package/dist/nips/nip65.d.mts +1 -1
- package/dist/nips/nip65.mjs +3 -3
- package/dist/nips/nip77.d.mts +1 -1
- package/dist/nips/nip77.mjs +1 -1
- package/dist/nips/nip77.mjs.map +1 -1
- package/dist/nips/nip98.d.mts +1 -1
- package/dist/nips/nip98.mjs +18 -7
- package/dist/nips/nip98.mjs.map +1 -1
- package/dist/{pool-DUuYJ0x4.d.mts → pool-MehgIIzm.d.mts} +20 -16
- package/dist/{pool-BMUsw2un.mjs → pool-lMT_mvzn.mjs} +25 -25
- package/dist/pool-lMT_mvzn.mjs.map +1 -0
- package/dist/{put-DRmV1wHD.mjs → put-BEM5j8xK.mjs} +5 -5
- package/dist/{put-DRmV1wHD.mjs.map → put-BEM5j8xK.mjs.map} +1 -1
- package/dist/{reactive-jyKrA9ej.d.mts → reactive-B7lPFgTS.d.mts} +3 -3
- package/dist/{reactive--pDpLt2H.mjs → reactive-DFHY-f00.mjs} +24 -9
- package/dist/reactive-DFHY-f00.mjs.map +1 -0
- package/dist/{relay-DXtwUEdX.mjs → relay-DIR77xQ2.mjs} +102 -89
- package/dist/relay-DIR77xQ2.mjs.map +1 -0
- package/dist/relay.d.mts +5 -5
- package/dist/relay.mjs +3 -3
- package/dist/{report-CHld4uWj.mjs → report-D2BM5dgn.mjs} +2 -2
- package/dist/{report-CHld4uWj.mjs.map → report-D2BM5dgn.mjs.map} +1 -1
- package/dist/{signer-CWtxpOxc.mjs → signer-DJcmC4nA.mjs} +176 -17
- package/dist/signer-DJcmC4nA.mjs.map +1 -0
- package/dist/signer.d.mts +2 -2
- package/dist/signer.mjs +3 -3
- package/dist/{storage-B5Wdt_Mn.mjs → storage-n2BE1bdY.mjs} +108 -179
- package/dist/storage-n2BE1bdY.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 -1
- package/dist/testing.d.mts +4 -4
- package/dist/testing.mjs +35 -10
- package/dist/testing.mjs.map +1 -1
- package/dist/{types-CqhnT-Mx.d.mts → types-DiVVyf1e.d.mts} +2 -2
- package/dist/{util-DDpPkiED.mjs → util-DfsyJ9nz.mjs} +30 -10
- package/dist/util-DfsyJ9nz.mjs.map +1 -0
- package/dist/{verifier-CjH1vnP2.mjs → verifier-DEHUy4AD.mjs} +3 -3
- package/dist/{verifier-CjH1vnP2.mjs.map → verifier-DEHUy4AD.mjs.map} +1 -1
- package/dist/{verifier-Cl24c9ph.d.mts → verifier-DhAbqVJF.d.mts} +2 -2
- package/dist/{websocket-CmeqvAkp.d.mts → websocket-DaCvJkF9.d.mts} +2 -5
- package/package.json +4 -3
- package/dist/client-CTmp5w84.mjs.map +0 -1
- package/dist/error-UHkqzWL7.mjs.map +0 -1
- package/dist/event-B3ua2_oL.mjs.map +0 -1
- package/dist/filter-B16P33dz.mjs.map +0 -1
- package/dist/key-k8LDEPZh.mjs.map +0 -1
- package/dist/kind-D8fENiiL.mjs.map +0 -1
- package/dist/loaders-Dp2nXEFk.mjs.map +0 -1
- package/dist/memory-VyPnOBU3.mjs.map +0 -1
- package/dist/memory-index-Bhwe0ZdG.mjs +0 -374
- package/dist/memory-index-Bhwe0ZdG.mjs.map +0 -1
- package/dist/message-pYfVKxPn.mjs.map +0 -1
- package/dist/pool-BMUsw2un.mjs.map +0 -1
- package/dist/reactive--pDpLt2H.mjs.map +0 -1
- package/dist/relay-DXtwUEdX.mjs.map +0 -1
- package/dist/signer-CWtxpOxc.mjs.map +0 -1
- package/dist/storage-B5Wdt_Mn.mjs.map +0 -1
- package/dist/util-DDpPkiED.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-index-3GSrPIWj.mjs","names":[],"sources":["../src/storage/sorted-events.ts","../src/storage/memory-index.ts"],"sourcesContent":["import type { Event } from \"../core/event.ts\";\n\n/**\n * Chunk bounds: a chunk splice moves at most `CHUNK_MAX` element refs and a chunk merge at\n * `CHUNK_MIN` keeps chunk count proportional to `size` under removals.\n */\nconst CHUNK_MAX = 1024;\nconst CHUNK_MIN = 128;\n\n/** Reverse of `compareEventsDesc`: `created_at` asc, `id` desc — the physical chunk order. */\nfunction compareAsc(\n a: { id: string; created_at: number },\n b: { id: string; created_at: number },\n): number {\n if (a.created_at !== b.created_at) {\n return a.created_at - b.created_at;\n }\n if (a.id < b.id) {\n return 1;\n }\n if (a.id > b.id) {\n return -1;\n }\n return 0;\n}\n\nfunction mid(lo: number, hi: number): number {\n return lo + Math.floor((hi - lo) / 2);\n}\n\n/** A read cursor over a `SortedEvents` range in canonical order; `value` is the current event. */\nexport class EventCursor {\n readonly #chunks: Event[][];\n #chunk: number;\n #index: number;\n readonly #endChunk: number;\n readonly #endIndex: number;\n\n constructor(chunks: Event[][], chunk: number, index: number, endChunk: number, endIndex: number) {\n this.#chunks = chunks;\n this.#chunk = chunk;\n this.#index = index;\n this.#endChunk = endChunk;\n this.#endIndex = endIndex;\n }\n\n get value(): Event | undefined {\n const chunk = this.#chunk;\n const index = this.#index;\n if (chunk < this.#endChunk || (chunk === this.#endChunk && index <= this.#endIndex)) {\n return undefined;\n }\n return this.#chunks[chunk]?.[index];\n }\n\n next(): void {\n this.#index -= 1;\n if (this.#index < 0) {\n this.#chunk -= 1;\n this.#index = (this.#chunks[this.#chunk]?.length ?? 1) - 1;\n }\n }\n}\n\n/**\n * Event refs sorted in canonical order (`compareEventsDesc`: `created_at` desc, `id` asc) stored as\n * chunks of ≤ `CHUNK_MAX` elements in ascending physical order so insertion and removal\n * binary-search a chunk and then move a bounded number of refs — appending newer events (live\n * ingest, ascending fills) pushes at the end, and appending older ones (relay backfill) moves at\n * most one chunk's worth of refs. Internal to the memory index; not exported.\n */\nexport class SortedEvents {\n #chunks: Event[][] = [];\n #size = 0;\n\n get size(): number {\n return this.#size;\n }\n\n clear(): void {\n this.#chunks = [];\n this.#size = 0;\n }\n\n /** Index of the chunk `event` belongs to: the last one whose first element is not newer. */\n #chunkOf(event: Event): number {\n const chunks = this.#chunks;\n let lo = 0;\n let hi = chunks.length;\n while (lo < hi) {\n const m = mid(lo, hi);\n const first = chunks[m]?.[0];\n if (first !== undefined && compareAsc(first, event) <= 0) {\n lo = m + 1;\n } else {\n hi = m;\n }\n }\n return lo - 1;\n }\n\n insert(event: Event): void {\n const chunks = this.#chunks;\n if (chunks.length === 0) {\n chunks.push([event]);\n this.#size = 1;\n return;\n }\n const chunkIndex = Math.max(this.#chunkOf(event), 0);\n const chunk = chunks[chunkIndex];\n if (chunk === undefined) {\n return;\n }\n // Upper bound inside the chunk: insert after all elements that sort before `event`.\n let a = 0;\n let b = chunk.length;\n while (a < b) {\n const m = mid(a, b);\n const element = chunk[m];\n if (element !== undefined && compareAsc(element, event) <= 0) {\n a = m + 1;\n } else {\n b = m;\n }\n }\n chunk.splice(a, 0, event);\n this.#size += 1;\n if (chunk.length > CHUNK_MAX) {\n const half = chunk.splice(Math.floor(CHUNK_MAX / 2));\n chunks.splice(chunkIndex + 1, 0, half);\n }\n }\n\n remove(event: Event): boolean {\n const chunks = this.#chunks;\n const chunkIndex = this.#chunkOf(event);\n if (chunkIndex < 0) {\n return false;\n }\n const chunk = chunks[chunkIndex];\n if (chunk === undefined) {\n return false;\n }\n let a = 0;\n let b = chunk.length;\n while (a < b) {\n const m = mid(a, b);\n const element = chunk[m];\n if (element !== undefined && compareAsc(element, event) < 0) {\n a = m + 1;\n } else {\n b = m;\n }\n }\n const found = chunk[a];\n if (found === undefined || compareAsc(found, event) !== 0) {\n return false;\n }\n chunk.splice(a, 1);\n this.#size -= 1;\n if (chunk.length === 0) {\n chunks.splice(chunkIndex, 1);\n } else if (chunk.length < CHUNK_MIN) {\n // Absorb a thin chunk into a neighbor when it fits; a small chunk left between\n // fuller ones is still sorted and correct.\n const next = chunks[chunkIndex + 1];\n const prev = chunks[chunkIndex - 1];\n if (next !== undefined && chunk.length + next.length <= CHUNK_MAX) {\n chunk.push(...next);\n chunks.splice(chunkIndex + 1, 1);\n } else if (prev !== undefined && prev.length + chunk.length <= CHUNK_MAX) {\n prev.push(...chunk);\n chunks.splice(chunkIndex, 1);\n }\n }\n return true;\n }\n\n /**\n * Cursor over the [since, until] `created_at` window in canonical (desc) order: iteration starts\n * at the newest element with `created_at <= until` and stops before the first with `created_at <\n * since`. Chunks and their elements are stored ascending; the cursor walks back.\n */\n cursor(since?: number, until?: number): EventCursor {\n const chunks = this.#chunks;\n // Start position: last element with created_at <= until.\n let chunk = chunks.length - 1;\n let index = -1;\n if (until === undefined) {\n index = (chunks[chunk]?.length ?? 1) - 1;\n } else {\n // Last chunk holding an element with created_at <= until.\n let lo = 0;\n let hi = chunks.length;\n while (lo < hi) {\n const m = mid(lo, hi);\n const first = chunks[m]?.[0];\n if (first !== undefined && first.created_at <= until) {\n lo = m + 1;\n } else {\n hi = m;\n }\n }\n chunk = lo - 1;\n if (chunk >= 0) {\n const bucket = chunks[chunk];\n let a = 0;\n let b = bucket?.length ?? 0;\n while (a < b) {\n const m = mid(a, b);\n const element = bucket?.[m];\n if (element !== undefined && element.created_at <= until) {\n a = m + 1;\n } else {\n b = m;\n }\n }\n index = a - 1;\n }\n }\n // End position: last element with created_at < since (walk stops once past it).\n let endChunk = -1;\n let endIndex = -1;\n if (since !== undefined) {\n // First chunk holding an element with created_at >= since; the position before the\n // first such element is the boundary.\n let lo = 0;\n let hi = chunks.length;\n while (lo < hi) {\n const m = mid(lo, hi);\n const bucket = chunks[m];\n const last = bucket?.at(-1);\n if (last !== undefined && last.created_at >= since) {\n hi = m;\n } else {\n lo = m + 1;\n }\n }\n if (lo === chunks.length) {\n // Everything is older than since: empty range.\n endChunk = Number.MAX_SAFE_INTEGER;\n } else {\n const bucket = chunks[lo];\n let a = 0;\n let b = bucket?.length ?? 0;\n while (a < b) {\n const m = mid(a, b);\n const element = bucket?.[m];\n if (element !== undefined && element.created_at >= since) {\n b = m;\n } else {\n a = m + 1;\n }\n }\n if (a === 0) {\n endChunk = lo - 1;\n endIndex = (chunks[endChunk]?.length ?? 1) - 1;\n } else {\n endChunk = lo;\n endIndex = a - 1;\n }\n }\n }\n return new EventCursor(chunks, chunk, index, endChunk, endIndex);\n }\n}\n","import { trimOldest } from \"../core/collections.ts\";\nimport type { Event } from \"../core/event.ts\";\nimport { compareEventsDesc, itemCompare, sortEvents } from \"../core/event.ts\";\nimport type { Filter } from \"../core/filter.ts\";\nimport { filterTagConditions, matchFilter } from \"../core/filter.ts\";\nimport { eventAddress, formatEventAddress, parseEventAddress } from \"../core/tag.ts\";\nimport { DeletionState } from \"./deletion.ts\";\nimport { applyPutMemory, decidePut, outboxBoundKey } from \"./put.ts\";\nimport type { PutLookup } from \"./put.ts\";\nimport { SortedEvents } from \"./sorted-events.ts\";\nimport type { EventCursor } from \"./sorted-events.ts\";\nimport type { NegentropyItem, OutboxBound, PutResult } from \"./types.ts\";\n\n/**\n * NIP-01 only defines single-letter tag conditions; `#e`/`#p` hold hex ids (filter side\n * lowercased).\n */\nconst SINGLE_ASCII_LETTER = /^[a-zA-Z]$/;\n\nexport type MemoryIndexOptions = {\n /**\n * FIFO cap applied independently to each deletion-state collection: tombstoned event ids, pending\n * e-tag ids, and coordinate tombstones. Default unbounded. Trade-off: once an entry is trimmed, a\n * re-arriving old deleted event or replaceable version can be accepted again.\n */\n maxTombstones?: number | undefined;\n /**\n * FIFO cap on replaceable/addressable winner watermarks recorded by {@link evict}. A watermark\n * keeps only `{ id, created_at }` per address so stale versions stay rejected after the winner's\n * body is evicted, and a re-put of the watermarked id re-inserts it. Default 0 (no watermark\n * retention); callers that evict should pass a bound. Once a watermark is trimmed, an older\n * evicted version can be accepted again — the same trade-off as deletion tombstones.\n */\n maxWatermarks?: number | undefined;\n /** Fires after every physical index insert (accept, replace, deletion event). */\n onInsert?: ((event: Event) => void) | undefined;\n /** Fires after every physical index remove (replace, delete, evict, remove, clear). */\n onRemove?: ((event: Event) => void) | undefined;\n};\n\n/**\n * Synchronous in-memory event index with NIP-01 replaceable / addressable / ephemeral handling and\n * NIP-09 deletion (kind 5) application. Shared by {@link MemoryEventStore} (async facade) and\n * `ReactiveEventStore`.\n */\nexport class MemoryIndex {\n readonly #byId = new Map<string, Event>();\n readonly #all = new SortedEvents();\n readonly #byPubkey = new Map<string, SortedEvents>();\n readonly #byKind = new Map<number, SortedEvents>();\n readonly #byKindPubkey = new Map<string, SortedEvents>(); // `${kind}:${pubkey}` bucket\n readonly #byTag = new Map<string, SortedEvents>(); // `${name}:${value}` for single-letter tag names (event-side verbatim)\n readonly #replaceable = new Map<string, string>(); // address -> event id\n // address -> last evicted winner; insertion order is the FIFO trim order\n readonly #watermarks = new Map<string, { id: string; created_at: number }>();\n // winner id -> address reverse index for O(1) watermark drops on deletion\n readonly #watermarkIds = new Map<string, string>();\n readonly #deletion = new DeletionState();\n readonly #outboxBounds = new Map<string, OutboxBound>();\n readonly #maxTombstones: number | undefined;\n readonly #maxWatermarks: number;\n readonly #onInsert: ((event: Event) => void) | undefined;\n readonly #onRemove: ((event: Event) => void) | undefined;\n\n constructor(opts?: MemoryIndexOptions) {\n this.#maxTombstones = opts?.maxTombstones;\n this.#maxWatermarks = opts?.maxWatermarks ?? 0;\n this.#onInsert = opts?.onInsert;\n this.#onRemove = opts?.onRemove;\n }\n\n put(raw: Event): PutResult {\n const lookup: PutLookup = {\n deletion: this.#deletion,\n getById: (id) => this.#byId.get(id),\n getReplaceable: (addr) => {\n const id = this.#replaceable.get(addr);\n const ev = id === undefined ? undefined : this.#byId.get(id);\n if (ev) {\n return { id: ev.id, created_at: ev.created_at };\n }\n const watermark = this.#watermarks.get(addr);\n return watermark === undefined ? undefined : { ...watermark, evicted: true };\n },\n };\n const decision = decidePut(raw, lookup);\n if (decision.action === \"delete\") {\n // Pending e-tag targets and coordinate tombstones never reach\n // indexRemove — drop their watermarks here.\n for (const p of decision.plan.pendingIds) {\n this.#dropWatermarkId(p.id);\n }\n for (const c of decision.plan.coordinates) {\n this.#dropWatermark(c.key);\n }\n }\n const result = applyPutMemory(\n {\n deletion: this.#deletion,\n indexInsert: (e) => this.#indexInsert(e),\n indexRemove: (id) => this.#indexRemove(id),\n },\n decision,\n );\n this.#trimTombstones();\n return result;\n }\n\n /** Sequential `put` in input order. */\n putMany(events: ReadonlyArray<Event>): PutResult[] {\n const results: PutResult[] = [];\n for (const event of events) {\n results.push(this.put(event));\n }\n return results;\n }\n\n get(id: string): Event | undefined {\n const key = id.toLowerCase();\n if (this.#deletion.ids.has(key)) {\n return undefined;\n }\n return this.#byId.get(key);\n }\n\n /** Current event at a `kind:pubkey:d` coordinate (replaceable or addressable). */\n getByAddress(address: string): Event | undefined {\n const coord = parseEventAddress(address);\n if (!coord) {\n return undefined;\n }\n const id = this.#replaceable.get(\n formatEventAddress(coord.kind, coord.pubkey, coord.identifier),\n );\n if (id === undefined) {\n return undefined;\n }\n if (this.#deletion.ids.has(id)) {\n return undefined;\n }\n return this.#byId.get(id);\n }\n\n query(filters: ReadonlyArray<Filter>): Event[] {\n const seen = new Set<string>();\n const events: Event[] = [];\n for (const filter of filters) {\n for (const event of this.#matchedEvents(filter)) {\n if (seen.has(event.id)) {\n continue;\n }\n seen.add(event.id);\n events.push(event);\n }\n }\n sortEvents(events);\n return events;\n }\n\n count(filters: ReadonlyArray<Filter>): number {\n const seen = new Set<string>();\n for (const filter of filters) {\n for (const event of this.#matchedEvents(filter)) {\n seen.add(event.id);\n }\n }\n return seen.size;\n }\n\n negentropyItems(filter: Filter): NegentropyItem[] {\n if (filter.limit === 0) {\n return [];\n }\n const items: NegentropyItem[] = [];\n const { limit } = filter;\n if (filter.ids) {\n const seen = new Set<string>();\n for (const raw of filter.ids) {\n const event = this.#byId.get(raw.toLowerCase());\n if (!event || seen.has(event.id)) {\n continue;\n }\n seen.add(event.id);\n if (this.#deletion.covers(event) || !matchFilter(filter, event)) {\n continue;\n }\n items.push({ id: event.id, created_at: event.created_at });\n }\n } else {\n const cursor = this.#candidates(filter);\n while (cursor.value !== undefined) {\n const event = cursor.value;\n cursor.next();\n if (this.#deletion.covers(event) || !matchFilter(filter, event)) {\n continue;\n }\n items.push({ id: event.id, created_at: event.created_at });\n if (limit !== undefined && items.length >= limit) {\n break;\n }\n }\n }\n if (filter.limit !== undefined) {\n // The ids path collected matches in filter order; cursor results are already canonical.\n if (filter.ids) {\n items.sort(compareEventsDesc);\n }\n if (items.length > filter.limit) {\n items.length = filter.limit;\n }\n }\n items.sort(itemCompare);\n return items;\n }\n\n /** Remove by id and tombstone (like a local NIP-09). Returns removed count. */\n remove(ids: ReadonlyArray<string>): number {\n let n = 0;\n for (const raw of ids) {\n const id = raw.toLowerCase();\n if (this.#indexRemove(id)) {\n n += 1;\n }\n this.#deletion.ids.add(id);\n this.#deletion.pending.delete(id);\n }\n this.#trimTombstones();\n return n;\n }\n\n /**\n * Remove by id without writing a tombstone (LRU eviction). When the evicted event is the current\n * winner of a replaceable/addressable address, a `{ id, created_at }` watermark is recorded so\n * stale versions stay rejected while the body is gone; the watermarks are FIFO-bounded by\n * `maxWatermarks`.\n */\n evict(ids: ReadonlyArray<string>): number {\n let n = 0;\n for (const raw of ids) {\n const id = raw.toLowerCase();\n const event = this.#byId.get(id);\n if (event === undefined) {\n continue;\n }\n const addr = eventAddress(event);\n const winner = addr !== undefined && this.#replaceable.get(addr) === id;\n if (this.#indexRemove(id)) {\n n += 1;\n }\n if (winner) {\n this.#setWatermark(addr, { id, created_at: event.created_at });\n }\n }\n return n;\n }\n\n /**\n * True when the id is tombstoned, or the coordinate has a deletion marker that still covers the\n * current event at that address: a replacement published after the marker's `until` clears the\n * deletion. Coordinates are matched on the canonical lowercase pubkey.\n */\n isDeleted(idOrAddress: string): boolean {\n if (this.#deletion.ids.has(idOrAddress.toLowerCase())) {\n return true;\n }\n const coord = parseEventAddress(idOrAddress);\n if (!coord) {\n return false;\n }\n const address = formatEventAddress(coord.kind, coord.pubkey, coord.identifier);\n const until = this.#deletion.coordinates.get(address);\n if (until === undefined) {\n return false;\n }\n const id = this.#replaceable.get(address);\n if (id === undefined) {\n return true;\n }\n const event = this.#byId.get(id);\n return event === undefined || event.created_at <= until;\n }\n\n getOutboxBound(pubkey: string, kind: number): OutboxBound | undefined {\n const persisted = this.#outboxBounds.get(outboxBoundKey(pubkey, kind));\n if (persisted) {\n return { oldest: persisted.oldest, newest: persisted.newest };\n }\n return this.#deriveOutboxBound(pubkey, kind);\n }\n\n setOutboxBound(pubkey: string, kind: number, bound: OutboxBound): void {\n this.#outboxBounds.set(outboxBoundKey(pubkey, kind), {\n oldest: bound.oldest,\n newest: bound.newest,\n });\n }\n\n clear(): void {\n if (this.#onRemove) {\n for (const event of this.#byId.values()) {\n this.#onRemove(event);\n }\n }\n this.#byId.clear();\n this.#all.clear();\n this.#byPubkey.clear();\n this.#byKind.clear();\n this.#byKindPubkey.clear();\n this.#byTag.clear();\n this.#replaceable.clear();\n this.#watermarks.clear();\n this.#watermarkIds.clear();\n this.#deletion.clear();\n this.#outboxBounds.clear();\n }\n\n get size(): number {\n return this.#byId.size;\n }\n\n #indexInsert(event: Event): void {\n this.#byId.set(event.id, event);\n const { pubkey } = event;\n this.#all.insert(event);\n insertBucket(this.#byPubkey, pubkey, event);\n insertBucket(this.#byKind, event.kind, event);\n insertBucket(this.#byKindPubkey, `${event.kind}:${pubkey}`, event);\n for (const key of tagKeys(event)) {\n insertBucket(this.#byTag, key, event);\n }\n const addr = eventAddress(event);\n if (addr !== undefined) {\n this.#replaceable.set(addr, event.id);\n this.#dropWatermark(addr);\n }\n this.#onInsert?.(event);\n }\n\n #indexRemove(id: string): boolean {\n // A removed/deleted id must not keep a stale watermark alive; eviction\n // re-records its winner watermark right after this call.\n this.#dropWatermarkId(id);\n const event = this.#byId.get(id);\n if (!event) {\n return false;\n }\n this.#byId.delete(id);\n const { pubkey } = event;\n this.#all.remove(event);\n removeBucket(this.#byPubkey, pubkey, event);\n removeBucket(this.#byKind, event.kind, event);\n removeBucket(this.#byKindPubkey, `${event.kind}:${pubkey}`, event);\n for (const key of tagKeys(event)) {\n removeBucket(this.#byTag, key, event);\n }\n const addr = eventAddress(event);\n if (addr !== undefined && this.#replaceable.get(addr) === id) {\n this.#replaceable.delete(addr);\n }\n this.#onRemove?.(event);\n return true;\n }\n\n #matchedEvents(filter: Filter): Event[] {\n if (filter.limit === 0) {\n return [];\n }\n const { limit } = filter;\n const matched: Event[] = [];\n if (filter.ids) {\n const seen = new Set<string>();\n for (const raw of filter.ids) {\n const event = this.#byId.get(raw.toLowerCase());\n if (!event || seen.has(event.id)) {\n continue;\n }\n seen.add(event.id);\n if (this.#deletion.covers(event) || !matchFilter(filter, event)) {\n continue;\n }\n matched.push(event);\n }\n sortEvents(matched);\n return limit === undefined ? matched : matched.slice(0, limit);\n }\n const cursor = this.#candidates(filter);\n while (cursor.value !== undefined) {\n const event = cursor.value;\n cursor.next();\n if (this.#deletion.covers(event) || !matchFilter(filter, event)) {\n continue;\n }\n matched.push(event);\n if (limit !== undefined && matched.length >= limit) {\n break;\n }\n }\n return matched;\n }\n\n /**\n * Smallest-total bucket set for the filter's usable indexes: `authors`×`kinds` (both present),\n * `authors`, `kinds`, each single-letter `#x` condition (a bucket per value; `#e`/`#p` filter\n * values lowercased like `matchFilter`, others verbatim), or `all` as the fallback.\n */\n #plan(filter: Filter): SortedEvents[] {\n const { authors, kinds } = filter;\n const tagConds = filterTagConditions(filter);\n\n let best: SortedEvents[] | undefined;\n let bestTotal = Number.POSITIVE_INFINITY;\n const consider = (buckets: Array<SortedEvents | undefined>): void => {\n let total = 0;\n const present: SortedEvents[] = [];\n for (const bucket of buckets) {\n if (bucket !== undefined) {\n present.push(bucket);\n total += bucket.size;\n }\n }\n if (total < bestTotal) {\n bestTotal = total;\n best = present;\n }\n };\n\n if (authors !== undefined && kinds !== undefined) {\n const buckets: Array<SortedEvents | undefined> = [];\n for (const pk of authors) {\n const pubkey = pk.toLowerCase();\n for (const kind of kinds) {\n buckets.push(this.#byKindPubkey.get(`${kind}:${pubkey}`));\n }\n }\n consider(buckets);\n }\n if (authors !== undefined) {\n consider(authors.map((pk) => this.#byPubkey.get(pk.toLowerCase())));\n }\n if (kinds !== undefined) {\n consider(kinds.map((kind) => this.#byKind.get(kind)));\n }\n for (const { name, values } of tagConds) {\n consider(values.map((v) => this.#byTag.get(`${name}:${v}`)));\n }\n if (best === undefined || bestTotal > this.#all.size) {\n return [this.#all];\n }\n return best;\n }\n\n /** Canonical-order candidates from the planned buckets, each clipped to `since`/`until`. */\n #candidates(filter: Filter): CandidateCursor {\n const buckets = this.#plan(filter);\n const cursors: EventCursor[] = [];\n for (const bucket of buckets) {\n const cursor = bucket.cursor(filter.since, filter.until);\n if (cursor.value !== undefined) {\n cursors.push(cursor);\n }\n }\n if (cursors.length === 0) {\n return EMPTY_CURSOR;\n }\n if (cursors.length === 1) {\n return cursors[0] ?? EMPTY_CURSOR;\n }\n return new CursorHeap(cursors);\n }\n\n #deriveOutboxBound(pubkey: string, kind: number): OutboxBound | undefined {\n const bucket = this.#byKindPubkey.get(`${kind}:${pubkey.toLowerCase()}`);\n if (bucket === undefined) {\n return undefined;\n }\n // Canonical order: first non-deleted event is the newest, last the oldest.\n let oldest: number | undefined;\n let newest: number | undefined;\n const cursor = bucket.cursor();\n while (cursor.value !== undefined) {\n const event = cursor.value;\n cursor.next();\n if (this.#deletion.covers(event)) {\n continue;\n }\n newest ??= event.created_at;\n oldest = event.created_at;\n }\n if (oldest === undefined || newest === undefined) {\n return undefined;\n }\n return { oldest, newest };\n }\n\n #setWatermark(addr: string, watermark: { id: string; created_at: number }): void {\n if (this.#maxWatermarks === 0) {\n return;\n }\n this.#dropWatermark(addr);\n this.#watermarks.set(addr, watermark);\n this.#watermarkIds.set(watermark.id, addr);\n trimOldest(this.#watermarks, this.#maxWatermarks, (a) => this.#dropWatermark(a));\n }\n\n #dropWatermark(addr: string): void {\n const watermark = this.#watermarks.get(addr);\n if (watermark === undefined) {\n return;\n }\n this.#watermarks.delete(addr);\n this.#watermarkIds.delete(watermark.id);\n }\n\n #dropWatermarkId(id: string): void {\n const addr = this.#watermarkIds.get(id);\n if (addr === undefined) {\n return;\n }\n this.#watermarkIds.delete(id);\n this.#watermarks.delete(addr);\n }\n\n #trimTombstones(): void {\n const cap = this.#maxTombstones;\n if (cap === undefined) {\n return;\n }\n trimOldest(this.#deletion.ids, cap, (id) => {\n this.#deletion.pending.delete(id);\n });\n trimOldest(this.#deletion.pending, cap);\n trimOldest(this.#deletion.coordinates, cap);\n }\n}\n\ntype CandidateCursor = {\n readonly value: Event | undefined;\n next: () => void;\n};\n\nconst EMPTY_CURSOR: CandidateCursor = {\n value: undefined,\n next: () => undefined,\n};\n\n/**\n * Binary-heap k-way merge of sorted cursors. `next()` advances every cursor sitting on the emitted\n * event, which dedupes an event present in several buckets (multi-value `#x` filters); copies\n * compare equal so they always surface adjacent. Entries pair each cursor with its head event so\n * heap comparisons never read `undefined`.\n */\nclass CursorHeap implements CandidateCursor {\n #heap: Array<{ cursor: EventCursor; event: Event }>;\n\n constructor(cursors: EventCursor[]) {\n const heap: Array<{ cursor: EventCursor; event: Event }> = [];\n for (const cursor of cursors) {\n const event = cursor.value;\n if (event !== undefined) {\n heap.push({ cursor, event });\n }\n }\n this.#heap = heap;\n for (let i = Math.floor(heap.length / 2) - 1; i >= 0; i--) {\n this.#siftDown(i);\n }\n }\n\n get value(): Event | undefined {\n return this.#heap[0]?.event;\n }\n\n next(): void {\n const emitted = this.#heap[0]?.event;\n if (emitted === undefined) {\n return;\n }\n while (this.#heap[0]?.event === emitted) {\n const [root] = this.#heap;\n if (root === undefined) {\n return;\n }\n root.cursor.next();\n const nextEvent = root.cursor.value;\n if (nextEvent === undefined) {\n const last = this.#heap.pop();\n if (this.#heap.length > 0 && last !== undefined) {\n this.#heap[0] = last;\n this.#siftDown(0);\n }\n } else {\n root.event = nextEvent;\n this.#siftDown(0);\n }\n }\n }\n\n #less(a: number, b: number): boolean {\n const x = this.#heap[a];\n const y = this.#heap[b];\n return x !== undefined && y !== undefined && compareEventsDesc(x.event, y.event) < 0;\n }\n\n #siftDown(start: number): void {\n const heap = this.#heap;\n let i = start;\n for (;;) {\n let min = i;\n if (this.#less(2 * i + 1, min)) {\n min = 2 * i + 1;\n }\n if (this.#less(2 * i + 2, min)) {\n min = 2 * i + 2;\n }\n if (min === i) {\n return;\n }\n const tmp = heap[i];\n const swap = heap[min];\n if (tmp === undefined || swap === undefined) {\n return;\n }\n heap[i] = swap;\n heap[min] = tmp;\n i = min;\n }\n }\n}\n\n/** Distinct `${name}:${value}` bucket keys for every ASCII single-letter tag name on the event. */\nfunction tagKeys(event: Event): string[] {\n const keys = new Set<string>();\n for (const tag of event.tags) {\n const [name, value] = tag;\n if (name === undefined || value === undefined || !SINGLE_ASCII_LETTER.test(name)) {\n continue;\n }\n keys.add(`${name}:${value}`);\n }\n return [...keys];\n}\n\nfunction insertBucket<K>(map: Map<K, SortedEvents>, key: K, event: Event): void {\n let bucket = map.get(key);\n if (bucket === undefined) {\n bucket = new SortedEvents();\n map.set(key, bucket);\n }\n bucket.insert(event);\n}\n\nfunction removeBucket<K>(map: Map<K, SortedEvents>, key: K, event: Event): void {\n const bucket = map.get(key);\n if (bucket === undefined || !bucket.remove(event)) {\n return;\n }\n if (bucket.size === 0) {\n map.delete(key);\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { h as validateSignedEvent } from "./event-
|
|
3
|
-
import { n as canonicalizeFilters, t as canonicalizeFilter } from "./filter-
|
|
1
|
+
import { b as MessageError, i as bytesToHex, s as hexToBytes, u as isRecord } from "./util-DfsyJ9nz.mjs";
|
|
2
|
+
import { h as validateSignedEvent } from "./event-DvM1f03b.mjs";
|
|
3
|
+
import { n as canonicalizeFilters, t as canonicalizeFilter } from "./filter-3jOh4uLN.mjs";
|
|
4
4
|
//#region src/core/message.ts
|
|
5
5
|
/** Validate a subscription id; throws {@link MessageError} unless 1..64 Unicode scalar values. */
|
|
6
6
|
function assertSubscriptionId(id) {
|
|
@@ -182,7 +182,7 @@ function parseRelayMessage(raw) {
|
|
|
182
182
|
items[2].toLowerCase()
|
|
183
183
|
];
|
|
184
184
|
case "NEG-ERR":
|
|
185
|
-
if (items.length !== 3
|
|
185
|
+
if (items.length !== 3 || typeof items[1] !== "string" || typeof items[2] !== "string") throw new MessageError("invalid NEG-ERR relay message");
|
|
186
186
|
return [
|
|
187
187
|
"NEG-ERR",
|
|
188
188
|
assertSubscriptionId(items[1]),
|
|
@@ -203,7 +203,7 @@ const HEX64_LOWER_RE = /^[0-9a-f]{64}$/;
|
|
|
203
203
|
* string, and `#<single letter>` arrays of strings (`#e`/`#p` lowercased). Multi-letter `#` keys
|
|
204
204
|
* and unknown non-`#` keys are dropped; wrong types throw {@link MessageError}. Duplicate keys and
|
|
205
205
|
* integer spellings (`1e3`, `1.0`, `-0`) need no handling: `JSON.parse` already applied last-wins
|
|
206
|
-
* and normalized the numbers before this runs
|
|
206
|
+
* and normalized the numbers before this runs.
|
|
207
207
|
*/
|
|
208
208
|
function parseWireFilter(value, kind) {
|
|
209
209
|
if (!isRecord(value)) throw new MessageError(`invalid ${kind} filter`);
|
|
@@ -237,4 +237,4 @@ function isNegHex(value) {
|
|
|
237
237
|
//#endregion
|
|
238
238
|
export { mergeCountHll as a, encodeRelayMessage as i, createSubscriptionId as n, parseClientMessage as o, encodeClientMessage as r, parseRelayMessage as s, assertSubscriptionId as t };
|
|
239
239
|
|
|
240
|
-
//# sourceMappingURL=message-
|
|
240
|
+
//# sourceMappingURL=message-BMfreCQc.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-BMfreCQc.mjs","names":[],"sources":["../src/core/message.ts"],"sourcesContent":["import { MessageError } from \"./error.ts\";\nimport type { Event } from \"./event.ts\";\nimport { validateSignedEvent } from \"./event.ts\";\nimport { canonicalizeFilter, canonicalizeFilters } from \"./filter.ts\";\nimport type { Filter } from \"./filter.ts\";\nimport { SUBSCRIPTION_ID_MAX_CHARS } from \"./limits.ts\";\nimport type { Mutable } from \"./util.ts\";\nimport { bytesToHex, hexToBytes, isRecord } from \"./util.ts\";\n\n/** NIP-01 subscription id: 1..64 chars. */\nexport type SubscriptionId = string;\n\n/** Validate a subscription id; throws {@link MessageError} unless 1..64 Unicode scalar values. */\nexport function assertSubscriptionId(id: string): SubscriptionId {\n // Iterating a string yields code points, so astral chars count once (NIP-01's \"chars\").\n let length = 0;\n for (const _ of id) {\n length += 1;\n }\n if (length === 0 || length > SUBSCRIPTION_ID_MAX_CHARS) {\n throw new MessageError(`subscription id length must be 1..${SUBSCRIPTION_ID_MAX_CHARS}`);\n }\n return id;\n}\n\n/** Validate the given id, or mint a random 16-hex-char subscription id. */\nexport function createSubscriptionId(id?: string): SubscriptionId {\n if (id !== undefined) {\n return assertSubscriptionId(id);\n }\n const bytes = new Uint8Array(8);\n crypto.getRandomValues(bytes);\n return bytesToHex(bytes);\n}\n\n/** Client → relay messages (NIP-01 + NIP-42 AUTH + NIP-45 COUNT + NIP-77). */\nexport type ClientMessage =\n | [\"EVENT\", Event]\n | [\"REQ\", SubscriptionId, ...Filter[]]\n | [\"CLOSE\", SubscriptionId]\n | [\"AUTH\", Event]\n | [\"COUNT\", SubscriptionId, ...Filter[]]\n | [\"NEG-OPEN\", SubscriptionId, Filter, string]\n | [\"NEG-MSG\", SubscriptionId, string]\n | [\"NEG-CLOSE\", SubscriptionId];\n\n/** Relay → client messages. */\nexport type RelayMessage =\n | [\"EVENT\", SubscriptionId, Event]\n | [\"OK\", string, boolean, string]\n | [\"EOSE\", SubscriptionId]\n | [\"CLOSED\", SubscriptionId, string]\n | [\"NOTICE\", string]\n | [\"AUTH\", string]\n | [\"COUNT\", SubscriptionId, CountResult]\n | [\"NEG-MSG\", SubscriptionId, string]\n | [\"NEG-ERR\", SubscriptionId, string];\n\n/** NIP-45 COUNT reply payload. */\nexport type CountResult = {\n readonly count: number;\n readonly approximate?: boolean | undefined;\n /**\n * Optional 512-char hex HyperLogLog sketch (256 registers). Merge sketches with `mergeCountHll`.\n * Estimation is unspecified by NIP-45 and not provided.\n */\n readonly hll?: string | undefined;\n};\n\nconst HLL_BYTES = 256; // 512 hex chars (NIP-45; hex only, not base64)\nconst HLL_HEX_LEN = HLL_BYTES * 2;\nconst HEX_RE = /^[0-9a-fA-F]+$/;\n\n/**\n * Register-wise max of NIP-45 HyperLogLog sketches. Output is always lowercase 512 hex. Empty input\n * is the zero sketch (identity).\n */\nexport function mergeCountHll(hexes: ReadonlyArray<string>): string {\n const merged = new Uint8Array(HLL_BYTES);\n for (const hex of hexes) {\n if (hex.length !== HLL_HEX_LEN || !HEX_RE.test(hex)) {\n throw new MessageError(\"invalid NIP-45 HLL sketch: expected 512-char hex\");\n }\n const bytes = hexToBytes(hex);\n for (const [i, b] of bytes.entries()) {\n const m = merged[i];\n if (m !== undefined && b > m) {\n merged[i] = b;\n }\n }\n }\n return bytesToHex(merged);\n}\n\n/** Serialize a client->relay message to its NIP-01 JSON wire form. */\nexport function encodeClientMessage(message: ClientMessage): string {\n // REQ/COUNT/NEG-OPEN filters are canonicalized so the emitted wire form is\n // the canonical filter `filterFingerprint` uses for subscription identity.\n switch (message[0]) {\n case \"REQ\":\n case \"COUNT\": {\n const [type, id, ...filters] = message;\n return JSON.stringify([type, id, ...canonicalizeFilters(filters)]);\n }\n case \"NEG-OPEN\":\n return JSON.stringify([\"NEG-OPEN\", message[1], canonicalizeFilter(message[2]), message[3]]);\n default:\n return JSON.stringify(message);\n }\n}\n\n/** Serialize a relay->client message to its NIP-01 JSON wire form. */\nexport function encodeRelayMessage(message: RelayMessage): string {\n return JSON.stringify(message);\n}\n\n/** Parse a client->relay JSON message; throws {@link MessageError} on malformed input. */\nexport function parseClientMessage(raw: string): ClientMessage {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new MessageError(\"client message is not valid JSON\");\n }\n if (!Array.isArray(parsed) || parsed.length === 0 || typeof parsed[0] !== \"string\") {\n throw new MessageError(\"client message must be a non-empty JSON array\");\n }\n const items: unknown[] = parsed;\n const [type] = items;\n switch (type) {\n case \"EVENT\":\n if (items.length !== 2 || !validateSignedEvent(items[1])) {\n throw new MessageError(\"invalid EVENT client message\");\n }\n return [\"EVENT\", items[1]];\n\n case \"REQ\":\n if (items.length < 3 || typeof items[1] !== \"string\") {\n throw new MessageError(\"invalid REQ client message\");\n }\n return [\"REQ\", assertSubscriptionId(items[1]), ...parseWireFilters(items.slice(2), \"REQ\")];\n\n case \"CLOSE\":\n if (items.length !== 2 || typeof items[1] !== \"string\") {\n throw new MessageError(\"invalid CLOSE client message\");\n }\n return [\"CLOSE\", assertSubscriptionId(items[1])];\n\n case \"AUTH\":\n if (items.length !== 2 || !validateSignedEvent(items[1])) {\n throw new MessageError(\"invalid AUTH client message\");\n }\n return [\"AUTH\", items[1]];\n\n case \"COUNT\":\n if (items.length < 3 || typeof items[1] !== \"string\") {\n throw new MessageError(\"invalid COUNT client message\");\n }\n return [\n \"COUNT\",\n assertSubscriptionId(items[1]),\n ...parseWireFilters(items.slice(2), \"COUNT\"),\n ];\n\n case \"NEG-OPEN\":\n if (items.length === 5) {\n throw new MessageError(\"obsolete 5-element NEG-OPEN; expected [NEG-OPEN, id, filter, hex]\");\n }\n if (\n items.length !== 4 ||\n typeof items[1] !== \"string\" ||\n !isRecord(items[2]) ||\n !isNegHex(items[3])\n ) {\n throw new MessageError(\"invalid NEG-OPEN client message\");\n }\n return [\n \"NEG-OPEN\",\n assertSubscriptionId(items[1]),\n parseWireFilter(items[2], \"NEG-OPEN\"),\n items[3].toLowerCase(),\n ];\n\n case \"NEG-MSG\":\n if (items.length !== 3 || typeof items[1] !== \"string\" || !isNegHex(items[2])) {\n throw new MessageError(\"invalid NEG-MSG client message\");\n }\n return [\"NEG-MSG\", assertSubscriptionId(items[1]), items[2].toLowerCase()];\n\n case \"NEG-CLOSE\":\n if (items.length !== 2 || typeof items[1] !== \"string\") {\n throw new MessageError(\"invalid NEG-CLOSE client message\");\n }\n return [\"NEG-CLOSE\", assertSubscriptionId(items[1])];\n\n default:\n throw new MessageError(`unknown client message type: ${String(type)}`);\n }\n}\n\n/** Parse a relay->client JSON message; throws {@link MessageError} on malformed input. */\nexport function parseRelayMessage(raw: string): RelayMessage {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new MessageError(\"relay message is not valid JSON\");\n }\n if (!Array.isArray(parsed) || parsed.length === 0 || typeof parsed[0] !== \"string\") {\n throw new MessageError(\"relay message must be a non-empty JSON array\");\n }\n const items: unknown[] = parsed;\n const [type] = items;\n switch (type) {\n case \"EVENT\":\n if (items.length !== 3 || typeof items[1] !== \"string\" || !validateSignedEvent(items[2])) {\n throw new MessageError(\"invalid EVENT relay message\");\n }\n return [\"EVENT\", assertSubscriptionId(items[1]), items[2]];\n\n case \"OK\":\n if (\n items.length !== 4 ||\n typeof items[1] !== \"string\" ||\n !HEX64_LOWER_RE.test(items[1]) ||\n typeof items[2] !== \"boolean\" ||\n typeof items[3] !== \"string\"\n ) {\n throw new MessageError(\"invalid OK relay message\");\n }\n return [\"OK\", items[1], items[2], items[3]];\n\n case \"EOSE\":\n if (items.length !== 2 || typeof items[1] !== \"string\") {\n throw new MessageError(\"invalid EOSE relay message\");\n }\n return [\"EOSE\", assertSubscriptionId(items[1])];\n\n case \"CLOSED\":\n if (items.length !== 3 || typeof items[1] !== \"string\" || typeof items[2] !== \"string\") {\n throw new MessageError(\"invalid CLOSED relay message\");\n }\n return [\"CLOSED\", assertSubscriptionId(items[1]), items[2]];\n\n case \"NOTICE\":\n if (items.length !== 2 || typeof items[1] !== \"string\") {\n throw new MessageError(\"invalid NOTICE relay message\");\n }\n return [\"NOTICE\", items[1]];\n\n case \"AUTH\":\n if (items.length !== 2 || typeof items[1] !== \"string\") {\n throw new MessageError(\"invalid AUTH relay message\");\n }\n return [\"AUTH\", items[1]];\n\n case \"COUNT\": {\n const payload = items.at(2);\n const count = isRecord(payload) ? payload[\"count\"] : undefined;\n if (\n items.length !== 3 ||\n typeof items[1] !== \"string\" ||\n !isRecord(payload) ||\n typeof count !== \"number\" ||\n !Number.isSafeInteger(count) ||\n count < 0\n ) {\n throw new MessageError(\"invalid COUNT relay message\");\n }\n const result: Mutable<CountResult> = { count };\n if (typeof payload[\"approximate\"] === \"boolean\") {\n result.approximate = payload[\"approximate\"];\n }\n const hll = parseCountHll(payload[\"hll\"]);\n if (hll !== undefined) {\n result.hll = hll;\n }\n return [\"COUNT\", assertSubscriptionId(items[1]), result];\n }\n case \"NEG-MSG\":\n if (items.length !== 3 || typeof items[1] !== \"string\" || !isNegHex(items[2])) {\n throw new MessageError(\"invalid NEG-MSG relay message\");\n }\n return [\"NEG-MSG\", assertSubscriptionId(items[1]), items[2].toLowerCase()];\n\n case \"NEG-ERR\":\n if (items.length !== 3 || typeof items[1] !== \"string\" || typeof items[2] !== \"string\") {\n throw new MessageError(\"invalid NEG-ERR relay message\");\n }\n return [\"NEG-ERR\", assertSubscriptionId(items[1]), items[2]];\n\n default:\n throw new MessageError(`unknown relay message type: ${String(type)}`);\n }\n}\n\nfunction parseCountHll(value: unknown): string | undefined {\n if (typeof value !== \"string\" || value.length !== HLL_HEX_LEN || !HEX_RE.test(value)) {\n return undefined;\n }\n return value.toLowerCase();\n}\n\nconst HEX64_RE = /^[0-9a-fA-F]{64}$/;\nconst HEX64_LOWER_RE = /^[0-9a-f]{64}$/;\n\n/**\n * Validates a wire filter per NIP-01: `ids`/`authors` are 64-hex arrays (normalized lowercase),\n * `kinds` are integers in 0..=65535, `since`/`until`/`limit` non-negative integers, `search` a\n * string, and `#<single letter>` arrays of strings (`#e`/`#p` lowercased). Multi-letter `#` keys\n * and unknown non-`#` keys are dropped; wrong types throw {@link MessageError}. Duplicate keys and\n * integer spellings (`1e3`, `1.0`, `-0`) need no handling: `JSON.parse` already applied last-wins\n * and normalized the numbers before this runs.\n */\nfunction parseWireFilter(value: unknown, kind: string): Filter {\n if (!isRecord(value)) {\n throw new MessageError(`invalid ${kind} filter`);\n }\n const fail = (): MessageError => new MessageError(`invalid ${kind} filter`);\n const out: Mutable<Filter> = {};\n for (const [key, v] of Object.entries(value)) {\n if (key.startsWith(\"#\")) {\n if (!/^#[a-zA-Z]$/.test(key)) {\n continue;\n }\n if (!Array.isArray(v) || !v.every((x): x is string => typeof x === \"string\")) {\n throw fail();\n }\n out[`#${key.slice(1)}`] = key === \"#e\" || key === \"#p\" ? v.map((x) => x.toLowerCase()) : v;\n } else if (key === \"ids\" || key === \"authors\") {\n if (\n !Array.isArray(v) ||\n !v.every((x): x is string => typeof x === \"string\" && HEX64_RE.test(x))\n ) {\n throw fail();\n }\n out[key] = v.map((x) => x.toLowerCase());\n } else if (key === \"kinds\") {\n if (\n !Array.isArray(v) ||\n !v.every(\n (x): x is number =>\n typeof x === \"number\" && Number.isSafeInteger(x) && x >= 0 && x <= 65535,\n )\n ) {\n throw fail();\n }\n out.kinds = v;\n } else if (key === \"since\" || key === \"until\" || key === \"limit\") {\n if (typeof v !== \"number\" || !Number.isSafeInteger(v) || v < 0) {\n throw fail();\n }\n out[key] = v;\n } else if (key === \"search\") {\n if (typeof v !== \"string\") {\n throw fail();\n }\n out.search = v;\n }\n }\n return out;\n}\n\nfunction parseWireFilters(items: unknown[], kind: string): Filter[] {\n return items.map((item) => parseWireFilter(item, kind));\n}\n\nfunction isNegHex(value: unknown): value is string {\n return (\n typeof value === \"string\" && value.length > 0 && value.length % 2 === 0 && HEX_RE.test(value)\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { h as Tag, t as Event } from "./event-j3BwI5dF.mjs";
|
|
2
|
-
import { t as EventBuilder } from "./builder-
|
|
3
|
-
import { a as NostrError } from "./error-
|
|
4
|
-
import { a as Rumor, l as WrapOptions, r as Nip59Crypto } from "./nip59-
|
|
2
|
+
import { t as EventBuilder } from "./builder-C72nJITG.mjs";
|
|
3
|
+
import { a as NostrError } from "./error-BPqAU5RB.mjs";
|
|
4
|
+
import { a as Rumor, l as WrapOptions, r as Nip59Crypto } from "./nip59-DfJ6Moeu.mjs";
|
|
5
5
|
//#region src/nips/nip17.d.ts
|
|
6
6
|
declare class Nip17Error extends NostrError {
|
|
7
7
|
name: string;
|
|
@@ -28,11 +28,11 @@ declare function dmRelayListEventBuilder(relays: ReadonlyArray<string>): EventBu
|
|
|
28
28
|
/** Accept a hex pubkey, a Recipient, or a readonly array of either. Dedup by pubkey. */
|
|
29
29
|
declare function normalizeRecipients(input: string | Recipient | ReadonlyArray<string | Recipient>): Recipient[];
|
|
30
30
|
declare function buildChatMessageRumor(senderPubkey: string, recipients: ReadonlyArray<Recipient>, content: string, opts?: ChatMessageOptions): Rumor;
|
|
31
|
-
declare function wrapDirectMessage(crypto: Nip59Crypto, recipients: ReadonlyArray<Recipient>, rumor: Rumor, opts?: Pick<WrapOptions, "now" | "
|
|
31
|
+
declare function wrapDirectMessage(crypto: Nip59Crypto, recipients: ReadonlyArray<Recipient>, rumor: Rumor, opts?: Pick<WrapOptions, "now" | "randomBytes" | "timestamps" | "expiration" | "ephemeral" | "randomize">): Promise<ReadonlyArray<{
|
|
32
32
|
recipient: string;
|
|
33
33
|
wrap: Event;
|
|
34
34
|
}>>;
|
|
35
35
|
declare function requireDmRelays(pubkey: string, relays: ReadonlyArray<string>): string[];
|
|
36
36
|
//#endregion
|
|
37
37
|
export { buildChatMessageRumor as a, normalizeRecipients as c, wrapDirectMessage as d, ReplyTo as i, parseDmRelayList as l, Nip17Error as n, dmRelayListEventBuilder as o, Recipient as r, dmRelayListToTags as s, ChatMessageOptions as t, requireDmRelays as u };
|
|
38
|
-
//# sourceMappingURL=nip17-
|
|
38
|
+
//# sourceMappingURL=nip17-BZvFQLcs.d.mts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as NostrError } from "./error-
|
|
1
|
+
import { a as NostrError } from "./error-BPqAU5RB.mjs";
|
|
2
2
|
//#region src/nips/nip19.d.ts
|
|
3
3
|
/** Bech32 `nprofile1…` string (TLV profile pointer). */
|
|
4
4
|
type NProfile = `nprofile1${string}`;
|
|
@@ -74,4 +74,4 @@ declare function naddrEncode(addr: AddressPointer): NAddr;
|
|
|
74
74
|
declare function decode(code: string): DecodedResult;
|
|
75
75
|
//#endregion
|
|
76
76
|
export { noteEncode as _, NAddr as a, nsecEncode as b, NPub as c, Note as d, ProfilePointer as f, neventEncode as g, naddrEncode as h, EventPointer as i, NSec as l, encodeBytes as m, Bech32MaxSize as n, NEvent as o, decode as p, DecodedResult as r, NProfile as s, AddressPointer as t, Nip19Error as u, nprofileEncode as v, npubEncode as y };
|
|
77
|
-
//# sourceMappingURL=nip19-
|
|
77
|
+
//# sourceMappingURL=nip19-D4RKn6GC.d.mts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as NostrError } from "./error-
|
|
1
|
+
import { a as NostrError } from "./error-BPqAU5RB.mjs";
|
|
2
2
|
//#region src/nips/nip46.d.ts
|
|
3
3
|
type BunkerPointer = {
|
|
4
4
|
/** Remote signer / bunker public key (hex). */
|
|
@@ -47,4 +47,4 @@ declare function encodeNip46Response(res: Nip46Response): string;
|
|
|
47
47
|
declare function decodeNip46Response(json: string): Nip46Response;
|
|
48
48
|
//#endregion
|
|
49
49
|
export { Nip46Response as a, decodeNip46Request as c, encodeNip46Response as d, parseBunkerURL as f, Nip46Request as i, decodeNip46Response as l, toBunkerURL as m, ClientMetadata as n, NostrConnectParams as o, parseNostrConnectURI as p, Nip46Error as r, createNostrConnectURI as s, BunkerPointer as t, encodeNip46Request as u };
|
|
50
|
-
//# sourceMappingURL=nip46-
|
|
50
|
+
//# sourceMappingURL=nip46-i8L8_nIm.d.mts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as Event } from "./event-j3BwI5dF.mjs";
|
|
2
|
-
import { t as EventBuilder } from "./builder-
|
|
2
|
+
import { t as EventBuilder } from "./builder-C72nJITG.mjs";
|
|
3
3
|
//#region src/nips/nip51.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Structural crypto used by NIP-51 private tags. Satisfied by NostrSigner when
|
|
@@ -84,4 +84,4 @@ declare function parseFollowPack(event: Pick<Event, "kind" | "tags">): {
|
|
|
84
84
|
};
|
|
85
85
|
//#endregion
|
|
86
86
|
export { encryptPrivateTags as a, parseEmojiSet as c, parseMuteList as d, parseMuteListPrivate as f, pinListEventBuilder as g, parseUserEmojiList as h, decryptPrivateTags as i, parseFavoriteRelays as l, parseRelaySet as m, Nip51Crypto as n, muteListEventBuilder as o, parsePinList as p, bookmarkListEventBuilder as r, parseBookmarkList as s, MuteItem as t, parseFollowPack as u };
|
|
87
|
-
//# sourceMappingURL=nip51-
|
|
87
|
+
//# sourceMappingURL=nip51-C_kTYcud.d.mts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { h as Tag, r as UnsignedEvent, t as Event } from "./event-j3BwI5dF.mjs";
|
|
2
|
-
import { a as NostrError } from "./error-
|
|
2
|
+
import { a as NostrError } from "./error-BPqAU5RB.mjs";
|
|
3
3
|
//#region src/nips/nip59.d.ts
|
|
4
4
|
/** Unsigned event with a computed id. Never has `sig`. */
|
|
5
5
|
type Rumor = UnsignedEvent & {
|
|
@@ -27,9 +27,17 @@ type TimestampRandomize = "wrap" | "seal+wrap";
|
|
|
27
27
|
type WrapOptions = {
|
|
28
28
|
/** Unix seconds used as the randomization window end. Default: floor(Date.now()/1000). */
|
|
29
29
|
readonly now?: number | undefined;
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Entropy source replacing @noble/hashes `randomBytes` for everything `wrap`/`createGiftWrap`
|
|
32
|
+
* draws — in stream order: the ephemeral secret key (32-byte draws, redrawn while not a valid
|
|
33
|
+
* scalar), the timestamp offset (4-byte big-endian u32 draws under the [0, 2 days) rejection
|
|
34
|
+
* bound), the NIP-44 nonce (32 bytes), and the BIP-340 auxiliary randomness (32 bytes). In
|
|
35
|
+
* `createSeal` only the timestamp offset is drawn here; the seal's nonce and aux belong to the
|
|
36
|
+
* injected {@link Nip59Crypto}. `wrap` = seal draws then gift-wrap draws. Default: noble
|
|
37
|
+
* `randomBytes`.
|
|
38
|
+
*/
|
|
39
|
+
readonly randomBytes?: ((n: number) => Uint8Array) | undefined;
|
|
40
|
+
/** When set, used as-is. Overrides now/randomBytes for this call. */
|
|
33
41
|
readonly timestamps?: GiftWrapTimestamps | undefined;
|
|
34
42
|
readonly relayHint?: string | undefined;
|
|
35
43
|
/** Appended after the required wrap `p` tag. Never applied to the seal. */
|
|
@@ -47,7 +55,7 @@ type WrapOptions = {
|
|
|
47
55
|
*/
|
|
48
56
|
readonly randomize?: TimestampRandomize | undefined;
|
|
49
57
|
};
|
|
50
|
-
type SealOptions = Pick<WrapOptions, "now" | "
|
|
58
|
+
type SealOptions = Pick<WrapOptions, "now" | "randomBytes" | "timestamps" | "expiration" | "randomize">;
|
|
51
59
|
declare class Nip59Error extends NostrError {
|
|
52
60
|
name: string;
|
|
53
61
|
}
|
|
@@ -68,7 +76,7 @@ declare function rumorToJson(rumor: Rumor): string;
|
|
|
68
76
|
declare function eventToJson(event: Event): string;
|
|
69
77
|
declare function randomPastTimestamp(opts?: {
|
|
70
78
|
now?: number | undefined;
|
|
71
|
-
|
|
79
|
+
randomBytes?: ((n: number) => Uint8Array) | undefined;
|
|
72
80
|
}): number;
|
|
73
81
|
declare function createRumor(pubkey: string, template: {
|
|
74
82
|
kind: number;
|
|
@@ -82,4 +90,4 @@ declare function wrap(crypto: Nip59Crypto, recipient: string, rumor: Rumor, opts
|
|
|
82
90
|
declare function unwrap(crypto: Nip44Decryptor, giftWrap: Event): Promise<Rumor>;
|
|
83
91
|
//#endregion
|
|
84
92
|
export { requireNip59Crypto as _, Rumor as a, wrap as b, TimestampRandomize as c, createRumor as d, createSeal as f, requireNip44Decryptor as g, randomPastTimestamp as h, Nip59Error as i, WrapOptions as l, isGiftWrapKind as m, Nip44Decryptor as n, SealOptions as o, eventToJson as p, Nip59Crypto as r, TWO_DAYS_SECS as s, GiftWrapTimestamps as t, createGiftWrap as u, rumorToJson as v, unwrap as y };
|
|
85
|
-
//# sourceMappingURL=nip59-
|
|
93
|
+
//# sourceMappingURL=nip59-DfJ6Moeu.d.mts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { h as Tag, t as Event } from "./event-j3BwI5dF.mjs";
|
|
2
|
-
import { t as EventBuilder } from "./builder-
|
|
2
|
+
import { t as EventBuilder } from "./builder-C72nJITG.mjs";
|
|
3
3
|
//#region src/nips/nip65.d.ts
|
|
4
4
|
/** NIP-65 `r` tag marker: read-only, write-only, or unmarked (both). */
|
|
5
5
|
type RelayMarker = "read" | "write" | "both";
|
|
@@ -20,4 +20,4 @@ declare function readRelays(items: ReadonlyArray<RelayListItem>): string[];
|
|
|
20
20
|
declare function writeRelays(items: ReadonlyArray<RelayListItem>): string[];
|
|
21
21
|
//#endregion
|
|
22
22
|
export { relayListEventBuilder as a, readRelays as i, RelayMarker as n, relayListToTags as o, parseRelayList as r, writeRelays as s, RelayListItem as t };
|
|
23
|
-
//# sourceMappingURL=nip65-
|
|
23
|
+
//# sourceMappingURL=nip65-CPNE0SV0.d.mts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as Event } from "./event-j3BwI5dF.mjs";
|
|
2
|
-
import { a as NostrError } from "./error-
|
|
2
|
+
import { a as NostrError } from "./error-BPqAU5RB.mjs";
|
|
3
3
|
//#region src/nips/nip77.d.ts
|
|
4
4
|
declare const PROTOCOL_VERSION = 97;
|
|
5
5
|
declare const DEFAULT_FRAME_SIZE_LIMIT = 60000;
|
|
@@ -52,4 +52,4 @@ declare function runNegSession(opts: {
|
|
|
52
52
|
}>;
|
|
53
53
|
//#endregion
|
|
54
54
|
export { NegentropyStorageVector as a, ReconcileOutcome as c, storageFromItems as d, Negentropy as i, runNegSession as l, MAX_NEG_ROUNDS as n, Nip77Error as o, NegItem as r, PROTOCOL_VERSION as s, DEFAULT_FRAME_SIZE_LIMIT as t, storageFromEvents as u };
|
|
55
|
-
//# sourceMappingURL=nip77-
|
|
55
|
+
//# sourceMappingURL=nip77-DhpoL7d8.d.mts.map
|
package/dist/nips/blossom.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as EventTemplate, t as Event } from "../event-j3BwI5dF.mjs";
|
|
2
|
-
import { t as EventBuilder } from "../builder-
|
|
3
|
-
import { a as NostrError } from "../error-
|
|
2
|
+
import { t as EventBuilder } from "../builder-C72nJITG.mjs";
|
|
3
|
+
import { a as NostrError } from "../error-BPqAU5RB.mjs";
|
|
4
4
|
import { t as ManualFetch } from "../http-kwWG-V1X.mjs";
|
|
5
5
|
//#region src/nips/blossom.d.ts
|
|
6
6
|
export type BlobDescriptor = {
|
package/dist/nips/blossom.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as Kind } from "../kind-
|
|
3
|
-
import { t as EventBuilder } from "../builder-
|
|
1
|
+
import { g as utf8Encoder, i as bytesToHex, n as assertHex32, p as nowSeconds, u as isRecord, v as EventValidationError, x as NostrError } from "../util-DfsyJ9nz.mjs";
|
|
2
|
+
import { t as Kind } from "../kind-I2Tvbe82.mjs";
|
|
3
|
+
import { t as EventBuilder } from "../builder-CGgM_QHT.mjs";
|
|
4
4
|
import { n as requireGlobalFetch, r as sendManual, t as fetchManual } from "../http-CuazNRIP.mjs";
|
|
5
5
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
6
6
|
import { base64urlnopad } from "@scure/base";
|
|
@@ -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 no-await-in-loop -- probes servers in order, returning on the first hit\n const status = await headStatus(fetchImpl, candidate, opts?.signal);\n if (status !== undefined && status >= 200 && status < 300) {\n return candidate;\n }\n }\n return url;\n}\n\n/** PUT /upload to each server in order until one descriptor succeeds. */\nexport async function uploadToServers(\n servers: ReadonlyArray<string>,\n file: Blob,\n auth: Event,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<BlobDescriptor> {\n if (servers.length === 0) {\n throw new BlossomError(\"no servers\");\n }\n let last: BlossomError | undefined;\n for (const server of servers) {\n try {\n // oxlint-disable-next-line no-await-in-loop -- tries servers sequentially until one succeeds\n return await upload(server, file, auth, opts);\n } catch (error) {\n if (isAbortError(error)) {\n throw error;\n }\n last =\n error instanceof BlossomError\n ? error\n : new BlossomError(\"blossom upload failed\", {\n cause: error,\n });\n }\n }\n throw last ?? new BlossomError(\"no servers\");\n}\n\nexport function blossomServerListEventBuilder(servers: ReadonlyArray<string>): EventBuilder {\n const tags: Tag[] = [];\n const seen = new Set<string>();\n for (const raw of servers) {\n const url = parseHttpUrl(raw);\n if (url === undefined || seen.has(url)) {\n continue;\n }\n seen.add(url);\n tags.push([\"server\", url]);\n }\n if (tags.length === 0) {\n throw new BlossomError(\"server list requires at least one valid http(s) URL\");\n }\n return new EventBuilder(Kind.BlossomServerList, \"\").tags(tags);\n}\n\nexport function parseBlossomServerList(event: Pick<Event, \"kind\" | \"tags\">): string[] {\n if (event.kind !== Kind.BlossomServerList) {\n throw new EventValidationError(`expected kind ${Kind.BlossomServerList}, got ${event.kind}`);\n }\n const out: string[] = [];\n const seen = new Set<string>();\n for (const tag of event.tags) {\n const tagValue = tag.at(1);\n if (tag[0] !== \"server\" || tagValue === undefined || tagValue === \"\") {\n continue;\n }\n const url = parseHttpUrl(tagValue);\n if (url === undefined || seen.has(url)) {\n continue;\n }\n seen.add(url);\n out.push(url);\n }\n return out;\n}\n\nfunction parseHttpUrl(raw: string): string | undefined {\n try {\n const u = new URL(raw);\n if (u.protocol !== \"http:\" && u.protocol !== \"https:\") {\n return undefined;\n }\n const path = u.pathname.replace(/\\/+$/, \"\");\n return `${u.origin}${path}`;\n } catch {\n return undefined;\n }\n}\n\n/** Join `/upload` etc. onto the stored server, keeping a path prefix (`/v1/upload`). */\nfunction blossomUrl(server: string, path: string): string {\n const base = parseHttpUrl(server);\n if (base === undefined) {\n throw new BlossomError(`invalid blossom server URL: ${server}`);\n }\n const suffix = path.startsWith(\"/\") ? path : `/${path}`;\n return `${base}${suffix}`;\n}\n\nfunction isAbortError(err: unknown): boolean {\n return err instanceof Error && err.name === \"AbortError\";\n}\n\n/** Path suffix after the last 64-hex segment (`HASH.png` → `.png`). */\nfunction extensionFromHashUrl(url: string, hash: string): string {\n let pathname: string;\n try {\n ({ pathname } = new URL(url));\n } catch {\n return \"\";\n }\n const i = pathname.toLowerCase().lastIndexOf(hash);\n if (i === -1) {\n return \"\";\n }\n const m = /^\\.[^/]+/.exec(pathname.slice(i + 64));\n return m?.[0] ?? \"\";\n}\n\nfunction missingBlossomFetch(): BlossomError {\n return new BlossomError(\"no fetch implementation available; pass opts.fetch\");\n}\n\ntype ManualResponse = Awaited<ReturnType<ManualFetch>>;\n\nfunction blossomHttpError(method: string, url: string, res: ManualResponse): BlossomError {\n let reason: string | undefined;\n try {\n reason = res.headers.get(\"x-reason\") ?? undefined;\n } catch {\n reason = undefined;\n }\n const detail = reason?.trim();\n return new BlossomError(\n detail === undefined || detail === \"\"\n ? `blossom ${method} ${url} failed (${res.status})`\n : detail,\n { status: res.status },\n );\n}\n\nasync function blossomFetch(\n fetchImpl: BlossomFetch | undefined,\n url: string,\n init: {\n method: string;\n headers?: Record<string, string>;\n body?: Blob | string;\n signal?: AbortSignal | undefined;\n },\n): Promise<ManualResponse> {\n return fetchManual(\n fetchImpl ?? requireGlobalFetch(missingBlossomFetch),\n url,\n init,\n (err) =>\n new BlossomError(`blossom ${init.method} ${url} failed`, {\n cause: err,\n }),\n );\n}\n\n/**\n * HEAD with redirect:manual. Network → undefined. AbortError propagates. Never throws on HTTP\n * status.\n */\nasync function headStatus(\n fetchImpl: ManualFetch,\n url: string,\n signal?: AbortSignal,\n): Promise<number | undefined> {\n try {\n const res = await sendManual(fetchImpl, url, { method: \"HEAD\", signal });\n return res.status;\n } catch (error) {\n if (isAbortError(error)) {\n throw error;\n }\n return undefined;\n }\n}\n\nasync function blossomRequest(\n fetchImpl: BlossomFetch | undefined,\n url: string,\n init: {\n method: string;\n headers?: Record<string, string>;\n body?: Blob | string;\n signal?: AbortSignal | undefined;\n },\n): Promise<ManualResponse> {\n const res = await blossomFetch(fetchImpl, url, init);\n if (res.ok) {\n return res;\n }\n throw blossomHttpError(init.method, url, res);\n}\n\nasync function readJson(res: ManualResponse): Promise<unknown> {\n try {\n return await res.json();\n } catch (error) {\n throw new BlossomError(\"invalid blossom JSON response\", {\n cause: error,\n status: res.status,\n });\n }\n}\n\nfunction parseBlobDescriptor(json: unknown): BlobDescriptor {\n if (!isRecord(json)) {\n throw new BlossomError(\"invalid blob descriptor\");\n }\n const raw = json;\n if (typeof raw[\"url\"] !== \"string\" || !raw[\"url\"]) {\n throw new BlossomError(\"blob descriptor missing url\");\n }\n if (typeof raw[\"sha256\"] !== \"string\") {\n throw new BlossomError(\"blob descriptor missing sha256\");\n }\n const hash = assertHex32(raw[\"sha256\"], \"blob sha256\");\n if (typeof raw[\"size\"] !== \"number\" || !Number.isInteger(raw[\"size\"]) || raw[\"size\"] < 0) {\n throw new BlossomError(\"blob descriptor missing size\");\n }\n const desc: Mutable<BlobDescriptor> = { url: raw[\"url\"], sha256: hash, size: raw[\"size\"] };\n if (typeof raw[\"type\"] === \"string\") {\n desc.type = raw[\"type\"];\n }\n if (\n typeof raw[\"uploaded\"] === \"number\" &&\n Number.isInteger(raw[\"uploaded\"]) &&\n raw[\"uploaded\"] >= 0\n ) {\n desc.uploaded = raw[\"uploaded\"];\n }\n return desc;\n}\n\nfunction requireDescriptorHash(desc: BlobDescriptor, expected: string): BlobDescriptor {\n if (desc.sha256 !== expected) {\n throw new BlossomError(\"blob descriptor sha256 mismatch\");\n }\n return desc;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"blossom.mjs","names":[],"sources":["../../src/nips/blossom.ts"],"sourcesContent":["/**\n * Blossom BUD-01/02/04/06 HTTP helpers, NIP-B7 kind 10063 server lists, and URL healing. Auth is\n * kind 24242, not NIP-98; the Authorization value is base64url.\n *\n * @see https://github.com/hzrd149/blossom\n * @see https://github.com/nostr-protocol/nips/blob/master/B7.md\n */\nimport { sha256 } from \"@noble/hashes/sha2.js\";\nimport { base64urlnopad } from \"@scure/base\";\n\nimport { EventBuilder } from \"../core/builder.ts\";\nimport { EventValidationError, NostrError } from \"../core/error.ts\";\nimport type { Event, EventTemplate } from \"../core/event.ts\";\nimport { Kind } from \"../core/kind.ts\";\nimport type { Tag } from \"../core/tag.ts\";\nimport type { Mutable } from \"../core/util.ts\";\nimport { assertHex32, bytesToHex, isRecord, nowSeconds, utf8Encoder } from \"../core/util.ts\";\nimport { fetchManual, requireGlobalFetch, sendManual } from \"./http.ts\";\nimport type { ManualFetch } from \"./http.ts\";\n\nexport type BlobDescriptor = {\n readonly url: string;\n readonly sha256: string;\n readonly size: number;\n readonly type?: string | undefined;\n readonly uploaded?: number | undefined;\n};\n\nexport type BlossomFetch = ManualFetch;\n\n/** Signs a kind 24242 auth template (draft; pubkey is filled by the signer). */\nexport type BlossomSign = (template: EventTemplate) => Promise<Event>;\n\n/** BUD-11 `t` verbs. Mirror reuses `upload`; there is no `t=mirror`. */\nexport type BlossomAuthVerb = \"upload\" | \"delete\" | \"list\" | \"get\" | \"media\";\n\nconst AUTH_VERBS = new Set<BlossomAuthVerb>([\"upload\", \"delete\", \"list\", \"get\", \"media\"]);\n\n/** Default BUD-11 expiration: now + 1h. Servers 401 when the tag is missing. */\nconst AUTH_EXPIRATION_SECS = 3600;\n\nexport class BlossomError extends NostrError {\n override name = \"BlossomError\";\n readonly status?: number | undefined;\n\n constructor(message: string, options?: ErrorOptions & { status?: number }) {\n const { status, ...rest } = options ?? {};\n super(message, rest);\n this.status = status;\n }\n}\n\n/** Last 64-char hex in the URL path (extension ignored). Undefined if none or the URL is invalid. */\nexport function getHashFromURL(url: string | URL): string | undefined {\n let path: string;\n try {\n path = (url instanceof URL ? url : new URL(url)).pathname;\n } catch {\n return undefined;\n }\n const matches = path.match(/[0-9a-f]{64}/gi);\n if (matches === null || matches.length === 0) {\n return undefined;\n }\n return matches.at(-1)?.toLowerCase();\n}\n\nexport async function sha256Blob(data: Blob | ArrayBuffer | Uint8Array): Promise<string> {\n let bytes: Uint8Array;\n if (data instanceof Uint8Array) {\n bytes = data;\n } else if (data instanceof ArrayBuffer) {\n bytes = new Uint8Array(data);\n } else {\n bytes = new Uint8Array(await data.arrayBuffer());\n }\n return bytesToHex(sha256(bytes));\n}\n\n/** True when `sha256Blob(data)` equals the expected hex (any case). */\nexport async function verifyBlob(\n data: Blob | ArrayBuffer | Uint8Array,\n sha256Hex: string,\n): Promise<boolean> {\n const expected = assertHex32(sha256Hex, \"blob sha256\");\n return (await sha256Blob(data)) === expected;\n}\n\n/** `Nostr ` + base64url(utf8 JSON), padding stripped. NIP-98 uses standard base64; mixing them is a 401. */\nexport function encodeAuthorizationHeader(event: Event): string {\n return `Nostr ${base64urlnopad.encode(utf8Encoder.encode(JSON.stringify(event)))}`;\n}\n\n/** Default BUD-11 human-readable content per verb. */\nconst AUTH_VERB_MESSAGES: Record<BlossomAuthVerb, string> = {\n upload: \"Upload Blob\",\n delete: \"Delete Blob\",\n list: \"List Blobs\",\n get: \"Get Blob\",\n media: \"Upload Media\",\n};\n\n/**\n * Hostname for a BUD-11 `server` tag: accepts a full URL or a bare domain; lowercased, undefined\n * when unparseable.\n */\nfunction serverHostname(raw: string): string | undefined {\n try {\n const url = new URL(raw.includes(\"://\") ? raw : `https://${raw}`);\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n return undefined;\n }\n const hostname = url.hostname.toLowerCase();\n return hostname === \"\" ? undefined : hostname;\n } catch {\n return undefined;\n }\n}\n\nexport function createAuthTemplate(\n verb: BlossomAuthVerb,\n opts?: {\n sha256?: string | undefined;\n expiration?: number | undefined;\n message?: string | undefined;\n servers?: ReadonlyArray<string> | undefined;\n },\n): EventTemplate {\n if (!AUTH_VERBS.has(verb)) {\n throw new BlossomError(`invalid blossom auth verb: ${verb}`);\n }\n if (opts?.message !== undefined && opts.message.trim() === \"\") {\n throw new BlossomError(\"blossom auth message must not be empty\");\n }\n const expiration = opts?.expiration ?? nowSeconds() + AUTH_EXPIRATION_SECS;\n const tags: Tag[] = [\n [\"t\", verb],\n [\"expiration\", String(expiration)],\n ];\n if (opts?.sha256 !== undefined) {\n tags.push([\"x\", assertHex32(opts.sha256, \"blob sha256\")]);\n }\n const seenServers = new Set<string>();\n for (const server of opts?.servers ?? []) {\n const hostname = serverHostname(server);\n if (hostname === undefined || seenServers.has(hostname)) {\n continue;\n }\n seenServers.add(hostname);\n tags.push([\"server\", hostname]);\n }\n return {\n kind: Kind.BlobsAuth,\n created_at: nowSeconds(),\n tags,\n content: opts?.message ?? AUTH_VERB_MESSAGES[verb],\n };\n}\n\nexport async function createUploadAuth(\n sign: BlossomSign,\n file: Blob,\n opts?: {\n message?: string | undefined;\n expiration?: number | undefined;\n servers?: ReadonlyArray<string> | undefined;\n },\n): Promise<Event> {\n const hash = await sha256Blob(file);\n return sign(createAuthTemplate(\"upload\", { sha256: hash, ...opts }));\n}\n\nexport async function upload(\n server: string,\n file: Blob,\n auth: Event,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal; sha256?: string | undefined },\n): Promise<BlobDescriptor> {\n const hash =\n opts?.sha256 === undefined ? await sha256Blob(file) : assertHex32(opts.sha256, \"blob sha256\");\n const headers: Record<string, string> = {\n Authorization: encodeAuthorizationHeader(auth),\n \"X-SHA-256\": hash,\n };\n if (file.type !== \"\") {\n headers[\"Content-Type\"] = file.type;\n }\n const res = await blossomRequest(opts?.fetch, blossomUrl(server, \"/upload\"), {\n method: \"PUT\",\n headers,\n body: file,\n signal: opts?.signal,\n });\n return requireDescriptorHash(parseBlobDescriptor(await readJson(res)), hash);\n}\n\nexport async function listBlobs(\n server: string,\n pubkey: string,\n auth: Event | undefined,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<BlobDescriptor[]> {\n const pk = assertHex32(pubkey, \"pubkey\");\n const headers: Record<string, string> = {};\n if (auth) {\n headers[\"Authorization\"] = encodeAuthorizationHeader(auth);\n }\n const res = await blossomRequest(opts?.fetch, blossomUrl(server, `/list/${pk}`), {\n method: \"GET\",\n headers,\n signal: opts?.signal,\n });\n const json = await readJson(res);\n if (!Array.isArray(json)) {\n throw new BlossomError(\"blossom list response must be a JSON array\", { status: res.status });\n }\n return json.map(parseBlobDescriptor);\n}\n\nexport async function deleteBlob(\n server: string,\n sha256Hex: string,\n auth: Event,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<void> {\n const hash = assertHex32(sha256Hex, \"blob sha256\");\n await blossomRequest(opts?.fetch, blossomUrl(server, `/${hash}`), {\n method: \"DELETE\",\n headers: { Authorization: encodeAuthorizationHeader(auth) },\n signal: opts?.signal,\n });\n}\n\nexport async function mirrorBlob(\n server: string,\n blob: BlobDescriptor,\n opts: { auth: Event; fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<BlobDescriptor> {\n const hash = assertHex32(blob.sha256, \"blob sha256\");\n const headers: Record<string, string> = {\n Authorization: encodeAuthorizationHeader(opts.auth),\n \"Content-Type\": \"application/json\",\n \"X-SHA-256\": hash,\n \"X-Content-Length\": String(blob.size),\n };\n if (blob.type !== undefined && blob.type !== \"\") {\n headers[\"X-Content-Type\"] = blob.type;\n }\n const res = await blossomRequest(opts.fetch, blossomUrl(server, \"/mirror\"), {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ url: blob.url }),\n signal: opts.signal,\n });\n return requireDescriptorHash(parseBlobDescriptor(await readJson(res)), hash);\n}\n\n/** BUD-06 preflight. Does not PUT the body. */\nexport async function checkUpload(\n server: string,\n file: Blob,\n auth: Event,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal; sha256?: string | undefined },\n): Promise<void> {\n const hash =\n opts?.sha256 === undefined ? await sha256Blob(file) : assertHex32(opts.sha256, \"blob sha256\");\n const headers: Record<string, string> = {\n Authorization: encodeAuthorizationHeader(auth),\n \"X-SHA-256\": hash,\n \"X-Content-Length\": String(file.size),\n };\n if (file.type) {\n headers[\"X-Content-Type\"] = file.type;\n }\n await blossomRequest(opts?.fetch, blossomUrl(server, \"/upload\"), {\n method: \"HEAD\",\n headers,\n signal: opts?.signal,\n });\n}\n\n/**\n * Strict HEAD probe. 2xx → true. 404 → false. Other HTTP (including 405) / network → throw\n * BlossomError. AbortError propagates. No GET fallback.\n */\nexport async function blobExists(\n server: string,\n sha256Hex: string,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<boolean> {\n const hash = assertHex32(sha256Hex, \"blob sha256\");\n const url = blossomUrl(server, `/${hash}`);\n const res = await blossomFetch(opts?.fetch, url, { method: \"HEAD\", signal: opts?.signal });\n if (res.status === 404) {\n return false;\n }\n if (res.ok) {\n return true;\n }\n throw blossomHttpError(\"HEAD\", url, res);\n}\n\n/** GET body, then verifyBlob. Non-2xx or sha256 mismatch → throw BlossomError. */\nexport async function getBlob(\n server: string,\n sha256Hex: string,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<Uint8Array> {\n const hash = assertHex32(sha256Hex, \"blob sha256\");\n const url = blossomUrl(server, `/${hash}`);\n const res = await blossomFetch(opts?.fetch, url, { method: \"GET\", signal: opts?.signal });\n if (!res.ok) {\n throw blossomHttpError(\"GET\", url, res);\n }\n const bytes = new Uint8Array(await res.arrayBuffer());\n if (!(await verifyBlob(bytes, hash))) {\n throw new BlossomError(\"blob sha256 mismatch\");\n }\n return bytes;\n}\n\n/**\n * If `url` is not available, try `servers` for the same hash (NIP-B7 SHOULD). Never throws on HTTP\n * status. AbortError still throws. Returns the original URL when no hash, original still available,\n * or nothing else responds.\n */\nexport async function healBlobUrl(\n url: string,\n servers: ReadonlyArray<string>,\n opts?: { fetch?: BlossomFetch; signal?: AbortSignal },\n): Promise<string> {\n const hash = getHashFromURL(url);\n if (hash === undefined) {\n return url;\n }\n\n const ext = extensionFromHashUrl(url, hash);\n const fetchImpl = opts?.fetch ?? requireGlobalFetch(missingBlossomFetch);\n\n const originalStatus = await headStatus(fetchImpl, url, opts?.signal);\n // 2xx/3xx: still available. NIP-B7 only heals URLs that are not.\n if (originalStatus !== undefined && originalStatus >= 200 && originalStatus < 400) {\n return url;\n }\n\n for (const server of servers) {\n const base = parseHttpUrl(server);\n if (base === undefined) {\n continue;\n }\n const candidate = `${base}/${hash}${ext}`;\n // oxlint-disable-next-line 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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"}
|
package/dist/nips/nip04.d.mts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
//#region src/nips/nip04.d.ts
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Encrypt plaintext with the raw 32-byte ECDH x-coordinate (NIP-04's key material, like NIP-44's
|
|
4
|
+
* conversation key), for key holders that only expose ECDH ({@link NostrKeyOperations}).
|
|
5
|
+
*/
|
|
6
|
+
export declare function encrypt(plaintext: string, sharedSecret: Uint8Array): string;
|
|
7
|
+
/** Decrypt a NIP-04 payload with the raw 32-byte ECDH x-coordinate. */
|
|
8
|
+
export declare function decrypt(payload: string, sharedSecret: Uint8Array): string;
|
|
9
|
+
/** 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
|
+
/** 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;
|
|
6
13
|
//#endregion
|
|
7
14
|
//# sourceMappingURL=nip04.d.mts.map
|
package/dist/nips/nip04.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as CryptoError, g as utf8Encoder, h as utf8Decoder, n as assertHex32, r as assertSecretKeyBytes, s as hexToBytes$1, t as assertByteLength } from "../util-DfsyJ9nz.mjs";
|
|
2
2
|
import { randomBytes } from "@noble/hashes/utils.js";
|
|
3
3
|
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
4
4
|
import { cbc } from "@noble/ciphers/aes.js";
|
|
@@ -13,35 +13,58 @@ function normalizeSharedSecret(privkey, pubkey) {
|
|
|
13
13
|
throw new CryptoError("invalid NIP-04 public key", { cause: error });
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
function encrypt(secretKey, pubkey, text) {
|
|
23
|
-
const normalizedKey = normalizeSharedSecret(resolveSecret(secretKey), pubkey);
|
|
16
|
+
/**
|
|
17
|
+
* Encrypt plaintext with the raw 32-byte ECDH x-coordinate (NIP-04's key material, like NIP-44's
|
|
18
|
+
* conversation key), for key holders that only expose ECDH ({@link NostrKeyOperations}).
|
|
19
|
+
*/
|
|
20
|
+
function encrypt(plaintext, sharedSecret) {
|
|
21
|
+
assertByteLength(sharedSecret, 32, "shared secret");
|
|
24
22
|
const iv = randomBytes(16);
|
|
25
|
-
const
|
|
26
|
-
const ciphertext = cbc(
|
|
23
|
+
const plaintextBytes = utf8Encoder.encode(plaintext);
|
|
24
|
+
const ciphertext = cbc(sharedSecret, iv).encrypt(plaintextBytes);
|
|
27
25
|
return `${base64.encode(ciphertext)}?iv=${base64.encode(iv)}`;
|
|
28
26
|
}
|
|
29
|
-
/** Decrypt a NIP-04 payload
|
|
30
|
-
function decrypt(
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
if (parts.length !== 2 || parts[0] === void 0 || parts[0] === "" || parts[1] === void 0 || parts[1] === "") throw new CryptoError("invalid NIP-04 payload: missing iv");
|
|
34
|
-
const normalizedKey = normalizeSharedSecret(privkey, pubkey);
|
|
27
|
+
/** Decrypt a NIP-04 payload with the raw 32-byte ECDH x-coordinate. */
|
|
28
|
+
function decrypt(payload, sharedSecret) {
|
|
29
|
+
assertByteLength(sharedSecret, 32, "shared secret");
|
|
30
|
+
const [ciphertextPart, ivPart] = splitPayload(payload);
|
|
35
31
|
try {
|
|
36
|
-
const iv = base64.decode(
|
|
37
|
-
const ciphertext = base64.decode(
|
|
38
|
-
const plaintext = cbc(
|
|
32
|
+
const iv = base64.decode(ivPart);
|
|
33
|
+
const ciphertext = base64.decode(ciphertextPart);
|
|
34
|
+
const plaintext = cbc(sharedSecret, iv).decrypt(ciphertext);
|
|
39
35
|
return utf8Decoder.decode(plaintext);
|
|
40
36
|
} catch (error) {
|
|
41
37
|
throw new CryptoError("invalid NIP-04 payload", { cause: error });
|
|
42
38
|
}
|
|
43
39
|
}
|
|
40
|
+
/** Encrypt plaintext to a peer pubkey, deriving the shared secret from `secretKey`. */
|
|
41
|
+
function encryptToPubkey(plaintext, secretKey, peerPubkey) {
|
|
42
|
+
assertSecretKeyBytes(secretKey);
|
|
43
|
+
const sharedSecret = normalizeSharedSecret(secretKey, peerPubkey);
|
|
44
|
+
try {
|
|
45
|
+
return encrypt(plaintext, sharedSecret);
|
|
46
|
+
} finally {
|
|
47
|
+
sharedSecret.fill(0);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Decrypt a NIP-04 payload from a peer pubkey, deriving the shared secret from `secretKey`. */
|
|
51
|
+
function decryptFromPubkey(payload, secretKey, peerPubkey) {
|
|
52
|
+
assertSecretKeyBytes(secretKey);
|
|
53
|
+
splitPayload(payload);
|
|
54
|
+
const sharedSecret = normalizeSharedSecret(secretKey, peerPubkey);
|
|
55
|
+
try {
|
|
56
|
+
return decrypt(payload, sharedSecret);
|
|
57
|
+
} finally {
|
|
58
|
+
sharedSecret.fill(0);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function splitPayload(data) {
|
|
62
|
+
const parts = data.split("?iv=");
|
|
63
|
+
const [ciphertext, iv] = parts;
|
|
64
|
+
if (parts.length !== 2 || ciphertext === void 0 || ciphertext === "" || iv === void 0 || iv === "") throw new CryptoError("invalid NIP-04 payload: missing iv");
|
|
65
|
+
return [ciphertext, iv];
|
|
66
|
+
}
|
|
44
67
|
//#endregion
|
|
45
|
-
export { decrypt, encrypt };
|
|
68
|
+
export { decrypt, decryptFromPubkey, encrypt, encryptToPubkey };
|
|
46
69
|
|
|
47
70
|
//# sourceMappingURL=nip04.mjs.map
|