alepha 0.14.1 → 0.14.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (402) hide show
  1. package/README.md +3 -3
  2. package/dist/api/audits/index.browser.js +5 -5
  3. package/dist/api/audits/index.browser.js.map +1 -1
  4. package/dist/api/audits/index.d.ts +784 -784
  5. package/dist/api/audits/index.d.ts.map +1 -1
  6. package/dist/api/audits/index.js +13 -13
  7. package/dist/api/audits/index.js.map +1 -1
  8. package/dist/api/files/index.browser.js +5 -5
  9. package/dist/api/files/index.browser.js.map +1 -1
  10. package/dist/api/files/index.d.ts +57 -57
  11. package/dist/api/files/index.d.ts.map +1 -1
  12. package/dist/api/files/index.js +71 -71
  13. package/dist/api/files/index.js.map +1 -1
  14. package/dist/api/jobs/index.browser.js +5 -5
  15. package/dist/api/jobs/index.browser.js.map +1 -1
  16. package/dist/api/jobs/index.d.ts +165 -165
  17. package/dist/api/jobs/index.d.ts.map +1 -1
  18. package/dist/api/jobs/index.js +10 -10
  19. package/dist/api/jobs/index.js.map +1 -1
  20. package/dist/api/notifications/index.browser.js +10 -10
  21. package/dist/api/notifications/index.browser.js.map +1 -1
  22. package/dist/api/notifications/index.d.ts +583 -171
  23. package/dist/api/notifications/index.d.ts.map +1 -1
  24. package/dist/api/notifications/index.js +12 -12
  25. package/dist/api/notifications/index.js.map +1 -1
  26. package/dist/api/parameters/index.browser.js +163 -10
  27. package/dist/api/parameters/index.browser.js.map +1 -1
  28. package/dist/api/parameters/index.d.ts +281 -276
  29. package/dist/api/parameters/index.d.ts.map +1 -1
  30. package/dist/api/parameters/index.js +196 -91
  31. package/dist/api/parameters/index.js.map +1 -1
  32. package/dist/api/users/index.browser.js +19 -19
  33. package/dist/api/users/index.browser.js.map +1 -1
  34. package/dist/api/users/index.d.ts +778 -764
  35. package/dist/api/users/index.d.ts.map +1 -1
  36. package/dist/api/users/index.js +831 -596
  37. package/dist/api/users/index.js.map +1 -1
  38. package/dist/api/verifications/index.browser.js +6 -6
  39. package/dist/api/verifications/index.browser.js.map +1 -1
  40. package/dist/api/verifications/index.d.ts +125 -125
  41. package/dist/api/verifications/index.d.ts.map +1 -1
  42. package/dist/api/verifications/index.js +6 -6
  43. package/dist/api/verifications/index.js.map +1 -1
  44. package/dist/batch/index.js.map +1 -1
  45. package/dist/bin/index.d.ts +1 -2
  46. package/dist/bin/index.js +0 -1
  47. package/dist/bin/index.js.map +1 -1
  48. package/dist/cache/core/index.js.map +1 -1
  49. package/dist/cli/index.d.ts +249 -218
  50. package/dist/cli/index.d.ts.map +1 -1
  51. package/dist/cli/index.js +951 -821
  52. package/dist/cli/index.js.map +1 -1
  53. package/dist/command/index.d.ts +40 -0
  54. package/dist/command/index.d.ts.map +1 -1
  55. package/dist/command/index.js +97 -17
  56. package/dist/command/index.js.map +1 -1
  57. package/dist/core/index.browser.js +14 -18
  58. package/dist/core/index.browser.js.map +1 -1
  59. package/dist/core/index.d.ts +29 -0
  60. package/dist/core/index.d.ts.map +1 -1
  61. package/dist/core/index.js +21 -24
  62. package/dist/core/index.js.map +1 -1
  63. package/dist/core/index.native.js +21 -24
  64. package/dist/core/index.native.js.map +1 -1
  65. package/dist/datetime/index.js.map +1 -1
  66. package/dist/fake/index.js +195 -168
  67. package/dist/fake/index.js.map +1 -1
  68. package/dist/file/index.d.ts +8 -0
  69. package/dist/file/index.d.ts.map +1 -1
  70. package/dist/file/index.js +3 -0
  71. package/dist/file/index.js.map +1 -1
  72. package/dist/lock/redis/index.js.map +1 -1
  73. package/dist/logger/index.js.map +1 -1
  74. package/dist/mcp/index.d.ts.map +1 -1
  75. package/dist/mcp/index.js.map +1 -1
  76. package/dist/orm/index.browser.js +26 -5
  77. package/dist/orm/index.browser.js.map +1 -1
  78. package/dist/orm/index.d.ts +146 -121
  79. package/dist/orm/index.d.ts.map +1 -1
  80. package/dist/orm/index.js +49 -24
  81. package/dist/orm/index.js.map +1 -1
  82. package/dist/redis/index.js.map +1 -1
  83. package/dist/retry/index.js.map +1 -1
  84. package/dist/router/index.js.map +1 -1
  85. package/dist/scheduler/index.d.ts +6 -6
  86. package/dist/scheduler/index.js.map +1 -1
  87. package/dist/security/index.d.ts +29 -29
  88. package/dist/security/index.d.ts.map +1 -1
  89. package/dist/security/index.js +1 -1
  90. package/dist/security/index.js.map +1 -1
  91. package/dist/server/auth/index.d.ts +171 -155
  92. package/dist/server/auth/index.d.ts.map +1 -1
  93. package/dist/server/auth/index.js +0 -1
  94. package/dist/server/auth/index.js.map +1 -1
  95. package/dist/server/cache/index.js.map +1 -1
  96. package/dist/server/compress/index.d.ts.map +1 -1
  97. package/dist/server/compress/index.js +2 -0
  98. package/dist/server/compress/index.js.map +1 -1
  99. package/dist/server/cookies/index.browser.js.map +1 -1
  100. package/dist/server/cookies/index.js.map +1 -1
  101. package/dist/server/core/index.browser.js.map +1 -1
  102. package/dist/server/core/index.d.ts.map +1 -1
  103. package/dist/server/core/index.js +1 -1
  104. package/dist/server/core/index.js.map +1 -1
  105. package/dist/server/health/index.d.ts +17 -17
  106. package/dist/server/helmet/index.js.map +1 -1
  107. package/dist/server/links/index.browser.js +22 -6
  108. package/dist/server/links/index.browser.js.map +1 -1
  109. package/dist/server/links/index.d.ts +46 -44
  110. package/dist/server/links/index.d.ts.map +1 -1
  111. package/dist/server/links/index.js +24 -41
  112. package/dist/server/links/index.js.map +1 -1
  113. package/dist/server/multipart/index.js.map +1 -1
  114. package/dist/server/rate-limit/index.js.map +1 -1
  115. package/dist/server/security/index.js.map +1 -1
  116. package/dist/server/swagger/index.d.ts +2 -1
  117. package/dist/server/swagger/index.d.ts.map +1 -1
  118. package/dist/server/swagger/index.js +8 -3
  119. package/dist/server/swagger/index.js.map +1 -1
  120. package/dist/thread/index.js.map +1 -1
  121. package/dist/topic/core/index.js.map +1 -1
  122. package/dist/vite/index.d.ts.map +1 -1
  123. package/dist/vite/index.js +12 -4
  124. package/dist/vite/index.js.map +1 -1
  125. package/dist/websocket/index.browser.js.map +1 -1
  126. package/dist/websocket/index.js.map +1 -1
  127. package/package.json +7 -7
  128. package/src/api/audits/controllers/{AuditController.ts → AdminAuditController.ts} +5 -6
  129. package/src/api/audits/entities/audits.ts +5 -5
  130. package/src/api/audits/index.browser.ts +1 -1
  131. package/src/api/audits/index.ts +3 -3
  132. package/src/api/audits/primitives/$audit.spec.ts +276 -0
  133. package/src/api/audits/services/AuditService.spec.ts +495 -0
  134. package/src/api/files/__tests__/$bucket.spec.ts +91 -0
  135. package/src/api/files/controllers/AdminFileStatsController.spec.ts +166 -0
  136. package/src/api/files/controllers/{StorageStatsController.ts → AdminFileStatsController.ts} +2 -2
  137. package/src/api/files/controllers/FileController.spec.ts +558 -0
  138. package/src/api/files/controllers/FileController.ts +4 -5
  139. package/src/api/files/entities/files.ts +5 -5
  140. package/src/api/files/index.browser.ts +1 -1
  141. package/src/api/files/index.ts +4 -4
  142. package/src/api/files/jobs/FileJobs.spec.ts +52 -0
  143. package/src/api/files/services/FileService.spec.ts +109 -0
  144. package/src/api/jobs/__tests__/JobController.spec.ts +343 -0
  145. package/src/api/jobs/controllers/{JobController.ts → AdminJobController.ts} +2 -2
  146. package/src/api/jobs/entities/jobExecutions.ts +5 -5
  147. package/src/api/jobs/index.ts +3 -3
  148. package/src/api/jobs/primitives/$job.spec.ts +476 -0
  149. package/src/api/notifications/controllers/{NotificationController.ts → AdminNotificationController.ts} +4 -5
  150. package/src/api/notifications/entities/notifications.ts +5 -5
  151. package/src/api/notifications/index.browser.ts +1 -1
  152. package/src/api/notifications/index.ts +4 -4
  153. package/src/api/parameters/controllers/{ConfigController.ts → AdminConfigController.ts} +46 -107
  154. package/src/api/parameters/entities/parameters.ts +7 -17
  155. package/src/api/parameters/index.ts +3 -3
  156. package/src/api/parameters/primitives/$config.spec.ts +356 -0
  157. package/src/api/parameters/schemas/activateConfigBodySchema.ts +12 -0
  158. package/src/api/parameters/schemas/checkScheduledResponseSchema.ts +8 -0
  159. package/src/api/parameters/schemas/configCurrentResponseSchema.ts +13 -0
  160. package/src/api/parameters/schemas/configHistoryResponseSchema.ts +9 -0
  161. package/src/api/parameters/schemas/configNameParamSchema.ts +10 -0
  162. package/src/api/parameters/schemas/configNamesResponseSchema.ts +8 -0
  163. package/src/api/parameters/schemas/configTreeNodeSchema.ts +13 -0
  164. package/src/api/parameters/schemas/configVersionParamSchema.ts +9 -0
  165. package/src/api/parameters/schemas/configVersionResponseSchema.ts +9 -0
  166. package/src/api/parameters/schemas/configsByStatusResponseSchema.ts +9 -0
  167. package/src/api/parameters/schemas/createConfigVersionBodySchema.ts +24 -0
  168. package/src/api/parameters/schemas/index.ts +15 -0
  169. package/src/api/parameters/schemas/parameterResponseSchema.ts +26 -0
  170. package/src/api/parameters/schemas/parameterStatusSchema.ts +13 -0
  171. package/src/api/parameters/schemas/rollbackConfigBodySchema.ts +15 -0
  172. package/src/api/parameters/schemas/statusParamSchema.ts +9 -0
  173. package/src/api/users/__tests__/EmailVerification.spec.ts +369 -0
  174. package/src/api/users/__tests__/PasswordReset.spec.ts +550 -0
  175. package/src/api/users/controllers/AdminIdentityController.spec.ts +365 -0
  176. package/src/api/users/controllers/{IdentityController.ts → AdminIdentityController.ts} +3 -4
  177. package/src/api/users/controllers/AdminSessionController.spec.ts +274 -0
  178. package/src/api/users/controllers/{SessionController.ts → AdminSessionController.ts} +3 -4
  179. package/src/api/users/controllers/AdminUserController.spec.ts +372 -0
  180. package/src/api/users/controllers/AdminUserController.ts +116 -0
  181. package/src/api/users/controllers/UserController.ts +4 -107
  182. package/src/api/users/controllers/UserRealmController.ts +3 -0
  183. package/src/api/users/entities/identities.ts +6 -6
  184. package/src/api/users/entities/sessions.ts +6 -6
  185. package/src/api/users/entities/users.ts +9 -9
  186. package/src/api/users/index.ts +13 -6
  187. package/src/api/users/primitives/$userRealm.ts +13 -8
  188. package/src/api/users/services/CredentialService.spec.ts +509 -0
  189. package/src/api/users/services/CredentialService.ts +46 -0
  190. package/src/api/users/services/IdentityService.ts +15 -0
  191. package/src/api/users/services/RegistrationService.spec.ts +630 -0
  192. package/src/api/users/services/RegistrationService.ts +18 -0
  193. package/src/api/users/services/SessionService.spec.ts +301 -0
  194. package/src/api/users/services/SessionService.ts +110 -1
  195. package/src/api/users/services/UserService.ts +67 -2
  196. package/src/api/verifications/__tests__/CodeVerification.spec.ts +318 -0
  197. package/src/api/verifications/__tests__/LinkVerification.spec.ts +279 -0
  198. package/src/api/verifications/entities/verifications.ts +6 -6
  199. package/src/api/verifications/jobs/VerificationJobs.spec.ts +50 -0
  200. package/src/batch/__tests__/startup-buffering.spec.ts +458 -0
  201. package/src/batch/primitives/$batch.spec.ts +766 -0
  202. package/src/batch/providers/BatchProvider.spec.ts +786 -0
  203. package/src/bin/index.ts +0 -1
  204. package/src/bucket/__tests__/shared.ts +194 -0
  205. package/src/bucket/primitives/$bucket.spec.ts +104 -0
  206. package/src/bucket/providers/FileStorageProvider.spec.ts +13 -0
  207. package/src/bucket/providers/LocalFileStorageProvider.spec.ts +77 -0
  208. package/src/bucket/providers/MemoryFileStorageProvider.spec.ts +82 -0
  209. package/src/cache/core/__tests__/shared.ts +377 -0
  210. package/src/cache/core/primitives/$cache.spec.ts +111 -0
  211. package/src/cache/redis/__tests__/cache-redis.spec.ts +70 -0
  212. package/src/cli/apps/AlephaCli.ts +54 -16
  213. package/src/cli/apps/AlephaPackageBuilderCli.ts +2 -1
  214. package/src/cli/assets/appRouterTs.ts +1 -1
  215. package/src/cli/commands/{ViteCommands.ts → build.ts} +2 -105
  216. package/src/cli/commands/clean.ts +14 -0
  217. package/src/cli/commands/{DrizzleCommands.ts → db.ts} +10 -117
  218. package/src/cli/commands/{DeployCommands.ts → deploy.ts} +1 -1
  219. package/src/cli/commands/dev.ts +69 -0
  220. package/src/cli/commands/format.ts +17 -0
  221. package/src/cli/commands/gen/changelog.spec.ts +315 -0
  222. package/src/cli/commands/{ChangelogCommands.ts → gen/changelog.ts} +16 -31
  223. package/src/cli/commands/gen/openapi.ts +71 -0
  224. package/src/cli/commands/gen.ts +18 -0
  225. package/src/cli/commands/{CoreCommands.ts → init.ts} +4 -40
  226. package/src/cli/commands/lint.ts +17 -0
  227. package/src/cli/commands/root.ts +41 -0
  228. package/src/cli/commands/run.ts +24 -0
  229. package/src/cli/commands/test.ts +42 -0
  230. package/src/cli/commands/typecheck.ts +24 -0
  231. package/src/cli/commands/{VerifyCommands.ts → verify.ts} +1 -13
  232. package/src/cli/defineConfig.ts +10 -1
  233. package/src/cli/index.ts +17 -7
  234. package/src/cli/services/AlephaCliUtils.ts +71 -32
  235. package/src/cli/services/GitMessageParser.ts +1 -1
  236. package/src/command/helpers/Asker.spec.ts +127 -0
  237. package/src/command/helpers/Runner.spec.ts +126 -0
  238. package/src/command/primitives/$command.spec.ts +1588 -0
  239. package/src/command/providers/CliProvider.ts +74 -24
  240. package/src/core/Alepha.ts +52 -4
  241. package/src/core/__tests__/Alepha-emit.spec.ts +22 -0
  242. package/src/core/__tests__/Alepha-graph.spec.ts +93 -0
  243. package/src/core/__tests__/Alepha-has.spec.ts +41 -0
  244. package/src/core/__tests__/Alepha-inject.spec.ts +93 -0
  245. package/src/core/__tests__/Alepha-register.spec.ts +81 -0
  246. package/src/core/__tests__/Alepha-start.spec.ts +176 -0
  247. package/src/core/__tests__/Alepha-with.spec.ts +14 -0
  248. package/src/core/__tests__/TypeBox-usecases.spec.ts +35 -0
  249. package/src/core/__tests__/TypeBoxLocale.spec.ts +15 -0
  250. package/src/core/__tests__/descriptor.spec.ts +34 -0
  251. package/src/core/__tests__/fixtures/A.ts +5 -0
  252. package/src/core/__tests__/pagination.spec.ts +77 -0
  253. package/src/core/helpers/jsonSchemaToTypeBox.ts +2 -2
  254. package/src/core/primitives/$atom.spec.ts +43 -0
  255. package/src/core/primitives/$hook.spec.ts +130 -0
  256. package/src/core/primitives/$inject.spec.ts +175 -0
  257. package/src/core/primitives/$module.spec.ts +115 -0
  258. package/src/core/providers/CodecManager.spec.ts +740 -0
  259. package/src/core/providers/EventManager.spec.ts +762 -0
  260. package/src/core/providers/EventManager.ts +4 -0
  261. package/src/core/providers/StateManager.spec.ts +365 -0
  262. package/src/core/providers/TypeProvider.spec.ts +1607 -0
  263. package/src/core/providers/TypeProvider.ts +20 -26
  264. package/src/datetime/primitives/$interval.spec.ts +103 -0
  265. package/src/datetime/providers/DateTimeProvider.spec.ts +86 -0
  266. package/src/email/primitives/$email.spec.ts +175 -0
  267. package/src/email/providers/LocalEmailProvider.spec.ts +341 -0
  268. package/src/fake/__tests__/keyName.example.ts +40 -0
  269. package/src/fake/__tests__/keyName.spec.ts +152 -0
  270. package/src/fake/__tests__/module.example.ts +32 -0
  271. package/src/fake/providers/FakeProvider.spec.ts +438 -0
  272. package/src/file/providers/FileSystemProvider.ts +8 -0
  273. package/src/file/providers/NodeFileSystemProvider.spec.ts +418 -0
  274. package/src/file/providers/NodeFileSystemProvider.ts +5 -0
  275. package/src/file/services/FileDetector.spec.ts +591 -0
  276. package/src/lock/core/__tests__/shared.ts +190 -0
  277. package/src/lock/core/providers/MemoryLockProvider.spec.ts +25 -0
  278. package/src/lock/redis/providers/RedisLockProvider.spec.ts +25 -0
  279. package/src/logger/__tests__/SimpleFormatterProvider.spec.ts +109 -0
  280. package/src/logger/primitives/$logger.spec.ts +108 -0
  281. package/src/logger/services/Logger.spec.ts +295 -0
  282. package/src/mcp/__tests__/errors.spec.ts +175 -0
  283. package/src/mcp/__tests__/integration.spec.ts +450 -0
  284. package/src/mcp/helpers/jsonrpc.spec.ts +380 -0
  285. package/src/mcp/primitives/$prompt.spec.ts +468 -0
  286. package/src/mcp/primitives/$resource.spec.ts +390 -0
  287. package/src/mcp/primitives/$tool.spec.ts +406 -0
  288. package/src/mcp/providers/McpServerProvider.spec.ts +797 -0
  289. package/src/orm/__tests__/$repository-crud.spec.ts +276 -0
  290. package/src/orm/__tests__/$repository-hooks.spec.ts +325 -0
  291. package/src/orm/__tests__/$repository-orderBy.spec.ts +128 -0
  292. package/src/orm/__tests__/$repository-pagination-sort.spec.ts +149 -0
  293. package/src/orm/__tests__/$repository-save.spec.ts +37 -0
  294. package/src/orm/__tests__/ModelBuilder-integration.spec.ts +490 -0
  295. package/src/orm/__tests__/ModelBuilder-types.spec.ts +186 -0
  296. package/src/orm/__tests__/PostgresProvider.spec.ts +46 -0
  297. package/src/orm/__tests__/delete-returning.spec.ts +256 -0
  298. package/src/orm/__tests__/deletedAt.spec.ts +80 -0
  299. package/src/orm/__tests__/enums.spec.ts +315 -0
  300. package/src/orm/__tests__/execute.spec.ts +72 -0
  301. package/src/orm/__tests__/fixtures/bigEntitySchema.ts +65 -0
  302. package/src/orm/__tests__/fixtures/userEntitySchema.ts +27 -0
  303. package/src/orm/__tests__/joins.spec.ts +1114 -0
  304. package/src/orm/__tests__/page.spec.ts +287 -0
  305. package/src/orm/__tests__/primaryKey.spec.ts +87 -0
  306. package/src/orm/__tests__/query-date-encoding.spec.ts +402 -0
  307. package/src/orm/__tests__/ref-auto-onDelete.spec.ts +156 -0
  308. package/src/orm/__tests__/references.spec.ts +102 -0
  309. package/src/orm/__tests__/security.spec.ts +710 -0
  310. package/src/orm/__tests__/sqlite.spec.ts +111 -0
  311. package/src/orm/__tests__/string-operators.spec.ts +429 -0
  312. package/src/orm/__tests__/timestamps.spec.ts +388 -0
  313. package/src/orm/__tests__/validation.spec.ts +183 -0
  314. package/src/orm/__tests__/version.spec.ts +64 -0
  315. package/src/orm/helpers/parseQueryString.spec.ts +196 -0
  316. package/src/orm/index.browser.ts +1 -1
  317. package/src/orm/index.ts +10 -6
  318. package/src/orm/primitives/$repository.spec.ts +137 -0
  319. package/src/orm/primitives/$sequence.spec.ts +29 -0
  320. package/src/orm/primitives/$transaction.spec.ts +82 -0
  321. package/src/orm/providers/{PostgresTypeProvider.ts → DatabaseTypeProvider.ts} +25 -3
  322. package/src/orm/providers/drivers/BunPostgresProvider.ts +3 -3
  323. package/src/orm/providers/drivers/BunSqliteProvider.ts +1 -1
  324. package/src/orm/providers/drivers/CloudflareD1Provider.ts +1 -1
  325. package/src/orm/providers/drivers/DatabaseProvider.ts +1 -1
  326. package/src/orm/providers/drivers/NodePostgresProvider.ts +3 -3
  327. package/src/orm/providers/drivers/NodeSqliteProvider.ts +1 -1
  328. package/src/orm/providers/drivers/PglitePostgresProvider.ts +2 -2
  329. package/src/orm/services/ModelBuilder.spec.ts +575 -0
  330. package/src/orm/services/Repository.spec.ts +137 -0
  331. package/src/queue/core/__tests__/shared.ts +143 -0
  332. package/src/queue/core/providers/MemoryQueueProvider.spec.ts +23 -0
  333. package/src/queue/core/providers/WorkerProvider.spec.ts +378 -0
  334. package/src/queue/redis/providers/RedisQueueProvider.spec.ts +23 -0
  335. package/src/redis/__tests__/redis.spec.ts +58 -0
  336. package/src/retry/primitives/$retry.spec.ts +234 -0
  337. package/src/retry/providers/RetryProvider.spec.ts +438 -0
  338. package/src/router/__tests__/match.spec.ts +252 -0
  339. package/src/router/providers/RouterProvider.spec.ts +197 -0
  340. package/src/scheduler/__tests__/$scheduler-cron.spec.ts +25 -0
  341. package/src/scheduler/__tests__/$scheduler-interval.spec.ts +25 -0
  342. package/src/scheduler/__tests__/shared.ts +77 -0
  343. package/src/security/__tests__/bug-1-wildcard-after-start.spec.ts +229 -0
  344. package/src/security/__tests__/bug-2-password-validation.spec.ts +245 -0
  345. package/src/security/__tests__/bug-3-regex-vulnerability.spec.ts +407 -0
  346. package/src/security/__tests__/bug-4-oauth2-validation.spec.ts +439 -0
  347. package/src/security/__tests__/multi-layer-permissions.spec.ts +522 -0
  348. package/src/security/primitives/$permission.spec.ts +30 -0
  349. package/src/security/primitives/$permission.ts +2 -2
  350. package/src/security/primitives/$realm.spec.ts +101 -0
  351. package/src/security/primitives/$role.spec.ts +52 -0
  352. package/src/security/primitives/$serviceAccount.spec.ts +61 -0
  353. package/src/security/providers/SecurityProvider.spec.ts +350 -0
  354. package/src/server/auth/providers/ServerAuthProvider.ts +0 -2
  355. package/src/server/cache/providers/ServerCacheProvider.spec.ts +942 -0
  356. package/src/server/compress/providers/ServerCompressProvider.spec.ts +31 -0
  357. package/src/server/compress/providers/ServerCompressProvider.ts +2 -0
  358. package/src/server/cookies/providers/ServerCookiesProvider.spec.ts +253 -0
  359. package/src/server/core/__tests__/ServerRouterProvider-getRoutes.spec.ts +334 -0
  360. package/src/server/core/__tests__/ServerRouterProvider-requestId.spec.ts +129 -0
  361. package/src/server/core/primitives/$action.spec.ts +191 -0
  362. package/src/server/core/primitives/$route.spec.ts +65 -0
  363. package/src/server/core/providers/ServerBodyParserProvider.spec.ts +93 -0
  364. package/src/server/core/providers/ServerLoggerProvider.spec.ts +100 -0
  365. package/src/server/core/providers/ServerProvider.ts +3 -1
  366. package/src/server/core/services/HttpClient.spec.ts +123 -0
  367. package/src/server/core/services/UserAgentParser.spec.ts +111 -0
  368. package/src/server/cors/providers/ServerCorsProvider.spec.ts +481 -0
  369. package/src/server/health/providers/ServerHealthProvider.spec.ts +22 -0
  370. package/src/server/helmet/providers/ServerHelmetProvider.spec.ts +105 -0
  371. package/src/server/links/__tests__/$action.spec.ts +238 -0
  372. package/src/server/links/__tests__/fixtures/CrudApp.ts +122 -0
  373. package/src/server/links/__tests__/requestId.spec.ts +120 -0
  374. package/src/server/links/primitives/$remote.spec.ts +228 -0
  375. package/src/server/links/providers/LinkProvider.spec.ts +54 -0
  376. package/src/server/links/providers/LinkProvider.ts +49 -3
  377. package/src/server/links/providers/ServerLinksProvider.ts +1 -53
  378. package/src/server/links/schemas/apiLinksResponseSchema.ts +7 -0
  379. package/src/server/metrics/providers/ServerMetricsProvider.spec.ts +25 -0
  380. package/src/server/multipart/providers/ServerMultipartProvider.spec.ts +528 -0
  381. package/src/server/proxy/primitives/$proxy.spec.ts +87 -0
  382. package/src/server/rate-limit/__tests__/ActionRateLimit.spec.ts +211 -0
  383. package/src/server/rate-limit/providers/ServerRateLimitProvider.spec.ts +344 -0
  384. package/src/server/security/__tests__/BasicAuth.spec.ts +684 -0
  385. package/src/server/security/__tests__/ServerSecurityProvider-realm.spec.ts +388 -0
  386. package/src/server/security/providers/ServerSecurityProvider.spec.ts +123 -0
  387. package/src/server/static/primitives/$serve.spec.ts +193 -0
  388. package/src/server/swagger/__tests__/ui.spec.ts +52 -0
  389. package/src/server/swagger/primitives/$swagger.spec.ts +193 -0
  390. package/src/server/swagger/providers/ServerSwaggerProvider.ts +18 -8
  391. package/src/sms/primitives/$sms.spec.ts +165 -0
  392. package/src/sms/providers/LocalSmsProvider.spec.ts +224 -0
  393. package/src/sms/providers/MemorySmsProvider.spec.ts +193 -0
  394. package/src/thread/primitives/$thread.spec.ts +186 -0
  395. package/src/topic/core/__tests__/shared.ts +144 -0
  396. package/src/topic/core/providers/MemoryTopicProvider.spec.ts +23 -0
  397. package/src/topic/redis/providers/RedisTopicProvider.spec.ts +23 -0
  398. package/src/vite/plugins/viteAlephaDev.ts +16 -4
  399. package/src/vite/tasks/runAlepha.ts +7 -1
  400. package/src/websocket/__tests__/$websocket-new.spec.ts +195 -0
  401. package/src/websocket/primitives/$channel.spec.ts +30 -0
  402. package/src/cli/commands/BiomeCommands.ts +0 -29
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["addons: Array<(config: Configuration) => void>","providers: AuthenticationProvider[]","user","user: UserAccount | undefined","parameters: Record<string, string>","parameters","tokens","identity","account: CredentialsFn | undefined","account: LinkAccountFn | undefined","res","user: OAuth2Profile","emails: any[]","account: LinkAccountFn | undefined"],"sources":["../../../src/server/auth/primitives/$auth.ts","../../../src/server/auth/constants/routes.ts","../../../src/server/auth/schemas/tokensSchema.ts","../../../src/server/auth/schemas/tokenResponseSchema.ts","../../../src/server/auth/schemas/userinfoResponseSchema.ts","../../../src/server/auth/providers/ServerAuthProvider.ts","../../../src/server/auth/schemas/authenticationProviderSchema.ts","../../../src/server/auth/primitives/$authCredentials.ts","../../../src/server/auth/primitives/$authGithub.ts","../../../src/server/auth/primitives/$authGoogle.ts","../../../src/server/auth/index.ts"],"sourcesContent":["import {\n $inject,\n AlephaError,\n type Async,\n createPrimitive,\n KIND,\n Primitive,\n} from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport {\n type AccessTokenResponse,\n type RealmPrimitive,\n SecurityError,\n SecurityProvider,\n type UserAccount,\n} from \"alepha/security\";\nimport {\n allowInsecureRequests,\n Configuration,\n discovery,\n refreshTokenGrant,\n} from \"openid-client\";\nimport type { OAuth2Profile } from \"../providers/ServerAuthProvider.ts\";\nimport type { Tokens } from \"../schemas/tokensSchema.ts\";\n\n/**\n * Creates an authentication provider primitive for handling user login flows.\n *\n * Supports multiple authentication strategies: credentials (username/password), OAuth2,\n * and OIDC (OpenID Connect). Handles token management, user profile retrieval, and\n * integration with both external identity providers (Auth0, Keycloak) and internal realms.\n *\n * **Authentication Types**: Credentials, OAuth2 (Google, GitHub), OIDC, External providers\n *\n * @example\n * ```ts\n * class AuthProviders {\n * // Internal credentials-based auth\n * credentials = $auth({\n * realm: this.userRealm,\n * credentials: {\n * account: async ({ username, password }) => {\n * return await this.validateUser(username, password);\n * }\n * }\n * });\n *\n * // External OIDC provider\n * keycloak = $auth({\n * oidc: {\n * issuer: \"https://auth.example.com\",\n * clientId: \"my-app\",\n * clientSecret: \"secret\",\n * redirectUri: \"/auth/callback\"\n * }\n * });\n * }\n * ```\n */\nexport const $auth = (options: AuthPrimitiveOptions): AuthPrimitive => {\n return createPrimitive(AuthPrimitive, options);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type AuthPrimitiveOptions = {\n /**\n * Name of the identity provider.\n * If not provided, it will be derived from the property key.\n */\n name?: string;\n\n /**\n * If true, auth provider will be skipped.\n */\n disabled?: boolean;\n} & (AuthExternal | AuthInternal);\n\n/**\n * When you let an external service handle authentication. (e.g. Keycloak, Auth0, etc.)\n */\nexport type AuthExternal = {\n /**\n * Only OIDC is supported for external authentication.\n */\n oidc: OidcOptions;\n\n /**\n * For anonymous access, this will expect a service account access token.\n *\n * ```ts\n * class App {\n * anonymous = $serviceAccount(...);\n * auth = $auth({\n * // ... config ...\n * fallback: this.anonymous,\n * })\n * }\n * ```\n */\n fallback?: () => Async<AccessToken>;\n};\n\n/**\n * When using your own authentication system, e.g. using a database to store user accounts.\n * This is usually used with a custom login form.\n *\n * This relies on the `realm`, which is used to create/verify the access token.\n */\nexport type AuthInternal = {\n realm: RealmPrimitive;\n} & (\n | {\n /**\n * The common username/password authentication.\n *\n * - It uses the OAuth2 Client Credentials flow to obtain an access token.\n *\n * This is usually used with a custom login form on your website or mobile app.\n */\n credentials: CredentialsOptions;\n }\n | {\n /**\n * OAuth2 authentication. Delegates authentication to an OAuth2 provider. (e.g. Google, GitHub, etc.)\n *\n * - It uses the OAuth2 Authorization Code flow to obtain an access token and user information.\n *\n * This is usually used with a login button that redirects to the OAuth2 provider.\n */\n oauth: OAuth2Options;\n }\n | {\n /**\n * Like OAuth2, but uses OIDC (OpenID Connect) for authentication and user information retrieval.\n * OIDC is an identity layer on top of OAuth2, providing user authentication and profile information.\n *\n * - It uses the OAuth2 Authorization Code flow to obtain an access token and user information.\n * - PCKE (Proof Key for Code Exchange) is recommended for security.\n *\n * This is usually used with a login button that redirects to the OIDC provider.\n */\n oidc: OidcOptions;\n }\n);\n\nexport type CredentialsOptions = {\n account: CredentialsFn;\n};\n\nexport type CredentialsFn = (\n credentials: Credentials,\n) => Async<UserAccount | undefined>;\n\nexport interface Credentials {\n username: string;\n password: string;\n}\n\nexport interface OidcOptions {\n /**\n * URL of the OIDC issuer.\n */\n issuer: string;\n\n /**\n * Client ID for the OIDC client.\n */\n clientId: string;\n\n /**\n * Client secret for the OIDC client.\n * Optional if PKCE (Proof Key for Code Exchange) is used.\n */\n clientSecret?: string;\n\n /**\n * Redirect URI for the OIDC client.\n * This is where the user will be redirected after authentication.\n */\n redirectUri?: string;\n\n /**\n * For external auth providers only.\n * Take the ID token instead of the access token for validation.\n */\n useIdToken?: boolean;\n\n /**\n * URI to redirect the user after logout.\n */\n logoutUri?: string;\n\n /**\n * Optional scope for the OIDC client.\n * @default \"openid profile email\".\n */\n scope?: string;\n\n account?: LinkAccountFn;\n}\n\nexport interface LinkAccountOptions {\n access_token: string;\n user: OAuth2Profile;\n id_token?: string;\n expires_in?: number;\n scope?: string;\n}\n\nexport type LinkAccountFn = (tokens: LinkAccountOptions) => Async<UserAccount>;\n\nexport interface OAuth2Options {\n /**\n * URL of the OAuth2 authorization endpoint.\n */\n clientId: string;\n\n /**\n * Client secret for the OAuth2 client.\n */\n clientSecret: string;\n\n /**\n * URL of the OAuth2 authorization endpoint.\n */\n authorization: string;\n\n /**\n * URL of the OAuth2 token endpoint.\n */\n token: string;\n\n /**\n * Function to retrieve user profile information from the OAuth2 tokens.\n */\n userinfo: (tokens: Tokens) => Async<OAuth2Profile>;\n\n account?: LinkAccountFn;\n\n /**\n * URL of the OAuth2 authorization endpoint.\n */\n redirectUri?: string;\n\n /**\n * URL of the OAuth2 authorization endpoint.\n */\n scope?: string;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class AuthPrimitive extends Primitive<AuthPrimitiveOptions> {\n protected readonly securityProvider = $inject(SecurityProvider);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n\n public oauth?: Configuration;\n\n public get name() {\n return this.options.name ?? this.config.propertyKey;\n }\n\n public get realm(): RealmPrimitive | undefined {\n if (\"realm\" in this.options) {\n return this.options.realm;\n }\n return undefined;\n }\n\n public get jwks_uri(): string {\n const jwks = this.oauth?.serverMetadata().jwks_uri;\n if (!jwks) {\n throw new AlephaError(\"No JWKS URI available for the auth provider\");\n }\n return jwks;\n }\n\n public get scope(): string | undefined {\n if (\"oauth\" in this.options) {\n return this.options.oauth.scope;\n }\n if (\"oidc\" in this.options) {\n return this.options.oidc.scope || \"openid profile email\";\n }\n throw new AlephaError(\n \"No OAuth2 or OIDC configuration available for the auth provider\",\n );\n }\n\n public get redirect_uri() {\n if (\"oauth\" in this.options) {\n return this.options.oauth.redirectUri;\n }\n if (\"oidc\" in this.options) {\n return this.options.oidc.redirectUri;\n }\n throw new AlephaError(\n \"No OAuth2 or OIDC configuration available for the auth provider\",\n );\n }\n\n /**\n * Refreshes the access token using the refresh token.\n * Can be used on oauth2, oidc or credentials auth providers.\n */\n public async refresh(\n refreshToken: string,\n accessToken?: string,\n ): Promise<AccessTokenResponse> {\n if (\"realm\" in this.options) {\n return this.options.realm\n .refreshToken(refreshToken, accessToken)\n .then((it) => it.tokens)\n .catch((error) => {\n throw new SecurityError(\n \"Failed to refresh access token using the refresh token (realm)\",\n {\n cause: error,\n },\n );\n });\n } else if (this.oauth) {\n try {\n return {\n ...(await refreshTokenGrant(this.oauth, refreshToken)),\n issued_at: this.dateTimeProvider.now().unix(),\n };\n } catch (error) {\n throw new SecurityError(\n \"Failed to refresh access token using the refresh token (oauth2)\",\n {\n cause: error,\n },\n );\n }\n }\n\n throw new AlephaError(\n \"No realm or OAuth2 configuration available for refreshing the access token\",\n );\n }\n\n /**\n * Extracts user information from the access token.\n * This is used to create a user account from the access token.\n */\n public async user(tokens: Tokens): Promise<UserAccount> {\n try {\n if (\"oauth\" in this.options) {\n const profile = await this.options.oauth.userinfo(tokens);\n\n if (this.options.oauth.account) {\n return this.options.oauth.account({\n ...tokens,\n user: profile,\n });\n }\n\n return this.securityProvider.createUserFromPayload(profile);\n }\n\n if (\"oidc\" in this.options) {\n const payload = this.getUserFromIdToken(tokens.id_token || \"\");\n\n if (this.options.oidc.account) {\n return this.options.oidc.account({\n ...tokens,\n user: payload,\n });\n }\n\n return this.securityProvider.createUserFromPayload(payload);\n }\n } catch (error) {\n throw new SecurityError(\n \"Failed to extract user from identity provider tokens\",\n {\n cause: error,\n },\n );\n }\n\n throw new AlephaError(\n \"This authentication does not support user extraction from tokens\",\n );\n }\n\n protected getUserFromIdToken(idToken: string): OAuth2Profile {\n try {\n return JSON.parse(\n Buffer.from(idToken.split(\".\")[1], \"base64\").toString(\"utf8\"),\n ) as OAuth2Profile;\n } catch (error) {\n throw new AlephaError(\"Failed to parse ID Token payload\", {\n cause: error,\n });\n }\n }\n\n public async prepare() {\n const addons: Array<(config: Configuration) => void> = [];\n\n addons.push(allowInsecureRequests);\n\n if (\"oidc\" in this.options) {\n const { oidc } = this.options;\n\n this.oauth = await discovery(\n new URL(oidc.issuer),\n oidc.clientId,\n {\n client_secret: oidc.clientSecret,\n },\n undefined,\n {\n execute: addons,\n },\n );\n }\n\n if (\"oauth\" in this.options) {\n const { oauth } = this.options;\n\n this.oauth = new Configuration(\n {\n authorization_endpoint: oauth.authorization,\n token_endpoint: oauth.token,\n issuer: oauth.authorization, // use authorization URL as a pseudo-issuer?\n // we don't need all of these endpoints\n jwks_uri: undefined,\n end_session_endpoint: undefined,\n },\n oauth.clientId,\n {\n client_secret: oauth.clientSecret,\n },\n );\n }\n }\n}\n\n$auth[KIND] = AuthPrimitive;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type AccessToken = string | { token: () => Async<string> };\n\nexport interface WithLinkFn {\n link?: (name: string) => (opts: LinkAccountOptions) => Async<UserAccount>;\n}\n\nexport interface WithLoginFn {\n login?: (\n provider: string,\n ) => (creds: Credentials) => Async<UserAccount | undefined>;\n}\n","export const alephaServerAuthRoutes = {\n login: \"/oauth/login\",\n callback: \"/oauth/callback\",\n logout: \"/oauth/logout\",\n token: \"/_auth/token\",\n refresh: \"/_auth/refresh\",\n userinfo: \"/_auth/userinfo\",\n};\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const tokensSchema = t.object({\n provider: t.text(),\n access_token: t.text({ size: \"rich\" }),\n issued_at: t.number(),\n expires_in: t.optional(t.number()),\n refresh_token: t.optional(t.text({ size: \"rich\" })),\n refresh_token_expires_in: t.optional(t.number()),\n refresh_expires_in: t.optional(\n t.number({\n description:\n \"Alias of `refresh_token_expires_in` for compatibility with some providers.\",\n }),\n ),\n id_token: t.optional(t.text({ size: \"rich\" })),\n scope: t.optional(t.text()),\n});\n\nexport type Tokens = Static<typeof tokensSchema>;\n","import { type Static, t } from \"alepha\";\nimport { userAccountInfoSchema } from \"alepha/security\";\nimport { apiLinksResponseSchema } from \"alepha/server/links\";\nimport { tokensSchema } from \"./tokensSchema.ts\";\n\nexport const tokenResponseSchema = t.extend(tokensSchema, {\n user: userAccountInfoSchema,\n api: apiLinksResponseSchema,\n});\n\nexport type TokenResponse = Static<typeof tokenResponseSchema>;\n","import { type Static, t } from \"alepha\";\nimport { userAccountInfoSchema } from \"alepha/security\";\nimport { apiLinksResponseSchema } from \"alepha/server/links\";\n\nexport const userinfoResponseSchema = t.object({\n user: t.optional(userAccountInfoSchema),\n api: apiLinksResponseSchema,\n});\n\nexport type UserinfoResponse = Static<typeof userinfoResponseSchema>;\n","import { $hook, $inject, Alepha, t } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport {\n InvalidCredentialsError,\n SecurityError,\n type UserAccount,\n} from \"alepha/security\";\nimport { $route, BadRequestError } from \"alepha/server\";\nimport {\n $cookie,\n type Cookies,\n ServerCookiesProvider,\n} from \"alepha/server/cookies\";\nimport { ServerLinksProvider } from \"alepha/server/links\";\nimport {\n authorizationCodeGrant,\n buildAuthorizationUrl,\n buildEndSessionUrl,\n calculatePKCECodeChallenge,\n randomPKCECodeVerifier,\n randomState,\n} from \"openid-client\";\nimport { alephaServerAuthRoutes } from \"../constants/routes.ts\";\nimport { $auth, type AuthPrimitive } from \"../primitives/$auth.ts\";\nimport type { AuthenticationProvider } from \"../schemas/authenticationProviderSchema.ts\";\nimport { tokenResponseSchema } from \"../schemas/tokenResponseSchema.ts\";\nimport { type Tokens, tokensSchema } from \"../schemas/tokensSchema.ts\";\nimport { userinfoResponseSchema } from \"../schemas/userinfoResponseSchema.ts\";\n\nexport class ServerAuthProvider {\n protected readonly log = $logger();\n protected readonly alepha = $inject(Alepha);\n protected readonly serverCookiesProvider = $inject(ServerCookiesProvider);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly serverLinksProvider = $inject(ServerLinksProvider);\n\n protected readonly authorizationCode = $cookie({\n name: \"authorizationCode\",\n ttl: [15, \"minutes\"],\n httpOnly: true,\n schema: t.object({\n provider: t.text(),\n realm: t.optional(t.text()),\n codeVerifier: t.optional(t.text({ size: \"long\" })),\n redirectUri: t.optional(t.text({ size: \"long\" })),\n state: t.optional(t.text()),\n nonce: t.optional(t.text()),\n }),\n });\n\n public readonly tokens = $cookie({\n name: \"tokens\",\n ttl: [30, \"days\"],\n httpOnly: true,\n compress: true,\n encrypt: true,\n schema: tokensSchema,\n });\n\n public get identities(): Array<AuthPrimitive> {\n return this.alepha\n .primitives($auth)\n .filter((auth) => !auth.options.disabled);\n }\n\n public getAuthenticationProviders(\n filters: { realmName?: string } = {},\n ): AuthenticationProvider[] {\n const providers: AuthenticationProvider[] = [];\n\n for (const identity of this.identities) {\n if (filters.realmName) {\n const realm = \"realm\" in identity.options && identity.options.realm;\n if (!realm || realm.name !== filters.realmName) {\n continue;\n }\n }\n\n const type =\n \"oidc\" in identity.options\n ? \"OIDC\"\n : \"oauth\" in identity.options\n ? \"OAUTH2\"\n : \"credentials\" in identity.options\n ? \"CREDENTIALS\"\n : undefined;\n\n if (!type) {\n continue;\n }\n\n providers.push({\n name: identity.name,\n type,\n });\n }\n\n return providers;\n }\n\n protected readonly configure = $hook({\n on: \"configure\",\n handler: async () => {\n for (const identity of this.identities) {\n await identity.prepare();\n }\n },\n });\n\n protected getAccessTokens(tokens: Tokens) {\n const idp = this.provider(tokens.provider);\n\n if (\n \"oidc\" in idp.options &&\n !(\"realm\" in idp.options) &&\n idp.options.oidc?.useIdToken\n ) {\n return tokens.id_token;\n }\n\n return tokens.access_token;\n }\n\n /**\n * Fill request headers with access token from cookies or fallback to provider's fallback function.\n */\n protected readonly onRequest = $hook({\n on: \"server:onRequest\",\n after: this.serverCookiesProvider,\n handler: async ({ request }) => {\n const cookies = request.cookies;\n\n // [feature] forward cookies to request headers\n if (cookies) {\n const tokens = await this.cookiesToTokens(cookies);\n if (tokens) {\n request.headers.authorization = `Bearer ${this.getAccessTokens(tokens)}`;\n this.log.trace(\"Access token set in request headers\", {\n provider: tokens.provider,\n });\n }\n }\n\n // [feature] support for auth providers with fallback\n if (!request.headers.authorization) {\n for (const provider of this.identities) {\n if (!(\"realm\" in provider.options) && !!provider.options.fallback) {\n const token = await provider.options.fallback();\n if (token) {\n request.headers.authorization = `Bearer ${token}`;\n break;\n }\n }\n }\n }\n },\n });\n\n /**\n * Convert cookies to tokens.\n * If the tokens are expired, try to refresh them using the refresh token.\n */\n protected async cookiesToTokens(\n cookies: Cookies,\n ): Promise<Tokens | undefined> {\n const tokens = this.getTokens(cookies);\n if (!tokens) {\n // no cookie, no tokens\n this.log.trace(\"No tokens found in cookies\");\n return;\n }\n\n this.log.trace(\"Tokens found in cookies\", {\n expires_in: tokens.expires_in,\n issued_at: tokens.issued_at,\n });\n\n // check if tokens are expired\n const refreshedTokens = await this.refreshTokens(tokens);\n if (!refreshedTokens) {\n this.tokens.del({ cookies });\n // 08/25: exception here will go to Server error handler, not the React one\n // better to remove cookie & session and let the page handle 401 Unauthorized\n //throw new SessionExpiredError(\"Session expired. Please login again.\");\n return;\n }\n\n if (refreshedTokens.access_token !== tokens.access_token) {\n this.setTokens(refreshedTokens, cookies);\n }\n\n return refreshedTokens;\n }\n\n protected async refreshTokens(tokens: Tokens): Promise<Tokens | undefined> {\n if (tokens.expires_in && tokens.issued_at) {\n const gracePeriodSec = 10;\n const expiresAt = tokens.issued_at + (tokens.expires_in - gracePeriodSec);\n\n if (expiresAt < this.dateTimeProvider.now().unix()) {\n this.log.trace(\"Tokens are expired\");\n\n // oh no, it is expired\n if (tokens.refresh_token) {\n this.log.trace(\"Trying to refresh tokens using refresh token\");\n // but has refresh token!\n try {\n const provider = this.provider(tokens);\n const result = await provider.refresh(\n tokens.refresh_token,\n tokens.access_token,\n );\n const newTokens = {\n ...result,\n provider: tokens.provider,\n issued_at: this.dateTimeProvider.now().unix(),\n };\n\n this.log.debug(\"Tokens refreshed successfully\");\n\n return newTokens;\n } catch (e) {\n this.log.warn(\"Failed to refresh token\", e);\n }\n }\n\n // session expired and no (valid) refresh token\n return;\n }\n }\n\n if (!tokens.issued_at && tokens.access_token) {\n return;\n }\n\n return tokens;\n }\n\n // -------------------------------------------------------------------------------------------------------------------\n\n /**\n * Get user information.\n */\n public readonly userinfo = $route({\n path: alephaServerAuthRoutes.userinfo,\n schema: {\n response: userinfoResponseSchema,\n },\n handler: async ({ user, headers, cookies }) => {\n const tokens = this.getTokens(cookies);\n if (tokens) {\n const provider = this.provider(tokens);\n if (!(\"realm\" in provider.options)) {\n const user = await provider.user(tokens);\n const api = await this.serverLinksProvider.getUserApiLinks({\n authorization: headers.authorization,\n user,\n });\n return {\n api,\n user,\n };\n }\n }\n\n const api = await this.serverLinksProvider.getUserApiLinks({\n authorization: headers.authorization,\n user,\n });\n\n return {\n api,\n user,\n };\n },\n });\n\n /**\n * Refresh a token for internal providers.\n */\n public readonly refresh = $route({\n path: alephaServerAuthRoutes.refresh,\n method: \"POST\",\n schema: {\n query: t.object({\n provider: t.text(),\n }),\n body: t.object({\n refresh_token: t.text({\n size: \"rich\",\n }),\n access_token: t.optional(\n t.text({\n size: \"rich\",\n description:\n \"Required if provider has stateless refresh token on credentials mode\",\n }),\n ),\n }),\n response: tokensSchema,\n },\n handler: async ({ query, body, cookies }) => {\n const provider = this.provider(query);\n\n const tokens = {\n provider: query.provider,\n ...(await provider.refresh(body.refresh_token, body.access_token)),\n };\n\n // for web applications, we store tokens in cookies\n this.setTokens(tokens, cookies);\n\n return tokens;\n },\n });\n\n /**\n * Login for local password-based authentication.\n */\n public readonly token = $route({\n path: alephaServerAuthRoutes.token,\n method: \"POST\",\n schema: {\n query: t.object({\n provider: t.text(),\n realm: t.optional(\n t.text({ description: \"Realm name for multi-realm setups\" }),\n ),\n }),\n body: t.object({\n username: t.text(),\n password: t.text(),\n }),\n response: tokenResponseSchema,\n },\n handler: async ({ query, body, cookies }) => {\n const provider = this.provider({\n provider: query.provider,\n realm: query.realm,\n });\n\n const realm = \"realm\" in provider.options && provider.options.realm;\n if (!realm) {\n throw new SecurityError(\n `Auth provider '${query.provider}' does not support password grant`,\n );\n }\n\n const credentials =\n \"credentials\" in provider.options && provider.options.credentials;\n\n if (!credentials) {\n throw new SecurityError(\n `Auth provider '${query.provider}' does not support password grant`,\n );\n }\n\n console.log(\"->\", body);\n\n let user: UserAccount | undefined;\n try {\n user = await credentials.account(body);\n } catch (e) {\n if (e instanceof InvalidCredentialsError) {\n throw e;\n }\n this.log.error(\"Failed to authenticate user\", e);\n throw new InvalidCredentialsError();\n }\n\n if (!user) {\n throw new InvalidCredentialsError();\n }\n\n const tokens = {\n provider: query.provider,\n ...(await realm.createToken(user)),\n };\n\n // for web applications, we store tokens in cookies\n this.setTokens(tokens, cookies);\n\n const api = await this.serverLinksProvider.getUserApiLinks({\n user,\n });\n\n // mobile apps require this\n return {\n ...tokens,\n user,\n api,\n };\n },\n });\n\n /**\n * Oauth2/OIDC login route.\n */\n public readonly login = $route({\n path: alephaServerAuthRoutes.login,\n schema: {\n query: t.object({\n provider: t.text(),\n realm: t.optional(\n t.text({ description: \"Realm name for multi-realm setups\" }),\n ),\n redirect_uri: t.optional(t.text({ size: \"rich\" })),\n }),\n },\n handler: async ({ query, url, reply }) => {\n const provider = this.provider({\n provider: query.provider,\n realm: query.realm,\n });\n const oauth = provider.oauth;\n if (!oauth) {\n throw new SecurityError(\n `Auth provider '${query.provider}' does not support OAuth2`,\n );\n }\n\n const scope = provider.scope;\n let redirect_uri =\n provider.redirect_uri || alephaServerAuthRoutes.callback;\n if (redirect_uri.startsWith(\"/\")) {\n redirect_uri = `${url.protocol}//${url.host}${redirect_uri}`;\n }\n\n const oidc = \"oidc\" in provider.options && provider.options.oidc;\n\n if (!oauth.serverMetadata().supportsPKCE()) {\n const state = randomState();\n const parameters: Record<string, string> = {\n redirect_uri,\n state,\n };\n\n if (oidc) {\n parameters.nonce = randomState();\n }\n\n if (scope) {\n parameters.scope = scope;\n }\n\n this.authorizationCode.set({\n state,\n nonce: parameters.nonce,\n redirectUri: query.redirect_uri ?? \"/\",\n provider: query.provider,\n realm: query.realm,\n });\n\n reply.redirect(buildAuthorizationUrl(oauth, parameters).toString());\n return;\n }\n\n const codeVerifier = randomPKCECodeVerifier();\n const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);\n\n const parameters: Record<string, string> = {\n redirect_uri,\n code_challenge: codeChallenge,\n code_challenge_method: \"S256\",\n };\n\n if (scope) {\n parameters.scope = scope;\n }\n\n this.authorizationCode.set({\n codeVerifier,\n redirectUri: query.redirect_uri ?? \"/\",\n provider: query.provider,\n realm: query.realm,\n });\n\n reply.redirect(buildAuthorizationUrl(oauth, parameters).toString());\n },\n });\n\n /**\n * Callback for OAuth2/OIDC providers.\n * It handles the authorization code flow and retrieves the access token.\n */\n public readonly callback = $route({\n path: alephaServerAuthRoutes.callback,\n handler: async ({ url, reply, cookies }) => {\n const authorizationCode = this.authorizationCode.get({ cookies });\n if (!authorizationCode) {\n throw new BadRequestError(\"Missing code verifier\");\n }\n\n const provider = this.provider(authorizationCode);\n const oauth = provider.oauth;\n if (!oauth) {\n throw new SecurityError(\n `Auth provider '${provider.name}' does not support OAuth2`,\n );\n }\n\n const redirectUri = authorizationCode.redirectUri ?? \"/\";\n\n const externalTokens = await authorizationCodeGrant(oauth, url, {\n pkceCodeVerifier: authorizationCode.codeVerifier,\n expectedState: authorizationCode.state,\n expectedNonce: authorizationCode.nonce,\n })\n .then((tokens) => ({\n issued_at: this.dateTimeProvider.now().unix(),\n provider: provider.name,\n ...tokens,\n }))\n .catch((e) => {\n this.log.error(\"Failed to get access token\", e);\n throw new SecurityError(\"Failed to get access token\", {\n cause: e,\n });\n });\n\n this.authorizationCode.del({ cookies });\n\n const realm = \"realm\" in provider.options && provider.options.realm;\n\n // external, full OIDC System (e.g. Keycloak, Auth0)\n if (!realm) {\n this.setTokens(externalTokens, cookies);\n reply.redirect(redirectUri);\n return;\n }\n\n // internal, we need to create our own tokens\n\n const user = await provider.user(externalTokens);\n const tokens = await realm.createToken(user);\n\n this.setTokens(\n {\n ...tokens,\n issued_at: this.dateTimeProvider.now().unix(),\n provider: provider.name,\n },\n cookies,\n );\n\n reply.redirect(redirectUri);\n },\n });\n\n /**\n * Logout route for OAuth2/OIDC providers.\n */\n public readonly logout = $route({\n path: alephaServerAuthRoutes.logout,\n method: \"GET\",\n schema: {\n query: t.object({\n post_logout_redirect_uri: t.optional(t.text()),\n }),\n },\n handler: async ({ query, reply, cookies }) => {\n const redirect = query.post_logout_redirect_uri ?? \"/\";\n const tokens = this.getTokens(cookies);\n if (!tokens) {\n reply.redirect(redirect);\n return;\n }\n\n const provider = this.provider(tokens.provider);\n\n this.tokens.del({ cookies });\n\n // for internal providers, we can delete the session - if available\n if (\"realm\" in provider.options && tokens.refresh_token) {\n const onDeleteSession =\n provider.options.realm.options.settings?.onDeleteSession;\n if (onDeleteSession) {\n try {\n await onDeleteSession(tokens.refresh_token);\n } catch (e) {\n this.log.error(\"Failed to delete session\", e);\n }\n }\n }\n\n const oauth = provider.oauth;\n if (!oauth) {\n reply.redirect(redirect);\n return;\n }\n\n const params = new URLSearchParams();\n const idToken = tokens?.id_token;\n\n params.set(\"post_logout_redirect_uri\", redirect);\n if (idToken) {\n params.set(\"id_token_hint\", idToken);\n }\n\n const customLogoutUri =\n \"oidc\" in provider.options\n ? provider.options.oidc?.logoutUri\n : undefined;\n\n if (customLogoutUri) {\n reply.redirect(`${customLogoutUri}?${params}`);\n return;\n }\n\n if (!oauth.serverMetadata().end_session_endpoint) {\n // await tokenRevocation(\n // \toauth,\n // \ttokens?.refresh_token ?? tokens.access_token,\n // );\n reply.redirect(redirect);\n return;\n }\n\n reply.redirect(buildEndSessionUrl(oauth, params).toString());\n },\n });\n\n /**\n * Find an auth provider by name and optionally by realm.\n * When realm is specified, it filters providers by both name and realm.\n * This enables multi-realm setups where multiple providers share the same name (e.g., \"credentials\").\n */\n protected provider(\n opts: string | { provider: string; realm?: string },\n ): AuthPrimitive {\n const name = typeof opts === \"string\" ? opts : opts.provider;\n const realmName = typeof opts === \"string\" ? undefined : opts.realm;\n\n const identity = this.identities.find((identity) => {\n if (identity.name !== name) {\n return false;\n }\n\n // If realm filter is specified, match against provider's realm\n if (realmName && identity.realm?.name !== realmName) {\n return false;\n }\n\n return true;\n });\n\n if (!identity) {\n const realmInfo = realmName ? ` for realm '${realmName}'` : \"\";\n throw new SecurityError(`Auth provider '${name}'${realmInfo} not found`);\n }\n\n return identity;\n }\n\n protected getTokens(cookies?: Cookies): Tokens | undefined {\n return this.tokens.get({ cookies });\n }\n\n protected setTokens(tokens: Tokens, cookies?: Cookies): void {\n const exp =\n tokens.refresh_token_expires_in ||\n tokens.refresh_expires_in ||\n tokens.expires_in;\n\n const ttl = exp\n ? this.dateTimeProvider.duration(exp, \"seconds\")\n : undefined;\n\n this.tokens.set(tokens, {\n cookies,\n ttl,\n });\n }\n}\n\nexport interface OAuth2Profile {\n sub: string; // Subject - unique ID per user (required by OpenID)\n email?: string;\n name?: string;\n given_name?: string;\n family_name?: string;\n middle_name?: string;\n nickname?: string;\n preferred_username?: string;\n profile?: string;\n picture?: string;\n website?: string;\n email_verified?: boolean;\n gender?: string;\n birthdate?: string; // ISO 8601: YYYY-MM-DD\n zoneinfo?: string;\n locale?: string;\n phone_number?: string;\n phone_number_verified?: boolean;\n address?: {\n formatted?: string;\n street_address?: string;\n locality?: string;\n region?: string;\n postal_code?: string;\n country?: string;\n };\n updated_at?: number; // seconds since epoch\n // Allow additional fields (provider-specific)\n [key: string]: unknown;\n}\n","import { type Static, t } from \"alepha\";\n\nexport const authenticationProviderSchema = t.object(\n {\n name: t.text({\n description: \"Name of the authentication provider.\",\n }),\n type: t.enum([\"OAUTH2\", \"OIDC\", \"CREDENTIALS\"], {\n description: \"Type of the authentication provider.\",\n }),\n },\n {\n title: \"AuthenticationProvider\",\n },\n);\n\nexport type AuthenticationProvider = Static<\n typeof authenticationProviderSchema\n>;\n","import { AlephaError } from \"alepha\";\nimport type { RealmPrimitive } from \"alepha/security\";\nimport {\n $auth,\n type CredentialsFn,\n type CredentialsOptions,\n type WithLoginFn,\n} from \"./$auth.ts\";\n\n/**\n * Already configured Credentials authentication primitive.\n *\n * Uses username and password to authenticate users.\n */\nexport const $authCredentials = (\n realm: RealmPrimitive & WithLoginFn,\n options: Partial<CredentialsOptions> = {},\n) => {\n const name = \"credentials\";\n\n const account: CredentialsFn | undefined = realm.login\n ? realm.login(name)\n : options.account;\n\n if (!account) {\n throw new AlephaError(\n \"Credentials authentication requires a login function in the realm primitive.\",\n );\n }\n\n return $auth({\n realm,\n name,\n credentials: {\n account,\n },\n });\n};\n","import { $context, AlephaError, t } from \"alepha\";\nimport type { RealmPrimitive } from \"alepha/security\";\nimport type { OAuth2Profile } from \"../providers/ServerAuthProvider.ts\";\nimport {\n $auth,\n type LinkAccountFn,\n type OidcOptions,\n type WithLinkFn,\n} from \"./$auth.ts\";\n\n/**\n * Already configured GitHub authentication primitive.\n *\n * Uses OAuth2 to authenticate users via their GitHub accounts.\n * Upon successful authentication, it links the GitHub account to a user session.\n *\n * Environment Variables:\n * - `GITHUB_CLIENT_ID`: The client ID obtained from the GitHub Developer Settings.\n * - `GITHUB_CLIENT_SECRET`: The client secret obtained from the GitHub Developer Settings.\n */\nexport const $authGithub = (\n realm: RealmPrimitive & WithLinkFn,\n options: Partial<OidcOptions> = {},\n) => {\n const { alepha } = $context();\n\n const env = alepha.parseEnv(\n t.object({\n GITHUB_CLIENT_ID: t.optional(t.text()),\n GITHUB_CLIENT_SECRET: t.optional(t.text()),\n }),\n );\n\n const disabled = !env.GITHUB_CLIENT_ID || !env.GITHUB_CLIENT_SECRET;\n\n const name = \"github\";\n\n const account: LinkAccountFn | undefined =\n options.account ?? (realm.link ? realm.link(name) : undefined);\n\n if (!account) {\n throw new AlephaError(\n \"Authentication requires a link function in the realm primitive.\",\n );\n }\n\n return $auth({\n realm,\n name,\n oauth: {\n clientId: env.GITHUB_CLIENT_ID!,\n clientSecret: env.GITHUB_CLIENT_SECRET!,\n authorization: \"https://github.com/login/oauth/authorize\",\n token: \"https://github.com/login/oauth/access_token\",\n scope: \"read:user user:email\",\n userinfo: async (tokens) => {\n const BASE_URL = \"https://api.github.com\";\n const res = await fetch(`${BASE_URL}/user`, {\n headers: {\n Authorization: `Bearer ${tokens.access_token}`,\n \"User-Agent\": \"Alepha\",\n },\n }).then((res) => res.json());\n\n const user: OAuth2Profile = {\n sub: res.id.toString(),\n };\n\n if (res.email) {\n user.email = res.email;\n }\n\n if (res.name) {\n user.name = res.name.trim();\n }\n\n if (res.avatar_url) {\n user.picture = res.avatar_url;\n }\n\n if (!user.email) {\n const res = await fetch(`${BASE_URL}/user/emails`, {\n headers: {\n Authorization: `Bearer ${tokens.access_token}`,\n \"User-Agent\": \"Alepha\",\n },\n });\n if (res.ok) {\n const emails: any[] = await res.json();\n user.email = (emails.find((e) => e.primary) ?? emails[0]).email;\n }\n }\n\n return user;\n },\n ...options,\n account,\n },\n disabled,\n });\n};\n","import { $context, AlephaError, t } from \"alepha\";\nimport type { RealmPrimitive } from \"alepha/security\";\nimport {\n $auth,\n type LinkAccountFn,\n type OidcOptions,\n type WithLinkFn,\n} from \"./$auth.ts\";\n\n/**\n * Already configured Google authentication primitive.\n *\n * Uses OpenID Connect (OIDC) to authenticate users via their Google accounts.\n * Upon successful authentication, it links the Google account to a user session.\n *\n * Environment Variables:\n * - `GOOGLE_CLIENT_ID`: The client ID obtained from the Google Developer Console.\n * - `GOOGLE_CLIENT_SECRET`: The client secret obtained from the Google Developer Console.\n */\nexport const $authGoogle = (\n realm: RealmPrimitive & WithLinkFn,\n options: Partial<OidcOptions> = {},\n) => {\n const { alepha } = $context();\n\n const env = alepha.parseEnv(\n t.object({\n GOOGLE_CLIENT_ID: t.optional(t.text()),\n GOOGLE_CLIENT_SECRET: t.optional(t.text()),\n }),\n );\n\n const disabled = !env.GOOGLE_CLIENT_ID || !env.GOOGLE_CLIENT_SECRET;\n\n const name = \"google\";\n\n const account: LinkAccountFn | undefined =\n options.account ?? (realm.link ? realm.link(name) : undefined);\n\n if (!account) {\n throw new AlephaError(\n \"Authentication requires a link function in the realm primitive.\",\n );\n }\n\n return $auth({\n realm,\n name,\n oidc: {\n issuer: \"https://accounts.google.com\",\n clientId: env.GOOGLE_CLIENT_ID!,\n clientSecret: env.GOOGLE_CLIENT_SECRET,\n ...options,\n account,\n },\n disabled,\n });\n};\n","import { $module } from \"alepha\";\nimport type { UserAccount } from \"alepha/security\";\nimport { AlephaServerCookies } from \"alepha/server/cookies\";\nimport { $auth } from \"./primitives/$auth.ts\";\nimport { ServerAuthProvider } from \"./providers/ServerAuthProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./index.shared.ts\";\nexport * from \"./primitives/$auth.ts\";\nexport * from \"./primitives/$authCredentials.ts\";\nexport * from \"./primitives/$authGithub.ts\";\nexport * from \"./primitives/$authGoogle.ts\";\nexport * from \"./providers/ServerAuthProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha\" {\n export interface State {\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\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Allow authentication services for server applications.\n * It provides login and logout functionalities.\n *\n * There are multiple authentication providers available (e.g., Google, GitHub).\n * You can also delegate authentication to your own OIDC/OAuth2, for example using Keycloak or Auth0.\n *\n * It's cookie-based and SSR friendly.\n *\n * @see {@link $auth}\n * @see {@link ServerAuthProvider}\n * @module alepha.server.auth\n */\nexport const AlephaServerAuth = $module({\n name: \"alepha.server.auth\",\n primitives: [$auth],\n services: [AlephaServerCookies, ServerAuthProvider],\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["user","parameters","tokens","identity","res"],"sources":["../../../src/server/auth/primitives/$auth.ts","../../../src/server/auth/constants/routes.ts","../../../src/server/auth/schemas/tokensSchema.ts","../../../src/server/auth/schemas/tokenResponseSchema.ts","../../../src/server/auth/schemas/userinfoResponseSchema.ts","../../../src/server/auth/providers/ServerAuthProvider.ts","../../../src/server/auth/schemas/authenticationProviderSchema.ts","../../../src/server/auth/primitives/$authCredentials.ts","../../../src/server/auth/primitives/$authGithub.ts","../../../src/server/auth/primitives/$authGoogle.ts","../../../src/server/auth/index.ts"],"sourcesContent":["import {\n $inject,\n AlephaError,\n type Async,\n createPrimitive,\n KIND,\n Primitive,\n} from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport {\n type AccessTokenResponse,\n type RealmPrimitive,\n SecurityError,\n SecurityProvider,\n type UserAccount,\n} from \"alepha/security\";\nimport {\n allowInsecureRequests,\n Configuration,\n discovery,\n refreshTokenGrant,\n} from \"openid-client\";\nimport type { OAuth2Profile } from \"../providers/ServerAuthProvider.ts\";\nimport type { Tokens } from \"../schemas/tokensSchema.ts\";\n\n/**\n * Creates an authentication provider primitive for handling user login flows.\n *\n * Supports multiple authentication strategies: credentials (username/password), OAuth2,\n * and OIDC (OpenID Connect). Handles token management, user profile retrieval, and\n * integration with both external identity providers (Auth0, Keycloak) and internal realms.\n *\n * **Authentication Types**: Credentials, OAuth2 (Google, GitHub), OIDC, External providers\n *\n * @example\n * ```ts\n * class AuthProviders {\n * // Internal credentials-based auth\n * credentials = $auth({\n * realm: this.userRealm,\n * credentials: {\n * account: async ({ username, password }) => {\n * return await this.validateUser(username, password);\n * }\n * }\n * });\n *\n * // External OIDC provider\n * keycloak = $auth({\n * oidc: {\n * issuer: \"https://auth.example.com\",\n * clientId: \"my-app\",\n * clientSecret: \"secret\",\n * redirectUri: \"/auth/callback\"\n * }\n * });\n * }\n * ```\n */\nexport const $auth = (options: AuthPrimitiveOptions): AuthPrimitive => {\n return createPrimitive(AuthPrimitive, options);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type AuthPrimitiveOptions = {\n /**\n * Name of the identity provider.\n * If not provided, it will be derived from the property key.\n */\n name?: string;\n\n /**\n * If true, auth provider will be skipped.\n */\n disabled?: boolean;\n} & (AuthExternal | AuthInternal);\n\n/**\n * When you let an external service handle authentication. (e.g. Keycloak, Auth0, etc.)\n */\nexport type AuthExternal = {\n /**\n * Only OIDC is supported for external authentication.\n */\n oidc: OidcOptions;\n\n /**\n * For anonymous access, this will expect a service account access token.\n *\n * ```ts\n * class App {\n * anonymous = $serviceAccount(...);\n * auth = $auth({\n * // ... config ...\n * fallback: this.anonymous,\n * })\n * }\n * ```\n */\n fallback?: () => Async<AccessToken>;\n};\n\n/**\n * When using your own authentication system, e.g. using a database to store user accounts.\n * This is usually used with a custom login form.\n *\n * This relies on the `realm`, which is used to create/verify the access token.\n */\nexport type AuthInternal = {\n realm: RealmPrimitive;\n} & (\n | {\n /**\n * The common username/password authentication.\n *\n * - It uses the OAuth2 Client Credentials flow to obtain an access token.\n *\n * This is usually used with a custom login form on your website or mobile app.\n */\n credentials: CredentialsOptions;\n }\n | {\n /**\n * OAuth2 authentication. Delegates authentication to an OAuth2 provider. (e.g. Google, GitHub, etc.)\n *\n * - It uses the OAuth2 Authorization Code flow to obtain an access token and user information.\n *\n * This is usually used with a login button that redirects to the OAuth2 provider.\n */\n oauth: OAuth2Options;\n }\n | {\n /**\n * Like OAuth2, but uses OIDC (OpenID Connect) for authentication and user information retrieval.\n * OIDC is an identity layer on top of OAuth2, providing user authentication and profile information.\n *\n * - It uses the OAuth2 Authorization Code flow to obtain an access token and user information.\n * - PCKE (Proof Key for Code Exchange) is recommended for security.\n *\n * This is usually used with a login button that redirects to the OIDC provider.\n */\n oidc: OidcOptions;\n }\n);\n\nexport type CredentialsOptions = {\n account: CredentialsFn;\n};\n\nexport type CredentialsFn = (\n credentials: Credentials,\n) => Async<UserAccount | undefined>;\n\nexport interface Credentials {\n username: string;\n password: string;\n}\n\nexport interface OidcOptions {\n /**\n * URL of the OIDC issuer.\n */\n issuer: string;\n\n /**\n * Client ID for the OIDC client.\n */\n clientId: string;\n\n /**\n * Client secret for the OIDC client.\n * Optional if PKCE (Proof Key for Code Exchange) is used.\n */\n clientSecret?: string;\n\n /**\n * Redirect URI for the OIDC client.\n * This is where the user will be redirected after authentication.\n */\n redirectUri?: string;\n\n /**\n * For external auth providers only.\n * Take the ID token instead of the access token for validation.\n */\n useIdToken?: boolean;\n\n /**\n * URI to redirect the user after logout.\n */\n logoutUri?: string;\n\n /**\n * Optional scope for the OIDC client.\n * @default \"openid profile email\".\n */\n scope?: string;\n\n account?: LinkAccountFn;\n}\n\nexport interface LinkAccountOptions {\n access_token: string;\n user: OAuth2Profile;\n id_token?: string;\n expires_in?: number;\n scope?: string;\n}\n\nexport type LinkAccountFn = (tokens: LinkAccountOptions) => Async<UserAccount>;\n\nexport interface OAuth2Options {\n /**\n * URL of the OAuth2 authorization endpoint.\n */\n clientId: string;\n\n /**\n * Client secret for the OAuth2 client.\n */\n clientSecret: string;\n\n /**\n * URL of the OAuth2 authorization endpoint.\n */\n authorization: string;\n\n /**\n * URL of the OAuth2 token endpoint.\n */\n token: string;\n\n /**\n * Function to retrieve user profile information from the OAuth2 tokens.\n */\n userinfo: (tokens: Tokens) => Async<OAuth2Profile>;\n\n account?: LinkAccountFn;\n\n /**\n * URL of the OAuth2 authorization endpoint.\n */\n redirectUri?: string;\n\n /**\n * URL of the OAuth2 authorization endpoint.\n */\n scope?: string;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class AuthPrimitive extends Primitive<AuthPrimitiveOptions> {\n protected readonly securityProvider = $inject(SecurityProvider);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n\n public oauth?: Configuration;\n\n public get name() {\n return this.options.name ?? this.config.propertyKey;\n }\n\n public get realm(): RealmPrimitive | undefined {\n if (\"realm\" in this.options) {\n return this.options.realm;\n }\n return undefined;\n }\n\n public get jwks_uri(): string {\n const jwks = this.oauth?.serverMetadata().jwks_uri;\n if (!jwks) {\n throw new AlephaError(\"No JWKS URI available for the auth provider\");\n }\n return jwks;\n }\n\n public get scope(): string | undefined {\n if (\"oauth\" in this.options) {\n return this.options.oauth.scope;\n }\n if (\"oidc\" in this.options) {\n return this.options.oidc.scope || \"openid profile email\";\n }\n throw new AlephaError(\n \"No OAuth2 or OIDC configuration available for the auth provider\",\n );\n }\n\n public get redirect_uri() {\n if (\"oauth\" in this.options) {\n return this.options.oauth.redirectUri;\n }\n if (\"oidc\" in this.options) {\n return this.options.oidc.redirectUri;\n }\n throw new AlephaError(\n \"No OAuth2 or OIDC configuration available for the auth provider\",\n );\n }\n\n /**\n * Refreshes the access token using the refresh token.\n * Can be used on oauth2, oidc or credentials auth providers.\n */\n public async refresh(\n refreshToken: string,\n accessToken?: string,\n ): Promise<AccessTokenResponse> {\n if (\"realm\" in this.options) {\n return this.options.realm\n .refreshToken(refreshToken, accessToken)\n .then((it) => it.tokens)\n .catch((error) => {\n throw new SecurityError(\n \"Failed to refresh access token using the refresh token (realm)\",\n {\n cause: error,\n },\n );\n });\n } else if (this.oauth) {\n try {\n return {\n ...(await refreshTokenGrant(this.oauth, refreshToken)),\n issued_at: this.dateTimeProvider.now().unix(),\n };\n } catch (error) {\n throw new SecurityError(\n \"Failed to refresh access token using the refresh token (oauth2)\",\n {\n cause: error,\n },\n );\n }\n }\n\n throw new AlephaError(\n \"No realm or OAuth2 configuration available for refreshing the access token\",\n );\n }\n\n /**\n * Extracts user information from the access token.\n * This is used to create a user account from the access token.\n */\n public async user(tokens: Tokens): Promise<UserAccount> {\n try {\n if (\"oauth\" in this.options) {\n const profile = await this.options.oauth.userinfo(tokens);\n\n if (this.options.oauth.account) {\n return this.options.oauth.account({\n ...tokens,\n user: profile,\n });\n }\n\n return this.securityProvider.createUserFromPayload(profile);\n }\n\n if (\"oidc\" in this.options) {\n const payload = this.getUserFromIdToken(tokens.id_token || \"\");\n\n if (this.options.oidc.account) {\n return this.options.oidc.account({\n ...tokens,\n user: payload,\n });\n }\n\n return this.securityProvider.createUserFromPayload(payload);\n }\n } catch (error) {\n throw new SecurityError(\n \"Failed to extract user from identity provider tokens\",\n {\n cause: error,\n },\n );\n }\n\n throw new AlephaError(\n \"This authentication does not support user extraction from tokens\",\n );\n }\n\n protected getUserFromIdToken(idToken: string): OAuth2Profile {\n try {\n return JSON.parse(\n Buffer.from(idToken.split(\".\")[1], \"base64\").toString(\"utf8\"),\n ) as OAuth2Profile;\n } catch (error) {\n throw new AlephaError(\"Failed to parse ID Token payload\", {\n cause: error,\n });\n }\n }\n\n public async prepare() {\n const addons: Array<(config: Configuration) => void> = [];\n\n addons.push(allowInsecureRequests);\n\n if (\"oidc\" in this.options) {\n const { oidc } = this.options;\n\n this.oauth = await discovery(\n new URL(oidc.issuer),\n oidc.clientId,\n {\n client_secret: oidc.clientSecret,\n },\n undefined,\n {\n execute: addons,\n },\n );\n }\n\n if (\"oauth\" in this.options) {\n const { oauth } = this.options;\n\n this.oauth = new Configuration(\n {\n authorization_endpoint: oauth.authorization,\n token_endpoint: oauth.token,\n issuer: oauth.authorization, // use authorization URL as a pseudo-issuer?\n // we don't need all of these endpoints\n jwks_uri: undefined,\n end_session_endpoint: undefined,\n },\n oauth.clientId,\n {\n client_secret: oauth.clientSecret,\n },\n );\n }\n }\n}\n\n$auth[KIND] = AuthPrimitive;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type AccessToken = string | { token: () => Async<string> };\n\nexport interface WithLinkFn {\n link?: (name: string) => (opts: LinkAccountOptions) => Async<UserAccount>;\n}\n\nexport interface WithLoginFn {\n login?: (\n provider: string,\n ) => (creds: Credentials) => Async<UserAccount | undefined>;\n}\n","export const alephaServerAuthRoutes = {\n login: \"/oauth/login\",\n callback: \"/oauth/callback\",\n logout: \"/oauth/logout\",\n token: \"/_auth/token\",\n refresh: \"/_auth/refresh\",\n userinfo: \"/_auth/userinfo\",\n};\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const tokensSchema = t.object({\n provider: t.text(),\n access_token: t.text({ size: \"rich\" }),\n issued_at: t.number(),\n expires_in: t.optional(t.number()),\n refresh_token: t.optional(t.text({ size: \"rich\" })),\n refresh_token_expires_in: t.optional(t.number()),\n refresh_expires_in: t.optional(\n t.number({\n description:\n \"Alias of `refresh_token_expires_in` for compatibility with some providers.\",\n }),\n ),\n id_token: t.optional(t.text({ size: \"rich\" })),\n scope: t.optional(t.text()),\n});\n\nexport type Tokens = Static<typeof tokensSchema>;\n","import { type Static, t } from \"alepha\";\nimport { userAccountInfoSchema } from \"alepha/security\";\nimport { apiLinksResponseSchema } from \"alepha/server/links\";\nimport { tokensSchema } from \"./tokensSchema.ts\";\n\nexport const tokenResponseSchema = t.extend(tokensSchema, {\n user: userAccountInfoSchema,\n api: apiLinksResponseSchema,\n});\n\nexport type TokenResponse = Static<typeof tokenResponseSchema>;\n","import { type Static, t } from \"alepha\";\nimport { userAccountInfoSchema } from \"alepha/security\";\nimport { apiLinksResponseSchema } from \"alepha/server/links\";\n\nexport const userinfoResponseSchema = t.object({\n user: t.optional(userAccountInfoSchema),\n api: apiLinksResponseSchema,\n});\n\nexport type UserinfoResponse = Static<typeof userinfoResponseSchema>;\n","import { $hook, $inject, Alepha, t } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport {\n InvalidCredentialsError,\n SecurityError,\n type UserAccount,\n} from \"alepha/security\";\nimport { $route, BadRequestError } from \"alepha/server\";\nimport {\n $cookie,\n type Cookies,\n ServerCookiesProvider,\n} from \"alepha/server/cookies\";\nimport { ServerLinksProvider } from \"alepha/server/links\";\nimport {\n authorizationCodeGrant,\n buildAuthorizationUrl,\n buildEndSessionUrl,\n calculatePKCECodeChallenge,\n randomPKCECodeVerifier,\n randomState,\n} from \"openid-client\";\nimport { alephaServerAuthRoutes } from \"../constants/routes.ts\";\nimport { $auth, type AuthPrimitive } from \"../primitives/$auth.ts\";\nimport type { AuthenticationProvider } from \"../schemas/authenticationProviderSchema.ts\";\nimport { tokenResponseSchema } from \"../schemas/tokenResponseSchema.ts\";\nimport { type Tokens, tokensSchema } from \"../schemas/tokensSchema.ts\";\nimport { userinfoResponseSchema } from \"../schemas/userinfoResponseSchema.ts\";\n\nexport class ServerAuthProvider {\n protected readonly log = $logger();\n protected readonly alepha = $inject(Alepha);\n protected readonly serverCookiesProvider = $inject(ServerCookiesProvider);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly serverLinksProvider = $inject(ServerLinksProvider);\n\n protected readonly authorizationCode = $cookie({\n name: \"authorizationCode\",\n ttl: [15, \"minutes\"],\n httpOnly: true,\n schema: t.object({\n provider: t.text(),\n realm: t.optional(t.text()),\n codeVerifier: t.optional(t.text({ size: \"long\" })),\n redirectUri: t.optional(t.text({ size: \"long\" })),\n state: t.optional(t.text()),\n nonce: t.optional(t.text()),\n }),\n });\n\n public readonly tokens = $cookie({\n name: \"tokens\",\n ttl: [30, \"days\"],\n httpOnly: true,\n compress: true,\n encrypt: true,\n schema: tokensSchema,\n });\n\n public get identities(): Array<AuthPrimitive> {\n return this.alepha\n .primitives($auth)\n .filter((auth) => !auth.options.disabled);\n }\n\n public getAuthenticationProviders(\n filters: { realmName?: string } = {},\n ): AuthenticationProvider[] {\n const providers: AuthenticationProvider[] = [];\n\n for (const identity of this.identities) {\n if (filters.realmName) {\n const realm = \"realm\" in identity.options && identity.options.realm;\n if (!realm || realm.name !== filters.realmName) {\n continue;\n }\n }\n\n const type =\n \"oidc\" in identity.options\n ? \"OIDC\"\n : \"oauth\" in identity.options\n ? \"OAUTH2\"\n : \"credentials\" in identity.options\n ? \"CREDENTIALS\"\n : undefined;\n\n if (!type) {\n continue;\n }\n\n providers.push({\n name: identity.name,\n type,\n });\n }\n\n return providers;\n }\n\n protected readonly configure = $hook({\n on: \"configure\",\n handler: async () => {\n for (const identity of this.identities) {\n await identity.prepare();\n }\n },\n });\n\n protected getAccessTokens(tokens: Tokens) {\n const idp = this.provider(tokens.provider);\n\n if (\n \"oidc\" in idp.options &&\n !(\"realm\" in idp.options) &&\n idp.options.oidc?.useIdToken\n ) {\n return tokens.id_token;\n }\n\n return tokens.access_token;\n }\n\n /**\n * Fill request headers with access token from cookies or fallback to provider's fallback function.\n */\n protected readonly onRequest = $hook({\n on: \"server:onRequest\",\n after: this.serverCookiesProvider,\n handler: async ({ request }) => {\n const cookies = request.cookies;\n\n // [feature] forward cookies to request headers\n if (cookies) {\n const tokens = await this.cookiesToTokens(cookies);\n if (tokens) {\n request.headers.authorization = `Bearer ${this.getAccessTokens(tokens)}`;\n this.log.trace(\"Access token set in request headers\", {\n provider: tokens.provider,\n });\n }\n }\n\n // [feature] support for auth providers with fallback\n if (!request.headers.authorization) {\n for (const provider of this.identities) {\n if (!(\"realm\" in provider.options) && !!provider.options.fallback) {\n const token = await provider.options.fallback();\n if (token) {\n request.headers.authorization = `Bearer ${token}`;\n break;\n }\n }\n }\n }\n },\n });\n\n /**\n * Convert cookies to tokens.\n * If the tokens are expired, try to refresh them using the refresh token.\n */\n protected async cookiesToTokens(\n cookies: Cookies,\n ): Promise<Tokens | undefined> {\n const tokens = this.getTokens(cookies);\n if (!tokens) {\n // no cookie, no tokens\n this.log.trace(\"No tokens found in cookies\");\n return;\n }\n\n this.log.trace(\"Tokens found in cookies\", {\n expires_in: tokens.expires_in,\n issued_at: tokens.issued_at,\n });\n\n // check if tokens are expired\n const refreshedTokens = await this.refreshTokens(tokens);\n if (!refreshedTokens) {\n this.tokens.del({ cookies });\n // 08/25: exception here will go to Server error handler, not the React one\n // better to remove cookie & session and let the page handle 401 Unauthorized\n //throw new SessionExpiredError(\"Session expired. Please login again.\");\n return;\n }\n\n if (refreshedTokens.access_token !== tokens.access_token) {\n this.setTokens(refreshedTokens, cookies);\n }\n\n return refreshedTokens;\n }\n\n protected async refreshTokens(tokens: Tokens): Promise<Tokens | undefined> {\n if (tokens.expires_in && tokens.issued_at) {\n const gracePeriodSec = 10;\n const expiresAt = tokens.issued_at + (tokens.expires_in - gracePeriodSec);\n\n if (expiresAt < this.dateTimeProvider.now().unix()) {\n this.log.trace(\"Tokens are expired\");\n\n // oh no, it is expired\n if (tokens.refresh_token) {\n this.log.trace(\"Trying to refresh tokens using refresh token\");\n // but has refresh token!\n try {\n const provider = this.provider(tokens);\n const result = await provider.refresh(\n tokens.refresh_token,\n tokens.access_token,\n );\n const newTokens = {\n ...result,\n provider: tokens.provider,\n issued_at: this.dateTimeProvider.now().unix(),\n };\n\n this.log.debug(\"Tokens refreshed successfully\");\n\n return newTokens;\n } catch (e) {\n this.log.warn(\"Failed to refresh token\", e);\n }\n }\n\n // session expired and no (valid) refresh token\n return;\n }\n }\n\n if (!tokens.issued_at && tokens.access_token) {\n return;\n }\n\n return tokens;\n }\n\n // -------------------------------------------------------------------------------------------------------------------\n\n /**\n * Get user information.\n */\n public readonly userinfo = $route({\n path: alephaServerAuthRoutes.userinfo,\n schema: {\n response: userinfoResponseSchema,\n },\n handler: async ({ user, headers, cookies }) => {\n const tokens = this.getTokens(cookies);\n if (tokens) {\n const provider = this.provider(tokens);\n if (!(\"realm\" in provider.options)) {\n const user = await provider.user(tokens);\n const api = await this.serverLinksProvider.getUserApiLinks({\n authorization: headers.authorization,\n user,\n });\n return {\n api,\n user,\n };\n }\n }\n\n const api = await this.serverLinksProvider.getUserApiLinks({\n authorization: headers.authorization,\n user,\n });\n\n return {\n api,\n user,\n };\n },\n });\n\n /**\n * Refresh a token for internal providers.\n */\n public readonly refresh = $route({\n path: alephaServerAuthRoutes.refresh,\n method: \"POST\",\n schema: {\n query: t.object({\n provider: t.text(),\n }),\n body: t.object({\n refresh_token: t.text({\n size: \"rich\",\n }),\n access_token: t.optional(\n t.text({\n size: \"rich\",\n description:\n \"Required if provider has stateless refresh token on credentials mode\",\n }),\n ),\n }),\n response: tokensSchema,\n },\n handler: async ({ query, body, cookies }) => {\n const provider = this.provider(query);\n\n const tokens = {\n provider: query.provider,\n ...(await provider.refresh(body.refresh_token, body.access_token)),\n };\n\n // for web applications, we store tokens in cookies\n this.setTokens(tokens, cookies);\n\n return tokens;\n },\n });\n\n /**\n * Login for local password-based authentication.\n */\n public readonly token = $route({\n path: alephaServerAuthRoutes.token,\n method: \"POST\",\n schema: {\n query: t.object({\n provider: t.text(),\n realm: t.optional(\n t.text({ description: \"Realm name for multi-realm setups\" }),\n ),\n }),\n body: t.object({\n username: t.text(),\n password: t.text(),\n }),\n response: tokenResponseSchema,\n },\n handler: async ({ query, body, cookies }) => {\n const provider = this.provider({\n provider: query.provider,\n realm: query.realm,\n });\n\n const realm = \"realm\" in provider.options && provider.options.realm;\n if (!realm) {\n throw new SecurityError(\n `Auth provider '${query.provider}' does not support password grant`,\n );\n }\n\n const credentials =\n \"credentials\" in provider.options && provider.options.credentials;\n\n if (!credentials) {\n throw new SecurityError(\n `Auth provider '${query.provider}' does not support password grant`,\n );\n }\n\n let user: UserAccount | undefined;\n try {\n user = await credentials.account(body);\n } catch (e) {\n if (e instanceof InvalidCredentialsError) {\n throw e;\n }\n this.log.error(\"Failed to authenticate user\", e);\n throw new InvalidCredentialsError();\n }\n\n if (!user) {\n throw new InvalidCredentialsError();\n }\n\n const tokens = {\n provider: query.provider,\n ...(await realm.createToken(user)),\n };\n\n // for web applications, we store tokens in cookies\n this.setTokens(tokens, cookies);\n\n const api = await this.serverLinksProvider.getUserApiLinks({\n user,\n });\n\n // mobile apps require this\n return {\n ...tokens,\n user,\n api,\n };\n },\n });\n\n /**\n * Oauth2/OIDC login route.\n */\n public readonly login = $route({\n path: alephaServerAuthRoutes.login,\n schema: {\n query: t.object({\n provider: t.text(),\n realm: t.optional(\n t.text({ description: \"Realm name for multi-realm setups\" }),\n ),\n redirect_uri: t.optional(t.text({ size: \"rich\" })),\n }),\n },\n handler: async ({ query, url, reply }) => {\n const provider = this.provider({\n provider: query.provider,\n realm: query.realm,\n });\n const oauth = provider.oauth;\n if (!oauth) {\n throw new SecurityError(\n `Auth provider '${query.provider}' does not support OAuth2`,\n );\n }\n\n const scope = provider.scope;\n let redirect_uri =\n provider.redirect_uri || alephaServerAuthRoutes.callback;\n if (redirect_uri.startsWith(\"/\")) {\n redirect_uri = `${url.protocol}//${url.host}${redirect_uri}`;\n }\n\n const oidc = \"oidc\" in provider.options && provider.options.oidc;\n\n if (!oauth.serverMetadata().supportsPKCE()) {\n const state = randomState();\n const parameters: Record<string, string> = {\n redirect_uri,\n state,\n };\n\n if (oidc) {\n parameters.nonce = randomState();\n }\n\n if (scope) {\n parameters.scope = scope;\n }\n\n this.authorizationCode.set({\n state,\n nonce: parameters.nonce,\n redirectUri: query.redirect_uri ?? \"/\",\n provider: query.provider,\n realm: query.realm,\n });\n\n reply.redirect(buildAuthorizationUrl(oauth, parameters).toString());\n return;\n }\n\n const codeVerifier = randomPKCECodeVerifier();\n const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);\n\n const parameters: Record<string, string> = {\n redirect_uri,\n code_challenge: codeChallenge,\n code_challenge_method: \"S256\",\n };\n\n if (scope) {\n parameters.scope = scope;\n }\n\n this.authorizationCode.set({\n codeVerifier,\n redirectUri: query.redirect_uri ?? \"/\",\n provider: query.provider,\n realm: query.realm,\n });\n\n reply.redirect(buildAuthorizationUrl(oauth, parameters).toString());\n },\n });\n\n /**\n * Callback for OAuth2/OIDC providers.\n * It handles the authorization code flow and retrieves the access token.\n */\n public readonly callback = $route({\n path: alephaServerAuthRoutes.callback,\n handler: async ({ url, reply, cookies }) => {\n const authorizationCode = this.authorizationCode.get({ cookies });\n if (!authorizationCode) {\n throw new BadRequestError(\"Missing code verifier\");\n }\n\n const provider = this.provider(authorizationCode);\n const oauth = provider.oauth;\n if (!oauth) {\n throw new SecurityError(\n `Auth provider '${provider.name}' does not support OAuth2`,\n );\n }\n\n const redirectUri = authorizationCode.redirectUri ?? \"/\";\n\n const externalTokens = await authorizationCodeGrant(oauth, url, {\n pkceCodeVerifier: authorizationCode.codeVerifier,\n expectedState: authorizationCode.state,\n expectedNonce: authorizationCode.nonce,\n })\n .then((tokens) => ({\n issued_at: this.dateTimeProvider.now().unix(),\n provider: provider.name,\n ...tokens,\n }))\n .catch((e) => {\n this.log.error(\"Failed to get access token\", e);\n throw new SecurityError(\"Failed to get access token\", {\n cause: e,\n });\n });\n\n this.authorizationCode.del({ cookies });\n\n const realm = \"realm\" in provider.options && provider.options.realm;\n\n // external, full OIDC System (e.g. Keycloak, Auth0)\n if (!realm) {\n this.setTokens(externalTokens, cookies);\n reply.redirect(redirectUri);\n return;\n }\n\n // internal, we need to create our own tokens\n\n const user = await provider.user(externalTokens);\n const tokens = await realm.createToken(user);\n\n this.setTokens(\n {\n ...tokens,\n issued_at: this.dateTimeProvider.now().unix(),\n provider: provider.name,\n },\n cookies,\n );\n\n reply.redirect(redirectUri);\n },\n });\n\n /**\n * Logout route for OAuth2/OIDC providers.\n */\n public readonly logout = $route({\n path: alephaServerAuthRoutes.logout,\n method: \"GET\",\n schema: {\n query: t.object({\n post_logout_redirect_uri: t.optional(t.text()),\n }),\n },\n handler: async ({ query, reply, cookies }) => {\n const redirect = query.post_logout_redirect_uri ?? \"/\";\n const tokens = this.getTokens(cookies);\n if (!tokens) {\n reply.redirect(redirect);\n return;\n }\n\n const provider = this.provider(tokens.provider);\n\n this.tokens.del({ cookies });\n\n // for internal providers, we can delete the session - if available\n if (\"realm\" in provider.options && tokens.refresh_token) {\n const onDeleteSession =\n provider.options.realm.options.settings?.onDeleteSession;\n if (onDeleteSession) {\n try {\n await onDeleteSession(tokens.refresh_token);\n } catch (e) {\n this.log.error(\"Failed to delete session\", e);\n }\n }\n }\n\n const oauth = provider.oauth;\n if (!oauth) {\n reply.redirect(redirect);\n return;\n }\n\n const params = new URLSearchParams();\n const idToken = tokens?.id_token;\n\n params.set(\"post_logout_redirect_uri\", redirect);\n if (idToken) {\n params.set(\"id_token_hint\", idToken);\n }\n\n const customLogoutUri =\n \"oidc\" in provider.options\n ? provider.options.oidc?.logoutUri\n : undefined;\n\n if (customLogoutUri) {\n reply.redirect(`${customLogoutUri}?${params}`);\n return;\n }\n\n if (!oauth.serverMetadata().end_session_endpoint) {\n // await tokenRevocation(\n // \toauth,\n // \ttokens?.refresh_token ?? tokens.access_token,\n // );\n reply.redirect(redirect);\n return;\n }\n\n reply.redirect(buildEndSessionUrl(oauth, params).toString());\n },\n });\n\n /**\n * Find an auth provider by name and optionally by realm.\n * When realm is specified, it filters providers by both name and realm.\n * This enables multi-realm setups where multiple providers share the same name (e.g., \"credentials\").\n */\n protected provider(\n opts: string | { provider: string; realm?: string },\n ): AuthPrimitive {\n const name = typeof opts === \"string\" ? opts : opts.provider;\n const realmName = typeof opts === \"string\" ? undefined : opts.realm;\n\n const identity = this.identities.find((identity) => {\n if (identity.name !== name) {\n return false;\n }\n\n // If realm filter is specified, match against provider's realm\n if (realmName && identity.realm?.name !== realmName) {\n return false;\n }\n\n return true;\n });\n\n if (!identity) {\n const realmInfo = realmName ? ` for realm '${realmName}'` : \"\";\n throw new SecurityError(`Auth provider '${name}'${realmInfo} not found`);\n }\n\n return identity;\n }\n\n protected getTokens(cookies?: Cookies): Tokens | undefined {\n return this.tokens.get({ cookies });\n }\n\n protected setTokens(tokens: Tokens, cookies?: Cookies): void {\n const exp =\n tokens.refresh_token_expires_in ||\n tokens.refresh_expires_in ||\n tokens.expires_in;\n\n const ttl = exp\n ? this.dateTimeProvider.duration(exp, \"seconds\")\n : undefined;\n\n this.tokens.set(tokens, {\n cookies,\n ttl,\n });\n }\n}\n\nexport interface OAuth2Profile {\n sub: string; // Subject - unique ID per user (required by OpenID)\n email?: string;\n name?: string;\n given_name?: string;\n family_name?: string;\n middle_name?: string;\n nickname?: string;\n preferred_username?: string;\n profile?: string;\n picture?: string;\n website?: string;\n email_verified?: boolean;\n gender?: string;\n birthdate?: string; // ISO 8601: YYYY-MM-DD\n zoneinfo?: string;\n locale?: string;\n phone_number?: string;\n phone_number_verified?: boolean;\n address?: {\n formatted?: string;\n street_address?: string;\n locality?: string;\n region?: string;\n postal_code?: string;\n country?: string;\n };\n updated_at?: number; // seconds since epoch\n // Allow additional fields (provider-specific)\n [key: string]: unknown;\n}\n","import { type Static, t } from \"alepha\";\n\nexport const authenticationProviderSchema = t.object(\n {\n name: t.text({\n description: \"Name of the authentication provider.\",\n }),\n type: t.enum([\"OAUTH2\", \"OIDC\", \"CREDENTIALS\"], {\n description: \"Type of the authentication provider.\",\n }),\n },\n {\n title: \"AuthenticationProvider\",\n },\n);\n\nexport type AuthenticationProvider = Static<\n typeof authenticationProviderSchema\n>;\n","import { AlephaError } from \"alepha\";\nimport type { RealmPrimitive } from \"alepha/security\";\nimport {\n $auth,\n type CredentialsFn,\n type CredentialsOptions,\n type WithLoginFn,\n} from \"./$auth.ts\";\n\n/**\n * Already configured Credentials authentication primitive.\n *\n * Uses username and password to authenticate users.\n */\nexport const $authCredentials = (\n realm: RealmPrimitive & WithLoginFn,\n options: Partial<CredentialsOptions> = {},\n) => {\n const name = \"credentials\";\n\n const account: CredentialsFn | undefined = realm.login\n ? realm.login(name)\n : options.account;\n\n if (!account) {\n throw new AlephaError(\n \"Credentials authentication requires a login function in the realm primitive.\",\n );\n }\n\n return $auth({\n realm,\n name,\n credentials: {\n account,\n },\n });\n};\n","import { $context, AlephaError, t } from \"alepha\";\nimport type { RealmPrimitive } from \"alepha/security\";\nimport type { OAuth2Profile } from \"../providers/ServerAuthProvider.ts\";\nimport {\n $auth,\n type LinkAccountFn,\n type OidcOptions,\n type WithLinkFn,\n} from \"./$auth.ts\";\n\n/**\n * Already configured GitHub authentication primitive.\n *\n * Uses OAuth2 to authenticate users via their GitHub accounts.\n * Upon successful authentication, it links the GitHub account to a user session.\n *\n * Environment Variables:\n * - `GITHUB_CLIENT_ID`: The client ID obtained from the GitHub Developer Settings.\n * - `GITHUB_CLIENT_SECRET`: The client secret obtained from the GitHub Developer Settings.\n */\nexport const $authGithub = (\n realm: RealmPrimitive & WithLinkFn,\n options: Partial<OidcOptions> = {},\n) => {\n const { alepha } = $context();\n\n const env = alepha.parseEnv(\n t.object({\n GITHUB_CLIENT_ID: t.optional(t.text()),\n GITHUB_CLIENT_SECRET: t.optional(t.text()),\n }),\n );\n\n const disabled = !env.GITHUB_CLIENT_ID || !env.GITHUB_CLIENT_SECRET;\n\n const name = \"github\";\n\n const account: LinkAccountFn | undefined =\n options.account ?? (realm.link ? realm.link(name) : undefined);\n\n if (!account) {\n throw new AlephaError(\n \"Authentication requires a link function in the realm primitive.\",\n );\n }\n\n return $auth({\n realm,\n name,\n oauth: {\n clientId: env.GITHUB_CLIENT_ID!,\n clientSecret: env.GITHUB_CLIENT_SECRET!,\n authorization: \"https://github.com/login/oauth/authorize\",\n token: \"https://github.com/login/oauth/access_token\",\n scope: \"read:user user:email\",\n userinfo: async (tokens) => {\n const BASE_URL = \"https://api.github.com\";\n const res = await fetch(`${BASE_URL}/user`, {\n headers: {\n Authorization: `Bearer ${tokens.access_token}`,\n \"User-Agent\": \"Alepha\",\n },\n }).then((res) => res.json());\n\n const user: OAuth2Profile = {\n sub: res.id.toString(),\n };\n\n if (res.email) {\n user.email = res.email;\n }\n\n if (res.name) {\n user.name = res.name.trim();\n }\n\n if (res.avatar_url) {\n user.picture = res.avatar_url;\n }\n\n if (!user.email) {\n const res = await fetch(`${BASE_URL}/user/emails`, {\n headers: {\n Authorization: `Bearer ${tokens.access_token}`,\n \"User-Agent\": \"Alepha\",\n },\n });\n if (res.ok) {\n const emails: any[] = await res.json();\n user.email = (emails.find((e) => e.primary) ?? emails[0]).email;\n }\n }\n\n return user;\n },\n ...options,\n account,\n },\n disabled,\n });\n};\n","import { $context, AlephaError, t } from \"alepha\";\nimport type { RealmPrimitive } from \"alepha/security\";\nimport {\n $auth,\n type LinkAccountFn,\n type OidcOptions,\n type WithLinkFn,\n} from \"./$auth.ts\";\n\n/**\n * Already configured Google authentication primitive.\n *\n * Uses OpenID Connect (OIDC) to authenticate users via their Google accounts.\n * Upon successful authentication, it links the Google account to a user session.\n *\n * Environment Variables:\n * - `GOOGLE_CLIENT_ID`: The client ID obtained from the Google Developer Console.\n * - `GOOGLE_CLIENT_SECRET`: The client secret obtained from the Google Developer Console.\n */\nexport const $authGoogle = (\n realm: RealmPrimitive & WithLinkFn,\n options: Partial<OidcOptions> = {},\n) => {\n const { alepha } = $context();\n\n const env = alepha.parseEnv(\n t.object({\n GOOGLE_CLIENT_ID: t.optional(t.text()),\n GOOGLE_CLIENT_SECRET: t.optional(t.text()),\n }),\n );\n\n const disabled = !env.GOOGLE_CLIENT_ID || !env.GOOGLE_CLIENT_SECRET;\n\n const name = \"google\";\n\n const account: LinkAccountFn | undefined =\n options.account ?? (realm.link ? realm.link(name) : undefined);\n\n if (!account) {\n throw new AlephaError(\n \"Authentication requires a link function in the realm primitive.\",\n );\n }\n\n return $auth({\n realm,\n name,\n oidc: {\n issuer: \"https://accounts.google.com\",\n clientId: env.GOOGLE_CLIENT_ID!,\n clientSecret: env.GOOGLE_CLIENT_SECRET,\n ...options,\n account,\n },\n disabled,\n });\n};\n","import { $module } from \"alepha\";\nimport type { UserAccount } from \"alepha/security\";\nimport { AlephaServerCookies } from \"alepha/server/cookies\";\nimport { $auth } from \"./primitives/$auth.ts\";\nimport { ServerAuthProvider } from \"./providers/ServerAuthProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./index.shared.ts\";\nexport * from \"./primitives/$auth.ts\";\nexport * from \"./primitives/$authCredentials.ts\";\nexport * from \"./primitives/$authGithub.ts\";\nexport * from \"./primitives/$authGoogle.ts\";\nexport * from \"./providers/ServerAuthProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha\" {\n export interface State {\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\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Allow authentication services for server applications.\n * It provides login and logout functionalities.\n *\n * There are multiple authentication providers available (e.g., Google, GitHub).\n * You can also delegate authentication to your own OIDC/OAuth2, for example using Keycloak or Auth0.\n *\n * It's cookie-based and SSR friendly.\n *\n * @see {@link $auth}\n * @see {@link ServerAuthProvider}\n * @module alepha.server.auth\n */\nexport const AlephaServerAuth = $module({\n name: \"alepha.server.auth\",\n primitives: [$auth],\n services: [AlephaServerCookies, ServerAuthProvider],\n});\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["directives: string[]","params: string[]"],"sources":["../../../src/server/cache/providers/ServerCacheProvider.ts","../../../src/server/cache/index.ts"],"sourcesContent":["import type { BinaryLike } from \"node:crypto\";\nimport { createHash } from \"node:crypto\";\nimport { $hook, $inject, Alepha } from \"alepha\";\nimport { $cache, type CachePrimitiveOptions } from \"alepha/cache\";\nimport { DateTimeProvider, type DurationLike } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport {\n ActionPrimitive,\n type RequestConfigSchema,\n type ServerRequest,\n type ServerRoute,\n} from \"alepha/server\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha/server\" {\n interface ServerRoute {\n /**\n * Enable caching for this route.\n * - If true: enables both store and etag\n * - If object: fine-grained control over store, etag, and cache-control headers\n *\n * @default false\n */\n cache?: ServerRouteCache;\n }\n\n interface ActionPrimitive<TConfig extends RequestConfigSchema> {\n invalidate: () => Promise<void>;\n }\n}\n\nActionPrimitive.prototype.invalidate = async function (\n this: ActionPrimitive<RequestConfigSchema>,\n) {\n await this.alepha.inject(ServerCacheProvider).invalidate(this.route);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class ServerCacheProvider {\n protected readonly log = $logger();\n protected readonly alepha = $inject(Alepha);\n protected readonly time = $inject(DateTimeProvider);\n protected readonly cache = $cache<RouteCacheEntry>({\n provider: \"memory\",\n });\n\n public generateETag(content: BinaryLike): string {\n return `\"${createHash(\"md5\").update(content).digest(\"hex\")}\"`;\n }\n\n public async invalidate(route: ServerRoute) {\n const cache = route.cache;\n if (!cache) {\n return;\n }\n\n await this.cache.invalidate(this.createCacheKey(route));\n }\n\n protected readonly onActionRequest = $hook({\n on: \"action:onRequest\",\n handler: async ({ action, request }) => {\n const cache = action.route.cache;\n\n const shouldStore = this.shouldStore(cache);\n\n // Only check cache if storing is enabled\n if (shouldStore) {\n const key = this.createCacheKey(action.route, request);\n const cached = await this.cache.get(key);\n\n if (cached) {\n const body =\n cached.contentType === \"application/json\"\n ? JSON.parse(cached.body)\n : cached.body;\n\n this.log.trace(\"Cache hit for action\", {\n key,\n action: action.name,\n });\n\n request.reply.body = body; // just re-use, full trust\n } else {\n this.log.trace(\"Cache miss for action\", {\n key,\n action: action.name,\n });\n }\n }\n },\n });\n\n protected readonly onActionResponse = $hook({\n on: \"action:onResponse\",\n handler: async ({ action, request, response }) => {\n const cache = action.route.cache;\n\n const shouldStore = this.shouldStore(cache);\n\n if (!shouldStore || !response) {\n return;\n }\n\n // Don't cache error responses (status >= 400)\n if (request.reply.status && request.reply.status >= 400) {\n return;\n }\n\n // TODO: serialize the response body, exactly like in the server response hook\n // this is bad\n const contentType =\n typeof response === \"string\" ? \"text/plain\" : \"application/json\";\n const body =\n contentType === \"text/plain\" ? response : JSON.stringify(response);\n\n const generatedEtag = this.generateETag(body);\n const lastModified = this.time.toISOString();\n\n // Store response for cached actions\n const key = this.createCacheKey(action.route, request);\n\n this.log.trace(\"Storing response\", {\n key,\n action: action.name,\n });\n\n await this.cache.set(key, {\n body: body,\n lastModified,\n contentType: contentType,\n hash: generatedEtag,\n });\n\n // Set Cache-Control header if configured (for HTTP responses)\n const cacheControl = this.buildCacheControlHeader(cache);\n if (cacheControl) {\n request.reply.setHeader(\"cache-control\", cacheControl);\n }\n },\n });\n\n protected readonly onRequest = $hook({\n on: \"server:onRequest\",\n handler: async ({ route, request }) => {\n const cache = route.cache;\n\n const shouldStore = this.shouldStore(cache);\n const shouldUseEtag = this.shouldUseEtag(cache);\n\n // Check for cached response or ETag\n if (!shouldStore && !shouldUseEtag) {\n return;\n }\n\n const key = this.createCacheKey(route, request);\n const cached = await this.cache.get(key);\n\n if (cached) {\n // Check if client has matching ETag - return 304 for both cached and etag-only routes\n if (\n request.headers[\"if-none-match\"] === cached.hash ||\n request.headers[\"if-modified-since\"] === cached.lastModified\n ) {\n request.reply.status = 304;\n request.reply.setHeader(\"etag\", cached.hash);\n request.reply.setHeader(\"last-modified\", cached.lastModified);\n this.log.trace(\"ETag match, returning 304\", {\n route: route.path,\n etag: cached.hash,\n });\n return;\n }\n\n // Only serve cached content if storing is enabled (not for etag-only routes)\n if (shouldStore) {\n this.log.trace(\"Cache hit for route\", {\n key,\n route: route.path,\n });\n\n // if the cache is found, we can skip the request processing\n // and return the cached response\n request.reply.body = cached.body;\n request.reply.status = cached.status ?? 200;\n\n if (cached.contentType) {\n request.reply.setHeader(\"Content-Type\", cached.contentType);\n }\n\n request.reply.setHeader(\"etag\", cached.hash);\n request.reply.setHeader(\"last-modified\", cached.lastModified);\n }\n } else if (shouldStore) {\n this.log.trace(\"Cache miss for route\", {\n key,\n route: route.path,\n });\n }\n },\n });\n\n protected readonly onSend = $hook({\n on: \"server:onSend\",\n handler: async ({ route, request }) => {\n // before sending the response, check if the ETag matches\n // and if so, return a 304 Not Modified response\n // -> this is only relevant for etag-only routes, not cached routes <-\n const cache = route.cache;\n\n const shouldStore = this.shouldStore(cache);\n const shouldUseEtag = this.shouldUseEtag(cache);\n\n if (request.reply.headers.etag) {\n // ETag already set, skip\n return;\n }\n\n if (\n !shouldStore &&\n shouldUseEtag &&\n request.reply.body != null &&\n (typeof request.reply.body === \"string\" ||\n Buffer.isBuffer(request.reply.body))\n ) {\n const generatedEtag = this.generateETag(request.reply.body);\n\n if (request.headers[\"if-none-match\"] === generatedEtag) {\n request.reply.status = 304;\n request.reply.body = undefined;\n request.reply.setHeader(\"etag\", generatedEtag);\n this.log.trace(\"ETag match on send, returning 304\", {\n route: route.path,\n etag: generatedEtag,\n });\n return;\n }\n }\n },\n });\n\n protected readonly onResponse = $hook({\n on: \"server:onResponse\",\n priority: \"first\",\n handler: async ({ route, request, response }) => {\n const cache = route.cache;\n\n // Set Cache-Control header if configured\n const cacheControl = this.buildCacheControlHeader(cache);\n if (cacheControl) {\n response.headers[\"cache-control\"] = cacheControl;\n }\n\n const shouldStore = this.shouldStore(cache);\n const shouldUseEtag = this.shouldUseEtag(cache);\n\n // Skip if neither cache nor etag is enabled\n if (!shouldStore && !shouldUseEtag) {\n return;\n }\n\n // Only process string responses (text, html, json, etc.)\n // Buffer is not supported by alepha/cache for now\n if (typeof response.body !== \"string\") {\n return;\n }\n\n // Don't cache error responses (status >= 400)\n if (response.status && response.status >= 400) {\n return;\n }\n\n const key = this.createCacheKey(route, request);\n const generatedEtag = this.generateETag(response.body);\n const lastModified = this.time.toISOString();\n\n // Initialize headers if not present\n response.headers ??= {};\n\n // Store response if storing is enabled\n if (shouldStore) {\n this.log.trace(\"Storing response\", {\n key,\n route: route.path,\n cache: !!cache,\n etag: shouldUseEtag,\n });\n\n await this.cache.set(key, {\n body: response.body,\n status: response.status,\n contentType: response.headers?.[\"content-type\"],\n lastModified,\n hash: generatedEtag,\n });\n }\n\n // Set ETag headers if etag is enabled\n if (shouldUseEtag) {\n response.headers.etag = generatedEtag;\n response.headers[\"last-modified\"] = lastModified;\n }\n },\n });\n\n public buildCacheControlHeader(cache?: ServerRouteCache): string | undefined {\n if (!cache) {\n return undefined;\n }\n\n // If cache is true or a DurationLike, no Cache-Control header is set\n if (\n cache === true ||\n typeof cache === \"string\" ||\n typeof cache === \"number\"\n ) {\n return undefined;\n }\n\n const control = cache.control;\n if (!control) {\n return undefined;\n }\n\n // If control is a string, return it directly\n if (typeof control === \"string\") {\n return control;\n }\n\n // If control is true, return default Cache-Control\n if (control === true) {\n return \"public, max-age=300\";\n }\n\n // Build Cache-Control from object directives\n const directives: string[] = [];\n\n if (control.public) {\n directives.push(\"public\");\n }\n if (control.private) {\n directives.push(\"private\");\n }\n if (control.noCache) {\n directives.push(\"no-cache\");\n }\n if (control.noStore) {\n directives.push(\"no-store\");\n }\n if (control.maxAge !== undefined) {\n const maxAgeSeconds = this.durationToSeconds(control.maxAge);\n directives.push(`max-age=${maxAgeSeconds}`);\n }\n if (control.sMaxAge !== undefined) {\n const sMaxAgeSeconds = this.durationToSeconds(control.sMaxAge);\n directives.push(`s-maxage=${sMaxAgeSeconds}`);\n }\n if (control.mustRevalidate) {\n directives.push(\"must-revalidate\");\n }\n if (control.proxyRevalidate) {\n directives.push(\"proxy-revalidate\");\n }\n if (control.immutable) {\n directives.push(\"immutable\");\n }\n\n return directives.length > 0 ? directives.join(\", \") : undefined;\n }\n\n protected durationToSeconds(duration: number | DurationLike): number {\n if (typeof duration === \"number\") {\n return duration;\n }\n\n return this.time.duration(duration).asSeconds();\n }\n\n protected shouldStore(cache?: ServerRouteCache): boolean {\n if (!cache) return false;\n if (cache === true) return true;\n if (typeof cache === \"object\" && cache.store) return true;\n return false;\n }\n\n protected shouldUseEtag(cache?: ServerRouteCache): boolean {\n // cache: true enables etag\n if (cache === true) return true;\n // Check object form\n if (typeof cache === \"object\" && cache.etag) return true;\n return false;\n }\n\n protected createCacheKey(route: ServerRoute, config?: ServerRequest): string {\n const params: string[] = [];\n for (const [key, value] of Object.entries(config?.params ?? {})) {\n params.push(`${key}=${value}`);\n }\n for (const [key, value] of Object.entries(config?.query ?? {})) {\n params.push(`${key}=${value}`);\n }\n\n return `${route.method}:${route.path.replaceAll(\":\", \"\")}:${params.join(\",\").replaceAll(\":\", \"\")}`;\n }\n}\n\nexport type ServerRouteCache =\n /**\n * If true, enables caching with:\n * - store: true\n * - etag: true\n */\n | boolean\n /**\n * Object configuration for fine-grained cache control.\n *\n * If empty, no caching will be applied.\n */\n | {\n /**\n * If true, enables storing cached responses. (in-memory, Redis, @see alepha/cache for other providers)\n * If a DurationLike is provided, it will be used as the TTL for the cache.\n * If CachePrimitiveOptions is provided, it will be used to configure the cache storage.\n *\n * @default false\n */\n store?: true | DurationLike | CachePrimitiveOptions;\n /**\n * If true, enables ETag support for the cached responses.\n */\n etag?: true;\n /**\n * - If true, sets Cache-Control to \"public, max-age=300\" (5 minutes).\n * - If string, sets Cache-Control to the provided value directly.\n * - If object, configures Cache-Control directives.\n */\n control?: /**\n * If true, sets Cache-Control to \"public, max-age=300\" (5 minutes).\n */\n | true\n /**\n * If string, sets Cache-Control to the provided value directly.\n */\n | string\n /**\n * If object, configures Cache-Control directives.\n */\n | {\n /**\n * Indicates that the response may be cached by any cache.\n */\n public?: boolean;\n /**\n * Indicates that the response is intended for a single user and must not be stored by a shared cache.\n */\n private?: boolean;\n /**\n * Forces caches to submit the request to the origin server for validation before releasing a cached copy.\n */\n noCache?: boolean;\n /**\n * Instructs caches not to store the response.\n */\n noStore?: boolean;\n /**\n * Maximum amount of time a resource is considered fresh.\n * Can be specified as a number (seconds) or as a DurationLike object.\n *\n * @example 300 // 5 minutes in seconds\n * @example { minutes: 5 } // 5 minutes\n * @example { hours: 1 } // 1 hour\n */\n maxAge?: number | DurationLike;\n /**\n * Overrides max-age for shared caches (e.g., CDNs).\n * Can be specified as a number (seconds) or as a DurationLike object.\n */\n sMaxAge?: number | DurationLike;\n /**\n * Indicates that once a resource becomes stale, caches must not use it without successful validation.\n */\n mustRevalidate?: boolean;\n /**\n * Similar to must-revalidate, but only for shared caches.\n */\n proxyRevalidate?: boolean;\n /**\n * Indicates that the response can be stored but must be revalidated before each use.\n */\n immutable?: boolean;\n };\n };\n\ninterface RouteCacheEntry {\n contentType?: string;\n body: any;\n status?: number;\n lastModified: string;\n hash: string;\n}\n","import { $module } from \"alepha\";\nimport { AlephaCache } from \"alepha/cache\";\nimport { ServerCacheProvider } from \"./providers/ServerCacheProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./providers/ServerCacheProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Plugin for Alepha Server that provides server-side caching capabilities.\n * It uses the Alepha Cache module to cache responses from server actions ($action).\n * It also provides a ETag-based cache invalidation mechanism.\n *\n * @example\n * ```ts\n * import { Alepha } from \"alepha\";\n * import { $action } from \"alepha/server\";\n * import { AlephaServerCache } from \"alepha/server/cache\";\n *\n * class ApiServer {\n * hello = $action({\n * cache: true,\n * handler: () => \"Hello, World!\",\n * });\n * }\n *\n * const alepha = Alepha.create()\n * .with(AlephaServerCache)\n * .with(ApiServer);\n *\n * run(alepha);\n * ```\n *\n * @see {@link ServerCacheProvider}\n * @module alepha.server.cache\n */\nexport const AlephaServerCache = $module({\n name: \"alepha.server.cache\",\n services: [AlephaCache, ServerCacheProvider],\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/server/cache/providers/ServerCacheProvider.ts","../../../src/server/cache/index.ts"],"sourcesContent":["import type { BinaryLike } from \"node:crypto\";\nimport { createHash } from \"node:crypto\";\nimport { $hook, $inject, Alepha } from \"alepha\";\nimport { $cache, type CachePrimitiveOptions } from \"alepha/cache\";\nimport { DateTimeProvider, type DurationLike } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport {\n ActionPrimitive,\n type RequestConfigSchema,\n type ServerRequest,\n type ServerRoute,\n} from \"alepha/server\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha/server\" {\n interface ServerRoute {\n /**\n * Enable caching for this route.\n * - If true: enables both store and etag\n * - If object: fine-grained control over store, etag, and cache-control headers\n *\n * @default false\n */\n cache?: ServerRouteCache;\n }\n\n interface ActionPrimitive<TConfig extends RequestConfigSchema> {\n invalidate: () => Promise<void>;\n }\n}\n\nActionPrimitive.prototype.invalidate = async function (\n this: ActionPrimitive<RequestConfigSchema>,\n) {\n await this.alepha.inject(ServerCacheProvider).invalidate(this.route);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class ServerCacheProvider {\n protected readonly log = $logger();\n protected readonly alepha = $inject(Alepha);\n protected readonly time = $inject(DateTimeProvider);\n protected readonly cache = $cache<RouteCacheEntry>({\n provider: \"memory\",\n });\n\n public generateETag(content: BinaryLike): string {\n return `\"${createHash(\"md5\").update(content).digest(\"hex\")}\"`;\n }\n\n public async invalidate(route: ServerRoute) {\n const cache = route.cache;\n if (!cache) {\n return;\n }\n\n await this.cache.invalidate(this.createCacheKey(route));\n }\n\n protected readonly onActionRequest = $hook({\n on: \"action:onRequest\",\n handler: async ({ action, request }) => {\n const cache = action.route.cache;\n\n const shouldStore = this.shouldStore(cache);\n\n // Only check cache if storing is enabled\n if (shouldStore) {\n const key = this.createCacheKey(action.route, request);\n const cached = await this.cache.get(key);\n\n if (cached) {\n const body =\n cached.contentType === \"application/json\"\n ? JSON.parse(cached.body)\n : cached.body;\n\n this.log.trace(\"Cache hit for action\", {\n key,\n action: action.name,\n });\n\n request.reply.body = body; // just re-use, full trust\n } else {\n this.log.trace(\"Cache miss for action\", {\n key,\n action: action.name,\n });\n }\n }\n },\n });\n\n protected readonly onActionResponse = $hook({\n on: \"action:onResponse\",\n handler: async ({ action, request, response }) => {\n const cache = action.route.cache;\n\n const shouldStore = this.shouldStore(cache);\n\n if (!shouldStore || !response) {\n return;\n }\n\n // Don't cache error responses (status >= 400)\n if (request.reply.status && request.reply.status >= 400) {\n return;\n }\n\n // TODO: serialize the response body, exactly like in the server response hook\n // this is bad\n const contentType =\n typeof response === \"string\" ? \"text/plain\" : \"application/json\";\n const body =\n contentType === \"text/plain\" ? response : JSON.stringify(response);\n\n const generatedEtag = this.generateETag(body);\n const lastModified = this.time.toISOString();\n\n // Store response for cached actions\n const key = this.createCacheKey(action.route, request);\n\n this.log.trace(\"Storing response\", {\n key,\n action: action.name,\n });\n\n await this.cache.set(key, {\n body: body,\n lastModified,\n contentType: contentType,\n hash: generatedEtag,\n });\n\n // Set Cache-Control header if configured (for HTTP responses)\n const cacheControl = this.buildCacheControlHeader(cache);\n if (cacheControl) {\n request.reply.setHeader(\"cache-control\", cacheControl);\n }\n },\n });\n\n protected readonly onRequest = $hook({\n on: \"server:onRequest\",\n handler: async ({ route, request }) => {\n const cache = route.cache;\n\n const shouldStore = this.shouldStore(cache);\n const shouldUseEtag = this.shouldUseEtag(cache);\n\n // Check for cached response or ETag\n if (!shouldStore && !shouldUseEtag) {\n return;\n }\n\n const key = this.createCacheKey(route, request);\n const cached = await this.cache.get(key);\n\n if (cached) {\n // Check if client has matching ETag - return 304 for both cached and etag-only routes\n if (\n request.headers[\"if-none-match\"] === cached.hash ||\n request.headers[\"if-modified-since\"] === cached.lastModified\n ) {\n request.reply.status = 304;\n request.reply.setHeader(\"etag\", cached.hash);\n request.reply.setHeader(\"last-modified\", cached.lastModified);\n this.log.trace(\"ETag match, returning 304\", {\n route: route.path,\n etag: cached.hash,\n });\n return;\n }\n\n // Only serve cached content if storing is enabled (not for etag-only routes)\n if (shouldStore) {\n this.log.trace(\"Cache hit for route\", {\n key,\n route: route.path,\n });\n\n // if the cache is found, we can skip the request processing\n // and return the cached response\n request.reply.body = cached.body;\n request.reply.status = cached.status ?? 200;\n\n if (cached.contentType) {\n request.reply.setHeader(\"Content-Type\", cached.contentType);\n }\n\n request.reply.setHeader(\"etag\", cached.hash);\n request.reply.setHeader(\"last-modified\", cached.lastModified);\n }\n } else if (shouldStore) {\n this.log.trace(\"Cache miss for route\", {\n key,\n route: route.path,\n });\n }\n },\n });\n\n protected readonly onSend = $hook({\n on: \"server:onSend\",\n handler: async ({ route, request }) => {\n // before sending the response, check if the ETag matches\n // and if so, return a 304 Not Modified response\n // -> this is only relevant for etag-only routes, not cached routes <-\n const cache = route.cache;\n\n const shouldStore = this.shouldStore(cache);\n const shouldUseEtag = this.shouldUseEtag(cache);\n\n if (request.reply.headers.etag) {\n // ETag already set, skip\n return;\n }\n\n if (\n !shouldStore &&\n shouldUseEtag &&\n request.reply.body != null &&\n (typeof request.reply.body === \"string\" ||\n Buffer.isBuffer(request.reply.body))\n ) {\n const generatedEtag = this.generateETag(request.reply.body);\n\n if (request.headers[\"if-none-match\"] === generatedEtag) {\n request.reply.status = 304;\n request.reply.body = undefined;\n request.reply.setHeader(\"etag\", generatedEtag);\n this.log.trace(\"ETag match on send, returning 304\", {\n route: route.path,\n etag: generatedEtag,\n });\n return;\n }\n }\n },\n });\n\n protected readonly onResponse = $hook({\n on: \"server:onResponse\",\n priority: \"first\",\n handler: async ({ route, request, response }) => {\n const cache = route.cache;\n\n // Set Cache-Control header if configured\n const cacheControl = this.buildCacheControlHeader(cache);\n if (cacheControl) {\n response.headers[\"cache-control\"] = cacheControl;\n }\n\n const shouldStore = this.shouldStore(cache);\n const shouldUseEtag = this.shouldUseEtag(cache);\n\n // Skip if neither cache nor etag is enabled\n if (!shouldStore && !shouldUseEtag) {\n return;\n }\n\n // Only process string responses (text, html, json, etc.)\n // Buffer is not supported by alepha/cache for now\n if (typeof response.body !== \"string\") {\n return;\n }\n\n // Don't cache error responses (status >= 400)\n if (response.status && response.status >= 400) {\n return;\n }\n\n const key = this.createCacheKey(route, request);\n const generatedEtag = this.generateETag(response.body);\n const lastModified = this.time.toISOString();\n\n // Initialize headers if not present\n response.headers ??= {};\n\n // Store response if storing is enabled\n if (shouldStore) {\n this.log.trace(\"Storing response\", {\n key,\n route: route.path,\n cache: !!cache,\n etag: shouldUseEtag,\n });\n\n await this.cache.set(key, {\n body: response.body,\n status: response.status,\n contentType: response.headers?.[\"content-type\"],\n lastModified,\n hash: generatedEtag,\n });\n }\n\n // Set ETag headers if etag is enabled\n if (shouldUseEtag) {\n response.headers.etag = generatedEtag;\n response.headers[\"last-modified\"] = lastModified;\n }\n },\n });\n\n public buildCacheControlHeader(cache?: ServerRouteCache): string | undefined {\n if (!cache) {\n return undefined;\n }\n\n // If cache is true or a DurationLike, no Cache-Control header is set\n if (\n cache === true ||\n typeof cache === \"string\" ||\n typeof cache === \"number\"\n ) {\n return undefined;\n }\n\n const control = cache.control;\n if (!control) {\n return undefined;\n }\n\n // If control is a string, return it directly\n if (typeof control === \"string\") {\n return control;\n }\n\n // If control is true, return default Cache-Control\n if (control === true) {\n return \"public, max-age=300\";\n }\n\n // Build Cache-Control from object directives\n const directives: string[] = [];\n\n if (control.public) {\n directives.push(\"public\");\n }\n if (control.private) {\n directives.push(\"private\");\n }\n if (control.noCache) {\n directives.push(\"no-cache\");\n }\n if (control.noStore) {\n directives.push(\"no-store\");\n }\n if (control.maxAge !== undefined) {\n const maxAgeSeconds = this.durationToSeconds(control.maxAge);\n directives.push(`max-age=${maxAgeSeconds}`);\n }\n if (control.sMaxAge !== undefined) {\n const sMaxAgeSeconds = this.durationToSeconds(control.sMaxAge);\n directives.push(`s-maxage=${sMaxAgeSeconds}`);\n }\n if (control.mustRevalidate) {\n directives.push(\"must-revalidate\");\n }\n if (control.proxyRevalidate) {\n directives.push(\"proxy-revalidate\");\n }\n if (control.immutable) {\n directives.push(\"immutable\");\n }\n\n return directives.length > 0 ? directives.join(\", \") : undefined;\n }\n\n protected durationToSeconds(duration: number | DurationLike): number {\n if (typeof duration === \"number\") {\n return duration;\n }\n\n return this.time.duration(duration).asSeconds();\n }\n\n protected shouldStore(cache?: ServerRouteCache): boolean {\n if (!cache) return false;\n if (cache === true) return true;\n if (typeof cache === \"object\" && cache.store) return true;\n return false;\n }\n\n protected shouldUseEtag(cache?: ServerRouteCache): boolean {\n // cache: true enables etag\n if (cache === true) return true;\n // Check object form\n if (typeof cache === \"object\" && cache.etag) return true;\n return false;\n }\n\n protected createCacheKey(route: ServerRoute, config?: ServerRequest): string {\n const params: string[] = [];\n for (const [key, value] of Object.entries(config?.params ?? {})) {\n params.push(`${key}=${value}`);\n }\n for (const [key, value] of Object.entries(config?.query ?? {})) {\n params.push(`${key}=${value}`);\n }\n\n return `${route.method}:${route.path.replaceAll(\":\", \"\")}:${params.join(\",\").replaceAll(\":\", \"\")}`;\n }\n}\n\nexport type ServerRouteCache =\n /**\n * If true, enables caching with:\n * - store: true\n * - etag: true\n */\n | boolean\n /**\n * Object configuration for fine-grained cache control.\n *\n * If empty, no caching will be applied.\n */\n | {\n /**\n * If true, enables storing cached responses. (in-memory, Redis, @see alepha/cache for other providers)\n * If a DurationLike is provided, it will be used as the TTL for the cache.\n * If CachePrimitiveOptions is provided, it will be used to configure the cache storage.\n *\n * @default false\n */\n store?: true | DurationLike | CachePrimitiveOptions;\n /**\n * If true, enables ETag support for the cached responses.\n */\n etag?: true;\n /**\n * - If true, sets Cache-Control to \"public, max-age=300\" (5 minutes).\n * - If string, sets Cache-Control to the provided value directly.\n * - If object, configures Cache-Control directives.\n */\n control?: /**\n * If true, sets Cache-Control to \"public, max-age=300\" (5 minutes).\n */\n | true\n /**\n * If string, sets Cache-Control to the provided value directly.\n */\n | string\n /**\n * If object, configures Cache-Control directives.\n */\n | {\n /**\n * Indicates that the response may be cached by any cache.\n */\n public?: boolean;\n /**\n * Indicates that the response is intended for a single user and must not be stored by a shared cache.\n */\n private?: boolean;\n /**\n * Forces caches to submit the request to the origin server for validation before releasing a cached copy.\n */\n noCache?: boolean;\n /**\n * Instructs caches not to store the response.\n */\n noStore?: boolean;\n /**\n * Maximum amount of time a resource is considered fresh.\n * Can be specified as a number (seconds) or as a DurationLike object.\n *\n * @example 300 // 5 minutes in seconds\n * @example { minutes: 5 } // 5 minutes\n * @example { hours: 1 } // 1 hour\n */\n maxAge?: number | DurationLike;\n /**\n * Overrides max-age for shared caches (e.g., CDNs).\n * Can be specified as a number (seconds) or as a DurationLike object.\n */\n sMaxAge?: number | DurationLike;\n /**\n * Indicates that once a resource becomes stale, caches must not use it without successful validation.\n */\n mustRevalidate?: boolean;\n /**\n * Similar to must-revalidate, but only for shared caches.\n */\n proxyRevalidate?: boolean;\n /**\n * Indicates that the response can be stored but must be revalidated before each use.\n */\n immutable?: boolean;\n };\n };\n\ninterface RouteCacheEntry {\n contentType?: string;\n body: any;\n status?: number;\n lastModified: string;\n hash: string;\n}\n","import { $module } from \"alepha\";\nimport { AlephaCache } from \"alepha/cache\";\nimport { ServerCacheProvider } from \"./providers/ServerCacheProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./providers/ServerCacheProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Plugin for Alepha Server that provides server-side caching capabilities.\n * It uses the Alepha Cache module to cache responses from server actions ($action).\n * It also provides a ETag-based cache invalidation mechanism.\n *\n * @example\n * ```ts\n * import { Alepha } from \"alepha\";\n * import { $action } from \"alepha/server\";\n * import { AlephaServerCache } from \"alepha/server/cache\";\n *\n * class ApiServer {\n * hello = $action({\n * cache: true,\n * handler: () => \"Hello, World!\",\n * });\n * }\n *\n * const alepha = Alepha.create()\n * .with(AlephaServerCache)\n * .with(ApiServer);\n *\n * run(alepha);\n * ```\n *\n * @see {@link ServerCacheProvider}\n * @module alepha.server.cache\n */\nexport const AlephaServerCache = $module({\n name: \"alepha.server.cache\",\n services: [AlephaCache, ServerCacheProvider],\n});\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/server/compress/providers/ServerCompressProvider.ts","../../../src/server/compress/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;uCAgBuC;EAXa;;AAAA,cAevC,sBAAA,CAJuD;EAAA,OAAA,WAAA,EAK9C,MAL8C,CAAA,MAAA,EAAA;IAAA,QAAA,EAAA,CAAA,GAAA,IAAA,EAAA,GAAA,EAAA,EAAA,GAQ9B,OAR8B,CAQtB,MARsB,CAAA;IAIvD,MAAA,EAAA,CAAA,OAAsB,CAAtB,EAAA,GAAsB,EAAA,GAKA,SALA;EAIW,CAAA,GAAA,SAAA,CAAA;EAAR,mBAAA,MAAA,EAsBX,MAtBW;EACH,cAAA,OAAA,CAAA,CAAA,EAuBR,6BAvBQ;EAJb,SAAA,UAAA,EA2BkC,OAAA,CAa5B,aAxCN,CAAA,mBAAA,CAAA;EAyBK,UAAA,oBAAA,CAAA,WAAA,EAAA,MAAA,GAAA,SAAA,CAAA,EAAA,OAAA;EAEA,UAAA,QAAA,CAAA,QAAA,EAAA,MAAA,OAwDA,sBAAA,CAAuB,WAxDvB,EAAA,QAAA,EAyDb,cAzDa,CAAA,EA0DtB,OA1DsB,CAAA,IAAA,CAAA;EAA6B,UAa5B,SAAA,CAAA,QAAA,EAAA,MAAA,OAkFD,sBAAA,CAAuB,WAlFtB,CAAA,EAmFvB,MAnFuB,CAAA,MAAA,EAAA,GAAA,CAAA;EA2CD,UAAA,UAAuB,CAAA,QAAA,EAwDpC,cAxDoC,EAAA,QAAA,EAAA,MAAA,OAyDvB,sBAAA,CAAuB,WAzDA,CAAA,EAAA,IAAA;;AAE7C,UA+DY,6BAAA,CA/DZ;EAqCsB,mBAAA,EAAuB,MAAA,EAAA;;;;;;;;;AA1IE,cCUvC,oBDVuC,ECUnB,OAAA,CAAA,ODVmB,CCalD,OAAA,CAH+B,MAAA,CDVmB"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/server/compress/providers/ServerCompressProvider.ts","../../../src/server/compress/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;uCAgBuC;EAXa;;AAAA,cAevC,sBAAA,CAJuD;EAAA,OAAA,WAAA,EAK9C,MAL8C,CAAA,MAAA,EAAA;IAAA,QAAA,EAAA,CAAA,GAAA,IAAA,EAAA,GAAA,EAAA,EAAA,GAQ9B,OAR8B,CAQtB,MARsB,CAAA;IAIvD,MAAA,EAAA,CAAA,OAAsB,CAAtB,EAAA,GAAsB,EAAA,GAKA,SALA;EAIW,CAAA,GAAA,SAAA,CAAA;EAAR,mBAAA,MAAA,EAsBX,MAtBW;EACH,cAAA,OAAA,CAAA,CAAA,EAuBR,6BAvBQ;EAJb,SAAA,UAAA,EA2BkC,OAAA,CAa5B,aAxCN,CAAA,mBAAA,CAAA;EAyBK,UAAA,oBAAA,CAAA,WAAA,EAAA,MAAA,GAAA,SAAA,CAAA,EAAA,OAAA;EAEA,UAAA,QAAA,CAAA,QAAA,EAAA,MAAA,OAwDA,sBAAA,CAAuB,WAxDvB,EAAA,QAAA,EAyDb,cAzDa,CAAA,EA0DtB,OA1DsB,CAAA,IAAA,CAAA;EAA6B,UAa5B,SAAA,CAAA,QAAA,EAAA,MAAA,OAoFD,sBAAA,CAAuB,WApFtB,CAAA,EAqFvB,MArFuB,CAAA,MAAA,EAAA,GAAA,CAAA;EA2CD,UAAA,UAAuB,CAAA,QAAA,EA0DpC,cA1DoC,EAAA,QAAA,EAAA,MAAA,OA2DvB,sBAAA,CAAuB,WA3DA,CAAA,EAAA,IAAA;;AAE7C,UAiEY,6BAAA,CAjEZ;EAuCsB,mBAAA,EAAuB,MAAA,EAAA;;;;;;;;;AA5IE,cCUvC,oBDVuC,ECUnB,OAAA,CAAA,ODVmB,CCalD,OAAA,CAH+B,MAAA,CDVmB"}
@@ -72,10 +72,12 @@ var ServerCompressProvider = class ServerCompressProvider {
72
72
  this.setHeaders(response, encoding);
73
73
  response.headers["content-length"] = compressed.length.toString();
74
74
  response.body = compressed;
75
+ return;
75
76
  }
76
77
  if (typeof body === "object" && body instanceof Readable) {
77
78
  this.setHeaders(response, encoding);
78
79
  response.body = body.pipe(compressor.stream({ params }));
80
+ return;
79
81
  }
80
82
  if (typeof body === "object" && body instanceof ReadableStream) {
81
83
  this.setHeaders(response, encoding);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/server/compress/providers/ServerCompressProvider.ts","../../../src/server/compress/index.ts"],"sourcesContent":["import { Readable, type Transform } from \"node:stream\";\nimport { ReadableStream } from \"node:stream/web\";\nimport { promisify } from \"node:util\";\nimport * as zlib from \"node:zlib\";\nimport { $hook, $inject, Alepha } from \"alepha\";\nimport type { ServerResponse } from \"alepha/server\";\n\nconst gzip = promisify(zlib.gzip);\nconst createGzip = zlib.createGzip;\nconst brotli = promisify(zlib.brotliCompress);\nconst createBrotliCompress = zlib.createBrotliCompress;\nconst zstd = zlib.zstdCompress ? promisify(zlib.zstdCompress) : undefined;\nconst createZstdCompress = zstd ? zlib.createZstdCompress : undefined;\n\ndeclare module \"alepha\" {\n interface State {\n \"alepha.server.compress.options\"?: ServerCompressProviderOptions;\n }\n}\n\nexport class ServerCompressProvider {\n static compressors: Record<\n string,\n | {\n compress: (...args: any[]) => Promise<Buffer>;\n stream: (options?: any) => Transform;\n }\n | undefined\n > = {\n gzip: {\n compress: gzip,\n stream: createGzip,\n },\n br: {\n compress: brotli,\n stream: createBrotliCompress,\n },\n zstd:\n zstd && createZstdCompress\n ? {\n compress: zstd,\n stream: createZstdCompress,\n }\n : undefined,\n };\n\n protected readonly alepha = $inject(Alepha);\n\n protected get options(): ServerCompressProviderOptions {\n return {\n allowedContentTypes: [\n \"application/json\",\n \"text/html\",\n \"application/javascript\",\n \"text/plain\",\n \"text/css\",\n ],\n ...this.alepha.store.get(\"alepha.server.compress.options\"),\n };\n }\n\n public readonly onResponse = $hook({\n on: \"server:onResponse\",\n handler: async ({ request, response }) => {\n // skip if already compressed\n if (response.headers[\"content-encoding\"]) {\n return;\n }\n\n const acceptEncoding = request.headers[\"accept-encoding\"]; // skip if no accept-encoding header\n if (!acceptEncoding) {\n return;\n }\n\n // skip if not json or html (for now)\n if (!this.isAllowedContentType(response.headers[\"content-type\"])) {\n return;\n }\n\n for (const encoding of [\"zstd\", \"br\", \"gzip\"] as const) {\n if (\n acceptEncoding.includes(encoding) &&\n ServerCompressProvider.compressors[encoding]\n ) {\n await this.compress(encoding, response);\n return;\n }\n }\n },\n });\n\n protected isAllowedContentType(contentType: string | undefined): boolean {\n if (!contentType) {\n return false;\n }\n\n const lowerContentType = contentType.toLowerCase();\n\n return !!this.options.allowedContentTypes.find((it) =>\n lowerContentType.includes(it),\n );\n }\n\n protected async compress(\n encoding: keyof typeof ServerCompressProvider.compressors,\n response: ServerResponse,\n ): Promise<void> {\n const body = response.body; // can be string or Buffer or ArrayBuffer or Readable\n\n const compressor = ServerCompressProvider.compressors[encoding];\n if (!compressor) {\n return;\n }\n\n const params = this.getParams(encoding);\n\n if (\n typeof body === \"string\" ||\n Buffer.isBuffer(body) ||\n body instanceof ArrayBuffer\n ) {\n const compressed = await compressor.compress(body, {\n params,\n });\n this.setHeaders(response, encoding);\n response.headers[\"content-length\"] = compressed.length.toString();\n response.body = compressed;\n }\n\n if (typeof body === \"object\" && body instanceof Readable) {\n this.setHeaders(response, encoding);\n response.body = body.pipe(compressor.stream({ params }));\n }\n\n if (typeof body === \"object\" && body instanceof ReadableStream) {\n this.setHeaders(response, encoding);\n response.body = Readable.fromWeb(body).pipe(\n compressor.stream({ params }),\n );\n }\n }\n\n protected getParams(\n encoding: keyof typeof ServerCompressProvider.compressors,\n ): Record<number, any> {\n if (encoding === \"zstd\") {\n return {\n [zlib.constants.ZSTD_c_compressionLevel]: 3, // default compression level for zstd\n };\n }\n if (encoding === \"br\") {\n return {};\n }\n if (encoding === \"gzip\") {\n return {};\n }\n return {};\n }\n\n protected setHeaders(\n response: ServerResponse,\n encoding: keyof typeof ServerCompressProvider.compressors,\n ): void {\n response.headers.vary = \"content-encoding\";\n response.headers[\"content-encoding\"] = encoding;\n response.headers[\"cache-control\"] = \"no-cache\";\n }\n}\n\nexport interface ServerCompressProviderOptions {\n allowedContentTypes: string[];\n}\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { ServerCompressProvider } from \"./providers/ServerCompressProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./providers/ServerCompressProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Plugin for Alepha Server that provides server-side compression capabilities.\n *\n * Compresses responses using gzip, brotli, or zstd based on the `Accept-Encoding` header.\n */\nexport const AlephaServerCompress = $module({\n name: \"alepha.server.compress\",\n services: [AlephaServer, ServerCompressProvider],\n});\n"],"mappings":";;;;;;;;AAOA,MAAM,OAAO,UAAU,KAAK,KAAK;AACjC,MAAM,aAAa,KAAK;AACxB,MAAM,SAAS,UAAU,KAAK,eAAe;AAC7C,MAAM,uBAAuB,KAAK;AAClC,MAAM,OAAO,KAAK,eAAe,UAAU,KAAK,aAAa,GAAG;AAChE,MAAM,qBAAqB,OAAO,KAAK,qBAAqB;AAQ5D,IAAa,yBAAb,MAAa,uBAAuB;CAClC,OAAO,cAOH;EACF,MAAM;GACJ,UAAU;GACV,QAAQ;GACT;EACD,IAAI;GACF,UAAU;GACV,QAAQ;GACT;EACD,MACE,QAAQ,qBACJ;GACE,UAAU;GACV,QAAQ;GACT,GACD;EACP;CAED,AAAmB,SAAS,QAAQ,OAAO;CAE3C,IAAc,UAAyC;AACrD,SAAO;GACL,qBAAqB;IACnB;IACA;IACA;IACA;IACA;IACD;GACD,GAAG,KAAK,OAAO,MAAM,IAAI,iCAAiC;GAC3D;;CAGH,AAAgB,aAAa,MAAM;EACjC,IAAI;EACJ,SAAS,OAAO,EAAE,SAAS,eAAe;AAExC,OAAI,SAAS,QAAQ,oBACnB;GAGF,MAAM,iBAAiB,QAAQ,QAAQ;AACvC,OAAI,CAAC,eACH;AAIF,OAAI,CAAC,KAAK,qBAAqB,SAAS,QAAQ,gBAAgB,CAC9D;AAGF,QAAK,MAAM,YAAY;IAAC;IAAQ;IAAM;IAAO,CAC3C,KACE,eAAe,SAAS,SAAS,IACjC,uBAAuB,YAAY,WACnC;AACA,UAAM,KAAK,SAAS,UAAU,SAAS;AACvC;;;EAIP,CAAC;CAEF,AAAU,qBAAqB,aAA0C;AACvE,MAAI,CAAC,YACH,QAAO;EAGT,MAAM,mBAAmB,YAAY,aAAa;AAElD,SAAO,CAAC,CAAC,KAAK,QAAQ,oBAAoB,MAAM,OAC9C,iBAAiB,SAAS,GAAG,CAC9B;;CAGH,MAAgB,SACd,UACA,UACe;EACf,MAAM,OAAO,SAAS;EAEtB,MAAM,aAAa,uBAAuB,YAAY;AACtD,MAAI,CAAC,WACH;EAGF,MAAM,SAAS,KAAK,UAAU,SAAS;AAEvC,MACE,OAAO,SAAS,YAChB,OAAO,SAAS,KAAK,IACrB,gBAAgB,aAChB;GACA,MAAM,aAAa,MAAM,WAAW,SAAS,MAAM,EACjD,QACD,CAAC;AACF,QAAK,WAAW,UAAU,SAAS;AACnC,YAAS,QAAQ,oBAAoB,WAAW,OAAO,UAAU;AACjE,YAAS,OAAO;;AAGlB,MAAI,OAAO,SAAS,YAAY,gBAAgB,UAAU;AACxD,QAAK,WAAW,UAAU,SAAS;AACnC,YAAS,OAAO,KAAK,KAAK,WAAW,OAAO,EAAE,QAAQ,CAAC,CAAC;;AAG1D,MAAI,OAAO,SAAS,YAAY,gBAAgB,gBAAgB;AAC9D,QAAK,WAAW,UAAU,SAAS;AACnC,YAAS,OAAO,SAAS,QAAQ,KAAK,CAAC,KACrC,WAAW,OAAO,EAAE,QAAQ,CAAC,CAC9B;;;CAIL,AAAU,UACR,UACqB;AACrB,MAAI,aAAa,OACf,QAAO,GACJ,KAAK,UAAU,0BAA0B,GAC3C;AAEH,MAAI,aAAa,KACf,QAAO,EAAE;AAEX,MAAI,aAAa,OACf,QAAO,EAAE;AAEX,SAAO,EAAE;;CAGX,AAAU,WACR,UACA,UACM;AACN,WAAS,QAAQ,OAAO;AACxB,WAAS,QAAQ,sBAAsB;AACvC,WAAS,QAAQ,mBAAmB;;;;;;;;;;;ACtJxC,MAAa,uBAAuB,QAAQ;CAC1C,MAAM;CACN,UAAU,CAAC,cAAc,uBAAuB;CACjD,CAAC"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/server/compress/providers/ServerCompressProvider.ts","../../../src/server/compress/index.ts"],"sourcesContent":["import { Readable, type Transform } from \"node:stream\";\nimport { ReadableStream } from \"node:stream/web\";\nimport { promisify } from \"node:util\";\nimport * as zlib from \"node:zlib\";\nimport { $hook, $inject, Alepha } from \"alepha\";\nimport type { ServerResponse } from \"alepha/server\";\n\nconst gzip = promisify(zlib.gzip);\nconst createGzip = zlib.createGzip;\nconst brotli = promisify(zlib.brotliCompress);\nconst createBrotliCompress = zlib.createBrotliCompress;\nconst zstd = zlib.zstdCompress ? promisify(zlib.zstdCompress) : undefined;\nconst createZstdCompress = zstd ? zlib.createZstdCompress : undefined;\n\ndeclare module \"alepha\" {\n interface State {\n \"alepha.server.compress.options\"?: ServerCompressProviderOptions;\n }\n}\n\nexport class ServerCompressProvider {\n static compressors: Record<\n string,\n | {\n compress: (...args: any[]) => Promise<Buffer>;\n stream: (options?: any) => Transform;\n }\n | undefined\n > = {\n gzip: {\n compress: gzip,\n stream: createGzip,\n },\n br: {\n compress: brotli,\n stream: createBrotliCompress,\n },\n zstd:\n zstd && createZstdCompress\n ? {\n compress: zstd,\n stream: createZstdCompress,\n }\n : undefined,\n };\n\n protected readonly alepha = $inject(Alepha);\n\n protected get options(): ServerCompressProviderOptions {\n return {\n allowedContentTypes: [\n \"application/json\",\n \"text/html\",\n \"application/javascript\",\n \"text/plain\",\n \"text/css\",\n ],\n ...this.alepha.store.get(\"alepha.server.compress.options\"),\n };\n }\n\n public readonly onResponse = $hook({\n on: \"server:onResponse\",\n handler: async ({ request, response }) => {\n // skip if already compressed\n if (response.headers[\"content-encoding\"]) {\n return;\n }\n\n const acceptEncoding = request.headers[\"accept-encoding\"]; // skip if no accept-encoding header\n if (!acceptEncoding) {\n return;\n }\n\n // skip if not json or html (for now)\n if (!this.isAllowedContentType(response.headers[\"content-type\"])) {\n return;\n }\n\n for (const encoding of [\"zstd\", \"br\", \"gzip\"] as const) {\n if (\n acceptEncoding.includes(encoding) &&\n ServerCompressProvider.compressors[encoding]\n ) {\n await this.compress(encoding, response);\n return;\n }\n }\n },\n });\n\n protected isAllowedContentType(contentType: string | undefined): boolean {\n if (!contentType) {\n return false;\n }\n\n const lowerContentType = contentType.toLowerCase();\n\n return !!this.options.allowedContentTypes.find((it) =>\n lowerContentType.includes(it),\n );\n }\n\n protected async compress(\n encoding: keyof typeof ServerCompressProvider.compressors,\n response: ServerResponse,\n ): Promise<void> {\n const body = response.body; // can be string or Buffer or ArrayBuffer or Readable\n\n const compressor = ServerCompressProvider.compressors[encoding];\n if (!compressor) {\n return;\n }\n\n const params = this.getParams(encoding);\n\n if (\n typeof body === \"string\" ||\n Buffer.isBuffer(body) ||\n body instanceof ArrayBuffer\n ) {\n const compressed = await compressor.compress(body, {\n params,\n });\n this.setHeaders(response, encoding);\n response.headers[\"content-length\"] = compressed.length.toString();\n response.body = compressed;\n return;\n }\n\n if (typeof body === \"object\" && body instanceof Readable) {\n this.setHeaders(response, encoding);\n response.body = body.pipe(compressor.stream({ params }));\n return;\n }\n\n if (typeof body === \"object\" && body instanceof ReadableStream) {\n this.setHeaders(response, encoding);\n response.body = Readable.fromWeb(body).pipe(\n compressor.stream({ params }),\n );\n }\n }\n\n protected getParams(\n encoding: keyof typeof ServerCompressProvider.compressors,\n ): Record<number, any> {\n if (encoding === \"zstd\") {\n return {\n [zlib.constants.ZSTD_c_compressionLevel]: 3, // default compression level for zstd\n };\n }\n if (encoding === \"br\") {\n return {};\n }\n if (encoding === \"gzip\") {\n return {};\n }\n return {};\n }\n\n protected setHeaders(\n response: ServerResponse,\n encoding: keyof typeof ServerCompressProvider.compressors,\n ): void {\n response.headers.vary = \"content-encoding\";\n response.headers[\"content-encoding\"] = encoding;\n response.headers[\"cache-control\"] = \"no-cache\";\n }\n}\n\nexport interface ServerCompressProviderOptions {\n allowedContentTypes: string[];\n}\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { ServerCompressProvider } from \"./providers/ServerCompressProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./providers/ServerCompressProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Plugin for Alepha Server that provides server-side compression capabilities.\n *\n * Compresses responses using gzip, brotli, or zstd based on the `Accept-Encoding` header.\n */\nexport const AlephaServerCompress = $module({\n name: \"alepha.server.compress\",\n services: [AlephaServer, ServerCompressProvider],\n});\n"],"mappings":";;;;;;;;AAOA,MAAM,OAAO,UAAU,KAAK,KAAK;AACjC,MAAM,aAAa,KAAK;AACxB,MAAM,SAAS,UAAU,KAAK,eAAe;AAC7C,MAAM,uBAAuB,KAAK;AAClC,MAAM,OAAO,KAAK,eAAe,UAAU,KAAK,aAAa,GAAG;AAChE,MAAM,qBAAqB,OAAO,KAAK,qBAAqB;AAQ5D,IAAa,yBAAb,MAAa,uBAAuB;CAClC,OAAO,cAOH;EACF,MAAM;GACJ,UAAU;GACV,QAAQ;GACT;EACD,IAAI;GACF,UAAU;GACV,QAAQ;GACT;EACD,MACE,QAAQ,qBACJ;GACE,UAAU;GACV,QAAQ;GACT,GACD;EACP;CAED,AAAmB,SAAS,QAAQ,OAAO;CAE3C,IAAc,UAAyC;AACrD,SAAO;GACL,qBAAqB;IACnB;IACA;IACA;IACA;IACA;IACD;GACD,GAAG,KAAK,OAAO,MAAM,IAAI,iCAAiC;GAC3D;;CAGH,AAAgB,aAAa,MAAM;EACjC,IAAI;EACJ,SAAS,OAAO,EAAE,SAAS,eAAe;AAExC,OAAI,SAAS,QAAQ,oBACnB;GAGF,MAAM,iBAAiB,QAAQ,QAAQ;AACvC,OAAI,CAAC,eACH;AAIF,OAAI,CAAC,KAAK,qBAAqB,SAAS,QAAQ,gBAAgB,CAC9D;AAGF,QAAK,MAAM,YAAY;IAAC;IAAQ;IAAM;IAAO,CAC3C,KACE,eAAe,SAAS,SAAS,IACjC,uBAAuB,YAAY,WACnC;AACA,UAAM,KAAK,SAAS,UAAU,SAAS;AACvC;;;EAIP,CAAC;CAEF,AAAU,qBAAqB,aAA0C;AACvE,MAAI,CAAC,YACH,QAAO;EAGT,MAAM,mBAAmB,YAAY,aAAa;AAElD,SAAO,CAAC,CAAC,KAAK,QAAQ,oBAAoB,MAAM,OAC9C,iBAAiB,SAAS,GAAG,CAC9B;;CAGH,MAAgB,SACd,UACA,UACe;EACf,MAAM,OAAO,SAAS;EAEtB,MAAM,aAAa,uBAAuB,YAAY;AACtD,MAAI,CAAC,WACH;EAGF,MAAM,SAAS,KAAK,UAAU,SAAS;AAEvC,MACE,OAAO,SAAS,YAChB,OAAO,SAAS,KAAK,IACrB,gBAAgB,aAChB;GACA,MAAM,aAAa,MAAM,WAAW,SAAS,MAAM,EACjD,QACD,CAAC;AACF,QAAK,WAAW,UAAU,SAAS;AACnC,YAAS,QAAQ,oBAAoB,WAAW,OAAO,UAAU;AACjE,YAAS,OAAO;AAChB;;AAGF,MAAI,OAAO,SAAS,YAAY,gBAAgB,UAAU;AACxD,QAAK,WAAW,UAAU,SAAS;AACnC,YAAS,OAAO,KAAK,KAAK,WAAW,OAAO,EAAE,QAAQ,CAAC,CAAC;AACxD;;AAGF,MAAI,OAAO,SAAS,YAAY,gBAAgB,gBAAgB;AAC9D,QAAK,WAAW,UAAU,SAAS;AACnC,YAAS,OAAO,SAAS,QAAQ,KAAK,CAAC,KACrC,WAAW,OAAO,EAAE,QAAQ,CAAC,CAC9B;;;CAIL,AAAU,UACR,UACqB;AACrB,MAAI,aAAa,OACf,QAAO,GACJ,KAAK,UAAU,0BAA0B,GAC3C;AAEH,MAAI,aAAa,KACf,QAAO,EAAE;AAEX,MAAI,aAAa,OACf,QAAO,EAAE;AAEX,SAAO,EAAE;;CAGX,AAAU,WACR,UACA,UACM;AACN,WAAS,QAAQ,OAAO;AACxB,WAAS,QAAQ,sBAAsB;AACvC,WAAS,QAAQ,mBAAmB;;;;;;;;;;;ACxJxC,MAAa,uBAAuB,QAAQ;CAC1C,MAAM;CACN,UAAU,CAAC,cAAc,uBAAuB;CACjD,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.browser.js","names":["cookies: Record<string, string>","parts: string[]","cookie: Cookie"],"sources":["../../../src/server/cookies/services/CookieParser.ts","../../../src/server/cookies/primitives/$cookie.browser.ts","../../../src/server/cookies/index.browser.ts"],"sourcesContent":["import type { Cookie } from \"../primitives/$cookie.ts\";\n\nexport class CookieParser {\n public parseRequestCookies(header: string): Record<string, string> {\n const cookies: Record<string, string> = {};\n const parts = header.split(\";\");\n for (const part of parts) {\n const [key, value] = part.split(\"=\");\n if (!key || !value) {\n continue;\n }\n\n cookies[key.trim()] = value.trim();\n }\n\n return cookies;\n }\n\n public serializeResponseCookies(\n cookies: Record<string, Cookie | null>,\n isHttps: boolean,\n ): string[] {\n const headers = [];\n\n for (const [name, cookie] of Object.entries(cookies)) {\n // If the cookie is null, we need to delete it\n if (cookie == null) {\n headers.push(`${name}=; Path=/; Max-Age=0`);\n continue;\n }\n\n if (!cookie.value) {\n continue;\n }\n\n headers.push(this.cookieToString(name, cookie, isHttps));\n }\n\n return headers;\n }\n\n public cookieToString(\n name: string,\n cookie: Cookie,\n isHttps?: boolean,\n ): string {\n const parts: string[] = [];\n\n parts.push(`${name}=${cookie.value}`);\n\n if (cookie.path) {\n parts.push(`Path=${cookie.path}`);\n }\n if (cookie.maxAge) {\n parts.push(`Max-Age=${cookie.maxAge}`);\n }\n if (cookie.secure !== false && isHttps) {\n parts.push(\"Secure\");\n }\n if (cookie.httpOnly) {\n parts.push(\"HttpOnly\");\n }\n if (cookie.sameSite) {\n parts.push(`SameSite=${cookie.sameSite}`);\n }\n if (cookie.domain) {\n parts.push(`Domain=${cookie.domain}`);\n }\n\n return parts.join(\"; \");\n }\n}\n","import {\n $inject,\n Alepha,\n AlephaError,\n createPrimitive,\n KIND,\n Primitive,\n type Static,\n type TSchema,\n} from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { CookieParser } from \"../services/CookieParser.ts\";\nimport type {\n AbstractCookiePrimitive,\n Cookie,\n CookiePrimitiveOptions,\n Cookies,\n} from \"./$cookie.ts\";\n\n/**\n * Creates a browser-side cookie primitive for client-side cookie management.\n *\n * Browser-specific version of $cookie that uses document.cookie API. Supports type-safe\n * cookie operations with schema validation but excludes encryption/signing (use server-side\n * $cookie for secure operations).\n *\n * **Note**: This is the browser version - encryption, signing, and compression are not supported.\n *\n * @example\n * ```ts\n * class ClientCookies {\n * preferences = $cookie({\n * name: \"user-prefs\",\n * schema: t.object({ theme: t.text(), language: t.text() }),\n * ttl: [30, \"days\"]\n * });\n *\n * savePreferences() {\n * this.preferences.set({ theme: \"dark\", language: \"en\" });\n * }\n *\n * getPreferences() {\n * return this.preferences.get() ?? { theme: \"light\", language: \"en\" };\n * }\n * }\n * ```\n */\nexport const $cookie = <T extends TSchema>(\n options: CookiePrimitiveOptions<T>,\n): AbstractCookiePrimitive<T> => {\n return createPrimitive(BrowserCookiePrimitive<T>, options);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class BrowserCookiePrimitive<T extends TSchema>\n extends Primitive<CookiePrimitiveOptions<T>>\n implements AbstractCookiePrimitive<T>\n{\n protected cookieParser = $inject(CookieParser);\n protected alepha = $inject(Alepha);\n protected dateTimeProvider = $inject(DateTimeProvider);\n protected cookie?: Cookie;\n\n public get name(): string {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n public set(data: Static<T>): void {\n const value = JSON.stringify(\n this.alepha.codec.decode(this.options.schema, data),\n );\n const options = this.options;\n\n if (options.compress) {\n throw new AlephaError(\"Compression is not supported in browser cookies.\");\n }\n\n if (options.encrypt) {\n throw new AlephaError(\"Encryption is not supported in browser cookies.\");\n }\n\n if (options.sign) {\n throw new AlephaError(\"Signing is not supported in browser cookies.\");\n }\n\n const cookie: Cookie = {\n value: encodeURIComponent(value),\n path: options.path ?? \"/\",\n sameSite: options.sameSite ?? \"lax\",\n secure: false,\n httpOnly: false,\n domain: options.domain,\n };\n\n if (options.ttl) {\n cookie.maxAge = this.dateTimeProvider.duration(options.ttl).as(\"seconds\");\n }\n\n // biome-ignore lint/suspicious/noDocumentCookie: ...\n document.cookie = this.cookieParser.cookieToString(this.name, cookie);\n }\n\n public get(options?: { cookies?: Cookies }): Static<T> | undefined {\n const cookie = this.cookieParser.parseRequestCookies(document.cookie)[\n this.name\n ];\n if (!cookie) {\n return undefined;\n }\n\n const rawValue = decodeURIComponent(cookie);\n\n if (this.options.compress) {\n throw new AlephaError(\"Compression is not supported in browser cookies.\");\n }\n\n if (this.options.encrypt) {\n throw new AlephaError(\"Encryption is not supported in browser cookies.\");\n }\n\n if (this.options.sign) {\n throw new AlephaError(\"Signing is not supported in browser cookies.\");\n }\n\n return this.alepha.codec.decode(this.options.schema, JSON.parse(rawValue));\n }\n\n public del(): void {\n const options = this.options;\n const cookie: Cookie = {\n value: \"\",\n path: options.path ?? \"/\",\n sameSite: options.sameSite ?? \"lax\",\n secure: false,\n httpOnly: false,\n domain: options.domain,\n maxAge: 0, // Set maxAge to 0 to delete the cookie\n };\n\n // biome-ignore lint/suspicious/noDocumentCookie: ...\n document.cookie = this.cookieParser.cookieToString(this.name, cookie);\n }\n}\n\n$cookie[KIND] = BrowserCookiePrimitive;\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$cookie.browser.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport const AlephaServerCookies = $module({\n name: \"alepha.server.cookies\",\n primitives: [],\n services: [AlephaServer],\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.browser.js","names":[],"sources":["../../../src/server/cookies/services/CookieParser.ts","../../../src/server/cookies/primitives/$cookie.browser.ts","../../../src/server/cookies/index.browser.ts"],"sourcesContent":["import type { Cookie } from \"../primitives/$cookie.ts\";\n\nexport class CookieParser {\n public parseRequestCookies(header: string): Record<string, string> {\n const cookies: Record<string, string> = {};\n const parts = header.split(\";\");\n for (const part of parts) {\n const [key, value] = part.split(\"=\");\n if (!key || !value) {\n continue;\n }\n\n cookies[key.trim()] = value.trim();\n }\n\n return cookies;\n }\n\n public serializeResponseCookies(\n cookies: Record<string, Cookie | null>,\n isHttps: boolean,\n ): string[] {\n const headers = [];\n\n for (const [name, cookie] of Object.entries(cookies)) {\n // If the cookie is null, we need to delete it\n if (cookie == null) {\n headers.push(`${name}=; Path=/; Max-Age=0`);\n continue;\n }\n\n if (!cookie.value) {\n continue;\n }\n\n headers.push(this.cookieToString(name, cookie, isHttps));\n }\n\n return headers;\n }\n\n public cookieToString(\n name: string,\n cookie: Cookie,\n isHttps?: boolean,\n ): string {\n const parts: string[] = [];\n\n parts.push(`${name}=${cookie.value}`);\n\n if (cookie.path) {\n parts.push(`Path=${cookie.path}`);\n }\n if (cookie.maxAge) {\n parts.push(`Max-Age=${cookie.maxAge}`);\n }\n if (cookie.secure !== false && isHttps) {\n parts.push(\"Secure\");\n }\n if (cookie.httpOnly) {\n parts.push(\"HttpOnly\");\n }\n if (cookie.sameSite) {\n parts.push(`SameSite=${cookie.sameSite}`);\n }\n if (cookie.domain) {\n parts.push(`Domain=${cookie.domain}`);\n }\n\n return parts.join(\"; \");\n }\n}\n","import {\n $inject,\n Alepha,\n AlephaError,\n createPrimitive,\n KIND,\n Primitive,\n type Static,\n type TSchema,\n} from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { CookieParser } from \"../services/CookieParser.ts\";\nimport type {\n AbstractCookiePrimitive,\n Cookie,\n CookiePrimitiveOptions,\n Cookies,\n} from \"./$cookie.ts\";\n\n/**\n * Creates a browser-side cookie primitive for client-side cookie management.\n *\n * Browser-specific version of $cookie that uses document.cookie API. Supports type-safe\n * cookie operations with schema validation but excludes encryption/signing (use server-side\n * $cookie for secure operations).\n *\n * **Note**: This is the browser version - encryption, signing, and compression are not supported.\n *\n * @example\n * ```ts\n * class ClientCookies {\n * preferences = $cookie({\n * name: \"user-prefs\",\n * schema: t.object({ theme: t.text(), language: t.text() }),\n * ttl: [30, \"days\"]\n * });\n *\n * savePreferences() {\n * this.preferences.set({ theme: \"dark\", language: \"en\" });\n * }\n *\n * getPreferences() {\n * return this.preferences.get() ?? { theme: \"light\", language: \"en\" };\n * }\n * }\n * ```\n */\nexport const $cookie = <T extends TSchema>(\n options: CookiePrimitiveOptions<T>,\n): AbstractCookiePrimitive<T> => {\n return createPrimitive(BrowserCookiePrimitive<T>, options);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class BrowserCookiePrimitive<T extends TSchema>\n extends Primitive<CookiePrimitiveOptions<T>>\n implements AbstractCookiePrimitive<T>\n{\n protected cookieParser = $inject(CookieParser);\n protected alepha = $inject(Alepha);\n protected dateTimeProvider = $inject(DateTimeProvider);\n protected cookie?: Cookie;\n\n public get name(): string {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n public set(data: Static<T>): void {\n const value = JSON.stringify(\n this.alepha.codec.decode(this.options.schema, data),\n );\n const options = this.options;\n\n if (options.compress) {\n throw new AlephaError(\"Compression is not supported in browser cookies.\");\n }\n\n if (options.encrypt) {\n throw new AlephaError(\"Encryption is not supported in browser cookies.\");\n }\n\n if (options.sign) {\n throw new AlephaError(\"Signing is not supported in browser cookies.\");\n }\n\n const cookie: Cookie = {\n value: encodeURIComponent(value),\n path: options.path ?? \"/\",\n sameSite: options.sameSite ?? \"lax\",\n secure: false,\n httpOnly: false,\n domain: options.domain,\n };\n\n if (options.ttl) {\n cookie.maxAge = this.dateTimeProvider.duration(options.ttl).as(\"seconds\");\n }\n\n // biome-ignore lint/suspicious/noDocumentCookie: ...\n document.cookie = this.cookieParser.cookieToString(this.name, cookie);\n }\n\n public get(options?: { cookies?: Cookies }): Static<T> | undefined {\n const cookie = this.cookieParser.parseRequestCookies(document.cookie)[\n this.name\n ];\n if (!cookie) {\n return undefined;\n }\n\n const rawValue = decodeURIComponent(cookie);\n\n if (this.options.compress) {\n throw new AlephaError(\"Compression is not supported in browser cookies.\");\n }\n\n if (this.options.encrypt) {\n throw new AlephaError(\"Encryption is not supported in browser cookies.\");\n }\n\n if (this.options.sign) {\n throw new AlephaError(\"Signing is not supported in browser cookies.\");\n }\n\n return this.alepha.codec.decode(this.options.schema, JSON.parse(rawValue));\n }\n\n public del(): void {\n const options = this.options;\n const cookie: Cookie = {\n value: \"\",\n path: options.path ?? \"/\",\n sameSite: options.sameSite ?? \"lax\",\n secure: false,\n httpOnly: false,\n domain: options.domain,\n maxAge: 0, // Set maxAge to 0 to delete the cookie\n };\n\n // biome-ignore lint/suspicious/noDocumentCookie: ...\n document.cookie = this.cookieParser.cookieToString(this.name, cookie);\n }\n}\n\n$cookie[KIND] = BrowserCookiePrimitive;\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$cookie.browser.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport const AlephaServerCookies = $module({\n name: \"alepha.server.cookies\",\n primitives: [],\n services: [AlephaServer],\n});\n"],"mappings":"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"}