@spfn/core 0.3.0-beta.23 → 0.3.0-beta.24

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/event/sse/bounded-writer.ts","../../../src/event/sse/handler.ts","../../../src/event/sse/token-manager.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Bounded SSE outbound writer\n *\n * Serializes writes to one SSE stream through a single drain loop that `await`s\n * each `writeSSE` — so a slow client applies real backpressure instead of letting\n * frames pile up unbounded in memory (OOM under a fast producer + slow consumer).\n *\n * The queue is per-connection, so a slow stream never blocks other connections.\n * When the queue exceeds `maxQueue`, the connection is closed (via `onClose`)\n * rather than silently dropping frames — dropping a chunk would corrupt an\n * ordered token/chat stream; the client reconnects and re-fetches instead.\n */\n\n/** A frame to write to the SSE stream (subset of Hono's SSEMessage). */\nexport interface SSEFrame\n{\n data: string;\n event?: string;\n id?: string;\n}\n\n/** Minimal stream surface this writer needs (Hono's SSEStreamingApi satisfies it). */\nexport interface SSEWritable\n{\n writeSSE(message: SSEFrame): Promise<void>;\n}\n\nexport interface BoundedWriter\n{\n /** Queue a frame for delivery; closes the connection if the queue overflows. */\n enqueue(frame: SSEFrame): void;\n\n /** Stop draining and drop any queued frames (called on connection cleanup). */\n close(): void;\n\n /** Current queue depth (for tests/diagnostics). */\n readonly queued: number;\n}\n\n/**\n * Create a bounded, backpressure-aware writer for one SSE stream.\n *\n * @param stream the SSE stream (`writeSSE` returns a promise that resolves when\n * the socket has drained — that is what gives us backpressure)\n * @param maxQueue max frames buffered before the connection is closed\n * @param onClose called once when the writer closes the connection (overflow or\n * a write error); the caller cleans up and logs\n */\nexport function createBoundedWriter(\n stream: SSEWritable,\n maxQueue: number,\n onClose: (reason: string) => void,\n): BoundedWriter\n{\n const pending: SSEFrame[] = [];\n let flushing = false;\n let closed = false;\n\n const flush = async (): Promise<void> =>\n {\n if (flushing || closed)\n {\n return;\n }\n\n flushing = true;\n\n while (pending.length > 0 && !closed)\n {\n const frame = pending.shift() as SSEFrame;\n try\n {\n await stream.writeSSE(frame);\n }\n catch (err)\n {\n flushing = false;\n onClose(err instanceof Error ? err.message : String(err));\n\n return;\n }\n }\n\n flushing = false;\n };\n\n return {\n enqueue(frame: SSEFrame): void\n {\n if (closed)\n {\n return;\n }\n\n pending.push(frame);\n\n // Slow consumer: the drain loop can't keep up, so the queue grew past\n // the bound. Close rather than buffer unboundedly or drop silently.\n if (pending.length > maxQueue)\n {\n onClose('outbound queue overflow (slow consumer)');\n\n return;\n }\n\n void flush();\n },\n\n close(): void\n {\n closed = true;\n pending.length = 0;\n },\n\n get queued(): number\n {\n return pending.length;\n },\n };\n}\n","/**\n * SSE Handler for Hono\n *\n * Creates SSE stream endpoint for event subscription\n *\n * @example\n * ```typescript\n * import { Hono } from 'hono';\n * import { createSSEHandler } from '@spfn/core/event/sse';\n * import { eventRouter } from './events';\n *\n * const app = new Hono();\n *\n * // GET /events/stream?events=userCreated,orderPlaced\n * app.get('/events/stream', createSSEHandler(eventRouter));\n * ```\n */\n\nimport type { Context } from 'hono';\nimport { streamSSE } from 'hono/streaming';\nimport { logger } from '@spfn/core/logger';\nimport type { EventRouterDef, InferEventNames } from '../router';\nimport type { SSEHandlerConfig, SSEHandlerAuthConfig } from './types';\nimport type { SSETokenManager } from './token-manager';\nimport { createBoundedWriter } from './bounded-writer';\n\nconst sseLogger = logger.child('@spfn/core:sse');\n\n/**\n * Serialize a fan-out frame once per (event, payload).\n *\n * One `emit` delivers the same payload object to every subscribed connection, so\n * without memoization each connection re-`JSON.stringify`s the identical payload\n * — O(N) for N subscribers on a hot broadcast. Keyed by the payload object\n * (WeakMap → auto-GC after the emit); primitives are cheap to stringify directly.\n */\nconst frameCache = new WeakMap<object, Map<string, string>>();\n\nexport function serializeFrame(eventName: string, payload: unknown): string\n{\n if (payload === null || typeof payload !== 'object')\n {\n return JSON.stringify({ event: eventName, data: payload });\n }\n\n let byEvent = frameCache.get(payload);\n if (!byEvent)\n {\n byEvent = new Map<string, string>();\n frameCache.set(payload, byEvent);\n }\n\n let serialized = byEvent.get(eventName);\n if (serialized === undefined)\n {\n serialized = JSON.stringify({ event: eventName, data: payload });\n byEvent.set(eventName, serialized);\n }\n\n return serialized;\n}\n\n/** Default outbound queue cap per connection before a slow consumer is dropped. */\nconst DEFAULT_MAX_QUEUE = 1000;\n\n// Extend Hono context with SSE subject\ndeclare module 'hono'\n{\n interface ContextVariableMap\n {\n sseSubject?: string;\n }\n}\n\n/**\n * Create SSE handler for Hono\n *\n * Query parameters:\n * - events: Comma-separated list of event names to subscribe\n * - token: One-time auth token (when auth is enabled)\n *\n * @example\n * ```typescript\n * app.get('/events/stream', createSSEHandler(eventRouter, {\n * pingInterval: 30000,\n * }));\n * ```\n */\nexport function createSSEHandler<TRouter extends EventRouterDef<any>>(\n router: TRouter,\n config: SSEHandlerConfig = {},\n tokenManager?: SSETokenManager,\n)\n{\n const {\n // 10s — keep-alive가 중간 프록시(GCP LB/nginx)의 idle 타임아웃(흔히 ~15-30s)보다 자주 와야\n // 첫 ping 전 침묵 구간에 연결이 끊기지 않는다. 30s 기본은 idle 타임아웃보다 길어 SSE가\n // 데이터 없는 동안 끊기던 문제가 있었다.\n pingInterval = 10000,\n auth: authConfig,\n maxQueue = DEFAULT_MAX_QUEUE,\n } = config;\n\n return async (c: Context) =>\n {\n // ── 1. Token Authentication ──\n const subject = await authenticateToken(c, tokenManager);\n if (subject === false)\n {\n return c.json({ error: 'Missing token parameter' }, 401);\n }\n if (subject === null)\n {\n return c.json({ error: 'Invalid or expired token' }, 401);\n }\n if (subject)\n {\n c.set('sseSubject', subject);\n }\n\n // ── 2. Parse events from query parameter ──\n const requestedEvents = parseRequestedEvents(c);\n if (!requestedEvents)\n {\n return c.json({ error: 'Missing events parameter' }, 400);\n }\n\n // ── 3. Validate event names ──\n const validEventNames = router.eventNames as string[];\n const invalidEvents = requestedEvents.filter(e => !validEventNames.includes(e));\n\n if (invalidEvents.length > 0)\n {\n return c.json({\n error: 'Invalid event names',\n invalidEvents,\n validEvents: validEventNames,\n }, 400);\n }\n\n // ── 4. Subscription Authorization ──\n const allowedEvents = await authorizeEvents(subject, requestedEvents, authConfig);\n if (allowedEvents === null)\n {\n return c.json({ error: 'Not authorized for any requested events' }, 403);\n }\n\n sseLogger.debug('SSE connection requested', {\n events: allowedEvents,\n subject: subject || undefined,\n clientIp: c.req.header('x-forwarded-for') || c.req.header('x-real-ip'),\n });\n\n // ── 5. SSE Stream ──\n c.header('X-Accel-Buffering', 'no');\n\n return streamSSE(c, async (stream) =>\n {\n const unsubscribes: (() => void)[] = [];\n let messageId = 0;\n let connectionDead = false;\n let pingTimer: ReturnType<typeof setInterval>;\n let writer: ReturnType<typeof createBoundedWriter>;\n // Resolves the disconnect wait below when the connection is closed\n // server-side (overflow/error), so teardown doesn't wait for the loop.\n let onClosed: (() => void) | undefined;\n\n const cleanup = (reason?: string) =>\n {\n if (connectionDead) return;\n connectionDead = true;\n clearInterval(pingTimer);\n unsubscribes.forEach(fn => fn());\n writer?.close();\n onClosed?.();\n sseLogger.info('SSE dead connection cleaned up', {\n events: allowedEvents,\n reason,\n });\n };\n\n // All writes go through one bounded, backpressure-aware drain loop so a\n // slow client can't pile frames up in memory (it's closed on overflow).\n writer = createBoundedWriter(stream, maxQueue, (reason) =>\n {\n sseLogger.warn('SSE connection closed', { events: allowedEvents, reason });\n cleanup(reason);\n });\n\n // Send initial connection message (first frame in the queue).\n writer.enqueue({\n event: 'connected',\n data: JSON.stringify({\n subscribedEvents: allowedEvents,\n timestamp: Date.now(),\n }),\n });\n\n for (const eventName of allowedEvents as InferEventNames<TRouter>[])\n {\n const eventDef = router.events[eventName];\n\n if (!eventDef)\n {\n continue;\n }\n\n const unsubscribe = eventDef.subscribe((payload: unknown) =>\n {\n if (connectionDead) return;\n\n // ── Payload Filtering ──\n if (subject && authConfig?.filter?.[eventName as string])\n {\n if (!authConfig.filter[eventName as string](subject, payload))\n {\n return;\n }\n }\n\n messageId++;\n\n sseLogger.debug('SSE sending event', {\n event: eventName,\n messageId,\n });\n\n writer.enqueue({\n id: String(messageId),\n event: eventName as string,\n data: serializeFrame(eventName as string, payload),\n });\n });\n\n unsubscribes.push(unsubscribe);\n }\n\n sseLogger.info('SSE connection established', {\n events: allowedEvents,\n subscriptionCount: unsubscribes.length,\n });\n\n // Keep-alive ping\n pingTimer = setInterval(() =>\n {\n if (connectionDead) return;\n\n writer.enqueue({\n event: 'ping',\n data: JSON.stringify({ timestamp: Date.now() }),\n });\n }, pingInterval);\n\n // Wait for client disconnect (abort) or a server-side close. Waiting on\n // the abort event instead of polling sleep(pingInterval) means a dropped\n // client is torn down immediately, not up to one ping interval later.\n const abortSignal = c.req.raw.signal;\n\n if (!abortSignal.aborted && !connectionDead)\n {\n await new Promise<void>((resolve) =>\n {\n const onAbort = () => resolve();\n abortSignal.addEventListener('abort', onAbort, { once: true });\n // Server-side close (cleanup) resolves the wait and drops the listener.\n onClosed = () =>\n {\n abortSignal.removeEventListener('abort', onAbort);\n resolve();\n };\n });\n }\n\n // Cleanup (normal disconnect path; idempotent if already closed)\n cleanup();\n }, async (err: Error) =>\n {\n sseLogger.error('SSE stream error', {\n error: err.message,\n });\n });\n };\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Authenticate via one-time token\n * @returns subject string if authenticated, undefined if no auth required,\n * false if token missing, null if token invalid/expired\n */\nasync function authenticateToken(\n c: Context,\n tokenManager?: SSETokenManager,\n): Promise<string | undefined | false | null>\n{\n if (!tokenManager)\n {\n return undefined;\n }\n\n const token = c.req.query('token');\n if (!token)\n {\n return false;\n }\n\n return await tokenManager.verify(token);\n}\n\n/**\n * Parse requested events from query parameter\n */\nfunction parseRequestedEvents(c: Context): string[] | null\n{\n const eventsParam = c.req.query('events');\n if (!eventsParam)\n {\n return null;\n }\n\n return eventsParam.split(',').map(e => e.trim());\n}\n\n/**\n * Authorize event subscription via auth hook\n * @returns allowed events array, or null if rejected\n */\nasync function authorizeEvents(\n subject: string | undefined,\n requestedEvents: string[],\n authConfig?: SSEHandlerAuthConfig,\n): Promise<string[] | null>\n{\n if (!subject || !authConfig?.authorize)\n {\n return requestedEvents;\n }\n\n const allowed = await authConfig.authorize(subject, requestedEvents);\n\n if (allowed.length === 0)\n {\n return null;\n }\n\n return allowed;\n}\n","/**\n * SSE Token Manager\n *\n * Auth-agnostic token issuance and verification for SSE connections.\n * Issues one-time-use tokens with TTL for Token Exchange pattern.\n *\n * @example\n * ```typescript\n * const manager = new SSETokenManager({ ttl: 30000 });\n *\n * // Issue token for authenticated user\n * const token = await manager.issue('user-123');\n *\n * // Verify and consume token (one-time use)\n * const subject = await manager.verify(token); // 'user-123'\n * const again = await manager.verify(token); // null (already consumed)\n *\n * // Cleanup on shutdown\n * manager.destroy();\n * ```\n */\n\nimport { randomBytes, createHash } from 'crypto';\n\n/**\n * Hash a token for storage. The raw token is a bearer secret — storing it at rest\n * (e.g. as a Redis key/value) means a store dump hands out live tokens. We persist\n * only sha256(token) and look up by the same hash; the raw token exists only in\n * transit and in the issue() return value.\n */\nfunction hashToken(token: string): string\n{\n return createHash('sha256').update(token).digest('hex');\n}\n\n/**\n * Minimal cache client interface (compatible with ioredis Redis | Cluster)\n */\ntype CacheClient = {\n set(key: string, value: string, ...args: any[]): Promise<any>;\n getdel?(key: string): Promise<string | null>;\n get(key: string): Promise<string | null>;\n del(...keys: string[]): Promise<number>;\n};\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Stored SSE token data\n */\nexport interface SSEToken\n{\n subject: string;\n expiresAt: number;\n}\n\n/**\n * Token storage interface\n *\n * Implement this for custom storage backends (e.g., Redis for multi-instance).\n */\nexport interface SSETokenStore\n{\n /** Store a token */\n set(token: string, data: SSEToken): Promise<void>;\n\n /** Get and delete a token (one-time use) */\n consume(token: string): Promise<SSEToken | null>;\n\n /** Remove expired tokens */\n cleanup(): Promise<void>;\n}\n\n/**\n * SSETokenManager configuration\n */\nexport interface SSETokenManagerConfig\n{\n /**\n * Token time-to-live in milliseconds\n * @default 30000\n */\n ttl?: number;\n\n /**\n * Custom token store (default: in-memory Map)\n */\n store?: SSETokenStore;\n\n /**\n * Cleanup interval in milliseconds\n * @default 60000\n */\n cleanupInterval?: number;\n}\n\n// ============================================================================\n// InMemoryTokenStore\n// ============================================================================\n\nclass InMemoryTokenStore implements SSETokenStore\n{\n private tokens = new Map<string, SSEToken>();\n\n async set(token: string, data: SSEToken): Promise<void>\n {\n this.tokens.set(hashToken(token), data);\n }\n\n async consume(token: string): Promise<SSEToken | null>\n {\n const key = hashToken(token);\n const data = this.tokens.get(key);\n if (!data)\n {\n return null;\n }\n\n this.tokens.delete(key);\n\n return data;\n }\n\n async cleanup(): Promise<void>\n {\n const now = Date.now();\n\n for (const [key, data] of this.tokens)\n {\n if (data.expiresAt <= now)\n {\n this.tokens.delete(key);\n }\n }\n }\n}\n\n// ============================================================================\n// CacheTokenStore (Redis/Valkey)\n// ============================================================================\n\n/**\n * Redis/Valkey-backed token store for multi-instance deployments.\n *\n * Uses SET EX for automatic TTL expiry and GETDEL for atomic one-time consumption.\n * No cleanup needed — Redis handles expiration automatically.\n *\n * @example\n * ```typescript\n * import { getCache } from '@spfn/core/cache';\n *\n * const cache = getCache();\n * if (cache) {\n * const store = new CacheTokenStore(cache);\n * const manager = new SSETokenManager({ store });\n * }\n * ```\n */\nexport class CacheTokenStore implements SSETokenStore\n{\n private prefix = 'sse:token:';\n\n constructor(private cache: CacheClient) \n {}\n\n async set(token: string, data: SSEToken): Promise<void>\n {\n const ttlSeconds = Math.max(1, Math.ceil((data.expiresAt - Date.now()) / 1000));\n await this.cache.set(\n this.prefix + hashToken(token),\n JSON.stringify(data),\n 'EX',\n ttlSeconds,\n );\n }\n\n async consume(token: string): Promise<SSEToken | null>\n {\n const key = this.prefix + hashToken(token);\n\n // GETDEL (Redis 6.2+) for atomic consume, fallback to GET+DEL\n let raw: string | null = null;\n\n if (this.cache.getdel)\n {\n raw = await this.cache.getdel(key);\n }\n else\n {\n raw = await this.cache.get(key);\n if (raw)\n {\n await this.cache.del(key);\n }\n }\n\n if (!raw)\n {\n return null;\n }\n\n return JSON.parse(raw) as SSEToken;\n }\n\n async cleanup(): Promise<void>\n {\n // No-op: Redis TTL handles expiration automatically\n }\n}\n\n// ============================================================================\n// SSETokenManager\n// ============================================================================\n\nexport class SSETokenManager\n{\n private store: SSETokenStore;\n private ttl: number;\n private cleanupTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor(config?: SSETokenManagerConfig)\n {\n this.ttl = config?.ttl ?? 30000;\n this.store = config?.store ?? new InMemoryTokenStore();\n\n const cleanupInterval = config?.cleanupInterval ?? 60000;\n this.cleanupTimer = setInterval(() => void this.store.cleanup(), cleanupInterval);\n this.cleanupTimer.unref();\n }\n\n /**\n * How long an issued token stays valid, in milliseconds.\n *\n * Exposed so a caller that has to report an expiry to its client computes the same\n * number this manager stamps on the token, instead of assuming the default.\n */\n get ttlMs(): number\n {\n return this.ttl;\n }\n\n /**\n * Issue a new one-time-use token for the given subject\n */\n async issue(subject: string): Promise<string>\n {\n const token = randomBytes(32).toString('hex');\n\n // The store keys by sha256(token); the raw token is never persisted.\n await this.store.set(token, {\n subject,\n expiresAt: Date.now() + this.ttl,\n });\n\n return token;\n }\n\n /**\n * Verify and consume a token\n * @returns subject string if valid, null if invalid/expired/already consumed\n */\n async verify(token: string): Promise<string | null>\n {\n const data = await this.store.consume(token);\n\n if (!data || data.expiresAt <= Date.now())\n {\n return null;\n }\n\n return data.subject;\n }\n\n /**\n * Cleanup timer and resources\n */\n destroy(): void\n {\n if (this.cleanupTimer)\n {\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n }\n}\n"]}
1
+ {"version":3,"sources":["../../../src/event/sse/bounded-writer.ts","../../../src/event/sse/connection-registry.ts","../../../src/event/sse/handler.ts","../../../src/event/sse/token-manager.ts"],"names":["logger"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Bounded SSE outbound writer\n *\n * Serializes writes to one SSE stream through a single drain loop that `await`s\n * each `writeSSE` — so a slow client applies real backpressure instead of letting\n * frames pile up unbounded in memory (OOM under a fast producer + slow consumer).\n *\n * The queue is per-connection, so a slow stream never blocks other connections.\n * When the queue exceeds `maxQueue`, the connection is closed (via `onClose`)\n * rather than silently dropping frames — dropping a chunk would corrupt an\n * ordered token/chat stream; the client reconnects and re-fetches instead.\n */\n\n/** A frame to write to the SSE stream (subset of Hono's SSEMessage). */\nexport interface SSEFrame\n{\n data: string;\n event?: string;\n id?: string;\n}\n\n/** Minimal stream surface this writer needs (Hono's SSEStreamingApi satisfies it). */\nexport interface SSEWritable\n{\n writeSSE(message: SSEFrame): Promise<void>;\n}\n\nexport interface BoundedWriter\n{\n /** Queue a frame for delivery; closes the connection if the queue overflows. */\n enqueue(frame: SSEFrame): void;\n\n /** Stop draining and drop any queued frames (called on connection cleanup). */\n close(): void;\n\n /** Current queue depth (for tests/diagnostics). */\n readonly queued: number;\n}\n\n/**\n * Create a bounded, backpressure-aware writer for one SSE stream.\n *\n * @param stream the SSE stream (`writeSSE` returns a promise that resolves when\n * the socket has drained — that is what gives us backpressure)\n * @param maxQueue max frames buffered before the connection is closed\n * @param onClose called once when the writer closes the connection (overflow or\n * a write error); the caller cleans up and logs\n */\nexport function createBoundedWriter(\n stream: SSEWritable,\n maxQueue: number,\n onClose: (reason: string) => void,\n): BoundedWriter\n{\n const pending: SSEFrame[] = [];\n let flushing = false;\n let closed = false;\n\n const flush = async (): Promise<void> =>\n {\n if (flushing || closed)\n {\n return;\n }\n\n flushing = true;\n\n while (pending.length > 0 && !closed)\n {\n const frame = pending.shift() as SSEFrame;\n try\n {\n await stream.writeSSE(frame);\n }\n catch (err)\n {\n flushing = false;\n onClose(err instanceof Error ? err.message : String(err));\n\n return;\n }\n }\n\n flushing = false;\n };\n\n return {\n enqueue(frame: SSEFrame): void\n {\n if (closed)\n {\n return;\n }\n\n pending.push(frame);\n\n // Slow consumer: the drain loop can't keep up, so the queue grew past\n // the bound. Close rather than buffer unboundedly or drop silently.\n if (pending.length > maxQueue)\n {\n onClose('outbound queue overflow (slow consumer)');\n\n return;\n }\n\n void flush();\n },\n\n close(): void\n {\n closed = true;\n pending.length = 0;\n },\n\n get queued(): number\n {\n return pending.length;\n },\n };\n}\n","/**\n * SSE connection registry\n *\n * What one `createSSEHandler` knows about its open connections, so that\n * `EventDef.grant` / `EventDef.revoke` can change a connection after it opened.\n * One registry per handler: two handlers in one process (tests) never share one.\n *\n * Neither operation consults `canSubscribe` or `authorize`: they are an\n * operator's statement about an open connection. Permission lives in app data\n * and is re-read at the next connect.\n *\n * A connect still reading app data (`authorize`, `resolvePrincipal`,\n * `canSubscribe`) is a pending admission: a `grant` / `revoke` that lands then\n * is recorded on it and applied, without a frame, when the connection opens —\n * so a call the app made is never lost to that window.\n */\n\nimport { logger } from '@spfn/core/logger';\nimport type { SubscribeRequest } from '../types';\n\nconst registryLogger = logger.child('@spfn/core:sse');\n\n/** Stream-owned frame: an event was activated on the open connection. */\nexport const SUBSCRIPTION_GRANTED = 'subscription.granted';\n\n/** Stream-owned frame: an event was deactivated on the open connection. */\nexport const SUBSCRIPTION_REVOKED = 'subscription.revoked';\n\nexport type SubscriptionFrame = typeof SUBSCRIPTION_GRANTED | typeof SUBSCRIPTION_REVOKED;\n\n/**\n * One open SSE connection. `allowed` and `denied` partition `requested`\n * (plus anything `authorize` added); only `allowed` events hold a live\n * subscription in `unsubscribes`.\n */\nexport interface SSEConnection\n{\n /** `undefined` when the stream runs without auth — such a connection is never registered. */\n readonly subject: string | undefined;\n readonly requested: readonly string[];\n readonly allowed: Set<string>;\n readonly denied: Set<string>;\n readonly unsubscribes: Map<string, () => void>;\n readonly request: SubscribeRequest;\n\n /** The per-event subscribe the connection opened with — `grant` reuses it, never a copy. */\n readonly subscribe: (eventName: string) => () => void;\n\n /** Enqueue a protocol frame about one event (dropped once the connection is dead). */\n readonly notify: (frame: SubscriptionFrame, eventName: string) => void;\n}\n\nexport type ConnectionRegistry = ReturnType<typeof createConnectionRegistry>;\n\ntype SubscriptionChange = 'grant' | 'revoke';\n\n/**\n * A connect of one subject between its first app-data read and its open.\n * The first of `open` / `cancel` ends it; both are synchronous. A client abort\n * cancels it at once — even while an app predicate never settles.\n */\nexport interface PendingAdmission\n{\n /**\n * Apply the recorded changes to `connection`, then swap this entry for the\n * open connection. `false`, with nothing applied or registered, once cancelled.\n */\n readonly open: (connection: SSEConnection) => boolean;\n\n /** Drop this entry (refused, aborted, or thrown); idempotent. */\n readonly cancel: () => void;\n}\n\n/** The changes recorded on one pending admission: event → last call (last one wins). */\ntype RecordedChanges = Map<string, SubscriptionChange>;\n\n/**\n * Create the registry of one SSE handler, keyed by subject → open connections\n * (a subject with two tabs has two).\n */\nexport function createConnectionRegistry()\n{\n const bySubject = new Map<string, Set<SSEConnection>>();\n const pendingBySubject = new Map<string, Set<RecordedChanges>>();\n\n /**\n * Register a pending admission of `subject`; `signal` is the connect's\n * abort signal, listened to until the admission ends either way.\n */\n const beginAdmission = (subject: string, signal: AbortSignal): PendingAdmission =>\n {\n const changes: RecordedChanges = new Map();\n addToSet(pendingBySubject, subject, changes);\n const end = endOnce(signal, () => removeFromSet(pendingBySubject, subject, changes));\n\n return {\n cancel: end,\n open: (connection: SSEConnection) =>\n {\n if (!end())\n {\n return false;\n }\n\n applyRecorded(connection, changes);\n addToSet(bySubject, subject, connection);\n\n return true;\n },\n };\n };\n\n const change = (kind: SubscriptionChange, subject: string, eventName: string): void =>\n {\n const pending = pendingBySubject.get(subject);\n const connections = bySubject.get(subject);\n pending?.forEach(changes => changes.set(eventName, kind));\n connections?.forEach(connection => (kind === 'grant' ? grantOn : revokeOn)(connection, eventName));\n\n if (!pending && !connections)\n {\n registryLogger.debug('SSE subscription change for a subject with no open connection ignored', {\n event: eventName,\n subject,\n });\n }\n };\n\n return {\n beginAdmission,\n\n remove: (connection: SSEConnection): void =>\n {\n if (connection.subject)\n {\n removeFromSet(bySubject, connection.subject, connection);\n }\n },\n\n /** Open connections only — a pending admission is not counted. */\n count: (subject: string): number => bySubject.get(subject)?.size ?? 0,\n\n pendingCount: (subject: string): number => pendingBySubject.get(subject)?.size ?? 0,\n\n grant: (subject: string, eventName: string): void => change('grant', subject, eventName),\n\n revoke: (subject: string, eventName: string): void => change('revoke', subject, eventName),\n };\n}\n\n/**\n * The end of one pending admission: `remove` runs on the first call only,\n * which returns `true`; later calls return `false`. An abort of `signal` is a\n * first call; the listener is dropped on every end, so it never outlives the\n * admission (an already aborted signal ends it at once).\n */\nfunction endOnce(signal: AbortSignal, remove: () => void): () => boolean\n{\n let ended = false;\n const end = (): boolean =>\n {\n if (ended)\n {\n return false;\n }\n\n ended = true;\n signal.removeEventListener('abort', end);\n remove();\n\n return true;\n };\n\n signal.addEventListener('abort', end, { once: true });\n if (signal.aborted)\n {\n end();\n }\n\n return end;\n}\n\nfunction addToSet<T>(map: Map<string, Set<T>>, key: string, value: T): void\n{\n const set = map.get(key) ?? new Set<T>();\n set.add(value);\n map.set(key, set);\n}\n\n/** Remove `value` and drop the key once its set is empty, so neither map grows on reconnects. */\nfunction removeFromSet<T>(map: Map<string, Set<T>>, key: string, value: T): void\n{\n const set = map.get(key);\n set?.delete(value);\n\n if (set?.size === 0)\n {\n map.delete(key);\n }\n}\n\n/**\n * Apply what was recorded during admission to a connection that has not\n * opened yet: move the event between `allowed` and `denied`, no frame. An\n * event the connection never requested is in neither set and stays out.\n */\nfunction applyRecorded(connection: SSEConnection, changes: RecordedChanges): void\n{\n changes.forEach((kind, eventName) =>\n {\n const [from, to] = kind === 'grant'\n ? [connection.denied, connection.allowed]\n : [connection.allowed, connection.denied];\n\n if (from.delete(eventName))\n {\n to.add(eventName);\n }\n });\n}\n\n/**\n * Activate a denied event on one connection; allowed or never requested → no change.\n */\nfunction grantOn(connection: SSEConnection, eventName: string): void\n{\n if (!connection.denied.has(eventName))\n {\n return;\n }\n\n connection.unsubscribes.set(eventName, connection.subscribe(eventName));\n connection.denied.delete(eventName);\n connection.allowed.add(eventName);\n connection.notify(SUBSCRIPTION_GRANTED, eventName);\n}\n\n/**\n * Deactivate an allowed event on one connection; denied or never requested → no change.\n *\n * A frame the event already enqueued before this call may still be written —\n * the queue is not rewritten — but no frame after the unsubscribe is.\n */\nfunction revokeOn(connection: SSEConnection, eventName: string): void\n{\n if (!connection.allowed.has(eventName))\n {\n return;\n }\n\n connection.unsubscribes.get(eventName)?.();\n connection.unsubscribes.delete(eventName);\n connection.allowed.delete(eventName);\n connection.denied.add(eventName);\n connection.notify(SUBSCRIPTION_REVOKED, eventName);\n}\n\n/**\n * Drop every live subscription of a closing connection. A revoked event was\n * already removed from `unsubscribes`, so nothing is unsubscribed twice.\n */\nexport function unsubscribeAll(connection: SSEConnection): void\n{\n connection.unsubscribes.forEach(unsubscribe => unsubscribe());\n connection.unsubscribes.clear();\n}\n","/**\n * SSE Handler for Hono\n *\n * Creates SSE stream endpoint for event subscription\n *\n * @example\n * ```typescript\n * import { Hono } from 'hono';\n * import { createSSEHandler } from '@spfn/core/event/sse';\n * import { eventRouter } from './events';\n *\n * const app = new Hono();\n *\n * // GET /events/stream?events=userCreated,orderPlaced\n * app.get('/events/stream', createSSEHandler(eventRouter));\n * ```\n */\n\nimport type { Context } from 'hono';\nimport { streamSSE } from 'hono/streaming';\nimport type { SSEStreamingApi } from 'hono/streaming';\nimport { logger } from '@spfn/core/logger';\nimport type { EventRouterDef } from '../router';\nimport type { EventDef, SubscribeContext, SubscribeRequest } from '../types';\nimport type { SSEHandlerConfig, SSEHandlerAuthConfig } from './types';\nimport type { SSETokenManager } from './token-manager';\nimport { createBoundedWriter } from './bounded-writer';\nimport type { BoundedWriter } from './bounded-writer';\nimport { createConnectionRegistry, unsubscribeAll } from './connection-registry';\nimport type { ConnectionRegistry, PendingAdmission, SSEConnection } from './connection-registry';\n\nconst sseLogger = logger.child('@spfn/core:sse');\n\n/**\n * Serialize a fan-out frame once per (event, payload).\n *\n * One `emit` delivers the same payload object to every subscribed connection, so\n * without memoization each connection re-`JSON.stringify`s the identical payload\n * — O(N) for N subscribers on a hot broadcast. Keyed by the payload object\n * (WeakMap → auto-GC after the emit); primitives are cheap to stringify directly.\n */\nconst frameCache = new WeakMap<object, Map<string, string>>();\n\nexport function serializeFrame(eventName: string, payload: unknown): string\n{\n if (payload === null || typeof payload !== 'object')\n {\n return JSON.stringify({ event: eventName, data: payload });\n }\n\n let byEvent = frameCache.get(payload);\n if (!byEvent)\n {\n byEvent = new Map<string, string>();\n frameCache.set(payload, byEvent);\n }\n\n let serialized = byEvent.get(eventName);\n if (serialized === undefined)\n {\n serialized = JSON.stringify({ event: eventName, data: payload });\n byEvent.set(eventName, serialized);\n }\n\n return serialized;\n}\n\n/** Default outbound queue cap per connection before a slow consumer is dropped. */\nconst DEFAULT_MAX_QUEUE = 1000;\n\n// Extend Hono context with SSE subject\ndeclare module 'hono'\n{\n interface ContextVariableMap\n {\n sseSubject?: string;\n }\n}\n\n/**\n * Create SSE handler for Hono\n *\n * Query parameters:\n * - events: Comma-separated list of event names to subscribe\n * - token: One-time auth token (when auth is enabled)\n *\n * Who receives what, in order: the global `authorize` (once, at connect) →\n * each requested event's `.canSubscribe()` (once per event, at connect) → the\n * global `filter` map and the event's `.filter()` (sync, per frame). An event\n * denied at connect stays in `deniedEvents`, so `EventDef.grant` can activate\n * it later; `EventDef.revoke` deactivates an allowed one. The handler\n * registers that control on every event of the router.\n *\n * @example\n * ```typescript\n * app.get('/events/stream', createSSEHandler(eventRouter, {\n * pingInterval: 30000,\n * }));\n * ```\n */\nexport function createSSEHandler<TRouter extends EventRouterDef<any>>(\n router: TRouter,\n config: SSEHandlerConfig = {},\n tokenManager?: SSETokenManager,\n)\n{\n const {\n // 10s — keep-alive가 중간 프록시(GCP LB/nginx)의 idle 타임아웃(흔히 ~15-30s)보다 자주 와야\n // 첫 ping 전 침묵 구간에 연결이 끊기지 않는다. 30s 기본은 idle 타임아웃보다 길어 SSE가\n // 데이터 없는 동안 끊기던 문제가 있었다.\n pingInterval = 10000,\n auth: authConfig,\n maxQueue = DEFAULT_MAX_QUEUE,\n } = config;\n\n const registry = createConnectionRegistry();\n registerSubscriptionControls(router, registry);\n\n const streamConfig: StreamConfig = { router, authConfig, registry, pingInterval, maxQueue };\n\n const handler = async (c: Context) =>\n {\n const admission = await admitConnection(c, streamConfig, tokenManager);\n if (admission instanceof Response)\n {\n return admission;\n }\n\n if (c.req.raw.signal.aborted)\n {\n // The client left during admission: nobody reads this response.\n // Its abort listener already cancelled `pending`; cancel is idempotent.\n admission.pending?.cancel();\n\n return new Response(null, { status: 499 });\n }\n\n // ── 6. SSE Stream ──\n c.header('X-Accel-Buffering', 'no');\n\n return streamSSE(c, stream => runStream(c, stream, admission, streamConfig), async (err: Error) =>\n {\n sseLogger.error('SSE stream error', {\n error: err.message,\n });\n });\n };\n\n return Object.assign(handler, {\n /** @internal Open registered connections of a subject (tests). */\n _openConnections: (subject: string): number => registry.count(subject),\n\n /** @internal Connects of a subject still in admission (tests). */\n _pendingAdmissions: (subject: string): number => registry.pendingCount(subject),\n });\n}\n\n// ============================================================================\n// Connect\n// ============================================================================\n\n/** What a request that passed every connect check opens the stream with. */\ninterface Admission\n{\n subject: string | undefined;\n requested: string[];\n allowed: string[];\n denied: string[];\n request: SubscribeRequest;\n\n /** Registered before the first app-data read; `runStream` opens it. Absent without a subject. */\n pending?: PendingAdmission;\n}\n\n/** What every stream of one handler shares. */\ninterface StreamConfig\n{\n router: EventRouterDef<any>;\n authConfig: SSEHandlerAuthConfig | undefined;\n registry: ConnectionRegistry;\n pingInterval: number;\n maxQueue: number;\n}\n\n/**\n * Let `EventDef.grant` / `revoke` reach this handler's connections, once per\n * router key (an event under two keys gets two controls, each naming its key).\n */\nfunction registerSubscriptionControls(router: EventRouterDef<any>, registry: ConnectionRegistry): void\n{\n for (const eventName of router.eventNames as string[])\n {\n router.events[eventName]._registerSubscriptionControl({\n grant: (subject: string) => registry.grant(subject, eventName),\n revoke: (subject: string) => registry.revoke(subject, eventName),\n });\n }\n}\n\n/**\n * Run the connect checks; a `Response` is the refusal to return.\n */\nasync function admitConnection(\n c: Context,\n config: StreamConfig,\n tokenManager: SSETokenManager | undefined,\n): Promise<Admission | Response>\n{\n const { router, authConfig, registry } = config;\n\n // ── 1. Token Authentication ──\n const subject = await authenticateToken(c, tokenManager);\n if (subject === false)\n {\n return c.json({ error: 'Missing token parameter' }, 401);\n }\n if (subject === null)\n {\n return c.json({ error: 'Invalid or expired token' }, 401);\n }\n if (subject)\n {\n c.set('sseSubject', subject);\n }\n\n // ── 2-3. Requested events, validated against the router ──\n const requested = parseRequestedEvents(c);\n if (!requested)\n {\n return c.json({ error: 'Missing events parameter' }, 400);\n }\n\n const validEventNames = router.eventNames as string[];\n const invalidEvents = requested.filter(e => !validEventNames.includes(e));\n if (invalidEvents.length > 0)\n {\n return c.json({\n error: 'Invalid event names',\n invalidEvents,\n validEvents: validEventNames,\n }, 400);\n }\n\n return whilePending(\n subject ? registry.beginAdmission(subject, c.req.raw.signal) : undefined,\n () => authorizeConnection(c, router, authConfig, subject, requested),\n );\n}\n\n/**\n * Run the app-data checks with the admission already registered — `pending`\n * is written synchronously, before the first `await`, so a `grant` / `revoke`\n * issued meanwhile is recorded on it. A refusal or a throw unregisters it; a\n * client abort already did, the moment it happened; an admitted connection\n * carries it to `runStream`.\n */\nasync function whilePending(\n pending: PendingAdmission | undefined,\n check: () => Promise<Admission | Response>,\n): Promise<Admission | Response>\n{\n let outcome: Admission | Response | undefined;\n\n try\n {\n outcome = await check();\n\n return outcome instanceof Response ? outcome : { ...outcome, pending };\n }\n finally\n {\n if (!outcome || outcome instanceof Response)\n {\n pending?.cancel();\n }\n }\n}\n\n/**\n * Steps 4-5: the global `authorize`, then the principal and each event's\n * `canSubscribe`. Only an empty `authorize` result or a failed principal\n * refuses; a `canSubscribe` denial only lands the event in `denied`.\n */\nasync function authorizeConnection(\n c: Context,\n router: EventRouterDef<any>,\n authConfig: SSEHandlerAuthConfig | undefined,\n subject: string | undefined,\n requested: string[],\n): Promise<Admission | Response>\n{\n // ── 4. Subscription Authorization ──\n const authorized = await authorizeEvents(subject, requested, authConfig);\n if (authorized === null)\n {\n return c.json({ error: 'Not authorized for any requested events' }, 403);\n }\n\n // ── 5. Principal + per-event canSubscribe ──\n const request: SubscribeRequest = { query: c.req.query(), headers: c.req.header() };\n const resolved = await resolveConnectionPrincipal(subject, request, authConfig);\n if (!resolved)\n {\n return c.json({ error: 'Principal could not be resolved' }, 403);\n }\n\n const allowed = subject\n ? await checkCanSubscribe(router, authorized, { subject, principal: resolved.principal, request })\n : authorized;\n const denied = requested.filter(e => !allowed.includes(e));\n\n sseLogger.debug('SSE connection requested', {\n events: allowed,\n deniedEvents: denied,\n subject: subject || undefined,\n clientIp: c.req.header('x-forwarded-for') || c.req.header('x-real-ip'),\n });\n\n return { subject, requested, allowed, denied, request };\n}\n\n/**\n * Resolve the principal once per connection; `null` refuses the connection.\n * The warning names the subject only — never the thrown error's text, which\n * may carry what the resolver read.\n */\nasync function resolveConnectionPrincipal(\n subject: string | undefined,\n request: SubscribeRequest,\n authConfig: SSEHandlerAuthConfig | undefined,\n): Promise<{ principal: unknown } | null>\n{\n if (!subject || !authConfig?.resolvePrincipal)\n {\n return { principal: undefined };\n }\n\n try\n {\n return { principal: await authConfig.resolvePrincipal(subject, request) };\n }\n catch\n {\n sseLogger.warn('SSE principal could not be resolved — connection refused', { subject });\n\n return null;\n }\n}\n\n/**\n * Ask each authorized event's `canSubscribe`, in parallel; returns the events\n * it allows. An event without a predicate is allowed.\n */\nasync function checkCanSubscribe(\n router: EventRouterDef<any>,\n events: string[],\n ctx: SubscribeContext,\n): Promise<string[]>\n{\n const verdicts = await Promise.all(events.map(eventName => askCanSubscribe(eventName, router.events[eventName], ctx)));\n\n return events.filter((_, i) => verdicts[i]);\n}\n\n/**\n * `true` → allowed; `false` or a throw → denied. A throw is warned with the\n * event name and never fails the connection.\n */\nasync function askCanSubscribe(\n eventName: string,\n eventDef: EventDef<any> | undefined,\n ctx: SubscribeContext,\n): Promise<boolean>\n{\n if (!eventDef)\n {\n return false;\n }\n\n if (!eventDef._canSubscribe)\n {\n return true;\n }\n\n try\n {\n return await eventDef._canSubscribe(ctx) === true;\n }\n catch\n {\n sseLogger.warn('SSE canSubscribe threw — event denied on this connection', {\n event: eventName,\n subject: ctx.subject,\n });\n\n return false;\n }\n}\n\n// ============================================================================\n// Stream\n// ============================================================================\n\n/**\n * The life of one open connection: connected frame, subscriptions, registry\n * entry, keep-alive, and teardown on disconnect or a server-side close.\n */\nasync function runStream(\n c: Context,\n stream: SSEStreamingApi,\n admission: Admission,\n config: StreamConfig,\n): Promise<void>\n{\n const { allowed, denied, subject, request } = admission;\n let connectionDead = false;\n let pingTimer: ReturnType<typeof setInterval> | undefined;\n // Resolves the disconnect wait below when the connection is closed\n // server-side (overflow/error), so teardown doesn't wait for the loop.\n let onClosed: (() => void) | undefined;\n\n const cleanup = (reason?: string) =>\n {\n if (connectionDead) return;\n connectionDead = true;\n clearInterval(pingTimer);\n unsubscribeAll(connection);\n config.registry.remove(connection);\n writer.close();\n onClosed?.();\n sseLogger.info('SSE dead connection cleaned up', {\n events: [...connection.allowed],\n reason,\n });\n };\n\n // All writes go through one bounded, backpressure-aware drain loop so a\n // slow client can't pile frames up in memory (it's closed on overflow).\n const writer = createBoundedWriter(stream, config.maxQueue, (reason) =>\n {\n sseLogger.warn('SSE connection closed', { events: [...connection.allowed], reason });\n cleanup(reason);\n });\n\n const connection: SSEConnection = {\n subject,\n requested: admission.requested,\n allowed: new Set(allowed),\n denied: new Set(denied),\n unsubscribes: new Map(),\n request,\n subscribe: createEventSubscriber({\n router: config.router,\n authConfig: config.authConfig,\n subject,\n request,\n writer,\n isDead: () => connectionDead,\n }),\n notify: (frame, eventName) =>\n {\n if (connectionDead) return;\n\n // A small literal — not routed through serializeFrame's per-payload cache.\n writer.enqueue({ event: frame, data: JSON.stringify({ event: eventName }) });\n },\n };\n\n // Synchronously before the `connected` frame: the changes recorded during\n // admission land in `allowed` / `denied` first, and the entry turns open.\n // A cancelled admission (the client left) opens nothing and sends no frame.\n // Defensive: unreachable today, since `streamSSE` runs this synchronously up\n // to here; the handler's `signal.aborted` → 499 check is the real path.\n if (admission.pending?.open(connection) === false)\n {\n cleanup('aborted during admission');\n\n return;\n }\n\n openSubscriptions(connection, writer);\n\n // Keep-alive ping\n pingTimer = setInterval(() =>\n {\n if (connectionDead) return;\n\n writer.enqueue({\n event: 'ping',\n data: JSON.stringify({ timestamp: Date.now() }),\n });\n }, config.pingInterval);\n\n await waitForDisconnect(c.req.raw.signal, () => connectionDead, (resolve) =>\n {\n onClosed = resolve;\n });\n\n // Cleanup (normal disconnect path; idempotent if already closed)\n cleanup();\n}\n\n/**\n * Send the `connected` frame (first in the queue) and subscribe every allowed event.\n */\nfunction openSubscriptions(connection: SSEConnection, writer: BoundedWriter): void\n{\n writer.enqueue({\n event: 'connected',\n data: JSON.stringify({\n subscribedEvents: [...connection.allowed],\n deniedEvents: [...connection.denied],\n timestamp: Date.now(),\n }),\n });\n\n for (const eventName of connection.allowed)\n {\n connection.unsubscribes.set(eventName, connection.subscribe(eventName));\n }\n\n sseLogger.info('SSE connection established', {\n events: [...connection.allowed],\n deniedEvents: [...connection.denied],\n subscriptionCount: connection.unsubscribes.size,\n });\n}\n\n/**\n * Wait for client disconnect (abort) or a server-side close. Waiting on the\n * abort event instead of polling sleep(pingInterval) means a dropped client is\n * torn down immediately, not up to one ping interval later.\n */\nasync function waitForDisconnect(\n abortSignal: AbortSignal,\n isDead: () => boolean,\n onServerClose: (resolve: () => void) => void,\n): Promise<void>\n{\n if (abortSignal.aborted || isDead())\n {\n return;\n }\n\n await new Promise<void>((resolve) =>\n {\n const onAbort = () => resolve();\n abortSignal.addEventListener('abort', onAbort, { once: true });\n // Server-side close (cleanup) resolves the wait and drops the listener.\n onServerClose(() =>\n {\n abortSignal.removeEventListener('abort', onAbort);\n resolve();\n });\n });\n}\n\n/** What the per-event subscribe of one connection closes over. */\ninterface SubscriberContext\n{\n router: EventRouterDef<any>;\n authConfig: SSEHandlerAuthConfig | undefined;\n subject: string | undefined;\n request: SubscribeRequest;\n writer: BoundedWriter;\n isDead: () => boolean;\n}\n\n/**\n * The per-event subscribe of one connection, used at connect and by `grant`\n * alike, so filter order and `messageId` numbering are one code path.\n *\n * The returned unsubscribe also deactivates the callback, so a frame of an\n * `emit` already fanning out when `revoke` lands is not enqueued after it.\n */\nfunction createEventSubscriber(ctx: SubscriberContext): (eventName: string) => () => void\n{\n let messageId = 0;\n\n return (eventName: string) =>\n {\n const eventDef = ctx.router.events[eventName];\n let active = true;\n\n const unsubscribe = eventDef.subscribe((payload: unknown) =>\n {\n if (!active || ctx.isDead() || !passesFrameFilters(eventName, eventDef, payload, ctx))\n {\n return;\n }\n\n messageId++;\n\n sseLogger.debug('SSE sending event', {\n event: eventName,\n messageId,\n });\n\n ctx.writer.enqueue({\n id: String(messageId),\n event: eventName,\n data: serializeFrame(eventName, payload),\n });\n });\n\n return () =>\n {\n active = false;\n unsubscribe();\n };\n };\n}\n\n/**\n * The global `filter` map (with a subject, as before) and the event's own\n * `.filter()` — both must pass.\n */\nfunction passesFrameFilters(\n eventName: string,\n eventDef: EventDef<any>,\n payload: unknown,\n ctx: SubscriberContext,\n): boolean\n{\n const globalFilter = ctx.authConfig?.filter?.[eventName];\n if (ctx.subject && globalFilter && !globalFilter(ctx.subject, payload))\n {\n return false;\n }\n\n if (!eventDef._filter)\n {\n return true;\n }\n\n try\n {\n return eventDef._filter(payload, { subject: ctx.subject, request: ctx.request }) === true;\n }\n catch\n {\n sseLogger.warn('SSE event filter threw — frame dropped', { event: eventName, subject: ctx.subject });\n\n return false;\n }\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Authenticate via one-time token\n * @returns subject string if authenticated, undefined if no auth required,\n * false if token missing, null if token invalid/expired\n */\nasync function authenticateToken(\n c: Context,\n tokenManager?: SSETokenManager,\n): Promise<string | undefined | false | null>\n{\n if (!tokenManager)\n {\n return undefined;\n }\n\n const token = c.req.query('token');\n if (!token)\n {\n return false;\n }\n\n return await tokenManager.verify(token);\n}\n\n/**\n * Parse requested events from query parameter\n */\nfunction parseRequestedEvents(c: Context): string[] | null\n{\n const eventsParam = c.req.query('events');\n if (!eventsParam)\n {\n return null;\n }\n\n // Deduplicated: one subscription per event, keyed by name in the registry.\n return [...new Set(eventsParam.split(',').map(e => e.trim()))];\n}\n\n/**\n * Authorize event subscription via auth hook\n * @returns allowed events array, or null if rejected\n */\nasync function authorizeEvents(\n subject: string | undefined,\n requestedEvents: string[],\n authConfig?: SSEHandlerAuthConfig,\n): Promise<string[] | null>\n{\n if (!subject || !authConfig?.authorize)\n {\n return requestedEvents;\n }\n\n const allowed = await authConfig.authorize(subject, requestedEvents);\n\n if (allowed.length === 0)\n {\n return null;\n }\n\n return allowed;\n}\n","/**\n * SSE Token Manager\n *\n * Auth-agnostic token issuance and verification for SSE connections.\n * Issues one-time-use tokens with TTL for Token Exchange pattern.\n *\n * @example\n * ```typescript\n * const manager = new SSETokenManager({ ttl: 30000 });\n *\n * // Issue token for authenticated user\n * const token = await manager.issue('user-123');\n *\n * // Verify and consume token (one-time use)\n * const subject = await manager.verify(token); // 'user-123'\n * const again = await manager.verify(token); // null (already consumed)\n *\n * // Cleanup on shutdown\n * manager.destroy();\n * ```\n */\n\nimport { randomBytes, createHash } from 'crypto';\n\n/**\n * Hash a token for storage. The raw token is a bearer secret — storing it at rest\n * (e.g. as a Redis key/value) means a store dump hands out live tokens. We persist\n * only sha256(token) and look up by the same hash; the raw token exists only in\n * transit and in the issue() return value.\n */\nfunction hashToken(token: string): string\n{\n return createHash('sha256').update(token).digest('hex');\n}\n\n/**\n * Minimal cache client interface (compatible with ioredis Redis | Cluster)\n */\ntype CacheClient = {\n set(key: string, value: string, ...args: any[]): Promise<any>;\n getdel?(key: string): Promise<string | null>;\n get(key: string): Promise<string | null>;\n del(...keys: string[]): Promise<number>;\n};\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Stored SSE token data\n */\nexport interface SSEToken\n{\n subject: string;\n expiresAt: number;\n}\n\n/**\n * Token storage interface\n *\n * Implement this for custom storage backends (e.g., Redis for multi-instance).\n */\nexport interface SSETokenStore\n{\n /** Store a token */\n set(token: string, data: SSEToken): Promise<void>;\n\n /** Get and delete a token (one-time use) */\n consume(token: string): Promise<SSEToken | null>;\n\n /** Remove expired tokens */\n cleanup(): Promise<void>;\n}\n\n/**\n * SSETokenManager configuration\n */\nexport interface SSETokenManagerConfig\n{\n /**\n * Token time-to-live in milliseconds\n * @default 30000\n */\n ttl?: number;\n\n /**\n * Custom token store (default: in-memory Map)\n */\n store?: SSETokenStore;\n\n /**\n * Cleanup interval in milliseconds\n * @default 60000\n */\n cleanupInterval?: number;\n}\n\n// ============================================================================\n// InMemoryTokenStore\n// ============================================================================\n\nclass InMemoryTokenStore implements SSETokenStore\n{\n private tokens = new Map<string, SSEToken>();\n\n async set(token: string, data: SSEToken): Promise<void>\n {\n this.tokens.set(hashToken(token), data);\n }\n\n async consume(token: string): Promise<SSEToken | null>\n {\n const key = hashToken(token);\n const data = this.tokens.get(key);\n if (!data)\n {\n return null;\n }\n\n this.tokens.delete(key);\n\n return data;\n }\n\n async cleanup(): Promise<void>\n {\n const now = Date.now();\n\n for (const [key, data] of this.tokens)\n {\n if (data.expiresAt <= now)\n {\n this.tokens.delete(key);\n }\n }\n }\n}\n\n// ============================================================================\n// CacheTokenStore (Redis/Valkey)\n// ============================================================================\n\n/**\n * Redis/Valkey-backed token store for multi-instance deployments.\n *\n * Uses SET EX for automatic TTL expiry and GETDEL for atomic one-time consumption.\n * No cleanup needed — Redis handles expiration automatically.\n *\n * @example\n * ```typescript\n * import { getCache } from '@spfn/core/cache';\n *\n * const cache = getCache();\n * if (cache) {\n * const store = new CacheTokenStore(cache);\n * const manager = new SSETokenManager({ store });\n * }\n * ```\n */\nexport class CacheTokenStore implements SSETokenStore\n{\n private prefix = 'sse:token:';\n\n constructor(private cache: CacheClient) \n {}\n\n async set(token: string, data: SSEToken): Promise<void>\n {\n const ttlSeconds = Math.max(1, Math.ceil((data.expiresAt - Date.now()) / 1000));\n await this.cache.set(\n this.prefix + hashToken(token),\n JSON.stringify(data),\n 'EX',\n ttlSeconds,\n );\n }\n\n async consume(token: string): Promise<SSEToken | null>\n {\n const key = this.prefix + hashToken(token);\n\n // GETDEL (Redis 6.2+) for atomic consume, fallback to GET+DEL\n let raw: string | null = null;\n\n if (this.cache.getdel)\n {\n raw = await this.cache.getdel(key);\n }\n else\n {\n raw = await this.cache.get(key);\n if (raw)\n {\n await this.cache.del(key);\n }\n }\n\n if (!raw)\n {\n return null;\n }\n\n return JSON.parse(raw) as SSEToken;\n }\n\n async cleanup(): Promise<void>\n {\n // No-op: Redis TTL handles expiration automatically\n }\n}\n\n// ============================================================================\n// SSETokenManager\n// ============================================================================\n\nexport class SSETokenManager\n{\n private store: SSETokenStore;\n private ttl: number;\n private cleanupTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor(config?: SSETokenManagerConfig)\n {\n this.ttl = config?.ttl ?? 30000;\n this.store = config?.store ?? new InMemoryTokenStore();\n\n const cleanupInterval = config?.cleanupInterval ?? 60000;\n this.cleanupTimer = setInterval(() => void this.store.cleanup(), cleanupInterval);\n this.cleanupTimer.unref();\n }\n\n /**\n * How long an issued token stays valid, in milliseconds.\n *\n * Exposed so a caller that has to report an expiry to its client computes the same\n * number this manager stamps on the token, instead of assuming the default.\n */\n get ttlMs(): number\n {\n return this.ttl;\n }\n\n /**\n * Issue a new one-time-use token for the given subject\n */\n async issue(subject: string): Promise<string>\n {\n const token = randomBytes(32).toString('hex');\n\n // The store keys by sha256(token); the raw token is never persisted.\n await this.store.set(token, {\n subject,\n expiresAt: Date.now() + this.ttl,\n });\n\n return token;\n }\n\n /**\n * Verify and consume a token\n * @returns subject string if valid, null if invalid/expired/already consumed\n */\n async verify(token: string): Promise<string | null>\n {\n const data = await this.store.consume(token);\n\n if (!data || data.expiresAt <= Date.now())\n {\n return null;\n }\n\n return data.subject;\n }\n\n /**\n * Cleanup timer and resources\n */\n destroy(): void\n {\n if (this.cleanupTimer)\n {\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n }\n}\n"]}
@@ -1,6 +1,6 @@
1
- import { W as WSRouterDef, a as WSSubscribeOptions, b as WSUnsubscribe, c as WSConnectionState, d as WSClientConfig } from '../../types-W4u_S0lF.js';
1
+ import { W as WSRouterDef, a as WSSubscribeOptions, b as WSUnsubscribe, c as WSConnectionState, d as WSClientConfig } from '../../types-cwMX0cyJ.js';
2
2
  import 'hono';
3
- import '../../router-CysBmgv3.js';
3
+ import '../../router-ZDV1ISTo.js';
4
4
  import '@sinclair/typebox';
5
5
  import '../../token-manager-Ckjt2-qq.js';
6
6
 
@@ -1,6 +1,6 @@
1
- import { a as EventDef } from '../../router-CysBmgv3.js';
2
- import { e as WSMessageHandlers, W as WSRouterDef, f as WSHandlerConfig } from '../../types-W4u_S0lF.js';
3
- export { g as WSAuthConfig, d as WSClientConfig, c as WSConnectionState, h as WSEventHandlers, i as WSHandlerAuthConfig, j as WSMessageContext, k as WSMessageHandlerFn, l as WSRawConnection, a as WSSubscribeOptions, b as WSUnsubscribe } from '../../types-W4u_S0lF.js';
1
+ import { a as EventDef } from '../../router-ZDV1ISTo.js';
2
+ import { e as WSMessageHandlers, W as WSRouterDef, f as WSHandlerConfig } from '../../types-cwMX0cyJ.js';
3
+ export { g as WSAuthConfig, d as WSClientConfig, c as WSConnectionState, h as WSEventHandlers, i as WSHandlerAuthConfig, j as WSMessageContext, k as WSMessageHandlerFn, l as WSRawConnection, a as WSSubscribeOptions, b as WSUnsubscribe } from '../../types-cwMX0cyJ.js';
4
4
  import { Server } from 'node:http';
5
5
  import { S as SSETokenManager } from '../../token-manager-Ckjt2-qq.js';
6
6
  import '@sinclair/typebox';
@@ -40,6 +40,63 @@ interface EventContract {
40
40
  /** Contract version the event was removed in. */
41
41
  removedIn?: string;
42
42
  }
43
+ /**
44
+ * What the stream request asked for, copied once at connect.
45
+ *
46
+ * A plain snapshot, never the live Hono context, so a frame filter can read it
47
+ * on every frame long after the request object is gone. Header names are
48
+ * lowercased. It may hold `cookie` and `authorization` values — never log it.
49
+ */
50
+ interface SubscribeRequest {
51
+ /** Query parameters of the stream URL (`events`, `token` and any the app adds). */
52
+ query: Record<string, string>;
53
+ /** Request headers, names lowercased. */
54
+ headers: Record<string, string>;
55
+ }
56
+ /**
57
+ * What `.canSubscribe()` decides on, once per connection per requested event.
58
+ *
59
+ * `P` is whatever the SSE auth config's `resolvePrincipal` returns — core never
60
+ * knows what a principal is.
61
+ */
62
+ interface SubscribeContext<P = unknown> {
63
+ /** The subject the one-time token was issued to. */
64
+ subject: string;
65
+ /** The value `resolvePrincipal` returned for this connection, `undefined` without one. */
66
+ principal: P | undefined;
67
+ /** The connect request snapshot. */
68
+ request: SubscribeRequest;
69
+ }
70
+ /**
71
+ * Decides at connect whether a subject may receive an event. `false` or a
72
+ * throw denies the event on that connection; the connection stays open.
73
+ */
74
+ type SubscribePredicate<P = unknown> = (ctx: SubscribeContext<P>) => boolean | Promise<boolean>;
75
+ /**
76
+ * What a per-event `.filter()` sees on every frame.
77
+ */
78
+ interface FrameContext {
79
+ /** The connection's subject, `undefined` when the stream runs without auth. */
80
+ subject: string | undefined;
81
+ /** The connect request snapshot. */
82
+ request: SubscribeRequest;
83
+ }
84
+ /**
85
+ * Decides per frame whether one connection receives one payload. Sync: it runs
86
+ * for every connection on every emit, so it matches the payload against what
87
+ * the connection asked for and never reads storage.
88
+ */
89
+ type FramePredicate<TPayload> = (payload: TPayload, ctx: FrameContext) => boolean;
90
+ /**
91
+ * How an SSE handler lets `grant` / `revoke` reach its open connections.
92
+ * Each handler registers one per router key of the event.
93
+ */
94
+ interface SubscriptionControl {
95
+ /** Activate the event on every open connection of `subject` where it is denied. */
96
+ grant: (subject: string) => void;
97
+ /** Deactivate the event on every open connection of `subject` where it is allowed. */
98
+ revoke: (subject: string) => void;
99
+ }
43
100
  /**
44
101
  * Event definition interface
45
102
  */
@@ -68,6 +125,43 @@ interface EventDef<TPayload = void> {
68
125
  * declarations silently winning.
69
126
  */
70
127
  contract: (contract: EventContract) => EventDef<TPayload>;
128
+ /**
129
+ * The predicate declared with `.canSubscribe()`, absent when any subject
130
+ * that passes the global `authorize` may subscribe.
131
+ */
132
+ readonly _canSubscribe?: SubscribePredicate<any>;
133
+ /**
134
+ * Declare who may subscribe to this event over SSE.
135
+ *
136
+ * Runs once per connection, after the global `authorize`, only when the
137
+ * connection has a subject. `P` is the type of the principal the SSE auth
138
+ * config's `resolvePrincipal` returns. Set once: a second call throws.
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * export const auditLogged = defineEvent('audit.logged', payload)
143
+ * .canSubscribe<AuthPrincipal>(({ principal }) => principal?.role === 'admin');
144
+ * ```
145
+ */
146
+ canSubscribe: <P = unknown>(predicate: SubscribePredicate<P>) => EventDef<TPayload>;
147
+ /**
148
+ * The per-frame predicate declared with `.filter()`.
149
+ */
150
+ readonly _filter?: FramePredicate<TPayload>;
151
+ /**
152
+ * Declare which frames of this event a connection receives.
153
+ *
154
+ * Runs per frame on every connection subscribed to the event, with or
155
+ * without a subject, beside the global `filter` map (both must pass). Sync.
156
+ * Set once: a second call throws.
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * export const runtimeChanged = defineEvent('runtime.changed', payload)
161
+ * .filter((payload, { request }) => payload.workspaceId === request.query.workspaceId);
162
+ * ```
163
+ */
164
+ filter: (predicate: FramePredicate<TPayload>) => EventDef<TPayload>;
71
165
  /**
72
166
  * Subscribe to this event (in-memory handler)
73
167
  */
@@ -80,6 +174,23 @@ interface EventDef<TPayload = void> {
80
174
  * Emit the event (triggers all subscribers and queued jobs)
81
175
  */
82
176
  emit: TPayload extends void ? () => Promise<void> : (payload: TPayload) => Promise<void>;
177
+ /**
178
+ * Activate this event on every open SSE connection of `subject` where it is
179
+ * denied — by `canSubscribe` or `authorize` at connect, or by a `revoke`.
180
+ *
181
+ * Reaches every pod when the cache transport is attached. Never re-runs
182
+ * `canSubscribe`: it is an operator's statement about an open connection.
183
+ * An event the connection never requested cannot be granted.
184
+ */
185
+ grant: (subject: string) => Promise<void>;
186
+ /**
187
+ * Deactivate this event on every open SSE connection of `subject` where it
188
+ * is allowed; the client receives a `subscription.revoked` frame.
189
+ *
190
+ * Reaches every pod when the cache transport is attached. Affects open
191
+ * connections only — the next connect asks `canSubscribe` afresh.
192
+ */
193
+ revoke: (subject: string) => Promise<void>;
83
194
  /**
84
195
  * Enable cache-based pub/sub for multi-instance support
85
196
  * Must await before emitting events to ensure subscription is ready
@@ -90,6 +201,12 @@ interface EventDef<TPayload = void> {
90
201
  * Called by job registration system
91
202
  */
92
203
  _registerJobQueue: (queueName: string, sender: JobQueueSender) => void;
204
+ /**
205
+ * Internal: Let an SSE handler's connection registry receive `grant` /
206
+ * `revoke`. Called by `createSSEHandler` once per router key; every
207
+ * registered control receives every call. Returns the unregister function.
208
+ */
209
+ _registerSubscriptionControl: (control: SubscriptionControl) => () => void;
93
210
  /**
94
211
  * Internal: Drop the cache binding (cache + subscribed flag).
95
212
  * Called by the event cache transport on shutdown so a later useCache can
@@ -229,4 +346,4 @@ type InferEventPayloads<T extends EventRouterBase<any>> = T['_types'];
229
346
  */
230
347
  declare function defineEventRouter<TEvents extends Record<string, EventDef<any>>>(events: TEvents): EventRouterDef<TEvents>;
231
348
 
232
- export { type EventRouterDef as E, type InferEventNames as I, type JobQueueSender as J, type PubSubCache as P, type EventDef as a, type EventContract as b, type EventHandler as c, type EventRouterContract as d, type InferEventPayload$1 as e, type InferEventPayloads as f, type InferEventPayload as g, defineEventRouter as h, type EventRouterBase as i };
349
+ export { type EventRouterDef as E, type FrameContext as F, type InferEventNames as I, type JobQueueSender as J, type PubSubCache as P, type SubscribeContext as S, type EventDef as a, type EventContract as b, type EventHandler as c, type EventRouterContract as d, type FramePredicate as e, type InferEventPayload$1 as f, type InferEventPayloads as g, type InferEventPayload as h, type SubscribePredicate as i, type SubscribeRequest as j, defineEventRouter as k, type EventRouterBase as l };
@@ -6,9 +6,9 @@ import { NamedMiddleware, Router } from '@spfn/core/route';
6
6
  import { OnErrorContext, ProxyGuardConfig, RateLimitOptions } from '@spfn/core/middleware';
7
7
  import { SafeFetchPolicy } from '@spfn/core/security';
8
8
  import { c as JobRouter, f as BossOptions } from '../boss-BiDCbbAQ.js';
9
- import { E as EventRouterDef, a as EventDef } from '../router-CysBmgv3.js';
10
- import { d as SSEHandlerConfig, e as SSEAuthConfig } from '../types-B8zAxFsE.js';
11
- import { W as WSRouterDef, f as WSHandlerConfig, e as WSMessageHandlers, g as WSAuthConfig } from '../types-W4u_S0lF.js';
9
+ import { E as EventRouterDef, a as EventDef } from '../router-ZDV1ISTo.js';
10
+ import { d as SSEHandlerConfig, e as SSEAuthConfig } from '../types-C_g6gD-w.js';
11
+ import { W as WSRouterDef, f as WSHandlerConfig, e as WSMessageHandlers, g as WSAuthConfig } from '../types-cwMX0cyJ.js';
12
12
  import { DatabaseProvider, MigrationStatus, MigrationStatusDb } from '@spfn/core/db';
13
13
  import * as _sinclair_typebox from '@sinclair/typebox';
14
14
  import { Static } from '@sinclair/typebox';