@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,421 @@
1
+ import { sql } from "bun";
2
+ import type { output, ZodType } from "zod";
3
+ import type {
4
+ BoundNotificationDefinition,
5
+ EmailNotificationPresentation,
6
+ NotificationChannelId,
7
+ NotificationPresentation,
8
+ NotificationRecipientKind,
9
+ NotificationSendInput,
10
+ } from "../../contracts/notification-types";
11
+ import { validateNotificationTargetHref } from "../../contracts/notification-types";
12
+ import { normalizeLocale } from "../../shared/locale";
13
+ import { logger } from "../logging";
14
+ import { encryptSecret } from "../secrets";
15
+ import { ensureNotificationDefinition } from "./catalog";
16
+ import { getNotificationChannel, type NotificationDestination, type ResolvedNotificationRecipient } from "./channels";
17
+ import { processNotificationDelivery } from "./dispatcher";
18
+ import { notificationLive } from "./live";
19
+ import { enqueueNotificationDeliveries, enqueueNotificationDelivery } from "./runtime";
20
+
21
+ export type TypedNotificationDeliveryStatus = "deferred" | "pending" | "sending" | "delivered" | "suppressed" | "failed";
22
+
23
+ export type TypedNotificationSendResult = {
24
+ id: string;
25
+ created: boolean;
26
+ status: "queued" | "delivered" | "suppressed" | "error";
27
+ deliveries: Array<{
28
+ id: string;
29
+ channel: string;
30
+ required: boolean;
31
+ status: TypedNotificationDeliveryStatus;
32
+ errorCode: string | null;
33
+ }>;
34
+ };
35
+
36
+ type PreparedDelivery = {
37
+ channel: string;
38
+ endpointId: string | null;
39
+ destinationKey: string;
40
+ destinationLabel: string;
41
+ payloadEncrypted: string | null;
42
+ required: boolean;
43
+ routePriority: number | null;
44
+ status: "deferred" | "pending" | "suppressed" | "failed";
45
+ errorCode: string | null;
46
+ errorMessage: string | null;
47
+ };
48
+
49
+ type DeliveryListRow = {
50
+ id: string;
51
+ channel: string;
52
+ required: boolean;
53
+ status: TypedNotificationDeliveryStatus;
54
+ error_code: string | null;
55
+ };
56
+
57
+ const unique = <T>(values: readonly T[]): T[] => [...new Set(values)];
58
+ const log = logger("notifications:platform");
59
+
60
+ const preparationFailure = (input: { channel: string; required: boolean; routePriority: number | null }): PreparedDelivery => ({
61
+ channel: input.channel,
62
+ endpointId: null,
63
+ destinationKey: "preparation-failed",
64
+ destinationLabel: input.channel,
65
+ payloadEncrypted: null,
66
+ required: input.required,
67
+ routePriority: input.routePriority,
68
+ status: "failed",
69
+ errorCode: "preparation_failed",
70
+ errorMessage: "Notification delivery could not be prepared.",
71
+ });
72
+
73
+ const validatePresentation = (presentation: NotificationPresentation): NotificationPresentation => {
74
+ const title = presentation.title.trim();
75
+ const body = presentation.body?.trim();
76
+ if (!title) throw new Error("Notification title is required");
77
+ if (title.length > 200) throw new Error("Notification title must not exceed 200 characters");
78
+ if (body && body.length > 4_000) throw new Error("Notification body must not exceed 4000 characters");
79
+ const targetHref = presentation.targetHref ? validateNotificationTargetHref(presentation.targetHref) : undefined;
80
+ return { title, ...(body ? { body } : {}), ...(targetHref ? { targetHref } : {}) };
81
+ };
82
+
83
+ const resolveRecipient = async (
84
+ recipient: { userId: string } | { email: string },
85
+ ): Promise<{
86
+ recipient: ResolvedNotificationRecipient;
87
+ recipientKey: string;
88
+ }> => {
89
+ if ("userId" in recipient) {
90
+ const rows = await sql<{ id: string; mail: string | null }[]>`
91
+ SELECT id, btrim(mail) AS mail FROM auth.users WHERE id = ${recipient.userId}::uuid LIMIT 1
92
+ `;
93
+ const user = rows[0];
94
+ if (!user) throw new Error("Notification recipient user was not found");
95
+ return {
96
+ recipient: { userId: user.id, email: user.mail?.trim().toLowerCase() || null },
97
+ recipientKey: `user:${user.id}`,
98
+ };
99
+ }
100
+
101
+ const email = recipient.email.trim().toLowerCase();
102
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) throw new Error("Notification recipient email is invalid");
103
+ return { recipient: { userId: null, email }, recipientKey: `email:${email}` };
104
+ };
105
+
106
+ const preferredChannels = async (
107
+ definitionId: string,
108
+ recipient: ResolvedNotificationRecipient,
109
+ recommended: readonly NotificationChannelId[],
110
+ ): Promise<{ channels: string[]; customized: boolean }> => {
111
+ if (!recipient.userId) return { channels: [], customized: false };
112
+ const rows = await sql<{ channels: string[] }[]>`
113
+ SELECT channels FROM notifications.preferences
114
+ WHERE user_id = ${recipient.userId}::uuid AND definition_id = ${definitionId}
115
+ LIMIT 1
116
+ `;
117
+ return rows[0] ? { channels: unique(rows[0].channels), customized: true } : { channels: unique(recommended), customized: false };
118
+ };
119
+
120
+ const prepareChannel = async (input: {
121
+ channel: string;
122
+ recipient: ResolvedNotificationRecipient;
123
+ presentation: NotificationPresentation;
124
+ emailPresentation?: () => Promise<EmailNotificationPresentation | undefined>;
125
+ required: boolean;
126
+ routePriority: number | null;
127
+ event: { id: string; definitionId: string };
128
+ }): Promise<PreparedDelivery[]> => {
129
+ const driver = getNotificationChannel(input.channel);
130
+ if (!driver) {
131
+ return [
132
+ {
133
+ channel: input.channel,
134
+ endpointId: null,
135
+ destinationKey: "channel-unavailable",
136
+ destinationLabel: input.channel,
137
+ payloadEncrypted: null,
138
+ required: input.required,
139
+ routePriority: input.routePriority,
140
+ status: "suppressed",
141
+ errorCode: "channel_unavailable",
142
+ errorMessage: `Notification channel "${input.channel}" is unavailable.`,
143
+ },
144
+ ];
145
+ }
146
+
147
+ let destinations: NotificationDestination[];
148
+ try {
149
+ destinations = await driver.resolveDestinations(input.recipient);
150
+ } catch (error) {
151
+ log.error("Notification destination resolution failed", {
152
+ channel: input.channel,
153
+ error: error instanceof Error ? error.message : "Notification destination resolution failed",
154
+ });
155
+ return [preparationFailure(input)];
156
+ }
157
+ if (destinations.length === 0) {
158
+ return [
159
+ {
160
+ channel: input.channel,
161
+ endpointId: null,
162
+ destinationKey: "no-endpoint",
163
+ destinationLabel: input.channel,
164
+ payloadEncrypted: null,
165
+ required: input.required,
166
+ routePriority: input.routePriority,
167
+ status: "suppressed",
168
+ errorCode: "no_endpoint",
169
+ errorMessage: `No configured ${input.channel} destination is available.`,
170
+ },
171
+ ];
172
+ }
173
+
174
+ try {
175
+ const emailPresentation = input.channel === "email" ? await input.emailPresentation?.() : undefined;
176
+ return await Promise.all(
177
+ destinations.map(async (destination) => ({
178
+ channel: input.channel,
179
+ endpointId: destination.endpointId ?? null,
180
+ destinationKey: destination.key,
181
+ destinationLabel: destination.label,
182
+ payloadEncrypted: await encryptSecret(
183
+ driver.createPayload({ presentation: input.presentation, email: emailPresentation, destination, event: input.event }),
184
+ ),
185
+ required: input.required,
186
+ routePriority: input.routePriority,
187
+ status: "deferred" as const,
188
+ errorCode: null,
189
+ errorMessage: null,
190
+ })),
191
+ );
192
+ } catch (error) {
193
+ log.error("Notification payload preparation failed", {
194
+ channel: input.channel,
195
+ error: error instanceof Error ? error.message : "Notification payload preparation failed",
196
+ });
197
+ return [preparationFailure(input)];
198
+ }
199
+ };
200
+
201
+ const listDeliveries = async (eventId: string): Promise<TypedNotificationSendResult["deliveries"]> => {
202
+ const rows = await sql<DeliveryListRow[]>`
203
+ SELECT id, channel, required, status, error_code
204
+ FROM notifications.deliveries
205
+ WHERE event_id = ${eventId}::uuid
206
+ ORDER BY required DESC, route_priority NULLS FIRST, created_at, id
207
+ `;
208
+ return rows.map((row) => ({
209
+ id: row.id,
210
+ channel: row.channel,
211
+ required: row.required,
212
+ status: row.status,
213
+ errorCode: row.error_code,
214
+ }));
215
+ };
216
+
217
+ const summarize = (id: string, created: boolean, deliveries: TypedNotificationSendResult["deliveries"]): TypedNotificationSendResult => {
218
+ const requiredFailures = deliveries.some(
219
+ (delivery) => delivery.required && (delivery.status === "failed" || delivery.status === "suppressed" || delivery.errorCode !== null),
220
+ );
221
+ const delivered = deliveries.some((delivery) => delivery.status === "delivered");
222
+ const queued = deliveries.some((delivery) => delivery.status === "pending" || delivery.status === "sending");
223
+ return {
224
+ id,
225
+ created,
226
+ status: requiredFailures ? "error" : delivered ? "delivered" : queued ? "queued" : "suppressed",
227
+ deliveries,
228
+ };
229
+ };
230
+
231
+ export const sendTypedNotification = async <
232
+ AppId extends string,
233
+ Key extends string,
234
+ R extends NotificationRecipientKind,
235
+ S extends ZodType,
236
+ >(
237
+ definition: BoundNotificationDefinition<AppId, Key, R, S>,
238
+ input: NotificationSendInput<BoundNotificationDefinition<AppId, Key, R, S>>,
239
+ ): Promise<TypedNotificationSendResult> => {
240
+ const idempotencyKey = input.idempotencyKey.trim();
241
+ if (!idempotencyKey || idempotencyKey.length > 300) throw new Error("Notification idempotencyKey must contain 1 to 300 characters");
242
+
243
+ const data: output<S> = definition.data.parse(input.data);
244
+ const renderContext = { locale: normalizeLocale(input.locale) };
245
+ const presentation = validatePresentation(await definition.render(data, renderContext));
246
+ const resolved = await resolveRecipient(input.recipient);
247
+ const candidateEventId = crypto.randomUUID();
248
+ await ensureNotificationDefinition(definition);
249
+
250
+ const requiredChannels = unique([...(definition.delivery?.required ?? [])]);
251
+ const requiredChannelSet = new Set<string>(requiredChannels);
252
+ const preference = await preferredChannels(definition.id, resolved.recipient, definition.delivery?.recommended ?? []);
253
+ const preferred = preference.channels.filter((channel) => !requiredChannelSet.has(channel));
254
+
255
+ const identity = await sql.begin(async (tx) => {
256
+ const eventRows = await tx<{ id: string }[]>`
257
+ INSERT INTO notifications.events (
258
+ id, definition_id, recipient_user_id, recipient_email, recipient_key,
259
+ idempotency_key, title, target_href, sent_by
260
+ ) VALUES (
261
+ ${candidateEventId}::uuid, ${definition.id}, ${resolved.recipient.userId}, ${resolved.recipient.userId ? null : resolved.recipient.email},
262
+ ${resolved.recipientKey}, ${idempotencyKey}, ${presentation.title}, ${presentation.targetHref ?? null}, ${input.sentBy ?? null}
263
+ )
264
+ ON CONFLICT (definition_id, recipient_key, idempotency_key) DO NOTHING
265
+ RETURNING id
266
+ `;
267
+ const createdId = eventRows[0]?.id;
268
+ if (createdId) return { id: createdId, created: true, needsPreparation: true };
269
+
270
+ const existing = await tx<{ id: string }[]>`
271
+ SELECT id FROM notifications.events
272
+ WHERE definition_id = ${definition.id} AND recipient_key = ${resolved.recipientKey}
273
+ AND idempotency_key = ${idempotencyKey}
274
+ FOR UPDATE
275
+ LIMIT 1
276
+ `;
277
+ const eventId = existing[0]?.id;
278
+ if (!eventId) throw new Error("Notification idempotency lookup failed");
279
+ const [state] = await tx<{ delivery_count: number; preparation_failure_count: number }[]>`
280
+ SELECT COUNT(*)::int AS delivery_count,
281
+ COUNT(*) FILTER (WHERE status = 'failed' AND error_code = 'preparation_failed')::int AS preparation_failure_count
282
+ FROM notifications.deliveries
283
+ WHERE event_id = ${eventId}::uuid
284
+ `;
285
+ return {
286
+ id: eventId,
287
+ created: false,
288
+ needsPreparation: Number(state?.delivery_count ?? 0) === 0 || Number(state?.preparation_failure_count ?? 0) > 0,
289
+ };
290
+ });
291
+
292
+ if (!identity.needsPreparation) {
293
+ return summarize(identity.id, identity.created, await listDeliveries(identity.id));
294
+ }
295
+
296
+ const event = { id: identity.id, definitionId: definition.id };
297
+ let emailPresentationPromise: Promise<EmailNotificationPresentation | undefined> | undefined;
298
+ const emailPresentation = (): Promise<EmailNotificationPresentation | undefined> => {
299
+ emailPresentationPromise ??= definition.email ? Promise.resolve(definition.email(data, renderContext)) : Promise.resolve(undefined);
300
+ return emailPresentationPromise;
301
+ };
302
+
303
+ const requiredPlans = (
304
+ await Promise.all(
305
+ requiredChannels.map((channel) =>
306
+ prepareChannel({
307
+ channel,
308
+ recipient: resolved.recipient,
309
+ presentation,
310
+ emailPresentation,
311
+ required: true,
312
+ routePriority: null,
313
+ event,
314
+ }),
315
+ ),
316
+ )
317
+ ).flat();
318
+ const preferredPlans = (
319
+ await Promise.all(
320
+ preferred.map((channel, routePriority) =>
321
+ prepareChannel({
322
+ channel,
323
+ recipient: resolved.recipient,
324
+ presentation,
325
+ emailPresentation,
326
+ required: false,
327
+ routePriority,
328
+ event,
329
+ }),
330
+ ),
331
+ )
332
+ ).flat();
333
+
334
+ if (requiredPlans.length === 0 && preferredPlans.length === 0) {
335
+ preferredPlans.push({
336
+ channel: "none",
337
+ endpointId: null,
338
+ destinationKey: "no-preferred-channel",
339
+ destinationLabel: "No delivery channel",
340
+ payloadEncrypted: null,
341
+ required: false,
342
+ routePriority: null,
343
+ status: "suppressed",
344
+ errorCode: preference.customized ? "disabled_by_user" : "no_preferred_channel",
345
+ errorMessage: preference.customized
346
+ ? "The user disabled delivery for this notification."
347
+ : "No preferred delivery channel is configured.",
348
+ });
349
+ }
350
+
351
+ const firstPreferredPriority = preferredPlans.find((delivery) => delivery.status === "deferred")?.routePriority;
352
+ const plans = [
353
+ ...requiredPlans.map((delivery) => ({ ...delivery, status: delivery.status === "deferred" ? ("pending" as const) : delivery.status })),
354
+ ...preferredPlans.map((delivery) => ({
355
+ ...delivery,
356
+ status: delivery.status === "deferred" && delivery.routePriority === firstPreferredPriority ? ("pending" as const) : delivery.status,
357
+ })),
358
+ ];
359
+
360
+ const persisted = await sql.begin(async (tx) => {
361
+ await tx`SELECT id FROM notifications.events WHERE id = ${identity.id}::uuid FOR UPDATE`;
362
+ const [state] = await tx<{ delivery_count: number; preparation_failure_count: number }[]>`
363
+ SELECT COUNT(*)::int AS delivery_count,
364
+ COUNT(*) FILTER (WHERE status = 'failed' AND error_code = 'preparation_failed')::int AS preparation_failure_count
365
+ FROM notifications.deliveries
366
+ WHERE event_id = ${identity.id}::uuid
367
+ `;
368
+ const shouldPrepare = Number(state?.delivery_count ?? 0) === 0 || Number(state?.preparation_failure_count ?? 0) > 0;
369
+ if (!shouldPrepare) {
370
+ return { prepared: false, pendingIds: [] as string[], requiredIds: [] as string[] };
371
+ }
372
+ await tx`
373
+ DELETE FROM notifications.deliveries
374
+ WHERE event_id = ${identity.id}::uuid AND status = 'failed' AND error_code = 'preparation_failed'
375
+ `;
376
+ await tx`
377
+ UPDATE notifications.events
378
+ SET title = ${presentation.title}, target_href = ${presentation.targetHref ?? null}
379
+ WHERE id = ${identity.id}::uuid
380
+ `;
381
+
382
+ const pendingIds: string[] = [];
383
+ const requiredIds: string[] = [];
384
+ for (const delivery of plans) {
385
+ const rows = await tx<{ id: string }[]>`
386
+ INSERT INTO notifications.deliveries (
387
+ event_id, channel, endpoint_id, destination_key, destination_label,
388
+ payload_encrypted, required, route_priority, status, next_attempt_at,
389
+ error_code, error_message
390
+ ) VALUES (
391
+ ${identity.id}::uuid, ${delivery.channel}, ${delivery.endpointId}::uuid, ${delivery.destinationKey},
392
+ ${delivery.destinationLabel}, ${delivery.payloadEncrypted}, ${delivery.required}, ${delivery.routePriority},
393
+ ${delivery.status}, CASE WHEN ${delivery.status} = 'pending' THEN now() ELSE NULL END,
394
+ ${delivery.errorCode}, ${delivery.errorMessage}
395
+ )
396
+ ON CONFLICT (event_id, channel, destination_key) DO NOTHING
397
+ RETURNING id
398
+ `;
399
+ const deliveryId = rows[0]?.id;
400
+ if (deliveryId && delivery.status === "pending") {
401
+ if (delivery.required) requiredIds.push(deliveryId);
402
+ else pendingIds.push(deliveryId);
403
+ }
404
+ }
405
+ return { prepared: true, pendingIds, requiredIds };
406
+ });
407
+
408
+ if (persisted.prepared) {
409
+ if (resolved.recipient.userId && (requiredChannelSet.has("browser") || preferred.includes("browser"))) {
410
+ await notificationLive.publish({ userId: resolved.recipient.userId, eventId: identity.id, presentation });
411
+ }
412
+ for (const deliveryId of persisted.requiredIds) {
413
+ const result = await processNotificationDelivery(deliveryId);
414
+ if (result.status === "retry") await enqueueNotificationDelivery(deliveryId, result.retryAfterMs);
415
+ if (result.activatedIds?.length) await enqueueNotificationDeliveries(result.activatedIds);
416
+ }
417
+ await enqueueNotificationDeliveries(persisted.pendingIds);
418
+ }
419
+
420
+ return summarize(identity.id, identity.created, await listDeliveries(identity.id));
421
+ };
@@ -0,0 +1,120 @@
1
+ import type { DeliveryConfig, JobContext, Worker } from "@k2b/sync";
2
+ import { lazySync } from "../../_internal/process-sync";
3
+ import { logger, trace } from "../logging";
4
+ import { createRuntimeLifecycle, createRuntimeTaskTracker, stopRuntimeJobs } from "../runtime-lifecycle";
5
+
6
+ import { processNotificationDelivery, recoverNotificationDeliveries } from "./dispatcher";
7
+
8
+ type DeliveryMessage = { deliveryId: string };
9
+
10
+ const log = logger("notifications:delivery");
11
+ const ENQUEUE_DEDUPLICATION_WINDOW_MS = 60_000;
12
+ // Postgres owns delivery retries: provider failures are persisted by the
13
+ // dispatcher, and the recovery scan re-enqueues pending rows. A handler throw
14
+ // therefore means the database itself was unreachable; a few spaced attempts
15
+ // cover a blip, and a longer outage dead-letters once while recovery restores
16
+ // the row after the outage anyway.
17
+ const HANDLER_RETRY = { maxAttempts: 3, backoffMs: [5_000, 30_000] } satisfies Partial<DeliveryConfig>;
18
+ const deliveryJob = lazySync((sync) => {
19
+ const handle = sync.job<DeliveryMessage>({
20
+ id: "cloud-notification-deliveries",
21
+ owner: "core",
22
+ delivery: { ackWaitMs: 60_000, ...HANDLER_RETRY },
23
+ dedupeWindowMs: ENQUEUE_DEDUPLICATION_WINDOW_MS * 2,
24
+ });
25
+
26
+ return handle;
27
+ });
28
+
29
+ export const enqueueNotificationDelivery = async (deliveryId: string, delayMs?: number): Promise<void> => {
30
+ const bucket = Math.floor(Date.now() / ENQUEUE_DEDUPLICATION_WINDOW_MS);
31
+ await deliveryJob().submit({
32
+ input: { deliveryId },
33
+ ...(delayMs ? { delayMs } : {}),
34
+ // Recovery scans are intentionally repetitive. A time bucket suppresses
35
+ // duplicate wakeups without permanently blocking a later recovery attempt.
36
+ key: `delivery:${deliveryId}:${bucket}`,
37
+ });
38
+ };
39
+
40
+ export const enqueueNotificationDeliveries = async (deliveryIds: readonly string[]): Promise<void> => {
41
+ const bucket = Math.floor(Date.now() / ENQUEUE_DEDUPLICATION_WINDOW_MS);
42
+ await deliveryJob().submitMany(
43
+ deliveryIds.map((deliveryId) => ({
44
+ key: `delivery:${deliveryId}:${bucket}`,
45
+ input: { deliveryId },
46
+ })),
47
+ );
48
+ };
49
+
50
+ const handleDelivery = async (context: JobContext<DeliveryMessage>): Promise<void> => {
51
+ const result = await trace.withSpan(
52
+ {
53
+ name: "Notification delivery",
54
+ source: "notifications:delivery",
55
+ appId: "core",
56
+ category: "job",
57
+ kind: "consumer",
58
+ spanKey: trace.syncSpanKey("job", "cloud-notification-deliveries", context.jobId),
59
+ attributes: { "cloud.notification.delivery_id": context.input.deliveryId },
60
+ },
61
+ () => processNotificationDelivery(context.input.deliveryId),
62
+ );
63
+ if (result.activatedIds?.length) await enqueueNotificationDeliveries(result.activatedIds);
64
+ if (result.status === "retry") context.resubmit({ delayMs: result.retryAfterMs });
65
+ };
66
+
67
+ type RuntimeOptions = { concurrency?: number; recoveryIntervalMs?: number };
68
+ let runtimeOptions: RuntimeOptions = {};
69
+ let worker: Worker | undefined;
70
+ let timer: ReturnType<typeof setInterval> | undefined;
71
+ let recovery: Promise<void> | undefined;
72
+ const recoveryTasks = createRuntimeTaskTracker();
73
+
74
+ const recover = (): Promise<void> => {
75
+ recovery ??= (
76
+ recoveryTasks.run(async () => {
77
+ try {
78
+ await enqueueNotificationDeliveries(await recoverNotificationDeliveries());
79
+ } catch (error) {
80
+ log.error("Delivery recovery failed", { error: error instanceof Error ? error.message : String(error) });
81
+ }
82
+ }) ?? Promise.resolve()
83
+ ).finally(() => {
84
+ recovery = undefined;
85
+ });
86
+ return recovery;
87
+ };
88
+
89
+ const lifecycle = createRuntimeLifecycle({
90
+ start: async () => {
91
+ recoveryTasks.open();
92
+ const concurrency = Math.min(Math.max(Math.floor(runtimeOptions.concurrency ?? 4), 1), 16);
93
+ const recoveryIntervalMs = Math.max(runtimeOptions.recoveryIntervalMs ?? 30_000, 5_000);
94
+ worker = await deliveryJob().process(
95
+ {
96
+ concurrency,
97
+ onError: ({ context, error }) => {
98
+ log.error("Delivery worker failed", { deliveryId: context.input.deliveryId, attempt: context.attempt, error: error.message });
99
+ return { action: "retry" };
100
+ },
101
+ },
102
+ handleDelivery,
103
+ );
104
+ await recover();
105
+ timer = setInterval(() => void recover(), recoveryIntervalMs);
106
+ if (typeof timer === "object" && "unref" in timer) timer.unref();
107
+ },
108
+ stop: async () => {
109
+ clearInterval(timer);
110
+ timer = undefined;
111
+ await stopRuntimeJobs(recoveryTasks, worker ? [worker] : []);
112
+ worker = undefined;
113
+ },
114
+ });
115
+
116
+ export const startNotificationRuntime = async (input: RuntimeOptions = {}): Promise<void> => {
117
+ runtimeOptions = input;
118
+ await lifecycle.start();
119
+ };
120
+ export const stopNotificationRuntime = lifecycle.stop;