🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@orangecheck/relay-filter

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@orangecheck/relay-filter - npm Package Compare versions

Comparing version
0.1.2
to
0.1.3
+1
-1
dist/index.js.map

@@ -1,1 +0,1 @@

{"version":3,"sources":["../src/cache.ts","../src/filter.ts"],"names":["check"],"mappings":"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","file":"index.js","sourcesContent":["import type { FilterDecision } from './types';\n\ninterface Entry {\n value: FilterDecision;\n expires: number;\n}\n\nexport class TtlLru {\n private readonly store = new Map<string, Entry>();\n\n constructor(\n private readonly max: number,\n private readonly ttlMs: number\n ) {}\n\n get(key: string): FilterDecision | undefined {\n const entry = this.store.get(key);\n if (!entry) return undefined;\n if (Date.now() > entry.expires) {\n this.store.delete(key);\n return undefined;\n }\n // Touch on access so hot entries survive insertion pressure. Without\n // this, the old implementation was FIFO-with-TTL, not actually LRU —\n // a frequently-hit key could be evicted by N cold writes before it\n // ever moved in the map's insertion order.\n this.store.delete(key);\n this.store.set(key, entry);\n return entry.value;\n }\n\n /** Optional per-call TTL override. Used by the filter to cache\n * lookup-error decisions with a short TTL (circuit-breaker pattern). */\n set(key: string, value: FilterDecision, ttlMs?: number): void {\n if (this.store.size >= this.max) {\n const first = this.store.keys().next().value;\n if (first !== undefined) this.store.delete(first);\n }\n this.store.set(key, { value, expires: Date.now() + (ttlMs ?? this.ttlMs) });\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","import type { FilterDecision, FilterOptions, MinimalNostrEvent } from './types';\n\nimport { check } from '@orangecheck/sdk';\n\nimport { TtlLru } from './cache';\n\nconst DEFAULT_ALLOW_KINDS = [0, 3, 10002]; // profile meta, contacts, relay list\n\n// Short TTL for lookup-error decisions — acts as a circuit breaker so we\n// don't thundering-herd /api/check when upstream is flapping.\nconst LOOKUP_ERROR_TTL_MS = 5_000;\n\n/**\n * Process-wide cache. The old WeakMap<FilterOptions, …> design meant callers\n * who constructed a fresh options object per event (very common) never\n * produced a cache hit — every event was a cold lookup. Keying by a stable\n * config signature instead lets identical-config callers share.\n */\nconst caches = new Map<string, TtlLru>();\n\nfunction configSignature(opts: FilterOptions): string {\n return JSON.stringify([\n opts.minSats ?? 0,\n opts.minDays ?? 0,\n (opts.allowKinds ?? DEFAULT_ALLOW_KINDS).slice().sort(),\n (opts.allowPubkeys ?? []).slice().sort(),\n (opts.relays ?? []).slice().sort(),\n opts.cacheMax ?? 1_000,\n opts.cacheTtlMs ?? 60_000,\n ]);\n}\n\nfunction cacheFor(opts: FilterOptions): TtlLru {\n const sig = configSignature(opts);\n let c = caches.get(sig);\n if (!c) {\n c = new TtlLru(opts.cacheMax ?? 1_000, opts.cacheTtlMs ?? 60_000);\n caches.set(sig, c);\n }\n return c;\n}\n\n/**\n * The Nostr public key an OrangeCheck attestation binds is the `nostr:npub…`\n * identity. Events on the wire carry the hex-encoded pubkey. We build the\n * identity lookup key using the hex form and let OrangeCheck's discovery\n * handle both formats.\n */\nfunction identityFor(pubkeyHex: string) {\n return { protocol: 'nostr', identifier: pubkeyHex } as const;\n}\n\nfunction reject(\n reason: FilterDecision['reason'],\n message: string,\n extras?: Partial<FilterDecision>\n): FilterDecision {\n return { action: 'reject', reason, message, ...extras };\n}\n\nfunction accept(\n reason: FilterDecision['reason'],\n extras?: Partial<FilterDecision>\n): FilterDecision {\n return { action: 'accept', reason, ...extras };\n}\n\n/**\n * Decide whether a relay should accept an event based on the OrangeCheck\n * status of the author's pubkey.\n *\n * Framework-agnostic. Use this from a custom relay, nostr-tools, or whatever.\n * For Strfry, see `@orangecheck/relay-filter/strfry`.\n */\nexport async function filterEvent(\n event: MinimalNostrEvent,\n options: FilterOptions\n): Promise<FilterDecision> {\n // Bypass: allowed kinds.\n const allowKinds = options.allowKinds ?? DEFAULT_ALLOW_KINDS;\n if (allowKinds.includes(event.kind)) {\n return finish(event, accept('allowed_kind'), options);\n }\n\n // Bypass: operator / admin pubkeys.\n if (options.allowPubkeys?.includes(event.pubkey)) {\n return finish(event, accept('allowed_pubkey', { pubkey: event.pubkey }), options);\n }\n\n // Check cache keyed by (pubkey, thresholds).\n const cache = cacheFor(options);\n const key = `${event.pubkey}:${options.minSats ?? 0}:${options.minDays ?? 0}`;\n const cached = cache.get(key);\n if (cached) {\n return finish(event, { ...cached, pubkey: event.pubkey }, options);\n }\n\n // Look up the pubkey's attestation via OrangeCheck.\n let decision: FilterDecision;\n try {\n const result = await check({\n identity: identityFor(event.pubkey),\n minSats: options.minSats,\n minDays: options.minDays,\n ...(options.relays ? { relays: options.relays } : {}),\n });\n\n if (result.ok) {\n decision = accept('ok', { check: result, pubkey: event.pubkey });\n } else if (result.reasons?.includes('not_found')) {\n decision = reject(\n 'no_attestation',\n `orangecheck: this relay requires a Bitcoin-stake proof. See https://ochk.io`,\n { check: result, pubkey: event.pubkey }\n );\n } else if (result.reasons?.some((r) => r === 'below_min_sats' || r === 'below_min_days')) {\n decision = reject(\n 'below_threshold',\n `orangecheck: proof below relay thresholds (min_sats=${options.minSats ?? 0}, min_days=${options.minDays ?? 0})`,\n { check: result, pubkey: event.pubkey }\n );\n } else {\n decision = reject(\n 'invalid_proof',\n `orangecheck: proof invalid (${result.reasons?.join(', ') ?? 'unknown'})`,\n { check: result, pubkey: event.pubkey }\n );\n }\n\n cache.set(key, decision);\n } catch (err) {\n // Lookup failure — fail open or closed per policy.\n if (options.failOpen) {\n decision = accept('fail_open', { pubkey: event.pubkey });\n } else {\n decision = reject('lookup_error', `orangecheck: lookup failed, try again later`, {\n pubkey: event.pubkey,\n });\n }\n // Cache the error decision with a short TTL so a burst of traffic\n // while /api/check is down doesn't all dogpile the upstream.\n cache.set(key, decision, LOOKUP_ERROR_TTL_MS);\n console.warn(\n '[orangecheck/relay-filter] lookup failed:',\n err instanceof Error ? err.message : String(err)\n );\n }\n\n return finish(event, decision, options);\n}\n\nfunction finish(\n event: MinimalNostrEvent,\n decision: FilterDecision,\n options: FilterOptions\n): FilterDecision {\n options.onDecision?.(event, decision);\n return decision;\n}\n"]}
{"version":3,"sources":["../src/cache.ts","../src/filter.ts"],"names":["check"],"mappings":"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","file":"index.js","sourcesContent":["import type { FilterDecision } from './types';\n\ninterface Entry {\n value: FilterDecision;\n expires: number;\n}\n\nexport class TtlLru {\n private readonly store = new Map<string, Entry>();\n\n constructor(\n private readonly max: number,\n private readonly ttlMs: number\n ) {}\n\n get(key: string): FilterDecision | undefined {\n const entry = this.store.get(key);\n if (!entry) return undefined;\n if (Date.now() > entry.expires) {\n this.store.delete(key);\n return undefined;\n }\n // Touch on access so hot entries survive insertion pressure. Without\n // this, the old implementation was FIFO-with-TTL, not actually LRU —\n // a frequently-hit key could be evicted by N cold writes before it\n // ever moved in the map's insertion order.\n this.store.delete(key);\n this.store.set(key, entry);\n return entry.value;\n }\n\n /** Optional per-call TTL override. Used by the filter to cache\n * lookup-error decisions with a short TTL (circuit-breaker pattern). */\n set(key: string, value: FilterDecision, ttlMs?: number): void {\n if (this.store.size >= this.max) {\n const first = this.store.keys().next().value;\n if (first !== undefined) this.store.delete(first);\n }\n this.store.set(key, { value, expires: Date.now() + (ttlMs ?? this.ttlMs) });\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","import type { FilterDecision, FilterOptions, MinimalNostrEvent } from './types';\n\nimport { check } from '@orangecheck/sdk';\n\nimport { TtlLru } from './cache';\n\nconst DEFAULT_ALLOW_KINDS = [0, 3, 10002]; // profile meta, contacts, relay list\n\n// Short TTL for lookup-error decisions — acts as a circuit breaker so we\n// don't thundering-herd /api/check when upstream is flapping.\nconst LOOKUP_ERROR_TTL_MS = 5_000;\n\n/**\n * Process-wide cache. The old WeakMap<FilterOptions, …> design meant callers\n * who constructed a fresh options object per event (very common) never\n * produced a cache hit — every event was a cold lookup. Keying by a stable\n * config signature instead lets identical-config callers share.\n */\nconst caches = new Map<string, TtlLru>();\n\nfunction configSignature(opts: FilterOptions): string {\n return JSON.stringify([\n opts.minSats ?? 0,\n opts.minDays ?? 0,\n (opts.allowKinds ?? DEFAULT_ALLOW_KINDS).slice().sort(),\n (opts.allowPubkeys ?? []).slice().sort(),\n (opts.relays ?? []).slice().sort(),\n opts.cacheMax ?? 1_000,\n opts.cacheTtlMs ?? 60_000,\n ]);\n}\n\nfunction cacheFor(opts: FilterOptions): TtlLru {\n const sig = configSignature(opts);\n let c = caches.get(sig);\n if (!c) {\n c = new TtlLru(opts.cacheMax ?? 1_000, opts.cacheTtlMs ?? 60_000);\n caches.set(sig, c);\n }\n return c;\n}\n\n/**\n * Test hook — drop every cached decision. Not exported from the package\n * index, and never exercised in production code paths. Test suites call\n * this in `beforeEach` so per-config signatures don't bleed between cases.\n */\nexport function __clearFilterCachesForTests(): void {\n caches.clear();\n}\n\n/**\n * The Nostr public key an OrangeCheck attestation binds is the `nostr:npub…`\n * identity. Events on the wire carry the hex-encoded pubkey. We build the\n * identity lookup key using the hex form and let OrangeCheck's discovery\n * handle both formats.\n */\nfunction identityFor(pubkeyHex: string) {\n return { protocol: 'nostr', identifier: pubkeyHex } as const;\n}\n\nfunction reject(\n reason: FilterDecision['reason'],\n message: string,\n extras?: Partial<FilterDecision>\n): FilterDecision {\n return { action: 'reject', reason, message, ...extras };\n}\n\nfunction accept(\n reason: FilterDecision['reason'],\n extras?: Partial<FilterDecision>\n): FilterDecision {\n return { action: 'accept', reason, ...extras };\n}\n\n/**\n * Decide whether a relay should accept an event based on the OrangeCheck\n * status of the author's pubkey.\n *\n * Framework-agnostic. Use this from a custom relay, nostr-tools, or whatever.\n * For Strfry, see `@orangecheck/relay-filter/strfry`.\n */\nexport async function filterEvent(\n event: MinimalNostrEvent,\n options: FilterOptions\n): Promise<FilterDecision> {\n // Bypass: allowed kinds.\n const allowKinds = options.allowKinds ?? DEFAULT_ALLOW_KINDS;\n if (allowKinds.includes(event.kind)) {\n return finish(event, accept('allowed_kind'), options);\n }\n\n // Bypass: operator / admin pubkeys.\n if (options.allowPubkeys?.includes(event.pubkey)) {\n return finish(event, accept('allowed_pubkey', { pubkey: event.pubkey }), options);\n }\n\n // Check cache keyed by (pubkey, thresholds).\n const cache = cacheFor(options);\n const key = `${event.pubkey}:${options.minSats ?? 0}:${options.minDays ?? 0}`;\n const cached = cache.get(key);\n if (cached) {\n return finish(event, { ...cached, pubkey: event.pubkey }, options);\n }\n\n // Look up the pubkey's attestation via OrangeCheck.\n let decision: FilterDecision;\n try {\n const result = await check({\n identity: identityFor(event.pubkey),\n minSats: options.minSats,\n minDays: options.minDays,\n ...(options.relays ? { relays: options.relays } : {}),\n });\n\n if (result.ok) {\n decision = accept('ok', { check: result, pubkey: event.pubkey });\n } else if (result.reasons?.includes('not_found')) {\n decision = reject(\n 'no_attestation',\n `orangecheck: this relay requires a Bitcoin-stake proof. See https://ochk.io`,\n { check: result, pubkey: event.pubkey }\n );\n } else if (result.reasons?.some((r) => r === 'below_min_sats' || r === 'below_min_days')) {\n decision = reject(\n 'below_threshold',\n `orangecheck: proof below relay thresholds (min_sats=${options.minSats ?? 0}, min_days=${options.minDays ?? 0})`,\n { check: result, pubkey: event.pubkey }\n );\n } else {\n decision = reject(\n 'invalid_proof',\n `orangecheck: proof invalid (${result.reasons?.join(', ') ?? 'unknown'})`,\n { check: result, pubkey: event.pubkey }\n );\n }\n\n cache.set(key, decision);\n } catch (err) {\n // Lookup failure — fail open or closed per policy.\n if (options.failOpen) {\n decision = accept('fail_open', { pubkey: event.pubkey });\n } else {\n decision = reject('lookup_error', `orangecheck: lookup failed, try again later`, {\n pubkey: event.pubkey,\n });\n }\n // Cache the error decision with a short TTL so a burst of traffic\n // while /api/check is down doesn't all dogpile the upstream.\n cache.set(key, decision, LOOKUP_ERROR_TTL_MS);\n console.warn(\n '[orangecheck/relay-filter] lookup failed:',\n err instanceof Error ? err.message : String(err)\n );\n }\n\n return finish(event, decision, options);\n}\n\nfunction finish(\n event: MinimalNostrEvent,\n decision: FilterDecision,\n options: FilterOptions\n): FilterDecision {\n options.onDecision?.(event, decision);\n return decision;\n}\n"]}

