@spfn/core 0.3.0-beta.27 → 0.3.0-beta.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codegen/index.d.ts +65 -2
- package/dist/codegen/index.js +160 -15
- package/dist/codegen/index.js.map +1 -1
- package/dist/contract/index.d.ts +2 -2
- package/dist/db/index.d.ts +39 -11
- package/dist/db/index.js +34 -14
- package/dist/db/index.js.map +1 -1
- package/dist/{error-TsUIjp_x.d.ts → error-CIZlYJlL.d.ts} +1 -1
- package/dist/event/index.d.ts +3 -3
- package/dist/event/index.js +10 -0
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +2 -2
- package/dist/event/sse/client.js +27 -17
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +3 -3
- package/dist/event/sse/index.js +115 -34
- 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/ops/index.d.ts +3 -3
- package/dist/ops/index.js +15 -5
- package/dist/ops/index.js.map +1 -1
- package/dist/route/index.d.ts +2 -2
- package/dist/route/index.js +15 -5
- package/dist/route/index.js.map +1 -1
- package/dist/{router-4YoIy5fN.d.ts → router-C57y-vz1.d.ts} +12 -0
- package/dist/{router-D5B0cpVN.d.ts → router-CX5uVjmU.d.ts} +29 -1
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.js +130 -39
- package/dist/server/index.js.map +1 -1
- package/dist/state/client.d.ts +73 -11
- package/dist/state/client.js +231 -31
- package/dist/state/client.js.map +1 -1
- package/dist/state/index.js +19 -4
- package/dist/state/index.js.map +1 -1
- package/dist/state/react.d.ts +18 -3
- package/dist/state/react.js +11 -5
- package/dist/state/react.js.map +1 -1
- package/dist/surface/index.d.ts +3 -3
- package/dist/surface/index.js +15 -5
- package/dist/surface/index.js.map +1 -1
- package/dist/{types-04gu3uMj.d.ts → types-CurMab2Q.d.ts} +19 -2
- package/dist/{types-CqTsDETV.d.ts → types-DncqXfWN.d.ts} +1 -1
- package/package.json +1 -1
- package/src/codegen/README.md +89 -2
- package/src/db/transaction/README.md +58 -35
- package/src/event/README.md +61 -14
- package/src/state/README.md +228 -35
|
@@ -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 * 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
|
+
{"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 ConnectedPayload,\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, onConnected } = 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, onConnected);\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 /**\n * `listener`, run only while `es` is this subscription's live\n * EventSource: a task an earlier EventSource queued before it was\n * superseded or closed is ignored, as a browser aborts it.\n */\n function whileCurrent<E>(es: EventSource, listener: (event: E) => void): (event: E) => void\n {\n return (event) =>\n {\n if (!conn.closed && conn.eventSource === es)\n {\n listener(event);\n }\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 onConnectedCb?: (payload: ConnectedPayload) => void,\n )\n {\n es.onopen = whileCurrent(es, () =>\n {\n state = 'open';\n conn.reconnectAttempts = 0;\n onOpenCb?.();\n });\n\n es.onerror = whileCurrent(es, (error: Event) =>\n {\n state = 'error';\n onErrorCb?.(error);\n });\n\n // Handle connected event (server sends this on connection)\n es.addEventListener('connected', whileCurrent(es, (e: MessageEvent) =>\n {\n let data: ConnectedPayload;\n try\n {\n data = JSON.parse(e.data);\n }\n catch\n {\n // Ignore parse errors\n return;\n }\n\n console.debug('[SSE] Connected:', data);\n onConnectedCb?.(data);\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, whileCurrent(es, (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, whileCurrent(es, (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 = whileCurrent(currentEs, (error: Event) =>\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-CX5uVjmU.js';
|
|
3
|
+
import { d as SSEHandlerConfig } from '../../types-CurMab2Q.js';
|
|
4
|
+
export { C as ConnectedPayload, 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-CurMab2Q.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
|
@@ -8,7 +8,15 @@ import { randomBytes, createHash } from 'crypto';
|
|
|
8
8
|
function createBoundedWriter(stream, maxQueue, onClose) {
|
|
9
9
|
const pending = [];
|
|
10
10
|
let flushing = false;
|
|
11
|
+
let reserved = 0;
|
|
11
12
|
let closed = false;
|
|
13
|
+
const overflows = () => {
|
|
14
|
+
if (pending.length + reserved <= maxQueue) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
onClose("outbound queue overflow (slow consumer)");
|
|
18
|
+
return true;
|
|
19
|
+
};
|
|
12
20
|
const flush = async () => {
|
|
13
21
|
if (flushing || closed) {
|
|
14
22
|
return;
|
|
@@ -32,11 +40,19 @@ function createBoundedWriter(stream, maxQueue, onClose) {
|
|
|
32
40
|
return;
|
|
33
41
|
}
|
|
34
42
|
pending.push(frame);
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
if (!overflows()) {
|
|
44
|
+
void flush();
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
reserve() {
|
|
48
|
+
if (closed) {
|
|
49
|
+
return false;
|
|
38
50
|
}
|
|
39
|
-
|
|
51
|
+
reserved++;
|
|
52
|
+
return !overflows();
|
|
53
|
+
},
|
|
54
|
+
clearReserved() {
|
|
55
|
+
reserved = 0;
|
|
40
56
|
},
|
|
41
57
|
close() {
|
|
42
58
|
closed = true;
|
|
@@ -288,7 +304,7 @@ async function authorizeConnection(c, router, authConfig, subject, requested) {
|
|
|
288
304
|
subject: subject || void 0,
|
|
289
305
|
clientIp: c.req.header("x-forwarded-for") || c.req.header("x-real-ip")
|
|
290
306
|
});
|
|
291
|
-
return { subject, requested, allowed, denied, request };
|
|
307
|
+
return { subject, requested, allowed, denied, request, principal: resolved.principal };
|
|
292
308
|
}
|
|
293
309
|
async function resolveConnectionPrincipal(subject, request, authConfig) {
|
|
294
310
|
if (!subject || !authConfig?.resolvePrincipal) {
|
|
@@ -344,6 +360,16 @@ async function runStream(c, stream, admission, config) {
|
|
|
344
360
|
sseLogger.warn("SSE connection closed", { events: [...connection.allowed], reason });
|
|
345
361
|
cleanup(reason);
|
|
346
362
|
});
|
|
363
|
+
const hold = createFrameHold(writer);
|
|
364
|
+
const subscriberContext = {
|
|
365
|
+
router: config.router,
|
|
366
|
+
authConfig: config.authConfig,
|
|
367
|
+
subject,
|
|
368
|
+
request,
|
|
369
|
+
writeFrame: createFrameWriter(writer),
|
|
370
|
+
send: hold.send,
|
|
371
|
+
isDead: () => connectionDead
|
|
372
|
+
};
|
|
347
373
|
const connection = {
|
|
348
374
|
subject,
|
|
349
375
|
requested: admission.requested,
|
|
@@ -351,45 +377,61 @@ async function runStream(c, stream, admission, config) {
|
|
|
351
377
|
denied: new Set(denied),
|
|
352
378
|
unsubscribes: /* @__PURE__ */ new Map(),
|
|
353
379
|
request,
|
|
354
|
-
subscribe: createEventSubscriber(
|
|
355
|
-
router: config.router,
|
|
356
|
-
authConfig: config.authConfig,
|
|
357
|
-
subject,
|
|
358
|
-
request,
|
|
359
|
-
writer,
|
|
360
|
-
isDead: () => connectionDead
|
|
361
|
-
}),
|
|
380
|
+
subscribe: createEventSubscriber(subscriberContext),
|
|
362
381
|
notify: (frame, eventName) => {
|
|
363
382
|
if (connectionDead) return;
|
|
364
|
-
writer.enqueue({ event: frame, data: JSON.stringify({ event: eventName }) });
|
|
383
|
+
hold.send(() => writer.enqueue({ event: frame, data: JSON.stringify({ event: eventName }) }));
|
|
365
384
|
}
|
|
366
385
|
};
|
|
367
386
|
if (admission.pending?.open(connection) === false) {
|
|
368
387
|
cleanup("aborted during admission");
|
|
369
388
|
return;
|
|
370
389
|
}
|
|
371
|
-
openSubscriptions(connection, writer
|
|
390
|
+
void openSubscriptions(connection, writer, hold, {
|
|
391
|
+
...subscriberContext,
|
|
392
|
+
principal: admission.principal
|
|
393
|
+
});
|
|
372
394
|
pingTimer = setInterval(() => {
|
|
373
395
|
if (connectionDead) return;
|
|
374
|
-
writer.enqueue({
|
|
396
|
+
hold.send(() => writer.enqueue({
|
|
375
397
|
event: "ping",
|
|
376
398
|
data: JSON.stringify({ timestamp: Date.now() })
|
|
377
|
-
});
|
|
399
|
+
}));
|
|
378
400
|
}, config.pingInterval);
|
|
379
401
|
await waitForDisconnect(c.req.raw.signal, () => connectionDead, (resolve) => {
|
|
380
402
|
onClosed = resolve;
|
|
381
403
|
});
|
|
382
404
|
cleanup();
|
|
383
405
|
}
|
|
384
|
-
function openSubscriptions(connection, writer) {
|
|
406
|
+
async function openSubscriptions(connection, writer, hold, ctx) {
|
|
407
|
+
const connected = { subscribedEvents: [...connection.allowed], deniedEvents: [...connection.denied] };
|
|
408
|
+
const producing = connected.subscribedEvents.filter((eventName) => ctx.router.events[eventName]._initial);
|
|
409
|
+
if (!ctx.subject || producing.length === 0) {
|
|
410
|
+
enqueueConnected(writer, { ...connected, initial: [] });
|
|
411
|
+
subscribeAllowed(connection);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
hold.hold();
|
|
415
|
+
subscribeAllowed(connection);
|
|
416
|
+
const served = await produceInitialFrames(producing, ctx.router, {
|
|
417
|
+
subject: ctx.subject,
|
|
418
|
+
principal: ctx.principal,
|
|
419
|
+
request: ctx.request
|
|
420
|
+
});
|
|
421
|
+
if (ctx.isDead()) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
enqueueConnected(writer, { ...connected, initial: served.map(({ eventName }) => eventName) });
|
|
425
|
+
served.forEach(({ eventName, frames }) => frames.forEach((data) => ctx.writeFrame(eventName, data)));
|
|
426
|
+
hold.release();
|
|
427
|
+
}
|
|
428
|
+
function enqueueConnected(writer, connected) {
|
|
385
429
|
writer.enqueue({
|
|
386
430
|
event: "connected",
|
|
387
|
-
data: JSON.stringify({
|
|
388
|
-
subscribedEvents: [...connection.allowed],
|
|
389
|
-
deniedEvents: [...connection.denied],
|
|
390
|
-
timestamp: Date.now()
|
|
391
|
-
})
|
|
431
|
+
data: JSON.stringify({ ...connected, timestamp: Date.now() })
|
|
392
432
|
});
|
|
433
|
+
}
|
|
434
|
+
function subscribeAllowed(connection) {
|
|
393
435
|
for (const eventName of connection.allowed) {
|
|
394
436
|
connection.unsubscribes.set(eventName, connection.subscribe(eventName));
|
|
395
437
|
}
|
|
@@ -399,6 +441,54 @@ function openSubscriptions(connection, writer) {
|
|
|
399
441
|
subscriptionCount: connection.unsubscribes.size
|
|
400
442
|
});
|
|
401
443
|
}
|
|
444
|
+
async function produceInitialFrames(events, router, ctx) {
|
|
445
|
+
const results = await Promise.all(events.map((eventName) => askInitial(eventName, router.events[eventName], ctx)));
|
|
446
|
+
return results.filter((frames) => frames !== null);
|
|
447
|
+
}
|
|
448
|
+
async function askInitial(eventName, eventDef, ctx) {
|
|
449
|
+
try {
|
|
450
|
+
const payloads = await eventDef._initial(ctx);
|
|
451
|
+
return { eventName, frames: payloads.map((payload) => serializeFrame(eventName, payload)) };
|
|
452
|
+
} catch {
|
|
453
|
+
sseLogger.warn("SSE initial frames could not be produced \u2014 none sent for this event", {
|
|
454
|
+
event: eventName,
|
|
455
|
+
subject: ctx.subject
|
|
456
|
+
});
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function createFrameHold(writer) {
|
|
461
|
+
let pending = null;
|
|
462
|
+
return {
|
|
463
|
+
send: (write) => {
|
|
464
|
+
if (!pending) {
|
|
465
|
+
write();
|
|
466
|
+
} else if (writer.reserve()) {
|
|
467
|
+
pending.push(write);
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
hold: () => {
|
|
471
|
+
pending = [];
|
|
472
|
+
},
|
|
473
|
+
release: () => {
|
|
474
|
+
const held = pending ?? [];
|
|
475
|
+
pending = null;
|
|
476
|
+
writer.clearReserved();
|
|
477
|
+
held.forEach((write) => write());
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function createFrameWriter(writer) {
|
|
482
|
+
let messageId = 0;
|
|
483
|
+
return (eventName, data) => {
|
|
484
|
+
messageId++;
|
|
485
|
+
sseLogger.debug("SSE sending event", {
|
|
486
|
+
event: eventName,
|
|
487
|
+
messageId
|
|
488
|
+
});
|
|
489
|
+
writer.enqueue({ id: String(messageId), event: eventName, data });
|
|
490
|
+
};
|
|
491
|
+
}
|
|
402
492
|
async function waitForDisconnect(abortSignal, isDead, onServerClose) {
|
|
403
493
|
if (abortSignal.aborted || isDead()) {
|
|
404
494
|
return;
|
|
@@ -413,7 +503,6 @@ async function waitForDisconnect(abortSignal, isDead, onServerClose) {
|
|
|
413
503
|
});
|
|
414
504
|
}
|
|
415
505
|
function createEventSubscriber(ctx) {
|
|
416
|
-
let messageId = 0;
|
|
417
506
|
return (eventName) => {
|
|
418
507
|
const eventDef = ctx.router.events[eventName];
|
|
419
508
|
let active = true;
|
|
@@ -421,16 +510,8 @@ function createEventSubscriber(ctx) {
|
|
|
421
510
|
if (!active || ctx.isDead() || !passesFrameFilters(eventName, eventDef, payload, ctx)) {
|
|
422
511
|
return;
|
|
423
512
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
event: eventName,
|
|
427
|
-
messageId
|
|
428
|
-
});
|
|
429
|
-
ctx.writer.enqueue({
|
|
430
|
-
id: String(messageId),
|
|
431
|
-
event: eventName,
|
|
432
|
-
data: serializeFrame(eventName, payload)
|
|
433
|
-
});
|
|
513
|
+
const data = serializeFrame(eventName, payload);
|
|
514
|
+
ctx.send(() => ctx.writeFrame(eventName, data));
|
|
434
515
|
});
|
|
435
516
|
return () => {
|
|
436
517
|
active = false;
|