alepha 0.20.4 → 0.20.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/audits/index.d.ts +391 -359
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +23 -1
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.d.ts +18 -0
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +51 -0
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.browser.js +33 -14
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +452 -155
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +474 -159
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +32 -4
- package/dist/api/keys/index.d.ts.map +1 -1
- package/dist/api/keys/index.js +53 -0
- package/dist/api/keys/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +29 -1
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +55 -13
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/organizations/index.js.map +1 -1
- package/dist/api/parameters/index.d.ts +15 -0
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +37 -0
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/users/index.d.ts +150 -9
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +237 -28
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +3 -3
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/batch/index.js.map +1 -1
- package/dist/bin/index.js +0 -0
- package/dist/bucket/index.d.ts +18 -0
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +47 -0
- package/dist/bucket/index.js.map +1 -1
- package/dist/bucket/index.workerd.js +24 -0
- package/dist/bucket/index.workerd.js.map +1 -1
- package/dist/cache/core/index.d.ts +20 -3
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/core/index.workerd.js.map +1 -1
- package/dist/cache/database/index.d.ts +155 -0
- package/dist/cache/database/index.d.ts.map +1 -0
- package/dist/cache/database/index.js +266 -0
- package/dist/cache/database/index.js.map +1 -0
- package/dist/cache/redis/index.js.map +1 -1
- package/dist/captcha/index.js.map +1 -1
- package/dist/cli/config/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +35 -5
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +85 -6
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/platform/index.js +1 -1
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/crypto/index.browser.js.map +1 -1
- package/dist/crypto/index.js.map +1 -1
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/brevo/index.js.map +1 -1
- package/dist/email/core/index.js.map +1 -1
- package/dist/email/core/index.workerd.js.map +1 -1
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/fake/index.js.map +1 -1
- package/dist/lock/core/index.js.map +1 -1
- package/dist/lock/redis/index.js.map +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.js.map +1 -1
- package/dist/queue/core/index.js.map +1 -1
- package/dist/queue/core/index.workerd.js.map +1 -1
- package/dist/queue/redis/index.js.map +1 -1
- package/dist/react/auth/index.browser.js.map +1 -1
- package/dist/react/auth/index.js.map +1 -1
- package/dist/react/core/index.js.map +1 -1
- package/dist/react/form/index.js +2 -0
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/head/index.browser.js.map +1 -1
- package/dist/react/head/index.js.map +1 -1
- package/dist/react/i18n/index.js.map +1 -1
- package/dist/react/intro/index.js.map +1 -1
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/testing/index.js.map +1 -1
- package/dist/react/ui/index.js.map +1 -1
- package/dist/react/websocket/index.js.map +1 -1
- package/dist/redis/index.bun.js.map +1 -1
- package/dist/redis/index.js.map +1 -1
- package/dist/retry/index.js.map +1 -1
- package/dist/router/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +22 -0
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +12 -0
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/index.workerd.js +12 -0
- package/dist/scheduler/index.workerd.js.map +1 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cookies/index.browser.js.map +1 -1
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.browser.js.map +1 -1
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.js.map +1 -1
- package/dist/server/etag/index.js.map +1 -1
- package/dist/server/health/index.js.map +1 -1
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/proxy/index.js.map +1 -1
- package/dist/server/rate-limit/index.js.map +1 -1
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js.map +1 -1
- package/dist/system/index.js.map +1 -1
- package/dist/system/index.workerd.js.map +1 -1
- package/dist/topic/core/index.js.map +1 -1
- package/dist/topic/redis/index.js.map +1 -1
- package/dist/websocket/index.browser.js +4 -0
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.js +10 -0
- package/dist/websocket/index.js.map +1 -1
- package/package.json +282 -272
- package/src/api/audits/controllers/AdminAuditController.ts +29 -0
- package/src/api/files/controllers/FileController.ts +24 -0
- package/src/api/files/services/FileService.ts +41 -0
- package/src/api/jobs/__tests__/$job.spec.ts +427 -2
- package/src/api/jobs/entities/jobExecutionEntity.ts +3 -3
- package/src/api/jobs/index.ts +47 -10
- package/src/api/jobs/primitives/$job.ts +22 -9
- package/src/api/jobs/providers/DirectJobDispatcher.ts +71 -0
- package/src/api/jobs/providers/JobDispatcher.ts +49 -0
- package/src/api/jobs/providers/JobProvider.ts +365 -142
- package/src/api/jobs/providers/JobQueueProvider.ts +43 -18
- package/src/api/jobs/schemas/jobConfigAtom.ts +4 -3
- package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +11 -0
- package/src/api/jobs/schemas/jobRegistrationSchema.ts +4 -2
- package/src/api/jobs/services/JobService.ts +21 -11
- package/src/api/keys/controllers/AdminApiKeyController.ts +23 -0
- package/src/api/keys/services/ApiKeyService.ts +42 -0
- package/src/api/notifications/__tests__/AlephaApiNotifications.spec.ts +63 -0
- package/src/api/notifications/controllers/AdminNotificationController.ts +48 -1
- package/src/api/notifications/index.ts +13 -3
- package/src/api/notifications/jobs/NotificationJobs.ts +0 -6
- package/src/api/parameters/controllers/AdminParameterController.ts +26 -0
- package/src/api/parameters/services/ParameterProvider.ts +18 -0
- package/src/api/users/__tests__/Registration-emailMode.spec.ts +203 -0
- package/src/api/users/__tests__/UsernameSlugger.spec.ts +138 -0
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +41 -3
- package/src/api/users/controllers/AdminSessionController.ts +29 -0
- package/src/api/users/controllers/AdminUserController.ts +32 -0
- package/src/api/users/index.ts +3 -0
- package/src/api/users/services/CredentialService.ts +5 -0
- package/src/api/users/services/RegistrationService.ts +49 -1
- package/src/api/users/services/SessionCrudService.ts +16 -0
- package/src/api/users/services/SessionService.ts +17 -59
- package/src/api/users/services/UsernameSlugger.ts +195 -0
- package/src/bucket/primitives/$bucket.ts +21 -0
- package/src/bucket/providers/CloudflareR2Provider.ts +15 -0
- package/src/bucket/providers/FileStorageProvider.ts +9 -0
- package/src/bucket/providers/LocalFileStorageProvider.ts +14 -0
- package/src/bucket/providers/MemoryFileStorageProvider.ts +9 -0
- package/src/bucket/providers/NodeS3BucketProvider.ts +35 -0
- package/src/cache/core/primitives/$cache.ts +20 -3
- package/src/cache/database/__tests__/DatabaseCacheProvider.behavior.spec.ts +203 -0
- package/src/cache/database/__tests__/DatabaseCacheProvider.spec.ts +110 -0
- package/src/cache/database/entities/cacheEntries.ts +55 -0
- package/src/cache/database/index.ts +36 -0
- package/src/cache/database/providers/DatabaseCacheProvider.ts +348 -0
- package/src/cli/core/services/ProjectScaffolder.ts +0 -2
- package/src/cli/core/tasks/BuildCloudflareTask.ts +17 -3
- package/src/cli/core/tasks/BuildSitemapTask.ts +7 -0
- package/src/cli/core/tasks/BuildVercelTask.ts +82 -3
- package/src/cli/platform/__tests__/detectResources.spec.ts +96 -0
- package/src/cli/platform/commands/platform.ts +7 -1
- package/src/scheduler/index.ts +14 -0
- package/src/scheduler/providers/CronProvider.ts +13 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/server/cors/providers/ServerCorsProvider.ts","../../../src/server/cors/primitives/$cors.ts","../../../src/server/cors/index.ts"],"sourcesContent":["import { $atom, $hook, $inject, $state, type Static, t } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { ServerRouterProvider } from \"alepha/server\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * CORS configuration atom (global defaults)\n */\nexport const corsOptions = $atom({\n name: \"alepha.server.cors.options\",\n schema: t.object({\n origin: t.optional(\n t.string({\n description:\n \"Allowed origins (* for all, string for single, comma-separated for multiple)\",\n default: \"*\",\n }),\n ),\n methods: t.array(t.string(), {\n description: \"Allowed HTTP methods\",\n default: [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"OPTIONS\"],\n }),\n headers: t.array(t.string(), {\n description: \"Allowed headers\",\n default: [\"Content-Type\", \"Authorization\"],\n }),\n credentials: t.optional(\n t.boolean({\n description: \"Allow credentials\",\n default: false,\n }),\n ),\n maxAge: t.optional(\n t.number({\n description: \"Preflight cache duration in seconds\",\n }),\n ),\n }),\n default: {\n origin: \"*\",\n methods: [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"OPTIONS\"],\n headers: [\"Content-Type\", \"Authorization\"],\n credentials: false,\n },\n});\n\nexport type CorsOptions = Static<typeof corsOptions.schema>;\n\ndeclare module \"alepha\" {\n interface State {\n [corsOptions.key]: CorsOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface CorsRegistration extends Partial<CorsOptions> {\n /**\n * Name identifier for this CORS config.\n */\n name?: string;\n /**\n * Path patterns to match (supports wildcards like /api/*).\n */\n paths?: string[];\n}\n\nexport class ServerCorsProvider {\n protected readonly log = $logger();\n protected readonly serverRouterProvider = $inject(ServerRouterProvider);\n protected readonly globalOptions = $state(corsOptions);\n\n /**\n * Registered CORS configurations with their path patterns\n */\n public readonly registeredConfigs: CorsRegistration[] = [];\n\n /**\n * Register a CORS configuration (called by primitives)\n */\n public registerCors(config: CorsRegistration): void {\n this.registeredConfigs.push(config);\n }\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n // Apply path-specific CORS configs to routes\n for (const config of this.registeredConfigs) {\n if (config.paths) {\n for (const pattern of config.paths) {\n const matchedRoutes = this.serverRouterProvider.getRoutes(pattern);\n for (const route of matchedRoutes) {\n route.cors = this.buildCorsOptions(config);\n }\n }\n }\n }\n\n if (this.registeredConfigs.length > 0) {\n this.log.info(\n `Initialized with ${this.registeredConfigs.length} registered CORS configurations.`,\n );\n }\n },\n });\n\n /**\n * Build complete CORS options by merging with global defaults\n */\n public buildCorsOptions(config: Partial<CorsOptions>): CorsOptions {\n return {\n origin: config.origin ?? this.globalOptions.origin,\n methods: config.methods ?? this.globalOptions.methods,\n headers: config.headers ?? this.globalOptions.headers,\n credentials: config.credentials ?? this.globalOptions.credentials,\n maxAge: config.maxAge ?? this.globalOptions.maxAge,\n };\n }\n\n /**\n * Apply CORS headers to the response\n */\n public applyCorsHeaders(\n request: {\n headers: { origin?: string };\n reply: { setHeader: (name: string, value: string) => void };\n },\n options: CorsOptions,\n ): void {\n const reqOrigin = request.headers.origin;\n const { origin, methods, headers, credentials, maxAge } = options;\n\n if (reqOrigin && this.isOriginAllowed(reqOrigin, origin)) {\n request.reply.setHeader(\"Access-Control-Allow-Origin\", reqOrigin);\n }\n\n if (credentials) {\n request.reply.setHeader(\"Access-Control-Allow-Credentials\", \"true\");\n }\n\n request.reply.setHeader(\"Access-Control-Allow-Methods\", methods.join(\", \"));\n request.reply.setHeader(\"Access-Control-Allow-Headers\", headers.join(\", \"));\n\n if (maxAge != null) {\n request.reply.setHeader(\"Access-Control-Max-Age\", String(maxAge));\n }\n }\n\n protected readonly configure = $hook({\n on: \"start\",\n handler: () => {\n const routes = this.serverRouterProvider.getRoutes();\n for (const route of routes) {\n if (\n !route.method ||\n route.method === \"GET\" ||\n route.method === \"OPTIONS\"\n ) {\n continue;\n }\n\n this.serverRouterProvider.createRoute({\n path: route.path,\n method: \"OPTIONS\",\n handler: ({ reply }) => {\n reply.setStatus(204);\n },\n });\n }\n },\n });\n\n protected readonly onRequest = $hook({\n on: \"server:onRequest\",\n handler: ({ route, request }) => {\n // Use route-specific CORS if defined, otherwise use global options\n const corsConfig = route.cors ?? this.globalOptions;\n this.applyCorsHeaders(request, corsConfig);\n },\n });\n\n public isOriginAllowed(\n origin: string | undefined,\n allowed: CorsOptions[\"origin\"],\n ): boolean {\n if (!allowed) return false;\n if (allowed === \"*\") return true;\n return allowed\n .split(\",\")\n .map((o) => o.trim())\n .includes(origin ?? \"\");\n }\n}\n\nexport type ServerCorsProviderOptions = CorsOptions;\n","import { AlephaError, createMiddleware, type Middleware } from \"alepha\";\nimport {\n type CorsOptions,\n ServerCorsProvider,\n} from \"../providers/ServerCorsProvider.ts\";\n\n/**\n * Middleware that applies CORS headers to the response and handles OPTIONS preflight.\n *\n * Reads the request from the ALS context and applies the configured\n * CORS headers via `ServerCorsProvider`. Options are merged with\n * global CORS defaults.\n *\n * For OPTIONS preflight requests, the middleware short-circuits with a 204 response\n * and skips the handler entirely.\n *\n * **Route middleware** — requires a request context (`$action`). Throws if used outside one.\n *\n * ```typescript\n * class ApiController {\n * getOrders = $action({\n * use: [$cors({ origin: \"https://app.example.com\", credentials: true })],\n * handler: async ({ query }) => { ... },\n * });\n * }\n * ```\n */\nexport const $cors = (options?: Partial<CorsOptions>): Middleware => {\n return createMiddleware({\n name: \"$cors\",\n options: options as unknown as Record<string, unknown>,\n handler: ({ alepha, next }) => {\n const corsProvider = alepha.inject(ServerCorsProvider);\n\n return async (...args) => {\n const request = alepha.get(\"alepha.http.request\");\n\n if (!request) {\n throw new AlephaError(\n \"$cors requires a request context (use inside $action)\",\n );\n }\n\n const corsConfig = corsProvider.buildCorsOptions(options ?? {});\n corsProvider.applyCorsHeaders(request, corsConfig);\n\n // OPTIONS preflight → respond immediately, skip handler\n if (request.method === \"OPTIONS\") {\n request.reply.setStatus(204);\n return;\n }\n\n return next(...args);\n };\n },\n });\n};\n","import { $module } from \"alepha\";\nimport {\n type CorsOptions,\n ServerCorsProvider,\n} from \"./providers/ServerCorsProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$cors.ts\";\nexport * from \"./providers/ServerCorsProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha/server\" {\n interface ServerRoute {\n /**\n * Route-specific CORS configuration.\n * If set, overrides the global CORS options for this route.\n */\n cors?: CorsOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Cross-Origin Resource Sharing configuration.\n *\n * **Features:**\n * - CORS policy definition\n *\n * @module alepha.server.cors\n */\nexport const AlephaServerCors = $module({\n name: \"alepha.server.cors\",\n services: [ServerCorsProvider],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/server/cors/providers/ServerCorsProvider.ts","../../../src/server/cors/primitives/$cors.ts","../../../src/server/cors/index.ts"],"sourcesContent":["import { $atom, $hook, $inject, $state, type Static, t } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { ServerRouterProvider } from \"alepha/server\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * CORS configuration atom (global defaults)\n */\nexport const corsOptions = $atom({\n name: \"alepha.server.cors.options\",\n schema: t.object({\n origin: t.optional(\n t.string({\n description:\n \"Allowed origins (* for all, string for single, comma-separated for multiple)\",\n default: \"*\",\n }),\n ),\n methods: t.array(t.string(), {\n description: \"Allowed HTTP methods\",\n default: [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"OPTIONS\"],\n }),\n headers: t.array(t.string(), {\n description: \"Allowed headers\",\n default: [\"Content-Type\", \"Authorization\"],\n }),\n credentials: t.optional(\n t.boolean({\n description: \"Allow credentials\",\n default: false,\n }),\n ),\n maxAge: t.optional(\n t.number({\n description: \"Preflight cache duration in seconds\",\n }),\n ),\n }),\n default: {\n origin: \"*\",\n methods: [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"OPTIONS\"],\n headers: [\"Content-Type\", \"Authorization\"],\n credentials: false,\n },\n});\n\nexport type CorsOptions = Static<typeof corsOptions.schema>;\n\ndeclare module \"alepha\" {\n interface State {\n [corsOptions.key]: CorsOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface CorsRegistration extends Partial<CorsOptions> {\n /**\n * Name identifier for this CORS config.\n */\n name?: string;\n /**\n * Path patterns to match (supports wildcards like /api/*).\n */\n paths?: string[];\n}\n\nexport class ServerCorsProvider {\n protected readonly log = $logger();\n protected readonly serverRouterProvider = $inject(ServerRouterProvider);\n protected readonly globalOptions = $state(corsOptions);\n\n /**\n * Registered CORS configurations with their path patterns\n */\n public readonly registeredConfigs: CorsRegistration[] = [];\n\n /**\n * Register a CORS configuration (called by primitives)\n */\n public registerCors(config: CorsRegistration): void {\n this.registeredConfigs.push(config);\n }\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n // Apply path-specific CORS configs to routes\n for (const config of this.registeredConfigs) {\n if (config.paths) {\n for (const pattern of config.paths) {\n const matchedRoutes = this.serverRouterProvider.getRoutes(pattern);\n for (const route of matchedRoutes) {\n route.cors = this.buildCorsOptions(config);\n }\n }\n }\n }\n\n if (this.registeredConfigs.length > 0) {\n this.log.info(\n `Initialized with ${this.registeredConfigs.length} registered CORS configurations.`,\n );\n }\n },\n });\n\n /**\n * Build complete CORS options by merging with global defaults\n */\n public buildCorsOptions(config: Partial<CorsOptions>): CorsOptions {\n return {\n origin: config.origin ?? this.globalOptions.origin,\n methods: config.methods ?? this.globalOptions.methods,\n headers: config.headers ?? this.globalOptions.headers,\n credentials: config.credentials ?? this.globalOptions.credentials,\n maxAge: config.maxAge ?? this.globalOptions.maxAge,\n };\n }\n\n /**\n * Apply CORS headers to the response\n */\n public applyCorsHeaders(\n request: {\n headers: { origin?: string };\n reply: { setHeader: (name: string, value: string) => void };\n },\n options: CorsOptions,\n ): void {\n const reqOrigin = request.headers.origin;\n const { origin, methods, headers, credentials, maxAge } = options;\n\n if (reqOrigin && this.isOriginAllowed(reqOrigin, origin)) {\n request.reply.setHeader(\"Access-Control-Allow-Origin\", reqOrigin);\n }\n\n if (credentials) {\n request.reply.setHeader(\"Access-Control-Allow-Credentials\", \"true\");\n }\n\n request.reply.setHeader(\"Access-Control-Allow-Methods\", methods.join(\", \"));\n request.reply.setHeader(\"Access-Control-Allow-Headers\", headers.join(\", \"));\n\n if (maxAge != null) {\n request.reply.setHeader(\"Access-Control-Max-Age\", String(maxAge));\n }\n }\n\n protected readonly configure = $hook({\n on: \"start\",\n handler: () => {\n const routes = this.serverRouterProvider.getRoutes();\n for (const route of routes) {\n if (\n !route.method ||\n route.method === \"GET\" ||\n route.method === \"OPTIONS\"\n ) {\n continue;\n }\n\n this.serverRouterProvider.createRoute({\n path: route.path,\n method: \"OPTIONS\",\n handler: ({ reply }) => {\n reply.setStatus(204);\n },\n });\n }\n },\n });\n\n protected readonly onRequest = $hook({\n on: \"server:onRequest\",\n handler: ({ route, request }) => {\n // Use route-specific CORS if defined, otherwise use global options\n const corsConfig = route.cors ?? this.globalOptions;\n this.applyCorsHeaders(request, corsConfig);\n },\n });\n\n public isOriginAllowed(\n origin: string | undefined,\n allowed: CorsOptions[\"origin\"],\n ): boolean {\n if (!allowed) return false;\n if (allowed === \"*\") return true;\n return allowed\n .split(\",\")\n .map((o) => o.trim())\n .includes(origin ?? \"\");\n }\n}\n\nexport type ServerCorsProviderOptions = CorsOptions;\n","import { AlephaError, createMiddleware, type Middleware } from \"alepha\";\nimport {\n type CorsOptions,\n ServerCorsProvider,\n} from \"../providers/ServerCorsProvider.ts\";\n\n/**\n * Middleware that applies CORS headers to the response and handles OPTIONS preflight.\n *\n * Reads the request from the ALS context and applies the configured\n * CORS headers via `ServerCorsProvider`. Options are merged with\n * global CORS defaults.\n *\n * For OPTIONS preflight requests, the middleware short-circuits with a 204 response\n * and skips the handler entirely.\n *\n * **Route middleware** — requires a request context (`$action`). Throws if used outside one.\n *\n * ```typescript\n * class ApiController {\n * getOrders = $action({\n * use: [$cors({ origin: \"https://app.example.com\", credentials: true })],\n * handler: async ({ query }) => { ... },\n * });\n * }\n * ```\n */\nexport const $cors = (options?: Partial<CorsOptions>): Middleware => {\n return createMiddleware({\n name: \"$cors\",\n options: options as unknown as Record<string, unknown>,\n handler: ({ alepha, next }) => {\n const corsProvider = alepha.inject(ServerCorsProvider);\n\n return async (...args) => {\n const request = alepha.get(\"alepha.http.request\");\n\n if (!request) {\n throw new AlephaError(\n \"$cors requires a request context (use inside $action)\",\n );\n }\n\n const corsConfig = corsProvider.buildCorsOptions(options ?? {});\n corsProvider.applyCorsHeaders(request, corsConfig);\n\n // OPTIONS preflight → respond immediately, skip handler\n if (request.method === \"OPTIONS\") {\n request.reply.setStatus(204);\n return;\n }\n\n return next(...args);\n };\n },\n });\n};\n","import { $module } from \"alepha\";\nimport {\n type CorsOptions,\n ServerCorsProvider,\n} from \"./providers/ServerCorsProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$cors.ts\";\nexport * from \"./providers/ServerCorsProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha/server\" {\n interface ServerRoute {\n /**\n * Route-specific CORS configuration.\n * If set, overrides the global CORS options for this route.\n */\n cors?: CorsOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Cross-Origin Resource Sharing configuration.\n *\n * **Features:**\n * - CORS policy definition\n *\n * @module alepha.server.cors\n */\nexport const AlephaServerCors = $module({\n name: \"alepha.server.cors\",\n services: [ServerCorsProvider],\n});\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/server/etag/providers/ServerEtagProvider.ts","../../../src/server/etag/primitives/$etag.ts","../../../src/server/etag/index.ts"],"sourcesContent":["import { $hook, $inject, Alepha } from \"alepha\";\nimport { $cache } from \"alepha/cache\";\nimport { CryptoProvider } from \"alepha/crypto\";\nimport { DateTimeProvider, type DurationLike } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport type { ServerRequest, ServerRoute } from \"alepha/server\";\nimport type { EtagMiddlewareOptionsResolved } from \"../primitives/$etag.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class ServerEtagProvider {\n protected readonly log = $logger();\n protected readonly alepha = $inject(Alepha);\n protected readonly crypto = $inject(CryptoProvider);\n protected readonly time = $inject(DateTimeProvider);\n protected readonly cache = $cache<RouteCacheEntry>({\n provider: \"memory\",\n name: \"http:server\",\n });\n\n public generateETag(content: string | Buffer): string {\n const data = typeof content === \"string\" ? content : content.toString();\n return `\"${this.crypto.hash(data, \"md5\")}\"`;\n }\n\n public async invalidate(route: ServerRoute) {\n await this.cache.invalidate(this.createCacheKey(route));\n }\n\n /**\n * Check cache for a stored response. Returns the cached body if found, undefined otherwise.\n * Called by the $etag() middleware before the handler runs.\n */\n public async checkCache(\n request: ServerRequest,\n options: EtagMiddlewareOptionsResolved,\n ): Promise<any> {\n if (!this.shouldStore(options)) {\n return undefined;\n }\n\n // Build key from route metadata (set by ServerRouterProvider before handler)\n // or from action request context (for .run() direct calls)\n const actionRequest = this.alepha.store.get(\"alepha.action.request\");\n\n const keySource = {\n method:\n request.metadata?.routeMethod ??\n actionRequest?.method ??\n request.method ??\n \"GET\",\n path:\n request.metadata?.routePath ??\n String(actionRequest?.url ?? request.url ?? \"\"),\n } as ServerRoute;\n\n const key = this.createCacheKey(keySource, actionRequest ?? request);\n const cached = await this.cache.get(key);\n\n if (!cached) {\n this.log.trace(\"Cache miss\", { key });\n return undefined;\n }\n\n this.log.trace(\"Cache hit\", { key });\n\n // Mark as cache hit in request metadata\n request.metadata ??= {} as any;\n request.metadata.etagHit = true;\n\n // For HTTP routes, set reply headers\n if (request.reply) {\n // Check if client has matching ETag - return 304\n if (\n request.headers?.[\"if-none-match\"] === cached.hash ||\n request.headers?.[\"if-modified-since\"] === cached.lastModified\n ) {\n request.reply.status = 304;\n request.reply.setHeader(\"etag\", cached.hash);\n request.reply.setHeader(\"last-modified\", cached.lastModified);\n this.log.trace(\"ETag match, returning 304\", {\n key,\n etag: cached.hash,\n });\n return request.reply.body;\n }\n\n request.reply.body = cached.body;\n request.reply.status = cached.status ?? 200;\n\n if (cached.contentType) {\n request.reply.setHeader(\"Content-Type\", cached.contentType);\n }\n\n request.reply.setHeader(\"etag\", cached.hash);\n request.reply.setHeader(\"last-modified\", cached.lastModified);\n }\n\n // For action direct invocations, return body\n const body =\n cached.contentType === \"application/json\"\n ? JSON.parse(cached.body)\n : cached.body;\n\n return body;\n }\n\n // -------------------------------------------------------------------------------------------------------------------\n // Hooks (post-handler, read from request.metadata)\n // -------------------------------------------------------------------------------------------------------------------\n\n /**\n * After an action response, store it in cache if store is enabled.\n */\n protected readonly onActionResponse = $hook({\n on: \"action:onResponse\",\n handler: async ({ action, request, response }) => {\n const options = request.metadata\n ?.etagOptions as EtagMiddlewareOptionsResolved;\n if (!options || !this.shouldStore(options)) {\n return;\n }\n\n // Skip if this was a cache hit (don't re-store)\n if (request.metadata?.etagHit) {\n return;\n }\n\n // Don't cache error responses (status >= 400)\n if (request.reply.status && request.reply.status >= 400) {\n return;\n }\n\n if (!response) {\n return;\n }\n\n const contentType =\n typeof response === \"string\" ? \"text/plain\" : \"application/json\";\n const body =\n contentType === \"text/plain\" ? response : JSON.stringify(response);\n\n const generatedEtag = this.generateETag(body);\n const lastModified = this.time.toISOString();\n\n const key = this.createCacheKey(action.route, request);\n\n this.log.trace(\"Storing action response\", {\n key,\n action: action.name,\n });\n\n await this.cache.set(key, {\n body: body,\n lastModified,\n contentType: contentType,\n hash: generatedEtag,\n });\n\n // Set Cache-Control header if configured\n const cacheControl = this.buildCacheControlHeader(options);\n if (cacheControl) {\n request.reply.setHeader(\"cache-control\", cacheControl);\n }\n },\n });\n\n /**\n * Before sending the response, check ETag for etag-only routes.\n * This handles the case where etag is enabled but store is not.\n */\n protected readonly onSend = $hook({\n on: \"server:onSend\",\n handler: ({ request }) => {\n const options = request.metadata\n ?.etagOptions as EtagMiddlewareOptionsResolved;\n if (!options) {\n return;\n }\n\n const shouldStore = this.shouldStore(options);\n const shouldUseEtag = this.shouldUseEtag(options);\n\n if (request.reply.headers.etag) {\n // ETag already set, skip\n return;\n }\n\n if (\n !shouldStore &&\n shouldUseEtag &&\n request.reply.body != null &&\n (typeof request.reply.body === \"string\" ||\n Buffer.isBuffer(request.reply.body))\n ) {\n const generatedEtag = this.generateETag(request.reply.body);\n\n if (request.headers[\"if-none-match\"] === generatedEtag) {\n request.reply.status = 304;\n request.reply.body = undefined;\n request.reply.setHeader(\"etag\", generatedEtag);\n this.log.trace(\"ETag match on send, returning 304\", {\n route: request.url,\n etag: generatedEtag,\n });\n return;\n }\n }\n },\n });\n\n /**\n * After the response is generated, store it and set ETag headers.\n */\n protected readonly onResponse = $hook({\n on: \"server:onResponse\",\n priority: \"first\",\n handler: async ({ route, request, response }) => {\n const options = request.metadata\n ?.etagOptions as EtagMiddlewareOptionsResolved;\n if (!options) {\n return;\n }\n\n // Set Cache-Control header if configured\n const cacheControl = this.buildCacheControlHeader(options);\n if (cacheControl) {\n response.headers[\"cache-control\"] = cacheControl;\n }\n\n const shouldStore = this.shouldStore(options);\n const shouldUseEtag = this.shouldUseEtag(options);\n\n // Skip if neither cache nor etag is enabled\n if (!shouldStore && !shouldUseEtag) {\n return;\n }\n\n // Skip if this was a cache hit (don't re-store)\n if (request.metadata?.etagHit) {\n return;\n }\n\n // Don't cache error responses (status >= 400)\n if (response.status && response.status >= 400) {\n return;\n }\n\n // Initialize headers if not present\n response.headers ??= {};\n\n const key = this.createCacheKey(route, request);\n\n // Handle ReadableStream responses (e.g., SSR streaming)\n if (response.body instanceof ReadableStream && shouldStore) {\n // Tee the stream: one for client, one for cache collection\n const [clientStream, cacheStream] = (\n response.body as ReadableStream<Uint8Array>\n ).tee();\n\n // Replace response body with client stream (continues streaming to client)\n response.body = clientStream as typeof response.body;\n\n // Collect cache stream in background (non-blocking)\n this.collectStreamForCache(\n cacheStream,\n key,\n response.status,\n response.headers?.[\"content-type\"],\n shouldUseEtag,\n )\n .then((hash) => {\n if (shouldUseEtag && hash) {\n this.log.trace(\"Stream cached with hash\", { key, hash });\n }\n })\n .catch((err) => {\n this.log.warn(\"Failed to cache stream\", { key, error: err });\n });\n\n return;\n }\n\n // Only process string responses (text, html, json, etc.)\n if (typeof response.body !== \"string\") {\n return;\n }\n\n const generatedEtag = this.generateETag(response.body);\n const lastModified = this.time.toISOString();\n\n // Store response if storing is enabled\n if (shouldStore) {\n this.log.trace(\"Storing response\", {\n key,\n route: route.path,\n etag: shouldUseEtag,\n });\n\n await this.cache.set(key, {\n body: response.body,\n status: response.status,\n contentType: response.headers?.[\"content-type\"],\n lastModified,\n hash: generatedEtag,\n });\n }\n\n // Set ETag headers if etag is enabled\n if (shouldUseEtag) {\n response.headers.etag = generatedEtag;\n response.headers[\"last-modified\"] = lastModified;\n }\n },\n });\n\n // -------------------------------------------------------------------------------------------------------------------\n // Public helpers\n // -------------------------------------------------------------------------------------------------------------------\n\n public buildCacheControlHeader(\n options?: EtagMiddlewareOptionsResolved,\n ): string | undefined {\n if (!options) {\n return undefined;\n }\n\n const control = options.control;\n if (!control) {\n return undefined;\n }\n\n // If control is a string, return it directly\n if (typeof control === \"string\") {\n return control;\n }\n\n // If control is true, return default Cache-Control\n if (control === true) {\n return \"public, max-age=300\";\n }\n\n // Build Cache-Control from object directives\n const directives: string[] = [];\n\n if (control.public) {\n directives.push(\"public\");\n }\n if (control.private) {\n directives.push(\"private\");\n }\n if (control.noCache) {\n directives.push(\"no-cache\");\n }\n if (control.noStore) {\n directives.push(\"no-store\");\n }\n if (control.maxAge !== undefined) {\n const maxAgeSeconds = this.durationToSeconds(control.maxAge);\n directives.push(`max-age=${maxAgeSeconds}`);\n }\n if (control.sMaxAge !== undefined) {\n const sMaxAgeSeconds = this.durationToSeconds(control.sMaxAge);\n directives.push(`s-maxage=${sMaxAgeSeconds}`);\n }\n if (control.mustRevalidate) {\n directives.push(\"must-revalidate\");\n }\n if (control.proxyRevalidate) {\n directives.push(\"proxy-revalidate\");\n }\n if (control.immutable) {\n directives.push(\"immutable\");\n }\n if (control.staleWhileRevalidate !== undefined) {\n const seconds = this.durationToSeconds(control.staleWhileRevalidate);\n directives.push(`stale-while-revalidate=${seconds}`);\n }\n\n return directives.length > 0 ? directives.join(\", \") : undefined;\n }\n\n public shouldStore(options?: EtagMiddlewareOptionsResolved): boolean {\n if (!options) return false;\n if (options.store) return true;\n return false;\n }\n\n public shouldUseEtag(options?: EtagMiddlewareOptionsResolved): boolean {\n if (!options) return false;\n if (options.etag) return true;\n return false;\n }\n\n // -------------------------------------------------------------------------------------------------------------------\n // Protected helpers\n // -------------------------------------------------------------------------------------------------------------------\n\n protected durationToSeconds(duration: number | DurationLike): number {\n if (typeof duration === \"number\") {\n return duration;\n }\n\n return this.time.duration(duration).asSeconds();\n }\n\n protected createCacheKey(route: ServerRoute, config?: ServerRequest): string {\n const params: string[] = [];\n for (const [key, value] of Object.entries(config?.params ?? {})) {\n params.push(`${key}=${value}`);\n }\n for (const [key, value] of Object.entries(config?.query ?? {})) {\n params.push(`${key}=${value}`);\n }\n\n return `${route.method}:${(route.path ?? \"\").replaceAll(\":\", \"\")}:${params.join(\",\").replaceAll(\":\", \"\")}`;\n }\n\n /**\n * Collect a ReadableStream into a string and store it in the cache.\n * This runs in the background while the original stream is sent to the client.\n */\n protected async collectStreamForCache(\n stream: ReadableStream<Uint8Array>,\n key: string,\n status: number | undefined,\n contentType: string | undefined,\n generateEtag: boolean,\n ): Promise<string | undefined> {\n const chunks: Uint8Array[] = [];\n const reader = stream.getReader();\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n }\n\n // Combine chunks into a single string\n const decoder = new TextDecoder();\n const body =\n chunks\n .map((chunk) => decoder.decode(chunk, { stream: true }))\n .join(\"\") + decoder.decode(); // Flush remaining\n\n const hash = this.generateETag(body);\n const lastModified = this.time.toISOString();\n\n this.log.trace(\"Storing streamed response\", { key });\n\n await this.cache.set(key, {\n body,\n status,\n contentType,\n lastModified,\n hash,\n });\n\n return generateEtag ? hash : undefined;\n } finally {\n reader.releaseLock();\n }\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ninterface RouteCacheEntry {\n contentType?: string;\n body: any;\n status?: number;\n lastModified: string;\n hash: string;\n}\n","import { createMiddleware, type Middleware } from \"alepha\";\nimport type { CachePrimitiveOptions } from \"alepha/cache\";\nimport type { DurationLike } from \"alepha/datetime\";\nimport { ServerEtagProvider } from \"../providers/ServerEtagProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Middleware that enables ETag-based response caching per-route.\n *\n * Sets per-request etag options in the ALS context.\n * The global `ServerEtagProvider` hooks read these\n * to generate ETags, handle 304s, and optionally store responses.\n *\n * When `store` is enabled, the middleware also checks the cache before\n * calling the handler, short-circuiting on cache hits.\n *\n * **Route middleware** — works inside `$action`, `$page`, or any pipeline.\n *\n * ```typescript\n * class UserController {\n * // ETag only (no response caching)\n * getUser = $action({\n * use: [$etag()],\n * handler: async ({ params }) => { ... },\n * });\n *\n * // ETag + response caching (store)\n * getProfile = $action({\n * use: [$etag(true)],\n * handler: async ({ params }) => { ... },\n * });\n *\n * // Fine-grained control\n * getStats = $action({\n * use: [$etag({ store: { ttl: [5, \"minutes\"] }, control: { public: true, maxAge: 300 } })],\n * handler: async ({ params }) => { ... },\n * });\n * }\n * ```\n */\nexport const $etag = (options?: EtagMiddlewareOptions): Middleware => {\n const resolved = resolveEtagOptions(options);\n\n return createMiddleware({\n name: \"$etag\",\n options: resolved as unknown as Record<string, unknown>,\n handler: ({ alepha, next }) => {\n const etagProvider = alepha.inject(ServerEtagProvider);\n\n return async (...args) => {\n const request = alepha.get(\"alepha.http.request\") ?? args[0];\n\n // Set etag options on request metadata for hooks to read\n if (request?.metadata) {\n request.metadata.etagOptions = resolved;\n }\n\n // If store is enabled, check cache before handler\n if (etagProvider.shouldStore(resolved)) {\n if (request) {\n const cached = await etagProvider.checkCache(request, resolved);\n\n // checkCache sets request.metadata.etagHit on cache hit\n // cached may be undefined for 304 responses (no body)\n if (cached !== undefined || request.metadata?.etagHit) {\n return cached;\n }\n }\n }\n\n return next(...args);\n };\n },\n });\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport function resolveEtagOptions(\n options?: EtagMiddlewareOptions,\n): EtagMiddlewareOptionsResolved {\n if (options === true) {\n return { store: true, etag: true };\n }\n\n if (!options) {\n return { etag: true };\n }\n\n return { etag: true, ...options };\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type EtagMiddlewareOptions =\n /**\n * If true, enables both store and etag.\n */\n | true\n /**\n * Object configuration for fine-grained control.\n */\n | {\n /**\n * If true, enables storing cached responses. (in-memory, Redis, @see alepha/cache for other providers)\n * If a DurationLike is provided, it will be used as the TTL for the cache.\n * If CachePrimitiveOptions is provided, it will be used to configure the cache storage.\n *\n * @default false\n */\n store?: true | DurationLike | CachePrimitiveOptions;\n\n /**\n * If true, enables ETag support for the cached responses.\n *\n * @default true (always true when using $etag)\n */\n etag?: true;\n\n /**\n * - If true, sets Cache-Control to \"public, max-age=300\" (5 minutes).\n * - If string, sets Cache-Control to the provided value directly.\n * - If object, configures Cache-Control directives.\n */\n control?:\n | true\n | string\n | {\n /**\n * Indicates that the response may be cached by any cache.\n */\n public?: boolean;\n /**\n * Indicates that the response is intended for a single user and must not be stored by a shared cache.\n */\n private?: boolean;\n /**\n * Forces caches to submit the request to the origin server for validation before releasing a cached copy.\n */\n noCache?: boolean;\n /**\n * Instructs caches not to store the response.\n */\n noStore?: boolean;\n /**\n * Maximum amount of time a resource is considered fresh.\n * Can be specified as a number (seconds) or as a DurationLike object.\n */\n maxAge?: number | DurationLike;\n /**\n * Overrides max-age for shared caches (e.g., CDNs).\n * Can be specified as a number (seconds) or as a DurationLike object.\n */\n sMaxAge?: number | DurationLike;\n /**\n * Indicates that once a resource becomes stale, caches must not use it without successful validation.\n */\n mustRevalidate?: boolean;\n /**\n * Similar to must-revalidate, but only for shared caches.\n */\n proxyRevalidate?: boolean;\n /**\n * Indicates that the response can be stored but must be revalidated before each use.\n */\n immutable?: boolean;\n /**\n * Time window (in seconds or DurationLike) during which a stale response may be served\n * while a fresh one is fetched in the background.\n * Supported by Cloudflare, modern browsers, and most CDNs.\n */\n staleWhileRevalidate?: number | DurationLike;\n };\n };\n\nexport interface EtagMiddlewareOptionsResolved {\n store?: true | DurationLike | CachePrimitiveOptions;\n etag?: true;\n control?:\n | true\n | string\n | {\n public?: boolean;\n private?: boolean;\n noCache?: boolean;\n noStore?: boolean;\n maxAge?: number | DurationLike;\n sMaxAge?: number | DurationLike;\n mustRevalidate?: boolean;\n proxyRevalidate?: boolean;\n immutable?: boolean;\n staleWhileRevalidate?: number | DurationLike;\n };\n}\n","import { $module } from \"alepha\";\nimport { AlephaCache } from \"alepha/cache\";\nimport { AlephaCrypto } from \"alepha/crypto\";\nimport { ServerEtagProvider } from \"./providers/ServerEtagProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$etag.ts\";\nexport * from \"./providers/ServerEtagProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * ETag-based response caching.\n *\n * **Features:**\n * - ETag generation and validation\n * - Conditional request handling (304 Not Modified)\n * - Optional response caching (store)\n * - Cache-Control header support\n *\n * @module alepha.server.etag\n */\nexport const AlephaServerEtag = $module({\n name: \"alepha.server.etag\",\n services: [AlephaCache, AlephaCrypto, ServerEtagProvider],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/server/etag/providers/ServerEtagProvider.ts","../../../src/server/etag/primitives/$etag.ts","../../../src/server/etag/index.ts"],"sourcesContent":["import { $hook, $inject, Alepha } from \"alepha\";\nimport { $cache } from \"alepha/cache\";\nimport { CryptoProvider } from \"alepha/crypto\";\nimport { DateTimeProvider, type DurationLike } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport type { ServerRequest, ServerRoute } from \"alepha/server\";\nimport type { EtagMiddlewareOptionsResolved } from \"../primitives/$etag.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class ServerEtagProvider {\n protected readonly log = $logger();\n protected readonly alepha = $inject(Alepha);\n protected readonly crypto = $inject(CryptoProvider);\n protected readonly time = $inject(DateTimeProvider);\n protected readonly cache = $cache<RouteCacheEntry>({\n provider: \"memory\",\n name: \"http:server\",\n });\n\n public generateETag(content: string | Buffer): string {\n const data = typeof content === \"string\" ? content : content.toString();\n return `\"${this.crypto.hash(data, \"md5\")}\"`;\n }\n\n public async invalidate(route: ServerRoute) {\n await this.cache.invalidate(this.createCacheKey(route));\n }\n\n /**\n * Check cache for a stored response. Returns the cached body if found, undefined otherwise.\n * Called by the $etag() middleware before the handler runs.\n */\n public async checkCache(\n request: ServerRequest,\n options: EtagMiddlewareOptionsResolved,\n ): Promise<any> {\n if (!this.shouldStore(options)) {\n return undefined;\n }\n\n // Build key from route metadata (set by ServerRouterProvider before handler)\n // or from action request context (for .run() direct calls)\n const actionRequest = this.alepha.store.get(\"alepha.action.request\");\n\n const keySource = {\n method:\n request.metadata?.routeMethod ??\n actionRequest?.method ??\n request.method ??\n \"GET\",\n path:\n request.metadata?.routePath ??\n String(actionRequest?.url ?? request.url ?? \"\"),\n } as ServerRoute;\n\n const key = this.createCacheKey(keySource, actionRequest ?? request);\n const cached = await this.cache.get(key);\n\n if (!cached) {\n this.log.trace(\"Cache miss\", { key });\n return undefined;\n }\n\n this.log.trace(\"Cache hit\", { key });\n\n // Mark as cache hit in request metadata\n request.metadata ??= {} as any;\n request.metadata.etagHit = true;\n\n // For HTTP routes, set reply headers\n if (request.reply) {\n // Check if client has matching ETag - return 304\n if (\n request.headers?.[\"if-none-match\"] === cached.hash ||\n request.headers?.[\"if-modified-since\"] === cached.lastModified\n ) {\n request.reply.status = 304;\n request.reply.setHeader(\"etag\", cached.hash);\n request.reply.setHeader(\"last-modified\", cached.lastModified);\n this.log.trace(\"ETag match, returning 304\", {\n key,\n etag: cached.hash,\n });\n return request.reply.body;\n }\n\n request.reply.body = cached.body;\n request.reply.status = cached.status ?? 200;\n\n if (cached.contentType) {\n request.reply.setHeader(\"Content-Type\", cached.contentType);\n }\n\n request.reply.setHeader(\"etag\", cached.hash);\n request.reply.setHeader(\"last-modified\", cached.lastModified);\n }\n\n // For action direct invocations, return body\n const body =\n cached.contentType === \"application/json\"\n ? JSON.parse(cached.body)\n : cached.body;\n\n return body;\n }\n\n // -------------------------------------------------------------------------------------------------------------------\n // Hooks (post-handler, read from request.metadata)\n // -------------------------------------------------------------------------------------------------------------------\n\n /**\n * After an action response, store it in cache if store is enabled.\n */\n protected readonly onActionResponse = $hook({\n on: \"action:onResponse\",\n handler: async ({ action, request, response }) => {\n const options = request.metadata\n ?.etagOptions as EtagMiddlewareOptionsResolved;\n if (!options || !this.shouldStore(options)) {\n return;\n }\n\n // Skip if this was a cache hit (don't re-store)\n if (request.metadata?.etagHit) {\n return;\n }\n\n // Don't cache error responses (status >= 400)\n if (request.reply.status && request.reply.status >= 400) {\n return;\n }\n\n if (!response) {\n return;\n }\n\n const contentType =\n typeof response === \"string\" ? \"text/plain\" : \"application/json\";\n const body =\n contentType === \"text/plain\" ? response : JSON.stringify(response);\n\n const generatedEtag = this.generateETag(body);\n const lastModified = this.time.toISOString();\n\n const key = this.createCacheKey(action.route, request);\n\n this.log.trace(\"Storing action response\", {\n key,\n action: action.name,\n });\n\n await this.cache.set(key, {\n body: body,\n lastModified,\n contentType: contentType,\n hash: generatedEtag,\n });\n\n // Set Cache-Control header if configured\n const cacheControl = this.buildCacheControlHeader(options);\n if (cacheControl) {\n request.reply.setHeader(\"cache-control\", cacheControl);\n }\n },\n });\n\n /**\n * Before sending the response, check ETag for etag-only routes.\n * This handles the case where etag is enabled but store is not.\n */\n protected readonly onSend = $hook({\n on: \"server:onSend\",\n handler: ({ request }) => {\n const options = request.metadata\n ?.etagOptions as EtagMiddlewareOptionsResolved;\n if (!options) {\n return;\n }\n\n const shouldStore = this.shouldStore(options);\n const shouldUseEtag = this.shouldUseEtag(options);\n\n if (request.reply.headers.etag) {\n // ETag already set, skip\n return;\n }\n\n if (\n !shouldStore &&\n shouldUseEtag &&\n request.reply.body != null &&\n (typeof request.reply.body === \"string\" ||\n Buffer.isBuffer(request.reply.body))\n ) {\n const generatedEtag = this.generateETag(request.reply.body);\n\n if (request.headers[\"if-none-match\"] === generatedEtag) {\n request.reply.status = 304;\n request.reply.body = undefined;\n request.reply.setHeader(\"etag\", generatedEtag);\n this.log.trace(\"ETag match on send, returning 304\", {\n route: request.url,\n etag: generatedEtag,\n });\n return;\n }\n }\n },\n });\n\n /**\n * After the response is generated, store it and set ETag headers.\n */\n protected readonly onResponse = $hook({\n on: \"server:onResponse\",\n priority: \"first\",\n handler: async ({ route, request, response }) => {\n const options = request.metadata\n ?.etagOptions as EtagMiddlewareOptionsResolved;\n if (!options) {\n return;\n }\n\n // Set Cache-Control header if configured\n const cacheControl = this.buildCacheControlHeader(options);\n if (cacheControl) {\n response.headers[\"cache-control\"] = cacheControl;\n }\n\n const shouldStore = this.shouldStore(options);\n const shouldUseEtag = this.shouldUseEtag(options);\n\n // Skip if neither cache nor etag is enabled\n if (!shouldStore && !shouldUseEtag) {\n return;\n }\n\n // Skip if this was a cache hit (don't re-store)\n if (request.metadata?.etagHit) {\n return;\n }\n\n // Don't cache error responses (status >= 400)\n if (response.status && response.status >= 400) {\n return;\n }\n\n // Initialize headers if not present\n response.headers ??= {};\n\n const key = this.createCacheKey(route, request);\n\n // Handle ReadableStream responses (e.g., SSR streaming)\n if (response.body instanceof ReadableStream && shouldStore) {\n // Tee the stream: one for client, one for cache collection\n const [clientStream, cacheStream] = (\n response.body as ReadableStream<Uint8Array>\n ).tee();\n\n // Replace response body with client stream (continues streaming to client)\n response.body = clientStream as typeof response.body;\n\n // Collect cache stream in background (non-blocking)\n this.collectStreamForCache(\n cacheStream,\n key,\n response.status,\n response.headers?.[\"content-type\"],\n shouldUseEtag,\n )\n .then((hash) => {\n if (shouldUseEtag && hash) {\n this.log.trace(\"Stream cached with hash\", { key, hash });\n }\n })\n .catch((err) => {\n this.log.warn(\"Failed to cache stream\", { key, error: err });\n });\n\n return;\n }\n\n // Only process string responses (text, html, json, etc.)\n if (typeof response.body !== \"string\") {\n return;\n }\n\n const generatedEtag = this.generateETag(response.body);\n const lastModified = this.time.toISOString();\n\n // Store response if storing is enabled\n if (shouldStore) {\n this.log.trace(\"Storing response\", {\n key,\n route: route.path,\n etag: shouldUseEtag,\n });\n\n await this.cache.set(key, {\n body: response.body,\n status: response.status,\n contentType: response.headers?.[\"content-type\"],\n lastModified,\n hash: generatedEtag,\n });\n }\n\n // Set ETag headers if etag is enabled\n if (shouldUseEtag) {\n response.headers.etag = generatedEtag;\n response.headers[\"last-modified\"] = lastModified;\n }\n },\n });\n\n // -------------------------------------------------------------------------------------------------------------------\n // Public helpers\n // -------------------------------------------------------------------------------------------------------------------\n\n public buildCacheControlHeader(\n options?: EtagMiddlewareOptionsResolved,\n ): string | undefined {\n if (!options) {\n return undefined;\n }\n\n const control = options.control;\n if (!control) {\n return undefined;\n }\n\n // If control is a string, return it directly\n if (typeof control === \"string\") {\n return control;\n }\n\n // If control is true, return default Cache-Control\n if (control === true) {\n return \"public, max-age=300\";\n }\n\n // Build Cache-Control from object directives\n const directives: string[] = [];\n\n if (control.public) {\n directives.push(\"public\");\n }\n if (control.private) {\n directives.push(\"private\");\n }\n if (control.noCache) {\n directives.push(\"no-cache\");\n }\n if (control.noStore) {\n directives.push(\"no-store\");\n }\n if (control.maxAge !== undefined) {\n const maxAgeSeconds = this.durationToSeconds(control.maxAge);\n directives.push(`max-age=${maxAgeSeconds}`);\n }\n if (control.sMaxAge !== undefined) {\n const sMaxAgeSeconds = this.durationToSeconds(control.sMaxAge);\n directives.push(`s-maxage=${sMaxAgeSeconds}`);\n }\n if (control.mustRevalidate) {\n directives.push(\"must-revalidate\");\n }\n if (control.proxyRevalidate) {\n directives.push(\"proxy-revalidate\");\n }\n if (control.immutable) {\n directives.push(\"immutable\");\n }\n if (control.staleWhileRevalidate !== undefined) {\n const seconds = this.durationToSeconds(control.staleWhileRevalidate);\n directives.push(`stale-while-revalidate=${seconds}`);\n }\n\n return directives.length > 0 ? directives.join(\", \") : undefined;\n }\n\n public shouldStore(options?: EtagMiddlewareOptionsResolved): boolean {\n if (!options) return false;\n if (options.store) return true;\n return false;\n }\n\n public shouldUseEtag(options?: EtagMiddlewareOptionsResolved): boolean {\n if (!options) return false;\n if (options.etag) return true;\n return false;\n }\n\n // -------------------------------------------------------------------------------------------------------------------\n // Protected helpers\n // -------------------------------------------------------------------------------------------------------------------\n\n protected durationToSeconds(duration: number | DurationLike): number {\n if (typeof duration === \"number\") {\n return duration;\n }\n\n return this.time.duration(duration).asSeconds();\n }\n\n protected createCacheKey(route: ServerRoute, config?: ServerRequest): string {\n const params: string[] = [];\n for (const [key, value] of Object.entries(config?.params ?? {})) {\n params.push(`${key}=${value}`);\n }\n for (const [key, value] of Object.entries(config?.query ?? {})) {\n params.push(`${key}=${value}`);\n }\n\n return `${route.method}:${(route.path ?? \"\").replaceAll(\":\", \"\")}:${params.join(\",\").replaceAll(\":\", \"\")}`;\n }\n\n /**\n * Collect a ReadableStream into a string and store it in the cache.\n * This runs in the background while the original stream is sent to the client.\n */\n protected async collectStreamForCache(\n stream: ReadableStream<Uint8Array>,\n key: string,\n status: number | undefined,\n contentType: string | undefined,\n generateEtag: boolean,\n ): Promise<string | undefined> {\n const chunks: Uint8Array[] = [];\n const reader = stream.getReader();\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n }\n\n // Combine chunks into a single string\n const decoder = new TextDecoder();\n const body =\n chunks\n .map((chunk) => decoder.decode(chunk, { stream: true }))\n .join(\"\") + decoder.decode(); // Flush remaining\n\n const hash = this.generateETag(body);\n const lastModified = this.time.toISOString();\n\n this.log.trace(\"Storing streamed response\", { key });\n\n await this.cache.set(key, {\n body,\n status,\n contentType,\n lastModified,\n hash,\n });\n\n return generateEtag ? hash : undefined;\n } finally {\n reader.releaseLock();\n }\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ninterface RouteCacheEntry {\n contentType?: string;\n body: any;\n status?: number;\n lastModified: string;\n hash: string;\n}\n","import { createMiddleware, type Middleware } from \"alepha\";\nimport type { CachePrimitiveOptions } from \"alepha/cache\";\nimport type { DurationLike } from \"alepha/datetime\";\nimport { ServerEtagProvider } from \"../providers/ServerEtagProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Middleware that enables ETag-based response caching per-route.\n *\n * Sets per-request etag options in the ALS context.\n * The global `ServerEtagProvider` hooks read these\n * to generate ETags, handle 304s, and optionally store responses.\n *\n * When `store` is enabled, the middleware also checks the cache before\n * calling the handler, short-circuiting on cache hits.\n *\n * **Route middleware** — works inside `$action`, `$page`, or any pipeline.\n *\n * ```typescript\n * class UserController {\n * // ETag only (no response caching)\n * getUser = $action({\n * use: [$etag()],\n * handler: async ({ params }) => { ... },\n * });\n *\n * // ETag + response caching (store)\n * getProfile = $action({\n * use: [$etag(true)],\n * handler: async ({ params }) => { ... },\n * });\n *\n * // Fine-grained control\n * getStats = $action({\n * use: [$etag({ store: { ttl: [5, \"minutes\"] }, control: { public: true, maxAge: 300 } })],\n * handler: async ({ params }) => { ... },\n * });\n * }\n * ```\n */\nexport const $etag = (options?: EtagMiddlewareOptions): Middleware => {\n const resolved = resolveEtagOptions(options);\n\n return createMiddleware({\n name: \"$etag\",\n options: resolved as unknown as Record<string, unknown>,\n handler: ({ alepha, next }) => {\n const etagProvider = alepha.inject(ServerEtagProvider);\n\n return async (...args) => {\n const request = alepha.get(\"alepha.http.request\") ?? args[0];\n\n // Set etag options on request metadata for hooks to read\n if (request?.metadata) {\n request.metadata.etagOptions = resolved;\n }\n\n // If store is enabled, check cache before handler\n if (etagProvider.shouldStore(resolved)) {\n if (request) {\n const cached = await etagProvider.checkCache(request, resolved);\n\n // checkCache sets request.metadata.etagHit on cache hit\n // cached may be undefined for 304 responses (no body)\n if (cached !== undefined || request.metadata?.etagHit) {\n return cached;\n }\n }\n }\n\n return next(...args);\n };\n },\n });\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport function resolveEtagOptions(\n options?: EtagMiddlewareOptions,\n): EtagMiddlewareOptionsResolved {\n if (options === true) {\n return { store: true, etag: true };\n }\n\n if (!options) {\n return { etag: true };\n }\n\n return { etag: true, ...options };\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type EtagMiddlewareOptions =\n /**\n * If true, enables both store and etag.\n */\n | true\n /**\n * Object configuration for fine-grained control.\n */\n | {\n /**\n * If true, enables storing cached responses. (in-memory, Redis, @see alepha/cache for other providers)\n * If a DurationLike is provided, it will be used as the TTL for the cache.\n * If CachePrimitiveOptions is provided, it will be used to configure the cache storage.\n *\n * @default false\n */\n store?: true | DurationLike | CachePrimitiveOptions;\n\n /**\n * If true, enables ETag support for the cached responses.\n *\n * @default true (always true when using $etag)\n */\n etag?: true;\n\n /**\n * - If true, sets Cache-Control to \"public, max-age=300\" (5 minutes).\n * - If string, sets Cache-Control to the provided value directly.\n * - If object, configures Cache-Control directives.\n */\n control?:\n | true\n | string\n | {\n /**\n * Indicates that the response may be cached by any cache.\n */\n public?: boolean;\n /**\n * Indicates that the response is intended for a single user and must not be stored by a shared cache.\n */\n private?: boolean;\n /**\n * Forces caches to submit the request to the origin server for validation before releasing a cached copy.\n */\n noCache?: boolean;\n /**\n * Instructs caches not to store the response.\n */\n noStore?: boolean;\n /**\n * Maximum amount of time a resource is considered fresh.\n * Can be specified as a number (seconds) or as a DurationLike object.\n */\n maxAge?: number | DurationLike;\n /**\n * Overrides max-age for shared caches (e.g., CDNs).\n * Can be specified as a number (seconds) or as a DurationLike object.\n */\n sMaxAge?: number | DurationLike;\n /**\n * Indicates that once a resource becomes stale, caches must not use it without successful validation.\n */\n mustRevalidate?: boolean;\n /**\n * Similar to must-revalidate, but only for shared caches.\n */\n proxyRevalidate?: boolean;\n /**\n * Indicates that the response can be stored but must be revalidated before each use.\n */\n immutable?: boolean;\n /**\n * Time window (in seconds or DurationLike) during which a stale response may be served\n * while a fresh one is fetched in the background.\n * Supported by Cloudflare, modern browsers, and most CDNs.\n */\n staleWhileRevalidate?: number | DurationLike;\n };\n };\n\nexport interface EtagMiddlewareOptionsResolved {\n store?: true | DurationLike | CachePrimitiveOptions;\n etag?: true;\n control?:\n | true\n | string\n | {\n public?: boolean;\n private?: boolean;\n noCache?: boolean;\n noStore?: boolean;\n maxAge?: number | DurationLike;\n sMaxAge?: number | DurationLike;\n mustRevalidate?: boolean;\n proxyRevalidate?: boolean;\n immutable?: boolean;\n staleWhileRevalidate?: number | DurationLike;\n };\n}\n","import { $module } from \"alepha\";\nimport { AlephaCache } from \"alepha/cache\";\nimport { AlephaCrypto } from \"alepha/crypto\";\nimport { ServerEtagProvider } from \"./providers/ServerEtagProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$etag.ts\";\nexport * from \"./providers/ServerEtagProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * ETag-based response caching.\n *\n * **Features:**\n * - ETag generation and validation\n * - Conditional request handling (304 Not Modified)\n * - Optional response caching (store)\n * - Cache-Control header support\n *\n * @module alepha.server.etag\n */\nexport const AlephaServerEtag = $module({\n name: \"alepha.server.etag\",\n services: [AlephaCache, AlephaCrypto, ServerEtagProvider],\n});\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/server/health/schemas/healthSchema.ts","../../../src/server/health/providers/ServerHealthProvider.ts","../../../src/server/health/index.ts"],"sourcesContent":["import { t } from \"alepha\";\n\nexport const healthSchema = t.object({\n message: t.text(),\n uptime: t.number(),\n date: t.datetime(),\n ready: t.boolean(),\n});\n","import { $inject, Alepha } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $route } from \"alepha/server\";\nimport { healthSchema } from \"../schemas/healthSchema.ts\";\n\n/**\n * Register `/health` & `/healthz` endpoint.\n *\n * - Provides basic health information about the server.\n */\nexport class ServerHealthProvider {\n protected readonly time: DateTimeProvider = $inject(DateTimeProvider);\n protected readonly alepha = $inject(Alepha);\n\n public readonly health = $route({\n path: \"/health\",\n schema: {\n response: healthSchema,\n },\n silent: true,\n handler: () => this.healthCheck(),\n });\n\n public readonly healthz = $route({\n path: \"/healthz\",\n schema: {\n response: healthSchema,\n },\n silent: true,\n handler: () => this.healthCheck(),\n });\n\n protected healthCheck() {\n return {\n message: \"OK\",\n uptime: Math.floor(process.uptime()),\n date: this.time.nowISOString(),\n ready: this.alepha.isReady(),\n };\n }\n}\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { ServerHealthProvider } from \"./providers/ServerHealthProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./providers/ServerHealthProvider.ts\";\nexport * from \"./schemas/healthSchema.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Application health monitoring endpoints.\n *\n * **Features:**\n * - `GET /health` endpoint\n *\n * @module alepha.server.health\n */\nexport const AlephaServerHealth = $module({\n name: \"alepha.server.health\",\n services: [AlephaServer, ServerHealthProvider],\n});\n"],"mappings":";;;;AAEA,MAAa,eAAe,EAAE,OAAO;CACnC,SAAS,EAAE,MAAM;CACjB,QAAQ,EAAE,QAAQ;CAClB,MAAM,EAAE,UAAU;CAClB,OAAO,EAAE,SAAS;CACnB,CAAC;;;;;;;;ACGF,IAAa,uBAAb,MAAkC;CAChC,OAA4C,QAAQ,iBAAiB;CACrE,SAA4B,QAAQ,OAAO;CAE3C,SAAyB,OAAO;EAC9B,MAAM;EACN,QAAQ,EACN,UAAU,cACX;EACD,QAAQ;EACR,eAAe,KAAK,aAAa;EAClC,CAAC;CAEF,UAA0B,OAAO;EAC/B,MAAM;EACN,QAAQ,EACN,UAAU,cACX;EACD,QAAQ;EACR,eAAe,KAAK,aAAa;EAClC,CAAC;CAEF,cAAwB;
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/server/health/schemas/healthSchema.ts","../../../src/server/health/providers/ServerHealthProvider.ts","../../../src/server/health/index.ts"],"sourcesContent":["import { t } from \"alepha\";\n\nexport const healthSchema = t.object({\n message: t.text(),\n uptime: t.number(),\n date: t.datetime(),\n ready: t.boolean(),\n});\n","import { $inject, Alepha } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $route } from \"alepha/server\";\nimport { healthSchema } from \"../schemas/healthSchema.ts\";\n\n/**\n * Register `/health` & `/healthz` endpoint.\n *\n * - Provides basic health information about the server.\n */\nexport class ServerHealthProvider {\n protected readonly time: DateTimeProvider = $inject(DateTimeProvider);\n protected readonly alepha = $inject(Alepha);\n\n public readonly health = $route({\n path: \"/health\",\n schema: {\n response: healthSchema,\n },\n silent: true,\n handler: () => this.healthCheck(),\n });\n\n public readonly healthz = $route({\n path: \"/healthz\",\n schema: {\n response: healthSchema,\n },\n silent: true,\n handler: () => this.healthCheck(),\n });\n\n protected healthCheck() {\n return {\n message: \"OK\",\n uptime: Math.floor(process.uptime()),\n date: this.time.nowISOString(),\n ready: this.alepha.isReady(),\n };\n }\n}\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { ServerHealthProvider } from \"./providers/ServerHealthProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./providers/ServerHealthProvider.ts\";\nexport * from \"./schemas/healthSchema.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Application health monitoring endpoints.\n *\n * **Features:**\n * - `GET /health` endpoint\n *\n * @module alepha.server.health\n */\nexport const AlephaServerHealth = $module({\n name: \"alepha.server.health\",\n services: [AlephaServer, ServerHealthProvider],\n});\n"],"mappings":";;;;AAEA,MAAa,eAAe,EAAE,OAAO;CACnC,SAAS,EAAE,MAAM;CACjB,QAAQ,EAAE,QAAQ;CAClB,MAAM,EAAE,UAAU;CAClB,OAAO,EAAE,SAAS;CACnB,CAAC;;;;;;;;ACGF,IAAa,uBAAb,MAAkC;CAChC,OAA4C,QAAQ,iBAAiB;CACrE,SAA4B,QAAQ,OAAO;CAE3C,SAAyB,OAAO;EAC9B,MAAM;EACN,QAAQ,EACN,UAAU,cACX;EACD,QAAQ;EACR,eAAe,KAAK,aAAa;EAClC,CAAC;CAEF,UAA0B,OAAO;EAC/B,MAAM;EACN,QAAQ,EACN,UAAU,cACX;EACD,QAAQ;EACR,eAAe,KAAK,aAAa;EAClC,CAAC;CAEF,cAAwB;EACtB,OAAO;GACL,SAAS;GACT,QAAQ,KAAK,MAAM,QAAQ,QAAQ,CAAC;GACpC,MAAM,KAAK,KAAK,cAAc;GAC9B,OAAO,KAAK,OAAO,SAAS;GAC7B;;;;;;;;;;;;;ACnBL,MAAa,qBAAqB,QAAQ;CACxC,MAAM;CACN,UAAU,CAAC,cAAc,qBAAqB;CAC/C,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.browser.js","names":[],"sources":["../../../src/server/links/schemas/apiLinksResponseSchema.ts","../../../src/server/links/atoms/apiLinksAtom.ts","../../../src/server/links/atoms/linkOptionsAtom.ts","../../../src/server/links/services/BatchCollector.ts","../../../src/server/links/providers/LinkProvider.ts","../../../src/server/links/primitives/$client.ts","../../../src/server/links/primitives/$remote.ts","../../../src/server/links/index.browser.ts"],"sourcesContent":["import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const apiActionSchema = t.object({\n path: t.text({\n description: \"Pathname used to access the action.\",\n }),\n\n method: t.optional(\n t.text({\n description:\n \"HTTP method. Omitted when GET (the default for ~75% of actions).\",\n }),\n ),\n\n contentType: t.optional(\n t.text({\n description:\n \"Content type for the request body. Only present for non-JSON types (e.g. 'multipart/form-data'). When absent, defaults to application/json.\",\n }),\n ),\n\n kind: t.optional(\n t.text({\n description:\n \"Action kind. Used to distinguish special action types (e.g. 'sse' for Server-Sent Events streams).\",\n }),\n ),\n\n service: t.optional(\n t.text({\n description:\n \"Service name associated with the action, used for service discovery and routing.\",\n }),\n ),\n});\n\nexport const apiRegistryResponseSchema = t.object({\n prefix: t.optional(t.text()),\n\n actions: t.record(t.text(), apiActionSchema),\n\n permissions: t.optional(t.array(t.text())),\n});\n\nexport type ApiRegistryResponse = Static<typeof apiRegistryResponseSchema>;\nexport type ApiAction = Static<typeof apiActionSchema>;\n\n/**\n * @deprecated Use `apiRegistryResponseSchema` and `ApiRegistryResponse` instead.\n */\nexport const apiLinksResponseSchema = apiRegistryResponseSchema;\n\n/**\n * @deprecated Use `ApiRegistryResponse` instead.\n */\nexport type ApiLinksResponse = ApiRegistryResponse;\n\n/**\n * @deprecated Use `ApiAction` instead.\n */\nexport type ApiLink = ApiAction;\n","import { $atom, t } from \"alepha\";\nimport { apiRegistryResponseSchema } from \"../schemas/apiLinksResponseSchema.ts\";\n\nexport const apiLinksAtom = $atom({\n name: \"alepha.server.request.apiLinks\",\n schema: t.optional(apiRegistryResponseSchema),\n});\n","import { $atom, t } from \"alepha\";\n\nexport const linkOptionsAtom = $atom({\n name: \"alepha.server.links.options\",\n description: \"Configuration options for the links module.\",\n schema: t.object({\n batch: t.boolean({\n description: \"Enable batch collection for browser-side calls.\",\n default: true,\n }),\n }),\n default: {\n batch: true,\n },\n});\n","import { $inject } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { HttpClient, HttpError } from \"alepha/server\";\n\n/**\n * Collects browser-side action calls within a microtask and\n * sends them as a single `POST /api/_batch` request.\n *\n * Key behaviors:\n * - Single call in the window → direct HTTP call (no batch overhead)\n * - Multiple calls → coalesced into one batch request\n * - Same action + same params/query/body → deduplicated, result shared\n * - Exceeding MAX_BATCH_SIZE → split into multiple batch calls\n * - Transport failure → all pending promises reject\n */\nexport class BatchCollector {\n protected static readonly MAX_BATCH_SIZE = 20;\n\n protected readonly log = $logger();\n protected readonly httpClient = $inject(HttpClient);\n\n protected pending: PendingBatchEntry[] = [];\n protected scheduled = false;\n\n /**\n * Add an action call to the batch. Returns the result when the batch resolves.\n */\n public add(entry: BatchEntry): Promise<any> {\n return new Promise((resolve, reject) => {\n this.pending.push({ entry, resolve, reject });\n\n if (!this.scheduled) {\n this.scheduled = true;\n setTimeout(() => {\n this.scheduled = false;\n this.flush().catch((err) => this.log.error(err));\n }, 10);\n }\n });\n }\n\n protected async flush(): Promise<void> {\n const batch = this.pending.splice(0);\n\n if (batch.length === 0) return;\n\n // Single request — skip batching, call directly via follow\n if (batch.length === 1) {\n const item = batch[0];\n try {\n const result = await item.entry.directCall();\n item.resolve(result);\n } catch (error) {\n item.reject(error);\n }\n return;\n }\n\n // Deduplicate: same action + same params → share result\n const { unique, indexMap } = this.dedupe(batch);\n\n // Split into chunks of MAX_BATCH_SIZE\n const chunks = this.chunk(unique, BatchCollector.MAX_BATCH_SIZE);\n\n try {\n const allResults = (\n await Promise.all(\n chunks.map((chunk) => {\n const actions = [...new Set(chunk.map((b) => b.entry.action))].join(\n \",\",\n );\n\n return this.httpClient\n .fetch(`/api/_batch?actions=${actions}`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(\n chunk.map((b) => ({\n action: b.entry.action,\n params: b.entry.params,\n query: b.entry.query,\n body: b.entry.body,\n })),\n ),\n })\n .then((res) => res.data as BatchResponse[]);\n }),\n )\n ).flat();\n\n // Distribute results back (including deduped slots)\n for (let i = 0; i < batch.length; i++) {\n const result = allResults[indexMap[i]];\n if (result.status >= 400) {\n batch[i].reject(\n new HttpError({\n message:\n result.error ?? `${result.action} failed (${result.status})`,\n status: result.status,\n }),\n );\n } else {\n batch[i].resolve(result.data);\n }\n }\n } catch (error) {\n // Transport-level failure — reject all pending promises\n for (const item of batch) {\n item.reject(error);\n }\n }\n }\n\n protected dedupe(batch: PendingBatchEntry[]): {\n unique: PendingBatchEntry[];\n indexMap: number[];\n } {\n const seen = new Map<string, number>();\n const unique: PendingBatchEntry[] = [];\n const indexMap: number[] = [];\n\n for (const item of batch) {\n const key = `${item.entry.action}:${JSON.stringify({\n params: item.entry.params,\n query: item.entry.query,\n body: item.entry.body,\n })}`;\n\n const existing = seen.get(key);\n if (existing !== undefined) {\n indexMap.push(existing);\n } else {\n const idx = unique.length;\n seen.set(key, idx);\n unique.push(item);\n indexMap.push(idx);\n }\n }\n\n return { unique, indexMap };\n }\n\n protected chunk<T>(arr: T[], size: number): T[][] {\n const chunks: T[][] = [];\n for (let i = 0; i < arr.length; i += size) {\n chunks.push(arr.slice(i, i + size));\n }\n return chunks;\n }\n}\n\n// ---\n\nexport interface BatchEntry {\n action: string;\n params?: Record<string, any>;\n query?: Record<string, any>;\n body?: Record<string, any>;\n directCall: () => Promise<any>;\n}\n\ninterface PendingBatchEntry {\n entry: BatchEntry;\n resolve: (value: any) => void;\n reject: (reason: any) => void;\n}\n\ninterface BatchResponse {\n action: string;\n status: number;\n data?: any;\n error?: string;\n}\n","import { $inject, $state, Alepha, AlephaError, type Async, t } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport type { SecureOptions } from \"alepha/security\";\nimport {\n type ActionPrimitive,\n type ClientRequestEntry,\n type ClientRequestOptions,\n type ClientRequestResponse,\n type FetchResponse,\n HttpClient,\n type RequestConfigSchema,\n ServerReply,\n type ServerRequest,\n type ServerRequestConfigEntry,\n type ServerResponseBody,\n type SseConfigSchema,\n type SseEventData,\n type SsePrimitive,\n type SseRequestEntry,\n type SseStream,\n UnauthorizedError,\n} from \"alepha/server\";\nimport { linkOptionsAtom } from \"../atoms/linkOptionsAtom.ts\";\nimport {\n type ApiRegistryResponse,\n apiRegistryResponseSchema,\n} from \"../schemas/apiLinksResponseSchema.ts\";\nimport { BatchCollector } from \"../services/BatchCollector.ts\";\n\n/**\n * Browser, SSR friendly, service to handle links.\n */\nexport class LinkProvider {\n static path = {\n apiLinks: \"/api/_links\",\n };\n\n protected readonly log = $logger();\n protected readonly alepha = $inject(Alepha);\n protected readonly httpClient = $inject(HttpClient);\n\n // Server-side: all registered links (local + remote), keyed by name\n protected serverLinkMap = new Map<string, HttpClientLink>();\n\n // Browser/SSR: parsed from the registry response\n protected actionMap = new Map<string, HttpClientLink>();\n protected permissions = new Set<string>();\n protected lastLoadedRegistry: ApiRegistryResponse | null = null;\n\n // Browser-only: batch collector for coalescing multiple calls\n protected batchCollector?: BatchCollector;\n\n protected readonly options = $state(linkOptionsAtom);\n\n /**\n * Get applicative links registered on the server.\n * This does not include lazy-loaded remote links.\n */\n public getServerLinks(): HttpClientLink[] {\n if (this.alepha.isBrowser()) {\n this.log.warn(\n \"Getting server links in the browser is not supported. Use `fetchLinks` to get links from the server.\",\n );\n return [];\n }\n\n return [...this.serverLinkMap.values()];\n }\n\n /**\n * Register a new link for the application.\n */\n public registerLink(link: HttpClientLink): void {\n if (this.alepha.isBrowser()) {\n this.log.warn(\n \"Registering links in the browser is not supported. Use `fetchLinks` to get links from the server.\",\n );\n return;\n }\n\n if (!link.handler && !link.host) {\n throw new AlephaError(\n \"Can't create link - 'handler' or 'host' is required\",\n );\n }\n\n // Detect duplicate local actions (programming error)\n const existing = this.serverLinkMap.get(link.name);\n if (existing?.handler && link.handler) {\n throw new AlephaError(\n `Duplicate action name \"${link.name}\". Each action must have a unique name.`,\n );\n }\n\n this.serverLinkMap.set(link.name, link);\n }\n\n /**\n * Load the registry response into internal stores (actionMap, permissions, definitions).\n * Called when storing from atom/fetch/SSR.\n */\n protected loadRegistry(registry: ApiRegistryResponse): void {\n this.lastLoadedRegistry = registry;\n this.permissions.clear();\n this.actionMap.clear();\n\n for (const [name, action] of Object.entries(registry.actions)) {\n this.actionMap.set(name, {\n name,\n path: action.path,\n kind: action.kind,\n method: action.method,\n contentType: action.contentType,\n service: action.service,\n });\n }\n\n if (registry.permissions) {\n for (const p of registry.permissions) {\n this.permissions.add(p);\n }\n }\n }\n\n public get links(): HttpClientLink[] {\n const registry = this.alepha.store.get(\"alepha.server.request.apiLinks\");\n\n if (registry) {\n if (this.alepha.isBrowser()) {\n // Browser side: use the parsed action map\n // Reload when registry changes (e.g. after login provides new authenticated links)\n if (this.actionMap.size === 0 || registry !== this.lastLoadedRegistry) {\n this.loadRegistry(registry);\n }\n return [...this.actionMap.values()];\n }\n\n // SSR side: map registry actions back to full server links\n const links: HttpClientLink[] = [];\n for (const name of Object.keys(registry.actions)) {\n const originalLink = this.serverLinkMap.get(name);\n if (originalLink) {\n links.push(originalLink);\n }\n }\n return links;\n }\n\n return [...this.serverLinkMap.values()];\n }\n\n /**\n * Force browser to refresh links from the server.\n */\n public async fetchLinks(): Promise<HttpClientLink[]> {\n const { data } = await this.httpClient.fetch(\n `${LinkProvider.path.apiLinks}`,\n {\n method: \"GET\",\n schema: {\n response: apiRegistryResponseSchema,\n },\n },\n );\n\n this.alepha.store.set(\"alepha.server.request.apiLinks\", data);\n this.loadRegistry(data);\n\n return [...this.actionMap.values()];\n }\n\n /**\n * Create a virtual client that can be used to call actions.\n *\n * Use js Proxy under the hood.\n */\n public client<T extends object>(\n scope: ClientScope = {},\n ): HttpVirtualClient<T> {\n return new Proxy<HttpVirtualClient<T>>({} as HttpVirtualClient<T>, {\n get: (_, prop) => {\n if (typeof prop !== \"string\") {\n return;\n }\n\n return this.createVirtualAction<RequestConfigSchema>(prop, scope);\n },\n });\n }\n\n /**\n * Check if a link with the given name exists or a permission matches.\n *\n * Action names never contain colons. Permission names always do.\n * - `can(\"getUsers\")` → O(1) map lookup\n * - `can(\"admin:*\")` → wildcard match against permissions set\n * - `can(\"admin:user:read\")` → O(1) set lookup\n */\n public can(name: string): boolean {\n // Action check — O(1) map lookup\n if (this.actionMap.size > 0) {\n if (this.actionMap.has(name)) return true;\n } else {\n // Fallback for server-side where actionMap may not be populated\n if (this.serverLinkMap.has(name)) return true;\n // Also check links getter (for SSR with atom)\n if (this.links.some((link) => link.name === name)) return true;\n }\n\n // Permission check — wildcard matching\n if (name.includes(\":\")) {\n if (name.endsWith(\"*\")) {\n const prefix = name.slice(0, -1);\n for (const p of this.permissions) {\n if (p.startsWith(prefix)) return true;\n }\n return false;\n }\n return this.permissions.has(name);\n }\n\n return false;\n }\n\n /**\n * Resolve a link by its name and call it.\n * - If link is local, it will call the local handler.\n * - If link is remote, it will make a fetch request to the remote server.\n */\n public async follow(\n name: string,\n config: Partial<ServerRequestConfigEntry> = {},\n options: ClientRequestOptions & ClientScope = {},\n ): Promise<any> {\n this.log.trace(\"Following link\", { name, config, options });\n const link = await this.getLinkByName(name, options);\n\n // if a handler is defined, use it (ssr)\n if (link.handler && !options.request) {\n this.log.trace(\"Local link found\", { name });\n return link.handler(\n {\n method: link.method,\n url: new URL(`http://localhost${link.path}`),\n query: config.query ?? {},\n body: config.body ?? {},\n params: config.params ?? {},\n headers: config.headers ?? {},\n metadata: {},\n reply: new ServerReply(),\n } as Partial<ServerRequest> as ServerRequest,\n options,\n );\n }\n\n this.log.trace(\"Remote link found\", {\n name,\n host: link.host,\n service: link.service,\n });\n\n // Browser-only: use batch collector for calls without explicit host\n if (this.options.batch && this.alepha.isBrowser() && !link.host) {\n this.batchCollector ??= this.alepha.inject(BatchCollector);\n return this.batchCollector.add({\n action: name,\n params: config.params as any,\n query: config.query as any,\n body: config.body as any,\n directCall: () =>\n this.followRemote(link, config, options).then((r) => r.data),\n });\n }\n\n return this.followRemote(link, config, options).then(\n (response) => response.data,\n );\n }\n\n protected createVirtualAction<T extends RequestConfigSchema>(\n name: string,\n scope: ClientScope = {},\n ): VirtualAction<T> {\n const $: VirtualAction<T> = async (\n config: any = {},\n options: ClientRequestOptions = {},\n ) => {\n return this.follow(name, config, {\n ...scope,\n ...options,\n });\n };\n\n Object.defineProperty($, \"name\", {\n value: name,\n writable: false,\n });\n\n $.run = async (config: any = {}, options: ClientRequestOptions = {}) => {\n return this.follow(name, config, {\n ...scope,\n ...options,\n });\n };\n\n $.fetch = async (config: any = {}, options: ClientRequestOptions = {}) => {\n const link = await this.getLinkByName(name, scope);\n return this.followRemote(link, config, options);\n };\n\n $.can = () => {\n return this.can(name);\n };\n\n return $;\n }\n\n protected async followRemote(\n link: HttpClientLink,\n config: Partial<ServerRequestConfigEntry> = {},\n options: ClientRequestOptions = {},\n ): Promise<FetchResponse> {\n options.request ??= {};\n options.request.headers = new Headers(options.request.headers);\n\n const als = this.alepha.store.get(\"alepha.http.request\");\n if (als?.headers.authorization) {\n options.request.headers.set(\"authorization\", als.headers.authorization);\n }\n\n const context = this.alepha.context.get(\"context\");\n if (typeof context === \"string\") {\n options.request.headers.set(\"x-request-id\", context);\n }\n\n const action = {\n ...link,\n // schema is not used in the client,\n // we assume that TypeScript will check\n schema: {\n body: t.any(),\n response: t.any(),\n },\n };\n\n // prefix with service when host is not defined (e.g. browser)\n if (!link.host && link.service) {\n action.path = `/${link.service}${action.path}`;\n }\n\n action.path = `${action.prefix ?? \"/api\"}${action.path}`;\n action.prefix = undefined; // prefix is not used in the client\n\n // else, make a request\n return this.httpClient.fetchAction({\n host: link.host,\n config,\n options,\n action: action as any, // schema.body TAny is not accepted\n });\n }\n\n protected async getLinkByName(\n name: string,\n options: ClientScope = {},\n ): Promise<HttpClientLink> {\n if (\n this.alepha.isBrowser() &&\n !this.alepha.store.get(\"alepha.server.request.apiLinks\")\n ) {\n await this.fetchLinks();\n }\n\n const link = this.links.find(\n (a) =>\n a.name === name && (!options.service || options.service === a.service),\n );\n\n if (!link) {\n const error = new UnauthorizedError(`Action ${name} not found.`);\n // mimic http error handling\n await this.alepha.events.emit(\"client:onError\", {\n route: link,\n error,\n });\n throw error;\n }\n\n if (options.hostname) {\n return {\n ...link,\n host: options.hostname,\n };\n }\n\n return link;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface HttpClientLink {\n name: string;\n path: string;\n method?: string;\n kind?: string;\n contentType?: string;\n service?: string;\n secured?: boolean | SecureOptions;\n prefix?: string;\n group?: string;\n // -- server only --\n host?: string;\n schema?: RequestConfigSchema;\n handler?: (\n request: ServerRequest,\n options: ClientRequestOptions,\n ) => Async<ServerResponseBody>;\n}\n\nexport interface ClientScope {\n service?: string;\n hostname?: string;\n}\n\nexport type HttpVirtualClient<T> = {\n [K in keyof T as T[K] extends ActionPrimitive<RequestConfigSchema>\n ? K\n : never]: T[K] extends ActionPrimitive<infer Schema>\n ? VirtualAction<Schema>\n : never;\n} & {\n [K in keyof T as T[K] extends SsePrimitive<SseConfigSchema>\n ? K\n : never]: T[K] extends SsePrimitive<infer Schema>\n ? VirtualSse<Schema>\n : never;\n};\n\nexport interface VirtualAction<T extends RequestConfigSchema>\n extends Pick<ActionPrimitive<T>, \"name\" | \"run\" | \"fetch\"> {\n (\n config?: ClientRequestEntry<T>,\n opts?: ClientRequestOptions,\n ): Promise<ClientRequestResponse<T>>;\n can: () => boolean;\n}\n\nexport interface VirtualSse<T extends SseConfigSchema> {\n (config?: SseRequestEntry<T>): Promise<SseStream<SseEventData<T>>>;\n name: string;\n can: () => boolean;\n}\n","import { $inject, KIND } from \"alepha\";\nimport {\n type ClientScope,\n type HttpVirtualClient,\n LinkProvider,\n} from \"../providers/LinkProvider.ts\";\n\n/**\n * Create a new client.\n */\nexport const $client = <T extends object>(\n scope?: ClientScope,\n): HttpVirtualClient<T> => {\n return $inject(LinkProvider).client<T>(scope);\n};\n\n$client[KIND] = \"$client\";\n","import { createPrimitive, KIND, Primitive } from \"alepha\";\nimport type { ServiceAccountPrimitive } from \"alepha/security\";\nimport type { ProxyPrimitiveOptions } from \"alepha/server/proxy\";\n\n/**\n * $remote is a primitive that allows you to define remote service access.\n *\n * Use it only when you have 2 or more services that need to communicate with each other.\n *\n * All remote services can be exposed as actions, ... or not.\n *\n * You can add a service account if you want to use a security layer.\n */\nexport const $remote = (options: RemotePrimitiveOptions) => {\n return createPrimitive(RemotePrimitive, options);\n};\n\nexport interface RemotePrimitiveOptions {\n /**\n * The URL of the remote service.\n * You can use a function to generate the URL dynamically.\n * You probably should use $env(env) to get the URL from the environment.\n *\n * @example\n * ```ts\n * import { $remote } from \"alepha/server\";\n * import { $inject, t } from \"alepha\";\n *\n * class App {\n * env = $env(t.object({\n * REMOTE_URL: t.text({default: \"http://localhost:3000\"}),\n * }));\n * remote = $remote({\n * url: this.env.REMOTE_URL,\n * });\n * }\n * ```\n */\n url: string | (() => string);\n\n /**\n * The name of the remote service.\n *\n * @default Member of the class containing the remote service.\n */\n name?: string;\n\n /**\n * If true, all methods of the remote service will be exposed as actions in this context.\n * > Note: Proxy will never use the service account, it just... proxies the request.\n */\n proxy?:\n | boolean\n | Partial<\n ProxyPrimitiveOptions & {\n /**\n * If true, the remote service won't be available internally, only through the proxy.\n */\n noInternal: boolean;\n }\n >;\n\n /**\n * For communication between the server and the remote service with a security layer.\n * This will be used for internal communication and will not be exposed to the client.\n */\n serviceAccount?: ServiceAccountPrimitive;\n}\n\nexport class RemotePrimitive extends Primitive<RemotePrimitiveOptions> {\n public get name(): string {\n return this.options.name ?? this.config.propertyKey;\n }\n}\n\n$remote[KIND] = RemotePrimitive;\n","import { $module } from \"alepha\";\nimport { apiLinksAtom } from \"./atoms/apiLinksAtom.ts\";\nimport { linkOptionsAtom } from \"./atoms/linkOptionsAtom.ts\";\nimport { $client } from \"./primitives/$client.ts\";\nimport { $remote } from \"./primitives/$remote.ts\";\nimport { LinkProvider } from \"./providers/LinkProvider.ts\";\nimport { BatchCollector } from \"./services/BatchCollector.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./atoms/apiLinksAtom.ts\";\nexport * from \"./atoms/linkOptionsAtom.ts\";\nexport * from \"./primitives/$client.ts\";\nexport * from \"./primitives/$remote.ts\";\nexport * from \"./providers/LinkProvider.ts\";\nexport * from \"./schemas/apiLinksResponseSchema.ts\";\nexport * from \"./services/BatchCollector.ts\";\n\n// ---------------------------------------------------------------- -----------------------------------------------------\n\nexport const AlephaServerLinks = $module({\n name: \"alepha.server.links\",\n atoms: [apiLinksAtom, linkOptionsAtom],\n primitives: [$remote, $client],\n services: [LinkProvider, BatchCollector],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.browser.js","names":[],"sources":["../../../src/server/links/schemas/apiLinksResponseSchema.ts","../../../src/server/links/atoms/apiLinksAtom.ts","../../../src/server/links/atoms/linkOptionsAtom.ts","../../../src/server/links/services/BatchCollector.ts","../../../src/server/links/providers/LinkProvider.ts","../../../src/server/links/primitives/$client.ts","../../../src/server/links/primitives/$remote.ts","../../../src/server/links/index.browser.ts"],"sourcesContent":["import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const apiActionSchema = t.object({\n path: t.text({\n description: \"Pathname used to access the action.\",\n }),\n\n method: t.optional(\n t.text({\n description:\n \"HTTP method. Omitted when GET (the default for ~75% of actions).\",\n }),\n ),\n\n contentType: t.optional(\n t.text({\n description:\n \"Content type for the request body. Only present for non-JSON types (e.g. 'multipart/form-data'). When absent, defaults to application/json.\",\n }),\n ),\n\n kind: t.optional(\n t.text({\n description:\n \"Action kind. Used to distinguish special action types (e.g. 'sse' for Server-Sent Events streams).\",\n }),\n ),\n\n service: t.optional(\n t.text({\n description:\n \"Service name associated with the action, used for service discovery and routing.\",\n }),\n ),\n});\n\nexport const apiRegistryResponseSchema = t.object({\n prefix: t.optional(t.text()),\n\n actions: t.record(t.text(), apiActionSchema),\n\n permissions: t.optional(t.array(t.text())),\n});\n\nexport type ApiRegistryResponse = Static<typeof apiRegistryResponseSchema>;\nexport type ApiAction = Static<typeof apiActionSchema>;\n\n/**\n * @deprecated Use `apiRegistryResponseSchema` and `ApiRegistryResponse` instead.\n */\nexport const apiLinksResponseSchema = apiRegistryResponseSchema;\n\n/**\n * @deprecated Use `ApiRegistryResponse` instead.\n */\nexport type ApiLinksResponse = ApiRegistryResponse;\n\n/**\n * @deprecated Use `ApiAction` instead.\n */\nexport type ApiLink = ApiAction;\n","import { $atom, t } from \"alepha\";\nimport { apiRegistryResponseSchema } from \"../schemas/apiLinksResponseSchema.ts\";\n\nexport const apiLinksAtom = $atom({\n name: \"alepha.server.request.apiLinks\",\n schema: t.optional(apiRegistryResponseSchema),\n});\n","import { $atom, t } from \"alepha\";\n\nexport const linkOptionsAtom = $atom({\n name: \"alepha.server.links.options\",\n description: \"Configuration options for the links module.\",\n schema: t.object({\n batch: t.boolean({\n description: \"Enable batch collection for browser-side calls.\",\n default: true,\n }),\n }),\n default: {\n batch: true,\n },\n});\n","import { $inject } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { HttpClient, HttpError } from \"alepha/server\";\n\n/**\n * Collects browser-side action calls within a microtask and\n * sends them as a single `POST /api/_batch` request.\n *\n * Key behaviors:\n * - Single call in the window → direct HTTP call (no batch overhead)\n * - Multiple calls → coalesced into one batch request\n * - Same action + same params/query/body → deduplicated, result shared\n * - Exceeding MAX_BATCH_SIZE → split into multiple batch calls\n * - Transport failure → all pending promises reject\n */\nexport class BatchCollector {\n protected static readonly MAX_BATCH_SIZE = 20;\n\n protected readonly log = $logger();\n protected readonly httpClient = $inject(HttpClient);\n\n protected pending: PendingBatchEntry[] = [];\n protected scheduled = false;\n\n /**\n * Add an action call to the batch. Returns the result when the batch resolves.\n */\n public add(entry: BatchEntry): Promise<any> {\n return new Promise((resolve, reject) => {\n this.pending.push({ entry, resolve, reject });\n\n if (!this.scheduled) {\n this.scheduled = true;\n setTimeout(() => {\n this.scheduled = false;\n this.flush().catch((err) => this.log.error(err));\n }, 10);\n }\n });\n }\n\n protected async flush(): Promise<void> {\n const batch = this.pending.splice(0);\n\n if (batch.length === 0) return;\n\n // Single request — skip batching, call directly via follow\n if (batch.length === 1) {\n const item = batch[0];\n try {\n const result = await item.entry.directCall();\n item.resolve(result);\n } catch (error) {\n item.reject(error);\n }\n return;\n }\n\n // Deduplicate: same action + same params → share result\n const { unique, indexMap } = this.dedupe(batch);\n\n // Split into chunks of MAX_BATCH_SIZE\n const chunks = this.chunk(unique, BatchCollector.MAX_BATCH_SIZE);\n\n try {\n const allResults = (\n await Promise.all(\n chunks.map((chunk) => {\n const actions = [...new Set(chunk.map((b) => b.entry.action))].join(\n \",\",\n );\n\n return this.httpClient\n .fetch(`/api/_batch?actions=${actions}`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(\n chunk.map((b) => ({\n action: b.entry.action,\n params: b.entry.params,\n query: b.entry.query,\n body: b.entry.body,\n })),\n ),\n })\n .then((res) => res.data as BatchResponse[]);\n }),\n )\n ).flat();\n\n // Distribute results back (including deduped slots)\n for (let i = 0; i < batch.length; i++) {\n const result = allResults[indexMap[i]];\n if (result.status >= 400) {\n batch[i].reject(\n new HttpError({\n message:\n result.error ?? `${result.action} failed (${result.status})`,\n status: result.status,\n }),\n );\n } else {\n batch[i].resolve(result.data);\n }\n }\n } catch (error) {\n // Transport-level failure — reject all pending promises\n for (const item of batch) {\n item.reject(error);\n }\n }\n }\n\n protected dedupe(batch: PendingBatchEntry[]): {\n unique: PendingBatchEntry[];\n indexMap: number[];\n } {\n const seen = new Map<string, number>();\n const unique: PendingBatchEntry[] = [];\n const indexMap: number[] = [];\n\n for (const item of batch) {\n const key = `${item.entry.action}:${JSON.stringify({\n params: item.entry.params,\n query: item.entry.query,\n body: item.entry.body,\n })}`;\n\n const existing = seen.get(key);\n if (existing !== undefined) {\n indexMap.push(existing);\n } else {\n const idx = unique.length;\n seen.set(key, idx);\n unique.push(item);\n indexMap.push(idx);\n }\n }\n\n return { unique, indexMap };\n }\n\n protected chunk<T>(arr: T[], size: number): T[][] {\n const chunks: T[][] = [];\n for (let i = 0; i < arr.length; i += size) {\n chunks.push(arr.slice(i, i + size));\n }\n return chunks;\n }\n}\n\n// ---\n\nexport interface BatchEntry {\n action: string;\n params?: Record<string, any>;\n query?: Record<string, any>;\n body?: Record<string, any>;\n directCall: () => Promise<any>;\n}\n\ninterface PendingBatchEntry {\n entry: BatchEntry;\n resolve: (value: any) => void;\n reject: (reason: any) => void;\n}\n\ninterface BatchResponse {\n action: string;\n status: number;\n data?: any;\n error?: string;\n}\n","import { $inject, $state, Alepha, AlephaError, type Async, t } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport type { SecureOptions } from \"alepha/security\";\nimport {\n type ActionPrimitive,\n type ClientRequestEntry,\n type ClientRequestOptions,\n type ClientRequestResponse,\n type FetchResponse,\n HttpClient,\n type RequestConfigSchema,\n ServerReply,\n type ServerRequest,\n type ServerRequestConfigEntry,\n type ServerResponseBody,\n type SseConfigSchema,\n type SseEventData,\n type SsePrimitive,\n type SseRequestEntry,\n type SseStream,\n UnauthorizedError,\n} from \"alepha/server\";\nimport { linkOptionsAtom } from \"../atoms/linkOptionsAtom.ts\";\nimport {\n type ApiRegistryResponse,\n apiRegistryResponseSchema,\n} from \"../schemas/apiLinksResponseSchema.ts\";\nimport { BatchCollector } from \"../services/BatchCollector.ts\";\n\n/**\n * Browser, SSR friendly, service to handle links.\n */\nexport class LinkProvider {\n static path = {\n apiLinks: \"/api/_links\",\n };\n\n protected readonly log = $logger();\n protected readonly alepha = $inject(Alepha);\n protected readonly httpClient = $inject(HttpClient);\n\n // Server-side: all registered links (local + remote), keyed by name\n protected serverLinkMap = new Map<string, HttpClientLink>();\n\n // Browser/SSR: parsed from the registry response\n protected actionMap = new Map<string, HttpClientLink>();\n protected permissions = new Set<string>();\n protected lastLoadedRegistry: ApiRegistryResponse | null = null;\n\n // Browser-only: batch collector for coalescing multiple calls\n protected batchCollector?: BatchCollector;\n\n protected readonly options = $state(linkOptionsAtom);\n\n /**\n * Get applicative links registered on the server.\n * This does not include lazy-loaded remote links.\n */\n public getServerLinks(): HttpClientLink[] {\n if (this.alepha.isBrowser()) {\n this.log.warn(\n \"Getting server links in the browser is not supported. Use `fetchLinks` to get links from the server.\",\n );\n return [];\n }\n\n return [...this.serverLinkMap.values()];\n }\n\n /**\n * Register a new link for the application.\n */\n public registerLink(link: HttpClientLink): void {\n if (this.alepha.isBrowser()) {\n this.log.warn(\n \"Registering links in the browser is not supported. Use `fetchLinks` to get links from the server.\",\n );\n return;\n }\n\n if (!link.handler && !link.host) {\n throw new AlephaError(\n \"Can't create link - 'handler' or 'host' is required\",\n );\n }\n\n // Detect duplicate local actions (programming error)\n const existing = this.serverLinkMap.get(link.name);\n if (existing?.handler && link.handler) {\n throw new AlephaError(\n `Duplicate action name \"${link.name}\". Each action must have a unique name.`,\n );\n }\n\n this.serverLinkMap.set(link.name, link);\n }\n\n /**\n * Load the registry response into internal stores (actionMap, permissions, definitions).\n * Called when storing from atom/fetch/SSR.\n */\n protected loadRegistry(registry: ApiRegistryResponse): void {\n this.lastLoadedRegistry = registry;\n this.permissions.clear();\n this.actionMap.clear();\n\n for (const [name, action] of Object.entries(registry.actions)) {\n this.actionMap.set(name, {\n name,\n path: action.path,\n kind: action.kind,\n method: action.method,\n contentType: action.contentType,\n service: action.service,\n });\n }\n\n if (registry.permissions) {\n for (const p of registry.permissions) {\n this.permissions.add(p);\n }\n }\n }\n\n public get links(): HttpClientLink[] {\n const registry = this.alepha.store.get(\"alepha.server.request.apiLinks\");\n\n if (registry) {\n if (this.alepha.isBrowser()) {\n // Browser side: use the parsed action map\n // Reload when registry changes (e.g. after login provides new authenticated links)\n if (this.actionMap.size === 0 || registry !== this.lastLoadedRegistry) {\n this.loadRegistry(registry);\n }\n return [...this.actionMap.values()];\n }\n\n // SSR side: map registry actions back to full server links\n const links: HttpClientLink[] = [];\n for (const name of Object.keys(registry.actions)) {\n const originalLink = this.serverLinkMap.get(name);\n if (originalLink) {\n links.push(originalLink);\n }\n }\n return links;\n }\n\n return [...this.serverLinkMap.values()];\n }\n\n /**\n * Force browser to refresh links from the server.\n */\n public async fetchLinks(): Promise<HttpClientLink[]> {\n const { data } = await this.httpClient.fetch(\n `${LinkProvider.path.apiLinks}`,\n {\n method: \"GET\",\n schema: {\n response: apiRegistryResponseSchema,\n },\n },\n );\n\n this.alepha.store.set(\"alepha.server.request.apiLinks\", data);\n this.loadRegistry(data);\n\n return [...this.actionMap.values()];\n }\n\n /**\n * Create a virtual client that can be used to call actions.\n *\n * Use js Proxy under the hood.\n */\n public client<T extends object>(\n scope: ClientScope = {},\n ): HttpVirtualClient<T> {\n return new Proxy<HttpVirtualClient<T>>({} as HttpVirtualClient<T>, {\n get: (_, prop) => {\n if (typeof prop !== \"string\") {\n return;\n }\n\n return this.createVirtualAction<RequestConfigSchema>(prop, scope);\n },\n });\n }\n\n /**\n * Check if a link with the given name exists or a permission matches.\n *\n * Action names never contain colons. Permission names always do.\n * - `can(\"getUsers\")` → O(1) map lookup\n * - `can(\"admin:*\")` → wildcard match against permissions set\n * - `can(\"admin:user:read\")` → O(1) set lookup\n */\n public can(name: string): boolean {\n // Action check — O(1) map lookup\n if (this.actionMap.size > 0) {\n if (this.actionMap.has(name)) return true;\n } else {\n // Fallback for server-side where actionMap may not be populated\n if (this.serverLinkMap.has(name)) return true;\n // Also check links getter (for SSR with atom)\n if (this.links.some((link) => link.name === name)) return true;\n }\n\n // Permission check — wildcard matching\n if (name.includes(\":\")) {\n if (name.endsWith(\"*\")) {\n const prefix = name.slice(0, -1);\n for (const p of this.permissions) {\n if (p.startsWith(prefix)) return true;\n }\n return false;\n }\n return this.permissions.has(name);\n }\n\n return false;\n }\n\n /**\n * Resolve a link by its name and call it.\n * - If link is local, it will call the local handler.\n * - If link is remote, it will make a fetch request to the remote server.\n */\n public async follow(\n name: string,\n config: Partial<ServerRequestConfigEntry> = {},\n options: ClientRequestOptions & ClientScope = {},\n ): Promise<any> {\n this.log.trace(\"Following link\", { name, config, options });\n const link = await this.getLinkByName(name, options);\n\n // if a handler is defined, use it (ssr)\n if (link.handler && !options.request) {\n this.log.trace(\"Local link found\", { name });\n return link.handler(\n {\n method: link.method,\n url: new URL(`http://localhost${link.path}`),\n query: config.query ?? {},\n body: config.body ?? {},\n params: config.params ?? {},\n headers: config.headers ?? {},\n metadata: {},\n reply: new ServerReply(),\n } as Partial<ServerRequest> as ServerRequest,\n options,\n );\n }\n\n this.log.trace(\"Remote link found\", {\n name,\n host: link.host,\n service: link.service,\n });\n\n // Browser-only: use batch collector for calls without explicit host\n if (this.options.batch && this.alepha.isBrowser() && !link.host) {\n this.batchCollector ??= this.alepha.inject(BatchCollector);\n return this.batchCollector.add({\n action: name,\n params: config.params as any,\n query: config.query as any,\n body: config.body as any,\n directCall: () =>\n this.followRemote(link, config, options).then((r) => r.data),\n });\n }\n\n return this.followRemote(link, config, options).then(\n (response) => response.data,\n );\n }\n\n protected createVirtualAction<T extends RequestConfigSchema>(\n name: string,\n scope: ClientScope = {},\n ): VirtualAction<T> {\n const $: VirtualAction<T> = async (\n config: any = {},\n options: ClientRequestOptions = {},\n ) => {\n return this.follow(name, config, {\n ...scope,\n ...options,\n });\n };\n\n Object.defineProperty($, \"name\", {\n value: name,\n writable: false,\n });\n\n $.run = async (config: any = {}, options: ClientRequestOptions = {}) => {\n return this.follow(name, config, {\n ...scope,\n ...options,\n });\n };\n\n $.fetch = async (config: any = {}, options: ClientRequestOptions = {}) => {\n const link = await this.getLinkByName(name, scope);\n return this.followRemote(link, config, options);\n };\n\n $.can = () => {\n return this.can(name);\n };\n\n return $;\n }\n\n protected async followRemote(\n link: HttpClientLink,\n config: Partial<ServerRequestConfigEntry> = {},\n options: ClientRequestOptions = {},\n ): Promise<FetchResponse> {\n options.request ??= {};\n options.request.headers = new Headers(options.request.headers);\n\n const als = this.alepha.store.get(\"alepha.http.request\");\n if (als?.headers.authorization) {\n options.request.headers.set(\"authorization\", als.headers.authorization);\n }\n\n const context = this.alepha.context.get(\"context\");\n if (typeof context === \"string\") {\n options.request.headers.set(\"x-request-id\", context);\n }\n\n const action = {\n ...link,\n // schema is not used in the client,\n // we assume that TypeScript will check\n schema: {\n body: t.any(),\n response: t.any(),\n },\n };\n\n // prefix with service when host is not defined (e.g. browser)\n if (!link.host && link.service) {\n action.path = `/${link.service}${action.path}`;\n }\n\n action.path = `${action.prefix ?? \"/api\"}${action.path}`;\n action.prefix = undefined; // prefix is not used in the client\n\n // else, make a request\n return this.httpClient.fetchAction({\n host: link.host,\n config,\n options,\n action: action as any, // schema.body TAny is not accepted\n });\n }\n\n protected async getLinkByName(\n name: string,\n options: ClientScope = {},\n ): Promise<HttpClientLink> {\n if (\n this.alepha.isBrowser() &&\n !this.alepha.store.get(\"alepha.server.request.apiLinks\")\n ) {\n await this.fetchLinks();\n }\n\n const link = this.links.find(\n (a) =>\n a.name === name && (!options.service || options.service === a.service),\n );\n\n if (!link) {\n const error = new UnauthorizedError(`Action ${name} not found.`);\n // mimic http error handling\n await this.alepha.events.emit(\"client:onError\", {\n route: link,\n error,\n });\n throw error;\n }\n\n if (options.hostname) {\n return {\n ...link,\n host: options.hostname,\n };\n }\n\n return link;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface HttpClientLink {\n name: string;\n path: string;\n method?: string;\n kind?: string;\n contentType?: string;\n service?: string;\n secured?: boolean | SecureOptions;\n prefix?: string;\n group?: string;\n // -- server only --\n host?: string;\n schema?: RequestConfigSchema;\n handler?: (\n request: ServerRequest,\n options: ClientRequestOptions,\n ) => Async<ServerResponseBody>;\n}\n\nexport interface ClientScope {\n service?: string;\n hostname?: string;\n}\n\nexport type HttpVirtualClient<T> = {\n [K in keyof T as T[K] extends ActionPrimitive<RequestConfigSchema>\n ? K\n : never]: T[K] extends ActionPrimitive<infer Schema>\n ? VirtualAction<Schema>\n : never;\n} & {\n [K in keyof T as T[K] extends SsePrimitive<SseConfigSchema>\n ? K\n : never]: T[K] extends SsePrimitive<infer Schema>\n ? VirtualSse<Schema>\n : never;\n};\n\nexport interface VirtualAction<T extends RequestConfigSchema>\n extends Pick<ActionPrimitive<T>, \"name\" | \"run\" | \"fetch\"> {\n (\n config?: ClientRequestEntry<T>,\n opts?: ClientRequestOptions,\n ): Promise<ClientRequestResponse<T>>;\n can: () => boolean;\n}\n\nexport interface VirtualSse<T extends SseConfigSchema> {\n (config?: SseRequestEntry<T>): Promise<SseStream<SseEventData<T>>>;\n name: string;\n can: () => boolean;\n}\n","import { $inject, KIND } from \"alepha\";\nimport {\n type ClientScope,\n type HttpVirtualClient,\n LinkProvider,\n} from \"../providers/LinkProvider.ts\";\n\n/**\n * Create a new client.\n */\nexport const $client = <T extends object>(\n scope?: ClientScope,\n): HttpVirtualClient<T> => {\n return $inject(LinkProvider).client<T>(scope);\n};\n\n$client[KIND] = \"$client\";\n","import { createPrimitive, KIND, Primitive } from \"alepha\";\nimport type { ServiceAccountPrimitive } from \"alepha/security\";\nimport type { ProxyPrimitiveOptions } from \"alepha/server/proxy\";\n\n/**\n * $remote is a primitive that allows you to define remote service access.\n *\n * Use it only when you have 2 or more services that need to communicate with each other.\n *\n * All remote services can be exposed as actions, ... or not.\n *\n * You can add a service account if you want to use a security layer.\n */\nexport const $remote = (options: RemotePrimitiveOptions) => {\n return createPrimitive(RemotePrimitive, options);\n};\n\nexport interface RemotePrimitiveOptions {\n /**\n * The URL of the remote service.\n * You can use a function to generate the URL dynamically.\n * You probably should use $env(env) to get the URL from the environment.\n *\n * @example\n * ```ts\n * import { $remote } from \"alepha/server\";\n * import { $inject, t } from \"alepha\";\n *\n * class App {\n * env = $env(t.object({\n * REMOTE_URL: t.text({default: \"http://localhost:3000\"}),\n * }));\n * remote = $remote({\n * url: this.env.REMOTE_URL,\n * });\n * }\n * ```\n */\n url: string | (() => string);\n\n /**\n * The name of the remote service.\n *\n * @default Member of the class containing the remote service.\n */\n name?: string;\n\n /**\n * If true, all methods of the remote service will be exposed as actions in this context.\n * > Note: Proxy will never use the service account, it just... proxies the request.\n */\n proxy?:\n | boolean\n | Partial<\n ProxyPrimitiveOptions & {\n /**\n * If true, the remote service won't be available internally, only through the proxy.\n */\n noInternal: boolean;\n }\n >;\n\n /**\n * For communication between the server and the remote service with a security layer.\n * This will be used for internal communication and will not be exposed to the client.\n */\n serviceAccount?: ServiceAccountPrimitive;\n}\n\nexport class RemotePrimitive extends Primitive<RemotePrimitiveOptions> {\n public get name(): string {\n return this.options.name ?? this.config.propertyKey;\n }\n}\n\n$remote[KIND] = RemotePrimitive;\n","import { $module } from \"alepha\";\nimport { apiLinksAtom } from \"./atoms/apiLinksAtom.ts\";\nimport { linkOptionsAtom } from \"./atoms/linkOptionsAtom.ts\";\nimport { $client } from \"./primitives/$client.ts\";\nimport { $remote } from \"./primitives/$remote.ts\";\nimport { LinkProvider } from \"./providers/LinkProvider.ts\";\nimport { BatchCollector } from \"./services/BatchCollector.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./atoms/apiLinksAtom.ts\";\nexport * from \"./atoms/linkOptionsAtom.ts\";\nexport * from \"./primitives/$client.ts\";\nexport * from \"./primitives/$remote.ts\";\nexport * from \"./providers/LinkProvider.ts\";\nexport * from \"./schemas/apiLinksResponseSchema.ts\";\nexport * from \"./services/BatchCollector.ts\";\n\n// ---------------------------------------------------------------- -----------------------------------------------------\n\nexport const AlephaServerLinks = $module({\n name: \"alepha.server.links\",\n atoms: [apiLinksAtom, linkOptionsAtom],\n primitives: [$remote, $client],\n services: [LinkProvider, BatchCollector],\n});\n"],"mappings":"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"}
|