@kehto/nip 0.1.0 → 0.4.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/5d/index.ts","../src/5d/artifact-cache.ts"],"sourcesContent":["import type { NostrEvent } from 'nostr-tools';\nimport { verifyEvent } from 'nostr-tools/pure';\nimport { sha256 } from '@noble/hashes/sha2.js';\nimport { bytesToHex } from '@noble/hashes/utils.js';\nimport {\n computeAggregateHash,\n pathEntriesFromTags,\n aggregateTagValue,\n type PathEntry,\n} from '../5a/index.js';\nimport type { NappletArtifactCache } from './artifact-cache.js';\nexport {\n CacheStorageNappletArtifactCache,\n NAPPLET_ARTIFACT_CACHE_NAME,\n coordinateKey,\n isCoordinateFresh,\n openNappletArtifactCache,\n type CachedCoordinate,\n type CacheStorageNappletArtifactCacheOptions,\n type CoordinateFreshnessOptions,\n type NappletAggregateIndexEntry,\n type NappletArtifactCacheIndex,\n type NappletArtifactCache,\n type NappletBlobIndexEntry,\n type NappletCacheDiagnostic,\n type OpenNappletArtifactCacheOptions,\n type WriteVerifiedResolutionInput,\n} from './artifact-cache.js';\n\n/**\n * `@kehto/nip/5d` — NIP-5D napplet manifest resolution.\n *\n * NIP-5D (`dskvr/nips` branch `nip/5d`) defines content-addressed \"napplet\"\n * web applets published as Nostr events that reference their files by hash\n * (NIP-5A `path` tags + an aggregate `x` tag). A runtime resolves a napplet by:\n *\n * 1. verifying the manifest event's signature,\n * 2. recomputing the NIP-5A aggregate from its `path` tags and checking the\n * `[\"x\",\"<hex>\",\"aggregate\"]` tag,\n * 3. fetching each referenced blob (e.g. from Blossom) and verifying its hash,\n * 4. assembling the verified `/index.html`.\n *\n * The napplet's identity is the `(dTag, aggregateHash)` tuple **computed** from\n * these verified bytes — it is never accepted from a host or gateway.\n */\n\n/** Snapshot manifest — regular event, immutable point-in-time release. */\nexport const NAPPLET_KIND_SNAPSHOT = 5129;\n/** Root manifest — replaceable event, an author's latest unnamed napplet. */\nexport const NAPPLET_KIND_ROOT = 15129;\n/** Named manifest — addressable event (carries a `d` tag identifier). */\nexport const NAPPLET_KIND_NAMED = 35129;\n\n/** All three NIP-5D napplet manifest kinds. */\nexport const NAPPLET_KINDS: readonly number[] = [\n NAPPLET_KIND_SNAPSHOT,\n NAPPLET_KIND_ROOT,\n NAPPLET_KIND_NAMED,\n];\n\n/**\n * Whether `kind` is one of the three NIP-5D napplet manifest kinds.\n *\n * @param kind - A Nostr event kind\n * @returns `true` for `5129` / `15129` / `35129`\n */\nexport function isNappletManifestKind(kind: number): boolean {\n return NAPPLET_KINDS.includes(kind);\n}\n\n/**\n * A parsed NIP-5D napplet manifest. All fields are derived from the manifest\n * event's tags; the aggregate is the declared `x` tag (verify it separately\n * with {@link resolveNapplet} or `@kehto/nip/5a` `verifyAggregate`).\n */\nexport interface NappletManifest {\n /** Manifest event kind (`5129` / `15129` / `35129`). */\n kind: number;\n /** Author hex public key. */\n pubkey: string;\n /** Named-napplet `d` identifier, or `''` for root/snapshot manifests. */\n dTag: string;\n /** File path entries from `path` tags. */\n paths: PathEntry[];\n /** Declared aggregate hash from the `[\"x\",\"<hex>\",\"aggregate\"]` tag. */\n aggregateHash: string;\n /** Blossom server URL hints from `server` tags. */\n servers: string[];\n /** Short NAP capability names from `requires` tags. */\n requires: string[];\n /**\n * Archetype slugs this napplet fulfills, from `archetype` manifest tags; the\n * optional `nap` is the recommended default wire protocol (the 3rd tag element).\n */\n archetypes: Array<{ slug: string; nap?: string }>;\n /** Optional human title. */\n title?: string;\n /** Optional human description. */\n description?: string;\n /** Optional upstream source URL from the `source` tag. */\n source?: string;\n}\n\n/** Error codes for every napplet resolution failure path. */\nexport type NappletResolutionErrorCode =\n | 'invalid-signature'\n | 'invalid-manifest'\n | 'aggregate-mismatch'\n | 'blob-hash-mismatch'\n | 'blob-unavailable'\n | 'missing-index';\n\n/**\n * Thrown on any napplet resolution/verification failure. The `code` field\n * identifies which guard rejected, so callers can fail closed without parsing\n * the message.\n */\nexport class NappletResolutionError extends Error {\n readonly code: NappletResolutionErrorCode;\n constructor(code: NappletResolutionErrorCode, message: string) {\n super(message);\n this.name = 'NappletResolutionError';\n this.code = code;\n }\n}\n\nfunction firstTagValue(tags: readonly (readonly string[])[], name: string): string | undefined {\n for (const tag of tags) {\n if (tag[0] === name && typeof tag[1] === 'string' && tag[1].length > 0) return tag[1];\n }\n return undefined;\n}\n\nfunction allTagValues(tags: readonly (readonly string[])[], name: string): string[] {\n const out: string[] = [];\n for (const tag of tags) {\n if (tag[0] === name && typeof tag[1] === 'string' && tag[1].length > 0) out.push(tag[1]);\n }\n return out;\n}\n\nfunction archetypesFromTags(\n tags: readonly (readonly string[])[],\n): Array<{ slug: string; nap?: string }> {\n const out: Array<{ slug: string; nap?: string }> = [];\n for (const tag of tags) {\n if (tag[0] !== 'archetype') continue;\n if (typeof tag[1] !== 'string' || tag[1].length === 0) continue;\n out.push(\n typeof tag[2] === 'string' && tag[2].length > 0\n ? { slug: tag[1], nap: tag[2] }\n : { slug: tag[1] },\n );\n }\n return out;\n}\n\n/**\n * Parse a NIP-5D manifest event into a {@link NappletManifest}.\n *\n * Does not verify the signature, aggregate, or blobs — use {@link resolveNapplet}\n * for full verification.\n *\n * @param event - A NIP-5D manifest event (`5129` / `15129` / `35129`)\n * @returns The parsed manifest\n * @throws {@link NappletResolutionError} `invalid-manifest` for a non-napplet\n * kind, missing `path` tags, or a missing aggregate `x` tag\n */\nexport function parseNappletManifest(event: NostrEvent): NappletManifest {\n if (!isNappletManifestKind(event.kind)) {\n throw new NappletResolutionError('invalid-manifest', `not a NIP-5D napplet kind: ${event.kind}`);\n }\n const paths = pathEntriesFromTags(event.tags);\n if (paths.length === 0) {\n throw new NappletResolutionError('invalid-manifest', 'manifest has no path tags');\n }\n const aggregateHash = aggregateTagValue(event.tags);\n if (!aggregateHash) {\n throw new NappletResolutionError('invalid-manifest', 'manifest has no aggregate x tag');\n }\n return {\n kind: event.kind,\n pubkey: event.pubkey,\n dTag: firstTagValue(event.tags, 'd') ?? '',\n paths,\n aggregateHash,\n servers: allTagValues(event.tags, 'server'),\n requires: allTagValues(event.tags, 'requires'),\n archetypes: archetypesFromTags(event.tags),\n title: firstTagValue(event.tags, 'title'),\n description: firstTagValue(event.tags, 'description'),\n source: firstTagValue(event.tags, 'source'),\n };\n}\n\n/**\n * Verify a manifest event's Nostr signature (id + schnorr sig).\n *\n * @param event - A manifest event\n * @returns `true` if the event is internally consistent and validly signed\n */\nexport function verifyManifestSignature(event: NostrEvent): boolean {\n try {\n return verifyEvent(event);\n } catch {\n return false;\n }\n}\n\n/**\n * Whether `bytes` hash to the expected lowercase-hex SHA-256.\n *\n * @param bytes - The blob bytes\n * @param sha256Hex - Expected lowercase-hex SHA-256\n * @returns `true` only on an exact match\n */\nexport function verifyBlobHash(bytes: Uint8Array, sha256Hex: string): boolean {\n return bytesToHex(sha256(bytes)) === sha256Hex;\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.endsWith('/') ? url.slice(0, -1) : url;\n}\n\n/**\n * Fetch a blob by SHA-256 from a list of Blossom servers, returning the first\n * server's bytes whose hash matches. The returned bytes are always re-verified\n * against `sha256Hex` — servers (and gateways) are never trusted.\n *\n * @param servers - Candidate Blossom server base URLs, tried in order\n * @param sha256Hex - The blob's lowercase-hex SHA-256\n * @param fetchBytes - Fetches raw bytes for a URL (`<server>/<sha256>`)\n * @returns The verified blob bytes\n * @throws {@link NappletResolutionError} `blob-unavailable` if no server serves\n * a hash-matching blob\n */\nexport async function fetchBlob(\n servers: readonly string[],\n sha256Hex: string,\n fetchBytes: (url: string) => Promise<Uint8Array>,\n): Promise<Uint8Array> {\n for (const server of servers) {\n const url = `${stripTrailingSlash(server)}/${sha256Hex}`;\n try {\n const bytes = await fetchBytes(url);\n if (verifyBlobHash(bytes, sha256Hex)) return bytes;\n } catch {\n // try the next server\n }\n }\n throw new NappletResolutionError('blob-unavailable', `no server served blob ${sha256Hex}`);\n}\n\n/** A fully verified napplet, ready to inject via `iframe.srcdoc`. */\nexport interface ResolvedNapplet {\n /** Computed `d` identifier (`''` for root/snapshot). */\n dTag: string;\n /** Computed (and verified) aggregate hash — the content address. */\n aggregateHash: string;\n /** Verified file bytes keyed by manifest path. */\n files: Map<string, Uint8Array>;\n /** The verified `/index.html` decoded to text. */\n indexHtml: string;\n /** The parsed manifest. */\n manifest: NappletManifest;\n}\n\n/** Options for {@link resolveNapplet}. */\nexport interface ResolveNappletOptions {\n /** The candidate manifest event (resolved from relays by the caller). */\n event: NostrEvent;\n /**\n * Fetch raw blob bytes by SHA-256. Backed by Blossom (or a gateway) by the\n * caller. The bytes are re-verified against the hash here, so the fetcher is\n * untrusted.\n */\n fetchBlob: (sha256Hex: string, servers: readonly string[]) => Promise<Uint8Array>;\n /** Decode blob bytes to text for `indexHtml` (default UTF-8). */\n textDecode?: (bytes: Uint8Array) => string;\n /**\n * Optional verified artifact cache. Cache hits are still re-verified against\n * the manifest hash before use; cache writes happen only after the signature,\n * aggregate, and every blob hash have been verified.\n */\n cache?: NappletArtifactCache;\n}\n\nconst INDEX_PATHS = ['/index.html', 'index.html', '/'];\n\nfunction defaultDecode(bytes: Uint8Array): string {\n return new TextDecoder().decode(bytes);\n}\n\n/**\n * Resolve a napplet end-to-end from a candidate manifest event: verify the\n * signature, parse the manifest, verify the NIP-5A aggregate, fetch and verify\n * every blob, then assemble the verified `/index.html`.\n *\n * The returned `(dTag, aggregateHash)` is computed from the verified bytes and\n * is the napplet's identity. Any failure throws a {@link NappletResolutionError}\n * — the caller must fail closed (never render unverified bytes).\n *\n * @param options - {@link ResolveNappletOptions}\n * @returns The {@link ResolvedNapplet}\n * @throws {@link NappletResolutionError} on signature, manifest, aggregate,\n * blob-hash, blob-availability, or missing-index failures\n *\n * @example\n * ```ts\n * import { resolveNapplet } from '@kehto/nip/5d';\n * const napplet = await resolveNapplet({ event, fetchBlob });\n * iframe.srcdoc = napplet.indexHtml; // sandbox=\"allow-scripts\", opaque origin\n * ```\n */\nexport async function resolveNapplet(options: ResolveNappletOptions): Promise<ResolvedNapplet> {\n const { event, fetchBlob: fetchBlobBytes, textDecode = defaultDecode, cache } = options;\n\n if (!verifyManifestSignature(event)) {\n throw new NappletResolutionError('invalid-signature', 'manifest signature is invalid');\n }\n\n const manifest = parseNappletManifest(event);\n\n const recomputed = computeAggregateHash(manifest.paths);\n if (recomputed !== manifest.aggregateHash) {\n throw new NappletResolutionError(\n 'aggregate-mismatch',\n `recomputed aggregate ${recomputed} != manifest ${manifest.aggregateHash}`,\n );\n }\n\n const files = new Map<string, Uint8Array>();\n for (const entry of manifest.paths) {\n let bytes = await cache?.readBlob(entry.sha256);\n if (bytes && !verifyBlobHash(bytes, entry.sha256)) {\n await cache?.deleteBlob(entry.sha256);\n bytes = undefined;\n }\n bytes ??= await fetchBlobBytes(entry.sha256, manifest.servers);\n if (!verifyBlobHash(bytes, entry.sha256)) {\n throw new NappletResolutionError(\n 'blob-hash-mismatch',\n `blob for ${entry.path} does not match hash ${entry.sha256}`,\n );\n }\n files.set(entry.path, bytes);\n }\n\n const indexEntry = manifest.paths.find((e) => INDEX_PATHS.includes(e.path));\n if (!indexEntry) {\n throw new NappletResolutionError('missing-index', 'manifest has no /index.html entry');\n }\n\n const indexHtml = textDecode(files.get(indexEntry.path)!);\n await cache?.writeVerifiedResolution({ event, manifest, files, indexHtml });\n\n return {\n dTag: manifest.dTag,\n aggregateHash: manifest.aggregateHash,\n files,\n indexHtml,\n manifest,\n };\n}\n","import type { NostrEvent } from 'nostr-tools';\nimport type { NappletManifest } from './index.js';\n\nconst MIB = 1024 * 1024;\n\n/** Versioned Cache Storage namespace for verified napplet artifacts. */\nexport const NAPPLET_ARTIFACT_CACHE_NAME = 'kehto:napplet-artifacts:v1';\nconst SCHEMA_VERSION = 1;\nconst DEFAULT_BASE_URL = 'https://kehto.invalid';\nconst DEFAULT_UNKNOWN_BUDGET_BYTES = 32 * MIB;\nconst DEFAULT_SOFT_CEILING_BYTES = 128 * MIB;\nconst DEFAULT_HARD_CEILING_BYTES = 256 * MIB;\nconst DEFAULT_FOREGROUND_TTL_MS = 15 * 60 * 1000;\nconst DEFAULT_BACKGROUND_TTL_MS = 60 * 60 * 1000;\n\nexport interface NappletCacheDiagnostic {\n type:\n | 'cache-open-failed'\n | 'cache-read-failed'\n | 'cache-write-failed'\n | 'cache-delete-failed'\n | 'cache-prune-failed'\n | 'storage-estimate-unavailable';\n message: string;\n cause?: unknown;\n}\n\nexport interface NappletBlobIndexEntry {\n size: number;\n lastAccessed: number;\n refCount: number;\n}\n\nexport interface NappletAggregateIndexEntry {\n dTag: string;\n aggregateHash: string;\n key: string;\n blobHashes: string[];\n size: number;\n lastAccessed: number;\n coordinateKey?: string;\n pinned?: boolean;\n}\n\nexport interface CachedCoordinate {\n key: string;\n kind: number;\n pubkey: string;\n dTag: string;\n aggregateHash: string;\n lastResolvedAt: number;\n foregroundTtlMs: number;\n backgroundTtlMs: number;\n}\n\nexport interface NappletArtifactCacheIndex {\n schemaVersion: number;\n updatedAt: number;\n blobs: Record<string, NappletBlobIndexEntry>;\n aggregates: Record<string, NappletAggregateIndexEntry>;\n coordinates: Record<string, CachedCoordinate>;\n}\n\ninterface AggregateRecord {\n schemaVersion: number;\n dTag: string;\n aggregateHash: string;\n manifest: {\n kind: number;\n pubkey: string;\n dTag: string;\n paths: Array<{ path: string; sha256: string }>;\n requires: string[];\n title?: string;\n description?: string;\n };\n indexHtml: string;\n blobHashes: string[];\n writtenAt: number;\n}\n\nexport interface WriteVerifiedResolutionInput {\n event: NostrEvent;\n manifest: NappletManifest;\n files: ReadonlyMap<string, Uint8Array>;\n indexHtml: string;\n now?: number;\n}\n\nexport interface CoordinateFreshnessOptions {\n now?: number;\n background?: boolean;\n}\n\nexport interface NappletArtifactCache {\n readBlob(sha256Hex: string): Promise<Uint8Array | undefined>;\n deleteBlob(sha256Hex: string): Promise<void>;\n writeVerifiedResolution(input: WriteVerifiedResolutionInput): Promise<void>;\n writeCoordinate(input: {\n kind: number;\n pubkey: string;\n dTag: string;\n aggregateHash: string;\n lastResolvedAt?: number;\n foregroundTtlMs?: number;\n backgroundTtlMs?: number;\n }): Promise<void>;\n getCoordinate(input: {\n kind: number;\n pubkey: string;\n dTag: string;\n }, options?: CoordinateFreshnessOptions): Promise<(CachedCoordinate & { fresh: boolean }) | undefined>;\n touchAggregate(dTag: string, aggregateHash: string, now?: number): Promise<void>;\n markAggregateActive(dTag: string, aggregateHash: string): void;\n releaseAggregateActive(dTag: string, aggregateHash: string): void;\n prune(now?: number): Promise<void>;\n}\n\nexport interface CacheStorageNappletArtifactCacheOptions {\n cacheStorage?: CacheStorage;\n storage?: StorageManager;\n cacheName?: string;\n baseUrl?: string;\n now?: () => number;\n onDiagnostic?: (diagnostic: NappletCacheDiagnostic) => void;\n unknownBudgetBytes?: number;\n softCeilingBytes?: number;\n hardCeilingBytes?: number;\n}\n\nexport interface OpenNappletArtifactCacheOptions extends CacheStorageNappletArtifactCacheOptions {\n requireStorageEstimate?: boolean;\n}\n\nfunction emptyIndex(now: number): NappletArtifactCacheIndex {\n return {\n schemaVersion: SCHEMA_VERSION,\n updatedAt: now,\n blobs: {},\n aggregates: {},\n coordinates: {},\n };\n}\n\nfunction aggregateKey(dTag: string, aggregateHash: string): string {\n return `${encodeURIComponent(dTag)}:${aggregateHash}`;\n}\n\nexport function coordinateKey(kind: number, pubkey: string, dTag: string): string {\n return `${kind}:${pubkey}:${encodeURIComponent(dTag)}`;\n}\n\nexport function isCoordinateFresh(\n coordinate: CachedCoordinate,\n options: CoordinateFreshnessOptions = {},\n): boolean {\n const now = options.now ?? Date.now();\n const ttl = options.background ? coordinate.backgroundTtlMs : coordinate.foregroundTtlMs;\n return ttl === Number.POSITIVE_INFINITY || now - coordinate.lastResolvedAt <= ttl;\n}\n\nfunction byteLength(bytes: Uint8Array): number {\n return bytes.byteLength;\n}\n\nfunction encodeKeyPart(value: string): string {\n return encodeURIComponent(value).replaceAll('%2F', '%252F');\n}\n\nfunction cloneBytes(bytes: Uint8Array): Uint8Array {\n return new Uint8Array(bytes);\n}\n\nfunction responseBody(bytes: Uint8Array): ArrayBuffer {\n return cloneBytes(bytes).buffer as ArrayBuffer;\n}\n\nfunction normalizeBaseUrl(baseUrl: string): string {\n return baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;\n}\n\nfunction resolveBaseUrl(baseUrl?: string): string {\n if (baseUrl) return normalizeBaseUrl(baseUrl);\n const maybeLocation = globalThis as typeof globalThis & { location?: { origin?: string } };\n if (maybeLocation.location?.origin) return normalizeBaseUrl(maybeLocation.location.origin);\n return normalizeBaseUrl(DEFAULT_BASE_URL);\n}\n\nasync function responseBytes(response: Response): Promise<Uint8Array> {\n return new Uint8Array(await response.arrayBuffer());\n}\n\nasync function responseJson<T>(response: Response): Promise<T> {\n return response.json() as Promise<T>;\n}\n\nfunction refCounts(index: NappletArtifactCacheIndex): Record<string, number> {\n const counts: Record<string, number> = {};\n for (const aggregate of Object.values(index.aggregates)) {\n for (const hash of aggregate.blobHashes) counts[hash] = (counts[hash] ?? 0) + 1;\n }\n return counts;\n}\n\nfunction cachedSize(index: NappletArtifactCacheIndex): number {\n const blobBytes = Object.values(index.blobs).reduce((sum, entry) => sum + entry.size, 0);\n const aggregateBytes = Object.values(index.aggregates).reduce((sum, entry) => sum + entry.size, 0);\n return blobBytes + aggregateBytes;\n}\n\nexport class CacheStorageNappletArtifactCache implements NappletArtifactCache {\n private readonly cacheStorage: CacheStorage;\n private readonly storage?: StorageManager;\n private readonly cacheName: string;\n private readonly baseUrl: string;\n private readonly now: () => number;\n private readonly activeAggregates = new Set<string>();\n private readonly onDiagnostic?: (diagnostic: NappletCacheDiagnostic) => void;\n private readonly unknownBudgetBytes: number;\n private readonly softCeilingBytes: number;\n private readonly hardCeilingBytes: number;\n private cachePromise?: Promise<Cache>;\n\n constructor(options: CacheStorageNappletArtifactCacheOptions = {}) {\n const maybeCacheStorage = options.cacheStorage ?? (globalThis as typeof globalThis & { caches?: CacheStorage }).caches;\n if (!maybeCacheStorage) {\n throw new Error('Cache Storage is unavailable');\n }\n this.cacheStorage = maybeCacheStorage;\n this.storage = options.storage ?? (globalThis.navigator as Navigator | undefined)?.storage;\n this.cacheName = options.cacheName ?? NAPPLET_ARTIFACT_CACHE_NAME;\n this.baseUrl = resolveBaseUrl(options.baseUrl);\n this.now = options.now ?? Date.now;\n this.onDiagnostic = options.onDiagnostic;\n this.unknownBudgetBytes = options.unknownBudgetBytes ?? DEFAULT_UNKNOWN_BUDGET_BYTES;\n this.softCeilingBytes = options.softCeilingBytes ?? DEFAULT_SOFT_CEILING_BYTES;\n this.hardCeilingBytes = options.hardCeilingBytes ?? DEFAULT_HARD_CEILING_BYTES;\n }\n\n async readBlob(sha256Hex: string): Promise<Uint8Array | undefined> {\n try {\n const cache = await this.open();\n const response = await cache.match(this.blobUrl(sha256Hex));\n if (!response) return undefined;\n const bytes = await responseBytes(response);\n const index = await this.readIndex(cache);\n const entry = index.blobs[sha256Hex];\n if (entry) {\n entry.lastAccessed = this.now();\n index.updatedAt = entry.lastAccessed;\n await this.writeIndex(cache, index);\n }\n return bytes;\n } catch (cause) {\n this.diagnostic({ type: 'cache-read-failed', message: `failed to read blob ${sha256Hex}`, cause });\n return undefined;\n }\n }\n\n async deleteBlob(sha256Hex: string): Promise<void> {\n try {\n const cache = await this.open();\n await cache.delete(this.blobUrl(sha256Hex));\n const index = await this.readIndex(cache);\n delete index.blobs[sha256Hex];\n index.updatedAt = this.now();\n await this.writeIndex(cache, index);\n } catch (cause) {\n this.diagnostic({ type: 'cache-delete-failed', message: `failed to delete blob ${sha256Hex}`, cause });\n }\n }\n\n async writeVerifiedResolution(input: WriteVerifiedResolutionInput): Promise<void> {\n try {\n await this.writeVerifiedResolutionOnce(input);\n } catch (firstCause) {\n try {\n await this.prune(input.now ?? this.now());\n await this.writeVerifiedResolutionOnce(input);\n } catch (cause) {\n this.diagnostic({\n type: 'cache-write-failed',\n message: 'failed to write verified napplet artifacts after prune retry',\n cause: cause ?? firstCause,\n });\n }\n }\n }\n\n async writeCoordinate(input: {\n kind: number;\n pubkey: string;\n dTag: string;\n aggregateHash: string;\n lastResolvedAt?: number;\n foregroundTtlMs?: number;\n backgroundTtlMs?: number;\n }): Promise<void> {\n const cache = await this.open();\n const now = this.now();\n const index = await this.readIndex(cache);\n const key = coordinateKey(input.kind, input.pubkey, input.dTag);\n index.coordinates[key] = {\n key,\n kind: input.kind,\n pubkey: input.pubkey,\n dTag: input.dTag,\n aggregateHash: input.aggregateHash,\n lastResolvedAt: input.lastResolvedAt ?? now,\n foregroundTtlMs: input.foregroundTtlMs ?? DEFAULT_FOREGROUND_TTL_MS,\n backgroundTtlMs: input.backgroundTtlMs ?? DEFAULT_BACKGROUND_TTL_MS,\n };\n index.updatedAt = now;\n await this.writeIndex(cache, index);\n }\n\n async getCoordinate(\n input: { kind: number; pubkey: string; dTag: string },\n options: CoordinateFreshnessOptions = {},\n ): Promise<(CachedCoordinate & { fresh: boolean }) | undefined> {\n const cache = await this.open();\n const index = await this.readIndex(cache);\n const coordinate = index.coordinates[coordinateKey(input.kind, input.pubkey, input.dTag)];\n if (!coordinate) return undefined;\n return { ...coordinate, fresh: isCoordinateFresh(coordinate, options) };\n }\n\n async touchAggregate(dTag: string, aggregateHash: string, now = this.now()): Promise<void> {\n const cache = await this.open();\n const index = await this.readIndex(cache);\n const key = aggregateKey(dTag, aggregateHash);\n const aggregate = index.aggregates[key];\n if (!aggregate) return;\n aggregate.lastAccessed = now;\n for (const hash of aggregate.blobHashes) {\n if (index.blobs[hash]) index.blobs[hash].lastAccessed = now;\n }\n index.updatedAt = now;\n await this.writeIndex(cache, index);\n }\n\n markAggregateActive(dTag: string, aggregateHash: string): void {\n this.activeAggregates.add(aggregateKey(dTag, aggregateHash));\n }\n\n releaseAggregateActive(dTag: string, aggregateHash: string): void {\n this.activeAggregates.delete(aggregateKey(dTag, aggregateHash));\n }\n\n async prune(now = this.now()): Promise<void> {\n try {\n const cache = await this.open();\n const index = await this.readIndex(cache);\n let size = cachedSize(index);\n const budget = await this.budgetBytes();\n const hardBudget = await this.hardBudgetBytes();\n const shouldPrune = size > budget || await this.originPressureHigh();\n if (!shouldPrune) return;\n\n this.rewriteRefCounts(index);\n\n for (const [hash, blob] of Object.entries(index.blobs)) {\n if (size <= budget) break;\n if (blob.refCount > 0) continue;\n await cache.delete(this.blobUrl(hash));\n delete index.blobs[hash];\n size -= blob.size;\n }\n\n const currentAggregateHashes = new Set(\n Object.values(index.coordinates).map((coordinate) => coordinate.aggregateHash),\n );\n const aggregates = Object.values(index.aggregates)\n .filter((entry) => !this.activeAggregates.has(entry.key))\n .sort((a, b) => {\n const currentA = currentAggregateHashes.has(a.aggregateHash) ? 1 : 0;\n const currentB = currentAggregateHashes.has(b.aggregateHash) ? 1 : 0;\n if (currentA !== currentB) return currentA - currentB;\n return a.lastAccessed - b.lastAccessed;\n });\n\n for (const aggregate of aggregates) {\n if (size <= budget) break;\n await cache.delete(this.aggregateUrl(aggregate.key));\n delete index.aggregates[aggregate.key];\n size -= aggregate.size;\n this.rewriteRefCounts(index);\n }\n\n if (size > hardBudget) {\n const active = Object.values(index.aggregates)\n .filter((entry) => this.activeAggregates.has(entry.key))\n .sort((a, b) => a.lastAccessed - b.lastAccessed);\n for (const aggregate of active) {\n if (size <= hardBudget) break;\n await cache.delete(this.aggregateUrl(aggregate.key));\n delete index.aggregates[aggregate.key];\n size -= aggregate.size;\n }\n }\n\n this.rewriteRefCounts(index);\n for (const [hash, blob] of Object.entries(index.blobs)) {\n if (blob.refCount === 0) {\n await cache.delete(this.blobUrl(hash));\n delete index.blobs[hash];\n }\n }\n index.updatedAt = now;\n await this.writeIndex(cache, index);\n } catch (cause) {\n this.diagnostic({ type: 'cache-prune-failed', message: 'failed to prune napplet artifact cache', cause });\n }\n }\n\n async snapshotIndex(): Promise<NappletArtifactCacheIndex> {\n return this.readIndex(await this.open());\n }\n\n private async writeVerifiedResolutionOnce(input: WriteVerifiedResolutionInput): Promise<void> {\n const cache = await this.open();\n const now = input.now ?? this.now();\n const index = await this.readIndex(cache);\n const blobHashes: string[] = [];\n\n for (const pathEntry of input.manifest.paths) {\n const bytes = input.files.get(pathEntry.path);\n if (!bytes) continue;\n blobHashes.push(pathEntry.sha256);\n await cache.put(this.blobUrl(pathEntry.sha256), new Response(responseBody(bytes)));\n index.blobs[pathEntry.sha256] = {\n size: byteLength(bytes),\n lastAccessed: now,\n refCount: index.blobs[pathEntry.sha256]?.refCount ?? 0,\n };\n }\n\n const key = aggregateKey(input.manifest.dTag, input.manifest.aggregateHash);\n const record: AggregateRecord = {\n schemaVersion: SCHEMA_VERSION,\n dTag: input.manifest.dTag,\n aggregateHash: input.manifest.aggregateHash,\n manifest: {\n kind: input.manifest.kind,\n pubkey: input.manifest.pubkey,\n dTag: input.manifest.dTag,\n paths: input.manifest.paths.map((entry) => ({ path: entry.path, sha256: entry.sha256 })),\n requires: [...input.manifest.requires],\n ...(input.manifest.title === undefined ? {} : { title: input.manifest.title }),\n ...(input.manifest.description === undefined ? {} : { description: input.manifest.description }),\n },\n indexHtml: input.indexHtml,\n blobHashes,\n writtenAt: now,\n };\n const aggregateBytes = new TextEncoder().encode(JSON.stringify(record));\n await cache.put(this.aggregateUrl(key), new Response(responseBody(aggregateBytes), {\n headers: { 'content-type': 'application/json' },\n }));\n\n const coordKey = coordinateKey(input.manifest.kind, input.manifest.pubkey, input.manifest.dTag);\n index.aggregates[key] = {\n dTag: input.manifest.dTag,\n aggregateHash: input.manifest.aggregateHash,\n key,\n blobHashes,\n size: byteLength(aggregateBytes),\n lastAccessed: now,\n coordinateKey: coordKey,\n };\n index.coordinates[coordKey] = {\n key: coordKey,\n kind: input.manifest.kind,\n pubkey: input.manifest.pubkey,\n dTag: input.manifest.dTag,\n aggregateHash: input.manifest.aggregateHash,\n lastResolvedAt: now,\n foregroundTtlMs: DEFAULT_FOREGROUND_TTL_MS,\n backgroundTtlMs: DEFAULT_BACKGROUND_TTL_MS,\n };\n\n this.rewriteRefCounts(index);\n index.updatedAt = now;\n await this.writeIndex(cache, index);\n await this.prune(now);\n }\n\n private async open(): Promise<Cache> {\n this.cachePromise ??= this.cacheStorage.open(this.cacheName);\n return this.cachePromise;\n }\n\n private indexUrl(): string {\n return new URL('__kehto_napplet_cache/index.json', this.baseUrl).toString();\n }\n\n private blobUrl(hash: string): string {\n return new URL(`__kehto_napplet_cache/blob/${encodeKeyPart(hash)}`, this.baseUrl).toString();\n }\n\n private aggregateUrl(key: string): string {\n return new URL(`__kehto_napplet_cache/aggregate/${encodeKeyPart(key)}.json`, this.baseUrl).toString();\n }\n\n private async readIndex(cache: Cache): Promise<NappletArtifactCacheIndex> {\n const response = await cache.match(this.indexUrl());\n if (!response) return emptyIndex(this.now());\n try {\n const index = await responseJson<NappletArtifactCacheIndex>(response);\n if (index.schemaVersion !== SCHEMA_VERSION) return emptyIndex(this.now());\n return index;\n } catch {\n return emptyIndex(this.now());\n }\n }\n\n private async writeIndex(cache: Cache, index: NappletArtifactCacheIndex): Promise<void> {\n await cache.put(this.indexUrl(), new Response(JSON.stringify(index), {\n headers: { 'content-type': 'application/json' },\n }));\n }\n\n private rewriteRefCounts(index: NappletArtifactCacheIndex): void {\n const counts = refCounts(index);\n for (const [hash, blob] of Object.entries(index.blobs)) {\n blob.refCount = counts[hash] ?? 0;\n }\n }\n\n private async budgetBytes(): Promise<number> {\n const estimate = await this.estimateStorage();\n if (!estimate?.quota) return this.unknownBudgetBytes;\n return Math.min(this.softCeilingBytes, Math.floor(estimate.quota * 0.1));\n }\n\n private async hardBudgetBytes(): Promise<number> {\n const estimate = await this.estimateStorage();\n if (!estimate?.quota) return this.unknownBudgetBytes;\n return Math.min(this.hardCeilingBytes, Math.floor(estimate.quota * 0.2));\n }\n\n private async originPressureHigh(): Promise<boolean> {\n const estimate = await this.estimateStorage();\n if (!estimate?.quota || estimate.usage === undefined) return false;\n return estimate.usage / estimate.quota >= 0.8;\n }\n\n private async estimateStorage(): Promise<StorageEstimate | undefined> {\n if (!this.storage?.estimate) return undefined;\n try {\n return this.storage.estimate();\n } catch (cause) {\n this.diagnostic({\n type: 'storage-estimate-unavailable',\n message: 'storage estimate failed; using unknown quota cache budget',\n cause,\n });\n return undefined;\n }\n }\n\n private diagnostic(diagnostic: NappletCacheDiagnostic): void {\n this.onDiagnostic?.(diagnostic);\n }\n}\n\nexport async function openNappletArtifactCache(\n options: OpenNappletArtifactCacheOptions = {},\n): Promise<CacheStorageNappletArtifactCache | undefined> {\n const cacheStorage = options.cacheStorage ?? (globalThis as typeof globalThis & { caches?: CacheStorage }).caches;\n if (!cacheStorage) return undefined;\n const storage = options.storage ?? (globalThis.navigator as Navigator | undefined)?.storage;\n if (options.requireStorageEstimate && !storage?.estimate) {\n options.onDiagnostic?.({\n type: 'storage-estimate-unavailable',\n message: 'storage estimates are unavailable; using network-only loading',\n });\n return undefined;\n }\n try {\n const cache = new CacheStorageNappletArtifactCache({ ...options, cacheStorage, storage });\n await cache.snapshotIndex();\n return cache;\n } catch (cause) {\n options.onDiagnostic?.({\n type: 'cache-open-failed',\n message: 'Cache Storage napplet artifact cache could not be opened',\n cause,\n });\n return undefined;\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,4 +1,4 @@
1
- // src/66.ts
1
+ // src/66/index.ts
2
2
  var DEFAULT_RELAY_ATTRIBUTE_GROUPS = {
3
3
  Indexer: [
4
4
  { tag: "T", values: ["Indexer", "indexer"] },
@@ -138,4 +138,4 @@ export {
138
138
  DEFAULT_RELAY_ATTRIBUTE_GROUPS,
139
139
  createNip66Aggregator
140
140
  };
141
- //# sourceMappingURL=chunk-N4WPLKIJ.js.map
141
+ //# sourceMappingURL=chunk-DADZ6NUI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/66/index.ts"],"sourcesContent":["import type { NostrEvent } from 'nostr-tools';\n\n/**\n * Minimal relay-pool contract `@kehto/nip/66` depends on. Consumers implement\n * this against their pool library of choice (nostr-tools SimplePool,\n * applesauce-relay, @snort/worker-relay, etc.). Intentionally smaller than\n * RxJS-style observable pools — one callback, one unsubscribe handle.\n *\n * @example\n * ```ts\n * import { SimplePool } from 'nostr-tools/pool';\n * const pool = new SimplePool();\n * const adapter: Nip66RelayPool = {\n * subscribe: (relays, filter, onEvent) => {\n * const sub = pool.subscribeMany(relays, [filter], { onevent: onEvent });\n * return () => sub.close();\n * },\n * };\n * ```\n */\nexport interface Nip66RelayPool {\n /**\n * Open a kind-30166 subscription against a set of monitor relays.\n *\n * @param relays - WebSocket URLs to subscribe against (the aggregator passes its bootstrap list)\n * @param filter - A kind-30166 filter, optionally narrowed with `#n` network tags\n * @param onEvent - Called synchronously with every matching event; the aggregator parses each one\n * @returns An unsubscribe handle the aggregator calls on `resync()` or teardown\n */\n subscribe(\n relays: ReadonlyArray<string>,\n filter: Nip66Filter,\n onEvent: (event: NostrEvent) => void,\n ): () => void;\n}\n\n/**\n * Minimal filter shape for kind-30166 subscriptions. `#n` optionally narrows\n * by network type (e.g. `['clearnet']`, `['clearnet', 'tor']`).\n *\n * @example\n * ```ts\n * const filter: Nip66Filter = { kinds: [30166], '#n': ['clearnet'] };\n * ```\n */\nexport type Nip66Filter = { kinds: [30166]; '#n'?: ReadonlyArray<string> };\n\n/**\n * Options for {@link createNip66Aggregator}.\n *\n * @example\n * ```ts\n * const options: Nip66AggregatorOptions = {\n * pool: myPoolAdapter,\n * bootstrap: ['wss://monitor1.example.com', 'wss://monitor2.example.com'],\n * networks: ['clearnet'],\n * };\n * ```\n */\nexport interface Nip66AggregatorOptions {\n /** A {@link Nip66RelayPool} adapter supplied by the consumer. */\n pool: Nip66RelayPool;\n /** Required non-empty list of monitor relay URLs to subscribe against. No default ships in the package. */\n bootstrap: ReadonlyArray<string>;\n /** Optional network-type narrowing passed to the filter as `#n` (e.g. `['clearnet', 'tor']`). Omit to receive every network. */\n networks?: ReadonlyArray<string>;\n /**\n * Optional named relay-attribute groups for interpreting ad hoc kind-30166 tags.\n * Omit to use {@link DEFAULT_RELAY_ATTRIBUTE_GROUPS}.\n */\n relayAttributeGroups?: RelayAttributeGroups;\n}\n\n/** A tag/value matcher for ad hoc relay attributes carried on kind-30166 events. */\nexport interface RelayAttributeMatcher {\n /** Tag name to match, for example `T`, `t`, or `k`. */\n tag: string;\n /** Optional accepted values. If omitted, any value for the tag matches. */\n values?: ReadonlyArray<string>;\n}\n\n/** Named groups of relay-attribute matchers. Matchers within a group are OR-ed. */\nexport type RelayAttributeGroups = Readonly<Record<string, ReadonlyArray<RelayAttributeMatcher>>>;\n\n/** Parsed attributes observed for one NIP-66 relay URL. */\nexport interface Nip66RelayAttributes {\n /** Raw tag copies from the latest observed kind-30166 event for the relay. */\n tags: ReadonlyArray<ReadonlyArray<string>>;\n /** Tag-value index, keyed by tag name. */\n values: ReadonlyMap<string, ReadonlySet<string>>;\n /** Parsed uppercase `N` tag support for convenience. */\n supportedNips: ReadonlySet<number>;\n}\n\nexport const DEFAULT_RELAY_ATTRIBUTE_GROUPS: RelayAttributeGroups = {\n Indexer: [\n { tag: 'T', values: ['Indexer', 'indexer'] },\n { tag: 't', values: ['Indexer', 'indexer'] },\n { tag: 'k', values: ['10002'] },\n ],\n RelayIndexer: [\n { tag: 'T', values: ['RelayIndexer', 'relay-indexer', 'relay_indexer'] },\n { tag: 't', values: ['RelayIndexer', 'relay-indexer', 'relay_indexer'] },\n { tag: 'k', values: ['30166', '10166'] },\n ],\n};\n\n/**\n * Aggregator handle returned by {@link createNip66Aggregator}. Each call\n * returns a fresh instance with its own closure-scoped state — multi-instance\n * safe (unlike a module-globals pattern).\n *\n * @example\n * ```ts\n * const aggregator = createNip66Aggregator({ pool, bootstrap: ['wss://monitor.example.com'] });\n * aggregator.start();\n * const relays = aggregator.getRelaySet();\n * ```\n */\nexport interface Nip66Aggregator {\n /** Begin subscribing against the bootstrap relay set. Idempotent — calling twice is a no-op. */\n start(): void;\n /** Tear down the current subscription, clear accumulated state, and re-subscribe. Call when `networks` semantics change upstream. */\n resync(): void;\n /**\n * Tear down the current subscription. Idempotent — calling without a prior\n * `start()` is a safe no-op. Preserves accumulated state (`getRelaySet`,\n * `relaySupportsNip`) — use `resync()` if you need to clear state AND\n * re-subscribe in one step. A subsequent `start()` re-subscribes against\n * the bootstrap relays (identical contract to the first start).\n *\n * Wire this to your consumer's teardown path (browser: `beforeunload`;\n * React: effect cleanup; Electron: `app.before-quit`) — the aggregator\n * holds a pool subscription that otherwise lives for the tab lifetime\n * (PITFALLS.md M-03).\n */\n stop(): void;\n /** The current set of relay URLs discovered from kind-30166 `d`-tags. Empty until `start()` fires at least one event. */\n getRelaySet(): ReadonlySet<string>;\n /**\n * Relay URLs whose `N`-tags declare support for the given NIP number.\n *\n * @param nip - NIP number to query support for (e.g. 44 for NIP-44 encrypted DMs)\n * @returns Array of relay URLs supporting that NIP\n */\n getRelaysSupportingNip(nip: number): string[];\n /**\n * Whether the relay at `url` has been seen with an `N`-tag declaring support for `nip`.\n *\n * @param url - Relay WebSocket URL to check\n * @param nip - NIP number to query support for\n * @returns `true` iff the relay has been observed with a matching `N`-tag\n */\n relaySupportsNip(url: string, nip: number): boolean;\n /**\n * Return parsed relay attributes for a relay URL, if that relay has been\n * observed in a kind-30166 event.\n */\n getRelayAttributes(url: string): Nip66RelayAttributes | undefined;\n /**\n * Return relays matching any supplied ad hoc attribute matcher.\n *\n * @param matchers - Tag/value matchers, OR semantics\n */\n getRelaysMatchingAttributes(matchers: ReadonlyArray<RelayAttributeMatcher>): string[];\n /**\n * Return relays in a named attribute group. Uses the group table passed to\n * `createNip66Aggregator`, or {@link DEFAULT_RELAY_ATTRIBUTE_GROUPS}.\n */\n getRelaysForAttributeGroup(groupName: string, groups?: RelayAttributeGroups): string[];\n}\n\n/**\n * Create a NIP-66 kind-30166 relay-discovery aggregator with closure-scoped state.\n *\n * Subscribes to kind-30166 events via the injected {@link Nip66RelayPool},\n * extracts relay URLs from `d`-tags, and parses `N`-tags to track which NIP\n * numbers each relay supports. Multi-instance safe — each factory call owns\n * its own `Set` + `Map`; multiple aggregators share nothing at module scope.\n *\n * The aggregator is framework-agnostic: it knows nothing about negentropy\n * (NIP-77), OPFS caching, worker relays, or network-type config stores.\n * Consumers inject those concerns through their {@link Nip66RelayPool}\n * implementation. `start()` is synchronous and idempotent — consumers\n * schedule their own cadence (delayed start, retry, etc.).\n *\n * @param options - Pool adapter + bootstrap monitor relays (+ optional network filter)\n * @returns A fresh {@link Nip66Aggregator} handle\n *\n * @example\n * ```ts\n * import { createNip66Aggregator } from '@kehto/nip/66';\n * import { SimplePool } from 'nostr-tools/pool';\n *\n * const pool = new SimplePool();\n * const aggregator = createNip66Aggregator({\n * pool: {\n * subscribe: (relays, filter, onEvent) => {\n * const sub = pool.subscribeMany(relays, [filter], { onevent: onEvent });\n * return () => sub.close();\n * },\n * },\n * bootstrap: ['wss://monitor1.example.com'],\n * });\n * aggregator.start();\n * // Later — inside a ShellAdapter hook:\n * const suggestions = Array.from(aggregator.getRelaySet());\n * ```\n */\nexport function createNip66Aggregator(options: Nip66AggregatorOptions): Nip66Aggregator {\n const relaySet = new Set<string>();\n const relaySupportedNips = new Map<string, Set<number>>();\n const relayAttributeTags = new Map<string, string[][]>();\n const relayAttributeValues = new Map<string, Map<string, Set<string>>>();\n const relayAttributeGroups = options.relayAttributeGroups ?? DEFAULT_RELAY_ATTRIBUTE_GROUPS;\n let unsubscribe: (() => void) | null = null;\n\n function buildFilter(): Nip66Filter {\n const filter: Nip66Filter = { kinds: [30166] };\n if (options.networks && options.networks.length > 0) {\n filter['#n'] = options.networks;\n }\n return filter;\n }\n\n function processEvent(event: NostrEvent): void {\n const dTag = event.tags.find((t) => t[0] === 'd');\n const relayUrl = dTag?.[1];\n if (!relayUrl) return;\n relaySet.add(relayUrl);\n captureRelayAttributes(relayUrl, event.tags);\n\n const nips = new Set<number>();\n for (const tag of event.tags) {\n if (tag[0] === 'N' && tag[1]) {\n const nipNum = parseInt(tag[1], 10);\n if (!Number.isNaN(nipNum)) nips.add(nipNum);\n }\n }\n if (nips.size > 0) relaySupportedNips.set(relayUrl, nips);\n }\n\n function captureRelayAttributes(relayUrl: string, tags: string[][]): void {\n const tagCopies = tags.map((tag) => [...tag]);\n const values = new Map<string, Set<string>>();\n for (const tag of tags) {\n const [name, value] = tag;\n if (!name || value === undefined) continue;\n let bucket = values.get(name);\n if (!bucket) {\n bucket = new Set<string>();\n values.set(name, bucket);\n }\n bucket.add(value);\n }\n relayAttributeTags.set(relayUrl, tagCopies);\n relayAttributeValues.set(relayUrl, values);\n }\n\n function start(): void {\n if (unsubscribe) return; // idempotent — already subscribed\n unsubscribe = options.pool.subscribe(options.bootstrap, buildFilter(), processEvent);\n }\n\n function resync(): void {\n if (unsubscribe) {\n unsubscribe();\n unsubscribe = null;\n }\n relaySet.clear();\n relaySupportedNips.clear();\n relayAttributeTags.clear();\n relayAttributeValues.clear();\n unsubscribe = options.pool.subscribe(options.bootstrap, buildFilter(), processEvent);\n }\n\n function stop(): void {\n if (unsubscribe) {\n unsubscribe();\n unsubscribe = null;\n }\n }\n\n function getRelaySet(): ReadonlySet<string> {\n return relaySet;\n }\n\n function getRelaysSupportingNip(nip: number): string[] {\n const result: string[] = [];\n for (const [url, nips] of relaySupportedNips) {\n if (nips.has(nip)) result.push(url);\n }\n return result;\n }\n\n function relaySupportsNip(url: string, nip: number): boolean {\n return relaySupportedNips.get(url)?.has(nip) ?? false;\n }\n\n function getRelayAttributes(url: string): Nip66RelayAttributes | undefined {\n const tagCopies = relayAttributeTags.get(url);\n const values = relayAttributeValues.get(url);\n if (!tagCopies || !values) return undefined;\n return {\n tags: tagCopies.map((tag) => [...tag]),\n values: new Map(Array.from(values, ([tag, tagValues]) => [tag, new Set(tagValues)])),\n supportedNips: new Set(relaySupportedNips.get(url) ?? []),\n };\n }\n\n function matcherMatches(url: string, matcher: RelayAttributeMatcher): boolean {\n const values = relayAttributeValues.get(url)?.get(matcher.tag);\n if (!values || values.size === 0) return false;\n if (!matcher.values || matcher.values.length === 0) return true;\n const accepted = new Set(matcher.values.map((value) => value.toLowerCase()));\n for (const value of values) {\n if (accepted.has(value.toLowerCase())) return true;\n }\n return false;\n }\n\n function getRelaysMatchingAttributes(matchers: ReadonlyArray<RelayAttributeMatcher>): string[] {\n const result: string[] = [];\n for (const url of relaySet) {\n if (matchers.some((matcher) => matcherMatches(url, matcher))) result.push(url);\n }\n return result;\n }\n\n function getRelaysForAttributeGroup(groupName: string, groups?: RelayAttributeGroups): string[] {\n const group = (groups ?? relayAttributeGroups)[groupName] ?? [];\n return getRelaysMatchingAttributes(group);\n }\n\n return {\n start,\n resync,\n stop,\n getRelaySet,\n getRelaysSupportingNip,\n relaySupportsNip,\n getRelayAttributes,\n getRelaysMatchingAttributes,\n getRelaysForAttributeGroup,\n };\n}\n"],"mappings":";AA8FO,IAAM,iCAAuD;AAAA,EAClE,SAAS;AAAA,IACP,EAAE,KAAK,KAAK,QAAQ,CAAC,WAAW,SAAS,EAAE;AAAA,IAC3C,EAAE,KAAK,KAAK,QAAQ,CAAC,WAAW,SAAS,EAAE;AAAA,IAC3C,EAAE,KAAK,KAAK,QAAQ,CAAC,OAAO,EAAE;AAAA,EAChC;AAAA,EACA,cAAc;AAAA,IACZ,EAAE,KAAK,KAAK,QAAQ,CAAC,gBAAgB,iBAAiB,eAAe,EAAE;AAAA,IACvE,EAAE,KAAK,KAAK,QAAQ,CAAC,gBAAgB,iBAAiB,eAAe,EAAE;AAAA,IACvE,EAAE,KAAK,KAAK,QAAQ,CAAC,SAAS,OAAO,EAAE;AAAA,EACzC;AACF;AAwGO,SAAS,sBAAsB,SAAkD;AACtF,QAAM,WAAW,oBAAI,IAAY;AACjC,QAAM,qBAAqB,oBAAI,IAAyB;AACxD,QAAM,qBAAqB,oBAAI,IAAwB;AACvD,QAAM,uBAAuB,oBAAI,IAAsC;AACvE,QAAM,uBAAuB,QAAQ,wBAAwB;AAC7D,MAAI,cAAmC;AAEvC,WAAS,cAA2B;AAClC,UAAM,SAAsB,EAAE,OAAO,CAAC,KAAK,EAAE;AAC7C,QAAI,QAAQ,YAAY,QAAQ,SAAS,SAAS,GAAG;AACnD,aAAO,IAAI,IAAI,QAAQ;AAAA,IACzB;AACA,WAAO;AAAA,EACT;AAEA,WAAS,aAAa,OAAyB;AAC7C,UAAM,OAAO,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC,MAAM,GAAG;AAChD,UAAM,WAAW,OAAO,CAAC;AACzB,QAAI,CAAC,SAAU;AACf,aAAS,IAAI,QAAQ;AACrB,2BAAuB,UAAU,MAAM,IAAI;AAE3C,UAAM,OAAO,oBAAI,IAAY;AAC7B,eAAW,OAAO,MAAM,MAAM;AAC5B,UAAI,IAAI,CAAC,MAAM,OAAO,IAAI,CAAC,GAAG;AAC5B,cAAM,SAAS,SAAS,IAAI,CAAC,GAAG,EAAE;AAClC,YAAI,CAAC,OAAO,MAAM,MAAM,EAAG,MAAK,IAAI,MAAM;AAAA,MAC5C;AAAA,IACF;AACA,QAAI,KAAK,OAAO,EAAG,oBAAmB,IAAI,UAAU,IAAI;AAAA,EAC1D;AAEA,WAAS,uBAAuB,UAAkB,MAAwB;AACxE,UAAM,YAAY,KAAK,IAAI,CAAC,QAAQ,CAAC,GAAG,GAAG,CAAC;AAC5C,UAAM,SAAS,oBAAI,IAAyB;AAC5C,eAAW,OAAO,MAAM;AACtB,YAAM,CAAC,MAAM,KAAK,IAAI;AACtB,UAAI,CAAC,QAAQ,UAAU,OAAW;AAClC,UAAI,SAAS,OAAO,IAAI,IAAI;AAC5B,UAAI,CAAC,QAAQ;AACX,iBAAS,oBAAI,IAAY;AACzB,eAAO,IAAI,MAAM,MAAM;AAAA,MACzB;AACA,aAAO,IAAI,KAAK;AAAA,IAClB;AACA,uBAAmB,IAAI,UAAU,SAAS;AAC1C,yBAAqB,IAAI,UAAU,MAAM;AAAA,EAC3C;AAEA,WAAS,QAAc;AACrB,QAAI,YAAa;AACjB,kBAAc,QAAQ,KAAK,UAAU,QAAQ,WAAW,YAAY,GAAG,YAAY;AAAA,EACrF;AAEA,WAAS,SAAe;AACtB,QAAI,aAAa;AACf,kBAAY;AACZ,oBAAc;AAAA,IAChB;AACA,aAAS,MAAM;AACf,uBAAmB,MAAM;AACzB,uBAAmB,MAAM;AACzB,yBAAqB,MAAM;AAC3B,kBAAc,QAAQ,KAAK,UAAU,QAAQ,WAAW,YAAY,GAAG,YAAY;AAAA,EACrF;AAEA,WAAS,OAAa;AACpB,QAAI,aAAa;AACf,kBAAY;AACZ,oBAAc;AAAA,IAChB;AAAA,EACF;AAEA,WAAS,cAAmC;AAC1C,WAAO;AAAA,EACT;AAEA,WAAS,uBAAuB,KAAuB;AACrD,UAAM,SAAmB,CAAC;AAC1B,eAAW,CAAC,KAAK,IAAI,KAAK,oBAAoB;AAC5C,UAAI,KAAK,IAAI,GAAG,EAAG,QAAO,KAAK,GAAG;AAAA,IACpC;AACA,WAAO;AAAA,EACT;AAEA,WAAS,iBAAiB,KAAa,KAAsB;AAC3D,WAAO,mBAAmB,IAAI,GAAG,GAAG,IAAI,GAAG,KAAK;AAAA,EAClD;AAEA,WAAS,mBAAmB,KAA+C;AACzE,UAAM,YAAY,mBAAmB,IAAI,GAAG;AAC5C,UAAM,SAAS,qBAAqB,IAAI,GAAG;AAC3C,QAAI,CAAC,aAAa,CAAC,OAAQ,QAAO;AAClC,WAAO;AAAA,MACL,MAAM,UAAU,IAAI,CAAC,QAAQ,CAAC,GAAG,GAAG,CAAC;AAAA,MACrC,QAAQ,IAAI,IAAI,MAAM,KAAK,QAAQ,CAAC,CAAC,KAAK,SAAS,MAAM,CAAC,KAAK,IAAI,IAAI,SAAS,CAAC,CAAC,CAAC;AAAA,MACnF,eAAe,IAAI,IAAI,mBAAmB,IAAI,GAAG,KAAK,CAAC,CAAC;AAAA,IAC1D;AAAA,EACF;AAEA,WAAS,eAAe,KAAa,SAAyC;AAC5E,UAAM,SAAS,qBAAqB,IAAI,GAAG,GAAG,IAAI,QAAQ,GAAG;AAC7D,QAAI,CAAC,UAAU,OAAO,SAAS,EAAG,QAAO;AACzC,QAAI,CAAC,QAAQ,UAAU,QAAQ,OAAO,WAAW,EAAG,QAAO;AAC3D,UAAM,WAAW,IAAI,IAAI,QAAQ,OAAO,IAAI,CAAC,UAAU,MAAM,YAAY,CAAC,CAAC;AAC3E,eAAW,SAAS,QAAQ;AAC1B,UAAI,SAAS,IAAI,MAAM,YAAY,CAAC,EAAG,QAAO;AAAA,IAChD;AACA,WAAO;AAAA,EACT;AAEA,WAAS,4BAA4B,UAA0D;AAC7F,UAAM,SAAmB,CAAC;AAC1B,eAAW,OAAO,UAAU;AAC1B,UAAI,SAAS,KAAK,CAAC,YAAY,eAAe,KAAK,OAAO,CAAC,EAAG,QAAO,KAAK,GAAG;AAAA,IAC/E;AACA,WAAO;AAAA,EACT;AAEA,WAAS,2BAA2B,WAAmB,QAAyC;AAC9F,UAAM,SAAS,UAAU,sBAAsB,SAAS,KAAK,CAAC;AAC9D,WAAO,4BAA4B,KAAK;AAAA,EAC1C;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,75 @@
1
+ // src/65/index.ts
2
+ function parseNip65RelayList(event) {
3
+ return event.tags.flatMap((tag) => {
4
+ if (tag[0] !== "r" || typeof tag[1] !== "string" || tag[1].length === 0) return [];
5
+ const marker = tag[2];
6
+ const read = marker !== "write";
7
+ const write = marker !== "read";
8
+ return [{ url: tag[1], read, write }];
9
+ });
10
+ }
11
+ function selectWriteRelays(entries) {
12
+ return dedupe(entries.filter((e) => e.write).map((e) => e.url));
13
+ }
14
+ function selectReadRelays(entries) {
15
+ return dedupe(entries.filter((e) => e.read).map((e) => e.url));
16
+ }
17
+ function createNip65Registry() {
18
+ const lists = /* @__PURE__ */ new Map();
19
+ function setRelayList(pubkey, entries) {
20
+ lists.set(pubkey, entries.map((e) => ({ ...e })));
21
+ }
22
+ function ingest(event) {
23
+ const entries = parseNip65RelayList(event);
24
+ setRelayList(event.pubkey, entries);
25
+ return entries;
26
+ }
27
+ function getRelayList(pubkey) {
28
+ const entries = lists.get(pubkey);
29
+ return entries ? entries.map((e) => ({ ...e })) : void 0;
30
+ }
31
+ function resolve(pubkeys, want) {
32
+ const urls = [];
33
+ const seen = /* @__PURE__ */ new Set();
34
+ for (const pubkey of pubkeys) {
35
+ const entries = lists.get(pubkey);
36
+ if (!entries) continue;
37
+ for (const entry of entries) {
38
+ if (entry[want] && !seen.has(entry.url)) {
39
+ seen.add(entry.url);
40
+ urls.push(entry.url);
41
+ }
42
+ }
43
+ }
44
+ return urls;
45
+ }
46
+ return {
47
+ ingest,
48
+ setRelayList,
49
+ getRelayList,
50
+ has: (pubkey) => lists.has(pubkey),
51
+ resolveOutboxRelays: (pubkeys) => resolve(pubkeys, "write"),
52
+ resolveReadRelays: (pubkeys) => resolve(pubkeys, "read"),
53
+ delete: (pubkey) => lists.delete(pubkey),
54
+ clear: () => lists.clear()
55
+ };
56
+ }
57
+ function dedupe(urls) {
58
+ const seen = /* @__PURE__ */ new Set();
59
+ const out = [];
60
+ for (const url of urls) {
61
+ if (!seen.has(url)) {
62
+ seen.add(url);
63
+ out.push(url);
64
+ }
65
+ }
66
+ return out;
67
+ }
68
+
69
+ export {
70
+ parseNip65RelayList,
71
+ selectWriteRelays,
72
+ selectReadRelays,
73
+ createNip65Registry
74
+ };
75
+ //# sourceMappingURL=chunk-UUMBCHTA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/65/index.ts"],"sourcesContent":["import type { NostrEvent } from 'nostr-tools';\n\n/**\n * `@kehto/nip/65` — NIP-65 relay-list (kind 10002) parsing and outbox-model\n * relay resolution.\n *\n * NIP-65 defines kind-10002 replaceable events whose `r` tags declare which\n * relays an author reads from and writes to — the \"outbox model\" (a.k.a.\n * \"gossip\"). A client that wants to *reach* an author publishes to (or reads\n * from) that author's advertised relays instead of a fixed relay set.\n *\n * `nostr-tools` ships no NIP-65 helper, so this module fills the gap with a\n * pure parser plus a closure-scoped registry (no module-global state, so\n * multiple runtimes in one process never alias each other — same multi-instance\n * contract as `@kehto/nip/66`).\n *\n * Tag grammar:\n * ```text\n * [\"r\", \"wss://relay.example.com\"] → read AND write\n * [\"r\", \"wss://relay.example.com\", \"read\"] → read-only\n * [\"r\", \"wss://relay.example.com\", \"write\"] → write-only\n * ```\n */\n\n/**\n * A single relay entry parsed from a NIP-65 kind-10002 `r` tag.\n *\n * @example\n * ```ts\n * const entry: RelayEntry = { url: 'wss://relay.example.com', read: true, write: false };\n * ```\n */\nexport interface RelayEntry {\n /** Relay WebSocket URL from the `r` tag's second element. */\n url: string;\n /** Whether the author reads (receives events) from this relay. */\n read: boolean;\n /** Whether the author writes (publishes events) to this relay. */\n write: boolean;\n}\n\n/**\n * Parse a NIP-65 kind-10002 relay-list event into structured {@link RelayEntry}\n * objects.\n *\n * Keeps tags where `tag[0] === 'r'` and `tag[1]` is a string URL. The optional\n * third element is the marker (`'read'`, `'write'`, or absent for both). Any\n * marker other than the two canonical strings is treated as \"unmarked\" (read +\n * write), matching how lenient NIP-65 consumers behave in practice.\n *\n * No URL normalization or hostname de-duplication is performed — the relay URL\n * is preserved verbatim. Normalize with a NIP-66 dataset (`@kehto/nip/66`) or a\n * dedicated relay-URL normalizer if you need canonical hostnames.\n *\n * @param event - A kind-10002 relay-list event (kind is not re-validated; pass the right event)\n * @returns Array of relay entries in tag order; duplicate URLs are preserved\n *\n * @example\n * ```ts\n * import { parseNip65RelayList } from '@kehto/nip/65';\n * const entries = parseNip65RelayList(kind10002Event);\n * // [{ url: 'wss://a', read: true, write: true }, { url: 'wss://b', read: true, write: false }]\n * ```\n */\nexport function parseNip65RelayList(event: NostrEvent): RelayEntry[] {\n return event.tags.flatMap((tag) => {\n if (tag[0] !== 'r' || typeof tag[1] !== 'string' || tag[1].length === 0) return [];\n const marker = tag[2];\n // Canonical markers are 'read' and 'write'; anything else (absent or a\n // typo) is treated as unmarked → both, so a listed relay is never silently\n // dropped from the outbox/inbox set.\n const read = marker !== 'write';\n const write = marker !== 'read';\n return [{ url: tag[1], read, write }];\n });\n}\n\n/**\n * Select the write (outbox) relay URLs from one author's parsed relay list,\n * de-duplicated and in first-seen order.\n *\n * @param entries - Parsed entries from {@link parseNip65RelayList}\n * @returns De-duplicated write-relay URLs\n *\n * @example\n * ```ts\n * const outbox = selectWriteRelays(parseNip65RelayList(event));\n * ```\n */\nexport function selectWriteRelays(entries: ReadonlyArray<RelayEntry>): string[] {\n return dedupe(entries.filter((e) => e.write).map((e) => e.url));\n}\n\n/**\n * Select the read (inbox) relay URLs from one author's parsed relay list,\n * de-duplicated and in first-seen order.\n *\n * @param entries - Parsed entries from {@link parseNip65RelayList}\n * @returns De-duplicated read-relay URLs\n */\nexport function selectReadRelays(entries: ReadonlyArray<RelayEntry>): string[] {\n return dedupe(entries.filter((e) => e.read).map((e) => e.url));\n}\n\n/**\n * A closure-scoped registry of parsed NIP-65 relay lists keyed by author\n * pubkey. Returned by {@link createNip65Registry}. Each registry owns its own\n * `Map` — two registries never share state (unlike a module-globals pattern).\n *\n * @example\n * ```ts\n * const registry = createNip65Registry();\n * registry.ingest(event); // store by event.pubkey\n * const outbox = registry.resolveOutboxRelays([authorA, authorB]);\n * ```\n */\nexport interface Nip65Registry {\n /**\n * Parse and store a kind-10002 event under its own `pubkey`. Overwrites any\n * previously stored list for that author (kind-10002 is replaceable).\n *\n * @param event - A kind-10002 relay-list event\n * @returns The parsed entries that were stored\n */\n ingest(event: NostrEvent): RelayEntry[];\n /**\n * Store an already-parsed relay list for an explicit pubkey. Use when you\n * parsed elsewhere or want to seed the registry without an event object.\n *\n * @param pubkey - Author hex public key\n * @param entries - Parsed relay entries\n */\n setRelayList(pubkey: string, entries: ReadonlyArray<RelayEntry>): void;\n /**\n * Get the stored relay list for an author, or `undefined` if none seen.\n *\n * @param pubkey - Author hex public key\n */\n getRelayList(pubkey: string): RelayEntry[] | undefined;\n /** Whether a relay list has been stored for `pubkey`. */\n has(pubkey: string): boolean;\n /**\n * Resolve write (outbox) relay URLs across the given authors, de-duplicated.\n * Authors with no stored list contribute nothing. Empty input or all-unknown\n * authors yield `[]`.\n *\n * @param pubkeys - Author hex public keys\n * @returns De-duplicated outbox relay URLs in first-seen order\n */\n resolveOutboxRelays(pubkeys: ReadonlyArray<string>): string[];\n /**\n * Resolve read (inbox) relay URLs across the given authors, de-duplicated.\n *\n * @param pubkeys - Author hex public keys\n * @returns De-duplicated inbox relay URLs in first-seen order\n */\n resolveReadRelays(pubkeys: ReadonlyArray<string>): string[];\n /** Drop a single author's stored relay list. Returns `true` if one existed. */\n delete(pubkey: string): boolean;\n /** Clear every stored relay list. */\n clear(): void;\n}\n\n/**\n * Create a closure-scoped {@link Nip65Registry} for accumulating kind-10002\n * relay lists and resolving outbox/inbox relays.\n *\n * Framework-agnostic: it knows nothing about relay pools, subscriptions, or\n * storage. Feed it kind-10002 events (e.g. from your pool's `onevent`\n * callback) via {@link Nip65Registry.ingest} and query the outbox model with\n * {@link Nip65Registry.resolveOutboxRelays}.\n *\n * @returns A fresh registry with its own state\n *\n * @example\n * ```ts\n * import { createNip65Registry } from '@kehto/nip/65';\n * import { SimplePool } from 'nostr-tools/pool';\n *\n * const registry = createNip65Registry();\n * const pool = new SimplePool();\n * pool.subscribeMany(discoveryRelays, [{ kinds: [10002], authors }], {\n * onevent: (e) => registry.ingest(e),\n * });\n * // Later, before publishing on behalf of `author`:\n * const outbox = registry.resolveOutboxRelays([author]);\n * ```\n */\nexport function createNip65Registry(): Nip65Registry {\n const lists = new Map<string, RelayEntry[]>();\n\n function setRelayList(pubkey: string, entries: ReadonlyArray<RelayEntry>): void {\n lists.set(pubkey, entries.map((e) => ({ ...e })));\n }\n\n function ingest(event: NostrEvent): RelayEntry[] {\n const entries = parseNip65RelayList(event);\n setRelayList(event.pubkey, entries);\n return entries;\n }\n\n function getRelayList(pubkey: string): RelayEntry[] | undefined {\n const entries = lists.get(pubkey);\n return entries ? entries.map((e) => ({ ...e })) : undefined;\n }\n\n function resolve(pubkeys: ReadonlyArray<string>, want: 'read' | 'write'): string[] {\n const urls: string[] = [];\n const seen = new Set<string>();\n for (const pubkey of pubkeys) {\n const entries = lists.get(pubkey);\n if (!entries) continue;\n for (const entry of entries) {\n if (entry[want] && !seen.has(entry.url)) {\n seen.add(entry.url);\n urls.push(entry.url);\n }\n }\n }\n return urls;\n }\n\n return {\n ingest,\n setRelayList,\n getRelayList,\n has: (pubkey) => lists.has(pubkey),\n resolveOutboxRelays: (pubkeys) => resolve(pubkeys, 'write'),\n resolveReadRelays: (pubkeys) => resolve(pubkeys, 'read'),\n delete: (pubkey) => lists.delete(pubkey),\n clear: () => lists.clear(),\n };\n}\n\nfunction dedupe(urls: ReadonlyArray<string>): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const url of urls) {\n if (!seen.has(url)) {\n seen.add(url);\n out.push(url);\n }\n }\n return out;\n}\n"],"mappings":";AAgEO,SAAS,oBAAoB,OAAiC;AACnE,SAAO,MAAM,KAAK,QAAQ,CAAC,QAAQ;AACjC,QAAI,IAAI,CAAC,MAAM,OAAO,OAAO,IAAI,CAAC,MAAM,YAAY,IAAI,CAAC,EAAE,WAAW,EAAG,QAAO,CAAC;AACjF,UAAM,SAAS,IAAI,CAAC;AAIpB,UAAM,OAAO,WAAW;AACxB,UAAM,QAAQ,WAAW;AACzB,WAAO,CAAC,EAAE,KAAK,IAAI,CAAC,GAAG,MAAM,MAAM,CAAC;AAAA,EACtC,CAAC;AACH;AAcO,SAAS,kBAAkB,SAA8C;AAC9E,SAAO,OAAO,QAAQ,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;AAChE;AASO,SAAS,iBAAiB,SAA8C;AAC7E,SAAO,OAAO,QAAQ,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;AAC/D;AAsFO,SAAS,sBAAqC;AACnD,QAAM,QAAQ,oBAAI,IAA0B;AAE5C,WAAS,aAAa,QAAgB,SAA0C;AAC9E,UAAM,IAAI,QAAQ,QAAQ,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC;AAAA,EAClD;AAEA,WAAS,OAAO,OAAiC;AAC/C,UAAM,UAAU,oBAAoB,KAAK;AACzC,iBAAa,MAAM,QAAQ,OAAO;AAClC,WAAO;AAAA,EACT;AAEA,WAAS,aAAa,QAA0C;AAC9D,UAAM,UAAU,MAAM,IAAI,MAAM;AAChC,WAAO,UAAU,QAAQ,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,IAAI;AAAA,EACpD;AAEA,WAAS,QAAQ,SAAgC,MAAkC;AACjF,UAAM,OAAiB,CAAC;AACxB,UAAM,OAAO,oBAAI,IAAY;AAC7B,eAAW,UAAU,SAAS;AAC5B,YAAM,UAAU,MAAM,IAAI,MAAM;AAChC,UAAI,CAAC,QAAS;AACd,iBAAW,SAAS,SAAS;AAC3B,YAAI,MAAM,IAAI,KAAK,CAAC,KAAK,IAAI,MAAM,GAAG,GAAG;AACvC,eAAK,IAAI,MAAM,GAAG;AAClB,eAAK,KAAK,MAAM,GAAG;AAAA,QACrB;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,KAAK,CAAC,WAAW,MAAM,IAAI,MAAM;AAAA,IACjC,qBAAqB,CAAC,YAAY,QAAQ,SAAS,OAAO;AAAA,IAC1D,mBAAmB,CAAC,YAAY,QAAQ,SAAS,MAAM;AAAA,IACvD,QAAQ,CAAC,WAAW,MAAM,OAAO,MAAM;AAAA,IACvC,OAAO,MAAM,MAAM,MAAM;AAAA,EAC3B;AACF;AAEA,SAAS,OAAO,MAAuC;AACrD,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,MAAgB,CAAC;AACvB,aAAW,OAAO,MAAM;AACtB,QAAI,CAAC,KAAK,IAAI,GAAG,GAAG;AAClB,WAAK,IAAI,GAAG;AACZ,UAAI,KAAK,GAAG;AAAA,IACd;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,7 @@
1
- export { DEFAULT_RELAY_ATTRIBUTE_GROUPS, Nip66Aggregator, Nip66AggregatorOptions, Nip66Filter, Nip66RelayAttributes, Nip66RelayPool, RelayAttributeGroups, RelayAttributeMatcher, createNip66Aggregator } from './66.js';
1
+ export { PathEntry, aggregateTagValue, computeAggregateHash, pathEntriesFromTags, verifyAggregate } from './5a/index.js';
2
+ export { CacheStorageNappletArtifactCache, CacheStorageNappletArtifactCacheOptions, CachedCoordinate, CoordinateFreshnessOptions, NAPPLET_ARTIFACT_CACHE_NAME, NAPPLET_KINDS, NAPPLET_KIND_NAMED, NAPPLET_KIND_ROOT, NAPPLET_KIND_SNAPSHOT, NappletAggregateIndexEntry, NappletArtifactCache, NappletArtifactCacheIndex, NappletBlobIndexEntry, NappletCacheDiagnostic, NappletManifest, NappletResolutionError, NappletResolutionErrorCode, OpenNappletArtifactCacheOptions, ResolveNappletOptions, ResolvedNapplet, WriteVerifiedResolutionInput, coordinateKey, fetchBlob, isCoordinateFresh, isNappletManifestKind, openNappletArtifactCache, parseNappletManifest, resolveNapplet, verifyBlobHash, verifyManifestSignature } from './5d/index.js';
3
+ export { ITEM_TAGS, LIST_KINDS, Nip44Decryptor, Nip51List, SET_KINDS, decryptPrivateItems, getTagValues, isListKind, isSetKind, listKindName, parseList } from './51/index.js';
4
+ export { Nip65Registry, RelayEntry, createNip65Registry, parseNip65RelayList, selectReadRelays, selectWriteRelays } from './65/index.js';
5
+ export { DEFAULT_RELAY_ATTRIBUTE_GROUPS, Nip66Aggregator, Nip66AggregatorOptions, Nip66Filter, Nip66RelayAttributes, Nip66RelayPool, RelayAttributeGroups, RelayAttributeMatcher, createNip66Aggregator } from './66/index.js';
6
+ export { HANDLER_INFORMATION_KIND, HANDLER_PLATFORMS, HANDLER_RECOMMENDATION_KIND, HandlerEntityValues, HandlerInformation, HandlerPlatform, HandlerRecommendation, HandlerRecommendationEntry, HandlerTarget, buildHandlerUrl, handlesKind, parseHandlerInformation, parseHandlerRecommendation } from './89/index.js';
2
7
  import 'nostr-tools';
package/dist/index.js CHANGED
@@ -1,9 +1,99 @@
1
+ import {
2
+ ITEM_TAGS,
3
+ LIST_KINDS,
4
+ SET_KINDS,
5
+ decryptPrivateItems,
6
+ getTagValues,
7
+ isListKind,
8
+ isSetKind,
9
+ listKindName,
10
+ parseList
11
+ } from "./chunk-2XYRP2S6.js";
12
+ import {
13
+ CacheStorageNappletArtifactCache,
14
+ NAPPLET_ARTIFACT_CACHE_NAME,
15
+ NAPPLET_KINDS,
16
+ NAPPLET_KIND_NAMED,
17
+ NAPPLET_KIND_ROOT,
18
+ NAPPLET_KIND_SNAPSHOT,
19
+ NappletResolutionError,
20
+ coordinateKey,
21
+ fetchBlob,
22
+ isCoordinateFresh,
23
+ isNappletManifestKind,
24
+ openNappletArtifactCache,
25
+ parseNappletManifest,
26
+ resolveNapplet,
27
+ verifyBlobHash,
28
+ verifyManifestSignature
29
+ } from "./chunk-CDBUCFLP.js";
30
+ import {
31
+ aggregateTagValue,
32
+ computeAggregateHash,
33
+ pathEntriesFromTags,
34
+ verifyAggregate
35
+ } from "./chunk-2J3CSGKL.js";
36
+ import {
37
+ createNip65Registry,
38
+ parseNip65RelayList,
39
+ selectReadRelays,
40
+ selectWriteRelays
41
+ } from "./chunk-UUMBCHTA.js";
1
42
  import {
2
43
  DEFAULT_RELAY_ATTRIBUTE_GROUPS,
3
44
  createNip66Aggregator
4
- } from "./chunk-N4WPLKIJ.js";
45
+ } from "./chunk-DADZ6NUI.js";
46
+ import {
47
+ HANDLER_INFORMATION_KIND,
48
+ HANDLER_PLATFORMS,
49
+ HANDLER_RECOMMENDATION_KIND,
50
+ buildHandlerUrl,
51
+ handlesKind,
52
+ parseHandlerInformation,
53
+ parseHandlerRecommendation
54
+ } from "./chunk-BCTIL523.js";
5
55
  export {
56
+ CacheStorageNappletArtifactCache,
6
57
  DEFAULT_RELAY_ATTRIBUTE_GROUPS,
7
- createNip66Aggregator
58
+ HANDLER_INFORMATION_KIND,
59
+ HANDLER_PLATFORMS,
60
+ HANDLER_RECOMMENDATION_KIND,
61
+ ITEM_TAGS,
62
+ LIST_KINDS,
63
+ NAPPLET_ARTIFACT_CACHE_NAME,
64
+ NAPPLET_KINDS,
65
+ NAPPLET_KIND_NAMED,
66
+ NAPPLET_KIND_ROOT,
67
+ NAPPLET_KIND_SNAPSHOT,
68
+ NappletResolutionError,
69
+ SET_KINDS,
70
+ aggregateTagValue,
71
+ buildHandlerUrl,
72
+ computeAggregateHash,
73
+ coordinateKey,
74
+ createNip65Registry,
75
+ createNip66Aggregator,
76
+ decryptPrivateItems,
77
+ fetchBlob,
78
+ getTagValues,
79
+ handlesKind,
80
+ isCoordinateFresh,
81
+ isListKind,
82
+ isNappletManifestKind,
83
+ isSetKind,
84
+ listKindName,
85
+ openNappletArtifactCache,
86
+ parseHandlerInformation,
87
+ parseHandlerRecommendation,
88
+ parseList,
89
+ parseNappletManifest,
90
+ parseNip65RelayList,
91
+ pathEntriesFromTags,
92
+ resolveNapplet,
93
+ selectReadRelays,
94
+ selectWriteRelays,
95
+ verifyAggregate,
96
+ verifyBlobHash,
97
+ verifyManifestSignature
8
98
  };
9
99
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kehto/nip",
3
- "version": "0.1.0",
3
+ "version": "0.4.0",
4
4
  "description": "Tree-shakable bundle of unique Nostr NIP utilities for napplet runtimes — each NIP at its own subpath (e.g. @kehto/nip/66)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -11,9 +11,29 @@
11
11
  "types": "./dist/index.d.ts",
12
12
  "import": "./dist/index.js"
13
13
  },
14
+ "./5a": {
15
+ "types": "./dist/5a/index.d.ts",
16
+ "import": "./dist/5a/index.js"
17
+ },
18
+ "./5d": {
19
+ "types": "./dist/5d/index.d.ts",
20
+ "import": "./dist/5d/index.js"
21
+ },
22
+ "./51": {
23
+ "types": "./dist/51/index.d.ts",
24
+ "import": "./dist/51/index.js"
25
+ },
26
+ "./65": {
27
+ "types": "./dist/65/index.d.ts",
28
+ "import": "./dist/65/index.js"
29
+ },
14
30
  "./66": {
15
- "types": "./dist/66.d.ts",
16
- "import": "./dist/66.js"
31
+ "types": "./dist/66/index.d.ts",
32
+ "import": "./dist/66/index.js"
33
+ },
34
+ "./89": {
35
+ "types": "./dist/89/index.d.ts",
36
+ "import": "./dist/89/index.js"
17
37
  }
18
38
  },
19
39
  "files": [
@@ -26,6 +46,9 @@
26
46
  "peerDependencies": {
27
47
  "nostr-tools": ">=2.23.3 <3.0.0"
28
48
  },
49
+ "dependencies": {
50
+ "@noble/hashes": "^2.0.0"
51
+ },
29
52
  "devDependencies": {
30
53
  "nostr-tools": "^2.23.3",
31
54
  "tsup": "^8.5.0",
@@ -41,15 +64,26 @@
41
64
  "keywords": [
42
65
  "nostr",
43
66
  "nip",
67
+ "nip5a",
68
+ "nip5d",
69
+ "nip51",
70
+ "nip65",
44
71
  "nip66",
72
+ "nip89",
73
+ "aggregate-hash",
74
+ "content-addressed",
75
+ "blossom",
76
+ "napplet-resolution",
45
77
  "relay-discovery",
78
+ "outbox-model",
79
+ "app-handlers",
46
80
  "kehto",
47
81
  "napplet"
48
82
  ],
49
83
  "scripts": {
50
84
  "build": "tsup",
51
85
  "type-check": "tsc --noEmit",
52
- "test:unit": "cd ../.. && vitest run --config vitest.config.ts packages/nip/src/66.test.ts",
86
+ "test:unit": "cd ../.. && vitest run --config vitest.config.ts packages/nip",
53
87
  "test": "pnpm test:unit"
54
88
  }
55
89
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/66.ts"],"sourcesContent":["import type { NostrEvent } from 'nostr-tools';\n\n/**\n * Minimal relay-pool contract `@kehto/nip/66` depends on. Consumers implement\n * this against their pool library of choice (nostr-tools SimplePool,\n * applesauce-relay, @snort/worker-relay, etc.). Intentionally smaller than\n * RxJS-style observable pools — one callback, one unsubscribe handle.\n *\n * @example\n * ```ts\n * import { SimplePool } from 'nostr-tools/pool';\n * const pool = new SimplePool();\n * const adapter: Nip66RelayPool = {\n * subscribe: (relays, filter, onEvent) => {\n * const sub = pool.subscribeMany(relays, [filter], { onevent: onEvent });\n * return () => sub.close();\n * },\n * };\n * ```\n */\nexport interface Nip66RelayPool {\n /**\n * Open a kind-30166 subscription against a set of monitor relays.\n *\n * @param relays - WebSocket URLs to subscribe against (the aggregator passes its bootstrap list)\n * @param filter - A kind-30166 filter, optionally narrowed with `#n` network tags\n * @param onEvent - Called synchronously with every matching event; the aggregator parses each one\n * @returns An unsubscribe handle the aggregator calls on `resync()` or teardown\n */\n subscribe(\n relays: ReadonlyArray<string>,\n filter: Nip66Filter,\n onEvent: (event: NostrEvent) => void,\n ): () => void;\n}\n\n/**\n * Minimal filter shape for kind-30166 subscriptions. `#n` optionally narrows\n * by network type (e.g. `['clearnet']`, `['clearnet', 'tor']`).\n *\n * @example\n * ```ts\n * const filter: Nip66Filter = { kinds: [30166], '#n': ['clearnet'] };\n * ```\n */\nexport type Nip66Filter = { kinds: [30166]; '#n'?: ReadonlyArray<string> };\n\n/**\n * Options for {@link createNip66Aggregator}.\n *\n * @example\n * ```ts\n * const options: Nip66AggregatorOptions = {\n * pool: myPoolAdapter,\n * bootstrap: ['wss://monitor1.example.com', 'wss://monitor2.example.com'],\n * networks: ['clearnet'],\n * };\n * ```\n */\nexport interface Nip66AggregatorOptions {\n /** A {@link Nip66RelayPool} adapter supplied by the consumer. */\n pool: Nip66RelayPool;\n /** Required non-empty list of monitor relay URLs to subscribe against. No default ships in the package. */\n bootstrap: ReadonlyArray<string>;\n /** Optional network-type narrowing passed to the filter as `#n` (e.g. `['clearnet', 'tor']`). Omit to receive every network. */\n networks?: ReadonlyArray<string>;\n /**\n * Optional named relay-attribute groups for interpreting ad hoc kind-30166 tags.\n * Omit to use {@link DEFAULT_RELAY_ATTRIBUTE_GROUPS}.\n */\n relayAttributeGroups?: RelayAttributeGroups;\n}\n\n/** A tag/value matcher for ad hoc relay attributes carried on kind-30166 events. */\nexport interface RelayAttributeMatcher {\n /** Tag name to match, for example `T`, `t`, or `k`. */\n tag: string;\n /** Optional accepted values. If omitted, any value for the tag matches. */\n values?: ReadonlyArray<string>;\n}\n\n/** Named groups of relay-attribute matchers. Matchers within a group are OR-ed. */\nexport type RelayAttributeGroups = Readonly<Record<string, ReadonlyArray<RelayAttributeMatcher>>>;\n\n/** Parsed attributes observed for one NIP-66 relay URL. */\nexport interface Nip66RelayAttributes {\n /** Raw tag copies from the latest observed kind-30166 event for the relay. */\n tags: ReadonlyArray<ReadonlyArray<string>>;\n /** Tag-value index, keyed by tag name. */\n values: ReadonlyMap<string, ReadonlySet<string>>;\n /** Parsed uppercase `N` tag support for convenience. */\n supportedNips: ReadonlySet<number>;\n}\n\nexport const DEFAULT_RELAY_ATTRIBUTE_GROUPS: RelayAttributeGroups = {\n Indexer: [\n { tag: 'T', values: ['Indexer', 'indexer'] },\n { tag: 't', values: ['Indexer', 'indexer'] },\n { tag: 'k', values: ['10002'] },\n ],\n RelayIndexer: [\n { tag: 'T', values: ['RelayIndexer', 'relay-indexer', 'relay_indexer'] },\n { tag: 't', values: ['RelayIndexer', 'relay-indexer', 'relay_indexer'] },\n { tag: 'k', values: ['30166', '10166'] },\n ],\n};\n\n/**\n * Aggregator handle returned by {@link createNip66Aggregator}. Each call\n * returns a fresh instance with its own closure-scoped state — multi-instance\n * safe (unlike a module-globals pattern).\n *\n * @example\n * ```ts\n * const aggregator = createNip66Aggregator({ pool, bootstrap: ['wss://monitor.example.com'] });\n * aggregator.start();\n * const relays = aggregator.getRelaySet();\n * ```\n */\nexport interface Nip66Aggregator {\n /** Begin subscribing against the bootstrap relay set. Idempotent — calling twice is a no-op. */\n start(): void;\n /** Tear down the current subscription, clear accumulated state, and re-subscribe. Call when `networks` semantics change upstream. */\n resync(): void;\n /**\n * Tear down the current subscription. Idempotent — calling without a prior\n * `start()` is a safe no-op. Preserves accumulated state (`getRelaySet`,\n * `relaySupportsNip`) — use `resync()` if you need to clear state AND\n * re-subscribe in one step. A subsequent `start()` re-subscribes against\n * the bootstrap relays (identical contract to the first start).\n *\n * Wire this to your consumer's teardown path (browser: `beforeunload`;\n * React: effect cleanup; Electron: `app.before-quit`) — the aggregator\n * holds a pool subscription that otherwise lives for the tab lifetime\n * (PITFALLS.md M-03).\n */\n stop(): void;\n /** The current set of relay URLs discovered from kind-30166 `d`-tags. Empty until `start()` fires at least one event. */\n getRelaySet(): ReadonlySet<string>;\n /**\n * Relay URLs whose `N`-tags declare support for the given NIP number.\n *\n * @param nip - NIP number to query support for (e.g. 44 for NIP-44 encrypted DMs)\n * @returns Array of relay URLs supporting that NIP\n */\n getRelaysSupportingNip(nip: number): string[];\n /**\n * Whether the relay at `url` has been seen with an `N`-tag declaring support for `nip`.\n *\n * @param url - Relay WebSocket URL to check\n * @param nip - NIP number to query support for\n * @returns `true` iff the relay has been observed with a matching `N`-tag\n */\n relaySupportsNip(url: string, nip: number): boolean;\n /**\n * Return parsed relay attributes for a relay URL, if that relay has been\n * observed in a kind-30166 event.\n */\n getRelayAttributes(url: string): Nip66RelayAttributes | undefined;\n /**\n * Return relays matching any supplied ad hoc attribute matcher.\n *\n * @param matchers - Tag/value matchers, OR semantics\n */\n getRelaysMatchingAttributes(matchers: ReadonlyArray<RelayAttributeMatcher>): string[];\n /**\n * Return relays in a named attribute group. Uses the group table passed to\n * `createNip66Aggregator`, or {@link DEFAULT_RELAY_ATTRIBUTE_GROUPS}.\n */\n getRelaysForAttributeGroup(groupName: string, groups?: RelayAttributeGroups): string[];\n}\n\n/**\n * Create a NIP-66 kind-30166 relay-discovery aggregator with closure-scoped state.\n *\n * Subscribes to kind-30166 events via the injected {@link Nip66RelayPool},\n * extracts relay URLs from `d`-tags, and parses `N`-tags to track which NIP\n * numbers each relay supports. Multi-instance safe — each factory call owns\n * its own `Set` + `Map`; multiple aggregators share nothing at module scope.\n *\n * The aggregator is framework-agnostic: it knows nothing about negentropy\n * (NIP-77), OPFS caching, worker relays, or network-type config stores.\n * Consumers inject those concerns through their {@link Nip66RelayPool}\n * implementation. `start()` is synchronous and idempotent — consumers\n * schedule their own cadence (delayed start, retry, etc.).\n *\n * @param options - Pool adapter + bootstrap monitor relays (+ optional network filter)\n * @returns A fresh {@link Nip66Aggregator} handle\n *\n * @example\n * ```ts\n * import { createNip66Aggregator } from '@kehto/nip/66';\n * import { SimplePool } from 'nostr-tools/pool';\n *\n * const pool = new SimplePool();\n * const aggregator = createNip66Aggregator({\n * pool: {\n * subscribe: (relays, filter, onEvent) => {\n * const sub = pool.subscribeMany(relays, [filter], { onevent: onEvent });\n * return () => sub.close();\n * },\n * },\n * bootstrap: ['wss://monitor1.example.com'],\n * });\n * aggregator.start();\n * // Later — inside a ShellAdapter hook:\n * const suggestions = Array.from(aggregator.getRelaySet());\n * ```\n */\nexport function createNip66Aggregator(options: Nip66AggregatorOptions): Nip66Aggregator {\n const relaySet = new Set<string>();\n const relaySupportedNips = new Map<string, Set<number>>();\n const relayAttributeTags = new Map<string, string[][]>();\n const relayAttributeValues = new Map<string, Map<string, Set<string>>>();\n const relayAttributeGroups = options.relayAttributeGroups ?? DEFAULT_RELAY_ATTRIBUTE_GROUPS;\n let unsubscribe: (() => void) | null = null;\n\n function buildFilter(): Nip66Filter {\n const filter: Nip66Filter = { kinds: [30166] };\n if (options.networks && options.networks.length > 0) {\n filter['#n'] = options.networks;\n }\n return filter;\n }\n\n function processEvent(event: NostrEvent): void {\n const dTag = event.tags.find((t) => t[0] === 'd');\n const relayUrl = dTag?.[1];\n if (!relayUrl) return;\n relaySet.add(relayUrl);\n captureRelayAttributes(relayUrl, event.tags);\n\n const nips = new Set<number>();\n for (const tag of event.tags) {\n if (tag[0] === 'N' && tag[1]) {\n const nipNum = parseInt(tag[1], 10);\n if (!Number.isNaN(nipNum)) nips.add(nipNum);\n }\n }\n if (nips.size > 0) relaySupportedNips.set(relayUrl, nips);\n }\n\n function captureRelayAttributes(relayUrl: string, tags: string[][]): void {\n const tagCopies = tags.map((tag) => [...tag]);\n const values = new Map<string, Set<string>>();\n for (const tag of tags) {\n const [name, value] = tag;\n if (!name || value === undefined) continue;\n let bucket = values.get(name);\n if (!bucket) {\n bucket = new Set<string>();\n values.set(name, bucket);\n }\n bucket.add(value);\n }\n relayAttributeTags.set(relayUrl, tagCopies);\n relayAttributeValues.set(relayUrl, values);\n }\n\n function start(): void {\n if (unsubscribe) return; // idempotent — already subscribed\n unsubscribe = options.pool.subscribe(options.bootstrap, buildFilter(), processEvent);\n }\n\n function resync(): void {\n if (unsubscribe) {\n unsubscribe();\n unsubscribe = null;\n }\n relaySet.clear();\n relaySupportedNips.clear();\n relayAttributeTags.clear();\n relayAttributeValues.clear();\n unsubscribe = options.pool.subscribe(options.bootstrap, buildFilter(), processEvent);\n }\n\n function stop(): void {\n if (unsubscribe) {\n unsubscribe();\n unsubscribe = null;\n }\n }\n\n function getRelaySet(): ReadonlySet<string> {\n return relaySet;\n }\n\n function getRelaysSupportingNip(nip: number): string[] {\n const result: string[] = [];\n for (const [url, nips] of relaySupportedNips) {\n if (nips.has(nip)) result.push(url);\n }\n return result;\n }\n\n function relaySupportsNip(url: string, nip: number): boolean {\n return relaySupportedNips.get(url)?.has(nip) ?? false;\n }\n\n function getRelayAttributes(url: string): Nip66RelayAttributes | undefined {\n const tagCopies = relayAttributeTags.get(url);\n const values = relayAttributeValues.get(url);\n if (!tagCopies || !values) return undefined;\n return {\n tags: tagCopies.map((tag) => [...tag]),\n values: new Map(Array.from(values, ([tag, tagValues]) => [tag, new Set(tagValues)])),\n supportedNips: new Set(relaySupportedNips.get(url) ?? []),\n };\n }\n\n function matcherMatches(url: string, matcher: RelayAttributeMatcher): boolean {\n const values = relayAttributeValues.get(url)?.get(matcher.tag);\n if (!values || values.size === 0) return false;\n if (!matcher.values || matcher.values.length === 0) return true;\n const accepted = new Set(matcher.values.map((value) => value.toLowerCase()));\n for (const value of values) {\n if (accepted.has(value.toLowerCase())) return true;\n }\n return false;\n }\n\n function getRelaysMatchingAttributes(matchers: ReadonlyArray<RelayAttributeMatcher>): string[] {\n const result: string[] = [];\n for (const url of relaySet) {\n if (matchers.some((matcher) => matcherMatches(url, matcher))) result.push(url);\n }\n return result;\n }\n\n function getRelaysForAttributeGroup(groupName: string, groups?: RelayAttributeGroups): string[] {\n const group = (groups ?? relayAttributeGroups)[groupName] ?? [];\n return getRelaysMatchingAttributes(group);\n }\n\n return {\n start,\n resync,\n stop,\n getRelaySet,\n getRelaysSupportingNip,\n relaySupportsNip,\n getRelayAttributes,\n getRelaysMatchingAttributes,\n getRelaysForAttributeGroup,\n };\n}\n"],"mappings":";AA8FO,IAAM,iCAAuD;AAAA,EAClE,SAAS;AAAA,IACP,EAAE,KAAK,KAAK,QAAQ,CAAC,WAAW,SAAS,EAAE;AAAA,IAC3C,EAAE,KAAK,KAAK,QAAQ,CAAC,WAAW,SAAS,EAAE;AAAA,IAC3C,EAAE,KAAK,KAAK,QAAQ,CAAC,OAAO,EAAE;AAAA,EAChC;AAAA,EACA,cAAc;AAAA,IACZ,EAAE,KAAK,KAAK,QAAQ,CAAC,gBAAgB,iBAAiB,eAAe,EAAE;AAAA,IACvE,EAAE,KAAK,KAAK,QAAQ,CAAC,gBAAgB,iBAAiB,eAAe,EAAE;AAAA,IACvE,EAAE,KAAK,KAAK,QAAQ,CAAC,SAAS,OAAO,EAAE;AAAA,EACzC;AACF;AAwGO,SAAS,sBAAsB,SAAkD;AACtF,QAAM,WAAW,oBAAI,IAAY;AACjC,QAAM,qBAAqB,oBAAI,IAAyB;AACxD,QAAM,qBAAqB,oBAAI,IAAwB;AACvD,QAAM,uBAAuB,oBAAI,IAAsC;AACvE,QAAM,uBAAuB,QAAQ,wBAAwB;AAC7D,MAAI,cAAmC;AAEvC,WAAS,cAA2B;AAClC,UAAM,SAAsB,EAAE,OAAO,CAAC,KAAK,EAAE;AAC7C,QAAI,QAAQ,YAAY,QAAQ,SAAS,SAAS,GAAG;AACnD,aAAO,IAAI,IAAI,QAAQ;AAAA,IACzB;AACA,WAAO;AAAA,EACT;AAEA,WAAS,aAAa,OAAyB;AAC7C,UAAM,OAAO,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC,MAAM,GAAG;AAChD,UAAM,WAAW,OAAO,CAAC;AACzB,QAAI,CAAC,SAAU;AACf,aAAS,IAAI,QAAQ;AACrB,2BAAuB,UAAU,MAAM,IAAI;AAE3C,UAAM,OAAO,oBAAI,IAAY;AAC7B,eAAW,OAAO,MAAM,MAAM;AAC5B,UAAI,IAAI,CAAC,MAAM,OAAO,IAAI,CAAC,GAAG;AAC5B,cAAM,SAAS,SAAS,IAAI,CAAC,GAAG,EAAE;AAClC,YAAI,CAAC,OAAO,MAAM,MAAM,EAAG,MAAK,IAAI,MAAM;AAAA,MAC5C;AAAA,IACF;AACA,QAAI,KAAK,OAAO,EAAG,oBAAmB,IAAI,UAAU,IAAI;AAAA,EAC1D;AAEA,WAAS,uBAAuB,UAAkB,MAAwB;AACxE,UAAM,YAAY,KAAK,IAAI,CAAC,QAAQ,CAAC,GAAG,GAAG,CAAC;AAC5C,UAAM,SAAS,oBAAI,IAAyB;AAC5C,eAAW,OAAO,MAAM;AACtB,YAAM,CAAC,MAAM,KAAK,IAAI;AACtB,UAAI,CAAC,QAAQ,UAAU,OAAW;AAClC,UAAI,SAAS,OAAO,IAAI,IAAI;AAC5B,UAAI,CAAC,QAAQ;AACX,iBAAS,oBAAI,IAAY;AACzB,eAAO,IAAI,MAAM,MAAM;AAAA,MACzB;AACA,aAAO,IAAI,KAAK;AAAA,IAClB;AACA,uBAAmB,IAAI,UAAU,SAAS;AAC1C,yBAAqB,IAAI,UAAU,MAAM;AAAA,EAC3C;AAEA,WAAS,QAAc;AACrB,QAAI,YAAa;AACjB,kBAAc,QAAQ,KAAK,UAAU,QAAQ,WAAW,YAAY,GAAG,YAAY;AAAA,EACrF;AAEA,WAAS,SAAe;AACtB,QAAI,aAAa;AACf,kBAAY;AACZ,oBAAc;AAAA,IAChB;AACA,aAAS,MAAM;AACf,uBAAmB,MAAM;AACzB,uBAAmB,MAAM;AACzB,yBAAqB,MAAM;AAC3B,kBAAc,QAAQ,KAAK,UAAU,QAAQ,WAAW,YAAY,GAAG,YAAY;AAAA,EACrF;AAEA,WAAS,OAAa;AACpB,QAAI,aAAa;AACf,kBAAY;AACZ,oBAAc;AAAA,IAChB;AAAA,EACF;AAEA,WAAS,cAAmC;AAC1C,WAAO;AAAA,EACT;AAEA,WAAS,uBAAuB,KAAuB;AACrD,UAAM,SAAmB,CAAC;AAC1B,eAAW,CAAC,KAAK,IAAI,KAAK,oBAAoB;AAC5C,UAAI,KAAK,IAAI,GAAG,EAAG,QAAO,KAAK,GAAG;AAAA,IACpC;AACA,WAAO;AAAA,EACT;AAEA,WAAS,iBAAiB,KAAa,KAAsB;AAC3D,WAAO,mBAAmB,IAAI,GAAG,GAAG,IAAI,GAAG,KAAK;AAAA,EAClD;AAEA,WAAS,mBAAmB,KAA+C;AACzE,UAAM,YAAY,mBAAmB,IAAI,GAAG;AAC5C,UAAM,SAAS,qBAAqB,IAAI,GAAG;AAC3C,QAAI,CAAC,aAAa,CAAC,OAAQ,QAAO;AAClC,WAAO;AAAA,MACL,MAAM,UAAU,IAAI,CAAC,QAAQ,CAAC,GAAG,GAAG,CAAC;AAAA,MACrC,QAAQ,IAAI,IAAI,MAAM,KAAK,QAAQ,CAAC,CAAC,KAAK,SAAS,MAAM,CAAC,KAAK,IAAI,IAAI,SAAS,CAAC,CAAC,CAAC;AAAA,MACnF,eAAe,IAAI,IAAI,mBAAmB,IAAI,GAAG,KAAK,CAAC,CAAC;AAAA,IAC1D;AAAA,EACF;AAEA,WAAS,eAAe,KAAa,SAAyC;AAC5E,UAAM,SAAS,qBAAqB,IAAI,GAAG,GAAG,IAAI,QAAQ,GAAG;AAC7D,QAAI,CAAC,UAAU,OAAO,SAAS,EAAG,QAAO;AACzC,QAAI,CAAC,QAAQ,UAAU,QAAQ,OAAO,WAAW,EAAG,QAAO;AAC3D,UAAM,WAAW,IAAI,IAAI,QAAQ,OAAO,IAAI,CAAC,UAAU,MAAM,YAAY,CAAC,CAAC;AAC3E,eAAW,SAAS,QAAQ;AAC1B,UAAI,SAAS,IAAI,MAAM,YAAY,CAAC,EAAG,QAAO;AAAA,IAChD;AACA,WAAO;AAAA,EACT;AAEA,WAAS,4BAA4B,UAA0D;AAC7F,UAAM,SAAmB,CAAC;AAC1B,eAAW,OAAO,UAAU;AAC1B,UAAI,SAAS,KAAK,CAAC,YAAY,eAAe,KAAK,OAAO,CAAC,EAAG,QAAO,KAAK,GAAG;AAAA,IAC/E;AACA,WAAO;AAAA,EACT;AAEA,WAAS,2BAA2B,WAAmB,QAAyC;AAC9F,UAAM,SAAS,UAAU,sBAAsB,SAAS,KAAK,CAAC;AAC9D,WAAO,4BAA4B,KAAK;AAAA,EAC1C;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
File without changes
File without changes