@connectum/auth 1.0.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +590 -0
- package/dist/index.d.ts +388 -0
- package/dist/index.js +637 -0
- package/dist/index.js.map +1 -0
- package/dist/testing/index.d.ts +104 -0
- package/dist/testing/index.js +52 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/types-IH8aZeWZ.d.ts +311 -0
- package/package.json +69 -0
- package/src/auth-interceptor.ts +137 -0
- package/src/authz-interceptor.ts +158 -0
- package/src/cache.ts +66 -0
- package/src/context.ts +63 -0
- package/src/errors.ts +45 -0
- package/src/gateway-auth-interceptor.ts +203 -0
- package/src/headers.ts +149 -0
- package/src/index.ts +49 -0
- package/src/jwt-auth-interceptor.ts +208 -0
- package/src/method-match.ts +46 -0
- package/src/session-auth-interceptor.ts +120 -0
- package/src/testing/index.ts +11 -0
- package/src/testing/mock-context.ts +44 -0
- package/src/testing/test-jwt.ts +75 -0
- package/src/testing/with-context.ts +33 -0
- package/src/types.ts +326 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/auth-interceptor.ts","../src/cache.ts","../src/context.ts","../src/types.ts","../src/headers.ts","../src/method-match.ts","../src/authz-interceptor.ts","../src/errors.ts","../src/gateway-auth-interceptor.ts","../src/jwt-auth-interceptor.ts","../src/session-auth-interceptor.ts"],"sourcesContent":["/**\n * Generic authentication interceptor\n *\n * Provides pluggable authentication for any credential type.\n * Extracts credentials, verifies them, and stores AuthContext\n * in AsyncLocalStorage for downstream access.\n *\n * @module auth-interceptor\n */\n\nimport type { Interceptor, StreamRequest, UnaryRequest } from \"@connectrpc/connect\";\nimport { Code, ConnectError } from \"@connectrpc/connect\";\nimport { LruCache } from \"./cache.ts\";\nimport { authContextStorage } from \"./context.ts\";\nimport { setAuthHeaders } from \"./headers.ts\";\nimport { matchesMethodPattern } from \"./method-match.ts\";\nimport type { AuthContext, AuthInterceptorOptions } from \"./types.ts\";\nimport { AUTH_HEADERS } from \"./types.ts\";\n\n/**\n * Default credential extractor.\n * Extracts Bearer token from Authorization header.\n */\nfunction defaultExtractCredentials(req: { header: Headers }): string | null {\n const authHeader = req.header.get(\"authorization\");\n if (!authHeader) {\n return null;\n }\n\n // Support \"Bearer <token>\" format\n const match = /^Bearer\\s+(.+)$/i.exec(authHeader);\n return match?.[1] ?? null;\n}\n\n/**\n * Create a generic authentication interceptor.\n *\n * Extracts credentials from request headers, verifies them using\n * a user-provided callback, and stores the resulting AuthContext\n * in AsyncLocalStorage for downstream access.\n *\n * @param options - Authentication options\n * @returns ConnectRPC interceptor\n *\n * @example API key authentication\n * ```typescript\n * import { createAuthInterceptor } from '@connectum/auth';\n *\n * const auth = createAuthInterceptor({\n * extractCredentials: (req) => req.header.get('x-api-key'),\n * verifyCredentials: async (apiKey) => {\n * const user = await db.findByApiKey(apiKey);\n * if (!user) throw new Error('Invalid API key');\n * return {\n * subject: user.id,\n * roles: user.roles,\n * scopes: [],\n * claims: {},\n * type: 'api-key',\n * };\n * },\n * });\n * ```\n *\n * @example Bearer token with default extractor\n * ```typescript\n * const auth = createAuthInterceptor({\n * verifyCredentials: async (token) => {\n * const payload = await verifyToken(token);\n * return {\n * subject: payload.sub,\n * roles: payload.roles ?? [],\n * scopes: payload.scope?.split(' ') ?? [],\n * claims: payload,\n * type: 'jwt',\n * };\n * },\n * });\n * ```\n */\nexport function createAuthInterceptor(options: AuthInterceptorOptions): Interceptor {\n const { extractCredentials = defaultExtractCredentials, verifyCredentials, skipMethods = [], propagateHeaders = false, cache: cacheOptions, propagatedClaims } = options;\n\n const cache = cacheOptions ? new LruCache<AuthContext>(cacheOptions) : undefined;\n\n return (next) => async (req: UnaryRequest | StreamRequest) => {\n const serviceName: string = req.service.typeName;\n const methodName: string = req.method.name;\n\n // Strip auth headers to prevent spoofing from external clients\n for (const headerName of Object.values(AUTH_HEADERS)) {\n req.header.delete(headerName);\n }\n\n // Skip specified methods\n if (matchesMethodPattern(serviceName, methodName, skipMethods)) {\n return await next(req);\n }\n\n // Extract credentials\n const credentials = await extractCredentials(req);\n if (!credentials) {\n throw new ConnectError(\"Missing credentials\", Code.Unauthenticated);\n }\n\n // Check cache before verification\n const cached = cache?.get(credentials);\n if (cached && (!cached.expiresAt || cached.expiresAt.getTime() > Date.now())) {\n if (propagateHeaders) {\n setAuthHeaders(req.header, cached, propagatedClaims);\n }\n return await authContextStorage.run(cached, () => next(req));\n }\n\n // Verify credentials\n let authContext: AuthContext;\n try {\n authContext = await verifyCredentials(credentials);\n } catch (err) {\n if (err instanceof ConnectError) {\n throw err;\n }\n throw new ConnectError(\"Authentication failed\", Code.Unauthenticated);\n }\n\n // Cache the verification result\n cache?.set(credentials, authContext);\n\n // Propagate auth context as headers if enabled\n if (propagateHeaders) {\n setAuthHeaders(req.header, authContext, propagatedClaims);\n }\n\n // Run downstream with auth context in AsyncLocalStorage\n return await authContextStorage.run(authContext, () => next(req));\n };\n}\n","/**\n * Minimal in-memory LRU cache with TTL expiration.\n *\n * Uses Map insertion order for LRU eviction.\n * No external dependencies.\n */\n\ninterface CacheEntry<T> {\n value: T;\n expiresAt: number;\n}\n\nexport class LruCache<T> {\n readonly #maxSize: number;\n readonly #ttl: number;\n readonly #entries = new Map<string, CacheEntry<T>>();\n\n constructor(options: { ttl: number; maxSize?: number | undefined }) {\n if (typeof options.ttl !== \"number\" || options.ttl <= 0) {\n throw new RangeError(\"ttl must be a positive number\");\n }\n this.#ttl = options.ttl;\n this.#maxSize = options.maxSize ?? 1000;\n }\n\n get(key: string): T | undefined {\n const entry = this.#entries.get(key);\n if (!entry) return undefined;\n\n if (Date.now() >= entry.expiresAt) {\n this.#entries.delete(key);\n return undefined;\n }\n\n // Move to end (most recently used)\n this.#entries.delete(key);\n this.#entries.set(key, entry);\n return entry.value;\n }\n\n set(key: string, value: T): void {\n // Delete first to update insertion order\n this.#entries.delete(key);\n\n // Evict LRU (first entry) if at capacity\n if (this.#entries.size >= this.#maxSize) {\n const firstKey = this.#entries.keys().next().value;\n if (firstKey !== undefined) {\n this.#entries.delete(firstKey);\n }\n }\n\n this.#entries.set(key, {\n value,\n expiresAt: Date.now() + this.#ttl,\n });\n }\n\n clear(): void {\n this.#entries.clear();\n }\n\n get size(): number {\n return this.#entries.size;\n }\n}\n","/**\n * Authentication context storage\n *\n * Uses AsyncLocalStorage to make auth context available to handlers\n * without passing it through function parameters.\n *\n * @module context\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport { Code, ConnectError } from \"@connectrpc/connect\";\nimport type { AuthContext } from \"./types.ts\";\n\n/**\n * Module-level AsyncLocalStorage for auth context.\n *\n * Set by auth interceptors, read by handlers via getAuthContext().\n * Automatically isolated per async context (request).\n */\nexport const authContextStorage = new AsyncLocalStorage<AuthContext>();\n\n/**\n * Get the current auth context.\n *\n * Returns the AuthContext set by the auth interceptor in the current\n * async context. Returns undefined if no auth interceptor is active\n * or the current method was skipped.\n *\n * @returns Current auth context or undefined\n *\n * @example Usage in a service handler\n * ```typescript\n * import { getAuthContext } from '@connectum/auth';\n *\n * const handler = {\n * async getUser(req) {\n * const auth = getAuthContext();\n * if (!auth) throw new ConnectError('Not authenticated', Code.Unauthenticated);\n * return { user: await db.getUser(auth.subject) };\n * },\n * };\n * ```\n */\nexport function getAuthContext(): AuthContext | undefined {\n return authContextStorage.getStore();\n}\n\n/**\n * Get the current auth context or throw.\n *\n * Like getAuthContext() but throws ConnectError(Code.Unauthenticated)\n * if no auth context is available. Use when auth is mandatory.\n *\n * @returns Current auth context (never undefined)\n * @throws ConnectError with Code.Unauthenticated if no context\n */\nexport function requireAuthContext(): AuthContext {\n const context = authContextStorage.getStore();\n if (!context) {\n throw new ConnectError(\"Authentication required\", Code.Unauthenticated);\n }\n return context;\n}\n","/**\n * Shared types for @connectum/auth\n *\n * @module types\n */\n\nimport type { Interceptor } from \"@connectrpc/connect\";\n\n/**\n * Interceptor factory function type\n *\n * @template TOptions - Options type for the interceptor\n */\nexport type InterceptorFactory<TOptions = void> = TOptions extends void ? () => Interceptor : (options: TOptions) => Interceptor;\n\n/**\n * Authenticated user context\n *\n * Represents the result of authentication. Set by auth interceptor,\n * accessible via getAuthContext() in handlers and downstream interceptors.\n */\nexport interface AuthContext {\n /** Authenticated subject identifier (user ID, service account, etc.) */\n readonly subject: string;\n /** Human-readable display name */\n readonly name?: string | undefined;\n /** Assigned roles (e.g., [\"admin\", \"user\"]) */\n readonly roles: ReadonlyArray<string>;\n /** Granted scopes (e.g., [\"read\", \"write\"]) */\n readonly scopes: ReadonlyArray<string>;\n /** Raw claims from the credential (JWT claims, API key metadata, etc.) */\n readonly claims: Readonly<Record<string, unknown>>;\n /** Credential type identifier (e.g., \"jwt\", \"api-key\", \"mtls\") */\n readonly type: string;\n /** Credential expiration time */\n readonly expiresAt?: Date | undefined;\n}\n\n/**\n * Standard header names for auth context propagation.\n *\n * Used for cross-service context propagation (similar to Envoy credential injection).\n * The auth interceptor sets these headers when propagateHeaders is true.\n *\n * WARNING: These headers are trusted ONLY in service-to-service communication\n * where transport security (mTLS) is established. Never trust these headers\n * from external clients without using createGatewayAuthInterceptor().\n */\nexport const AUTH_HEADERS = {\n /** Authenticated subject identifier */\n SUBJECT: \"x-auth-subject\",\n /** JSON-encoded roles array */\n ROLES: \"x-auth-roles\",\n /** Space-separated scopes */\n SCOPES: \"x-auth-scopes\",\n /** JSON-encoded claims object */\n CLAIMS: \"x-auth-claims\",\n /** Human-readable display name */\n NAME: \"x-auth-name\",\n /** Credential type (jwt, api-key, mtls, etc.) */\n TYPE: \"x-auth-type\",\n} as const;\n\n/**\n * Authorization rule effect\n */\nexport const AuthzEffect = {\n ALLOW: \"allow\",\n DENY: \"deny\",\n} as const;\n\nexport type AuthzEffect = (typeof AuthzEffect)[keyof typeof AuthzEffect];\n\n/**\n * Authorization rule definition.\n *\n * When a rule has `requires`, the match semantics are:\n * - **roles**: \"any-of\" -- the user must have **at least one** of the listed roles.\n * - **scopes**: \"all-of\" -- the user must have **every** listed scope.\n */\nexport interface AuthzRule {\n /** Rule name for logging/debugging */\n readonly name: string;\n /** Method patterns to match (e.g., \"admin.v1.AdminService/*\", \"user.v1.UserService/DeleteUser\") */\n readonly methods: ReadonlyArray<string>;\n /** Effect when rule matches */\n readonly effect: AuthzEffect;\n /**\n * Required roles/scopes for this rule.\n *\n * - `roles` uses \"any-of\" semantics: user needs at least one of the listed roles.\n * - `scopes` uses \"all-of\" semantics: user needs every listed scope.\n */\n readonly requires?:\n | {\n readonly roles?: ReadonlyArray<string>;\n readonly scopes?: ReadonlyArray<string>;\n }\n | undefined;\n}\n\n/**\n * LRU cache configuration for credentials verification\n */\nexport interface CacheOptions {\n /** Cache entry time-to-live in milliseconds */\n readonly ttl: number;\n /** Maximum number of cached entries */\n readonly maxSize?: number | undefined;\n}\n\n/**\n * Generic auth interceptor options\n */\nexport interface AuthInterceptorOptions {\n /**\n * Extract credentials from request.\n * Default: extracts Bearer token from Authorization header.\n *\n * @param req - Request with headers\n * @returns Credential string or null if no credentials found\n */\n extractCredentials?: (req: { header: Headers }) => string | null | Promise<string | null>;\n\n /**\n * Verify credentials and return auth context.\n * REQUIRED. Must throw on invalid credentials.\n *\n * @param credentials - Extracted credential string\n * @returns AuthContext for valid credentials\n */\n verifyCredentials: (credentials: string) => AuthContext | Promise<AuthContext>;\n\n /**\n * Methods to skip authentication for.\n * Patterns: \"Service/Method\" or \"Service/*\"\n * @default [] (health and reflection methods are NOT auto-skipped)\n */\n skipMethods?: string[] | undefined;\n\n /**\n * Propagate auth context as headers for downstream services.\n * @default false\n */\n propagateHeaders?: boolean | undefined;\n\n /**\n * LRU cache for credentials verification results.\n * Caches AuthContext by credential string to reduce verification overhead.\n */\n cache?: CacheOptions | undefined;\n\n /**\n * Filter which claims are propagated in headers (SEC-001).\n * When set, only listed claim keys are included in x-auth-claims header.\n * When not set, all claims are propagated.\n */\n propagatedClaims?: string[] | undefined;\n}\n\n/**\n * JWT auth interceptor options\n */\nexport interface JwtAuthInterceptorOptions {\n /** JWKS endpoint URL for remote key set */\n jwksUri?: string | undefined;\n /** HMAC symmetric secret (for HS256/HS384/HS512) */\n secret?: string | undefined;\n /** Asymmetric public key */\n publicKey?: CryptoKey | undefined;\n /** Expected issuer(s) */\n issuer?: string | string[] | undefined;\n /** Expected audience(s) */\n audience?: string | string[] | undefined;\n /** Allowed algorithms */\n algorithms?: string[] | undefined;\n /**\n * Mapping from JWT claims to AuthContext fields.\n * Supports dot-notation paths (e.g., \"realm_access.roles\").\n */\n claimsMapping?:\n | {\n subject?: string | undefined;\n name?: string | undefined;\n roles?: string | undefined;\n scopes?: string | undefined;\n }\n | undefined;\n /**\n * Maximum token age.\n * Passed to jose jwtVerify options.\n * Number (seconds) or string (e.g., \"2h\", \"7d\").\n */\n maxTokenAge?: number | string | undefined;\n /**\n * Methods to skip authentication for.\n * @default []\n */\n skipMethods?: string[] | undefined;\n /**\n * Propagate auth context as headers for downstream services.\n * @default false\n */\n propagateHeaders?: boolean | undefined;\n}\n\n/**\n * Authorization interceptor options\n */\nexport interface AuthzInterceptorOptions {\n /**\n * Default policy when no rule matches.\n * @default \"deny\"\n */\n defaultPolicy?: AuthzEffect | undefined;\n\n /**\n * Declarative authorization rules.\n * Evaluated in order; first matching rule wins.\n */\n rules?: AuthzRule[] | undefined;\n\n /**\n * Programmatic authorization callback.\n * Called after rule evaluation if no rule matched,\n * or always if no rules are defined.\n *\n * @param context - Authenticated user context\n * @param req - Request info (service and method names)\n * @returns true if authorized, false otherwise\n */\n authorize?: (context: AuthContext, req: { service: string; method: string }) => boolean | Promise<boolean>;\n\n /**\n * Methods to skip authorization for.\n * @default []\n */\n skipMethods?: string[] | undefined;\n}\n\n/**\n * Header name mapping for gateway auth context extraction.\n *\n * Maps AuthContext fields to custom header names used by the API gateway.\n */\nexport interface GatewayHeaderMapping {\n /** Header containing the authenticated subject */\n readonly subject: string;\n /** Header containing the display name */\n readonly name?: string | undefined;\n /** Header containing JSON-encoded roles array */\n readonly roles?: string | undefined;\n /** Header containing space-separated scopes */\n readonly scopes?: string | undefined;\n /** Header containing credential type */\n readonly type?: string | undefined;\n /** Header containing JSON-encoded claims */\n readonly claims?: string | undefined;\n}\n\n/**\n * Gateway auth interceptor options.\n *\n * For services behind an API gateway that has already performed authentication.\n * Extracts auth context from gateway-injected headers.\n */\nexport interface GatewayAuthInterceptorOptions {\n /** Mapping from AuthContext fields to gateway header names */\n readonly headerMapping: GatewayHeaderMapping;\n /** Trust verification: check that request came from a trusted gateway */\n readonly trustSource: {\n /** Header set by the gateway to prove trust */\n readonly header: string;\n /** Accepted values for the trust header */\n readonly expectedValues: string[];\n };\n /** Headers to strip from the request after extraction (prevent spoofing) */\n readonly stripHeaders?: string[] | undefined;\n /** Methods to skip authentication for */\n readonly skipMethods?: string[] | undefined;\n /** Propagate auth context as headers for downstream services */\n readonly propagateHeaders?: boolean | undefined;\n /** Default credential type when not provided by gateway */\n readonly defaultType?: string | undefined;\n}\n\n/**\n * Session-based auth interceptor options.\n *\n * Two-step authentication: verify session token, then map session data to AuthContext.\n */\nexport interface SessionAuthInterceptorOptions {\n /**\n * Verify session token and return raw session data.\n * Must throw on invalid/expired sessions.\n *\n * @param token - Session token string\n * @param headers - Request headers (for additional context)\n * @returns Raw session data\n */\n readonly verifySession: (token: string, headers: Headers) => unknown | Promise<unknown>;\n /**\n * Map raw session data to AuthContext.\n *\n * @param session - Raw session data from verifySession\n * @returns Normalized auth context\n */\n readonly mapSession: (session: unknown) => AuthContext | Promise<AuthContext>;\n /**\n * Custom token extraction.\n * Default: extracts Bearer token from Authorization header.\n */\n readonly extractToken?: ((req: { header: Headers }) => string | null | Promise<string | null>) | undefined;\n /** LRU cache for session verification results */\n readonly cache?: CacheOptions | undefined;\n /** Methods to skip authentication for */\n readonly skipMethods?: string[] | undefined;\n /** Propagate auth context as headers for downstream services */\n readonly propagateHeaders?: boolean | undefined;\n /**\n * Filter which claims are propagated in headers.\n * When set, only listed claim keys are included in x-auth-claims header.\n * When not set, all claims are propagated.\n */\n readonly propagatedClaims?: string[] | undefined;\n}\n","/**\n * Auth header propagation utilities\n *\n * Handles serialization/deserialization of AuthContext to/from\n * HTTP headers for cross-service context propagation.\n *\n * @module headers\n */\n\nimport type { AuthContext } from \"./types.ts\";\nimport { AUTH_HEADERS } from \"./types.ts\";\n\nconst MAX_HEADER_BYTES = 8192;\n\n/**\n * Sanitize a header value by removing control characters and enforcing length limits.\n */\nfunction sanitizeHeaderValue(value: string, maxLength: number): string {\n // Remove control characters (except tab/LF/CR which are valid in headers)\n // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional control char removal for header sanitization\n const cleaned = value.replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]/g, \"\");\n return cleaned.slice(0, maxLength);\n}\n\n/**\n * Serialize AuthContext to request headers.\n *\n * Sets standard auth headers on the provided Headers object.\n * Used by auth interceptors when propagateHeaders is enabled.\n *\n * @param headers - Headers object to set auth headers on\n * @param context - Auth context to serialize\n * @param propagatedClaims - Optional list of claim keys to propagate (all if undefined)\n */\nexport function setAuthHeaders(headers: Headers, context: AuthContext, propagatedClaims?: string[]): void {\n headers.set(AUTH_HEADERS.SUBJECT, sanitizeHeaderValue(context.subject, 512));\n headers.set(AUTH_HEADERS.TYPE, sanitizeHeaderValue(context.type, 128));\n\n if (context.name) {\n headers.set(AUTH_HEADERS.NAME, sanitizeHeaderValue(context.name, 256));\n }\n\n if (context.roles.length > 0) {\n const rolesValue = JSON.stringify(context.roles);\n if (rolesValue.length <= MAX_HEADER_BYTES) {\n headers.set(AUTH_HEADERS.ROLES, rolesValue);\n }\n }\n\n if (context.scopes.length > 0) {\n const scopesValue = context.scopes.join(\" \");\n if (scopesValue.length <= MAX_HEADER_BYTES) {\n headers.set(AUTH_HEADERS.SCOPES, scopesValue);\n }\n }\n\n const claimKeys = Object.keys(context.claims);\n if (claimKeys.length > 0) {\n const filteredClaims = propagatedClaims ? Object.fromEntries(Object.entries(context.claims).filter(([key]) => propagatedClaims.includes(key))) : context.claims;\n if (Object.keys(filteredClaims).length > 0) {\n const claimsValue = JSON.stringify(filteredClaims);\n if (claimsValue.length <= MAX_HEADER_BYTES) {\n headers.set(AUTH_HEADERS.CLAIMS, claimsValue);\n }\n }\n }\n}\n\n/**\n * Parse AuthContext from request headers.\n *\n * Deserializes auth context from standard headers set by an upstream\n * service or gateway. Returns undefined if required headers are missing.\n *\n * WARNING: Only use this in trusted environments (behind mTLS, mesh, etc.).\n * For untrusted environments, use createTrustedHeadersReader() instead.\n *\n * @param headers - Request headers to parse\n * @returns Parsed AuthContext or undefined if headers are missing\n *\n * @example Trust upstream auth headers\n * ```typescript\n * import { parseAuthHeaders } from '@connectum/auth';\n *\n * const context = parseAuthHeaders(req.header);\n * if (context) {\n * console.log(`Authenticated as ${context.subject}`);\n * }\n * ```\n */\nexport function parseAuthHeaders(headers: Headers): AuthContext | undefined {\n const subjectHeader = headers.get(AUTH_HEADERS.SUBJECT);\n if (!subjectHeader) {\n return undefined;\n }\n\n const subject = sanitizeHeaderValue(subjectHeader, 512);\n const typeHeader = headers.get(AUTH_HEADERS.TYPE);\n const type = typeHeader ? sanitizeHeaderValue(typeHeader, 128) : \"unknown\";\n const rolesRaw = headers.get(AUTH_HEADERS.ROLES);\n const scopesRaw = headers.get(AUTH_HEADERS.SCOPES);\n const claimsRaw = headers.get(AUTH_HEADERS.CLAIMS);\n\n let roles: string[] = [];\n if (rolesRaw && rolesRaw.length <= MAX_HEADER_BYTES) {\n try {\n const parsed: unknown = JSON.parse(rolesRaw);\n if (Array.isArray(parsed)) {\n roles = parsed.filter((r): r is string => typeof r === \"string\");\n }\n } catch {\n // Invalid JSON — ignore malformed header\n }\n }\n\n let scopes: string[] = [];\n if (scopesRaw && scopesRaw.length <= MAX_HEADER_BYTES) {\n scopes = scopesRaw.split(\" \").filter(Boolean);\n }\n\n const nameRaw = headers.get(AUTH_HEADERS.NAME);\n const name = nameRaw ? sanitizeHeaderValue(nameRaw, 256) : undefined;\n\n let claims: Record<string, unknown> = {};\n if (claimsRaw) {\n if (claimsRaw.length > MAX_HEADER_BYTES) {\n // Claims too large — ignore to prevent abuse\n claims = {};\n } else {\n try {\n const parsed: unknown = JSON.parse(claimsRaw);\n if (parsed !== null && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n claims = parsed as Record<string, unknown>;\n }\n } catch {\n // Invalid JSON — ignore malformed header\n }\n }\n }\n\n return {\n subject,\n name,\n type,\n roles,\n scopes,\n claims,\n };\n}\n","/**\n * Method pattern matching utility\n *\n * Shared logic for matching gRPC methods against patterns.\n * Used by both auth and authz interceptors.\n *\n * @module method-match\n */\n\n/**\n * Check if a method matches any of the given patterns.\n *\n * Patterns:\n * - \"*\" — matches all methods\n * - \"Service/*\" — matches all methods of a service\n * - \"Service/Method\" — matches exact method\n *\n * @param serviceName - Fully-qualified service name (e.g., \"user.v1.UserService\")\n * @param methodName - Method name (e.g., \"GetUser\")\n * @param patterns - Readonly array of match patterns\n * @returns true if the method matches any pattern\n */\nexport function matchesMethodPattern(serviceName: string, methodName: string, patterns: readonly string[]): boolean {\n if (patterns.length === 0) {\n return false;\n }\n\n const fullMethod = `${serviceName}/${methodName}`;\n\n for (const pattern of patterns) {\n if (pattern === \"*\") {\n return true;\n }\n if (pattern === fullMethod) {\n return true;\n }\n if (pattern.endsWith(\"/*\")) {\n const servicePattern = pattern.slice(0, -2);\n if (serviceName === servicePattern) {\n return true;\n }\n }\n }\n\n return false;\n}\n","/**\n * Authorization interceptor\n *\n * Declarative rules-based authorization with RBAC support.\n * Evaluates rules against AuthContext from the auth interceptor.\n *\n * @module authz-interceptor\n */\n\nimport type { Interceptor, StreamRequest, UnaryRequest } from \"@connectrpc/connect\";\nimport { Code, ConnectError } from \"@connectrpc/connect\";\nimport { getAuthContext } from \"./context.ts\";\nimport type { AuthzDeniedDetails } from \"./errors.ts\";\nimport { AuthzDeniedError } from \"./errors.ts\";\nimport { matchesMethodPattern } from \"./method-match.ts\";\nimport type { AuthzInterceptorOptions, AuthzRule } from \"./types.ts\";\nimport { AuthzEffect } from \"./types.ts\";\n\n/**\n * Check if the auth context satisfies a rule's requirements.\n */\nfunction satisfiesRequirements(context: { roles: ReadonlyArray<string>; scopes: ReadonlyArray<string> }, requires: NonNullable<AuthzRule[\"requires\"]>): boolean {\n // Check roles: user must have at least one of the required roles\n if (requires.roles && requires.roles.length > 0) {\n const hasRole = requires.roles.some((role) => context.roles.includes(role));\n if (!hasRole) {\n return false;\n }\n }\n\n // Check scopes: user must have ALL required scopes\n if (requires.scopes && requires.scopes.length > 0) {\n const hasAllScopes = requires.scopes.every((scope) => context.scopes.includes(scope));\n if (!hasAllScopes) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Evaluate authorization rules against auth context for a specific method.\n *\n * Returns the effect of the first matching rule, or undefined if no rule matches.\n */\nfunction evaluateRules(\n rules: AuthzRule[],\n context: { roles: ReadonlyArray<string>; scopes: ReadonlyArray<string> },\n serviceName: string,\n methodName: string,\n): { effect: string; ruleName: string; requiredRoles?: readonly string[]; requiredScopes?: readonly string[] } | undefined {\n for (const rule of rules) {\n // Check if any of the rule's method patterns match\n const matches = matchesMethodPattern(serviceName, methodName, rule.methods);\n if (!matches) {\n continue;\n }\n\n // If rule has requirements, check them\n if (rule.requires) {\n if (satisfiesRequirements(context, rule.requires)) {\n const result: { effect: string; ruleName: string; requiredRoles?: readonly string[]; requiredScopes?: readonly string[] } = {\n effect: rule.effect,\n ruleName: rule.name,\n };\n if (rule.requires.roles) result.requiredRoles = rule.requires.roles;\n if (rule.requires.scopes) result.requiredScopes = rule.requires.scopes;\n return result;\n }\n // Requirements not met — this rule doesn't match, continue to next\n continue;\n }\n\n // No requirements — rule matches unconditionally\n return { effect: rule.effect, ruleName: rule.name };\n }\n\n return undefined;\n}\n\n/**\n * Create an authorization interceptor.\n *\n * Evaluates declarative rules and/or a programmatic callback against\n * the AuthContext established by the authentication interceptor.\n *\n * IMPORTANT: This interceptor MUST run AFTER an authentication interceptor\n * in the chain.\n *\n * @param options - Authorization options\n * @returns ConnectRPC interceptor\n *\n * @example RBAC with declarative rules\n * ```typescript\n * import { createAuthzInterceptor } from '@connectum/auth';\n *\n * const authz = createAuthzInterceptor({\n * defaultPolicy: 'deny',\n * rules: [\n * { name: 'public', methods: ['public.v1.PublicService/*'], effect: 'allow' },\n * { name: 'admin', methods: ['admin.v1.AdminService/*'], requires: { roles: ['admin'] }, effect: 'allow' },\n * ],\n * });\n * ```\n */\nexport function createAuthzInterceptor(options: AuthzInterceptorOptions = {}): Interceptor {\n const { defaultPolicy = AuthzEffect.DENY, rules = [], authorize, skipMethods = [] } = options;\n\n return (next) => async (req: UnaryRequest | StreamRequest) => {\n const serviceName: string = req.service.typeName;\n const methodName: string = req.method.name;\n\n // Skip specified methods\n if (matchesMethodPattern(serviceName, methodName, skipMethods)) {\n return await next(req);\n }\n\n // Get auth context from AsyncLocalStorage\n const authContext = getAuthContext();\n if (!authContext) {\n throw new ConnectError(\"Authentication required for authorization\", Code.Unauthenticated);\n }\n\n // Evaluate declarative rules first\n if (rules.length > 0) {\n const ruleResult = evaluateRules(rules, authContext, serviceName, methodName);\n if (ruleResult) {\n if (ruleResult.effect === AuthzEffect.DENY) {\n const details: AuthzDeniedDetails = {\n ruleName: ruleResult.ruleName,\n ...(ruleResult.requiredRoles && { requiredRoles: [...ruleResult.requiredRoles] }),\n ...(ruleResult.requiredScopes && { requiredScopes: [...ruleResult.requiredScopes] }),\n };\n throw new AuthzDeniedError(details);\n }\n // ALLOW — continue\n return await next(req);\n }\n }\n\n // If no rules matched, try programmatic callback\n if (authorize) {\n const allowed = await authorize(authContext, { service: serviceName, method: methodName });\n if (!allowed) {\n throw new ConnectError(\"Access denied\", Code.PermissionDenied);\n }\n return await next(req);\n }\n\n // No rules matched, no callback — apply default policy\n if (defaultPolicy === AuthzEffect.DENY) {\n throw new ConnectError(\"Access denied by default policy\", Code.PermissionDenied);\n }\n\n return await next(req);\n };\n}\n","/**\n * Auth-specific error types\n *\n * @module errors\n */\n\nimport { Code, ConnectError } from \"@connectrpc/connect\";\n// biome-ignore lint/correctness/useImportExtensions: workspace package import\nimport type { SanitizableError } from \"@connectum/core\";\n\n/**\n * Details for authorization denied errors.\n */\nexport interface AuthzDeniedDetails {\n readonly ruleName: string;\n readonly requiredRoles?: readonly string[];\n readonly requiredScopes?: readonly string[];\n}\n\n/**\n * Authorization denied error.\n *\n * Carries server-side details (rule name, required roles/scopes) while\n * exposing only \"Access denied\" to the client via SanitizableError protocol.\n */\nexport class AuthzDeniedError extends ConnectError implements SanitizableError {\n readonly clientMessage = \"Access denied\";\n readonly ruleName: string;\n readonly authzDetails: AuthzDeniedDetails;\n\n get serverDetails(): Readonly<Record<string, unknown>> {\n return {\n ruleName: this.authzDetails.ruleName,\n requiredRoles: this.authzDetails.requiredRoles,\n requiredScopes: this.authzDetails.requiredScopes,\n };\n }\n\n constructor(details: AuthzDeniedDetails) {\n super(`Access denied by rule: ${details.ruleName}`, Code.PermissionDenied);\n this.name = \"AuthzDeniedError\";\n this.ruleName = details.ruleName;\n this.authzDetails = details;\n }\n}\n","/**\n * Gateway authentication interceptor\n *\n * For services behind an API gateway that has already performed authentication.\n * Extracts auth context from gateway-injected headers after verifying trust.\n *\n * Trust is established via a header (e.g., x-gateway-secret) rather than\n * peerAddress, since ConnectRPC interceptors don't have access to peer info.\n *\n * @module gateway-auth-interceptor\n */\n\nimport type { Interceptor, StreamRequest, UnaryRequest } from \"@connectrpc/connect\";\nimport { Code, ConnectError } from \"@connectrpc/connect\";\nimport { authContextStorage } from \"./context.ts\";\nimport { setAuthHeaders } from \"./headers.ts\";\nimport { matchesMethodPattern } from \"./method-match.ts\";\nimport type { AuthContext, GatewayAuthInterceptorOptions } from \"./types.ts\";\n\n/**\n * Match an IP address against a pattern (exact or CIDR notation).\n *\n * Supports:\n * - Exact match: \"10.0.0.1\"\n * - CIDR range: \"10.0.0.0/8\"\n */\nfunction isValidOctet(value: number): boolean {\n return Number.isInteger(value) && value >= 0 && value <= 255;\n}\n\nfunction matchesIp(address: string, pattern: string): boolean {\n if (address === pattern) return true;\n\n if (pattern.includes(\"/\")) {\n const [network, prefixStr] = pattern.split(\"/\");\n if (!network || !prefixStr) return false;\n const prefix = Number.parseInt(prefixStr, 10);\n if (Number.isNaN(prefix) || prefix < 0 || prefix > 32) return false;\n const peerParts = address.split(\".\").map(Number);\n const networkParts = network.split(\".\").map(Number);\n if (peerParts.length !== 4 || networkParts.length !== 4) return false;\n if (!peerParts.every(isValidOctet) || !networkParts.every(isValidOctet)) return false;\n const [p0 = 0, p1 = 0, p2 = 0, p3 = 0] = peerParts;\n const [n0 = 0, n1 = 0, n2 = 0, n3 = 0] = networkParts;\n const peerInt = ((p0 << 24) | (p1 << 16) | (p2 << 8) | p3) >>> 0;\n const networkInt = ((n0 << 24) | (n1 << 16) | (n2 << 8) | n3) >>> 0;\n const mask = prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0;\n return (peerInt & mask) === (networkInt & mask);\n }\n\n return false;\n}\n\n/**\n * Check if a trust header value matches any of the expected values.\n *\n * For each expected value, tries exact match first, then CIDR match.\n */\nfunction isTrusted(headerValue: string, expectedValues: readonly string[]): boolean {\n for (const expected of expectedValues) {\n if (headerValue === expected) return true;\n if (expected.includes(\"/\") && matchesIp(headerValue, expected)) return true;\n }\n return false;\n}\n\n/**\n * Create a gateway authentication interceptor.\n *\n * Reads pre-authenticated identity from gateway-injected headers.\n * Trust is established by checking a designated header value against\n * a list of expected values (shared secrets or trusted IP ranges).\n *\n * @param options - Gateway auth configuration\n * @returns ConnectRPC interceptor\n *\n * @example Kong/Envoy gateway with shared secret\n * ```typescript\n * const gatewayAuth = createGatewayAuthInterceptor({\n * headerMapping: {\n * subject: 'x-user-id',\n * name: 'x-user-name',\n * roles: 'x-user-roles',\n * },\n * trustSource: {\n * header: 'x-gateway-secret',\n * expectedValues: [process.env.GATEWAY_SECRET],\n * },\n * });\n * ```\n */\nexport function createGatewayAuthInterceptor(options: GatewayAuthInterceptorOptions): Interceptor {\n const { headerMapping, trustSource, stripHeaders = [], skipMethods = [], propagateHeaders = false, defaultType = \"gateway\" } = options;\n\n // Fail-closed: require subject mapping and non-empty expectedValues\n if (!headerMapping.subject) {\n throw new Error(\"@connectum/auth: Gateway auth requires headerMapping.subject\");\n }\n if (trustSource.expectedValues.length === 0) {\n throw new Error(\"@connectum/auth: Gateway auth requires non-empty trustSource.expectedValues\");\n }\n\n // Pre-compute headers to strip (prevents downstream spoofing on all routes)\n const headersToStrip = [\n headerMapping.subject,\n headerMapping.name,\n headerMapping.roles,\n headerMapping.scopes,\n headerMapping.type,\n headerMapping.claims,\n trustSource.header,\n ...stripHeaders,\n ];\n\n function stripGatewayHeaders(headers: Headers): void {\n for (const header of headersToStrip) {\n if (header) headers.delete(header);\n }\n }\n\n return (next) => async (req: UnaryRequest | StreamRequest) => {\n const serviceName: string = req.service.typeName;\n const methodName: string = req.method.name;\n\n if (matchesMethodPattern(serviceName, methodName, skipMethods)) {\n // Strip gateway headers even for skipped methods to prevent spoofing\n stripGatewayHeaders(req.header);\n return await next(req);\n }\n\n // Verify trust\n const trustHeaderValue = req.header.get(trustSource.header);\n if (!trustHeaderValue || !isTrusted(trustHeaderValue, trustSource.expectedValues)) {\n throw new ConnectError(\"Untrusted request source\", Code.Unauthenticated);\n }\n\n // Extract subject (required)\n const subject = req.header.get(headerMapping.subject);\n if (!subject) {\n throw new ConnectError(\"Missing subject header from gateway\", Code.Unauthenticated);\n }\n\n // Extract optional fields\n const name = headerMapping.name ? (req.header.get(headerMapping.name) ?? undefined) : undefined;\n const type = headerMapping.type ? (req.header.get(headerMapping.type) ?? defaultType) : defaultType;\n\n // Parse roles: JSON array or comma-separated\n let roles: string[] = [];\n if (headerMapping.roles) {\n const rolesRaw = req.header.get(headerMapping.roles);\n if (rolesRaw) {\n try {\n const parsed: unknown = JSON.parse(rolesRaw);\n if (Array.isArray(parsed)) {\n roles = parsed.filter((r): r is string => typeof r === \"string\");\n }\n } catch {\n // Not JSON — try comma-separated\n roles = rolesRaw\n .split(\",\")\n .map((r) => r.trim())\n .filter(Boolean);\n }\n }\n }\n\n // Parse scopes: space-separated\n let scopes: string[] = [];\n if (headerMapping.scopes) {\n const scopesRaw = req.header.get(headerMapping.scopes);\n if (scopesRaw) {\n scopes = scopesRaw.split(\" \").filter(Boolean);\n }\n }\n\n // Parse claims: JSON object\n let claims: Record<string, unknown> = {};\n if (headerMapping.claims) {\n const claimsRaw = req.header.get(headerMapping.claims);\n if (claimsRaw && claimsRaw.length <= 8192) {\n try {\n const parsed: unknown = JSON.parse(claimsRaw);\n if (parsed !== null && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n claims = parsed as Record<string, unknown>;\n }\n } catch {\n // Invalid JSON — ignore\n }\n }\n }\n\n const authContext: AuthContext = { subject, name, roles, scopes, claims, type };\n\n // Strip mapped headers to prevent downstream spoofing\n stripGatewayHeaders(req.header);\n\n if (propagateHeaders) {\n setAuthHeaders(req.header, authContext);\n }\n\n return await authContextStorage.run(authContext, () => next(req));\n };\n}\n","/**\n * JWT authentication interceptor\n *\n * Convenience wrapper for JWT-based authentication using the jose library.\n * Supports JWKS remote key sets, HMAC secrets, and asymmetric public keys.\n *\n * @module jwt-auth-interceptor\n */\n\nimport type { Interceptor } from \"@connectrpc/connect\";\nimport { Code, ConnectError } from \"@connectrpc/connect\";\nimport * as jose from \"jose\";\nimport { createAuthInterceptor } from \"./auth-interceptor.ts\";\nimport type { AuthContext, JwtAuthInterceptorOptions } from \"./types.ts\";\n\n/**\n * Resolve a value at a dot-notation path in an object.\n *\n * @example getNestedValue({ a: { b: [1, 2] } }, \"a.b\") // [1, 2]\n */\nfunction getNestedValue(obj: Record<string, unknown>, path: string): unknown {\n let current: unknown = obj;\n for (const key of path.split(\".\")) {\n if (current === null || current === undefined || typeof current !== \"object\") {\n return undefined;\n }\n current = (current as Record<string, unknown>)[key];\n }\n return current;\n}\n\n/**\n * Get minimum HMAC key size in bytes per RFC 7518.\n * HS256 requires 32 bytes, HS384 requires 48, HS512 requires 64.\n */\nfunction getMinHmacKeyBytes(algorithms?: string[]): number {\n if (!algorithms) return 32;\n if (algorithms.includes(\"HS512\")) return 64;\n if (algorithms.includes(\"HS384\")) return 48;\n return 32;\n}\n\n/**\n * Build a JWT verification function from options.\n *\n * Separates JWKS (dynamic key resolution) from static keys (HMAC / asymmetric)\n * to satisfy jose's overloaded jwtVerify signatures.\n *\n * Priority: jwksUri > secret > publicKey\n */\nfunction buildVerifier(options: JwtAuthInterceptorOptions, verifyOptions: jose.JWTVerifyOptions): (token: string) => Promise<jose.JWTVerifyResult> {\n if (options.jwksUri) {\n const jwks = jose.createRemoteJWKSet(new URL(options.jwksUri));\n return (token) => jose.jwtVerify(token, jwks, verifyOptions);\n }\n if (options.secret) {\n const key = new TextEncoder().encode(options.secret);\n const minBytes = getMinHmacKeyBytes(options.algorithms);\n if (key.byteLength < minBytes) {\n throw new Error(\n `@connectum/auth: HMAC secret must be at least ${minBytes} bytes (${minBytes * 8} bits) per RFC 7518. ` +\n `Got ${key.byteLength} bytes. Generate with: openssl rand -base64 ${minBytes}`,\n );\n }\n return (token) => jose.jwtVerify(token, key, verifyOptions);\n }\n if (options.publicKey) {\n const key = options.publicKey;\n return (token) => jose.jwtVerify(token, key, verifyOptions);\n }\n throw new Error(\"@connectum/auth: JWT interceptor requires one of: jwksUri, secret, or publicKey\");\n}\n\n/**\n * Mutable intermediate type for claim mapping results.\n */\ninterface MappedClaims {\n subject?: string;\n name?: string;\n roles?: string[];\n scopes?: string[];\n}\n\n/**\n * Map JWT claims to AuthContext using configurable claim paths.\n */\nfunction mapClaimsToContext(payload: jose.JWTPayload, mapping: NonNullable<JwtAuthInterceptorOptions[\"claimsMapping\"]>): MappedClaims {\n const result: MappedClaims = {};\n const claims = payload as Record<string, unknown>;\n\n // Subject\n if (mapping.subject) {\n const val = getNestedValue(claims, mapping.subject);\n if (typeof val === \"string\") {\n result.subject = val;\n }\n }\n\n // Name\n if (mapping.name) {\n const val = getNestedValue(claims, mapping.name);\n if (typeof val === \"string\") {\n result.name = val;\n }\n }\n\n // Roles\n if (mapping.roles) {\n const val = getNestedValue(claims, mapping.roles);\n if (Array.isArray(val)) {\n result.roles = val.filter((r): r is string => typeof r === \"string\");\n }\n }\n\n // Scopes (can be space-separated string or array)\n if (mapping.scopes) {\n const val = getNestedValue(claims, mapping.scopes);\n if (typeof val === \"string\") {\n result.scopes = val.split(\" \").filter(Boolean);\n } else if (Array.isArray(val)) {\n result.scopes = val.filter((s): s is string => typeof s === \"string\");\n }\n }\n\n return result;\n}\n\n/**\n * Throw an Unauthenticated error for a missing JWT subject claim.\n */\nfunction throwMissingSubject(): never {\n throw new ConnectError(\"JWT missing subject claim\", Code.Unauthenticated);\n}\n\n/**\n * Create a JWT authentication interceptor.\n *\n * Convenience wrapper around createAuthInterceptor() that handles\n * JWT extraction from Authorization header, verification via jose,\n * and standard claim mapping to AuthContext.\n *\n * @param options - JWT authentication options\n * @returns ConnectRPC interceptor\n *\n * @example JWKS-based JWT auth (Auth0, Keycloak, etc.)\n * ```typescript\n * import { createJwtAuthInterceptor } from '@connectum/auth';\n *\n * const jwtAuth = createJwtAuthInterceptor({\n * jwksUri: 'https://auth.example.com/.well-known/jwks.json',\n * issuer: 'https://auth.example.com/',\n * audience: 'my-api',\n * claimsMapping: {\n * roles: 'realm_access.roles',\n * scopes: 'scope',\n * },\n * });\n * ```\n *\n * @example HMAC secret (testing / simple setups)\n * ```typescript\n * const jwtAuth = createJwtAuthInterceptor({\n * secret: process.env.JWT_SECRET,\n * issuer: 'my-service',\n * });\n * ```\n */\nexport function createJwtAuthInterceptor(options: JwtAuthInterceptorOptions): Interceptor {\n const { claimsMapping = {}, skipMethods, propagateHeaders } = options;\n\n const verifyOptions: jose.JWTVerifyOptions = {};\n if (options.issuer) {\n verifyOptions.issuer = options.issuer;\n }\n if (options.audience) {\n verifyOptions.audience = options.audience;\n }\n if (options.algorithms) {\n verifyOptions.algorithms = options.algorithms;\n }\n if (options.maxTokenAge) {\n verifyOptions.maxTokenAge = options.maxTokenAge;\n }\n\n const verify = buildVerifier(options, verifyOptions);\n\n return createAuthInterceptor({\n skipMethods,\n propagateHeaders,\n verifyCredentials: async (token: string): Promise<AuthContext> => {\n const { payload } = await verify(token);\n\n // Map standard + custom claims\n const mapped = mapClaimsToContext(payload, claimsMapping);\n const claims = payload as Record<string, unknown>;\n\n return {\n subject: mapped.subject ?? payload.sub ?? throwMissingSubject(),\n name: mapped.name ?? (typeof claims.name === \"string\" ? claims.name : undefined),\n roles: mapped.roles ?? [],\n scopes: mapped.scopes ?? (typeof payload.scope === \"string\" ? payload.scope.split(\" \").filter(Boolean) : []),\n claims,\n type: \"jwt\",\n expiresAt: payload.exp ? new Date(payload.exp * 1000) : undefined,\n };\n },\n });\n}\n","/**\n * Session-based authentication interceptor\n *\n * Convenience wrapper for session-based auth systems (e.g., better-auth).\n * Implements interceptor directly (not via createAuthInterceptor) to pass\n * full request headers to verifySession for cookie-based auth support.\n *\n * @module session-auth-interceptor\n */\n\nimport type { Interceptor, StreamRequest, UnaryRequest } from \"@connectrpc/connect\";\nimport { Code, ConnectError } from \"@connectrpc/connect\";\nimport { LruCache } from \"./cache.ts\";\nimport { authContextStorage } from \"./context.ts\";\nimport { setAuthHeaders } from \"./headers.ts\";\nimport { matchesMethodPattern } from \"./method-match.ts\";\nimport type { AuthContext, SessionAuthInterceptorOptions } from \"./types.ts\";\nimport { AUTH_HEADERS } from \"./types.ts\";\n\n/**\n * Default token extractor.\n * Extracts Bearer token from Authorization header.\n */\nfunction defaultExtractToken(req: { header: Headers }): string | null {\n const authHeader = req.header.get(\"authorization\");\n if (!authHeader) return null;\n const match = /^Bearer\\s+(.+)$/i.exec(authHeader);\n return match?.[1] ?? null;\n}\n\n/**\n * Create a session-based authentication interceptor.\n *\n * Two-step authentication:\n * 1. Extract token from request\n * 2. Verify session via user-provided callback (receives full headers for cookie support)\n * 3. Map session data to AuthContext via user-provided mapper\n *\n * @param options - Session auth configuration\n * @returns ConnectRPC interceptor\n *\n * @example better-auth integration\n * ```typescript\n * import { createSessionAuthInterceptor } from '@connectum/auth';\n *\n * const sessionAuth = createSessionAuthInterceptor({\n * verifySession: (token, headers) => auth.api.getSession({ headers }),\n * mapSession: (s) => ({\n * subject: s.user.id,\n * name: s.user.name,\n * roles: [],\n * scopes: [],\n * claims: s.user,\n * type: 'session',\n * }),\n * cache: { ttl: 60_000 },\n * });\n * ```\n */\nexport function createSessionAuthInterceptor(options: SessionAuthInterceptorOptions): Interceptor {\n const { verifySession, mapSession, extractToken = defaultExtractToken, cache: cacheOptions, skipMethods = [], propagateHeaders = false, propagatedClaims } = options;\n\n const cache = cacheOptions ? new LruCache<AuthContext>(cacheOptions) : undefined;\n\n return (next) => async (req: UnaryRequest | StreamRequest) => {\n const serviceName: string = req.service.typeName;\n const methodName: string = req.method.name;\n\n // Strip auth headers to prevent spoofing\n for (const headerName of Object.values(AUTH_HEADERS)) {\n req.header.delete(headerName);\n }\n\n if (matchesMethodPattern(serviceName, methodName, skipMethods)) {\n return await next(req);\n }\n\n // Extract token\n const token = await extractToken(req);\n if (!token) {\n throw new ConnectError(\"Missing credentials\", Code.Unauthenticated);\n }\n\n // Check cache\n const cached = cache?.get(token);\n if (cached && (!cached.expiresAt || cached.expiresAt.getTime() > Date.now())) {\n if (propagateHeaders) {\n setAuthHeaders(req.header, cached, propagatedClaims);\n }\n return await authContextStorage.run(cached, () => next(req));\n }\n\n // Verify session — pass full headers for cookie-based auth\n let session: unknown;\n try {\n session = await verifySession(token, req.header);\n } catch (err) {\n if (err instanceof ConnectError) throw err;\n throw new ConnectError(\"Session verification failed\", Code.Unauthenticated);\n }\n\n // Map session to AuthContext\n let authContext: AuthContext;\n try {\n authContext = await mapSession(session);\n } catch (err) {\n if (err instanceof ConnectError) throw err;\n throw new ConnectError(\"Session mapping failed\", Code.Unauthenticated);\n }\n\n // Cache result\n cache?.set(token, authContext);\n\n if (propagateHeaders) {\n setAuthHeaders(req.header, authContext, propagatedClaims);\n }\n\n return await authContextStorage.run(authContext, () => next(req));\n };\n}\n"],"mappings":"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","names":["Code","ConnectError","ConnectError","Code","Code","ConnectError","Code","ConnectError","ConnectError","Code","Code","ConnectError","ConnectError","Code","Code","ConnectError","ConnectError","Code","Code","ConnectError","ConnectError","Code"]}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { b as AuthContext } from '../types-IH8aZeWZ.js';
|
|
2
|
+
import '@connectrpc/connect';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mock auth context for testing
|
|
6
|
+
*
|
|
7
|
+
* @module testing/mock-context
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a mock AuthContext for testing.
|
|
12
|
+
*
|
|
13
|
+
* Merges provided overrides with sensible test defaults.
|
|
14
|
+
*
|
|
15
|
+
* @param overrides - Partial AuthContext to override defaults
|
|
16
|
+
* @returns Complete AuthContext
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { createMockAuthContext } from '@connectum/auth/testing';
|
|
21
|
+
*
|
|
22
|
+
* const ctx = createMockAuthContext({ subject: 'admin-1', roles: ['admin'] });
|
|
23
|
+
* assert.strictEqual(ctx.subject, 'admin-1');
|
|
24
|
+
* assert.deepStrictEqual(ctx.roles, ['admin']);
|
|
25
|
+
* assert.strictEqual(ctx.type, 'test'); // default preserved
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
declare function createMockAuthContext(overrides?: Partial<AuthContext>): AuthContext;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Test JWT utilities
|
|
32
|
+
*
|
|
33
|
+
* Provides deterministic JWT creation for testing.
|
|
34
|
+
* NOT for production use.
|
|
35
|
+
*
|
|
36
|
+
* @module testing/test-jwt
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Deterministic test secret for HS256 JWTs.
|
|
40
|
+
*
|
|
41
|
+
* WARNING: This is a well-known secret for testing only.
|
|
42
|
+
* NEVER use in production.
|
|
43
|
+
*/
|
|
44
|
+
declare const TEST_JWT_SECRET = "connectum-test-secret-do-not-use-in-production";
|
|
45
|
+
/**
|
|
46
|
+
* Create a signed test JWT for integration testing.
|
|
47
|
+
*
|
|
48
|
+
* Uses HS256 algorithm with a deterministic test key.
|
|
49
|
+
* NOT for production use.
|
|
50
|
+
*
|
|
51
|
+
* @param payload - JWT claims
|
|
52
|
+
* @param options - Signing options
|
|
53
|
+
* @returns Signed JWT string
|
|
54
|
+
*
|
|
55
|
+
* @example Create a test token
|
|
56
|
+
* ```typescript
|
|
57
|
+
* import { createTestJwt, TEST_JWT_SECRET } from '@connectum/auth/testing';
|
|
58
|
+
*
|
|
59
|
+
* const token = await createTestJwt({
|
|
60
|
+
* sub: 'user-123',
|
|
61
|
+
* roles: ['admin'],
|
|
62
|
+
* scope: 'read write',
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* // Use with createJwtAuthInterceptor in tests
|
|
66
|
+
* const auth = createJwtAuthInterceptor({ secret: TEST_JWT_SECRET });
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
declare function createTestJwt(payload: Record<string, unknown>, options?: {
|
|
70
|
+
expiresIn?: string;
|
|
71
|
+
issuer?: string;
|
|
72
|
+
audience?: string;
|
|
73
|
+
}): Promise<string>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Auth context test helper
|
|
77
|
+
*
|
|
78
|
+
* @module testing/with-context
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Run a function with a pre-set AuthContext.
|
|
83
|
+
*
|
|
84
|
+
* Sets the provided AuthContext in AsyncLocalStorage for the duration
|
|
85
|
+
* of the callback. Useful for testing handlers that call getAuthContext().
|
|
86
|
+
*
|
|
87
|
+
* @param context - Auth context to set
|
|
88
|
+
* @param fn - Function to execute within the context
|
|
89
|
+
* @returns Return value of fn
|
|
90
|
+
*
|
|
91
|
+
* @example Test a handler that reads auth context
|
|
92
|
+
* ```typescript
|
|
93
|
+
* import { withAuthContext, createMockAuthContext } from '@connectum/auth/testing';
|
|
94
|
+
* import { getAuthContext } from '@connectum/auth';
|
|
95
|
+
*
|
|
96
|
+
* await withAuthContext(createMockAuthContext({ subject: 'test-user' }), async () => {
|
|
97
|
+
* const ctx = getAuthContext();
|
|
98
|
+
* assert.strictEqual(ctx?.subject, 'test-user');
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
declare function withAuthContext<T>(context: AuthContext, fn: () => T | Promise<T>): Promise<T>;
|
|
103
|
+
|
|
104
|
+
export { TEST_JWT_SECRET, createMockAuthContext, createTestJwt, withAuthContext };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/testing/mock-context.ts
|
|
2
|
+
var DEFAULT_MOCK_CONTEXT = {
|
|
3
|
+
subject: "test-user",
|
|
4
|
+
name: "Test User",
|
|
5
|
+
roles: ["user"],
|
|
6
|
+
scopes: ["read"],
|
|
7
|
+
claims: {},
|
|
8
|
+
type: "test"
|
|
9
|
+
};
|
|
10
|
+
function createMockAuthContext(overrides) {
|
|
11
|
+
return {
|
|
12
|
+
...DEFAULT_MOCK_CONTEXT,
|
|
13
|
+
...overrides
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/testing/test-jwt.ts
|
|
18
|
+
import * as jose from "jose";
|
|
19
|
+
var TEST_JWT_SECRET = "connectum-test-secret-do-not-use-in-production";
|
|
20
|
+
var encodedSecret = new TextEncoder().encode(TEST_JWT_SECRET);
|
|
21
|
+
async function createTestJwt(payload, options) {
|
|
22
|
+
let builder = new jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt();
|
|
23
|
+
if (options?.expiresIn) {
|
|
24
|
+
builder = builder.setExpirationTime(options.expiresIn);
|
|
25
|
+
} else {
|
|
26
|
+
builder = builder.setExpirationTime("1h");
|
|
27
|
+
}
|
|
28
|
+
if (options?.issuer) {
|
|
29
|
+
builder = builder.setIssuer(options.issuer);
|
|
30
|
+
}
|
|
31
|
+
if (options?.audience) {
|
|
32
|
+
builder = builder.setAudience(options.audience);
|
|
33
|
+
}
|
|
34
|
+
return await builder.sign(encodedSecret);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/context.ts
|
|
38
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
39
|
+
import { Code, ConnectError } from "@connectrpc/connect";
|
|
40
|
+
var authContextStorage = new AsyncLocalStorage();
|
|
41
|
+
|
|
42
|
+
// src/testing/with-context.ts
|
|
43
|
+
async function withAuthContext(context, fn) {
|
|
44
|
+
return await authContextStorage.run(context, fn);
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
TEST_JWT_SECRET,
|
|
48
|
+
createMockAuthContext,
|
|
49
|
+
createTestJwt,
|
|
50
|
+
withAuthContext
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/testing/mock-context.ts","../../src/testing/test-jwt.ts","../../src/context.ts","../../src/testing/with-context.ts"],"sourcesContent":["/**\n * Mock auth context for testing\n *\n * @module testing/mock-context\n */\n\nimport type { AuthContext } from \"../types.ts\";\n\n/**\n * Default mock auth context values.\n */\nconst DEFAULT_MOCK_CONTEXT: AuthContext = {\n subject: \"test-user\",\n name: \"Test User\",\n roles: [\"user\"],\n scopes: [\"read\"],\n claims: {},\n type: \"test\",\n};\n\n/**\n * Create a mock AuthContext for testing.\n *\n * Merges provided overrides with sensible test defaults.\n *\n * @param overrides - Partial AuthContext to override defaults\n * @returns Complete AuthContext\n *\n * @example\n * ```typescript\n * import { createMockAuthContext } from '@connectum/auth/testing';\n *\n * const ctx = createMockAuthContext({ subject: 'admin-1', roles: ['admin'] });\n * assert.strictEqual(ctx.subject, 'admin-1');\n * assert.deepStrictEqual(ctx.roles, ['admin']);\n * assert.strictEqual(ctx.type, 'test'); // default preserved\n * ```\n */\nexport function createMockAuthContext(overrides?: Partial<AuthContext>): AuthContext {\n return {\n ...DEFAULT_MOCK_CONTEXT,\n ...overrides,\n };\n}\n","/**\n * Test JWT utilities\n *\n * Provides deterministic JWT creation for testing.\n * NOT for production use.\n *\n * @module testing/test-jwt\n */\n\nimport * as jose from \"jose\";\n\n/**\n * Deterministic test secret for HS256 JWTs.\n *\n * WARNING: This is a well-known secret for testing only.\n * NEVER use in production.\n */\nexport const TEST_JWT_SECRET = \"connectum-test-secret-do-not-use-in-production\";\n\n/**\n * Encoded test secret for jose.\n */\nconst encodedSecret = new TextEncoder().encode(TEST_JWT_SECRET);\n\n/**\n * Create a signed test JWT for integration testing.\n *\n * Uses HS256 algorithm with a deterministic test key.\n * NOT for production use.\n *\n * @param payload - JWT claims\n * @param options - Signing options\n * @returns Signed JWT string\n *\n * @example Create a test token\n * ```typescript\n * import { createTestJwt, TEST_JWT_SECRET } from '@connectum/auth/testing';\n *\n * const token = await createTestJwt({\n * sub: 'user-123',\n * roles: ['admin'],\n * scope: 'read write',\n * });\n *\n * // Use with createJwtAuthInterceptor in tests\n * const auth = createJwtAuthInterceptor({ secret: TEST_JWT_SECRET });\n * ```\n */\nexport async function createTestJwt(\n payload: Record<string, unknown>,\n options?: {\n expiresIn?: string;\n issuer?: string;\n audience?: string;\n },\n): Promise<string> {\n let builder = new jose.SignJWT(payload).setProtectedHeader({ alg: \"HS256\" }).setIssuedAt();\n\n if (options?.expiresIn) {\n builder = builder.setExpirationTime(options.expiresIn);\n } else {\n // Default: 1 hour expiry\n builder = builder.setExpirationTime(\"1h\");\n }\n\n if (options?.issuer) {\n builder = builder.setIssuer(options.issuer);\n }\n\n if (options?.audience) {\n builder = builder.setAudience(options.audience);\n }\n\n return await builder.sign(encodedSecret);\n}\n","/**\n * Authentication context storage\n *\n * Uses AsyncLocalStorage to make auth context available to handlers\n * without passing it through function parameters.\n *\n * @module context\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport { Code, ConnectError } from \"@connectrpc/connect\";\nimport type { AuthContext } from \"./types.ts\";\n\n/**\n * Module-level AsyncLocalStorage for auth context.\n *\n * Set by auth interceptors, read by handlers via getAuthContext().\n * Automatically isolated per async context (request).\n */\nexport const authContextStorage = new AsyncLocalStorage<AuthContext>();\n\n/**\n * Get the current auth context.\n *\n * Returns the AuthContext set by the auth interceptor in the current\n * async context. Returns undefined if no auth interceptor is active\n * or the current method was skipped.\n *\n * @returns Current auth context or undefined\n *\n * @example Usage in a service handler\n * ```typescript\n * import { getAuthContext } from '@connectum/auth';\n *\n * const handler = {\n * async getUser(req) {\n * const auth = getAuthContext();\n * if (!auth) throw new ConnectError('Not authenticated', Code.Unauthenticated);\n * return { user: await db.getUser(auth.subject) };\n * },\n * };\n * ```\n */\nexport function getAuthContext(): AuthContext | undefined {\n return authContextStorage.getStore();\n}\n\n/**\n * Get the current auth context or throw.\n *\n * Like getAuthContext() but throws ConnectError(Code.Unauthenticated)\n * if no auth context is available. Use when auth is mandatory.\n *\n * @returns Current auth context (never undefined)\n * @throws ConnectError with Code.Unauthenticated if no context\n */\nexport function requireAuthContext(): AuthContext {\n const context = authContextStorage.getStore();\n if (!context) {\n throw new ConnectError(\"Authentication required\", Code.Unauthenticated);\n }\n return context;\n}\n","/**\n * Auth context test helper\n *\n * @module testing/with-context\n */\n\nimport { authContextStorage } from \"../context.ts\";\nimport type { AuthContext } from \"../types.ts\";\n\n/**\n * Run a function with a pre-set AuthContext.\n *\n * Sets the provided AuthContext in AsyncLocalStorage for the duration\n * of the callback. Useful for testing handlers that call getAuthContext().\n *\n * @param context - Auth context to set\n * @param fn - Function to execute within the context\n * @returns Return value of fn\n *\n * @example Test a handler that reads auth context\n * ```typescript\n * import { withAuthContext, createMockAuthContext } from '@connectum/auth/testing';\n * import { getAuthContext } from '@connectum/auth';\n *\n * await withAuthContext(createMockAuthContext({ subject: 'test-user' }), async () => {\n * const ctx = getAuthContext();\n * assert.strictEqual(ctx?.subject, 'test-user');\n * });\n * ```\n */\nexport async function withAuthContext<T>(context: AuthContext, fn: () => T | Promise<T>): Promise<T> {\n return await authContextStorage.run(context, fn);\n}\n"],"mappings":";AAWA,IAAM,uBAAoC;AAAA,EACtC,SAAS;AAAA,EACT,MAAM;AAAA,EACN,OAAO,CAAC,MAAM;AAAA,EACd,QAAQ,CAAC,MAAM;AAAA,EACf,QAAQ,CAAC;AAAA,EACT,MAAM;AACV;AAoBO,SAAS,sBAAsB,WAA+C;AACjF,SAAO;AAAA,IACH,GAAG;AAAA,IACH,GAAG;AAAA,EACP;AACJ;;;AClCA,YAAY,UAAU;AAQf,IAAM,kBAAkB;AAK/B,IAAM,gBAAgB,IAAI,YAAY,EAAE,OAAO,eAAe;AA0B9D,eAAsB,cAClB,SACA,SAKe;AACf,MAAI,UAAU,IAAS,aAAQ,OAAO,EAAE,mBAAmB,EAAE,KAAK,QAAQ,CAAC,EAAE,YAAY;AAEzF,MAAI,SAAS,WAAW;AACpB,cAAU,QAAQ,kBAAkB,QAAQ,SAAS;AAAA,EACzD,OAAO;AAEH,cAAU,QAAQ,kBAAkB,IAAI;AAAA,EAC5C;AAEA,MAAI,SAAS,QAAQ;AACjB,cAAU,QAAQ,UAAU,QAAQ,MAAM;AAAA,EAC9C;AAEA,MAAI,SAAS,UAAU;AACnB,cAAU,QAAQ,YAAY,QAAQ,QAAQ;AAAA,EAClD;AAEA,SAAO,MAAM,QAAQ,KAAK,aAAa;AAC3C;;;ACjEA,SAAS,yBAAyB;AAClC,SAAS,MAAM,oBAAoB;AAS5B,IAAM,qBAAqB,IAAI,kBAA+B;;;ACWrE,eAAsB,gBAAmB,SAAsB,IAAsC;AACjG,SAAO,MAAM,mBAAmB,IAAI,SAAS,EAAE;AACnD;","names":[]}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { Interceptor } from '@connectrpc/connect';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared types for @connectum/auth
|
|
5
|
+
*
|
|
6
|
+
* @module types
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Interceptor factory function type
|
|
11
|
+
*
|
|
12
|
+
* @template TOptions - Options type for the interceptor
|
|
13
|
+
*/
|
|
14
|
+
type InterceptorFactory<TOptions = void> = TOptions extends void ? () => Interceptor : (options: TOptions) => Interceptor;
|
|
15
|
+
/**
|
|
16
|
+
* Authenticated user context
|
|
17
|
+
*
|
|
18
|
+
* Represents the result of authentication. Set by auth interceptor,
|
|
19
|
+
* accessible via getAuthContext() in handlers and downstream interceptors.
|
|
20
|
+
*/
|
|
21
|
+
interface AuthContext {
|
|
22
|
+
/** Authenticated subject identifier (user ID, service account, etc.) */
|
|
23
|
+
readonly subject: string;
|
|
24
|
+
/** Human-readable display name */
|
|
25
|
+
readonly name?: string | undefined;
|
|
26
|
+
/** Assigned roles (e.g., ["admin", "user"]) */
|
|
27
|
+
readonly roles: ReadonlyArray<string>;
|
|
28
|
+
/** Granted scopes (e.g., ["read", "write"]) */
|
|
29
|
+
readonly scopes: ReadonlyArray<string>;
|
|
30
|
+
/** Raw claims from the credential (JWT claims, API key metadata, etc.) */
|
|
31
|
+
readonly claims: Readonly<Record<string, unknown>>;
|
|
32
|
+
/** Credential type identifier (e.g., "jwt", "api-key", "mtls") */
|
|
33
|
+
readonly type: string;
|
|
34
|
+
/** Credential expiration time */
|
|
35
|
+
readonly expiresAt?: Date | undefined;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Standard header names for auth context propagation.
|
|
39
|
+
*
|
|
40
|
+
* Used for cross-service context propagation (similar to Envoy credential injection).
|
|
41
|
+
* The auth interceptor sets these headers when propagateHeaders is true.
|
|
42
|
+
*
|
|
43
|
+
* WARNING: These headers are trusted ONLY in service-to-service communication
|
|
44
|
+
* where transport security (mTLS) is established. Never trust these headers
|
|
45
|
+
* from external clients without using createGatewayAuthInterceptor().
|
|
46
|
+
*/
|
|
47
|
+
declare const AUTH_HEADERS: {
|
|
48
|
+
/** Authenticated subject identifier */
|
|
49
|
+
readonly SUBJECT: "x-auth-subject";
|
|
50
|
+
/** JSON-encoded roles array */
|
|
51
|
+
readonly ROLES: "x-auth-roles";
|
|
52
|
+
/** Space-separated scopes */
|
|
53
|
+
readonly SCOPES: "x-auth-scopes";
|
|
54
|
+
/** JSON-encoded claims object */
|
|
55
|
+
readonly CLAIMS: "x-auth-claims";
|
|
56
|
+
/** Human-readable display name */
|
|
57
|
+
readonly NAME: "x-auth-name";
|
|
58
|
+
/** Credential type (jwt, api-key, mtls, etc.) */
|
|
59
|
+
readonly TYPE: "x-auth-type";
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Authorization rule effect
|
|
63
|
+
*/
|
|
64
|
+
declare const AuthzEffect: {
|
|
65
|
+
readonly ALLOW: "allow";
|
|
66
|
+
readonly DENY: "deny";
|
|
67
|
+
};
|
|
68
|
+
type AuthzEffect = (typeof AuthzEffect)[keyof typeof AuthzEffect];
|
|
69
|
+
/**
|
|
70
|
+
* Authorization rule definition.
|
|
71
|
+
*
|
|
72
|
+
* When a rule has `requires`, the match semantics are:
|
|
73
|
+
* - **roles**: "any-of" -- the user must have **at least one** of the listed roles.
|
|
74
|
+
* - **scopes**: "all-of" -- the user must have **every** listed scope.
|
|
75
|
+
*/
|
|
76
|
+
interface AuthzRule {
|
|
77
|
+
/** Rule name for logging/debugging */
|
|
78
|
+
readonly name: string;
|
|
79
|
+
/** Method patterns to match (e.g., "admin.v1.AdminService/*", "user.v1.UserService/DeleteUser") */
|
|
80
|
+
readonly methods: ReadonlyArray<string>;
|
|
81
|
+
/** Effect when rule matches */
|
|
82
|
+
readonly effect: AuthzEffect;
|
|
83
|
+
/**
|
|
84
|
+
* Required roles/scopes for this rule.
|
|
85
|
+
*
|
|
86
|
+
* - `roles` uses "any-of" semantics: user needs at least one of the listed roles.
|
|
87
|
+
* - `scopes` uses "all-of" semantics: user needs every listed scope.
|
|
88
|
+
*/
|
|
89
|
+
readonly requires?: {
|
|
90
|
+
readonly roles?: ReadonlyArray<string>;
|
|
91
|
+
readonly scopes?: ReadonlyArray<string>;
|
|
92
|
+
} | undefined;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* LRU cache configuration for credentials verification
|
|
96
|
+
*/
|
|
97
|
+
interface CacheOptions {
|
|
98
|
+
/** Cache entry time-to-live in milliseconds */
|
|
99
|
+
readonly ttl: number;
|
|
100
|
+
/** Maximum number of cached entries */
|
|
101
|
+
readonly maxSize?: number | undefined;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Generic auth interceptor options
|
|
105
|
+
*/
|
|
106
|
+
interface AuthInterceptorOptions {
|
|
107
|
+
/**
|
|
108
|
+
* Extract credentials from request.
|
|
109
|
+
* Default: extracts Bearer token from Authorization header.
|
|
110
|
+
*
|
|
111
|
+
* @param req - Request with headers
|
|
112
|
+
* @returns Credential string or null if no credentials found
|
|
113
|
+
*/
|
|
114
|
+
extractCredentials?: (req: {
|
|
115
|
+
header: Headers;
|
|
116
|
+
}) => string | null | Promise<string | null>;
|
|
117
|
+
/**
|
|
118
|
+
* Verify credentials and return auth context.
|
|
119
|
+
* REQUIRED. Must throw on invalid credentials.
|
|
120
|
+
*
|
|
121
|
+
* @param credentials - Extracted credential string
|
|
122
|
+
* @returns AuthContext for valid credentials
|
|
123
|
+
*/
|
|
124
|
+
verifyCredentials: (credentials: string) => AuthContext | Promise<AuthContext>;
|
|
125
|
+
/**
|
|
126
|
+
* Methods to skip authentication for.
|
|
127
|
+
* Patterns: "Service/Method" or "Service/*"
|
|
128
|
+
* @default [] (health and reflection methods are NOT auto-skipped)
|
|
129
|
+
*/
|
|
130
|
+
skipMethods?: string[] | undefined;
|
|
131
|
+
/**
|
|
132
|
+
* Propagate auth context as headers for downstream services.
|
|
133
|
+
* @default false
|
|
134
|
+
*/
|
|
135
|
+
propagateHeaders?: boolean | undefined;
|
|
136
|
+
/**
|
|
137
|
+
* LRU cache for credentials verification results.
|
|
138
|
+
* Caches AuthContext by credential string to reduce verification overhead.
|
|
139
|
+
*/
|
|
140
|
+
cache?: CacheOptions | undefined;
|
|
141
|
+
/**
|
|
142
|
+
* Filter which claims are propagated in headers (SEC-001).
|
|
143
|
+
* When set, only listed claim keys are included in x-auth-claims header.
|
|
144
|
+
* When not set, all claims are propagated.
|
|
145
|
+
*/
|
|
146
|
+
propagatedClaims?: string[] | undefined;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* JWT auth interceptor options
|
|
150
|
+
*/
|
|
151
|
+
interface JwtAuthInterceptorOptions {
|
|
152
|
+
/** JWKS endpoint URL for remote key set */
|
|
153
|
+
jwksUri?: string | undefined;
|
|
154
|
+
/** HMAC symmetric secret (for HS256/HS384/HS512) */
|
|
155
|
+
secret?: string | undefined;
|
|
156
|
+
/** Asymmetric public key */
|
|
157
|
+
publicKey?: CryptoKey | undefined;
|
|
158
|
+
/** Expected issuer(s) */
|
|
159
|
+
issuer?: string | string[] | undefined;
|
|
160
|
+
/** Expected audience(s) */
|
|
161
|
+
audience?: string | string[] | undefined;
|
|
162
|
+
/** Allowed algorithms */
|
|
163
|
+
algorithms?: string[] | undefined;
|
|
164
|
+
/**
|
|
165
|
+
* Mapping from JWT claims to AuthContext fields.
|
|
166
|
+
* Supports dot-notation paths (e.g., "realm_access.roles").
|
|
167
|
+
*/
|
|
168
|
+
claimsMapping?: {
|
|
169
|
+
subject?: string | undefined;
|
|
170
|
+
name?: string | undefined;
|
|
171
|
+
roles?: string | undefined;
|
|
172
|
+
scopes?: string | undefined;
|
|
173
|
+
} | undefined;
|
|
174
|
+
/**
|
|
175
|
+
* Maximum token age.
|
|
176
|
+
* Passed to jose jwtVerify options.
|
|
177
|
+
* Number (seconds) or string (e.g., "2h", "7d").
|
|
178
|
+
*/
|
|
179
|
+
maxTokenAge?: number | string | undefined;
|
|
180
|
+
/**
|
|
181
|
+
* Methods to skip authentication for.
|
|
182
|
+
* @default []
|
|
183
|
+
*/
|
|
184
|
+
skipMethods?: string[] | undefined;
|
|
185
|
+
/**
|
|
186
|
+
* Propagate auth context as headers for downstream services.
|
|
187
|
+
* @default false
|
|
188
|
+
*/
|
|
189
|
+
propagateHeaders?: boolean | undefined;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Authorization interceptor options
|
|
193
|
+
*/
|
|
194
|
+
interface AuthzInterceptorOptions {
|
|
195
|
+
/**
|
|
196
|
+
* Default policy when no rule matches.
|
|
197
|
+
* @default "deny"
|
|
198
|
+
*/
|
|
199
|
+
defaultPolicy?: AuthzEffect | undefined;
|
|
200
|
+
/**
|
|
201
|
+
* Declarative authorization rules.
|
|
202
|
+
* Evaluated in order; first matching rule wins.
|
|
203
|
+
*/
|
|
204
|
+
rules?: AuthzRule[] | undefined;
|
|
205
|
+
/**
|
|
206
|
+
* Programmatic authorization callback.
|
|
207
|
+
* Called after rule evaluation if no rule matched,
|
|
208
|
+
* or always if no rules are defined.
|
|
209
|
+
*
|
|
210
|
+
* @param context - Authenticated user context
|
|
211
|
+
* @param req - Request info (service and method names)
|
|
212
|
+
* @returns true if authorized, false otherwise
|
|
213
|
+
*/
|
|
214
|
+
authorize?: (context: AuthContext, req: {
|
|
215
|
+
service: string;
|
|
216
|
+
method: string;
|
|
217
|
+
}) => boolean | Promise<boolean>;
|
|
218
|
+
/**
|
|
219
|
+
* Methods to skip authorization for.
|
|
220
|
+
* @default []
|
|
221
|
+
*/
|
|
222
|
+
skipMethods?: string[] | undefined;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Header name mapping for gateway auth context extraction.
|
|
226
|
+
*
|
|
227
|
+
* Maps AuthContext fields to custom header names used by the API gateway.
|
|
228
|
+
*/
|
|
229
|
+
interface GatewayHeaderMapping {
|
|
230
|
+
/** Header containing the authenticated subject */
|
|
231
|
+
readonly subject: string;
|
|
232
|
+
/** Header containing the display name */
|
|
233
|
+
readonly name?: string | undefined;
|
|
234
|
+
/** Header containing JSON-encoded roles array */
|
|
235
|
+
readonly roles?: string | undefined;
|
|
236
|
+
/** Header containing space-separated scopes */
|
|
237
|
+
readonly scopes?: string | undefined;
|
|
238
|
+
/** Header containing credential type */
|
|
239
|
+
readonly type?: string | undefined;
|
|
240
|
+
/** Header containing JSON-encoded claims */
|
|
241
|
+
readonly claims?: string | undefined;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Gateway auth interceptor options.
|
|
245
|
+
*
|
|
246
|
+
* For services behind an API gateway that has already performed authentication.
|
|
247
|
+
* Extracts auth context from gateway-injected headers.
|
|
248
|
+
*/
|
|
249
|
+
interface GatewayAuthInterceptorOptions {
|
|
250
|
+
/** Mapping from AuthContext fields to gateway header names */
|
|
251
|
+
readonly headerMapping: GatewayHeaderMapping;
|
|
252
|
+
/** Trust verification: check that request came from a trusted gateway */
|
|
253
|
+
readonly trustSource: {
|
|
254
|
+
/** Header set by the gateway to prove trust */
|
|
255
|
+
readonly header: string;
|
|
256
|
+
/** Accepted values for the trust header */
|
|
257
|
+
readonly expectedValues: string[];
|
|
258
|
+
};
|
|
259
|
+
/** Headers to strip from the request after extraction (prevent spoofing) */
|
|
260
|
+
readonly stripHeaders?: string[] | undefined;
|
|
261
|
+
/** Methods to skip authentication for */
|
|
262
|
+
readonly skipMethods?: string[] | undefined;
|
|
263
|
+
/** Propagate auth context as headers for downstream services */
|
|
264
|
+
readonly propagateHeaders?: boolean | undefined;
|
|
265
|
+
/** Default credential type when not provided by gateway */
|
|
266
|
+
readonly defaultType?: string | undefined;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Session-based auth interceptor options.
|
|
270
|
+
*
|
|
271
|
+
* Two-step authentication: verify session token, then map session data to AuthContext.
|
|
272
|
+
*/
|
|
273
|
+
interface SessionAuthInterceptorOptions {
|
|
274
|
+
/**
|
|
275
|
+
* Verify session token and return raw session data.
|
|
276
|
+
* Must throw on invalid/expired sessions.
|
|
277
|
+
*
|
|
278
|
+
* @param token - Session token string
|
|
279
|
+
* @param headers - Request headers (for additional context)
|
|
280
|
+
* @returns Raw session data
|
|
281
|
+
*/
|
|
282
|
+
readonly verifySession: (token: string, headers: Headers) => unknown | Promise<unknown>;
|
|
283
|
+
/**
|
|
284
|
+
* Map raw session data to AuthContext.
|
|
285
|
+
*
|
|
286
|
+
* @param session - Raw session data from verifySession
|
|
287
|
+
* @returns Normalized auth context
|
|
288
|
+
*/
|
|
289
|
+
readonly mapSession: (session: unknown) => AuthContext | Promise<AuthContext>;
|
|
290
|
+
/**
|
|
291
|
+
* Custom token extraction.
|
|
292
|
+
* Default: extracts Bearer token from Authorization header.
|
|
293
|
+
*/
|
|
294
|
+
readonly extractToken?: ((req: {
|
|
295
|
+
header: Headers;
|
|
296
|
+
}) => string | null | Promise<string | null>) | undefined;
|
|
297
|
+
/** LRU cache for session verification results */
|
|
298
|
+
readonly cache?: CacheOptions | undefined;
|
|
299
|
+
/** Methods to skip authentication for */
|
|
300
|
+
readonly skipMethods?: string[] | undefined;
|
|
301
|
+
/** Propagate auth context as headers for downstream services */
|
|
302
|
+
readonly propagateHeaders?: boolean | undefined;
|
|
303
|
+
/**
|
|
304
|
+
* Filter which claims are propagated in headers.
|
|
305
|
+
* When set, only listed claim keys are included in x-auth-claims header.
|
|
306
|
+
* When not set, all claims are propagated.
|
|
307
|
+
*/
|
|
308
|
+
readonly propagatedClaims?: string[] | undefined;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export { type AuthInterceptorOptions as A, type CacheOptions as C, type GatewayAuthInterceptorOptions as G, type InterceptorFactory as I, type JwtAuthInterceptorOptions as J, type SessionAuthInterceptorOptions as S, type AuthzInterceptorOptions as a, type AuthContext as b, AUTH_HEADERS as c, AuthzEffect as d, type AuthzRule as e, type GatewayHeaderMapping as f };
|