alepha 0.15.3 → 0.15.5

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 (318) hide show
  1. package/README.md +26 -11
  2. package/dist/api/audits/index.d.ts +335 -335
  3. package/dist/api/audits/index.d.ts.map +1 -1
  4. package/dist/api/audits/index.js +11 -3
  5. package/dist/api/audits/index.js.map +1 -1
  6. package/dist/api/files/index.d.ts +3 -3
  7. package/dist/api/files/index.js +4 -3
  8. package/dist/api/files/index.js.map +1 -1
  9. package/dist/api/jobs/index.d.ts +198 -155
  10. package/dist/api/jobs/index.d.ts.map +1 -1
  11. package/dist/api/jobs/index.js +103 -5
  12. package/dist/api/jobs/index.js.map +1 -1
  13. package/dist/api/keys/index.d.ts +198 -198
  14. package/dist/api/keys/index.d.ts.map +1 -1
  15. package/dist/api/keys/index.js +3 -3
  16. package/dist/api/keys/index.js.map +1 -1
  17. package/dist/api/notifications/index.browser.js +1 -0
  18. package/dist/api/notifications/index.browser.js.map +1 -1
  19. package/dist/api/notifications/index.d.ts +3 -3
  20. package/dist/api/notifications/index.js +4 -3
  21. package/dist/api/notifications/index.js.map +1 -1
  22. package/dist/api/parameters/index.d.ts +263 -263
  23. package/dist/api/parameters/index.d.ts.map +1 -1
  24. package/dist/api/parameters/index.js +41 -30
  25. package/dist/api/parameters/index.js.map +1 -1
  26. package/dist/api/users/index.d.ts +383 -77
  27. package/dist/api/users/index.d.ts.map +1 -1
  28. package/dist/api/users/index.js +284 -72
  29. package/dist/api/users/index.js.map +1 -1
  30. package/dist/api/verifications/index.d.ts +131 -131
  31. package/dist/api/verifications/index.d.ts.map +1 -1
  32. package/dist/api/verifications/index.js +3 -3
  33. package/dist/api/verifications/index.js.map +1 -1
  34. package/dist/batch/index.d.ts +3 -3
  35. package/dist/batch/index.js +3 -3
  36. package/dist/batch/index.js.map +1 -1
  37. package/dist/bucket/index.d.ts +3 -3
  38. package/dist/bucket/index.js +6 -6
  39. package/dist/bucket/index.js.map +1 -1
  40. package/dist/cache/core/index.d.ts +3 -3
  41. package/dist/cache/core/index.js +3 -3
  42. package/dist/cache/core/index.js.map +1 -1
  43. package/dist/cli/index.d.ts +5612 -20
  44. package/dist/cli/index.d.ts.map +1 -1
  45. package/dist/cli/index.js +122 -91
  46. package/dist/cli/index.js.map +1 -1
  47. package/dist/command/index.d.ts +11 -4
  48. package/dist/command/index.d.ts.map +1 -1
  49. package/dist/command/index.js +8 -6
  50. package/dist/command/index.js.map +1 -1
  51. package/dist/core/index.browser.js.map +1 -1
  52. package/dist/core/index.d.ts +4 -8
  53. package/dist/core/index.d.ts.map +1 -1
  54. package/dist/core/index.js +3 -3
  55. package/dist/core/index.js.map +1 -1
  56. package/dist/core/index.native.js.map +1 -1
  57. package/dist/datetime/index.d.ts +3 -3
  58. package/dist/datetime/index.js +3 -3
  59. package/dist/datetime/index.js.map +1 -1
  60. package/dist/email/index.d.ts +16 -16
  61. package/dist/email/index.d.ts.map +1 -1
  62. package/dist/email/index.js +10562 -10
  63. package/dist/email/index.js.map +1 -1
  64. package/dist/fake/index.d.ts +3 -3
  65. package/dist/fake/index.js +3 -3
  66. package/dist/fake/index.js.map +1 -1
  67. package/dist/lock/core/index.d.ts +9 -4
  68. package/dist/lock/core/index.d.ts.map +1 -1
  69. package/dist/lock/core/index.js +12 -4
  70. package/dist/lock/core/index.js.map +1 -1
  71. package/dist/logger/index.d.ts +3 -3
  72. package/dist/logger/index.js +6 -3
  73. package/dist/logger/index.js.map +1 -1
  74. package/dist/mcp/index.d.ts +3 -3
  75. package/dist/mcp/index.js +3 -3
  76. package/dist/mcp/index.js.map +1 -1
  77. package/dist/orm/index.d.ts +12 -12
  78. package/dist/orm/index.js +4 -4
  79. package/dist/orm/index.js.map +1 -1
  80. package/dist/queue/core/index.d.ts +3 -3
  81. package/dist/queue/core/index.js +3 -3
  82. package/dist/queue/core/index.js.map +1 -1
  83. package/dist/react/auth/index.browser.js +2 -1
  84. package/dist/react/auth/index.browser.js.map +1 -1
  85. package/dist/react/auth/index.d.ts +3 -3
  86. package/dist/react/auth/index.js +5 -4
  87. package/dist/react/auth/index.js.map +1 -1
  88. package/dist/react/core/index.d.ts +6 -6
  89. package/dist/react/core/index.js +3 -3
  90. package/dist/react/core/index.js.map +1 -1
  91. package/dist/react/form/index.d.ts +3 -3
  92. package/dist/react/form/index.js +3 -3
  93. package/dist/react/form/index.js.map +1 -1
  94. package/dist/react/head/index.d.ts +3 -3
  95. package/dist/react/head/index.js +3 -3
  96. package/dist/react/head/index.js.map +1 -1
  97. package/dist/react/i18n/index.d.ts +3 -3
  98. package/dist/react/i18n/index.js +3 -3
  99. package/dist/react/i18n/index.js.map +1 -1
  100. package/dist/react/intro/index.css +337 -0
  101. package/dist/react/intro/index.css.map +1 -0
  102. package/dist/react/intro/index.d.ts +10 -0
  103. package/dist/react/intro/index.d.ts.map +1 -0
  104. package/dist/react/intro/index.js +222 -0
  105. package/dist/react/intro/index.js.map +1 -0
  106. package/dist/react/router/index.browser.js +2 -2
  107. package/dist/react/router/index.browser.js.map +1 -1
  108. package/dist/react/router/index.d.ts +11 -1
  109. package/dist/react/router/index.d.ts.map +1 -1
  110. package/dist/react/router/index.js +21 -11
  111. package/dist/react/router/index.js.map +1 -1
  112. package/dist/redis/index.d.ts +22 -22
  113. package/dist/redis/index.js +3 -3
  114. package/dist/redis/index.js.map +1 -1
  115. package/dist/retry/index.d.ts +3 -3
  116. package/dist/retry/index.js +3 -3
  117. package/dist/retry/index.js.map +1 -1
  118. package/dist/scheduler/index.d.ts +16 -4
  119. package/dist/scheduler/index.d.ts.map +1 -1
  120. package/dist/scheduler/index.js +45 -7
  121. package/dist/scheduler/index.js.map +1 -1
  122. package/dist/security/index.d.ts +3 -3
  123. package/dist/security/index.js +5 -5
  124. package/dist/security/index.js.map +1 -1
  125. package/dist/server/auth/index.d.ts +3 -3
  126. package/dist/server/auth/index.js +3 -3
  127. package/dist/server/auth/index.js.map +1 -1
  128. package/dist/server/cache/index.d.ts +3 -3
  129. package/dist/server/cache/index.js +3 -3
  130. package/dist/server/cache/index.js.map +1 -1
  131. package/dist/server/compress/index.d.ts +3 -3
  132. package/dist/server/compress/index.d.ts.map +1 -1
  133. package/dist/server/compress/index.js +4 -3
  134. package/dist/server/compress/index.js.map +1 -1
  135. package/dist/server/cookies/index.d.ts +3 -3
  136. package/dist/server/cookies/index.js +3 -3
  137. package/dist/server/cookies/index.js.map +1 -1
  138. package/dist/server/core/index.d.ts +14 -25
  139. package/dist/server/core/index.d.ts.map +1 -1
  140. package/dist/server/core/index.js +13 -29
  141. package/dist/server/core/index.js.map +1 -1
  142. package/dist/server/cors/index.d.ts +3 -3
  143. package/dist/server/cors/index.js +3 -3
  144. package/dist/server/cors/index.js.map +1 -1
  145. package/dist/server/health/index.d.ts +20 -20
  146. package/dist/server/health/index.js +3 -3
  147. package/dist/server/health/index.js.map +1 -1
  148. package/dist/server/helmet/index.d.ts +3 -3
  149. package/dist/server/helmet/index.js +3 -3
  150. package/dist/server/helmet/index.js.map +1 -1
  151. package/dist/server/links/index.d.ts +42 -42
  152. package/dist/server/links/index.d.ts.map +1 -1
  153. package/dist/server/links/index.js +4 -4
  154. package/dist/server/links/index.js.map +1 -1
  155. package/dist/server/metrics/index.d.ts +3 -3
  156. package/dist/server/metrics/index.js +3 -3
  157. package/dist/server/metrics/index.js.map +1 -1
  158. package/dist/server/multipart/index.d.ts +3 -3
  159. package/dist/server/multipart/index.js +3 -3
  160. package/dist/server/multipart/index.js.map +1 -1
  161. package/dist/server/proxy/index.d.ts +3 -3
  162. package/dist/server/proxy/index.js +3 -3
  163. package/dist/server/proxy/index.js.map +1 -1
  164. package/dist/server/rate-limit/index.d.ts +3 -3
  165. package/dist/server/rate-limit/index.js +3 -3
  166. package/dist/server/rate-limit/index.js.map +1 -1
  167. package/dist/server/static/index.d.ts +3 -3
  168. package/dist/server/static/index.js +6 -6
  169. package/dist/server/static/index.js.map +1 -1
  170. package/dist/server/swagger/index.d.ts +3 -3
  171. package/dist/server/swagger/index.js +6 -6
  172. package/dist/server/swagger/index.js.map +1 -1
  173. package/dist/sms/index.d.ts +3 -3
  174. package/dist/sms/index.js +6 -6
  175. package/dist/sms/index.js.map +1 -1
  176. package/dist/system/index.d.ts +3 -3
  177. package/dist/system/index.js +3 -3
  178. package/dist/system/index.js.map +1 -1
  179. package/dist/thread/index.d.ts +3 -3
  180. package/dist/thread/index.js +3 -3
  181. package/dist/thread/index.js.map +1 -1
  182. package/dist/topic/core/index.d.ts +3 -3
  183. package/dist/topic/core/index.js +3 -3
  184. package/dist/topic/core/index.js.map +1 -1
  185. package/dist/vite/index.d.ts +6286 -4
  186. package/dist/vite/index.d.ts.map +1 -1
  187. package/dist/vite/index.js +28 -2
  188. package/dist/vite/index.js.map +1 -1
  189. package/dist/websocket/index.d.ts +37 -37
  190. package/dist/websocket/index.d.ts.map +1 -1
  191. package/dist/websocket/index.js +3 -3
  192. package/dist/websocket/index.js.map +1 -1
  193. package/package.json +12 -4
  194. package/src/api/audits/controllers/AdminAuditController.ts +8 -0
  195. package/src/api/audits/index.ts +3 -3
  196. package/src/api/files/controllers/AdminFileStatsController.ts +1 -0
  197. package/src/api/files/index.ts +3 -3
  198. package/src/api/jobs/controllers/AdminJobController.ts +18 -2
  199. package/src/api/jobs/index.ts +4 -3
  200. package/src/api/jobs/services/JobAudits.spec.ts +89 -0
  201. package/src/api/jobs/services/JobAudits.ts +101 -0
  202. package/src/api/keys/index.ts +3 -3
  203. package/src/api/notifications/controllers/AdminNotificationController.ts +1 -0
  204. package/src/api/notifications/index.ts +3 -3
  205. package/src/api/parameters/controllers/AdminConfigController.ts +10 -0
  206. package/src/api/parameters/index.ts +5 -3
  207. package/src/api/users/__tests__/ApiKeys-integration.spec.ts +1 -1
  208. package/src/api/users/__tests__/ApiKeys.spec.ts +1 -1
  209. package/src/api/users/__tests__/EmailVerification.spec.ts +16 -1
  210. package/src/api/users/__tests__/PasswordReset.spec.ts +11 -0
  211. package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -0
  212. package/src/api/users/controllers/AdminIdentityController.ts +3 -0
  213. package/src/api/users/controllers/AdminSessionController.ts +3 -0
  214. package/src/api/users/controllers/AdminUserController.ts +5 -0
  215. package/src/api/users/index.ts +8 -9
  216. package/src/api/users/primitives/$realm.ts +117 -19
  217. package/src/api/users/providers/RealmProvider.ts +15 -7
  218. package/src/api/users/services/CredentialService.spec.ts +11 -0
  219. package/src/api/users/services/CredentialService.ts +47 -24
  220. package/src/api/users/services/IdentityService.ts +12 -4
  221. package/src/api/users/services/RegistrationService.spec.ts +11 -0
  222. package/src/api/users/services/RegistrationService.ts +33 -12
  223. package/src/api/users/services/SessionService.ts +83 -12
  224. package/src/api/users/services/UserAudits.ts +47 -0
  225. package/src/api/users/services/UserFiles.ts +19 -0
  226. package/src/api/users/services/UserJobs.spec.ts +107 -0
  227. package/src/api/users/services/UserJobs.ts +62 -0
  228. package/src/api/users/services/UserParameters.ts +23 -0
  229. package/src/api/users/services/UserService.ts +34 -17
  230. package/src/api/verifications/index.ts +3 -3
  231. package/src/batch/index.ts +3 -3
  232. package/src/bucket/index.ts +3 -3
  233. package/src/cache/core/index.ts +3 -3
  234. package/src/cli/commands/build.ts +1 -0
  235. package/src/cli/commands/db.ts +9 -0
  236. package/src/cli/commands/init.spec.ts +2 -17
  237. package/src/cli/commands/init.ts +37 -1
  238. package/src/cli/providers/ViteDevServerProvider.ts +36 -2
  239. package/src/cli/services/AlephaCliUtils.ts +17 -0
  240. package/src/cli/services/PackageManagerUtils.ts +15 -1
  241. package/src/cli/services/ProjectScaffolder.ts +8 -13
  242. package/src/cli/templates/agentMd.ts +2 -25
  243. package/src/cli/templates/apiAppSecurityTs.ts +37 -2
  244. package/src/cli/templates/mainCss.ts +2 -32
  245. package/src/cli/templates/webAppRouterTs.ts +5 -5
  246. package/src/cli/templates/webHomeComponentTsx.ts +10 -0
  247. package/src/command/helpers/Runner.ts +14 -1
  248. package/src/command/index.ts +3 -3
  249. package/src/core/helpers/primitive.ts +0 -5
  250. package/src/core/index.ts +3 -3
  251. package/src/datetime/index.ts +3 -3
  252. package/src/email/index.ts +3 -3
  253. package/src/email/index.workerd.ts +36 -0
  254. package/src/email/providers/LocalEmailProvider.ts +2 -2
  255. package/src/email/providers/WorkermailerEmailProvider.ts +221 -0
  256. package/src/fake/index.ts +3 -3
  257. package/src/lock/core/index.ts +3 -3
  258. package/src/lock/core/primitives/$lock.ts +13 -1
  259. package/src/logger/index.ts +3 -3
  260. package/src/logger/providers/PrettyFormatterProvider.ts +7 -0
  261. package/src/mcp/index.ts +3 -3
  262. package/src/orm/index.ts +3 -3
  263. package/src/orm/providers/drivers/NodeSqliteProvider.ts +1 -1
  264. package/src/queue/core/index.ts +3 -3
  265. package/src/react/auth/index.ts +3 -3
  266. package/src/react/auth/services/ReactAuth.ts +3 -1
  267. package/src/react/core/index.ts +3 -3
  268. package/src/react/form/index.ts +3 -3
  269. package/src/react/head/index.ts +3 -3
  270. package/src/react/i18n/index.ts +3 -3
  271. package/src/react/intro/components/GettingStarted.css +334 -0
  272. package/src/react/intro/components/GettingStarted.tsx +276 -0
  273. package/src/react/intro/index.ts +1 -0
  274. package/src/react/router/atoms/ssrManifestAtom.ts +7 -0
  275. package/src/react/router/index.browser.ts +2 -0
  276. package/src/react/router/index.ts +2 -0
  277. package/src/react/router/providers/ReactServerProvider.ts +14 -4
  278. package/src/react/router/providers/SSRManifestProvider.ts +7 -0
  279. package/src/redis/index.ts +3 -3
  280. package/src/retry/index.ts +3 -3
  281. package/src/router/index.ts +3 -3
  282. package/src/scheduler/index.ts +3 -3
  283. package/src/scheduler/index.workerd.ts +43 -0
  284. package/src/scheduler/providers/CronProvider.ts +53 -6
  285. package/src/scheduler/providers/WorkerdCronProvider.ts +102 -0
  286. package/src/security/index.ts +3 -3
  287. package/src/security/providers/JwtProvider.ts +2 -2
  288. package/src/server/auth/index.ts +3 -3
  289. package/src/server/cache/index.ts +3 -3
  290. package/src/server/compress/index.ts +3 -3
  291. package/src/server/compress/providers/ServerCompressProvider.ts +6 -0
  292. package/src/server/cookies/index.ts +3 -3
  293. package/src/server/core/index.ts +3 -3
  294. package/src/server/core/primitives/$action.spec.ts +3 -2
  295. package/src/server/core/primitives/$action.ts +6 -2
  296. package/src/server/core/providers/NodeHttpServerProvider.ts +2 -15
  297. package/src/server/core/providers/ServerProvider.ts +4 -2
  298. package/src/server/core/providers/ServerRouterProvider.ts +5 -27
  299. package/src/server/cors/index.ts +3 -3
  300. package/src/server/health/index.ts +3 -3
  301. package/src/server/helmet/index.ts +3 -3
  302. package/src/server/links/index.ts +3 -3
  303. package/src/server/links/providers/ServerLinksProvider.spec.ts +332 -0
  304. package/src/server/links/providers/ServerLinksProvider.ts +1 -1
  305. package/src/server/metrics/index.ts +3 -3
  306. package/src/server/multipart/index.ts +3 -3
  307. package/src/server/proxy/index.ts +3 -3
  308. package/src/server/rate-limit/index.ts +3 -3
  309. package/src/server/static/index.ts +3 -3
  310. package/src/server/swagger/index.ts +3 -3
  311. package/src/sms/index.ts +3 -3
  312. package/src/system/index.ts +3 -3
  313. package/src/thread/index.ts +3 -3
  314. package/src/topic/core/index.ts +3 -3
  315. package/src/vite/tasks/generateCloudflare.ts +38 -2
  316. package/src/websocket/index.ts +3 -3
  317. package/src/cli/templates/webHelloComponentTsx.ts +0 -30
  318. /package/src/api/users/{notifications → services}/UserNotifications.ts +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/api/keys/entities/apiKeyEntity.ts","../../../src/api/keys/services/ApiKeyService.ts","../../../src/api/keys/controllers/AdminApiKeyController.ts","../../../src/api/keys/controllers/ApiKeyController.ts","../../../src/api/keys/schemas/adminApiKeyQuerySchema.ts","../../../src/api/keys/schemas/adminApiKeyResourceSchema.ts","../../../src/api/keys/schemas/createApiKeyBodySchema.ts","../../../src/api/keys/schemas/createApiKeyResponseSchema.ts","../../../src/api/keys/schemas/listApiKeyResponseSchema.ts","../../../src/api/keys/schemas/revokeApiKeyParamsSchema.ts","../../../src/api/keys/schemas/revokeApiKeyResponseSchema.ts","../../../src/api/keys/index.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/api/keys/entities/apiKeyEntity.ts","../../../src/api/keys/services/ApiKeyService.ts","../../../src/api/keys/controllers/AdminApiKeyController.ts","../../../src/api/keys/controllers/ApiKeyController.ts","../../../src/api/keys/schemas/adminApiKeyQuerySchema.ts","../../../src/api/keys/schemas/adminApiKeyResourceSchema.ts","../../../src/api/keys/schemas/createApiKeyBodySchema.ts","../../../src/api/keys/schemas/createApiKeyResponseSchema.ts","../../../src/api/keys/schemas/listApiKeyResponseSchema.ts","../../../src/api/keys/schemas/revokeApiKeyParamsSchema.ts","../../../src/api/keys/schemas/revokeApiKeyResponseSchema.ts","../../../src/api/keys/index.ts"],"mappings":"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"}