@@ -1,1 +0,1 @@

{"version":3,"sources":["../src/cache.ts","../src/filter.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import type { FilterDecision } from './types';\n\ninterface Entry {\n value: FilterDecision;\n expires: number;\n}\n\nexport class TtlLru {\n private readonly store = new Map<string, Entry>();\n\n constructor(\n private readonly max: number,\n private readonly ttlMs: number\n ) {}\n\n get(key: string): FilterDecision | undefined {\n const entry = this.store.get(key);\n if (!entry) return undefined;\n if (Date.now() > entry.expires) {\n this.store.delete(key);\n return undefined;\n }\n // Touch on access so hot entries survive insertion pressure. Without\n // this, the old implementation was FIFO-with-TTL, not actually LRU —\n // a frequently-hit key could be evicted by N cold writes before it\n // ever moved in the map's insertion order.\n this.store.delete(key);\n this.store.set(key, entry);\n return entry.value;\n }\n\n /** Optional per-call TTL override. Used by the filter to cache\n * lookup-error decisions with a short TTL (circuit-breaker pattern). */\n set(key: string, value: FilterDecision, ttlMs?: number): void {\n if (this.store.size >= this.max) {\n const first = this.store.keys().next().value;\n if (first !== undefined) this.store.delete(first);\n }\n this.store.set(key, { value, expires: Date.now() + (ttlMs ?? this.ttlMs) });\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","import type { FilterDecision, FilterOptions, MinimalNostrEvent } from './types';\n\nimport { check } from '@orangecheck/sdk';\n\nimport { TtlLru } from './cache';\n\nconst DEFAULT_ALLOW_KINDS = [0, 3, 10002]; // profile meta, contacts, relay list\n\n// Short TTL for lookup-error decisions — acts as a circuit breaker so we\n// don't thundering-herd /api/check when upstream is flapping.\nconst LOOKUP_ERROR_TTL_MS = 5_000;\n\n/**\n * Process-wide cache. The old WeakMap<FilterOptions, …> design meant callers\n * who constructed a fresh options object per event (very common) never\n * produced a cache hit — every event was a cold lookup. Keying by a stable\n * config signature instead lets identical-config callers share.\n */\nconst caches = new Map<string, TtlLru>();\n\nfunction configSignature(opts: FilterOptions): string {\n return JSON.stringify([\n opts.minSats ?? 0,\n opts.minDays ?? 0,\n (opts.allowKinds ?? DEFAULT_ALLOW_KINDS).slice().sort(),\n (opts.allowPubkeys ?? []).slice().sort(),\n (opts.relays ?? []).slice().sort(),\n opts.cacheMax ?? 1_000,\n opts.cacheTtlMs ?? 60_000,\n ]);\n}\n\nfunction cacheFor(opts: FilterOptions): TtlLru {\n const sig = configSignature(opts);\n let c = caches.get(sig);\n if (!c) {\n c = new TtlLru(opts.cacheMax ?? 1_000, opts.cacheTtlMs ?? 60_000);\n caches.set(sig, c);\n }\n return c;\n}\n\n/**\n * The Nostr public key an OrangeCheck attestation binds is the `nostr:npub…`\n * identity. Events on the wire carry the hex-encoded pubkey. We build the\n * identity lookup key using the hex form and let OrangeCheck's discovery\n * handle both formats.\n */\nfunction identityFor(pubkeyHex: string) {\n return { protocol: 'nostr', identifier: pubkeyHex } as const;\n}\n\nfunction reject(\n reason: FilterDecision['reason'],\n message: string,\n extras?: Partial<FilterDecision>\n): FilterDecision {\n return { action: 'reject', reason, message, ...extras };\n}\n\nfunction accept(\n reason: FilterDecision['reason'],\n extras?: Partial<FilterDecision>\n): FilterDecision {\n return { action: 'accept', reason, ...extras };\n}\n\n/**\n * Decide whether a relay should accept an event based on the OrangeCheck\n * status of the author's pubkey.\n *\n * Framework-agnostic. Use this from a custom relay, nostr-tools, or whatever.\n * For Strfry, see `@orangecheck/relay-filter/strfry`.\n */\nexport async function filterEvent(\n event: MinimalNostrEvent,\n options: FilterOptions\n): Promise<FilterDecision> {\n // Bypass: allowed kinds.\n const allowKinds = options.allowKinds ?? DEFAULT_ALLOW_KINDS;\n if (allowKinds.includes(event.kind)) {\n return finish(event, accept('allowed_kind'), options);\n }\n\n // Bypass: operator / admin pubkeys.\n if (options.allowPubkeys?.includes(event.pubkey)) {\n return finish(event, accept('allowed_pubkey', { pubkey: event.pubkey }), options);\n }\n\n // Check cache keyed by (pubkey, thresholds).\n const cache = cacheFor(options);\n const key = `${event.pubkey}:${options.minSats ?? 0}:${options.minDays ?? 0}`;\n const cached = cache.get(key);\n if (cached) {\n return finish(event, { ...cached, pubkey: event.pubkey }, options);\n }\n\n // Look up the pubkey's attestation via OrangeCheck.\n let decision: FilterDecision;\n try {\n const result = await check({\n identity: identityFor(event.pubkey),\n minSats: options.minSats,\n minDays: options.minDays,\n ...(options.relays ? { relays: options.relays } : {}),\n });\n\n if (result.ok) {\n decision = accept('ok', { check: result, pubkey: event.pubkey });\n } else if (result.reasons?.includes('not_found')) {\n decision = reject(\n 'no_attestation',\n `orangecheck: this relay requires a Bitcoin-stake proof. See https://ochk.io`,\n { check: result, pubkey: event.pubkey }\n );\n } else if (result.reasons?.some((r) => r === 'below_min_sats' || r === 'below_min_days')) {\n decision = reject(\n 'below_threshold',\n `orangecheck: proof below relay thresholds (min_sats=${options.minSats ?? 0}, min_days=${options.minDays ?? 0})`,\n { check: result, pubkey: event.pubkey }\n );\n } else {\n decision = reject(\n 'invalid_proof',\n `orangecheck: proof invalid (${result.reasons?.join(', ') ?? 'unknown'})`,\n { check: result, pubkey: event.pubkey }\n );\n }\n\n cache.set(key, decision);\n } catch (err) {\n // Lookup failure — fail open or closed per policy.\n if (options.failOpen) {\n decision = accept('fail_open', { pubkey: event.pubkey });\n } else {\n decision = reject('lookup_error', `orangecheck: lookup failed, try again later`, {\n pubkey: event.pubkey,\n });\n }\n // Cache the error decision with a short TTL so a burst of traffic\n // while /api/check is down doesn't all dogpile the upstream.\n cache.set(key, decision, LOOKUP_ERROR_TTL_MS);\n console.warn(\n '[orangecheck/relay-filter] lookup failed:',\n err instanceof Error ? err.message : String(err)\n );\n }\n\n return finish(event, decision, options);\n}\n\nfunction finish(\n event: MinimalNostrEvent,\n decision: FilterDecision,\n options: FilterOptions\n): FilterDecision {\n options.onDecision?.(event, decision);\n return decision;\n}\n"]}
{"version":3,"sources":["../src/cache.ts","../src/filter.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import type { FilterDecision } from './types';\n\ninterface Entry {\n value: FilterDecision;\n expires: number;\n}\n\nexport class TtlLru {\n private readonly store = new Map<string, Entry>();\n\n constructor(\n private readonly max: number,\n private readonly ttlMs: number\n ) {}\n\n get(key: string): FilterDecision | undefined {\n const entry = this.store.get(key);\n if (!entry) return undefined;\n if (Date.now() > entry.expires) {\n this.store.delete(key);\n return undefined;\n }\n // Touch on access so hot entries survive insertion pressure. Without\n // this, the old implementation was FIFO-with-TTL, not actually LRU —\n // a frequently-hit key could be evicted by N cold writes before it\n // ever moved in the map's insertion order.\n this.store.delete(key);\n this.store.set(key, entry);\n return entry.value;\n }\n\n /** Optional per-call TTL override. Used by the filter to cache\n * lookup-error decisions with a short TTL (circuit-breaker pattern). */\n set(key: string, value: FilterDecision, ttlMs?: number): void {\n if (this.store.size >= this.max) {\n const first = this.store.keys().next().value;\n if (first !== undefined) this.store.delete(first);\n }\n this.store.set(key, { value, expires: Date.now() + (ttlMs ?? this.ttlMs) });\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","import type { FilterDecision, FilterOptions, MinimalNostrEvent } from './types';\n\nimport { check } from '@orangecheck/sdk';\n\nimport { TtlLru } from './cache';\n\nconst DEFAULT_ALLOW_KINDS = [0, 3, 10002]; // profile meta, contacts, relay list\n\n// Short TTL for lookup-error decisions — acts as a circuit breaker so we\n// don't thundering-herd /api/check when upstream is flapping.\nconst LOOKUP_ERROR_TTL_MS = 5_000;\n\n/**\n * Process-wide cache. The old WeakMap<FilterOptions, …> design meant callers\n * who constructed a fresh options object per event (very common) never\n * produced a cache hit — every event was a cold lookup. Keying by a stable\n * config signature instead lets identical-config callers share.\n */\nconst caches = new Map<string, TtlLru>();\n\nfunction configSignature(opts: FilterOptions): string {\n return JSON.stringify([\n opts.minSats ?? 0,\n opts.minDays ?? 0,\n (opts.allowKinds ?? DEFAULT_ALLOW_KINDS).slice().sort(),\n (opts.allowPubkeys ?? []).slice().sort(),\n (opts.relays ?? []).slice().sort(),\n opts.cacheMax ?? 1_000,\n opts.cacheTtlMs ?? 60_000,\n ]);\n}\n\nfunction cacheFor(opts: FilterOptions): TtlLru {\n const sig = configSignature(opts);\n let c = caches.get(sig);\n if (!c) {\n c = new TtlLru(opts.cacheMax ?? 1_000, opts.cacheTtlMs ?? 60_000);\n caches.set(sig, c);\n }\n return c;\n}\n\n/**\n * Test hook — drop every cached decision. Not exported from the package\n * index, and never exercised in production code paths. Test suites call\n * this in `beforeEach` so per-config signatures don't bleed between cases.\n */\nexport function __clearFilterCachesForTests(): void {\n caches.clear();\n}\n\n/**\n * The Nostr public key an OrangeCheck attestation binds is the `nostr:npub…`\n * identity. Events on the wire carry the hex-encoded pubkey. We build the\n * identity lookup key using the hex form and let OrangeCheck's discovery\n * handle both formats.\n */\nfunction identityFor(pubkeyHex: string) {\n return { protocol: 'nostr', identifier: pubkeyHex } as const;\n}\n\nfunction reject(\n reason: FilterDecision['reason'],\n message: string,\n extras?: Partial<FilterDecision>\n): FilterDecision {\n return { action: 'reject', reason, message, ...extras };\n}\n\nfunction accept(\n reason: FilterDecision['reason'],\n extras?: Partial<FilterDecision>\n): FilterDecision {\n return { action: 'accept', reason, ...extras };\n}\n\n/**\n * Decide whether a relay should accept an event based on the OrangeCheck\n * status of the author's pubkey.\n *\n * Framework-agnostic. Use this from a custom relay, nostr-tools, or whatever.\n * For Strfry, see `@orangecheck/relay-filter/strfry`.\n */\nexport async function filterEvent(\n event: MinimalNostrEvent,\n options: FilterOptions\n): Promise<FilterDecision> {\n // Bypass: allowed kinds.\n const allowKinds = options.allowKinds ?? DEFAULT_ALLOW_KINDS;\n if (allowKinds.includes(event.kind)) {\n return finish(event, accept('allowed_kind'), options);\n }\n\n // Bypass: operator / admin pubkeys.\n if (options.allowPubkeys?.includes(event.pubkey)) {\n return finish(event, accept('allowed_pubkey', { pubkey: event.pubkey }), options);\n }\n\n // Check cache keyed by (pubkey, thresholds).\n const cache = cacheFor(options);\n const key = `${event.pubkey}:${options.minSats ?? 0}:${options.minDays ?? 0}`;\n const cached = cache.get(key);\n if (cached) {\n return finish(event, { ...cached, pubkey: event.pubkey }, options);\n }\n\n // Look up the pubkey's attestation via OrangeCheck.\n let decision: FilterDecision;\n try {\n const result = await check({\n identity: identityFor(event.pubkey),\n minSats: options.minSats,\n minDays: options.minDays,\n ...(options.relays ? { relays: options.relays } : {}),\n });\n\n if (result.ok) {\n decision = accept('ok', { check: result, pubkey: event.pubkey });\n } else if (result.reasons?.includes('not_found')) {\n decision = reject(\n 'no_attestation',\n `orangecheck: this relay requires a Bitcoin-stake proof. See https://ochk.io`,\n { check: result, pubkey: event.pubkey }\n );\n } else if (result.reasons?.some((r) => r === 'below_min_sats' || r === 'below_min_days')) {\n decision = reject(\n 'below_threshold',\n `orangecheck: proof below relay thresholds (min_sats=${options.minSats ?? 0}, min_days=${options.minDays ?? 0})`,\n { check: result, pubkey: event.pubkey }\n );\n } else {\n decision = reject(\n 'invalid_proof',\n `orangecheck: proof invalid (${result.reasons?.join(', ') ?? 'unknown'})`,\n { check: result, pubkey: event.pubkey }\n );\n }\n\n cache.set(key, decision);\n } catch (err) {\n // Lookup failure — fail open or closed per policy.\n if (options.failOpen) {\n decision = accept('fail_open', { pubkey: event.pubkey });\n } else {\n decision = reject('lookup_error', `orangecheck: lookup failed, try again later`, {\n pubkey: event.pubkey,\n });\n }\n // Cache the error decision with a short TTL so a burst of traffic\n // while /api/check is down doesn't all dogpile the upstream.\n cache.set(key, decision, LOOKUP_ERROR_TTL_MS);\n console.warn(\n '[orangecheck/relay-filter] lookup failed:',\n err instanceof Error ? err.message : String(err)\n );\n }\n\n return finish(event, decision, options);\n}\n\nfunction finish(\n event: MinimalNostrEvent,\n decision: FilterDecision,\n options: FilterOptions\n): FilterDecision {\n options.onDecision?.(event, decision);\n return decision;\n}\n"]}

