@eminuckan/spine 0.3.6 → 0.3.8
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/CHANGELOG.md +22 -0
- package/README.md +29 -6
- package/dist/api-client/api-config.server.d.ts +11 -1
- package/dist/api-client/api-config.server.d.ts.map +1 -1
- package/dist/api-client/fetch-client.server.d.ts.map +1 -1
- package/dist/api-client/index.d.ts +1 -1
- package/dist/api-client/index.d.ts.map +1 -1
- package/dist/api-client/server.mjs +1 -1
- package/dist/api-client/types.d.ts +9 -1
- package/dist/api-client/types.d.ts.map +1 -1
- package/dist/auth/auth.server.d.ts.map +1 -1
- package/dist/auth/redis-session-storage.server.d.ts.map +1 -1
- package/dist/auth/route-protection.server.d.ts.map +1 -1
- package/dist/auth/server.mjs +2 -2
- package/dist/auth/token-refresh.server.d.ts.map +1 -1
- package/dist/react-router/server.mjs +5 -5
- package/dist/{redis-session-storage.server-DGgQcJ9B.mjs → redis-session-storage.server-n0lWWyoh.mjs} +20 -4
- package/dist/{redis-session-storage.server-DGgQcJ9B.mjs.map → redis-session-storage.server-n0lWWyoh.mjs.map} +1 -1
- package/dist/{server-DPCu40ud.mjs → server-C3_iNZfj.mjs} +62 -22
- package/dist/server-C3_iNZfj.mjs.map +1 -0
- package/dist/{server-ByScGRXA.mjs → server-pnsLybIH.mjs} +63 -16
- package/dist/server-pnsLybIH.mjs.map +1 -0
- package/dist/server.mjs +5 -5
- package/dist/tanstack-start/index.d.ts +9 -0
- package/dist/tanstack-start/index.d.ts.map +1 -0
- package/dist/tanstack-start/index.mjs +11 -0
- package/dist/tanstack-start/server.d.ts +9 -0
- package/dist/tanstack-start/server.d.ts.map +1 -0
- package/dist/tanstack-start/server.mjs +17 -0
- package/dist/tenant/server.mjs +47 -7
- package/dist/tenant/server.mjs.map +1 -1
- package/dist/tenant/tenant-cookie.server.d.ts +5 -1
- package/dist/tenant/tenant-cookie.server.d.ts.map +1 -1
- package/dist/tenant/tenant.server.d.ts +9 -1
- package/dist/tenant/tenant.server.d.ts.map +1 -1
- package/dist/types-DqCXinAk.mjs.map +1 -1
- package/docs/adapters.md +53 -1
- package/docs/architecture.md +6 -1
- package/docs/module-reference.md +1 -0
- package/package.json +13 -4
- package/dist/server-ByScGRXA.mjs.map +0 -1
- package/dist/server-DPCu40ud.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-pnsLybIH.mjs","names":[],"sources":["../src/api-client/api-config.server.ts","../src/api-client/fetch-client.server.ts"],"sourcesContent":["/**\n * API Configuration Server Module\n *\n * Centralized configuration for all API clients,\n * automatically handling authentication tokens and tenant context.\n */\n\nimport type {\n APIConfig,\n CreateAPIConfigOptions,\n GetAccessTokenFn,\n GetAvailableTenantsFn,\n GetCurrentTenantFn,\n APILogger,\n ValidateTenantFn,\n} from './types';\n\n/**\n * Base URL Configuration\n * Single unified base URL for all API operations\n */\nconst DEFAULT_API_BASE_URL = process.env.API_BASE_URL || '';\n\n/**\n * Default Headers\n * Applied to all API requests\n */\nconst DEFAULT_HEADERS: Record<string, string> = {\n Accept: 'application/json, application/problem+json',\n 'Content-Type': 'application/json',\n};\n\n/**\n * API Config Factory Options\n */\nexport interface APIConfigFactoryOptions {\n /**\n * Base URL for API requests\n */\n baseURL?: string;\n /**\n * Default headers applied to all requests\n */\n defaultHeaders?: Record<string, string>;\n /**\n * User agent string\n */\n userAgent?: string;\n /**\n * Default timeout in milliseconds\n */\n timeout?: number;\n /**\n * Header name used for bearer-style auth. Set to null when buildHeaders owns auth.\n * @default 'Authorization'\n */\n authHeaderName?: string | null;\n /**\n * Build an auth header value from the resolved access token.\n * @default token => `Bearer ${token}`\n */\n authHeaderValue?: (accessToken: string) => string;\n /**\n * Header name used for tenant context. Set to null when buildHeaders owns tenancy.\n * @default 'X-Tenant-Id'\n */\n tenantHeaderName?: string | null;\n /**\n * Validate a resolved tenant selector against current server-side\n * membership before it is placed in an API configuration.\n */\n validateTenant?: ValidateTenantFn;\n /**\n * Optional membership resolver shorthand. `validateTenant` takes\n * precedence when both are provided.\n */\n getAvailableTenants?: GetAvailableTenantsFn;\n /**\n * Final app-specific header hook. Returned headers are merged last.\n */\n buildHeaders?: (context: APIHeaderStrategyContext) => Record<string, string>;\n}\n\nexport interface APIHeaderStrategyContext {\n request: Request;\n options: CreateAPIConfigOptions;\n accessToken?: string;\n tenantId: string | null;\n includeAuth: boolean;\n requireTenant: boolean;\n}\n\n/**\n * Create API Config Factory\n *\n * Creates a factory function that generates API configurations\n * with the provided auth and tenant resolvers.\n */\nexport function createAPIConfigFactory(\n getAccessToken: GetAccessTokenFn,\n getCurrentTenant: GetCurrentTenantFn,\n logger?: APILogger,\n factoryOptions: APIConfigFactoryOptions = {}\n) {\n const {\n baseURL = DEFAULT_API_BASE_URL,\n defaultHeaders = DEFAULT_HEADERS,\n userAgent = 'Spine/1.0',\n timeout = 30000,\n authHeaderName = 'Authorization',\n authHeaderValue = (token: string) => `Bearer ${token}`,\n tenantHeaderName = 'X-Tenant-Id',\n validateTenant,\n getAvailableTenants,\n buildHeaders,\n } = factoryOptions;\n\n const isTenantAuthorized: ValidateTenantFn | undefined = validateTenant ?? (\n getAvailableTenants\n ? async (request, tenantId) => (await getAvailableTenants(request)).includes(tenantId)\n : undefined\n );\n\n function finalizeTenantHeader(\n headers: Record<string, string>,\n tenantId: string | null,\n ): void {\n if (!tenantHeaderName) {\n return;\n }\n\n const normalizedTenantHeaderName = tenantHeaderName.toLowerCase();\n for (const headerName of Object.keys(headers)) {\n if (headerName.toLowerCase() === normalizedTenantHeaderName) {\n delete headers[headerName];\n }\n }\n\n if (tenantId) {\n headers[tenantHeaderName] = tenantId;\n }\n }\n\n /**\n * Resolve tenant ID from request or options\n */\n async function resolveTenantId(\n request: Request,\n options: CreateAPIConfigOptions\n ): Promise<string | null> {\n // Use explicit tenant ID if provided\n if (options.tenantId) {\n const requestedTenantId = options.tenantId;\n\n if (!isTenantAuthorized) {\n // An explicit override is a selector, not proof of membership. When a\n // caller has not supplied a membership validator, only an already\n // resolved current tenant may be reused.\n const currentTenant = await getCurrentTenant(request, requestedTenantId);\n if (currentTenant !== requestedTenantId) {\n logger?.warn?.('Rejected explicit tenant override without membership validation');\n return null;\n }\n } else {\n try {\n const authorized = await isTenantAuthorized(request, requestedTenantId);\n if (!authorized) {\n logger?.warn?.('Rejected explicit tenant override outside current membership');\n return null;\n }\n } catch (error) {\n logger?.warn?.('Failed to validate explicit tenant override', error instanceof Error ? error : undefined);\n return null;\n }\n }\n\n logger?.debug?.('Using validated explicit tenant ID', { tenantId: requestedTenantId });\n return requestedTenantId;\n }\n\n // Resolve from current tenant context\n try {\n const tenantId = await getCurrentTenant(request);\n if (tenantId) {\n if (isTenantAuthorized) {\n let authorized = false;\n try {\n authorized = await isTenantAuthorized(request, tenantId);\n } catch (error) {\n logger?.warn?.('Failed to validate current tenant', error instanceof Error ? error : undefined);\n }\n\n if (!authorized) {\n logger?.warn?.('Rejected current tenant outside current membership');\n return null;\n }\n }\n\n logger?.debug?.('Resolved validated tenant from context', { tenantId });\n }\n return tenantId;\n } catch (error) {\n logger?.warn?.('Failed to resolve current tenant', error instanceof Error ? error : undefined);\n return null;\n }\n }\n\n /**\n * Creates unified API configuration for all services.\n *\n * This function automatically:\n * - Adds authentication token using the configured auth header strategy\n * - Injects tenant context using the configured tenant header strategy\n * - Configures base URL from options or environment\n * - Applies default headers\n *\n * @param request - The incoming request object\n * @param options - Configuration options\n * @returns API configuration object\n * @throws Error if required authentication or tenant context is missing\n *\n * @example\n * `\typescript\n * // Standard usage with auth and tenant\n * const config = await createAPIConfig(request);\n * const api = new IdentityApi(config);\n *\n * // Without tenant requirement\n * const config = await createAPIConfig(request, { requireTenant: false });\n *\n * // With explicit tenant override\n * const config = await createAPIConfig(request, { tenantId: 'specific-tenant-id' });\n * `\n */\n async function createAPIConfig(\n request: Request,\n options: CreateAPIConfigOptions = {}\n ): Promise<APIConfig> {\n const {\n requireTenant = true,\n includeAuth = true,\n customHeaders = {},\n } = options;\n\n // Start with default headers\n const headers: Record<string, string> = {\n ...defaultHeaders,\n 'User-Agent': userAgent,\n ...customHeaders,\n };\n\n let accessToken: string | undefined;\n\n // Add authentication if required\n if (includeAuth) {\n const token = await getAccessToken(request);\n if (!token) {\n logger?.error?.('No access token available for API request');\n throw new Error('No access token available');\n }\n\n accessToken = token;\n if (authHeaderName) {\n headers[authHeaderName] = authHeaderValue(token);\n }\n logger?.debug?.('Added authorization header to API config');\n }\n\n // Resolve tenant context\n const tenantId = await resolveTenantId(request, options);\n\n // Add tenant header if required\n if (requireTenant) {\n if (!tenantId) {\n logger?.error?.('Tenant context required but not available');\n throw new Error('Tenant context is required for this operation');\n }\n\n if (tenantHeaderName) {\n headers[tenantHeaderName] = tenantId;\n }\n logger?.debug?.('Added tenant context header', { tenantId });\n }\n\n if (buildHeaders) {\n const headerOptions: CreateAPIConfigOptions = tenantId\n ? options\n : { ...options, tenantId: undefined };\n\n Object.assign(headers, buildHeaders({\n request,\n options: headerOptions,\n accessToken,\n tenantId,\n includeAuth,\n requireTenant,\n }));\n }\n\n // Tenant selectors are never authorization evidence. Reconcile the\n // authoritative header after every custom merge so neither customHeaders\n // nor buildHeaders can introduce an unvalidated or conflicting value.\n finalizeTenantHeader(headers, tenantId);\n\n logger?.debug?.('API config created', {\n basePath: baseURL,\n hasAuth: includeAuth,\n hasTenant: !!tenantId,\n requireTenant,\n });\n\n return {\n basePath: baseURL,\n accessToken,\n headers,\n tenantId: tenantId || '',\n baseOptions: {\n timeout,\n },\n };\n }\n\n /**\n * Get the configured API base URL\n */\n function getAPIBaseURL(): string {\n return baseURL;\n }\n\n return {\n createAPIConfig,\n getAPIBaseURL,\n };\n}\n\nexport type APIConfigFactory = ReturnType<typeof createAPIConfigFactory>;\n","/**\n * Fetch-native API client setup for generated OpenAPI clients.\n *\n * This module preserves the shared request behaviors we need around fetch:\n * - request ID enrichment\n * - serialized token refresh on 401\n * - retry with exponential backoff\n * - ProblemDetails-aware error shaping\n *\n * It gives downstream apps a single fetch-native entry point for generated clients.\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport { createHash, randomUUID } from 'node:crypto';\nimport type {\n APIClientError,\n APIConfig,\n APIResponse,\n APILogger,\n CreateAPIConfigOptions,\n EnhancedAPIClient,\n FetchErrorContext,\n FetchMiddleware,\n FetchResponseContext,\n ProblemDetails,\n RetryConfig,\n TokenRefreshFn,\n TokenRefreshResult,\n} from './types';\nimport { isApiClientError } from './types';\nimport { calculateBackoffDelay, fetchWithTimeout, isRetryableError } from './retry-handler';\n\ntype RuntimeApiResponse<T> = {\n raw: Response;\n value(): Promise<T>;\n};\n\ntype GeneratedFetchClient = object;\n\ntype SpineRequestInit = RequestInit & {\n __spineMeta?: {\n requestId?: string;\n retryCount?: number;\n refreshed?: boolean;\n };\n};\n\ntype ResponseRequestMeta = {\n url: string;\n method: string;\n headers: Record<string, string>;\n};\n\nconst refreshPromiseMap = new Map<string, Promise<TokenRefreshResult>>();\nconst responseRequestMeta = new WeakMap<Response, ResponseRequestMeta>();\nconst rawResponseStore = new AsyncLocalStorage<Map<string, RuntimeApiResponse<unknown>>>();\n\nfunction getSessionKeyFromRequest(request: Request): string {\n const cookie = request.headers.get('cookie')?.trim() || '';\n\n // A missing cookie does not identify a Redis session. Never put all such\n // requests into the same process-wide refresh flight, or one request can\n // receive another request's newly issued access token. The caller creates a\n // fresh key per middleware instance, so even two clients built from the same\n // cookie-less Request cannot share a token accidentally.\n if (!cookie) {\n return `request:${randomUUID()}`;\n }\n const fingerprint = createHash('sha256').update(cookie).digest('hex');\n return `session:${fingerprint}`;\n}\n\nfunction getOrCreateRefreshPromise(\n sessionKey: string,\n refreshFn: () => Promise<TokenRefreshResult>,\n logger?: APILogger\n): Promise<TokenRefreshResult> {\n const existingPromise = refreshPromiseMap.get(sessionKey);\n if (existingPromise) {\n logger?.info?.('Token refresh already in progress, waiting for existing refresh');\n return existingPromise;\n }\n\n logger?.info?.('Starting new token refresh');\n\n const refreshPromise = refreshFn().finally(() => {\n refreshPromiseMap.delete(sessionKey);\n });\n\n refreshPromiseMap.set(sessionKey, refreshPromise);\n return refreshPromise;\n}\n\nfunction ensureRequestMeta(init: SpineRequestInit): NonNullable<SpineRequestInit['__spineMeta']> {\n if (!init.__spineMeta) {\n init.__spineMeta = {};\n }\n\n return init.__spineMeta;\n}\n\nfunction headersToRecord(headersInit?: HeadersInit): Record<string, string> {\n return Object.fromEntries(new Headers(headersInit).entries());\n}\n\nfunction cloneInit(init: RequestInit): SpineRequestInit {\n const source = init as SpineRequestInit;\n\n return {\n ...source,\n headers: headersToRecord(source.headers),\n __spineMeta: source.__spineMeta ? { ...source.__spineMeta } : undefined,\n };\n}\n\nfunction createTimeoutFetch(timeout?: number): typeof fetch | undefined {\n if (!timeout) {\n return undefined;\n }\n\n return (input, init) => fetchWithTimeout(input, { ...init, timeout });\n}\n\nfunction isProblemDetails(value: unknown): value is ProblemDetails {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'title' in value &&\n 'status' in value &&\n 'detail' in value\n );\n}\n\nasync function parseResponseBody(response: Response): Promise<unknown> {\n const clone = response.clone();\n const contentType = clone.headers.get('content-type') || '';\n\n if (contentType.includes('application/json') || contentType.includes('+json')) {\n try {\n return await clone.json();\n } catch {\n return undefined;\n }\n }\n\n try {\n const text = await clone.text();\n return text || undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction createClientError<T = unknown>(params: {\n message: string;\n response?: APIResponse<T>;\n config?: ResponseRequestMeta;\n problemDetails?: ProblemDetails;\n shouldLogout?: boolean;\n originalError?: unknown;\n cause?: unknown;\n}): APIClientError<T> {\n const error = new Error(params.message) as APIClientError<T>;\n\n error.name = 'APIClientError';\n error.isApiClientError = true;\n error.response = params.response;\n error.config = params.config;\n error.problemDetails = params.problemDetails;\n error.shouldLogout = params.shouldLogout;\n error.originalError = params.originalError;\n error.cause = params.cause;\n\n return error;\n}\n\nasync function toAPIResponse<T>(\n rawResponse: Response,\n resolvedData?: T\n): Promise<APIResponse<T>> {\n const data = resolvedData === undefined ? ((await parseResponseBody(rawResponse)) as T) : resolvedData;\n\n return {\n data,\n status: rawResponse.status,\n statusText: rawResponse.statusText,\n headers: headersToRecord(rawResponse.headers),\n raw: rawResponse,\n };\n}\n\nasync function normalizeClientError(error: unknown): Promise<APIClientError> {\n if (isApiClientError(error)) {\n return error;\n }\n\n if (error && typeof error === 'object' && 'response' in error && (error as { response?: unknown }).response instanceof Response) {\n const response = (error as { response: Response }).response;\n const responseData = await parseResponseBody(response);\n const problemDetails = isProblemDetails(responseData) ? responseData : undefined;\n\n return createClientError({\n message:\n problemDetails?.detail ||\n problemDetails?.title ||\n response.statusText ||\n 'API request failed',\n response: await toAPIResponse(response, responseData),\n config: responseRequestMeta.get(response),\n problemDetails,\n cause: error,\n });\n }\n\n return createClientError({\n message: error instanceof Error ? error.message : 'API request failed',\n cause: error,\n });\n}\n\nfunction isRetryableFetchError(error: unknown): boolean {\n if (isRetryableError(error)) {\n return true;\n }\n\n if (error && typeof error === 'object' && 'cause' in error) {\n return isRetryableError((error as { cause?: unknown }).cause);\n }\n\n return false;\n}\n\nasync function maybeRetryResponse(\n context: FetchResponseContext,\n retryConfig: Required<Pick<RetryConfig, 'maxRetries' | 'baseDelay' | 'maxDelay'>>,\n logger?: APILogger\n): Promise<Response | undefined> {\n const init = cloneInit(context.init);\n const meta = ensureRequestMeta(init);\n\n if (context.response.status === 401 || context.response.status === 409) {\n return undefined;\n }\n\n if (!isRetryableError(context.response)) {\n return undefined;\n }\n\n const retryCount = meta.retryCount ?? 0;\n if (retryCount >= retryConfig.maxRetries) {\n return undefined;\n }\n\n meta.retryCount = retryCount + 1;\n\n const delay = calculateBackoffDelay(meta.retryCount, retryConfig.baseDelay, retryConfig.maxDelay);\n logger?.info?.('Retrying request', {\n url: context.url,\n method: (init.method ?? 'GET').toUpperCase(),\n attempt: meta.retryCount,\n maxRetries: retryConfig.maxRetries,\n delay,\n status: context.response.status,\n });\n\n await new Promise((resolve) => setTimeout(resolve, delay));\n\n return context.fetch(context.url, init);\n}\n\nasync function maybeRetryError(\n context: FetchErrorContext,\n retryConfig: Required<Pick<RetryConfig, 'maxRetries' | 'baseDelay' | 'maxDelay'>>,\n logger?: APILogger\n): Promise<Response | undefined> {\n const init = cloneInit(context.init);\n const meta = ensureRequestMeta(init);\n const retryCount = meta.retryCount ?? 0;\n\n if (!isRetryableFetchError(context.error) || retryCount >= retryConfig.maxRetries) {\n return undefined;\n }\n\n meta.retryCount = retryCount + 1;\n\n const delay = calculateBackoffDelay(meta.retryCount, retryConfig.baseDelay, retryConfig.maxDelay);\n logger?.info?.('Retrying request after fetch failure', {\n url: context.url,\n method: (init.method ?? 'GET').toUpperCase(),\n attempt: meta.retryCount,\n maxRetries: retryConfig.maxRetries,\n delay,\n });\n\n await new Promise((resolve) => setTimeout(resolve, delay));\n\n return context.fetch(context.url, init);\n}\n\nexport interface FetchSetupOptions {\n logger?: APILogger;\n attemptTokenRefresh?: TokenRefreshFn;\n retryConfig?: RetryConfig;\n addRequestId?: boolean;\n}\n\nexport function createFetchMiddleware(\n request: Request,\n options: FetchSetupOptions = {}\n): FetchMiddleware[] {\n const {\n logger,\n attemptTokenRefresh,\n retryConfig = {},\n addRequestId = true,\n } = options;\n const refreshSessionKey = getSessionKeyFromRequest(request);\n\n const normalizedRetryConfig = {\n maxRetries: retryConfig.maxRetries ?? 3,\n baseDelay: retryConfig.baseDelay ?? 1000,\n maxDelay: retryConfig.maxDelay ?? 10000,\n };\n\n return [\n {\n async pre(context) {\n const init = cloneInit(context.init);\n const meta = ensureRequestMeta(init);\n const headers = new Headers(init.headers);\n\n if (addRequestId) {\n const requestId = headers.get('X-Request-Id') || crypto.randomUUID();\n headers.set('X-Request-Id', requestId);\n meta.requestId = requestId;\n }\n\n init.headers = headersToRecord(headers);\n\n logger?.debug?.('API request initiated', {\n method: (init.method ?? 'GET').toUpperCase(),\n url: context.url,\n requestId: meta.requestId,\n });\n\n return {\n url: context.url,\n init,\n };\n },\n\n async post(context) {\n const init = cloneInit(context.init);\n const meta = ensureRequestMeta(init);\n\n responseRequestMeta.set(context.response, {\n url: context.url,\n method: (init.method ?? 'GET').toUpperCase(),\n headers: headersToRecord(init.headers),\n });\n\n if (context.response.status === 401 && attemptTokenRefresh && !meta.refreshed) {\n meta.refreshed = true;\n\n logger?.info?.('Received 401 from backend, attempting token refresh', {\n url: context.url,\n method: (init.method ?? 'GET').toUpperCase(),\n });\n\n const refreshResult = await getOrCreateRefreshPromise(\n refreshSessionKey,\n () => attemptTokenRefresh(request),\n logger\n );\n\n if (refreshResult.success && refreshResult.newAccessToken) {\n const headers = new Headers(init.headers);\n headers.set('Authorization', `Bearer ${refreshResult.newAccessToken}`);\n init.headers = headersToRecord(headers);\n\n logger?.info?.('Token refresh successful, retrying original request');\n\n return context.fetch(context.url, init);\n }\n\n // A failed or malformed refresh must not fall through to the stale\n // 401 response. Treat every unsuccessful refresh as a terminal auth\n // failure so callers can clear their local session.\n throw createClientError({\n message: 'REFRESH_TOKEN_EXPIRED',\n response: await toAPIResponse(context.response),\n config: responseRequestMeta.get(context.response),\n shouldLogout: true,\n originalError: refreshResult,\n cause: refreshResult,\n });\n }\n\n const retryResponse = await maybeRetryResponse(context, normalizedRetryConfig, logger);\n if (retryResponse) {\n return retryResponse;\n }\n\n if (context.response.status >= 400) {\n const responseData = await parseResponseBody(context.response);\n const problemDetails = isProblemDetails(responseData) ? responseData : undefined;\n\n logger?.error?.('API error response received', undefined, {\n url: context.url,\n method: (init.method ?? 'GET').toUpperCase(),\n status: context.response.status,\n code: problemDetails?.title,\n detail: problemDetails?.detail,\n traceId: problemDetails?.traceId,\n });\n }\n\n return context.response;\n },\n\n async onError(context) {\n const retryResponse = await maybeRetryError(context, normalizedRetryConfig, logger);\n if (retryResponse) {\n return retryResponse;\n }\n\n return undefined;\n },\n },\n ];\n}\n\nexport interface CreateApiClientOptions extends FetchSetupOptions, CreateAPIConfigOptions {\n credentials?: RequestCredentials;\n}\n\nfunction instrumentRawMethods<T extends GeneratedFetchClient>(client: T): void {\n if ((client as { __spineRawMethodsInstrumented?: boolean }).__spineRawMethodsInstrumented) {\n return;\n }\n\n const prototype = Object.getPrototypeOf(client) as Record<string, unknown>;\n\n for (const key of Object.getOwnPropertyNames(prototype)) {\n if (!key.endsWith('Raw')) {\n continue;\n }\n\n const original = prototype[key];\n if (typeof original !== 'function') {\n continue;\n }\n\n Object.defineProperty(client, key, {\n configurable: true,\n enumerable: false,\n writable: true,\n value: async (...args: unknown[]) => {\n const result = await (original as (...innerArgs: unknown[]) => Promise<RuntimeApiResponse<unknown>>).apply(client, args);\n rawResponseStore.getStore()?.set(key, result);\n return result;\n },\n });\n }\n\n (client as { __spineRawMethodsInstrumented?: boolean }).__spineRawMethodsInstrumented = true;\n}\n\nfunction wrapClient<T extends GeneratedFetchClient>(client: T): EnhancedAPIClient<T> {\n instrumentRawMethods(client);\n\n const methodCache = new Map<PropertyKey, unknown>();\n\n return new Proxy(client, {\n get(target, prop, receiver) {\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof prop !== 'string' || typeof value !== 'function' || prop.endsWith('Raw')) {\n return typeof value === 'function' ? value.bind(target) : value;\n }\n\n if (methodCache.has(prop)) {\n return methodCache.get(prop);\n }\n\n const wrapped = async (...args: unknown[]) =>\n rawResponseStore.run(new Map(), async () => {\n try {\n const resolvedData = await (value as (...innerArgs: unknown[]) => Promise<unknown>).apply(target, args);\n const rawResponse = rawResponseStore.getStore()?.get(`${prop}Raw`);\n\n if (rawResponse) {\n return toAPIResponse(rawResponse.raw, resolvedData);\n }\n\n return {\n data: resolvedData,\n status: 200,\n statusText: 'OK',\n headers: {},\n };\n } catch (error) {\n throw await normalizeClientError(error);\n }\n });\n\n methodCache.set(prop, wrapped);\n return wrapped;\n },\n }) as EnhancedAPIClient<T>;\n}\n\nexport async function createApiClient<T extends GeneratedFetchClient>(\n request: Request,\n ClientClass: new (configuration?: unknown) => T,\n createAPIConfig: (request: Request, options?: CreateAPIConfigOptions) => Promise<APIConfig>,\n ConfigurationClass: new (params?: Record<string, unknown>) => unknown,\n options: CreateApiClientOptions = {}\n): Promise<{ client: EnhancedAPIClient<T>; tenantId: string }> {\n const {\n requireTenant,\n includeAuth,\n tenantId,\n customHeaders,\n credentials,\n ...fetchOptions\n } = options;\n\n const config = await createAPIConfig(request, {\n requireTenant,\n includeAuth,\n tenantId,\n customHeaders,\n });\n\n const configuration = new ConfigurationClass({\n basePath: config.basePath,\n accessToken: config.accessToken,\n headers: config.headers,\n credentials: credentials ?? config.credentials,\n middleware: createFetchMiddleware(request, fetchOptions),\n fetchApi: createTimeoutFetch(config.baseOptions?.timeout),\n });\n\n const client = new ClientClass(configuration);\n\n return {\n client: wrapClient(client),\n tenantId: config.tenantId,\n };\n}\n"],"mappings":"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"}
|
package/dist/server.mjs
CHANGED
|
@@ -8,11 +8,11 @@ import { t as isApiClientError } from "./types-DqCXinAk.mjs";
|
|
|
8
8
|
import { a as withRetry, c as handleApiError, i as isRetryableError, l as ErrorCodes, n as createRetryFetch, o as ApiError, r as fetchWithTimeout, s as ErrorHandler, t as calculateBackoffDelay } from "./api-client-CB7XZaMg.mjs";
|
|
9
9
|
import { a as getIdentitySignalRClient, i as createIdentitySignalRClient, n as SignalRClient, o as initializeSignalR, r as cleanupSignalR, t as IdentitySignalRClient } from "./signalr-C-TA8nEE.mjs";
|
|
10
10
|
import { i as invalidationHelpers, n as createQueryClient, r as createQueryKeyFactory, t as cachePresets } from "./query-client-_xB300gc.mjs";
|
|
11
|
-
import { S as updateAuthSession, _ as isSessionValid, a as createOAuthState, b as listCurrentUserAuthSessions, c as destroyAuthSessionById, d as destroyAuthSessionsBySid, f as destroyAuthSessionsForUser, g as getOAuthState, h as getCurrentAuthSessionId, i as createAuthSession, l as destroyAuthSessionsByIdentitySession, m as getAuthSessionById, n as cleanupExpiredOAuthStates, o as deleteOAuthState, p as getAuthSession, r as closeRedisConnection, s as destroyAuthSession, t as AUTH_ERROR_COOKIE_PREFIX, u as destroyAuthSessionsBySessionState, v as listAuthSessionDataForUser, x as requireAuthSession, y as listAuthSessionsForUser } from "./redis-session-storage.server-
|
|
12
|
-
import { A as resetAuthClaimMapping, C as handleCallback, D as logout, E as login, O as refreshTokens, S as handleBackChannelLogout, T as hasAuthError, _ as createAuthorizationResponseUrl, a as configureRouteProtection, b as getAuthError, c as protectRoute, d as resetRouteProtectionConfig, f as shouldRefreshToken, g as configureAuthClaimMapping, h as clearAuthServerCache, i as authRoute, k as requireAuth, l as protectedRoute, m as clearAuthErrorHeaders, n as isTokenExpiredResponse, o as onboardingRoute, p as subscriptionRoute, r as shouldAttemptTokenRefresh, s as policyRoute, t as attemptTokenRefresh, u as publicRoute, v as extractUserInfo, w as handleFrontChannelLogout, x as getUser, y as getAccessToken } from "./server-
|
|
13
|
-
import { clearActiveTenant, clearCurrentTenant, configureIdentityContextFetcher, configureTenantCookie, configureTenantResolution, getActiveTenant, getAvailableTenants, getCurrentTenant, getTenantFromIdentityContext, initializeTenant, resetTenantCookieConfig, resetTenantResolutionConfig, setActiveTenant, setCurrentTenant } from "./tenant/server.mjs";
|
|
11
|
+
import { S as updateAuthSession, _ as isSessionValid, a as createOAuthState, b as listCurrentUserAuthSessions, c as destroyAuthSessionById, d as destroyAuthSessionsBySid, f as destroyAuthSessionsForUser, g as getOAuthState, h as getCurrentAuthSessionId, i as createAuthSession, l as destroyAuthSessionsByIdentitySession, m as getAuthSessionById, n as cleanupExpiredOAuthStates, o as deleteOAuthState, p as getAuthSession, r as closeRedisConnection, s as destroyAuthSession, t as AUTH_ERROR_COOKIE_PREFIX, u as destroyAuthSessionsBySessionState, v as listAuthSessionDataForUser, x as requireAuthSession, y as listAuthSessionsForUser } from "./redis-session-storage.server-n0lWWyoh.mjs";
|
|
12
|
+
import { A as resetAuthClaimMapping, C as handleCallback, D as logout, E as login, O as refreshTokens, S as handleBackChannelLogout, T as hasAuthError, _ as createAuthorizationResponseUrl, a as configureRouteProtection, b as getAuthError, c as protectRoute, d as resetRouteProtectionConfig, f as shouldRefreshToken, g as configureAuthClaimMapping, h as clearAuthServerCache, i as authRoute, k as requireAuth, l as protectedRoute, m as clearAuthErrorHeaders, n as isTokenExpiredResponse, o as onboardingRoute, p as subscriptionRoute, r as shouldAttemptTokenRefresh, s as policyRoute, t as attemptTokenRefresh, u as publicRoute, v as extractUserInfo, w as handleFrontChannelLogout, x as getUser, y as getAccessToken } from "./server-C3_iNZfj.mjs";
|
|
13
|
+
import { clearActiveTenant, clearCurrentTenant, configureIdentityContextFetcher, configureTenantCookie, configureTenantResolution, getActiveTenant, getAvailableTenants, getCurrentTenant, getTenantFromIdentityContext, initializeTenant, isTenantMember, resetTenantCookieConfig, resetTenantResolutionConfig, setActiveTenant, setCurrentTenant } from "./tenant/server.mjs";
|
|
14
14
|
import { clearIdentityContextCache, configureIdentityAPIFetcher, configurePermissionFetcher, contextToUserInfo, fetchIdentityContext, fetchUserPermissions, getIdentityContext, hasContextVersionChanged, updateContextVersion } from "./identity/server.mjs";
|
|
15
|
-
import { n as createFetchMiddleware, r as createAPIConfigFactory, t as createApiClient } from "./server-
|
|
15
|
+
import { n as createFetchMiddleware, r as createAPIConfigFactory, t as createApiClient } from "./server-pnsLybIH.mjs";
|
|
16
16
|
//#region src/permissions/route-protection.server.ts
|
|
17
17
|
const DEFAULT_UNAUTHORIZED_RESPONSE = () => new Response("Unauthorized", {
|
|
18
18
|
status: 401,
|
|
@@ -85,6 +85,6 @@ function withPermission(permission, loader, options) {
|
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
87
|
//#endregion
|
|
88
|
-
export { AUTH_ERROR_COOKIE_PREFIX, ApiError, ErrorCodes, ErrorHandler, IdentityContextProvider, IdentitySignalRClient, LOG_LEVEL_PRIORITY, LogLevel, Logger, PERMISSION_MODULES, PermissionChecker, PermissionInitializer, ProtectedAction, ProtectedButton, ProtectedLink, ProtectedSection, SignalRClient, TenantProvider, attemptTokenRefresh, authRoute, cachePresets, calculateBackoffDelay, checkPermission, cleanupExpiredOAuthStates, cleanupSignalR, clearActiveTenant, clearAuthErrorHeaders, clearAuthServerCache, clearCurrentTenant, clearIdentityContextCache, closeRedisConnection, configureAuthClaimMapping, configureIdentityAPIFetcher, configureIdentityContextFetcher, configureIdentityEndpoints, configureIdentityStore, configurePermissionFetcher, configurePermissionRouteProtection, configureRouteProtection, configureTenantClient, configureTenantCookie, configureTenantEndpoints, configureTenantResolution, contextToUserInfo, createAPIConfigFactory, createApiClient, createAuthSession, createAuthorizationResponseUrl, createFetchMiddleware, createIdentitySignalRClient, createLogger, createOAuthState, createQueryClient, createQueryKeyFactory, createRetryFetch, defaultLoggerConfig, deleteOAuthState, destroyAuthSession, destroyAuthSessionById, destroyAuthSessionsByIdentitySession, destroyAuthSessionsBySessionState, destroyAuthSessionsBySid, destroyAuthSessionsForUser, evaluateAccessRequirement, extractUserInfo, fetchIdentityContext, fetchUserPermissions, fetchWithTimeout, getAccessToken, getActiveTenant, getAuthError, getAuthSession, getAuthSessionById, getAvailableTenants, getCurrentAuthSessionId, getCurrentTenant, getIdentityContext, getIdentitySignalRClient, getOAuthState, getPermissionChecker, getPermissions, getTenantCookieName, getTenantFromIdentityContext, getUser, handleApiError, handleBackChannelLogout, handleCallback, handleFrontChannelLogout, hasAuthError, hasContextVersionChanged, initialIdentityContext, initializePermissions, initializeSignalR, initializeTenant, initializeTenantStore, invalidationHelpers, isApiClientError, isRetryableError, isSessionValid, isTokenExpiredResponse, listAuthSessionDataForUser, listAuthSessionsForUser, listCurrentUserAuthSessions, logger, login, logout, normalizeIdentityContextPayload, normalizeIdentityPermissionsPayload, normalizeTenantDataPayload, normalizeTenantSwitchPayload, onboardingRoute, policyRoute, protectRoute, protectedRoute, publicRoute, refreshTokens, requireAuth, requireAuthSession, requirePermission, resetAuthClaimMapping, resetIdentityStoreConfig, resetPermissionRouteProtection, resetRouteProtectionConfig, resetTenantClientConfig, resetTenantCookieConfig, resetTenantResolutionConfig, setActiveTenant, setCurrentTenant, shouldAttemptTokenRefresh, shouldRefreshToken, subscriptionRoute, updateAuthSession, updateContextVersion, useAllPermissions, useCurrentOrganization, useCurrentTenant, useCurrentTenantData, useHasEntitlement, useHasModulePermission, useHasPermission, useHasPermissions, useHasSubscription, useIdentityContext, useIdentityStore, useIsOnboarded, useModulePermission, useOrganization, usePermission, usePermissionChecker, usePermissionStore, usePermissions, useTenant, useTenantData, useTenantStore, useTenantSwitcher, useUserPermissions, useUserTenants, withPermission, withRetry };
|
|
88
|
+
export { AUTH_ERROR_COOKIE_PREFIX, ApiError, ErrorCodes, ErrorHandler, IdentityContextProvider, IdentitySignalRClient, LOG_LEVEL_PRIORITY, LogLevel, Logger, PERMISSION_MODULES, PermissionChecker, PermissionInitializer, ProtectedAction, ProtectedButton, ProtectedLink, ProtectedSection, SignalRClient, TenantProvider, attemptTokenRefresh, authRoute, cachePresets, calculateBackoffDelay, checkPermission, cleanupExpiredOAuthStates, cleanupSignalR, clearActiveTenant, clearAuthErrorHeaders, clearAuthServerCache, clearCurrentTenant, clearIdentityContextCache, closeRedisConnection, configureAuthClaimMapping, configureIdentityAPIFetcher, configureIdentityContextFetcher, configureIdentityEndpoints, configureIdentityStore, configurePermissionFetcher, configurePermissionRouteProtection, configureRouteProtection, configureTenantClient, configureTenantCookie, configureTenantEndpoints, configureTenantResolution, contextToUserInfo, createAPIConfigFactory, createApiClient, createAuthSession, createAuthorizationResponseUrl, createFetchMiddleware, createIdentitySignalRClient, createLogger, createOAuthState, createQueryClient, createQueryKeyFactory, createRetryFetch, defaultLoggerConfig, deleteOAuthState, destroyAuthSession, destroyAuthSessionById, destroyAuthSessionsByIdentitySession, destroyAuthSessionsBySessionState, destroyAuthSessionsBySid, destroyAuthSessionsForUser, evaluateAccessRequirement, extractUserInfo, fetchIdentityContext, fetchUserPermissions, fetchWithTimeout, getAccessToken, getActiveTenant, getAuthError, getAuthSession, getAuthSessionById, getAvailableTenants, getCurrentAuthSessionId, getCurrentTenant, getIdentityContext, getIdentitySignalRClient, getOAuthState, getPermissionChecker, getPermissions, getTenantCookieName, getTenantFromIdentityContext, getUser, handleApiError, handleBackChannelLogout, handleCallback, handleFrontChannelLogout, hasAuthError, hasContextVersionChanged, initialIdentityContext, initializePermissions, initializeSignalR, initializeTenant, initializeTenantStore, invalidationHelpers, isApiClientError, isRetryableError, isSessionValid, isTenantMember, isTokenExpiredResponse, listAuthSessionDataForUser, listAuthSessionsForUser, listCurrentUserAuthSessions, logger, login, logout, normalizeIdentityContextPayload, normalizeIdentityPermissionsPayload, normalizeTenantDataPayload, normalizeTenantSwitchPayload, onboardingRoute, policyRoute, protectRoute, protectedRoute, publicRoute, refreshTokens, requireAuth, requireAuthSession, requirePermission, resetAuthClaimMapping, resetIdentityStoreConfig, resetPermissionRouteProtection, resetRouteProtectionConfig, resetTenantClientConfig, resetTenantCookieConfig, resetTenantResolutionConfig, setActiveTenant, setCurrentTenant, shouldAttemptTokenRefresh, shouldRefreshToken, subscriptionRoute, updateAuthSession, updateContextVersion, useAllPermissions, useCurrentOrganization, useCurrentTenant, useCurrentTenantData, useHasEntitlement, useHasModulePermission, useHasPermission, useHasPermissions, useHasSubscription, useIdentityContext, useIdentityStore, useIsOnboarded, useModulePermission, useOrganization, usePermission, usePermissionChecker, usePermissionStore, usePermissions, useTenant, useTenantData, useTenantStore, useTenantSwitcher, useUserPermissions, useUserTenants, withPermission, withRetry };
|
|
89
89
|
|
|
90
90
|
//# sourceMappingURL=server.mjs.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TanStack Start adapter exports.
|
|
3
|
+
*
|
|
4
|
+
* TanStack Start owns server-function and route-handler glue in the consuming
|
|
5
|
+
* app. This entry point keeps the import path framework-shaped while exposing
|
|
6
|
+
* the same browser-safe primitives as the framework-neutral client surface.
|
|
7
|
+
*/
|
|
8
|
+
export * from '../index';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tanstack-start/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,UAAU,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { LOG_LEVEL_PRIORITY, LogLevel, Logger, createLogger, defaultLoggerConfig, logger } from "../logging/index.mjs";
|
|
2
|
+
import { t as PermissionChecker } from "../permission-service-DUyKomU3.mjs";
|
|
3
|
+
import { _ as initializePermissions, a as ProtectedSection, c as useHasPermission, d as usePermission, f as usePermissionChecker, g as getPermissions, h as getPermissionChecker, i as ProtectedLink, l as useHasPermissions, m as PermissionInitializer, n as ProtectedAction, o as useAllPermissions, p as usePermissions, r as ProtectedButton, s as useHasModulePermission, t as PERMISSION_MODULES, u as useModulePermission, v as usePermissionStore } from "../permissions-C5dTP9y4.mjs";
|
|
4
|
+
import { t as evaluateAccessRequirement } from "../access-BHy1v5sr.mjs";
|
|
5
|
+
import { TenantProvider, configureTenantClient, configureTenantEndpoints, getTenantCookieName, initializeTenantStore, normalizeTenantDataPayload, normalizeTenantSwitchPayload, resetTenantClientConfig, useCurrentOrganization, useCurrentTenant, useCurrentTenantData, useOrganization, useTenant, useTenantData, useTenantStore, useTenantSwitcher } from "../tenant/index.mjs";
|
|
6
|
+
import { IdentityContextProvider, configureIdentityEndpoints, configureIdentityStore, initialIdentityContext, normalizeIdentityContextPayload, normalizeIdentityPermissionsPayload, resetIdentityStoreConfig, useHasEntitlement, useHasSubscription, useIdentityContext, useIdentityStore, useIsOnboarded, useUserPermissions, useUserTenants } from "../identity/index.mjs";
|
|
7
|
+
import { t as isApiClientError } from "../types-DqCXinAk.mjs";
|
|
8
|
+
import { a as withRetry, c as handleApiError, i as isRetryableError, l as ErrorCodes, n as createRetryFetch, o as ApiError, r as fetchWithTimeout, s as ErrorHandler, t as calculateBackoffDelay } from "../api-client-CB7XZaMg.mjs";
|
|
9
|
+
import { a as getIdentitySignalRClient, i as createIdentitySignalRClient, n as SignalRClient, o as initializeSignalR, r as cleanupSignalR, t as IdentitySignalRClient } from "../signalr-C-TA8nEE.mjs";
|
|
10
|
+
import { i as invalidationHelpers, n as createQueryClient, r as createQueryKeyFactory, t as cachePresets } from "../query-client-_xB300gc.mjs";
|
|
11
|
+
export { ApiError, ErrorCodes, ErrorHandler, IdentityContextProvider, IdentitySignalRClient, LOG_LEVEL_PRIORITY, LogLevel, Logger, PERMISSION_MODULES, PermissionChecker, PermissionInitializer, ProtectedAction, ProtectedButton, ProtectedLink, ProtectedSection, SignalRClient, TenantProvider, cachePresets, calculateBackoffDelay, cleanupSignalR, configureIdentityEndpoints, configureIdentityStore, configureTenantClient, configureTenantEndpoints, createIdentitySignalRClient, createLogger, createQueryClient, createQueryKeyFactory, createRetryFetch, defaultLoggerConfig, evaluateAccessRequirement, fetchWithTimeout, getIdentitySignalRClient, getPermissionChecker, getPermissions, getTenantCookieName, handleApiError, initialIdentityContext, initializePermissions, initializeSignalR, initializeTenantStore, invalidationHelpers, isApiClientError, isRetryableError, logger, normalizeIdentityContextPayload, normalizeIdentityPermissionsPayload, normalizeTenantDataPayload, normalizeTenantSwitchPayload, resetIdentityStoreConfig, resetTenantClientConfig, useAllPermissions, useCurrentOrganization, useCurrentTenant, useCurrentTenantData, useHasEntitlement, useHasModulePermission, useHasPermission, useHasPermissions, useHasSubscription, useIdentityContext, useIdentityStore, useIsOnboarded, useModulePermission, useOrganization, usePermission, usePermissionChecker, usePermissionStore, usePermissions, useTenant, useTenantData, useTenantStore, useTenantSwitcher, useUserPermissions, useUserTenants, withRetry };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TanStack Start server adapter exports.
|
|
3
|
+
*
|
|
4
|
+
* Server functions, middleware, and route handlers stay app-owned. The
|
|
5
|
+
* adapter only aliases Spine's framework-neutral server contract so native
|
|
6
|
+
* Web Request/Response behavior is preserved.
|
|
7
|
+
*/
|
|
8
|
+
export * from '../server';
|
|
9
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/tanstack-start/server.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,WAAW,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { LOG_LEVEL_PRIORITY, LogLevel, Logger, createLogger, defaultLoggerConfig, logger } from "../logging/index.mjs";
|
|
2
|
+
import { t as PermissionChecker } from "../permission-service-DUyKomU3.mjs";
|
|
3
|
+
import { _ as initializePermissions, a as ProtectedSection, c as useHasPermission, d as usePermission, f as usePermissionChecker, g as getPermissions, h as getPermissionChecker, i as ProtectedLink, l as useHasPermissions, m as PermissionInitializer, n as ProtectedAction, o as useAllPermissions, p as usePermissions, r as ProtectedButton, s as useHasModulePermission, t as PERMISSION_MODULES, u as useModulePermission, v as usePermissionStore } from "../permissions-C5dTP9y4.mjs";
|
|
4
|
+
import { t as evaluateAccessRequirement } from "../access-BHy1v5sr.mjs";
|
|
5
|
+
import { TenantProvider, configureTenantClient, configureTenantEndpoints, getTenantCookieName, initializeTenantStore, normalizeTenantDataPayload, normalizeTenantSwitchPayload, resetTenantClientConfig, useCurrentOrganization, useCurrentTenant, useCurrentTenantData, useOrganization, useTenant, useTenantData, useTenantStore, useTenantSwitcher } from "../tenant/index.mjs";
|
|
6
|
+
import { IdentityContextProvider, configureIdentityEndpoints, configureIdentityStore, initialIdentityContext, normalizeIdentityContextPayload, normalizeIdentityPermissionsPayload, resetIdentityStoreConfig, useHasEntitlement, useHasSubscription, useIdentityContext, useIdentityStore, useIsOnboarded, useUserPermissions, useUserTenants } from "../identity/index.mjs";
|
|
7
|
+
import { t as isApiClientError } from "../types-DqCXinAk.mjs";
|
|
8
|
+
import { a as withRetry, c as handleApiError, i as isRetryableError, l as ErrorCodes, n as createRetryFetch, o as ApiError, r as fetchWithTimeout, s as ErrorHandler, t as calculateBackoffDelay } from "../api-client-CB7XZaMg.mjs";
|
|
9
|
+
import { a as getIdentitySignalRClient, i as createIdentitySignalRClient, n as SignalRClient, o as initializeSignalR, r as cleanupSignalR, t as IdentitySignalRClient } from "../signalr-C-TA8nEE.mjs";
|
|
10
|
+
import { i as invalidationHelpers, n as createQueryClient, r as createQueryKeyFactory, t as cachePresets } from "../query-client-_xB300gc.mjs";
|
|
11
|
+
import { S as updateAuthSession, _ as isSessionValid, a as createOAuthState, b as listCurrentUserAuthSessions, c as destroyAuthSessionById, d as destroyAuthSessionsBySid, f as destroyAuthSessionsForUser, g as getOAuthState, h as getCurrentAuthSessionId, i as createAuthSession, l as destroyAuthSessionsByIdentitySession, m as getAuthSessionById, n as cleanupExpiredOAuthStates, o as deleteOAuthState, p as getAuthSession, r as closeRedisConnection, s as destroyAuthSession, t as AUTH_ERROR_COOKIE_PREFIX, u as destroyAuthSessionsBySessionState, v as listAuthSessionDataForUser, x as requireAuthSession, y as listAuthSessionsForUser } from "../redis-session-storage.server-n0lWWyoh.mjs";
|
|
12
|
+
import { A as resetAuthClaimMapping, C as handleCallback, D as logout, E as login, O as refreshTokens, S as handleBackChannelLogout, T as hasAuthError, _ as createAuthorizationResponseUrl, a as configureRouteProtection, b as getAuthError, c as protectRoute, d as resetRouteProtectionConfig, f as shouldRefreshToken, g as configureAuthClaimMapping, h as clearAuthServerCache, i as authRoute, k as requireAuth, l as protectedRoute, m as clearAuthErrorHeaders, n as isTokenExpiredResponse, o as onboardingRoute, p as subscriptionRoute, r as shouldAttemptTokenRefresh, s as policyRoute, t as attemptTokenRefresh, u as publicRoute, v as extractUserInfo, w as handleFrontChannelLogout, x as getUser, y as getAccessToken } from "../server-C3_iNZfj.mjs";
|
|
13
|
+
import { clearActiveTenant, clearCurrentTenant, configureIdentityContextFetcher, configureTenantCookie, configureTenantResolution, getActiveTenant, getAvailableTenants, getCurrentTenant, getTenantFromIdentityContext, initializeTenant, isTenantMember, resetTenantCookieConfig, resetTenantResolutionConfig, setActiveTenant, setCurrentTenant } from "../tenant/server.mjs";
|
|
14
|
+
import { clearIdentityContextCache, configureIdentityAPIFetcher, configurePermissionFetcher, contextToUserInfo, fetchIdentityContext, fetchUserPermissions, getIdentityContext, hasContextVersionChanged, updateContextVersion } from "../identity/server.mjs";
|
|
15
|
+
import { n as createFetchMiddleware, r as createAPIConfigFactory, t as createApiClient } from "../server-pnsLybIH.mjs";
|
|
16
|
+
import { checkPermission, configurePermissionRouteProtection, requirePermission, resetPermissionRouteProtection, withPermission } from "../server.mjs";
|
|
17
|
+
export { AUTH_ERROR_COOKIE_PREFIX, ApiError, ErrorCodes, ErrorHandler, IdentityContextProvider, IdentitySignalRClient, LOG_LEVEL_PRIORITY, LogLevel, Logger, PERMISSION_MODULES, PermissionChecker, PermissionInitializer, ProtectedAction, ProtectedButton, ProtectedLink, ProtectedSection, SignalRClient, TenantProvider, attemptTokenRefresh, authRoute, cachePresets, calculateBackoffDelay, checkPermission, cleanupExpiredOAuthStates, cleanupSignalR, clearActiveTenant, clearAuthErrorHeaders, clearAuthServerCache, clearCurrentTenant, clearIdentityContextCache, closeRedisConnection, configureAuthClaimMapping, configureIdentityAPIFetcher, configureIdentityContextFetcher, configureIdentityEndpoints, configureIdentityStore, configurePermissionFetcher, configurePermissionRouteProtection, configureRouteProtection, configureTenantClient, configureTenantCookie, configureTenantEndpoints, configureTenantResolution, contextToUserInfo, createAPIConfigFactory, createApiClient, createAuthSession, createAuthorizationResponseUrl, createFetchMiddleware, createIdentitySignalRClient, createLogger, createOAuthState, createQueryClient, createQueryKeyFactory, createRetryFetch, defaultLoggerConfig, deleteOAuthState, destroyAuthSession, destroyAuthSessionById, destroyAuthSessionsByIdentitySession, destroyAuthSessionsBySessionState, destroyAuthSessionsBySid, destroyAuthSessionsForUser, evaluateAccessRequirement, extractUserInfo, fetchIdentityContext, fetchUserPermissions, fetchWithTimeout, getAccessToken, getActiveTenant, getAuthError, getAuthSession, getAuthSessionById, getAvailableTenants, getCurrentAuthSessionId, getCurrentTenant, getIdentityContext, getIdentitySignalRClient, getOAuthState, getPermissionChecker, getPermissions, getTenantCookieName, getTenantFromIdentityContext, getUser, handleApiError, handleBackChannelLogout, handleCallback, handleFrontChannelLogout, hasAuthError, hasContextVersionChanged, initialIdentityContext, initializePermissions, initializeSignalR, initializeTenant, initializeTenantStore, invalidationHelpers, isApiClientError, isRetryableError, isSessionValid, isTenantMember, isTokenExpiredResponse, listAuthSessionDataForUser, listAuthSessionsForUser, listCurrentUserAuthSessions, logger, login, logout, normalizeIdentityContextPayload, normalizeIdentityPermissionsPayload, normalizeTenantDataPayload, normalizeTenantSwitchPayload, onboardingRoute, policyRoute, protectRoute, protectedRoute, publicRoute, refreshTokens, requireAuth, requireAuthSession, requirePermission, resetAuthClaimMapping, resetIdentityStoreConfig, resetPermissionRouteProtection, resetRouteProtectionConfig, resetTenantClientConfig, resetTenantCookieConfig, resetTenantResolutionConfig, setActiveTenant, setCurrentTenant, shouldAttemptTokenRefresh, shouldRefreshToken, subscriptionRoute, updateAuthSession, updateContextVersion, useAllPermissions, useCurrentOrganization, useCurrentTenant, useCurrentTenantData, useHasEntitlement, useHasModulePermission, useHasPermission, useHasPermissions, useHasSubscription, useIdentityContext, useIdentityStore, useIsOnboarded, useModulePermission, useOrganization, usePermission, usePermissionChecker, usePermissionStore, usePermissions, useTenant, useTenantData, useTenantStore, useTenantSwitcher, useUserPermissions, useUserTenants, withPermission, withRetry };
|
package/dist/tenant/server.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { p as getAuthSession } from "../redis-session-storage.server-
|
|
1
|
+
import { p as getAuthSession } from "../redis-session-storage.server-n0lWWyoh.mjs";
|
|
2
2
|
//#region src/tenant/tenant-cookie.server.ts
|
|
3
3
|
const DEFAULT_TENANT_COOKIE_CONFIG = {
|
|
4
4
|
name: "__spine_tenant",
|
|
@@ -21,7 +21,11 @@ function readCookieValue(cookieHeader, name) {
|
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* Read the active tenant selector from the cookie.
|
|
25
|
+
*
|
|
26
|
+
* This is intentionally a raw cookie helper. Call `getCurrentTenant` from the
|
|
27
|
+
* tenant server module before using the value in an authorization-sensitive
|
|
28
|
+
* server operation.
|
|
25
29
|
*/
|
|
26
30
|
async function getActiveTenant(request) {
|
|
27
31
|
const config = getTenantCookieConfig();
|
|
@@ -70,6 +74,7 @@ function resetTenantCookieConfig() {
|
|
|
70
74
|
* Server-side tenant management.
|
|
71
75
|
*/
|
|
72
76
|
let identityContextFetcher = null;
|
|
77
|
+
const identityContextResolutionRequests = /* @__PURE__ */ new WeakSet();
|
|
73
78
|
let resolveInitialTenant = ({ identityContext }) => {
|
|
74
79
|
const membership = identityContext?.memberships?.[0];
|
|
75
80
|
return membership?.tenantId ?? membership?.id ?? null;
|
|
@@ -97,13 +102,28 @@ function resetTenantResolutionConfig() {
|
|
|
97
102
|
};
|
|
98
103
|
resolveAvailableTenants = ({ identityContext }) => identityContext?.memberships?.map((membership) => membership.tenantId ?? membership.id).filter((tenantId) => Boolean(tenantId)) ?? [];
|
|
99
104
|
}
|
|
105
|
+
function isIdentityContextResolutionInProgress(request) {
|
|
106
|
+
return identityContextResolutionRequests.has(request);
|
|
107
|
+
}
|
|
108
|
+
async function fetchConfiguredIdentityContext(request) {
|
|
109
|
+
const fetcher = identityContextFetcher;
|
|
110
|
+
if (!fetcher || isIdentityContextResolutionInProgress(request)) return null;
|
|
111
|
+
identityContextResolutionRequests.add(request);
|
|
112
|
+
try {
|
|
113
|
+
return await fetcher(request);
|
|
114
|
+
} finally {
|
|
115
|
+
identityContextResolutionRequests.delete(request);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
100
118
|
/**
|
|
101
119
|
* Get current active tenant from cookie
|
|
102
120
|
*/
|
|
103
|
-
async function getCurrentTenant(request) {
|
|
121
|
+
async function getCurrentTenant(request, requestedTenantId) {
|
|
122
|
+
if (isIdentityContextResolutionInProgress(request)) return null;
|
|
104
123
|
try {
|
|
124
|
+
if (requestedTenantId) return await isTenantMember(request, requestedTenantId) ? requestedTenantId : null;
|
|
105
125
|
const tenantFromCookie = await getActiveTenant(request);
|
|
106
|
-
if (tenantFromCookie) return tenantFromCookie;
|
|
126
|
+
if (tenantFromCookie && await isTenantMember(request, tenantFromCookie)) return tenantFromCookie;
|
|
107
127
|
return null;
|
|
108
128
|
} catch (error) {
|
|
109
129
|
console.error("Error getting current tenant:", error);
|
|
@@ -114,14 +134,16 @@ async function getCurrentTenant(request) {
|
|
|
114
134
|
* Get tenant from identity context
|
|
115
135
|
*/
|
|
116
136
|
async function getTenantFromIdentityContext(request) {
|
|
137
|
+
if (isIdentityContextResolutionInProgress(request)) return null;
|
|
117
138
|
try {
|
|
118
139
|
const sessionData = await getAuthSession(request);
|
|
119
140
|
if (!sessionData?.user?.sub) return null;
|
|
141
|
+
if (typeof sessionData.expiresAt === "number" && Date.now() >= sessionData.expiresAt) return null;
|
|
120
142
|
if (!identityContextFetcher) {
|
|
121
143
|
console.warn("Identity context fetcher not configured");
|
|
122
144
|
return null;
|
|
123
145
|
}
|
|
124
|
-
const context = await
|
|
146
|
+
const context = await fetchConfiguredIdentityContext(request);
|
|
125
147
|
return resolveInitialTenant({
|
|
126
148
|
request,
|
|
127
149
|
session: sessionData,
|
|
@@ -136,14 +158,16 @@ async function getTenantFromIdentityContext(request) {
|
|
|
136
158
|
* Get available tenants from identity context
|
|
137
159
|
*/
|
|
138
160
|
async function getAvailableTenants(request) {
|
|
161
|
+
if (isIdentityContextResolutionInProgress(request)) return [];
|
|
139
162
|
try {
|
|
140
163
|
const sessionData = await getAuthSession(request);
|
|
141
164
|
if (!sessionData?.user?.sub) return [];
|
|
165
|
+
if (typeof sessionData.expiresAt === "number" && Date.now() >= sessionData.expiresAt) return [];
|
|
142
166
|
if (!identityContextFetcher) {
|
|
143
167
|
console.warn("Identity context fetcher not configured");
|
|
144
168
|
return [];
|
|
145
169
|
}
|
|
146
|
-
const context = await
|
|
170
|
+
const context = await fetchConfiguredIdentityContext(request);
|
|
147
171
|
return resolveAvailableTenants({
|
|
148
172
|
request,
|
|
149
173
|
session: sessionData,
|
|
@@ -155,6 +179,17 @@ async function getAvailableTenants(request) {
|
|
|
155
179
|
}
|
|
156
180
|
}
|
|
157
181
|
/**
|
|
182
|
+
* Validate a tenant selector against the current identity membership set.
|
|
183
|
+
*
|
|
184
|
+
* Cookies and request options are selectors only; this check is the boundary
|
|
185
|
+
* before a tenant ID is used in a server-authoritative operation such as an
|
|
186
|
+
* API header or a session-backed tenant switch.
|
|
187
|
+
*/
|
|
188
|
+
async function isTenantMember(request, tenantId) {
|
|
189
|
+
if (!tenantId || isIdentityContextResolutionInProgress(request)) return false;
|
|
190
|
+
return (await getAvailableTenants(request)).includes(tenantId);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
158
193
|
* Set current active tenant
|
|
159
194
|
*/
|
|
160
195
|
async function setCurrentTenant(request, tenantId) {
|
|
@@ -164,6 +199,11 @@ async function setCurrentTenant(request, tenantId) {
|
|
|
164
199
|
success: false,
|
|
165
200
|
error: "No active session"
|
|
166
201
|
};
|
|
202
|
+
if (!await isTenantMember(request, tenantId)) return {
|
|
203
|
+
headers: new Headers(),
|
|
204
|
+
success: false,
|
|
205
|
+
error: "Tenant is not available for the active user"
|
|
206
|
+
};
|
|
167
207
|
const cookieValue = await setActiveTenant(tenantId);
|
|
168
208
|
const headers = new Headers();
|
|
169
209
|
headers.append("Set-Cookie", cookieValue);
|
|
@@ -213,6 +253,6 @@ async function clearCurrentTenant() {
|
|
|
213
253
|
return headers;
|
|
214
254
|
}
|
|
215
255
|
//#endregion
|
|
216
|
-
export { clearActiveTenant, clearCurrentTenant, configureIdentityContextFetcher, configureTenantCookie, configureTenantResolution, getActiveTenant, getAvailableTenants, getCurrentTenant, getTenantFromIdentityContext, initializeTenant, resetTenantCookieConfig, resetTenantResolutionConfig, setActiveTenant, setCurrentTenant };
|
|
256
|
+
export { clearActiveTenant, clearCurrentTenant, configureIdentityContextFetcher, configureTenantCookie, configureTenantResolution, getActiveTenant, getAvailableTenants, getCurrentTenant, getTenantFromIdentityContext, initializeTenant, isTenantMember, resetTenantCookieConfig, resetTenantResolutionConfig, setActiveTenant, setCurrentTenant };
|
|
217
257
|
|
|
218
258
|
//# sourceMappingURL=server.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.mjs","names":[],"sources":["../../src/tenant/tenant-cookie.server.ts","../../src/tenant/tenant.server.ts"],"sourcesContent":["/**\n * Tenant Cookie Server Module\n * \n * Cookie-based tenant storage.\n */\n\nexport type TenantCookieSameSite = 'Strict' | 'Lax' | 'None';\n\nexport interface TenantCookieConfig {\n name?: string;\n maxAge?: number;\n path?: string;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: TenantCookieSameSite;\n}\n\nconst DEFAULT_TENANT_COOKIE_CONFIG: Required<TenantCookieConfig> = {\n name: '__spine_tenant',\n maxAge: 60 * 60 * 24 * 365,\n path: '/',\n secure: process.env.NODE_ENV === 'production',\n httpOnly: false,\n sameSite: 'Lax',\n};\n\nlet tenantCookieConfig: Required<TenantCookieConfig> = {\n ...DEFAULT_TENANT_COOKIE_CONFIG,\n};\n\nfunction getTenantCookieConfig(): Required<TenantCookieConfig> {\n return tenantCookieConfig;\n}\n\nfunction readCookieValue(cookieHeader: string, name: string): string | null {\n for (const segment of cookieHeader.split(';')) {\n const [rawName, ...rawValue] = segment.split('=');\n if (rawName?.trim() !== name || rawValue.length === 0) {\n continue;\n }\n\n return decodeURIComponent(rawValue.join('='));\n }\n\n return null;\n}\n\n/**\n * Get active tenant from cookie\n */\nexport async function getActiveTenant(request: Request): Promise<string | null> {\n const config = getTenantCookieConfig();\n const cookies = request.headers.get('Cookie');\n if (!cookies) return null;\n\n return readCookieValue(cookies, config.name);\n}\n\n/**\n * Set active tenant cookie value\n */\nexport async function setActiveTenant(tenantId: string): Promise<string> {\n const config = getTenantCookieConfig();\n const secure = config.secure ? '; Secure' : '';\n const httpOnly = config.httpOnly ? '; HttpOnly' : '';\n return `${config.name}=${encodeURIComponent(tenantId)}; Path=${config.path}${httpOnly}; SameSite=${config.sameSite}; Max-Age=${config.maxAge}${secure}`;\n}\n\n/**\n * Clear active tenant cookie value\n */\nexport async function clearActiveTenant(): Promise<string> {\n const config = getTenantCookieConfig();\n const secure = config.secure ? '; Secure' : '';\n const httpOnly = config.httpOnly ? '; HttpOnly' : '';\n return `${config.name}=; Path=${config.path}${httpOnly}; SameSite=${config.sameSite}; Max-Age=0${secure}`;\n}\n\n/**\n * Configure tenant cookie name\n */\nexport function configureTenantCookie(config: TenantCookieConfig): void {\n tenantCookieConfig = {\n ...tenantCookieConfig,\n ...config,\n };\n}\n\n/**\n * Reset tenant cookie config back to defaults.\n */\nexport function resetTenantCookieConfig(): void {\n tenantCookieConfig = {\n ...DEFAULT_TENANT_COOKIE_CONFIG,\n };\n}\n","/**\n * Tenant Server Module\n * \n * Server-side tenant management.\n */\n\nimport { getAuthSession } from '../auth/redis-session-storage.server';\nimport { getActiveTenant, setActiveTenant, clearActiveTenant } from './tenant-cookie.server';\nimport type { SessionData } from '../auth/types';\n\n/**\n * Identity context fetcher type (to avoid circular dependency)\n */\nexport type IdentityContextFetcher = (request: Request) => Promise<{\n memberships?: Array<{ tenantId?: string; id?: string; [key: string]: unknown }>;\n [key: string]: unknown;\n} | null>;\n\nexport interface TenantResolutionContext {\n request: Request;\n session: SessionData;\n identityContext: Awaited<ReturnType<IdentityContextFetcher>>;\n}\n\nexport interface TenantResolutionConfig {\n identityContextFetcher?: IdentityContextFetcher;\n resolveInitialTenant?: (context: TenantResolutionContext) => Promise<string | null> | string | null;\n resolveAvailableTenants?: (context: TenantResolutionContext) => Promise<string[]> | string[];\n}\n\nlet identityContextFetcher: IdentityContextFetcher | null = null;\nlet resolveInitialTenant: NonNullable<TenantResolutionConfig['resolveInitialTenant']> =\n ({ identityContext }) => {\n const membership = identityContext?.memberships?.[0];\n return membership?.tenantId ?? membership?.id ?? null;\n };\nlet resolveAvailableTenants: NonNullable<TenantResolutionConfig['resolveAvailableTenants']> =\n ({ identityContext }) =>\n identityContext?.memberships\n ?.map((membership) => membership.tenantId ?? membership.id)\n .filter((tenantId): tenantId is string => Boolean(tenantId)) ?? [];\n\n/**\n * Configure identity context fetcher\n */\nexport function configureIdentityContextFetcher(fetcher: IdentityContextFetcher): void {\n identityContextFetcher = fetcher;\n}\n\n/**\n * Configure tenant resolution for custom backend identity contracts.\n */\nexport function configureTenantResolution(config: TenantResolutionConfig): void {\n if (config.identityContextFetcher) {\n identityContextFetcher = config.identityContextFetcher;\n }\n if (config.resolveInitialTenant) {\n resolveInitialTenant = config.resolveInitialTenant;\n }\n if (config.resolveAvailableTenants) {\n resolveAvailableTenants = config.resolveAvailableTenants;\n }\n}\n\nexport function resetTenantResolutionConfig(): void {\n identityContextFetcher = null;\n resolveInitialTenant = ({ identityContext }) => {\n const membership = identityContext?.memberships?.[0];\n return membership?.tenantId ?? membership?.id ?? null;\n };\n resolveAvailableTenants = ({ identityContext }) =>\n identityContext?.memberships\n ?.map((membership) => membership.tenantId ?? membership.id)\n .filter((tenantId): tenantId is string => Boolean(tenantId)) ?? [];\n}\n\n/**\n * Get current active tenant from cookie\n */\nexport async function getCurrentTenant(request: Request): Promise<string | null> {\n try {\n const tenantFromCookie = await getActiveTenant(request);\n if (tenantFromCookie) {\n return tenantFromCookie;\n }\n return null;\n } catch (error) {\n console.error('Error getting current tenant:', error);\n return null;\n }\n}\n\n/**\n * Get tenant from identity context\n */\nexport async function getTenantFromIdentityContext(request: Request): Promise<string | null> {\n try {\n const sessionData = await getAuthSession(request);\n if (!sessionData?.user?.sub) {\n return null;\n }\n\n if (!identityContextFetcher) {\n console.warn('Identity context fetcher not configured');\n return null;\n }\n\n const context = await identityContextFetcher(request);\n return resolveInitialTenant({ request, session: sessionData, identityContext: context });\n } catch (error) {\n console.error('Error getting tenant from identity context:', error);\n return null;\n }\n}\n\n/**\n * Get available tenants from identity context\n */\nexport async function getAvailableTenants(request: Request): Promise<string[]> {\n try {\n const sessionData = await getAuthSession(request);\n if (!sessionData?.user?.sub) {\n return [];\n }\n\n if (!identityContextFetcher) {\n console.warn('Identity context fetcher not configured');\n return [];\n }\n\n const context = await identityContextFetcher(request);\n return resolveAvailableTenants({ request, session: sessionData, identityContext: context });\n } catch (error) {\n console.error('Error getting available tenants:', error);\n return [];\n }\n}\n\n/**\n * Set current active tenant\n */\nexport async function setCurrentTenant(\n request: Request,\n tenantId: string\n): Promise<{ headers: Headers; success: boolean; error?: string }> {\n try {\n const sessionData = await getAuthSession(request);\n\n if (!sessionData.user) {\n return { headers: new Headers(), success: false, error: 'No active session' };\n }\n\n const cookieValue = await setActiveTenant(tenantId);\n const headers = new Headers();\n headers.append('Set-Cookie', cookieValue);\n\n return { headers, success: true };\n } catch (error) {\n console.error('Failed to set current tenant:', error);\n return {\n headers: new Headers(),\n success: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n}\n\n/**\n * Initialize tenant on first login\n */\nexport async function initializeTenant(\n request: Request\n): Promise<{ headers: Headers; tenantId: string } | null> {\n try {\n const sessionData = await getAuthSession(request);\n if (!sessionData?.user) {\n return null;\n }\n\n // Check if tenant is already set in cookie\n const currentTenant = await getCurrentTenant(request);\n if (currentTenant) {\n return null;\n }\n\n // Get tenant from identity context\n const tenantId = await getTenantFromIdentityContext(request);\n\n if (tenantId) {\n const cookieValue = await setActiveTenant(tenantId);\n const headers = new Headers();\n headers.append('Set-Cookie', cookieValue);\n return { headers, tenantId };\n }\n\n return null;\n } catch (error) {\n console.error('Failed to initialize tenant:', error);\n return null;\n }\n}\n\n/**\n * Clear current tenant\n */\nexport async function clearCurrentTenant(): Promise<Headers> {\n const cookieValue = await clearActiveTenant();\n const headers = new Headers();\n headers.append('Set-Cookie', cookieValue);\n return headers;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.mjs","names":[],"sources":["../../src/tenant/tenant-cookie.server.ts","../../src/tenant/tenant.server.ts"],"sourcesContent":["/**\n * Tenant Cookie Server Module\n * \n * Cookie-based tenant storage.\n */\n\nexport type TenantCookieSameSite = 'Strict' | 'Lax' | 'None';\n\nexport interface TenantCookieConfig {\n name?: string;\n maxAge?: number;\n path?: string;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: TenantCookieSameSite;\n}\n\nconst DEFAULT_TENANT_COOKIE_CONFIG: Required<TenantCookieConfig> = {\n name: '__spine_tenant',\n maxAge: 60 * 60 * 24 * 365,\n path: '/',\n secure: process.env.NODE_ENV === 'production',\n httpOnly: false,\n sameSite: 'Lax',\n};\n\nlet tenantCookieConfig: Required<TenantCookieConfig> = {\n ...DEFAULT_TENANT_COOKIE_CONFIG,\n};\n\nfunction getTenantCookieConfig(): Required<TenantCookieConfig> {\n return tenantCookieConfig;\n}\n\nfunction readCookieValue(cookieHeader: string, name: string): string | null {\n for (const segment of cookieHeader.split(';')) {\n const [rawName, ...rawValue] = segment.split('=');\n if (rawName?.trim() !== name || rawValue.length === 0) {\n continue;\n }\n\n return decodeURIComponent(rawValue.join('='));\n }\n\n return null;\n}\n\n/**\n * Read the active tenant selector from the cookie.\n *\n * This is intentionally a raw cookie helper. Call `getCurrentTenant` from the\n * tenant server module before using the value in an authorization-sensitive\n * server operation.\n */\nexport async function getActiveTenant(request: Request): Promise<string | null> {\n const config = getTenantCookieConfig();\n const cookies = request.headers.get('Cookie');\n if (!cookies) return null;\n\n return readCookieValue(cookies, config.name);\n}\n\n/**\n * Set active tenant cookie value\n */\nexport async function setActiveTenant(tenantId: string): Promise<string> {\n const config = getTenantCookieConfig();\n const secure = config.secure ? '; Secure' : '';\n const httpOnly = config.httpOnly ? '; HttpOnly' : '';\n return `${config.name}=${encodeURIComponent(tenantId)}; Path=${config.path}${httpOnly}; SameSite=${config.sameSite}; Max-Age=${config.maxAge}${secure}`;\n}\n\n/**\n * Clear active tenant cookie value\n */\nexport async function clearActiveTenant(): Promise<string> {\n const config = getTenantCookieConfig();\n const secure = config.secure ? '; Secure' : '';\n const httpOnly = config.httpOnly ? '; HttpOnly' : '';\n return `${config.name}=; Path=${config.path}${httpOnly}; SameSite=${config.sameSite}; Max-Age=0${secure}`;\n}\n\n/**\n * Configure tenant cookie name\n */\nexport function configureTenantCookie(config: TenantCookieConfig): void {\n tenantCookieConfig = {\n ...tenantCookieConfig,\n ...config,\n };\n}\n\n/**\n * Reset tenant cookie config back to defaults.\n */\nexport function resetTenantCookieConfig(): void {\n tenantCookieConfig = {\n ...DEFAULT_TENANT_COOKIE_CONFIG,\n };\n}\n","/**\n * Tenant Server Module\n * \n * Server-side tenant management.\n */\n\nimport { getAuthSession } from '../auth/redis-session-storage.server';\nimport { getActiveTenant, setActiveTenant, clearActiveTenant } from './tenant-cookie.server';\nimport type { SessionData } from '../auth/types';\n\n/**\n * Identity context fetcher type (to avoid circular dependency)\n */\nexport type IdentityContextFetcher = (request: Request) => Promise<{\n memberships?: Array<{ tenantId?: string; id?: string; [key: string]: unknown }>;\n [key: string]: unknown;\n} | null>;\n\nexport interface TenantResolutionContext {\n request: Request;\n session: SessionData;\n identityContext: Awaited<ReturnType<IdentityContextFetcher>>;\n}\n\nexport interface TenantResolutionConfig {\n identityContextFetcher?: IdentityContextFetcher;\n resolveInitialTenant?: (context: TenantResolutionContext) => Promise<string | null> | string | null;\n resolveAvailableTenants?: (context: TenantResolutionContext) => Promise<string[]> | string[];\n}\n\nlet identityContextFetcher: IdentityContextFetcher | null = null;\nconst identityContextResolutionRequests = new WeakSet<Request>();\nlet resolveInitialTenant: NonNullable<TenantResolutionConfig['resolveInitialTenant']> =\n ({ identityContext }) => {\n const membership = identityContext?.memberships?.[0];\n return membership?.tenantId ?? membership?.id ?? null;\n };\nlet resolveAvailableTenants: NonNullable<TenantResolutionConfig['resolveAvailableTenants']> =\n ({ identityContext }) =>\n identityContext?.memberships\n ?.map((membership) => membership.tenantId ?? membership.id)\n .filter((tenantId): tenantId is string => Boolean(tenantId)) ?? [];\n\n/**\n * Configure identity context fetcher\n */\nexport function configureIdentityContextFetcher(fetcher: IdentityContextFetcher): void {\n identityContextFetcher = fetcher;\n}\n\n/**\n * Configure tenant resolution for custom backend identity contracts.\n */\nexport function configureTenantResolution(config: TenantResolutionConfig): void {\n if (config.identityContextFetcher) {\n identityContextFetcher = config.identityContextFetcher;\n }\n if (config.resolveInitialTenant) {\n resolveInitialTenant = config.resolveInitialTenant;\n }\n if (config.resolveAvailableTenants) {\n resolveAvailableTenants = config.resolveAvailableTenants;\n }\n}\n\nexport function resetTenantResolutionConfig(): void {\n identityContextFetcher = null;\n resolveInitialTenant = ({ identityContext }) => {\n const membership = identityContext?.memberships?.[0];\n return membership?.tenantId ?? membership?.id ?? null;\n };\n resolveAvailableTenants = ({ identityContext }) =>\n identityContext?.memberships\n ?.map((membership) => membership.tenantId ?? membership.id)\n .filter((tenantId): tenantId is string => Boolean(tenantId)) ?? [];\n}\n\nfunction isIdentityContextResolutionInProgress(request: Request): boolean {\n return identityContextResolutionRequests.has(request);\n}\n\nasync function fetchConfiguredIdentityContext(\n request: Request,\n): Promise<Awaited<ReturnType<IdentityContextFetcher>>> {\n const fetcher = identityContextFetcher;\n if (!fetcher || isIdentityContextResolutionInProgress(request)) {\n return null;\n }\n\n identityContextResolutionRequests.add(request);\n try {\n return await fetcher(request);\n } finally {\n identityContextResolutionRequests.delete(request);\n }\n}\n\n/**\n * Get current active tenant from cookie\n */\nexport async function getCurrentTenant(\n request: Request,\n requestedTenantId?: string,\n): Promise<string | null> {\n // Identity fetchers commonly build their request through the same tenant\n // resolver. A nested selector is not authorization evidence and must not\n // recurse into another membership fetch.\n if (isIdentityContextResolutionInProgress(request)) {\n return null;\n }\n\n try {\n if (requestedTenantId) {\n return await isTenantMember(request, requestedTenantId) ? requestedTenantId : null;\n }\n\n const tenantFromCookie = await getActiveTenant(request);\n if (tenantFromCookie && await isTenantMember(request, tenantFromCookie)) {\n return tenantFromCookie;\n }\n\n return null;\n } catch (error) {\n console.error('Error getting current tenant:', error);\n return null;\n }\n}\n\n/**\n * Get tenant from identity context\n */\nexport async function getTenantFromIdentityContext(request: Request): Promise<string | null> {\n if (isIdentityContextResolutionInProgress(request)) {\n return null;\n }\n\n try {\n const sessionData = await getAuthSession(request);\n if (!sessionData?.user?.sub) {\n return null;\n }\n\n if (typeof sessionData.expiresAt === 'number' && Date.now() >= sessionData.expiresAt) {\n return null;\n }\n\n if (!identityContextFetcher) {\n console.warn('Identity context fetcher not configured');\n return null;\n }\n\n const context = await fetchConfiguredIdentityContext(request);\n return resolveInitialTenant({ request, session: sessionData, identityContext: context });\n } catch (error) {\n console.error('Error getting tenant from identity context:', error);\n return null;\n }\n}\n\n/**\n * Get available tenants from identity context\n */\nexport async function getAvailableTenants(request: Request): Promise<string[]> {\n if (isIdentityContextResolutionInProgress(request)) {\n return [];\n }\n\n try {\n const sessionData = await getAuthSession(request);\n if (!sessionData?.user?.sub) {\n return [];\n }\n\n if (typeof sessionData.expiresAt === 'number' && Date.now() >= sessionData.expiresAt) {\n return [];\n }\n\n if (!identityContextFetcher) {\n console.warn('Identity context fetcher not configured');\n return [];\n }\n\n const context = await fetchConfiguredIdentityContext(request);\n return resolveAvailableTenants({ request, session: sessionData, identityContext: context });\n } catch (error) {\n console.error('Error getting available tenants:', error);\n return [];\n }\n}\n\n/**\n * Validate a tenant selector against the current identity membership set.\n *\n * Cookies and request options are selectors only; this check is the boundary\n * before a tenant ID is used in a server-authoritative operation such as an\n * API header or a session-backed tenant switch.\n */\nexport async function isTenantMember(request: Request, tenantId: string): Promise<boolean> {\n if (!tenantId || isIdentityContextResolutionInProgress(request)) {\n return false;\n }\n\n const availableTenants = await getAvailableTenants(request);\n return availableTenants.includes(tenantId);\n}\n\n/**\n * Set current active tenant\n */\nexport async function setCurrentTenant(\n request: Request,\n tenantId: string\n): Promise<{ headers: Headers; success: boolean; error?: string }> {\n try {\n const sessionData = await getAuthSession(request);\n\n if (!sessionData.user) {\n return { headers: new Headers(), success: false, error: 'No active session' };\n }\n\n if (!await isTenantMember(request, tenantId)) {\n return {\n headers: new Headers(),\n success: false,\n error: 'Tenant is not available for the active user',\n };\n }\n\n const cookieValue = await setActiveTenant(tenantId);\n const headers = new Headers();\n headers.append('Set-Cookie', cookieValue);\n\n return { headers, success: true };\n } catch (error) {\n console.error('Failed to set current tenant:', error);\n return {\n headers: new Headers(),\n success: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n}\n\n/**\n * Initialize tenant on first login\n */\nexport async function initializeTenant(\n request: Request\n): Promise<{ headers: Headers; tenantId: string } | null> {\n try {\n const sessionData = await getAuthSession(request);\n if (!sessionData?.user) {\n return null;\n }\n\n // Check if tenant is already set in cookie\n const currentTenant = await getCurrentTenant(request);\n if (currentTenant) {\n return null;\n }\n\n // Get tenant from identity context\n const tenantId = await getTenantFromIdentityContext(request);\n\n if (tenantId) {\n const cookieValue = await setActiveTenant(tenantId);\n const headers = new Headers();\n headers.append('Set-Cookie', cookieValue);\n return { headers, tenantId };\n }\n\n return null;\n } catch (error) {\n console.error('Failed to initialize tenant:', error);\n return null;\n }\n}\n\n/**\n * Clear current tenant\n */\nexport async function clearCurrentTenant(): Promise<Headers> {\n const cookieValue = await clearActiveTenant();\n const headers = new Headers();\n headers.append('Set-Cookie', cookieValue);\n return headers;\n}\n"],"mappings":"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"}
|
|
@@ -13,7 +13,11 @@ export interface TenantCookieConfig {
|
|
|
13
13
|
sameSite?: TenantCookieSameSite;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Read the active tenant selector from the cookie.
|
|
17
|
+
*
|
|
18
|
+
* This is intentionally a raw cookie helper. Call `getCurrentTenant` from the
|
|
19
|
+
* tenant server module before using the value in an authorization-sensitive
|
|
20
|
+
* server operation.
|
|
17
21
|
*/
|
|
18
22
|
export declare function getActiveTenant(request: Request): Promise<string | null>;
|
|
19
23
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant-cookie.server.d.ts","sourceRoot":"","sources":["../../src/tenant/tenant-cookie.server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;AAE7D,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,oBAAoB,CAAC;CACjC;AAgCD
|
|
1
|
+
{"version":3,"file":"tenant-cookie.server.d.ts","sourceRoot":"","sources":["../../src/tenant/tenant-cookie.server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;AAE7D,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,oBAAoB,CAAC;CACjC;AAgCD;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAM9E;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAKvE;AAED;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CAKzD;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAKtE;AAED;;GAEG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAI9C"}
|
|
@@ -37,7 +37,7 @@ export declare function resetTenantResolutionConfig(): void;
|
|
|
37
37
|
/**
|
|
38
38
|
* Get current active tenant from cookie
|
|
39
39
|
*/
|
|
40
|
-
export declare function getCurrentTenant(request: Request): Promise<string | null>;
|
|
40
|
+
export declare function getCurrentTenant(request: Request, requestedTenantId?: string): Promise<string | null>;
|
|
41
41
|
/**
|
|
42
42
|
* Get tenant from identity context
|
|
43
43
|
*/
|
|
@@ -46,6 +46,14 @@ export declare function getTenantFromIdentityContext(request: Request): Promise<
|
|
|
46
46
|
* Get available tenants from identity context
|
|
47
47
|
*/
|
|
48
48
|
export declare function getAvailableTenants(request: Request): Promise<string[]>;
|
|
49
|
+
/**
|
|
50
|
+
* Validate a tenant selector against the current identity membership set.
|
|
51
|
+
*
|
|
52
|
+
* Cookies and request options are selectors only; this check is the boundary
|
|
53
|
+
* before a tenant ID is used in a server-authoritative operation such as an
|
|
54
|
+
* API header or a session-backed tenant switch.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isTenantMember(request: Request, tenantId: string): Promise<boolean>;
|
|
49
57
|
/**
|
|
50
58
|
* Set current active tenant
|
|
51
59
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant.server.d.ts","sourceRoot":"","sources":["../../src/tenant/tenant.server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;IACjE,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC,CAAC;IAChF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,GAAG,IAAI,CAAC,CAAC;AAEV,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,WAAW,CAAC;IACrB,eAAe,EAAE,OAAO,CAAC,UAAU,CAAC,sBAAsB,CAAC,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,sBAAsB;IACrC,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;IAChD,oBAAoB,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC;IACpG,uBAAuB,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC;CAC9F;
|
|
1
|
+
{"version":3,"file":"tenant.server.d.ts","sourceRoot":"","sources":["../../src/tenant/tenant.server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;IACjE,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC,CAAC;IAChF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,GAAG,IAAI,CAAC,CAAC;AAEV,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,WAAW,CAAC;IACrB,eAAe,EAAE,OAAO,CAAC,UAAU,CAAC,sBAAsB,CAAC,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,sBAAsB;IACrC,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;IAChD,oBAAoB,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC;IACpG,uBAAuB,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC;CAC9F;AAeD;;GAEG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,sBAAsB,GAAG,IAAI,CAErF;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,sBAAsB,GAAG,IAAI,CAU9E;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAUlD;AAsBD;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,OAAO,EAChB,iBAAiB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAuBxB;AAED;;GAEG;AACH,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA0B3F;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA0B7E;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOzF;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA6BjE;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,OAAO,GACf,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA4BxD;AAED;;GAEG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,OAAO,CAAC,CAK3D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types-DqCXinAk.mjs","names":[],"sources":["../src/api-client/types.ts"],"sourcesContent":["/**\n * API Client Types\n */\n\n// Re-export Logger from logging to avoid duplication\nexport type { LoggerConfig as Logger } from '../logging/types';\n\nexport interface APIConfig {\n basePath: string;\n accessToken?: string;\n headers: Record<string, string>;\n tenantId: string;\n credentials?: RequestCredentials;\n baseOptions?: {\n timeout?: number;\n };\n}\n\nexport interface CreateAPIConfigOptions {\n requireTenant?: boolean;\n includeAuth?: boolean;\n tenantId?: string;\n customHeaders?: Record<string, string>;\n}\n\nexport interface ProblemDetails {\n type: string;\n title: string;\n status: number;\n detail: string;\n traceId: string;\n requestId?: string;\n instance?: string;\n meta?: {\n errors?: ValidationError[];\n [key: string]: unknown;\n };\n}\n\nexport interface ValidationError {\n field: string;\n message: string;\n code?: string;\n}\n\nexport interface RetryConfig {\n maxRetries?: number;\n baseDelay?: number;\n maxDelay?: number;\n retryCondition?: (error: unknown) => boolean;\n}\n\nexport interface APIRequestConfig {\n url?: string;\n method?: string;\n headers?: Record<string, string>;\n}\n\nexport interface APIResponse<T = unknown> {\n data: T;\n status: number;\n statusText: string;\n headers: Record<string, string>;\n raw?: Response;\n}\n\nexport interface APIClientError<T = unknown> extends Error {\n isApiClientError: true;\n response?: APIResponse<T>;\n config?: APIRequestConfig;\n problemDetails?: ProblemDetails;\n shouldLogout?: boolean;\n originalError?: unknown;\n cause?: unknown;\n}\n\nexport function isApiClientError(error: unknown): error is APIClientError {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'isApiClientError' in error &&\n (error as { isApiClientError?: boolean }).isApiClientError === true\n );\n}\n\nexport type FetchAPI = typeof fetch;\n\nexport interface FetchRequestContext {\n fetch: FetchAPI;\n url: string;\n init: RequestInit;\n}\n\nexport interface FetchResponseContext extends FetchRequestContext {\n response: Response;\n}\n\nexport interface FetchErrorContext extends FetchRequestContext {\n error: unknown;\n response?: Response;\n}\n\nexport interface FetchMiddleware {\n pre?: (context: FetchRequestContext) => Promise<{ url: string; init: RequestInit } | void>;\n post?: (context: FetchResponseContext) => Promise<Response | void>;\n onError?: (context: FetchErrorContext) => Promise<Response | void>;\n}\n\nexport type EnhancedAPIClient<T> = {\n [K in keyof T]: T[K] extends (...args: infer A) => Promise<infer R>\n ? K extends `${string}Raw`\n ? T[K]\n : (...args: A) => Promise<APIResponse<R>>\n : T[K];\n};\n\n/**\n * API Logger interface for dependency injection (simplified)\n */\nexport interface APILogger {\n debug?: (message: string, context?: Record<string, unknown>) => void;\n info?: (message: string, context?: Record<string, unknown>) => void;\n warn?: (message: string, error?: Error, context?: Record<string, unknown>) => void;\n error?: (message: string, error?: Error, context?: Record<string, unknown>) => void;\n}\n\n/**\n * Function type for getting access token from request\n */\nexport type GetAccessTokenFn = (request: Request) => Promise<string | null>;\n\n/**\n * Function type for getting current tenant from request\n */\nexport type GetCurrentTenantFn = (request: Request) => Promise<string | null>;\n\n/**\n * Token refresh result\n */\nexport interface TokenRefreshResult {\n success: boolean;\n newAccessToken?: string;\n shouldLogout?: boolean;\n error?: string;\n}\n\n/**\n * Function type for attempting token refresh\n */\nexport type TokenRefreshFn = (request: Request) => Promise<TokenRefreshResult>;\n"],"mappings":";AA4EA,SAAgB,iBAAiB,OAAyC;CACxE,OACE,OAAO,UAAU,YACjB,UAAU,QACV,sBAAsB,SACrB,MAAyC,qBAAqB;AAEnE"}
|
|
1
|
+
{"version":3,"file":"types-DqCXinAk.mjs","names":[],"sources":["../src/api-client/types.ts"],"sourcesContent":["/**\n * API Client Types\n */\n\n// Re-export Logger from logging to avoid duplication\nexport type { LoggerConfig as Logger } from '../logging/types';\n\nexport interface APIConfig {\n basePath: string;\n accessToken?: string;\n headers: Record<string, string>;\n tenantId: string;\n credentials?: RequestCredentials;\n baseOptions?: {\n timeout?: number;\n };\n}\n\nexport interface CreateAPIConfigOptions {\n requireTenant?: boolean;\n includeAuth?: boolean;\n tenantId?: string;\n customHeaders?: Record<string, string>;\n}\n\nexport interface ProblemDetails {\n type: string;\n title: string;\n status: number;\n detail: string;\n traceId: string;\n requestId?: string;\n instance?: string;\n meta?: {\n errors?: ValidationError[];\n [key: string]: unknown;\n };\n}\n\nexport interface ValidationError {\n field: string;\n message: string;\n code?: string;\n}\n\nexport interface RetryConfig {\n maxRetries?: number;\n baseDelay?: number;\n maxDelay?: number;\n retryCondition?: (error: unknown) => boolean;\n}\n\nexport interface APIRequestConfig {\n url?: string;\n method?: string;\n headers?: Record<string, string>;\n}\n\nexport interface APIResponse<T = unknown> {\n data: T;\n status: number;\n statusText: string;\n headers: Record<string, string>;\n raw?: Response;\n}\n\nexport interface APIClientError<T = unknown> extends Error {\n isApiClientError: true;\n response?: APIResponse<T>;\n config?: APIRequestConfig;\n problemDetails?: ProblemDetails;\n shouldLogout?: boolean;\n originalError?: unknown;\n cause?: unknown;\n}\n\nexport function isApiClientError(error: unknown): error is APIClientError {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'isApiClientError' in error &&\n (error as { isApiClientError?: boolean }).isApiClientError === true\n );\n}\n\nexport type FetchAPI = typeof fetch;\n\nexport interface FetchRequestContext {\n fetch: FetchAPI;\n url: string;\n init: RequestInit;\n}\n\nexport interface FetchResponseContext extends FetchRequestContext {\n response: Response;\n}\n\nexport interface FetchErrorContext extends FetchRequestContext {\n error: unknown;\n response?: Response;\n}\n\nexport interface FetchMiddleware {\n pre?: (context: FetchRequestContext) => Promise<{ url: string; init: RequestInit } | void>;\n post?: (context: FetchResponseContext) => Promise<Response | void>;\n onError?: (context: FetchErrorContext) => Promise<Response | void>;\n}\n\nexport type EnhancedAPIClient<T> = {\n [K in keyof T]: T[K] extends (...args: infer A) => Promise<infer R>\n ? K extends `${string}Raw`\n ? T[K]\n : (...args: A) => Promise<APIResponse<R>>\n : T[K];\n};\n\n/**\n * API Logger interface for dependency injection (simplified)\n */\nexport interface APILogger {\n debug?: (message: string, context?: Record<string, unknown>) => void;\n info?: (message: string, context?: Record<string, unknown>) => void;\n warn?: (message: string, error?: Error, context?: Record<string, unknown>) => void;\n error?: (message: string, error?: Error, context?: Record<string, unknown>) => void;\n}\n\n/**\n * Function type for getting access token from request\n */\nexport type GetAccessTokenFn = (request: Request) => Promise<string | null>;\n\n/**\n * Function type for getting current tenant from request\n */\nexport type GetCurrentTenantFn = (\n request: Request,\n requestedTenantId?: string,\n) => Promise<string | null>;\n\n/**\n * Validate a tenant selector against the current server-side membership set.\n * Tenant cookies and explicit request overrides must not be treated as\n * authorization evidence by themselves.\n */\nexport type ValidateTenantFn = (request: Request, tenantId: string) => Promise<boolean> | boolean;\n\n/** Resolve the current server-side membership IDs for a request. */\nexport type GetAvailableTenantsFn = (request: Request) => Promise<readonly string[]> | readonly string[];\n\n/**\n * Token refresh result\n */\nexport interface TokenRefreshResult {\n success: boolean;\n newAccessToken?: string;\n shouldLogout?: boolean;\n error?: string;\n}\n\n/**\n * Function type for attempting token refresh\n */\nexport type TokenRefreshFn = (request: Request) => Promise<TokenRefreshResult>;\n"],"mappings":";AA4EA,SAAgB,iBAAiB,OAAyC;CACxE,OACE,OAAO,UAAU,YACjB,UAAU,QACV,sBAAsB,SACrB,MAAyC,qBAAqB;AAEnE"}
|
package/docs/adapters.md
CHANGED
|
@@ -13,7 +13,9 @@ Core primitives should not know about:
|
|
|
13
13
|
|
|
14
14
|
Adapters solve that by providing a framework-shaped surface without polluting the core package.
|
|
15
15
|
|
|
16
|
-
## Current
|
|
16
|
+
## Current Adapters
|
|
17
|
+
|
|
18
|
+
### React Router
|
|
17
19
|
|
|
18
20
|
Available entry points:
|
|
19
21
|
|
|
@@ -28,6 +30,56 @@ Benefits:
|
|
|
28
30
|
- adapter-specific helpers can be added later without changing import strategy
|
|
29
31
|
- future adapters can follow the same pattern
|
|
30
32
|
|
|
33
|
+
### TanStack Start
|
|
34
|
+
|
|
35
|
+
Available entry points:
|
|
36
|
+
|
|
37
|
+
- `@eminuckan/spine/tanstack-start`
|
|
38
|
+
- `@eminuckan/spine/tanstack-start/server`
|
|
39
|
+
|
|
40
|
+
These are also thin aliases over the framework-neutral client and server
|
|
41
|
+
exports. Spine does not import `@tanstack/react-start`, `@tanstack/react-router`,
|
|
42
|
+
or expose `createServerFn`/`createFileRoute` from its adapter. TanStack Start
|
|
43
|
+
server functions, middleware, route handlers, redirects, and CSRF configuration
|
|
44
|
+
remain owned by the consuming app.
|
|
45
|
+
|
|
46
|
+
Keep server-only request access inside the app-owned function module and return
|
|
47
|
+
only a safe projection across the RPC boundary:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// app/lib/viewer.functions.ts
|
|
51
|
+
import { createServerFn } from '@tanstack/react-start';
|
|
52
|
+
import { getRequest } from '@tanstack/react-start/server';
|
|
53
|
+
import { authRoute } from '@eminuckan/spine/tanstack-start/server';
|
|
54
|
+
|
|
55
|
+
export const getViewerStatus = createServerFn({ method: 'GET' }).handler(() =>
|
|
56
|
+
authRoute(getRequest(), () => ({ authenticated: true })),
|
|
57
|
+
);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use server routes for HTTP auth endpoints and return Spine's native
|
|
61
|
+
`Response` unchanged so status, redirects, and repeated `Set-Cookie` headers
|
|
62
|
+
survive the framework boundary:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// app/routes/auth.callback.tsx
|
|
66
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
67
|
+
import { handleCallback } from '@eminuckan/spine/tanstack-start/server';
|
|
68
|
+
|
|
69
|
+
export const Route = createFileRoute('/auth/callback')({
|
|
70
|
+
server: {
|
|
71
|
+
handlers: {
|
|
72
|
+
GET: ({ request }) => handleCallback(request),
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Do not return `SessionData`, access tokens, refresh tokens, or raw request
|
|
79
|
+
context from a server function or route. Keep tenant membership checks and
|
|
80
|
+
product-specific redirects in the app-owned wrapper, and configure TanStack
|
|
81
|
+
Start's CSRF middleware when the app defines a custom `src/start.ts`.
|
|
82
|
+
|
|
31
83
|
## Import Strategy
|
|
32
84
|
|
|
33
85
|
### Use root/core imports when
|
package/docs/architecture.md
CHANGED
|
@@ -47,8 +47,13 @@ Examples:
|
|
|
47
47
|
|
|
48
48
|
- `@eminuckan/spine/react-router`
|
|
49
49
|
- `@eminuckan/spine/react-router/server`
|
|
50
|
+
- `@eminuckan/spine/tanstack-start`
|
|
51
|
+
- `@eminuckan/spine/tanstack-start/server`
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
The React Router and TanStack Start adapters are intentionally thin. That is a
|
|
54
|
+
feature, not a gap: they keep the core honest while leaving framework glue and
|
|
55
|
+
product policy in the consuming app. TanStack Start server functions, route
|
|
56
|
+
handlers, redirects, and CSRF configuration remain app-owned.
|
|
52
57
|
|
|
53
58
|
### 3. Application Adapters
|
|
54
59
|
|