alepha 0.20.5 → 0.20.7
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/AGENTS.md +0 -1
- package/CLAUDE.md +0 -1
- package/assets/agents-template.md +0 -1
- package/dist/api/audits/index.browser.js +1 -0
- package/dist/api/audits/index.browser.js.map +1 -1
- package/dist/api/audits/index.d.ts +701 -654
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +24 -1
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.browser.js +1 -0
- package/dist/api/files/index.browser.js.map +1 -1
- package/dist/api/files/index.d.ts +193 -166
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +52 -0
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.browser.js +40 -14
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +639 -333
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +495 -162
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +222 -188
- package/dist/api/keys/index.d.ts.map +1 -1
- package/dist/api/keys/index.js +54 -0
- package/dist/api/keys/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +265 -236
- 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.d.ts +100 -97
- package/dist/api/organizations/index.d.ts.map +1 -1
- package/dist/api/organizations/index.js.map +1 -1
- package/dist/api/parameters/index.d.ts +332 -314
- 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.d.ts +431 -376
- package/dist/api/payments/index.d.ts.map +1 -1
- package/dist/api/payments/index.js +202 -87
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/subscriptions/index.d.ts +1695 -0
- package/dist/api/subscriptions/index.d.ts.map +1 -0
- package/dist/api/subscriptions/index.js +1919 -0
- package/dist/api/subscriptions/index.js.map +1 -0
- package/dist/api/users/index.d.ts +1001 -844
- 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 +123 -122
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/batch/index.js.map +1 -1
- package/dist/bucket/index.d.ts +21 -2
- 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 +134 -7
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/core/index.js +181 -15
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/core/index.workerd.js +181 -15
- package/dist/cache/core/index.workerd.js.map +1 -1
- package/dist/cache/database/index.d.ts +156 -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.d.ts +3 -2
- package/dist/cache/redis/index.d.ts.map +1 -1
- 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 +142 -128
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +160 -13
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.d.ts +3 -2
- package/dist/cli/devtools/index.d.ts.map +1 -1
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +346 -290
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +106 -7
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +12 -11
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/command/index.d.ts +6 -5
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +1 -1
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +119 -118
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +1 -1
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +1 -1
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/crypto/index.browser.js.map +1 -1
- package/dist/crypto/index.d.ts +3 -2
- package/dist/crypto/index.d.ts.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.d.ts +3 -2
- package/dist/email/core/index.d.ts.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.d.ts +7 -6
- package/dist/email/smtp/index.d.ts.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.d.ts +5 -4
- package/dist/lock/core/index.d.ts.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.d.ts +10 -9
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.d.ts +9 -8
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +9 -3
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +31 -10
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +33 -14
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +31 -10
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.d.ts +6 -5
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/orm/postgres/index.js.map +1 -1
- package/dist/queue/core/index.d.ts +5 -4
- package/dist/queue/core/index.d.ts.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.d.ts +3 -2
- package/dist/queue/redis/index.d.ts.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.d.ts +5 -0
- package/dist/react/form/index.d.ts.map +1 -1
- package/dist/react/form/index.js +8 -4
- 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.d.ts +2 -1
- package/dist/react/i18n/index.d.ts.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.d.ts +206 -205
- package/dist/react/router/index.d.ts.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.d.ts +11 -11
- package/dist/react/ui/index.d.ts.map +1 -1
- package/dist/react/ui/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 +25 -2
- 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 +29 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.d.ts +82 -35
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +56 -3
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +163 -158
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +16 -4
- 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.d.ts +35 -34
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.d.ts +7 -6
- package/dist/server/cors/index.d.ts.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.d.ts +16 -15
- package/dist/server/health/index.d.ts.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.d.ts +51 -50
- package/dist/server/links/index.d.ts.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.d.ts +6 -5
- package/dist/server/rate-limit/index.d.ts.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.d.ts +2 -1
- package/dist/server/swagger/index.d.ts.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.d.ts +3 -2
- package/dist/topic/redis/index.d.ts.map +1 -1
- package/dist/topic/redis/index.js.map +1 -1
- package/package.json +33 -39
- package/src/api/audits/controllers/AdminAuditController.ts +29 -0
- package/src/api/audits/entities/audits.ts +1 -0
- package/src/api/files/controllers/FileController.ts +24 -0
- package/src/api/files/entities/files.ts +1 -0
- package/src/api/files/services/FileService.ts +41 -0
- package/src/api/jobs/__tests__/$job.spec.ts +501 -24
- package/src/api/jobs/entities/jobExecutionEntity.ts +4 -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 +385 -147
- package/src/api/jobs/providers/JobQueueProvider.ts +43 -18
- package/src/api/jobs/schemas/jobConfigAtom.ts +9 -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/entities/apiKeyEntity.ts +1 -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/payments/controllers/MockCheckoutController.ts +146 -0
- package/src/api/payments/index.ts +3 -0
- package/src/api/payments/providers/MemoryPaymentProvider.ts +9 -4
- package/src/api/payments/providers/PaymentProvider.ts +25 -9
- package/src/api/payments/services/PaymentService.ts +3 -0
- package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
- package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
- package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
- package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
- package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
- package/src/api/subscriptions/entities/subscriptions.ts +68 -0
- package/src/api/subscriptions/index.ts +133 -0
- package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
- package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
- package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
- package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
- package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
- package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
- package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
- package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
- package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
- package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
- package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
- package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
- package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
- package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
- package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
- package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
- package/src/api/subscriptions/services/BillingService.ts +437 -0
- package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
- package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
- package/src/api/subscriptions/services/UsageService.ts +118 -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/__tests__/$cache.memory.spec.ts +450 -0
- package/src/cache/core/__tests__/$cache.swr.spec.ts +394 -0
- package/src/cache/core/index.ts +16 -0
- package/src/cache/core/primitives/$cache.ts +367 -24
- 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 +33 -3
- package/src/cli/core/tasks/BuildSitemapTask.ts +7 -0
- package/src/cli/core/tasks/BuildVercelTask.ts +82 -3
- package/src/cli/core/templates/agentMd.ts +39 -4
- package/src/cli/core/templates/biomeJson.ts +25 -1
- package/src/cli/core/templates/saasAdminLayoutTsx.ts +2 -2
- package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +117 -0
- package/src/cli/platform/__tests__/detectResources.spec.ts +96 -0
- package/src/cli/platform/adapters/CloudflareAdapter.ts +104 -7
- package/src/cli/platform/atoms/platformOptions.ts +13 -0
- package/src/cli/platform/commands/platform.ts +7 -1
- package/src/cli/platform/schemas/platform.ts +1 -0
- package/src/cli/platform/services/CloudflareApi.ts +61 -0
- package/src/cli/platform/services/PlatformOrchestrator.ts +9 -4
- package/src/core/__tests__/$module.spec.ts +2 -2
- package/src/core/primitives/$module.ts +4 -4
- package/src/mcp/providers/McpServerProvider.ts +1 -1
- package/src/orm/core/providers/DatabaseTypeProvider.ts +9 -3
- package/src/orm/core/providers/drivers/DatabaseProvider.ts +1 -1
- package/src/orm/core/schemas/insertSchema.ts +10 -2
- package/src/orm/core/services/Repository.ts +27 -7
- package/src/react/form/hooks/useFormState.ts +8 -1
- package/src/react/form/index.ts +10 -1
- package/src/react/form/services/FormModel.ts +9 -3
- package/src/scheduler/index.ts +14 -0
- package/src/scheduler/providers/CronProvider.ts +13 -0
- package/src/security/atoms/currentTenantAtom.ts +34 -0
- package/src/security/index.browser.ts +1 -0
- package/src/security/index.ts +12 -1
- package/src/security/primitives/$issuer.ts +17 -1
- package/src/security/providers/SecurityProvider.ts +37 -0
- package/src/server/auth/__tests__/validateRedirectUri.spec.ts +78 -0
- package/src/server/auth/providers/ServerAuthProvider.ts +21 -5
- package/tsconfig.base.json +2 -1
- package/dist/react/websocket/index.d.ts +0 -117
- package/dist/react/websocket/index.d.ts.map +0 -1
- package/dist/react/websocket/index.js +0 -108
- package/dist/react/websocket/index.js.map +0 -1
- package/dist/websocket/index.browser.js +0 -844
- package/dist/websocket/index.browser.js.map +0 -1
- package/dist/websocket/index.d.ts +0 -876
- package/dist/websocket/index.d.ts.map +0 -1
- package/dist/websocket/index.js +0 -1175
- package/dist/websocket/index.js.map +0 -1
- package/src/react/websocket/hooks/useRoom.tsx +0 -251
- package/src/react/websocket/index.ts +0 -7
- package/src/websocket/__tests__/$channel.spec.ts +0 -30
- package/src/websocket/__tests__/$websocket-new.spec.ts +0 -195
- package/src/websocket/__tests__/RoomManager.spec.ts +0 -146
- package/src/websocket/__tests__/websocket-integration.spec.ts +0 -951
- package/src/websocket/errors/WebSocketError.ts +0 -34
- package/src/websocket/index.browser.ts +0 -25
- package/src/websocket/index.shared.ts +0 -8
- package/src/websocket/index.ts +0 -85
- package/src/websocket/interfaces/WebSocketInterfaces.ts +0 -252
- package/src/websocket/primitives/$channel.ts +0 -131
- package/src/websocket/primitives/$websocket.ts +0 -107
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +0 -617
- package/src/websocket/providers/WebSocketServerProvider.ts +0 -56
- package/src/websocket/services/RoomManager.ts +0 -160
- package/src/websocket/services/WebSocketClient.ts +0 -642
- package/src/websocket/services/WebSocketTopicService.ts +0 -108
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["WebStream"],"sources":["../../../src/server/proxy/primitives/$proxy.ts","../../../src/server/proxy/providers/ServerProxyProvider.ts","../../../src/server/proxy/index.ts"],"sourcesContent":["import { type Async, createPrimitive, KIND, Primitive } from \"alepha\";\nimport type { ServerRequest } from \"alepha/server\";\n\n/**\n * Creates a proxy primitive to forward requests to another server.\n *\n * This primitive enables you to create reverse proxy functionality, allowing your Alepha server\n * to forward requests to other services while maintaining a unified API surface. It's particularly\n * useful for microservice architectures, API gateways, or when you need to aggregate multiple\n * services behind a single endpoint.\n *\n * **Key Features**\n *\n * - **Path-based routing**: Match specific paths or patterns to proxy\n * - **Dynamic targets**: Support both static and dynamic target resolution\n * - **Request/Response hooks**: Modify requests before forwarding and responses after receiving\n * - **URL rewriting**: Transform URLs before forwarding to the target\n * - **Conditional proxying**: Enable/disable proxies based on environment or conditions\n *\n * @example\n * **Basic proxy setup:**\n * ```ts\n * import { $proxy } from \"alepha/server/proxy\";\n *\n * class ApiGateway {\n * // Forward all /api/* requests to external service\n * api = $proxy({\n * path: \"/api/*\",\n * target: \"https://api.example.com\"\n * });\n * }\n * ```\n *\n * @example\n * **Dynamic target with environment-based routing:**\n * ```ts\n * class ApiGateway {\n * // Route to different environments based on configuration\n * api = $proxy({\n * path: \"/api/*\",\n * target: () => process.env.NODE_ENV === \"production\"\n * ? \"https://api.prod.example.com\"\n * : \"https://api.dev.example.com\"\n * });\n * }\n * ```\n *\n * @example\n * **Advanced proxy with request/response modification:**\n * ```ts\n * class SecureProxy {\n * secure = $proxy({\n * path: \"/secure/*\",\n * target: \"https://secure-api.example.com\",\n * beforeRequest: async (request, proxyRequest) => {\n * // Add authentication headers\n * proxyRequest.headers = {\n * ...proxyRequest.headers,\n * 'Authorization': `Bearer ${await getServiceToken()}`,\n * 'X-Forwarded-For': request.headers['x-forwarded-for'] || request.ip\n * };\n * },\n * afterResponse: async (request, proxyResponse) => {\n * // Log response for monitoring\n * console.log(`Proxied ${request.url} -> ${proxyResponse.status}`);\n * },\n * rewrite: (url) => {\n * // Remove /secure prefix when forwarding\n * url.pathname = url.pathname.replace('/secure', '');\n * }\n * });\n * }\n * ```\n *\n * @example\n * **Conditional proxy based on feature flags:**\n * ```ts\n * class FeatureProxy {\n * newApi = $proxy({\n * path: \"/v2/*\",\n * target: \"https://new-api.example.com\",\n * disabled: !process.env.ENABLE_V2_API // Disable if feature flag is off\n * });\n * }\n * ```\n */\nexport const $proxy = (options: ProxyPrimitiveOptions): ProxyPrimitive => {\n return createPrimitive(ProxyPrimitive, options);\n};\n\nexport type ProxyPrimitiveOptions = {\n /**\n * Path pattern to match for proxying requests.\n *\n * Supports wildcards and path parameters:\n * - `/api/*` - Matches all paths starting with `/api/`\n * - `/api/v1/*` - Matches all paths starting with `/api/v1/`\n * - `/users/:id` - Matches `/users/123`, `/users/abc`, etc.\n *\n * @example \"/api/*\"\n * @example \"/secure/admin/*\"\n * @example \"/users/:id/posts\"\n */\n path: string;\n\n /**\n * Target URL to which matching requests should be forwarded.\n *\n * Can be either:\n * - **Static string**: A fixed URL like `\"https://api.example.com\"`\n * - **Dynamic function**: A function that returns the URL, enabling runtime target resolution\n *\n * The target URL will be combined with the remaining path from the original request.\n *\n * @example \"https://api.example.com\"\n * @example () => process.env.API_URL || \"http://localhost:3001\"\n */\n target: string | (() => string);\n\n /**\n * Whether this proxy is disabled.\n *\n * When `true`, requests matching the path will not be proxied and will be handled\n * by other routes or return 404. Useful for feature toggles or conditional proxying.\n *\n * @default false\n * @example !process.env.ENABLE_PROXY\n */\n disabled?: boolean;\n\n /**\n * Hook called before forwarding the request to the target server.\n *\n * Use this to:\n * - Add authentication headers\n * - Modify request headers or body\n * - Add request tracking/logging\n * - Transform the request before forwarding\n *\n * @param request - The original incoming server request\n * @param proxyRequest - The request that will be sent to the target (modifiable)\n *\n * @example\n * ```ts\n * beforeRequest: async (request, proxyRequest) => {\n * proxyRequest.headers = {\n * ...proxyRequest.headers,\n * 'Authorization': `Bearer ${await getToken()}`,\n * 'X-Request-ID': generateRequestId()\n * };\n * }\n * ```\n */\n beforeRequest?: (\n request: ServerRequest,\n proxyRequest: RequestInit,\n ) => Async<void>;\n\n /**\n * Hook called after receiving the response from the target server.\n *\n * Use this to:\n * - Log response details for monitoring\n * - Add custom headers to the response\n * - Transform response data\n * - Handle error responses\n *\n * @param request - The original incoming server request\n * @param proxyResponse - The response received from the target server\n *\n * @example\n * ```ts\n * afterResponse: async (request, proxyResponse) => {\n * console.log(`Proxy ${request.method} ${request.url} -> ${proxyResponse.status}`);\n *\n * if (!proxyResponse.ok) {\n * await logError(`Proxy error: ${proxyResponse.status}`, { request, response: proxyResponse });\n * }\n * }\n * ```\n */\n afterResponse?: (\n request: ServerRequest,\n proxyResponse: Response,\n ) => Async<void>;\n\n /**\n * Function to rewrite the URL before sending to the target server.\n *\n * Use this to:\n * - Remove or add path prefixes\n * - Transform path parameters\n * - Modify query parameters\n * - Change the URL structure entirely\n *\n * The function receives a mutable URL object and should modify it in-place.\n *\n * @param url - The URL object to modify (mutable)\n *\n * @example\n * ```ts\n * // Remove /api prefix when forwarding\n * rewrite: (url) => {\n * url.pathname = url.pathname.replace('/api', '');\n * }\n * ```\n *\n * @example\n * ```ts\n * // Add version prefix\n * rewrite: (url) => {\n * url.pathname = `/v2${url.pathname}`;\n * }\n * ```\n */\n rewrite?: (url: URL) => void;\n\n // TODO: Add retry functionality\n // retry?: RetryOptions;\n};\n\nexport class ProxyPrimitive extends Primitive<ProxyPrimitiveOptions> {}\n\n$proxy[KIND] = ProxyPrimitive;\n","import { ReadableStream as WebStream } from \"node:stream/web\";\nimport { $hook, $inject, Alepha, AlephaError } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport {\n routeMethods,\n type ServerHandler,\n type ServerRequest,\n ServerRouterProvider,\n} from \"alepha/server\";\nimport { $proxy, type ProxyPrimitiveOptions } from \"../primitives/$proxy.ts\";\n\nexport class ServerProxyProvider {\n protected readonly log = $logger();\n protected readonly routerProvider = $inject(ServerRouterProvider);\n protected readonly alepha = $inject(Alepha);\n\n protected readonly configure = $hook({\n on: \"configure\",\n handler: () => {\n for (const proxy of this.alepha.primitives($proxy)) {\n this.createProxy(proxy.options);\n }\n },\n });\n\n public createProxy(options: ProxyPrimitiveOptions): void {\n if (options.disabled) {\n return;\n }\n\n const path = options.path;\n const target =\n typeof options.target === \"function\" ? options.target() : options.target;\n\n if (!path.endsWith(\"/*\")) {\n throw new AlephaError(\"Proxy path should end with '/*'\");\n }\n\n // Extract base path without /*\n const handler = this.createProxyHandler(target, options);\n\n for (const method of routeMethods) {\n this.routerProvider.createRoute({\n method,\n path,\n handler,\n });\n }\n\n this.log.info(\"Proxying\", { path, target });\n }\n\n public createProxyHandler(\n target: string,\n options: Omit<ProxyPrimitiveOptions, \"path\">,\n ): ServerHandler {\n return async (request) => {\n const url = new URL(target + request.url.pathname);\n if (request.url.search) {\n url.search = request.url.search;\n }\n\n options.rewrite?.(url);\n\n const requestInit = {\n url: url.toString(),\n method: request.method,\n headers: {\n ...request.headers,\n \"accept-encoding\": \"identity\", // ignore compression\n },\n body: this.getRawRequestBody(request),\n };\n\n if (requestInit.body) {\n (requestInit as any).duplex = \"half\";\n }\n\n if (options.beforeRequest) {\n await options.beforeRequest(request, requestInit);\n }\n\n this.log.debug(\"Proxying request\", {\n url: url.toString(),\n method: request.method,\n headers: request.headers,\n });\n\n const response = await fetch(requestInit.url, requestInit);\n\n request.reply.status = response.status;\n request.reply.headers = Object.fromEntries(response.headers.entries());\n request.reply.body = response.body;\n\n this.log.debug(\"Received response\", {\n status: request.reply.status,\n headers: request.reply.headers,\n });\n\n if (options.afterResponse) {\n await options.afterResponse(request, response);\n }\n };\n }\n\n protected getRawRequestBody(req: ServerRequest): ReadableStream | undefined {\n const { method } = req;\n\n if (method === \"GET\" || method === \"HEAD\" || method === \"OPTIONS\") {\n return;\n }\n\n if (req.raw?.web?.req) {\n return req.raw.web.req.body as ReadableStream;\n }\n\n if (req.raw?.node?.req) {\n const nodeReq = req.raw.node.req;\n return WebStream.from(nodeReq) as unknown as ReadableStream;\n }\n }\n}\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { $proxy } from \"./primitives/$proxy.ts\";\nimport { ServerProxyProvider } from \"./providers/ServerProxyProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$proxy.ts\";\nexport * from \"./providers/ServerProxyProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Reverse proxy routing.\n *\n * **Features:**\n * - Proxy configuration and routing\n *\n * @module alepha.server.proxy\n */\nexport const AlephaServerProxy = $module({\n name: \"alepha.server.proxy\",\n primitives: [$proxy],\n services: [AlephaServer, ServerProxyProvider],\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsFA,MAAa,UAAU,YAAmD;AACxE,QAAO,gBAAgB,gBAAgB,QAAQ;;AAsIjD,IAAa,iBAAb,cAAoC,UAAiC;AAErE,OAAO,QAAQ;;;ACpNf,IAAa,sBAAb,MAAiC;CAC/B,MAAyB,SAAS;CAClC,iBAAoC,QAAQ,qBAAqB;CACjE,SAA4B,QAAQ,OAAO;CAE3C,YAA+B,MAAM;EACnC,IAAI;EACJ,eAAe;AACb,QAAK,MAAM,SAAS,KAAK,OAAO,WAAW,OAAO,CAChD,MAAK,YAAY,MAAM,QAAQ;;EAGpC,CAAC;CAEF,YAAmB,SAAsC;AACvD,MAAI,QAAQ,SACV;EAGF,MAAM,OAAO,QAAQ;EACrB,MAAM,SACJ,OAAO,QAAQ,WAAW,aAAa,QAAQ,QAAQ,GAAG,QAAQ;AAEpE,MAAI,CAAC,KAAK,SAAS,KAAK,CACtB,OAAM,IAAI,YAAY,kCAAkC;EAI1D,MAAM,UAAU,KAAK,mBAAmB,QAAQ,QAAQ;AAExD,OAAK,MAAM,UAAU,aACnB,MAAK,eAAe,YAAY;GAC9B;GACA;GACA;GACD,CAAC;AAGJ,OAAK,IAAI,KAAK,YAAY;GAAE;GAAM;GAAQ,CAAC;;CAG7C,mBACE,QACA,SACe;AACf,SAAO,OAAO,YAAY;GACxB,MAAM,MAAM,IAAI,IAAI,SAAS,QAAQ,IAAI,SAAS;AAClD,OAAI,QAAQ,IAAI,OACd,KAAI,SAAS,QAAQ,IAAI;AAG3B,WAAQ,UAAU,IAAI;GAEtB,MAAM,cAAc;IAClB,KAAK,IAAI,UAAU;IACnB,QAAQ,QAAQ;IAChB,SAAS;KACP,GAAG,QAAQ;KACX,mBAAmB;KACpB;IACD,MAAM,KAAK,kBAAkB,QAAQ;IACtC;AAED,OAAI,YAAY,KACb,aAAoB,SAAS;AAGhC,OAAI,QAAQ,cACV,OAAM,QAAQ,cAAc,SAAS,YAAY;AAGnD,QAAK,IAAI,MAAM,oBAAoB;IACjC,KAAK,IAAI,UAAU;IACnB,QAAQ,QAAQ;IAChB,SAAS,QAAQ;IAClB,CAAC;GAEF,MAAM,WAAW,MAAM,MAAM,YAAY,KAAK,YAAY;AAE1D,WAAQ,MAAM,SAAS,SAAS;AAChC,WAAQ,MAAM,UAAU,OAAO,YAAY,SAAS,QAAQ,SAAS,CAAC;AACtE,WAAQ,MAAM,OAAO,SAAS;AAE9B,QAAK,IAAI,MAAM,qBAAqB;IAClC,QAAQ,QAAQ,MAAM;IACtB,SAAS,QAAQ,MAAM;IACxB,CAAC;AAEF,OAAI,QAAQ,cACV,OAAM,QAAQ,cAAc,SAAS,SAAS;;;CAKpD,kBAA4B,KAAgD;EAC1E,MAAM,EAAE,WAAW;AAEnB,MAAI,WAAW,SAAS,WAAW,UAAU,WAAW,UACtD;AAGF,MAAI,IAAI,KAAK,KAAK,IAChB,QAAO,IAAI,IAAI,IAAI,IAAI;AAGzB,MAAI,IAAI,KAAK,MAAM,KAAK;GACtB,MAAM,UAAU,IAAI,IAAI,KAAK;AAC7B,UAAOA,eAAU,KAAK,QAAQ;;;;;;;;;;;;;;AClGpC,MAAa,oBAAoB,QAAQ;CACvC,MAAM;CACN,YAAY,CAAC,OAAO;CACpB,UAAU,CAAC,cAAc,oBAAoB;CAC9C,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["WebStream"],"sources":["../../../src/server/proxy/primitives/$proxy.ts","../../../src/server/proxy/providers/ServerProxyProvider.ts","../../../src/server/proxy/index.ts"],"sourcesContent":["import { type Async, createPrimitive, KIND, Primitive } from \"alepha\";\nimport type { ServerRequest } from \"alepha/server\";\n\n/**\n * Creates a proxy primitive to forward requests to another server.\n *\n * This primitive enables you to create reverse proxy functionality, allowing your Alepha server\n * to forward requests to other services while maintaining a unified API surface. It's particularly\n * useful for microservice architectures, API gateways, or when you need to aggregate multiple\n * services behind a single endpoint.\n *\n * **Key Features**\n *\n * - **Path-based routing**: Match specific paths or patterns to proxy\n * - **Dynamic targets**: Support both static and dynamic target resolution\n * - **Request/Response hooks**: Modify requests before forwarding and responses after receiving\n * - **URL rewriting**: Transform URLs before forwarding to the target\n * - **Conditional proxying**: Enable/disable proxies based on environment or conditions\n *\n * @example\n * **Basic proxy setup:**\n * ```ts\n * import { $proxy } from \"alepha/server/proxy\";\n *\n * class ApiGateway {\n * // Forward all /api/* requests to external service\n * api = $proxy({\n * path: \"/api/*\",\n * target: \"https://api.example.com\"\n * });\n * }\n * ```\n *\n * @example\n * **Dynamic target with environment-based routing:**\n * ```ts\n * class ApiGateway {\n * // Route to different environments based on configuration\n * api = $proxy({\n * path: \"/api/*\",\n * target: () => process.env.NODE_ENV === \"production\"\n * ? \"https://api.prod.example.com\"\n * : \"https://api.dev.example.com\"\n * });\n * }\n * ```\n *\n * @example\n * **Advanced proxy with request/response modification:**\n * ```ts\n * class SecureProxy {\n * secure = $proxy({\n * path: \"/secure/*\",\n * target: \"https://secure-api.example.com\",\n * beforeRequest: async (request, proxyRequest) => {\n * // Add authentication headers\n * proxyRequest.headers = {\n * ...proxyRequest.headers,\n * 'Authorization': `Bearer ${await getServiceToken()}`,\n * 'X-Forwarded-For': request.headers['x-forwarded-for'] || request.ip\n * };\n * },\n * afterResponse: async (request, proxyResponse) => {\n * // Log response for monitoring\n * console.log(`Proxied ${request.url} -> ${proxyResponse.status}`);\n * },\n * rewrite: (url) => {\n * // Remove /secure prefix when forwarding\n * url.pathname = url.pathname.replace('/secure', '');\n * }\n * });\n * }\n * ```\n *\n * @example\n * **Conditional proxy based on feature flags:**\n * ```ts\n * class FeatureProxy {\n * newApi = $proxy({\n * path: \"/v2/*\",\n * target: \"https://new-api.example.com\",\n * disabled: !process.env.ENABLE_V2_API // Disable if feature flag is off\n * });\n * }\n * ```\n */\nexport const $proxy = (options: ProxyPrimitiveOptions): ProxyPrimitive => {\n return createPrimitive(ProxyPrimitive, options);\n};\n\nexport type ProxyPrimitiveOptions = {\n /**\n * Path pattern to match for proxying requests.\n *\n * Supports wildcards and path parameters:\n * - `/api/*` - Matches all paths starting with `/api/`\n * - `/api/v1/*` - Matches all paths starting with `/api/v1/`\n * - `/users/:id` - Matches `/users/123`, `/users/abc`, etc.\n *\n * @example \"/api/*\"\n * @example \"/secure/admin/*\"\n * @example \"/users/:id/posts\"\n */\n path: string;\n\n /**\n * Target URL to which matching requests should be forwarded.\n *\n * Can be either:\n * - **Static string**: A fixed URL like `\"https://api.example.com\"`\n * - **Dynamic function**: A function that returns the URL, enabling runtime target resolution\n *\n * The target URL will be combined with the remaining path from the original request.\n *\n * @example \"https://api.example.com\"\n * @example () => process.env.API_URL || \"http://localhost:3001\"\n */\n target: string | (() => string);\n\n /**\n * Whether this proxy is disabled.\n *\n * When `true`, requests matching the path will not be proxied and will be handled\n * by other routes or return 404. Useful for feature toggles or conditional proxying.\n *\n * @default false\n * @example !process.env.ENABLE_PROXY\n */\n disabled?: boolean;\n\n /**\n * Hook called before forwarding the request to the target server.\n *\n * Use this to:\n * - Add authentication headers\n * - Modify request headers or body\n * - Add request tracking/logging\n * - Transform the request before forwarding\n *\n * @param request - The original incoming server request\n * @param proxyRequest - The request that will be sent to the target (modifiable)\n *\n * @example\n * ```ts\n * beforeRequest: async (request, proxyRequest) => {\n * proxyRequest.headers = {\n * ...proxyRequest.headers,\n * 'Authorization': `Bearer ${await getToken()}`,\n * 'X-Request-ID': generateRequestId()\n * };\n * }\n * ```\n */\n beforeRequest?: (\n request: ServerRequest,\n proxyRequest: RequestInit,\n ) => Async<void>;\n\n /**\n * Hook called after receiving the response from the target server.\n *\n * Use this to:\n * - Log response details for monitoring\n * - Add custom headers to the response\n * - Transform response data\n * - Handle error responses\n *\n * @param request - The original incoming server request\n * @param proxyResponse - The response received from the target server\n *\n * @example\n * ```ts\n * afterResponse: async (request, proxyResponse) => {\n * console.log(`Proxy ${request.method} ${request.url} -> ${proxyResponse.status}`);\n *\n * if (!proxyResponse.ok) {\n * await logError(`Proxy error: ${proxyResponse.status}`, { request, response: proxyResponse });\n * }\n * }\n * ```\n */\n afterResponse?: (\n request: ServerRequest,\n proxyResponse: Response,\n ) => Async<void>;\n\n /**\n * Function to rewrite the URL before sending to the target server.\n *\n * Use this to:\n * - Remove or add path prefixes\n * - Transform path parameters\n * - Modify query parameters\n * - Change the URL structure entirely\n *\n * The function receives a mutable URL object and should modify it in-place.\n *\n * @param url - The URL object to modify (mutable)\n *\n * @example\n * ```ts\n * // Remove /api prefix when forwarding\n * rewrite: (url) => {\n * url.pathname = url.pathname.replace('/api', '');\n * }\n * ```\n *\n * @example\n * ```ts\n * // Add version prefix\n * rewrite: (url) => {\n * url.pathname = `/v2${url.pathname}`;\n * }\n * ```\n */\n rewrite?: (url: URL) => void;\n\n // TODO: Add retry functionality\n // retry?: RetryOptions;\n};\n\nexport class ProxyPrimitive extends Primitive<ProxyPrimitiveOptions> {}\n\n$proxy[KIND] = ProxyPrimitive;\n","import { ReadableStream as WebStream } from \"node:stream/web\";\nimport { $hook, $inject, Alepha, AlephaError } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport {\n routeMethods,\n type ServerHandler,\n type ServerRequest,\n ServerRouterProvider,\n} from \"alepha/server\";\nimport { $proxy, type ProxyPrimitiveOptions } from \"../primitives/$proxy.ts\";\n\nexport class ServerProxyProvider {\n protected readonly log = $logger();\n protected readonly routerProvider = $inject(ServerRouterProvider);\n protected readonly alepha = $inject(Alepha);\n\n protected readonly configure = $hook({\n on: \"configure\",\n handler: () => {\n for (const proxy of this.alepha.primitives($proxy)) {\n this.createProxy(proxy.options);\n }\n },\n });\n\n public createProxy(options: ProxyPrimitiveOptions): void {\n if (options.disabled) {\n return;\n }\n\n const path = options.path;\n const target =\n typeof options.target === \"function\" ? options.target() : options.target;\n\n if (!path.endsWith(\"/*\")) {\n throw new AlephaError(\"Proxy path should end with '/*'\");\n }\n\n // Extract base path without /*\n const handler = this.createProxyHandler(target, options);\n\n for (const method of routeMethods) {\n this.routerProvider.createRoute({\n method,\n path,\n handler,\n });\n }\n\n this.log.info(\"Proxying\", { path, target });\n }\n\n public createProxyHandler(\n target: string,\n options: Omit<ProxyPrimitiveOptions, \"path\">,\n ): ServerHandler {\n return async (request) => {\n const url = new URL(target + request.url.pathname);\n if (request.url.search) {\n url.search = request.url.search;\n }\n\n options.rewrite?.(url);\n\n const requestInit = {\n url: url.toString(),\n method: request.method,\n headers: {\n ...request.headers,\n \"accept-encoding\": \"identity\", // ignore compression\n },\n body: this.getRawRequestBody(request),\n };\n\n if (requestInit.body) {\n (requestInit as any).duplex = \"half\";\n }\n\n if (options.beforeRequest) {\n await options.beforeRequest(request, requestInit);\n }\n\n this.log.debug(\"Proxying request\", {\n url: url.toString(),\n method: request.method,\n headers: request.headers,\n });\n\n const response = await fetch(requestInit.url, requestInit);\n\n request.reply.status = response.status;\n request.reply.headers = Object.fromEntries(response.headers.entries());\n request.reply.body = response.body;\n\n this.log.debug(\"Received response\", {\n status: request.reply.status,\n headers: request.reply.headers,\n });\n\n if (options.afterResponse) {\n await options.afterResponse(request, response);\n }\n };\n }\n\n protected getRawRequestBody(req: ServerRequest): ReadableStream | undefined {\n const { method } = req;\n\n if (method === \"GET\" || method === \"HEAD\" || method === \"OPTIONS\") {\n return;\n }\n\n if (req.raw?.web?.req) {\n return req.raw.web.req.body as ReadableStream;\n }\n\n if (req.raw?.node?.req) {\n const nodeReq = req.raw.node.req;\n return WebStream.from(nodeReq) as unknown as ReadableStream;\n }\n }\n}\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { $proxy } from \"./primitives/$proxy.ts\";\nimport { ServerProxyProvider } from \"./providers/ServerProxyProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$proxy.ts\";\nexport * from \"./providers/ServerProxyProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Reverse proxy routing.\n *\n * **Features:**\n * - Proxy configuration and routing\n *\n * @module alepha.server.proxy\n */\nexport const AlephaServerProxy = $module({\n name: \"alepha.server.proxy\",\n primitives: [$proxy],\n services: [AlephaServer, ServerProxyProvider],\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsFA,MAAa,UAAU,YAAmD;CACxE,OAAO,gBAAgB,gBAAgB,QAAQ;;AAsIjD,IAAa,iBAAb,cAAoC,UAAiC;AAErE,OAAO,QAAQ;;;ACpNf,IAAa,sBAAb,MAAiC;CAC/B,MAAyB,SAAS;CAClC,iBAAoC,QAAQ,qBAAqB;CACjE,SAA4B,QAAQ,OAAO;CAE3C,YAA+B,MAAM;EACnC,IAAI;EACJ,eAAe;GACb,KAAK,MAAM,SAAS,KAAK,OAAO,WAAW,OAAO,EAChD,KAAK,YAAY,MAAM,QAAQ;;EAGpC,CAAC;CAEF,YAAmB,SAAsC;EACvD,IAAI,QAAQ,UACV;EAGF,MAAM,OAAO,QAAQ;EACrB,MAAM,SACJ,OAAO,QAAQ,WAAW,aAAa,QAAQ,QAAQ,GAAG,QAAQ;EAEpE,IAAI,CAAC,KAAK,SAAS,KAAK,EACtB,MAAM,IAAI,YAAY,kCAAkC;EAI1D,MAAM,UAAU,KAAK,mBAAmB,QAAQ,QAAQ;EAExD,KAAK,MAAM,UAAU,cACnB,KAAK,eAAe,YAAY;GAC9B;GACA;GACA;GACD,CAAC;EAGJ,KAAK,IAAI,KAAK,YAAY;GAAE;GAAM;GAAQ,CAAC;;CAG7C,mBACE,QACA,SACe;EACf,OAAO,OAAO,YAAY;GACxB,MAAM,MAAM,IAAI,IAAI,SAAS,QAAQ,IAAI,SAAS;GAClD,IAAI,QAAQ,IAAI,QACd,IAAI,SAAS,QAAQ,IAAI;GAG3B,QAAQ,UAAU,IAAI;GAEtB,MAAM,cAAc;IAClB,KAAK,IAAI,UAAU;IACnB,QAAQ,QAAQ;IAChB,SAAS;KACP,GAAG,QAAQ;KACX,mBAAmB;KACpB;IACD,MAAM,KAAK,kBAAkB,QAAQ;IACtC;GAED,IAAI,YAAY,MACd,YAAqB,SAAS;GAGhC,IAAI,QAAQ,eACV,MAAM,QAAQ,cAAc,SAAS,YAAY;GAGnD,KAAK,IAAI,MAAM,oBAAoB;IACjC,KAAK,IAAI,UAAU;IACnB,QAAQ,QAAQ;IAChB,SAAS,QAAQ;IAClB,CAAC;GAEF,MAAM,WAAW,MAAM,MAAM,YAAY,KAAK,YAAY;GAE1D,QAAQ,MAAM,SAAS,SAAS;GAChC,QAAQ,MAAM,UAAU,OAAO,YAAY,SAAS,QAAQ,SAAS,CAAC;GACtE,QAAQ,MAAM,OAAO,SAAS;GAE9B,KAAK,IAAI,MAAM,qBAAqB;IAClC,QAAQ,QAAQ,MAAM;IACtB,SAAS,QAAQ,MAAM;IACxB,CAAC;GAEF,IAAI,QAAQ,eACV,MAAM,QAAQ,cAAc,SAAS,SAAS;;;CAKpD,kBAA4B,KAAgD;EAC1E,MAAM,EAAE,WAAW;EAEnB,IAAI,WAAW,SAAS,WAAW,UAAU,WAAW,WACtD;EAGF,IAAI,IAAI,KAAK,KAAK,KAChB,OAAO,IAAI,IAAI,IAAI,IAAI;EAGzB,IAAI,IAAI,KAAK,MAAM,KAAK;GACtB,MAAM,UAAU,IAAI,IAAI,KAAK;GAC7B,OAAOA,eAAU,KAAK,QAAQ;;;;;;;;;;;;;;AClGpC,MAAa,oBAAoB,QAAQ;CACvC,MAAM;CACN,YAAY,CAAC,OAAO;CACpB,UAAU,CAAC,cAAc,oBAAoB;CAC9C,CAAC"}
|
|
@@ -4,6 +4,7 @@ import { ServerRequest, ServerRouterProvider } from "alepha/server";
|
|
|
4
4
|
import { CacheProvider } from "alepha/cache";
|
|
5
5
|
import { DateTimeProvider } from "alepha/datetime";
|
|
6
6
|
import * as _$alepha_logger0 from "alepha/logger";
|
|
7
|
+
import * as _$typebox from "typebox";
|
|
7
8
|
|
|
8
9
|
//#region ../../src/server/rate-limit/primitives/$rateLimit.d.ts
|
|
9
10
|
interface RateLimitMiddlewareOptions extends RateLimitOptions {
|
|
@@ -48,11 +49,11 @@ interface RateLimitResult {
|
|
|
48
49
|
/**
|
|
49
50
|
* Rate limit configuration atom (global defaults)
|
|
50
51
|
*/
|
|
51
|
-
declare const rateLimitOptions: _$alepha.Atom<_$
|
|
52
|
-
windowMs: _$
|
|
53
|
-
max: _$
|
|
54
|
-
skipFailedRequests: _$
|
|
55
|
-
skipSuccessfulRequests: _$
|
|
52
|
+
declare const rateLimitOptions: _$alepha.Atom<_$typebox.TObject<{
|
|
53
|
+
windowMs: _$typebox.TNumber;
|
|
54
|
+
max: _$typebox.TNumber;
|
|
55
|
+
skipFailedRequests: _$typebox.TOptional<_$typebox.TBoolean>;
|
|
56
|
+
skipSuccessfulRequests: _$typebox.TOptional<_$typebox.TBoolean>;
|
|
56
57
|
}>, "alepha.server.rate-limit.options">;
|
|
57
58
|
type RateLimitAtomOptions = Static<typeof rateLimitOptions.schema>;
|
|
58
59
|
declare module "alepha" {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/server/rate-limit/primitives/$rateLimit.ts","../../../src/server/rate-limit/providers/ServerRateLimitProvider.ts","../../../src/server/rate-limit/index.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/server/rate-limit/primitives/$rateLimit.ts","../../../src/server/rate-limit/providers/ServerRateLimitProvider.ts","../../../src/server/rate-limit/index.ts"],"mappings":";;;;;;;;;UAKiB,0BAAA,SAAmC,gBAAA;;;;;EAKlD,GAAA,OAAU,IAAA;AAAA;AALZ;;;;;;;;;AA8BA;;;;;;;;;;;;ACtBA;ADRA,cA8Ba,UAAA,GACX,OAAA,GAAU,0BAAA,KACT,UAAA;;;UCxBc,eAAA;EACf,OAAA;EACA,KAAA;EACA,SAAA;EACA,SAAA;EACA,UAAA;AAAA;;;;cAMW,gBAAA,EAAgB,QAAA,CAAA,IAAA,WAAA,OAAA;YA0B3B,SAAA,CAAA,OAAA;;;;;KAEU,oBAAA,GAAuB,MAAA,QAAc,gBAAA,CAAiB,MAAA;AAAA;EAAA,UAGtD,KAAA;IAAA,CACP,gBAAA,CAAiB,GAAA,GAAM,oBAAA;EAAA;AAAA;AAAA,UAIX,qBAAA,SAA8B,gBAAA;;;AA/C/C;EAmDE,IAAA;;;;EAIA,KAAA;AAAA;AAAA,cAGW,uBAAA;EAAA,mBACQ,GAAA,EADe,gBAAA,CACZ,MAAA;EAAA,mBACH,QAAA,EAAQ,gBAAA;EAAA,mBACR,oBAAA,EAAoB,oBAAA;EAAA,mBACpB,aAAA,EAAa,aAAA;EAAA,mBACb,aAAA,EAAa,QAAA;;;;;;4BAEN,UAAA;;;;WAKV,iBAAA,EAAmB,qBAAA;EA3DR;;;EAgEpB,iBAAA,CAAkB,MAAA,EAAQ,qBAAA;EAAA,mBAId,OAAA,EAJmC,QAAA,CAI5B,aAAA;EAAA,SAuBV,SAAA,EAvBU,QAAA,CAuBD,aAAA;EAAA,SAuBT,eAAA,EAvBS,QAAA,CAuBM,aAAA;;;;YA2BrB,qBAAA,CACR,MAAA,EAAQ,qBAAA,GACP,gBAAA;;;;EAgBI,mBAAA,CACL,OAAA,EAAS,aAAA,EACT,MAAA,EAAQ,eAAA;EAiBG,UAAA,CACX,GAAA,EAAK,IAAA,CAAK,aAAA,SACV,OAAA,GAAS,gBAAA,GACR,OAAA,CAAQ,eAAA;EAzJmB;;;;EAkKjB,eAAA,CACX,OAAA,UACA,OAAA,GAAS,gBAAA,GACR,OAAA,CAAQ,eAAA;EAAA,UAoCD,WAAA,CAAY,GAAA,EAAK,IAAA,CAAK,aAAA;AAAA;;;;YCjPtB,sBAAA;;;;;IAKR,SAAA,GAAY,gBAAA;EAAA;EAAA,UAGJ,WAAA;IFfwC;;;;IEoBhD,SAAA,GAAY,gBAAA;EAAA;AAAA;AAAA,UAMC,gBAAA;EFyChB;;;EErCC,GAAA;EFCA;;;EEGA,QAAA;;;;EAIA,YAAA,IAAgB,GAAA;ED9Bc;;;ECkC9B,kBAAA;EDhCA;;;ECoCA,sBAAA;AAAA;;AD3BF;;;;;;;cCsCa,qBAAA,EAAqB,QAAA,CAAA,OAAA,CAGhC,QAAA,CAHgC,MAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/server/rate-limit/providers/ServerRateLimitProvider.ts","../../../src/server/rate-limit/primitives/$rateLimit.ts","../../../src/server/rate-limit/index.ts"],"sourcesContent":["import { $atom, $hook, $inject, $state, type Static, t } from \"alepha\";\nimport { CacheProvider } from \"alepha/cache\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport {\n HttpError,\n type ServerRequest,\n ServerRouterProvider,\n} from \"alepha/server\";\nimport type { RateLimitOptions } from \"../index.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface RateLimitResult {\n allowed: boolean;\n limit: number;\n remaining: number;\n resetTime: number;\n retryAfter?: number;\n}\n\n/**\n * Rate limit configuration atom (global defaults)\n */\nexport const rateLimitOptions = $atom({\n name: \"alepha.server.rate-limit.options\",\n schema: t.object({\n windowMs: t.number({\n default: 15 * 60 * 1000,\n description: \"Window duration in milliseconds\",\n }),\n max: t.number({\n default: 100,\n description: \"Maximum number of requests per window\",\n }),\n skipFailedRequests: t.optional(\n t.boolean({\n description: \"Skip rate limiting for failed requests\",\n }),\n ),\n skipSuccessfulRequests: t.optional(\n t.boolean({\n description: \"Skip rate limiting for successful requests\",\n }),\n ),\n }),\n default: {\n windowMs: 15 * 60 * 1000,\n max: 100,\n },\n});\n\nexport type RateLimitAtomOptions = Static<typeof rateLimitOptions.schema>;\n\ndeclare module \"alepha\" {\n interface State {\n [rateLimitOptions.key]: RateLimitAtomOptions;\n }\n}\n\nexport interface RateLimitRegistration extends RateLimitOptions {\n /**\n * Name identifier for this rate limit.\n */\n name?: string;\n /**\n * Path patterns to match (supports wildcards like /api/*).\n */\n paths?: string[];\n}\n\nexport class ServerRateLimitProvider {\n protected readonly log = $logger();\n protected readonly dateTime = $inject(DateTimeProvider);\n protected readonly serverRouterProvider = $inject(ServerRouterProvider);\n protected readonly cacheProvider = $inject(CacheProvider);\n protected readonly globalOptions = $state(rateLimitOptions);\n\n protected static readonly CACHE_NAME = \"rate-limit\";\n\n /**\n * Registered rate limit configurations with their path patterns\n */\n public readonly registeredConfigs: RateLimitRegistration[] = [];\n\n /**\n * Register a rate limit configuration (called by primitives)\n */\n public registerRateLimit(config: RateLimitRegistration): void {\n this.registeredConfigs.push(config);\n }\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n // Apply path-specific rate limit 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.rateLimit = this.buildRateLimitOptions(config);\n }\n }\n }\n }\n\n if (this.registeredConfigs.length > 0) {\n this.log.info(\n `Initialized with ${this.registeredConfigs.length} registered rate-limit configurations.`,\n );\n }\n },\n });\n\n public readonly onRequest = $hook({\n on: \"server:onRequest\",\n handler: async ({ route, request }) => {\n // Use route-specific rate limit if defined, otherwise use global options\n const rateLimitConfig = route.rateLimit ?? this.globalOptions;\n\n // Skip if no rate limiting configured\n if (!rateLimitConfig.max && !rateLimitConfig.windowMs) {\n return;\n }\n\n const result = await this.checkLimit(request, rateLimitConfig);\n this.setRateLimitHeaders(request, result);\n\n if (!result.allowed) {\n throw new HttpError({\n status: 429,\n message: \"Too Many Requests\",\n });\n }\n },\n });\n\n public readonly onActionRequest = $hook({\n on: \"action:onRequest\",\n handler: async ({ action, request }) => {\n // Check if this action has rate limiting enabled\n const rateLimit = action.options?.rateLimit;\n if (!rateLimit) {\n return; // No rate limiting for this action\n }\n\n const result = await this.checkLimit(request, rateLimit);\n\n if (!result.allowed) {\n // Actions are internal - don't set HTTP headers\n // Only throw error to prevent action execution\n throw new HttpError({\n status: 429,\n message: \"Too Many Requests\",\n });\n }\n\n // Action allowed - no headers to set since actions are internal\n },\n });\n\n /**\n * Build complete rate limit options by merging with global defaults\n */\n protected buildRateLimitOptions(\n config: RateLimitRegistration,\n ): RateLimitOptions {\n return {\n max: config.max ?? this.globalOptions.max,\n windowMs: config.windowMs ?? this.globalOptions.windowMs,\n keyGenerator: config.keyGenerator,\n skipFailedRequests:\n config.skipFailedRequests ?? this.globalOptions.skipFailedRequests,\n skipSuccessfulRequests:\n config.skipSuccessfulRequests ??\n this.globalOptions.skipSuccessfulRequests,\n };\n }\n\n /**\n * Set rate limit headers on the response\n */\n public setRateLimitHeaders(\n request: ServerRequest,\n result: RateLimitResult,\n ): void {\n request.reply.setHeader(\"X-RateLimit-Limit\", result.limit.toString());\n request.reply.setHeader(\n \"X-RateLimit-Remaining\",\n result.remaining.toString(),\n );\n request.reply.setHeader(\n \"X-RateLimit-Reset\",\n Math.ceil(result.resetTime / 1000).toString(),\n );\n\n if (!result.allowed && result.retryAfter) {\n request.reply.setHeader(\"Retry-After\", result.retryAfter.toString());\n }\n }\n\n public async checkLimit(\n req: Pick<ServerRequest, \"ip\">,\n options: RateLimitOptions = {},\n ): Promise<RateLimitResult> {\n const baseKey = this.generateKey(req);\n return this.checkLimitByKey(baseKey, options);\n }\n\n /**\n * Check rate limit by an explicit key string.\n * Useful when no request context is available (e.g. `$job`, `$pipeline`).\n */\n public async checkLimitByKey(\n baseKey: string,\n options: RateLimitOptions = {},\n ): Promise<RateLimitResult> {\n const windowMs = options.windowMs ?? this.globalOptions.windowMs;\n const max = options.max ?? this.globalOptions.max;\n\n const now = this.dateTime.nowMillis();\n // Fixed window: round down to nearest window boundary\n const windowStart = Math.floor(now / windowMs) * windowMs;\n const resetTime = windowStart + windowMs;\n\n // Include window timestamp in key for automatic expiration of old windows\n const key = `${baseKey}:${windowStart}`;\n\n // Atomic increment - returns the new count after incrementing\n const count = await this.cacheProvider.incr(\n ServerRateLimitProvider.CACHE_NAME,\n key,\n 1,\n );\n\n const allowed = count <= max;\n const remaining = Math.max(0, max - count);\n\n const result: RateLimitResult = {\n allowed,\n limit: max,\n remaining,\n resetTime,\n };\n\n if (!allowed) {\n result.retryAfter = Math.ceil((resetTime - now) / 1000);\n }\n\n return result;\n }\n\n protected generateKey(req: Pick<ServerRequest, \"ip\">): string {\n // Use req.ip which is resolved by ServerRequestParser with proper trust proxy handling\n return `ip:${req.ip || \"unknown\"}`;\n }\n}\n","import { createMiddleware, type Middleware } from \"alepha\";\nimport { HttpError } from \"alepha/server\";\nimport type { RateLimitOptions } from \"../index.ts\";\nimport { ServerRateLimitProvider } from \"../providers/ServerRateLimitProvider.ts\";\n\nexport interface RateLimitMiddlewareOptions extends RateLimitOptions {\n /**\n * Custom key function. Receives the handler arguments.\n * When provided, bypasses request-based key generation — works outside `$action`.\n */\n key?: (...args: any[]) => string;\n}\n\n/**\n * Middleware that enforces rate limiting.\n *\n * **Key resolution** (in order):\n * 1. Explicit `key` function — user controls the key. Works anywhere (`$action`, `$job`, `$pipeline`).\n * 2. Auto-detect `request.ip` from ALS — default for `$action` context.\n * 3. `\"global\"` fallback — when no request context and no `key`. All calls share one bucket.\n *\n * Sets `X-RateLimit-*` response headers when a request context is available.\n * Throws `HttpError(429)` when the limit is exceeded.\n *\n * ```typescript\n * // In $action: automatically rate limits by IP\n * $action({ use: [$rateLimit({ max: 100, windowMs: 60000 })] })\n *\n * // In $action: rate limit by custom key\n * $action({ use: [$rateLimit({ max: 10, windowMs: 60000, key: (req) => req.user?.id })] })\n *\n * // In $job: rate limit all executions globally\n * $job({ use: [$rateLimit({ max: 5, windowMs: 3600000 })] })\n * ```\n */\nexport const $rateLimit = (\n options?: RateLimitMiddlewareOptions,\n): Middleware => {\n return createMiddleware({\n name: \"$rateLimit\",\n options: options as unknown as Record<string, unknown>,\n handler: ({ alepha, next }) => {\n const rateLimitProvider = alepha.inject(ServerRateLimitProvider);\n\n return async (...args) => {\n const request = alepha.get(\"alepha.http.request\");\n\n const result = options?.key\n ? await rateLimitProvider.checkLimitByKey(\n options.key(...args),\n options,\n )\n : await rateLimitProvider.checkLimit(\n request ?? { ip: \"global\" },\n options,\n );\n\n if (request) {\n rateLimitProvider.setRateLimitHeaders(request, result);\n }\n\n if (!result.allowed) {\n throw new HttpError({\n status: 429,\n message: \"Too Many Requests\",\n });\n }\n\n return next(...args);\n };\n },\n });\n};\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { ServerRateLimitProvider } from \"./providers/ServerRateLimitProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$rateLimit.ts\";\nexport * from \"./providers/ServerRateLimitProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha/server\" {\n interface ActionPrimitiveOptions<TConfig> {\n /**\n * Rate limiting configuration for this action.\n * When specified, the action will be rate limited according to these settings.\n */\n rateLimit?: RateLimitOptions;\n }\n\n interface ServerRoute {\n /**\n * Route-specific rate limit configuration.\n * If set, overrides the global rate limit options for this route.\n */\n rateLimit?: RateLimitOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface RateLimitOptions {\n /**\n * Maximum number of requests per window (default: 100).\n */\n max?: number;\n /**\n * Window duration in milliseconds (default: 15 minutes).\n */\n windowMs?: number;\n /**\n * Custom key generator function.\n */\n keyGenerator?: (req: any) => string;\n /**\n * Skip rate limiting for failed requests.\n */\n skipFailedRequests?: boolean;\n /**\n * Skip rate limiting for successful requests.\n */\n skipSuccessfulRequests?: boolean;\n}\n\n/**\n * Request rate limiting on actions.\n *\n * **Features:**\n * - Rate limit configuration per action\n *\n * @module alepha.server.rate-limit\n */\nexport const AlephaServerRateLimit = $module({\n name: \"alepha.server.rate-limit\",\n services: [AlephaServer, ServerRateLimitProvider],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/server/rate-limit/providers/ServerRateLimitProvider.ts","../../../src/server/rate-limit/primitives/$rateLimit.ts","../../../src/server/rate-limit/index.ts"],"sourcesContent":["import { $atom, $hook, $inject, $state, type Static, t } from \"alepha\";\nimport { CacheProvider } from \"alepha/cache\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport {\n HttpError,\n type ServerRequest,\n ServerRouterProvider,\n} from \"alepha/server\";\nimport type { RateLimitOptions } from \"../index.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface RateLimitResult {\n allowed: boolean;\n limit: number;\n remaining: number;\n resetTime: number;\n retryAfter?: number;\n}\n\n/**\n * Rate limit configuration atom (global defaults)\n */\nexport const rateLimitOptions = $atom({\n name: \"alepha.server.rate-limit.options\",\n schema: t.object({\n windowMs: t.number({\n default: 15 * 60 * 1000,\n description: \"Window duration in milliseconds\",\n }),\n max: t.number({\n default: 100,\n description: \"Maximum number of requests per window\",\n }),\n skipFailedRequests: t.optional(\n t.boolean({\n description: \"Skip rate limiting for failed requests\",\n }),\n ),\n skipSuccessfulRequests: t.optional(\n t.boolean({\n description: \"Skip rate limiting for successful requests\",\n }),\n ),\n }),\n default: {\n windowMs: 15 * 60 * 1000,\n max: 100,\n },\n});\n\nexport type RateLimitAtomOptions = Static<typeof rateLimitOptions.schema>;\n\ndeclare module \"alepha\" {\n interface State {\n [rateLimitOptions.key]: RateLimitAtomOptions;\n }\n}\n\nexport interface RateLimitRegistration extends RateLimitOptions {\n /**\n * Name identifier for this rate limit.\n */\n name?: string;\n /**\n * Path patterns to match (supports wildcards like /api/*).\n */\n paths?: string[];\n}\n\nexport class ServerRateLimitProvider {\n protected readonly log = $logger();\n protected readonly dateTime = $inject(DateTimeProvider);\n protected readonly serverRouterProvider = $inject(ServerRouterProvider);\n protected readonly cacheProvider = $inject(CacheProvider);\n protected readonly globalOptions = $state(rateLimitOptions);\n\n protected static readonly CACHE_NAME = \"rate-limit\";\n\n /**\n * Registered rate limit configurations with their path patterns\n */\n public readonly registeredConfigs: RateLimitRegistration[] = [];\n\n /**\n * Register a rate limit configuration (called by primitives)\n */\n public registerRateLimit(config: RateLimitRegistration): void {\n this.registeredConfigs.push(config);\n }\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n // Apply path-specific rate limit 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.rateLimit = this.buildRateLimitOptions(config);\n }\n }\n }\n }\n\n if (this.registeredConfigs.length > 0) {\n this.log.info(\n `Initialized with ${this.registeredConfigs.length} registered rate-limit configurations.`,\n );\n }\n },\n });\n\n public readonly onRequest = $hook({\n on: \"server:onRequest\",\n handler: async ({ route, request }) => {\n // Use route-specific rate limit if defined, otherwise use global options\n const rateLimitConfig = route.rateLimit ?? this.globalOptions;\n\n // Skip if no rate limiting configured\n if (!rateLimitConfig.max && !rateLimitConfig.windowMs) {\n return;\n }\n\n const result = await this.checkLimit(request, rateLimitConfig);\n this.setRateLimitHeaders(request, result);\n\n if (!result.allowed) {\n throw new HttpError({\n status: 429,\n message: \"Too Many Requests\",\n });\n }\n },\n });\n\n public readonly onActionRequest = $hook({\n on: \"action:onRequest\",\n handler: async ({ action, request }) => {\n // Check if this action has rate limiting enabled\n const rateLimit = action.options?.rateLimit;\n if (!rateLimit) {\n return; // No rate limiting for this action\n }\n\n const result = await this.checkLimit(request, rateLimit);\n\n if (!result.allowed) {\n // Actions are internal - don't set HTTP headers\n // Only throw error to prevent action execution\n throw new HttpError({\n status: 429,\n message: \"Too Many Requests\",\n });\n }\n\n // Action allowed - no headers to set since actions are internal\n },\n });\n\n /**\n * Build complete rate limit options by merging with global defaults\n */\n protected buildRateLimitOptions(\n config: RateLimitRegistration,\n ): RateLimitOptions {\n return {\n max: config.max ?? this.globalOptions.max,\n windowMs: config.windowMs ?? this.globalOptions.windowMs,\n keyGenerator: config.keyGenerator,\n skipFailedRequests:\n config.skipFailedRequests ?? this.globalOptions.skipFailedRequests,\n skipSuccessfulRequests:\n config.skipSuccessfulRequests ??\n this.globalOptions.skipSuccessfulRequests,\n };\n }\n\n /**\n * Set rate limit headers on the response\n */\n public setRateLimitHeaders(\n request: ServerRequest,\n result: RateLimitResult,\n ): void {\n request.reply.setHeader(\"X-RateLimit-Limit\", result.limit.toString());\n request.reply.setHeader(\n \"X-RateLimit-Remaining\",\n result.remaining.toString(),\n );\n request.reply.setHeader(\n \"X-RateLimit-Reset\",\n Math.ceil(result.resetTime / 1000).toString(),\n );\n\n if (!result.allowed && result.retryAfter) {\n request.reply.setHeader(\"Retry-After\", result.retryAfter.toString());\n }\n }\n\n public async checkLimit(\n req: Pick<ServerRequest, \"ip\">,\n options: RateLimitOptions = {},\n ): Promise<RateLimitResult> {\n const baseKey = this.generateKey(req);\n return this.checkLimitByKey(baseKey, options);\n }\n\n /**\n * Check rate limit by an explicit key string.\n * Useful when no request context is available (e.g. `$job`, `$pipeline`).\n */\n public async checkLimitByKey(\n baseKey: string,\n options: RateLimitOptions = {},\n ): Promise<RateLimitResult> {\n const windowMs = options.windowMs ?? this.globalOptions.windowMs;\n const max = options.max ?? this.globalOptions.max;\n\n const now = this.dateTime.nowMillis();\n // Fixed window: round down to nearest window boundary\n const windowStart = Math.floor(now / windowMs) * windowMs;\n const resetTime = windowStart + windowMs;\n\n // Include window timestamp in key for automatic expiration of old windows\n const key = `${baseKey}:${windowStart}`;\n\n // Atomic increment - returns the new count after incrementing\n const count = await this.cacheProvider.incr(\n ServerRateLimitProvider.CACHE_NAME,\n key,\n 1,\n );\n\n const allowed = count <= max;\n const remaining = Math.max(0, max - count);\n\n const result: RateLimitResult = {\n allowed,\n limit: max,\n remaining,\n resetTime,\n };\n\n if (!allowed) {\n result.retryAfter = Math.ceil((resetTime - now) / 1000);\n }\n\n return result;\n }\n\n protected generateKey(req: Pick<ServerRequest, \"ip\">): string {\n // Use req.ip which is resolved by ServerRequestParser with proper trust proxy handling\n return `ip:${req.ip || \"unknown\"}`;\n }\n}\n","import { createMiddleware, type Middleware } from \"alepha\";\nimport { HttpError } from \"alepha/server\";\nimport type { RateLimitOptions } from \"../index.ts\";\nimport { ServerRateLimitProvider } from \"../providers/ServerRateLimitProvider.ts\";\n\nexport interface RateLimitMiddlewareOptions extends RateLimitOptions {\n /**\n * Custom key function. Receives the handler arguments.\n * When provided, bypasses request-based key generation — works outside `$action`.\n */\n key?: (...args: any[]) => string;\n}\n\n/**\n * Middleware that enforces rate limiting.\n *\n * **Key resolution** (in order):\n * 1. Explicit `key` function — user controls the key. Works anywhere (`$action`, `$job`, `$pipeline`).\n * 2. Auto-detect `request.ip` from ALS — default for `$action` context.\n * 3. `\"global\"` fallback — when no request context and no `key`. All calls share one bucket.\n *\n * Sets `X-RateLimit-*` response headers when a request context is available.\n * Throws `HttpError(429)` when the limit is exceeded.\n *\n * ```typescript\n * // In $action: automatically rate limits by IP\n * $action({ use: [$rateLimit({ max: 100, windowMs: 60000 })] })\n *\n * // In $action: rate limit by custom key\n * $action({ use: [$rateLimit({ max: 10, windowMs: 60000, key: (req) => req.user?.id })] })\n *\n * // In $job: rate limit all executions globally\n * $job({ use: [$rateLimit({ max: 5, windowMs: 3600000 })] })\n * ```\n */\nexport const $rateLimit = (\n options?: RateLimitMiddlewareOptions,\n): Middleware => {\n return createMiddleware({\n name: \"$rateLimit\",\n options: options as unknown as Record<string, unknown>,\n handler: ({ alepha, next }) => {\n const rateLimitProvider = alepha.inject(ServerRateLimitProvider);\n\n return async (...args) => {\n const request = alepha.get(\"alepha.http.request\");\n\n const result = options?.key\n ? await rateLimitProvider.checkLimitByKey(\n options.key(...args),\n options,\n )\n : await rateLimitProvider.checkLimit(\n request ?? { ip: \"global\" },\n options,\n );\n\n if (request) {\n rateLimitProvider.setRateLimitHeaders(request, result);\n }\n\n if (!result.allowed) {\n throw new HttpError({\n status: 429,\n message: \"Too Many Requests\",\n });\n }\n\n return next(...args);\n };\n },\n });\n};\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { ServerRateLimitProvider } from \"./providers/ServerRateLimitProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$rateLimit.ts\";\nexport * from \"./providers/ServerRateLimitProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha/server\" {\n interface ActionPrimitiveOptions<TConfig> {\n /**\n * Rate limiting configuration for this action.\n * When specified, the action will be rate limited according to these settings.\n */\n rateLimit?: RateLimitOptions;\n }\n\n interface ServerRoute {\n /**\n * Route-specific rate limit configuration.\n * If set, overrides the global rate limit options for this route.\n */\n rateLimit?: RateLimitOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface RateLimitOptions {\n /**\n * Maximum number of requests per window (default: 100).\n */\n max?: number;\n /**\n * Window duration in milliseconds (default: 15 minutes).\n */\n windowMs?: number;\n /**\n * Custom key generator function.\n */\n keyGenerator?: (req: any) => string;\n /**\n * Skip rate limiting for failed requests.\n */\n skipFailedRequests?: boolean;\n /**\n * Skip rate limiting for successful requests.\n */\n skipSuccessfulRequests?: boolean;\n}\n\n/**\n * Request rate limiting on actions.\n *\n * **Features:**\n * - Rate limit configuration per action\n *\n * @module alepha.server.rate-limit\n */\nexport const AlephaServerRateLimit = $module({\n name: \"alepha.server.rate-limit\",\n services: [AlephaServer, ServerRateLimitProvider],\n});\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/server/static/primitives/$serve.ts","../../../src/server/static/providers/ServerStaticProvider.ts","../../../src/server/static/index.ts"],"sourcesContent":["import { createPrimitive, KIND, Primitive } from \"alepha\";\nimport type { DurationLike } from \"alepha/datetime\";\n\n/**\n * Create a new static file handler.\n */\nexport const $serve = (options: ServePrimitiveOptions = {}): ServePrimitive => {\n return createPrimitive(ServePrimitive, options);\n};\n\nexport interface ServePrimitiveOptions {\n /**\n * Prefix for the served path.\n *\n * @default \"/\"\n */\n path?: string;\n\n /**\n * Path to the directory to serve.\n *\n * @default process.cwd()\n */\n root?: string;\n\n /**\n * If true, primitive will be ignored.\n *\n * @default false\n */\n disabled?: boolean;\n\n /**\n * Whether to keep dot files (e.g. `.gitignore`, `.env`) in the served directory.\n *\n * @default true\n */\n ignoreDotEnvFiles?: boolean;\n\n /**\n * Whether to use the index.html file when the path is a directory.\n *\n * @default true\n */\n indexFallback?: boolean;\n\n /**\n * Force all requests \"not found\" to be served with the index.html file.\n * This is useful for single-page applications (SPAs) that use client-side only routing.\n */\n historyApiFallback?: boolean;\n\n /**\n * Optional name of the primitive.\n * This is used for logging and debugging purposes.\n *\n * @default Key name.\n */\n name?: string;\n\n /**\n * Whether to use cache control headers.\n *\n * @default {}\n */\n cacheControl?: Partial<CacheControlOptions> | false;\n\n /**\n * Whether to suppress logging for this primitive.\n *\n * @default false\n */\n silent?: boolean;\n}\n\nexport interface CacheControlOptions {\n /**\n * Whether to use cache control headers.\n *\n * @default [.js, .css]\n */\n fileTypes: string[];\n\n /**\n * The maximum age of the cache in seconds.\n *\n * @default 60 * 60 * 24 * 2 // 2 days\n */\n maxAge: DurationLike;\n\n /**\n * Whether to use immutable cache control headers.\n *\n * @default true\n */\n immutable: boolean;\n}\n\nexport class ServePrimitive extends Primitive<ServePrimitiveOptions> {}\n\n$serve[KIND] = ServePrimitive;\n","import { createReadStream } from \"node:fs\";\nimport { access, readdir, stat } from \"node:fs/promises\";\nimport { basename, isAbsolute, join, sep } from \"node:path\";\nimport type { Readable as NodeStream } from \"node:stream\";\nimport { $hook, $inject, Alepha } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { type ServerHandler, ServerRouterProvider } from \"alepha/server\";\nimport { FileDetector } from \"alepha/system\";\nimport { $serve, type ServePrimitiveOptions } from \"../primitives/$serve.ts\";\n\nexport class ServerStaticProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly routerProvider = $inject(ServerRouterProvider);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly fileDetector = $inject(FileDetector);\n protected readonly log = $logger();\n protected readonly directories: ServeDirectory[] = [];\n\n protected readonly configure = $hook({\n on: \"configure\",\n handler: async () => {\n await Promise.all(\n this.alepha\n .primitives($serve)\n .map((it) => this.createStaticServer(it.options)),\n );\n },\n });\n\n public async createStaticServer(\n options: ServePrimitiveOptions,\n ): Promise<void> {\n const prefix = options.path ?? \"/\";\n\n let root = options.root ?? process.cwd();\n if (!isAbsolute(root)) {\n root = join(process.cwd(), root);\n }\n\n this.log.debug(\"Serve static files\", { prefix, root });\n\n await stat(root);\n\n // 1. get all files in the root directory (recursively)\n const files = await this.getAllFiles(root, options.ignoreDotEnvFiles);\n\n // 2. create a $route for each file (yes, this could be a lot of routes)\n const routes = await Promise.all(\n files.map(async (file) => {\n // Normalize to forward slashes for URL paths\n const urlPath = file.replace(root, \"\").replace(/\\\\/g, \"/\");\n const routePath = `${prefix}${encodeURI(urlPath)}`.replace(/\\/+/g, \"/\");\n const filePath = join(root, urlPath.replace(/\\//g, sep));\n this.log.trace(`Mount ${routePath} -> ${filePath}`);\n return {\n silent: options.silent,\n path: routePath,\n handler: await this.createFileHandler(filePath, options),\n };\n }),\n );\n\n for (const route of routes) {\n this.routerProvider.createRoute(route);\n\n // if route is for index.html, also create a route without it\n // e.g. /my/path/index.html -> /my/path/\n if (\n options.indexFallback !== false &&\n route.path.endsWith(\"index.html\")\n ) {\n this.routerProvider.createRoute({\n silent: options.silent,\n path: route.path.replace(/index\\.html$/, \"\"),\n handler: route.handler,\n });\n }\n }\n\n // 3. store the directory info for reference\n this.directories.push({\n options,\n files: files.map((file) => file.replace(root, \"\").replace(/\\\\/g, \"/\")),\n });\n\n // bonus! for SPAs, handle history API fallback\n if (options.historyApiFallback) {\n // meaning all unmatched routes should serve index.html\n this.routerProvider.createRoute({\n silent: options.silent,\n path: join(prefix, \"*\").replace(/\\\\/g, \"/\"),\n handler: async (request) => {\n const { reply } = request;\n\n if (request.url.pathname.includes(\".\")) {\n // If the request is for a file (e.g., /style.css), do not fall back\n reply.headers[\"content-type\"] = \"text/plain\";\n reply.body = \"Not Found\";\n reply.status = 404;\n return;\n }\n\n reply.headers[\"content-type\"] = \"text/html\";\n reply.status = 200;\n\n return new Promise<any>((resolve, reject) => {\n const stream = createReadStream(join(root, \"index.html\"));\n stream.on(\"open\", () => {\n resolve(stream);\n });\n stream.on(\"error\", (err) => {\n reject(err);\n });\n });\n },\n });\n }\n }\n\n public async createFileHandler(\n filepath: string,\n options: ServePrimitiveOptions,\n ): Promise<ServerHandler> {\n const filename = basename(filepath);\n\n const hasGzip = await access(`${filepath}.gz`)\n .then(() => true)\n .catch(() => false);\n\n const hasBr = await access(`${filepath}.br`)\n .then(() => true)\n .catch(() => false);\n\n const fileStat = await stat(filepath);\n const lastModified = fileStat.mtime.toUTCString();\n const etag = `\"${fileStat.size}-${fileStat.mtime.getTime()}\"`;\n const contentType = this.fileDetector.getContentType(filename);\n const cacheControl = this.getCacheControl(filename, options);\n\n return async (request): Promise<NodeStream | undefined> => {\n const { headers, reply } = request;\n let path = filepath;\n\n // 01/26 - when calling \"/directory\", redirect to \"/directory/\"\n if (\n options.path &&\n options.path === request.url.pathname &&\n !options.path.endsWith(\"/\")\n ) {\n reply.redirect(`${options.path}/`, 301);\n return;\n }\n\n const encoding = headers[\"accept-encoding\"];\n if (encoding) {\n if (hasBr && encoding.includes(\"br\")) {\n reply.headers[\"content-encoding\"] = \"br\";\n path += \".br\";\n } else if (hasGzip && encoding.includes(\"gzip\")) {\n reply.headers[\"content-encoding\"] = \"gzip\";\n path += \".gz\";\n }\n }\n\n reply.headers[\"content-type\"] = contentType;\n reply.headers[\"accept-ranges\"] = \"bytes\";\n reply.headers[\"last-modified\"] = lastModified;\n\n if (cacheControl) {\n reply.headers[\"cache-control\"] =\n `public, max-age=${cacheControl.maxAge}`;\n if (cacheControl.immutable) {\n reply.headers[\"cache-control\"] += \", immutable\";\n }\n }\n\n reply.headers.etag = etag;\n if (\n headers[\"if-none-match\"] === etag ||\n headers[\"if-modified-since\"] === lastModified\n ) {\n reply.status = 304;\n return;\n }\n\n return new Promise<any>((resolve, reject) => {\n const stream = createReadStream(path);\n stream.on(\"open\", () => {\n resolve(stream);\n });\n stream.on(\"error\", (err) => {\n reject(err);\n });\n });\n };\n }\n\n protected getCacheFileTypes(): string[] {\n return [\n \".js\",\n \".css\",\n \".woff\",\n \".woff2\",\n \".ttf\",\n \".eot\",\n \".otf\",\n \".jpg\",\n \".jpeg\",\n \".png\",\n \".svg\",\n \".gif\",\n ];\n }\n\n protected getCacheControl(\n filename: string,\n options: ServePrimitiveOptions,\n ): { maxAge: number; immutable: boolean } | undefined {\n if (!options.cacheControl) {\n return;\n }\n\n const fileTypes =\n options.cacheControl.fileTypes ?? this.getCacheFileTypes();\n\n for (const type of fileTypes) {\n if (filename.endsWith(type)) {\n return {\n immutable: options.cacheControl.immutable ?? true,\n maxAge: this.dateTimeProvider\n .duration(options.cacheControl.maxAge ?? [30, \"days\"])\n .as(\"seconds\"),\n };\n }\n }\n }\n\n public async getAllFiles(\n dir: string,\n ignoreDotEnvFiles = true,\n ): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n\n const files = await Promise.all(\n entries.map((dirent) => {\n // skip .env & other dot files\n if (ignoreDotEnvFiles && dirent.name.startsWith(\".\")) {\n return [];\n }\n\n const fullPath = join(dir, dirent.name);\n return dirent.isDirectory() ? this.getAllFiles(fullPath) : fullPath;\n }),\n );\n\n return files.flat();\n }\n}\n\nexport interface ServeDirectory {\n options: ServePrimitiveOptions;\n files: string[];\n}\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { $serve } from \"./primitives/$serve.ts\";\nimport { ServerStaticProvider } from \"./providers/ServerStaticProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$serve.ts\";\nexport * from \"./providers/ServerStaticProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Static file serving.\n *\n * **Features:**\n * - Serve static files from directory\n *\n * @module alepha.server.static\n */\nexport const AlephaServerStatic = $module({\n name: \"alepha.server.static\",\n primitives: [$serve],\n services: [AlephaServer, ServerStaticProvider],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/server/static/primitives/$serve.ts","../../../src/server/static/providers/ServerStaticProvider.ts","../../../src/server/static/index.ts"],"sourcesContent":["import { createPrimitive, KIND, Primitive } from \"alepha\";\nimport type { DurationLike } from \"alepha/datetime\";\n\n/**\n * Create a new static file handler.\n */\nexport const $serve = (options: ServePrimitiveOptions = {}): ServePrimitive => {\n return createPrimitive(ServePrimitive, options);\n};\n\nexport interface ServePrimitiveOptions {\n /**\n * Prefix for the served path.\n *\n * @default \"/\"\n */\n path?: string;\n\n /**\n * Path to the directory to serve.\n *\n * @default process.cwd()\n */\n root?: string;\n\n /**\n * If true, primitive will be ignored.\n *\n * @default false\n */\n disabled?: boolean;\n\n /**\n * Whether to keep dot files (e.g. `.gitignore`, `.env`) in the served directory.\n *\n * @default true\n */\n ignoreDotEnvFiles?: boolean;\n\n /**\n * Whether to use the index.html file when the path is a directory.\n *\n * @default true\n */\n indexFallback?: boolean;\n\n /**\n * Force all requests \"not found\" to be served with the index.html file.\n * This is useful for single-page applications (SPAs) that use client-side only routing.\n */\n historyApiFallback?: boolean;\n\n /**\n * Optional name of the primitive.\n * This is used for logging and debugging purposes.\n *\n * @default Key name.\n */\n name?: string;\n\n /**\n * Whether to use cache control headers.\n *\n * @default {}\n */\n cacheControl?: Partial<CacheControlOptions> | false;\n\n /**\n * Whether to suppress logging for this primitive.\n *\n * @default false\n */\n silent?: boolean;\n}\n\nexport interface CacheControlOptions {\n /**\n * Whether to use cache control headers.\n *\n * @default [.js, .css]\n */\n fileTypes: string[];\n\n /**\n * The maximum age of the cache in seconds.\n *\n * @default 60 * 60 * 24 * 2 // 2 days\n */\n maxAge: DurationLike;\n\n /**\n * Whether to use immutable cache control headers.\n *\n * @default true\n */\n immutable: boolean;\n}\n\nexport class ServePrimitive extends Primitive<ServePrimitiveOptions> {}\n\n$serve[KIND] = ServePrimitive;\n","import { createReadStream } from \"node:fs\";\nimport { access, readdir, stat } from \"node:fs/promises\";\nimport { basename, isAbsolute, join, sep } from \"node:path\";\nimport type { Readable as NodeStream } from \"node:stream\";\nimport { $hook, $inject, Alepha } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { type ServerHandler, ServerRouterProvider } from \"alepha/server\";\nimport { FileDetector } from \"alepha/system\";\nimport { $serve, type ServePrimitiveOptions } from \"../primitives/$serve.ts\";\n\nexport class ServerStaticProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly routerProvider = $inject(ServerRouterProvider);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly fileDetector = $inject(FileDetector);\n protected readonly log = $logger();\n protected readonly directories: ServeDirectory[] = [];\n\n protected readonly configure = $hook({\n on: \"configure\",\n handler: async () => {\n await Promise.all(\n this.alepha\n .primitives($serve)\n .map((it) => this.createStaticServer(it.options)),\n );\n },\n });\n\n public async createStaticServer(\n options: ServePrimitiveOptions,\n ): Promise<void> {\n const prefix = options.path ?? \"/\";\n\n let root = options.root ?? process.cwd();\n if (!isAbsolute(root)) {\n root = join(process.cwd(), root);\n }\n\n this.log.debug(\"Serve static files\", { prefix, root });\n\n await stat(root);\n\n // 1. get all files in the root directory (recursively)\n const files = await this.getAllFiles(root, options.ignoreDotEnvFiles);\n\n // 2. create a $route for each file (yes, this could be a lot of routes)\n const routes = await Promise.all(\n files.map(async (file) => {\n // Normalize to forward slashes for URL paths\n const urlPath = file.replace(root, \"\").replace(/\\\\/g, \"/\");\n const routePath = `${prefix}${encodeURI(urlPath)}`.replace(/\\/+/g, \"/\");\n const filePath = join(root, urlPath.replace(/\\//g, sep));\n this.log.trace(`Mount ${routePath} -> ${filePath}`);\n return {\n silent: options.silent,\n path: routePath,\n handler: await this.createFileHandler(filePath, options),\n };\n }),\n );\n\n for (const route of routes) {\n this.routerProvider.createRoute(route);\n\n // if route is for index.html, also create a route without it\n // e.g. /my/path/index.html -> /my/path/\n if (\n options.indexFallback !== false &&\n route.path.endsWith(\"index.html\")\n ) {\n this.routerProvider.createRoute({\n silent: options.silent,\n path: route.path.replace(/index\\.html$/, \"\"),\n handler: route.handler,\n });\n }\n }\n\n // 3. store the directory info for reference\n this.directories.push({\n options,\n files: files.map((file) => file.replace(root, \"\").replace(/\\\\/g, \"/\")),\n });\n\n // bonus! for SPAs, handle history API fallback\n if (options.historyApiFallback) {\n // meaning all unmatched routes should serve index.html\n this.routerProvider.createRoute({\n silent: options.silent,\n path: join(prefix, \"*\").replace(/\\\\/g, \"/\"),\n handler: async (request) => {\n const { reply } = request;\n\n if (request.url.pathname.includes(\".\")) {\n // If the request is for a file (e.g., /style.css), do not fall back\n reply.headers[\"content-type\"] = \"text/plain\";\n reply.body = \"Not Found\";\n reply.status = 404;\n return;\n }\n\n reply.headers[\"content-type\"] = \"text/html\";\n reply.status = 200;\n\n return new Promise<any>((resolve, reject) => {\n const stream = createReadStream(join(root, \"index.html\"));\n stream.on(\"open\", () => {\n resolve(stream);\n });\n stream.on(\"error\", (err) => {\n reject(err);\n });\n });\n },\n });\n }\n }\n\n public async createFileHandler(\n filepath: string,\n options: ServePrimitiveOptions,\n ): Promise<ServerHandler> {\n const filename = basename(filepath);\n\n const hasGzip = await access(`${filepath}.gz`)\n .then(() => true)\n .catch(() => false);\n\n const hasBr = await access(`${filepath}.br`)\n .then(() => true)\n .catch(() => false);\n\n const fileStat = await stat(filepath);\n const lastModified = fileStat.mtime.toUTCString();\n const etag = `\"${fileStat.size}-${fileStat.mtime.getTime()}\"`;\n const contentType = this.fileDetector.getContentType(filename);\n const cacheControl = this.getCacheControl(filename, options);\n\n return async (request): Promise<NodeStream | undefined> => {\n const { headers, reply } = request;\n let path = filepath;\n\n // 01/26 - when calling \"/directory\", redirect to \"/directory/\"\n if (\n options.path &&\n options.path === request.url.pathname &&\n !options.path.endsWith(\"/\")\n ) {\n reply.redirect(`${options.path}/`, 301);\n return;\n }\n\n const encoding = headers[\"accept-encoding\"];\n if (encoding) {\n if (hasBr && encoding.includes(\"br\")) {\n reply.headers[\"content-encoding\"] = \"br\";\n path += \".br\";\n } else if (hasGzip && encoding.includes(\"gzip\")) {\n reply.headers[\"content-encoding\"] = \"gzip\";\n path += \".gz\";\n }\n }\n\n reply.headers[\"content-type\"] = contentType;\n reply.headers[\"accept-ranges\"] = \"bytes\";\n reply.headers[\"last-modified\"] = lastModified;\n\n if (cacheControl) {\n reply.headers[\"cache-control\"] =\n `public, max-age=${cacheControl.maxAge}`;\n if (cacheControl.immutable) {\n reply.headers[\"cache-control\"] += \", immutable\";\n }\n }\n\n reply.headers.etag = etag;\n if (\n headers[\"if-none-match\"] === etag ||\n headers[\"if-modified-since\"] === lastModified\n ) {\n reply.status = 304;\n return;\n }\n\n return new Promise<any>((resolve, reject) => {\n const stream = createReadStream(path);\n stream.on(\"open\", () => {\n resolve(stream);\n });\n stream.on(\"error\", (err) => {\n reject(err);\n });\n });\n };\n }\n\n protected getCacheFileTypes(): string[] {\n return [\n \".js\",\n \".css\",\n \".woff\",\n \".woff2\",\n \".ttf\",\n \".eot\",\n \".otf\",\n \".jpg\",\n \".jpeg\",\n \".png\",\n \".svg\",\n \".gif\",\n ];\n }\n\n protected getCacheControl(\n filename: string,\n options: ServePrimitiveOptions,\n ): { maxAge: number; immutable: boolean } | undefined {\n if (!options.cacheControl) {\n return;\n }\n\n const fileTypes =\n options.cacheControl.fileTypes ?? this.getCacheFileTypes();\n\n for (const type of fileTypes) {\n if (filename.endsWith(type)) {\n return {\n immutable: options.cacheControl.immutable ?? true,\n maxAge: this.dateTimeProvider\n .duration(options.cacheControl.maxAge ?? [30, \"days\"])\n .as(\"seconds\"),\n };\n }\n }\n }\n\n public async getAllFiles(\n dir: string,\n ignoreDotEnvFiles = true,\n ): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n\n const files = await Promise.all(\n entries.map((dirent) => {\n // skip .env & other dot files\n if (ignoreDotEnvFiles && dirent.name.startsWith(\".\")) {\n return [];\n }\n\n const fullPath = join(dir, dirent.name);\n return dirent.isDirectory() ? this.getAllFiles(fullPath) : fullPath;\n }),\n );\n\n return files.flat();\n }\n}\n\nexport interface ServeDirectory {\n options: ServePrimitiveOptions;\n files: string[];\n}\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { $serve } from \"./primitives/$serve.ts\";\nimport { ServerStaticProvider } from \"./providers/ServerStaticProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$serve.ts\";\nexport * from \"./providers/ServerStaticProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Static file serving.\n *\n * **Features:**\n * - Serve static files from directory\n *\n * @module alepha.server.static\n */\nexport const AlephaServerStatic = $module({\n name: \"alepha.server.static\",\n primitives: [$serve],\n services: [AlephaServer, ServerStaticProvider],\n});\n"],"mappings":"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"}
|
|
@@ -4,6 +4,7 @@ import { ActionPrimitive, RequestConfigSchema, ServerProvider, ServerRouterProvi
|
|
|
4
4
|
import { ServerStaticProvider } from "alepha/server/static";
|
|
5
5
|
import * as _$alepha_logger0 from "alepha/logger";
|
|
6
6
|
import { FileSystemProvider } from "alepha/system";
|
|
7
|
+
import * as _$typebox from "typebox";
|
|
7
8
|
|
|
8
9
|
//#region ../../src/server/swagger/primitives/$swagger.d.ts
|
|
9
10
|
/**
|
|
@@ -176,7 +177,7 @@ interface OpenApiOperation {
|
|
|
176
177
|
* Swagger provider configuration atom
|
|
177
178
|
*/
|
|
178
179
|
declare const swaggerOptions: _$alepha.Atom<TObject<{
|
|
179
|
-
excludeKeys: _$
|
|
180
|
+
excludeKeys: _$typebox.TOptional<_$typebox.TArray<_$typebox.TString>>;
|
|
180
181
|
}>, "alepha.server.swagger.options">;
|
|
181
182
|
type ServerSwaggerProviderOptions = Static<typeof swaggerOptions.schema>;
|
|
182
183
|
declare module "alepha" {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/server/swagger/primitives/$swagger.ts","../../../src/server/swagger/providers/ServerSwaggerProvider.ts","../../../src/server/swagger/index.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/server/swagger/primitives/$swagger.ts","../../../src/server/swagger/providers/ServerSwaggerProvider.ts","../../../src/server/swagger/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;AAyBA;;;;;;;;;;;;;;cAAa,QAAA;EAAA,WACF,uBAAA,GACR,gBAAA;EAAA;;UAIc,uBAAA;EACf,IAAA,GAAO,eAAA;EAqBG;;;EAhBV,MAAA;EA4B+B;;;EAvB/B,QAAA;EAAA;;;EAKA,WAAA;EAaA;;;;EAPA,OAAA,GAAU,aAAA;EAYqB;;AAGjC;;;EARE,EAAA,aAAe,gBAAA;EAUJ;AAGb;;EARE,OAAA,IAAW,GAAA,EAAK,eAAA;AAAA;AAAA,UAGD,aAAA;EACf,GAAA;EACA,WAAA;AAAA;AAAA,UAGe,gBAAA;EACf,IAAA;EA+BE;;;;;EAxBF,oBAAA;EAEA,SAAA;IAsDmC;AAIrC;;IAtDI,QAAA;IAsD4C;;AAIhD;IArDI,KAAA;IAqD4B;;;IAhD5B,OAAA;IA2DkB;;;;;;IAnDlB,cAAA;IA6CA;;;;;;IArCA,MAAA;IA0CU;;;;IApCV,2BAAA;MAAA,CAAiC,GAAA;IAAA;IAyCJ;;;;;;;;IA/B7B,yCAAA;IAgCF;;;;;;IAxBE,iCAAA;EAAA;AAAA;AAAA,cAIS,gBAAA,SAAyB,SAAA,CAAU,uBAAA;AAAA,UAI/B,eAAA;EACf,OAAA;EACA,IAAA;IACE,KAAA;IACA,OAAA;IACA,WAAA;EAAA;EAEF,OAAA,GAAU,aAAA;EACV,KAAA,EAAO,MAAA;EACP,UAAA;IACE,OAAA,GAAU,MAAA;IACV,eAAA,GAAkB,MAAA;EAAA;AAAA;AAAA,UAIL,gBAAA;EACf,IAAA;EACA,OAAA;EACA,WAAA;EACA,WAAA;EACA,UAAA;EACA,UAAA,GAAa,KAAA;IACX,IAAA;IACA,EAAA;IACA,WAAA;IACA,QAAA;IACA,MAAA;EAAA;EAEF,WAAA;IACE,WAAA;IACA,OAAA,EAAS,MAAA;MAGL,MAAA;IAAA;IAGJ,QAAA;EAAA;EAEF,SAAA,EAAW,MAAA;IAGP,WAAA;IACA,OAAA,GAAU,MAAA;MAGN,MAAA;IAAA;EAAA;EAKR,QAAA,GAAW,KAAA,CAAM,MAAA;AAAA;;;;;;cC/JN,cAAA,EAAc,QAAA,CAAA,IAAA,CAAA,OAAA;oDAYzB,SAAA,CAAA,OAAA;AAAA;AAAA,KAEU,4BAAA,GAA+B,MAAA,QAAc,cAAA,CAAe,MAAA;AAAA;EAAA,UAG5D,KAAA;IAAA,CACP,cAAA,CAAe,GAAA,GAAM,4BAAA;EAAA;AAAA;AAAA,cAMb,qBAAA;EAAA,mBACQ,oBAAA,EAAoB,oBAAA;EAAA,mBACpB,oBAAA,EAAoB,oBAAA;EAAA,mBACpB,cAAA,EAAc,cAAA;EAAA,mBACd,MAAA,EAAM,MAAA;EAAA,mBACN,GAAA,EADM,gBAAA,CACH,MAAA;EAAA,mBACH,OAAA,EAAO,QAAA;;;qBACP,EAAA,EAAE,kBAAA;EAEd,IAAA,GAAO,eAAA;EAAA,mBAEK,SAAA,EAFU,QAAA,CAED,aAAA;EAarB,kBAAA,CAAmB,OAAA,EAAS,uBAAA,GAA0B,eAAA;EAAA,UAa7C,kBAAA,CACd,OAAA,EAAS,uBAAA,GACR,OAAA,CAAQ,eAAA;EAAA,UAoBD,gBAAA,CACR,OAAA,EAAS,eAAA,CAAgB,mBAAA,KACzB,GAAA,EAAK,uBAAA,GACJ,eAAA;EAyLI,eAAA,CAAgB,MAAA,EAAQ,OAAA;EASxB,iBAAA,CAAkB,GAAA;EAOlB,iBAAA,CAAkB,KAAA,EAAO,eAAA,CAAgB,mBAAA;IAE1C,IAAA;IACA,MAAA;IACA,MAAA;EAAA;EAAA,UA0DI,cAAA,CAAe,MAAA;EAAA,UAUf,oBAAA,CAAqB,MAAA;EAAA,UAuBrB,cAAA,CAAe,MAAA;EAAA,UAiBf,mBAAA,CAAoB,MAAA,UAAgB,IAAA,EAAM,eAAA;EAAA,UAWpC,kBAAA,CACd,MAAA,UACA,OAAA,EAAS,uBAAA,GACR,OAAA;EAAA,UAmEa,YAAA,CAAA,GACX,KAAA,2BACF,OAAA;EAUI,mBAAA,WAA8B,MAAA,cAAA,CACnC,GAAA,EAAK,CAAA,EACL,WAAA,aACC,CAAA;AAAA;;;;YCngBO,sBAAA,iBAAuC,mBAAA;;;;IAI/C,OAAA;IFKS;;;IEAT,IAAA;IFED;;;IEGC,UAAA;EAAA;AAAA;;;;;;;AFCJ;;;cEca,mBAAA,EAAmB,QAAA,CAAA,OAAA,CAW9B,QAAA,CAX8B,MAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/server/swagger/primitives/$swagger.ts","../../../src/server/swagger/providers/ServerSwaggerProvider.ts","../../../src/server/swagger/index.ts"],"sourcesContent":["import { createPrimitive, KIND, Primitive } from \"alepha\";\n\n/**\n * Creates an OpenAPI/Swagger documentation primitive with interactive UI.\n *\n * Automatically generates API documentation from your $action primitives and serves\n * an interactive Swagger UI for testing endpoints. Supports customization, tag filtering,\n * and OAuth configuration.\n *\n * @example\n * ```ts\n * class App {\n * docs = $swagger({\n * prefix: \"/api-docs\",\n * info: {\n * title: \"My API\",\n * version: \"1.0.0\",\n * description: \"REST API documentation\"\n * },\n * excludeTags: [\"internal\"],\n * ui: { root: \"/swagger\" }\n * });\n * }\n * ```\n */\nexport const $swagger = (\n options: SwaggerPrimitiveOptions = {},\n): SwaggerPrimitive => {\n return createPrimitive(SwaggerPrimitive, options);\n};\n\nexport interface SwaggerPrimitiveOptions {\n info?: OpenApiDocument[\"info\"];\n\n /**\n * @default: \"/docs\"\n */\n prefix?: string;\n\n /**\n * If true, docs will be disabled.\n */\n disabled?: boolean;\n\n /**\n * Tags to exclude from the documentation.\n */\n excludeTags?: string[];\n\n /**\n * Server URLs for the API.\n * If not provided, the server hostname is used automatically.\n */\n servers?: OpenApiServer[];\n\n /**\n * Enable Swagger UI.\n *\n * @default true\n */\n ui?: boolean | SwaggerUiOptions;\n\n /**\n * Function to rewrite the OpenAPI document before serving it.\n */\n rewrite?: (doc: OpenApiDocument) => void;\n}\n\nexport interface OpenApiServer {\n url: string;\n description?: string;\n}\n\nexport interface SwaggerUiOptions {\n root?: string;\n\n /**\n * If true, the authorization data is persisted in browser localStorage.\n *\n * @default true\n */\n persistAuthorization?: boolean;\n\n initOAuth?: {\n /**\n * Default clientId.\n */\n clientId?: string;\n\n /**\n * realm query parameter (for oauth1) added to authorizationUrl and tokenUrl.\n */\n realm?: string;\n\n /**\n * application name, displayed in authorization popup.\n */\n appName?: string;\n\n /**\n * scope separator for passing scopes, encoded before calling, default\n * value is a space (encoded value %20).\n *\n * @default ' '\n */\n scopeSeparator?: string;\n\n /**\n * string array or scope separator (i.e. space) separated string of\n * initially selected oauth scopes\n *\n * @default []\n */\n scopes?: string | string[];\n\n /**\n * Additional query parameters added to authorizationUrl and tokenUrl.\n * MUST be an object\n */\n additionalQueryStringParams?: { [key: string]: any };\n\n /**\n * Only activated for the accessCode flow. During the authorization_code\n * request to the tokenUrl, pass the Client Password using the HTTP Basic\n * Authentication scheme (Authorization header with Basic\n * base64encode(client_id + client_secret)).\n *\n * @default false\n */\n useBasicAuthenticationWithAccessCodeGrant?: boolean;\n\n /**\n * Only applies to Authorization Code flows. Proof Key for Code Exchange\n * brings enhanced security for OAuth public clients.\n *\n * @default false\n */\n usePkceWithAuthorizationCodeGrant?: boolean;\n };\n}\n\nexport class SwaggerPrimitive extends Primitive<SwaggerPrimitiveOptions> {}\n\n$swagger[KIND] = SwaggerPrimitive;\n\nexport interface OpenApiDocument {\n openapi: string;\n info: {\n title: string;\n version: string;\n description?: string;\n };\n servers?: OpenApiServer[];\n paths: Record<string, any>;\n components?: {\n schemas?: Record<string, any>;\n securitySchemes?: Record<string, any>;\n };\n}\n\nexport interface OpenApiOperation {\n tags?: string[];\n summary?: string;\n description?: string;\n operationId?: string;\n deprecated?: boolean;\n parameters?: Array<{\n name: string;\n in: \"query\" | \"header\" | \"path\" | \"cookie\";\n description?: string;\n required?: boolean;\n schema: any;\n }>;\n requestBody?: {\n description?: string;\n content: Record<\n string,\n {\n schema: any;\n }\n >;\n required?: boolean;\n };\n responses: Record<\n string,\n {\n description: string;\n content?: Record<\n string,\n {\n schema: any;\n }\n >;\n }\n >;\n security?: Array<Record<string, any[]>>;\n}\n","import { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport {\n $atom,\n $hook,\n $inject,\n $state,\n Alepha,\n isTypeFile,\n type Static,\n type TObject,\n type TSchema,\n t,\n} from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport {\n $action,\n type ActionPrimitive,\n type RequestConfigSchema,\n ServerProvider,\n ServerRouterProvider,\n} from \"alepha/server\";\nimport { ServerStaticProvider } from \"alepha/server/static\";\nimport { FileSystemProvider } from \"alepha/system\";\nimport {\n $swagger,\n type OpenApiDocument,\n type OpenApiOperation,\n type SwaggerPrimitiveOptions,\n} from \"../primitives/$swagger.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Swagger provider configuration atom\n */\nexport const swaggerOptions = $atom({\n name: \"alepha.server.swagger.options\",\n schema: t.object({\n excludeKeys: t.optional(\n t.array(t.string(), {\n description: \"Keys to exclude from swagger schema\",\n }),\n ),\n }),\n default: {\n excludeKeys: [],\n },\n});\n\nexport type ServerSwaggerProviderOptions = Static<typeof swaggerOptions.schema>;\n\ndeclare module \"alepha\" {\n interface State {\n [swaggerOptions.key]: ServerSwaggerProviderOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class ServerSwaggerProvider {\n protected readonly serverStaticProvider = $inject(ServerStaticProvider);\n protected readonly serverRouterProvider = $inject(ServerRouterProvider);\n protected readonly serverProvider = $inject(ServerProvider);\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly options = $state(swaggerOptions);\n protected readonly fs = $inject(FileSystemProvider);\n\n public json?: OpenApiDocument;\n\n protected readonly configure = $hook({\n on: \"configure\",\n priority: \"last\", // wait for all configurations, sometimes some actions are registered late!\n handler: async (alepha) => {\n const options = alepha.primitives($swagger)?.[0]?.options;\n if (!options) {\n return;\n }\n\n this.json = await this.setupSwaggerPlugin(options);\n },\n });\n\n public generateSwaggerDoc(options: SwaggerPrimitiveOptions): OpenApiDocument {\n const json = this.configureOpenApi(\n this.alepha.primitives($action),\n options,\n );\n\n if (options.rewrite) {\n options.rewrite(json);\n }\n\n return json;\n }\n\n protected async setupSwaggerPlugin(\n options: SwaggerPrimitiveOptions,\n ): Promise<OpenApiDocument | undefined> {\n if (options.disabled) {\n return;\n }\n\n const json = this.generateSwaggerDoc(options);\n\n const prefix = options.prefix ?? \"/docs\";\n\n this.configureSwaggerApi(prefix, json);\n\n if (options.ui !== false) {\n await this.configureSwaggerUi(prefix, options);\n } else {\n this.log.info(`Swagger API available at ${prefix}/json`);\n }\n\n return json;\n }\n\n protected configureOpenApi(\n actions: ActionPrimitive<RequestConfigSchema>[],\n doc: SwaggerPrimitiveOptions,\n ): OpenApiDocument {\n const openApi: OpenApiDocument = {\n openapi: \"3.0.0\",\n info: doc.info ?? {\n title: \"API Documentation\",\n version: \"1.0.0\",\n },\n servers: doc.servers ?? [{ url: this.serverProvider.hostname }],\n paths: {},\n components: {},\n };\n\n let hasSecurity = false;\n const excludeTags = doc.excludeTags ?? [];\n const schemas: Record<string, any> = {};\n\n const schema = (source: TSchema) => {\n if (\"title\" in source && typeof source.title === \"string\") {\n schemas[source.title] = copy(source);\n return { $ref: `#/components/schemas/${source.title}` };\n }\n return copy(source);\n };\n\n const copy = (obj: any) => {\n const newValue = JSON.parse(JSON.stringify(obj));\n this.removePrivateFields(newValue, [\n ...(this.options.excludeKeys || []),\n \"~options\",\n ]);\n return newValue;\n };\n\n for (const route of actions) {\n if (!route.options.schema) {\n continue;\n }\n\n const response = this.getResponseSchema(route);\n if (!response) {\n continue;\n }\n\n if (excludeTags.includes(route.group)) {\n continue;\n }\n\n if (route.options.hide) {\n continue;\n }\n\n const operation: OpenApiOperation = {\n operationId: route.name,\n summary: route.options.summary,\n description: route.options.description,\n deprecated: route.options.deprecated || undefined,\n tags: [route.group.replaceAll(\":\", \" / \")],\n responses: {\n [response.status]: {\n description: this.getStatusDescription(response.status),\n content: response.type\n ? {\n [response.type]: {\n schema: schema(response.schema),\n },\n }\n : undefined,\n },\n },\n };\n\n const isSecured = route.middlewares.some((m) => m?.name === \"$secure\");\n\n if (isSecured) {\n operation.security = [{ bearerAuth: [] }];\n operation.responses[\"401\"] = {\n description: \"Unauthorized\",\n };\n hasSecurity = true;\n }\n\n if (\n t.schema.isObject(route.options.schema.body) ||\n t.schema.isArray(route.options.schema.body)\n ) {\n if (\n t.schema.isObject(route.options.schema.body) &&\n this.isBodyMultipart(route.options.schema.body)\n ) {\n operation.requestBody = {\n required: true,\n content: {\n \"multipart/form-data\": {\n schema: schema(route.options.schema.body),\n },\n },\n };\n } else {\n operation.requestBody = {\n required: true,\n content: {\n \"application/json\": {\n schema: schema(route.options.schema.body),\n },\n },\n };\n }\n }\n\n if (t.schema.isObject(route.options.schema.query)) {\n operation.parameters ??= [];\n const requiredKeys: string[] =\n route.options.schema.query.required ?? [];\n for (const [key, value] of Object.entries(\n route.options.schema.query.properties,\n )) {\n const param: any = {\n name: key,\n in: \"query\",\n required: requiredKeys.includes(key),\n schema: schema(value),\n };\n const example = this.extractExample(value);\n if (example !== undefined) param.example = example;\n operation.parameters.push(param);\n }\n }\n\n if (t.schema.isObject(route.options.schema.params)) {\n operation.parameters ??= [];\n for (const [key, value] of Object.entries(\n route.options.schema.params.properties,\n )) {\n const description =\n \"description\" in value && typeof value.description === \"string\"\n ? value.description\n : undefined;\n const ref = schema(value);\n ref.description = undefined;\n const param: any = {\n name: key,\n in: \"path\",\n required: true,\n description,\n schema: ref,\n };\n const example = this.extractExample(value);\n if (example !== undefined) param.example = example;\n operation.parameters.push(param);\n }\n }\n\n const hasValidation =\n operation.requestBody || operation.parameters?.length;\n if (hasValidation) {\n operation.responses[\"400\"] = {\n description: \"Bad Request\",\n };\n }\n\n const url = route.prefix + this.replacePathParams(route.path);\n\n openApi.paths[url] = {\n ...openApi.paths[url],\n [route.method.toLowerCase()]: operation,\n };\n }\n\n if (hasSecurity && openApi.components) {\n openApi.components.securitySchemes = {\n bearerAuth: {\n type: \"http\",\n scheme: \"bearer\",\n bearerFormat: \"JWT\",\n description:\n \"Enter a JWT token or API key. Both are accepted as Bearer tokens.\",\n },\n };\n }\n\n if (openApi.components) openApi.components.schemas = schemas;\n\n return JSON.parse(JSON.stringify(openApi));\n }\n\n public isBodyMultipart(schema: TObject): boolean {\n for (const key in schema.properties) {\n if (isTypeFile(schema.properties[key])) {\n return true;\n }\n }\n return false;\n }\n\n public replacePathParams(url: string): string {\n return url.replace(/:\\w+/g, (match) => {\n const paramName = match.slice(1);\n return `{${paramName}}`;\n });\n }\n\n public getResponseSchema(route: ActionPrimitive<RequestConfigSchema>):\n | {\n type?: string;\n schema?: any;\n status: number;\n }\n | undefined {\n const schema: any = route.options.schema?.response;\n if (!schema) {\n return {\n status: 204,\n };\n }\n\n if (t.schema.isObject(schema) || t.schema.isArray(schema)) {\n return {\n schema,\n status: 200,\n type: \"application/json\",\n };\n }\n\n if (t.schema.isString(schema)) {\n return {\n schema,\n status: 200,\n type: \"text/plain\",\n };\n }\n\n if (\n t.schema.isNumber(schema) ||\n t.schema.isInteger(schema) ||\n t.schema.isBoolean(schema)\n ) {\n return {\n schema,\n status: 200,\n type: \"application/json\",\n };\n }\n\n if (isTypeFile(schema)) {\n return {\n schema,\n status: 200,\n type: \"application/octet-stream\",\n };\n }\n\n // Status-code-keyed map: e.g. { 201: t.object({...}) }\n const status = Object.keys(schema)[0];\n if (status && !Number.isNaN(Number(status))) {\n const inner = schema[status];\n return {\n schema: inner,\n status: Number(status),\n type: this.getContentType(inner),\n };\n }\n }\n\n protected extractExample(schema: any): any {\n if (\"examples\" in schema && Array.isArray(schema.examples)) {\n return schema.examples[0];\n }\n if (\"default\" in schema) {\n return schema.default;\n }\n return undefined;\n }\n\n protected getStatusDescription(status: number): string {\n switch (status) {\n case 200:\n return \"OK\";\n case 201:\n return \"Created\";\n case 204:\n return \"No Content\";\n case 400:\n return \"Bad Request\";\n case 401:\n return \"Unauthorized\";\n case 403:\n return \"Forbidden\";\n case 404:\n return \"Not Found\";\n case 500:\n return \"Internal Server Error\";\n default:\n return \"\";\n }\n }\n\n protected getContentType(schema: any): string | undefined {\n if (!schema) return undefined;\n if (t.schema.isObject(schema) || t.schema.isArray(schema)) {\n return \"application/json\";\n }\n if (t.schema.isString(schema)) return \"text/plain\";\n if (\n t.schema.isNumber(schema) ||\n t.schema.isInteger(schema) ||\n t.schema.isBoolean(schema)\n ) {\n return \"application/json\";\n }\n if (isTypeFile(schema)) return \"application/octet-stream\";\n return \"application/json\";\n }\n\n protected configureSwaggerApi(prefix: string, json: OpenApiDocument): void {\n this.serverRouterProvider.createRoute({\n method: \"GET\",\n path: `${prefix}/json`,\n schema: {\n response: t.json(),\n },\n handler: () => json,\n });\n }\n\n protected async configureSwaggerUi(\n prefix: string,\n options: SwaggerPrimitiveOptions,\n ): Promise<void> {\n const ui = typeof options.ui === \"object\" ? options.ui : {};\n const persistAuth = ui.persistAuthorization !== false;\n const initializer = `\nwindow.onload = function() {\n\twindow.ui = SwaggerUIBundle({\n\t\turl: \"${prefix}/json\",\n\t\tdom_id: '#swagger-ui',\n\t\tdeepLinking: true,\n\t\tpersistAuthorization: ${persistAuth},\n\t\tpresets: [\n\t\t\tSwaggerUIBundle.presets.apis,\n\t\t\tSwaggerUIStandalonePreset\n\t\t],\n\t\tplugins: [\n\t\t\tSwaggerUIBundle.plugins.DownloadUrl\n\t\t],\n\t\tlayout: \"BaseLayout\"\n\t});\n\n document.body.style.backgroundColor = \"#f2f2f2\";\n\n\tconst options = ${JSON.stringify(ui)};\n\n\tif (options.initOAuth) {\n\t\tui.initOAuth(options.initOAuth);\n\t}\n};\n\t\t`.trim();\n\n if (!this.alepha.isServerless()) {\n const dirname = fileURLToPath(import.meta.url);\n\n const root = await this.getAssetPath(\n ui.root,\n // TODO: this is shitty, take time to get the correct path\n join(dirname, \"../../assets/swagger-ui\"),\n join(dirname, \"../../../assets/swagger-ui\"),\n join(dirname, \"../../../../assets/swagger-ui\"),\n join(dirname, \"../../../../../assets/swagger-ui\"),\n );\n\n if (!root) {\n this.log.warn(`Failed to locate Swagger UI assets for path ${prefix}`);\n return;\n }\n\n await this.serverStaticProvider.createStaticServer({\n path: prefix,\n root,\n });\n }\n\n this.serverRouterProvider.createRoute({\n method: \"GET\",\n path: `${prefix}/swagger-initializer.js`,\n handler: ({ reply }) => {\n reply.headers[\"content-type\"] = \"application/javascript; charset=utf-8\";\n return initializer;\n },\n });\n\n this.log.info(\"SwaggerUI OK\", {\n url: `${this.serverProvider.hostname}${prefix}`,\n });\n }\n\n protected async getAssetPath(\n ...paths: (string | undefined)[]\n ): Promise<string | undefined> {\n for (const path of paths) {\n if (!path) continue;\n const exists = await this.fs.exists(path);\n if (exists) {\n return path;\n }\n }\n }\n\n public removePrivateFields<T extends Record<string, any>>(\n obj: T,\n excludeList: string[],\n ): T {\n if (obj === null || typeof obj !== \"object\") return obj;\n\n const visited = new WeakSet();\n\n const traverse = (o: any): void => {\n if (visited.has(o)) return;\n visited.add(o);\n\n if (Array.isArray(o)) {\n for (let i = 0; i < o.length; i++) {\n const item = o[i];\n if (item !== null && typeof item === \"object\") {\n traverse(item);\n }\n }\n } else {\n for (const excludeKey of excludeList) {\n if (excludeKey in o) {\n delete o[excludeKey];\n }\n }\n for (const key in o) {\n const item = o[key];\n if (item !== null && typeof item === \"object\") {\n traverse(item);\n }\n }\n }\n };\n\n traverse(obj);\n return obj;\n }\n}\n","import \"alepha/security\";\nimport { $module } from \"alepha\";\nimport { AlephaServer, type RequestConfigSchema } from \"alepha/server\";\nimport { AlephaServerEtag } from \"alepha/server/etag\";\nimport { AlephaServerStatic } from \"alepha/server/static\";\nimport { $swagger } from \"./primitives/$swagger.ts\";\nimport { ServerSwaggerProvider } from \"./providers/ServerSwaggerProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$swagger.ts\";\nexport * from \"./providers/ServerSwaggerProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha/server\" {\n interface ActionPrimitiveOptions<TConfig extends RequestConfigSchema> {\n /**\n * Short description of the route.\n */\n summary?: string;\n\n /**\n * Don't include this action in the Swagger documentation.\n */\n hide?: boolean;\n\n /**\n * Mark this action as deprecated in the documentation.\n */\n deprecated?: boolean;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Automatic API documentation generation.\n *\n * **Features:**\n * - Swagger/OpenAPI configuration\n * - Routes: `GET /swagger/ui`, `GET /swagger.json`\n *\n * @module alepha.server.swagger\n */\nexport const AlephaServerSwagger = $module({\n name: \"alepha.server.swagger\",\n primitives: [$swagger],\n services: [ServerSwaggerProvider],\n register: (alepha) => {\n alepha.with(AlephaServer);\n alepha.with(AlephaServerEtag);\n alepha.with(AlephaServerStatic);\n alepha.with(ServerSwaggerProvider);\n alepha.store.push(\"alepha.build.assets\", \"alepha\");\n },\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/server/swagger/primitives/$swagger.ts","../../../src/server/swagger/providers/ServerSwaggerProvider.ts","../../../src/server/swagger/index.ts"],"sourcesContent":["import { createPrimitive, KIND, Primitive } from \"alepha\";\n\n/**\n * Creates an OpenAPI/Swagger documentation primitive with interactive UI.\n *\n * Automatically generates API documentation from your $action primitives and serves\n * an interactive Swagger UI for testing endpoints. Supports customization, tag filtering,\n * and OAuth configuration.\n *\n * @example\n * ```ts\n * class App {\n * docs = $swagger({\n * prefix: \"/api-docs\",\n * info: {\n * title: \"My API\",\n * version: \"1.0.0\",\n * description: \"REST API documentation\"\n * },\n * excludeTags: [\"internal\"],\n * ui: { root: \"/swagger\" }\n * });\n * }\n * ```\n */\nexport const $swagger = (\n options: SwaggerPrimitiveOptions = {},\n): SwaggerPrimitive => {\n return createPrimitive(SwaggerPrimitive, options);\n};\n\nexport interface SwaggerPrimitiveOptions {\n info?: OpenApiDocument[\"info\"];\n\n /**\n * @default: \"/docs\"\n */\n prefix?: string;\n\n /**\n * If true, docs will be disabled.\n */\n disabled?: boolean;\n\n /**\n * Tags to exclude from the documentation.\n */\n excludeTags?: string[];\n\n /**\n * Server URLs for the API.\n * If not provided, the server hostname is used automatically.\n */\n servers?: OpenApiServer[];\n\n /**\n * Enable Swagger UI.\n *\n * @default true\n */\n ui?: boolean | SwaggerUiOptions;\n\n /**\n * Function to rewrite the OpenAPI document before serving it.\n */\n rewrite?: (doc: OpenApiDocument) => void;\n}\n\nexport interface OpenApiServer {\n url: string;\n description?: string;\n}\n\nexport interface SwaggerUiOptions {\n root?: string;\n\n /**\n * If true, the authorization data is persisted in browser localStorage.\n *\n * @default true\n */\n persistAuthorization?: boolean;\n\n initOAuth?: {\n /**\n * Default clientId.\n */\n clientId?: string;\n\n /**\n * realm query parameter (for oauth1) added to authorizationUrl and tokenUrl.\n */\n realm?: string;\n\n /**\n * application name, displayed in authorization popup.\n */\n appName?: string;\n\n /**\n * scope separator for passing scopes, encoded before calling, default\n * value is a space (encoded value %20).\n *\n * @default ' '\n */\n scopeSeparator?: string;\n\n /**\n * string array or scope separator (i.e. space) separated string of\n * initially selected oauth scopes\n *\n * @default []\n */\n scopes?: string | string[];\n\n /**\n * Additional query parameters added to authorizationUrl and tokenUrl.\n * MUST be an object\n */\n additionalQueryStringParams?: { [key: string]: any };\n\n /**\n * Only activated for the accessCode flow. During the authorization_code\n * request to the tokenUrl, pass the Client Password using the HTTP Basic\n * Authentication scheme (Authorization header with Basic\n * base64encode(client_id + client_secret)).\n *\n * @default false\n */\n useBasicAuthenticationWithAccessCodeGrant?: boolean;\n\n /**\n * Only applies to Authorization Code flows. Proof Key for Code Exchange\n * brings enhanced security for OAuth public clients.\n *\n * @default false\n */\n usePkceWithAuthorizationCodeGrant?: boolean;\n };\n}\n\nexport class SwaggerPrimitive extends Primitive<SwaggerPrimitiveOptions> {}\n\n$swagger[KIND] = SwaggerPrimitive;\n\nexport interface OpenApiDocument {\n openapi: string;\n info: {\n title: string;\n version: string;\n description?: string;\n };\n servers?: OpenApiServer[];\n paths: Record<string, any>;\n components?: {\n schemas?: Record<string, any>;\n securitySchemes?: Record<string, any>;\n };\n}\n\nexport interface OpenApiOperation {\n tags?: string[];\n summary?: string;\n description?: string;\n operationId?: string;\n deprecated?: boolean;\n parameters?: Array<{\n name: string;\n in: \"query\" | \"header\" | \"path\" | \"cookie\";\n description?: string;\n required?: boolean;\n schema: any;\n }>;\n requestBody?: {\n description?: string;\n content: Record<\n string,\n {\n schema: any;\n }\n >;\n required?: boolean;\n };\n responses: Record<\n string,\n {\n description: string;\n content?: Record<\n string,\n {\n schema: any;\n }\n >;\n }\n >;\n security?: Array<Record<string, any[]>>;\n}\n","import { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport {\n $atom,\n $hook,\n $inject,\n $state,\n Alepha,\n isTypeFile,\n type Static,\n type TObject,\n type TSchema,\n t,\n} from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport {\n $action,\n type ActionPrimitive,\n type RequestConfigSchema,\n ServerProvider,\n ServerRouterProvider,\n} from \"alepha/server\";\nimport { ServerStaticProvider } from \"alepha/server/static\";\nimport { FileSystemProvider } from \"alepha/system\";\nimport {\n $swagger,\n type OpenApiDocument,\n type OpenApiOperation,\n type SwaggerPrimitiveOptions,\n} from \"../primitives/$swagger.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Swagger provider configuration atom\n */\nexport const swaggerOptions = $atom({\n name: \"alepha.server.swagger.options\",\n schema: t.object({\n excludeKeys: t.optional(\n t.array(t.string(), {\n description: \"Keys to exclude from swagger schema\",\n }),\n ),\n }),\n default: {\n excludeKeys: [],\n },\n});\n\nexport type ServerSwaggerProviderOptions = Static<typeof swaggerOptions.schema>;\n\ndeclare module \"alepha\" {\n interface State {\n [swaggerOptions.key]: ServerSwaggerProviderOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class ServerSwaggerProvider {\n protected readonly serverStaticProvider = $inject(ServerStaticProvider);\n protected readonly serverRouterProvider = $inject(ServerRouterProvider);\n protected readonly serverProvider = $inject(ServerProvider);\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly options = $state(swaggerOptions);\n protected readonly fs = $inject(FileSystemProvider);\n\n public json?: OpenApiDocument;\n\n protected readonly configure = $hook({\n on: \"configure\",\n priority: \"last\", // wait for all configurations, sometimes some actions are registered late!\n handler: async (alepha) => {\n const options = alepha.primitives($swagger)?.[0]?.options;\n if (!options) {\n return;\n }\n\n this.json = await this.setupSwaggerPlugin(options);\n },\n });\n\n public generateSwaggerDoc(options: SwaggerPrimitiveOptions): OpenApiDocument {\n const json = this.configureOpenApi(\n this.alepha.primitives($action),\n options,\n );\n\n if (options.rewrite) {\n options.rewrite(json);\n }\n\n return json;\n }\n\n protected async setupSwaggerPlugin(\n options: SwaggerPrimitiveOptions,\n ): Promise<OpenApiDocument | undefined> {\n if (options.disabled) {\n return;\n }\n\n const json = this.generateSwaggerDoc(options);\n\n const prefix = options.prefix ?? \"/docs\";\n\n this.configureSwaggerApi(prefix, json);\n\n if (options.ui !== false) {\n await this.configureSwaggerUi(prefix, options);\n } else {\n this.log.info(`Swagger API available at ${prefix}/json`);\n }\n\n return json;\n }\n\n protected configureOpenApi(\n actions: ActionPrimitive<RequestConfigSchema>[],\n doc: SwaggerPrimitiveOptions,\n ): OpenApiDocument {\n const openApi: OpenApiDocument = {\n openapi: \"3.0.0\",\n info: doc.info ?? {\n title: \"API Documentation\",\n version: \"1.0.0\",\n },\n servers: doc.servers ?? [{ url: this.serverProvider.hostname }],\n paths: {},\n components: {},\n };\n\n let hasSecurity = false;\n const excludeTags = doc.excludeTags ?? [];\n const schemas: Record<string, any> = {};\n\n const schema = (source: TSchema) => {\n if (\"title\" in source && typeof source.title === \"string\") {\n schemas[source.title] = copy(source);\n return { $ref: `#/components/schemas/${source.title}` };\n }\n return copy(source);\n };\n\n const copy = (obj: any) => {\n const newValue = JSON.parse(JSON.stringify(obj));\n this.removePrivateFields(newValue, [\n ...(this.options.excludeKeys || []),\n \"~options\",\n ]);\n return newValue;\n };\n\n for (const route of actions) {\n if (!route.options.schema) {\n continue;\n }\n\n const response = this.getResponseSchema(route);\n if (!response) {\n continue;\n }\n\n if (excludeTags.includes(route.group)) {\n continue;\n }\n\n if (route.options.hide) {\n continue;\n }\n\n const operation: OpenApiOperation = {\n operationId: route.name,\n summary: route.options.summary,\n description: route.options.description,\n deprecated: route.options.deprecated || undefined,\n tags: [route.group.replaceAll(\":\", \" / \")],\n responses: {\n [response.status]: {\n description: this.getStatusDescription(response.status),\n content: response.type\n ? {\n [response.type]: {\n schema: schema(response.schema),\n },\n }\n : undefined,\n },\n },\n };\n\n const isSecured = route.middlewares.some((m) => m?.name === \"$secure\");\n\n if (isSecured) {\n operation.security = [{ bearerAuth: [] }];\n operation.responses[\"401\"] = {\n description: \"Unauthorized\",\n };\n hasSecurity = true;\n }\n\n if (\n t.schema.isObject(route.options.schema.body) ||\n t.schema.isArray(route.options.schema.body)\n ) {\n if (\n t.schema.isObject(route.options.schema.body) &&\n this.isBodyMultipart(route.options.schema.body)\n ) {\n operation.requestBody = {\n required: true,\n content: {\n \"multipart/form-data\": {\n schema: schema(route.options.schema.body),\n },\n },\n };\n } else {\n operation.requestBody = {\n required: true,\n content: {\n \"application/json\": {\n schema: schema(route.options.schema.body),\n },\n },\n };\n }\n }\n\n if (t.schema.isObject(route.options.schema.query)) {\n operation.parameters ??= [];\n const requiredKeys: string[] =\n route.options.schema.query.required ?? [];\n for (const [key, value] of Object.entries(\n route.options.schema.query.properties,\n )) {\n const param: any = {\n name: key,\n in: \"query\",\n required: requiredKeys.includes(key),\n schema: schema(value),\n };\n const example = this.extractExample(value);\n if (example !== undefined) param.example = example;\n operation.parameters.push(param);\n }\n }\n\n if (t.schema.isObject(route.options.schema.params)) {\n operation.parameters ??= [];\n for (const [key, value] of Object.entries(\n route.options.schema.params.properties,\n )) {\n const description =\n \"description\" in value && typeof value.description === \"string\"\n ? value.description\n : undefined;\n const ref = schema(value);\n ref.description = undefined;\n const param: any = {\n name: key,\n in: \"path\",\n required: true,\n description,\n schema: ref,\n };\n const example = this.extractExample(value);\n if (example !== undefined) param.example = example;\n operation.parameters.push(param);\n }\n }\n\n const hasValidation =\n operation.requestBody || operation.parameters?.length;\n if (hasValidation) {\n operation.responses[\"400\"] = {\n description: \"Bad Request\",\n };\n }\n\n const url = route.prefix + this.replacePathParams(route.path);\n\n openApi.paths[url] = {\n ...openApi.paths[url],\n [route.method.toLowerCase()]: operation,\n };\n }\n\n if (hasSecurity && openApi.components) {\n openApi.components.securitySchemes = {\n bearerAuth: {\n type: \"http\",\n scheme: \"bearer\",\n bearerFormat: \"JWT\",\n description:\n \"Enter a JWT token or API key. Both are accepted as Bearer tokens.\",\n },\n };\n }\n\n if (openApi.components) openApi.components.schemas = schemas;\n\n return JSON.parse(JSON.stringify(openApi));\n }\n\n public isBodyMultipart(schema: TObject): boolean {\n for (const key in schema.properties) {\n if (isTypeFile(schema.properties[key])) {\n return true;\n }\n }\n return false;\n }\n\n public replacePathParams(url: string): string {\n return url.replace(/:\\w+/g, (match) => {\n const paramName = match.slice(1);\n return `{${paramName}}`;\n });\n }\n\n public getResponseSchema(route: ActionPrimitive<RequestConfigSchema>):\n | {\n type?: string;\n schema?: any;\n status: number;\n }\n | undefined {\n const schema: any = route.options.schema?.response;\n if (!schema) {\n return {\n status: 204,\n };\n }\n\n if (t.schema.isObject(schema) || t.schema.isArray(schema)) {\n return {\n schema,\n status: 200,\n type: \"application/json\",\n };\n }\n\n if (t.schema.isString(schema)) {\n return {\n schema,\n status: 200,\n type: \"text/plain\",\n };\n }\n\n if (\n t.schema.isNumber(schema) ||\n t.schema.isInteger(schema) ||\n t.schema.isBoolean(schema)\n ) {\n return {\n schema,\n status: 200,\n type: \"application/json\",\n };\n }\n\n if (isTypeFile(schema)) {\n return {\n schema,\n status: 200,\n type: \"application/octet-stream\",\n };\n }\n\n // Status-code-keyed map: e.g. { 201: t.object({...}) }\n const status = Object.keys(schema)[0];\n if (status && !Number.isNaN(Number(status))) {\n const inner = schema[status];\n return {\n schema: inner,\n status: Number(status),\n type: this.getContentType(inner),\n };\n }\n }\n\n protected extractExample(schema: any): any {\n if (\"examples\" in schema && Array.isArray(schema.examples)) {\n return schema.examples[0];\n }\n if (\"default\" in schema) {\n return schema.default;\n }\n return undefined;\n }\n\n protected getStatusDescription(status: number): string {\n switch (status) {\n case 200:\n return \"OK\";\n case 201:\n return \"Created\";\n case 204:\n return \"No Content\";\n case 400:\n return \"Bad Request\";\n case 401:\n return \"Unauthorized\";\n case 403:\n return \"Forbidden\";\n case 404:\n return \"Not Found\";\n case 500:\n return \"Internal Server Error\";\n default:\n return \"\";\n }\n }\n\n protected getContentType(schema: any): string | undefined {\n if (!schema) return undefined;\n if (t.schema.isObject(schema) || t.schema.isArray(schema)) {\n return \"application/json\";\n }\n if (t.schema.isString(schema)) return \"text/plain\";\n if (\n t.schema.isNumber(schema) ||\n t.schema.isInteger(schema) ||\n t.schema.isBoolean(schema)\n ) {\n return \"application/json\";\n }\n if (isTypeFile(schema)) return \"application/octet-stream\";\n return \"application/json\";\n }\n\n protected configureSwaggerApi(prefix: string, json: OpenApiDocument): void {\n this.serverRouterProvider.createRoute({\n method: \"GET\",\n path: `${prefix}/json`,\n schema: {\n response: t.json(),\n },\n handler: () => json,\n });\n }\n\n protected async configureSwaggerUi(\n prefix: string,\n options: SwaggerPrimitiveOptions,\n ): Promise<void> {\n const ui = typeof options.ui === \"object\" ? options.ui : {};\n const persistAuth = ui.persistAuthorization !== false;\n const initializer = `\nwindow.onload = function() {\n\twindow.ui = SwaggerUIBundle({\n\t\turl: \"${prefix}/json\",\n\t\tdom_id: '#swagger-ui',\n\t\tdeepLinking: true,\n\t\tpersistAuthorization: ${persistAuth},\n\t\tpresets: [\n\t\t\tSwaggerUIBundle.presets.apis,\n\t\t\tSwaggerUIStandalonePreset\n\t\t],\n\t\tplugins: [\n\t\t\tSwaggerUIBundle.plugins.DownloadUrl\n\t\t],\n\t\tlayout: \"BaseLayout\"\n\t});\n\n document.body.style.backgroundColor = \"#f2f2f2\";\n\n\tconst options = ${JSON.stringify(ui)};\n\n\tif (options.initOAuth) {\n\t\tui.initOAuth(options.initOAuth);\n\t}\n};\n\t\t`.trim();\n\n if (!this.alepha.isServerless()) {\n const dirname = fileURLToPath(import.meta.url);\n\n const root = await this.getAssetPath(\n ui.root,\n // TODO: this is shitty, take time to get the correct path\n join(dirname, \"../../assets/swagger-ui\"),\n join(dirname, \"../../../assets/swagger-ui\"),\n join(dirname, \"../../../../assets/swagger-ui\"),\n join(dirname, \"../../../../../assets/swagger-ui\"),\n );\n\n if (!root) {\n this.log.warn(`Failed to locate Swagger UI assets for path ${prefix}`);\n return;\n }\n\n await this.serverStaticProvider.createStaticServer({\n path: prefix,\n root,\n });\n }\n\n this.serverRouterProvider.createRoute({\n method: \"GET\",\n path: `${prefix}/swagger-initializer.js`,\n handler: ({ reply }) => {\n reply.headers[\"content-type\"] = \"application/javascript; charset=utf-8\";\n return initializer;\n },\n });\n\n this.log.info(\"SwaggerUI OK\", {\n url: `${this.serverProvider.hostname}${prefix}`,\n });\n }\n\n protected async getAssetPath(\n ...paths: (string | undefined)[]\n ): Promise<string | undefined> {\n for (const path of paths) {\n if (!path) continue;\n const exists = await this.fs.exists(path);\n if (exists) {\n return path;\n }\n }\n }\n\n public removePrivateFields<T extends Record<string, any>>(\n obj: T,\n excludeList: string[],\n ): T {\n if (obj === null || typeof obj !== \"object\") return obj;\n\n const visited = new WeakSet();\n\n const traverse = (o: any): void => {\n if (visited.has(o)) return;\n visited.add(o);\n\n if (Array.isArray(o)) {\n for (let i = 0; i < o.length; i++) {\n const item = o[i];\n if (item !== null && typeof item === \"object\") {\n traverse(item);\n }\n }\n } else {\n for (const excludeKey of excludeList) {\n if (excludeKey in o) {\n delete o[excludeKey];\n }\n }\n for (const key in o) {\n const item = o[key];\n if (item !== null && typeof item === \"object\") {\n traverse(item);\n }\n }\n }\n };\n\n traverse(obj);\n return obj;\n }\n}\n","import \"alepha/security\";\nimport { $module } from \"alepha\";\nimport { AlephaServer, type RequestConfigSchema } from \"alepha/server\";\nimport { AlephaServerEtag } from \"alepha/server/etag\";\nimport { AlephaServerStatic } from \"alepha/server/static\";\nimport { $swagger } from \"./primitives/$swagger.ts\";\nimport { ServerSwaggerProvider } from \"./providers/ServerSwaggerProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$swagger.ts\";\nexport * from \"./providers/ServerSwaggerProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha/server\" {\n interface ActionPrimitiveOptions<TConfig extends RequestConfigSchema> {\n /**\n * Short description of the route.\n */\n summary?: string;\n\n /**\n * Don't include this action in the Swagger documentation.\n */\n hide?: boolean;\n\n /**\n * Mark this action as deprecated in the documentation.\n */\n deprecated?: boolean;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Automatic API documentation generation.\n *\n * **Features:**\n * - Swagger/OpenAPI configuration\n * - Routes: `GET /swagger/ui`, `GET /swagger.json`\n *\n * @module alepha.server.swagger\n */\nexport const AlephaServerSwagger = $module({\n name: \"alepha.server.swagger\",\n primitives: [$swagger],\n services: [ServerSwaggerProvider],\n register: (alepha) => {\n alepha.with(AlephaServer);\n alepha.with(AlephaServerEtag);\n alepha.with(AlephaServerStatic);\n alepha.with(ServerSwaggerProvider);\n alepha.store.push(\"alepha.build.assets\", \"alepha\");\n },\n});\n"],"mappings":"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"}
|
package/dist/sms/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/sms/providers/MemorySmsProvider.ts","../../src/sms/providers/SmsProvider.ts","../../src/sms/primitives/$sms.ts","../../src/sms/errors/SmsError.ts","../../src/sms/providers/LocalSmsProvider.ts","../../src/sms/index.ts"],"sourcesContent":["import { $logger } from \"alepha/logger\";\nimport type { SmsProvider, SmsSendOptions } from \"./SmsProvider.ts\";\n\nexport interface SmsRecord {\n to: string;\n message: string;\n sentAt: Date;\n}\n\nexport class MemorySmsProvider implements SmsProvider {\n protected readonly log = $logger();\n public records: SmsRecord[] = [];\n\n public async send(options: SmsSendOptions): Promise<void> {\n const { to, message } = options;\n this.log.debug(\"Sending SMS to memory store\", { to, message });\n\n for (const recipient of Array.isArray(to) ? to : [to]) {\n this.records.push({\n to: recipient,\n message,\n sentAt: new Date(),\n });\n }\n }\n\n /**\n * Get the last SMS sent (for testing purposes).\n */\n public get last(): SmsRecord | undefined {\n return this.records[this.records.length - 1];\n }\n}\n","/**\n * SMS provider interface.\n *\n * All methods are asynchronous and return promises.\n */\nexport abstract class SmsProvider {\n /**\n * Send an SMS.\n *\n * @return Promise that resolves when the SMS is sent.\n */\n public abstract send(options: SmsSendOptions): Promise<void>;\n}\n\nexport type SmsSendOptions = {\n to: string | string[];\n message: string;\n};\n","import {\n AlephaError,\n createPrimitive,\n type InstantiableClass,\n KIND,\n Primitive,\n} from \"alepha\";\nimport { MemorySmsProvider } from \"../providers/MemorySmsProvider.ts\";\nimport type { SmsSendOptions } from \"../providers/SmsProvider.ts\";\nimport { SmsProvider } from \"../providers/SmsProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport const $sms = (options: SmsPrimitiveOptions = {}) =>\n createPrimitive(SmsPrimitive, options);\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface SmsPrimitiveOptions {\n name?: string;\n provider?: InstantiableClass<SmsProvider> | \"memory\";\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * SMS primitive for sending SMS messages through various providers.\n *\n * Usage:\n * ```typescript\n * class MyService {\n * private readonly welcomeSms = $sms({ name: \"welcome\" });\n *\n * async sendWelcome(phoneNumber: string, userName: string) {\n * await this.welcomeSms.send({\n * to: phoneNumber,\n * message: `Hello ${userName}! Welcome to our service.`\n * });\n * }\n * }\n * ```\n */\nexport class SmsPrimitive extends Primitive<SmsPrimitiveOptions> {\n protected readonly provider = this.$provider();\n\n public get name() {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n /**\n * Send an SMS using the configured provider.\n */\n public async send(options: SmsSendOptions): Promise<void> {\n await this.alepha.events.emit(\"sms:sending\", {\n to: options.to,\n template: this.name,\n variables: {},\n provider: this.provider,\n abort: () => {\n throw new AlephaError(\"SMS sending aborted by hook\");\n },\n });\n\n await this.provider.send(options);\n\n await this.alepha.events.emit(\"sms:sent\", {\n to: options.to,\n template: this.name,\n provider: this.provider,\n });\n }\n\n protected $provider(): SmsProvider {\n if (!this.options.provider) {\n return this.alepha.inject(SmsProvider);\n }\n if (this.options.provider === \"memory\") {\n return this.alepha.inject(MemorySmsProvider);\n }\n return this.alepha.inject(this.options.provider);\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n$sms[KIND] = SmsPrimitive;\n","import { AlephaError } from \"alepha\";\n\nexport class SmsError extends AlephaError {\n constructor(message: string, cause?: Error) {\n super(message);\n this.name = \"SmsError\";\n this.cause = cause;\n }\n}\n","import { $inject } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { FileSystemProvider } from \"alepha/system\";\nimport { SmsError } from \"../errors/SmsError.ts\";\nimport type { SmsProvider, SmsSendOptions } from \"./SmsProvider.ts\";\n\nexport interface LocalSmsProviderOptions {\n /**\n * Directory to save SMS files.\n * @default \"node_modules/.alepha/sms\" (relative to project root)\n */\n directory?: string;\n}\n\nexport class LocalSmsProvider implements SmsProvider {\n protected readonly log = $logger();\n protected readonly fs = $inject(FileSystemProvider);\n protected readonly directory: string;\n\n constructor(options: LocalSmsProviderOptions = {}) {\n this.directory = options.directory ?? \"node_modules/.alepha/sms\";\n }\n\n public async send(options: SmsSendOptions): Promise<void> {\n const { to, message } = options;\n\n this.log.debug(\"Sending SMS to local file\", {\n to,\n message,\n directory: this.directory,\n });\n\n try {\n // Ensure directory exists\n await this.fs.mkdir(this.directory, { recursive: true });\n\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n for (const recipient of Array.isArray(to) ? to : [to]) {\n const sanitizedPhone = recipient.replace(/[^0-9+]/g, \"_\");\n const filename = `${sanitizedPhone},${timestamp}.sms.json`;\n const filepath = this.fs.join(this.directory, filename);\n\n const content = this.createSmsJson({ to: recipient, message });\n await this.fs.writeFile(filepath, JSON.stringify(content, null, 2));\n\n this.log.info(\"SMS saved to local file\", { filepath, to });\n }\n } catch (error) {\n const message = `Failed to save SMS to local file: ${error instanceof Error ? error.message : String(error)}`;\n this.log.error(message, { to, directory: this.directory });\n throw new SmsError(message, error instanceof Error ? error : undefined);\n }\n }\n\n public createSmsJson(options: { to: string; message: string }): {\n to: string;\n message: string;\n sentAt: string;\n } {\n return {\n to: options.to,\n message: options.message,\n sentAt: new Date().toISOString(),\n };\n }\n}\n","import { $module } from \"alepha\";\nimport { $sms } from \"./primitives/$sms.ts\";\nimport { LocalSmsProvider } from \"./providers/LocalSmsProvider.ts\";\nimport { MemorySmsProvider } from \"./providers/MemorySmsProvider.ts\";\nimport { SmsProvider } from \"./providers/SmsProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./errors/SmsError.ts\";\nexport * from \"./primitives/$sms.ts\";\nexport * from \"./providers/LocalSmsProvider.ts\";\nexport * from \"./providers/MemorySmsProvider.ts\";\nexport * from \"./providers/SmsProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha\" {\n interface Hooks {\n \"sms:sending\": {\n to: string | string[];\n template: string;\n variables: Record<string, unknown>;\n provider: SmsProvider;\n abort(): void;\n };\n \"sms:sent\": {\n to: string | string[];\n template: string;\n provider: SmsProvider;\n };\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * SMS delivery with multiple provider support.\n *\n * **Features:**\n * - Send SMS with templates\n * - Multiple recipients\n * - Provider abstraction\n *\n * @module alepha.sms\n */\nexport const AlephaSms = $module({\n name: \"alepha.sms\",\n primitives: [$sms],\n services: [SmsProvider, MemorySmsProvider, LocalSmsProvider],\n register: (alepha) =>\n alepha.with({\n optional: true,\n provide: SmsProvider,\n use: MemorySmsProvider,\n }),\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/sms/providers/MemorySmsProvider.ts","../../src/sms/providers/SmsProvider.ts","../../src/sms/primitives/$sms.ts","../../src/sms/errors/SmsError.ts","../../src/sms/providers/LocalSmsProvider.ts","../../src/sms/index.ts"],"sourcesContent":["import { $logger } from \"alepha/logger\";\nimport type { SmsProvider, SmsSendOptions } from \"./SmsProvider.ts\";\n\nexport interface SmsRecord {\n to: string;\n message: string;\n sentAt: Date;\n}\n\nexport class MemorySmsProvider implements SmsProvider {\n protected readonly log = $logger();\n public records: SmsRecord[] = [];\n\n public async send(options: SmsSendOptions): Promise<void> {\n const { to, message } = options;\n this.log.debug(\"Sending SMS to memory store\", { to, message });\n\n for (const recipient of Array.isArray(to) ? to : [to]) {\n this.records.push({\n to: recipient,\n message,\n sentAt: new Date(),\n });\n }\n }\n\n /**\n * Get the last SMS sent (for testing purposes).\n */\n public get last(): SmsRecord | undefined {\n return this.records[this.records.length - 1];\n }\n}\n","/**\n * SMS provider interface.\n *\n * All methods are asynchronous and return promises.\n */\nexport abstract class SmsProvider {\n /**\n * Send an SMS.\n *\n * @return Promise that resolves when the SMS is sent.\n */\n public abstract send(options: SmsSendOptions): Promise<void>;\n}\n\nexport type SmsSendOptions = {\n to: string | string[];\n message: string;\n};\n","import {\n AlephaError,\n createPrimitive,\n type InstantiableClass,\n KIND,\n Primitive,\n} from \"alepha\";\nimport { MemorySmsProvider } from \"../providers/MemorySmsProvider.ts\";\nimport type { SmsSendOptions } from \"../providers/SmsProvider.ts\";\nimport { SmsProvider } from \"../providers/SmsProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport const $sms = (options: SmsPrimitiveOptions = {}) =>\n createPrimitive(SmsPrimitive, options);\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface SmsPrimitiveOptions {\n name?: string;\n provider?: InstantiableClass<SmsProvider> | \"memory\";\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * SMS primitive for sending SMS messages through various providers.\n *\n * Usage:\n * ```typescript\n * class MyService {\n * private readonly welcomeSms = $sms({ name: \"welcome\" });\n *\n * async sendWelcome(phoneNumber: string, userName: string) {\n * await this.welcomeSms.send({\n * to: phoneNumber,\n * message: `Hello ${userName}! Welcome to our service.`\n * });\n * }\n * }\n * ```\n */\nexport class SmsPrimitive extends Primitive<SmsPrimitiveOptions> {\n protected readonly provider = this.$provider();\n\n public get name() {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n /**\n * Send an SMS using the configured provider.\n */\n public async send(options: SmsSendOptions): Promise<void> {\n await this.alepha.events.emit(\"sms:sending\", {\n to: options.to,\n template: this.name,\n variables: {},\n provider: this.provider,\n abort: () => {\n throw new AlephaError(\"SMS sending aborted by hook\");\n },\n });\n\n await this.provider.send(options);\n\n await this.alepha.events.emit(\"sms:sent\", {\n to: options.to,\n template: this.name,\n provider: this.provider,\n });\n }\n\n protected $provider(): SmsProvider {\n if (!this.options.provider) {\n return this.alepha.inject(SmsProvider);\n }\n if (this.options.provider === \"memory\") {\n return this.alepha.inject(MemorySmsProvider);\n }\n return this.alepha.inject(this.options.provider);\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n$sms[KIND] = SmsPrimitive;\n","import { AlephaError } from \"alepha\";\n\nexport class SmsError extends AlephaError {\n constructor(message: string, cause?: Error) {\n super(message);\n this.name = \"SmsError\";\n this.cause = cause;\n }\n}\n","import { $inject } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { FileSystemProvider } from \"alepha/system\";\nimport { SmsError } from \"../errors/SmsError.ts\";\nimport type { SmsProvider, SmsSendOptions } from \"./SmsProvider.ts\";\n\nexport interface LocalSmsProviderOptions {\n /**\n * Directory to save SMS files.\n * @default \"node_modules/.alepha/sms\" (relative to project root)\n */\n directory?: string;\n}\n\nexport class LocalSmsProvider implements SmsProvider {\n protected readonly log = $logger();\n protected readonly fs = $inject(FileSystemProvider);\n protected readonly directory: string;\n\n constructor(options: LocalSmsProviderOptions = {}) {\n this.directory = options.directory ?? \"node_modules/.alepha/sms\";\n }\n\n public async send(options: SmsSendOptions): Promise<void> {\n const { to, message } = options;\n\n this.log.debug(\"Sending SMS to local file\", {\n to,\n message,\n directory: this.directory,\n });\n\n try {\n // Ensure directory exists\n await this.fs.mkdir(this.directory, { recursive: true });\n\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n for (const recipient of Array.isArray(to) ? to : [to]) {\n const sanitizedPhone = recipient.replace(/[^0-9+]/g, \"_\");\n const filename = `${sanitizedPhone},${timestamp}.sms.json`;\n const filepath = this.fs.join(this.directory, filename);\n\n const content = this.createSmsJson({ to: recipient, message });\n await this.fs.writeFile(filepath, JSON.stringify(content, null, 2));\n\n this.log.info(\"SMS saved to local file\", { filepath, to });\n }\n } catch (error) {\n const message = `Failed to save SMS to local file: ${error instanceof Error ? error.message : String(error)}`;\n this.log.error(message, { to, directory: this.directory });\n throw new SmsError(message, error instanceof Error ? error : undefined);\n }\n }\n\n public createSmsJson(options: { to: string; message: string }): {\n to: string;\n message: string;\n sentAt: string;\n } {\n return {\n to: options.to,\n message: options.message,\n sentAt: new Date().toISOString(),\n };\n }\n}\n","import { $module } from \"alepha\";\nimport { $sms } from \"./primitives/$sms.ts\";\nimport { LocalSmsProvider } from \"./providers/LocalSmsProvider.ts\";\nimport { MemorySmsProvider } from \"./providers/MemorySmsProvider.ts\";\nimport { SmsProvider } from \"./providers/SmsProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./errors/SmsError.ts\";\nexport * from \"./primitives/$sms.ts\";\nexport * from \"./providers/LocalSmsProvider.ts\";\nexport * from \"./providers/MemorySmsProvider.ts\";\nexport * from \"./providers/SmsProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha\" {\n interface Hooks {\n \"sms:sending\": {\n to: string | string[];\n template: string;\n variables: Record<string, unknown>;\n provider: SmsProvider;\n abort(): void;\n };\n \"sms:sent\": {\n to: string | string[];\n template: string;\n provider: SmsProvider;\n };\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * SMS delivery with multiple provider support.\n *\n * **Features:**\n * - Send SMS with templates\n * - Multiple recipients\n * - Provider abstraction\n *\n * @module alepha.sms\n */\nexport const AlephaSms = $module({\n name: \"alepha.sms\",\n primitives: [$sms],\n services: [SmsProvider, MemorySmsProvider, LocalSmsProvider],\n register: (alepha) =>\n alepha.with({\n optional: true,\n provide: SmsProvider,\n use: MemorySmsProvider,\n }),\n});\n"],"mappings":"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"}
|