@@ -435,9 +435,9 @@ var ApiKeyController = class {
435
435
  //#endregion
436
436
  //#region ../../src/api/keys/index.ts
437
437
  /**
438
- * | type | quality | stability |
439
- * |------|---------|--------------|
440
- * | backend | good | experimental |
438
+ * | Stability | Since | Runtime |
439
+ * |-----------|-------|---------|
440
+ * | 3 - stable | 0.11.0 | node, bun, workerd|
441
441
  *
442
442
  * API key management module for programmatic access.
443
443
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/api/keys/schemas/adminApiKeyQuerySchema.ts","../../../src/api/keys/schemas/adminApiKeyResourceSchema.ts","../../../src/api/keys/entities/apiKeyEntity.ts","../../../src/api/keys/services/ApiKeyService.ts","../../../src/api/keys/controllers/AdminApiKeyController.ts","../../../src/api/keys/schemas/createApiKeyBodySchema.ts","../../../src/api/keys/schemas/createApiKeyResponseSchema.ts","../../../src/api/keys/schemas/listApiKeyResponseSchema.ts","../../../src/api/keys/schemas/revokeApiKeyParamsSchema.ts","../../../src/api/keys/schemas/revokeApiKeyResponseSchema.ts","../../../src/api/keys/controllers/ApiKeyController.ts","../../../src/api/keys/index.ts"],"sourcesContent":["import { t } from \"alepha\";\nimport { pageQuerySchema } from \"alepha/orm\";\n\nexport const adminApiKeyQuerySchema = t.extend(pageQuerySchema, {\n userId: t.optional(t.uuid()),\n includeRevoked: t.optional(t.boolean()),\n});\n","import { t } from \"alepha\";\n\nexport const adminApiKeyResourceSchema = t.object({\n id: t.uuid(),\n userId: t.uuid(),\n name: t.string(),\n description: t.optional(t.string()),\n tokenPrefix: t.string(),\n tokenSuffix: t.string(),\n roles: t.array(t.string()),\n createdAt: t.datetime(),\n lastUsedAt: t.optional(t.datetime()),\n lastUsedIp: t.optional(t.string()),\n expiresAt: t.optional(t.datetime()),\n revokedAt: t.optional(t.datetime()),\n usageCount: t.integer(),\n});\n","import { type Static, t } from \"alepha\";\nimport { $entity, db } from \"alepha/orm\";\n\nexport const apiKeyEntity = $entity({\n name: \"api_keys\",\n schema: t.object({\n id: db.primaryKey(t.uuid()),\n createdAt: db.createdAt(),\n updatedAt: db.updatedAt(),\n\n // Owner\n userId: t.uuid(),\n\n // Key metadata\n name: t.text({ maxLength: 100 }),\n description: t.optional(t.text({ maxLength: 500 })),\n\n // Token (hashed) - internal, not user input\n tokenHash: t.string({ maxLength: 256 }),\n tokenPrefix: t.string({ maxLength: 10 }),\n tokenSuffix: t.string({ maxLength: 8 }),\n\n // Roles (snapshot from user at creation)\n roles: db.default(t.array(t.string()), []),\n\n // Tracking\n lastUsedAt: t.optional(t.datetime()),\n lastUsedIp: t.optional(t.string({ maxLength: 45 })),\n usageCount: db.default(t.integer(), 0),\n\n // Lifecycle\n expiresAt: t.optional(t.datetime()),\n revokedAt: t.optional(t.datetime()),\n }),\n indexes: [\n { columns: [\"userId\", \"name\"], unique: true },\n { columns: [\"tokenHash\"], unique: true },\n ],\n});\n\nexport type ApiKeyEntity = Static<typeof apiKeyEntity.schema>;\n","import { createHash, randomBytes } from \"node:crypto\";\nimport { $inject, Alepha } from \"alepha\";\nimport { $cache } from \"alepha/cache\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository, sql } from \"alepha/orm\";\nimport type { IssuerResolver, UserInfo } from \"alepha/security\";\nimport {\n ForbiddenError,\n NotFoundError,\n type ServerRequest,\n} from \"alepha/server\";\nimport { type ApiKeyEntity, apiKeyEntity } from \"../entities/apiKeyEntity.ts\";\n\nexport class ApiKeyService {\n protected readonly alepha = $inject(Alepha);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly log = $logger();\n protected readonly repo = $repository(apiKeyEntity);\n\n /**\n * Cache validated API keys for 15 minutes.\n */\n protected readonly validationCache = $cache<ApiKeyEntity | null, [string]>({\n name: \"api-key-validation\",\n ttl: [15, \"minutes\"],\n });\n\n // -------------------------------------------------------------------------\n // Resolver\n // -------------------------------------------------------------------------\n\n /**\n * Create an issuer resolver for API key authentication.\n * Lower priority means it runs before JWT resolver.\n *\n * @param options.priority - Priority of this resolver (default: 50, JWT is 100)\n * @param options.prefix - API key prefix to match in Bearer header (default: \"ak\")\n */\n public createResolver(\n options: { priority?: number; prefix?: string } = {},\n ): IssuerResolver {\n const { priority = 50, prefix = \"ak\" } = options;\n const prefixPattern = `${prefix}_`;\n\n return {\n priority,\n onRequest: async (req: ServerRequest) => {\n // Try query param first\n const url = typeof req.url === \"string\" ? new URL(req.url) : req.url;\n let token = url.searchParams.get(\"api_key\");\n\n // Try Bearer header - only if token starts with expected prefix\n if (!token) {\n const auth = req.headers.authorization;\n if (auth?.startsWith(\"Bearer \")) {\n const bearerToken = auth.slice(7);\n if (bearerToken.startsWith(prefixPattern)) {\n token = bearerToken;\n }\n }\n }\n\n if (!token) {\n return null;\n }\n\n return this.validate(token);\n },\n };\n }\n\n // -------------------------------------------------------------------------\n // CRUD\n // -------------------------------------------------------------------------\n\n /**\n * Create a new API key for a user.\n * Returns both the API key entity and the plain token (which is only available once).\n */\n public async create(options: {\n userId: string;\n name: string;\n roles: string[];\n description?: string;\n expiresAt?: Date;\n prefix?: string;\n }): Promise<{ apiKey: ApiKeyEntity; token: string }> {\n const prefix = options.prefix ?? \"ak\";\n const random = randomBytes(24).toString(\"base64url\");\n const token = `${prefix}_${random}`;\n const hash = this.hashToken(token);\n const suffix = token.slice(-8);\n\n const apiKey = await this.repo.create({\n userId: options.userId,\n name: options.name,\n description: options.description,\n tokenHash: hash,\n tokenPrefix: prefix,\n tokenSuffix: suffix,\n roles: options.roles,\n expiresAt: options.expiresAt?.toISOString(),\n });\n\n this.log.info(\"API key created\", {\n apiKeyId: apiKey.id,\n userId: options.userId,\n name: options.name,\n });\n\n return { apiKey, token };\n }\n\n /**\n * List all non-revoked API keys for a user.\n */\n public async list(userId: string): Promise<ApiKeyEntity[]> {\n return this.repo.findMany({\n where: {\n userId: { eq: userId },\n revokedAt: { isNull: true },\n },\n orderBy: { column: \"createdAt\", direction: \"desc\" },\n });\n }\n\n // -------------------------------------------------------------------------\n // Admin Operations\n // -------------------------------------------------------------------------\n\n /**\n * Find all API keys with optional filtering (admin only).\n */\n public async findAll(query: {\n userId?: string;\n includeRevoked?: boolean;\n page?: number;\n size?: number;\n sort?: string;\n }) {\n query.sort ??= \"-createdAt\";\n\n const where = this.repo.createQueryWhere();\n\n if (query.userId) {\n where.userId = { eq: query.userId };\n }\n\n if (!query.includeRevoked) {\n where.revokedAt = { isNull: true };\n }\n\n return this.repo.paginate(query, { where }, { count: true });\n }\n\n /**\n * Get an API key by ID (admin only).\n */\n public async getById(id: string): Promise<ApiKeyEntity> {\n const apiKey = await this.repo.findById(id).catch(() => null);\n\n if (!apiKey) {\n throw new NotFoundError(\"API key not found\");\n }\n\n return apiKey;\n }\n\n /**\n * Revoke any API key (admin only).\n */\n public async revokeByAdmin(id: string): Promise<void> {\n const apiKey = await this.repo.findById(id).catch(() => null);\n\n if (!apiKey) {\n throw new NotFoundError(\"API key not found\");\n }\n\n if (apiKey.revokedAt) {\n return; // Already revoked\n }\n\n // Invalidate cache\n await this.validationCache.invalidate(apiKey.tokenHash);\n\n await this.repo.updateById(id, {\n revokedAt: this.dateTimeProvider.now().toISOString(),\n });\n\n this.log.info(\"API key revoked by admin\", {\n apiKeyId: id,\n userId: apiKey.userId,\n });\n }\n\n // -------------------------------------------------------------------------\n // User Operations\n // -------------------------------------------------------------------------\n\n /**\n * Revoke an API key. Only the owner can revoke their own keys.\n */\n public async revoke(id: string, userId: string): Promise<void> {\n const apiKey = await this.repo.findById(id).catch(() => null);\n\n if (!apiKey) {\n throw new NotFoundError(\"API key not found\");\n }\n\n if (apiKey.userId !== userId) {\n throw new ForbiddenError(\"Not your API key\");\n }\n\n await this.validationCache.invalidate(apiKey.tokenHash);\n\n await this.repo.updateById(id, {\n revokedAt: this.dateTimeProvider.now().toISOString(),\n });\n\n this.log.info(\"API key revoked\", {\n apiKeyId: id,\n userId,\n });\n }\n\n // -------------------------------------------------------------------------\n // Validation\n // -------------------------------------------------------------------------\n\n /**\n * Validate an API key token and return user info if valid.\n */\n public async validate(token: string): Promise<UserInfo | null> {\n // Quick check for API key format\n if (!token.includes(\"_\")) {\n return null;\n }\n\n const hash = this.hashToken(token);\n\n // Try cache first\n let apiKey = await this.validationCache.get(hash);\n\n // If not in cache, look up in database\n if (apiKey === undefined) {\n apiKey = await this.repo\n .findOne({\n where: { tokenHash: { eq: hash } },\n })\n .catch(() => null);\n\n // Store in cache (even if null, to prevent repeated lookups)\n if (apiKey) {\n await this.validationCache.set(hash, apiKey);\n }\n }\n\n if (!apiKey) {\n return null;\n }\n\n // Check revocation\n if (apiKey.revokedAt) {\n return null;\n }\n\n // Check expiration\n if (\n apiKey.expiresAt &&\n this.dateTimeProvider.now().isAfter(apiKey.expiresAt)\n ) {\n return null;\n }\n\n // Update usage stats (fire and forget)\n this.updateUsage(apiKey.id).catch((error) => {\n this.log.warn(\"Failed to update API key usage\", { error });\n });\n\n return {\n id: apiKey.userId,\n roles: apiKey.roles,\n };\n }\n\n /**\n * Update usage statistics for an API key.\n */\n protected async updateUsage(id: string): Promise<void> {\n const request = this.alepha.context.get<ServerRequest>(\"request\");\n\n await this.repo.updateById(id, {\n lastUsedAt: this.dateTimeProvider.now().toISOString(),\n lastUsedIp: request?.ip,\n usageCount: sql`${this.repo.table.usageCount} + 1`,\n });\n }\n\n /**\n * Hash a token using SHA-256.\n */\n protected hashToken(token: string): string {\n return createHash(\"sha256\").update(token).digest(\"hex\");\n }\n}\n","import { $inject, t } from \"alepha\";\nimport { $action, okSchema } from \"alepha/server\";\nimport { adminApiKeyQuerySchema } from \"../schemas/adminApiKeyQuerySchema.ts\";\nimport { adminApiKeyResourceSchema } from \"../schemas/adminApiKeyResourceSchema.ts\";\nimport { ApiKeyService } from \"../services/ApiKeyService.ts\";\n\n/**\n * REST API controller for admin API key management.\n * Admins can list, view, and revoke any API key.\n */\nexport class AdminApiKeyController {\n protected readonly url = \"/admin/api-keys\";\n protected readonly group = \"admin:api-keys\";\n protected readonly apiKeyService = $inject(ApiKeyService);\n\n /**\n * Find all API keys with optional filtering.\n */\n public readonly findApiKeys = $action({\n path: this.url,\n group: this.group,\n secure: true,\n description: \"Find API keys with pagination and filtering\",\n schema: {\n query: adminApiKeyQuerySchema,\n response: t.page(adminApiKeyResourceSchema),\n },\n handler: ({ query }) => {\n const { userId, includeRevoked, ...pagination } = query;\n return this.apiKeyService.findAll({\n userId,\n includeRevoked,\n ...pagination,\n });\n },\n });\n\n /**\n * Get an API key by ID.\n */\n public readonly getApiKey = $action({\n path: `${this.url}/:id`,\n group: this.group,\n secure: true,\n description: \"Get an API key by ID\",\n schema: {\n params: t.object({\n id: t.uuid(),\n }),\n response: adminApiKeyResourceSchema,\n },\n handler: ({ params }) => this.apiKeyService.getById(params.id),\n });\n\n /**\n * Revoke any API key.\n */\n public readonly revokeApiKey = $action({\n method: \"DELETE\",\n path: `${this.url}/:id`,\n group: this.group,\n secure: true,\n description: \"Revoke an API key\",\n schema: {\n params: t.object({\n id: t.uuid(),\n }),\n response: okSchema,\n },\n handler: async ({ params }) => {\n await this.apiKeyService.revokeByAdmin(params.id);\n return { ok: true, id: params.id };\n },\n });\n}\n","import { t } from \"alepha\";\n\nexport const createApiKeyBodySchema = t.object({\n name: t.text({ minLength: 1, maxLength: 100 }),\n description: t.optional(t.text({ maxLength: 500 })),\n expiresAt: t.optional(t.datetime()),\n});\n","import { t } from \"alepha\";\n\nexport const createApiKeyResponseSchema = t.object({\n id: t.uuid(),\n name: t.string(),\n token: t.string(),\n tokenSuffix: t.string(),\n roles: t.array(t.string()),\n createdAt: t.datetime(),\n expiresAt: t.optional(t.datetime()),\n});\n","import { t } from \"alepha\";\n\nexport const listApiKeyItemSchema = t.object({\n id: t.uuid(),\n name: t.string(),\n tokenPrefix: t.string(),\n tokenSuffix: t.string(),\n roles: t.array(t.string()),\n createdAt: t.datetime(),\n lastUsedAt: t.optional(t.datetime()),\n expiresAt: t.optional(t.datetime()),\n usageCount: t.integer(),\n});\n\nexport const listApiKeyResponseSchema = t.array(listApiKeyItemSchema);\n","import { t } from \"alepha\";\n\nexport const revokeApiKeyParamsSchema = t.object({\n id: t.uuid(),\n});\n","import { t } from \"alepha\";\n\nexport const revokeApiKeyResponseSchema = t.object({\n ok: t.boolean(),\n});\n","import { $inject } from \"alepha\";\nimport { $action } from \"alepha/server\";\nimport { createApiKeyBodySchema } from \"../schemas/createApiKeyBodySchema.ts\";\nimport { createApiKeyResponseSchema } from \"../schemas/createApiKeyResponseSchema.ts\";\nimport { listApiKeyResponseSchema } from \"../schemas/listApiKeyResponseSchema.ts\";\nimport { revokeApiKeyParamsSchema } from \"../schemas/revokeApiKeyParamsSchema.ts\";\nimport { revokeApiKeyResponseSchema } from \"../schemas/revokeApiKeyResponseSchema.ts\";\nimport { ApiKeyService } from \"../services/ApiKeyService.ts\";\n\n/**\n * REST API controller for user's own API key management.\n * Users can create, list, and revoke their own API keys.\n */\nexport class ApiKeyController {\n protected readonly url = \"/api-keys\";\n protected readonly group = \"api-keys\";\n protected readonly apiKeyService = $inject(ApiKeyService);\n\n /**\n * Create a new API key for the authenticated user.\n * The token is only returned once upon creation.\n */\n public readonly createApiKey = $action({\n method: \"POST\",\n path: this.url,\n group: this.group,\n description: \"Create a new API key\",\n secure: true,\n schema: {\n body: createApiKeyBodySchema,\n response: createApiKeyResponseSchema,\n },\n handler: async (request) => {\n const { apiKey, token } = await this.apiKeyService.create({\n userId: request.user.id,\n name: request.body.name,\n description: request.body.description,\n roles: request.user.roles ?? [],\n expiresAt: request.body.expiresAt\n ? new Date(request.body.expiresAt)\n : undefined,\n });\n\n return {\n id: apiKey.id,\n name: apiKey.name,\n token,\n tokenSuffix: apiKey.tokenSuffix,\n roles: apiKey.roles,\n createdAt: apiKey.createdAt,\n expiresAt: apiKey.expiresAt,\n };\n },\n });\n\n /**\n * List all active API keys for the authenticated user.\n * Does not return the actual tokens.\n */\n public readonly listApiKeys = $action({\n path: this.url,\n group: this.group,\n description: \"List your API keys\",\n secure: true,\n schema: {\n response: listApiKeyResponseSchema,\n },\n handler: async (request) => {\n const apiKeys = await this.apiKeyService.list(request.user.id);\n\n return apiKeys.map((apiKey) => ({\n id: apiKey.id,\n name: apiKey.name,\n tokenPrefix: apiKey.tokenPrefix,\n tokenSuffix: apiKey.tokenSuffix,\n roles: apiKey.roles,\n createdAt: apiKey.createdAt,\n lastUsedAt: apiKey.lastUsedAt,\n expiresAt: apiKey.expiresAt,\n usageCount: apiKey.usageCount,\n }));\n },\n });\n\n /**\n * Revoke an API key. Only the owner can revoke their own keys.\n */\n public readonly revokeApiKey = $action({\n method: \"DELETE\",\n path: `${this.url}/:id`,\n group: this.group,\n description: \"Revoke an API key\",\n secure: true,\n schema: {\n params: revokeApiKeyParamsSchema,\n response: revokeApiKeyResponseSchema,\n },\n handler: async (request) => {\n await this.apiKeyService.revoke(request.params.id, request.user.id);\n return { ok: true };\n },\n });\n}\n","import { $module } from \"alepha\";\nimport { AdminApiKeyController } from \"./controllers/AdminApiKeyController.ts\";\nimport { ApiKeyController } from \"./controllers/ApiKeyController.ts\";\nimport { ApiKeyService } from \"./services/ApiKeyService.ts\";\n\nexport * from \"./controllers/AdminApiKeyController.ts\";\nexport * from \"./controllers/ApiKeyController.ts\";\nexport * from \"./entities/apiKeyEntity.ts\";\nexport * from \"./schemas/adminApiKeyQuerySchema.ts\";\nexport * from \"./schemas/adminApiKeyResourceSchema.ts\";\nexport * from \"./schemas/createApiKeyBodySchema.ts\";\nexport * from \"./schemas/createApiKeyResponseSchema.ts\";\nexport * from \"./schemas/listApiKeyResponseSchema.ts\";\nexport * from \"./schemas/revokeApiKeyParamsSchema.ts\";\nexport * from \"./schemas/revokeApiKeyResponseSchema.ts\";\nexport * from \"./services/ApiKeyService.ts\";\n\n/**\n * | type | quality | stability |\n * |------|---------|--------------|\n * | backend | good | experimental |\n *\n * API key management module for programmatic access.\n *\n * **Features:**\n * - Create API keys with role snapshots\n * - List and revoke API keys\n * - 15-minute validation caching\n * - Query param (?api_key=) and Bearer header support\n *\n * **Integration:**\n * To enable API key authentication for an issuer, register the resolver:\n *\n * ```ts\n * class MyApp {\n * apiKeyService = $inject(ApiKeyService);\n * issuer = $issuer({\n * secret: env.APP_SECRET,\n * resolvers: [this.apiKeyService.createResolver()],\n * });\n * }\n * ```\n *\n * @module alepha.api.keys\n */\nexport const AlephaApiKeys = $module({\n name: \"alepha.api.keys\",\n services: [ApiKeyService, ApiKeyController, AdminApiKeyController],\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/api/keys/schemas/adminApiKeyQuerySchema.ts","../../../src/api/keys/schemas/adminApiKeyResourceSchema.ts","../../../src/api/keys/entities/apiKeyEntity.ts","../../../src/api/keys/services/ApiKeyService.ts","../../../src/api/keys/controllers/AdminApiKeyController.ts","../../../src/api/keys/schemas/createApiKeyBodySchema.ts","../../../src/api/keys/schemas/createApiKeyResponseSchema.ts","../../../src/api/keys/schemas/listApiKeyResponseSchema.ts","../../../src/api/keys/schemas/revokeApiKeyParamsSchema.ts","../../../src/api/keys/schemas/revokeApiKeyResponseSchema.ts","../../../src/api/keys/controllers/ApiKeyController.ts","../../../src/api/keys/index.ts"],"sourcesContent":["import { t } from \"alepha\";\nimport { pageQuerySchema } from \"alepha/orm\";\n\nexport const adminApiKeyQuerySchema = t.extend(pageQuerySchema, {\n userId: t.optional(t.uuid()),\n includeRevoked: t.optional(t.boolean()),\n});\n","import { t } from \"alepha\";\n\nexport const adminApiKeyResourceSchema = t.object({\n id: t.uuid(),\n userId: t.uuid(),\n name: t.string(),\n description: t.optional(t.string()),\n tokenPrefix: t.string(),\n tokenSuffix: t.string(),\n roles: t.array(t.string()),\n createdAt: t.datetime(),\n lastUsedAt: t.optional(t.datetime()),\n lastUsedIp: t.optional(t.string()),\n expiresAt: t.optional(t.datetime()),\n revokedAt: t.optional(t.datetime()),\n usageCount: t.integer(),\n});\n","import { type Static, t } from \"alepha\";\nimport { $entity, db } from \"alepha/orm\";\n\nexport const apiKeyEntity = $entity({\n name: \"api_keys\",\n schema: t.object({\n id: db.primaryKey(t.uuid()),\n createdAt: db.createdAt(),\n updatedAt: db.updatedAt(),\n\n // Owner\n userId: t.uuid(),\n\n // Key metadata\n name: t.text({ maxLength: 100 }),\n description: t.optional(t.text({ maxLength: 500 })),\n\n // Token (hashed) - internal, not user input\n tokenHash: t.string({ maxLength: 256 }),\n tokenPrefix: t.string({ maxLength: 10 }),\n tokenSuffix: t.string({ maxLength: 8 }),\n\n // Roles (snapshot from user at creation)\n roles: db.default(t.array(t.string()), []),\n\n // Tracking\n lastUsedAt: t.optional(t.datetime()),\n lastUsedIp: t.optional(t.string({ maxLength: 45 })),\n usageCount: db.default(t.integer(), 0),\n\n // Lifecycle\n expiresAt: t.optional(t.datetime()),\n revokedAt: t.optional(t.datetime()),\n }),\n indexes: [\n { columns: [\"userId\", \"name\"], unique: true },\n { columns: [\"tokenHash\"], unique: true },\n ],\n});\n\nexport type ApiKeyEntity = Static<typeof apiKeyEntity.schema>;\n","import { createHash, randomBytes } from \"node:crypto\";\nimport { $inject, Alepha } from \"alepha\";\nimport { $cache } from \"alepha/cache\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository, sql } from \"alepha/orm\";\nimport type { IssuerResolver, UserInfo } from \"alepha/security\";\nimport {\n ForbiddenError,\n NotFoundError,\n type ServerRequest,\n} from \"alepha/server\";\nimport { type ApiKeyEntity, apiKeyEntity } from \"../entities/apiKeyEntity.ts\";\n\nexport class ApiKeyService {\n protected readonly alepha = $inject(Alepha);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly log = $logger();\n protected readonly repo = $repository(apiKeyEntity);\n\n /**\n * Cache validated API keys for 15 minutes.\n */\n protected readonly validationCache = $cache<ApiKeyEntity | null, [string]>({\n name: \"api-key-validation\",\n ttl: [15, \"minutes\"],\n });\n\n // -------------------------------------------------------------------------\n // Resolver\n // -------------------------------------------------------------------------\n\n /**\n * Create an issuer resolver for API key authentication.\n * Lower priority means it runs before JWT resolver.\n *\n * @param options.priority - Priority of this resolver (default: 50, JWT is 100)\n * @param options.prefix - API key prefix to match in Bearer header (default: \"ak\")\n */\n public createResolver(\n options: { priority?: number; prefix?: string } = {},\n ): IssuerResolver {\n const { priority = 50, prefix = \"ak\" } = options;\n const prefixPattern = `${prefix}_`;\n\n return {\n priority,\n onRequest: async (req: ServerRequest) => {\n // Try query param first\n const url = typeof req.url === \"string\" ? new URL(req.url) : req.url;\n let token = url.searchParams.get(\"api_key\");\n\n // Try Bearer header - only if token starts with expected prefix\n if (!token) {\n const auth = req.headers.authorization;\n if (auth?.startsWith(\"Bearer \")) {\n const bearerToken = auth.slice(7);\n if (bearerToken.startsWith(prefixPattern)) {\n token = bearerToken;\n }\n }\n }\n\n if (!token) {\n return null;\n }\n\n return this.validate(token);\n },\n };\n }\n\n // -------------------------------------------------------------------------\n // CRUD\n // -------------------------------------------------------------------------\n\n /**\n * Create a new API key for a user.\n * Returns both the API key entity and the plain token (which is only available once).\n */\n public async create(options: {\n userId: string;\n name: string;\n roles: string[];\n description?: string;\n expiresAt?: Date;\n prefix?: string;\n }): Promise<{ apiKey: ApiKeyEntity; token: string }> {\n const prefix = options.prefix ?? \"ak\";\n const random = randomBytes(24).toString(\"base64url\");\n const token = `${prefix}_${random}`;\n const hash = this.hashToken(token);\n const suffix = token.slice(-8);\n\n const apiKey = await this.repo.create({\n userId: options.userId,\n name: options.name,\n description: options.description,\n tokenHash: hash,\n tokenPrefix: prefix,\n tokenSuffix: suffix,\n roles: options.roles,\n expiresAt: options.expiresAt?.toISOString(),\n });\n\n this.log.info(\"API key created\", {\n apiKeyId: apiKey.id,\n userId: options.userId,\n name: options.name,\n });\n\n return { apiKey, token };\n }\n\n /**\n * List all non-revoked API keys for a user.\n */\n public async list(userId: string): Promise<ApiKeyEntity[]> {\n return this.repo.findMany({\n where: {\n userId: { eq: userId },\n revokedAt: { isNull: true },\n },\n orderBy: { column: \"createdAt\", direction: \"desc\" },\n });\n }\n\n // -------------------------------------------------------------------------\n // Admin Operations\n // -------------------------------------------------------------------------\n\n /**\n * Find all API keys with optional filtering (admin only).\n */\n public async findAll(query: {\n userId?: string;\n includeRevoked?: boolean;\n page?: number;\n size?: number;\n sort?: string;\n }) {\n query.sort ??= \"-createdAt\";\n\n const where = this.repo.createQueryWhere();\n\n if (query.userId) {\n where.userId = { eq: query.userId };\n }\n\n if (!query.includeRevoked) {\n where.revokedAt = { isNull: true };\n }\n\n return this.repo.paginate(query, { where }, { count: true });\n }\n\n /**\n * Get an API key by ID (admin only).\n */\n public async getById(id: string): Promise<ApiKeyEntity> {\n const apiKey = await this.repo.findById(id).catch(() => null);\n\n if (!apiKey) {\n throw new NotFoundError(\"API key not found\");\n }\n\n return apiKey;\n }\n\n /**\n * Revoke any API key (admin only).\n */\n public async revokeByAdmin(id: string): Promise<void> {\n const apiKey = await this.repo.findById(id).catch(() => null);\n\n if (!apiKey) {\n throw new NotFoundError(\"API key not found\");\n }\n\n if (apiKey.revokedAt) {\n return; // Already revoked\n }\n\n // Invalidate cache\n await this.validationCache.invalidate(apiKey.tokenHash);\n\n await this.repo.updateById(id, {\n revokedAt: this.dateTimeProvider.now().toISOString(),\n });\n\n this.log.info(\"API key revoked by admin\", {\n apiKeyId: id,\n userId: apiKey.userId,\n });\n }\n\n // -------------------------------------------------------------------------\n // User Operations\n // -------------------------------------------------------------------------\n\n /**\n * Revoke an API key. Only the owner can revoke their own keys.\n */\n public async revoke(id: string, userId: string): Promise<void> {\n const apiKey = await this.repo.findById(id).catch(() => null);\n\n if (!apiKey) {\n throw new NotFoundError(\"API key not found\");\n }\n\n if (apiKey.userId !== userId) {\n throw new ForbiddenError(\"Not your API key\");\n }\n\n await this.validationCache.invalidate(apiKey.tokenHash);\n\n await this.repo.updateById(id, {\n revokedAt: this.dateTimeProvider.now().toISOString(),\n });\n\n this.log.info(\"API key revoked\", {\n apiKeyId: id,\n userId,\n });\n }\n\n // -------------------------------------------------------------------------\n // Validation\n // -------------------------------------------------------------------------\n\n /**\n * Validate an API key token and return user info if valid.\n */\n public async validate(token: string): Promise<UserInfo | null> {\n // Quick check for API key format\n if (!token.includes(\"_\")) {\n return null;\n }\n\n const hash = this.hashToken(token);\n\n // Try cache first\n let apiKey = await this.validationCache.get(hash);\n\n // If not in cache, look up in database\n if (apiKey === undefined) {\n apiKey = await this.repo\n .findOne({\n where: { tokenHash: { eq: hash } },\n })\n .catch(() => null);\n\n // Store in cache (even if null, to prevent repeated lookups)\n if (apiKey) {\n await this.validationCache.set(hash, apiKey);\n }\n }\n\n if (!apiKey) {\n return null;\n }\n\n // Check revocation\n if (apiKey.revokedAt) {\n return null;\n }\n\n // Check expiration\n if (\n apiKey.expiresAt &&\n this.dateTimeProvider.now().isAfter(apiKey.expiresAt)\n ) {\n return null;\n }\n\n // Update usage stats (fire and forget)\n this.updateUsage(apiKey.id).catch((error) => {\n this.log.warn(\"Failed to update API key usage\", { error });\n });\n\n return {\n id: apiKey.userId,\n roles: apiKey.roles,\n };\n }\n\n /**\n * Update usage statistics for an API key.\n */\n protected async updateUsage(id: string): Promise<void> {\n const request = this.alepha.context.get<ServerRequest>(\"request\");\n\n await this.repo.updateById(id, {\n lastUsedAt: this.dateTimeProvider.now().toISOString(),\n lastUsedIp: request?.ip,\n usageCount: sql`${this.repo.table.usageCount} + 1`,\n });\n }\n\n /**\n * Hash a token using SHA-256.\n */\n protected hashToken(token: string): string {\n return createHash(\"sha256\").update(token).digest(\"hex\");\n }\n}\n","import { $inject, t } from \"alepha\";\nimport { $action, okSchema } from \"alepha/server\";\nimport { adminApiKeyQuerySchema } from \"../schemas/adminApiKeyQuerySchema.ts\";\nimport { adminApiKeyResourceSchema } from \"../schemas/adminApiKeyResourceSchema.ts\";\nimport { ApiKeyService } from \"../services/ApiKeyService.ts\";\n\n/**\n * REST API controller for admin API key management.\n * Admins can list, view, and revoke any API key.\n */\nexport class AdminApiKeyController {\n protected readonly url = \"/admin/api-keys\";\n protected readonly group = \"admin:api-keys\";\n protected readonly apiKeyService = $inject(ApiKeyService);\n\n /**\n * Find all API keys with optional filtering.\n */\n public readonly findApiKeys = $action({\n path: this.url,\n group: this.group,\n secure: true,\n description: \"Find API keys with pagination and filtering\",\n schema: {\n query: adminApiKeyQuerySchema,\n response: t.page(adminApiKeyResourceSchema),\n },\n handler: ({ query }) => {\n const { userId, includeRevoked, ...pagination } = query;\n return this.apiKeyService.findAll({\n userId,\n includeRevoked,\n ...pagination,\n });\n },\n });\n\n /**\n * Get an API key by ID.\n */\n public readonly getApiKey = $action({\n path: `${this.url}/:id`,\n group: this.group,\n secure: true,\n description: \"Get an API key by ID\",\n schema: {\n params: t.object({\n id: t.uuid(),\n }),\n response: adminApiKeyResourceSchema,\n },\n handler: ({ params }) => this.apiKeyService.getById(params.id),\n });\n\n /**\n * Revoke any API key.\n */\n public readonly revokeApiKey = $action({\n method: \"DELETE\",\n path: `${this.url}/:id`,\n group: this.group,\n secure: true,\n description: \"Revoke an API key\",\n schema: {\n params: t.object({\n id: t.uuid(),\n }),\n response: okSchema,\n },\n handler: async ({ params }) => {\n await this.apiKeyService.revokeByAdmin(params.id);\n return { ok: true, id: params.id };\n },\n });\n}\n","import { t } from \"alepha\";\n\nexport const createApiKeyBodySchema = t.object({\n name: t.text({ minLength: 1, maxLength: 100 }),\n description: t.optional(t.text({ maxLength: 500 })),\n expiresAt: t.optional(t.datetime()),\n});\n","import { t } from \"alepha\";\n\nexport const createApiKeyResponseSchema = t.object({\n id: t.uuid(),\n name: t.string(),\n token: t.string(),\n tokenSuffix: t.string(),\n roles: t.array(t.string()),\n createdAt: t.datetime(),\n expiresAt: t.optional(t.datetime()),\n});\n","import { t } from \"alepha\";\n\nexport const listApiKeyItemSchema = t.object({\n id: t.uuid(),\n name: t.string(),\n tokenPrefix: t.string(),\n tokenSuffix: t.string(),\n roles: t.array(t.string()),\n createdAt: t.datetime(),\n lastUsedAt: t.optional(t.datetime()),\n expiresAt: t.optional(t.datetime()),\n usageCount: t.integer(),\n});\n\nexport const listApiKeyResponseSchema = t.array(listApiKeyItemSchema);\n","import { t } from \"alepha\";\n\nexport const revokeApiKeyParamsSchema = t.object({\n id: t.uuid(),\n});\n","import { t } from \"alepha\";\n\nexport const revokeApiKeyResponseSchema = t.object({\n ok: t.boolean(),\n});\n","import { $inject } from \"alepha\";\nimport { $action } from \"alepha/server\";\nimport { createApiKeyBodySchema } from \"../schemas/createApiKeyBodySchema.ts\";\nimport { createApiKeyResponseSchema } from \"../schemas/createApiKeyResponseSchema.ts\";\nimport { listApiKeyResponseSchema } from \"../schemas/listApiKeyResponseSchema.ts\";\nimport { revokeApiKeyParamsSchema } from \"../schemas/revokeApiKeyParamsSchema.ts\";\nimport { revokeApiKeyResponseSchema } from \"../schemas/revokeApiKeyResponseSchema.ts\";\nimport { ApiKeyService } from \"../services/ApiKeyService.ts\";\n\n/**\n * REST API controller for user's own API key management.\n * Users can create, list, and revoke their own API keys.\n */\nexport class ApiKeyController {\n protected readonly url = \"/api-keys\";\n protected readonly group = \"api-keys\";\n protected readonly apiKeyService = $inject(ApiKeyService);\n\n /**\n * Create a new API key for the authenticated user.\n * The token is only returned once upon creation.\n */\n public readonly createApiKey = $action({\n method: \"POST\",\n path: this.url,\n group: this.group,\n description: \"Create a new API key\",\n secure: true,\n schema: {\n body: createApiKeyBodySchema,\n response: createApiKeyResponseSchema,\n },\n handler: async (request) => {\n const { apiKey, token } = await this.apiKeyService.create({\n userId: request.user.id,\n name: request.body.name,\n description: request.body.description,\n roles: request.user.roles ?? [],\n expiresAt: request.body.expiresAt\n ? new Date(request.body.expiresAt)\n : undefined,\n });\n\n return {\n id: apiKey.id,\n name: apiKey.name,\n token,\n tokenSuffix: apiKey.tokenSuffix,\n roles: apiKey.roles,\n createdAt: apiKey.createdAt,\n expiresAt: apiKey.expiresAt,\n };\n },\n });\n\n /**\n * List all active API keys for the authenticated user.\n * Does not return the actual tokens.\n */\n public readonly listApiKeys = $action({\n path: this.url,\n group: this.group,\n description: \"List your API keys\",\n secure: true,\n schema: {\n response: listApiKeyResponseSchema,\n },\n handler: async (request) => {\n const apiKeys = await this.apiKeyService.list(request.user.id);\n\n return apiKeys.map((apiKey) => ({\n id: apiKey.id,\n name: apiKey.name,\n tokenPrefix: apiKey.tokenPrefix,\n tokenSuffix: apiKey.tokenSuffix,\n roles: apiKey.roles,\n createdAt: apiKey.createdAt,\n lastUsedAt: apiKey.lastUsedAt,\n expiresAt: apiKey.expiresAt,\n usageCount: apiKey.usageCount,\n }));\n },\n });\n\n /**\n * Revoke an API key. Only the owner can revoke their own keys.\n */\n public readonly revokeApiKey = $action({\n method: \"DELETE\",\n path: `${this.url}/:id`,\n group: this.group,\n description: \"Revoke an API key\",\n secure: true,\n schema: {\n params: revokeApiKeyParamsSchema,\n response: revokeApiKeyResponseSchema,\n },\n handler: async (request) => {\n await this.apiKeyService.revoke(request.params.id, request.user.id);\n return { ok: true };\n },\n });\n}\n","import { $module } from \"alepha\";\nimport { AdminApiKeyController } from \"./controllers/AdminApiKeyController.ts\";\nimport { ApiKeyController } from \"./controllers/ApiKeyController.ts\";\nimport { ApiKeyService } from \"./services/ApiKeyService.ts\";\n\nexport * from \"./controllers/AdminApiKeyController.ts\";\nexport * from \"./controllers/ApiKeyController.ts\";\nexport * from \"./entities/apiKeyEntity.ts\";\nexport * from \"./schemas/adminApiKeyQuerySchema.ts\";\nexport * from \"./schemas/adminApiKeyResourceSchema.ts\";\nexport * from \"./schemas/createApiKeyBodySchema.ts\";\nexport * from \"./schemas/createApiKeyResponseSchema.ts\";\nexport * from \"./schemas/listApiKeyResponseSchema.ts\";\nexport * from \"./schemas/revokeApiKeyParamsSchema.ts\";\nexport * from \"./schemas/revokeApiKeyResponseSchema.ts\";\nexport * from \"./services/ApiKeyService.ts\";\n\n/**\n * | Stability | Since | Runtime |\n * |-----------|-------|---------|\n * | 3 - stable | 0.11.0 | node, bun, workerd|\n *\n * API key management module for programmatic access.\n *\n * **Features:**\n * - Create API keys with role snapshots\n * - List and revoke API keys\n * - 15-minute validation caching\n * - Query param (?api_key=) and Bearer header support\n *\n * **Integration:**\n * To enable API key authentication for an issuer, register the resolver:\n *\n * ```ts\n * class MyApp {\n * apiKeyService = $inject(ApiKeyService);\n * issuer = $issuer({\n * secret: env.APP_SECRET,\n * resolvers: [this.apiKeyService.createResolver()],\n * });\n * }\n * ```\n *\n * @module alepha.api.keys\n */\nexport const AlephaApiKeys = $module({\n name: \"alepha.api.keys\",\n services: [ApiKeyService, ApiKeyController, AdminApiKeyController],\n});\n"],"mappings":"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"}
@@ -344,6 +344,7 @@ var AdminNotificationController = class {
344
344
  findNotifications = $action({
345
345
  path: this.url,
346
346
  group: this.group,
347
+ secure: true,
347
348
  description: "Find notifications with pagination and filtering",
348
349
  schema: {
349
350
  query: notificationQuerySchema,
@@ -1 +1 @@
1
- {"version":3,"file":"index.browser.js","names":[],"sources":["../../../src/api/notifications/entities/notifications.ts","../../../src/api/notifications/schemas/notificationQuerySchema.ts","../../../src/api/notifications/primitives/$notification.ts","../../../src/api/notifications/services/NotificationSenderService.ts","../../../src/api/notifications/queues/NotificationQueues.ts","../../../src/api/notifications/schemas/notificationCreateSchema.ts","../../../src/api/notifications/services/NotificationService.ts","../../../src/api/notifications/controllers/AdminNotificationController.ts","../../../src/api/notifications/schemas/notificationContactPreferencesSchema.ts","../../../src/api/notifications/schemas/notificationContactSchema.ts","../../../src/api/notifications/index.browser.ts"],"sourcesContent":["import { type Static, t } from \"alepha\";\nimport { $entity, db } from \"alepha/orm\";\n\nexport const notifications = $entity({\n name: \"notifications\",\n schema: t.object({\n id: db.primaryKey(t.uuid()),\n\n version: db.version(),\n\n createdAt: db.createdAt(),\n\n updatedAt: db.updatedAt(),\n\n // -----------------------------------------------------------------------------------------------------------------\n\n type: t.enum([\"email\", \"sms\"]),\n\n template: t.text(), // e.g. 'resetPassword'\n\n category: t.optional(\n t.text({\n description:\n \"For grouping related notifications (e.g., 'authentication', 'marketing'). Contact can filter notifications by category.\",\n }),\n ),\n\n critical: t.optional(\n t.boolean({\n description:\n \"Prioritize delivery of this notification. Set to true for important system alerts.\",\n }),\n ),\n\n sensitive: t.optional(\n t.boolean({\n description:\n \"Message won't be logged or stored in plain text. Set to true when notification contains passwords or codes.\",\n }),\n ),\n\n // -----------------------------------------------------------------------------------------------------------------\n\n contact: t.text(), // e.g. email address or phone number or user ID or whatever\n\n variables: t.optional(t.record(t.text(), t.any())),\n\n scheduledAt: t.optional(\n t.datetime({\n description:\n \"When set, the notification will be sent at or after this date/time.\",\n }),\n ),\n\n // -----------------------------------------------------------------------------------------------------------------\n\n sentAt: t.optional(t.datetime()),\n\n error: t.optional(\n t.object({\n at: t.datetime(),\n name: t.text(),\n message: t.text({ size: \"rich\" }),\n }),\n ),\n\n // TODO: retryCount, lastRetryAt, etc.\n }),\n});\n\nexport type NotificationEntity = Static<typeof notifications.schema>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\nimport { pageQuerySchema } from \"alepha/orm\";\n\nexport const notificationQuerySchema = t.extend(pageQuerySchema, {\n type: t.optional(t.enum([\"email\", \"sms\"])),\n template: t.optional(t.string()),\n contact: t.optional(t.string()),\n category: t.optional(t.string()),\n status: t.optional(t.enum([\"pending\", \"sent\", \"failed\"])),\n});\n\nexport type NotificationQuery = Static<typeof notificationQuerySchema>;\n","import {\n $inject,\n createPrimitive,\n KIND,\n Primitive,\n type Static,\n type StaticEncode,\n type TObject,\n} from \"alepha\";\nimport { NotificationService } from \"../services/NotificationService.ts\";\n\n/**\n * Creates a notification primitive for managing email/SMS notification templates.\n *\n * Provides type-safe, reusable notification templates with multi-language support,\n * variable substitution, and categorization for different notification channels.\n *\n * @example\n * ```ts\n * class NotificationTemplates {\n * welcomeEmail = $notification({\n * name: \"welcome-email\",\n * category: \"onboarding\",\n * schema: t.object({ username: t.text(), activationLink: t.text() }),\n * email: {\n * subject: \"Welcome to our platform!\",\n * body: (vars) => `Hello ${vars.username}, click: ${vars.activationLink}`\n * }\n * });\n *\n * async sendWelcome(user: User) {\n * await this.welcomeEmail.push({\n * variables: { username: user.name, activationLink: generateLink() },\n * contact: user.email\n * });\n * }\n * }\n * ```\n */\nexport const $notification = <T extends TObject>(\n options: NotificationPrimitiveOptions<T>,\n) => createPrimitive(NotificationPrimitive<T>, options);\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface NotificationPrimitiveOptions<T extends TObject>\n extends NotificationMessage<T> {\n name?: string;\n description?: string;\n category?: string;\n critical?: boolean;\n sensitive?: boolean;\n translations?: {\n // e.g., \"en\", \"fr\", even \"en-US\"\n [lang: string]: NotificationMessage<T>;\n };\n schema: T;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class NotificationPrimitive<T extends TObject> extends Primitive<\n NotificationPrimitiveOptions<T>\n> {\n protected readonly notificationService = $inject(NotificationService);\n\n public get name() {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n public async push(options: NotificationPushOptions<T>) {\n if (this.options.email) {\n await this.notificationService.createNotification({\n ...options,\n type: \"email\",\n template: this.name,\n });\n }\n }\n\n public configure(options: Partial<NotificationPrimitiveOptions<T>>) {\n Object.assign(this.options, options);\n }\n}\n\n$notification[KIND] = NotificationPrimitive;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface NotificationPushOptions<T extends TObject> {\n variables: StaticEncode<T>;\n contact: string;\n}\n\nexport interface NotificationMessage<T extends TObject> {\n email?: {\n subject: string;\n body: string | ((variables: Static<T>) => string);\n };\n sms?: {\n message: string | ((variables: Static<T>) => string);\n };\n}\n","import { $inject, Alepha, AlephaError } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { EmailProvider } from \"alepha/email\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository } from \"alepha/orm\";\nimport { SmsProvider } from \"alepha/sms\";\nimport {\n type NotificationEntity,\n notifications,\n} from \"../entities/notifications.ts\";\nimport { $notification } from \"../primitives/$notification.ts\";\n\nexport class NotificationSenderService {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly notificationRepository = $repository(notifications);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly emailProvider = $inject(EmailProvider);\n protected readonly smsProvider = $inject(SmsProvider);\n\n public async send(notificationId: string | NotificationEntity) {\n this.log.trace(\"Sending notification\", {\n notificationId:\n typeof notificationId === \"string\" ? notificationId : notificationId.id,\n });\n\n const notification =\n typeof notificationId === \"string\"\n ? await this.notificationRepository.findById(notificationId)\n : notificationId;\n\n if (notification.sentAt) {\n this.log.debug(\"Notification already sent\", {\n notificationId: notification.id,\n sentAt: notification.sentAt,\n });\n return;\n }\n\n this.log.debug(\"Processing notification\", {\n id: notification.id,\n type: notification.type,\n template: notification.template,\n contact: notification.contact,\n });\n\n try {\n if (notification.type === \"email\") {\n await this.emailProvider.send(this.renderEmail(notification));\n notification.sentAt = this.dateTimeProvider.nowISOString();\n this.log.info(\"Email notification sent\", {\n id: notification.id,\n template: notification.template,\n contact: notification.contact,\n });\n }\n if (notification.type === \"sms\") {\n await this.smsProvider.send(this.renderSms(notification));\n notification.sentAt = this.dateTimeProvider.nowISOString();\n this.log.info(\"SMS notification sent\", {\n id: notification.id,\n template: notification.template,\n contact: notification.contact,\n });\n }\n } catch (e) {\n this.log.error(\"Failed to send notification\", {\n id: notification.id,\n type: notification.type,\n template: notification.template,\n contact: notification.contact,\n error: e,\n });\n if (e instanceof Error) {\n notification.error = {\n at: this.dateTimeProvider.nowISOString(),\n name: e.name,\n message: e.message,\n };\n }\n } finally {\n await this.notificationRepository.save(notification);\n }\n }\n\n public renderSms(notification: NotificationEntity) {\n this.log.trace(\"Rendering SMS notification\", {\n id: notification.id,\n template: notification.template,\n });\n\n const { variables, contact, template } = this.load(notification);\n\n const sms = template.options.sms;\n if (!sms) {\n this.log.error(\"Notification template has no SMS defined\", {\n id: notification.id,\n template: notification.template,\n });\n throw new AlephaError(\n `Notification template ${notification.template} has no sms defined`,\n );\n }\n\n this.log.debug(\"Rendering SMS\", {\n template: notification.template,\n contact,\n });\n\n const message =\n typeof sms.message === \"function\"\n ? sms.message(variables as any)\n : sms.message;\n\n return {\n to: contact,\n message,\n };\n }\n\n public renderEmail(notification: NotificationEntity) {\n this.log.trace(\"Rendering email notification\", {\n id: notification.id,\n template: notification.template,\n });\n\n const { variables, contact, template } = this.load(notification);\n\n const email = template.options.email;\n if (!email) {\n this.log.error(\"Notification template has no email defined\", {\n id: notification.id,\n template: notification.template,\n });\n throw new AlephaError(\n `Notification template ${notification.template} has no email defined`,\n );\n }\n\n this.log.debug(\"Rendering email\", {\n template: notification.template,\n contact,\n subject: email.subject,\n });\n\n const subject = email.subject;\n\n const body =\n typeof email.body === \"function\"\n ? email.body(variables as any)\n : email.body;\n\n return {\n to: contact,\n subject,\n body,\n };\n }\n\n protected load(notification: NotificationEntity) {\n const variables = notification.variables || {};\n const contact = notification.contact;\n const template = this.alepha\n .primitives($notification)\n .find((it) => it.name === notification.template);\n\n if (!template) {\n this.log.error(\"Notification template not found\", {\n id: notification.id,\n template: notification.template,\n });\n throw new AlephaError(\n `No notification template found for ${notification.template}`,\n );\n }\n\n return {\n template,\n variables,\n contact,\n };\n }\n}\n","import { $inject, t } from \"alepha\";\nimport { $queue } from \"alepha/queue\";\nimport { NotificationSenderService } from \"../services/NotificationSenderService.ts\";\n\nexport class NotificationQueues {\n protected readonly notificationSenderService = $inject(\n NotificationSenderService,\n );\n\n public readonly processNotification = $queue({\n description: \"Queue for processing notifications\",\n schema: t.object({\n notificationId: t.string({ format: \"uuid\" }),\n }),\n handler: async (message) => {\n await this.notificationSenderService.send(message.payload.notificationId);\n },\n });\n}\n","import { type Static, t } from \"alepha\";\nimport { notifications } from \"../entities/notifications.ts\";\n\nexport const notificationCreateSchema = t.pick(notifications.schema, [\n \"type\",\n \"contact\",\n \"template\",\n \"variables\",\n]);\n\nexport type NotificationCreate = Static<typeof notificationCreateSchema>;\n","import { $env, $inject, Alepha, type Static, t } from \"alepha\";\nimport { $batch } from \"alepha/batch\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository, type Page } from \"alepha/orm\";\nimport {\n type NotificationEntity,\n notifications,\n} from \"../entities/notifications.ts\";\nimport { NotificationQueues } from \"../queues/NotificationQueues.ts\";\nimport {\n type NotificationCreate,\n notificationCreateSchema,\n} from \"../schemas/notificationCreateSchema.ts\";\nimport type { NotificationQuery } from \"../schemas/notificationQuerySchema.ts\";\nimport { NotificationSenderService } from \"./NotificationSenderService.ts\";\n\nexport const notificationServiceEnvSchema = t.object({\n NOTIFICATION_QUEUE: t.optional(\n t.boolean({\n description:\n \"If true, notifications will be queued instead of sent immediately\",\n }),\n ),\n});\n\ndeclare module \"alepha\" {\n interface Env extends Partial<Static<typeof notificationServiceEnvSchema>> {}\n}\n\nexport class NotificationService {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly env = $env(notificationServiceEnvSchema);\n protected readonly notificationRepository = $repository(notifications);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly notificationSenderService = $inject(\n NotificationSenderService,\n );\n\n public readonly notificationBatch = $batch({\n maxSize: 100,\n maxDuration: [15, \"seconds\"],\n schema: notificationCreateSchema,\n handler: async (notifications: NotificationCreate[]) => {\n this.log.debug(\"Processing notification batch\", {\n size: notifications.length,\n templates: [...new Set(notifications.map((n) => n.template))],\n });\n\n const entities =\n await this.notificationRepository.createMany(notifications);\n\n await this.alepha\n .inject(NotificationQueues)\n .processNotification.push(\n ...entities.map((it) => ({ notificationId: it.id })),\n );\n\n this.log.info(\"Notification batch queued\", {\n count: entities.length,\n ids: entities.map((it) => it.id),\n });\n },\n });\n\n public async findNotificationById(id: string) {\n this.log.trace(\"Finding notification by ID\", { id });\n return this.notificationRepository.findOne({ where: { id } });\n }\n\n public async findNotifications(\n q: NotificationQuery = {},\n ): Promise<Page<NotificationEntity>> {\n this.log.trace(\"Finding notifications\", { query: q });\n q.sort ??= \"-createdAt\";\n\n const where = this.notificationRepository.createQueryWhere();\n\n if (q.type) {\n where.type = { eq: q.type };\n }\n\n if (q.template) {\n where.template = { like: `%${q.template}%` };\n }\n\n if (q.contact) {\n where.contact = { like: `%${q.contact}%` };\n }\n\n if (q.category) {\n where.category = { eq: q.category };\n }\n\n if (q.status) {\n if (q.status === \"sent\") {\n where.sentAt = { isNotNull: true };\n where.error = { isNull: true };\n } else if (q.status === \"failed\") {\n where.error = { isNotNull: true };\n } else if (q.status === \"pending\") {\n where.sentAt = { isNull: true };\n where.error = { isNull: true };\n }\n }\n\n return this.notificationRepository.paginate(q, { where }, { count: true });\n }\n\n /**\n * Create a new notification.\n */\n public async createNotification(entry: NotificationCreate): Promise<void> {\n this.log.trace(\"Creating notification\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n });\n\n if (\n this.env.NOTIFICATION_QUEUE !== true ||\n this.alepha.isServerless() ||\n this.alepha.isTest()\n ) {\n this.log.debug(\"Sending notification immediately\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n });\n const notification = await this.notificationRepository.create(entry);\n await this.notificationSenderService.send(notification);\n return;\n }\n\n this.log.debug(\"Queuing notification to batch\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n });\n\n this.notificationBatch.push(entry).catch((e) => {\n this.log.error(\"Failed to push notification to batch\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n error: e,\n });\n });\n }\n}\n","import { $inject, t } from \"alepha\";\nimport { $action } from \"alepha/server\";\nimport { notifications } from \"../entities/notifications.ts\";\nimport { notificationQuerySchema } from \"../schemas/notificationQuerySchema.ts\";\nimport { NotificationService } from \"../services/NotificationService.ts\";\n\nexport class AdminNotificationController {\n protected readonly url = \"/notifications\";\n protected readonly group = \"admin:notifications\";\n protected readonly notificationService = $inject(NotificationService);\n\n /**\n * Find notifications with pagination and filtering.\n */\n public readonly findNotifications = $action({\n path: this.url,\n group: this.group,\n description: \"Find notifications with pagination and filtering\",\n schema: {\n query: notificationQuerySchema,\n response: t.page(notifications.schema),\n },\n handler: ({ query }) => this.notificationService.findNotifications(query),\n });\n}\n","import { type Static, t } from \"alepha\";\n\nexport const notificationContactPreferencesSchema = t.object({\n language: t.optional(t.text()),\n exclude: t.array(t.text()),\n});\n\nexport type NotificationContactPreferences = Static<\n typeof notificationContactPreferencesSchema\n>;\n","import { type Static, t } from \"alepha\";\n\nexport const notificationContactSchema = t.object({\n email: t.optional(t.email()),\n phoneNumber: t.optional(t.e164()),\n firstName: t.optional(t.shortText()),\n lastName: t.optional(t.text({ size: \"short\" })),\n language: t.optional(t.bcp47()),\n});\n\nexport type NotificationContact = Static<typeof notificationContactSchema>;\n","import { $module } from \"alepha\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./controllers/AdminNotificationController.ts\";\nexport * from \"./entities/notifications.ts\";\nexport * from \"./schemas/notificationContactPreferencesSchema.ts\";\nexport * from \"./schemas/notificationContactSchema.ts\";\nexport * from \"./schemas/notificationCreateSchema.ts\";\nexport * from \"./schemas/notificationQuerySchema.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport const AlephaApiNotifications = $module({\n name: \"alepha.api.notifications\",\n services: [],\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.browser.js","names":[],"sources":["../../../src/api/notifications/entities/notifications.ts","../../../src/api/notifications/schemas/notificationQuerySchema.ts","../../../src/api/notifications/primitives/$notification.ts","../../../src/api/notifications/services/NotificationSenderService.ts","../../../src/api/notifications/queues/NotificationQueues.ts","../../../src/api/notifications/schemas/notificationCreateSchema.ts","../../../src/api/notifications/services/NotificationService.ts","../../../src/api/notifications/controllers/AdminNotificationController.ts","../../../src/api/notifications/schemas/notificationContactPreferencesSchema.ts","../../../src/api/notifications/schemas/notificationContactSchema.ts","../../../src/api/notifications/index.browser.ts"],"sourcesContent":["import { type Static, t } from \"alepha\";\nimport { $entity, db } from \"alepha/orm\";\n\nexport const notifications = $entity({\n name: \"notifications\",\n schema: t.object({\n id: db.primaryKey(t.uuid()),\n\n version: db.version(),\n\n createdAt: db.createdAt(),\n\n updatedAt: db.updatedAt(),\n\n // -----------------------------------------------------------------------------------------------------------------\n\n type: t.enum([\"email\", \"sms\"]),\n\n template: t.text(), // e.g. 'resetPassword'\n\n category: t.optional(\n t.text({\n description:\n \"For grouping related notifications (e.g., 'authentication', 'marketing'). Contact can filter notifications by category.\",\n }),\n ),\n\n critical: t.optional(\n t.boolean({\n description:\n \"Prioritize delivery of this notification. Set to true for important system alerts.\",\n }),\n ),\n\n sensitive: t.optional(\n t.boolean({\n description:\n \"Message won't be logged or stored in plain text. Set to true when notification contains passwords or codes.\",\n }),\n ),\n\n // -----------------------------------------------------------------------------------------------------------------\n\n contact: t.text(), // e.g. email address or phone number or user ID or whatever\n\n variables: t.optional(t.record(t.text(), t.any())),\n\n scheduledAt: t.optional(\n t.datetime({\n description:\n \"When set, the notification will be sent at or after this date/time.\",\n }),\n ),\n\n // -----------------------------------------------------------------------------------------------------------------\n\n sentAt: t.optional(t.datetime()),\n\n error: t.optional(\n t.object({\n at: t.datetime(),\n name: t.text(),\n message: t.text({ size: \"rich\" }),\n }),\n ),\n\n // TODO: retryCount, lastRetryAt, etc.\n }),\n});\n\nexport type NotificationEntity = Static<typeof notifications.schema>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\nimport { pageQuerySchema } from \"alepha/orm\";\n\nexport const notificationQuerySchema = t.extend(pageQuerySchema, {\n type: t.optional(t.enum([\"email\", \"sms\"])),\n template: t.optional(t.string()),\n contact: t.optional(t.string()),\n category: t.optional(t.string()),\n status: t.optional(t.enum([\"pending\", \"sent\", \"failed\"])),\n});\n\nexport type NotificationQuery = Static<typeof notificationQuerySchema>;\n","import {\n $inject,\n createPrimitive,\n KIND,\n Primitive,\n type Static,\n type StaticEncode,\n type TObject,\n} from \"alepha\";\nimport { NotificationService } from \"../services/NotificationService.ts\";\n\n/**\n * Creates a notification primitive for managing email/SMS notification templates.\n *\n * Provides type-safe, reusable notification templates with multi-language support,\n * variable substitution, and categorization for different notification channels.\n *\n * @example\n * ```ts\n * class NotificationTemplates {\n * welcomeEmail = $notification({\n * name: \"welcome-email\",\n * category: \"onboarding\",\n * schema: t.object({ username: t.text(), activationLink: t.text() }),\n * email: {\n * subject: \"Welcome to our platform!\",\n * body: (vars) => `Hello ${vars.username}, click: ${vars.activationLink}`\n * }\n * });\n *\n * async sendWelcome(user: User) {\n * await this.welcomeEmail.push({\n * variables: { username: user.name, activationLink: generateLink() },\n * contact: user.email\n * });\n * }\n * }\n * ```\n */\nexport const $notification = <T extends TObject>(\n options: NotificationPrimitiveOptions<T>,\n) => createPrimitive(NotificationPrimitive<T>, options);\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface NotificationPrimitiveOptions<T extends TObject>\n extends NotificationMessage<T> {\n name?: string;\n description?: string;\n category?: string;\n critical?: boolean;\n sensitive?: boolean;\n translations?: {\n // e.g., \"en\", \"fr\", even \"en-US\"\n [lang: string]: NotificationMessage<T>;\n };\n schema: T;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class NotificationPrimitive<T extends TObject> extends Primitive<\n NotificationPrimitiveOptions<T>\n> {\n protected readonly notificationService = $inject(NotificationService);\n\n public get name() {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n public async push(options: NotificationPushOptions<T>) {\n if (this.options.email) {\n await this.notificationService.createNotification({\n ...options,\n type: \"email\",\n template: this.name,\n });\n }\n }\n\n public configure(options: Partial<NotificationPrimitiveOptions<T>>) {\n Object.assign(this.options, options);\n }\n}\n\n$notification[KIND] = NotificationPrimitive;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface NotificationPushOptions<T extends TObject> {\n variables: StaticEncode<T>;\n contact: string;\n}\n\nexport interface NotificationMessage<T extends TObject> {\n email?: {\n subject: string;\n body: string | ((variables: Static<T>) => string);\n };\n sms?: {\n message: string | ((variables: Static<T>) => string);\n };\n}\n","import { $inject, Alepha, AlephaError } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { EmailProvider } from \"alepha/email\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository } from \"alepha/orm\";\nimport { SmsProvider } from \"alepha/sms\";\nimport {\n type NotificationEntity,\n notifications,\n} from \"../entities/notifications.ts\";\nimport { $notification } from \"../primitives/$notification.ts\";\n\nexport class NotificationSenderService {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly notificationRepository = $repository(notifications);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly emailProvider = $inject(EmailProvider);\n protected readonly smsProvider = $inject(SmsProvider);\n\n public async send(notificationId: string | NotificationEntity) {\n this.log.trace(\"Sending notification\", {\n notificationId:\n typeof notificationId === \"string\" ? notificationId : notificationId.id,\n });\n\n const notification =\n typeof notificationId === \"string\"\n ? await this.notificationRepository.findById(notificationId)\n : notificationId;\n\n if (notification.sentAt) {\n this.log.debug(\"Notification already sent\", {\n notificationId: notification.id,\n sentAt: notification.sentAt,\n });\n return;\n }\n\n this.log.debug(\"Processing notification\", {\n id: notification.id,\n type: notification.type,\n template: notification.template,\n contact: notification.contact,\n });\n\n try {\n if (notification.type === \"email\") {\n await this.emailProvider.send(this.renderEmail(notification));\n notification.sentAt = this.dateTimeProvider.nowISOString();\n this.log.info(\"Email notification sent\", {\n id: notification.id,\n template: notification.template,\n contact: notification.contact,\n });\n }\n if (notification.type === \"sms\") {\n await this.smsProvider.send(this.renderSms(notification));\n notification.sentAt = this.dateTimeProvider.nowISOString();\n this.log.info(\"SMS notification sent\", {\n id: notification.id,\n template: notification.template,\n contact: notification.contact,\n });\n }\n } catch (e) {\n this.log.error(\"Failed to send notification\", {\n id: notification.id,\n type: notification.type,\n template: notification.template,\n contact: notification.contact,\n error: e,\n });\n if (e instanceof Error) {\n notification.error = {\n at: this.dateTimeProvider.nowISOString(),\n name: e.name,\n message: e.message,\n };\n }\n } finally {\n await this.notificationRepository.save(notification);\n }\n }\n\n public renderSms(notification: NotificationEntity) {\n this.log.trace(\"Rendering SMS notification\", {\n id: notification.id,\n template: notification.template,\n });\n\n const { variables, contact, template } = this.load(notification);\n\n const sms = template.options.sms;\n if (!sms) {\n this.log.error(\"Notification template has no SMS defined\", {\n id: notification.id,\n template: notification.template,\n });\n throw new AlephaError(\n `Notification template ${notification.template} has no sms defined`,\n );\n }\n\n this.log.debug(\"Rendering SMS\", {\n template: notification.template,\n contact,\n });\n\n const message =\n typeof sms.message === \"function\"\n ? sms.message(variables as any)\n : sms.message;\n\n return {\n to: contact,\n message,\n };\n }\n\n public renderEmail(notification: NotificationEntity) {\n this.log.trace(\"Rendering email notification\", {\n id: notification.id,\n template: notification.template,\n });\n\n const { variables, contact, template } = this.load(notification);\n\n const email = template.options.email;\n if (!email) {\n this.log.error(\"Notification template has no email defined\", {\n id: notification.id,\n template: notification.template,\n });\n throw new AlephaError(\n `Notification template ${notification.template} has no email defined`,\n );\n }\n\n this.log.debug(\"Rendering email\", {\n template: notification.template,\n contact,\n subject: email.subject,\n });\n\n const subject = email.subject;\n\n const body =\n typeof email.body === \"function\"\n ? email.body(variables as any)\n : email.body;\n\n return {\n to: contact,\n subject,\n body,\n };\n }\n\n protected load(notification: NotificationEntity) {\n const variables = notification.variables || {};\n const contact = notification.contact;\n const template = this.alepha\n .primitives($notification)\n .find((it) => it.name === notification.template);\n\n if (!template) {\n this.log.error(\"Notification template not found\", {\n id: notification.id,\n template: notification.template,\n });\n throw new AlephaError(\n `No notification template found for ${notification.template}`,\n );\n }\n\n return {\n template,\n variables,\n contact,\n };\n }\n}\n","import { $inject, t } from \"alepha\";\nimport { $queue } from \"alepha/queue\";\nimport { NotificationSenderService } from \"../services/NotificationSenderService.ts\";\n\nexport class NotificationQueues {\n protected readonly notificationSenderService = $inject(\n NotificationSenderService,\n );\n\n public readonly processNotification = $queue({\n description: \"Queue for processing notifications\",\n schema: t.object({\n notificationId: t.string({ format: \"uuid\" }),\n }),\n handler: async (message) => {\n await this.notificationSenderService.send(message.payload.notificationId);\n },\n });\n}\n","import { type Static, t } from \"alepha\";\nimport { notifications } from \"../entities/notifications.ts\";\n\nexport const notificationCreateSchema = t.pick(notifications.schema, [\n \"type\",\n \"contact\",\n \"template\",\n \"variables\",\n]);\n\nexport type NotificationCreate = Static<typeof notificationCreateSchema>;\n","import { $env, $inject, Alepha, type Static, t } from \"alepha\";\nimport { $batch } from \"alepha/batch\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository, type Page } from \"alepha/orm\";\nimport {\n type NotificationEntity,\n notifications,\n} from \"../entities/notifications.ts\";\nimport { NotificationQueues } from \"../queues/NotificationQueues.ts\";\nimport {\n type NotificationCreate,\n notificationCreateSchema,\n} from \"../schemas/notificationCreateSchema.ts\";\nimport type { NotificationQuery } from \"../schemas/notificationQuerySchema.ts\";\nimport { NotificationSenderService } from \"./NotificationSenderService.ts\";\n\nexport const notificationServiceEnvSchema = t.object({\n NOTIFICATION_QUEUE: t.optional(\n t.boolean({\n description:\n \"If true, notifications will be queued instead of sent immediately\",\n }),\n ),\n});\n\ndeclare module \"alepha\" {\n interface Env extends Partial<Static<typeof notificationServiceEnvSchema>> {}\n}\n\nexport class NotificationService {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly env = $env(notificationServiceEnvSchema);\n protected readonly notificationRepository = $repository(notifications);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly notificationSenderService = $inject(\n NotificationSenderService,\n );\n\n public readonly notificationBatch = $batch({\n maxSize: 100,\n maxDuration: [15, \"seconds\"],\n schema: notificationCreateSchema,\n handler: async (notifications: NotificationCreate[]) => {\n this.log.debug(\"Processing notification batch\", {\n size: notifications.length,\n templates: [...new Set(notifications.map((n) => n.template))],\n });\n\n const entities =\n await this.notificationRepository.createMany(notifications);\n\n await this.alepha\n .inject(NotificationQueues)\n .processNotification.push(\n ...entities.map((it) => ({ notificationId: it.id })),\n );\n\n this.log.info(\"Notification batch queued\", {\n count: entities.length,\n ids: entities.map((it) => it.id),\n });\n },\n });\n\n public async findNotificationById(id: string) {\n this.log.trace(\"Finding notification by ID\", { id });\n return this.notificationRepository.findOne({ where: { id } });\n }\n\n public async findNotifications(\n q: NotificationQuery = {},\n ): Promise<Page<NotificationEntity>> {\n this.log.trace(\"Finding notifications\", { query: q });\n q.sort ??= \"-createdAt\";\n\n const where = this.notificationRepository.createQueryWhere();\n\n if (q.type) {\n where.type = { eq: q.type };\n }\n\n if (q.template) {\n where.template = { like: `%${q.template}%` };\n }\n\n if (q.contact) {\n where.contact = { like: `%${q.contact}%` };\n }\n\n if (q.category) {\n where.category = { eq: q.category };\n }\n\n if (q.status) {\n if (q.status === \"sent\") {\n where.sentAt = { isNotNull: true };\n where.error = { isNull: true };\n } else if (q.status === \"failed\") {\n where.error = { isNotNull: true };\n } else if (q.status === \"pending\") {\n where.sentAt = { isNull: true };\n where.error = { isNull: true };\n }\n }\n\n return this.notificationRepository.paginate(q, { where }, { count: true });\n }\n\n /**\n * Create a new notification.\n */\n public async createNotification(entry: NotificationCreate): Promise<void> {\n this.log.trace(\"Creating notification\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n });\n\n if (\n this.env.NOTIFICATION_QUEUE !== true ||\n this.alepha.isServerless() ||\n this.alepha.isTest()\n ) {\n this.log.debug(\"Sending notification immediately\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n });\n const notification = await this.notificationRepository.create(entry);\n await this.notificationSenderService.send(notification);\n return;\n }\n\n this.log.debug(\"Queuing notification to batch\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n });\n\n this.notificationBatch.push(entry).catch((e) => {\n this.log.error(\"Failed to push notification to batch\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n error: e,\n });\n });\n }\n}\n","import { $inject, t } from \"alepha\";\nimport { $action } from \"alepha/server\";\nimport { notifications } from \"../entities/notifications.ts\";\nimport { notificationQuerySchema } from \"../schemas/notificationQuerySchema.ts\";\nimport { NotificationService } from \"../services/NotificationService.ts\";\n\nexport class AdminNotificationController {\n protected readonly url = \"/notifications\";\n protected readonly group = \"admin:notifications\";\n protected readonly notificationService = $inject(NotificationService);\n\n /**\n * Find notifications with pagination and filtering.\n */\n public readonly findNotifications = $action({\n path: this.url,\n group: this.group,\n secure: true,\n description: \"Find notifications with pagination and filtering\",\n schema: {\n query: notificationQuerySchema,\n response: t.page(notifications.schema),\n },\n handler: ({ query }) => this.notificationService.findNotifications(query),\n });\n}\n","import { type Static, t } from \"alepha\";\n\nexport const notificationContactPreferencesSchema = t.object({\n language: t.optional(t.text()),\n exclude: t.array(t.text()),\n});\n\nexport type NotificationContactPreferences = Static<\n typeof notificationContactPreferencesSchema\n>;\n","import { type Static, t } from \"alepha\";\n\nexport const notificationContactSchema = t.object({\n email: t.optional(t.email()),\n phoneNumber: t.optional(t.e164()),\n firstName: t.optional(t.shortText()),\n lastName: t.optional(t.text({ size: \"short\" })),\n language: t.optional(t.bcp47()),\n});\n\nexport type NotificationContact = Static<typeof notificationContactSchema>;\n","import { $module } from \"alepha\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./controllers/AdminNotificationController.ts\";\nexport * from \"./entities/notifications.ts\";\nexport * from \"./schemas/notificationContactPreferencesSchema.ts\";\nexport * from \"./schemas/notificationContactSchema.ts\";\nexport * from \"./schemas/notificationCreateSchema.ts\";\nexport * from \"./schemas/notificationQuerySchema.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport const AlephaApiNotifications = $module({\n name: \"alepha.api.notifications\",\n services: [],\n});\n"],"mappings":"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"}
@@ -553,9 +553,9 @@ type NotificationContactPreferences = Static<typeof notificationContactPreferenc
553
553
  //#endregion
554
554
  //#region ../../src/api/notifications/index.d.ts
555
555
  /**
556
- * | type | quality | stability |
557
- * |------|---------|-----------|
558
- * | backend | standard | stable |
556
+ * | Stability | Since | Runtime |
557
+ * |-----------|-------|---------|
558
+ * | 3 - stable | 0.10.0 | node, bun, workerd|
559
559
  *
560
560
  * User notification management.
561
561
  *
@@ -344,6 +344,7 @@ var AdminNotificationController = class {
344
344
  findNotifications = $action({
345
345
  path: this.url,
346
346
  group: this.group,
347
+ secure: true,
347
348
  description: "Find notifications with pagination and filtering",
348
349
  schema: {
349
350
  query: notificationQuerySchema,
@@ -367,9 +368,9 @@ const notificationContactPreferencesSchema = t.object({
367
368
  //#endregion
368
369
  //#region ../../src/api/notifications/index.ts
369
370
  /**
370
- * | type | quality | stability |
371
- * |------|---------|-----------|
372
- * | backend | standard | stable |
371
+ * | Stability | Since | Runtime |
372
+ * |-----------|-------|---------|
373
+ * | 3 - stable | 0.10.0 | node, bun, workerd|
373
374
  *
374
375
  * User notification management.
375
376
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/api/notifications/entities/notifications.ts","../../../src/api/notifications/schemas/notificationQuerySchema.ts","../../../src/api/notifications/primitives/$notification.ts","../../../src/api/notifications/services/NotificationSenderService.ts","../../../src/api/notifications/queues/NotificationQueues.ts","../../../src/api/notifications/schemas/notificationCreateSchema.ts","../../../src/api/notifications/services/NotificationService.ts","../../../src/api/notifications/controllers/AdminNotificationController.ts","../../../src/api/notifications/jobs/NotificationJobs.ts","../../../src/api/notifications/schemas/notificationContactPreferencesSchema.ts","../../../src/api/notifications/index.ts"],"sourcesContent":["import { type Static, t } from \"alepha\";\nimport { $entity, db } from \"alepha/orm\";\n\nexport const notifications = $entity({\n name: \"notifications\",\n schema: t.object({\n id: db.primaryKey(t.uuid()),\n\n version: db.version(),\n\n createdAt: db.createdAt(),\n\n updatedAt: db.updatedAt(),\n\n // -----------------------------------------------------------------------------------------------------------------\n\n type: t.enum([\"email\", \"sms\"]),\n\n template: t.text(), // e.g. 'resetPassword'\n\n category: t.optional(\n t.text({\n description:\n \"For grouping related notifications (e.g., 'authentication', 'marketing'). Contact can filter notifications by category.\",\n }),\n ),\n\n critical: t.optional(\n t.boolean({\n description:\n \"Prioritize delivery of this notification. Set to true for important system alerts.\",\n }),\n ),\n\n sensitive: t.optional(\n t.boolean({\n description:\n \"Message won't be logged or stored in plain text. Set to true when notification contains passwords or codes.\",\n }),\n ),\n\n // -----------------------------------------------------------------------------------------------------------------\n\n contact: t.text(), // e.g. email address or phone number or user ID or whatever\n\n variables: t.optional(t.record(t.text(), t.any())),\n\n scheduledAt: t.optional(\n t.datetime({\n description:\n \"When set, the notification will be sent at or after this date/time.\",\n }),\n ),\n\n // -----------------------------------------------------------------------------------------------------------------\n\n sentAt: t.optional(t.datetime()),\n\n error: t.optional(\n t.object({\n at: t.datetime(),\n name: t.text(),\n message: t.text({ size: \"rich\" }),\n }),\n ),\n\n // TODO: retryCount, lastRetryAt, etc.\n }),\n});\n\nexport type NotificationEntity = Static<typeof notifications.schema>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\nimport { pageQuerySchema } from \"alepha/orm\";\n\nexport const notificationQuerySchema = t.extend(pageQuerySchema, {\n type: t.optional(t.enum([\"email\", \"sms\"])),\n template: t.optional(t.string()),\n contact: t.optional(t.string()),\n category: t.optional(t.string()),\n status: t.optional(t.enum([\"pending\", \"sent\", \"failed\"])),\n});\n\nexport type NotificationQuery = Static<typeof notificationQuerySchema>;\n","import {\n $inject,\n createPrimitive,\n KIND,\n Primitive,\n type Static,\n type StaticEncode,\n type TObject,\n} from \"alepha\";\nimport { NotificationService } from \"../services/NotificationService.ts\";\n\n/**\n * Creates a notification primitive for managing email/SMS notification templates.\n *\n * Provides type-safe, reusable notification templates with multi-language support,\n * variable substitution, and categorization for different notification channels.\n *\n * @example\n * ```ts\n * class NotificationTemplates {\n * welcomeEmail = $notification({\n * name: \"welcome-email\",\n * category: \"onboarding\",\n * schema: t.object({ username: t.text(), activationLink: t.text() }),\n * email: {\n * subject: \"Welcome to our platform!\",\n * body: (vars) => `Hello ${vars.username}, click: ${vars.activationLink}`\n * }\n * });\n *\n * async sendWelcome(user: User) {\n * await this.welcomeEmail.push({\n * variables: { username: user.name, activationLink: generateLink() },\n * contact: user.email\n * });\n * }\n * }\n * ```\n */\nexport const $notification = <T extends TObject>(\n options: NotificationPrimitiveOptions<T>,\n) => createPrimitive(NotificationPrimitive<T>, options);\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface NotificationPrimitiveOptions<T extends TObject>\n extends NotificationMessage<T> {\n name?: string;\n description?: string;\n category?: string;\n critical?: boolean;\n sensitive?: boolean;\n translations?: {\n // e.g., \"en\", \"fr\", even \"en-US\"\n [lang: string]: NotificationMessage<T>;\n };\n schema: T;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class NotificationPrimitive<T extends TObject> extends Primitive<\n NotificationPrimitiveOptions<T>\n> {\n protected readonly notificationService = $inject(NotificationService);\n\n public get name() {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n public async push(options: NotificationPushOptions<T>) {\n if (this.options.email) {\n await this.notificationService.createNotification({\n ...options,\n type: \"email\",\n template: this.name,\n });\n }\n }\n\n public configure(options: Partial<NotificationPrimitiveOptions<T>>) {\n Object.assign(this.options, options);\n }\n}\n\n$notification[KIND] = NotificationPrimitive;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface NotificationPushOptions<T extends TObject> {\n variables: StaticEncode<T>;\n contact: string;\n}\n\nexport interface NotificationMessage<T extends TObject> {\n email?: {\n subject: string;\n body: string | ((variables: Static<T>) => string);\n };\n sms?: {\n message: string | ((variables: Static<T>) => string);\n };\n}\n","import { $inject, Alepha, AlephaError } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { EmailProvider } from \"alepha/email\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository } from \"alepha/orm\";\nimport { SmsProvider } from \"alepha/sms\";\nimport {\n type NotificationEntity,\n notifications,\n} from \"../entities/notifications.ts\";\nimport { $notification } from \"../primitives/$notification.ts\";\n\nexport class NotificationSenderService {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly notificationRepository = $repository(notifications);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly emailProvider = $inject(EmailProvider);\n protected readonly smsProvider = $inject(SmsProvider);\n\n public async send(notificationId: string | NotificationEntity) {\n this.log.trace(\"Sending notification\", {\n notificationId:\n typeof notificationId === \"string\" ? notificationId : notificationId.id,\n });\n\n const notification =\n typeof notificationId === \"string\"\n ? await this.notificationRepository.findById(notificationId)\n : notificationId;\n\n if (notification.sentAt) {\n this.log.debug(\"Notification already sent\", {\n notificationId: notification.id,\n sentAt: notification.sentAt,\n });\n return;\n }\n\n this.log.debug(\"Processing notification\", {\n id: notification.id,\n type: notification.type,\n template: notification.template,\n contact: notification.contact,\n });\n\n try {\n if (notification.type === \"email\") {\n await this.emailProvider.send(this.renderEmail(notification));\n notification.sentAt = this.dateTimeProvider.nowISOString();\n this.log.info(\"Email notification sent\", {\n id: notification.id,\n template: notification.template,\n contact: notification.contact,\n });\n }\n if (notification.type === \"sms\") {\n await this.smsProvider.send(this.renderSms(notification));\n notification.sentAt = this.dateTimeProvider.nowISOString();\n this.log.info(\"SMS notification sent\", {\n id: notification.id,\n template: notification.template,\n contact: notification.contact,\n });\n }\n } catch (e) {\n this.log.error(\"Failed to send notification\", {\n id: notification.id,\n type: notification.type,\n template: notification.template,\n contact: notification.contact,\n error: e,\n });\n if (e instanceof Error) {\n notification.error = {\n at: this.dateTimeProvider.nowISOString(),\n name: e.name,\n message: e.message,\n };\n }\n } finally {\n await this.notificationRepository.save(notification);\n }\n }\n\n public renderSms(notification: NotificationEntity) {\n this.log.trace(\"Rendering SMS notification\", {\n id: notification.id,\n template: notification.template,\n });\n\n const { variables, contact, template } = this.load(notification);\n\n const sms = template.options.sms;\n if (!sms) {\n this.log.error(\"Notification template has no SMS defined\", {\n id: notification.id,\n template: notification.template,\n });\n throw new AlephaError(\n `Notification template ${notification.template} has no sms defined`,\n );\n }\n\n this.log.debug(\"Rendering SMS\", {\n template: notification.template,\n contact,\n });\n\n const message =\n typeof sms.message === \"function\"\n ? sms.message(variables as any)\n : sms.message;\n\n return {\n to: contact,\n message,\n };\n }\n\n public renderEmail(notification: NotificationEntity) {\n this.log.trace(\"Rendering email notification\", {\n id: notification.id,\n template: notification.template,\n });\n\n const { variables, contact, template } = this.load(notification);\n\n const email = template.options.email;\n if (!email) {\n this.log.error(\"Notification template has no email defined\", {\n id: notification.id,\n template: notification.template,\n });\n throw new AlephaError(\n `Notification template ${notification.template} has no email defined`,\n );\n }\n\n this.log.debug(\"Rendering email\", {\n template: notification.template,\n contact,\n subject: email.subject,\n });\n\n const subject = email.subject;\n\n const body =\n typeof email.body === \"function\"\n ? email.body(variables as any)\n : email.body;\n\n return {\n to: contact,\n subject,\n body,\n };\n }\n\n protected load(notification: NotificationEntity) {\n const variables = notification.variables || {};\n const contact = notification.contact;\n const template = this.alepha\n .primitives($notification)\n .find((it) => it.name === notification.template);\n\n if (!template) {\n this.log.error(\"Notification template not found\", {\n id: notification.id,\n template: notification.template,\n });\n throw new AlephaError(\n `No notification template found for ${notification.template}`,\n );\n }\n\n return {\n template,\n variables,\n contact,\n };\n }\n}\n","import { $inject, t } from \"alepha\";\nimport { $queue } from \"alepha/queue\";\nimport { NotificationSenderService } from \"../services/NotificationSenderService.ts\";\n\nexport class NotificationQueues {\n protected readonly notificationSenderService = $inject(\n NotificationSenderService,\n );\n\n public readonly processNotification = $queue({\n description: \"Queue for processing notifications\",\n schema: t.object({\n notificationId: t.string({ format: \"uuid\" }),\n }),\n handler: async (message) => {\n await this.notificationSenderService.send(message.payload.notificationId);\n },\n });\n}\n","import { type Static, t } from \"alepha\";\nimport { notifications } from \"../entities/notifications.ts\";\n\nexport const notificationCreateSchema = t.pick(notifications.schema, [\n \"type\",\n \"contact\",\n \"template\",\n \"variables\",\n]);\n\nexport type NotificationCreate = Static<typeof notificationCreateSchema>;\n","import { $env, $inject, Alepha, type Static, t } from \"alepha\";\nimport { $batch } from \"alepha/batch\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository, type Page } from \"alepha/orm\";\nimport {\n type NotificationEntity,\n notifications,\n} from \"../entities/notifications.ts\";\nimport { NotificationQueues } from \"../queues/NotificationQueues.ts\";\nimport {\n type NotificationCreate,\n notificationCreateSchema,\n} from \"../schemas/notificationCreateSchema.ts\";\nimport type { NotificationQuery } from \"../schemas/notificationQuerySchema.ts\";\nimport { NotificationSenderService } from \"./NotificationSenderService.ts\";\n\nexport const notificationServiceEnvSchema = t.object({\n NOTIFICATION_QUEUE: t.optional(\n t.boolean({\n description:\n \"If true, notifications will be queued instead of sent immediately\",\n }),\n ),\n});\n\ndeclare module \"alepha\" {\n interface Env extends Partial<Static<typeof notificationServiceEnvSchema>> {}\n}\n\nexport class NotificationService {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly env = $env(notificationServiceEnvSchema);\n protected readonly notificationRepository = $repository(notifications);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly notificationSenderService = $inject(\n NotificationSenderService,\n );\n\n public readonly notificationBatch = $batch({\n maxSize: 100,\n maxDuration: [15, \"seconds\"],\n schema: notificationCreateSchema,\n handler: async (notifications: NotificationCreate[]) => {\n this.log.debug(\"Processing notification batch\", {\n size: notifications.length,\n templates: [...new Set(notifications.map((n) => n.template))],\n });\n\n const entities =\n await this.notificationRepository.createMany(notifications);\n\n await this.alepha\n .inject(NotificationQueues)\n .processNotification.push(\n ...entities.map((it) => ({ notificationId: it.id })),\n );\n\n this.log.info(\"Notification batch queued\", {\n count: entities.length,\n ids: entities.map((it) => it.id),\n });\n },\n });\n\n public async findNotificationById(id: string) {\n this.log.trace(\"Finding notification by ID\", { id });\n return this.notificationRepository.findOne({ where: { id } });\n }\n\n public async findNotifications(\n q: NotificationQuery = {},\n ): Promise<Page<NotificationEntity>> {\n this.log.trace(\"Finding notifications\", { query: q });\n q.sort ??= \"-createdAt\";\n\n const where = this.notificationRepository.createQueryWhere();\n\n if (q.type) {\n where.type = { eq: q.type };\n }\n\n if (q.template) {\n where.template = { like: `%${q.template}%` };\n }\n\n if (q.contact) {\n where.contact = { like: `%${q.contact}%` };\n }\n\n if (q.category) {\n where.category = { eq: q.category };\n }\n\n if (q.status) {\n if (q.status === \"sent\") {\n where.sentAt = { isNotNull: true };\n where.error = { isNull: true };\n } else if (q.status === \"failed\") {\n where.error = { isNotNull: true };\n } else if (q.status === \"pending\") {\n where.sentAt = { isNull: true };\n where.error = { isNull: true };\n }\n }\n\n return this.notificationRepository.paginate(q, { where }, { count: true });\n }\n\n /**\n * Create a new notification.\n */\n public async createNotification(entry: NotificationCreate): Promise<void> {\n this.log.trace(\"Creating notification\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n });\n\n if (\n this.env.NOTIFICATION_QUEUE !== true ||\n this.alepha.isServerless() ||\n this.alepha.isTest()\n ) {\n this.log.debug(\"Sending notification immediately\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n });\n const notification = await this.notificationRepository.create(entry);\n await this.notificationSenderService.send(notification);\n return;\n }\n\n this.log.debug(\"Queuing notification to batch\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n });\n\n this.notificationBatch.push(entry).catch((e) => {\n this.log.error(\"Failed to push notification to batch\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n error: e,\n });\n });\n }\n}\n","import { $inject, t } from \"alepha\";\nimport { $action } from \"alepha/server\";\nimport { notifications } from \"../entities/notifications.ts\";\nimport { notificationQuerySchema } from \"../schemas/notificationQuerySchema.ts\";\nimport { NotificationService } from \"../services/NotificationService.ts\";\n\nexport class AdminNotificationController {\n protected readonly url = \"/notifications\";\n protected readonly group = \"admin:notifications\";\n protected readonly notificationService = $inject(NotificationService);\n\n /**\n * Find notifications with pagination and filtering.\n */\n public readonly findNotifications = $action({\n path: this.url,\n group: this.group,\n description: \"Find notifications with pagination and filtering\",\n schema: {\n query: notificationQuerySchema,\n response: t.page(notifications.schema),\n },\n handler: ({ query }) => this.notificationService.findNotifications(query),\n });\n}\n","export class NotificationJobs {\n // - retry (lost, failed) notifications\n // - purge old notifications\n}\n","import { type Static, t } from \"alepha\";\n\nexport const notificationContactPreferencesSchema = t.object({\n language: t.optional(t.text()),\n exclude: t.array(t.text()),\n});\n\nexport type NotificationContactPreferences = Static<\n typeof notificationContactPreferencesSchema\n>;\n","import { $module } from \"alepha\";\nimport { AdminNotificationController } from \"./controllers/AdminNotificationController.ts\";\nimport { NotificationJobs } from \"./jobs/NotificationJobs.ts\";\nimport { $notification } from \"./primitives/$notification.ts\";\nimport { NotificationQueues } from \"./queues/NotificationQueues.ts\";\nimport { NotificationSenderService } from \"./services/NotificationSenderService.ts\";\nimport {\n NotificationService,\n notificationServiceEnvSchema,\n} from \"./services/NotificationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./controllers/AdminNotificationController.ts\";\nexport * from \"./entities/notifications.ts\";\nexport * from \"./jobs/NotificationJobs.ts\";\nexport * from \"./primitives/$notification.ts\";\nexport * from \"./queues/NotificationQueues.ts\";\nexport * from \"./schemas/notificationContactPreferencesSchema.ts\";\nexport * from \"./schemas/notificationCreateSchema.ts\";\nexport * from \"./schemas/notificationQuerySchema.ts\";\nexport * from \"./services/NotificationSenderService.ts\";\nexport * from \"./services/NotificationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * | type | quality | stability |\n * |------|---------|-----------|\n * | backend | standard | stable |\n *\n * User notification management.\n *\n * **Features:**\n * - Notification definitions\n * - Email/SMS notification sending\n * - Status tracking\n * - User preferences\n * - Queue integration\n *\n * @module alepha.api.notifications\n */\nexport const AlephaApiNotifications = $module({\n name: \"alepha.api.notifications\",\n primitives: [$notification],\n services: [\n AdminNotificationController,\n NotificationService,\n NotificationSenderService,\n NotificationQueues,\n NotificationJobs,\n ],\n register: (alepha) => {\n const env = alepha.parseEnv(notificationServiceEnvSchema);\n if (env.NOTIFICATION_QUEUE) {\n alepha.with(NotificationQueues);\n }\n\n alepha\n .with(AdminNotificationController)\n .with(NotificationService)\n .with(NotificationSenderService)\n .with(NotificationJobs);\n },\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/api/notifications/entities/notifications.ts","../../../src/api/notifications/schemas/notificationQuerySchema.ts","../../../src/api/notifications/primitives/$notification.ts","../../../src/api/notifications/services/NotificationSenderService.ts","../../../src/api/notifications/queues/NotificationQueues.ts","../../../src/api/notifications/schemas/notificationCreateSchema.ts","../../../src/api/notifications/services/NotificationService.ts","../../../src/api/notifications/controllers/AdminNotificationController.ts","../../../src/api/notifications/jobs/NotificationJobs.ts","../../../src/api/notifications/schemas/notificationContactPreferencesSchema.ts","../../../src/api/notifications/index.ts"],"sourcesContent":["import { type Static, t } from \"alepha\";\nimport { $entity, db } from \"alepha/orm\";\n\nexport const notifications = $entity({\n name: \"notifications\",\n schema: t.object({\n id: db.primaryKey(t.uuid()),\n\n version: db.version(),\n\n createdAt: db.createdAt(),\n\n updatedAt: db.updatedAt(),\n\n // -----------------------------------------------------------------------------------------------------------------\n\n type: t.enum([\"email\", \"sms\"]),\n\n template: t.text(), // e.g. 'resetPassword'\n\n category: t.optional(\n t.text({\n description:\n \"For grouping related notifications (e.g., 'authentication', 'marketing'). Contact can filter notifications by category.\",\n }),\n ),\n\n critical: t.optional(\n t.boolean({\n description:\n \"Prioritize delivery of this notification. Set to true for important system alerts.\",\n }),\n ),\n\n sensitive: t.optional(\n t.boolean({\n description:\n \"Message won't be logged or stored in plain text. Set to true when notification contains passwords or codes.\",\n }),\n ),\n\n // -----------------------------------------------------------------------------------------------------------------\n\n contact: t.text(), // e.g. email address or phone number or user ID or whatever\n\n variables: t.optional(t.record(t.text(), t.any())),\n\n scheduledAt: t.optional(\n t.datetime({\n description:\n \"When set, the notification will be sent at or after this date/time.\",\n }),\n ),\n\n // -----------------------------------------------------------------------------------------------------------------\n\n sentAt: t.optional(t.datetime()),\n\n error: t.optional(\n t.object({\n at: t.datetime(),\n name: t.text(),\n message: t.text({ size: \"rich\" }),\n }),\n ),\n\n // TODO: retryCount, lastRetryAt, etc.\n }),\n});\n\nexport type NotificationEntity = Static<typeof notifications.schema>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\nimport { pageQuerySchema } from \"alepha/orm\";\n\nexport const notificationQuerySchema = t.extend(pageQuerySchema, {\n type: t.optional(t.enum([\"email\", \"sms\"])),\n template: t.optional(t.string()),\n contact: t.optional(t.string()),\n category: t.optional(t.string()),\n status: t.optional(t.enum([\"pending\", \"sent\", \"failed\"])),\n});\n\nexport type NotificationQuery = Static<typeof notificationQuerySchema>;\n","import {\n $inject,\n createPrimitive,\n KIND,\n Primitive,\n type Static,\n type StaticEncode,\n type TObject,\n} from \"alepha\";\nimport { NotificationService } from \"../services/NotificationService.ts\";\n\n/**\n * Creates a notification primitive for managing email/SMS notification templates.\n *\n * Provides type-safe, reusable notification templates with multi-language support,\n * variable substitution, and categorization for different notification channels.\n *\n * @example\n * ```ts\n * class NotificationTemplates {\n * welcomeEmail = $notification({\n * name: \"welcome-email\",\n * category: \"onboarding\",\n * schema: t.object({ username: t.text(), activationLink: t.text() }),\n * email: {\n * subject: \"Welcome to our platform!\",\n * body: (vars) => `Hello ${vars.username}, click: ${vars.activationLink}`\n * }\n * });\n *\n * async sendWelcome(user: User) {\n * await this.welcomeEmail.push({\n * variables: { username: user.name, activationLink: generateLink() },\n * contact: user.email\n * });\n * }\n * }\n * ```\n */\nexport const $notification = <T extends TObject>(\n options: NotificationPrimitiveOptions<T>,\n) => createPrimitive(NotificationPrimitive<T>, options);\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface NotificationPrimitiveOptions<T extends TObject>\n extends NotificationMessage<T> {\n name?: string;\n description?: string;\n category?: string;\n critical?: boolean;\n sensitive?: boolean;\n translations?: {\n // e.g., \"en\", \"fr\", even \"en-US\"\n [lang: string]: NotificationMessage<T>;\n };\n schema: T;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class NotificationPrimitive<T extends TObject> extends Primitive<\n NotificationPrimitiveOptions<T>\n> {\n protected readonly notificationService = $inject(NotificationService);\n\n public get name() {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n public async push(options: NotificationPushOptions<T>) {\n if (this.options.email) {\n await this.notificationService.createNotification({\n ...options,\n type: \"email\",\n template: this.name,\n });\n }\n }\n\n public configure(options: Partial<NotificationPrimitiveOptions<T>>) {\n Object.assign(this.options, options);\n }\n}\n\n$notification[KIND] = NotificationPrimitive;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface NotificationPushOptions<T extends TObject> {\n variables: StaticEncode<T>;\n contact: string;\n}\n\nexport interface NotificationMessage<T extends TObject> {\n email?: {\n subject: string;\n body: string | ((variables: Static<T>) => string);\n };\n sms?: {\n message: string | ((variables: Static<T>) => string);\n };\n}\n","import { $inject, Alepha, AlephaError } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { EmailProvider } from \"alepha/email\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository } from \"alepha/orm\";\nimport { SmsProvider } from \"alepha/sms\";\nimport {\n type NotificationEntity,\n notifications,\n} from \"../entities/notifications.ts\";\nimport { $notification } from \"../primitives/$notification.ts\";\n\nexport class NotificationSenderService {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly notificationRepository = $repository(notifications);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly emailProvider = $inject(EmailProvider);\n protected readonly smsProvider = $inject(SmsProvider);\n\n public async send(notificationId: string | NotificationEntity) {\n this.log.trace(\"Sending notification\", {\n notificationId:\n typeof notificationId === \"string\" ? notificationId : notificationId.id,\n });\n\n const notification =\n typeof notificationId === \"string\"\n ? await this.notificationRepository.findById(notificationId)\n : notificationId;\n\n if (notification.sentAt) {\n this.log.debug(\"Notification already sent\", {\n notificationId: notification.id,\n sentAt: notification.sentAt,\n });\n return;\n }\n\n this.log.debug(\"Processing notification\", {\n id: notification.id,\n type: notification.type,\n template: notification.template,\n contact: notification.contact,\n });\n\n try {\n if (notification.type === \"email\") {\n await this.emailProvider.send(this.renderEmail(notification));\n notification.sentAt = this.dateTimeProvider.nowISOString();\n this.log.info(\"Email notification sent\", {\n id: notification.id,\n template: notification.template,\n contact: notification.contact,\n });\n }\n if (notification.type === \"sms\") {\n await this.smsProvider.send(this.renderSms(notification));\n notification.sentAt = this.dateTimeProvider.nowISOString();\n this.log.info(\"SMS notification sent\", {\n id: notification.id,\n template: notification.template,\n contact: notification.contact,\n });\n }\n } catch (e) {\n this.log.error(\"Failed to send notification\", {\n id: notification.id,\n type: notification.type,\n template: notification.template,\n contact: notification.contact,\n error: e,\n });\n if (e instanceof Error) {\n notification.error = {\n at: this.dateTimeProvider.nowISOString(),\n name: e.name,\n message: e.message,\n };\n }\n } finally {\n await this.notificationRepository.save(notification);\n }\n }\n\n public renderSms(notification: NotificationEntity) {\n this.log.trace(\"Rendering SMS notification\", {\n id: notification.id,\n template: notification.template,\n });\n\n const { variables, contact, template } = this.load(notification);\n\n const sms = template.options.sms;\n if (!sms) {\n this.log.error(\"Notification template has no SMS defined\", {\n id: notification.id,\n template: notification.template,\n });\n throw new AlephaError(\n `Notification template ${notification.template} has no sms defined`,\n );\n }\n\n this.log.debug(\"Rendering SMS\", {\n template: notification.template,\n contact,\n });\n\n const message =\n typeof sms.message === \"function\"\n ? sms.message(variables as any)\n : sms.message;\n\n return {\n to: contact,\n message,\n };\n }\n\n public renderEmail(notification: NotificationEntity) {\n this.log.trace(\"Rendering email notification\", {\n id: notification.id,\n template: notification.template,\n });\n\n const { variables, contact, template } = this.load(notification);\n\n const email = template.options.email;\n if (!email) {\n this.log.error(\"Notification template has no email defined\", {\n id: notification.id,\n template: notification.template,\n });\n throw new AlephaError(\n `Notification template ${notification.template} has no email defined`,\n );\n }\n\n this.log.debug(\"Rendering email\", {\n template: notification.template,\n contact,\n subject: email.subject,\n });\n\n const subject = email.subject;\n\n const body =\n typeof email.body === \"function\"\n ? email.body(variables as any)\n : email.body;\n\n return {\n to: contact,\n subject,\n body,\n };\n }\n\n protected load(notification: NotificationEntity) {\n const variables = notification.variables || {};\n const contact = notification.contact;\n const template = this.alepha\n .primitives($notification)\n .find((it) => it.name === notification.template);\n\n if (!template) {\n this.log.error(\"Notification template not found\", {\n id: notification.id,\n template: notification.template,\n });\n throw new AlephaError(\n `No notification template found for ${notification.template}`,\n );\n }\n\n return {\n template,\n variables,\n contact,\n };\n }\n}\n","import { $inject, t } from \"alepha\";\nimport { $queue } from \"alepha/queue\";\nimport { NotificationSenderService } from \"../services/NotificationSenderService.ts\";\n\nexport class NotificationQueues {\n protected readonly notificationSenderService = $inject(\n NotificationSenderService,\n );\n\n public readonly processNotification = $queue({\n description: \"Queue for processing notifications\",\n schema: t.object({\n notificationId: t.string({ format: \"uuid\" }),\n }),\n handler: async (message) => {\n await this.notificationSenderService.send(message.payload.notificationId);\n },\n });\n}\n","import { type Static, t } from \"alepha\";\nimport { notifications } from \"../entities/notifications.ts\";\n\nexport const notificationCreateSchema = t.pick(notifications.schema, [\n \"type\",\n \"contact\",\n \"template\",\n \"variables\",\n]);\n\nexport type NotificationCreate = Static<typeof notificationCreateSchema>;\n","import { $env, $inject, Alepha, type Static, t } from \"alepha\";\nimport { $batch } from \"alepha/batch\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository, type Page } from \"alepha/orm\";\nimport {\n type NotificationEntity,\n notifications,\n} from \"../entities/notifications.ts\";\nimport { NotificationQueues } from \"../queues/NotificationQueues.ts\";\nimport {\n type NotificationCreate,\n notificationCreateSchema,\n} from \"../schemas/notificationCreateSchema.ts\";\nimport type { NotificationQuery } from \"../schemas/notificationQuerySchema.ts\";\nimport { NotificationSenderService } from \"./NotificationSenderService.ts\";\n\nexport const notificationServiceEnvSchema = t.object({\n NOTIFICATION_QUEUE: t.optional(\n t.boolean({\n description:\n \"If true, notifications will be queued instead of sent immediately\",\n }),\n ),\n});\n\ndeclare module \"alepha\" {\n interface Env extends Partial<Static<typeof notificationServiceEnvSchema>> {}\n}\n\nexport class NotificationService {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly env = $env(notificationServiceEnvSchema);\n protected readonly notificationRepository = $repository(notifications);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly notificationSenderService = $inject(\n NotificationSenderService,\n );\n\n public readonly notificationBatch = $batch({\n maxSize: 100,\n maxDuration: [15, \"seconds\"],\n schema: notificationCreateSchema,\n handler: async (notifications: NotificationCreate[]) => {\n this.log.debug(\"Processing notification batch\", {\n size: notifications.length,\n templates: [...new Set(notifications.map((n) => n.template))],\n });\n\n const entities =\n await this.notificationRepository.createMany(notifications);\n\n await this.alepha\n .inject(NotificationQueues)\n .processNotification.push(\n ...entities.map((it) => ({ notificationId: it.id })),\n );\n\n this.log.info(\"Notification batch queued\", {\n count: entities.length,\n ids: entities.map((it) => it.id),\n });\n },\n });\n\n public async findNotificationById(id: string) {\n this.log.trace(\"Finding notification by ID\", { id });\n return this.notificationRepository.findOne({ where: { id } });\n }\n\n public async findNotifications(\n q: NotificationQuery = {},\n ): Promise<Page<NotificationEntity>> {\n this.log.trace(\"Finding notifications\", { query: q });\n q.sort ??= \"-createdAt\";\n\n const where = this.notificationRepository.createQueryWhere();\n\n if (q.type) {\n where.type = { eq: q.type };\n }\n\n if (q.template) {\n where.template = { like: `%${q.template}%` };\n }\n\n if (q.contact) {\n where.contact = { like: `%${q.contact}%` };\n }\n\n if (q.category) {\n where.category = { eq: q.category };\n }\n\n if (q.status) {\n if (q.status === \"sent\") {\n where.sentAt = { isNotNull: true };\n where.error = { isNull: true };\n } else if (q.status === \"failed\") {\n where.error = { isNotNull: true };\n } else if (q.status === \"pending\") {\n where.sentAt = { isNull: true };\n where.error = { isNull: true };\n }\n }\n\n return this.notificationRepository.paginate(q, { where }, { count: true });\n }\n\n /**\n * Create a new notification.\n */\n public async createNotification(entry: NotificationCreate): Promise<void> {\n this.log.trace(\"Creating notification\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n });\n\n if (\n this.env.NOTIFICATION_QUEUE !== true ||\n this.alepha.isServerless() ||\n this.alepha.isTest()\n ) {\n this.log.debug(\"Sending notification immediately\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n });\n const notification = await this.notificationRepository.create(entry);\n await this.notificationSenderService.send(notification);\n return;\n }\n\n this.log.debug(\"Queuing notification to batch\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n });\n\n this.notificationBatch.push(entry).catch((e) => {\n this.log.error(\"Failed to push notification to batch\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n error: e,\n });\n });\n }\n}\n","import { $inject, t } from \"alepha\";\nimport { $action } from \"alepha/server\";\nimport { notifications } from \"../entities/notifications.ts\";\nimport { notificationQuerySchema } from \"../schemas/notificationQuerySchema.ts\";\nimport { NotificationService } from \"../services/NotificationService.ts\";\n\nexport class AdminNotificationController {\n protected readonly url = \"/notifications\";\n protected readonly group = \"admin:notifications\";\n protected readonly notificationService = $inject(NotificationService);\n\n /**\n * Find notifications with pagination and filtering.\n */\n public readonly findNotifications = $action({\n path: this.url,\n group: this.group,\n secure: true,\n description: \"Find notifications with pagination and filtering\",\n schema: {\n query: notificationQuerySchema,\n response: t.page(notifications.schema),\n },\n handler: ({ query }) => this.notificationService.findNotifications(query),\n });\n}\n","export class NotificationJobs {\n // - retry (lost, failed) notifications\n // - purge old notifications\n}\n","import { type Static, t } from \"alepha\";\n\nexport const notificationContactPreferencesSchema = t.object({\n language: t.optional(t.text()),\n exclude: t.array(t.text()),\n});\n\nexport type NotificationContactPreferences = Static<\n typeof notificationContactPreferencesSchema\n>;\n","import { $module } from \"alepha\";\nimport { AdminNotificationController } from \"./controllers/AdminNotificationController.ts\";\nimport { NotificationJobs } from \"./jobs/NotificationJobs.ts\";\nimport { $notification } from \"./primitives/$notification.ts\";\nimport { NotificationQueues } from \"./queues/NotificationQueues.ts\";\nimport { NotificationSenderService } from \"./services/NotificationSenderService.ts\";\nimport {\n NotificationService,\n notificationServiceEnvSchema,\n} from \"./services/NotificationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./controllers/AdminNotificationController.ts\";\nexport * from \"./entities/notifications.ts\";\nexport * from \"./jobs/NotificationJobs.ts\";\nexport * from \"./primitives/$notification.ts\";\nexport * from \"./queues/NotificationQueues.ts\";\nexport * from \"./schemas/notificationContactPreferencesSchema.ts\";\nexport * from \"./schemas/notificationCreateSchema.ts\";\nexport * from \"./schemas/notificationQuerySchema.ts\";\nexport * from \"./services/NotificationSenderService.ts\";\nexport * from \"./services/NotificationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * | Stability | Since | Runtime |\n * |-----------|-------|---------|\n * | 3 - stable | 0.10.0 | node, bun, workerd|\n *\n * User notification management.\n *\n * **Features:**\n * - Notification definitions\n * - Email/SMS notification sending\n * - Status tracking\n * - User preferences\n * - Queue integration\n *\n * @module alepha.api.notifications\n */\nexport const AlephaApiNotifications = $module({\n name: \"alepha.api.notifications\",\n primitives: [$notification],\n services: [\n AdminNotificationController,\n NotificationService,\n NotificationSenderService,\n NotificationQueues,\n NotificationJobs,\n ],\n register: (alepha) => {\n const env = alepha.parseEnv(notificationServiceEnvSchema);\n if (env.NOTIFICATION_QUEUE) {\n alepha.with(NotificationQueues);\n }\n\n alepha\n .with(AdminNotificationController)\n .with(NotificationService)\n .with(NotificationSenderService)\n .with(NotificationJobs);\n },\n});\n"],"mappings":"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"}