@k2b/cloud 0.6.0-rc.0

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 (540) hide show
  1. package/README.md +98 -0
  2. package/package.json +128 -0
  3. package/public/logo.svg +1 -0
  4. package/scripts/app-favicon.test.ts +49 -0
  5. package/scripts/app-favicon.ts +51 -0
  6. package/scripts/build.ts +205 -0
  7. package/scripts/preload.ts +164 -0
  8. package/scripts/sync-recovery-smoke.ts +200 -0
  9. package/src/_internal/app-favicon.ts +2 -0
  10. package/src/_internal/app-presentation.ts +61 -0
  11. package/src/_internal/app-workspace-state.ts +24 -0
  12. package/src/_internal/bounded-json.ts +47 -0
  13. package/src/_internal/build-metadata.ts +9 -0
  14. package/src/_internal/capabilities.ts +1118 -0
  15. package/src/_internal/define-app.ts +801 -0
  16. package/src/_internal/heartbeat.ts +167 -0
  17. package/src/_internal/help-catalog.ts +241 -0
  18. package/src/_internal/help.ts +95 -0
  19. package/src/_internal/nats-connection.ts +34 -0
  20. package/src/_internal/page-responses.ts +40 -0
  21. package/src/_internal/process-sync.ts +117 -0
  22. package/src/_internal/readiness.ts +3 -0
  23. package/src/_internal/registry-validation.ts +136 -0
  24. package/src/_internal/registry-watch.ts +52 -0
  25. package/src/_internal/registry.ts +338 -0
  26. package/src/_internal/runtime-compatibility.ts +64 -0
  27. package/src/_internal/runtime-context.ts +84 -0
  28. package/src/_internal/runtime-watcher.ts +78 -0
  29. package/src/_internal/static-assets.ts +86 -0
  30. package/src/_internal/status-preserving-ssr.ts +49 -0
  31. package/src/access/PermissionEditor.tsx +486 -0
  32. package/src/access/ResourceApiKeys.tsx +271 -0
  33. package/src/access/messages.ts +121 -0
  34. package/src/access/ui.ts +16 -0
  35. package/src/account/Avatar.tsx +28 -0
  36. package/src/account/EntitySearch.tsx +363 -0
  37. package/src/account/avatar-upload-dialog.tsx +197 -0
  38. package/src/account/avatar-upload.ts +149 -0
  39. package/src/account/ui.ts +3 -0
  40. package/src/ai/admin.ts +47 -0
  41. package/src/ai/approval-routes.ts +130 -0
  42. package/src/ai/approvals.ts +139 -0
  43. package/src/ai/assistant-models.ts +61 -0
  44. package/src/ai/attachments.ts +40 -0
  45. package/src/ai/browser.ts +92 -0
  46. package/src/ai/capabilities.ts +714 -0
  47. package/src/ai/capability-execution.ts +151 -0
  48. package/src/ai/chat/blocks.render.test.tsx +1108 -0
  49. package/src/ai/chat/blocks.tsx +730 -0
  50. package/src/ai/chat/builtin-tools.tsx +428 -0
  51. package/src/ai/chat/capability-table.tsx +87 -0
  52. package/src/ai/chat/composer-adapter.ts +169 -0
  53. package/src/ai/chat/file-tools.tsx +81 -0
  54. package/src/ai/chat/message-actions.tsx +370 -0
  55. package/src/ai/chat/message-utils.ts +479 -0
  56. package/src/ai/chat/messages.ts +43 -0
  57. package/src/ai/chat/presentation.tsx +395 -0
  58. package/src/ai/chat/primitives.tsx +3 -0
  59. package/src/ai/chat/tool-disclosure.tsx +41 -0
  60. package/src/ai/chat/turn-navigator-utils.ts +28 -0
  61. package/src/ai/chat/turn-navigator.tsx +164 -0
  62. package/src/ai/chat/user-message.tsx +149 -0
  63. package/src/ai/chat/visual-tools.tsx +435 -0
  64. package/src/ai/chat/web-tools.tsx +202 -0
  65. package/src/ai/chat-task-contracts.ts +113 -0
  66. package/src/ai/chat-tasks.ts +952 -0
  67. package/src/ai/client/controller.ts +1211 -0
  68. package/src/ai/client/file-source.ts +95 -0
  69. package/src/ai/client/live-connection.ts +140 -0
  70. package/src/ai/client/projection.ts +178 -0
  71. package/src/ai/client/transport.ts +111 -0
  72. package/src/ai/compaction.ts +182 -0
  73. package/src/ai/credentials.ts +124 -0
  74. package/src/ai/default-tool-contracts.ts +108 -0
  75. package/src/ai/default-tools.ts +127 -0
  76. package/src/ai/enrich.ts +280 -0
  77. package/src/ai/executor.ts +1624 -0
  78. package/src/ai/fetch-file-tool.ts +313 -0
  79. package/src/ai/file-context.ts +92 -0
  80. package/src/ai/file-media-type.ts +28 -0
  81. package/src/ai/file-mount.ts +16 -0
  82. package/src/ai/file-tools.ts +566 -0
  83. package/src/ai/files-store.ts +390 -0
  84. package/src/ai/firecrawl-tools.ts +305 -0
  85. package/src/ai/grids-skill.ts +126 -0
  86. package/src/ai/http.ts +265 -0
  87. package/src/ai/index.ts +337 -0
  88. package/src/ai/kit-skill.ts +22 -0
  89. package/src/ai/limits.ts +5 -0
  90. package/src/ai/live-events.ts +289 -0
  91. package/src/ai/live-messages.ts +45 -0
  92. package/src/ai/live-outbox.ts +102 -0
  93. package/src/ai/live-routes.ts +412 -0
  94. package/src/ai/live.ts +2 -0
  95. package/src/ai/maintenance.ts +302 -0
  96. package/src/ai/markdown-pdf-tool.ts +95 -0
  97. package/src/ai/memories.ts +605 -0
  98. package/src/ai/memory-learning-runs.ts +222 -0
  99. package/src/ai/memory-learning.ts +619 -0
  100. package/src/ai/memory-tool.ts +120 -0
  101. package/src/ai/memory-workflow-evidence.ts +158 -0
  102. package/src/ai/migrate.ts +2082 -0
  103. package/src/ai/model-access-migrate.ts +30 -0
  104. package/src/ai/model-access.ts +191 -0
  105. package/src/ai/personal-agent.ts +6 -0
  106. package/src/ai/prefs.ts +72 -0
  107. package/src/ai/project-tool.ts +94 -0
  108. package/src/ai/projects-routes.ts +277 -0
  109. package/src/ai/projects.ts +902 -0
  110. package/src/ai/protocol.ts +279 -0
  111. package/src/ai/provider.ts +73 -0
  112. package/src/ai/public-projection.ts +25 -0
  113. package/src/ai/resource-markers.ts +31 -0
  114. package/src/ai/resource-refs.ts +70 -0
  115. package/src/ai/routes.ts +1131 -0
  116. package/src/ai/runtime-tools.ts +39 -0
  117. package/src/ai/runtime.ts +544 -0
  118. package/src/ai/settings.ts +529 -0
  119. package/src/ai/short-id.ts +45 -0
  120. package/src/ai/skill-catalog.ts +73 -0
  121. package/src/ai/skill-format.ts +145 -0
  122. package/src/ai/skill-seeds.ts +430 -0
  123. package/src/ai/skill-tool.ts +81 -0
  124. package/src/ai/skills-routes.ts +143 -0
  125. package/src/ai/skills.ts +934 -0
  126. package/src/ai/solid.ts +27 -0
  127. package/src/ai/store.ts +3113 -0
  128. package/src/ai/stream.ts +308 -0
  129. package/src/ai/structured-runs.ts +66 -0
  130. package/src/ai/structured.ts +186 -0
  131. package/src/ai/system-prompt.ts +170 -0
  132. package/src/ai/task-prompt.ts +22 -0
  133. package/src/ai/timeline.ts +148 -0
  134. package/src/ai/tool-audit.ts +177 -0
  135. package/src/ai/tool-result-budget.ts +16 -0
  136. package/src/ai/tools.ts +155 -0
  137. package/src/ai/types.ts +967 -0
  138. package/src/ai/ui.tsx +39 -0
  139. package/src/ai/usage-migrate.ts +81 -0
  140. package/src/ai/usage.ts +315 -0
  141. package/src/ai/validate.ts +54 -0
  142. package/src/ai/vision-tool.ts +79 -0
  143. package/src/api/accounts-entities.ts +174 -0
  144. package/src/api/admin-ai-projects.ts +74 -0
  145. package/src/api/admin-ai-skills.ts +76 -0
  146. package/src/api/admin-ai-usage.ts +42 -0
  147. package/src/api/admin-core-settings.ts +508 -0
  148. package/src/api/admin-identity.ts +148 -0
  149. package/src/api/admin-lifecycle.ts +206 -0
  150. package/src/api/admin-linux-identities.ts +66 -0
  151. package/src/api/announcements.ts +131 -0
  152. package/src/api/app-approval.ts +129 -0
  153. package/src/api/apps.ts +51 -0
  154. package/src/api/auth/schemas.ts +54 -0
  155. package/src/api/auth.ts +370 -0
  156. package/src/api/capabilities.ts +658 -0
  157. package/src/api/help-messages.ts +19 -0
  158. package/src/api/help.ts +78 -0
  159. package/src/api/index.ts +108 -0
  160. package/src/api/mcp.ts +744 -0
  161. package/src/api/me-rail.ts +45 -0
  162. package/src/api/me.ts +753 -0
  163. package/src/api/search/schemas.ts +79 -0
  164. package/src/api/search.bench.ts +185 -0
  165. package/src/api/search.ts +407 -0
  166. package/src/api/sync-ops.ts +79 -0
  167. package/src/api/widgets.ts +143 -0
  168. package/src/browser/CloudResourceSearch.behavior.test.tsx +74 -0
  169. package/src/browser/CloudResourceSearch.tsx +791 -0
  170. package/src/browser/app-approval-vault.ts +305 -0
  171. package/src/browser/app-approval.ts +234 -0
  172. package/src/browser/live-websocket.ts +268 -0
  173. package/src/browser/locale-preference.ts +25 -0
  174. package/src/browser/notification-target.ts +27 -0
  175. package/src/browser/notification-timeout.ts +14 -0
  176. package/src/browser/notifications.ts +181 -0
  177. package/src/browser/resource-clipboard.ts +70 -0
  178. package/src/browser/resource-picker.tsx +39 -0
  179. package/src/browser/resource-search-messages.ts +55 -0
  180. package/src/browser/resource-search.ts +27 -0
  181. package/src/capabilities/client.ts +149 -0
  182. package/src/capabilities/response.ts +39 -0
  183. package/src/capabilities/server.ts +215 -0
  184. package/src/capabilities/signals.ts +4 -0
  185. package/src/capabilities/testing.ts +12 -0
  186. package/src/capabilities/types.ts +32 -0
  187. package/src/cli/access.ts +499 -0
  188. package/src/cli/account.ts +382 -0
  189. package/src/cli/admin/account-administration.ts +45 -0
  190. package/src/cli/admin/account-categories.ts +49 -0
  191. package/src/cli/admin/ai-usage.ts +118 -0
  192. package/src/cli/admin/app-sign-in.ts +49 -0
  193. package/src/cli/admin/data.ts +343 -0
  194. package/src/cli/admin/documentation.ts +34 -0
  195. package/src/cli/admin/gateway.ts +155 -0
  196. package/src/cli/admin/index.ts +61 -0
  197. package/src/cli/admin/instance.ts +250 -0
  198. package/src/cli/admin/jobs.ts +223 -0
  199. package/src/cli/admin/legal.ts +122 -0
  200. package/src/cli/admin/linux.ts +53 -0
  201. package/src/cli/admin/logs.ts +324 -0
  202. package/src/cli/admin/metrics.ts +142 -0
  203. package/src/cli/admin/nats.ts +167 -0
  204. package/src/cli/admin/notifications.ts +516 -0
  205. package/src/cli/admin/shared.ts +113 -0
  206. package/src/cli/admin/sync.ts +188 -0
  207. package/src/cli/admin/telemetry.ts +314 -0
  208. package/src/cli/admin/webhooks.ts +222 -0
  209. package/src/cli/admin/workflows.ts +367 -0
  210. package/src/cli/apps.ts +40 -0
  211. package/src/cli/capabilities.ts +239 -0
  212. package/src/cli/commands.ts +604 -0
  213. package/src/cli/index.ts +61 -0
  214. package/src/cli/locale.ts +31 -0
  215. package/src/cli/output.ts +58 -0
  216. package/src/clients/core.ts +20 -0
  217. package/src/config/env.ts +54 -0
  218. package/src/config/index.ts +6 -0
  219. package/src/contracts/account-categories.ts +62 -0
  220. package/src/contracts/announcements.ts +121 -0
  221. package/src/contracts/app-approval.ts +197 -0
  222. package/src/contracts/app.ts +162 -0
  223. package/src/contracts/capabilities.ts +484 -0
  224. package/src/contracts/index.ts +14 -0
  225. package/src/contracts/notification-live.ts +66 -0
  226. package/src/contracts/notification-types.ts +235 -0
  227. package/src/contracts/posix.ts +69 -0
  228. package/src/contracts/profile.ts +85 -0
  229. package/src/contracts/rail-preferences.ts +53 -0
  230. package/src/contracts/registry.ts +110 -0
  231. package/src/contracts/resource-clipboard.ts +33 -0
  232. package/src/contracts/settings-types.ts +98 -0
  233. package/src/contracts/shared.ts +408 -0
  234. package/src/contracts/user-notifications.ts +101 -0
  235. package/src/contracts/widgets.ts +183 -0
  236. package/src/desktop/index.ts +704 -0
  237. package/src/desktop/solid.tsx +938 -0
  238. package/src/index.ts +58 -0
  239. package/src/server/actor.ts +41 -0
  240. package/src/server/api/index.ts +2 -0
  241. package/src/server/api/respond.ts +96 -0
  242. package/src/server/api-client.ts +54 -0
  243. package/src/server/app-context.ts +39 -0
  244. package/src/server/help.ts +187 -0
  245. package/src/server/index.ts +89 -0
  246. package/src/server/locale.ts +55 -0
  247. package/src/server/middleware/auth.ts +386 -0
  248. package/src/server/middleware/index.ts +28 -0
  249. package/src/server/middleware/invocation.ts +55 -0
  250. package/src/server/middleware/middleware.ts +65 -0
  251. package/src/server/middleware/openapi.ts +126 -0
  252. package/src/server/middleware/rate-limit.ts +125 -0
  253. package/src/server/middleware/request-logger.ts +41 -0
  254. package/src/server/middleware/route-template.ts +46 -0
  255. package/src/server/middleware/runtime.ts +22 -0
  256. package/src/server/middleware/settings.ts +55 -0
  257. package/src/server/middleware/validator.ts +49 -0
  258. package/src/server/middleware/workload.ts +22 -0
  259. package/src/server/ratelimit.ts +151 -0
  260. package/src/server/request-cookies.ts +18 -0
  261. package/src/server/services/access.ts +644 -0
  262. package/src/server/services/freeipa/client.ts +137 -0
  263. package/src/server/services/freeipa/index.ts +9 -0
  264. package/src/server/services/freeipa/session.ts +123 -0
  265. package/src/server/services/freeipa/test-certificates.ts +44 -0
  266. package/src/server/services/freeipa/tls.ts +44 -0
  267. package/src/server/services/freeipa/transport.ts +116 -0
  268. package/src/server/services/freeipa/util.ts +59 -0
  269. package/src/server/services/geo.ts +198 -0
  270. package/src/server/services/index.ts +48 -0
  271. package/src/server/services/pagination.ts +22 -0
  272. package/src/server/services/services.ts +13 -0
  273. package/src/server/time.ts +33 -0
  274. package/src/services/account-category-policy.ts +51 -0
  275. package/src/services/account-lifecycle/audit.ts +41 -0
  276. package/src/services/account-lifecycle/index.ts +940 -0
  277. package/src/services/account-lifecycle/ipa-backfill.ts +152 -0
  278. package/src/services/account-lifecycle/notification-sender.ts +13 -0
  279. package/src/services/account-lifecycle/scheduler.ts +506 -0
  280. package/src/services/account-model.ts +22 -0
  281. package/src/services/accounts/app.ts +1671 -0
  282. package/src/services/accounts/authz.ts +44 -0
  283. package/src/services/accounts/avatar.ts +120 -0
  284. package/src/services/accounts/base-group.ts +11 -0
  285. package/src/services/accounts/base-user.ts +46 -0
  286. package/src/services/accounts/duplicate-emails.ts +82 -0
  287. package/src/services/accounts/entities.ts +622 -0
  288. package/src/services/accounts/group-sql.ts +106 -0
  289. package/src/services/accounts/groups.ts +272 -0
  290. package/src/services/accounts/index.ts +14 -0
  291. package/src/services/accounts/ipa-data.ts +66 -0
  292. package/src/services/accounts/lifecycle.ts +2 -0
  293. package/src/services/accounts/local-groups.ts +499 -0
  294. package/src/services/accounts/model.ts +129 -0
  295. package/src/services/accounts/notification-sender.ts +31 -0
  296. package/src/services/accounts/posix.ts +386 -0
  297. package/src/services/accounts/request-policy.ts +11 -0
  298. package/src/services/accounts/switching.ts +126 -0
  299. package/src/services/accounts/users.ts +745 -0
  300. package/src/services/announcements/index.ts +224 -0
  301. package/src/services/app-approval.ts +498 -0
  302. package/src/services/audit/index.ts +438 -0
  303. package/src/services/auth-flows/index.ts +15 -0
  304. package/src/services/auth-flows/ipa.ts +197 -0
  305. package/src/services/auth-flows/magic-link.ts +185 -0
  306. package/src/services/auth-flows/notification-sender.ts +10 -0
  307. package/src/services/auth-flows/password-reset.ts +268 -0
  308. package/src/services/auth-flows/proxy-return.ts +49 -0
  309. package/src/services/document-extraction.ts +148 -0
  310. package/src/services/freeipa-config.ts +179 -0
  311. package/src/services/gateway.ts +186 -0
  312. package/src/services/identity/constants.ts +18 -0
  313. package/src/services/identity/index.ts +52 -0
  314. package/src/services/identity/invocation-actor.ts +120 -0
  315. package/src/services/identity/invocation-authority.ts +53 -0
  316. package/src/services/identity/invocation-operations.ts +15 -0
  317. package/src/services/identity/invocation-token.ts +276 -0
  318. package/src/services/identity/key-config.ts +41 -0
  319. package/src/services/identity/key-ring.ts +530 -0
  320. package/src/services/identity/metrics.ts +29 -0
  321. package/src/services/identity/routes.ts +18 -0
  322. package/src/services/identity/runtime-config.ts +88 -0
  323. package/src/services/identity/session-token.ts +152 -0
  324. package/src/services/identity/signer-refresh.ts +44 -0
  325. package/src/services/identity/workload-auth.ts +64 -0
  326. package/src/services/index.ts +187 -0
  327. package/src/services/ipa/auth.ts +216 -0
  328. package/src/services/ipa/connection.ts +57 -0
  329. package/src/services/ipa/effective-groups.ts +70 -0
  330. package/src/services/ipa/groups.ts +722 -0
  331. package/src/services/ipa/guard.ts +17 -0
  332. package/src/services/ipa/index.ts +17 -0
  333. package/src/services/ipa/posix.ts +19 -0
  334. package/src/services/ipa/profile.ts +132 -0
  335. package/src/services/ipa/runtime.ts +28 -0
  336. package/src/services/ipa/search.ts +154 -0
  337. package/src/services/ipa/service-account.ts +15 -0
  338. package/src/services/ipa/sync-planning.ts +100 -0
  339. package/src/services/ipa/sync.ts +862 -0
  340. package/src/services/ipa/users.ts +806 -0
  341. package/src/services/legal-consent.ts +76 -0
  342. package/src/services/logging/index.ts +477 -0
  343. package/src/services/logging/redaction.ts +20 -0
  344. package/src/services/logging/trace.ts +1231 -0
  345. package/src/services/mandates/index.ts +855 -0
  346. package/src/services/mandates/policy.ts +85 -0
  347. package/src/services/network-security.ts +31 -0
  348. package/src/services/notifications/batches.ts +722 -0
  349. package/src/services/notifications/browser.ts +252 -0
  350. package/src/services/notifications/catalog.ts +113 -0
  351. package/src/services/notifications/channels.ts +104 -0
  352. package/src/services/notifications/dispatcher.ts +139 -0
  353. package/src/services/notifications/email.ts +118 -0
  354. package/src/services/notifications/index.ts +633 -0
  355. package/src/services/notifications/live.ts +50 -0
  356. package/src/services/notifications/observability.ts +407 -0
  357. package/src/services/notifications/platform.ts +421 -0
  358. package/src/services/notifications/runtime.ts +120 -0
  359. package/src/services/notifications/user.ts +248 -0
  360. package/src/services/notifications/web-push-transport.ts +135 -0
  361. package/src/services/oauth-tokens.ts +209 -0
  362. package/src/services/outbox.ts +187 -0
  363. package/src/services/pdf/gotenberg.ts +290 -0
  364. package/src/services/pdf/index.ts +43 -0
  365. package/src/services/pdf/markdown.ts +209 -0
  366. package/src/services/pdf/template-preview.ts +122 -0
  367. package/src/services/postgres.ts +76 -0
  368. package/src/services/providers/index.ts +27 -0
  369. package/src/services/providers/local/auth.ts +51 -0
  370. package/src/services/providers/local/index.ts +4 -0
  371. package/src/services/providers/local/users.ts +263 -0
  372. package/src/services/rail-preferences.ts +27 -0
  373. package/src/services/runtime-lifecycle.ts +191 -0
  374. package/src/services/secrets.ts +10 -0
  375. package/src/services/service-account-credentials.ts +891 -0
  376. package/src/services/service-accounts.ts +184 -0
  377. package/src/services/session/index.ts +236 -0
  378. package/src/services/session/test-fixture.ts +18 -0
  379. package/src/services/session/user.ts +144 -0
  380. package/src/services/settings/api.ts +61 -0
  381. package/src/services/settings/app.ts +86 -0
  382. package/src/services/settings/core-settings.ts +796 -0
  383. package/src/services/settings/crypto.ts +69 -0
  384. package/src/services/settings/defaults.ts +374 -0
  385. package/src/services/settings/index.ts +215 -0
  386. package/src/services/settings/namespace.ts +9 -0
  387. package/src/services/settings/snapshot.ts +49 -0
  388. package/src/services/settings/store.ts +241 -0
  389. package/src/services/settings/templates.ts +4 -0
  390. package/src/services/sync-ops.ts +403 -0
  391. package/src/services/topic-cursor.ts +22 -0
  392. package/src/services/weather/forecast.ts +323 -0
  393. package/src/services/weather/geo.ts +119 -0
  394. package/src/services/weather/index.ts +67 -0
  395. package/src/services/weather/location.ts +24 -0
  396. package/src/services/weather/locations.ts +133 -0
  397. package/src/services/weather/migrate.ts +45 -0
  398. package/src/services/weather/settings.ts +66 -0
  399. package/src/services/weather/short-id.ts +57 -0
  400. package/src/services/weather/types.ts +61 -0
  401. package/src/services/weather/ui.ts +50 -0
  402. package/src/services/webauthn.ts +385 -0
  403. package/src/shared/account-action-notice.ts +70 -0
  404. package/src/shared/account-display.ts +15 -0
  405. package/src/shared/account-session.ts +15 -0
  406. package/src/shared/ai-platform-prompt.ts +134 -0
  407. package/src/shared/ai-usage.ts +49 -0
  408. package/src/shared/app-presentation.ts +48 -0
  409. package/src/shared/app-url.ts +12 -0
  410. package/src/shared/branding.ts +14 -0
  411. package/src/shared/capability-messages.ts +70 -0
  412. package/src/shared/email-html.ts +54 -0
  413. package/src/shared/format.ts +91 -0
  414. package/src/shared/help.ts +68 -0
  415. package/src/shared/icons.ts +400 -0
  416. package/src/shared/index.ts +54 -0
  417. package/src/shared/locale.ts +48 -0
  418. package/src/shared/login-method.ts +44 -0
  419. package/src/shared/markdown/client.ts +130 -0
  420. package/src/shared/markdown/extensions/code.ts +105 -0
  421. package/src/shared/markdown/extensions/guided-help.ts +100 -0
  422. package/src/shared/markdown/extensions/images.ts +79 -0
  423. package/src/shared/markdown/extensions/info-blocks.ts +97 -0
  424. package/src/shared/markdown/extensions/katex.ts +120 -0
  425. package/src/shared/markdown/extensions/links.ts +43 -0
  426. package/src/shared/markdown/extensions/mark.ts +48 -0
  427. package/src/shared/markdown/extensions/sub-sup.ts +60 -0
  428. package/src/shared/markdown/extensions/tables.ts +109 -0
  429. package/src/shared/markdown/extensions/task-list.ts +53 -0
  430. package/src/shared/markdown/formula.ts +1187 -0
  431. package/src/shared/markdown/index.ts +193 -0
  432. package/src/shared/markdown/shared.ts +36 -0
  433. package/src/shared/mock-cover.ts +130 -0
  434. package/src/shared/network-address.ts +106 -0
  435. package/src/shared/redirect.ts +56 -0
  436. package/src/shared/template-rendering.ts +140 -0
  437. package/src/shared/theme.ts +68 -0
  438. package/src/shared/time.ts +13 -0
  439. package/src/ssr/AdminLayout.tsx +53 -0
  440. package/src/ssr/AdminSidebar.tsx +50 -0
  441. package/src/ssr/AppLaunchpad.island.tsx +236 -0
  442. package/src/ssr/AppWorkspaceController.island.tsx +21 -0
  443. package/src/ssr/BrowserNotifications.island.tsx +135 -0
  444. package/src/ssr/GlobalAnnouncements.island.tsx +142 -0
  445. package/src/ssr/GlobalSearchDialog.behavior.test.tsx +96 -0
  446. package/src/ssr/GlobalSearchDialog.tsx +35 -0
  447. package/src/ssr/GlobalSearchHelpDialog.tsx +104 -0
  448. package/src/ssr/GlobalSearchTrigger.island.tsx +55 -0
  449. package/src/ssr/HotkeysHelpRail.island.tsx +55 -0
  450. package/src/ssr/Layout.render.test.tsx +148 -0
  451. package/src/ssr/Layout.tsx +292 -0
  452. package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
  453. package/src/ssr/LayoutFooter.tsx +22 -0
  454. package/src/ssr/LayoutHeader.tsx +81 -0
  455. package/src/ssr/LayoutHelp.tsx +869 -0
  456. package/src/ssr/LayoutPreferences.island.tsx +36 -0
  457. package/src/ssr/LayoutRail.tsx +58 -0
  458. package/src/ssr/MinimalLayout.tsx +46 -0
  459. package/src/ssr/PageError.tsx +29 -0
  460. package/src/ssr/ProfilePreferences.island.tsx +92 -0
  461. package/src/ssr/RailApps.island.tsx +61 -0
  462. package/src/ssr/RailEditor.tsx +293 -0
  463. package/src/ssr/RegisteredHelpDocuments.island.tsx +3 -0
  464. package/src/ssr/TimezoneCookie.island.tsx +23 -0
  465. package/src/ssr/admin-active-link.ts +47 -0
  466. package/src/ssr/admin-navigation.ts +78 -0
  467. package/src/ssr/app-appearance.ts +23 -0
  468. package/src/ssr/app-navigation.ts +41 -0
  469. package/src/ssr/help-messages.ts +104 -0
  470. package/src/ssr/index.ts +14 -0
  471. package/src/ssr/islands/SearchBar.island.tsx +80 -0
  472. package/src/ssr/islands/index.ts +16 -0
  473. package/src/ssr/layout-context.ts +17 -0
  474. package/src/ssr/layout-help-markdown.ts +9 -0
  475. package/src/ssr/layout-help-navigation.ts +24 -0
  476. package/src/ssr/layout-help-url.ts +5 -0
  477. package/src/ssr/layout-runtime.ts +22 -0
  478. package/src/ssr/page-error-messages.ts +25 -0
  479. package/src/ssr/platform-messages.ts +135 -0
  480. package/src/ssr/preference-controller.ts +44 -0
  481. package/src/ssr/profile-preferences-messages.ts +29 -0
  482. package/src/ssr/rail-context.ts +21 -0
  483. package/src/ssr/rail-messages.ts +66 -0
  484. package/src/ssr/rail-navigation.ts +60 -0
  485. package/src/ssr/runtime.ts +31 -0
  486. package/src/ssr/url-filter.ts +127 -0
  487. package/src/styles/base-popover.css +36 -0
  488. package/src/styles/effects.css +851 -0
  489. package/src/styles/global.css +268 -0
  490. package/src/styles/input.css +58 -0
  491. package/src/styles/tokens.css +230 -0
  492. package/src/styles/utilities-buttons.css +405 -0
  493. package/src/styles/utilities-data.css +33 -0
  494. package/src/styles/utilities-feedback.css +232 -0
  495. package/src/styles/utilities-layout.css +603 -0
  496. package/src/styles/utilities-markdown-editor.css +525 -0
  497. package/src/styles/utilities-markdown-table.css +234 -0
  498. package/src/styles/utilities-navigation.css +1054 -0
  499. package/src/types/ambient.d.ts +24 -0
  500. package/src/workflows/ai/index.ts +17 -0
  501. package/src/workflows/ai/runtime.ts +286 -0
  502. package/src/workflows/ai/store.ts +314 -0
  503. package/src/workflows/ai/types.ts +111 -0
  504. package/src/workflows/ai-actions.ts +269 -0
  505. package/src/workflows/authoring.ts +88 -0
  506. package/src/workflows/builtins.ts +193 -0
  507. package/src/workflows/contracts.ts +280 -0
  508. package/src/workflows/definition.ts +334 -0
  509. package/src/workflows/editor.ts +113 -0
  510. package/src/workflows/index.ts +71 -0
  511. package/src/workflows/language/binder.ts +45 -0
  512. package/src/workflows/language/canonical.ts +29 -0
  513. package/src/workflows/language/compiler.ts +396 -0
  514. package/src/workflows/language/expressions.ts +34 -0
  515. package/src/workflows/language/index.ts +20 -0
  516. package/src/workflows/language/references.ts +55 -0
  517. package/src/workflows/language/schema.ts +134 -0
  518. package/src/workflows/language/strict-yaml.ts +131 -0
  519. package/src/workflows/manifest.ts +24 -0
  520. package/src/workflows/module.ts +12 -0
  521. package/src/workflows/runtime/coordinator.ts +115 -0
  522. package/src/workflows/runtime/executor.ts +1003 -0
  523. package/src/workflows/runtime/index.ts +60 -0
  524. package/src/workflows/runtime/instant.ts +8 -0
  525. package/src/workflows/runtime/ports.ts +213 -0
  526. package/src/workflows/runtime/schedule.ts +203 -0
  527. package/src/workflows/runtime/trigger.ts +37 -0
  528. package/src/workflows/runtime/wake.ts +13 -0
  529. package/src/workflows/store/actions.ts +438 -0
  530. package/src/workflows/store/budget.ts +145 -0
  531. package/src/workflows/store/definitions.ts +345 -0
  532. package/src/workflows/store/events.ts +383 -0
  533. package/src/workflows/store/index.ts +107 -0
  534. package/src/workflows/store/observability.ts +594 -0
  535. package/src/workflows/store/runs.ts +975 -0
  536. package/src/workflows/store/transaction.ts +23 -0
  537. package/src/workflows/store/worker.ts +439 -0
  538. package/src/workflows/testing/conformance.ts +72 -0
  539. package/src/workflows/testing/index.ts +11 -0
  540. package/src/workflows/testing/process-fixtures.ts +275 -0