@@ -1,1 +0,1 @@

{"version":3,"sources":["../src/cache.ts","../src/filter.ts","../src/strfry.ts"],"names":["check","createInterface"],"mappings":"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","file":"strfry.js","sourcesContent":["import type { FilterDecision } from './types';\n\ninterface Entry {\n value: FilterDecision;\n expires: number;\n}\n\nexport class TtlLru {\n private readonly store = new Map<string, Entry>();\n\n constructor(\n private readonly max: number,\n private readonly ttlMs: number\n ) {}\n\n get(key: string): FilterDecision | undefined {\n const entry = this.store.get(key);\n if (!entry) return undefined;\n if (Date.now() > entry.expires) {\n this.store.delete(key);\n return undefined;\n }\n // Touch on access so hot entries survive insertion pressure. Without\n // this, the old implementation was FIFO-with-TTL, not actually LRU —\n // a frequently-hit key could be evicted by N cold writes before it\n // ever moved in the map's insertion order.\n this.store.delete(key);\n this.store.set(key, entry);\n return entry.value;\n }\n\n /** Optional per-call TTL override. Used by the filter to cache\n * lookup-error decisions with a short TTL (circuit-breaker pattern). */\n set(key: string, value: FilterDecision, ttlMs?: number): void {\n if (this.store.size >= this.max) {\n const first = this.store.keys().next().value;\n if (first !== undefined) this.store.delete(first);\n }\n this.store.set(key, { value, expires: Date.now() + (ttlMs ?? this.ttlMs) });\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","import type { FilterDecision, FilterOptions, MinimalNostrEvent } from './types';\n\nimport { check } from '@orangecheck/sdk';\n\nimport { TtlLru } from './cache';\n\nconst DEFAULT_ALLOW_KINDS = [0, 3, 10002]; // profile meta, contacts, relay list\n\n// Short TTL for lookup-error decisions — acts as a circuit breaker so we\n// don't thundering-herd /api/check when upstream is flapping.\nconst LOOKUP_ERROR_TTL_MS = 5_000;\n\n/**\n * Process-wide cache. The old WeakMap<FilterOptions, …> design meant callers\n * who constructed a fresh options object per event (very common) never\n * produced a cache hit — every event was a cold lookup. Keying by a stable\n * config signature instead lets identical-config callers share.\n */\nconst caches = new Map<string, TtlLru>();\n\nfunction configSignature(opts: FilterOptions): string {\n return JSON.stringify([\n opts.minSats ?? 0,\n opts.minDays ?? 0,\n (opts.allowKinds ?? DEFAULT_ALLOW_KINDS).slice().sort(),\n (opts.allowPubkeys ?? []).slice().sort(),\n (opts.relays ?? []).slice().sort(),\n opts.cacheMax ?? 1_000,\n opts.cacheTtlMs ?? 60_000,\n ]);\n}\n\nfunction cacheFor(opts: FilterOptions): TtlLru {\n const sig = configSignature(opts);\n let c = caches.get(sig);\n if (!c) {\n c = new TtlLru(opts.cacheMax ?? 1_000, opts.cacheTtlMs ?? 60_000);\n caches.set(sig, c);\n }\n return c;\n}\n\n/**\n * The Nostr public key an OrangeCheck attestation binds is the `nostr:npub…`\n * identity. Events on the wire carry the hex-encoded pubkey. We build the\n * identity lookup key using the hex form and let OrangeCheck's discovery\n * handle both formats.\n */\nfunction identityFor(pubkeyHex: string) {\n return { protocol: 'nostr', identifier: pubkeyHex } as const;\n}\n\nfunction reject(\n reason: FilterDecision['reason'],\n message: string,\n extras?: Partial<FilterDecision>\n): FilterDecision {\n return { action: 'reject', reason, message, ...extras };\n}\n\nfunction accept(\n reason: FilterDecision['reason'],\n extras?: Partial<FilterDecision>\n): FilterDecision {\n return { action: 'accept', reason, ...extras };\n}\n\n/**\n * Decide whether a relay should accept an event based on the OrangeCheck\n * status of the author's pubkey.\n *\n * Framework-agnostic. Use this from a custom relay, nostr-tools, or whatever.\n * For Strfry, see `@orangecheck/relay-filter/strfry`.\n */\nexport async function filterEvent(\n event: MinimalNostrEvent,\n options: FilterOptions\n): Promise<FilterDecision> {\n // Bypass: allowed kinds.\n const allowKinds = options.allowKinds ?? DEFAULT_ALLOW_KINDS;\n if (allowKinds.includes(event.kind)) {\n return finish(event, accept('allowed_kind'), options);\n }\n\n // Bypass: operator / admin pubkeys.\n if (options.allowPubkeys?.includes(event.pubkey)) {\n return finish(event, accept('allowed_pubkey', { pubkey: event.pubkey }), options);\n }\n\n // Check cache keyed by (pubkey, thresholds).\n const cache = cacheFor(options);\n const key = `${event.pubkey}:${options.minSats ?? 0}:${options.minDays ?? 0}`;\n const cached = cache.get(key);\n if (cached) {\n return finish(event, { ...cached, pubkey: event.pubkey }, options);\n }\n\n // Look up the pubkey's attestation via OrangeCheck.\n let decision: FilterDecision;\n try {\n const result = await check({\n identity: identityFor(event.pubkey),\n minSats: options.minSats,\n minDays: options.minDays,\n ...(options.relays ? { relays: options.relays } : {}),\n });\n\n if (result.ok) {\n decision = accept('ok', { check: result, pubkey: event.pubkey });\n } else if (result.reasons?.includes('not_found')) {\n decision = reject(\n 'no_attestation',\n `orangecheck: this relay requires a Bitcoin-stake proof. See https://ochk.io`,\n { check: result, pubkey: event.pubkey }\n );\n } else if (result.reasons?.some((r) => r === 'below_min_sats' || r === 'below_min_days')) {\n decision = reject(\n 'below_threshold',\n `orangecheck: proof below relay thresholds (min_sats=${options.minSats ?? 0}, min_days=${options.minDays ?? 0})`,\n { check: result, pubkey: event.pubkey }\n );\n } else {\n decision = reject(\n 'invalid_proof',\n `orangecheck: proof invalid (${result.reasons?.join(', ') ?? 'unknown'})`,\n { check: result, pubkey: event.pubkey }\n );\n }\n\n cache.set(key, decision);\n } catch (err) {\n // Lookup failure — fail open or closed per policy.\n if (options.failOpen) {\n decision = accept('fail_open', { pubkey: event.pubkey });\n } else {\n decision = reject('lookup_error', `orangecheck: lookup failed, try again later`, {\n pubkey: event.pubkey,\n });\n }\n // Cache the error decision with a short TTL so a burst of traffic\n // while /api/check is down doesn't all dogpile the upstream.\n cache.set(key, decision, LOOKUP_ERROR_TTL_MS);\n console.warn(\n '[orangecheck/relay-filter] lookup failed:',\n err instanceof Error ? err.message : String(err)\n );\n }\n\n return finish(event, decision, options);\n}\n\nfunction finish(\n event: MinimalNostrEvent,\n decision: FilterDecision,\n options: FilterOptions\n): FilterDecision {\n options.onDecision?.(event, decision);\n return decision;\n}\n","/**\n * Strfry policy plugin for OrangeCheck.\n *\n * tsup adds `#!/usr/bin/env node` automatically because this file is listed\n * under `bin` in package.json.\n *\n * Configure Strfry with the path to this binary and it will filter EVENT\n * submissions against OrangeCheck thresholds.\n *\n * Strfry's policy plugin protocol:\n * - read JSON lines from stdin; one line per inbound event\n * - each line has shape: { type: \"new\", event: {...}, ... }\n * - emit one JSON line per decision:\n * { id: \"<event_id>\", action: \"accept\"|\"reject\"|\"shadowReject\", msg?: \"...\" }\n *\n * Runtime config — set via environment variables (env is the cleanest way\n * to pass policy into a plugin Strfry spawns on your behalf):\n *\n * OC_MIN_SATS — minimum sats bonded (default: 0)\n * OC_MIN_DAYS — minimum days unspent (default: 0)\n * OC_ALLOW_KINDS — comma-separated kinds to bypass (default: \"0,3,10002\")\n * OC_ALLOW_PUBKEYS — comma-separated hex pubkeys to bypass (default: none)\n * OC_FAIL_OPEN — \"true\" to allow events through on lookup failure\n * OC_RELAYS — comma-separated Nostr relay URLs (default: SDK defaults)\n * OC_CACHE_TTL_MS — cache TTL in ms (default: 60000)\n *\n * Usage with Strfry:\n *\n * // strfry.conf\n * writePolicy = {\n * plugin = \"/usr/local/bin/oc-strfry\"\n * }\n *\n * Or via npx during development:\n *\n * writePolicy = { plugin = \"npx -y @orangecheck/relay-filter\" }\n *\n * (Strfry docs: https://github.com/hoytech/strfry)\n */\n\nimport type { FilterOptions, MinimalNostrEvent } from './types';\n\nimport { createInterface } from 'node:readline';\n\nimport { filterEvent } from './filter';\n\nfunction parseList(raw: string | undefined): string[] | undefined {\n if (!raw) return undefined;\n return raw\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\nfunction parseNumList(raw: string | undefined): number[] | undefined {\n const list = parseList(raw);\n if (!list) return undefined;\n return list.map((s) => Number(s)).filter((n) => Number.isFinite(n));\n}\n\nfunction optionsFromEnv(): FilterOptions {\n const env = process.env;\n return {\n minSats: env.OC_MIN_SATS ? Number(env.OC_MIN_SATS) : 0,\n minDays: env.OC_MIN_DAYS ? Number(env.OC_MIN_DAYS) : 0,\n allowKinds: parseNumList(env.OC_ALLOW_KINDS) ?? [0, 3, 10002],\n allowPubkeys: parseList(env.OC_ALLOW_PUBKEYS),\n relays: parseList(env.OC_RELAYS),\n failOpen: env.OC_FAIL_OPEN === 'true',\n cacheTtlMs: env.OC_CACHE_TTL_MS ? Number(env.OC_CACHE_TTL_MS) : 60_000,\n onDecision: (event, decision) => {\n if (env.OC_LOG !== 'false') {\n process.stderr.write(\n `[oc-strfry] ${decision.action} ${event.kind} ${event.pubkey.slice(0, 12)}… (${decision.reason})\\n`\n );\n }\n },\n };\n}\n\ninterface StrfryInput {\n type: 'new' | 'lookback';\n event?: MinimalNostrEvent;\n receivedAt?: number;\n sourceType?: string;\n sourceInfo?: string;\n}\n\nconst HEX_64_RE = /^[0-9a-f]{64}$/;\n\nasync function handleLine(line: string, options: FilterOptions): Promise<string | null> {\n let input: StrfryInput;\n try {\n input = JSON.parse(line);\n } catch {\n return null;\n }\n\n // Lookback events are already stored; skip entirely so we don't emit a\n // malformed echo (Strfry expects the id to match the input event).\n if (input.type !== 'new') {\n return null;\n }\n if (!input.event || typeof input.event !== 'object') {\n return null;\n }\n\n // Validate event shape before it reaches the filter — otherwise a\n // malformed `pubkey` becomes a cache-poisoning vector (a bogus key like\n // `undefined` shares a cache entry with every future malformed event).\n const ev = input.event;\n if (\n typeof ev.id !== 'string' ||\n typeof ev.pubkey !== 'string' ||\n typeof ev.kind !== 'number' ||\n !HEX_64_RE.test(ev.id) ||\n !HEX_64_RE.test(ev.pubkey)\n ) {\n return JSON.stringify({\n id: typeof ev.id === 'string' ? ev.id : '',\n action: 'reject',\n msg: 'orangecheck: malformed event shape',\n });\n }\n\n const decision = await filterEvent(ev, options);\n return JSON.stringify({\n id: ev.id,\n action: decision.action,\n ...(decision.message ? { msg: decision.message } : {}),\n });\n}\n\nasync function main(): Promise<void> {\n const options = optionsFromEnv();\n const rl = createInterface({ input: process.stdin, terminal: false });\n\n for await (const line of rl) {\n if (!line.trim()) continue;\n // Per-line isolation: a single throw here used to kill the whole\n // plugin (and then Strfry's default fallback determined whether the\n // relay accepted or rejected every subsequent event). Catch and emit\n // an explicit reject instead.\n try {\n const out = await handleLine(line, options);\n if (out) process.stdout.write(out + '\\n');\n } catch (err) {\n let id = '';\n try {\n const parsed = JSON.parse(line);\n if (parsed?.event?.id && typeof parsed.event.id === 'string') {\n id = parsed.event.id.slice(0, 64);\n }\n } catch {\n // leave id empty\n }\n process.stderr.write(\n `[oc-strfry] handleLine threw: ${err instanceof Error ? err.message : String(err)}\\n`\n );\n process.stdout.write(\n JSON.stringify({\n id,\n action: 'reject',\n msg: 'orangecheck: filter error',\n }) + '\\n'\n );\n }\n }\n}\n\nmain().catch((err) => {\n process.stderr.write(`[oc-strfry] fatal: ${err instanceof Error ? err.message : err}\\n`);\n process.exit(1);\n});\n\nexport { filterEvent } from './filter';\nexport type { FilterDecision, FilterOptions, MinimalNostrEvent } from './types';\n"]}
{"version":3,"sources":["../src/cache.ts","../src/filter.ts","../src/strfry.ts"],"names":["check","createInterface"],"mappings":"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","file":"strfry.js","sourcesContent":["import type { FilterDecision } from './types';\n\ninterface Entry {\n value: FilterDecision;\n expires: number;\n}\n\nexport class TtlLru {\n private readonly store = new Map<string, Entry>();\n\n constructor(\n private readonly max: number,\n private readonly ttlMs: number\n ) {}\n\n get(key: string): FilterDecision | undefined {\n const entry = this.store.get(key);\n if (!entry) return undefined;\n if (Date.now() > entry.expires) {\n this.store.delete(key);\n return undefined;\n }\n // Touch on access so hot entries survive insertion pressure. Without\n // this, the old implementation was FIFO-with-TTL, not actually LRU —\n // a frequently-hit key could be evicted by N cold writes before it\n // ever moved in the map's insertion order.\n this.store.delete(key);\n this.store.set(key, entry);\n return entry.value;\n }\n\n /** Optional per-call TTL override. Used by the filter to cache\n * lookup-error decisions with a short TTL (circuit-breaker pattern). */\n set(key: string, value: FilterDecision, ttlMs?: number): void {\n if (this.store.size >= this.max) {\n const first = this.store.keys().next().value;\n if (first !== undefined) this.store.delete(first);\n }\n this.store.set(key, { value, expires: Date.now() + (ttlMs ?? this.ttlMs) });\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","import type { FilterDecision, FilterOptions, MinimalNostrEvent } from './types';\n\nimport { check } from '@orangecheck/sdk';\n\nimport { TtlLru } from './cache';\n\nconst DEFAULT_ALLOW_KINDS = [0, 3, 10002]; // profile meta, contacts, relay list\n\n// Short TTL for lookup-error decisions — acts as a circuit breaker so we\n// don't thundering-herd /api/check when upstream is flapping.\nconst LOOKUP_ERROR_TTL_MS = 5_000;\n\n/**\n * Process-wide cache. The old WeakMap<FilterOptions, …> design meant callers\n * who constructed a fresh options object per event (very common) never\n * produced a cache hit — every event was a cold lookup. Keying by a stable\n * config signature instead lets identical-config callers share.\n */\nconst caches = new Map<string, TtlLru>();\n\nfunction configSignature(opts: FilterOptions): string {\n return JSON.stringify([\n opts.minSats ?? 0,\n opts.minDays ?? 0,\n (opts.allowKinds ?? DEFAULT_ALLOW_KINDS).slice().sort(),\n (opts.allowPubkeys ?? []).slice().sort(),\n (opts.relays ?? []).slice().sort(),\n opts.cacheMax ?? 1_000,\n opts.cacheTtlMs ?? 60_000,\n ]);\n}\n\nfunction cacheFor(opts: FilterOptions): TtlLru {\n const sig = configSignature(opts);\n let c = caches.get(sig);\n if (!c) {\n c = new TtlLru(opts.cacheMax ?? 1_000, opts.cacheTtlMs ?? 60_000);\n caches.set(sig, c);\n }\n return c;\n}\n\n/**\n * Test hook — drop every cached decision. Not exported from the package\n * index, and never exercised in production code paths. Test suites call\n * this in `beforeEach` so per-config signatures don't bleed between cases.\n */\nexport function __clearFilterCachesForTests(): void {\n caches.clear();\n}\n\n/**\n * The Nostr public key an OrangeCheck attestation binds is the `nostr:npub…`\n * identity. Events on the wire carry the hex-encoded pubkey. We build the\n * identity lookup key using the hex form and let OrangeCheck's discovery\n * handle both formats.\n */\nfunction identityFor(pubkeyHex: string) {\n return { protocol: 'nostr', identifier: pubkeyHex } as const;\n}\n\nfunction reject(\n reason: FilterDecision['reason'],\n message: string,\n extras?: Partial<FilterDecision>\n): FilterDecision {\n return { action: 'reject', reason, message, ...extras };\n}\n\nfunction accept(\n reason: FilterDecision['reason'],\n extras?: Partial<FilterDecision>\n): FilterDecision {\n return { action: 'accept', reason, ...extras };\n}\n\n/**\n * Decide whether a relay should accept an event based on the OrangeCheck\n * status of the author's pubkey.\n *\n * Framework-agnostic. Use this from a custom relay, nostr-tools, or whatever.\n * For Strfry, see `@orangecheck/relay-filter/strfry`.\n */\nexport async function filterEvent(\n event: MinimalNostrEvent,\n options: FilterOptions\n): Promise<FilterDecision> {\n // Bypass: allowed kinds.\n const allowKinds = options.allowKinds ?? DEFAULT_ALLOW_KINDS;\n if (allowKinds.includes(event.kind)) {\n return finish(event, accept('allowed_kind'), options);\n }\n\n // Bypass: operator / admin pubkeys.\n if (options.allowPubkeys?.includes(event.pubkey)) {\n return finish(event, accept('allowed_pubkey', { pubkey: event.pubkey }), options);\n }\n\n // Check cache keyed by (pubkey, thresholds).\n const cache = cacheFor(options);\n const key = `${event.pubkey}:${options.minSats ?? 0}:${options.minDays ?? 0}`;\n const cached = cache.get(key);\n if (cached) {\n return finish(event, { ...cached, pubkey: event.pubkey }, options);\n }\n\n // Look up the pubkey's attestation via OrangeCheck.\n let decision: FilterDecision;\n try {\n const result = await check({\n identity: identityFor(event.pubkey),\n minSats: options.minSats,\n minDays: options.minDays,\n ...(options.relays ? { relays: options.relays } : {}),\n });\n\n if (result.ok) {\n decision = accept('ok', { check: result, pubkey: event.pubkey });\n } else if (result.reasons?.includes('not_found')) {\n decision = reject(\n 'no_attestation',\n `orangecheck: this relay requires a Bitcoin-stake proof. See https://ochk.io`,\n { check: result, pubkey: event.pubkey }\n );\n } else if (result.reasons?.some((r) => r === 'below_min_sats' || r === 'below_min_days')) {\n decision = reject(\n 'below_threshold',\n `orangecheck: proof below relay thresholds (min_sats=${options.minSats ?? 0}, min_days=${options.minDays ?? 0})`,\n { check: result, pubkey: event.pubkey }\n );\n } else {\n decision = reject(\n 'invalid_proof',\n `orangecheck: proof invalid (${result.reasons?.join(', ') ?? 'unknown'})`,\n { check: result, pubkey: event.pubkey }\n );\n }\n\n cache.set(key, decision);\n } catch (err) {\n // Lookup failure — fail open or closed per policy.\n if (options.failOpen) {\n decision = accept('fail_open', { pubkey: event.pubkey });\n } else {\n decision = reject('lookup_error', `orangecheck: lookup failed, try again later`, {\n pubkey: event.pubkey,\n });\n }\n // Cache the error decision with a short TTL so a burst of traffic\n // while /api/check is down doesn't all dogpile the upstream.\n cache.set(key, decision, LOOKUP_ERROR_TTL_MS);\n console.warn(\n '[orangecheck/relay-filter] lookup failed:',\n err instanceof Error ? err.message : String(err)\n );\n }\n\n return finish(event, decision, options);\n}\n\nfunction finish(\n event: MinimalNostrEvent,\n decision: FilterDecision,\n options: FilterOptions\n): FilterDecision {\n options.onDecision?.(event, decision);\n return decision;\n}\n","/**\n * Strfry policy plugin for OrangeCheck.\n *\n * tsup adds `#!/usr/bin/env node` automatically because this file is listed\n * under `bin` in package.json.\n *\n * Configure Strfry with the path to this binary and it will filter EVENT\n * submissions against OrangeCheck thresholds.\n *\n * Strfry's policy plugin protocol:\n * - read JSON lines from stdin; one line per inbound event\n * - each line has shape: { type: \"new\", event: {...}, ... }\n * - emit one JSON line per decision:\n * { id: \"<event_id>\", action: \"accept\"|\"reject\"|\"shadowReject\", msg?: \"...\" }\n *\n * Runtime config — set via environment variables (env is the cleanest way\n * to pass policy into a plugin Strfry spawns on your behalf):\n *\n * OC_MIN_SATS — minimum sats bonded (default: 0)\n * OC_MIN_DAYS — minimum days unspent (default: 0)\n * OC_ALLOW_KINDS — comma-separated kinds to bypass (default: \"0,3,10002\")\n * OC_ALLOW_PUBKEYS — comma-separated hex pubkeys to bypass (default: none)\n * OC_FAIL_OPEN — \"true\" to allow events through on lookup failure\n * OC_RELAYS — comma-separated Nostr relay URLs (default: SDK defaults)\n * OC_CACHE_TTL_MS — cache TTL in ms (default: 60000)\n *\n * Usage with Strfry:\n *\n * // strfry.conf\n * writePolicy = {\n * plugin = \"/usr/local/bin/oc-strfry\"\n * }\n *\n * Or via npx during development:\n *\n * writePolicy = { plugin = \"npx -y @orangecheck/relay-filter\" }\n *\n * (Strfry docs: https://github.com/hoytech/strfry)\n */\n\nimport type { FilterOptions, MinimalNostrEvent } from './types';\n\nimport { createInterface } from 'node:readline';\n\nimport { filterEvent } from './filter';\n\nfunction parseList(raw: string | undefined): string[] | undefined {\n if (!raw) return undefined;\n return raw\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\nfunction parseNumList(raw: string | undefined): number[] | undefined {\n const list = parseList(raw);\n if (!list) return undefined;\n return list.map((s) => Number(s)).filter((n) => Number.isFinite(n));\n}\n\nfunction optionsFromEnv(): FilterOptions {\n const env = process.env;\n return {\n minSats: env.OC_MIN_SATS ? Number(env.OC_MIN_SATS) : 0,\n minDays: env.OC_MIN_DAYS ? Number(env.OC_MIN_DAYS) : 0,\n allowKinds: parseNumList(env.OC_ALLOW_KINDS) ?? [0, 3, 10002],\n allowPubkeys: parseList(env.OC_ALLOW_PUBKEYS),\n relays: parseList(env.OC_RELAYS),\n failOpen: env.OC_FAIL_OPEN === 'true',\n cacheTtlMs: env.OC_CACHE_TTL_MS ? Number(env.OC_CACHE_TTL_MS) : 60_000,\n onDecision: (event, decision) => {\n if (env.OC_LOG !== 'false') {\n process.stderr.write(\n `[oc-strfry] ${decision.action} ${event.kind} ${event.pubkey.slice(0, 12)}… (${decision.reason})\\n`\n );\n }\n },\n };\n}\n\ninterface StrfryInput {\n type: 'new' | 'lookback';\n event?: MinimalNostrEvent;\n receivedAt?: number;\n sourceType?: string;\n sourceInfo?: string;\n}\n\nconst HEX_64_RE = /^[0-9a-f]{64}$/;\n\nasync function handleLine(line: string, options: FilterOptions): Promise<string | null> {\n let input: StrfryInput;\n try {\n input = JSON.parse(line);\n } catch {\n return null;\n }\n\n // Lookback events are already stored; skip entirely so we don't emit a\n // malformed echo (Strfry expects the id to match the input event).\n if (input.type !== 'new') {\n return null;\n }\n if (!input.event || typeof input.event !== 'object') {\n return null;\n }\n\n // Validate event shape before it reaches the filter — otherwise a\n // malformed `pubkey` becomes a cache-poisoning vector (a bogus key like\n // `undefined` shares a cache entry with every future malformed event).\n const ev = input.event;\n if (\n typeof ev.id !== 'string' ||\n typeof ev.pubkey !== 'string' ||\n typeof ev.kind !== 'number' ||\n !HEX_64_RE.test(ev.id) ||\n !HEX_64_RE.test(ev.pubkey)\n ) {\n return JSON.stringify({\n id: typeof ev.id === 'string' ? ev.id : '',\n action: 'reject',\n msg: 'orangecheck: malformed event shape',\n });\n }\n\n const decision = await filterEvent(ev, options);\n return JSON.stringify({\n id: ev.id,\n action: decision.action,\n ...(decision.message ? { msg: decision.message } : {}),\n });\n}\n\nasync function main(): Promise<void> {\n const options = optionsFromEnv();\n const rl = createInterface({ input: process.stdin, terminal: false });\n\n for await (const line of rl) {\n if (!line.trim()) continue;\n // Per-line isolation: a single throw here used to kill the whole\n // plugin (and then Strfry's default fallback determined whether the\n // relay accepted or rejected every subsequent event). Catch and emit\n // an explicit reject instead.\n try {\n const out = await handleLine(line, options);\n if (out) process.stdout.write(out + '\\n');\n } catch (err) {\n let id = '';\n try {\n const parsed = JSON.parse(line);\n if (parsed?.event?.id && typeof parsed.event.id === 'string') {\n id = parsed.event.id.slice(0, 64);\n }\n } catch {\n // leave id empty\n }\n process.stderr.write(\n `[oc-strfry] handleLine threw: ${err instanceof Error ? err.message : String(err)}\\n`\n );\n process.stdout.write(\n JSON.stringify({\n id,\n action: 'reject',\n msg: 'orangecheck: filter error',\n }) + '\\n'\n );\n }\n }\n}\n\nmain().catch((err) => {\n process.stderr.write(`[oc-strfry] fatal: ${err instanceof Error ? err.message : err}\\n`);\n process.exit(1);\n});\n\nexport { filterEvent } from './filter';\nexport type { FilterDecision, FilterOptions, MinimalNostrEvent } from './types';\n"]}

