@spfn/core 0.3.0-beta.24 → 0.3.0-beta.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/contract/index.d.ts +1 -1
- package/dist/event/index.d.ts +3 -3
- package/dist/event/index.js +30 -10
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +2 -2
- package/dist/event/sse/client.js +12 -1
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +3 -3
- package/dist/event/sse/index.js +19 -9
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +2 -2
- package/dist/event/ws/index.d.ts +3 -3
- package/dist/{router-ZDV1ISTo.d.ts → router-D5B0cpVN.d.ts} +39 -10
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.js +19 -9
- package/dist/server/index.js.map +1 -1
- package/dist/state/client.d.ts +144 -0
- package/dist/state/client.js +478 -0
- package/dist/state/client.js.map +1 -0
- package/dist/state/index.d.ts +395 -0
- package/dist/state/index.js +578 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/react.d.ts +34 -0
- package/dist/state/react.js +44 -0
- package/dist/state/react.js.map +1 -0
- package/dist/{types-C_g6gD-w.d.ts → types-04gu3uMj.d.ts} +8 -1
- package/dist/{types-cwMX0cyJ.d.ts → types-CqTsDETV.d.ts} +1 -1
- package/package.json +26 -2
- package/src/event/README.md +48 -10
- package/src/state/README.md +539 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/nextjs/client/csrf.ts","../../../src/event/sse/client.ts"],"names":[],"mappings":"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","file":"client.js","sourcesContent":["/**\n * CSRF token transport for the RPC client\n *\n * The Next.js proxy sets a readable (non-HttpOnly) cookie carrying an HMAC of\n * the session's key id; this module mirrors it into a request header. The proxy\n * never compares cookie against header — it recomputes the value from the\n * session — so the cookie is transport only and reading it here is not a trust\n * decision. See the CSRF section of the @spfn/auth README.\n */\n\n/**\n * Header the readable CSRF cookie is mirrored into.\n *\n * Must match CSRF_HEADER in @spfn/auth. It is duplicated rather than imported\n * because @spfn/core cannot depend on @spfn/auth.\n */\nexport const CSRF_HEADER = 'x-spfn-csrf';\n\n/**\n * Cookie names carrying a CSRF token.\n *\n * @spfn/auth suffixes its cookie names with the server port so several local dev\n * instances on the same host do not overwrite each other's session, and the\n * browser cannot know that port — so match the family, not one name. Every\n * candidate is sent and the proxy accepts the request if any of them recomputes\n * to the expected value; a candidate an attacker tossed in never will.\n */\nconst CSRF_COOKIE_PATTERN = /^spfn_csrf(_\\d+)?$/;\n\n/**\n * Values that can be put in a header at all: printable ASCII, no space.\n *\n * A cookie jar is attacker-writable in the very case this feature exists for, and\n * `new Headers()` rejects a value outside Latin-1 — so a sibling subdomain that\n * tosses `spfn_csrf=한` would make every RPC call throw before it was sent, not\n * merely fail the check. A genuine token is 64 hex characters; anything that\n * cannot be a header value cannot be one.\n */\nconst HEADER_SAFE_VALUE = /^[!-~]+$/;\n\n/**\n * Upper bound on candidates sent, so a cookie-flooded jar stays a bounded header.\n *\n * Deliberately generous. The server recomputes the expected value once and then\n * only compares fixed-length strings, so extra candidates cost it almost nothing,\n * while every candidate dropped here is a chance to lock the user out of every\n * mutation — the far worse failure. Must not exceed the matching bound in\n * @spfn/auth's `matchesCsrfToken`, or values sent would never be looked at.\n */\nconst MAX_CANDIDATES = 32;\n\n/**\n * Choose which candidates to send when the jar holds more than the cap.\n *\n * `document.cookie` exposes neither Domain nor Path, so a tossed cookie is\n * indistinguishable from the genuine one here — the proxy is what tells them\n * apart, by recomputing. What this can do is refuse to drop the genuine value on\n * the floor. Browsers order the jar by path length descending, then by age\n * ascending, so a flood pushes the genuine cookie towards the end (attacker\n * cookies on a longer path) or towards the start (attacker cookies written later\n * on the same path). Keeping both ends survives either, where a plain prefix\n * survives only one.\n *\n * A jar flooded far past the cap is a denial of service that no client-side\n * choice fully prevents; the structural fix is a `__Host-` prefixed cookie name,\n * which a sibling subdomain cannot write at all.\n */\nfunction selectCandidates(values: string[]): string[]\n{\n // Identical duplicates carry no extra information, so collapsing them buys\n // back slots that a flood of one repeated value would otherwise burn.\n const unique = Array.from(new Set(values));\n\n if (unique.length <= MAX_CANDIDATES)\n {\n return unique;\n }\n\n const half = MAX_CANDIDATES / 2;\n\n return [...unique.slice(0, half), ...unique.slice(-half)];\n}\n\n/**\n * Collect the CSRF token candidates from cookie entries.\n *\n * Takes entries rather than a map because a name can legitimately appear twice:\n * a sibling subdomain that tosses `spfn_csrf` for the parent domain leaves the\n * browser holding two cookies of that name, and folding them into a map would\n * throw away whichever the tossed one displaced — locking the user out of every\n * mutation. Sending both keeps the real one in play, and the tossed one is inert\n * because the proxy recomputes what it expects.\n *\n * @returns Header value (comma-separated), or undefined when no cookie exists\n */\nexport function csrfHeaderValue(cookies: Iterable<[string, string]>): string | undefined\n{\n // A comma separates candidates on the wire, so a value containing one would\n // arrive as several. That cannot be a genuine token — they are hex — but it\n // would let a single tossed cookie spend the receiver's whole candidate\n // budget and bury the real value behind it. Drop those, and anything that\n // could not be a header value at all.\n const values = Array.from(cookies)\n .filter(([name, value]) => CSRF_COOKIE_PATTERN.test(name)\n && HEADER_SAFE_VALUE.test(value)\n && !value.includes(','))\n .map(([, value]) => value);\n\n const candidates = selectCandidates(values);\n\n return candidates.length > 0 ? candidates.join(',') : undefined;\n}\n\n/**\n * Read the browser cookie jar as entries, duplicates preserved.\n *\n * Empty outside a browser (SSR reads the jar it forwards instead).\n */\nexport function documentCookieEntries(): Array<[string, string]>\n{\n if (typeof document === 'undefined' || !document.cookie)\n {\n return [];\n }\n\n const entries: Array<[string, string]> = [];\n\n for (const pair of document.cookie.split(';'))\n {\n const separator = pair.indexOf('=');\n\n if (separator > 0)\n {\n entries.push([pair.slice(0, separator).trim(), pair.slice(separator + 1).trim()]);\n }\n }\n\n return entries;\n}\n","/**\n * SSE Client\n *\n * Type-safe EventSource wrapper for event subscription\n *\n * @example\n * ```typescript\n * import { createSSEClient } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * // Uses defaults: NEXT_PUBLIC_SPFN_API_URL + /events/stream\n * const client = createSSEClient<EventRouter>();\n *\n * // Or with custom host/pathname\n * const client = createSSEClient<EventRouter>({\n * host: 'https://api.example.com',\n * pathname: '/sse',\n * });\n *\n * // With token authentication (recommended: use createAuthSSEClient)\n * import { createAuthSSEClient } from '@spfn/core/event/sse/client';\n * const client = createAuthSSEClient<EventRouter>();\n *\n * const unsubscribe = client.subscribe({\n * events: ['userCreated', 'orderPlaced'],\n * handlers: {\n * userCreated: (payload) => console.log('User:', payload.userId),\n * orderPlaced: (payload) => console.log('Order:', payload.orderId),\n * },\n * });\n *\n * // Later: cleanup\n * unsubscribe();\n * ```\n */\n\nimport type { EventRouterDef, InferEventNames } from '../router';\nimport { CSRF_HEADER, csrfHeaderValue, documentCookieEntries } from '../../nextjs/client/csrf';\nimport type {\n SSEClientConfig,\n SSESubscribeOptions,\n SSEUnsubscribe,\n SSEConnectionState,\n SSEMessage,\n} from './types';\n\n/**\n * SSE Client instance\n */\nexport interface SSEClient<TRouter extends EventRouterDef<any>>\n{\n /**\n * Subscribe to events\n */\n subscribe(options: SSESubscribeOptions<TRouter>): SSEUnsubscribe;\n\n /**\n * Get current connection state\n */\n getState(): SSEConnectionState;\n\n /**\n * Close all connections\n */\n close(): void;\n}\n\n/**\n * Default SSE configuration\n */\nconst SSE_DEFAULTS = {\n host: typeof process !== 'undefined'\n ? (process.env.NEXT_PUBLIC_SPFN_API_URL || 'http://localhost:8790')\n : 'http://localhost:8790',\n pathname: '/events/stream',\n} as const;\n\n/**\n * Create type-safe SSE client\n *\n * @example\n * ```typescript\n * // Uses defaults (NEXT_PUBLIC_SPFN_API_URL + /events/stream)\n * const client = createSSEClient<EventRouter>();\n *\n * // Or with custom configuration\n * const client = createSSEClient<EventRouter>({\n * host: 'https://api.example.com',\n * pathname: '/sse',\n * reconnect: true,\n * reconnectDelay: 3000,\n * });\n *\n * // Subscribe to events\n * const unsubscribe = client.subscribe({\n * events: ['userCreated', 'orderPlaced'],\n * handlers: {\n * userCreated: (payload) => {\n * console.log('New user:', payload.userId);\n * },\n * orderPlaced: (payload) => {\n * console.log('New order:', payload.orderId);\n * },\n * },\n * onOpen: () => console.log('Connected'),\n * onError: (err) => console.error('Error:', err),\n * onReconnect: (attempt) => console.log('Reconnecting...', attempt),\n * });\n *\n * // Cleanup\n * unsubscribe();\n * ```\n */\n/**\n * Per-subscription connection resources.\n *\n * Grouped so an in-flight async connect can be cancelled atomically and\n * `close()` can tear down whichever subscription is currently active —\n * the shared module-level EventSource used to leak across the token await.\n */\ninterface SSEConnection\n{\n eventSource: EventSource | null;\n reconnectTimer: ReturnType<typeof setTimeout> | null;\n reconnectAttempts: number;\n closed: boolean;\n onClose?: () => void;\n}\n\nexport function createSSEClient<TRouter extends EventRouterDef<any>>(\n config: SSEClientConfig = {},\n): SSEClient<TRouter>\n{\n const {\n url,\n host = SSE_DEFAULTS.host,\n pathname = SSE_DEFAULTS.pathname,\n reconnect = true,\n reconnectDelay = 3000,\n maxReconnectAttempts = 0,\n withCredentials = false,\n acquireToken,\n } = config;\n\n // Build base URL: url takes precedence, otherwise host + pathname\n const baseUrl = url || `${host}${pathname}`;\n\n let state: SSEConnectionState = 'closed';\n let active: SSEConnection | null = null;\n\n // Idempotent teardown: closes the connection, fires onClose exactly once,\n // and clears the active slot if this was the live connection.\n function closeConn(conn: SSEConnection)\n {\n if (conn.closed)\n {\n return;\n }\n\n conn.closed = true;\n\n if (conn.reconnectTimer)\n {\n clearTimeout(conn.reconnectTimer);\n conn.reconnectTimer = null;\n }\n\n if (conn.eventSource)\n {\n conn.eventSource.close();\n conn.eventSource = null;\n }\n\n if (active === conn)\n {\n active = null;\n state = 'closed';\n }\n\n conn.onClose?.();\n }\n\n function subscribe(options: SSESubscribeOptions<TRouter>): SSEUnsubscribe\n {\n const { events, handlers, onOpen, onError, onReconnect, onClose, onSubscriptionChange } = options;\n\n // A new subscription supersedes any previous one on this client.\n if (active)\n {\n closeConn(active);\n }\n\n const conn: SSEConnection = {\n eventSource: null,\n reconnectTimer: null,\n reconnectAttempts: 0,\n closed: false,\n onClose,\n };\n active = conn;\n\n const eventNames = events as string[];\n\n function connect()\n {\n state = 'connecting';\n\n const init = async () =>\n {\n let tokenParam = '';\n\n if (acquireToken)\n {\n const token = await acquireToken();\n\n // Cancelled during the token await (e.g. StrictMode cleanup) —\n // don't open a connection that has no teardown waiting for it.\n if (conn.closed)\n {\n return;\n }\n\n tokenParam = `&token=${encodeURIComponent(token)}`;\n }\n\n if (conn.closed)\n {\n return;\n }\n\n const streamUrl = `${baseUrl}?events=${eventNames.join(',')}${tokenParam}`;\n\n conn.eventSource = new EventSource(streamUrl, {\n withCredentials,\n });\n\n setupEventHandlers(conn.eventSource, eventNames, handlers, onOpen, onError);\n setupSubscriptionChange(conn.eventSource, onSubscriptionChange);\n setupReconnect(onReconnect);\n };\n\n init().catch(() =>\n {\n // Don't resurrect a torn-down subscription via reconnect.\n if (conn.closed)\n {\n return;\n }\n\n state = 'error';\n attemptReconnect(onReconnect);\n });\n }\n\n function setupEventHandlers(\n es: EventSource,\n names: string[],\n handlerMap: SSESubscribeOptions<TRouter>['handlers'],\n onOpenCb?: () => void,\n onErrorCb?: (error: Event) => void,\n )\n {\n es.onopen = () =>\n {\n state = 'open';\n conn.reconnectAttempts = 0;\n onOpenCb?.();\n };\n\n es.onerror = (error) =>\n {\n state = 'error';\n onErrorCb?.(error);\n };\n\n // Handle connected event (server sends this on connection)\n es.addEventListener('connected', (e: MessageEvent) =>\n {\n try\n {\n const data = JSON.parse(e.data);\n console.debug('[SSE] Connected:', data);\n }\n catch\n {\n // Ignore parse errors\n }\n });\n\n // Handle ping (keep-alive)\n es.addEventListener('ping', () =>\n {\n // Ping received, connection is alive\n });\n\n // Register handlers for each event\n for (const eventName of names)\n {\n const handler = (handlerMap as Record<string, ((payload: unknown) => void) | undefined>)[eventName];\n\n if (!handler)\n {\n continue;\n }\n\n es.addEventListener(eventName, (e: MessageEvent) =>\n {\n try\n {\n const message: SSEMessage = JSON.parse(e.data);\n handler(message.data);\n }\n catch (err)\n {\n console.error(`[SSE] Failed to parse event \"${eventName}\":`, err);\n }\n });\n }\n }\n\n // The server's grant/revoke frames (data `{ event }`); ignored without a callback.\n function setupSubscriptionChange(\n es: EventSource,\n onChange?: SSESubscribeOptions<TRouter>['onSubscriptionChange'],\n )\n {\n if (!onChange)\n {\n return;\n }\n\n const frames = { 'subscription.granted': 'granted', 'subscription.revoked': 'revoked' } as const;\n\n for (const [frame, status] of Object.entries(frames))\n {\n es.addEventListener(frame, (e: MessageEvent) =>\n {\n let event: InferEventNames<TRouter>;\n try\n {\n ({ event } = JSON.parse(e.data));\n }\n catch (err)\n {\n console.error(`[SSE] Failed to parse \"${frame}\" frame:`, err);\n\n return;\n }\n\n onChange({ event, status });\n });\n }\n }\n\n function setupReconnect(onReconnectCb?: (attempt: number) => void)\n {\n if (!conn.eventSource)\n {\n return;\n }\n\n const currentEs = conn.eventSource;\n const originalOnError = currentEs.onerror;\n\n currentEs.onerror = (error) =>\n {\n if (originalOnError)\n {\n (originalOnError as (ev: Event) => void)(error);\n }\n\n // Token-auth 사용 시 브라우저 auto-retry는 소비된 토큰으로 재시도하므로\n // 즉시 close하고 우리 reconnect로 새 토큰 발급받아 재연결\n if (reconnect && acquireToken)\n {\n currentEs.close();\n attemptReconnect(onReconnectCb);\n }\n else if (reconnect && currentEs.readyState === EventSource.CLOSED)\n {\n attemptReconnect(onReconnectCb);\n }\n };\n }\n\n function attemptReconnect(onReconnectCb?: (attempt: number) => void)\n {\n if (conn.closed || !reconnect)\n {\n return;\n }\n\n if (maxReconnectAttempts > 0 && conn.reconnectAttempts >= maxReconnectAttempts)\n {\n closeConn(conn);\n\n return;\n }\n\n conn.reconnectAttempts++;\n onReconnectCb?.(conn.reconnectAttempts);\n\n conn.reconnectTimer = setTimeout(() =>\n {\n connect();\n }, reconnectDelay);\n }\n\n // Start connection\n connect();\n\n // Return unsubscribe function\n return () =>\n {\n closeConn(conn);\n };\n }\n\n function getState(): SSEConnectionState\n {\n return state;\n }\n\n function close()\n {\n if (active)\n {\n closeConn(active);\n }\n else\n {\n state = 'closed';\n }\n }\n\n return {\n subscribe,\n getState,\n close,\n };\n}\n\n/**\n * Simple subscribe function for one-off subscriptions\n *\n * @example\n * ```typescript\n * import { subscribeToEvents } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * // Using defaults\n * const unsubscribe = subscribeToEvents<EventRouter>(\n * ['userCreated', 'orderPlaced'],\n * {\n * userCreated: (payload) => console.log('User:', payload),\n * orderPlaced: (payload) => console.log('Order:', payload),\n * }\n * );\n *\n * // With custom host\n * const unsubscribe = subscribeToEvents<EventRouter>(\n * ['userCreated'],\n * { userCreated: (payload) => console.log('User:', payload) },\n * { host: 'https://api.example.com' }\n * );\n * ```\n */\nexport function subscribeToEvents<TRouter extends EventRouterDef<any>>(\n events: InferEventNames<TRouter>[],\n handlers: SSESubscribeOptions<TRouter>['handlers'],\n options?: SSEClientConfig,\n): SSEUnsubscribe\n{\n const client = createSSEClient<TRouter>(options);\n\n return client.subscribe({\n events,\n handlers,\n });\n}\n\n// ============================================================================\n// Auth SSE Client\n// ============================================================================\n\n/**\n * SSE client configuration for authenticated connections\n *\n * Same as SSEClientConfig but without acquireToken (auto-configured).\n */\nexport interface AuthSSEClientConfig extends Omit<SSEClientConfig, 'acquireToken'>\n{\n /**\n * RPC proxy base URL for token acquisition\n * @default '/api/rpc'\n */\n rpcBaseUrl?: string;\n}\n\n/**\n * Headers of the one-time token request.\n *\n * The token route is a cookie-authenticated POST through the RPC proxy, so the\n * @spfn/auth CSRF rule applies to it exactly as to any other mutation: the\n * readable `spfn_csrf` cookie family is mirrored into `x-spfn-csrf`, the way\n * the api client does. Without the header, `SPFN_AUTH_CSRF=enforce` refuses\n * the request and the stream never opens.\n *\n * @internal Exported for tests\n */\nexport function tokenRequestHeaders(cookies: Iterable<[string, string]> = documentCookieEntries()): Record<string, string>\n{\n const csrf = csrfHeaderValue(cookies);\n\n return csrf === undefined\n ? { 'Content-Type': 'application/json' }\n : { 'Content-Type': 'application/json', [CSRF_HEADER]: csrf };\n}\n\n/**\n * Create SSE client with built-in token authentication\n *\n * Acquires one-time SSE tokens via RPC proxy automatically, sending the\n * `x-spfn-csrf` header whenever a `spfn_csrf` cookie is present.\n * Requires eventRouteMap to be merged into RPC proxy config.\n *\n * @example\n * ```typescript\n * import { createAuthSSEClient } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * const client = createAuthSSEClient<EventRouter>();\n *\n * client.subscribe({\n * events: ['userCreated'],\n * handlers: {\n * userCreated: (payload) => console.log(payload),\n * },\n * });\n * ```\n */\nexport function createAuthSSEClient<TRouter extends EventRouterDef<any>>(\n config: AuthSSEClientConfig = {},\n): SSEClient<TRouter>\n{\n const { rpcBaseUrl = '/api/rpc', ...sseConfig } = config;\n\n return createSSEClient<TRouter>({\n ...sseConfig,\n acquireToken: async () =>\n {\n const res = await fetch(`${rpcBaseUrl}/eventsToken`, {\n method: 'POST',\n credentials: 'include',\n headers: tokenRequestHeaders(),\n body: JSON.stringify({}),\n });\n\n if (!res.ok)\n {\n throw new Error(`Failed to acquire SSE token: ${res.status}`);\n }\n\n const data = await res.json();\n\n return data.token;\n },\n });\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/nextjs/client/csrf.ts","../../../src/event/sse/client.ts"],"names":[],"mappings":"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","file":"client.js","sourcesContent":["/**\n * CSRF token transport for the RPC client\n *\n * The Next.js proxy sets a readable (non-HttpOnly) cookie carrying an HMAC of\n * the session's key id; this module mirrors it into a request header. The proxy\n * never compares cookie against header — it recomputes the value from the\n * session — so the cookie is transport only and reading it here is not a trust\n * decision. See the CSRF section of the @spfn/auth README.\n */\n\n/**\n * Header the readable CSRF cookie is mirrored into.\n *\n * Must match CSRF_HEADER in @spfn/auth. It is duplicated rather than imported\n * because @spfn/core cannot depend on @spfn/auth.\n */\nexport const CSRF_HEADER = 'x-spfn-csrf';\n\n/**\n * Cookie names carrying a CSRF token.\n *\n * @spfn/auth suffixes its cookie names with the server port so several local dev\n * instances on the same host do not overwrite each other's session, and the\n * browser cannot know that port — so match the family, not one name. Every\n * candidate is sent and the proxy accepts the request if any of them recomputes\n * to the expected value; a candidate an attacker tossed in never will.\n */\nconst CSRF_COOKIE_PATTERN = /^spfn_csrf(_\\d+)?$/;\n\n/**\n * Values that can be put in a header at all: printable ASCII, no space.\n *\n * A cookie jar is attacker-writable in the very case this feature exists for, and\n * `new Headers()` rejects a value outside Latin-1 — so a sibling subdomain that\n * tosses `spfn_csrf=한` would make every RPC call throw before it was sent, not\n * merely fail the check. A genuine token is 64 hex characters; anything that\n * cannot be a header value cannot be one.\n */\nconst HEADER_SAFE_VALUE = /^[!-~]+$/;\n\n/**\n * Upper bound on candidates sent, so a cookie-flooded jar stays a bounded header.\n *\n * Deliberately generous. The server recomputes the expected value once and then\n * only compares fixed-length strings, so extra candidates cost it almost nothing,\n * while every candidate dropped here is a chance to lock the user out of every\n * mutation — the far worse failure. Must not exceed the matching bound in\n * @spfn/auth's `matchesCsrfToken`, or values sent would never be looked at.\n */\nconst MAX_CANDIDATES = 32;\n\n/**\n * Choose which candidates to send when the jar holds more than the cap.\n *\n * `document.cookie` exposes neither Domain nor Path, so a tossed cookie is\n * indistinguishable from the genuine one here — the proxy is what tells them\n * apart, by recomputing. What this can do is refuse to drop the genuine value on\n * the floor. Browsers order the jar by path length descending, then by age\n * ascending, so a flood pushes the genuine cookie towards the end (attacker\n * cookies on a longer path) or towards the start (attacker cookies written later\n * on the same path). Keeping both ends survives either, where a plain prefix\n * survives only one.\n *\n * A jar flooded far past the cap is a denial of service that no client-side\n * choice fully prevents; the structural fix is a `__Host-` prefixed cookie name,\n * which a sibling subdomain cannot write at all.\n */\nfunction selectCandidates(values: string[]): string[]\n{\n // Identical duplicates carry no extra information, so collapsing them buys\n // back slots that a flood of one repeated value would otherwise burn.\n const unique = Array.from(new Set(values));\n\n if (unique.length <= MAX_CANDIDATES)\n {\n return unique;\n }\n\n const half = MAX_CANDIDATES / 2;\n\n return [...unique.slice(0, half), ...unique.slice(-half)];\n}\n\n/**\n * Collect the CSRF token candidates from cookie entries.\n *\n * Takes entries rather than a map because a name can legitimately appear twice:\n * a sibling subdomain that tosses `spfn_csrf` for the parent domain leaves the\n * browser holding two cookies of that name, and folding them into a map would\n * throw away whichever the tossed one displaced — locking the user out of every\n * mutation. Sending both keeps the real one in play, and the tossed one is inert\n * because the proxy recomputes what it expects.\n *\n * @returns Header value (comma-separated), or undefined when no cookie exists\n */\nexport function csrfHeaderValue(cookies: Iterable<[string, string]>): string | undefined\n{\n // A comma separates candidates on the wire, so a value containing one would\n // arrive as several. That cannot be a genuine token — they are hex — but it\n // would let a single tossed cookie spend the receiver's whole candidate\n // budget and bury the real value behind it. Drop those, and anything that\n // could not be a header value at all.\n const values = Array.from(cookies)\n .filter(([name, value]) => CSRF_COOKIE_PATTERN.test(name)\n && HEADER_SAFE_VALUE.test(value)\n && !value.includes(','))\n .map(([, value]) => value);\n\n const candidates = selectCandidates(values);\n\n return candidates.length > 0 ? candidates.join(',') : undefined;\n}\n\n/**\n * Read the browser cookie jar as entries, duplicates preserved.\n *\n * Empty outside a browser (SSR reads the jar it forwards instead).\n */\nexport function documentCookieEntries(): Array<[string, string]>\n{\n if (typeof document === 'undefined' || !document.cookie)\n {\n return [];\n }\n\n const entries: Array<[string, string]> = [];\n\n for (const pair of document.cookie.split(';'))\n {\n const separator = pair.indexOf('=');\n\n if (separator > 0)\n {\n entries.push([pair.slice(0, separator).trim(), pair.slice(separator + 1).trim()]);\n }\n }\n\n return entries;\n}\n","/**\n * SSE Client\n *\n * Type-safe EventSource wrapper for event subscription\n *\n * @example\n * ```typescript\n * import { createSSEClient } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * // Uses defaults: NEXT_PUBLIC_SPFN_API_URL + /events/stream\n * const client = createSSEClient<EventRouter>();\n *\n * // Or with custom host/pathname\n * const client = createSSEClient<EventRouter>({\n * host: 'https://api.example.com',\n * pathname: '/sse',\n * });\n *\n * // With token authentication (recommended: use createAuthSSEClient)\n * import { createAuthSSEClient } from '@spfn/core/event/sse/client';\n * const client = createAuthSSEClient<EventRouter>();\n *\n * const unsubscribe = client.subscribe({\n * events: ['userCreated', 'orderPlaced'],\n * handlers: {\n * userCreated: (payload) => console.log('User:', payload.userId),\n * orderPlaced: (payload) => console.log('Order:', payload.orderId),\n * },\n * });\n *\n * // Later: cleanup\n * unsubscribe();\n * ```\n */\n\nimport type { EventRouterDef, InferEventNames } from '../router';\nimport { CSRF_HEADER, csrfHeaderValue, documentCookieEntries } from '../../nextjs/client/csrf';\nimport type {\n SSEClientConfig,\n SSESubscribeOptions,\n SSEUnsubscribe,\n SSEConnectionState,\n SSEMessage,\n} from './types';\n\n/**\n * SSE Client instance\n */\nexport interface SSEClient<TRouter extends EventRouterDef<any>>\n{\n /**\n * Subscribe to events\n */\n subscribe(options: SSESubscribeOptions<TRouter>): SSEUnsubscribe;\n\n /**\n * Get current connection state\n */\n getState(): SSEConnectionState;\n\n /**\n * Close all connections\n */\n close(): void;\n}\n\n/**\n * Connect-query names the stream itself reads; a `query` entry may not shadow them.\n */\nconst RESERVED_QUERY_NAMES = ['events', 'token'];\n\n/**\n * The `query` option as `&name=value` pairs, both sides encoded. A reserved\n * name is refused before anything connects: the server would read it as the\n * event list or the token.\n */\nfunction connectQuery(query: Record<string, string> = {}): string\n{\n const reserved = Object.keys(query).filter(name => RESERVED_QUERY_NAMES.includes(name));\n\n if (reserved.length > 0)\n {\n throw new Error(\n `SSE subscribe: query names ${reserved.map(name => `\"${name}\"`).join(', ')} are reserved. `\n + `The stream reads ${RESERVED_QUERY_NAMES.map(name => `\"${name}\"`).join(' and ')} from its connect query itself. `\n + 'Rename the field.',\n );\n }\n\n return Object.entries(query)\n .map(([name, value]) => `&${encodeURIComponent(name)}=${encodeURIComponent(value)}`)\n .join('');\n}\n\n/**\n * Default SSE configuration\n */\nconst SSE_DEFAULTS = {\n host: typeof process !== 'undefined'\n ? (process.env.NEXT_PUBLIC_SPFN_API_URL || 'http://localhost:8790')\n : 'http://localhost:8790',\n pathname: '/events/stream',\n} as const;\n\n/**\n * Create type-safe SSE client\n *\n * @example\n * ```typescript\n * // Uses defaults (NEXT_PUBLIC_SPFN_API_URL + /events/stream)\n * const client = createSSEClient<EventRouter>();\n *\n * // Or with custom configuration\n * const client = createSSEClient<EventRouter>({\n * host: 'https://api.example.com',\n * pathname: '/sse',\n * reconnect: true,\n * reconnectDelay: 3000,\n * });\n *\n * // Subscribe to events\n * const unsubscribe = client.subscribe({\n * events: ['userCreated', 'orderPlaced'],\n * handlers: {\n * userCreated: (payload) => {\n * console.log('New user:', payload.userId);\n * },\n * orderPlaced: (payload) => {\n * console.log('New order:', payload.orderId);\n * },\n * },\n * onOpen: () => console.log('Connected'),\n * onError: (err) => console.error('Error:', err),\n * onReconnect: (attempt) => console.log('Reconnecting...', attempt),\n * });\n *\n * // Cleanup\n * unsubscribe();\n * ```\n */\n/**\n * Per-subscription connection resources.\n *\n * Grouped so an in-flight async connect can be cancelled atomically and\n * `close()` can tear down whichever subscription is currently active —\n * the shared module-level EventSource used to leak across the token await.\n */\ninterface SSEConnection\n{\n eventSource: EventSource | null;\n reconnectTimer: ReturnType<typeof setTimeout> | null;\n reconnectAttempts: number;\n closed: boolean;\n onClose?: () => void;\n}\n\nexport function createSSEClient<TRouter extends EventRouterDef<any>>(\n config: SSEClientConfig = {},\n): SSEClient<TRouter>\n{\n const {\n url,\n host = SSE_DEFAULTS.host,\n pathname = SSE_DEFAULTS.pathname,\n reconnect = true,\n reconnectDelay = 3000,\n maxReconnectAttempts = 0,\n withCredentials = false,\n acquireToken,\n } = config;\n\n // Build base URL: url takes precedence, otherwise host + pathname\n const baseUrl = url || `${host}${pathname}`;\n\n let state: SSEConnectionState = 'closed';\n let active: SSEConnection | null = null;\n\n // Idempotent teardown: closes the connection, fires onClose exactly once,\n // and clears the active slot if this was the live connection.\n function closeConn(conn: SSEConnection)\n {\n if (conn.closed)\n {\n return;\n }\n\n conn.closed = true;\n\n if (conn.reconnectTimer)\n {\n clearTimeout(conn.reconnectTimer);\n conn.reconnectTimer = null;\n }\n\n if (conn.eventSource)\n {\n conn.eventSource.close();\n conn.eventSource = null;\n }\n\n if (active === conn)\n {\n active = null;\n state = 'closed';\n }\n\n conn.onClose?.();\n }\n\n function subscribe(options: SSESubscribeOptions<TRouter>): SSEUnsubscribe\n {\n const { events, handlers, onOpen, onError, onReconnect, onClose, onSubscriptionChange } = options;\n const queryParam = connectQuery(options.query);\n\n // A new subscription supersedes any previous one on this client.\n if (active)\n {\n closeConn(active);\n }\n\n const conn: SSEConnection = {\n eventSource: null,\n reconnectTimer: null,\n reconnectAttempts: 0,\n closed: false,\n onClose,\n };\n active = conn;\n\n const eventNames = events as string[];\n\n function connect()\n {\n state = 'connecting';\n\n const init = async () =>\n {\n let tokenParam = '';\n\n if (acquireToken)\n {\n const token = await acquireToken();\n\n // Cancelled during the token await (e.g. StrictMode cleanup) —\n // don't open a connection that has no teardown waiting for it.\n if (conn.closed)\n {\n return;\n }\n\n tokenParam = `&token=${encodeURIComponent(token)}`;\n }\n\n if (conn.closed)\n {\n return;\n }\n\n const streamUrl = `${baseUrl}?events=${eventNames.join(',')}${queryParam}${tokenParam}`;\n\n conn.eventSource = new EventSource(streamUrl, {\n withCredentials,\n });\n\n setupEventHandlers(conn.eventSource, eventNames, handlers, onOpen, onError);\n setupSubscriptionChange(conn.eventSource, onSubscriptionChange);\n setupReconnect(onReconnect);\n };\n\n init().catch(() =>\n {\n // Don't resurrect a torn-down subscription via reconnect.\n if (conn.closed)\n {\n return;\n }\n\n state = 'error';\n attemptReconnect(onReconnect);\n });\n }\n\n function setupEventHandlers(\n es: EventSource,\n names: string[],\n handlerMap: SSESubscribeOptions<TRouter>['handlers'],\n onOpenCb?: () => void,\n onErrorCb?: (error: Event) => void,\n )\n {\n es.onopen = () =>\n {\n state = 'open';\n conn.reconnectAttempts = 0;\n onOpenCb?.();\n };\n\n es.onerror = (error) =>\n {\n state = 'error';\n onErrorCb?.(error);\n };\n\n // Handle connected event (server sends this on connection)\n es.addEventListener('connected', (e: MessageEvent) =>\n {\n try\n {\n const data = JSON.parse(e.data);\n console.debug('[SSE] Connected:', data);\n }\n catch\n {\n // Ignore parse errors\n }\n });\n\n // Handle ping (keep-alive)\n es.addEventListener('ping', () =>\n {\n // Ping received, connection is alive\n });\n\n // Register handlers for each event\n for (const eventName of names)\n {\n const handler = (handlerMap as Record<string, ((payload: unknown) => void) | undefined>)[eventName];\n\n if (!handler)\n {\n continue;\n }\n\n es.addEventListener(eventName, (e: MessageEvent) =>\n {\n try\n {\n const message: SSEMessage = JSON.parse(e.data);\n handler(message.data);\n }\n catch (err)\n {\n console.error(`[SSE] Failed to parse event \"${eventName}\":`, err);\n }\n });\n }\n }\n\n // The server's grant/revoke frames (data `{ event }`); ignored without a callback.\n function setupSubscriptionChange(\n es: EventSource,\n onChange?: SSESubscribeOptions<TRouter>['onSubscriptionChange'],\n )\n {\n if (!onChange)\n {\n return;\n }\n\n const frames = { 'subscription.granted': 'granted', 'subscription.revoked': 'revoked' } as const;\n\n for (const [frame, status] of Object.entries(frames))\n {\n es.addEventListener(frame, (e: MessageEvent) =>\n {\n let event: InferEventNames<TRouter>;\n try\n {\n ({ event } = JSON.parse(e.data));\n }\n catch (err)\n {\n console.error(`[SSE] Failed to parse \"${frame}\" frame:`, err);\n\n return;\n }\n\n onChange({ event, status });\n });\n }\n }\n\n function setupReconnect(onReconnectCb?: (attempt: number) => void)\n {\n if (!conn.eventSource)\n {\n return;\n }\n\n const currentEs = conn.eventSource;\n const originalOnError = currentEs.onerror;\n\n currentEs.onerror = (error) =>\n {\n if (originalOnError)\n {\n (originalOnError as (ev: Event) => void)(error);\n }\n\n // Token-auth 사용 시 브라우저 auto-retry는 소비된 토큰으로 재시도하므로\n // 즉시 close하고 우리 reconnect로 새 토큰 발급받아 재연결\n if (reconnect && acquireToken)\n {\n currentEs.close();\n attemptReconnect(onReconnectCb);\n }\n else if (reconnect && currentEs.readyState === EventSource.CLOSED)\n {\n attemptReconnect(onReconnectCb);\n }\n };\n }\n\n function attemptReconnect(onReconnectCb?: (attempt: number) => void)\n {\n if (conn.closed || !reconnect)\n {\n return;\n }\n\n if (maxReconnectAttempts > 0 && conn.reconnectAttempts >= maxReconnectAttempts)\n {\n closeConn(conn);\n\n return;\n }\n\n conn.reconnectAttempts++;\n onReconnectCb?.(conn.reconnectAttempts);\n\n conn.reconnectTimer = setTimeout(() =>\n {\n connect();\n }, reconnectDelay);\n }\n\n // Start connection\n connect();\n\n // Return unsubscribe function\n return () =>\n {\n closeConn(conn);\n };\n }\n\n function getState(): SSEConnectionState\n {\n return state;\n }\n\n function close()\n {\n if (active)\n {\n closeConn(active);\n }\n else\n {\n state = 'closed';\n }\n }\n\n return {\n subscribe,\n getState,\n close,\n };\n}\n\n/**\n * Simple subscribe function for one-off subscriptions\n *\n * @example\n * ```typescript\n * import { subscribeToEvents } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * // Using defaults\n * const unsubscribe = subscribeToEvents<EventRouter>(\n * ['userCreated', 'orderPlaced'],\n * {\n * userCreated: (payload) => console.log('User:', payload),\n * orderPlaced: (payload) => console.log('Order:', payload),\n * }\n * );\n *\n * // With custom host\n * const unsubscribe = subscribeToEvents<EventRouter>(\n * ['userCreated'],\n * { userCreated: (payload) => console.log('User:', payload) },\n * { host: 'https://api.example.com' }\n * );\n * ```\n */\nexport function subscribeToEvents<TRouter extends EventRouterDef<any>>(\n events: InferEventNames<TRouter>[],\n handlers: SSESubscribeOptions<TRouter>['handlers'],\n options?: SSEClientConfig,\n): SSEUnsubscribe\n{\n const client = createSSEClient<TRouter>(options);\n\n return client.subscribe({\n events,\n handlers,\n });\n}\n\n// ============================================================================\n// Auth SSE Client\n// ============================================================================\n\n/**\n * SSE client configuration for authenticated connections\n *\n * Same as SSEClientConfig but without acquireToken (auto-configured).\n */\nexport interface AuthSSEClientConfig extends Omit<SSEClientConfig, 'acquireToken'>\n{\n /**\n * RPC proxy base URL for token acquisition\n * @default '/api/rpc'\n */\n rpcBaseUrl?: string;\n}\n\n/**\n * Headers of the one-time token request.\n *\n * The token route is a cookie-authenticated POST through the RPC proxy, so the\n * @spfn/auth CSRF rule applies to it exactly as to any other mutation: the\n * readable `spfn_csrf` cookie family is mirrored into `x-spfn-csrf`, the way\n * the api client does. Without the header, `SPFN_AUTH_CSRF=enforce` refuses\n * the request and the stream never opens.\n *\n * @internal Exported for tests\n */\nexport function tokenRequestHeaders(cookies: Iterable<[string, string]> = documentCookieEntries()): Record<string, string>\n{\n const csrf = csrfHeaderValue(cookies);\n\n return csrf === undefined\n ? { 'Content-Type': 'application/json' }\n : { 'Content-Type': 'application/json', [CSRF_HEADER]: csrf };\n}\n\n/**\n * Create SSE client with built-in token authentication\n *\n * Acquires one-time SSE tokens via RPC proxy automatically, sending the\n * `x-spfn-csrf` header whenever a `spfn_csrf` cookie is present.\n * Requires eventRouteMap to be merged into RPC proxy config.\n *\n * @example\n * ```typescript\n * import { createAuthSSEClient } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * const client = createAuthSSEClient<EventRouter>();\n *\n * client.subscribe({\n * events: ['userCreated'],\n * handlers: {\n * userCreated: (payload) => console.log(payload),\n * },\n * });\n * ```\n */\nexport function createAuthSSEClient<TRouter extends EventRouterDef<any>>(\n config: AuthSSEClientConfig = {},\n): SSEClient<TRouter>\n{\n const { rpcBaseUrl = '/api/rpc', ...sseConfig } = config;\n\n return createSSEClient<TRouter>({\n ...sseConfig,\n acquireToken: async () =>\n {\n const res = await fetch(`${rpcBaseUrl}/eventsToken`, {\n method: 'POST',\n credentials: 'include',\n headers: tokenRequestHeaders(),\n body: JSON.stringify({}),\n });\n\n if (!res.ok)\n {\n throw new Error(`Failed to acquire SSE token: ${res.status}`);\n }\n\n const data = await res.json();\n\n return data.token;\n },\n });\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Context } from 'hono';
|
|
2
|
-
import { E as EventRouterDef } from '../../router-
|
|
3
|
-
import { d as SSEHandlerConfig } from '../../types-
|
|
4
|
-
export { e as SSEAuthConfig, S as SSEClientConfig, c as SSEConnectionState, f as SSEEventHandler, g as SSEEventHandlers, h as SSEHandlerAuthConfig, i as SSEMessage, a as SSESubscribeOptions, j as SSESubscriptionChange, b as SSEUnsubscribe } from '../../types-
|
|
2
|
+
import { E as EventRouterDef } from '../../router-D5B0cpVN.js';
|
|
3
|
+
import { d as SSEHandlerConfig } from '../../types-04gu3uMj.js';
|
|
4
|
+
export { e as SSEAuthConfig, S as SSEClientConfig, c as SSEConnectionState, f as SSEEventHandler, g as SSEEventHandlers, h as SSEHandlerAuthConfig, i as SSEMessage, a as SSESubscribeOptions, j as SSESubscriptionChange, b as SSEUnsubscribe } from '../../types-04gu3uMj.js';
|
|
5
5
|
import { S as SSETokenManager } from '../../token-manager-Ckjt2-qq.js';
|
|
6
6
|
export { C as CacheTokenStore, a as SSEToken, b as SSETokenManagerConfig, c as SSETokenStore } from '../../token-manager-Ckjt2-qq.js';
|
|
7
7
|
import '@sinclair/typebox';
|
package/dist/event/sse/index.js
CHANGED
|
@@ -54,7 +54,7 @@ function createConnectionRegistry() {
|
|
|
54
54
|
const bySubject = /* @__PURE__ */ new Map();
|
|
55
55
|
const pendingBySubject = /* @__PURE__ */ new Map();
|
|
56
56
|
const beginAdmission = (subject, signal) => {
|
|
57
|
-
const changes =
|
|
57
|
+
const changes = [];
|
|
58
58
|
addToSet(pendingBySubject, subject, changes);
|
|
59
59
|
const end = endOnce(signal, () => removeFromSet(pendingBySubject, subject, changes));
|
|
60
60
|
return {
|
|
@@ -69,11 +69,15 @@ function createConnectionRegistry() {
|
|
|
69
69
|
}
|
|
70
70
|
};
|
|
71
71
|
};
|
|
72
|
-
const change = (kind, subject, eventName) => {
|
|
72
|
+
const change = (kind, subject, eventName, match) => {
|
|
73
73
|
const pending = pendingBySubject.get(subject);
|
|
74
74
|
const connections = bySubject.get(subject);
|
|
75
|
-
pending?.forEach((changes) => changes.
|
|
76
|
-
connections?.forEach((connection) =>
|
|
75
|
+
pending?.forEach((changes) => changes.push({ kind, eventName, match }));
|
|
76
|
+
connections?.forEach((connection) => {
|
|
77
|
+
if (matchesRequest(match, connection.request)) {
|
|
78
|
+
(kind === "grant" ? grantOn : revokeOn)(connection, eventName);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
77
81
|
if (!pending && !connections) {
|
|
78
82
|
registryLogger.debug("SSE subscription change for a subject with no open connection ignored", {
|
|
79
83
|
event: eventName,
|
|
@@ -91,8 +95,8 @@ function createConnectionRegistry() {
|
|
|
91
95
|
/** Open connections only — a pending admission is not counted. */
|
|
92
96
|
count: (subject) => bySubject.get(subject)?.size ?? 0,
|
|
93
97
|
pendingCount: (subject) => pendingBySubject.get(subject)?.size ?? 0,
|
|
94
|
-
grant: (subject, eventName) => change("grant", subject, eventName),
|
|
95
|
-
revoke: (subject, eventName) => change("revoke", subject, eventName)
|
|
98
|
+
grant: (subject, eventName, match) => change("grant", subject, eventName, match),
|
|
99
|
+
revoke: (subject, eventName, match) => change("revoke", subject, eventName, match)
|
|
96
100
|
};
|
|
97
101
|
}
|
|
98
102
|
function endOnce(signal, remove) {
|
|
@@ -124,8 +128,14 @@ function removeFromSet(map, key, value) {
|
|
|
124
128
|
map.delete(key);
|
|
125
129
|
}
|
|
126
130
|
}
|
|
131
|
+
function matchesRequest(match, request) {
|
|
132
|
+
return match === void 0 || Object.entries(match.query).every(([field, value]) => Object.hasOwn(request.query, field) && request.query[field] === value);
|
|
133
|
+
}
|
|
127
134
|
function applyRecorded(connection, changes) {
|
|
128
|
-
changes.forEach((kind, eventName) => {
|
|
135
|
+
changes.forEach(({ kind, eventName, match }) => {
|
|
136
|
+
if (!matchesRequest(match, connection.request)) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
129
139
|
const [from, to] = kind === "grant" ? [connection.denied, connection.allowed] : [connection.allowed, connection.denied];
|
|
130
140
|
if (from.delete(eventName)) {
|
|
131
141
|
to.add(eventName);
|
|
@@ -214,8 +224,8 @@ function createSSEHandler(router, config = {}, tokenManager) {
|
|
|
214
224
|
function registerSubscriptionControls(router, registry) {
|
|
215
225
|
for (const eventName of router.eventNames) {
|
|
216
226
|
router.events[eventName]._registerSubscriptionControl({
|
|
217
|
-
grant: (subject) => registry.grant(subject, eventName),
|
|
218
|
-
revoke: (subject) => registry.revoke(subject, eventName)
|
|
227
|
+
grant: (subject, match) => registry.grant(subject, eventName, match),
|
|
228
|
+
revoke: (subject, match) => registry.revoke(subject, eventName, match)
|
|
219
229
|
});
|
|
220
230
|
}
|
|
221
231
|
}
|
|
@@ -1 +1 @@
|
|
|
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":";;;;;;;AAgDO,SAAS,mBAAA,CACZ,MAAA,EACA,QAAA,EACA,OAAA,EAEJ;AACI,EAAA,MAAM,UAAsB,EAAC;AAC7B,EAAA,IAAI,QAAA,GAAW,KAAA;AACf,EAAA,IAAI,MAAA,GAAS,KAAA;AAEb,EAAA,MAAM,QAAQ,YACd;AACI,IAAA,IAAI,YAAY,MAAA,EAChB;AACI,MAAA;AAAA,IACJ;AAEA,IAAA,QAAA,GAAW,IAAA;AAEX,IAAA,OAAO,OAAA,CAAQ,MAAA,GAAS,CAAA,IAAK,CAAC,MAAA,EAC9B;AACI,MAAA,MAAM,KAAA,GAAQ,QAAQ,KAAA,EAAM;AAC5B,MAAA,IACA;AACI,QAAA,MAAM,MAAA,CAAO,SAAS,KAAK,CAAA;AAAA,MAC/B,SACO,GAAA,EACP;AACI,QAAA,QAAA,GAAW,KAAA;AACX,QAAA,OAAA,CAAQ,eAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,MAAA,CAAO,GAAG,CAAC,CAAA;AAExD,QAAA;AAAA,MACJ;AAAA,IACJ;AAEA,IAAA,QAAA,GAAW,KAAA;AAAA,EACf,CAAA;AAEA,EAAA,OAAO;AAAA,IACH,QAAQ,KAAA,EACR;AACI,MAAA,IAAI,MAAA,EACJ;AACI,QAAA;AAAA,MACJ;AAEA,MAAA,OAAA,CAAQ,KAAK,KAAK,CAAA;AAIlB,MAAA,IAAI,OAAA,CAAQ,SAAS,QAAA,EACrB;AACI,QAAA,OAAA,CAAQ,yCAAyC,CAAA;AAEjD,QAAA;AAAA,MACJ;AAEA,MAAA,KAAK,KAAA,EAAM;AAAA,IACf,CAAA;AAAA,IAEA,KAAA,GACA;AACI,MAAA,MAAA,GAAS,IAAA;AACT,MAAA,OAAA,CAAQ,MAAA,GAAS,CAAA;AAAA,IACrB,CAAA;AAAA,IAEA,IAAI,MAAA,GACJ;AACI,MAAA,OAAO,OAAA,CAAQ,MAAA;AAAA,IACnB;AAAA,GACJ;AACJ;ACnGA,IAAM,cAAA,GAAiB,MAAA,CAAO,KAAA,CAAM,gBAAgB,CAAA;AAG7C,IAAM,oBAAA,GAAuB,sBAAA;AAG7B,IAAM,oBAAA,GAAuB,sBAAA;AAsD7B,SAAS,wBAAA,GAChB;AACI,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAgC;AACtD,EAAA,MAAM,gBAAA,uBAAuB,GAAA,EAAkC;AAM/D,EAAA,MAAM,cAAA,GAAiB,CAAC,OAAA,EAAiB,MAAA,KACzC;AACI,IAAA,MAAM,OAAA,uBAA+B,GAAA,EAAI;AACzC,IAAA,QAAA,CAAS,gBAAA,EAAkB,SAAS,OAAO,CAAA;AAC3C,IAAA,MAAM,GAAA,GAAM,QAAQ,MAAA,EAAQ,MAAM,cAAc,gBAAA,EAAkB,OAAA,EAAS,OAAO,CAAC,CAAA;AAEnF,IAAA,OAAO;AAAA,MACH,MAAA,EAAQ,GAAA;AAAA,MACR,IAAA,EAAM,CAAC,UAAA,KACP;AACI,QAAA,IAAI,CAAC,KAAI,EACT;AACI,UAAA,OAAO,KAAA;AAAA,QACX;AAEA,QAAA,aAAA,CAAc,YAAY,OAAO,CAAA;AACjC,QAAA,QAAA,CAAS,SAAA,EAAW,SAAS,UAAU,CAAA;AAEvC,QAAA,OAAO,IAAA;AAAA,MACX;AAAA,KACJ;AAAA,EACJ,CAAA;AAEA,EAAA,MAAM,MAAA,GAAS,CAAC,IAAA,EAA0B,OAAA,EAAiB,SAAA,KAC3D;AACI,IAAA,MAAM,OAAA,GAAU,gBAAA,CAAiB,GAAA,CAAI,OAAO,CAAA;AAC5C,IAAA,MAAM,WAAA,GAAc,SAAA,CAAU,GAAA,CAAI,OAAO,CAAA;AACzC,IAAA,OAAA,EAAS,QAAQ,CAAA,OAAA,KAAW,OAAA,CAAQ,GAAA,CAAI,SAAA,EAAW,IAAI,CAAC,CAAA;AACxD,IAAA,WAAA,EAAa,OAAA,CAAQ,iBAAe,IAAA,KAAS,OAAA,GAAU,UAAU,QAAA,EAAU,UAAA,EAAY,SAAS,CAAC,CAAA;AAEjG,IAAA,IAAI,CAAC,OAAA,IAAW,CAAC,WAAA,EACjB;AACI,MAAA,cAAA,CAAe,MAAM,uEAAA,EAAyE;AAAA,QAC1F,KAAA,EAAO,SAAA;AAAA,QACP;AAAA,OACH,CAAA;AAAA,IACL;AAAA,EACJ,CAAA;AAEA,EAAA,OAAO;AAAA,IACH,cAAA;AAAA,IAEA,MAAA,EAAQ,CAAC,UAAA,KACT;AACI,MAAA,IAAI,WAAW,OAAA,EACf;AACI,QAAA,aAAA,CAAc,SAAA,EAAW,UAAA,CAAW,OAAA,EAAS,UAAU,CAAA;AAAA,MAC3D;AAAA,IACJ,CAAA;AAAA;AAAA,IAGA,OAAO,CAAC,OAAA,KAA4B,UAAU,GAAA,CAAI,OAAO,GAAG,IAAA,IAAQ,CAAA;AAAA,IAEpE,cAAc,CAAC,OAAA,KAA4B,iBAAiB,GAAA,CAAI,OAAO,GAAG,IAAA,IAAQ,CAAA;AAAA,IAElF,OAAO,CAAC,OAAA,EAAiB,cAA4B,MAAA,CAAO,OAAA,EAAS,SAAS,SAAS,CAAA;AAAA,IAEvF,QAAQ,CAAC,OAAA,EAAiB,cAA4B,MAAA,CAAO,QAAA,EAAU,SAAS,SAAS;AAAA,GAC7F;AACJ;AAQA,SAAS,OAAA,CAAQ,QAAqB,MAAA,EACtC;AACI,EAAA,IAAI,KAAA,GAAQ,KAAA;AACZ,EAAA,MAAM,MAAM,MACZ;AACI,IAAA,IAAI,KAAA,EACJ;AACI,MAAA,OAAO,KAAA;AAAA,IACX;AAEA,IAAA,KAAA,GAAQ,IAAA;AACR,IAAA,MAAA,CAAO,mBAAA,CAAoB,SAAS,GAAG,CAAA;AACvC,IAAA,MAAA,EAAO;AAEP,IAAA,OAAO,IAAA;AAAA,EACX,CAAA;AAEA,EAAA,MAAA,CAAO,iBAAiB,OAAA,EAAS,GAAA,EAAK,EAAE,IAAA,EAAM,MAAM,CAAA;AACpD,EAAA,IAAI,OAAO,OAAA,EACX;AACI,IAAA,GAAA,EAAI;AAAA,EACR;AAEA,EAAA,OAAO,GAAA;AACX;AAEA,SAAS,QAAA,CAAY,GAAA,EAA0B,GAAA,EAAa,KAAA,EAC5D;AACI,EAAA,MAAM,MAAM,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA,wBAAS,GAAA,EAAO;AACvC,EAAA,GAAA,CAAI,IAAI,KAAK,CAAA;AACb,EAAA,GAAA,CAAI,GAAA,CAAI,KAAK,GAAG,CAAA;AACpB;AAGA,SAAS,aAAA,CAAiB,GAAA,EAA0B,GAAA,EAAa,KAAA,EACjE;AACI,EAAA,MAAM,GAAA,GAAM,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA;AACvB,EAAA,GAAA,EAAK,OAAO,KAAK,CAAA;AAEjB,EAAA,IAAI,GAAA,EAAK,SAAS,CAAA,EAClB;AACI,IAAA,GAAA,CAAI,OAAO,GAAG,CAAA;AAAA,EAClB;AACJ;AAOA,SAAS,aAAA,CAAc,YAA2B,OAAA,EAClD;AACI,EAAA,OAAA,CAAQ,OAAA,CAAQ,CAAC,IAAA,EAAM,SAAA,KACvB;AACI,IAAA,MAAM,CAAC,IAAA,EAAM,EAAE,CAAA,GAAI,IAAA,KAAS,UACtB,CAAC,UAAA,CAAW,MAAA,EAAQ,UAAA,CAAW,OAAO,CAAA,GACtC,CAAC,UAAA,CAAW,OAAA,EAAS,WAAW,MAAM,CAAA;AAE5C,IAAA,IAAI,IAAA,CAAK,MAAA,CAAO,SAAS,CAAA,EACzB;AACI,MAAA,EAAA,CAAG,IAAI,SAAS,CAAA;AAAA,IACpB;AAAA,EACJ,CAAC,CAAA;AACL;AAKA,SAAS,OAAA,CAAQ,YAA2B,SAAA,EAC5C;AACI,EAAA,IAAI,CAAC,UAAA,CAAW,MAAA,CAAO,GAAA,CAAI,SAAS,CAAA,EACpC;AACI,IAAA;AAAA,EACJ;AAEA,EAAA,UAAA,CAAW,aAAa,GAAA,CAAI,SAAA,EAAW,UAAA,CAAW,SAAA,CAAU,SAAS,CAAC,CAAA;AACtE,EAAA,UAAA,CAAW,MAAA,CAAO,OAAO,SAAS,CAAA;AAClC,EAAA,UAAA,CAAW,OAAA,CAAQ,IAAI,SAAS,CAAA;AAChC,EAAA,UAAA,CAAW,MAAA,CAAO,sBAAsB,SAAS,CAAA;AACrD;AAQA,SAAS,QAAA,CAAS,YAA2B,SAAA,EAC7C;AACI,EAAA,IAAI,CAAC,UAAA,CAAW,OAAA,CAAQ,GAAA,CAAI,SAAS,CAAA,EACrC;AACI,IAAA;AAAA,EACJ;AAEA,EAAA,UAAA,CAAW,YAAA,CAAa,GAAA,CAAI,SAAS,CAAA,IAAI;AACzC,EAAA,UAAA,CAAW,YAAA,CAAa,OAAO,SAAS,CAAA;AACxC,EAAA,UAAA,CAAW,OAAA,CAAQ,OAAO,SAAS,CAAA;AACnC,EAAA,UAAA,CAAW,MAAA,CAAO,IAAI,SAAS,CAAA;AAC/B,EAAA,UAAA,CAAW,MAAA,CAAO,sBAAsB,SAAS,CAAA;AACrD;AAMO,SAAS,eAAe,UAAA,EAC/B;AACI,EAAA,UAAA,CAAW,YAAA,CAAa,OAAA,CAAQ,CAAA,WAAA,KAAe,WAAA,EAAa,CAAA;AAC5D,EAAA,UAAA,CAAW,aAAa,KAAA,EAAM;AAClC;;;AC1OA,IAAM,SAAA,GAAYA,MAAAA,CAAO,KAAA,CAAM,gBAAgB,CAAA;AAU/C,IAAM,UAAA,uBAAiB,OAAA,EAAqC;AAErD,SAAS,cAAA,CAAe,WAAmB,OAAA,EAClD;AACI,EAAA,IAAI,OAAA,KAAY,IAAA,IAAQ,OAAO,OAAA,KAAY,QAAA,EAC3C;AACI,IAAA,OAAO,KAAK,SAAA,CAAU,EAAE,OAAO,SAAA,EAAW,IAAA,EAAM,SAAS,CAAA;AAAA,EAC7D;AAEA,EAAA,IAAI,OAAA,GAAU,UAAA,CAAW,GAAA,CAAI,OAAO,CAAA;AACpC,EAAA,IAAI,CAAC,OAAA,EACL;AACI,IAAA,OAAA,uBAAc,GAAA,EAAoB;AAClC,IAAA,UAAA,CAAW,GAAA,CAAI,SAAS,OAAO,CAAA;AAAA,EACnC;AAEA,EAAA,IAAI,UAAA,GAAa,OAAA,CAAQ,GAAA,CAAI,SAAS,CAAA;AACtC,EAAA,IAAI,eAAe,MAAA,EACnB;AACI,IAAA,UAAA,GAAa,KAAK,SAAA,CAAU,EAAE,OAAO,SAAA,EAAW,IAAA,EAAM,SAAS,CAAA;AAC/D,IAAA,OAAA,CAAQ,GAAA,CAAI,WAAW,UAAU,CAAA;AAAA,EACrC;AAEA,EAAA,OAAO,UAAA;AACX;AAGA,IAAM,iBAAA,GAAoB,GAAA;AAgCnB,SAAS,gBAAA,CACZ,MAAA,EACA,MAAA,GAA2B,IAC3B,YAAA,EAEJ;AACI,EAAA,MAAM;AAAA;AAAA;AAAA;AAAA,IAIF,YAAA,GAAe,GAAA;AAAA,IACf,IAAA,EAAM,UAAA;AAAA,IACN,QAAA,GAAW;AAAA,GACf,GAAI,MAAA;AAEJ,EAAA,MAAM,WAAW,wBAAA,EAAyB;AAC1C,EAAA,4BAAA,CAA6B,QAAQ,QAAQ,CAAA;AAE7C,EAAA,MAAM,eAA6B,EAAE,MAAA,EAAQ,UAAA,EAAY,QAAA,EAAU,cAAc,QAAA,EAAS;AAE1F,EAAA,MAAM,OAAA,GAAU,OAAO,CAAA,KACvB;AACI,IAAA,MAAM,SAAA,GAAY,MAAM,eAAA,CAAgB,CAAA,EAAG,cAAc,YAAY,CAAA;AACrE,IAAA,IAAI,qBAAqB,QAAA,EACzB;AACI,MAAA,OAAO,SAAA;AAAA,IACX;AAEA,IAAA,IAAI,CAAA,CAAE,GAAA,CAAI,GAAA,CAAI,MAAA,CAAO,OAAA,EACrB;AAGI,MAAA,SAAA,CAAU,SAAS,MAAA,EAAO;AAE1B,MAAA,OAAO,IAAI,QAAA,CAAS,IAAA,EAAM,EAAE,MAAA,EAAQ,KAAK,CAAA;AAAA,IAC7C;AAGA,IAAA,CAAA,CAAE,MAAA,CAAO,qBAAqB,IAAI,CAAA;AAElC,IAAA,OAAO,SAAA,CAAU,CAAA,EAAG,CAAA,MAAA,KAAU,SAAA,CAAU,CAAA,EAAG,QAAQ,SAAA,EAAW,YAAY,CAAA,EAAG,OAAO,GAAA,KACpF;AACI,MAAA,SAAA,CAAU,MAAM,kBAAA,EAAoB;AAAA,QAChC,OAAO,GAAA,CAAI;AAAA,OACd,CAAA;AAAA,IACL,CAAC,CAAA;AAAA,EACL,CAAA;AAEA,EAAA,OAAO,MAAA,CAAO,OAAO,OAAA,EAAS;AAAA;AAAA,IAE1B,gBAAA,EAAkB,CAAC,OAAA,KAA4B,QAAA,CAAS,MAAM,OAAO,CAAA;AAAA;AAAA,IAGrE,kBAAA,EAAoB,CAAC,OAAA,KAA4B,QAAA,CAAS,aAAa,OAAO;AAAA,GACjF,CAAA;AACL;AAiCA,SAAS,4BAAA,CAA6B,QAA6B,QAAA,EACnE;AACI,EAAA,KAAA,MAAW,SAAA,IAAa,OAAO,UAAA,EAC/B;AACI,IAAA,MAAA,CAAO,MAAA,CAAO,SAAS,CAAA,CAAE,4BAAA,CAA6B;AAAA,MAClD,OAAO,CAAC,OAAA,KAAoB,QAAA,CAAS,KAAA,CAAM,SAAS,SAAS,CAAA;AAAA,MAC7D,QAAQ,CAAC,OAAA,KAAoB,QAAA,CAAS,MAAA,CAAO,SAAS,SAAS;AAAA,KAClE,CAAA;AAAA,EACL;AACJ;AAKA,eAAe,eAAA,CACX,CAAA,EACA,MAAA,EACA,YAAA,EAEJ;AACI,EAAA,MAAM,EAAE,MAAA,EAAQ,UAAA,EAAY,QAAA,EAAS,GAAI,MAAA;AAGzC,EAAA,MAAM,OAAA,GAAU,MAAM,iBAAA,CAAkB,CAAA,EAAG,YAAY,CAAA;AACvD,EAAA,IAAI,YAAY,KAAA,EAChB;AACI,IAAA,OAAO,EAAE,IAAA,CAAK,EAAE,KAAA,EAAO,yBAAA,IAA6B,GAAG,CAAA;AAAA,EAC3D;AACA,EAAA,IAAI,YAAY,IAAA,EAChB;AACI,IAAA,OAAO,EAAE,IAAA,CAAK,EAAE,KAAA,EAAO,0BAAA,IAA8B,GAAG,CAAA;AAAA,EAC5D;AACA,EAAA,IAAI,OAAA,EACJ;AACI,IAAA,CAAA,CAAE,GAAA,CAAI,cAAc,OAAO,CAAA;AAAA,EAC/B;AAGA,EAAA,MAAM,SAAA,GAAY,qBAAqB,CAAC,CAAA;AACxC,EAAA,IAAI,CAAC,SAAA,EACL;AACI,IAAA,OAAO,EAAE,IAAA,CAAK,EAAE,KAAA,EAAO,0BAAA,IAA8B,GAAG,CAAA;AAAA,EAC5D;AAEA,EAAA,MAAM,kBAAkB,MAAA,CAAO,UAAA;AAC/B,EAAA,MAAM,aAAA,GAAgB,UAAU,MAAA,CAAO,CAAA,CAAA,KAAK,CAAC,eAAA,CAAgB,QAAA,CAAS,CAAC,CAAC,CAAA;AACxE,EAAA,IAAI,aAAA,CAAc,SAAS,CAAA,EAC3B;AACI,IAAA,OAAO,EAAE,IAAA,CAAK;AAAA,MACV,KAAA,EAAO,qBAAA;AAAA,MACP,aAAA;AAAA,MACA,WAAA,EAAa;AAAA,OACd,GAAG,CAAA;AAAA,EACV;AAEA,EAAA,OAAO,YAAA;AAAA,IACH,OAAA,GAAU,SAAS,cAAA,CAAe,OAAA,EAAS,EAAE,GAAA,CAAI,GAAA,CAAI,MAAM,CAAA,GAAI,MAAA;AAAA,IAC/D,MAAM,mBAAA,CAAoB,CAAA,EAAG,MAAA,EAAQ,UAAA,EAAY,SAAS,SAAS;AAAA,GACvE;AACJ;AASA,eAAe,YAAA,CACX,SACA,KAAA,EAEJ;AACI,EAAA,IAAI,OAAA;AAEJ,EAAA,IACA;AACI,IAAA,OAAA,GAAU,MAAM,KAAA,EAAM;AAEtB,IAAA,OAAO,mBAAmB,QAAA,GAAW,OAAA,GAAU,EAAE,GAAG,SAAS,OAAA,EAAQ;AAAA,EACzE,CAAA,SACA;AAEI,IAAA,IAAI,CAAC,OAAA,IAAW,OAAA,YAAmB,QAAA,EACnC;AACI,MAAA,OAAA,EAAS,MAAA,EAAO;AAAA,IACpB;AAAA,EACJ;AACJ;AAOA,eAAe,mBAAA,CACX,CAAA,EACA,MAAA,EACA,UAAA,EACA,SACA,SAAA,EAEJ;AAEI,EAAA,MAAM,UAAA,GAAa,MAAM,eAAA,CAAgB,OAAA,EAAS,WAAW,UAAU,CAAA;AACvE,EAAA,IAAI,eAAe,IAAA,EACnB;AACI,IAAA,OAAO,EAAE,IAAA,CAAK,EAAE,KAAA,EAAO,yCAAA,IAA6C,GAAG,CAAA;AAAA,EAC3E;AAGA,EAAA,MAAM,OAAA,GAA4B,EAAE,KAAA,EAAO,CAAA,CAAE,GAAA,CAAI,KAAA,EAAM,EAAG,OAAA,EAAS,CAAA,CAAE,GAAA,CAAI,MAAA,EAAO,EAAE;AAClF,EAAA,MAAM,QAAA,GAAW,MAAM,0BAAA,CAA2B,OAAA,EAAS,SAAS,UAAU,CAAA;AAC9E,EAAA,IAAI,CAAC,QAAA,EACL;AACI,IAAA,OAAO,EAAE,IAAA,CAAK,EAAE,KAAA,EAAO,iCAAA,IAAqC,GAAG,CAAA;AAAA,EACnE;AAEA,EAAA,MAAM,OAAA,GAAU,OAAA,GACV,MAAM,iBAAA,CAAkB,MAAA,EAAQ,UAAA,EAAY,EAAE,OAAA,EAAS,SAAA,EAAW,QAAA,CAAS,SAAA,EAAW,OAAA,EAAS,CAAA,GAC/F,UAAA;AACN,EAAA,MAAM,MAAA,GAAS,UAAU,MAAA,CAAO,CAAA,CAAA,KAAK,CAAC,OAAA,CAAQ,QAAA,CAAS,CAAC,CAAC,CAAA;AAEzD,EAAA,SAAA,CAAU,MAAM,0BAAA,EAA4B;AAAA,IACxC,MAAA,EAAQ,OAAA;AAAA,IACR,YAAA,EAAc,MAAA;AAAA,IACd,SAAS,OAAA,IAAW,MAAA;AAAA,IACpB,QAAA,EAAU,EAAE,GAAA,CAAI,MAAA,CAAO,iBAAiB,CAAA,IAAK,CAAA,CAAE,GAAA,CAAI,MAAA,CAAO,WAAW;AAAA,GACxE,CAAA;AAED,EAAA,OAAO,EAAE,OAAA,EAAS,SAAA,EAAW,OAAA,EAAS,QAAQ,OAAA,EAAQ;AAC1D;AAOA,eAAe,0BAAA,CACX,OAAA,EACA,OAAA,EACA,UAAA,EAEJ;AACI,EAAA,IAAI,CAAC,OAAA,IAAW,CAAC,UAAA,EAAY,gBAAA,EAC7B;AACI,IAAA,OAAO,EAAE,WAAW,MAAA,EAAU;AAAA,EAClC;AAEA,EAAA,IACA;AACI,IAAA,OAAO,EAAE,SAAA,EAAW,MAAM,WAAW,gBAAA,CAAiB,OAAA,EAAS,OAAO,CAAA,EAAE;AAAA,EAC5E,CAAA,CAAA,MAEA;AACI,IAAA,SAAA,CAAU,IAAA,CAAK,+DAAA,EAA4D,EAAE,OAAA,EAAS,CAAA;AAEtF,IAAA,OAAO,IAAA;AAAA,EACX;AACJ;AAMA,eAAe,iBAAA,CACX,MAAA,EACA,MAAA,EACA,GAAA,EAEJ;AACI,EAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,GAAA,CAAI,OAAO,GAAA,CAAI,CAAA,SAAA,KAAa,eAAA,CAAgB,SAAA,EAAW,OAAO,MAAA,CAAO,SAAS,CAAA,EAAG,GAAG,CAAC,CAAC,CAAA;AAErH,EAAA,OAAO,OAAO,MAAA,CAAO,CAAC,GAAG,CAAA,KAAM,QAAA,CAAS,CAAC,CAAC,CAAA;AAC9C;AAMA,eAAe,eAAA,CACX,SAAA,EACA,QAAA,EACA,GAAA,EAEJ;AACI,EAAA,IAAI,CAAC,QAAA,EACL;AACI,IAAA,OAAO,KAAA;AAAA,EACX;AAEA,EAAA,IAAI,CAAC,SAAS,aAAA,EACd;AACI,IAAA,OAAO,IAAA;AAAA,EACX;AAEA,EAAA,IACA;AACI,IAAA,OAAO,MAAM,QAAA,CAAS,aAAA,CAAc,GAAG,CAAA,KAAM,IAAA;AAAA,EACjD,CAAA,CAAA,MAEA;AACI,IAAA,SAAA,CAAU,KAAK,+DAAA,EAA4D;AAAA,MACvE,KAAA,EAAO,SAAA;AAAA,MACP,SAAS,GAAA,CAAI;AAAA,KAChB,CAAA;AAED,IAAA,OAAO,KAAA;AAAA,EACX;AACJ;AAUA,eAAe,SAAA,CACX,CAAA,EACA,MAAA,EACA,SAAA,EACA,MAAA,EAEJ;AACI,EAAA,MAAM,EAAE,OAAA,EAAS,MAAA,EAAQ,OAAA,EAAS,SAAQ,GAAI,SAAA;AAC9C,EAAA,IAAI,cAAA,GAAiB,KAAA;AACrB,EAAA,IAAI,SAAA;AAGJ,EAAA,IAAI,QAAA;AAEJ,EAAA,MAAM,OAAA,GAAU,CAAC,MAAA,KACjB;AACI,IAAA,IAAI,cAAA,EAAgB;AACpB,IAAA,cAAA,GAAiB,IAAA;AACjB,IAAA,aAAA,CAAc,SAAS,CAAA;AACvB,IAAA,cAAA,CAAe,UAAU,CAAA;AACzB,IAAA,MAAA,CAAO,QAAA,CAAS,OAAO,UAAU,CAAA;AACjC,IAAA,MAAA,CAAO,KAAA,EAAM;AACb,IAAA,QAAA,IAAW;AACX,IAAA,SAAA,CAAU,KAAK,gCAAA,EAAkC;AAAA,MAC7C,MAAA,EAAQ,CAAC,GAAG,UAAA,CAAW,OAAO,CAAA;AAAA,MAC9B;AAAA,KACH,CAAA;AAAA,EACL,CAAA;AAIA,EAAA,MAAM,SAAS,mBAAA,CAAoB,MAAA,EAAQ,MAAA,CAAO,QAAA,EAAU,CAAC,MAAA,KAC7D;AACI,IAAA,SAAA,CAAU,IAAA,CAAK,uBAAA,EAAyB,EAAE,MAAA,EAAQ,CAAC,GAAG,UAAA,CAAW,OAAO,CAAA,EAAG,MAAA,EAAQ,CAAA;AACnF,IAAA,OAAA,CAAQ,MAAM,CAAA;AAAA,EAClB,CAAC,CAAA;AAED,EAAA,MAAM,UAAA,GAA4B;AAAA,IAC9B,OAAA;AAAA,IACA,WAAW,SAAA,CAAU,SAAA;AAAA,IACrB,OAAA,EAAS,IAAI,GAAA,CAAI,OAAO,CAAA;AAAA,IACxB,MAAA,EAAQ,IAAI,GAAA,CAAI,MAAM,CAAA;AAAA,IACtB,YAAA,sBAAkB,GAAA,EAAI;AAAA,IACtB,OAAA;AAAA,IACA,WAAW,qBAAA,CAAsB;AAAA,MAC7B,QAAQ,MAAA,CAAO,MAAA;AAAA,MACf,YAAY,MAAA,CAAO,UAAA;AAAA,MACnB,OAAA;AAAA,MACA,OAAA;AAAA,MACA,MAAA;AAAA,MACA,QAAQ,MAAM;AAAA,KACjB,CAAA;AAAA,IACD,MAAA,EAAQ,CAAC,KAAA,EAAO,SAAA,KAChB;AACI,MAAA,IAAI,cAAA,EAAgB;AAGpB,MAAA,MAAA,CAAO,OAAA,CAAQ,EAAE,KAAA,EAAO,KAAA,EAAO,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,EAAE,KAAA,EAAO,SAAA,EAAW,CAAA,EAAG,CAAA;AAAA,IAC/E;AAAA,GACJ;AAOA,EAAA,IAAI,SAAA,CAAU,OAAA,EAAS,IAAA,CAAK,UAAU,MAAM,KAAA,EAC5C;AACI,IAAA,OAAA,CAAQ,0BAA0B,CAAA;AAElC,IAAA;AAAA,EACJ;AAEA,EAAA,iBAAA,CAAkB,YAAY,MAAM,CAAA;AAGpC,EAAA,SAAA,GAAY,YAAY,MACxB;AACI,IAAA,IAAI,cAAA,EAAgB;AAEpB,IAAA,MAAA,CAAO,OAAA,CAAQ;AAAA,MACX,KAAA,EAAO,MAAA;AAAA,MACP,IAAA,EAAM,KAAK,SAAA,CAAU,EAAE,WAAW,IAAA,CAAK,GAAA,IAAO;AAAA,KACjD,CAAA;AAAA,EACL,CAAA,EAAG,OAAO,YAAY,CAAA;AAEtB,EAAA,MAAM,iBAAA,CAAkB,EAAE,GAAA,CAAI,GAAA,CAAI,QAAQ,MAAM,cAAA,EAAgB,CAAC,OAAA,KACjE;AACI,IAAA,QAAA,GAAW,OAAA;AAAA,EACf,CAAC,CAAA;AAGD,EAAA,OAAA,EAAQ;AACZ;AAKA,SAAS,iBAAA,CAAkB,YAA2B,MAAA,EACtD;AACI,EAAA,MAAA,CAAO,OAAA,CAAQ;AAAA,IACX,KAAA,EAAO,WAAA;AAAA,IACP,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,MACjB,gBAAA,EAAkB,CAAC,GAAG,UAAA,CAAW,OAAO,CAAA;AAAA,MACxC,YAAA,EAAc,CAAC,GAAG,UAAA,CAAW,MAAM,CAAA;AAAA,MACnC,SAAA,EAAW,KAAK,GAAA;AAAI,KACvB;AAAA,GACJ,CAAA;AAED,EAAA,KAAA,MAAW,SAAA,IAAa,WAAW,OAAA,EACnC;AACI,IAAA,UAAA,CAAW,aAAa,GAAA,CAAI,SAAA,EAAW,UAAA,CAAW,SAAA,CAAU,SAAS,CAAC,CAAA;AAAA,EAC1E;AAEA,EAAA,SAAA,CAAU,KAAK,4BAAA,EAA8B;AAAA,IACzC,MAAA,EAAQ,CAAC,GAAG,UAAA,CAAW,OAAO,CAAA;AAAA,IAC9B,YAAA,EAAc,CAAC,GAAG,UAAA,CAAW,MAAM,CAAA;AAAA,IACnC,iBAAA,EAAmB,WAAW,YAAA,CAAa;AAAA,GAC9C,CAAA;AACL;AAOA,eAAe,iBAAA,CACX,WAAA,EACA,MAAA,EACA,aAAA,EAEJ;AACI,EAAA,IAAI,WAAA,CAAY,OAAA,IAAW,MAAA,EAAO,EAClC;AACI,IAAA;AAAA,EACJ;AAEA,EAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,KACzB;AACI,IAAA,MAAM,OAAA,GAAU,MAAM,OAAA,EAAQ;AAC9B,IAAA,WAAA,CAAY,iBAAiB,OAAA,EAAS,OAAA,EAAS,EAAE,IAAA,EAAM,MAAM,CAAA;AAE7D,IAAA,aAAA,CAAc,MACd;AACI,MAAA,WAAA,CAAY,mBAAA,CAAoB,SAAS,OAAO,CAAA;AAChD,MAAA,OAAA,EAAQ;AAAA,IACZ,CAAC,CAAA;AAAA,EACL,CAAC,CAAA;AACL;AAoBA,SAAS,sBAAsB,GAAA,EAC/B;AACI,EAAA,IAAI,SAAA,GAAY,CAAA;AAEhB,EAAA,OAAO,CAAC,SAAA,KACR;AACI,IAAA,MAAM,QAAA,GAAW,GAAA,CAAI,MAAA,CAAO,MAAA,CAAO,SAAS,CAAA;AAC5C,IAAA,IAAI,MAAA,GAAS,IAAA;AAEb,IAAA,MAAM,WAAA,GAAc,QAAA,CAAS,SAAA,CAAU,CAAC,OAAA,KACxC;AACI,MAAA,IAAI,CAAC,MAAA,IAAU,GAAA,CAAI,MAAA,EAAO,IAAK,CAAC,kBAAA,CAAmB,SAAA,EAAW,QAAA,EAAU,OAAA,EAAS,GAAG,CAAA,EACpF;AACI,QAAA;AAAA,MACJ;AAEA,MAAA,SAAA,EAAA;AAEA,MAAA,SAAA,CAAU,MAAM,mBAAA,EAAqB;AAAA,QACjC,KAAA,EAAO,SAAA;AAAA,QACP;AAAA,OACH,CAAA;AAED,MAAA,GAAA,CAAI,OAAO,OAAA,CAAQ;AAAA,QACf,EAAA,EAAI,OAAO,SAAS,CAAA;AAAA,QACpB,KAAA,EAAO,SAAA;AAAA,QACP,IAAA,EAAM,cAAA,CAAe,SAAA,EAAW,OAAO;AAAA,OAC1C,CAAA;AAAA,IACL,CAAC,CAAA;AAED,IAAA,OAAO,MACP;AACI,MAAA,MAAA,GAAS,KAAA;AACT,MAAA,WAAA,EAAY;AAAA,IAChB,CAAA;AAAA,EACJ,CAAA;AACJ;AAMA,SAAS,kBAAA,CACL,SAAA,EACA,QAAA,EACA,OAAA,EACA,GAAA,EAEJ;AACI,EAAA,MAAM,YAAA,GAAe,GAAA,CAAI,UAAA,EAAY,MAAA,GAAS,SAAS,CAAA;AACvD,EAAA,IAAI,GAAA,CAAI,WAAW,YAAA,IAAgB,CAAC,aAAa,GAAA,CAAI,OAAA,EAAS,OAAO,CAAA,EACrE;AACI,IAAA,OAAO,KAAA;AAAA,EACX;AAEA,EAAA,IAAI,CAAC,SAAS,OAAA,EACd;AACI,IAAA,OAAO,IAAA;AAAA,EACX;AAEA,EAAA,IACA;AACI,IAAA,OAAO,QAAA,CAAS,OAAA,CAAQ,OAAA,EAAS,EAAE,OAAA,EAAS,GAAA,CAAI,OAAA,EAAS,OAAA,EAAS,GAAA,CAAI,OAAA,EAAS,CAAA,KAAM,IAAA;AAAA,EACzF,CAAA,CAAA,MAEA;AACI,IAAA,SAAA,CAAU,IAAA,CAAK,+CAA0C,EAAE,KAAA,EAAO,WAAW,OAAA,EAAS,GAAA,CAAI,SAAS,CAAA;AAEnG,IAAA,OAAO,KAAA;AAAA,EACX;AACJ;AAWA,eAAe,iBAAA,CACX,GACA,YAAA,EAEJ;AACI,EAAA,IAAI,CAAC,YAAA,EACL;AACI,IAAA,OAAO,MAAA;AAAA,EACX;AAEA,EAAA,MAAM,KAAA,GAAQ,CAAA,CAAE,GAAA,CAAI,KAAA,CAAM,OAAO,CAAA;AACjC,EAAA,IAAI,CAAC,KAAA,EACL;AACI,IAAA,OAAO,KAAA;AAAA,EACX;AAEA,EAAA,OAAO,MAAM,YAAA,CAAa,MAAA,CAAO,KAAK,CAAA;AAC1C;AAKA,SAAS,qBAAqB,CAAA,EAC9B;AACI,EAAA,MAAM,WAAA,GAAc,CAAA,CAAE,GAAA,CAAI,KAAA,CAAM,QAAQ,CAAA;AACxC,EAAA,IAAI,CAAC,WAAA,EACL;AACI,IAAA,OAAO,IAAA;AAAA,EACX;AAGA,EAAA,OAAO,CAAC,GAAG,IAAI,GAAA,CAAI,YAAY,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,CAAE,IAAA,EAAM,CAAC,CAAC,CAAA;AACjE;AAMA,eAAe,eAAA,CACX,OAAA,EACA,eAAA,EACA,UAAA,EAEJ;AACI,EAAA,IAAI,CAAC,OAAA,IAAW,CAAC,UAAA,EAAY,SAAA,EAC7B;AACI,IAAA,OAAO,eAAA;AAAA,EACX;AAEA,EAAA,MAAM,OAAA,GAAU,MAAM,UAAA,CAAW,SAAA,CAAU,SAAS,eAAe,CAAA;AAEnE,EAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EACvB;AACI,IAAA,OAAO,IAAA;AAAA,EACX;AAEA,EAAA,OAAO,OAAA;AACX;AC3qBA,SAAS,UAAU,KAAA,EACnB;AACI,EAAA,OAAO,WAAW,QAAQ,CAAA,CAAE,OAAO,KAAK,CAAA,CAAE,OAAO,KAAK,CAAA;AAC1D;AAqEA,IAAM,qBAAN,MACA;AAAA,EACY,MAAA,uBAAa,GAAA,EAAsB;AAAA,EAE3C,MAAM,GAAA,CAAI,KAAA,EAAe,IAAA,EACzB;AACI,IAAA,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,SAAA,CAAU,KAAK,GAAG,IAAI,CAAA;AAAA,EAC1C;AAAA,EAEA,MAAM,QAAQ,KAAA,EACd;AACI,IAAA,MAAM,GAAA,GAAM,UAAU,KAAK,CAAA;AAC3B,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,GAAG,CAAA;AAChC,IAAA,IAAI,CAAC,IAAA,EACL;AACI,MAAA,OAAO,IAAA;AAAA,IACX;AAEA,IAAA,IAAA,CAAK,MAAA,CAAO,OAAO,GAAG,CAAA;AAEtB,IAAA,OAAO,IAAA;AAAA,EACX;AAAA,EAEA,MAAM,OAAA,GACN;AACI,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AAErB,IAAA,KAAA,MAAW,CAAC,GAAA,EAAK,IAAI,CAAA,IAAK,KAAK,MAAA,EAC/B;AACI,MAAA,IAAI,IAAA,CAAK,aAAa,GAAA,EACtB;AACI,QAAA,IAAA,CAAK,MAAA,CAAO,OAAO,GAAG,CAAA;AAAA,MAC1B;AAAA,IACJ;AAAA,EACJ;AACJ,CAAA;AAuBO,IAAM,kBAAN,MACP;AAAA,EAGI,YAAoB,KAAA,EACpB;AADoB,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AAAA,EACnB;AAAA,EAHO,MAAA,GAAS,YAAA;AAAA,EAKjB,MAAM,GAAA,CAAI,KAAA,EAAe,IAAA,EACzB;AACI,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,IAAA,CAAA,CAAM,IAAA,CAAK,SAAA,GAAY,IAAA,CAAK,GAAA,EAAI,IAAK,GAAI,CAAC,CAAA;AAC9E,IAAA,MAAM,KAAK,KAAA,CAAM,GAAA;AAAA,MACb,IAAA,CAAK,MAAA,GAAS,SAAA,CAAU,KAAK,CAAA;AAAA,MAC7B,IAAA,CAAK,UAAU,IAAI,CAAA;AAAA,MACnB,IAAA;AAAA,MACA;AAAA,KACJ;AAAA,EACJ;AAAA,EAEA,MAAM,QAAQ,KAAA,EACd;AACI,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,MAAA,GAAS,SAAA,CAAU,KAAK,CAAA;AAGzC,IAAA,IAAI,GAAA,GAAqB,IAAA;AAEzB,IAAA,IAAI,IAAA,CAAK,MAAM,MAAA,EACf;AACI,MAAA,GAAA,GAAM,MAAM,IAAA,CAAK,KAAA,CAAM,MAAA,CAAO,GAAG,CAAA;AAAA,IACrC,CAAA,MAEA;AACI,MAAA,GAAA,GAAM,MAAM,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AAC9B,MAAA,IAAI,GAAA,EACJ;AACI,QAAA,MAAM,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AAAA,MAC5B;AAAA,IACJ;AAEA,IAAA,IAAI,CAAC,GAAA,EACL;AACI,MAAA,OAAO,IAAA;AAAA,IACX;AAEA,IAAA,OAAO,IAAA,CAAK,MAAM,GAAG,CAAA;AAAA,EACzB;AAAA,EAEA,MAAM,OAAA,GACN;AAAA,EAEA;AACJ;AAMO,IAAM,kBAAN,MACP;AAAA,EACY,KAAA;AAAA,EACA,GAAA;AAAA,EACA,YAAA,GAAsD,IAAA;AAAA,EAE9D,YAAY,MAAA,EACZ;AACI,IAAA,IAAA,CAAK,GAAA,GAAM,QAAQ,GAAA,IAAO,GAAA;AAC1B,IAAA,IAAA,CAAK,KAAA,GAAQ,MAAA,EAAQ,KAAA,IAAS,IAAI,kBAAA,EAAmB;AAErD,IAAA,MAAM,eAAA,GAAkB,QAAQ,eAAA,IAAmB,GAAA;AACnD,IAAA,IAAA,CAAK,YAAA,GAAe,YAAY,MAAM,KAAK,KAAK,KAAA,CAAM,OAAA,IAAW,eAAe,CAAA;AAChF,IAAA,IAAA,CAAK,aAAa,KAAA,EAAM;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,IAAI,KAAA,GACJ;AACI,IAAA,OAAO,IAAA,CAAK,GAAA;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,MAAM,OAAA,EACZ;AACI,IAAA,MAAM,KAAA,GAAQ,WAAA,CAAY,EAAE,CAAA,CAAE,SAAS,KAAK,CAAA;AAG5C,IAAA,MAAM,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,KAAA,EAAO;AAAA,MACxB,OAAA;AAAA,MACA,SAAA,EAAW,IAAA,CAAK,GAAA,EAAI,GAAI,IAAA,CAAK;AAAA,KAChC,CAAA;AAED,IAAA,OAAO,KAAA;AAAA,EACX;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OAAO,KAAA,EACb;AACI,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,KAAA,CAAM,QAAQ,KAAK,CAAA;AAE3C,IAAA,IAAI,CAAC,IAAA,IAAQ,IAAA,CAAK,SAAA,IAAa,IAAA,CAAK,KAAI,EACxC;AACI,MAAA,OAAO,IAAA;AAAA,IACX;AAEA,IAAA,OAAO,IAAA,CAAK,OAAA;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA,EAKA,OAAA,GACA;AACI,IAAA,IAAI,KAAK,YAAA,EACT;AACI,MAAA,aAAA,CAAc,KAAK,YAAY,CAAA;AAC/B,MAAA,IAAA,CAAK,YAAA,GAAe,IAAA;AAAA,IACxB;AAAA,EACJ;AACJ","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
|
+
{"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. A `match` narrows either to the\n * connections whose connect query carries its fields.\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, SubscriptionMatch } 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/** One `grant` / `revoke` recorded on a pending admission. */\ninterface RecordedChange\n{\n kind: SubscriptionChange;\n eventName: string;\n match?: SubscriptionMatch;\n}\n\n/** The changes recorded on one pending admission, in call order (the last one wins). */\ntype RecordedChanges = RecordedChange[];\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 = [];\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, match?: SubscriptionMatch): void =>\n {\n const pending = pendingBySubject.get(subject);\n const connections = bySubject.get(subject);\n pending?.forEach(changes => changes.push({ kind, eventName, match }));\n connections?.forEach(connection =>\n {\n if (matchesRequest(match, connection.request))\n {\n (kind === 'grant' ? grantOn : revokeOn)(connection, eventName);\n }\n });\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, match?: SubscriptionMatch): void =>\n change('grant', subject, eventName, match),\n\n revoke: (subject: string, eventName: string, match?: SubscriptionMatch): void =>\n change('revoke', subject, eventName, match),\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 * Whether `match` reaches a connection: every named field equals the connect\n * query's value, string to string. A field the connection never declared\n * never matches, not even an empty string. No `match` reaches every one.\n */\nexport function matchesRequest(match: SubscriptionMatch | undefined, request: SubscribeRequest): boolean\n{\n return match === undefined || Object.entries(match.query).every(([field, value]) =>\n Object.hasOwn(request.query, field) && request.query[field] === value);\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; a\n * change whose `match` the connection's query does not meet is skipped.\n */\nfunction applyRecorded(connection: SSEConnection, changes: RecordedChanges): void\n{\n changes.forEach(({ kind, eventName, match }) =>\n {\n if (!matchesRequest(match, connection.request))\n {\n return;\n }\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, SubscriptionMatch } 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, match?: SubscriptionMatch) => registry.grant(subject, eventName, match),\n revoke: (subject: string, match?: SubscriptionMatch) => registry.revoke(subject, eventName, match),\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-
|
|
1
|
+
import { W as WSRouterDef, a as WSSubscribeOptions, b as WSUnsubscribe, c as WSConnectionState, d as WSClientConfig } from '../../types-CqTsDETV.js';
|
|
2
2
|
import 'hono';
|
|
3
|
-
import '../../router-
|
|
3
|
+
import '../../router-D5B0cpVN.js';
|
|
4
4
|
import '@sinclair/typebox';
|
|
5
5
|
import '../../token-manager-Ckjt2-qq.js';
|
|
6
6
|
|
package/dist/event/ws/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as EventDef } from '../../router-
|
|
2
|
-
import { e as WSMessageHandlers, W as WSRouterDef, f as WSHandlerConfig } from '../../types-
|
|
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-
|
|
1
|
+
import { a as EventDef } from '../../router-D5B0cpVN.js';
|
|
2
|
+
import { e as WSMessageHandlers, W as WSRouterDef, f as WSHandlerConfig } from '../../types-CqTsDETV.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-CqTsDETV.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';
|