alepha 0.14.0 → 0.14.2

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.
Files changed (149) hide show
  1. package/README.md +3 -3
  2. package/dist/api/audits/index.d.ts +80 -1
  3. package/dist/api/audits/index.d.ts.map +1 -1
  4. package/dist/api/audits/index.js.map +1 -1
  5. package/dist/api/files/index.d.ts +80 -1
  6. package/dist/api/files/index.d.ts.map +1 -1
  7. package/dist/api/files/index.js.map +1 -1
  8. package/dist/api/jobs/index.d.ts +236 -157
  9. package/dist/api/jobs/index.d.ts.map +1 -1
  10. package/dist/api/jobs/index.js.map +1 -1
  11. package/dist/api/notifications/index.d.ts +21 -1
  12. package/dist/api/notifications/index.d.ts.map +1 -1
  13. package/dist/api/parameters/index.d.ts +451 -4
  14. package/dist/api/parameters/index.d.ts.map +1 -1
  15. package/dist/api/parameters/index.js.map +1 -1
  16. package/dist/api/users/index.d.ts +252 -249
  17. package/dist/api/users/index.d.ts.map +1 -1
  18. package/dist/api/users/index.js +4 -0
  19. package/dist/api/users/index.js.map +1 -1
  20. package/dist/api/verifications/index.d.ts +128 -128
  21. package/dist/api/verifications/index.d.ts.map +1 -1
  22. package/dist/batch/index.js.map +1 -1
  23. package/dist/cache/core/index.js.map +1 -1
  24. package/dist/cli/index.d.ts +304 -115
  25. package/dist/cli/index.d.ts.map +1 -1
  26. package/dist/cli/index.js +650 -531
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/command/index.d.ts +210 -13
  29. package/dist/command/index.d.ts.map +1 -1
  30. package/dist/command/index.js +306 -69
  31. package/dist/command/index.js.map +1 -1
  32. package/dist/core/index.browser.js.map +1 -1
  33. package/dist/core/index.d.ts +1 -1
  34. package/dist/core/index.d.ts.map +1 -1
  35. package/dist/core/index.js +7 -6
  36. package/dist/core/index.js.map +1 -1
  37. package/dist/core/index.native.js +7 -6
  38. package/dist/core/index.native.js.map +1 -1
  39. package/dist/datetime/index.js.map +1 -1
  40. package/dist/fake/index.js.map +1 -1
  41. package/dist/file/index.d.ts.map +1 -1
  42. package/dist/file/index.js.map +1 -1
  43. package/dist/lock/redis/index.js.map +1 -1
  44. package/dist/logger/index.js.map +1 -1
  45. package/dist/mcp/index.js.map +1 -1
  46. package/dist/orm/index.browser.js +26 -5
  47. package/dist/orm/index.browser.js.map +1 -1
  48. package/dist/orm/index.d.ts +294 -215
  49. package/dist/orm/index.d.ts.map +1 -1
  50. package/dist/orm/index.js +522 -523
  51. package/dist/orm/index.js.map +1 -1
  52. package/dist/queue/redis/index.js +2 -4
  53. package/dist/queue/redis/index.js.map +1 -1
  54. package/dist/redis/index.d.ts +400 -29
  55. package/dist/redis/index.d.ts.map +1 -1
  56. package/dist/redis/index.js +412 -21
  57. package/dist/redis/index.js.map +1 -1
  58. package/dist/retry/index.js.map +1 -1
  59. package/dist/router/index.js.map +1 -1
  60. package/dist/scheduler/index.js.map +1 -1
  61. package/dist/security/index.d.ts.map +1 -1
  62. package/dist/security/index.js.map +1 -1
  63. package/dist/server/auth/index.d.ts +155 -155
  64. package/dist/server/auth/index.js.map +1 -1
  65. package/dist/server/cache/index.js.map +1 -1
  66. package/dist/server/cookies/index.browser.js.map +1 -1
  67. package/dist/server/cookies/index.js.map +1 -1
  68. package/dist/server/core/index.browser.js.map +1 -1
  69. package/dist/server/core/index.d.ts +0 -1
  70. package/dist/server/core/index.d.ts.map +1 -1
  71. package/dist/server/core/index.js.map +1 -1
  72. package/dist/server/helmet/index.d.ts +4 -1
  73. package/dist/server/helmet/index.d.ts.map +1 -1
  74. package/dist/server/helmet/index.js.map +1 -1
  75. package/dist/server/links/index.browser.js.map +1 -1
  76. package/dist/server/links/index.js.map +1 -1
  77. package/dist/server/multipart/index.d.ts.map +1 -1
  78. package/dist/server/multipart/index.js.map +1 -1
  79. package/dist/server/proxy/index.js.map +1 -1
  80. package/dist/server/rate-limit/index.js.map +1 -1
  81. package/dist/server/security/index.d.ts +9 -9
  82. package/dist/server/security/index.js.map +1 -1
  83. package/dist/server/swagger/index.js.map +1 -1
  84. package/dist/thread/index.js.map +1 -1
  85. package/dist/topic/core/index.js.map +1 -1
  86. package/dist/topic/redis/index.js +3 -3
  87. package/dist/topic/redis/index.js.map +1 -1
  88. package/dist/vite/index.js +9 -6
  89. package/dist/vite/index.js.map +1 -1
  90. package/dist/websocket/index.browser.js.map +1 -1
  91. package/dist/websocket/index.d.ts +7 -7
  92. package/dist/websocket/index.js.map +1 -1
  93. package/package.json +3 -3
  94. package/src/api/users/index.ts +4 -0
  95. package/src/cli/apps/AlephaCli.ts +36 -14
  96. package/src/cli/apps/AlephaPackageBuilderCli.ts +5 -1
  97. package/src/cli/assets/appRouterTs.ts +1 -1
  98. package/src/cli/atoms/changelogOptions.ts +45 -0
  99. package/src/cli/commands/{ViteCommands.ts → build.ts} +4 -93
  100. package/src/cli/commands/changelog.ts +244 -0
  101. package/src/cli/commands/clean.ts +14 -0
  102. package/src/cli/commands/{DrizzleCommands.ts → db.ts} +37 -124
  103. package/src/cli/commands/deploy.ts +118 -0
  104. package/src/cli/commands/dev.ts +57 -0
  105. package/src/cli/commands/format.ts +17 -0
  106. package/src/cli/commands/{CoreCommands.ts → init.ts} +2 -40
  107. package/src/cli/commands/lint.ts +17 -0
  108. package/src/cli/commands/root.ts +32 -0
  109. package/src/cli/commands/run.ts +24 -0
  110. package/src/cli/commands/test.ts +42 -0
  111. package/src/cli/commands/typecheck.ts +19 -0
  112. package/src/cli/commands/{VerifyCommands.ts → verify.ts} +1 -13
  113. package/src/cli/defineConfig.ts +24 -0
  114. package/src/cli/index.ts +17 -5
  115. package/src/cli/services/AlephaCliUtils.ts +4 -21
  116. package/src/cli/services/GitMessageParser.ts +77 -0
  117. package/src/command/helpers/EnvUtils.ts +37 -0
  118. package/src/command/index.ts +3 -1
  119. package/src/command/primitives/$command.ts +172 -6
  120. package/src/command/providers/CliProvider.ts +424 -91
  121. package/src/core/Alepha.ts +8 -5
  122. package/src/file/providers/NodeFileSystemProvider.ts +3 -1
  123. package/src/orm/index.browser.ts +1 -1
  124. package/src/orm/index.ts +18 -10
  125. package/src/orm/interfaces/PgQueryWhere.ts +1 -26
  126. package/src/orm/providers/{PostgresTypeProvider.ts → DatabaseTypeProvider.ts} +25 -3
  127. package/src/orm/providers/drivers/BunPostgresProvider.ts +225 -0
  128. package/src/orm/providers/drivers/BunSqliteProvider.ts +180 -0
  129. package/src/orm/providers/drivers/DatabaseProvider.ts +25 -0
  130. package/src/orm/providers/drivers/NodePostgresProvider.ts +0 -25
  131. package/src/orm/services/QueryManager.ts +10 -125
  132. package/src/queue/redis/providers/RedisQueueProvider.ts +2 -7
  133. package/src/redis/index.ts +65 -3
  134. package/src/redis/providers/BunRedisProvider.ts +304 -0
  135. package/src/redis/providers/BunRedisSubscriberProvider.ts +94 -0
  136. package/src/redis/providers/NodeRedisProvider.ts +280 -0
  137. package/src/redis/providers/NodeRedisSubscriberProvider.ts +94 -0
  138. package/src/redis/providers/RedisProvider.ts +134 -140
  139. package/src/redis/providers/RedisSubscriberProvider.ts +58 -49
  140. package/src/server/core/providers/BunHttpServerProvider.ts +0 -3
  141. package/src/server/core/providers/ServerBodyParserProvider.ts +3 -1
  142. package/src/server/core/providers/ServerProvider.ts +7 -4
  143. package/src/server/multipart/providers/ServerMultipartProvider.ts +3 -1
  144. package/src/server/proxy/providers/ServerProxyProvider.ts +1 -1
  145. package/src/topic/redis/providers/RedisTopicProvider.ts +3 -3
  146. package/src/vite/tasks/buildServer.ts +1 -0
  147. package/src/cli/commands/BiomeCommands.ts +0 -29
  148. package/src/cli/commands/ChangelogCommands.ts +0 -389
  149. package/src/orm/services/PgJsonQueryManager.ts +0 -511
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["result: RateLimitResult"],"sources":["../../../src/server/rate-limit/providers/ServerRateLimitProvider.ts","../../../src/server/rate-limit/primitives/$rateLimit.ts","../../../src/server/rate-limit/index.ts"],"sourcesContent":["import { $atom, $env, $hook, $inject, $use, type Static, t } from \"alepha\";\nimport { $cache } from \"alepha/cache\";\nimport { $logger } from \"alepha/logger\";\nimport {\n HttpError,\n type ServerRequest,\n ServerRouterProvider,\n} from \"alepha/server\";\nimport type { RateLimitOptions } from \"../index.ts\";\nimport type { RateLimitPrimitiveOptions } from \"../primitives/$rateLimit.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.optional(\n t.number({\n description: \"Window duration in milliseconds\",\n }),\n ),\n max: t.optional(\n t.number({\n description: \"Maximum number of requests per window\",\n }),\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});\n\nexport type RateLimitAtomOptions = Static<typeof rateLimitOptions.schema>;\n\ndeclare module \"alepha\" {\n interface State {\n [rateLimitOptions.key]: RateLimitAtomOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nconst envSchema = t.object({\n RATE_LIMIT_WINDOW_MS: t.number({\n default: 15 * 60 * 1000, // 15 minutes\n description: \"Rate limit window in milliseconds\",\n }),\n RATE_LIMIT_MAX_REQUESTS: t.number({\n default: 100,\n description: \"Maximum requests per window\",\n }),\n});\n\nexport class ServerRateLimitProvider {\n protected readonly log = $logger();\n protected readonly serverRouterProvider = $inject(ServerRouterProvider);\n protected readonly env = $env(envSchema);\n\n protected readonly cache = $cache<RateLimitData>({\n name: \"server-rate-limit\",\n ttl: [this.env.RATE_LIMIT_WINDOW_MS, \"milliseconds\"],\n });\n\n protected readonly globalOptions = $use(rateLimitOptions);\n\n /**\n * Registered rate limit configurations with their path patterns\n */\n public readonly registeredConfigs: RateLimitPrimitiveOptions[] = [];\n\n /**\n * Register a rate limit configuration (called by primitives)\n */\n public registerRateLimit(config: RateLimitPrimitiveOptions): 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: RateLimitPrimitiveOptions,\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 protected 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: ServerRequest,\n options: RateLimitOptions = {},\n ): Promise<RateLimitResult> {\n const windowMs = options.windowMs ?? this.env.RATE_LIMIT_WINDOW_MS;\n const max = options.max ?? this.env.RATE_LIMIT_MAX_REQUESTS;\n const key = this.generateKey(req);\n\n const now = Date.now();\n const windowStart = now - windowMs;\n\n // Get current rate limit data\n const currentData = (await this.cache.get(key)) || {\n count: 0,\n windowStart: now,\n hits: [],\n };\n\n // Clean old hits outside the current window\n const validHits = currentData.hits.filter(\n (hit: number) => hit >= windowStart,\n );\n\n // Check if limit exceeded\n const allowed = validHits.length < max;\n const remaining = Math.max(0, max - validHits.length);\n const resetTime = Math.max(...validHits, windowStart) + windowMs;\n\n // If allowed, record this request\n if (allowed) {\n validHits.push(now);\n await this.cache.set(key, {\n count: validHits.length,\n windowStart: Math.min(currentData.windowStart, windowStart),\n hits: validHits,\n });\n }\n\n const result: RateLimitResult = {\n allowed,\n limit: max,\n remaining: allowed ? remaining - 1 : 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: ServerRequest): string {\n // Default to IP-based rate limiting\n const ip = this.getClientIP(req);\n return `ip:${ip}`;\n }\n\n protected getClientIP(req: ServerRequest): string {\n // Check x-forwarded-for header first (for proxies/load balancers)\n const forwarded = req.headers?.[\"x-forwarded-for\"];\n if (forwarded) {\n // x-forwarded-for can contain multiple IPs, get the first one (original client)\n const firstIp = forwarded.split(\",\")[0].trim();\n if (firstIp) return firstIp;\n }\n\n return req.ip || \"unknown\";\n }\n}\n\ninterface RateLimitData {\n count: number;\n windowStart: number;\n hits: number[];\n}\n","import { $inject, createPrimitive, KIND, Primitive } from \"alepha\";\nimport type { ServerRequest } from \"alepha/server\";\nimport type { RateLimitOptions } from \"../index.ts\";\nimport {\n type RateLimitResult,\n ServerRateLimitProvider,\n} from \"../providers/ServerRateLimitProvider.ts\";\n\n/**\n * Declares rate limiting for server routes or custom usage.\n * This primitive provides methods to check rate limits and configure behavior\n * within the server request/response cycle.\n *\n * @example\n * ```ts\n * class ApiService {\n * // Apply rate limiting to specific paths\n * apiRateLimit = $rateLimit({\n * paths: [\"/api/*\"],\n * max: 100,\n * windowMs: 15 * 60 * 1000, // 15 minutes\n * });\n *\n * // Or use check() method for manual rate limiting\n * customAction = $action({\n * handler: async (req) => {\n * const result = await this.apiRateLimit.check(req);\n * if (!result.allowed) throw new Error(\"Rate limited\");\n * return \"ok\";\n * },\n * });\n * }\n * ```\n */\nexport const $rateLimit = (\n options: RateLimitPrimitiveOptions = {},\n): AbstractRateLimitPrimitive => {\n return createPrimitive(RateLimitPrimitive, options);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface RateLimitPrimitiveOptions extends RateLimitOptions {\n /** Name identifier for this rate limit (default: property key) */\n name?: string;\n /** Path patterns to match (supports wildcards like /api/*) */\n paths?: string[];\n}\n\nexport interface AbstractRateLimitPrimitive {\n readonly name: string;\n readonly options: RateLimitPrimitiveOptions;\n check(\n request: ServerRequest,\n options?: RateLimitOptions,\n ): Promise<RateLimitResult>;\n}\n\nexport class RateLimitPrimitive\n extends Primitive<RateLimitPrimitiveOptions>\n implements AbstractRateLimitPrimitive\n{\n protected readonly serverRateLimitProvider = $inject(ServerRateLimitProvider);\n\n public get name(): string {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n protected onInit() {\n // Register this rate limit configuration with the provider\n this.serverRateLimitProvider.registerRateLimit(this.options);\n }\n\n /**\n * Checks rate limit for the given request using this primitive's configuration.\n */\n public async check(\n request: ServerRequest,\n options?: RateLimitOptions,\n ): Promise<RateLimitResult> {\n const mergedOptions = { ...this.options, ...options };\n return this.serverRateLimitProvider.checkLimit(request, mergedOptions);\n }\n}\n\n$rateLimit[KIND] = RateLimitPrimitive;\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { $rateLimit } from \"./primitives/$rateLimit.ts\";\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 /** Maximum number of requests per window (default: 100) */\n max?: number;\n /** Window duration in milliseconds (default: 15 minutes) */\n windowMs?: number;\n /** Custom key generator function */\n keyGenerator?: (req: any) => string;\n /** Skip rate limiting for failed requests */\n skipFailedRequests?: boolean;\n /** Skip rate limiting for successful requests */\n skipSuccessfulRequests?: boolean;\n}\n\n/**\n * Provides rate limiting capabilities for server routes and actions with configurable limits and windows.\n *\n * The server-rate-limit module enables per-route and per-action rate limiting using either:\n * - The `$rateLimit` primitive with `paths` option for path-based rate limiting\n * - The `rateLimit` option in action primitives for action-specific limiting\n *\n * It offers sliding window rate limiting, custom key generation, and seamless integration with server routes.\n *\n * @example\n * ```ts\n * import { $rateLimit, AlephaServerRateLimit } from \"alepha/server/rate-limit\";\n *\n * class ApiService {\n * // Path-specific rate limiting\n * apiRateLimit = $rateLimit({\n * paths: [\"/api/*\"],\n * max: 100,\n * windowMs: 15 * 60 * 1000, // 15 minutes\n * });\n * }\n * ```\n *\n * @see {@link $rateLimit}\n * @module alepha.server.rate-limit\n */\nexport const AlephaServerRateLimit = $module({\n name: \"alepha.server.rate-limit\",\n primitives: [$rateLimit],\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, $env, $hook, $inject, $use, type Static, t } from \"alepha\";\nimport { $cache } from \"alepha/cache\";\nimport { $logger } from \"alepha/logger\";\nimport {\n HttpError,\n type ServerRequest,\n ServerRouterProvider,\n} from \"alepha/server\";\nimport type { RateLimitOptions } from \"../index.ts\";\nimport type { RateLimitPrimitiveOptions } from \"../primitives/$rateLimit.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.optional(\n t.number({\n description: \"Window duration in milliseconds\",\n }),\n ),\n max: t.optional(\n t.number({\n description: \"Maximum number of requests per window\",\n }),\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});\n\nexport type RateLimitAtomOptions = Static<typeof rateLimitOptions.schema>;\n\ndeclare module \"alepha\" {\n interface State {\n [rateLimitOptions.key]: RateLimitAtomOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nconst envSchema = t.object({\n RATE_LIMIT_WINDOW_MS: t.number({\n default: 15 * 60 * 1000, // 15 minutes\n description: \"Rate limit window in milliseconds\",\n }),\n RATE_LIMIT_MAX_REQUESTS: t.number({\n default: 100,\n description: \"Maximum requests per window\",\n }),\n});\n\nexport class ServerRateLimitProvider {\n protected readonly log = $logger();\n protected readonly serverRouterProvider = $inject(ServerRouterProvider);\n protected readonly env = $env(envSchema);\n\n protected readonly cache = $cache<RateLimitData>({\n name: \"server-rate-limit\",\n ttl: [this.env.RATE_LIMIT_WINDOW_MS, \"milliseconds\"],\n });\n\n protected readonly globalOptions = $use(rateLimitOptions);\n\n /**\n * Registered rate limit configurations with their path patterns\n */\n public readonly registeredConfigs: RateLimitPrimitiveOptions[] = [];\n\n /**\n * Register a rate limit configuration (called by primitives)\n */\n public registerRateLimit(config: RateLimitPrimitiveOptions): 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: RateLimitPrimitiveOptions,\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 protected 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: ServerRequest,\n options: RateLimitOptions = {},\n ): Promise<RateLimitResult> {\n const windowMs = options.windowMs ?? this.env.RATE_LIMIT_WINDOW_MS;\n const max = options.max ?? this.env.RATE_LIMIT_MAX_REQUESTS;\n const key = this.generateKey(req);\n\n const now = Date.now();\n const windowStart = now - windowMs;\n\n // Get current rate limit data\n const currentData = (await this.cache.get(key)) || {\n count: 0,\n windowStart: now,\n hits: [],\n };\n\n // Clean old hits outside the current window\n const validHits = currentData.hits.filter(\n (hit: number) => hit >= windowStart,\n );\n\n // Check if limit exceeded\n const allowed = validHits.length < max;\n const remaining = Math.max(0, max - validHits.length);\n const resetTime = Math.max(...validHits, windowStart) + windowMs;\n\n // If allowed, record this request\n if (allowed) {\n validHits.push(now);\n await this.cache.set(key, {\n count: validHits.length,\n windowStart: Math.min(currentData.windowStart, windowStart),\n hits: validHits,\n });\n }\n\n const result: RateLimitResult = {\n allowed,\n limit: max,\n remaining: allowed ? remaining - 1 : 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: ServerRequest): string {\n // Default to IP-based rate limiting\n const ip = this.getClientIP(req);\n return `ip:${ip}`;\n }\n\n protected getClientIP(req: ServerRequest): string {\n // Check x-forwarded-for header first (for proxies/load balancers)\n const forwarded = req.headers?.[\"x-forwarded-for\"];\n if (forwarded) {\n // x-forwarded-for can contain multiple IPs, get the first one (original client)\n const firstIp = forwarded.split(\",\")[0].trim();\n if (firstIp) return firstIp;\n }\n\n return req.ip || \"unknown\";\n }\n}\n\ninterface RateLimitData {\n count: number;\n windowStart: number;\n hits: number[];\n}\n","import { $inject, createPrimitive, KIND, Primitive } from \"alepha\";\nimport type { ServerRequest } from \"alepha/server\";\nimport type { RateLimitOptions } from \"../index.ts\";\nimport {\n type RateLimitResult,\n ServerRateLimitProvider,\n} from \"../providers/ServerRateLimitProvider.ts\";\n\n/**\n * Declares rate limiting for server routes or custom usage.\n * This primitive provides methods to check rate limits and configure behavior\n * within the server request/response cycle.\n *\n * @example\n * ```ts\n * class ApiService {\n * // Apply rate limiting to specific paths\n * apiRateLimit = $rateLimit({\n * paths: [\"/api/*\"],\n * max: 100,\n * windowMs: 15 * 60 * 1000, // 15 minutes\n * });\n *\n * // Or use check() method for manual rate limiting\n * customAction = $action({\n * handler: async (req) => {\n * const result = await this.apiRateLimit.check(req);\n * if (!result.allowed) throw new Error(\"Rate limited\");\n * return \"ok\";\n * },\n * });\n * }\n * ```\n */\nexport const $rateLimit = (\n options: RateLimitPrimitiveOptions = {},\n): AbstractRateLimitPrimitive => {\n return createPrimitive(RateLimitPrimitive, options);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface RateLimitPrimitiveOptions extends RateLimitOptions {\n /** Name identifier for this rate limit (default: property key) */\n name?: string;\n /** Path patterns to match (supports wildcards like /api/*) */\n paths?: string[];\n}\n\nexport interface AbstractRateLimitPrimitive {\n readonly name: string;\n readonly options: RateLimitPrimitiveOptions;\n check(\n request: ServerRequest,\n options?: RateLimitOptions,\n ): Promise<RateLimitResult>;\n}\n\nexport class RateLimitPrimitive\n extends Primitive<RateLimitPrimitiveOptions>\n implements AbstractRateLimitPrimitive\n{\n protected readonly serverRateLimitProvider = $inject(ServerRateLimitProvider);\n\n public get name(): string {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n protected onInit() {\n // Register this rate limit configuration with the provider\n this.serverRateLimitProvider.registerRateLimit(this.options);\n }\n\n /**\n * Checks rate limit for the given request using this primitive's configuration.\n */\n public async check(\n request: ServerRequest,\n options?: RateLimitOptions,\n ): Promise<RateLimitResult> {\n const mergedOptions = { ...this.options, ...options };\n return this.serverRateLimitProvider.checkLimit(request, mergedOptions);\n }\n}\n\n$rateLimit[KIND] = RateLimitPrimitive;\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { $rateLimit } from \"./primitives/$rateLimit.ts\";\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 /** Maximum number of requests per window (default: 100) */\n max?: number;\n /** Window duration in milliseconds (default: 15 minutes) */\n windowMs?: number;\n /** Custom key generator function */\n keyGenerator?: (req: any) => string;\n /** Skip rate limiting for failed requests */\n skipFailedRequests?: boolean;\n /** Skip rate limiting for successful requests */\n skipSuccessfulRequests?: boolean;\n}\n\n/**\n * Provides rate limiting capabilities for server routes and actions with configurable limits and windows.\n *\n * The server-rate-limit module enables per-route and per-action rate limiting using either:\n * - The `$rateLimit` primitive with `paths` option for path-based rate limiting\n * - The `rateLimit` option in action primitives for action-specific limiting\n *\n * It offers sliding window rate limiting, custom key generation, and seamless integration with server routes.\n *\n * @example\n * ```ts\n * import { $rateLimit, AlephaServerRateLimit } from \"alepha/server/rate-limit\";\n *\n * class ApiService {\n * // Path-specific rate limiting\n * apiRateLimit = $rateLimit({\n * paths: [\"/api/*\"],\n * max: 100,\n * windowMs: 15 * 60 * 1000, // 15 minutes\n * });\n * }\n * ```\n *\n * @see {@link $rateLimit}\n * @module alepha.server.rate-limit\n */\nexport const AlephaServerRateLimit = $module({\n name: \"alepha.server.rate-limit\",\n primitives: [$rateLimit],\n services: [AlephaServer, ServerRateLimitProvider],\n});\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import * as alepha1 from "alepha";
1
+ import * as alepha5 from "alepha";
2
2
  import { Alepha, KIND, Primitive } from "alepha";
3
3
  import { JwtProvider, Permission, SecurityProvider, UserAccount, UserAccountToken } from "alepha/security";
4
4
  import { FetchOptions, ServerRequest, ServerRouterProvider } from "alepha/server";
@@ -28,15 +28,15 @@ declare class ServerBasicAuthProvider {
28
28
  * Register a basic auth configuration (called by primitives)
29
29
  */
30
30
  registerAuth(config: BasicAuthPrimitiveConfig): void;
31
- readonly onStart: alepha1.HookPrimitive<"start">;
31
+ readonly onStart: alepha5.HookPrimitive<"start">;
32
32
  /**
33
33
  * Hook into server:onRequest to check basic auth
34
34
  */
35
- readonly onRequest: alepha1.HookPrimitive<"server:onRequest">;
35
+ readonly onRequest: alepha5.HookPrimitive<"server:onRequest">;
36
36
  /**
37
37
  * Hook into action:onRequest to check basic auth for actions
38
38
  */
39
- readonly onActionRequest: alepha1.HookPrimitive<"action:onRequest">;
39
+ readonly onActionRequest: alepha5.HookPrimitive<"action:onRequest">;
40
40
  /**
41
41
  * Check basic authentication
42
42
  */
@@ -66,9 +66,9 @@ declare class ServerSecurityProvider {
66
66
  protected readonly securityProvider: SecurityProvider;
67
67
  protected readonly jwtProvider: JwtProvider;
68
68
  protected readonly alepha: Alepha;
69
- protected readonly onConfigure: alepha1.HookPrimitive<"configure">;
70
- protected readonly onActionRequest: alepha1.HookPrimitive<"action:onRequest">;
71
- protected readonly onRequest: alepha1.HookPrimitive<"server:onRequest">;
69
+ protected readonly onConfigure: alepha5.HookPrimitive<"configure">;
70
+ protected readonly onActionRequest: alepha5.HookPrimitive<"action:onRequest">;
71
+ protected readonly onRequest: alepha5.HookPrimitive<"server:onRequest">;
72
72
  protected check(user: UserAccountToken, secure: ServerRouteSecure): void;
73
73
  /**
74
74
  * Get the user account token for a local action call.
@@ -85,7 +85,7 @@ declare class ServerSecurityProvider {
85
85
  user?: UserAccountToken | "system" | "context";
86
86
  }, permission?: Permission): UserAccountToken;
87
87
  protected createTestUser(): UserAccountToken;
88
- protected readonly onClientRequest: alepha1.HookPrimitive<"client:onRequest">;
88
+ protected readonly onClientRequest: alepha5.HookPrimitive<"client:onRequest">;
89
89
  }
90
90
  type ServerRouteSecure = {
91
91
  realm?: string;
@@ -167,7 +167,7 @@ declare module "alepha/server" {
167
167
  * @see {@link ServerSecurityProvider}
168
168
  * @module alepha.server.security
169
169
  */
170
- declare const AlephaServerSecurity: alepha1.Service<alepha1.Module>;
170
+ declare const AlephaServerSecurity: alepha5.Service<alepha5.Module>;
171
171
  //#endregion
172
172
  export { $basicAuth, AbstractBasicAuthPrimitive, AlephaServerSecurity, BasicAuthOptions, BasicAuthPrimitive, BasicAuthPrimitiveConfig, ServerBasicAuthProvider, ServerRouteSecure, ServerSecurityProvider, isBasicAuth };
173
173
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["user: UserAccountToken | undefined","ownership: boolean | string | undefined"],"sources":["../../../src/server/security/providers/ServerBasicAuthProvider.ts","../../../src/server/security/primitives/$basicAuth.ts","../../../src/server/security/providers/ServerSecurityProvider.ts","../../../src/server/security/index.ts"],"sourcesContent":["import { timingSafeEqual } from \"node:crypto\";\nimport { $hook, $inject, Alepha } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport {\n HttpError,\n type ServerRequest,\n ServerRouterProvider,\n} from \"alepha/server\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BasicAuthOptions {\n username: string;\n password: string;\n}\n\nexport interface BasicAuthPrimitiveConfig extends BasicAuthOptions {\n /** Name identifier for this basic auth (default: property key) */\n name?: string;\n /** Path patterns to match (supports wildcards like /devtools/*) */\n paths?: string[];\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class ServerBasicAuthProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly routerProvider = $inject(ServerRouterProvider);\n protected readonly realm = \"Secure Area\";\n\n /**\n * Registered basic auth primitives with their configurations\n */\n public readonly registeredAuths: BasicAuthPrimitiveConfig[] = [];\n\n /**\n * Register a basic auth configuration (called by primitives)\n */\n public registerAuth(config: BasicAuthPrimitiveConfig): void {\n this.registeredAuths.push(config);\n }\n\n public readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n for (const auth of this.registeredAuths) {\n if (auth.paths) {\n for (const pattern of auth.paths) {\n const matchedRoutes = this.routerProvider.getRoutes(pattern);\n for (const route of matchedRoutes) {\n route.secure = {\n basic: {\n username: auth.username,\n password: auth.password,\n },\n };\n }\n }\n }\n }\n\n if (this.registeredAuths.length > 0) {\n this.log.info(\n `Initialized with ${this.registeredAuths.length} registered basic-auth configurations.`,\n );\n }\n },\n });\n\n /**\n * Hook into server:onRequest to check basic auth\n */\n public readonly onRequest = $hook({\n on: \"server:onRequest\",\n handler: async ({ route, request }) => {\n const routeAuth = route.secure;\n if (\n typeof routeAuth === \"object\" &&\n \"basic\" in routeAuth &&\n routeAuth.basic\n ) {\n this.checkAuth(request, routeAuth.basic);\n }\n },\n });\n\n /**\n * Hook into action:onRequest to check basic auth for actions\n */\n public readonly onActionRequest = $hook({\n on: \"action:onRequest\",\n handler: async ({ action, request }) => {\n const routeAuth = action.route.secure;\n if (isBasicAuth(routeAuth)) {\n this.checkAuth(request, routeAuth.basic);\n }\n },\n });\n\n /**\n * Check basic authentication\n */\n public checkAuth(request: ServerRequest, options: BasicAuthOptions): void {\n const authHeader = request.headers?.authorization;\n\n if (!authHeader || !authHeader.startsWith(\"Basic \")) {\n this.sendAuthRequired(request);\n throw new HttpError({\n status: 401,\n message: \"Authentication required\",\n });\n }\n\n // decode base64 credentials\n const base64Credentials = authHeader.slice(6); // Remove \"Basic \"\n const credentials = Buffer.from(base64Credentials, \"base64\").toString(\n \"utf-8\",\n );\n\n // split only on the first colon to handle passwords with colons\n const colonIndex = credentials.indexOf(\":\");\n const username =\n colonIndex !== -1 ? credentials.slice(0, colonIndex) : credentials;\n const password = colonIndex !== -1 ? credentials.slice(colonIndex + 1) : \"\";\n\n // verify credentials using timing-safe comparison to prevent timing attacks\n const isValid = this.timingSafeCredentialCheck(\n username,\n password,\n options.username,\n options.password,\n );\n\n if (!isValid) {\n this.sendAuthRequired(request);\n this.log.warn(`Failed basic auth attempt for user`, {\n username,\n });\n throw new HttpError({\n status: 401,\n message: \"Invalid credentials\",\n });\n }\n }\n\n /**\n * Performs a timing-safe comparison of credentials to prevent timing attacks.\n * Always compares both username and password to avoid leaking which one is wrong.\n */\n protected timingSafeCredentialCheck(\n inputUsername: string,\n inputPassword: string,\n expectedUsername: string,\n expectedPassword: string,\n ): boolean {\n // Convert to buffers for timing-safe comparison\n const inputUserBuf = Buffer.from(inputUsername, \"utf-8\");\n const expectedUserBuf = Buffer.from(expectedUsername, \"utf-8\");\n const inputPassBuf = Buffer.from(inputPassword, \"utf-8\");\n const expectedPassBuf = Buffer.from(expectedPassword, \"utf-8\");\n\n // timingSafeEqual requires same-length buffers\n // When lengths differ, we compare against a dummy buffer to maintain constant time\n const userMatch = this.safeCompare(inputUserBuf, expectedUserBuf);\n const passMatch = this.safeCompare(inputPassBuf, expectedPassBuf);\n\n // Both must match - bitwise AND avoids short-circuit evaluation\n // eslint-disable-next-line no-bitwise\n return (userMatch & passMatch) === 1;\n }\n\n /**\n * Compares two buffers in constant time, handling different lengths safely.\n * Returns 1 if equal, 0 if not equal.\n */\n protected safeCompare(input: Buffer, expected: Buffer): number {\n // If lengths differ, compare input against itself to maintain timing\n // but return 0 (not equal)\n if (input.length !== expected.length) {\n // Still perform a comparison to keep timing consistent\n timingSafeEqual(input, input);\n return 0;\n }\n\n return timingSafeEqual(input, expected) ? 1 : 0;\n }\n\n /**\n * Send WWW-Authenticate header\n */\n protected sendAuthRequired(request: ServerRequest): void {\n request.reply.setHeader(\"WWW-Authenticate\", `Basic realm=\"${this.realm}\"`);\n }\n}\n\nexport const isBasicAuth = (\n value: unknown,\n): value is { basic: BasicAuthOptions } => {\n return (\n typeof value === \"object\" && !!value && \"basic\" in value && !!value.basic\n );\n};\n","import { $inject, createPrimitive, KIND, Primitive } from \"alepha\";\nimport type { ServerRequest } from \"alepha/server\";\nimport type {\n BasicAuthOptions,\n BasicAuthPrimitiveConfig,\n} from \"../providers/ServerBasicAuthProvider.ts\";\nimport { ServerBasicAuthProvider } from \"../providers/ServerBasicAuthProvider.ts\";\n\n/**\n * Declares HTTP Basic Authentication for server routes.\n * This primitive provides methods to protect routes with username/password authentication.\n */\nexport const $basicAuth = (\n options: BasicAuthPrimitiveConfig,\n): AbstractBasicAuthPrimitive => {\n return createPrimitive(BasicAuthPrimitive, options);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface AbstractBasicAuthPrimitive {\n readonly name: string;\n readonly options: BasicAuthPrimitiveConfig;\n check(request: ServerRequest, options?: BasicAuthOptions): void;\n}\n\nexport class BasicAuthPrimitive\n extends Primitive<BasicAuthPrimitiveConfig>\n implements AbstractBasicAuthPrimitive\n{\n protected readonly serverBasicAuthProvider = $inject(ServerBasicAuthProvider);\n\n public get name(): string {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n protected onInit() {\n // Register this auth configuration with the provider\n this.serverBasicAuthProvider.registerAuth(this.options);\n }\n\n /**\n * Checks basic auth for the given request using this primitive's configuration.\n */\n public check(request: ServerRequest, options?: BasicAuthOptions): void {\n const mergedOptions = { ...this.options, ...options };\n this.serverBasicAuthProvider.checkAuth(request, mergedOptions);\n }\n}\n\n$basicAuth[KIND] = BasicAuthPrimitive;\n","import { randomUUID } from \"node:crypto\";\nimport { $hook, $inject, Alepha } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport {\n JwtProvider,\n type Permission,\n SecurityProvider,\n type UserAccountToken,\n userAccountInfoSchema,\n} from \"alepha/security\";\nimport {\n $action,\n ForbiddenError,\n type ServerRequest,\n UnauthorizedError,\n} from \"alepha/server\";\nimport {\n type BasicAuthOptions,\n isBasicAuth,\n} from \"./ServerBasicAuthProvider.ts\";\n\nexport class ServerSecurityProvider {\n protected readonly log = $logger();\n protected readonly securityProvider = $inject(SecurityProvider);\n protected readonly jwtProvider = $inject(JwtProvider);\n protected readonly alepha = $inject(Alepha);\n\n protected readonly onConfigure = $hook({\n on: \"configure\",\n handler: async () => {\n for (const action of this.alepha.primitives($action)) {\n // -------------------------------------------------------------------------------------------------------------\n // if the action is disabled or not secure, we do NOT create a permission for it\n // -------------------------------------------------------------------------------------------------------------\n if (\n action.options.disabled ||\n action.options.secure === false ||\n this.securityProvider.getRealms().length === 0\n ) {\n continue;\n }\n\n const secure = action.options.secure;\n if (typeof secure !== \"object\") {\n this.securityProvider.createPermission({\n name: action.name,\n group: action.group,\n method: action.route.method,\n path: action.route.path,\n });\n }\n }\n },\n });\n\n // -------------------------------------------------------------------------------------------------------------------\n\n protected readonly onActionRequest = $hook({\n on: \"action:onRequest\",\n handler: async ({ action, request, options }) => {\n // if you set explicitly secure: false, we assume you don't want any security check\n // but only if no user is provided in options\n if (action.options.secure === false && !options.user) {\n this.log.trace(\"Skipping security check for route\");\n return;\n }\n\n if (isBasicAuth(action.route.secure)) {\n return;\n }\n\n const permission = this.securityProvider\n .getPermissions()\n .find(\n (it) =>\n it.path === action.route.path && it.method === action.route.method,\n );\n\n try {\n request.user = this.createUserFromLocalFunctionContext(\n options,\n permission,\n );\n\n const route = action.route;\n if (typeof route.secure === \"object\") {\n this.check(request.user, route.secure);\n }\n\n this.alepha.store.set(\n \"alepha.server.request.user\",\n this.alepha.codec.decode(userAccountInfoSchema, request.user),\n );\n } catch (error) {\n if (action.options.secure || permission) {\n throw error;\n }\n // else, we skip the security check\n this.log.trace(\"Skipping security check for action\");\n }\n },\n });\n\n protected readonly onRequest = $hook({\n on: \"server:onRequest\",\n priority: \"last\",\n handler: async ({ request, route }) => {\n // if you set explicitly secure: false, we assume you don't want any security check\n if (route.secure === false) {\n this.log.trace(\n \"Skipping security check for route - explicitly disabled\",\n );\n return;\n }\n\n if (isBasicAuth(route.secure)) {\n return;\n }\n\n const permission = this.securityProvider\n .getPermissions()\n .find((it) => it.path === route.path && it.method === route.method);\n\n if (!request.headers.authorization && !route.secure && !permission) {\n this.log.trace(\n \"Skipping security check for route - no authorization header and not secure\",\n );\n return;\n }\n\n try {\n // set user to request\n request.user = await this.securityProvider.createUserFromToken(\n request.headers.authorization,\n { permission },\n );\n\n if (typeof route.secure === \"object\") {\n this.check(request.user, route.secure);\n }\n\n this.alepha.store.set(\n \"alepha.server.request.user\",\n // remove sensitive info\n this.alepha.codec.decode(userAccountInfoSchema, request.user),\n );\n\n this.log.trace(\"User set from request token\", {\n user: request.user,\n permission,\n });\n } catch (error) {\n if (route.secure || permission) {\n throw error;\n }\n\n // else, we skip the security check\n this.log.trace(\n \"Skipping security check for route - error occurred\",\n error,\n );\n }\n },\n });\n\n // -------------------------------------------------------------------------------------------------------------------\n\n protected check(user: UserAccountToken, secure: ServerRouteSecure) {\n if (secure.realm) {\n if (user.realm !== secure.realm) {\n throw new ForbiddenError(\n `User must belong to realm '${secure.realm}' to access this route`,\n );\n }\n }\n }\n\n /**\n * Get the user account token for a local action call.\n * There are three possible sources for the user:\n * - `options.user`: the user passed in the options\n * - `\"system\"`: the system user from the state (you MUST set state `server.security.system.user`)\n * - `\"context\"`: the user from the request context (you MUST be in an HTTP request context)\n *\n * Priority order: `options.user` > `\"system\"` > `\"context\"`.\n *\n * In testing environment, if no user is provided, a test user is created based on the SecurityProvider's roles.\n */\n protected createUserFromLocalFunctionContext(\n options: { user?: UserAccountToken | \"system\" | \"context\" },\n permission?: Permission,\n ): UserAccountToken {\n const fromOptions =\n typeof options.user === \"object\" ? options.user : undefined;\n\n const type = typeof options.user === \"string\" ? options.user : undefined;\n\n let user: UserAccountToken | undefined;\n\n const fromContext = this.alepha.context.get<ServerRequest>(\"request\")?.user;\n const fromSystem = this.alepha.store.get(\n \"alepha.server.security.system.user\",\n );\n\n if (type === \"system\") {\n user = fromSystem;\n } else if (type === \"context\") {\n user = fromContext;\n } else {\n user = fromOptions ?? fromContext ?? fromSystem;\n }\n\n if (!user) {\n // in testing mode, we create a test user\n if (this.alepha.isTest() && !(\"user\" in options)) {\n return this.createTestUser();\n }\n\n throw new UnauthorizedError(\"User is required for calling this action\");\n }\n\n const roles =\n user.roles ??\n (this.alepha.isTest()\n ? this.securityProvider.getRoles().map((role) => role.name)\n : []);\n let ownership: boolean | string | undefined;\n\n if (permission) {\n const result = this.securityProvider.checkPermission(\n permission,\n ...roles,\n );\n if (!result.isAuthorized) {\n throw new ForbiddenError(\n `Permission '${this.securityProvider.permissionToString(permission)}' is required for this route`,\n );\n }\n ownership = result.ownership;\n }\n\n // create a new user object with ownership if needed\n return {\n ...user,\n ownership,\n };\n }\n\n // ---------------------------------------------------------------------------------------------------------------\n // TESTING ONLY\n // ---------------------------------------------------------------------------------------------------------------\n\n protected createTestUser(): UserAccountToken {\n return {\n id: randomUUID(),\n name: \"Test\",\n roles: this.securityProvider.getRoles().map((role) => role.name),\n };\n }\n\n protected readonly onClientRequest = $hook({\n on: \"client:onRequest\",\n handler: async ({ request, options }) => {\n if (!this.alepha.isTest()) {\n return;\n }\n\n // skip helper if user is explicitly set to undefined\n if (\"user\" in options && options.user === undefined) {\n return;\n }\n\n request.headers = new Headers(request.headers);\n\n if (!request.headers.has(\"authorization\")) {\n const test = this.createTestUser();\n const user =\n typeof options?.user === \"object\" ? options.user : undefined;\n const sub = user?.id ?? test.id;\n const roles = user?.roles ?? test.roles;\n\n const token = await this.jwtProvider.create(\n {\n sub,\n roles,\n },\n user?.realm ?? this.securityProvider.getRealms()[0]?.name,\n );\n\n request.headers.set(\"authorization\", `Bearer ${token}`);\n }\n },\n });\n}\n\nexport type ServerRouteSecure = {\n realm?: string;\n basic?: BasicAuthOptions;\n};\n","import { $module } from \"alepha\";\nimport {\n $permission,\n $realm,\n $role,\n AlephaSecurity,\n type UserAccount,\n type UserAccountToken,\n} from \"alepha/security\";\nimport { AlephaServer, type FetchOptions } from \"alepha/server\";\nimport { $basicAuth } from \"./primitives/$basicAuth.ts\";\nimport { ServerBasicAuthProvider } from \"./providers/ServerBasicAuthProvider.ts\";\nimport {\n type ServerRouteSecure,\n ServerSecurityProvider,\n} from \"./providers/ServerSecurityProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$basicAuth.ts\";\nexport * from \"./providers/ServerBasicAuthProvider.ts\";\nexport * from \"./providers/ServerSecurityProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha\" {\n interface State {\n /**\n * Real (or fake) user account, used for internal actions.\n *\n * If you define this, you assume that all actions are executed by this user by default.\n * > To force a different user, you need to pass it explicitly in the options.\n */\n\n \"alepha.server.security.system.user\"?: UserAccountToken;\n\n /**\n * The authenticated user account attached to the server request state.\n *\n * @internal\n */\n \"alepha.server.request.user\"?: UserAccount;\n }\n}\n\ndeclare module \"alepha/server\" {\n interface ServerRequest<TConfig> {\n user?: UserAccountToken; // for all routes, user is maybe present\n }\n\n interface ServerActionRequest<TConfig> {\n user: UserAccountToken; // for actions, user is always present\n }\n\n interface ServerRoute {\n /**\n * If true, the route will be protected by the security provider.\n * All actions are secure by default, but you can disable it for specific actions.\n */\n secure?: boolean | ServerRouteSecure;\n }\n\n interface ClientRequestOptions extends FetchOptions {\n /**\n * Forward user from the previous request.\n * If \"system\", use system user. @see {ServerSecurityProvider.localSystemUser}\n * If \"context\", use the user from the current context (e.g. request).\n *\n * @default \"system\" if provided, else \"context\" if available.\n */\n user?: UserAccountToken | \"system\" | \"context\";\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Plugin for Alepha Server that provides security features. Based on the Alepha Security module.\n *\n * By default, all $action will be guarded by a permission check.\n *\n * @see {@link ServerSecurityProvider}\n * @module alepha.server.security\n */\nexport const AlephaServerSecurity = $module({\n name: \"alepha.server.security\",\n primitives: [$realm, $role, $permission, $basicAuth],\n services: [\n AlephaServer,\n AlephaSecurity,\n ServerSecurityProvider,\n ServerBasicAuthProvider,\n ],\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/server/security/providers/ServerBasicAuthProvider.ts","../../../src/server/security/primitives/$basicAuth.ts","../../../src/server/security/providers/ServerSecurityProvider.ts","../../../src/server/security/index.ts"],"sourcesContent":["import { timingSafeEqual } from \"node:crypto\";\nimport { $hook, $inject, Alepha } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport {\n HttpError,\n type ServerRequest,\n ServerRouterProvider,\n} from \"alepha/server\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BasicAuthOptions {\n username: string;\n password: string;\n}\n\nexport interface BasicAuthPrimitiveConfig extends BasicAuthOptions {\n /** Name identifier for this basic auth (default: property key) */\n name?: string;\n /** Path patterns to match (supports wildcards like /devtools/*) */\n paths?: string[];\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class ServerBasicAuthProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly routerProvider = $inject(ServerRouterProvider);\n protected readonly realm = \"Secure Area\";\n\n /**\n * Registered basic auth primitives with their configurations\n */\n public readonly registeredAuths: BasicAuthPrimitiveConfig[] = [];\n\n /**\n * Register a basic auth configuration (called by primitives)\n */\n public registerAuth(config: BasicAuthPrimitiveConfig): void {\n this.registeredAuths.push(config);\n }\n\n public readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n for (const auth of this.registeredAuths) {\n if (auth.paths) {\n for (const pattern of auth.paths) {\n const matchedRoutes = this.routerProvider.getRoutes(pattern);\n for (const route of matchedRoutes) {\n route.secure = {\n basic: {\n username: auth.username,\n password: auth.password,\n },\n };\n }\n }\n }\n }\n\n if (this.registeredAuths.length > 0) {\n this.log.info(\n `Initialized with ${this.registeredAuths.length} registered basic-auth configurations.`,\n );\n }\n },\n });\n\n /**\n * Hook into server:onRequest to check basic auth\n */\n public readonly onRequest = $hook({\n on: \"server:onRequest\",\n handler: async ({ route, request }) => {\n const routeAuth = route.secure;\n if (\n typeof routeAuth === \"object\" &&\n \"basic\" in routeAuth &&\n routeAuth.basic\n ) {\n this.checkAuth(request, routeAuth.basic);\n }\n },\n });\n\n /**\n * Hook into action:onRequest to check basic auth for actions\n */\n public readonly onActionRequest = $hook({\n on: \"action:onRequest\",\n handler: async ({ action, request }) => {\n const routeAuth = action.route.secure;\n if (isBasicAuth(routeAuth)) {\n this.checkAuth(request, routeAuth.basic);\n }\n },\n });\n\n /**\n * Check basic authentication\n */\n public checkAuth(request: ServerRequest, options: BasicAuthOptions): void {\n const authHeader = request.headers?.authorization;\n\n if (!authHeader || !authHeader.startsWith(\"Basic \")) {\n this.sendAuthRequired(request);\n throw new HttpError({\n status: 401,\n message: \"Authentication required\",\n });\n }\n\n // decode base64 credentials\n const base64Credentials = authHeader.slice(6); // Remove \"Basic \"\n const credentials = Buffer.from(base64Credentials, \"base64\").toString(\n \"utf-8\",\n );\n\n // split only on the first colon to handle passwords with colons\n const colonIndex = credentials.indexOf(\":\");\n const username =\n colonIndex !== -1 ? credentials.slice(0, colonIndex) : credentials;\n const password = colonIndex !== -1 ? credentials.slice(colonIndex + 1) : \"\";\n\n // verify credentials using timing-safe comparison to prevent timing attacks\n const isValid = this.timingSafeCredentialCheck(\n username,\n password,\n options.username,\n options.password,\n );\n\n if (!isValid) {\n this.sendAuthRequired(request);\n this.log.warn(`Failed basic auth attempt for user`, {\n username,\n });\n throw new HttpError({\n status: 401,\n message: \"Invalid credentials\",\n });\n }\n }\n\n /**\n * Performs a timing-safe comparison of credentials to prevent timing attacks.\n * Always compares both username and password to avoid leaking which one is wrong.\n */\n protected timingSafeCredentialCheck(\n inputUsername: string,\n inputPassword: string,\n expectedUsername: string,\n expectedPassword: string,\n ): boolean {\n // Convert to buffers for timing-safe comparison\n const inputUserBuf = Buffer.from(inputUsername, \"utf-8\");\n const expectedUserBuf = Buffer.from(expectedUsername, \"utf-8\");\n const inputPassBuf = Buffer.from(inputPassword, \"utf-8\");\n const expectedPassBuf = Buffer.from(expectedPassword, \"utf-8\");\n\n // timingSafeEqual requires same-length buffers\n // When lengths differ, we compare against a dummy buffer to maintain constant time\n const userMatch = this.safeCompare(inputUserBuf, expectedUserBuf);\n const passMatch = this.safeCompare(inputPassBuf, expectedPassBuf);\n\n // Both must match - bitwise AND avoids short-circuit evaluation\n // eslint-disable-next-line no-bitwise\n return (userMatch & passMatch) === 1;\n }\n\n /**\n * Compares two buffers in constant time, handling different lengths safely.\n * Returns 1 if equal, 0 if not equal.\n */\n protected safeCompare(input: Buffer, expected: Buffer): number {\n // If lengths differ, compare input against itself to maintain timing\n // but return 0 (not equal)\n if (input.length !== expected.length) {\n // Still perform a comparison to keep timing consistent\n timingSafeEqual(input, input);\n return 0;\n }\n\n return timingSafeEqual(input, expected) ? 1 : 0;\n }\n\n /**\n * Send WWW-Authenticate header\n */\n protected sendAuthRequired(request: ServerRequest): void {\n request.reply.setHeader(\"WWW-Authenticate\", `Basic realm=\"${this.realm}\"`);\n }\n}\n\nexport const isBasicAuth = (\n value: unknown,\n): value is { basic: BasicAuthOptions } => {\n return (\n typeof value === \"object\" && !!value && \"basic\" in value && !!value.basic\n );\n};\n","import { $inject, createPrimitive, KIND, Primitive } from \"alepha\";\nimport type { ServerRequest } from \"alepha/server\";\nimport type {\n BasicAuthOptions,\n BasicAuthPrimitiveConfig,\n} from \"../providers/ServerBasicAuthProvider.ts\";\nimport { ServerBasicAuthProvider } from \"../providers/ServerBasicAuthProvider.ts\";\n\n/**\n * Declares HTTP Basic Authentication for server routes.\n * This primitive provides methods to protect routes with username/password authentication.\n */\nexport const $basicAuth = (\n options: BasicAuthPrimitiveConfig,\n): AbstractBasicAuthPrimitive => {\n return createPrimitive(BasicAuthPrimitive, options);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface AbstractBasicAuthPrimitive {\n readonly name: string;\n readonly options: BasicAuthPrimitiveConfig;\n check(request: ServerRequest, options?: BasicAuthOptions): void;\n}\n\nexport class BasicAuthPrimitive\n extends Primitive<BasicAuthPrimitiveConfig>\n implements AbstractBasicAuthPrimitive\n{\n protected readonly serverBasicAuthProvider = $inject(ServerBasicAuthProvider);\n\n public get name(): string {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n protected onInit() {\n // Register this auth configuration with the provider\n this.serverBasicAuthProvider.registerAuth(this.options);\n }\n\n /**\n * Checks basic auth for the given request using this primitive's configuration.\n */\n public check(request: ServerRequest, options?: BasicAuthOptions): void {\n const mergedOptions = { ...this.options, ...options };\n this.serverBasicAuthProvider.checkAuth(request, mergedOptions);\n }\n}\n\n$basicAuth[KIND] = BasicAuthPrimitive;\n","import { randomUUID } from \"node:crypto\";\nimport { $hook, $inject, Alepha } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport {\n JwtProvider,\n type Permission,\n SecurityProvider,\n type UserAccountToken,\n userAccountInfoSchema,\n} from \"alepha/security\";\nimport {\n $action,\n ForbiddenError,\n type ServerRequest,\n UnauthorizedError,\n} from \"alepha/server\";\nimport {\n type BasicAuthOptions,\n isBasicAuth,\n} from \"./ServerBasicAuthProvider.ts\";\n\nexport class ServerSecurityProvider {\n protected readonly log = $logger();\n protected readonly securityProvider = $inject(SecurityProvider);\n protected readonly jwtProvider = $inject(JwtProvider);\n protected readonly alepha = $inject(Alepha);\n\n protected readonly onConfigure = $hook({\n on: \"configure\",\n handler: async () => {\n for (const action of this.alepha.primitives($action)) {\n // -------------------------------------------------------------------------------------------------------------\n // if the action is disabled or not secure, we do NOT create a permission for it\n // -------------------------------------------------------------------------------------------------------------\n if (\n action.options.disabled ||\n action.options.secure === false ||\n this.securityProvider.getRealms().length === 0\n ) {\n continue;\n }\n\n const secure = action.options.secure;\n if (typeof secure !== \"object\") {\n this.securityProvider.createPermission({\n name: action.name,\n group: action.group,\n method: action.route.method,\n path: action.route.path,\n });\n }\n }\n },\n });\n\n // -------------------------------------------------------------------------------------------------------------------\n\n protected readonly onActionRequest = $hook({\n on: \"action:onRequest\",\n handler: async ({ action, request, options }) => {\n // if you set explicitly secure: false, we assume you don't want any security check\n // but only if no user is provided in options\n if (action.options.secure === false && !options.user) {\n this.log.trace(\"Skipping security check for route\");\n return;\n }\n\n if (isBasicAuth(action.route.secure)) {\n return;\n }\n\n const permission = this.securityProvider\n .getPermissions()\n .find(\n (it) =>\n it.path === action.route.path && it.method === action.route.method,\n );\n\n try {\n request.user = this.createUserFromLocalFunctionContext(\n options,\n permission,\n );\n\n const route = action.route;\n if (typeof route.secure === \"object\") {\n this.check(request.user, route.secure);\n }\n\n this.alepha.store.set(\n \"alepha.server.request.user\",\n this.alepha.codec.decode(userAccountInfoSchema, request.user),\n );\n } catch (error) {\n if (action.options.secure || permission) {\n throw error;\n }\n // else, we skip the security check\n this.log.trace(\"Skipping security check for action\");\n }\n },\n });\n\n protected readonly onRequest = $hook({\n on: \"server:onRequest\",\n priority: \"last\",\n handler: async ({ request, route }) => {\n // if you set explicitly secure: false, we assume you don't want any security check\n if (route.secure === false) {\n this.log.trace(\n \"Skipping security check for route - explicitly disabled\",\n );\n return;\n }\n\n if (isBasicAuth(route.secure)) {\n return;\n }\n\n const permission = this.securityProvider\n .getPermissions()\n .find((it) => it.path === route.path && it.method === route.method);\n\n if (!request.headers.authorization && !route.secure && !permission) {\n this.log.trace(\n \"Skipping security check for route - no authorization header and not secure\",\n );\n return;\n }\n\n try {\n // set user to request\n request.user = await this.securityProvider.createUserFromToken(\n request.headers.authorization,\n { permission },\n );\n\n if (typeof route.secure === \"object\") {\n this.check(request.user, route.secure);\n }\n\n this.alepha.store.set(\n \"alepha.server.request.user\",\n // remove sensitive info\n this.alepha.codec.decode(userAccountInfoSchema, request.user),\n );\n\n this.log.trace(\"User set from request token\", {\n user: request.user,\n permission,\n });\n } catch (error) {\n if (route.secure || permission) {\n throw error;\n }\n\n // else, we skip the security check\n this.log.trace(\n \"Skipping security check for route - error occurred\",\n error,\n );\n }\n },\n });\n\n // -------------------------------------------------------------------------------------------------------------------\n\n protected check(user: UserAccountToken, secure: ServerRouteSecure) {\n if (secure.realm) {\n if (user.realm !== secure.realm) {\n throw new ForbiddenError(\n `User must belong to realm '${secure.realm}' to access this route`,\n );\n }\n }\n }\n\n /**\n * Get the user account token for a local action call.\n * There are three possible sources for the user:\n * - `options.user`: the user passed in the options\n * - `\"system\"`: the system user from the state (you MUST set state `server.security.system.user`)\n * - `\"context\"`: the user from the request context (you MUST be in an HTTP request context)\n *\n * Priority order: `options.user` > `\"system\"` > `\"context\"`.\n *\n * In testing environment, if no user is provided, a test user is created based on the SecurityProvider's roles.\n */\n protected createUserFromLocalFunctionContext(\n options: { user?: UserAccountToken | \"system\" | \"context\" },\n permission?: Permission,\n ): UserAccountToken {\n const fromOptions =\n typeof options.user === \"object\" ? options.user : undefined;\n\n const type = typeof options.user === \"string\" ? options.user : undefined;\n\n let user: UserAccountToken | undefined;\n\n const fromContext = this.alepha.context.get<ServerRequest>(\"request\")?.user;\n const fromSystem = this.alepha.store.get(\n \"alepha.server.security.system.user\",\n );\n\n if (type === \"system\") {\n user = fromSystem;\n } else if (type === \"context\") {\n user = fromContext;\n } else {\n user = fromOptions ?? fromContext ?? fromSystem;\n }\n\n if (!user) {\n // in testing mode, we create a test user\n if (this.alepha.isTest() && !(\"user\" in options)) {\n return this.createTestUser();\n }\n\n throw new UnauthorizedError(\"User is required for calling this action\");\n }\n\n const roles =\n user.roles ??\n (this.alepha.isTest()\n ? this.securityProvider.getRoles().map((role) => role.name)\n : []);\n let ownership: boolean | string | undefined;\n\n if (permission) {\n const result = this.securityProvider.checkPermission(\n permission,\n ...roles,\n );\n if (!result.isAuthorized) {\n throw new ForbiddenError(\n `Permission '${this.securityProvider.permissionToString(permission)}' is required for this route`,\n );\n }\n ownership = result.ownership;\n }\n\n // create a new user object with ownership if needed\n return {\n ...user,\n ownership,\n };\n }\n\n // ---------------------------------------------------------------------------------------------------------------\n // TESTING ONLY\n // ---------------------------------------------------------------------------------------------------------------\n\n protected createTestUser(): UserAccountToken {\n return {\n id: randomUUID(),\n name: \"Test\",\n roles: this.securityProvider.getRoles().map((role) => role.name),\n };\n }\n\n protected readonly onClientRequest = $hook({\n on: \"client:onRequest\",\n handler: async ({ request, options }) => {\n if (!this.alepha.isTest()) {\n return;\n }\n\n // skip helper if user is explicitly set to undefined\n if (\"user\" in options && options.user === undefined) {\n return;\n }\n\n request.headers = new Headers(request.headers);\n\n if (!request.headers.has(\"authorization\")) {\n const test = this.createTestUser();\n const user =\n typeof options?.user === \"object\" ? options.user : undefined;\n const sub = user?.id ?? test.id;\n const roles = user?.roles ?? test.roles;\n\n const token = await this.jwtProvider.create(\n {\n sub,\n roles,\n },\n user?.realm ?? this.securityProvider.getRealms()[0]?.name,\n );\n\n request.headers.set(\"authorization\", `Bearer ${token}`);\n }\n },\n });\n}\n\nexport type ServerRouteSecure = {\n realm?: string;\n basic?: BasicAuthOptions;\n};\n","import { $module } from \"alepha\";\nimport {\n $permission,\n $realm,\n $role,\n AlephaSecurity,\n type UserAccount,\n type UserAccountToken,\n} from \"alepha/security\";\nimport { AlephaServer, type FetchOptions } from \"alepha/server\";\nimport { $basicAuth } from \"./primitives/$basicAuth.ts\";\nimport { ServerBasicAuthProvider } from \"./providers/ServerBasicAuthProvider.ts\";\nimport {\n type ServerRouteSecure,\n ServerSecurityProvider,\n} from \"./providers/ServerSecurityProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$basicAuth.ts\";\nexport * from \"./providers/ServerBasicAuthProvider.ts\";\nexport * from \"./providers/ServerSecurityProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha\" {\n interface State {\n /**\n * Real (or fake) user account, used for internal actions.\n *\n * If you define this, you assume that all actions are executed by this user by default.\n * > To force a different user, you need to pass it explicitly in the options.\n */\n\n \"alepha.server.security.system.user\"?: UserAccountToken;\n\n /**\n * The authenticated user account attached to the server request state.\n *\n * @internal\n */\n \"alepha.server.request.user\"?: UserAccount;\n }\n}\n\ndeclare module \"alepha/server\" {\n interface ServerRequest<TConfig> {\n user?: UserAccountToken; // for all routes, user is maybe present\n }\n\n interface ServerActionRequest<TConfig> {\n user: UserAccountToken; // for actions, user is always present\n }\n\n interface ServerRoute {\n /**\n * If true, the route will be protected by the security provider.\n * All actions are secure by default, but you can disable it for specific actions.\n */\n secure?: boolean | ServerRouteSecure;\n }\n\n interface ClientRequestOptions extends FetchOptions {\n /**\n * Forward user from the previous request.\n * If \"system\", use system user. @see {ServerSecurityProvider.localSystemUser}\n * If \"context\", use the user from the current context (e.g. request).\n *\n * @default \"system\" if provided, else \"context\" if available.\n */\n user?: UserAccountToken | \"system\" | \"context\";\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Plugin for Alepha Server that provides security features. Based on the Alepha Security module.\n *\n * By default, all $action will be guarded by a permission check.\n *\n * @see {@link ServerSecurityProvider}\n * @module alepha.server.security\n */\nexport const AlephaServerSecurity = $module({\n name: \"alepha.server.security\",\n primitives: [$realm, $role, $permission, $basicAuth],\n services: [\n AlephaServer,\n AlephaSecurity,\n ServerSecurityProvider,\n ServerBasicAuthProvider,\n ],\n});\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["openApi: OpenApiDocument","schemas: Record<string, any>","operation: OpenApiOperation","schema: any"],"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 * 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 SwaggerUiOptions {\n root?: string;\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 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 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 $use,\n Alepha,\n isTypeFile,\n type Static,\n type TObject,\n type TSchema,\n t,\n} from \"alepha\";\nimport { FileSystemProvider } from \"alepha/file\";\nimport { $logger } from \"alepha/logger\";\nimport { AlephaSecurity } from \"alepha/security\";\nimport {\n $action,\n type ActionPrimitive,\n type RequestConfigSchema,\n ServerProvider,\n ServerRouterProvider,\n} from \"alepha/server\";\nimport { ServerStaticProvider } from \"alepha/server/static\";\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 = $use(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.createSwagger(options);\n },\n });\n\n public async createSwagger(\n options: SwaggerPrimitiveOptions,\n ): Promise<OpenApiDocument | undefined> {\n if (options.disabled) {\n return;\n }\n\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 const prefix = options.prefix ?? \"/docs\";\n\n this.configureSwaggerApi(prefix, json);\n\n if (options.ui !== false) {\n await this.configureSwaggerUi(prefix, options);\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 paths: {},\n components: {},\n };\n\n const hasSecurity = this.alepha.has(AlephaSecurity);\n if (hasSecurity && openApi.components) {\n openApi.components.securitySchemes = {\n bearerAuth: {\n type: \"http\",\n scheme: \"bearer\",\n bearerFormat: \"JWT\",\n },\n };\n }\n\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 tags: [route.group.replaceAll(\":\", \" / \")],\n responses: {\n [response.status]: {\n description: \"\",\n content: response.type\n ? {\n [response.type]: {\n schema: schema(response.schema),\n },\n }\n : undefined,\n },\n },\n };\n\n if (route.options.secure !== false && hasSecurity) {\n operation.security = [{ bearerAuth: [] }];\n }\n\n const g = t.raw;\n\n if (\n g.IsObject(route.options.schema.body) ||\n g.IsArray(route.options.schema.body)\n ) {\n if (\n g.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 (g.IsObject(route.options.schema.query)) {\n operation.parameters ??= [];\n for (const [key, value] of Object.entries(\n route.options.schema.query.properties,\n )) {\n operation.parameters.push({\n name: key,\n in: \"query\",\n required: false,\n schema: schema(value),\n });\n }\n }\n\n if (g.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 delete ref.description;\n operation.parameters.push({\n name: key,\n in: \"path\",\n required: true,\n description,\n schema: ref,\n });\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 (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 (isTypeFile(schema)) {\n return {\n schema,\n status: 200,\n type: \"application/octet-stream\",\n };\n }\n\n const status = Object.keys(schema)[0];\n if (t.schema.isObject(schema[status]) || isTypeFile(schema[status])) {\n return {\n schema,\n type: t.schema.isObject(schema[status])\n ? \"application/json\"\n : \"application/octet-stream\",\n status: Number(status),\n };\n }\n }\n\n protected configureSwaggerApi(prefix: string, json: OpenApiDocument): void {\n this.serverRouterProvider.createRoute({\n method: \"GET\",\n path: `${prefix}/json`,\n cache: {\n etag: true,\n },\n schema: {\n response: t.json(),\n },\n handler: () => json,\n });\n this.log.info(`Swagger API available at ${prefix}/json`);\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 initializer = `\nwindow.onload = function() {\n\twindow.ui = SwaggerUIBundle({\n\t\turl: \"/docs/json\",\n\t\tdom_id: '#swagger-ui',\n\t\tdeepLinking: true,\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 const dirname = fileURLToPath(import.meta.url);\n\n const root = await this.getAssetPath(\n ui.root,\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 this.serverRouterProvider.createRoute({\n method: \"GET\",\n path: `${prefix}/swagger-initializer.js`,\n cache: {\n etag: true,\n },\n handler: ({ reply }) => {\n reply.headers[\"content-type\"] = \"application/javascript; charset=utf-8\";\n return initializer;\n },\n });\n\n this.log.info(\n `Swagger UI available at ${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/server/security\";\nimport { $module } from \"alepha\";\nimport { AlephaServer, type RequestConfigSchema } from \"alepha/server\";\nimport { AlephaServerCache } from \"alepha/server/cache\";\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\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Plugin for Alepha Server that provides Swagger documentation capabilities.\n * It generates OpenAPI v3 documentation for the server's endpoints ($action).\n * It also provides a Swagger UI for interactive API documentation.\n *\n * @see {@link ServerSwaggerProvider}\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(AlephaServerCache);\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 * 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 SwaggerUiOptions {\n root?: string;\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 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 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 $use,\n Alepha,\n isTypeFile,\n type Static,\n type TObject,\n type TSchema,\n t,\n} from \"alepha\";\nimport { FileSystemProvider } from \"alepha/file\";\nimport { $logger } from \"alepha/logger\";\nimport { AlephaSecurity } from \"alepha/security\";\nimport {\n $action,\n type ActionPrimitive,\n type RequestConfigSchema,\n ServerProvider,\n ServerRouterProvider,\n} from \"alepha/server\";\nimport { ServerStaticProvider } from \"alepha/server/static\";\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 = $use(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.createSwagger(options);\n },\n });\n\n public async createSwagger(\n options: SwaggerPrimitiveOptions,\n ): Promise<OpenApiDocument | undefined> {\n if (options.disabled) {\n return;\n }\n\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 const prefix = options.prefix ?? \"/docs\";\n\n this.configureSwaggerApi(prefix, json);\n\n if (options.ui !== false) {\n await this.configureSwaggerUi(prefix, options);\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 paths: {},\n components: {},\n };\n\n const hasSecurity = this.alepha.has(AlephaSecurity);\n if (hasSecurity && openApi.components) {\n openApi.components.securitySchemes = {\n bearerAuth: {\n type: \"http\",\n scheme: \"bearer\",\n bearerFormat: \"JWT\",\n },\n };\n }\n\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 tags: [route.group.replaceAll(\":\", \" / \")],\n responses: {\n [response.status]: {\n description: \"\",\n content: response.type\n ? {\n [response.type]: {\n schema: schema(response.schema),\n },\n }\n : undefined,\n },\n },\n };\n\n if (route.options.secure !== false && hasSecurity) {\n operation.security = [{ bearerAuth: [] }];\n }\n\n const g = t.raw;\n\n if (\n g.IsObject(route.options.schema.body) ||\n g.IsArray(route.options.schema.body)\n ) {\n if (\n g.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 (g.IsObject(route.options.schema.query)) {\n operation.parameters ??= [];\n for (const [key, value] of Object.entries(\n route.options.schema.query.properties,\n )) {\n operation.parameters.push({\n name: key,\n in: \"query\",\n required: false,\n schema: schema(value),\n });\n }\n }\n\n if (g.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 delete ref.description;\n operation.parameters.push({\n name: key,\n in: \"path\",\n required: true,\n description,\n schema: ref,\n });\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 (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 (isTypeFile(schema)) {\n return {\n schema,\n status: 200,\n type: \"application/octet-stream\",\n };\n }\n\n const status = Object.keys(schema)[0];\n if (t.schema.isObject(schema[status]) || isTypeFile(schema[status])) {\n return {\n schema,\n type: t.schema.isObject(schema[status])\n ? \"application/json\"\n : \"application/octet-stream\",\n status: Number(status),\n };\n }\n }\n\n protected configureSwaggerApi(prefix: string, json: OpenApiDocument): void {\n this.serverRouterProvider.createRoute({\n method: \"GET\",\n path: `${prefix}/json`,\n cache: {\n etag: true,\n },\n schema: {\n response: t.json(),\n },\n handler: () => json,\n });\n this.log.info(`Swagger API available at ${prefix}/json`);\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 initializer = `\nwindow.onload = function() {\n\twindow.ui = SwaggerUIBundle({\n\t\turl: \"/docs/json\",\n\t\tdom_id: '#swagger-ui',\n\t\tdeepLinking: true,\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 const dirname = fileURLToPath(import.meta.url);\n\n const root = await this.getAssetPath(\n ui.root,\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 this.serverRouterProvider.createRoute({\n method: \"GET\",\n path: `${prefix}/swagger-initializer.js`,\n cache: {\n etag: true,\n },\n handler: ({ reply }) => {\n reply.headers[\"content-type\"] = \"application/javascript; charset=utf-8\";\n return initializer;\n },\n });\n\n this.log.info(\n `Swagger UI available at ${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/server/security\";\nimport { $module } from \"alepha\";\nimport { AlephaServer, type RequestConfigSchema } from \"alepha/server\";\nimport { AlephaServerCache } from \"alepha/server/cache\";\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\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Plugin for Alepha Server that provides Swagger documentation capabilities.\n * It generates OpenAPI v3 documentation for the server's endpoints ($action).\n * It also provides a Swagger UI for interactive API documentation.\n *\n * @see {@link ServerSwaggerProvider}\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(AlephaServerCache);\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 +1 @@
1
- {"version":3,"file":"index.js","names":["name: string","maxPoolSize: number","idleTimeout: number","script: string","instance: ThreadInstance","message: ThreadMessage"],"sources":["../../src/thread/primitives/$thread.ts","../../src/thread/providers/ThreadProvider.ts","../../src/thread/index.ts"],"sourcesContent":["import { cpus } from \"node:os\";\nimport { MessageChannel, type MessagePort, Worker } from \"node:worker_threads\";\nimport type { TSchema } from \"alepha\";\nimport { createPrimitive, KIND, Primitive, Value } from \"alepha\";\n\n/**\n * Creates a worker thread primitive for offloading CPU-intensive tasks to separate threads.\n *\n * This primitive enables you to run JavaScript code in Node.js worker threads, allowing you to\n * leverage multiple CPU cores and avoid blocking the main event loop. It provides a pool-based\n * approach with intelligent thread reuse and automatic lifecycle management.\n *\n * **Key Features**\n *\n * - **Thread Pool Management**: Automatically manages a pool of worker threads with configurable limits\n * - **Thread Reuse**: Reuses existing threads to avoid expensive initialization overhead\n * - **Idle Cleanup**: Automatically terminates unused threads after a configurable timeout\n * - **Type-Safe Communication**: Optional TypeBox schema validation for data passed to threads\n * - **CPU-Aware Defaults**: Pool size defaults to CPU count × 2 for optimal performance\n * - **Error Handling**: Proper error propagation and thread cleanup on failures\n *\n * **Use Cases**\n *\n * Perfect for CPU-intensive tasks that would otherwise block the main thread:\n * - Image/video processing\n * - Data transformation and analysis\n * - Cryptographic operations\n * - Heavy computations and algorithms\n * - Background data processing\n *\n * @example\n * **Basic thread usage:**\n * ```ts\n * import { $thread } from \"alepha/thread\";\n *\n * class DataProcessor {\n * heavyComputation = $thread({\n * name: \"compute\",\n * handler: async () => {\n * // This runs in a separate worker thread\n * let result = 0;\n * for (let i = 0; i < 1000000; i++) {\n * result += Math.sqrt(i);\n * }\n * return { result, timestamp: Date.now() };\n * }\n * });\n *\n * async processData() {\n * // Execute in worker thread without blocking main thread\n * const result = await this.heavyComputation.execute();\n * console.log(`Computation result: ${result.result}`);\n * }\n * }\n * ```\n *\n * @example\n * **Configured thread pool with custom settings:**\n * ```ts\n * class ImageProcessor {\n * imageProcessor = $thread({\n * name: \"image-processing\",\n * maxPoolSize: 4, // Limit to 4 concurrent threads\n * idleTimeout: 30000, // Clean up idle threads after 30 seconds\n * handler: async () => {\n * // CPU-intensive image processing logic\n * return await processImageData();\n * }\n * });\n * }\n * ```\n *\n * @example\n * **Thread with data validation:**\n * ```ts\n * import { t } from \"alepha\";\n *\n * class CryptoService {\n * encrypt = $thread({\n * name: \"encryption\",\n * handler: async () => {\n * // Perform encryption operations\n * return await encryptData();\n * }\n * });\n *\n * async encryptSensitiveData(data: { text: string; key: string }) {\n * // Validate input data before sending to thread\n * const schema = t.object({\n * text: t.text(),\n * key: t.text()\n * });\n *\n * return await this.encrypt.execute(data, schema);\n * }\n * }\n * ```\n *\n * @example\n * **Parallel processing with multiple threads:**\n * ```ts\n * class BatchProcessor {\n * processor = $thread({\n * name: \"batch-worker\",\n * maxPoolSize: 8, // Allow up to 8 concurrent workers\n * handler: async () => {\n * return await processBatchItem();\n * }\n * });\n *\n * async processBatch(items: any[]) {\n * // Process multiple items in parallel across different threads\n * const promises = items.map(() => this.processor.execute());\n * const results = await Promise.all(promises);\n * return results;\n * }\n * }\n * ```\n */\nexport const $thread = (options: ThreadPrimitiveOptions): ThreadPrimitive => {\n return createPrimitive(ThreadPrimitive, options);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface ThreadPrimitiveOptions {\n /**\n * Unique name for this thread worker.\n *\n * Used for:\n * - Thread pool identification (threads with same name share the same pool)\n * - Logging and debugging\n * - Error messages and stack traces\n *\n * If not provided, defaults to the property key of the primitive.\n *\n * @example \"image-processor\"\n * @example \"crypto-worker\"\n * @example \"data-analysis\"\n */\n name?: string;\n\n /**\n * The function to execute in the worker thread.\n *\n * This function:\n * - Runs in a separate Node.js worker thread\n * - Should contain the CPU-intensive logic\n * - Can be async and return any serializable data\n * - Has access to standard Node.js APIs and modules\n * - Cannot directly access the main thread's memory or variables\n *\n * **Important**: The handler function is serialized and sent to the worker thread,\n * so it cannot reference variables from the parent scope (closures won't work).\n * All required data must be passed via the `execute()` method.\n *\n * @example\n * ```ts\n * handler: async () => {\n * // CPU-intensive work here\n * const result = performComplexCalculation();\n * return { result, completed: Date.now() };\n * }\n * ```\n */\n handler: () => any | Promise<any>;\n\n /**\n * Maximum number of worker threads in the pool.\n *\n * Controls how many threads can run concurrently for this named thread worker.\n * When all threads are busy, additional `execute()` calls will queue until a thread becomes available.\n *\n * **Default**: `cpus().length * 2` (number of CPU cores × 2)\n *\n * **Guidelines**:\n * - For CPU-bound tasks: Set to number of CPU cores\n * - For I/O-bound tasks in workers: Can be higher (2x CPU cores)\n * - For memory-intensive tasks: Set lower to avoid memory pressure\n * - For short-lived tasks: Can be higher for better throughput\n *\n * @default cpus().length * 2\n * @example 4 // Limit to 4 concurrent threads\n * @example 1 // Single worker thread (sequential processing)\n * @example 16 // High concurrency for lightweight tasks\n */\n maxPoolSize?: number;\n\n /**\n * Time in milliseconds before idle worker threads are terminated.\n *\n * When a worker thread has been idle (not executing any tasks) for this duration,\n * it will be automatically terminated to free up system resources. New threads\n * will be created as needed when new tasks are submitted.\n *\n * **Default**: 60000 (60 seconds)\n *\n * **Considerations**:\n * - Shorter timeouts: Save memory but increase thread creation overhead\n * - Longer timeouts: Keep threads ready but consume more memory\n * - Very short timeouts may cause constant thread creation/destruction\n *\n * @default 60000\n * @example 30000 // 30 seconds\n * @example 120000 // 2 minutes\n * @example 5000 // 5 seconds (for very memory-constrained environments)\n */\n idleTimeout?: number;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class ThreadPrimitive extends Primitive<ThreadPrimitiveOptions> {\n protected readonly script = process.argv[1];\n static readonly globalPool = new Map<string, ThreadPool>();\n\n public get name(): string {\n return this.options.name || this.config.propertyKey;\n }\n\n public get maxPoolSize(): number {\n return this.options.maxPoolSize || cpus().length * 2;\n }\n\n public get idleTimeout(): number {\n return this.options.idleTimeout || 60000; // 1 minute default\n }\n\n private getPool(): ThreadPool {\n if (!ThreadPrimitive.globalPool.has(this.name)) {\n ThreadPrimitive.globalPool.set(\n this.name,\n new ThreadPool(\n this.name,\n this.maxPoolSize,\n this.idleTimeout,\n this.script,\n ),\n );\n }\n return ThreadPrimitive.globalPool.get(this.name)!;\n }\n\n public async execute<T = any>(data?: any, schema?: TSchema): Promise<T> {\n if (schema && data) {\n try {\n Value.Decode(schema, data);\n } catch (error) {\n throw new Error(\n `Invalid data: ${error instanceof Error ? error.message : error}`,\n );\n }\n }\n\n const pool = this.getPool();\n return await pool.execute<T>(data);\n }\n\n public async create(): Promise<void> {\n const pool = this.getPool();\n await pool.warmUp();\n }\n\n public async terminate(): Promise<void> {\n const pool = this.getPool();\n await pool.terminate();\n ThreadPrimitive.globalPool.delete(this.name);\n }\n}\n\n$thread[KIND] = ThreadPrimitive;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ninterface ThreadMessage<T = any> {\n id: string;\n type: \"execute\" | \"response\" | \"error\";\n data?: T;\n error?: string;\n}\n\ninterface ThreadInstance {\n worker: Worker;\n port: MessagePort;\n busy: boolean;\n lastUsed: number;\n pendingMessages: Map<\n string,\n { resolve: (value: any) => void; reject: (error: Error) => void }\n >;\n}\n\nclass ThreadPool {\n private instances: ThreadInstance[] = [];\n private queue: Array<{\n data: any;\n resolve: (value: any) => void;\n reject: (error: Error) => void;\n }> = [];\n private idleTimer?: NodeJS.Timeout;\n\n constructor(\n private readonly name: string,\n private readonly maxPoolSize: number,\n private readonly idleTimeout: number,\n private readonly script: string,\n ) {}\n\n async warmUp(): Promise<void> {\n if (this.instances.length === 0) {\n await this.createInstance();\n }\n }\n\n private async createInstance(): Promise<ThreadInstance> {\n const { port1, port2 } = new MessageChannel();\n\n const worker = new Worker(this.script, {\n env: {\n ...process.env,\n ALEPHA_WORKER: this.name,\n APP_NAME: \"WORKER\",\n },\n workerData: { port: port2 },\n transferList: [port2],\n });\n\n const instance: ThreadInstance = {\n worker,\n port: port1,\n busy: false,\n lastUsed: Date.now(),\n pendingMessages: new Map(),\n };\n\n instance.port.on(\"message\", (message: ThreadMessage) => {\n if (message.type === \"response\" || message.type === \"error\") {\n const pending = instance.pendingMessages.get(message.id);\n if (pending) {\n instance.pendingMessages.delete(message.id);\n instance.busy = false;\n instance.lastUsed = Date.now();\n\n if (message.type === \"error\") {\n pending.reject(new Error(message.error));\n } else {\n pending.resolve(message.data);\n }\n\n this.processQueue();\n }\n }\n });\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(\n () => reject(new Error(\"Thread initialization timeout\")),\n 5000,\n );\n\n worker.once(\"online\", () => {\n clearTimeout(timeout);\n resolve();\n });\n\n worker.once(\"error\", (error) => {\n clearTimeout(timeout);\n reject(error);\n });\n });\n\n this.instances.push(instance);\n this.resetIdleTimer();\n\n return instance;\n }\n\n async execute<T = any>(data?: any): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n this.queue.push({ data, resolve, reject });\n this.processQueue();\n });\n }\n\n private async processQueue(): Promise<void> {\n if (this.queue.length === 0) {\n return;\n }\n\n let instance = this.instances.find((i) => !i.busy);\n\n if (!instance && this.instances.length < this.maxPoolSize) {\n try {\n instance = await this.createInstance();\n } catch (error) {\n const { reject } = this.queue.shift()!;\n reject(\n error instanceof Error\n ? error\n : new Error(\"Failed to create thread instance\"),\n );\n return;\n }\n }\n\n if (!instance) {\n return; // Wait for an instance to become available\n }\n\n const { data, resolve, reject } = this.queue.shift()!;\n const messageId = `${Date.now()}-${Math.random()}`;\n\n instance.busy = true;\n instance.pendingMessages.set(messageId, { resolve, reject });\n\n const message: ThreadMessage = {\n id: messageId,\n type: \"execute\",\n data,\n };\n\n instance.port.postMessage(message);\n }\n\n private resetIdleTimer(): void {\n if (this.idleTimer) {\n clearTimeout(this.idleTimer);\n }\n\n this.idleTimer = setTimeout(() => {\n this.cleanupIdleInstances();\n }, this.idleTimeout);\n }\n\n private cleanupIdleInstances(): void {\n const now = Date.now();\n const instancesToRemove = this.instances.filter(\n (instance) =>\n !instance.busy && now - instance.lastUsed > this.idleTimeout,\n );\n\n for (const instance of instancesToRemove) {\n const index = this.instances.indexOf(instance);\n if (index > -1) {\n this.instances.splice(index, 1);\n instance.port.close();\n void instance.worker.terminate();\n }\n }\n\n if (this.instances.length > 0) {\n this.resetIdleTimer();\n }\n }\n\n async terminate(): Promise<void> {\n if (this.idleTimer) {\n clearTimeout(this.idleTimer);\n }\n\n await Promise.all(\n this.instances.map(async (instance) => {\n instance.port.close();\n await instance.worker.terminate();\n }),\n );\n\n this.instances = [];\n this.queue = [];\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n","import { parentPort, workerData } from \"node:worker_threads\";\nimport { $env, $hook, t } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { $thread } from \"../primitives/$thread.ts\";\n\ninterface ThreadMessage<T = any> {\n id: string;\n type: \"execute\" | \"response\" | \"error\" | \"ready\";\n data?: T;\n error?: string;\n}\n\nexport class ThreadProvider {\n protected readonly log = $logger();\n protected readonly env = $env(\n t.object({\n ALEPHA_WORKER: t.optional(t.text()),\n }),\n );\n\n protected readonly ready = $hook({\n on: \"ready\",\n handler: async (alepha) => {\n const worker = this.env.ALEPHA_WORKER;\n if (!worker) {\n return;\n }\n\n const threads = alepha.primitives($thread);\n const threadPrimitive = threads.find((thread) => thread.name === worker);\n\n if (!threadPrimitive) {\n this.log.error(`Thread not found: ${worker}`);\n return;\n }\n\n this.log.info(`Thread ready: ${threadPrimitive.name}`);\n\n // Use the message channel port from worker data if available, fallback to parentPort\n const communicationPort = workerData?.port || parentPort;\n\n if (!communicationPort) {\n this.log.error(\"No communication port available\");\n return;\n }\n\n // Set up message handling\n communicationPort.on(\"message\", async (message: ThreadMessage) => {\n if (message.type === \"execute\") {\n try {\n this.log.debug(`Executing thread handler: ${threadPrimitive.name}`);\n const result = await threadPrimitive.options.handler();\n\n communicationPort.postMessage({\n id: message.id,\n type: \"response\",\n data: result,\n } as ThreadMessage);\n } catch (error) {\n this.log.error(`Thread execution error: ${error}`);\n\n communicationPort.postMessage({\n id: message.id,\n type: \"error\",\n error: error instanceof Error ? error.message : String(error),\n } as ThreadMessage);\n }\n }\n });\n\n // Signal that the worker is ready\n communicationPort.postMessage({ type: \"ready\" } as ThreadMessage);\n },\n });\n\n public static async cleanup(): Promise<void> {\n if (parentPort) {\n parentPort.removeAllListeners();\n }\n }\n}\n","import { $module, Alepha } from \"alepha\";\nimport { $thread } from \"./primitives/$thread.ts\";\nimport { ThreadProvider } from \"./providers/ThreadProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$thread.ts\";\nexport * from \"./providers/ThreadProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha\" {\n interface Alepha {\n isWorkerThread(): boolean;\n }\n}\n\nAlepha.prototype.isWorkerThread = function (this: Alepha): boolean {\n return !!this.env.ALEPHA_WORKER;\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Simple interface for managing worker threads in Alepha.\n *\n * @see {@link $thread}\n * @module alepha.thread\n */\nexport const AlephaThread = $module({\n name: \"alepha.thread\",\n primitives: [$thread],\n services: [ThreadProvider],\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/thread/primitives/$thread.ts","../../src/thread/providers/ThreadProvider.ts","../../src/thread/index.ts"],"sourcesContent":["import { cpus } from \"node:os\";\nimport { MessageChannel, type MessagePort, Worker } from \"node:worker_threads\";\nimport type { TSchema } from \"alepha\";\nimport { createPrimitive, KIND, Primitive, Value } from \"alepha\";\n\n/**\n * Creates a worker thread primitive for offloading CPU-intensive tasks to separate threads.\n *\n * This primitive enables you to run JavaScript code in Node.js worker threads, allowing you to\n * leverage multiple CPU cores and avoid blocking the main event loop. It provides a pool-based\n * approach with intelligent thread reuse and automatic lifecycle management.\n *\n * **Key Features**\n *\n * - **Thread Pool Management**: Automatically manages a pool of worker threads with configurable limits\n * - **Thread Reuse**: Reuses existing threads to avoid expensive initialization overhead\n * - **Idle Cleanup**: Automatically terminates unused threads after a configurable timeout\n * - **Type-Safe Communication**: Optional TypeBox schema validation for data passed to threads\n * - **CPU-Aware Defaults**: Pool size defaults to CPU count × 2 for optimal performance\n * - **Error Handling**: Proper error propagation and thread cleanup on failures\n *\n * **Use Cases**\n *\n * Perfect for CPU-intensive tasks that would otherwise block the main thread:\n * - Image/video processing\n * - Data transformation and analysis\n * - Cryptographic operations\n * - Heavy computations and algorithms\n * - Background data processing\n *\n * @example\n * **Basic thread usage:**\n * ```ts\n * import { $thread } from \"alepha/thread\";\n *\n * class DataProcessor {\n * heavyComputation = $thread({\n * name: \"compute\",\n * handler: async () => {\n * // This runs in a separate worker thread\n * let result = 0;\n * for (let i = 0; i < 1000000; i++) {\n * result += Math.sqrt(i);\n * }\n * return { result, timestamp: Date.now() };\n * }\n * });\n *\n * async processData() {\n * // Execute in worker thread without blocking main thread\n * const result = await this.heavyComputation.execute();\n * console.log(`Computation result: ${result.result}`);\n * }\n * }\n * ```\n *\n * @example\n * **Configured thread pool with custom settings:**\n * ```ts\n * class ImageProcessor {\n * imageProcessor = $thread({\n * name: \"image-processing\",\n * maxPoolSize: 4, // Limit to 4 concurrent threads\n * idleTimeout: 30000, // Clean up idle threads after 30 seconds\n * handler: async () => {\n * // CPU-intensive image processing logic\n * return await processImageData();\n * }\n * });\n * }\n * ```\n *\n * @example\n * **Thread with data validation:**\n * ```ts\n * import { t } from \"alepha\";\n *\n * class CryptoService {\n * encrypt = $thread({\n * name: \"encryption\",\n * handler: async () => {\n * // Perform encryption operations\n * return await encryptData();\n * }\n * });\n *\n * async encryptSensitiveData(data: { text: string; key: string }) {\n * // Validate input data before sending to thread\n * const schema = t.object({\n * text: t.text(),\n * key: t.text()\n * });\n *\n * return await this.encrypt.execute(data, schema);\n * }\n * }\n * ```\n *\n * @example\n * **Parallel processing with multiple threads:**\n * ```ts\n * class BatchProcessor {\n * processor = $thread({\n * name: \"batch-worker\",\n * maxPoolSize: 8, // Allow up to 8 concurrent workers\n * handler: async () => {\n * return await processBatchItem();\n * }\n * });\n *\n * async processBatch(items: any[]) {\n * // Process multiple items in parallel across different threads\n * const promises = items.map(() => this.processor.execute());\n * const results = await Promise.all(promises);\n * return results;\n * }\n * }\n * ```\n */\nexport const $thread = (options: ThreadPrimitiveOptions): ThreadPrimitive => {\n return createPrimitive(ThreadPrimitive, options);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface ThreadPrimitiveOptions {\n /**\n * Unique name for this thread worker.\n *\n * Used for:\n * - Thread pool identification (threads with same name share the same pool)\n * - Logging and debugging\n * - Error messages and stack traces\n *\n * If not provided, defaults to the property key of the primitive.\n *\n * @example \"image-processor\"\n * @example \"crypto-worker\"\n * @example \"data-analysis\"\n */\n name?: string;\n\n /**\n * The function to execute in the worker thread.\n *\n * This function:\n * - Runs in a separate Node.js worker thread\n * - Should contain the CPU-intensive logic\n * - Can be async and return any serializable data\n * - Has access to standard Node.js APIs and modules\n * - Cannot directly access the main thread's memory or variables\n *\n * **Important**: The handler function is serialized and sent to the worker thread,\n * so it cannot reference variables from the parent scope (closures won't work).\n * All required data must be passed via the `execute()` method.\n *\n * @example\n * ```ts\n * handler: async () => {\n * // CPU-intensive work here\n * const result = performComplexCalculation();\n * return { result, completed: Date.now() };\n * }\n * ```\n */\n handler: () => any | Promise<any>;\n\n /**\n * Maximum number of worker threads in the pool.\n *\n * Controls how many threads can run concurrently for this named thread worker.\n * When all threads are busy, additional `execute()` calls will queue until a thread becomes available.\n *\n * **Default**: `cpus().length * 2` (number of CPU cores × 2)\n *\n * **Guidelines**:\n * - For CPU-bound tasks: Set to number of CPU cores\n * - For I/O-bound tasks in workers: Can be higher (2x CPU cores)\n * - For memory-intensive tasks: Set lower to avoid memory pressure\n * - For short-lived tasks: Can be higher for better throughput\n *\n * @default cpus().length * 2\n * @example 4 // Limit to 4 concurrent threads\n * @example 1 // Single worker thread (sequential processing)\n * @example 16 // High concurrency for lightweight tasks\n */\n maxPoolSize?: number;\n\n /**\n * Time in milliseconds before idle worker threads are terminated.\n *\n * When a worker thread has been idle (not executing any tasks) for this duration,\n * it will be automatically terminated to free up system resources. New threads\n * will be created as needed when new tasks are submitted.\n *\n * **Default**: 60000 (60 seconds)\n *\n * **Considerations**:\n * - Shorter timeouts: Save memory but increase thread creation overhead\n * - Longer timeouts: Keep threads ready but consume more memory\n * - Very short timeouts may cause constant thread creation/destruction\n *\n * @default 60000\n * @example 30000 // 30 seconds\n * @example 120000 // 2 minutes\n * @example 5000 // 5 seconds (for very memory-constrained environments)\n */\n idleTimeout?: number;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class ThreadPrimitive extends Primitive<ThreadPrimitiveOptions> {\n protected readonly script = process.argv[1];\n static readonly globalPool = new Map<string, ThreadPool>();\n\n public get name(): string {\n return this.options.name || this.config.propertyKey;\n }\n\n public get maxPoolSize(): number {\n return this.options.maxPoolSize || cpus().length * 2;\n }\n\n public get idleTimeout(): number {\n return this.options.idleTimeout || 60000; // 1 minute default\n }\n\n private getPool(): ThreadPool {\n if (!ThreadPrimitive.globalPool.has(this.name)) {\n ThreadPrimitive.globalPool.set(\n this.name,\n new ThreadPool(\n this.name,\n this.maxPoolSize,\n this.idleTimeout,\n this.script,\n ),\n );\n }\n return ThreadPrimitive.globalPool.get(this.name)!;\n }\n\n public async execute<T = any>(data?: any, schema?: TSchema): Promise<T> {\n if (schema && data) {\n try {\n Value.Decode(schema, data);\n } catch (error) {\n throw new Error(\n `Invalid data: ${error instanceof Error ? error.message : error}`,\n );\n }\n }\n\n const pool = this.getPool();\n return await pool.execute<T>(data);\n }\n\n public async create(): Promise<void> {\n const pool = this.getPool();\n await pool.warmUp();\n }\n\n public async terminate(): Promise<void> {\n const pool = this.getPool();\n await pool.terminate();\n ThreadPrimitive.globalPool.delete(this.name);\n }\n}\n\n$thread[KIND] = ThreadPrimitive;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ninterface ThreadMessage<T = any> {\n id: string;\n type: \"execute\" | \"response\" | \"error\";\n data?: T;\n error?: string;\n}\n\ninterface ThreadInstance {\n worker: Worker;\n port: MessagePort;\n busy: boolean;\n lastUsed: number;\n pendingMessages: Map<\n string,\n { resolve: (value: any) => void; reject: (error: Error) => void }\n >;\n}\n\nclass ThreadPool {\n private instances: ThreadInstance[] = [];\n private queue: Array<{\n data: any;\n resolve: (value: any) => void;\n reject: (error: Error) => void;\n }> = [];\n private idleTimer?: NodeJS.Timeout;\n\n constructor(\n private readonly name: string,\n private readonly maxPoolSize: number,\n private readonly idleTimeout: number,\n private readonly script: string,\n ) {}\n\n async warmUp(): Promise<void> {\n if (this.instances.length === 0) {\n await this.createInstance();\n }\n }\n\n private async createInstance(): Promise<ThreadInstance> {\n const { port1, port2 } = new MessageChannel();\n\n const worker = new Worker(this.script, {\n env: {\n ...process.env,\n ALEPHA_WORKER: this.name,\n APP_NAME: \"WORKER\",\n },\n workerData: { port: port2 },\n transferList: [port2],\n });\n\n const instance: ThreadInstance = {\n worker,\n port: port1,\n busy: false,\n lastUsed: Date.now(),\n pendingMessages: new Map(),\n };\n\n instance.port.on(\"message\", (message: ThreadMessage) => {\n if (message.type === \"response\" || message.type === \"error\") {\n const pending = instance.pendingMessages.get(message.id);\n if (pending) {\n instance.pendingMessages.delete(message.id);\n instance.busy = false;\n instance.lastUsed = Date.now();\n\n if (message.type === \"error\") {\n pending.reject(new Error(message.error));\n } else {\n pending.resolve(message.data);\n }\n\n this.processQueue();\n }\n }\n });\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(\n () => reject(new Error(\"Thread initialization timeout\")),\n 5000,\n );\n\n worker.once(\"online\", () => {\n clearTimeout(timeout);\n resolve();\n });\n\n worker.once(\"error\", (error) => {\n clearTimeout(timeout);\n reject(error);\n });\n });\n\n this.instances.push(instance);\n this.resetIdleTimer();\n\n return instance;\n }\n\n async execute<T = any>(data?: any): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n this.queue.push({ data, resolve, reject });\n this.processQueue();\n });\n }\n\n private async processQueue(): Promise<void> {\n if (this.queue.length === 0) {\n return;\n }\n\n let instance = this.instances.find((i) => !i.busy);\n\n if (!instance && this.instances.length < this.maxPoolSize) {\n try {\n instance = await this.createInstance();\n } catch (error) {\n const { reject } = this.queue.shift()!;\n reject(\n error instanceof Error\n ? error\n : new Error(\"Failed to create thread instance\"),\n );\n return;\n }\n }\n\n if (!instance) {\n return; // Wait for an instance to become available\n }\n\n const { data, resolve, reject } = this.queue.shift()!;\n const messageId = `${Date.now()}-${Math.random()}`;\n\n instance.busy = true;\n instance.pendingMessages.set(messageId, { resolve, reject });\n\n const message: ThreadMessage = {\n id: messageId,\n type: \"execute\",\n data,\n };\n\n instance.port.postMessage(message);\n }\n\n private resetIdleTimer(): void {\n if (this.idleTimer) {\n clearTimeout(this.idleTimer);\n }\n\n this.idleTimer = setTimeout(() => {\n this.cleanupIdleInstances();\n }, this.idleTimeout);\n }\n\n private cleanupIdleInstances(): void {\n const now = Date.now();\n const instancesToRemove = this.instances.filter(\n (instance) =>\n !instance.busy && now - instance.lastUsed > this.idleTimeout,\n );\n\n for (const instance of instancesToRemove) {\n const index = this.instances.indexOf(instance);\n if (index > -1) {\n this.instances.splice(index, 1);\n instance.port.close();\n void instance.worker.terminate();\n }\n }\n\n if (this.instances.length > 0) {\n this.resetIdleTimer();\n }\n }\n\n async terminate(): Promise<void> {\n if (this.idleTimer) {\n clearTimeout(this.idleTimer);\n }\n\n await Promise.all(\n this.instances.map(async (instance) => {\n instance.port.close();\n await instance.worker.terminate();\n }),\n );\n\n this.instances = [];\n this.queue = [];\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n","import { parentPort, workerData } from \"node:worker_threads\";\nimport { $env, $hook, t } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { $thread } from \"../primitives/$thread.ts\";\n\ninterface ThreadMessage<T = any> {\n id: string;\n type: \"execute\" | \"response\" | \"error\" | \"ready\";\n data?: T;\n error?: string;\n}\n\nexport class ThreadProvider {\n protected readonly log = $logger();\n protected readonly env = $env(\n t.object({\n ALEPHA_WORKER: t.optional(t.text()),\n }),\n );\n\n protected readonly ready = $hook({\n on: \"ready\",\n handler: async (alepha) => {\n const worker = this.env.ALEPHA_WORKER;\n if (!worker) {\n return;\n }\n\n const threads = alepha.primitives($thread);\n const threadPrimitive = threads.find((thread) => thread.name === worker);\n\n if (!threadPrimitive) {\n this.log.error(`Thread not found: ${worker}`);\n return;\n }\n\n this.log.info(`Thread ready: ${threadPrimitive.name}`);\n\n // Use the message channel port from worker data if available, fallback to parentPort\n const communicationPort = workerData?.port || parentPort;\n\n if (!communicationPort) {\n this.log.error(\"No communication port available\");\n return;\n }\n\n // Set up message handling\n communicationPort.on(\"message\", async (message: ThreadMessage) => {\n if (message.type === \"execute\") {\n try {\n this.log.debug(`Executing thread handler: ${threadPrimitive.name}`);\n const result = await threadPrimitive.options.handler();\n\n communicationPort.postMessage({\n id: message.id,\n type: \"response\",\n data: result,\n } as ThreadMessage);\n } catch (error) {\n this.log.error(`Thread execution error: ${error}`);\n\n communicationPort.postMessage({\n id: message.id,\n type: \"error\",\n error: error instanceof Error ? error.message : String(error),\n } as ThreadMessage);\n }\n }\n });\n\n // Signal that the worker is ready\n communicationPort.postMessage({ type: \"ready\" } as ThreadMessage);\n },\n });\n\n public static async cleanup(): Promise<void> {\n if (parentPort) {\n parentPort.removeAllListeners();\n }\n }\n}\n","import { $module, Alepha } from \"alepha\";\nimport { $thread } from \"./primitives/$thread.ts\";\nimport { ThreadProvider } from \"./providers/ThreadProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$thread.ts\";\nexport * from \"./providers/ThreadProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha\" {\n interface Alepha {\n isWorkerThread(): boolean;\n }\n}\n\nAlepha.prototype.isWorkerThread = function (this: Alepha): boolean {\n return !!this.env.ALEPHA_WORKER;\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Simple interface for managing worker threads in Alepha.\n *\n * @see {@link $thread}\n * @module alepha.thread\n */\nexport const AlephaThread = $module({\n name: \"alepha.thread\",\n primitives: [$thread],\n services: [ThreadProvider],\n});\n"],"mappings":"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"}