@@ -1,1 +0,1 @@

{"version":3,"sources":["../src/cache.ts","../src/filter.ts","../src/strfry.ts"],"names":[],"mappings":"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","file":"strfry.mjs","sourcesContent":["import type { FilterDecision } from './types';\n\ninterface Entry {\n value: FilterDecision;\n expires: number;\n}\n\nexport class TtlLru {\n private readonly store = new Map<string, Entry>();\n\n constructor(\n private readonly max: number,\n private readonly ttlMs: number\n ) {}\n\n get(key: string): FilterDecision | undefined {\n const entry = this.store.get(key);\n if (!entry) return undefined;\n if (Date.now() > entry.expires) {\n this.store.delete(key);\n return undefined;\n }\n // Touch on access so hot entries survive insertion pressure. Without\n // this, the old implementation was FIFO-with-TTL, not actually LRU —\n // a frequently-hit key could be evicted by N cold writes before it\n // ever moved in the map's insertion order.\n this.store.delete(key);\n this.store.set(key, entry);\n return entry.value;\n }\n\n /** Optional per-call TTL override. Used by the filter to cache\n * lookup-error decisions with a short TTL (circuit-breaker pattern). */\n set(key: string, value: FilterDecision, ttlMs?: number): void {\n if (this.store.size >= this.max) {\n const first = this.store.keys().next().value;\n if (first !== undefined) this.store.delete(first);\n }\n this.store.set(key, { value, expires: Date.now() + (ttlMs ?? this.ttlMs) });\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","import type { FilterDecision, FilterOptions, MinimalNostrEvent } from './types';\n\nimport { check } from '@orangecheck/sdk';\n\nimport { TtlLru } from './cache';\n\nconst DEFAULT_ALLOW_KINDS = [0, 3, 10002]; // profile meta, contacts, relay list\n\n// Short TTL for lookup-error decisions — acts as a circuit breaker so we\n// don't thundering-herd /api/check when upstream is flapping.\nconst LOOKUP_ERROR_TTL_MS = 5_000;\n\n/**\n * Process-wide cache. The old WeakMap<FilterOptions, …> design meant callers\n * who constructed a fresh options object per event (very common) never\n * produced a cache hit — every event was a cold lookup. Keying by a stable\n * config signature instead lets identical-config callers share.\n */\nconst caches = new Map<string, TtlLru>();\n\nfunction configSignature(opts: FilterOptions): string {\n return JSON.stringify([\n opts.minSats ?? 0,\n opts.minDays ?? 0,\n (opts.allowKinds ?? DEFAULT_ALLOW_KINDS).slice().sort(),\n (opts.allowPubkeys ?? []).slice().sort(),\n (opts.relays ?? []).slice().sort(),\n opts.cacheMax ?? 1_000,\n opts.cacheTtlMs ?? 60_000,\n ]);\n}\n\nfunction cacheFor(opts: FilterOptions): TtlLru {\n const sig = configSignature(opts);\n let c = caches.get(sig);\n if (!c) {\n c = new TtlLru(opts.cacheMax ?? 1_000, opts.cacheTtlMs ?? 60_000);\n caches.set(sig, c);\n }\n return c;\n}\n\n/**\n * The Nostr public key an OrangeCheck attestation binds is the `nostr:npub…`\n * identity. Events on the wire carry the hex-encoded pubkey. We build the\n * identity lookup key using the hex form and let OrangeCheck's discovery\n * handle both formats.\n */\nfunction identityFor(pubkeyHex: string) {\n return { protocol: 'nostr', identifier: pubkeyHex } as const;\n}\n\nfunction reject(\n reason: FilterDecision['reason'],\n message: string,\n extras?: Partial<FilterDecision>\n): FilterDecision {\n return { action: 'reject', reason, message, ...extras };\n}\n\nfunction accept(\n reason: FilterDecision['reason'],\n extras?: Partial<FilterDecision>\n): FilterDecision {\n return { action: 'accept', reason, ...extras };\n}\n\n/**\n * Decide whether a relay should accept an event based on the OrangeCheck\n * status of the author's pubkey.\n *\n * Framework-agnostic. Use this from a custom relay, nostr-tools, or whatever.\n * For Strfry, see `@orangecheck/relay-filter/strfry`.\n */\nexport async function filterEvent(\n event: MinimalNostrEvent,\n options: FilterOptions\n): Promise<FilterDecision> {\n // Bypass: allowed kinds.\n const allowKinds = options.allowKinds ?? DEFAULT_ALLOW_KINDS;\n if (allowKinds.includes(event.kind)) {\n return finish(event, accept('allowed_kind'), options);\n }\n\n // Bypass: operator / admin pubkeys.\n if (options.allowPubkeys?.includes(event.pubkey)) {\n return finish(event, accept('allowed_pubkey', { pubkey: event.pubkey }), options);\n }\n\n // Check cache keyed by (pubkey, thresholds).\n const cache = cacheFor(options);\n const key = `${event.pubkey}:${options.minSats ?? 0}:${options.minDays ?? 0}`;\n const cached = cache.get(key);\n if (cached) {\n return finish(event, { ...cached, pubkey: event.pubkey }, options);\n }\n\n // Look up the pubkey's attestation via OrangeCheck.\n let decision: FilterDecision;\n try {\n const result = await check({\n identity: identityFor(event.pubkey),\n minSats: options.minSats,\n minDays: options.minDays,\n ...(options.relays ? { relays: options.relays } : {}),\n });\n\n if (result.ok) {\n decision = accept('ok', { check: result, pubkey: event.pubkey });\n } else if (result.reasons?.includes('not_found')) {\n decision = reject(\n 'no_attestation',\n `orangecheck: this relay requires a Bitcoin-stake proof. See https://ochk.io`,\n { check: result, pubkey: event.pubkey }\n );\n } else if (result.reasons?.some((r) => r === 'below_min_sats' || r === 'below_min_days')) {\n decision = reject(\n 'below_threshold',\n `orangecheck: proof below relay thresholds (min_sats=${options.minSats ?? 0}, min_days=${options.minDays ?? 0})`,\n { check: result, pubkey: event.pubkey }\n );\n } else {\n decision = reject(\n 'invalid_proof',\n `orangecheck: proof invalid (${result.reasons?.join(', ') ?? 'unknown'})`,\n { check: result, pubkey: event.pubkey }\n );\n }\n\n cache.set(key, decision);\n } catch (err) {\n // Lookup failure — fail open or closed per policy.\n if (options.failOpen) {\n decision = accept('fail_open', { pubkey: event.pubkey });\n } else {\n decision = reject('lookup_error', `orangecheck: lookup failed, try again later`, {\n pubkey: event.pubkey,\n });\n }\n // Cache the error decision with a short TTL so a burst of traffic\n // while /api/check is down doesn't all dogpile the upstream.\n cache.set(key, decision, LOOKUP_ERROR_TTL_MS);\n console.warn(\n '[orangecheck/relay-filter] lookup failed:',\n err instanceof Error ? err.message : String(err)\n );\n }\n\n return finish(event, decision, options);\n}\n\nfunction finish(\n event: MinimalNostrEvent,\n decision: FilterDecision,\n options: FilterOptions\n): FilterDecision {\n options.onDecision?.(event, decision);\n return decision;\n}\n","/**\n * Strfry policy plugin for OrangeCheck.\n *\n * tsup adds `#!/usr/bin/env node` automatically because this file is listed\n * under `bin` in package.json.\n *\n * Configure Strfry with the path to this binary and it will filter EVENT\n * submissions against OrangeCheck thresholds.\n *\n * Strfry's policy plugin protocol:\n * - read JSON lines from stdin; one line per inbound event\n * - each line has shape: { type: \"new\", event: {...}, ... }\n * - emit one JSON line per decision:\n * { id: \"<event_id>\", action: \"accept\"|\"reject\"|\"shadowReject\", msg?: \"...\" }\n *\n * Runtime config — set via environment variables (env is the cleanest way\n * to pass policy into a plugin Strfry spawns on your behalf):\n *\n * OC_MIN_SATS — minimum sats bonded (default: 0)\n * OC_MIN_DAYS — minimum days unspent (default: 0)\n * OC_ALLOW_KINDS — comma-separated kinds to bypass (default: \"0,3,10002\")\n * OC_ALLOW_PUBKEYS — comma-separated hex pubkeys to bypass (default: none)\n * OC_FAIL_OPEN — \"true\" to allow events through on lookup failure\n * OC_RELAYS — comma-separated Nostr relay URLs (default: SDK defaults)\n * OC_CACHE_TTL_MS — cache TTL in ms (default: 60000)\n *\n * Usage with Strfry:\n *\n * // strfry.conf\n * writePolicy = {\n * plugin = \"/usr/local/bin/oc-strfry\"\n * }\n *\n * Or via npx during development:\n *\n * writePolicy = { plugin = \"npx -y @orangecheck/relay-filter\" }\n *\n * (Strfry docs: https://github.com/hoytech/strfry)\n */\n\nimport type { FilterOptions, MinimalNostrEvent } from './types';\n\nimport { createInterface } from 'node:readline';\n\nimport { filterEvent } from './filter';\n\nfunction parseList(raw: string | undefined): string[] | undefined {\n if (!raw) return undefined;\n return raw\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\nfunction parseNumList(raw: string | undefined): number[] | undefined {\n const list = parseList(raw);\n if (!list) return undefined;\n return list.map((s) => Number(s)).filter((n) => Number.isFinite(n));\n}\n\nfunction optionsFromEnv(): FilterOptions {\n const env = process.env;\n return {\n minSats: env.OC_MIN_SATS ? Number(env.OC_MIN_SATS) : 0,\n minDays: env.OC_MIN_DAYS ? Number(env.OC_MIN_DAYS) : 0,\n allowKinds: parseNumList(env.OC_ALLOW_KINDS) ?? [0, 3, 10002],\n allowPubkeys: parseList(env.OC_ALLOW_PUBKEYS),\n relays: parseList(env.OC_RELAYS),\n failOpen: env.OC_FAIL_OPEN === 'true',\n cacheTtlMs: env.OC_CACHE_TTL_MS ? Number(env.OC_CACHE_TTL_MS) : 60_000,\n onDecision: (event, decision) => {\n if (env.OC_LOG !== 'false') {\n process.stderr.write(\n `[oc-strfry] ${decision.action} ${event.kind} ${event.pubkey.slice(0, 12)}… (${decision.reason})\\n`\n );\n }\n },\n };\n}\n\ninterface StrfryInput {\n type: 'new' | 'lookback';\n event?: MinimalNostrEvent;\n receivedAt?: number;\n sourceType?: string;\n sourceInfo?: string;\n}\n\nconst HEX_64_RE = /^[0-9a-f]{64}$/;\n\nasync function handleLine(line: string, options: FilterOptions): Promise<string | null> {\n let input: StrfryInput;\n try {\n input = JSON.parse(line);\n } catch {\n return null;\n }\n\n // Lookback events are already stored; skip entirely so we don't emit a\n // malformed echo (Strfry expects the id to match the input event).\n if (input.type !== 'new') {\n return null;\n }\n if (!input.event || typeof input.event !== 'object') {\n return null;\n }\n\n // Validate event shape before it reaches the filter — otherwise a\n // malformed `pubkey` becomes a cache-poisoning vector (a bogus key like\n // `undefined` shares a cache entry with every future malformed event).\n const ev = input.event;\n if (\n typeof ev.id !== 'string' ||\n typeof ev.pubkey !== 'string' ||\n typeof ev.kind !== 'number' ||\n !HEX_64_RE.test(ev.id) ||\n !HEX_64_RE.test(ev.pubkey)\n ) {\n return JSON.stringify({\n id: typeof ev.id === 'string' ? ev.id : '',\n action: 'reject',\n msg: 'orangecheck: malformed event shape',\n });\n }\n\n const decision = await filterEvent(ev, options);\n return JSON.stringify({\n id: ev.id,\n action: decision.action,\n ...(decision.message ? { msg: decision.message } : {}),\n });\n}\n\nasync function main(): Promise<void> {\n const options = optionsFromEnv();\n const rl = createInterface({ input: process.stdin, terminal: false });\n\n for await (const line of rl) {\n if (!line.trim()) continue;\n // Per-line isolation: a single throw here used to kill the whole\n // plugin (and then Strfry's default fallback determined whether the\n // relay accepted or rejected every subsequent event). Catch and emit\n // an explicit reject instead.\n try {\n const out = await handleLine(line, options);\n if (out) process.stdout.write(out + '\\n');\n } catch (err) {\n let id = '';\n try {\n const parsed = JSON.parse(line);\n if (parsed?.event?.id && typeof parsed.event.id === 'string') {\n id = parsed.event.id.slice(0, 64);\n }\n } catch {\n // leave id empty\n }\n process.stderr.write(\n `[oc-strfry] handleLine threw: ${err instanceof Error ? err.message : String(err)}\\n`\n );\n process.stdout.write(\n JSON.stringify({\n id,\n action: 'reject',\n msg: 'orangecheck: filter error',\n }) + '\\n'\n );\n }\n }\n}\n\nmain().catch((err) => {\n process.stderr.write(`[oc-strfry] fatal: ${err instanceof Error ? err.message : err}\\n`);\n process.exit(1);\n});\n\nexport { filterEvent } from './filter';\nexport type { FilterDecision, FilterOptions, MinimalNostrEvent } from './types';\n"]}
{"version":3,"sources":["../src/cache.ts","../src/filter.ts","../src/strfry.ts"],"names":[],"mappings":"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","file":"strfry.mjs","sourcesContent":["import type { FilterDecision } from './types';\n\ninterface Entry {\n value: FilterDecision;\n expires: number;\n}\n\nexport class TtlLru {\n private readonly store = new Map<string, Entry>();\n\n constructor(\n private readonly max: number,\n private readonly ttlMs: number\n ) {}\n\n get(key: string): FilterDecision | undefined {\n const entry = this.store.get(key);\n if (!entry) return undefined;\n if (Date.now() > entry.expires) {\n this.store.delete(key);\n return undefined;\n }\n // Touch on access so hot entries survive insertion pressure. Without\n // this, the old implementation was FIFO-with-TTL, not actually LRU —\n // a frequently-hit key could be evicted by N cold writes before it\n // ever moved in the map's insertion order.\n this.store.delete(key);\n this.store.set(key, entry);\n return entry.value;\n }\n\n /** Optional per-call TTL override. Used by the filter to cache\n * lookup-error decisions with a short TTL (circuit-breaker pattern). */\n set(key: string, value: FilterDecision, ttlMs?: number): void {\n if (this.store.size >= this.max) {\n const first = this.store.keys().next().value;\n if (first !== undefined) this.store.delete(first);\n }\n this.store.set(key, { value, expires: Date.now() + (ttlMs ?? this.ttlMs) });\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","import type { FilterDecision, FilterOptions, MinimalNostrEvent } from './types';\n\nimport { check } from '@orangecheck/sdk';\n\nimport { TtlLru } from './cache';\n\nconst DEFAULT_ALLOW_KINDS = [0, 3, 10002]; // profile meta, contacts, relay list\n\n// Short TTL for lookup-error decisions — acts as a circuit breaker so we\n// don't thundering-herd /api/check when upstream is flapping.\nconst LOOKUP_ERROR_TTL_MS = 5_000;\n\n/**\n * Process-wide cache. The old WeakMap<FilterOptions, …> design meant callers\n * who constructed a fresh options object per event (very common) never\n * produced a cache hit — every event was a cold lookup. Keying by a stable\n * config signature instead lets identical-config callers share.\n */\nconst caches = new Map<string, TtlLru>();\n\nfunction configSignature(opts: FilterOptions): string {\n return JSON.stringify([\n opts.minSats ?? 0,\n opts.minDays ?? 0,\n (opts.allowKinds ?? DEFAULT_ALLOW_KINDS).slice().sort(),\n (opts.allowPubkeys ?? []).slice().sort(),\n (opts.relays ?? []).slice().sort(),\n opts.cacheMax ?? 1_000,\n opts.cacheTtlMs ?? 60_000,\n ]);\n}\n\nfunction cacheFor(opts: FilterOptions): TtlLru {\n const sig = configSignature(opts);\n let c = caches.get(sig);\n if (!c) {\n c = new TtlLru(opts.cacheMax ?? 1_000, opts.cacheTtlMs ?? 60_000);\n caches.set(sig, c);\n }\n return c;\n}\n\n/**\n * Test hook — drop every cached decision. Not exported from the package\n * index, and never exercised in production code paths. Test suites call\n * this in `beforeEach` so per-config signatures don't bleed between cases.\n */\nexport function __clearFilterCachesForTests(): void {\n caches.clear();\n}\n\n/**\n * The Nostr public key an OrangeCheck attestation binds is the `nostr:npub…`\n * identity. Events on the wire carry the hex-encoded pubkey. We build the\n * identity lookup key using the hex form and let OrangeCheck's discovery\n * handle both formats.\n */\nfunction identityFor(pubkeyHex: string) {\n return { protocol: 'nostr', identifier: pubkeyHex } as const;\n}\n\nfunction reject(\n reason: FilterDecision['reason'],\n message: string,\n extras?: Partial<FilterDecision>\n): FilterDecision {\n return { action: 'reject', reason, message, ...extras };\n}\n\nfunction accept(\n reason: FilterDecision['reason'],\n extras?: Partial<FilterDecision>\n): FilterDecision {\n return { action: 'accept', reason, ...extras };\n}\n\n/**\n * Decide whether a relay should accept an event based on the OrangeCheck\n * status of the author's pubkey.\n *\n * Framework-agnostic. Use this from a custom relay, nostr-tools, or whatever.\n * For Strfry, see `@orangecheck/relay-filter/strfry`.\n */\nexport async function filterEvent(\n event: MinimalNostrEvent,\n options: FilterOptions\n): Promise<FilterDecision> {\n // Bypass: allowed kinds.\n const allowKinds = options.allowKinds ?? DEFAULT_ALLOW_KINDS;\n if (allowKinds.includes(event.kind)) {\n return finish(event, accept('allowed_kind'), options);\n }\n\n // Bypass: operator / admin pubkeys.\n if (options.allowPubkeys?.includes(event.pubkey)) {\n return finish(event, accept('allowed_pubkey', { pubkey: event.pubkey }), options);\n }\n\n // Check cache keyed by (pubkey, thresholds).\n const cache = cacheFor(options);\n const key = `${event.pubkey}:${options.minSats ?? 0}:${options.minDays ?? 0}`;\n const cached = cache.get(key);\n if (cached) {\n return finish(event, { ...cached, pubkey: event.pubkey }, options);\n }\n\n // Look up the pubkey's attestation via OrangeCheck.\n let decision: FilterDecision;\n try {\n const result = await check({\n identity: identityFor(event.pubkey),\n minSats: options.minSats,\n minDays: options.minDays,\n ...(options.relays ? { relays: options.relays } : {}),\n });\n\n if (result.ok) {\n decision = accept('ok', { check: result, pubkey: event.pubkey });\n } else if (result.reasons?.includes('not_found')) {\n decision = reject(\n 'no_attestation',\n `orangecheck: this relay requires a Bitcoin-stake proof. See https://ochk.io`,\n { check: result, pubkey: event.pubkey }\n );\n } else if (result.reasons?.some((r) => r === 'below_min_sats' || r === 'below_min_days')) {\n decision = reject(\n 'below_threshold',\n `orangecheck: proof below relay thresholds (min_sats=${options.minSats ?? 0}, min_days=${options.minDays ?? 0})`,\n { check: result, pubkey: event.pubkey }\n );\n } else {\n decision = reject(\n 'invalid_proof',\n `orangecheck: proof invalid (${result.reasons?.join(', ') ?? 'unknown'})`,\n { check: result, pubkey: event.pubkey }\n );\n }\n\n cache.set(key, decision);\n } catch (err) {\n // Lookup failure — fail open or closed per policy.\n if (options.failOpen) {\n decision = accept('fail_open', { pubkey: event.pubkey });\n } else {\n decision = reject('lookup_error', `orangecheck: lookup failed, try again later`, {\n pubkey: event.pubkey,\n });\n }\n // Cache the error decision with a short TTL so a burst of traffic\n // while /api/check is down doesn't all dogpile the upstream.\n cache.set(key, decision, LOOKUP_ERROR_TTL_MS);\n console.warn(\n '[orangecheck/relay-filter] lookup failed:',\n err instanceof Error ? err.message : String(err)\n );\n }\n\n return finish(event, decision, options);\n}\n\nfunction finish(\n event: MinimalNostrEvent,\n decision: FilterDecision,\n options: FilterOptions\n): FilterDecision {\n options.onDecision?.(event, decision);\n return decision;\n}\n","/**\n * Strfry policy plugin for OrangeCheck.\n *\n * tsup adds `#!/usr/bin/env node` automatically because this file is listed\n * under `bin` in package.json.\n *\n * Configure Strfry with the path to this binary and it will filter EVENT\n * submissions against OrangeCheck thresholds.\n *\n * Strfry's policy plugin protocol:\n * - read JSON lines from stdin; one line per inbound event\n * - each line has shape: { type: \"new\", event: {...}, ... }\n * - emit one JSON line per decision:\n * { id: \"<event_id>\", action: \"accept\"|\"reject\"|\"shadowReject\", msg?: \"...\" }\n *\n * Runtime config — set via environment variables (env is the cleanest way\n * to pass policy into a plugin Strfry spawns on your behalf):\n *\n * OC_MIN_SATS — minimum sats bonded (default: 0)\n * OC_MIN_DAYS — minimum days unspent (default: 0)\n * OC_ALLOW_KINDS — comma-separated kinds to bypass (default: \"0,3,10002\")\n * OC_ALLOW_PUBKEYS — comma-separated hex pubkeys to bypass (default: none)\n * OC_FAIL_OPEN — \"true\" to allow events through on lookup failure\n * OC_RELAYS — comma-separated Nostr relay URLs (default: SDK defaults)\n * OC_CACHE_TTL_MS — cache TTL in ms (default: 60000)\n *\n * Usage with Strfry:\n *\n * // strfry.conf\n * writePolicy = {\n * plugin = \"/usr/local/bin/oc-strfry\"\n * }\n *\n * Or via npx during development:\n *\n * writePolicy = { plugin = \"npx -y @orangecheck/relay-filter\" }\n *\n * (Strfry docs: https://github.com/hoytech/strfry)\n */\n\nimport type { FilterOptions, MinimalNostrEvent } from './types';\n\nimport { createInterface } from 'node:readline';\n\nimport { filterEvent } from './filter';\n\nfunction parseList(raw: string | undefined): string[] | undefined {\n if (!raw) return undefined;\n return raw\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\nfunction parseNumList(raw: string | undefined): number[] | undefined {\n const list = parseList(raw);\n if (!list) return undefined;\n return list.map((s) => Number(s)).filter((n) => Number.isFinite(n));\n}\n\nfunction optionsFromEnv(): FilterOptions {\n const env = process.env;\n return {\n minSats: env.OC_MIN_SATS ? Number(env.OC_MIN_SATS) : 0,\n minDays: env.OC_MIN_DAYS ? Number(env.OC_MIN_DAYS) : 0,\n allowKinds: parseNumList(env.OC_ALLOW_KINDS) ?? [0, 3, 10002],\n allowPubkeys: parseList(env.OC_ALLOW_PUBKEYS),\n relays: parseList(env.OC_RELAYS),\n failOpen: env.OC_FAIL_OPEN === 'true',\n cacheTtlMs: env.OC_CACHE_TTL_MS ? Number(env.OC_CACHE_TTL_MS) : 60_000,\n onDecision: (event, decision) => {\n if (env.OC_LOG !== 'false') {\n process.stderr.write(\n `[oc-strfry] ${decision.action} ${event.kind} ${event.pubkey.slice(0, 12)}… (${decision.reason})\\n`\n );\n }\n },\n };\n}\n\ninterface StrfryInput {\n type: 'new' | 'lookback';\n event?: MinimalNostrEvent;\n receivedAt?: number;\n sourceType?: string;\n sourceInfo?: string;\n}\n\nconst HEX_64_RE = /^[0-9a-f]{64}$/;\n\nasync function handleLine(line: string, options: FilterOptions): Promise<string | null> {\n let input: StrfryInput;\n try {\n input = JSON.parse(line);\n } catch {\n return null;\n }\n\n // Lookback events are already stored; skip entirely so we don't emit a\n // malformed echo (Strfry expects the id to match the input event).\n if (input.type !== 'new') {\n return null;\n }\n if (!input.event || typeof input.event !== 'object') {\n return null;\n }\n\n // Validate event shape before it reaches the filter — otherwise a\n // malformed `pubkey` becomes a cache-poisoning vector (a bogus key like\n // `undefined` shares a cache entry with every future malformed event).\n const ev = input.event;\n if (\n typeof ev.id !== 'string' ||\n typeof ev.pubkey !== 'string' ||\n typeof ev.kind !== 'number' ||\n !HEX_64_RE.test(ev.id) ||\n !HEX_64_RE.test(ev.pubkey)\n ) {\n return JSON.stringify({\n id: typeof ev.id === 'string' ? ev.id : '',\n action: 'reject',\n msg: 'orangecheck: malformed event shape',\n });\n }\n\n const decision = await filterEvent(ev, options);\n return JSON.stringify({\n id: ev.id,\n action: decision.action,\n ...(decision.message ? { msg: decision.message } : {}),\n });\n}\n\nasync function main(): Promise<void> {\n const options = optionsFromEnv();\n const rl = createInterface({ input: process.stdin, terminal: false });\n\n for await (const line of rl) {\n if (!line.trim()) continue;\n // Per-line isolation: a single throw here used to kill the whole\n // plugin (and then Strfry's default fallback determined whether the\n // relay accepted or rejected every subsequent event). Catch and emit\n // an explicit reject instead.\n try {\n const out = await handleLine(line, options);\n if (out) process.stdout.write(out + '\\n');\n } catch (err) {\n let id = '';\n try {\n const parsed = JSON.parse(line);\n if (parsed?.event?.id && typeof parsed.event.id === 'string') {\n id = parsed.event.id.slice(0, 64);\n }\n } catch {\n // leave id empty\n }\n process.stderr.write(\n `[oc-strfry] handleLine threw: ${err instanceof Error ? err.message : String(err)}\\n`\n );\n process.stdout.write(\n JSON.stringify({\n id,\n action: 'reject',\n msg: 'orangecheck: filter error',\n }) + '\\n'\n );\n }\n }\n}\n\nmain().catch((err) => {\n process.stderr.write(`[oc-strfry] fatal: ${err instanceof Error ? err.message : err}\\n`);\n process.exit(1);\n});\n\nexport { filterEvent } from './filter';\nexport type { FilterDecision, FilterOptions, MinimalNostrEvent } from './types';\n"]}
{
"name": "@orangecheck/relay-filter",
"version": "0.1.2",
"version": "0.1.3",
"description": "Sybil filter for Nostr relays — reject events from pubkeys without an OrangeCheck proof meeting configurable thresholds.",

@@ -24,3 +24,3 @@ "keywords": [

"bugs": {
"url": "https://github.com/orangecheck/oc-web/issues"
"url": "https://github.com/orangecheck/oc-packages/issues"
},

@@ -53,2 +53,5 @@ "main": "./dist/index.js",

"dev": "tsup --watch",
"test": "vitest run",
"test:watch": "vitest",
"type-check": "tsc --noEmit",
"clean": "rm -rf dist",

@@ -63,3 +66,4 @@ "prepublishOnly": "npm run clean && npm run build"

"tsup": "^8.3.5",
"typescript": "^5.7.2"
"typescript": "^5.7.2",
"vitest": "^4.1.5"
},

@@ -66,0 +70,0 @@ "engines": {