@@ -0,0 +1,252 @@
1
+ import { createHash } from "node:crypto";
2
+ import { sql } from "bun";
3
+ import webpush from "web-push";
4
+ import { z } from "zod";
5
+ import { isSafeNotificationTargetHref } from "../../contracts/notification-types";
6
+ import { type BrowserPushSubscription, BrowserPushSubscriptionSchema } from "../../contracts/user-notifications";
7
+ import { decryptSecret, encryptSecret } from "../secrets";
8
+ import { coreSettings } from "../settings/api";
9
+ import { type NotificationDestination, registerNotificationChannel } from "./channels";
10
+ import { sendPinnedWebPush } from "./web-push-transport";
11
+
12
+ type EndpointRow = {
13
+ id: string;
14
+ endpoint_hash: string;
15
+ label: string;
16
+ secret_encrypted: string;
17
+ };
18
+
19
+ type BrowserDeliveryPayload = {
20
+ endpointId: string;
21
+ subscription: BrowserPushSubscription;
22
+ eventId: string;
23
+ title: string;
24
+ targetHref?: string;
25
+ };
26
+
27
+ const BrowserDeliveryPayloadSchema = z.object({
28
+ endpointId: z.uuid(),
29
+ subscription: BrowserPushSubscriptionSchema,
30
+ eventId: z.uuid(),
31
+ title: z.string().min(1).max(200),
32
+ targetHref: z.string().max(4_000).refine(isSafeNotificationTargetHref).optional(),
33
+ });
34
+
35
+ const BrowserDestinationContextSchema = z.object({
36
+ endpointId: z.uuid(),
37
+ subscription: BrowserPushSubscriptionSchema,
38
+ });
39
+
40
+ const endpointHash = (endpoint: string): string => createHash("sha256").update(endpoint).digest("hex");
41
+
42
+ let vapidConfigPromise: Promise<{ publicKey: string; privateKey: string }> | null = null;
43
+ let webPushConfigurationPromise: Promise<void> | null = null;
44
+ let unregisterDriver: (() => void) | null = null;
45
+
46
+ const ensureVapidConfig = (): Promise<{ publicKey: string; privateKey: string }> => {
47
+ if (vapidConfigPromise) return vapidConfigPromise;
48
+ vapidConfigPromise = sql
49
+ .begin(async (tx) => {
50
+ await tx`SELECT pg_advisory_xact_lock(hashtextextended('notifications:web-push-vapid', 0))`;
51
+ let [publicKey, privateKey] = await Promise.all([
52
+ coreSettings.get<string>("notifications.web_push_public_key"),
53
+ coreSettings.get<string>("notifications.web_push_private_key"),
54
+ ]);
55
+ if (!publicKey || !privateKey) {
56
+ const generated = webpush.generateVAPIDKeys();
57
+ publicKey = generated.publicKey;
58
+ privateKey = generated.privateKey;
59
+ await coreSettings.set("notifications.web_push_public_key", publicKey);
60
+ await coreSettings.set("notifications.web_push_private_key", privateKey);
61
+ }
62
+ return { publicKey, privateKey };
63
+ })
64
+ .catch((error) => {
65
+ vapidConfigPromise = null;
66
+ throw error;
67
+ });
68
+ return vapidConfigPromise;
69
+ };
70
+
71
+ const vapidSubject = async (): Promise<string> => {
72
+ const [configuredUrl, contactEmail] = await Promise.all([
73
+ coreSettings.get<string>("app.url"),
74
+ coreSettings.get<string>("app.contact_email"),
75
+ ]);
76
+ const rawUrl = configuredUrl?.trim();
77
+ if (rawUrl) {
78
+ try {
79
+ const parsed = new URL(/^https?:\/\//.test(rawUrl) ? rawUrl : `https://${rawUrl}`);
80
+ const isLocalhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]";
81
+ if (parsed.protocol === "https:" && !isLocalhost) return parsed.origin;
82
+ } catch {
83
+ // Fall through to the deployment contact below.
84
+ }
85
+ }
86
+ return contactEmail?.trim() ? `mailto:${contactEmail.trim()}` : "mailto:notifications@cloud.local";
87
+ };
88
+
89
+ const ensureWebPushConfigured = (): Promise<void> => {
90
+ if (webPushConfigurationPromise) return webPushConfigurationPromise;
91
+ webPushConfigurationPromise = Promise.all([ensureVapidConfig(), vapidSubject()])
92
+ .then(([config, subject]) => {
93
+ webpush.setVapidDetails(subject, config.publicKey, config.privateKey);
94
+ })
95
+ .catch((error) => {
96
+ webPushConfigurationPromise = null;
97
+ throw error;
98
+ });
99
+ return webPushConfigurationPromise;
100
+ };
101
+
102
+ const disableEndpoint = async (endpointId: string): Promise<void> => {
103
+ await sql`
104
+ UPDATE notifications.endpoints
105
+ SET disabled_at = now(), updated_at = now()
106
+ WHERE id = ${endpointId}::uuid AND channel = 'browser'
107
+ `;
108
+ };
109
+
110
+ const browserDriver = {
111
+ id: "browser" as const,
112
+ resolveDestinations: async (recipient: { userId: string | null }) => {
113
+ if (!recipient.userId) return [];
114
+ const rows = await sql<EndpointRow[]>`
115
+ SELECT id, endpoint_hash, label, secret_encrypted
116
+ FROM notifications.endpoints
117
+ WHERE user_id = ${recipient.userId}::uuid
118
+ AND channel = 'browser'
119
+ AND verified_at IS NOT NULL
120
+ AND disabled_at IS NULL
121
+ ORDER BY last_seen_at DESC, id
122
+ `;
123
+ const destinations: NotificationDestination[] = [];
124
+ for (const row of rows) {
125
+ try {
126
+ const subscription = BrowserPushSubscriptionSchema.parse(await decryptSecret(row.secret_encrypted));
127
+ if (subscription.expirationTime && subscription.expirationTime <= Date.now()) {
128
+ await disableEndpoint(row.id);
129
+ continue;
130
+ }
131
+ destinations.push({
132
+ key: row.endpoint_hash,
133
+ label: row.label,
134
+ endpointId: row.id,
135
+ context: { endpointId: row.id, subscription },
136
+ });
137
+ } catch {
138
+ await disableEndpoint(row.id);
139
+ }
140
+ }
141
+ return destinations;
142
+ },
143
+ createPayload: ({ presentation, destination, event }) => {
144
+ const context = BrowserDestinationContextSchema.parse(destination.context);
145
+ return BrowserDeliveryPayloadSchema.parse({
146
+ endpointId: context.endpointId,
147
+ subscription: context.subscription,
148
+ eventId: event.id,
149
+ title: presentation.title,
150
+ targetHref: presentation.targetHref,
151
+ });
152
+ },
153
+ deliver: async (value: unknown) => {
154
+ const payload: BrowserDeliveryPayload = BrowserDeliveryPayloadSchema.parse(value);
155
+ await ensureWebPushConfigured();
156
+ try {
157
+ await sendPinnedWebPush(
158
+ payload.subscription,
159
+ JSON.stringify({
160
+ type: "cloud-notification",
161
+ eventId: payload.eventId,
162
+ title: payload.title,
163
+ targetHref: payload.targetHref,
164
+ }),
165
+ {
166
+ TTL: 24 * 60 * 60,
167
+ urgency: "normal",
168
+ topic: createHash("sha256").update(payload.eventId).digest("base64url").slice(0, 32),
169
+ },
170
+ );
171
+ } catch (error) {
172
+ const statusCode = error && typeof error === "object" && "statusCode" in error ? Number(error.statusCode) : null;
173
+ if (statusCode === 404 || statusCode === 410) {
174
+ await disableEndpoint(payload.endpointId);
175
+ throw Object.assign(new Error("Browser notification endpoint is no longer registered"), {
176
+ code: "endpoint_gone",
177
+ retryable: false,
178
+ });
179
+ }
180
+ if (statusCode !== null && statusCode >= 400 && statusCode < 500 && statusCode !== 408 && statusCode !== 429) {
181
+ throw Object.assign(new Error("Browser push provider rejected the delivery"), {
182
+ code: "provider_rejected",
183
+ retryable: false,
184
+ });
185
+ }
186
+ throw error;
187
+ }
188
+ },
189
+ } satisfies import("./channels").NotificationChannelDriver;
190
+
191
+ // Every app process may create deliveries; Core remains the only process that
192
+ // runs the durable delivery queue.
193
+ unregisterDriver = registerNotificationChannel(browserDriver);
194
+
195
+ export const browserNotifications = {
196
+ start: async (): Promise<void> => {
197
+ unregisterDriver ??= registerNotificationChannel(browserDriver);
198
+ await ensureWebPushConfigured();
199
+ },
200
+ stop: (): void => {
201
+ unregisterDriver?.();
202
+ unregisterDriver = null;
203
+ },
204
+ configuration: async (): Promise<{ publicKey: string }> => {
205
+ const config = await ensureVapidConfig();
206
+ return { publicKey: config.publicKey };
207
+ },
208
+ registerEndpoint: async (config: {
209
+ userId: string;
210
+ subscription: BrowserPushSubscription;
211
+ label: string;
212
+ }): Promise<{ id: string; label: string }> => {
213
+ const subscription = BrowserPushSubscriptionSchema.parse(config.subscription);
214
+ const hash = endpointHash(subscription.endpoint);
215
+ const encrypted = await encryptSecret(subscription);
216
+ const label = config.label.trim().slice(0, 120) || "This browser";
217
+ const rows = await sql.begin(async (tx) => {
218
+ await tx`SELECT pg_advisory_xact_lock(hashtextextended(${`notifications:browser:${hash}`}, 0))`;
219
+ await tx`
220
+ UPDATE notifications.endpoints
221
+ SET disabled_at = now(), updated_at = now()
222
+ WHERE channel = 'browser' AND endpoint_hash = ${hash}
223
+ AND user_id <> ${config.userId}::uuid AND disabled_at IS NULL
224
+ `;
225
+ return tx<{ id: string; label: string }[]>`
226
+ INSERT INTO notifications.endpoints (
227
+ user_id, channel, endpoint_hash, label, secret_encrypted,
228
+ verified_at, last_seen_at, disabled_at, updated_at
229
+ ) VALUES (
230
+ ${config.userId}::uuid, 'browser', ${hash}, ${label}, ${encrypted},
231
+ now(), now(), NULL, now()
232
+ )
233
+ ON CONFLICT (user_id, channel, endpoint_hash) DO UPDATE
234
+ SET label = EXCLUDED.label, secret_encrypted = EXCLUDED.secret_encrypted,
235
+ verified_at = now(), last_seen_at = now(), disabled_at = NULL, updated_at = now()
236
+ RETURNING id, label
237
+ `;
238
+ });
239
+ return rows[0]!;
240
+ },
241
+ disableEndpoint: async (config: { userId: string; subscription: BrowserPushSubscription }): Promise<boolean> => {
242
+ const subscription = BrowserPushSubscriptionSchema.parse(config.subscription);
243
+ const rows = await sql<{ id: string }[]>`
244
+ UPDATE notifications.endpoints
245
+ SET disabled_at = now(), updated_at = now()
246
+ WHERE user_id = ${config.userId}::uuid AND channel = 'browser'
247
+ AND endpoint_hash = ${endpointHash(subscription.endpoint)} AND disabled_at IS NULL
248
+ RETURNING id
249
+ `;
250
+ return rows.length > 0;
251
+ },
252
+ } as const;
@@ -0,0 +1,113 @@
1
+ import { sql } from "bun";
2
+ import type { AnyBoundNotificationDefinition } from "../../contracts/notification-types";
3
+ import { logger } from "../logging";
4
+ import { toPgTextArray } from "../postgres";
5
+
6
+ type NotificationCatalog = Readonly<Record<string, AnyBoundNotificationDefinition>>;
7
+
8
+ const RETRY_MS = 5_000;
9
+ const log = logger("notifications:catalog");
10
+
11
+ const isSchemaUnavailable = (error: unknown): boolean => {
12
+ if (!error || typeof error !== "object" || !("code" in error)) return false;
13
+ return error.code === "42P01" || error.code === "42703" || error.code === "3F000";
14
+ };
15
+
16
+ const upsertDefinition = async (definition: AnyBoundNotificationDefinition, db: typeof sql = sql): Promise<void> => {
17
+ const recommended = toPgTextArray([...(definition.delivery?.recommended ?? [])]);
18
+ const required = toPgTextArray([...(definition.delivery?.required ?? [])]);
19
+ await db`
20
+ INSERT INTO notifications.definitions (
21
+ id, app_id, kind, label, description, presentation, recipient_kind,
22
+ recommended_channels, required_channels, active, last_seen_at, updated_at
23
+ ) VALUES (
24
+ ${definition.id}, ${definition.appId}, ${definition.key}, ${definition.label},
25
+ ${definition.description}, (${definition.presentation ? JSON.stringify(definition.presentation) : null}::text)::jsonb,
26
+ ${definition.recipient}, ${recommended}::text[],
27
+ ${required}::text[], true, now(), now()
28
+ )
29
+ ON CONFLICT (id) DO UPDATE SET
30
+ app_id = EXCLUDED.app_id,
31
+ kind = EXCLUDED.kind,
32
+ label = EXCLUDED.label,
33
+ description = EXCLUDED.description,
34
+ presentation = EXCLUDED.presentation,
35
+ recipient_kind = EXCLUDED.recipient_kind,
36
+ recommended_channels = EXCLUDED.recommended_channels,
37
+ required_channels = EXCLUDED.required_channels,
38
+ active = true,
39
+ last_seen_at = now(),
40
+ updated_at = now()
41
+ `;
42
+ };
43
+
44
+ /** Persist serializable app metadata; schemas and renderers remain code-owned. */
45
+ export const registerNotificationDefinitions = async (appId: string, definitions: NotificationCatalog): Promise<void> => {
46
+ const current = Object.values(definitions);
47
+ if (current.some((definition) => definition.appId !== appId)) {
48
+ throw new Error(`Notification catalog for app "${appId}" contains a definition owned by another app`);
49
+ }
50
+ const ids = toPgTextArray(current.map((definition) => definition.id));
51
+ await sql.begin(async (tx) => {
52
+ for (const definition of current) await upsertDefinition(definition, tx);
53
+ await tx`
54
+ UPDATE notifications.definitions
55
+ SET active = false, updated_at = now()
56
+ WHERE app_id = ${appId} AND NOT (id = ANY(${ids}::text[]))
57
+ `;
58
+ });
59
+ };
60
+
61
+ /** Keep app startup rollout-safe when Core has not expanded the schema yet. */
62
+ export const startNotificationDefinitionRegistration = async (
63
+ appId: string,
64
+ definitions: NotificationCatalog,
65
+ options: {
66
+ register?: (appId: string, definitions: NotificationCatalog) => Promise<void>;
67
+ retryMs?: number;
68
+ } = {},
69
+ ): Promise<() => void> => {
70
+ const register = options.register ?? registerNotificationDefinitions;
71
+ const retryMs = options.retryMs ?? RETRY_MS;
72
+ let stopped = false;
73
+ let timer: ReturnType<typeof setTimeout> | undefined;
74
+
75
+ const schedule = () => {
76
+ if (stopped || timer) return;
77
+ timer = setTimeout(() => {
78
+ timer = undefined;
79
+ void attempt();
80
+ }, retryMs);
81
+ if (typeof timer === "object" && "unref" in timer) timer.unref();
82
+ };
83
+
84
+ const attempt = async (): Promise<void> => {
85
+ if (stopped) return;
86
+ try {
87
+ await register(appId, definitions);
88
+ log.info("Notification definitions registered after schema became available", { appId });
89
+ } catch (error) {
90
+ log.warn("Notification definition registration failed; retrying", {
91
+ appId,
92
+ error: error instanceof Error ? error.message : "Notification catalog registration failed",
93
+ });
94
+ schedule();
95
+ }
96
+ };
97
+
98
+ try {
99
+ await register(appId, definitions);
100
+ } catch (error) {
101
+ if (!isSchemaUnavailable(error)) throw error;
102
+ log.warn("Notification schema is not available yet; registration will retry", { appId });
103
+ schedule();
104
+ }
105
+
106
+ return () => {
107
+ stopped = true;
108
+ if (timer) clearTimeout(timer);
109
+ timer = undefined;
110
+ };
111
+ };
112
+
113
+ export const ensureNotificationDefinition = upsertDefinition;
@@ -0,0 +1,104 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { EmailNotificationPresentation, NotificationChannelId, NotificationPresentation } from "../../contracts/notification-types";
3
+ import { sendEmail } from "./email";
4
+
5
+ export type ResolvedNotificationRecipient = {
6
+ userId: string | null;
7
+ email: string | null;
8
+ };
9
+
10
+ export type NotificationDestination = {
11
+ key: string;
12
+ label: string;
13
+ endpointId?: string;
14
+ context: unknown;
15
+ };
16
+
17
+ export type NotificationChannelDriver = {
18
+ id: NotificationChannelId | (string & {});
19
+ resolveDestinations: (recipient: ResolvedNotificationRecipient) => Promise<NotificationDestination[]>;
20
+ createPayload: (input: {
21
+ presentation: NotificationPresentation;
22
+ email?: EmailNotificationPresentation;
23
+ destination: NotificationDestination;
24
+ event: { id: string; definitionId: string };
25
+ }) => unknown;
26
+ deliver: (payload: unknown) => Promise<void>;
27
+ };
28
+
29
+ const drivers = new Map<string, NotificationChannelDriver>();
30
+
31
+ export const registerNotificationChannel = (driver: NotificationChannelDriver): (() => void) => {
32
+ if (!/^[a-z][a-z0-9_-]{0,79}$/.test(driver.id)) {
33
+ throw new Error("Notification channel id must be a lowercase identifier of at most 80 characters");
34
+ }
35
+ const existing = drivers.get(driver.id);
36
+ if (existing && existing !== driver) throw new Error(`Notification channel "${driver.id}" is already registered`);
37
+ drivers.set(driver.id, driver);
38
+ return () => {
39
+ if (drivers.get(driver.id) === driver) drivers.delete(driver.id);
40
+ };
41
+ };
42
+
43
+ export const getNotificationChannel = (id: string): NotificationChannelDriver | undefined => drivers.get(id);
44
+
45
+ export const listNotificationChannels = (): string[] => [...drivers.keys()].sort();
46
+
47
+ const normalizeEmail = (value: string): string => value.trim().toLowerCase();
48
+ const emailKey = (value: string): string => createHash("sha256").update(normalizeEmail(value)).digest("hex");
49
+
50
+ const emailLabel = (value: string): string => {
51
+ const [local = "", domain = ""] = normalizeEmail(value).split("@", 2);
52
+ if (!domain) return "Email";
53
+ return `${local.slice(0, 1) || "*"}***@${domain}`;
54
+ };
55
+
56
+ type EmailPayload = {
57
+ to: string;
58
+ subject: string;
59
+ content?: string;
60
+ rawHtml?: string;
61
+ messageId?: string;
62
+ };
63
+
64
+ const parseEmailPayload = (value: unknown): EmailPayload => {
65
+ if (!value || typeof value !== "object") throw new Error("Invalid email notification payload");
66
+ const payload = value as Partial<EmailPayload>;
67
+ if (typeof payload.to !== "string" || typeof payload.subject !== "string") {
68
+ throw new Error("Invalid email notification payload");
69
+ }
70
+ if (payload.content !== undefined && typeof payload.content !== "string") throw new Error("Invalid email notification payload");
71
+ if (payload.rawHtml !== undefined && typeof payload.rawHtml !== "string") throw new Error("Invalid email notification payload");
72
+ if (payload.messageId !== undefined && typeof payload.messageId !== "string") throw new Error("Invalid email notification payload");
73
+ return { to: payload.to, subject: payload.subject, content: payload.content, rawHtml: payload.rawHtml, messageId: payload.messageId };
74
+ };
75
+
76
+ const emailDriver: NotificationChannelDriver = {
77
+ id: "email",
78
+ resolveDestinations: async (recipient) => {
79
+ if (!recipient.email) return [];
80
+ const email = normalizeEmail(recipient.email);
81
+ return [{ key: emailKey(email), label: emailLabel(email), context: { email } }];
82
+ },
83
+ createPayload: ({ presentation, email, destination, event }) => {
84
+ const context = destination.context as { email?: unknown };
85
+ if (typeof context.email !== "string") throw new Error("Email destination is missing an address");
86
+ return {
87
+ to: context.email,
88
+ subject: email?.subject ?? presentation.title,
89
+ content: email?.content ?? presentation.body,
90
+ rawHtml: email?.rawHtml,
91
+ messageId: `<cloud-notification-${event.id}@cloud.invalid>`,
92
+ } satisfies EmailPayload;
93
+ },
94
+ deliver: async (value) => {
95
+ const payload = parseEmailPayload(value);
96
+ await sendEmail(payload.to, payload.subject, {
97
+ content: payload.content,
98
+ rawHtml: payload.rawHtml,
99
+ messageId: payload.messageId,
100
+ });
101
+ },
102
+ };
103
+
104
+ registerNotificationChannel(emailDriver);
@@ -0,0 +1,139 @@
1
+ import { sql } from "bun";
2
+ import { decryptSecret } from "../secrets";
3
+ import { getNotificationChannel } from "./channels";
4
+
5
+ const MAX_DELIVERY_ATTEMPTS = 5;
6
+ const BASE_RETRY_MS = 2_000;
7
+ const MAX_RETRY_MS = 5 * 60_000;
8
+
9
+ type DeliveryRow = {
10
+ id: string;
11
+ event_id: string;
12
+ channel: string;
13
+ payload_encrypted: string | null;
14
+ required: boolean;
15
+ route_priority: number | null;
16
+ attempt_count: number;
17
+ };
18
+
19
+ export type DeliveryAttemptResult =
20
+ | { status: "skipped" | "delivered" | "failed" }
21
+ | { status: "retry"; retryAfterMs: number; error: string };
22
+
23
+ const retryDelay = (attempt: number): number => Math.min(BASE_RETRY_MS * 2 ** Math.max(0, attempt - 1), MAX_RETRY_MS);
24
+
25
+ const activateNextFallback = async (eventId: string): Promise<string[]> => {
26
+ const delivered = await sql<{ exists: boolean }[]>`
27
+ SELECT EXISTS (
28
+ SELECT 1 FROM notifications.deliveries
29
+ WHERE event_id = ${eventId}::uuid AND required = false AND status = 'delivered'
30
+ ) AS exists
31
+ `;
32
+ if (delivered[0]?.exists) {
33
+ await sql`
34
+ UPDATE notifications.deliveries
35
+ SET status = 'suppressed', error_code = 'fallback_not_needed',
36
+ error_message = 'A preferred channel was delivered.', payload_encrypted = NULL, updated_at = now()
37
+ WHERE event_id = ${eventId}::uuid AND required = false AND status = 'deferred'
38
+ `;
39
+ return [];
40
+ }
41
+
42
+ const active = await sql<{ exists: boolean }[]>`
43
+ SELECT EXISTS (
44
+ SELECT 1 FROM notifications.deliveries
45
+ WHERE event_id = ${eventId}::uuid AND required = false AND status IN ('pending', 'sending')
46
+ ) AS exists
47
+ `;
48
+ if (active[0]?.exists) return [];
49
+
50
+ const rows = await sql<{ id: string }[]>`
51
+ WITH next_route AS (
52
+ SELECT MIN(route_priority) AS priority
53
+ FROM notifications.deliveries
54
+ WHERE event_id = ${eventId}::uuid AND required = false AND status = 'deferred'
55
+ )
56
+ UPDATE notifications.deliveries d
57
+ SET status = 'pending', next_attempt_at = now(), updated_at = now()
58
+ FROM next_route
59
+ WHERE d.event_id = ${eventId}::uuid
60
+ AND d.required = false
61
+ AND d.status = 'deferred'
62
+ AND d.route_priority = next_route.priority
63
+ RETURNING d.id
64
+ `;
65
+ return rows.map((row) => row.id);
66
+ };
67
+
68
+ export const processNotificationDelivery = async (deliveryId: string): Promise<DeliveryAttemptResult & { activatedIds?: string[] }> => {
69
+ const rows = await sql<DeliveryRow[]>`
70
+ UPDATE notifications.deliveries
71
+ SET status = 'sending', attempt_count = attempt_count + 1,
72
+ last_attempt_at = now(), updated_at = now()
73
+ WHERE id = ${deliveryId}::uuid
74
+ AND status = 'pending'
75
+ AND (next_attempt_at IS NULL OR next_attempt_at <= now())
76
+ RETURNING id, event_id, channel, payload_encrypted, required, route_priority, attempt_count
77
+ `;
78
+ const delivery = rows[0];
79
+ if (!delivery) return { status: "skipped" };
80
+
81
+ try {
82
+ const driver = getNotificationChannel(delivery.channel);
83
+ if (!driver)
84
+ throw Object.assign(new Error(`Notification channel "${delivery.channel}" is unavailable`), { code: "channel_unavailable" });
85
+ if (!delivery.payload_encrypted) {
86
+ throw Object.assign(new Error("Notification delivery payload is unavailable"), { code: "payload_missing", retryable: false });
87
+ }
88
+ const payload = await decryptSecret(delivery.payload_encrypted);
89
+ await driver.deliver(payload);
90
+ await sql`
91
+ UPDATE notifications.deliveries
92
+ SET status = 'delivered', delivered_at = now(), next_attempt_at = NULL,
93
+ error_code = NULL, error_message = NULL, payload_encrypted = NULL, updated_at = now()
94
+ WHERE id = ${delivery.id}::uuid
95
+ `;
96
+ const activatedIds = delivery.required ? [] : await activateNextFallback(delivery.event_id);
97
+ return { status: "delivered", activatedIds };
98
+ } catch (error) {
99
+ const message = error instanceof Error ? error.message : "Notification delivery failed";
100
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : "provider_error";
101
+ const retryable = !(error && typeof error === "object" && "retryable" in error && error.retryable === false);
102
+ if (retryable && delivery.attempt_count < MAX_DELIVERY_ATTEMPTS) {
103
+ const retryAfterMs = retryDelay(delivery.attempt_count);
104
+ await sql`
105
+ UPDATE notifications.deliveries
106
+ SET status = 'pending', next_attempt_at = now() + (${retryAfterMs}::int * INTERVAL '1 millisecond'),
107
+ error_code = ${code}, error_message = ${message}, updated_at = now()
108
+ WHERE id = ${delivery.id}::uuid
109
+ `;
110
+ return { status: "retry", retryAfterMs, error: message };
111
+ }
112
+
113
+ await sql`
114
+ UPDATE notifications.deliveries
115
+ SET status = 'failed', next_attempt_at = NULL, error_code = ${code},
116
+ error_message = ${message}, payload_encrypted = NULL, updated_at = now()
117
+ WHERE id = ${delivery.id}::uuid
118
+ `;
119
+ const activatedIds = delivery.required ? [] : await activateNextFallback(delivery.event_id);
120
+ return { status: "failed", activatedIds };
121
+ }
122
+ };
123
+
124
+ export const recoverNotificationDeliveries = async (): Promise<string[]> => {
125
+ await sql`
126
+ UPDATE notifications.deliveries
127
+ SET status = 'pending', next_attempt_at = now(), error_code = 'lease_recovered',
128
+ error_message = 'Recovered an interrupted delivery attempt.', updated_at = now()
129
+ WHERE status = 'sending' AND last_attempt_at < now() - INTERVAL '5 minutes'
130
+ `;
131
+ const rows = await sql<{ id: string }[]>`
132
+ SELECT id
133
+ FROM notifications.deliveries
134
+ WHERE status = 'pending' AND (next_attempt_at IS NULL OR next_attempt_at <= now())
135
+ ORDER BY next_attempt_at NULLS FIRST, created_at
136
+ LIMIT 500
137
+ `;
138
+ return rows.map((row) => row.id);
139
+ };