@geekmidas/constructs 10.0.0-alpha.21 → 10.0.0-alpha.23
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 +29 -23
- package/dist/{Endpoint-DbYQJgb-.d.cts → Endpoint-CrhoafsT.d.cts} +2 -2
- package/dist/{Endpoint-BxkRWQ8G.d.mts.map → Endpoint-CrhoafsT.d.cts.map} +1 -1
- package/dist/{Endpoint-BxkRWQ8G.d.mts → Endpoint-pTZrMs_h.d.mts} +2 -2
- package/dist/{Endpoint-DbYQJgb-.d.cts.map → Endpoint-pTZrMs_h.d.mts.map} +1 -1
- package/dist/{EndpointFactory-T06kTFHg.d.mts → EndpointFactory-BSLH4hSU.d.mts} +2 -2
- package/dist/{EndpointFactory-DE6nKtFZ.d.cts.map → EndpointFactory-BSLH4hSU.d.mts.map} +1 -1
- package/dist/{EndpointFactory-DE6nKtFZ.d.cts → EndpointFactory-CiCzFdMx.d.cts} +2 -2
- package/dist/{EndpointFactory-T06kTFHg.d.mts.map → EndpointFactory-CiCzFdMx.d.cts.map} +1 -1
- package/dist/Queue-BB2-DgqJ.cjs +123 -0
- package/dist/Queue-BB2-DgqJ.cjs.map +1 -0
- package/dist/Queue-Dfumm1WR.mjs +118 -0
- package/dist/Queue-Dfumm1WR.mjs.map +1 -0
- package/dist/Topic-BaNCgP61.cjs +107 -0
- package/dist/Topic-BaNCgP61.cjs.map +1 -0
- package/dist/Topic-QqnO_6Dl.mjs +102 -0
- package/dist/Topic-QqnO_6Dl.mjs.map +1 -0
- package/dist/adaptors/aws.d.cts +1 -1
- package/dist/adaptors/aws.d.mts +1 -1
- package/dist/adaptors/hono.d.cts +1 -1
- package/dist/adaptors/hono.d.mts +1 -1
- package/dist/adaptors/testing.cjs +136 -32
- package/dist/adaptors/testing.cjs.map +1 -1
- package/dist/adaptors/testing.d.cts +101 -13
- package/dist/adaptors/testing.d.cts.map +1 -1
- package/dist/adaptors/testing.d.mts +101 -13
- package/dist/adaptors/testing.d.mts.map +1 -1
- package/dist/adaptors/testing.mjs +133 -33
- package/dist/adaptors/testing.mjs.map +1 -1
- package/dist/{auth-BlO8MUkR.d.cts → auth-BR2eYxIW.d.cts} +16 -1
- package/dist/{auth-BlO8MUkR.d.cts.map → auth-BR2eYxIW.d.cts.map} +1 -1
- package/dist/{auth-CHWIdcQO.d.mts → auth-DtprqoJP.d.mts} +16 -1
- package/dist/{auth-CHWIdcQO.d.mts.map → auth-DtprqoJP.d.mts.map} +1 -1
- package/dist/auth.cjs +28 -6
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +1 -1
- package/dist/auth.d.mts +1 -1
- package/dist/auth.mjs +28 -6
- package/dist/auth.mjs.map +1 -1
- package/dist/endpoints/index.d.cts +2 -2
- package/dist/endpoints/index.d.mts +2 -2
- package/dist/queue/index.cjs +3 -116
- package/dist/queue/index.cjs.map +1 -1
- package/dist/queue/index.mjs +1 -114
- package/dist/queue/index.mjs.map +1 -1
- package/dist/rest-api.d.cts +2 -2
- package/dist/rest-api.d.mts +2 -2
- package/dist/topic/index.cjs +3 -100
- package/dist/topic/index.cjs.map +1 -1
- package/dist/topic/index.mjs +1 -98
- package/dist/topic/index.mjs.map +1 -1
- package/package.json +31 -31
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.mjs","names":["TestBrowser"],"sources":["../../src/endpoints/MswEndpointAdaptor.ts","../../src/endpoints/TestEndpointAdaptor.ts","../../src/functions/TestFunctionAdaptor.ts","../../src/queue/TestQueueAdaptor.ts","../../src/subscribers/TestSubscriberAdaptor.ts","../../src/testing/featureTest.ts"],"sourcesContent":["import { EnvironmentParser } from '@geekmidas/envkit';\nimport type { Service } from '@geekmidas/services';\nimport { ServiceDiscovery } from '@geekmidas/services';\nimport { TEST_CONTEXT_HEADER } from '@geekmidas/testkit/context';\nimport { Hono } from 'hono';\nimport { type HttpHandler, http } from 'msw';\nimport type { Endpoint } from './Endpoint';\nimport { HonoEndpoint } from './HonoEndpointAdaptor';\n\n/**\n * Header used to identify which test context a request belongs to — defined\n * once, in testkit, where the stamped `fetch` that writes it lives.\n */\nexport { TEST_CONTEXT_HEADER };\n\n/**\n * Per-test context containing an isolated ServiceDiscovery instance.\n * Registered in the context map and looked up per-request via the context ID header.\n */\nexport interface MswTestContext {\n\tserviceDiscovery: ServiceDiscovery<any>;\n}\n\n/**\n * Options for creating MSW handlers from endpoint constructs.\n *\n * @example\n * ```typescript\n * import { createMswHandlers, registerTestContext } from '@geekmidas/constructs/testing';\n * import { setupServer } from 'msw/node';\n *\n * // Create handlers once (global)\n * const { handlers, registerContext, removeContext } = createMswHandlers(\n * [getUsers, createUser],\n * { baseURL: 'http://localhost:3000' },\n * );\n * const server = setupServer(...handlers);\n *\n * // Per test: register an isolated context with its own services/transaction\n * it('should list users', async ({ db }) => {\n * const contextId = crypto.randomUUID();\n * registerContext(contextId, {\n * services: { database: db, auth: mockAuth },\n * });\n *\n * const api = createApi({\n * baseURL: 'http://localhost:3000',\n * headers: { [TEST_CONTEXT_HEADER]: contextId },\n * });\n *\n * const result = await api('GET /users');\n * expect(result).toBeDefined();\n *\n * removeContext(contextId);\n * });\n * ```\n */\nexport interface CreateMswHandlersOptions {\n\t/** Base URL the client fetches from (e.g., 'http://localhost:3000') */\n\tbaseURL: string;\n}\n\n/**\n * Options for registering a per-test context.\n * Follows the same pattern as TestRequestAdaptor — services, database,\n * publisher, and auditorStorage are provided explicitly.\n */\nexport interface RegisterContextOptions {\n\t/** Service instances keyed by serviceName */\n\tservices?: Record<string, unknown>;\n\t/** Database instance — required when endpoints use .database() */\n\tdatabase?: unknown;\n\t/** Event publisher service definition */\n\tpublisher?: Service;\n\t/** Audit storage instance — required when endpoints use .auditor() */\n\tauditorStorage?: unknown;\n}\n\nconst MSW_HTTP_METHODS = [\n\t'get',\n\t'post',\n\t'put',\n\t'patch',\n\t'delete',\n\t'options',\n] as const;\n\ntype MswHttpMethod = (typeof MSW_HTTP_METHODS)[number];\n\n/**\n * Result of creating MSW handlers from endpoint constructs.\n */\nexport interface CreateMswHandlersResult {\n\t/** MSW handlers to pass to setupServer() */\n\thandlers: HttpHandler[];\n\t/** Register an isolated context for a test */\n\tregisterContext: (id: string, options: RegisterContextOptions) => void;\n}\n\n/**\n * Creates MSW HTTP handlers from endpoint constructs.\n *\n * Mounts endpoints on a Hono app and creates MSW handlers that intercept\n * fetch requests and route them through `app.request()` — giving frontend\n * tests full endpoint behavior (validation, auth, sessions) without HTTP.\n *\n * Each test registers its own isolated context via `registerContext()`,\n * which is resolved per-request using the `x-test-context-id` header.\n * This allows concurrent tests with their own transactions and services.\n *\n * @param endpoints - Array of endpoint constructs to create handlers for\n * @param options - Configuration including baseURL\n * @returns MSW handlers, the Hono app, and context management functions\n */\nexport function createMswHandlers(\n\tendpoints: Endpoint<any, any, any, any, any, any>[],\n\toptions: CreateMswHandlersOptions,\n): CreateMswHandlersResult {\n\tconst { baseURL } = options;\n\tconst contexts = new Map<string, MswTestContext>();\n\n\t/**\n\t * Register an isolated context for a test.\n\t * Creates a fresh ServiceDiscovery and pre-registers all provided services.\n\t */\n\tfunction registerContext(id: string, ctxOptions: RegisterContextOptions) {\n\t\tconst envParser = new EnvironmentParser({});\n\t\tconst serviceDiscovery = new ServiceDiscovery(envParser);\n\n\t\tconst serviceDefs: Service[] = [];\n\n\t\t// Register explicit services\n\t\tif (ctxOptions.services) {\n\t\t\tfor (const [name, instance] of Object.entries(ctxOptions.services)) {\n\t\t\t\tserviceDefs.push({\n\t\t\t\t\tserviceName: name,\n\t\t\t\t\tregister: () => instance,\n\t\t\t\t} as Service);\n\t\t\t}\n\t\t}\n\n\t\t// Register database service from endpoint metadata\n\t\tif (ctxOptions.database !== undefined) {\n\t\t\tfor (const endpoint of endpoints) {\n\t\t\t\tif (endpoint.databaseService) {\n\t\t\t\t\tserviceDefs.push({\n\t\t\t\t\t\tserviceName: endpoint.databaseService.serviceName,\n\t\t\t\t\t\tregister: () => ctxOptions.database,\n\t\t\t\t\t} as Service);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Register publisher service\n\t\tif (ctxOptions.publisher) {\n\t\t\tserviceDefs.push(ctxOptions.publisher);\n\t\t}\n\n\t\t// Register auditor storage service from endpoint metadata\n\t\tif (ctxOptions.auditorStorage !== undefined) {\n\t\t\tfor (const endpoint of endpoints) {\n\t\t\t\tif (endpoint.auditorStorageService) {\n\t\t\t\t\tserviceDefs.push({\n\t\t\t\t\t\tserviceName: endpoint.auditorStorageService.serviceName,\n\t\t\t\t\t\tregister: () => ctxOptions.auditorStorage,\n\t\t\t\t\t} as Service);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (serviceDefs.length > 0) {\n\t\t\tserviceDiscovery.register(serviceDefs);\n\t\t}\n\n\t\tcontexts.set(id, { serviceDiscovery });\n\t}\n\n\t// Create a Hono app per-request that uses the correct ServiceDiscovery.\n\t// We use a wrapper app that resolves the context from the header,\n\t// then delegates to a context-specific Hono app.\n\tconst handlers: HttpHandler[] = [];\n\n\tfor (const endpoint of endpoints) {\n\t\tconst method = endpoint.method.toLowerCase() as MswHttpMethod;\n\t\tif (!MSW_HTTP_METHODS.includes(method)) continue;\n\n\t\tconst mswUrl = `${baseURL}${endpoint.route}`;\n\t\tconst mswMethod = http[method];\n\n\t\thandlers.push(\n\t\t\tmswMethod(mswUrl, async ({ request }) => {\n\t\t\t\tconst contextId = request.headers.get(TEST_CONTEXT_HEADER);\n\t\t\t\tconst ctx = contextId ? contexts.get(contextId) : undefined;\n\n\t\t\t\tif (!ctx) {\n\t\t\t\t\treturn new Response(\n\t\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\t\terror: 'Missing or unknown test context ID',\n\t\t\t\t\t\t\thint: `Set the '${TEST_CONTEXT_HEADER}' header to a registered context ID`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t\t{ status: 500, headers: { 'content-type': 'application/json' } },\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\t// Build a fresh Hono app with this context's ServiceDiscovery\n\t\t\t\tconst app = new Hono();\n\t\t\t\tHonoEndpoint.addRoute(endpoint, ctx.serviceDiscovery as any, app);\n\n\t\t\t\tconst response = await app.request(request);\n\n\t\t\t\treturn new Response(response.body, {\n\t\t\t\t\tstatus: response.status,\n\t\t\t\t\tstatusText: response.statusText,\n\t\t\t\t\theaders: response.headers,\n\t\t\t\t});\n\t\t\t}),\n\t\t);\n\t}\n\n\treturn {\n\t\thandlers,\n\t\tregisterContext,\n\t};\n}\n","import type {\n\tAuditActor,\n\tAuditableAction,\n\tAuditStorage,\n} from '@geekmidas/audit';\nimport { DefaultAuditor } from '@geekmidas/audit';\nimport { EnvironmentParser } from '@geekmidas/envkit';\nimport { ForbiddenError } from '@geekmidas/errors';\nimport type { EventPublisher } from '@geekmidas/events';\nimport type { Logger } from '@geekmidas/logger';\nimport type {\n\tInferComposableStandardSchema,\n\tInferStandardSchema,\n} from '@geekmidas/schema';\nimport {\n\trunWithRequestContext,\n\ttype Service,\n\tServiceDiscovery,\n\ttype ServiceRecord,\n} from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { publishConstructEvents } from '../publisher';\nimport type { HttpMethod } from '../types';\nimport type { MappedAudit } from './audit';\nimport {\n\ttype CookieOptions,\n\tEndpoint,\n\ttype EndpointSchemas,\n\tResponseBuilder,\n} from './Endpoint';\nimport {\n\tcreateCookieHeaderAccessor,\n\tcreateObjectHeaders,\n} from './lazyAccessors';\nimport {\n\ttype AuditExecutionContext,\n\texecuteWithAuditTransaction,\n} from './processAudits';\n\nexport type TestHttpResponse<TBody = any> = {\n\tbody: TBody;\n\tstatus: number;\n\theaders: Record<string, string | string[]>;\n};\n\n/**\n * Serializes a cookie into a Set-Cookie header string\n */\nfunction serializeCookie(\n\tname: string,\n\tvalue: string,\n\toptions?: CookieOptions,\n): string {\n\tlet cookieString = `${name}=${value}`;\n\n\tif (options) {\n\t\tif (options.maxAge !== undefined) {\n\t\t\tcookieString += `; Max-Age=${options.maxAge}`;\n\t\t}\n\t\tif (options.expires) {\n\t\t\tcookieString += `; Expires=${options.expires.toUTCString()}`;\n\t\t}\n\t\tif (options.domain) {\n\t\t\tcookieString += `; Domain=${options.domain}`;\n\t\t}\n\t\tif (options.path) {\n\t\t\tcookieString += `; Path=${options.path}`;\n\t\t}\n\t\tif (options.httpOnly) {\n\t\t\tcookieString += '; HttpOnly';\n\t\t}\n\t\tif (options.secure) {\n\t\t\tcookieString += '; Secure';\n\t\t}\n\t\tif (options.sameSite) {\n\t\t\tcookieString += `; SameSite=${options.sameSite}`;\n\t\t}\n\t}\n\n\treturn cookieString;\n}\n\nexport class TestEndpointAdaptor<\n\tTRoute extends string,\n\tTMethod extends HttpMethod,\n\tTInput extends EndpointSchemas = {},\n\tTOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tTSession = unknown,\n\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\tTEventPublisherServiceName extends string = string,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n\tTDatabase = undefined,\n\tTDatabaseServiceName extends string = string,\n> {\n\tstatic getDefaultServiceDiscover<\n\t\tTRoute extends string,\n\t\tTMethod extends HttpMethod,\n\t\tTInput extends EndpointSchemas = {},\n\t\tTOutSchema extends StandardSchemaV1 | undefined = undefined,\n\t\tTServices extends Service[] = [],\n\t\tTLogger extends Logger = Logger,\n\t\tTSession = unknown,\n\t\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\t\tTEventPublisherServiceName extends string = string,\n\t\tTAuditStorage extends AuditStorage | undefined = undefined,\n\t\tTAuditStorageServiceName extends string = string,\n\t\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\t\tstring,\n\t\t\tunknown\n\t\t>,\n\t\tTDatabase = undefined,\n\t\tTDatabaseServiceName extends string = string,\n\t>(\n\t\t_endpoint: Endpoint<\n\t\t\tTRoute,\n\t\t\tTMethod,\n\t\t\tTInput,\n\t\t\tTOutSchema,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tTSession,\n\t\t\tTEventPublisher,\n\t\t\tTEventPublisherServiceName,\n\t\t\tTAuditStorage,\n\t\t\tTAuditStorageServiceName,\n\t\t\tTAuditAction,\n\t\t\tTDatabase,\n\t\t\tTDatabaseServiceName\n\t\t>,\n\t) {\n\t\treturn ServiceDiscovery.getInstance(new EnvironmentParser({}));\n\t}\n\tconstructor(\n\t\tprivate readonly endpoint: Endpoint<\n\t\t\tTRoute,\n\t\t\tTMethod,\n\t\t\tTInput,\n\t\t\tTOutSchema,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tTSession,\n\t\t\tTEventPublisher,\n\t\t\tTEventPublisherServiceName,\n\t\t\tTAuditStorage,\n\t\t\tTAuditStorageServiceName,\n\t\t\tTAuditAction,\n\t\t\tTDatabase,\n\t\t\tTDatabaseServiceName\n\t\t>,\n\t\tprivate serviceDiscovery: ServiceDiscovery<any> = TestEndpointAdaptor.getDefaultServiceDiscover(\n\t\t\tendpoint,\n\t\t),\n\t) {}\n\n\tasync fullRequest(\n\t\tctx: TestRequestAdaptor<\n\t\t\tTInput,\n\t\t\tTServices,\n\t\t\tTEventPublisher,\n\t\t\tTEventPublisherServiceName,\n\t\t\tTAuditStorage,\n\t\t\tTAuditStorageServiceName,\n\t\t\tTDatabase,\n\t\t\tTDatabaseServiceName\n\t\t>,\n\t): Promise<TestHttpResponse<InferStandardSchema<TOutSchema>>> {\n\t\tconst body = await this.endpoint.parseInput((ctx as any).body, 'body');\n\t\tconst query = await this.endpoint.parseInput((ctx as any).query, 'query');\n\t\tconst params = await this.endpoint.parseInput(\n\t\t\t(ctx as any).params,\n\t\t\t'params',\n\t\t);\n\n\t\t// Lazy accessors - defer parsing until needed\n\t\tconst header = createObjectHeaders(ctx.headers);\n\t\tconst cookie = createCookieHeaderAccessor(ctx.headers.cookie);\n\n\t\t// Request context setup\n\t\tconst startTime = Date.now();\n\t\tconst requestId =\n\t\t\t(ctx as any).requestId ??\n\t\t\tctx.headers['x-request-id'] ??\n\t\t\tcrypto.randomUUID();\n\n\t\tconst logger = this.endpoint.logger.child({\n\t\t\trequestId,\n\t\t\troute: this.endpoint.route,\n\t\t\thost: ctx.headers.host,\n\t\t\tmethod: this.endpoint.method,\n\t\t}) as TLogger;\n\n\t\t// Get database from context for session extraction\n\t\tconst rawDb = (ctx as any).database as TDatabase;\n\n\t\t// Wrap handler execution in request context\n\t\treturn runWithRequestContext({ logger, requestId, startTime }, async () => {\n\t\t\tconst session = await this.endpoint.getSession({\n\t\t\t\tlogger,\n\t\t\t\tservices: ctx.services,\n\t\t\t\theader,\n\t\t\t\tcookie,\n\t\t\t\t...(rawDb !== undefined && { db: rawDb }),\n\t\t\t} as any);\n\n\t\t\t// Check authorization\n\t\t\tconst isAuthorized = await this.endpoint.authorize({\n\t\t\t\theader,\n\t\t\t\tcookie,\n\t\t\t\tservices: ctx.services,\n\t\t\t\tlogger,\n\t\t\t\tsession,\n\t\t\t\t...(rawDb !== undefined && { db: rawDb }),\n\t\t\t\tbody,\n\t\t\t\tquery,\n\t\t\t\tparams,\n\t\t\t} as any);\n\n\t\t\tif (!isAuthorized) {\n\t\t\t\tlogger.warn('Forbidden access attempt');\n\t\t\t\tthrow new ForbiddenError(\n\t\t\t\t\t'Forbidden access to the endpoint',\n\t\t\t\t\t'You do not have permission to access this resource.',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Create audit context if audit storage is provided\n\t\t\t// The auditorStorage instance is required when endpoint uses .auditor()\n\t\t\tconst auditorStorage = (ctx as any).auditorStorage as TAuditStorage;\n\t\t\tlet auditContext: AuditExecutionContext<TAuditAction> | undefined;\n\n\t\t\tif (auditorStorage) {\n\t\t\t\t// Extract actor if configured\n\t\t\t\tlet actor: AuditActor = { id: 'system', type: 'system' };\n\t\t\t\tif (this.endpoint.actorExtractor) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tactor = await this.endpoint.actorExtractor({\n\t\t\t\t\t\t\tservices: ctx.services as any,\n\t\t\t\t\t\t\tsession,\n\t\t\t\t\t\t\theader,\n\t\t\t\t\t\t\tcookie,\n\t\t\t\t\t\t\tlogger,\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tlogger.error(error as Error, 'Failed to extract actor for audits');\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst auditor = new DefaultAuditor<TAuditAction>({\n\t\t\t\t\tactor,\n\t\t\t\t\tstorage: auditorStorage as AuditStorage,\n\t\t\t\t\tmetadata: {\n\t\t\t\t\t\tendpoint: this.endpoint.route,\n\t\t\t\t\t\tmethod: this.endpoint.method,\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tauditContext = { auditor, storage: auditorStorage as AuditStorage };\n\t\t\t}\n\n\t\t\t// Warn if declarative audits are configured but no audit storage\n\t\t\tconst audits = this.endpoint.audits as MappedAudit<\n\t\t\t\tTAuditAction,\n\t\t\t\tTOutSchema\n\t\t\t>[];\n\t\t\tif (!auditContext && audits?.length) {\n\t\t\t\tlogger.warn('No auditor storage service available');\n\t\t\t}\n\n\t\t\t// Execute handler with automatic audit transaction support\n\t\t\tconst result = await executeWithAuditTransaction(\n\t\t\t\tauditContext,\n\t\t\t\tasync (auditor) => {\n\t\t\t\t\t// Use audit transaction as db if available (when storage has same database)\n\t\t\t\t\t// For testing, the tester controls whether to use transactional auditing\n\t\t\t\t\tconst trx = auditor?.getTransaction?.();\n\t\t\t\t\tconst db = trx ?? rawDb;\n\n\t\t\t\t\tconst responseBuilder = new ResponseBuilder();\n\t\t\t\t\tconst response = await this.endpoint.handler(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tbody,\n\t\t\t\t\t\t\tquery,\n\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t\tsession,\n\t\t\t\t\t\t\tservices: ctx.services,\n\t\t\t\t\t\t\tlogger,\n\t\t\t\t\t\t\theader,\n\t\t\t\t\t\t\tcookie,\n\t\t\t\t\t\t\tauditor,\n\t\t\t\t\t\t\tdb,\n\t\t\t\t\t\t} as any,\n\t\t\t\t\t\tresponseBuilder,\n\t\t\t\t\t);\n\n\t\t\t\t\t// Check if response has metadata\n\t\t\t\t\tlet data = response;\n\t\t\t\t\tlet metadata = responseBuilder.getMetadata();\n\n\t\t\t\t\tif (Endpoint.hasMetadata(response)) {\n\t\t\t\t\t\tdata = response.data;\n\t\t\t\t\t\tmetadata = response.metadata;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst output = await this.endpoint.parseOutput(data);\n\n\t\t\t\t\treturn { output, metadata, responseBuilder };\n\t\t\t\t},\n\t\t\t\t// Process declarative audits after handler (inside transaction)\n\t\t\t\tasync (result, auditor) => {\n\t\t\t\t\tif (!audits?.length) return;\n\n\t\t\t\t\tfor (const audit of audits) {\n\t\t\t\t\t\tif (audit.when && !audit.when(result.output as any)) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst payload = audit.payload(result.output as any);\n\t\t\t\t\t\tconst entityId = audit.entityId?.(result.output as any);\n\t\t\t\t\t\tauditor.audit(audit.type as any, payload as any, {\n\t\t\t\t\t\t\ttable: audit.table,\n\t\t\t\t\t\t\tentityId,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t// Pass rawDb so storage can reuse existing transactions\n\t\t\t\t{ db: rawDb },\n\t\t\t);\n\n\t\t\tconst { output, metadata } = result;\n\n\t\t\tctx.publisher && (await this.serviceDiscovery.register([ctx.publisher]));\n\t\t\tawait publishConstructEvents(\n\t\t\t\tthis.endpoint,\n\t\t\t\toutput,\n\t\t\t\tthis.serviceDiscovery,\n\t\t\t);\n\n\t\t\t// Convert cookies to Set-Cookie headers\n\t\t\tconst headers: Record<string, string | string[]> = {\n\t\t\t\t...(metadata.headers || {}),\n\t\t\t};\n\n\t\t\tif (metadata.cookies && metadata.cookies.size > 0) {\n\t\t\t\tconst setCookieValues: string[] = [];\n\t\t\t\tfor (const [name, cookie] of metadata.cookies.entries()) {\n\t\t\t\t\tsetCookieValues.push(\n\t\t\t\t\t\tserializeCookie(name, cookie.value, cookie.options),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\theaders['set-cookie'] = setCookieValues;\n\t\t\t}\n\n\t\t\t// Return HTTP response format\n\t\t\treturn {\n\t\t\t\tbody: output,\n\t\t\t\tstatus: metadata.status || 200,\n\t\t\t\theaders,\n\t\t\t};\n\t\t});\n\t}\n\n\tasync request(\n\t\tctx: TestRequestAdaptor<\n\t\t\tTInput,\n\t\t\tTServices,\n\t\t\tTEventPublisher,\n\t\t\tTEventPublisherServiceName,\n\t\t\tTAuditStorage,\n\t\t\tTAuditStorageServiceName,\n\t\t\tTDatabase,\n\t\t\tTDatabaseServiceName\n\t\t>,\n\t): Promise<InferStandardSchema<TOutSchema>> {\n\t\tconst response = await this.fullRequest(ctx);\n\t\treturn response.body;\n\t}\n}\n\n/**\n * Conditional audit storage requirement - required when TAuditStorage is configured\n */\ntype AuditStorageRequirement<\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n> = TAuditStorage extends undefined\n\t? {}\n\t: {\n\t\t\t/** Audit storage instance - required when endpoint uses .auditor() */\n\t\t\tauditorStorage: TAuditStorage;\n\t\t};\n\n/**\n * Conditional database requirement - required when TDatabase is configured\n */\ntype DatabaseRequirement<TDatabase = undefined> = TDatabase extends undefined\n\t? {}\n\t: {\n\t\t\t/** Database instance - required when endpoint uses .database() */\n\t\t\tdatabase: TDatabase;\n\t\t};\n\nexport type TestRequestAdaptor<\n\tTInput extends EndpointSchemas = {},\n\tTServices extends Service[] = [],\n\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\tTEventPublisherServiceName extends string = string,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\t_TAuditStorageServiceName extends string = string,\n\tTDatabase = undefined,\n\t_TDatabaseServiceName extends string = string,\n> = {\n\tservices: ServiceRecord<TServices>;\n\theaders: Record<string, string>;\n\tpublisher?: Service<TEventPublisherServiceName, TEventPublisher>;\n} & InferComposableStandardSchema<TInput> &\n\tAuditStorageRequirement<TAuditStorage> &\n\tDatabaseRequirement<TDatabase>;\n","import type { AuditableAction, Auditor, AuditStorage } from '@geekmidas/audit';\nimport { DefaultAuditor } from '@geekmidas/audit';\nimport { EnvironmentParser } from '@geekmidas/envkit';\nimport type { EventPublisher } from '@geekmidas/events';\nimport type { Logger } from '@geekmidas/logger';\nimport type {\n\tComposableStandardSchema,\n\tInferComposableStandardSchema,\n\tInferStandardSchema,\n} from '@geekmidas/schema';\nimport {\n\trunWithRequestContext,\n\ttype Service,\n\tServiceDiscovery,\n\ttype ServiceRecord,\n} from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { publishEvents } from '../publisher';\nimport type { Function } from './Function';\nimport { FunctionBuilder } from './FunctionBuilder';\n\nexport class TestFunctionAdaptor<\n\tTInput extends ComposableStandardSchema | undefined = undefined,\n\tTOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\tTEventPublisherServiceName extends string = string,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTDatabase = undefined,\n\tTDatabaseServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n> {\n\tstatic getDefaultServiceDiscovery<\n\t\tTInput extends ComposableStandardSchema | undefined = undefined,\n\t\tTOutSchema extends StandardSchemaV1 | undefined = undefined,\n\t\tTServices extends Service[] = [],\n\t\tTLogger extends Logger = Logger,\n\t\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\t\tTEventPublisherServiceName extends string = string,\n\t\tTAuditStorage extends AuditStorage | undefined = undefined,\n\t\tTAuditStorageServiceName extends string = string,\n\t\tTDatabase = undefined,\n\t\tTDatabaseServiceName extends string = string,\n\t\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\t\tstring,\n\t\t\tunknown\n\t\t>,\n\t>(\n\t\t_fn: Function<\n\t\t\tTInput,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tTOutSchema,\n\t\t\tTEventPublisher,\n\t\t\tTEventPublisherServiceName,\n\t\t\tTAuditStorage,\n\t\t\tTAuditStorageServiceName,\n\t\t\tTDatabase,\n\t\t\tTDatabaseServiceName,\n\t\t\tTAuditAction,\n\t\t\tany\n\t\t>,\n\t) {\n\t\treturn ServiceDiscovery.getInstance(new EnvironmentParser({}));\n\t}\n\n\tconstructor(\n\t\tprivate readonly fn: Function<\n\t\t\tTInput,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tTOutSchema,\n\t\t\tTEventPublisher,\n\t\t\tTEventPublisherServiceName,\n\t\t\tTAuditStorage,\n\t\t\tTAuditStorageServiceName,\n\t\t\tTDatabase,\n\t\t\tTDatabaseServiceName,\n\t\t\tTAuditAction,\n\t\t\tany\n\t\t>,\n\t\tprivate serviceDiscovery: ServiceDiscovery<any> = TestFunctionAdaptor.getDefaultServiceDiscovery(\n\t\t\tfn,\n\t\t),\n\t) {}\n\n\tasync invoke(\n\t\tctx: TestFunctionRequest<\n\t\t\tTInput,\n\t\t\tTServices,\n\t\t\tTEventPublisher,\n\t\t\tTEventPublisherServiceName,\n\t\t\tTDatabase,\n\t\t\tTAuditAction\n\t\t>,\n\t): Promise<InferStandardSchema<TOutSchema>> {\n\t\t// Parse input if schema is provided\n\n\t\tconst parsedInput = await FunctionBuilder.parseComposableStandardSchema(\n\t\t\tctx.input,\n\t\t\tthis.fn.input,\n\t\t);\n\n\t\t// Create logger with context\n\t\tconst logger = this.fn.logger.child({\n\t\t\ttest: true,\n\t\t}) as TLogger;\n\n\t\t// Register services (use provided services or register from function)\n\t\tlet services: ServiceRecord<TServices>;\n\t\tif (ctx.services) {\n\t\t\tservices = ctx.services;\n\t\t} else {\n\t\t\tservices = await this.serviceDiscovery.register(this.fn.services);\n\t\t}\n\n\t\t// Resolve database (use provided db or register from function)\n\t\tlet db: TDatabase | undefined;\n\t\tif ('db' in ctx && ctx.db !== undefined) {\n\t\t\tdb = ctx.db;\n\t\t} else if (this.fn.databaseService) {\n\t\t\tconst dbServices = await this.serviceDiscovery.register([\n\t\t\t\tthis.fn.databaseService,\n\t\t\t]);\n\t\t\tdb = dbServices[\n\t\t\t\tthis.fn.databaseService.serviceName as keyof typeof dbServices\n\t\t\t] as TDatabase;\n\t\t}\n\n\t\t// Resolve auditor (use provided auditor or create from function)\n\t\tlet auditor: Auditor<TAuditAction> | undefined;\n\t\tif ('auditor' in ctx && ctx.auditor !== undefined) {\n\t\t\tauditor = ctx.auditor;\n\t\t} else if (this.fn.auditorStorageService) {\n\t\t\tconst auditServices = await this.serviceDiscovery.register([\n\t\t\t\tthis.fn.auditorStorageService,\n\t\t\t]);\n\t\t\tconst storage = auditServices[\n\t\t\t\tthis.fn.auditorStorageService.serviceName as keyof typeof auditServices\n\t\t\t] as AuditStorage;\n\n\t\t\tauditor = new DefaultAuditor<TAuditAction>({\n\t\t\t\tactor: { id: 'system', type: 'system' },\n\t\t\t\tstorage,\n\t\t\t\tmetadata: {\n\t\t\t\t\tfunction: this.fn.type,\n\t\t\t\t\ttest: true,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\t// Wrap execution in request context for service access via context.getLogger()\n\t\tconst requestId = `test-${Date.now()}`;\n\t\tconst startTime = Date.now();\n\n\t\treturn runWithRequestContext({ logger, requestId, startTime }, async () => {\n\t\t\t// Execute the function\n\t\t\tconst response = await this.fn.fn({\n\t\t\t\tinput: parsedInput,\n\t\t\t\tservices,\n\t\t\t\tlogger,\n\t\t\t\tdb,\n\t\t\t\tauditor,\n\t\t\t} as any);\n\n\t\t\t// Parse output if schema is provided\n\t\t\tconst output = await this.fn.parseOutput(response);\n\n\t\t\t// Flush audits if any were recorded\n\t\t\tif (auditor) {\n\t\t\t\tconst records = auditor.getRecords();\n\t\t\t\tif (records.length > 0) {\n\t\t\t\t\tlogger.debug(\n\t\t\t\t\t\t{ auditCount: records.length },\n\t\t\t\t\t\t'Flushing function audits',\n\t\t\t\t\t);\n\t\t\t\t\tawait auditor.flush();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Register publisher service if provided in context\n\n\t\t\tawait publishEvents(\n\t\t\t\tlogger,\n\t\t\t\tthis.serviceDiscovery,\n\t\t\t\tthis.fn.events,\n\t\t\t\toutput,\n\t\t\t\tthis.fn.publisherService,\n\t\t\t);\n\n\t\t\treturn output;\n\t\t}) as Promise<InferStandardSchema<TOutSchema>>;\n\t}\n}\n\nexport type TestFunctionRequest<\n\tTInput extends ComposableStandardSchema | undefined = undefined,\n\tTServices extends Service[] = [],\n\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\tTEventPublisherServiceName extends string = string,\n\tTDatabase = undefined,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n> = {\n\tinput: InferComposableStandardSchema<TInput>;\n\tservices: ServiceRecord<TServices>;\n\tpublisher?: Service<TEventPublisherServiceName, TEventPublisher>;\n\tdb?: TDatabase;\n\tauditor?: Auditor<TAuditAction>;\n} & InferComposableStandardSchema<{ input: TInput }>;\n","import { EnvironmentParser } from '@geekmidas/envkit';\nimport type { Logger } from '@geekmidas/logger';\nimport type { InferStandardSchema } from '@geekmidas/schema';\nimport type { Service, ServiceRecord } from '@geekmidas/services';\nimport { runWithRequestContext, ServiceDiscovery } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { Queue } from './Queue';\n\n/**\n * In-memory test driver for a {@link Queue} worker. Hand it a batch of messages\n * and it registers the queue's services, runs the handler in a request context,\n * and returns the handler result — no SQS/pg-boss required.\n *\n * @example\n * const adapter = new TestQueueAdaptor(ordersQueue);\n * await adapter.invoke({ messages: [{ orderId: '123' }] });\n */\nexport class TestQueueAdaptor<\n\tTName extends string = string,\n\tTMessage extends StandardSchemaV1 = StandardSchemaV1,\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n> {\n\tstatic getDefaultServiceDiscovery() {\n\t\treturn ServiceDiscovery.getInstance(new EnvironmentParser({}));\n\t}\n\n\tconstructor(\n\t\tprivate readonly queue: Queue<TName, TMessage, TServices, TLogger>,\n\t\tprivate serviceDiscovery: ServiceDiscovery<any> = TestQueueAdaptor.getDefaultServiceDiscovery(),\n\t) {}\n\n\tasync invoke(\n\t\trequest: TestQueueRequest<TMessage, TServices>,\n\t): Promise<unknown> {\n\t\tconst logger = this.queue.logger.child({ test: true }) as TLogger;\n\n\t\tconst services =\n\t\t\trequest.services ??\n\t\t\t((await this.serviceDiscovery.register(\n\t\t\t\tthis.queue.services,\n\t\t\t)) as ServiceRecord<TServices>);\n\n\t\tconst requestId = `test-${Date.now()}`;\n\t\tconst startTime = Date.now();\n\n\t\treturn runWithRequestContext({ logger, requestId, startTime }, () =>\n\t\t\tthis.queue.handler({\n\t\t\t\tmessages: request.messages,\n\t\t\t\tservices,\n\t\t\t\tlogger,\n\t\t\t}),\n\t\t);\n\t}\n}\n\nexport type TestQueueRequest<\n\tTMessage extends StandardSchemaV1 = StandardSchemaV1,\n\tTServices extends Service[] = [],\n> = {\n\tmessages: InferStandardSchema<TMessage>[];\n\tservices?: ServiceRecord<TServices>;\n};\n","import { EnvironmentParser } from '@geekmidas/envkit';\nimport type {\n\tEventPublisher,\n\tExtractPublisherMessage,\n} from '@geekmidas/events';\nimport type { Logger } from '@geekmidas/logger';\nimport type { InferStandardSchema } from '@geekmidas/schema';\nimport type { Service, ServiceRecord } from '@geekmidas/services';\nimport { runWithRequestContext, ServiceDiscovery } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { publishEvents } from '../publisher';\nimport type { Subscriber } from './Subscriber';\n\n// Helper type to extract payload types for subscribed events\ntype ExtractEventPayloads<\n\tTPublisher extends EventPublisher<any> | undefined,\n\tTEventTypes extends any[],\n> = TPublisher extends EventPublisher<any>\n\t? Extract<ExtractPublisherMessage<TPublisher>, { type: TEventTypes[number] }>\n\t: never;\n\nexport class TestSubscriberAdaptor<\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\tTEventPublisherServiceName extends string = string,\n\tTSubscribedEvents extends\n\t\tExtractPublisherMessage<TEventPublisher>['type'][] = ExtractPublisherMessage<TEventPublisher>['type'][],\n> {\n\tstatic getDefaultServiceDiscovery() {\n\t\treturn ServiceDiscovery.getInstance(new EnvironmentParser({}));\n\t}\n\n\tconstructor(\n\t\tprivate readonly subscriber: Subscriber<\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tOutSchema,\n\t\t\tTEventPublisher,\n\t\t\tTEventPublisherServiceName,\n\t\t\tTSubscribedEvents\n\t\t>,\n\t\tprivate serviceDiscovery: ServiceDiscovery<any> = TestSubscriberAdaptor.getDefaultServiceDiscovery(),\n\t) {}\n\n\tasync invoke(\n\t\trequest: TestSubscriberRequest<\n\t\t\tTEventPublisher,\n\t\t\tTSubscribedEvents,\n\t\t\tTServices\n\t\t>,\n\t): Promise<InferStandardSchema<OutSchema>> {\n\t\t// Create logger with test context\n\t\tconst logger = this.subscriber.logger.child({\n\t\t\ttest: true,\n\t\t}) as TLogger;\n\n\t\t// Resolve services (use provided or auto-register)\n\t\tlet services: ServiceRecord<TServices>;\n\t\tif (request.services) {\n\t\t\tservices = request.services;\n\t\t} else {\n\t\t\tservices = await this.serviceDiscovery.register(this.subscriber.services);\n\t\t}\n\n\t\t// Filter events to only subscribed types\n\t\tconst filteredEvents = this.filterEvents(request.events);\n\n\t\t// Return early if no events after filtering (mirrors AWSLambdaSubscriber)\n\t\tif (filteredEvents.length === 0) {\n\t\t\treturn { batchItemFailures: [] } as any;\n\t\t}\n\n\t\t// Wrap execution in request context for service access via context.getLogger()\n\t\tconst requestId = `test-${Date.now()}`;\n\t\tconst startTime = Date.now();\n\n\t\treturn runWithRequestContext({ logger, requestId, startTime }, async () => {\n\t\t\t// Execute the subscriber handler\n\t\t\tconst result = await this.subscriber.handler({\n\t\t\t\tevents: filteredEvents,\n\t\t\t\tservices,\n\t\t\t\tlogger,\n\t\t\t});\n\n\t\t\t// Validate output if schema is provided\n\t\t\tlet output: any = result;\n\t\t\tif (this.subscriber.outputSchema && result) {\n\t\t\t\tconst validationResult =\n\t\t\t\t\tawait this.subscriber.outputSchema['~standard'].validate(result);\n\n\t\t\t\tif (validationResult.issues) {\n\t\t\t\t\tthrow new Error('Subscriber output validation failed');\n\t\t\t\t}\n\n\t\t\t\toutput = validationResult.value;\n\t\t\t}\n\n\t\t\t// Publish events if configured\n\t\t\tawait publishEvents(\n\t\t\t\tlogger,\n\t\t\t\tthis.serviceDiscovery,\n\t\t\t\tthis.subscriber.events,\n\t\t\t\toutput,\n\t\t\t\tthis.subscriber.publisherService,\n\t\t\t);\n\n\t\t\treturn output;\n\t\t}) as Promise<InferStandardSchema<OutSchema>>;\n\t}\n\n\tprivate filterEvents(\n\t\tevents: ExtractEventPayloads<TEventPublisher, TSubscribedEvents>[],\n\t): ExtractEventPayloads<TEventPublisher, TSubscribedEvents>[] {\n\t\tif (!this.subscriber.subscribedEvents) {\n\t\t\treturn events;\n\t\t}\n\n\t\treturn events.filter((event: any) =>\n\t\t\tthis.subscriber.subscribedEvents!.includes(event.type),\n\t\t);\n\t}\n}\n\nexport type TestSubscriberRequest<\n\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\tTSubscribedEvents extends any[] = [],\n\tTServices extends Service[] = [],\n> = {\n\tevents: ExtractEventPayloads<TEventPublisher, TSubscribedEvents>[];\n\tservices?: ServiceRecord<TServices>;\n};\n","/**\n * `featureTest`: an app, driven the way it runs deployed.\n *\n * A browser signs in and calls the API; the API calls the auth server; each\n * over its URL, each database in its own transaction. Nothing runs as a server:\n * every URL an app's constructs answer on is served in-process through MSW,\n * from the real handler, for the test that made the request. Everything else\n * with a real local equivalent — Postgres, Mailpit, MinIO — is used directly.\n *\n * ```ts\n * // test/config.ts\n * export const it = featureTest({\n * modules: import.meta.glob(['../../constructs/*.ts', '../src/endpoints/**\\/*.ts'], { eager: true }),\n * database,\n * browser: Browser,\n * });\n *\n * // a test\n * it('shows the signed-in user their profile', async ({ browser, mailbox }) => {\n * await browser.auth.signIn.magicLink({ email: 'ada@shop.test' });\n * await browser.visit((await mailbox('ada@shop.test').last()).link!);\n * expect(await browser.api.get('/profile')).toMatchObject({ email: 'ada@shop.test' });\n * });\n * ```\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { EnvironmentParser } from '@geekmidas/envkit';\nimport { provideKey } from '@geekmidas/manifest';\nimport { ServiceDiscovery, serviceContext } from '@geekmidas/services';\nimport { Browser as TestBrowser } from '@geekmidas/testkit/browser';\nimport {\n\tcurrentTestContext,\n\tinstallContextFetch,\n\trunAsServer,\n\trunInTestContext,\n\ttestContextOf,\n} from '@geekmidas/testkit/context';\nimport { createMailbox, type Mailbox } from '@geekmidas/testkit/mailbox';\nimport { TransactionRegistry } from '@geekmidas/testkit/transactions';\nimport { Hono } from 'hono';\nimport type { Kysely } from 'kysely';\nimport { http } from 'msw';\nimport { setupServer } from 'msw/node';\nimport { afterAll, beforeAll, test } from 'vitest';\nimport { BetterAuth } from '../auth';\nimport { KyselyDatabase } from '../database/kysely';\nimport { Email } from '../email';\nimport { Endpoint } from '../endpoints/Endpoint';\nimport { HonoEndpoint } from '../endpoints/HonoEndpointAdaptor';\n\nexport interface FeatureTestOptions<TBrowser extends TestBrowser, DB> {\n\t/**\n\t * The modules an app's constructs and endpoints are exported from — what\n\t * `import.meta.glob(…, { eager: true })` returns, or a list of modules.\n\t * Discovery reads exports, as the build does.\n\t */\n\tmodules: Record<string, unknown> | readonly unknown[];\n\t/** The app's own database: what a test receives as `db`. */\n\tdatabase?: KyselyDatabase<DB>;\n\t/** The browser each test gets — the app's subclass, with its clients. */\n\tbrowser?: new () => TBrowser;\n\t/** The environment `gkm test` injected. Defaults to `process.env`. */\n\tenv?: Record<string, string | undefined>;\n}\n\nexport interface FeatureContext<TBrowser extends TestBrowser, DB> {\n\t/** This test's browser, already the global `fetch`. */\n\tbrowser: TBrowser;\n\t/** The app database's transaction for this test, rolled back after it. */\n\tdb: Kysely<DB>;\n\t/** The mail sent to an address during this test, read from Mailpit. */\n\tmailbox: (address: string) => Mailbox;\n}\n\ntype FeatureFn<TBrowser extends TestBrowser, DB> = (\n\tcontext: FeatureContext<TBrowser, DB>,\n) => unknown;\n\nexport interface FeatureIt<TBrowser extends TestBrowser, DB> {\n\t(name: string, fn: FeatureFn<TBrowser, DB>, timeout?: number): void;\n\tonly(name: string, fn: FeatureFn<TBrowser, DB>, timeout?: number): void;\n\tskip(name: string, fn: FeatureFn<TBrowser, DB>, timeout?: number): void;\n}\n\n/** Everything one test owns, found again from its id. */\ninterface ContextState {\n\ttransactions: TransactionRegistry;\n\tdiscovery: ServiceDiscovery<any>;\n\t/** One app per surface, with this test's services. */\n\tsurfaces: Map<string, Promise<Hono>>;\n\t/** One auth server per construct, on this test's transaction. */\n\tauth: Map<BetterAuth, Promise<Hono>>;\n\t/** The addresses this test read mail for, cleared after it. */\n\taddresses: Set<string>;\n}\n\n/** Every test in flight in this process, by id. */\nconst contexts = new Map<string, ContextState>();\n\n/** Databases already resolving to test transactions — patched once each. */\nconst bound = new WeakSet<KyselyDatabase>();\n\nexport function featureTest<\n\tTBrowser extends TestBrowser = TestBrowser,\n\tDB = unknown,\n>(options: FeatureTestOptions<TBrowser, DB>): FeatureIt<TBrowser, DB> {\n\tconst env = options.env ?? process.env;\n\tconst envParser = new EnvironmentParser({ ...env });\n\tconst found = discover(options.modules);\n\tconst BrowserClass = (options.browser ?? TestBrowser) as new () => TBrowser;\n\n\tfor (const database of found.databases) bindToTests(database);\n\n\tconst network = setupServer(\n\t\t...surfaceHandlers(found.endpoints, env),\n\t\t...found.auths.map((auth) => authHandler(auth, env, envParser)),\n\t);\n\n\tconst inbox = found.emails\n\t\t.map((email) => env[provideKey(email.id, 'inboxUrl')])\n\t\t.find(Boolean);\n\tconst readMail = inbox ? createMailbox({ inbox }) : undefined;\n\n\tlet restoreFetch: () => void = () => {};\n\tbeforeAll(() => {\n\t\tnetwork.listen({\n\t\t\tonUnhandledRequest(request, print) {\n\t\t\t\t// A real local service — Mailpit, MinIO — is used directly; only a\n\t\t\t\t// URL with nothing real behind it has to be answered here.\n\t\t\t\tif (isLocal(new URL(request.url))) return;\n\t\t\t\tprint.error();\n\t\t\t},\n\t\t});\n\t\t// After MSW's interceptor, so the stamp is on before a request is caught.\n\t\trestoreFetch = installContextFetch();\n\t});\n\tafterAll(() => {\n\t\trestoreFetch();\n\t\tnetwork.close();\n\t});\n\n\tconst run = (fn: FeatureFn<TBrowser, DB>) => async (): Promise<void> => {\n\t\tconst id = randomUUID();\n\t\tconst state: ContextState = {\n\t\t\ttransactions: new TransactionRegistry(),\n\t\t\tdiscovery: new ServiceDiscovery(envParser),\n\t\t\tsurfaces: new Map(),\n\t\t\tauth: new Map(),\n\t\t\taddresses: new Set(),\n\t\t};\n\t\tcontexts.set(id, state);\n\n\t\ttry {\n\t\t\tawait runInTestContext(id, async () => {\n\t\t\t\tconst browser = new BrowserClass();\n\t\t\t\tconst restore = browser.install();\n\t\t\t\ttry {\n\t\t\t\t\tawait fn({\n\t\t\t\t\t\tbrowser,\n\t\t\t\t\t\tdb: options.database\n\t\t\t\t\t\t\t? await state.transactions.get(\n\t\t\t\t\t\t\t\t\toptions.database.id,\n\t\t\t\t\t\t\t\t\turlOf(options.database, envParser),\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t: (undefined as never),\n\t\t\t\t\t\tmailbox: (address) => {\n\t\t\t\t\t\t\tif (!readMail) throw new NoInbox();\n\t\t\t\t\t\t\tstate.addresses.add(address);\n\t\t\t\t\t\t\treturn readMail(address);\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t} finally {\n\t\t\t\t\trestore();\n\t\t\t\t}\n\t\t\t});\n\t\t} finally {\n\t\t\tcontexts.delete(id);\n\t\t\tawait Promise.all([\n\t\t\t\tstate.transactions.rollbackAll(),\n\t\t\t\t...[...state.addresses].map((address) => readMail?.(address).clear()),\n\t\t\t]);\n\t\t}\n\t};\n\n\tconst it = ((name, fn, timeout) => test(name, run(fn), timeout)) as FeatureIt<\n\t\tTBrowser,\n\t\tDB\n\t>;\n\tit.only = (name, fn, timeout) => test.only(name, run(fn), timeout);\n\tit.skip = (name, fn, timeout) => test.skip(name, run(fn), timeout);\n\treturn it;\n}\n\n/** What an app's modules export, sorted by what a test does with it. */\nfunction discover(modules: FeatureTestOptions<any, any>['modules']) {\n\tconst values = (\n\t\tArray.isArray(modules) ? modules : Object.values(modules)\n\t).flatMap((module) =>\n\t\tmodule && typeof module === 'object' ? Object.values(module) : [],\n\t);\n\n\treturn {\n\t\tendpoints: values.filter((value): value is Endpoint<any, any, any, any> =>\n\t\t\tEndpoint.isEndpoint(value),\n\t\t),\n\t\tdatabases: values.filter(\n\t\t\t(value): value is KyselyDatabase => value instanceof KyselyDatabase,\n\t\t),\n\t\tauths: values.filter(\n\t\t\t(value): value is BetterAuth => value instanceof BetterAuth,\n\t\t),\n\t\temails: values.filter((value): value is Email => value instanceof Email),\n\t};\n}\n\n/**\n * Make a database resolve, inside a test, to that test's transaction.\n *\n * Everything reaches a database through its service — an endpoint through\n * service discovery, the auth server through its tenant directly — so this is\n * the one place a test's connection can be handed over. Outside a test it is\n * the database it always was.\n */\nfunction bindToTests(database: KyselyDatabase): void {\n\tif (bound.has(database)) return;\n\tbound.add(database);\n\n\tconst service = database.service as { register: (options: any) => unknown };\n\tconst register = service.register;\n\n\tservice.register = (options) => {\n\t\tconst context = currentTestContext();\n\t\tconst state = context && contexts.get(context.id);\n\t\tif (!state) return register(options);\n\n\t\treturn state.transactions.get(\n\t\t\tdatabase.id,\n\t\t\turlOf(database, options.envParser),\n\t\t);\n\t};\n}\n\nfunction urlOf(database: { id: string }, envParser: EnvironmentParser<{}>) {\n\treturn envParser\n\t\t.create((get) => ({ url: get(provideKey(database.id, 'url')).string() }))\n\t\t.parse().url;\n}\n\n/** Each surface's endpoints, served at its URL for the test that asked. */\nfunction surfaceHandlers(\n\tendpoints: Endpoint<any, any, any, any>[],\n\tenv: Record<string, string | undefined>,\n) {\n\tconst bySurface = new Map<string, Endpoint<any, any, any, any>[]>();\n\tfor (const endpoint of endpoints) {\n\t\tconst surface = endpoint.surface?.id;\n\t\tif (!surface) continue;\n\t\tbySurface.set(surface, [...(bySurface.get(surface) ?? []), endpoint]);\n\t}\n\n\treturn [...bySurface].flatMap(([surface, served]) => {\n\t\tconst base = env[provideKey(surface, 'url')];\n\t\tif (!base) return [];\n\n\t\treturn http.all(`${trim(base)}/*`, ({ request }) =>\n\t\t\tserve(request, async (state) => {\n\t\t\t\tlet app = state.surfaces.get(surface);\n\t\t\t\tif (!app) {\n\t\t\t\t\tapp = Promise.resolve().then(() => {\n\t\t\t\t\t\tconst hono = new Hono();\n\t\t\t\t\t\tHonoEndpoint.addRoutes(served, state.discovery, hono);\n\t\t\t\t\t\treturn hono;\n\t\t\t\t\t});\n\t\t\t\t\tstate.surfaces.set(surface, app);\n\t\t\t\t}\n\t\t\t\treturn (await app).fetch(request);\n\t\t\t}),\n\t\t);\n\t});\n}\n\n/** An auth server, served at its URL, on the transaction of the test that asked. */\nfunction authHandler(\n\tauth: BetterAuth,\n\tenv: Record<string, string | undefined>,\n\tenvParser: EnvironmentParser<{}>,\n) {\n\tconst base = trim(env[provideKey(auth.id, 'url')] ?? '');\n\n\treturn http.all(`${base}${auth.basePath}/*`, ({ request }) =>\n\t\tserve(request, async (state) => {\n\t\t\tlet app = state.auth.get(auth);\n\t\t\tif (!app) {\n\t\t\t\tapp = auth\n\t\t\t\t\t.server({ envParser, context: serviceContext })\n\t\t\t\t\t.then(({ app }) => app);\n\t\t\t\tstate.auth.set(auth, app);\n\t\t\t}\n\t\t\treturn (await app).fetch(request);\n\t\t}),\n\t);\n}\n\n/** Handle a request as the server side of the test it was made for. */\nasync function serve(\n\trequest: Request,\n\thandle: (state: ContextState) => Promise<Response>,\n): Promise<Response> {\n\tconst id = testContextOf(request);\n\tconst state = id ? contexts.get(id) : undefined;\n\tif (!id || !state) throw new UnknownTestContext(request.url, id);\n\n\treturn runAsServer(id, () => handle(state));\n}\n\nfunction trim(url: string): string {\n\treturn url.replace(/\\/$/, '');\n}\n\nfunction isLocal(url: URL): boolean {\n\treturn (\n\t\turl.hostname === 'localhost' ||\n\t\turl.hostname === '127.0.0.1' ||\n\t\turl.hostname.endsWith('.localhost')\n\t);\n}\n\n/** A request reached an in-process server without a test to belong to. */\nexport class UnknownTestContext extends Error {\n\tconstructor(\n\t\treadonly url: string,\n\t\treadonly id: string | undefined,\n\t) {\n\t\tsuper(\n\t\t\tid\n\t\t\t\t? `${url} was requested for test ${id}, which is not running. A ` +\n\t\t\t\t\t\t`request that outlives its test — an un-awaited call — lands here.`\n\t\t\t\t: `${url} was requested outside any test. Make the request inside a ` +\n\t\t\t\t\t\t`featureTest, so it reaches that test's transactions.`,\n\t\t);\n\t\tthis.name = 'UnknownTestContext';\n\t}\n}\n\n/** `mailbox` was used in an app that declares no Email construct. */\nexport class NoInbox extends Error {\n\tconstructor() {\n\t\tsuper(\n\t\t\t`mailbox() reads the inbox of the app's Email construct, and none was ` +\n\t\t\t\t`found in featureTest's modules — or \\`gkm test\\` did not inject its ` +\n\t\t\t\t`<ID>_INBOX_URL.`,\n\t\t);\n\t\tthis.name = 'NoInbox';\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"testing.mjs","names":["TestBrowser"],"sources":["../../src/endpoints/MswEndpointAdaptor.ts","../../src/endpoints/TestEndpointAdaptor.ts","../../src/functions/TestFunctionAdaptor.ts","../../src/queue/TestQueueAdaptor.ts","../../src/subscribers/TestSubscriberAdaptor.ts","../../src/testing/manifest.ts","../../src/testing/featureTest.ts"],"sourcesContent":["import { EnvironmentParser } from '@geekmidas/envkit';\nimport type { Service } from '@geekmidas/services';\nimport { ServiceDiscovery } from '@geekmidas/services';\nimport { TEST_CONTEXT_HEADER } from '@geekmidas/testkit/context';\nimport { Hono } from 'hono';\nimport { type HttpHandler, http } from 'msw';\nimport type { Endpoint } from './Endpoint';\nimport { HonoEndpoint } from './HonoEndpointAdaptor';\n\n/**\n * Header used to identify which test context a request belongs to — defined\n * once, in testkit, where the stamped `fetch` that writes it lives.\n */\nexport { TEST_CONTEXT_HEADER };\n\n/**\n * Per-test context containing an isolated ServiceDiscovery instance.\n * Registered in the context map and looked up per-request via the context ID header.\n */\nexport interface MswTestContext {\n\tserviceDiscovery: ServiceDiscovery<any>;\n}\n\n/**\n * Options for creating MSW handlers from endpoint constructs.\n *\n * @example\n * ```typescript\n * import { createMswHandlers, registerTestContext } from '@geekmidas/constructs/testing';\n * import { setupServer } from 'msw/node';\n *\n * // Create handlers once (global)\n * const { handlers, registerContext, removeContext } = createMswHandlers(\n * [getUsers, createUser],\n * { baseURL: 'http://localhost:3000' },\n * );\n * const server = setupServer(...handlers);\n *\n * // Per test: register an isolated context with its own services/transaction\n * it('should list users', async ({ db }) => {\n * const contextId = crypto.randomUUID();\n * registerContext(contextId, {\n * services: { database: db, auth: mockAuth },\n * });\n *\n * const api = createApi({\n * baseURL: 'http://localhost:3000',\n * headers: { [TEST_CONTEXT_HEADER]: contextId },\n * });\n *\n * const result = await api('GET /users');\n * expect(result).toBeDefined();\n *\n * removeContext(contextId);\n * });\n * ```\n */\nexport interface CreateMswHandlersOptions {\n\t/** Base URL the client fetches from (e.g., 'http://localhost:3000') */\n\tbaseURL: string;\n}\n\n/**\n * Options for registering a per-test context.\n * Follows the same pattern as TestRequestAdaptor — services, database,\n * publisher, and auditorStorage are provided explicitly.\n */\nexport interface RegisterContextOptions {\n\t/** Service instances keyed by serviceName */\n\tservices?: Record<string, unknown>;\n\t/** Database instance — required when endpoints use .database() */\n\tdatabase?: unknown;\n\t/** Event publisher service definition */\n\tpublisher?: Service;\n\t/** Audit storage instance — required when endpoints use .auditor() */\n\tauditorStorage?: unknown;\n}\n\nconst MSW_HTTP_METHODS = [\n\t'get',\n\t'post',\n\t'put',\n\t'patch',\n\t'delete',\n\t'options',\n] as const;\n\ntype MswHttpMethod = (typeof MSW_HTTP_METHODS)[number];\n\n/**\n * Result of creating MSW handlers from endpoint constructs.\n */\nexport interface CreateMswHandlersResult {\n\t/** MSW handlers to pass to setupServer() */\n\thandlers: HttpHandler[];\n\t/** Register an isolated context for a test */\n\tregisterContext: (id: string, options: RegisterContextOptions) => void;\n}\n\n/**\n * Creates MSW HTTP handlers from endpoint constructs.\n *\n * Mounts endpoints on a Hono app and creates MSW handlers that intercept\n * fetch requests and route them through `app.request()` — giving frontend\n * tests full endpoint behavior (validation, auth, sessions) without HTTP.\n *\n * Each test registers its own isolated context via `registerContext()`,\n * which is resolved per-request using the `x-test-context-id` header.\n * This allows concurrent tests with their own transactions and services.\n *\n * @param endpoints - Array of endpoint constructs to create handlers for\n * @param options - Configuration including baseURL\n * @returns MSW handlers, the Hono app, and context management functions\n */\nexport function createMswHandlers(\n\tendpoints: Endpoint<any, any, any, any, any, any>[],\n\toptions: CreateMswHandlersOptions,\n): CreateMswHandlersResult {\n\tconst { baseURL } = options;\n\tconst contexts = new Map<string, MswTestContext>();\n\n\t/**\n\t * Register an isolated context for a test.\n\t * Creates a fresh ServiceDiscovery and pre-registers all provided services.\n\t */\n\tfunction registerContext(id: string, ctxOptions: RegisterContextOptions) {\n\t\tconst envParser = new EnvironmentParser({});\n\t\tconst serviceDiscovery = new ServiceDiscovery(envParser);\n\n\t\tconst serviceDefs: Service[] = [];\n\n\t\t// Register explicit services\n\t\tif (ctxOptions.services) {\n\t\t\tfor (const [name, instance] of Object.entries(ctxOptions.services)) {\n\t\t\t\tserviceDefs.push({\n\t\t\t\t\tserviceName: name,\n\t\t\t\t\tregister: () => instance,\n\t\t\t\t} as Service);\n\t\t\t}\n\t\t}\n\n\t\t// Register database service from endpoint metadata\n\t\tif (ctxOptions.database !== undefined) {\n\t\t\tfor (const endpoint of endpoints) {\n\t\t\t\tif (endpoint.databaseService) {\n\t\t\t\t\tserviceDefs.push({\n\t\t\t\t\t\tserviceName: endpoint.databaseService.serviceName,\n\t\t\t\t\t\tregister: () => ctxOptions.database,\n\t\t\t\t\t} as Service);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Register publisher service\n\t\tif (ctxOptions.publisher) {\n\t\t\tserviceDefs.push(ctxOptions.publisher);\n\t\t}\n\n\t\t// Register auditor storage service from endpoint metadata\n\t\tif (ctxOptions.auditorStorage !== undefined) {\n\t\t\tfor (const endpoint of endpoints) {\n\t\t\t\tif (endpoint.auditorStorageService) {\n\t\t\t\t\tserviceDefs.push({\n\t\t\t\t\t\tserviceName: endpoint.auditorStorageService.serviceName,\n\t\t\t\t\t\tregister: () => ctxOptions.auditorStorage,\n\t\t\t\t\t} as Service);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (serviceDefs.length > 0) {\n\t\t\tserviceDiscovery.register(serviceDefs);\n\t\t}\n\n\t\tcontexts.set(id, { serviceDiscovery });\n\t}\n\n\t// Create a Hono app per-request that uses the correct ServiceDiscovery.\n\t// We use a wrapper app that resolves the context from the header,\n\t// then delegates to a context-specific Hono app.\n\tconst handlers: HttpHandler[] = [];\n\n\tfor (const endpoint of endpoints) {\n\t\tconst method = endpoint.method.toLowerCase() as MswHttpMethod;\n\t\tif (!MSW_HTTP_METHODS.includes(method)) continue;\n\n\t\tconst mswUrl = `${baseURL}${endpoint.route}`;\n\t\tconst mswMethod = http[method];\n\n\t\thandlers.push(\n\t\t\tmswMethod(mswUrl, async ({ request }) => {\n\t\t\t\tconst contextId = request.headers.get(TEST_CONTEXT_HEADER);\n\t\t\t\tconst ctx = contextId ? contexts.get(contextId) : undefined;\n\n\t\t\t\tif (!ctx) {\n\t\t\t\t\treturn new Response(\n\t\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\t\terror: 'Missing or unknown test context ID',\n\t\t\t\t\t\t\thint: `Set the '${TEST_CONTEXT_HEADER}' header to a registered context ID`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t\t{ status: 500, headers: { 'content-type': 'application/json' } },\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\t// Build a fresh Hono app with this context's ServiceDiscovery\n\t\t\t\tconst app = new Hono();\n\t\t\t\tHonoEndpoint.addRoute(endpoint, ctx.serviceDiscovery as any, app);\n\n\t\t\t\tconst response = await app.request(request);\n\n\t\t\t\treturn new Response(response.body, {\n\t\t\t\t\tstatus: response.status,\n\t\t\t\t\tstatusText: response.statusText,\n\t\t\t\t\theaders: response.headers,\n\t\t\t\t});\n\t\t\t}),\n\t\t);\n\t}\n\n\treturn {\n\t\thandlers,\n\t\tregisterContext,\n\t};\n}\n","import type {\n\tAuditActor,\n\tAuditableAction,\n\tAuditStorage,\n} from '@geekmidas/audit';\nimport { DefaultAuditor } from '@geekmidas/audit';\nimport { EnvironmentParser } from '@geekmidas/envkit';\nimport { ForbiddenError } from '@geekmidas/errors';\nimport type { EventPublisher } from '@geekmidas/events';\nimport type { Logger } from '@geekmidas/logger';\nimport type {\n\tInferComposableStandardSchema,\n\tInferStandardSchema,\n} from '@geekmidas/schema';\nimport {\n\trunWithRequestContext,\n\ttype Service,\n\tServiceDiscovery,\n\ttype ServiceRecord,\n} from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { publishConstructEvents } from '../publisher';\nimport type { HttpMethod } from '../types';\nimport type { MappedAudit } from './audit';\nimport {\n\ttype CookieOptions,\n\tEndpoint,\n\ttype EndpointSchemas,\n\tResponseBuilder,\n} from './Endpoint';\nimport {\n\tcreateCookieHeaderAccessor,\n\tcreateObjectHeaders,\n} from './lazyAccessors';\nimport {\n\ttype AuditExecutionContext,\n\texecuteWithAuditTransaction,\n} from './processAudits';\n\nexport type TestHttpResponse<TBody = any> = {\n\tbody: TBody;\n\tstatus: number;\n\theaders: Record<string, string | string[]>;\n};\n\n/**\n * Serializes a cookie into a Set-Cookie header string\n */\nfunction serializeCookie(\n\tname: string,\n\tvalue: string,\n\toptions?: CookieOptions,\n): string {\n\tlet cookieString = `${name}=${value}`;\n\n\tif (options) {\n\t\tif (options.maxAge !== undefined) {\n\t\t\tcookieString += `; Max-Age=${options.maxAge}`;\n\t\t}\n\t\tif (options.expires) {\n\t\t\tcookieString += `; Expires=${options.expires.toUTCString()}`;\n\t\t}\n\t\tif (options.domain) {\n\t\t\tcookieString += `; Domain=${options.domain}`;\n\t\t}\n\t\tif (options.path) {\n\t\t\tcookieString += `; Path=${options.path}`;\n\t\t}\n\t\tif (options.httpOnly) {\n\t\t\tcookieString += '; HttpOnly';\n\t\t}\n\t\tif (options.secure) {\n\t\t\tcookieString += '; Secure';\n\t\t}\n\t\tif (options.sameSite) {\n\t\t\tcookieString += `; SameSite=${options.sameSite}`;\n\t\t}\n\t}\n\n\treturn cookieString;\n}\n\nexport class TestEndpointAdaptor<\n\tTRoute extends string,\n\tTMethod extends HttpMethod,\n\tTInput extends EndpointSchemas = {},\n\tTOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tTSession = unknown,\n\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\tTEventPublisherServiceName extends string = string,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n\tTDatabase = undefined,\n\tTDatabaseServiceName extends string = string,\n> {\n\tstatic getDefaultServiceDiscover<\n\t\tTRoute extends string,\n\t\tTMethod extends HttpMethod,\n\t\tTInput extends EndpointSchemas = {},\n\t\tTOutSchema extends StandardSchemaV1 | undefined = undefined,\n\t\tTServices extends Service[] = [],\n\t\tTLogger extends Logger = Logger,\n\t\tTSession = unknown,\n\t\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\t\tTEventPublisherServiceName extends string = string,\n\t\tTAuditStorage extends AuditStorage | undefined = undefined,\n\t\tTAuditStorageServiceName extends string = string,\n\t\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\t\tstring,\n\t\t\tunknown\n\t\t>,\n\t\tTDatabase = undefined,\n\t\tTDatabaseServiceName extends string = string,\n\t>(\n\t\t_endpoint: Endpoint<\n\t\t\tTRoute,\n\t\t\tTMethod,\n\t\t\tTInput,\n\t\t\tTOutSchema,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tTSession,\n\t\t\tTEventPublisher,\n\t\t\tTEventPublisherServiceName,\n\t\t\tTAuditStorage,\n\t\t\tTAuditStorageServiceName,\n\t\t\tTAuditAction,\n\t\t\tTDatabase,\n\t\t\tTDatabaseServiceName\n\t\t>,\n\t) {\n\t\treturn ServiceDiscovery.getInstance(new EnvironmentParser({}));\n\t}\n\tconstructor(\n\t\tprivate readonly endpoint: Endpoint<\n\t\t\tTRoute,\n\t\t\tTMethod,\n\t\t\tTInput,\n\t\t\tTOutSchema,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tTSession,\n\t\t\tTEventPublisher,\n\t\t\tTEventPublisherServiceName,\n\t\t\tTAuditStorage,\n\t\t\tTAuditStorageServiceName,\n\t\t\tTAuditAction,\n\t\t\tTDatabase,\n\t\t\tTDatabaseServiceName\n\t\t>,\n\t\tprivate serviceDiscovery: ServiceDiscovery<any> = TestEndpointAdaptor.getDefaultServiceDiscover(\n\t\t\tendpoint,\n\t\t),\n\t) {}\n\n\tasync fullRequest(\n\t\tctx: TestRequestAdaptor<\n\t\t\tTInput,\n\t\t\tTServices,\n\t\t\tTEventPublisher,\n\t\t\tTEventPublisherServiceName,\n\t\t\tTAuditStorage,\n\t\t\tTAuditStorageServiceName,\n\t\t\tTDatabase,\n\t\t\tTDatabaseServiceName\n\t\t>,\n\t): Promise<TestHttpResponse<InferStandardSchema<TOutSchema>>> {\n\t\tconst body = await this.endpoint.parseInput((ctx as any).body, 'body');\n\t\tconst query = await this.endpoint.parseInput((ctx as any).query, 'query');\n\t\tconst params = await this.endpoint.parseInput(\n\t\t\t(ctx as any).params,\n\t\t\t'params',\n\t\t);\n\n\t\t// Lazy accessors - defer parsing until needed\n\t\tconst header = createObjectHeaders(ctx.headers);\n\t\tconst cookie = createCookieHeaderAccessor(ctx.headers.cookie);\n\n\t\t// Request context setup\n\t\tconst startTime = Date.now();\n\t\tconst requestId =\n\t\t\t(ctx as any).requestId ??\n\t\t\tctx.headers['x-request-id'] ??\n\t\t\tcrypto.randomUUID();\n\n\t\tconst logger = this.endpoint.logger.child({\n\t\t\trequestId,\n\t\t\troute: this.endpoint.route,\n\t\t\thost: ctx.headers.host,\n\t\t\tmethod: this.endpoint.method,\n\t\t}) as TLogger;\n\n\t\t// Get database from context for session extraction\n\t\tconst rawDb = (ctx as any).database as TDatabase;\n\n\t\t// Wrap handler execution in request context\n\t\treturn runWithRequestContext({ logger, requestId, startTime }, async () => {\n\t\t\tconst session = await this.endpoint.getSession({\n\t\t\t\tlogger,\n\t\t\t\tservices: ctx.services,\n\t\t\t\theader,\n\t\t\t\tcookie,\n\t\t\t\t...(rawDb !== undefined && { db: rawDb }),\n\t\t\t} as any);\n\n\t\t\t// Check authorization\n\t\t\tconst isAuthorized = await this.endpoint.authorize({\n\t\t\t\theader,\n\t\t\t\tcookie,\n\t\t\t\tservices: ctx.services,\n\t\t\t\tlogger,\n\t\t\t\tsession,\n\t\t\t\t...(rawDb !== undefined && { db: rawDb }),\n\t\t\t\tbody,\n\t\t\t\tquery,\n\t\t\t\tparams,\n\t\t\t} as any);\n\n\t\t\tif (!isAuthorized) {\n\t\t\t\tlogger.warn('Forbidden access attempt');\n\t\t\t\tthrow new ForbiddenError(\n\t\t\t\t\t'Forbidden access to the endpoint',\n\t\t\t\t\t'You do not have permission to access this resource.',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Create audit context if audit storage is provided\n\t\t\t// The auditorStorage instance is required when endpoint uses .auditor()\n\t\t\tconst auditorStorage = (ctx as any).auditorStorage as TAuditStorage;\n\t\t\tlet auditContext: AuditExecutionContext<TAuditAction> | undefined;\n\n\t\t\tif (auditorStorage) {\n\t\t\t\t// Extract actor if configured\n\t\t\t\tlet actor: AuditActor = { id: 'system', type: 'system' };\n\t\t\t\tif (this.endpoint.actorExtractor) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tactor = await this.endpoint.actorExtractor({\n\t\t\t\t\t\t\tservices: ctx.services as any,\n\t\t\t\t\t\t\tsession,\n\t\t\t\t\t\t\theader,\n\t\t\t\t\t\t\tcookie,\n\t\t\t\t\t\t\tlogger,\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tlogger.error(error as Error, 'Failed to extract actor for audits');\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst auditor = new DefaultAuditor<TAuditAction>({\n\t\t\t\t\tactor,\n\t\t\t\t\tstorage: auditorStorage as AuditStorage,\n\t\t\t\t\tmetadata: {\n\t\t\t\t\t\tendpoint: this.endpoint.route,\n\t\t\t\t\t\tmethod: this.endpoint.method,\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tauditContext = { auditor, storage: auditorStorage as AuditStorage };\n\t\t\t}\n\n\t\t\t// Warn if declarative audits are configured but no audit storage\n\t\t\tconst audits = this.endpoint.audits as MappedAudit<\n\t\t\t\tTAuditAction,\n\t\t\t\tTOutSchema\n\t\t\t>[];\n\t\t\tif (!auditContext && audits?.length) {\n\t\t\t\tlogger.warn('No auditor storage service available');\n\t\t\t}\n\n\t\t\t// Execute handler with automatic audit transaction support\n\t\t\tconst result = await executeWithAuditTransaction(\n\t\t\t\tauditContext,\n\t\t\t\tasync (auditor) => {\n\t\t\t\t\t// Use audit transaction as db if available (when storage has same database)\n\t\t\t\t\t// For testing, the tester controls whether to use transactional auditing\n\t\t\t\t\tconst trx = auditor?.getTransaction?.();\n\t\t\t\t\tconst db = trx ?? rawDb;\n\n\t\t\t\t\tconst responseBuilder = new ResponseBuilder();\n\t\t\t\t\tconst response = await this.endpoint.handler(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tbody,\n\t\t\t\t\t\t\tquery,\n\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t\tsession,\n\t\t\t\t\t\t\tservices: ctx.services,\n\t\t\t\t\t\t\tlogger,\n\t\t\t\t\t\t\theader,\n\t\t\t\t\t\t\tcookie,\n\t\t\t\t\t\t\tauditor,\n\t\t\t\t\t\t\tdb,\n\t\t\t\t\t\t} as any,\n\t\t\t\t\t\tresponseBuilder,\n\t\t\t\t\t);\n\n\t\t\t\t\t// Check if response has metadata\n\t\t\t\t\tlet data = response;\n\t\t\t\t\tlet metadata = responseBuilder.getMetadata();\n\n\t\t\t\t\tif (Endpoint.hasMetadata(response)) {\n\t\t\t\t\t\tdata = response.data;\n\t\t\t\t\t\tmetadata = response.metadata;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst output = await this.endpoint.parseOutput(data);\n\n\t\t\t\t\treturn { output, metadata, responseBuilder };\n\t\t\t\t},\n\t\t\t\t// Process declarative audits after handler (inside transaction)\n\t\t\t\tasync (result, auditor) => {\n\t\t\t\t\tif (!audits?.length) return;\n\n\t\t\t\t\tfor (const audit of audits) {\n\t\t\t\t\t\tif (audit.when && !audit.when(result.output as any)) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst payload = audit.payload(result.output as any);\n\t\t\t\t\t\tconst entityId = audit.entityId?.(result.output as any);\n\t\t\t\t\t\tauditor.audit(audit.type as any, payload as any, {\n\t\t\t\t\t\t\ttable: audit.table,\n\t\t\t\t\t\t\tentityId,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t// Pass rawDb so storage can reuse existing transactions\n\t\t\t\t{ db: rawDb },\n\t\t\t);\n\n\t\t\tconst { output, metadata } = result;\n\n\t\t\tctx.publisher && (await this.serviceDiscovery.register([ctx.publisher]));\n\t\t\tawait publishConstructEvents(\n\t\t\t\tthis.endpoint,\n\t\t\t\toutput,\n\t\t\t\tthis.serviceDiscovery,\n\t\t\t);\n\n\t\t\t// Convert cookies to Set-Cookie headers\n\t\t\tconst headers: Record<string, string | string[]> = {\n\t\t\t\t...(metadata.headers || {}),\n\t\t\t};\n\n\t\t\tif (metadata.cookies && metadata.cookies.size > 0) {\n\t\t\t\tconst setCookieValues: string[] = [];\n\t\t\t\tfor (const [name, cookie] of metadata.cookies.entries()) {\n\t\t\t\t\tsetCookieValues.push(\n\t\t\t\t\t\tserializeCookie(name, cookie.value, cookie.options),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\theaders['set-cookie'] = setCookieValues;\n\t\t\t}\n\n\t\t\t// Return HTTP response format\n\t\t\treturn {\n\t\t\t\tbody: output,\n\t\t\t\tstatus: metadata.status || 200,\n\t\t\t\theaders,\n\t\t\t};\n\t\t});\n\t}\n\n\tasync request(\n\t\tctx: TestRequestAdaptor<\n\t\t\tTInput,\n\t\t\tTServices,\n\t\t\tTEventPublisher,\n\t\t\tTEventPublisherServiceName,\n\t\t\tTAuditStorage,\n\t\t\tTAuditStorageServiceName,\n\t\t\tTDatabase,\n\t\t\tTDatabaseServiceName\n\t\t>,\n\t): Promise<InferStandardSchema<TOutSchema>> {\n\t\tconst response = await this.fullRequest(ctx);\n\t\treturn response.body;\n\t}\n}\n\n/**\n * Conditional audit storage requirement - required when TAuditStorage is configured\n */\ntype AuditStorageRequirement<\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n> = TAuditStorage extends undefined\n\t? {}\n\t: {\n\t\t\t/** Audit storage instance - required when endpoint uses .auditor() */\n\t\t\tauditorStorage: TAuditStorage;\n\t\t};\n\n/**\n * Conditional database requirement - required when TDatabase is configured\n */\ntype DatabaseRequirement<TDatabase = undefined> = TDatabase extends undefined\n\t? {}\n\t: {\n\t\t\t/** Database instance - required when endpoint uses .database() */\n\t\t\tdatabase: TDatabase;\n\t\t};\n\nexport type TestRequestAdaptor<\n\tTInput extends EndpointSchemas = {},\n\tTServices extends Service[] = [],\n\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\tTEventPublisherServiceName extends string = string,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\t_TAuditStorageServiceName extends string = string,\n\tTDatabase = undefined,\n\t_TDatabaseServiceName extends string = string,\n> = {\n\tservices: ServiceRecord<TServices>;\n\theaders: Record<string, string>;\n\tpublisher?: Service<TEventPublisherServiceName, TEventPublisher>;\n} & InferComposableStandardSchema<TInput> &\n\tAuditStorageRequirement<TAuditStorage> &\n\tDatabaseRequirement<TDatabase>;\n","import type { AuditableAction, Auditor, AuditStorage } from '@geekmidas/audit';\nimport { DefaultAuditor } from '@geekmidas/audit';\nimport { EnvironmentParser } from '@geekmidas/envkit';\nimport type { EventPublisher } from '@geekmidas/events';\nimport type { Logger } from '@geekmidas/logger';\nimport type {\n\tComposableStandardSchema,\n\tInferComposableStandardSchema,\n\tInferStandardSchema,\n} from '@geekmidas/schema';\nimport {\n\trunWithRequestContext,\n\ttype Service,\n\tServiceDiscovery,\n\ttype ServiceRecord,\n} from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { publishEvents } from '../publisher';\nimport type { Function } from './Function';\nimport { FunctionBuilder } from './FunctionBuilder';\n\nexport class TestFunctionAdaptor<\n\tTInput extends ComposableStandardSchema | undefined = undefined,\n\tTOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\tTEventPublisherServiceName extends string = string,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTDatabase = undefined,\n\tTDatabaseServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n> {\n\tstatic getDefaultServiceDiscovery<\n\t\tTInput extends ComposableStandardSchema | undefined = undefined,\n\t\tTOutSchema extends StandardSchemaV1 | undefined = undefined,\n\t\tTServices extends Service[] = [],\n\t\tTLogger extends Logger = Logger,\n\t\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\t\tTEventPublisherServiceName extends string = string,\n\t\tTAuditStorage extends AuditStorage | undefined = undefined,\n\t\tTAuditStorageServiceName extends string = string,\n\t\tTDatabase = undefined,\n\t\tTDatabaseServiceName extends string = string,\n\t\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\t\tstring,\n\t\t\tunknown\n\t\t>,\n\t>(\n\t\t_fn: Function<\n\t\t\tTInput,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tTOutSchema,\n\t\t\tTEventPublisher,\n\t\t\tTEventPublisherServiceName,\n\t\t\tTAuditStorage,\n\t\t\tTAuditStorageServiceName,\n\t\t\tTDatabase,\n\t\t\tTDatabaseServiceName,\n\t\t\tTAuditAction,\n\t\t\tany\n\t\t>,\n\t) {\n\t\treturn ServiceDiscovery.getInstance(new EnvironmentParser({}));\n\t}\n\n\tconstructor(\n\t\tprivate readonly fn: Function<\n\t\t\tTInput,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tTOutSchema,\n\t\t\tTEventPublisher,\n\t\t\tTEventPublisherServiceName,\n\t\t\tTAuditStorage,\n\t\t\tTAuditStorageServiceName,\n\t\t\tTDatabase,\n\t\t\tTDatabaseServiceName,\n\t\t\tTAuditAction,\n\t\t\tany\n\t\t>,\n\t\tprivate serviceDiscovery: ServiceDiscovery<any> = TestFunctionAdaptor.getDefaultServiceDiscovery(\n\t\t\tfn,\n\t\t),\n\t) {}\n\n\tasync invoke(\n\t\tctx: TestFunctionRequest<\n\t\t\tTInput,\n\t\t\tTServices,\n\t\t\tTEventPublisher,\n\t\t\tTEventPublisherServiceName,\n\t\t\tTDatabase,\n\t\t\tTAuditAction\n\t\t>,\n\t): Promise<InferStandardSchema<TOutSchema>> {\n\t\t// Parse input if schema is provided\n\n\t\tconst parsedInput = await FunctionBuilder.parseComposableStandardSchema(\n\t\t\tctx.input,\n\t\t\tthis.fn.input,\n\t\t);\n\n\t\t// Create logger with context\n\t\tconst logger = this.fn.logger.child({\n\t\t\ttest: true,\n\t\t}) as TLogger;\n\n\t\t// Register services (use provided services or register from function)\n\t\tlet services: ServiceRecord<TServices>;\n\t\tif (ctx.services) {\n\t\t\tservices = ctx.services;\n\t\t} else {\n\t\t\tservices = await this.serviceDiscovery.register(this.fn.services);\n\t\t}\n\n\t\t// Resolve database (use provided db or register from function)\n\t\tlet db: TDatabase | undefined;\n\t\tif ('db' in ctx && ctx.db !== undefined) {\n\t\t\tdb = ctx.db;\n\t\t} else if (this.fn.databaseService) {\n\t\t\tconst dbServices = await this.serviceDiscovery.register([\n\t\t\t\tthis.fn.databaseService,\n\t\t\t]);\n\t\t\tdb = dbServices[\n\t\t\t\tthis.fn.databaseService.serviceName as keyof typeof dbServices\n\t\t\t] as TDatabase;\n\t\t}\n\n\t\t// Resolve auditor (use provided auditor or create from function)\n\t\tlet auditor: Auditor<TAuditAction> | undefined;\n\t\tif ('auditor' in ctx && ctx.auditor !== undefined) {\n\t\t\tauditor = ctx.auditor;\n\t\t} else if (this.fn.auditorStorageService) {\n\t\t\tconst auditServices = await this.serviceDiscovery.register([\n\t\t\t\tthis.fn.auditorStorageService,\n\t\t\t]);\n\t\t\tconst storage = auditServices[\n\t\t\t\tthis.fn.auditorStorageService.serviceName as keyof typeof auditServices\n\t\t\t] as AuditStorage;\n\n\t\t\tauditor = new DefaultAuditor<TAuditAction>({\n\t\t\t\tactor: { id: 'system', type: 'system' },\n\t\t\t\tstorage,\n\t\t\t\tmetadata: {\n\t\t\t\t\tfunction: this.fn.type,\n\t\t\t\t\ttest: true,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\t// Wrap execution in request context for service access via context.getLogger()\n\t\tconst requestId = `test-${Date.now()}`;\n\t\tconst startTime = Date.now();\n\n\t\treturn runWithRequestContext({ logger, requestId, startTime }, async () => {\n\t\t\t// Execute the function\n\t\t\tconst response = await this.fn.fn({\n\t\t\t\tinput: parsedInput,\n\t\t\t\tservices,\n\t\t\t\tlogger,\n\t\t\t\tdb,\n\t\t\t\tauditor,\n\t\t\t} as any);\n\n\t\t\t// Parse output if schema is provided\n\t\t\tconst output = await this.fn.parseOutput(response);\n\n\t\t\t// Flush audits if any were recorded\n\t\t\tif (auditor) {\n\t\t\t\tconst records = auditor.getRecords();\n\t\t\t\tif (records.length > 0) {\n\t\t\t\t\tlogger.debug(\n\t\t\t\t\t\t{ auditCount: records.length },\n\t\t\t\t\t\t'Flushing function audits',\n\t\t\t\t\t);\n\t\t\t\t\tawait auditor.flush();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Register publisher service if provided in context\n\n\t\t\tawait publishEvents(\n\t\t\t\tlogger,\n\t\t\t\tthis.serviceDiscovery,\n\t\t\t\tthis.fn.events,\n\t\t\t\toutput,\n\t\t\t\tthis.fn.publisherService,\n\t\t\t);\n\n\t\t\treturn output;\n\t\t}) as Promise<InferStandardSchema<TOutSchema>>;\n\t}\n}\n\nexport type TestFunctionRequest<\n\tTInput extends ComposableStandardSchema | undefined = undefined,\n\tTServices extends Service[] = [],\n\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\tTEventPublisherServiceName extends string = string,\n\tTDatabase = undefined,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n> = {\n\tinput: InferComposableStandardSchema<TInput>;\n\tservices: ServiceRecord<TServices>;\n\tpublisher?: Service<TEventPublisherServiceName, TEventPublisher>;\n\tdb?: TDatabase;\n\tauditor?: Auditor<TAuditAction>;\n} & InferComposableStandardSchema<{ input: TInput }>;\n","import { EnvironmentParser } from '@geekmidas/envkit';\nimport type { Logger } from '@geekmidas/logger';\nimport type { InferStandardSchema } from '@geekmidas/schema';\nimport type { Service, ServiceRecord } from '@geekmidas/services';\nimport { runWithRequestContext, ServiceDiscovery } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { Queue } from './Queue';\n\n/**\n * In-memory test driver for a {@link Queue} worker. Hand it a batch of messages\n * and it registers the queue's services, runs the handler in a request context,\n * and returns the handler result — no SQS/pg-boss required.\n *\n * @example\n * const adapter = new TestQueueAdaptor(ordersQueue);\n * await adapter.invoke({ messages: [{ orderId: '123' }] });\n */\nexport class TestQueueAdaptor<\n\tTName extends string = string,\n\tTMessage extends StandardSchemaV1 = StandardSchemaV1,\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n> {\n\tstatic getDefaultServiceDiscovery() {\n\t\treturn ServiceDiscovery.getInstance(new EnvironmentParser({}));\n\t}\n\n\tconstructor(\n\t\tprivate readonly queue: Queue<TName, TMessage, TServices, TLogger>,\n\t\tprivate serviceDiscovery: ServiceDiscovery<any> = TestQueueAdaptor.getDefaultServiceDiscovery(),\n\t) {}\n\n\tasync invoke(\n\t\trequest: TestQueueRequest<TMessage, TServices>,\n\t): Promise<unknown> {\n\t\tconst logger = this.queue.logger.child({ test: true }) as TLogger;\n\n\t\tconst services =\n\t\t\trequest.services ??\n\t\t\t((await this.serviceDiscovery.register(\n\t\t\t\tthis.queue.services,\n\t\t\t)) as ServiceRecord<TServices>);\n\n\t\tconst requestId = `test-${Date.now()}`;\n\t\tconst startTime = Date.now();\n\n\t\treturn runWithRequestContext({ logger, requestId, startTime }, () =>\n\t\t\tthis.queue.handler({\n\t\t\t\tmessages: request.messages,\n\t\t\t\tservices,\n\t\t\t\tlogger,\n\t\t\t}),\n\t\t);\n\t}\n}\n\nexport type TestQueueRequest<\n\tTMessage extends StandardSchemaV1 = StandardSchemaV1,\n\tTServices extends Service[] = [],\n> = {\n\tmessages: InferStandardSchema<TMessage>[];\n\tservices?: ServiceRecord<TServices>;\n};\n","import { EnvironmentParser } from '@geekmidas/envkit';\nimport type {\n\tEventPublisher,\n\tExtractPublisherMessage,\n} from '@geekmidas/events';\nimport type { Logger } from '@geekmidas/logger';\nimport type { InferStandardSchema } from '@geekmidas/schema';\nimport type { Service, ServiceRecord } from '@geekmidas/services';\nimport { runWithRequestContext, ServiceDiscovery } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { publishEvents } from '../publisher';\nimport type { Subscriber } from './Subscriber';\n\n// Helper type to extract payload types for subscribed events\ntype ExtractEventPayloads<\n\tTPublisher extends EventPublisher<any> | undefined,\n\tTEventTypes extends any[],\n> = TPublisher extends EventPublisher<any>\n\t? Extract<ExtractPublisherMessage<TPublisher>, { type: TEventTypes[number] }>\n\t: never;\n\nexport class TestSubscriberAdaptor<\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\tTEventPublisherServiceName extends string = string,\n\tTSubscribedEvents extends\n\t\tExtractPublisherMessage<TEventPublisher>['type'][] = ExtractPublisherMessage<TEventPublisher>['type'][],\n> {\n\tstatic getDefaultServiceDiscovery() {\n\t\treturn ServiceDiscovery.getInstance(new EnvironmentParser({}));\n\t}\n\n\tconstructor(\n\t\tprivate readonly subscriber: Subscriber<\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tOutSchema,\n\t\t\tTEventPublisher,\n\t\t\tTEventPublisherServiceName,\n\t\t\tTSubscribedEvents\n\t\t>,\n\t\tprivate serviceDiscovery: ServiceDiscovery<any> = TestSubscriberAdaptor.getDefaultServiceDiscovery(),\n\t) {}\n\n\tasync invoke(\n\t\trequest: TestSubscriberRequest<\n\t\t\tTEventPublisher,\n\t\t\tTSubscribedEvents,\n\t\t\tTServices\n\t\t>,\n\t): Promise<InferStandardSchema<OutSchema>> {\n\t\t// Create logger with test context\n\t\tconst logger = this.subscriber.logger.child({\n\t\t\ttest: true,\n\t\t}) as TLogger;\n\n\t\t// Resolve services (use provided or auto-register)\n\t\tlet services: ServiceRecord<TServices>;\n\t\tif (request.services) {\n\t\t\tservices = request.services;\n\t\t} else {\n\t\t\tservices = await this.serviceDiscovery.register(this.subscriber.services);\n\t\t}\n\n\t\t// Filter events to only subscribed types\n\t\tconst filteredEvents = this.filterEvents(request.events);\n\n\t\t// Return early if no events after filtering (mirrors AWSLambdaSubscriber)\n\t\tif (filteredEvents.length === 0) {\n\t\t\treturn { batchItemFailures: [] } as any;\n\t\t}\n\n\t\t// Wrap execution in request context for service access via context.getLogger()\n\t\tconst requestId = `test-${Date.now()}`;\n\t\tconst startTime = Date.now();\n\n\t\treturn runWithRequestContext({ logger, requestId, startTime }, async () => {\n\t\t\t// Execute the subscriber handler\n\t\t\tconst result = await this.subscriber.handler({\n\t\t\t\tevents: filteredEvents,\n\t\t\t\tservices,\n\t\t\t\tlogger,\n\t\t\t});\n\n\t\t\t// Validate output if schema is provided\n\t\t\tlet output: any = result;\n\t\t\tif (this.subscriber.outputSchema && result) {\n\t\t\t\tconst validationResult =\n\t\t\t\t\tawait this.subscriber.outputSchema['~standard'].validate(result);\n\n\t\t\t\tif (validationResult.issues) {\n\t\t\t\t\tthrow new Error('Subscriber output validation failed');\n\t\t\t\t}\n\n\t\t\t\toutput = validationResult.value;\n\t\t\t}\n\n\t\t\t// Publish events if configured\n\t\t\tawait publishEvents(\n\t\t\t\tlogger,\n\t\t\t\tthis.serviceDiscovery,\n\t\t\t\tthis.subscriber.events,\n\t\t\t\toutput,\n\t\t\t\tthis.subscriber.publisherService,\n\t\t\t);\n\n\t\t\treturn output;\n\t\t}) as Promise<InferStandardSchema<OutSchema>>;\n\t}\n\n\tprivate filterEvents(\n\t\tevents: ExtractEventPayloads<TEventPublisher, TSubscribedEvents>[],\n\t): ExtractEventPayloads<TEventPublisher, TSubscribedEvents>[] {\n\t\tif (!this.subscriber.subscribedEvents) {\n\t\t\treturn events;\n\t\t}\n\n\t\treturn events.filter((event: any) =>\n\t\t\tthis.subscriber.subscribedEvents!.includes(event.type),\n\t\t);\n\t}\n}\n\nexport type TestSubscriberRequest<\n\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\tTSubscribedEvents extends any[] = [],\n\tTServices extends Service[] = [],\n> = {\n\tevents: ExtractEventPayloads<TEventPublisher, TSubscribedEvents>[];\n\tservices?: ServiceRecord<TServices>;\n};\n","/**\n * The test manifest: what `gkm test` knows about an app, written down for the\n * suite it starts.\n *\n * `gkm test` discovers the constructs, reconciles the test stage and resolves\n * its environment — then, until this existed, threw all of it away and left a\n * test to declare it again by hand. The manifest keeps it: which construct and\n * endpoint is exported where, and the environment the test stage resolved,\n * under the keys the constructs themselves derive. A feature test is built from\n * it, so nothing a test says can drift from what the app declares.\n */\n\nimport { readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\n\n/** Where an export lives: what an import statement has to say. */\nexport interface TestManifestSource {\n\t/** Absolute path to the module. */\n\tfile: string;\n\t/** The name it is exported as. */\n\texport: string;\n}\n\nexport interface TestManifest {\n\t/** The stage the environment was resolved for — `test`. */\n\tstage: string;\n\t/** Every construct the app declares, by id. */\n\tconstructs: Record<string, { kind: string; source: TestManifestSource }>;\n\t/** Every endpoint, and the surface that serves it. */\n\tendpoints: { surface: string; source: TestManifestSource }[];\n\t/**\n\t * The test stage's environment — reconcile's output, keyed as the\n\t * constructs derive their keys.\n\t */\n\tenv: Record<string, string>;\n}\n\n/** The environment variable `gkm test` names the manifest in. */\nexport const TEST_MANIFEST_ENV = 'GKM_TEST_MANIFEST';\n\n/**\n * Read a test manifest: from a path, a `file:` URL, or — given nothing — the\n * one `gkm test` named in `GKM_TEST_MANIFEST`.\n */\nexport function loadTestManifest(from?: string | URL): TestManifest {\n\tconst location = from ?? process.env[TEST_MANIFEST_ENV];\n\tif (!location) throw new NoTestManifest();\n\n\tconst path =\n\t\tlocation instanceof URL || location.startsWith('file:')\n\t\t\t? fileURLToPath(location)\n\t\t\t: location;\n\n\treturn JSON.parse(readFileSync(path, 'utf-8')) as TestManifest;\n}\n\n/** A feature test ran without a manifest to build itself from. */\nexport class NoTestManifest extends Error {\n\tconstructor() {\n\t\tsuper(\n\t\t\t`No test manifest: run the suite with \\`gkm test\\`, which writes one ` +\n\t\t\t\t`and names it in ${TEST_MANIFEST_ENV}.`,\n\t\t);\n\t\tthis.name = 'NoTestManifest';\n\t}\n}\n","/**\n * `featureTest`: an app, driven the way it runs deployed.\n *\n * A browser signs in and calls the API; the API calls the auth server; each\n * over its URL, each database in its own transaction. Nothing runs as a server:\n * every URL an app's constructs answer on is served in-process through MSW,\n * from the real handler, for the test that made the request. Everything else\n * with a real local equivalent — Postgres, Mailpit, MinIO — is used directly.\n *\n * It is built from the test manifest `gkm test` writes — the constructs and\n * endpoints the app exports, and the environment the test stage resolved — so\n * a test declares none of it. `gkm test` also generates the configured `it`\n * and a `Browser` with a typed client per surface:\n *\n * ```ts\n * import { it } from '#test';\n *\n * it('shows the signed-in user their profile', async ({ browser, mailbox }) => {\n * await browser.auth.signIn.magicLink({ email: 'ada@shop.test' });\n * await browser.visit((await mailbox('ada@shop.test').last()).link!);\n * expect(await browser.api.get('/profile')).toMatchObject({ email: 'ada@shop.test' });\n * });\n * ```\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { EnvironmentParser } from '@geekmidas/envkit';\nimport { provideKey } from '@geekmidas/manifest';\nimport { ServiceDiscovery, serviceContext } from '@geekmidas/services';\nimport { Browser as TestBrowser } from '@geekmidas/testkit/browser';\nimport {\n\tcurrentTestContext,\n\tinstallContextFetch,\n\trunAsServer,\n\trunInTestContext,\n\ttestContextOf,\n} from '@geekmidas/testkit/context';\nimport { createMailbox, type Mailbox } from '@geekmidas/testkit/mailbox';\nimport { TransactionRegistry } from '@geekmidas/testkit/transactions';\nimport { Hono } from 'hono';\nimport type { Kysely } from 'kysely';\nimport { http } from 'msw';\nimport { type SetupServer, setupServer } from 'msw/node';\nimport { afterAll, beforeAll, test } from 'vitest';\nimport { BetterAuth } from '../auth';\nimport { KyselyDatabase } from '../database/kysely';\nimport { Email } from '../email';\nimport { Endpoint } from '../endpoints/Endpoint';\nimport { HonoEndpoint } from '../endpoints/HonoEndpointAdaptor';\nimport { Queue } from '../queue/Queue';\nimport { TestQueueAdaptor } from '../queue/TestQueueAdaptor';\nimport type { Subscriber } from '../subscribers/Subscriber';\nimport { TestSubscriberAdaptor } from '../subscribers/TestSubscriberAdaptor';\nimport { Topic } from '../topic/Topic';\nimport { loadTestManifest, type TestManifest } from './manifest';\n\nexport interface FeatureTestOptions<TBrowser extends TestBrowser> {\n\t/**\n\t * What the app declares and the environment its test stage resolved.\n\t * Defaults to the manifest `gkm test` names in `GKM_TEST_MANIFEST`.\n\t */\n\tmanifest?: TestManifest;\n\t/**\n\t * The app's modules, already imported, by the absolute path the manifest\n\t * records for them. The generated harness imports them statically, from\n\t * inside the app, so they resolve the way the app's own code does — its\n\t * tsconfig paths included. A module not here is imported by path.\n\t */\n\tmodules?: Readonly<Record<string, Record<string, unknown>>>;\n\t/** The browser each test gets — the generated one, with the app's clients. */\n\tbrowser?: new () => TBrowser;\n\t/**\n\t * The database a test receives as `db`, by construct id. Defaults to the\n\t * one the app's endpoints name with `.database()`.\n\t */\n\tdatabase?: string;\n}\n\nexport interface FeatureContext<TBrowser extends TestBrowser, DB> {\n\t/** This test's browser, already the global `fetch`. */\n\tbrowser: TBrowser;\n\t/** The app database's transaction for this test, rolled back after it. */\n\tdb: Kysely<DB>;\n\t/** The mail sent to an address during this test, read from Mailpit. */\n\tmailbox: (address: string) => Mailbox;\n\t/**\n\t * What this test published to a topic or enqueued on a queue, in order.\n\t * Nothing is delivered — a subscriber or a worker is tested on its own, by\n\t * handing it events — so this is where publishing is asserted.\n\t */\n\tpublished: (channel: { id: string }) => PublishedMessage[];\n\t/**\n\t * A subscriber, run on its own — handed events rather than delivered them —\n\t * with this test's services: its databases resolve to this test's\n\t * transactions, as an endpoint's do.\n\t */\n\tsubscriber: <S extends Subscriber<any, any, any, any, any, any>>(\n\t\tsubscriber: S,\n\t) => SubscriberAdaptorOf<S>;\n\t/** A queue's worker, run on its own with this test's services. */\n\tqueue: <Q extends Queue<any, any, any, any>>(queue: Q) => QueueAdaptorOf<Q>;\n}\n\ntype SubscriberAdaptorOf<S> =\n\tS extends Subscriber<\n\t\tinfer TServices,\n\t\tinfer TLogger,\n\t\tinfer OutSchema,\n\t\tinfer TEventPublisher,\n\t\tinfer TEventPublisherServiceName,\n\t\tinfer TSubscribedEvents\n\t>\n\t\t? TestSubscriberAdaptor<\n\t\t\t\tTServices,\n\t\t\t\tTLogger,\n\t\t\t\tOutSchema,\n\t\t\t\tTEventPublisher,\n\t\t\t\tTEventPublisherServiceName,\n\t\t\t\tTSubscribedEvents\n\t\t\t>\n\t\t: never;\n\ntype QueueAdaptorOf<Q> =\n\tQ extends Queue<infer TName, infer TMessage, infer TServices, infer TLogger>\n\t\t? TestQueueAdaptor<TName, TMessage, TServices, TLogger>\n\t\t: never;\n\n/** The schema a database construct was declared with — what `db` is typed by. */\nexport type DatabaseOf<C> =\n\tC extends KyselyDatabase<infer DB, any> ? DB : unknown;\n\n/** One message as it was published: its type and its payload. */\nexport interface PublishedMessage {\n\ttype: string;\n\tpayload: unknown;\n}\n\ntype FeatureFn<TBrowser extends TestBrowser, DB> = (\n\tcontext: FeatureContext<TBrowser, DB>,\n) => unknown;\n\nexport interface FeatureIt<TBrowser extends TestBrowser, DB> {\n\t(name: string, fn: FeatureFn<TBrowser, DB>, timeout?: number): void;\n\tonly(name: string, fn: FeatureFn<TBrowser, DB>, timeout?: number): void;\n\tskip(name: string, fn: FeatureFn<TBrowser, DB>, timeout?: number): void;\n}\n\n/** Everything one test owns, found again from its id. */\ninterface ContextState {\n\ttransactions: TransactionRegistry;\n\tdiscovery: ServiceDiscovery<any>;\n\t/** One app per surface, with this test's services. */\n\tsurfaces: Map<string, Promise<Hono>>;\n\t/** One auth server per construct, on this test's transaction. */\n\tauth: Map<BetterAuth, Promise<Hono>>;\n\t/** The addresses this test read mail for, cleared after it. */\n\taddresses: Set<string>;\n\t/** What this test published, by topic or queue id. */\n\tpublished: Map<string, PublishedMessage[]>;\n}\n\n/** Every test in flight in this process, by id. */\nconst contexts = new Map<string, ContextState>();\n\n/** Databases already resolving to test transactions — patched once each. */\nconst bound = new WeakSet<KyselyDatabase>();\n\n/** What the manifest points at, imported: the app's own instances. */\ninterface LoadedApp {\n\tenvParser: EnvironmentParser<{}>;\n\tendpoints: Endpoint<any, any, any, any>[];\n\tdatabases: KyselyDatabase[];\n\tauths: BetterAuth[];\n\temails: Email[];\n\t/** Every topic and queue — what a test's publishing is recorded for. */\n\tchannels: (Topic<any, any> | Queue<any, any>)[];\n\t/** What `db` is. */\n\tdatabase?: KyselyDatabase;\n\treadMail?: (address: string) => Mailbox;\n}\n\nexport function featureTest<\n\tTBrowser extends TestBrowser = TestBrowser,\n\tDB = unknown,\n>(options: FeatureTestOptions<TBrowser> = {}): FeatureIt<TBrowser, DB> {\n\tconst manifest = options.manifest ?? loadTestManifest();\n\tconst BrowserClass = (options.browser ?? TestBrowser) as new () => TBrowser;\n\n\tlet app: LoadedApp;\n\tlet network: SetupServer;\n\tlet restoreFetch: () => void = () => {};\n\n\tbeforeAll(async () => {\n\t\tapp = await load(manifest, options.database, options.modules ?? {});\n\t\tfor (const database of app.databases) bindToTests(database);\n\n\t\tnetwork = setupServer(\n\t\t\t...surfaceHandlers(app.endpoints, manifest.env),\n\t\t\t...app.auths.map((auth) =>\n\t\t\t\tauthHandler(auth, manifest.env, app.envParser),\n\t\t\t),\n\t\t);\n\t\tnetwork.listen({\n\t\t\tonUnhandledRequest(request, print) {\n\t\t\t\t// A real local service — Mailpit, MinIO — is used directly; only a\n\t\t\t\t// URL with nothing real behind it has to be answered here.\n\t\t\t\tif (isLocal(new URL(request.url))) return;\n\t\t\t\tprint.error();\n\t\t\t},\n\t\t});\n\t\t// After MSW's interceptor, so the stamp is on before a request is caught.\n\t\trestoreFetch = installContextFetch();\n\t});\n\tafterAll(() => {\n\t\trestoreFetch();\n\t\tnetwork?.close();\n\t});\n\n\tconst run = (fn: FeatureFn<TBrowser, DB>) => async (): Promise<void> => {\n\t\tconst id = randomUUID();\n\t\tconst state: ContextState = {\n\t\t\ttransactions: new TransactionRegistry(),\n\t\t\tdiscovery: new ServiceDiscovery(app.envParser),\n\t\t\tsurfaces: new Map(),\n\t\t\tauth: new Map(),\n\t\t\taddresses: new Set(),\n\t\t\tpublished: new Map(),\n\t\t};\n\t\tcontexts.set(id, state);\n\t\t// Before anything resolves them: a publisher is a service, so the one this\n\t\t// test's endpoints get is whichever is registered under its name first.\n\t\tawait state.discovery.register(recorders(app.channels, state));\n\n\t\ttry {\n\t\t\tawait runInTestContext(id, async () => {\n\t\t\t\tconst browser = new BrowserClass();\n\t\t\t\tconst restore = browser.install();\n\t\t\t\ttry {\n\t\t\t\t\tawait fn({\n\t\t\t\t\t\tbrowser,\n\t\t\t\t\t\tdb: app.database\n\t\t\t\t\t\t\t? ((await state.transactions.get(\n\t\t\t\t\t\t\t\t\tapp.database.id,\n\t\t\t\t\t\t\t\t\turlOf(app.database, app.envParser),\n\t\t\t\t\t\t\t\t)) as Kysely<DB>)\n\t\t\t\t\t\t\t: (undefined as never),\n\t\t\t\t\t\tmailbox: (address) => {\n\t\t\t\t\t\t\tif (!app.readMail) throw new NoInbox();\n\t\t\t\t\t\t\tstate.addresses.add(address);\n\t\t\t\t\t\t\treturn app.readMail(address);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tpublished: (channel) => [\n\t\t\t\t\t\t\t...(state.published.get(channel.id) ?? []),\n\t\t\t\t\t\t],\n\t\t\t\t\t\tsubscriber: (subscriber) =>\n\t\t\t\t\t\t\tnew TestSubscriberAdaptor(\n\t\t\t\t\t\t\t\tsubscriber,\n\t\t\t\t\t\t\t\tstate.discovery,\n\t\t\t\t\t\t\t) as SubscriberAdaptorOf<typeof subscriber>,\n\t\t\t\t\t\tqueue: (queue) =>\n\t\t\t\t\t\t\tnew TestQueueAdaptor(queue, state.discovery) as QueueAdaptorOf<\n\t\t\t\t\t\t\t\ttypeof queue\n\t\t\t\t\t\t\t>,\n\t\t\t\t\t});\n\t\t\t\t} finally {\n\t\t\t\t\trestore();\n\t\t\t\t}\n\t\t\t});\n\t\t} finally {\n\t\t\tcontexts.delete(id);\n\t\t\tawait Promise.all([\n\t\t\t\tstate.transactions.rollbackAll(),\n\t\t\t\t...[...state.addresses].map((address) =>\n\t\t\t\t\tapp.readMail?.(address).clear(),\n\t\t\t\t),\n\t\t\t]);\n\t\t}\n\t};\n\n\tconst it = ((name, fn, timeout) => test(name, run(fn), timeout)) as FeatureIt<\n\t\tTBrowser,\n\t\tDB\n\t>;\n\tit.only = (name, fn, timeout) => test.only(name, run(fn), timeout);\n\tit.skip = (name, fn, timeout) => test.skip(name, run(fn), timeout);\n\treturn it;\n}\n\n/**\n * Import what the manifest points at.\n *\n * The same modules the app runs, so a construct here *is* the app's construct —\n * the one its endpoints were built from — never a copy declared again.\n */\nasync function load(\n\tmanifest: TestManifest,\n\tdatabase: string | undefined,\n\tmodules: Readonly<Record<string, Record<string, unknown>>>,\n): Promise<LoadedApp> {\n\tconst envParser = new EnvironmentParser({ ...manifest.env });\n\tconst imported = async ({\n\t\tfile,\n\t\texport: name,\n\t}: {\n\t\tfile: string;\n\t\texport: string;\n\t}) =>\n\t\t// Imported by path only as a fallback: from inside `node_modules`, a\n\t\t// dynamic import is left to Node, which knows nothing of the app's\n\t\t// tsconfig paths — `~/router.ts` resolves in the app and not here.\n\t\t(modules[file] ?? ((await import(file)) as Record<string, unknown>))[name];\n\n\tconst constructs = await Promise.all(\n\t\tObject.values(manifest.constructs).map(({ source }) => imported(source)),\n\t);\n\tconst endpoints = (\n\t\tawait Promise.all(manifest.endpoints.map(({ source }) => imported(source)))\n\t).filter((value): value is Endpoint<any, any, any, any> =>\n\t\tEndpoint.isEndpoint(value),\n\t);\n\n\tconst unique = <T>(values: T[]) => [...new Set(values)];\n\tconst databases = unique(\n\t\tconstructs.filter(\n\t\t\t(value): value is KyselyDatabase => value instanceof KyselyDatabase,\n\t\t),\n\t);\n\tconst auths = unique(\n\t\tconstructs.filter(\n\t\t\t(value): value is BetterAuth => value instanceof BetterAuth,\n\t\t),\n\t);\n\tconst emails = unique(\n\t\tconstructs.filter((value): value is Email => value instanceof Email),\n\t);\n\tconst channels = unique(\n\t\tconstructs.filter(\n\t\t\t(value): value is Topic<any, any> | Queue<any, any> =>\n\t\t\t\tvalue instanceof Topic || value instanceof Queue,\n\t\t),\n\t);\n\n\tconst inbox = emails\n\t\t.map((email) => manifest.env[provideKey(email.id, 'inboxUrl')])\n\t\t.find(Boolean);\n\n\treturn {\n\t\tenvParser,\n\t\tendpoints,\n\t\tdatabases,\n\t\tauths,\n\t\temails,\n\t\tchannels,\n\t\tdatabase: databaseOf(databases, endpoints, database),\n\t\t...(inbox ? { readMail: createMailbox({ inbox }) } : {}),\n\t};\n}\n\n/**\n * A publisher that records rather than sends, under every name a topic or\n * queue is injected by — `users`, and the older `usersPublisher`.\n */\nfunction recorders(\n\tchannels: LoadedApp['channels'],\n\tstate: ContextState,\n): { serviceName: string; register: () => unknown }[] {\n\treturn channels.flatMap((channel) => {\n\t\tconst publisher = {\n\t\t\tasync publish(messages: PublishedMessage[]) {\n\t\t\t\tstate.published.set(channel.id, [\n\t\t\t\t\t...(state.published.get(channel.id) ?? []),\n\t\t\t\t\t...messages.map(({ type, payload }) => ({ type, payload })),\n\t\t\t\t]);\n\t\t\t},\n\t\t};\n\t\treturn [channel.service.serviceName, channel.publisher.serviceName].map(\n\t\t\t(serviceName) => ({ serviceName, register: () => publisher }),\n\t\t);\n\t});\n}\n\n/**\n * The database a test sees as `db`: the one named, or else the one the\n * endpoints were given with `.database()` — found by the service they hold,\n * which is the construct's own.\n */\nfunction databaseOf(\n\tdatabases: KyselyDatabase[],\n\tendpoints: Endpoint<any, any, any, any>[],\n\tnamed: string | undefined,\n): KyselyDatabase | undefined {\n\tif (named) {\n\t\tconst found = databases.find(({ id }) => id === named);\n\t\tif (!found)\n\t\t\tthrow new UnknownDatabase(\n\t\t\t\tnamed,\n\t\t\t\tdatabases.map(({ id }) => id),\n\t\t\t);\n\t\treturn found;\n\t}\n\n\tconst services = new Set<unknown>(\n\t\tendpoints.map((e) => e.databaseService).filter(Boolean),\n\t);\n\treturn databases.find((database) => services.has(database.service));\n}\n\n/**\n * Make a database resolve, inside a test, to that test's transaction.\n *\n * Everything reaches a database through its service — an endpoint through\n * service discovery, the auth server through its tenant directly — so this is\n * the one place a test's connection can be handed over. Outside a test it is\n * the database it always was.\n */\nfunction bindToTests(database: KyselyDatabase): void {\n\tif (bound.has(database)) return;\n\tbound.add(database);\n\n\tconst service = database.service as { register: (options: any) => unknown };\n\tconst register = service.register;\n\n\tservice.register = (options) => {\n\t\tconst context = currentTestContext();\n\t\tconst state = context && contexts.get(context.id);\n\t\tif (!state) return register(options);\n\n\t\treturn state.transactions.get(\n\t\t\tdatabase.id,\n\t\t\turlOf(database, options.envParser),\n\t\t);\n\t};\n}\n\nfunction urlOf(database: { id: string }, envParser: EnvironmentParser<{}>) {\n\treturn envParser\n\t\t.create((get) => ({ url: get(provideKey(database.id, 'url')).string() }))\n\t\t.parse().url;\n}\n\n/** Each surface's endpoints, served at its URL for the test that asked. */\nfunction surfaceHandlers(\n\tendpoints: Endpoint<any, any, any, any>[],\n\tenv: Record<string, string | undefined>,\n) {\n\tconst bySurface = new Map<string, Endpoint<any, any, any, any>[]>();\n\tfor (const endpoint of endpoints) {\n\t\tconst surface = endpoint.surface?.id;\n\t\tif (!surface) continue;\n\t\tbySurface.set(surface, [...(bySurface.get(surface) ?? []), endpoint]);\n\t}\n\n\treturn [...bySurface].flatMap(([surface, served]) => {\n\t\tconst base = env[provideKey(surface, 'url')];\n\t\tif (!base) return [];\n\n\t\treturn http.all(`${trim(base)}/*`, ({ request }) =>\n\t\t\tserve(request, async (state) => {\n\t\t\t\tlet app = state.surfaces.get(surface);\n\t\t\t\tif (!app) {\n\t\t\t\t\tapp = Promise.resolve().then(() => {\n\t\t\t\t\t\tconst hono = new Hono();\n\t\t\t\t\t\tHonoEndpoint.addRoutes(served, state.discovery, hono);\n\t\t\t\t\t\treturn hono;\n\t\t\t\t\t});\n\t\t\t\t\tstate.surfaces.set(surface, app);\n\t\t\t\t}\n\t\t\t\treturn (await app).fetch(request);\n\t\t\t}),\n\t\t);\n\t});\n}\n\n/** An auth server, served at its URL, on the transaction of the test that asked. */\nfunction authHandler(\n\tauth: BetterAuth,\n\tenv: Record<string, string | undefined>,\n\tenvParser: EnvironmentParser<{}>,\n) {\n\tconst base = trim(env[provideKey(auth.id, 'url')] ?? '');\n\n\t// The whole origin, not only `basePath`: the auth server answers its own\n\t// host deployed, and a redirect it issues — to `/` after verifying a magic\n\t// link — lands on it. Paths it does not route get its own 404.\n\treturn http.all(`${base}/*`, ({ request }) =>\n\t\tserve(request, async (state) => {\n\t\t\tlet app = state.auth.get(auth);\n\t\t\tif (!app) {\n\t\t\t\tapp = auth\n\t\t\t\t\t.server({ envParser, context: serviceContext })\n\t\t\t\t\t.then(({ app }) => app);\n\t\t\t\tstate.auth.set(auth, app);\n\t\t\t}\n\t\t\treturn (await app).fetch(request);\n\t\t}),\n\t);\n}\n\n/** Handle a request as the server side of the test it was made for. */\nasync function serve(\n\trequest: Request,\n\thandle: (state: ContextState) => Promise<Response>,\n): Promise<Response> {\n\tconst id = testContextOf(request);\n\tconst state = id ? contexts.get(id) : undefined;\n\tif (!id || !state) throw new UnknownTestContext(request.url, id);\n\n\treturn runAsServer(id, () => handle(state));\n}\n\nfunction trim(url: string): string {\n\treturn url.replace(/\\/$/, '');\n}\n\nfunction isLocal(url: URL): boolean {\n\treturn (\n\t\turl.hostname === 'localhost' ||\n\t\turl.hostname === '127.0.0.1' ||\n\t\turl.hostname.endsWith('.localhost')\n\t);\n}\n\n/** A request reached an in-process server without a test to belong to. */\nexport class UnknownTestContext extends Error {\n\tconstructor(\n\t\treadonly url: string,\n\t\treadonly id: string | undefined,\n\t) {\n\t\tsuper(\n\t\t\tid\n\t\t\t\t? `${url} was requested for test ${id}, which is not running. A ` +\n\t\t\t\t\t\t`request that outlives its test — an un-awaited call — lands here.`\n\t\t\t\t: `${url} was requested outside any test. Make the request inside a ` +\n\t\t\t\t\t\t`featureTest, so it reaches that test's transactions.`,\n\t\t);\n\t\tthis.name = 'UnknownTestContext';\n\t}\n}\n\n/** `mailbox` was used in an app that declares no Email construct. */\nexport class NoInbox extends Error {\n\tconstructor() {\n\t\tsuper(\n\t\t\t`mailbox() reads the inbox of the app's Email construct, and none was ` +\n\t\t\t\t`found in featureTest's modules — or \\`gkm test\\` did not inject its ` +\n\t\t\t\t`<ID>_INBOX_URL.`,\n\t\t);\n\t\tthis.name = 'NoInbox';\n\t}\n}\n\n/** `featureTest({ database })` named a database the app does not declare. */\nexport class UnknownDatabase extends Error {\n\tconstructor(\n\t\treadonly id: string,\n\t\treadonly declared: string[],\n\t) {\n\t\tsuper(\n\t\t\t`featureTest was asked for the database '${id}', and the app declares ` +\n\t\t\t\t`${declared.length ? declared.map((d) => `'${d}'`).join(', ') : 'none'}.`,\n\t\t);\n\t\tthis.name = 'UnknownDatabase';\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -110,7 +110,22 @@ declare class BetterAuth<TName extends string = string, TDatabase extends Consum
|
|
|
110
110
|
server(options: ServiceRegisterOptions): Promise<{
|
|
111
111
|
app: Hono;
|
|
112
112
|
}>;
|
|
113
|
+
/**
|
|
114
|
+
* The ids of the better-auth plugins this server runs — what a client has
|
|
115
|
+
* to pair its own plugins with (`magic-link` with `magicLinkClient`).
|
|
116
|
+
* Resolved the way the server resolves them, options callback included.
|
|
117
|
+
*/
|
|
118
|
+
pluginIds(options: ServiceRegisterOptions): Promise<string[]>;
|
|
119
|
+
/** Better-auth's options as given — a value, or a callback of the environment. */
|
|
120
|
+
private configured;
|
|
113
121
|
private connect;
|
|
122
|
+
/**
|
|
123
|
+
* Everything better-auth is given: the app's options, and what the construct
|
|
124
|
+
* owns — its secret, its URL, its tenant's connection, the origins and cookie
|
|
125
|
+
* domain its graph derives. One place, so the server and its migrations are
|
|
126
|
+
* built from the same options.
|
|
127
|
+
*/
|
|
128
|
+
private optionsFor;
|
|
114
129
|
}
|
|
115
130
|
/** The auth server answered a session check with something other than 200. */
|
|
116
131
|
declare class SessionCheckFailed extends Error {
|
|
@@ -120,4 +135,4 @@ declare class SessionCheckFailed extends Error {
|
|
|
120
135
|
}
|
|
121
136
|
//#endregion
|
|
122
137
|
export { BetterAuthOptions as a, BetterAuthConfig as i, AuthSession as n, SessionCheckFailed as o, BetterAuth as r, AuthServer as t };
|
|
123
|
-
//# sourceMappingURL=auth-
|
|
138
|
+
//# sourceMappingURL=auth-BR2eYxIW.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-
|
|
1
|
+
{"version":3,"file":"auth-BR2eYxIW.d.cts","names":[],"sources":["../src/auth.ts"],"mappings":";;;;;;;;KAgDY,aAAa,kBAAkB;;KAG/B,cAAc;;KAGd,oBAAoB,KAC/B,kBAAkB;UAIF,iBAAiB,kBAAkB;;;;;;;;EAQnD,UAAU;;;;;;;;EAQV;;;;;;;EAOA;;;;;;;;;;EAUA,UACG,sBAEA,SAAS,2BACJ,oBAAoB,QAAQ;;cAKxB,WACZ,+BACA,kBAAkB,aAAa,uBACnB,UAAU,OAAO,aAAa,cAAc;mBAoBtC;WAlBT,IAAI;WACJ,SAAS,QAAQ,aAAa,QAAQ;WACtC;;;;;mBAMQ;EAQjB,YACC,IAAI,cAAc,QACD,QAAQ,iBAAiB;;;;;;;;;;;;;;;EAuC3C,WAAW;;;;;;;;;EA0CL,OACL,SAAS,SACT,WAAW,wBACT,QAAQ;;;;;;;EAyBL,WACL,SAAS,yBACP,cAAc;;;;;;;;;;;;;;;;;;;;EAuCX,OAAO,SAAS,yBAAyB;IAAU,KAAK;;;;;;;EAoDxD,UAAU,SAAS,yBAAyB;;UAMpC;UAWA;;;;;;;UAkBA;;;cAqFF,2BAA2B;WAE7B;WACA;EAFV,YACU,uBACA"}
|
|
@@ -110,7 +110,22 @@ declare class BetterAuth<TName extends string = string, TDatabase extends Consum
|
|
|
110
110
|
server(options: ServiceRegisterOptions): Promise<{
|
|
111
111
|
app: Hono;
|
|
112
112
|
}>;
|
|
113
|
+
/**
|
|
114
|
+
* The ids of the better-auth plugins this server runs — what a client has
|
|
115
|
+
* to pair its own plugins with (`magic-link` with `magicLinkClient`).
|
|
116
|
+
* Resolved the way the server resolves them, options callback included.
|
|
117
|
+
*/
|
|
118
|
+
pluginIds(options: ServiceRegisterOptions): Promise<string[]>;
|
|
119
|
+
/** Better-auth's options as given — a value, or a callback of the environment. */
|
|
120
|
+
private configured;
|
|
113
121
|
private connect;
|
|
122
|
+
/**
|
|
123
|
+
* Everything better-auth is given: the app's options, and what the construct
|
|
124
|
+
* owns — its secret, its URL, its tenant's connection, the origins and cookie
|
|
125
|
+
* domain its graph derives. One place, so the server and its migrations are
|
|
126
|
+
* built from the same options.
|
|
127
|
+
*/
|
|
128
|
+
private optionsFor;
|
|
114
129
|
}
|
|
115
130
|
/** The auth server answered a session check with something other than 200. */
|
|
116
131
|
declare class SessionCheckFailed extends Error {
|
|
@@ -120,4 +135,4 @@ declare class SessionCheckFailed extends Error {
|
|
|
120
135
|
}
|
|
121
136
|
//#endregion
|
|
122
137
|
export { BetterAuthOptions as a, BetterAuthConfig as i, AuthSession as n, SessionCheckFailed as o, BetterAuth as r, AuthServer as t };
|
|
123
|
-
//# sourceMappingURL=auth-
|
|
138
|
+
//# sourceMappingURL=auth-DtprqoJP.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-
|
|
1
|
+
{"version":3,"file":"auth-DtprqoJP.d.mts","names":[],"sources":["../src/auth.ts"],"mappings":";;;;;;;;KAgDY,aAAa,kBAAkB;;KAG/B,cAAc;;KAGd,oBAAoB,KAC/B,kBAAkB;UAIF,iBAAiB,kBAAkB;;;;;;;;EAQnD,UAAU;;;;;;;;EAQV;;;;;;;EAOA;;;;;;;;;;EAUA,UACG,sBAEA,SAAS,2BACJ,oBAAoB,QAAQ;;cAKxB,WACZ,+BACA,kBAAkB,aAAa,uBACnB,UAAU,OAAO,aAAa,cAAc;mBAoBtC;WAlBT,IAAI;WACJ,SAAS,QAAQ,aAAa,QAAQ;WACtC;;;;;mBAMQ;EAQjB,YACC,IAAI,cAAc,QACD,QAAQ,iBAAiB;;;;;;;;;;;;;;;EAuC3C,WAAW;;;;;;;;;EA0CL,OACL,SAAS,SACT,WAAW,wBACT,QAAQ;;;;;;;EAyBL,WACL,SAAS,yBACP,cAAc;;;;;;;;;;;;;;;;;;;;EAuCX,OAAO,SAAS,yBAAyB;IAAU,KAAK;;;;;;;EAoDxD,UAAU,SAAS,yBAAyB;;UAMpC;UAWA;;;;;;;UAkBA;;;cAqFF,2BAA2B;WAE7B;WACA;EAFV,YACU,uBACA"}
|
package/dist/auth.cjs
CHANGED
|
@@ -91,9 +91,9 @@ var BetterAuth = class {
|
|
|
91
91
|
* the app never writes those tables and never gets to drift from them.
|
|
92
92
|
*/
|
|
93
93
|
async migrations(options) {
|
|
94
|
-
const
|
|
94
|
+
const resolved = await this.optionsFor(options, { owner: true });
|
|
95
95
|
const { getMigrations } = await import("better-auth/db/migration");
|
|
96
|
-
const { runMigrations } = await getMigrations(
|
|
96
|
+
const { runMigrations } = await getMigrations(resolved);
|
|
97
97
|
return runMigrations;
|
|
98
98
|
}
|
|
99
99
|
/**
|
|
@@ -140,7 +140,30 @@ var BetterAuth = class {
|
|
|
140
140
|
}
|
|
141
141
|
return { app };
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* The ids of the better-auth plugins this server runs — what a client has
|
|
145
|
+
* to pair its own plugins with (`magic-link` with `magicLinkClient`).
|
|
146
|
+
* Resolved the way the server resolves them, options callback included.
|
|
147
|
+
*/
|
|
148
|
+
async pluginIds(options) {
|
|
149
|
+
const { plugins = [] } = await this.configured(options);
|
|
150
|
+
return plugins.map(({ id }) => id);
|
|
151
|
+
}
|
|
152
|
+
/** Better-auth's options as given — a value, or a callback of the environment. */
|
|
153
|
+
async configured(options) {
|
|
154
|
+
const configure = this.config.options;
|
|
155
|
+
return typeof configure === "function" ? await configure(options) : configure ?? {};
|
|
156
|
+
}
|
|
143
157
|
async connect(options, as = {}) {
|
|
158
|
+
return (0, better_auth.betterAuth)(await this.optionsFor(options, as));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Everything better-auth is given: the app's options, and what the construct
|
|
162
|
+
* owns — its secret, its URL, its tenant's connection, the origins and cookie
|
|
163
|
+
* domain its graph derives. One place, so the server and its migrations are
|
|
164
|
+
* built from the same options.
|
|
165
|
+
*/
|
|
166
|
+
async optionsFor(options, as = {}) {
|
|
144
167
|
const { secret, baseUrl, trustedOrigins, cookieDomain } = options.envParser.create((get) => ({
|
|
145
168
|
secret: get(this.keys.secret).string(),
|
|
146
169
|
baseUrl: get(this.keys.url).string(),
|
|
@@ -149,9 +172,8 @@ var BetterAuth = class {
|
|
|
149
172
|
})).parse();
|
|
150
173
|
const tenant = this.config.database;
|
|
151
174
|
const db = await (as.owner && tenant.owner ? tenant.owner : tenant.service).register(options);
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
return (0, better_auth.betterAuth)({
|
|
175
|
+
const configured = await this.configured(options);
|
|
176
|
+
return {
|
|
155
177
|
...configured,
|
|
156
178
|
secret,
|
|
157
179
|
baseURL: baseUrl,
|
|
@@ -169,7 +191,7 @@ var BetterAuth = class {
|
|
|
169
191
|
...configured.advanced?.crossSubDomainCookies
|
|
170
192
|
} } : {}
|
|
171
193
|
}
|
|
172
|
-
}
|
|
194
|
+
};
|
|
173
195
|
}
|
|
174
196
|
};
|
|
175
197
|
/** The auth server answered a session check with something other than 200. */
|
package/dist/auth.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.cjs","names":["canonicalId","environmentCase","provideKey","serviceKey","edgeTo","betterAuth"],"sources":["../src/auth.ts"],"sourcesContent":["/**\n * `BetterAuth` — a declared auth server.\n *\n * It is three things at once, which is what makes it the construct that\n * exercises most of the model: a consumer of a database, a producer of an\n * authorizer, and a set of endpoints. This lands the first two.\n *\n * Its tables live in a schema tenant it is *given* — `orders.schema('AuthDb')`\n * — rather than one it invents. The tenant declares the schema and the role;\n * this declares the one thing left, the signing secret, and reads back the keys\n * it needs.\n *\n * :::caution\n * The isolation that arrangement is *for* — the application's own role holding\n * no grant on these tables, so a compromised handler cannot read sessions — does\n * **not hold yet**. No target creates the per-schema role: both connect as the\n * database owner, so today the tenant is a `search_path`, which is a namespace\n * and not a privilege boundary. See \"Roles are declared and never created\" in\n * `docs/design/constructs-outstanding.md`.\n * :::\n *\n * It declares its own surface, so where it answers, who may call it, and the\n * domain its cookies are scoped to all arrive as environment resolved by the\n * target — none of them guessed from a port, and none of them listed by hand in\n * application config.\n */\n\nimport type { EnvironmentParser } from '@geekmidas/envkit';\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\ttype Declaration,\n\tenvironmentCase,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type { Service, ServiceRegisterOptions } from '@geekmidas/services';\nimport { betterAuth } from 'better-auth';\nimport type { Hono } from 'hono';\nimport type { Kysely } from 'kysely';\nimport {\n\ttype Authenticator,\n\ttype Construct,\n\ttype Consumable,\n\tedgeTo,\n} from './construct-interface';\n\n/** The server better-auth hands back. */\nexport type AuthServer = ReturnType<typeof betterAuth>;\n\n/** A signed-in session as better-auth hands it back: `{ user, session }`. */\nexport type AuthSession = AuthServer['$Infer']['Session'];\n\n/** Everything better-auth takes, minus what the construct owns. */\nexport type BetterAuthOptions = Omit<\n\tParameters<typeof betterAuth>[0],\n\t'database' | 'secret' | 'basePath' | 'baseURL'\n>;\n\nexport interface BetterAuthConfig<TDatabase extends Consumable> {\n\t/**\n\t * The schema tenant its tables live in.\n\t *\n\t * A construct rather than a URL: the tenant already declares its schema and\n\t * its role, and taking the declaration is what puts the edge in the graph\n\t * instead of a connection string in two places.\n\t */\n\tdatabase: TDatabase;\n\t/**\n\t * The app that serves this auth server, relative to the workspace root —\n\t * `'apps/auth'`.\n\t *\n\t * Required, for the reason `RestApi.path` is. Not to be confused with\n\t * `basePath`, which is where its routes are mounted in a URL.\n\t */\n\tpath: string;\n\t/**\n\t * Where the auth routes are mounted, e.g. `/api/auth`.\n\t *\n\t * Structural — it is part of the URL every client calls, so it cannot differ\n\t * between stages the way the host can.\n\t */\n\tbasePath?: string;\n\t/**\n\t * The rest of better-auth's options: providers, plugins, email settings.\n\t *\n\t * A function when they need another construct — a magic-link plugin has to\n\t * *send* the link, and the thing that sends mail is a construct whose client\n\t * has to be registered. It is handed the same `ServiceRegisterOptions` this\n\t * construct was, so the env parser flows through the graph rather than being\n\t * imported out of application config.\n\t */\n\toptions?:\n\t\t| BetterAuthOptions\n\t\t| ((\n\t\t\t\toptions: ServiceRegisterOptions,\n\t\t ) => BetterAuthOptions | Promise<BetterAuthOptions>);\n}\n\nconst DEFAULT_BASE_PATH = '/api/auth';\n\nexport class BetterAuth<\n\tTName extends string = string,\n\tTDatabase extends Consumable = Consumable,\n> implements Construct<TName, AuthServer>, Authenticator<AuthSession>\n{\n\treadonly id: TName;\n\treadonly service: Service<Uncapitalize<TName>, AuthServer>;\n\treadonly basePath: string;\n\n\t/**\n\t * Declared once and read by both `declare()` and `connect()`, so the keys the\n\t * target publishes and the keys the server reads cannot drift.\n\t */\n\tprivate readonly keys: {\n\t\tsecretId: string;\n\t\tsecret: string;\n\t\turl: string;\n\t\ttrustedOrigins: string;\n\t\tcookieDomain: string;\n\t};\n\n\tconstructor(\n\t\tid: ConstructName<TName>,\n\t\tprivate readonly config: BetterAuthConfig<TDatabase>,\n\t) {\n\t\tconst canonical = canonicalId(id as string);\n\n\t\tthis.id = canonical as TName;\n\t\tthis.basePath = config.basePath ?? DEFAULT_BASE_PATH;\n\n\t\t// A secret's name is its key: `Auth` signs with `AUTH_SECRET`, which is\n\t\t// also what better-auth's own tooling looks for.\n\t\tconst secretId = `${canonical}Secret`;\n\t\tthis.keys = {\n\t\t\tsecretId,\n\t\t\tsecret: environmentCase(secretId),\n\t\t\turl: provideKey(canonical, 'url'),\n\t\t\ttrustedOrigins: provideKey(canonical, 'trustedOrigins'),\n\t\t\tcookieDomain: provideKey(canonical, 'cookieDomain'),\n\t\t};\n\n\t\t// A field, not a getter: consumers cache services by object identity.\n\t\tthis.service = {\n\t\t\tserviceName: serviceKey(canonical) as Uncapitalize<TName>,\n\t\t\tregister: (options) => this.connect(options),\n\t\t};\n\t}\n\n\t/**\n\t * A signing secret and a surface.\n\t *\n\t * The database is *not* declared here — the tenant that was passed in\n\t * declares it, and declaring it twice is the duplication the whole model\n\t * removes. What is declared is what this construct owns: the key it signs\n\t * with, and the routes it answers on.\n\t *\n\t * One endpoint, wildcarded. Better Auth routes internally and the set of\n\t * paths depends on which capabilities are enabled, so enumerating them here\n\t * would be a copy of its router that goes stale on its next release. The\n\t * surface's job is to send everything under `basePath` to one handler, which\n\t * is exactly what the declaration says.\n\t */\n\tdeclare(): Declaration[] {\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: 'secret',\n\t\t\t\tid: this.keys.secretId,\n\t\t\t\tprovides: [this.keys.secret],\n\t\t\t},\n\t\t\t{\n\t\t\t\tkind: 'rest-api',\n\t\t\t\tid: this.id,\n\t\t\t\tpath: this.config.path,\n\t\t\t\tprovides: [\n\t\t\t\t\tthis.keys.url,\n\t\t\t\t\tthis.keys.trustedOrigins,\n\t\t\t\t\tthis.keys.cookieDomain,\n\t\t\t\t],\n\t\t\t\tendpoints: [\n\t\t\t\t\t{\n\t\t\t\t\t\tid: `${this.id}Handler`,\n\t\t\t\t\t\thandler: `${this.id}.handler`,\n\t\t\t\t\t\tmethod: 'ANY',\n\t\t\t\t\t\tpath: `${this.basePath}/*`,\n\t\t\t\t\t\t// Read off the tenant rather than written down: this\n\t\t\t\t\t\t// construct takes whatever database it is given, and a\n\t\t\t\t\t\t// hardcoded kind is a second statement of a fact the\n\t\t\t\t\t\t// tenant already makes.\n\t\t\t\t\t\tdependencies: [edgeTo(this.config.database)],\n\t\t\t\t\t\trequires: [this.keys.secret],\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t];\n\t}\n\n\t/**\n\t * Whose request this is: the session for these headers, or `null`.\n\t *\n\t * Asked of the auth server over HTTP, at the URL the `.auth()` edge injects —\n\t * so it is the same call whether the server runs in this process, beside\n\t * it, or behind MSW in a test. Only the session headers are forwarded; the\n\t * surface decides which.\n\t */\n\tasync verify(\n\t\theaders: Headers,\n\t\tenvParser: EnvironmentParser<{}>,\n\t): Promise<AuthSession | null> {\n\t\tconst { url } = envParser\n\t\t\t.create((get) => ({ url: get(this.keys.url).string() }))\n\t\t\t.parse();\n\n\t\tconst response = await fetch(`${url}${this.basePath}/get-session`, {\n\t\t\theaders,\n\t\t});\n\n\t\t// Better Auth answers `null` for a request with no session. Anything\n\t\t// other than a 200 is the server failing, which is not the same thing as\n\t\t// \"signed out\" and must not be read as it.\n\t\tif (!response.ok) {\n\t\t\tthrow new SessionCheckFailed(this.id, response.status);\n\t\t}\n\n\t\treturn (await response.json()) as AuthSession | null;\n\t}\n\n\t/**\n\t * Better Auth's own migrations, for the app's migrate step.\n\t *\n\t * It brings its own schema — users, sessions, accounts, verifications — so\n\t * the app never writes those tables and never gets to drift from them.\n\t */\n\tasync migrations(\n\t\toptions: ServiceRegisterOptions,\n\t): Promise<() => Promise<void>> {\n\t\t// As the owner, not the runtime role. Creating tables is DDL, and the\n\t\t// whole point of the split is that a handler's role cannot do it — so a\n\t\t// migrator that connected the way a handler does would fail on the first\n\t\t// `CREATE TABLE` rather than working by accident.\n\t\tconst auth = await this.connect(options, { owner: true });\n\t\t// better-auth 1.7 split the migration builder out of `better-auth/db`\n\t\t// into its own entry, so importing it no longer drags the whole db layer\n\t\t// in behind it.\n\t\tconst { getMigrations } = await import('better-auth/db/migration');\n\t\tconst { runMigrations } = await getMigrations(auth.options);\n\n\t\treturn runMigrations;\n\t}\n\n\t/**\n\t * The server this surface runs as, built from its own declaration.\n\t *\n\t * A surface is a deploy unit, so something has to serve it, and the thing\n\t * that knows how is the construct — not a generator writing a mount into\n\t * somebody else's entry point. That is how the auth server came to be\n\t * mounted by a hook in the application it was supposed to be separate from,\n\t * and then to be served by nothing at all when the hook was deleted.\n\t *\n\t * The routes come from {@link declare}: one wildcard under `basePath`, read\n\t * off the same declaration the manifest carries, so the surface the graph\n\t * describes and the surface that answers requests cannot disagree.\n\t *\n\t * ```ts\n\t * // .gkm/server/app.ts — the whole generated entry\n\t * import { auth } from '@acme/constructs/auth.js';\n\t * export const { app } = await auth.server({ envParser });\n\t * ```\n\t */\n\tasync server(options: ServiceRegisterOptions): Promise<{ app: Hono }> {\n\t\tconst { Hono } = await import('hono');\n\t\tconst { cors } = await import('hono/cors');\n\t\tconst app = new Hono();\n\t\tconst server = await this.connect(options);\n\n\t\t// Who may call this surface, read off the graph. The same list Better\n\t\t// Auth checks for CSRF — it arrives as `<ID>_TRUSTED_ORIGINS`, composed\n\t\t// by the target from whatever declared an edge to this construct, so a\n\t\t// site that depends on the auth server is allowed to reach it and\n\t\t// nothing else is. Nobody writes an origin down.\n\t\tconst { origins } = options.envParser\n\t\t\t.create((get) => ({\n\t\t\t\torigins: get(this.keys.trustedOrigins)\n\t\t\t\t\t.string()\n\t\t\t\t\t.default('')\n\t\t\t\t\t.transform((value: string) =>\n\t\t\t\t\t\tvalue\n\t\t\t\t\t\t\t.split(',')\n\t\t\t\t\t\t\t.map((origin) => origin.trim())\n\t\t\t\t\t\t\t.filter(Boolean),\n\t\t\t\t\t),\n\t\t\t}))\n\t\t\t.parse();\n\n\t\tapp.use('*', cors({ origin: origins, credentials: true, maxAge: 3600 }));\n\n\t\t// Read off the declaration rather than restated here. `ANY` is every\n\t\t// method Better Auth routes internally, which is the reason the\n\t\t// declaration wildcards rather than enumerating.\n\t\tconst [surface] = this.declare().filter(\n\t\t\t(d): d is Extract<Declaration, { kind: 'rest-api' }> =>\n\t\t\t\td.kind === 'rest-api',\n\t\t);\n\n\t\tfor (const endpoint of surface?.endpoints ?? []) {\n\t\t\tconst methods =\n\t\t\t\tendpoint.method === 'ANY'\n\t\t\t\t\t? (['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] as const)\n\t\t\t\t\t: ([endpoint.method] as const);\n\n\t\t\tapp.on([...methods], endpoint.path, (c) => server.handler(c.req.raw));\n\t\t}\n\n\t\treturn { app };\n\t}\n\n\tprivate async connect(\n\t\toptions: ServiceRegisterOptions,\n\t\tas: { owner?: boolean } = {},\n\t): Promise<AuthServer> {\n\t\tconst { secret, baseUrl, trustedOrigins, cookieDomain } = options.envParser\n\t\t\t.create((get) => ({\n\t\t\t\tsecret: get(this.keys.secret).string(),\n\t\t\t\t// The surface's own URL, resolved by the target — not guessed from\n\t\t\t\t// a port, which was wrong the moment the server moved to 3001.\n\t\t\t\tbaseUrl: get(this.keys.url).string(),\n\t\t\t\t// Better Auth's CSRF check applies to every caller, not just\n\t\t\t\t// browsers, so an API calling the auth server is rejected unless\n\t\t\t\t// its origin is trusted. The list is derived from the graph and\n\t\t\t\t// arrives as one comma-separated value, for the same reason every\n\t\t\t\t// other derived value arrives as a string: it crosses a process\n\t\t\t\t// boundary as env.\n\t\t\t\ttrustedOrigins: get(this.keys.trustedOrigins)\n\t\t\t\t\t.string()\n\t\t\t\t\t.default('')\n\t\t\t\t\t.transform((value) =>\n\t\t\t\t\t\tvalue\n\t\t\t\t\t\t\t.split(',')\n\t\t\t\t\t\t\t.map((origin) => origin.trim())\n\t\t\t\t\t\t\t.filter(Boolean),\n\t\t\t\t\t),\n\t\t\t\t// The domain a session cookie has to carry to be readable by a\n\t\t\t\t// frontend on a sibling host. Optional because there is often\n\t\t\t\t// nothing to widen to: locally everything shares `localhost`,\n\t\t\t\t// where cookies ignore the port and a `Domain` would only be a\n\t\t\t\t// value the browser refuses.\n\t\t\t\tcookieDomain: get(this.keys.cookieDomain).string().optional(),\n\t\t\t}))\n\t\t\t.parse();\n\n\t\t// The tenant's own client: one construct, one connection, so what auth\n\t\t// writes and what the browser inspects cannot be two different databases.\n\t\t// A migration asks the tenant for its owner connection, which the tenant\n\t\t// exposes as a `Service` and not as anything `.dependsOn()` would take.\n\t\tconst tenant = this.config.database as Consumable & {\n\t\t\towner?: Service<string, unknown>;\n\t\t};\n\t\tconst source = as.owner && tenant.owner ? tenant.owner : tenant.service;\n\n\t\tconst db = (await source.register(options)) as Kysely<\n\t\t\tRecord<string, never>\n\t\t>;\n\n\t\t// Hoisted: narrowing a property of `this` is not preserved across the\n\t\t// await, and the false branch is the plain-object form.\n\t\tconst configure = this.config.options;\n\t\tconst configured: BetterAuthOptions =\n\t\t\ttypeof configure === 'function'\n\t\t\t\t? await configure(options)\n\t\t\t\t: (configure ?? {});\n\n\t\t// `Auth<O>` is invariant in its options in better-auth 1.7, so the value\n\t\t// built from a concrete literal is not assignable to the `AuthServer`\n\t\t// alias, which names the constraint. The runtime object is the same one\n\t\t// either way; the assertion keeps the variance where it happens instead\n\t\t// of spreading a generic through the construct's public type.\n\t\treturn betterAuth({\n\t\t\t...configured,\n\t\t\tsecret,\n\t\t\tbaseURL: baseUrl,\n\t\t\tbasePath: this.basePath,\n\t\t\tdatabase: { db, type: 'postgres' },\n\t\t\t// Whatever the app added wins over the derived list rather than\n\t\t\t// replacing it: an origin nobody declared is still sometimes real.\n\t\t\ttrustedOrigins: [\n\t\t\t\t...trustedOrigins,\n\t\t\t\t...(configured.trustedOrigins &&\n\t\t\t\tArray.isArray(configured.trustedOrigins)\n\t\t\t\t\t? configured.trustedOrigins\n\t\t\t\t\t: []),\n\t\t\t],\n\t\t\tadvanced: {\n\t\t\t\t...configured.advanced,\n\t\t\t\t// Only when a domain was derived. Better Auth reads the presence\n\t\t\t\t// of this block as intent, so enabling it with no domain would\n\t\t\t\t// widen the cookie to whatever host happened to set it — and the\n\t\t\t\t// case with nothing to widen to is the common one, not an error.\n\t\t\t\t...(cookieDomain\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tcrossSubDomainCookies: {\n\t\t\t\t\t\t\t\tenabled: true,\n\t\t\t\t\t\t\t\tdomain: cookieDomain,\n\t\t\t\t\t\t\t\t...configured.advanced?.crossSubDomainCookies,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}\n\t\t\t\t\t: {}),\n\t\t\t},\n\t\t}) as AuthServer;\n\t}\n}\n\n/** The auth server answered a session check with something other than 200. */\nexport class SessionCheckFailed extends Error {\n\tconstructor(\n\t\treadonly authenticator: string,\n\t\treadonly status: number,\n\t) {\n\t\tsuper(\n\t\t\t`'${authenticator}' answered a session check with ${status}. ` +\n\t\t\t\t`A request with no session gets 200 and null; this is the server ` +\n\t\t\t\t`failing — check that it is running and reachable at its URL.`,\n\t\t);\n\t\tthis.name = 'SessionCheckFailed';\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"auth.cjs","names":["canonicalId","environmentCase","provideKey","serviceKey","edgeTo","betterAuth"],"sources":["../src/auth.ts"],"sourcesContent":["/**\n * `BetterAuth` — a declared auth server.\n *\n * It is three things at once, which is what makes it the construct that\n * exercises most of the model: a consumer of a database, a producer of an\n * authorizer, and a set of endpoints. This lands the first two.\n *\n * Its tables live in a schema tenant it is *given* — `orders.schema('AuthDb')`\n * — rather than one it invents. The tenant declares the schema and the role;\n * this declares the one thing left, the signing secret, and reads back the keys\n * it needs.\n *\n * :::caution\n * The isolation that arrangement is *for* — the application's own role holding\n * no grant on these tables, so a compromised handler cannot read sessions — does\n * **not hold yet**. No target creates the per-schema role: both connect as the\n * database owner, so today the tenant is a `search_path`, which is a namespace\n * and not a privilege boundary. See \"Roles are declared and never created\" in\n * `docs/design/constructs-outstanding.md`.\n * :::\n *\n * It declares its own surface, so where it answers, who may call it, and the\n * domain its cookies are scoped to all arrive as environment resolved by the\n * target — none of them guessed from a port, and none of them listed by hand in\n * application config.\n */\n\nimport type { EnvironmentParser } from '@geekmidas/envkit';\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\ttype Declaration,\n\tenvironmentCase,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type { Service, ServiceRegisterOptions } from '@geekmidas/services';\nimport { betterAuth } from 'better-auth';\nimport type { Hono } from 'hono';\nimport type { Kysely } from 'kysely';\nimport {\n\ttype Authenticator,\n\ttype Construct,\n\ttype Consumable,\n\tedgeTo,\n} from './construct-interface';\n\n/** The server better-auth hands back. */\nexport type AuthServer = ReturnType<typeof betterAuth>;\n\n/** A signed-in session as better-auth hands it back: `{ user, session }`. */\nexport type AuthSession = AuthServer['$Infer']['Session'];\n\n/** Everything better-auth takes, minus what the construct owns. */\nexport type BetterAuthOptions = Omit<\n\tParameters<typeof betterAuth>[0],\n\t'database' | 'secret' | 'basePath' | 'baseURL'\n>;\n\nexport interface BetterAuthConfig<TDatabase extends Consumable> {\n\t/**\n\t * The schema tenant its tables live in.\n\t *\n\t * A construct rather than a URL: the tenant already declares its schema and\n\t * its role, and taking the declaration is what puts the edge in the graph\n\t * instead of a connection string in two places.\n\t */\n\tdatabase: TDatabase;\n\t/**\n\t * The app that serves this auth server, relative to the workspace root —\n\t * `'apps/auth'`.\n\t *\n\t * Required, for the reason `RestApi.path` is. Not to be confused with\n\t * `basePath`, which is where its routes are mounted in a URL.\n\t */\n\tpath: string;\n\t/**\n\t * Where the auth routes are mounted, e.g. `/api/auth`.\n\t *\n\t * Structural — it is part of the URL every client calls, so it cannot differ\n\t * between stages the way the host can.\n\t */\n\tbasePath?: string;\n\t/**\n\t * The rest of better-auth's options: providers, plugins, email settings.\n\t *\n\t * A function when they need another construct — a magic-link plugin has to\n\t * *send* the link, and the thing that sends mail is a construct whose client\n\t * has to be registered. It is handed the same `ServiceRegisterOptions` this\n\t * construct was, so the env parser flows through the graph rather than being\n\t * imported out of application config.\n\t */\n\toptions?:\n\t\t| BetterAuthOptions\n\t\t| ((\n\t\t\t\toptions: ServiceRegisterOptions,\n\t\t ) => BetterAuthOptions | Promise<BetterAuthOptions>);\n}\n\nconst DEFAULT_BASE_PATH = '/api/auth';\n\nexport class BetterAuth<\n\tTName extends string = string,\n\tTDatabase extends Consumable = Consumable,\n> implements Construct<TName, AuthServer>, Authenticator<AuthSession>\n{\n\treadonly id: TName;\n\treadonly service: Service<Uncapitalize<TName>, AuthServer>;\n\treadonly basePath: string;\n\n\t/**\n\t * Declared once and read by both `declare()` and `connect()`, so the keys the\n\t * target publishes and the keys the server reads cannot drift.\n\t */\n\tprivate readonly keys: {\n\t\tsecretId: string;\n\t\tsecret: string;\n\t\turl: string;\n\t\ttrustedOrigins: string;\n\t\tcookieDomain: string;\n\t};\n\n\tconstructor(\n\t\tid: ConstructName<TName>,\n\t\tprivate readonly config: BetterAuthConfig<TDatabase>,\n\t) {\n\t\tconst canonical = canonicalId(id as string);\n\n\t\tthis.id = canonical as TName;\n\t\tthis.basePath = config.basePath ?? DEFAULT_BASE_PATH;\n\n\t\t// A secret's name is its key: `Auth` signs with `AUTH_SECRET`, which is\n\t\t// also what better-auth's own tooling looks for.\n\t\tconst secretId = `${canonical}Secret`;\n\t\tthis.keys = {\n\t\t\tsecretId,\n\t\t\tsecret: environmentCase(secretId),\n\t\t\turl: provideKey(canonical, 'url'),\n\t\t\ttrustedOrigins: provideKey(canonical, 'trustedOrigins'),\n\t\t\tcookieDomain: provideKey(canonical, 'cookieDomain'),\n\t\t};\n\n\t\t// A field, not a getter: consumers cache services by object identity.\n\t\tthis.service = {\n\t\t\tserviceName: serviceKey(canonical) as Uncapitalize<TName>,\n\t\t\tregister: (options) => this.connect(options),\n\t\t};\n\t}\n\n\t/**\n\t * A signing secret and a surface.\n\t *\n\t * The database is *not* declared here — the tenant that was passed in\n\t * declares it, and declaring it twice is the duplication the whole model\n\t * removes. What is declared is what this construct owns: the key it signs\n\t * with, and the routes it answers on.\n\t *\n\t * One endpoint, wildcarded. Better Auth routes internally and the set of\n\t * paths depends on which capabilities are enabled, so enumerating them here\n\t * would be a copy of its router that goes stale on its next release. The\n\t * surface's job is to send everything under `basePath` to one handler, which\n\t * is exactly what the declaration says.\n\t */\n\tdeclare(): Declaration[] {\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: 'secret',\n\t\t\t\tid: this.keys.secretId,\n\t\t\t\tprovides: [this.keys.secret],\n\t\t\t},\n\t\t\t{\n\t\t\t\tkind: 'rest-api',\n\t\t\t\tid: this.id,\n\t\t\t\tpath: this.config.path,\n\t\t\t\tprovides: [\n\t\t\t\t\tthis.keys.url,\n\t\t\t\t\tthis.keys.trustedOrigins,\n\t\t\t\t\tthis.keys.cookieDomain,\n\t\t\t\t],\n\t\t\t\tendpoints: [\n\t\t\t\t\t{\n\t\t\t\t\t\tid: `${this.id}Handler`,\n\t\t\t\t\t\thandler: `${this.id}.handler`,\n\t\t\t\t\t\tmethod: 'ANY',\n\t\t\t\t\t\tpath: `${this.basePath}/*`,\n\t\t\t\t\t\t// Read off the tenant rather than written down: this\n\t\t\t\t\t\t// construct takes whatever database it is given, and a\n\t\t\t\t\t\t// hardcoded kind is a second statement of a fact the\n\t\t\t\t\t\t// tenant already makes.\n\t\t\t\t\t\tdependencies: [edgeTo(this.config.database)],\n\t\t\t\t\t\trequires: [this.keys.secret],\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t];\n\t}\n\n\t/**\n\t * Whose request this is: the session for these headers, or `null`.\n\t *\n\t * Asked of the auth server over HTTP, at the URL the `.auth()` edge injects —\n\t * so it is the same call whether the server runs in this process, beside\n\t * it, or behind MSW in a test. Only the session headers are forwarded; the\n\t * surface decides which.\n\t */\n\tasync verify(\n\t\theaders: Headers,\n\t\tenvParser: EnvironmentParser<{}>,\n\t): Promise<AuthSession | null> {\n\t\tconst { url } = envParser\n\t\t\t.create((get) => ({ url: get(this.keys.url).string() }))\n\t\t\t.parse();\n\n\t\tconst response = await fetch(`${url}${this.basePath}/get-session`, {\n\t\t\theaders,\n\t\t});\n\n\t\t// Better Auth answers `null` for a request with no session. Anything\n\t\t// other than a 200 is the server failing, which is not the same thing as\n\t\t// \"signed out\" and must not be read as it.\n\t\tif (!response.ok) {\n\t\t\tthrow new SessionCheckFailed(this.id, response.status);\n\t\t}\n\n\t\treturn (await response.json()) as AuthSession | null;\n\t}\n\n\t/**\n\t * Better Auth's own migrations, for the app's migrate step.\n\t *\n\t * It brings its own schema — users, sessions, accounts, verifications — so\n\t * the app never writes those tables and never gets to drift from them.\n\t */\n\tasync migrations(\n\t\toptions: ServiceRegisterOptions,\n\t): Promise<() => Promise<void>> {\n\t\t// As the owner, not the runtime role. Creating tables is DDL, and the\n\t\t// whole point of the split is that a handler's role cannot do it — so a\n\t\t// migrator that connected the way a handler does would fail on the first\n\t\t// `CREATE TABLE` rather than working by accident.\n\t\t//\n\t\t// The options, not a server built from them: better-auth checks its\n\t\t// schema when a server starts, and before the migration that creates the\n\t\t// tables it can only report them missing — an error in every migrate\n\t\t// log, about the very tables the next line creates.\n\t\tconst resolved = await this.optionsFor(options, { owner: true });\n\t\t// better-auth 1.7 split the migration builder out of `better-auth/db`\n\t\t// into its own entry, so importing it no longer drags the whole db layer\n\t\t// in behind it.\n\t\tconst { getMigrations } = await import('better-auth/db/migration');\n\t\tconst { runMigrations } = await getMigrations(resolved);\n\n\t\treturn runMigrations;\n\t}\n\n\t/**\n\t * The server this surface runs as, built from its own declaration.\n\t *\n\t * A surface is a deploy unit, so something has to serve it, and the thing\n\t * that knows how is the construct — not a generator writing a mount into\n\t * somebody else's entry point. That is how the auth server came to be\n\t * mounted by a hook in the application it was supposed to be separate from,\n\t * and then to be served by nothing at all when the hook was deleted.\n\t *\n\t * The routes come from {@link declare}: one wildcard under `basePath`, read\n\t * off the same declaration the manifest carries, so the surface the graph\n\t * describes and the surface that answers requests cannot disagree.\n\t *\n\t * ```ts\n\t * // .gkm/server/app.ts — the whole generated entry\n\t * import { auth } from '@acme/constructs/auth.js';\n\t * export const { app } = await auth.server({ envParser });\n\t * ```\n\t */\n\tasync server(options: ServiceRegisterOptions): Promise<{ app: Hono }> {\n\t\tconst { Hono } = await import('hono');\n\t\tconst { cors } = await import('hono/cors');\n\t\tconst app = new Hono();\n\t\tconst server = await this.connect(options);\n\n\t\t// Who may call this surface, read off the graph. The same list Better\n\t\t// Auth checks for CSRF — it arrives as `<ID>_TRUSTED_ORIGINS`, composed\n\t\t// by the target from whatever declared an edge to this construct, so a\n\t\t// site that depends on the auth server is allowed to reach it and\n\t\t// nothing else is. Nobody writes an origin down.\n\t\tconst { origins } = options.envParser\n\t\t\t.create((get) => ({\n\t\t\t\torigins: get(this.keys.trustedOrigins)\n\t\t\t\t\t.string()\n\t\t\t\t\t.default('')\n\t\t\t\t\t.transform((value: string) =>\n\t\t\t\t\t\tvalue\n\t\t\t\t\t\t\t.split(',')\n\t\t\t\t\t\t\t.map((origin) => origin.trim())\n\t\t\t\t\t\t\t.filter(Boolean),\n\t\t\t\t\t),\n\t\t\t}))\n\t\t\t.parse();\n\n\t\tapp.use('*', cors({ origin: origins, credentials: true, maxAge: 3600 }));\n\n\t\t// Read off the declaration rather than restated here. `ANY` is every\n\t\t// method Better Auth routes internally, which is the reason the\n\t\t// declaration wildcards rather than enumerating.\n\t\tconst [surface] = this.declare().filter(\n\t\t\t(d): d is Extract<Declaration, { kind: 'rest-api' }> =>\n\t\t\t\td.kind === 'rest-api',\n\t\t);\n\n\t\tfor (const endpoint of surface?.endpoints ?? []) {\n\t\t\tconst methods =\n\t\t\t\tendpoint.method === 'ANY'\n\t\t\t\t\t? (['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] as const)\n\t\t\t\t\t: ([endpoint.method] as const);\n\n\t\t\tapp.on([...methods], endpoint.path, (c) => server.handler(c.req.raw));\n\t\t}\n\n\t\treturn { app };\n\t}\n\n\t/**\n\t * The ids of the better-auth plugins this server runs — what a client has\n\t * to pair its own plugins with (`magic-link` with `magicLinkClient`).\n\t * Resolved the way the server resolves them, options callback included.\n\t */\n\tasync pluginIds(options: ServiceRegisterOptions): Promise<string[]> {\n\t\tconst { plugins = [] } = await this.configured(options);\n\t\treturn plugins.map(({ id }) => id);\n\t}\n\n\t/** Better-auth's options as given — a value, or a callback of the environment. */\n\tprivate async configured(\n\t\toptions: ServiceRegisterOptions,\n\t): Promise<BetterAuthOptions> {\n\t\t// Hoisted: narrowing a property of `this` is not preserved across the\n\t\t// await, and the false branch is the plain-object form.\n\t\tconst configure = this.config.options;\n\t\treturn typeof configure === 'function'\n\t\t\t? await configure(options)\n\t\t\t: (configure ?? {});\n\t}\n\n\tprivate async connect(\n\t\toptions: ServiceRegisterOptions,\n\t\tas: { owner?: boolean } = {},\n\t): Promise<AuthServer> {\n\t\t// `Auth<O>` is invariant in its options in better-auth 1.7, so the value\n\t\t// built from a concrete literal is not assignable to the `AuthServer`\n\t\t// alias, which names the constraint. The runtime object is the same one\n\t\t// either way; the assertion keeps the variance where it happens instead\n\t\t// of spreading a generic through the construct's public type.\n\t\treturn betterAuth(await this.optionsFor(options, as)) as AuthServer;\n\t}\n\n\t/**\n\t * Everything better-auth is given: the app's options, and what the construct\n\t * owns — its secret, its URL, its tenant's connection, the origins and cookie\n\t * domain its graph derives. One place, so the server and its migrations are\n\t * built from the same options.\n\t */\n\tprivate async optionsFor(\n\t\toptions: ServiceRegisterOptions,\n\t\tas: { owner?: boolean } = {},\n\t): Promise<Parameters<typeof betterAuth>[0]> {\n\t\tconst { secret, baseUrl, trustedOrigins, cookieDomain } = options.envParser\n\t\t\t.create((get) => ({\n\t\t\t\tsecret: get(this.keys.secret).string(),\n\t\t\t\t// The surface's own URL, resolved by the target — not guessed from\n\t\t\t\t// a port, which was wrong the moment the server moved to 3001.\n\t\t\t\tbaseUrl: get(this.keys.url).string(),\n\t\t\t\t// Better Auth's CSRF check applies to every caller, not just\n\t\t\t\t// browsers, so an API calling the auth server is rejected unless\n\t\t\t\t// its origin is trusted. The list is derived from the graph and\n\t\t\t\t// arrives as one comma-separated value, for the same reason every\n\t\t\t\t// other derived value arrives as a string: it crosses a process\n\t\t\t\t// boundary as env.\n\t\t\t\ttrustedOrigins: get(this.keys.trustedOrigins)\n\t\t\t\t\t.string()\n\t\t\t\t\t.default('')\n\t\t\t\t\t.transform((value) =>\n\t\t\t\t\t\tvalue\n\t\t\t\t\t\t\t.split(',')\n\t\t\t\t\t\t\t.map((origin) => origin.trim())\n\t\t\t\t\t\t\t.filter(Boolean),\n\t\t\t\t\t),\n\t\t\t\t// The domain a session cookie has to carry to be readable by a\n\t\t\t\t// frontend on a sibling host. Optional because there is often\n\t\t\t\t// nothing to widen to: locally everything shares `localhost`,\n\t\t\t\t// where cookies ignore the port and a `Domain` would only be a\n\t\t\t\t// value the browser refuses.\n\t\t\t\tcookieDomain: get(this.keys.cookieDomain).string().optional(),\n\t\t\t}))\n\t\t\t.parse();\n\n\t\t// The tenant's own client: one construct, one connection, so what auth\n\t\t// writes and what the browser inspects cannot be two different databases.\n\t\t// A migration asks the tenant for its owner connection, which the tenant\n\t\t// exposes as a `Service` and not as anything `.dependsOn()` would take.\n\t\tconst tenant = this.config.database as Consumable & {\n\t\t\towner?: Service<string, unknown>;\n\t\t};\n\t\tconst source = as.owner && tenant.owner ? tenant.owner : tenant.service;\n\n\t\tconst db = (await source.register(options)) as Kysely<\n\t\t\tRecord<string, never>\n\t\t>;\n\n\t\tconst configured = await this.configured(options);\n\n\t\treturn {\n\t\t\t...configured,\n\t\t\tsecret,\n\t\t\tbaseURL: baseUrl,\n\t\t\tbasePath: this.basePath,\n\t\t\tdatabase: { db, type: 'postgres' },\n\t\t\t// Whatever the app added wins over the derived list rather than\n\t\t\t// replacing it: an origin nobody declared is still sometimes real.\n\t\t\ttrustedOrigins: [\n\t\t\t\t...trustedOrigins,\n\t\t\t\t...(configured.trustedOrigins &&\n\t\t\t\tArray.isArray(configured.trustedOrigins)\n\t\t\t\t\t? configured.trustedOrigins\n\t\t\t\t\t: []),\n\t\t\t],\n\t\t\tadvanced: {\n\t\t\t\t...configured.advanced,\n\t\t\t\t// Only when a domain was derived. Better Auth reads the presence\n\t\t\t\t// of this block as intent, so enabling it with no domain would\n\t\t\t\t// widen the cookie to whatever host happened to set it — and the\n\t\t\t\t// case with nothing to widen to is the common one, not an error.\n\t\t\t\t...(cookieDomain\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tcrossSubDomainCookies: {\n\t\t\t\t\t\t\t\tenabled: true,\n\t\t\t\t\t\t\t\tdomain: cookieDomain,\n\t\t\t\t\t\t\t\t...configured.advanced?.crossSubDomainCookies,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}\n\t\t\t\t\t: {}),\n\t\t\t},\n\t\t};\n\t}\n}\n\n/** The auth server answered a session check with something other than 200. */\nexport class SessionCheckFailed extends Error {\n\tconstructor(\n\t\treadonly authenticator: string,\n\t\treadonly status: number,\n\t) {\n\t\tsuper(\n\t\t\t`'${authenticator}' answered a session check with ${status}. ` +\n\t\t\t\t`A request with no session gets 200 and null; this is the server ` +\n\t\t\t\t`failing — check that it is running and reachable at its URL.`,\n\t\t);\n\t\tthis.name = 'SessionCheckFailed';\n\t}\n}\n"],"mappings":"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"}
|
package/dist/auth.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as BetterAuthOptions, i as BetterAuthConfig, n as AuthSession, o as SessionCheckFailed, r as BetterAuth, t as AuthServer } from "./auth-
|
|
1
|
+
import { a as BetterAuthOptions, i as BetterAuthConfig, n as AuthSession, o as SessionCheckFailed, r as BetterAuth, t as AuthServer } from "./auth-BR2eYxIW.cjs";
|
|
2
2
|
export { AuthServer, AuthSession, BetterAuth, BetterAuthConfig, BetterAuthOptions, SessionCheckFailed };
|
package/dist/auth.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as BetterAuthOptions, i as BetterAuthConfig, n as AuthSession, o as SessionCheckFailed, r as BetterAuth, t as AuthServer } from "./auth-
|
|
1
|
+
import { a as BetterAuthOptions, i as BetterAuthConfig, n as AuthSession, o as SessionCheckFailed, r as BetterAuth, t as AuthServer } from "./auth-DtprqoJP.mjs";
|
|
2
2
|
export { AuthServer, AuthSession, BetterAuth, BetterAuthConfig, BetterAuthOptions, SessionCheckFailed };
|