@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,248 @@
1
+ import { sql } from "bun";
2
+ import {
3
+ isSafeNotificationTargetHref,
4
+ type NotificationDefinitionPresentationCatalog,
5
+ resolveNotificationDefinitionPresentation,
6
+ } from "../../contracts/notification-types";
7
+ import type { MutationResult } from "../../contracts/shared";
8
+ import type {
9
+ NotificationDeliveryStatus,
10
+ UserNotificationHistoryResponse,
11
+ UserNotificationPreference,
12
+ UserNotificationPreferencesResponse,
13
+ } from "../../contracts/user-notifications";
14
+ import { normalizeLocale } from "../../shared/locale";
15
+ import { toPgTextArray } from "../postgres";
16
+ import { listNotificationChannels } from "./channels";
17
+
18
+ type PreferenceRow = {
19
+ id: string;
20
+ app_id: string;
21
+ kind: string;
22
+ label: string;
23
+ description: string;
24
+ presentation: NotificationDefinitionPresentationCatalog | null;
25
+ recommended_channels: string[];
26
+ required_channels: string[];
27
+ channels: string[] | null;
28
+ customized: boolean;
29
+ };
30
+
31
+ type HistoryRow = {
32
+ id: string;
33
+ event_id: string;
34
+ definition_id: string;
35
+ app_id: string;
36
+ label: string;
37
+ description: string;
38
+ presentation: NotificationDefinitionPresentationCatalog | null;
39
+ title: string;
40
+ target_href: string | null;
41
+ channel: string;
42
+ destination_label: string;
43
+ required: boolean;
44
+ status: NotificationDeliveryStatus;
45
+ attempt_count: number;
46
+ error_code: string | null;
47
+ created_at: Date;
48
+ delivered_at: Date | null;
49
+ };
50
+
51
+ const unique = (values: readonly string[]): string[] => [...new Set(values)];
52
+
53
+ const definitionPresentation = (
54
+ row: Pick<PreferenceRow, "label" | "description" | "presentation">,
55
+ locale?: string,
56
+ ): { label: string; description: string } => resolveNotificationDefinitionPresentation(row, locale);
57
+
58
+ const mapPreference = (row: PreferenceRow, locale?: string): UserNotificationPreference => {
59
+ const requiredChannels = unique(row.required_channels);
60
+ const required = new Set(requiredChannels);
61
+ const selectedChannels = unique(row.customized ? (row.channels ?? []) : row.recommended_channels).filter(
62
+ (channel) => !required.has(channel),
63
+ );
64
+ const presentation = definitionPresentation(row, locale);
65
+ return {
66
+ id: row.id,
67
+ appId: row.app_id,
68
+ kind: row.kind,
69
+ label: presentation.label,
70
+ description: presentation.description,
71
+ recommendedChannels: unique(row.recommended_channels),
72
+ requiredChannels,
73
+ selectedChannels,
74
+ effectiveChannels: unique([...requiredChannels, ...selectedChannels]),
75
+ customized: row.customized,
76
+ };
77
+ };
78
+
79
+ const listPreferences = async (userId: string, locale?: string): Promise<UserNotificationPreferencesResponse> => {
80
+ const rows = await sql<PreferenceRow[]>`
81
+ SELECT d.id, d.app_id, d.kind, d.label, d.description, d.presentation,
82
+ d.recommended_channels, d.required_channels, p.channels,
83
+ (p.user_id IS NOT NULL) AS customized
84
+ FROM notifications.definitions d
85
+ LEFT JOIN notifications.preferences p
86
+ ON p.definition_id = d.id AND p.user_id = ${userId}::uuid
87
+ WHERE d.active = true AND d.recipient_kind = 'user'
88
+ ORDER BY d.app_id, d.id
89
+ `;
90
+ const definitions = rows
91
+ .map((row) => mapPreference(row, locale))
92
+ .sort(
93
+ (left, right) =>
94
+ left.appId.localeCompare(right.appId) ||
95
+ left.label.localeCompare(right.label, normalizeLocale(locale)) ||
96
+ left.id.localeCompare(right.id),
97
+ );
98
+ return { availableChannels: listNotificationChannels(), definitions };
99
+ };
100
+
101
+ const findPreference = async (userId: string, definitionId: string, locale?: string): Promise<UserNotificationPreference | null> => {
102
+ const result = await listPreferences(userId, locale);
103
+ return result.definitions.find((definition) => definition.id === definitionId) ?? null;
104
+ };
105
+
106
+ const setPreference = async (config: {
107
+ userId: string;
108
+ definitionId: string;
109
+ channels: string[];
110
+ locale?: string;
111
+ }): Promise<MutationResult<UserNotificationPreference>> => {
112
+ const rows = await sql<{ required_channels: string[] }[]>`
113
+ SELECT required_channels
114
+ FROM notifications.definitions
115
+ WHERE id = ${config.definitionId} AND active = true AND recipient_kind = 'user'
116
+ LIMIT 1
117
+ `;
118
+ const definition = rows[0];
119
+ if (!definition) return { ok: false, error: "Notification preference not found", status: 404 };
120
+
121
+ const required = new Set(definition.required_channels);
122
+ const channels = unique(config.channels.map((channel) => channel.trim()).filter(Boolean)).filter((channel) => !required.has(channel));
123
+ const available = new Set(listNotificationChannels());
124
+ const unavailable = channels.filter((channel) => !available.has(channel));
125
+ if (unavailable.length > 0) {
126
+ return { ok: false, error: `Unavailable notification channel: ${unavailable.join(", ")}`, status: 400 };
127
+ }
128
+
129
+ await sql`
130
+ INSERT INTO notifications.preferences (user_id, definition_id, channels, updated_at)
131
+ VALUES (${config.userId}::uuid, ${config.definitionId}, ${toPgTextArray(channels)}::text[], now())
132
+ ON CONFLICT (user_id, definition_id) DO UPDATE
133
+ SET channels = EXCLUDED.channels, updated_at = now()
134
+ `;
135
+ const preference = await findPreference(config.userId, config.definitionId, config.locale);
136
+ if (!preference) return { ok: false, error: "Notification preference not found", status: 404 };
137
+ return { ok: true, data: preference };
138
+ };
139
+
140
+ const resetPreference = async (config: {
141
+ userId: string;
142
+ definitionId: string;
143
+ locale?: string;
144
+ }): Promise<MutationResult<UserNotificationPreference>> => {
145
+ const existing = await findPreference(config.userId, config.definitionId, config.locale);
146
+ if (!existing) return { ok: false, error: "Notification preference not found", status: 404 };
147
+ await sql`
148
+ DELETE FROM notifications.preferences
149
+ WHERE user_id = ${config.userId}::uuid AND definition_id = ${config.definitionId}
150
+ `;
151
+ const preference = await findPreference(config.userId, config.definitionId, config.locale);
152
+ if (!preference) return { ok: false, error: "Notification preference not found", status: 404 };
153
+ return { ok: true, data: preference };
154
+ };
155
+
156
+ const publicDeliveryError = (code: string | null, status: NotificationDeliveryStatus): { code: string | null; message: string | null } => {
157
+ if (!code) return { code: null, message: null };
158
+ switch (code) {
159
+ case "disabled_by_user":
160
+ return { code, message: "Delivery is disabled in your notification preferences." };
161
+ case "no_preferred_channel":
162
+ return { code, message: "No preferred delivery channel is configured." };
163
+ case "channel_unavailable":
164
+ return { code, message: "This delivery channel is currently unavailable." };
165
+ case "no_endpoint":
166
+ return { code, message: "No registered destination is available for this channel." };
167
+ case "fallback_not_needed":
168
+ return { code, message: "Another preferred channel delivered this notification." };
169
+ case "lease_recovered":
170
+ return { code, message: "Delivery resumed after an interrupted attempt." };
171
+ default:
172
+ return {
173
+ code: "provider_error",
174
+ message:
175
+ status === "pending" || status === "sending"
176
+ ? "The provider reported an error; delivery will retry."
177
+ : "The provider could not deliver this notification.",
178
+ };
179
+ }
180
+ };
181
+
182
+ const listHistory = async (config: {
183
+ userId: string;
184
+ page: number;
185
+ perPage: number;
186
+ status?: NotificationDeliveryStatus;
187
+ locale?: string;
188
+ }): Promise<UserNotificationHistoryResponse> => {
189
+ const page = Math.max(1, config.page);
190
+ const perPage = Math.min(100, Math.max(1, config.perPage));
191
+ const offset = (page - 1) * perPage;
192
+ const status = config.status ?? null;
193
+ const [countRows, rows] = await Promise.all([
194
+ sql<{ count: number }[]>`
195
+ SELECT COUNT(*)::int AS count
196
+ FROM notifications.deliveries d
197
+ JOIN notifications.events e ON e.id = d.event_id
198
+ WHERE e.recipient_user_id = ${config.userId}::uuid
199
+ AND (${status}::text IS NULL OR d.status = ${status})
200
+ `,
201
+ sql<HistoryRow[]>`
202
+ SELECT d.id, d.event_id, e.definition_id, n.app_id, n.label, n.description, n.presentation, e.title, e.target_href,
203
+ d.channel, d.destination_label, d.required, d.status, d.attempt_count,
204
+ d.error_code, d.created_at, d.delivered_at
205
+ FROM notifications.deliveries d
206
+ JOIN notifications.events e ON e.id = d.event_id
207
+ JOIN notifications.definitions n ON n.id = e.definition_id
208
+ WHERE e.recipient_user_id = ${config.userId}::uuid
209
+ AND (${status}::text IS NULL OR d.status = ${status})
210
+ ORDER BY e.created_at DESC, d.required DESC, d.route_priority NULLS FIRST, d.created_at, d.id
211
+ LIMIT ${perPage} OFFSET ${offset}
212
+ `,
213
+ ]);
214
+ const total = Number(countRows[0]?.count ?? 0);
215
+ return {
216
+ items: rows.map((row) => {
217
+ const error = publicDeliveryError(row.error_code, row.status);
218
+ const presentation = definitionPresentation(row, config.locale);
219
+ return {
220
+ id: row.id,
221
+ eventId: row.event_id,
222
+ definitionId: row.definition_id,
223
+ appId: row.app_id,
224
+ label: presentation.label,
225
+ title: row.title,
226
+ targetHref: row.target_href && isSafeNotificationTargetHref(row.target_href) ? row.target_href : null,
227
+ channel: row.channel,
228
+ destinationLabel: row.destination_label,
229
+ required: row.required,
230
+ status: row.status,
231
+ attemptCount: row.attempt_count,
232
+ errorCode: error.code,
233
+ errorMessage: error.message,
234
+ createdAt: row.created_at.toISOString(),
235
+ deliveredAt: row.delivered_at?.toISOString() ?? null,
236
+ };
237
+ }),
238
+ total,
239
+ page,
240
+ perPage,
241
+ totalPages: total === 0 ? 0 : Math.ceil(total / perPage),
242
+ };
243
+ };
244
+
245
+ export const userNotifications = {
246
+ preferences: { list: listPreferences, set: setPreference, reset: resetPreference },
247
+ history: { list: listHistory },
248
+ } as const;
@@ -0,0 +1,135 @@
1
+ import type { ClientRequest, IncomingMessage } from "node:http";
2
+ import { type RequestOptions as HttpsRequestOptions, request as httpsRequest } from "node:https";
3
+ import webpush, { type RequestDetails as WebPushRequestDetails, type RequestOptions as WebPushRequestOptions } from "web-push";
4
+ import { type BrowserPushSubscription, BrowserPushSubscriptionSchema } from "../../contracts/user-notifications";
5
+ import { normalizeNetworkHostname } from "../../shared/network-address";
6
+ import { type PublicNetworkAddress, resolvePublicNetworkAddresses } from "../network-security";
7
+
8
+ const RESPONSE_LIMIT_BYTES = 64 * 1_024;
9
+ const REQUEST_TIMEOUT_MS = 15_000;
10
+ const MAX_ADDRESS_ATTEMPTS = 4;
11
+
12
+ type RequestFactory = (options: HttpsRequestOptions, callback: (response: IncomingMessage) => void) => ClientRequest;
13
+ type RequestDetailsFactory = (
14
+ subscription: BrowserPushSubscription,
15
+ payload: string,
16
+ options: WebPushRequestOptions,
17
+ ) => WebPushRequestDetails;
18
+
19
+ type WebPushTransportDeps = {
20
+ generateRequestDetails?: RequestDetailsFactory;
21
+ request?: RequestFactory;
22
+ resolve?: (hostname: string) => Promise<PublicNetworkAddress[]>;
23
+ };
24
+
25
+ export const buildPinnedWebPushRequests = async (
26
+ subscription: BrowserPushSubscription,
27
+ payload: string,
28
+ options: WebPushRequestOptions,
29
+ deps: WebPushTransportDeps = {},
30
+ ): Promise<{ body: Buffer | null; endpoint: string; attempts: HttpsRequestOptions[] }> => {
31
+ const validatedSubscription = BrowserPushSubscriptionSchema.parse(subscription);
32
+ const endpoint = new URL(validatedSubscription.endpoint);
33
+ const hostname = normalizeNetworkHostname(endpoint.hostname);
34
+ const resolvedAddresses = await (deps.resolve ?? resolvePublicNetworkAddresses)(hostname);
35
+ const uniqueAddresses = resolvedAddresses.filter(
36
+ (target, index) => resolvedAddresses.findIndex((candidate) => candidate.address === target.address) === index,
37
+ );
38
+ const firstFamily = uniqueAddresses[0]?.family ?? 4;
39
+ const primary = uniqueAddresses.filter((target) => target.family === firstFamily);
40
+ const secondary = uniqueAddresses.filter((target) => target.family !== firstFamily);
41
+ const addresses: PublicNetworkAddress[] = [];
42
+ for (let index = 0; addresses.length < MAX_ADDRESS_ATTEMPTS && (index < primary.length || index < secondary.length); index++) {
43
+ const primaryTarget = primary[index];
44
+ const secondaryTarget = secondary[index];
45
+ if (primaryTarget) addresses.push(primaryTarget);
46
+ if (secondaryTarget && addresses.length < MAX_ADDRESS_ATTEMPTS) addresses.push(secondaryTarget);
47
+ }
48
+ if (addresses.length === 0) throw new Error("Push endpoint has no public network address");
49
+
50
+ const details = (deps.generateRequestDetails ?? webpush.generateRequestDetails)(validatedSubscription, payload, options);
51
+ if (details.endpoint !== validatedSubscription.endpoint) throw new Error("Web Push request endpoint changed during preparation");
52
+ return {
53
+ body: details.body,
54
+ endpoint: validatedSubscription.endpoint,
55
+ attempts: addresses.map((target) => ({
56
+ protocol: "https:",
57
+ hostname: target.address,
58
+ port: endpoint.port ? Number(endpoint.port) : 443,
59
+ path: `${endpoint.pathname}${endpoint.search}`,
60
+ method: details.method,
61
+ headers: { ...details.headers, host: endpoint.host },
62
+ servername: hostname,
63
+ rejectUnauthorized: true,
64
+ timeout: REQUEST_TIMEOUT_MS,
65
+ })),
66
+ };
67
+ };
68
+
69
+ const sendPreparedWebPushRequest = async (request: RequestFactory, options: HttpsRequestOptions, body: Buffer | null): Promise<void> =>
70
+ new Promise<void>((resolve, reject) => {
71
+ let settled = false;
72
+ let responseReceived = false;
73
+ const finish = (result: { error?: Error }) => {
74
+ if (settled) return;
75
+ settled = true;
76
+ if (result.error) {
77
+ reject(responseReceived ? Object.assign(result.error, { providerResponse: true }) : result.error);
78
+ } else {
79
+ resolve();
80
+ }
81
+ };
82
+ const outgoing = request(options, (response) => {
83
+ responseReceived = true;
84
+ let responseBody = "";
85
+ let bytes = 0;
86
+ response.on("data", (chunk: Buffer | string) => {
87
+ bytes += Buffer.byteLength(chunk);
88
+ if (bytes > RESPONSE_LIMIT_BYTES) {
89
+ response.destroy();
90
+ finish({ error: new Error("Web Push provider response is too large") });
91
+ return;
92
+ }
93
+ responseBody += chunk.toString();
94
+ });
95
+ response.on("end", () => {
96
+ const statusCode = response.statusCode ?? 0;
97
+ if (statusCode >= 200 && statusCode <= 299) {
98
+ finish({});
99
+ return;
100
+ }
101
+ finish({
102
+ error: Object.assign(new Error("Web Push provider returned an unexpected response"), {
103
+ statusCode,
104
+ responseBody,
105
+ }),
106
+ });
107
+ });
108
+ response.on("error", (error) => finish({ error }));
109
+ });
110
+ outgoing.on("timeout", () => outgoing.destroy(new Error("Web Push provider request timed out")));
111
+ outgoing.on("error", (error) => finish({ error }));
112
+ if (body) outgoing.write(body);
113
+ outgoing.end();
114
+ });
115
+
116
+ export const sendPinnedWebPush = async (
117
+ subscription: BrowserPushSubscription,
118
+ payload: string,
119
+ options: WebPushRequestOptions,
120
+ deps: WebPushTransportDeps = {},
121
+ ): Promise<void> => {
122
+ const request = deps.request ?? httpsRequest;
123
+ const prepared = await buildPinnedWebPushRequests(subscription, payload, options, deps);
124
+ let lastError: unknown;
125
+ for (const attempt of prepared.attempts) {
126
+ try {
127
+ await sendPreparedWebPushRequest(request, attempt, prepared.body);
128
+ return;
129
+ } catch (error) {
130
+ if (error && typeof error === "object" && "providerResponse" in error) throw error;
131
+ lastError = error;
132
+ }
133
+ }
134
+ throw lastError instanceof Error ? lastError : new Error("Web Push provider request failed");
135
+ };
@@ -0,0 +1,209 @@
1
+ import { sql } from "bun";
2
+ import { createRemoteJWKSet, decodeProtectedHeader, errors, type JWTPayload, type JWTVerifyGetKey, jwtVerify } from "jose";
3
+ import type { User } from "../contracts/shared";
4
+ import { isAccountCategoryAllowed } from "./account-category-policy";
5
+ import { isAccountExpired } from "./account-model";
6
+ import { IDENTITY_JWKS_MAX_AGE_SECONDS } from "./identity/constants";
7
+ import { getIdentityRuntimeConfig } from "./identity/runtime-config";
8
+ import type { ServiceAccount } from "./service-accounts";
9
+ import { buildProjectedUser, userProjectionSql } from "./session/user";
10
+
11
+ type ServiceAccountActorRow = Record<string, unknown> & {
12
+ service_account_id: string;
13
+ service_account_name: string;
14
+ service_account_kind: ServiceAccount["kind"];
15
+ service_account_status: ServiceAccount["status"];
16
+ delegated_user_id: string | null;
17
+ app_id: string | null;
18
+ resource_type: string | null;
19
+ resource_id: string | null;
20
+ service_account_created_by: string | null;
21
+ service_account_created_at: Date | string;
22
+ };
23
+
24
+ type RemoteSetState = { key: JWTVerifyGetKey; lastForcedRefreshAt: number };
25
+ const remoteSets = new Map<string, RemoteSetState>();
26
+
27
+ const createRemoteSet = (url: URL): JWTVerifyGetKey =>
28
+ createRemoteJWKSet(url, {
29
+ cacheMaxAge: IDENTITY_JWKS_MAX_AGE_SECONDS * 1_000,
30
+ cooldownDuration: 30_000,
31
+ timeoutDuration: 5_000,
32
+ });
33
+
34
+ const remoteSet = (url: URL): RemoteSetState => {
35
+ const existing = remoteSets.get(url.href);
36
+ if (existing) return existing;
37
+ const created = { key: createRemoteSet(url), lastForcedRefreshAt: 0 };
38
+ remoteSets.set(url.href, created);
39
+ return created;
40
+ };
41
+
42
+ const parseScopeClaim = (payload: JWTPayload): string[] => {
43
+ const value = payload.scope;
44
+ if (typeof value !== "string") return [];
45
+ return value
46
+ .split(/\s+/)
47
+ .map((scope) => scope.trim())
48
+ .filter(Boolean);
49
+ };
50
+
51
+ export type AuthenticatedOAuthToken =
52
+ | {
53
+ kind: "user";
54
+ payload: JWTPayload;
55
+ user: User;
56
+ scopes: string[];
57
+ }
58
+ | {
59
+ kind: "service_account";
60
+ payload: JWTPayload;
61
+ serviceAccount: ServiceAccount;
62
+ delegatedUser: User | null;
63
+ scopes: string[];
64
+ };
65
+
66
+ const getStringClaim = (payload: JWTPayload, key: string): string | null => {
67
+ const value = payload[key];
68
+ return typeof value === "string" && value.length > 0 ? value : null;
69
+ };
70
+
71
+ const uuidClaim = (payload: JWTPayload, key: string): string | null => {
72
+ const value = getStringClaim(payload, key);
73
+ return value && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value) ? value : null;
74
+ };
75
+
76
+ const mapServiceAccount = (row: ServiceAccountActorRow): ServiceAccount => ({
77
+ id: row.service_account_id,
78
+ name: row.service_account_name,
79
+ kind: row.service_account_kind,
80
+ status: row.service_account_status,
81
+ delegatedUserId: row.delegated_user_id,
82
+ appId: row.app_id,
83
+ resourceType: row.resource_type,
84
+ resourceId: row.resource_id,
85
+ createdBy: row.service_account_created_by,
86
+ createdAt: new Date(row.service_account_created_at).toISOString(),
87
+ });
88
+
89
+ /** Resolve the live OAuth client/principal, then check current account-category policy. */
90
+ export const resolveOAuthTokenActor = async (
91
+ payload: JWTPayload,
92
+ groupsAdmin: string[],
93
+ query: typeof sql = sql,
94
+ ): Promise<AuthenticatedOAuthToken | null> => {
95
+ const clientId = getStringClaim(payload, "client_id");
96
+ if (!clientId) return null;
97
+ const scopes = parseScopeClaim(payload);
98
+ const rawServiceAccountId = getStringClaim(payload, "service_account_id");
99
+
100
+ if (rawServiceAccountId) {
101
+ const serviceAccountId = uuidClaim(payload, "service_account_id");
102
+ if (!serviceAccountId) return null;
103
+ const rows = await query<ServiceAccountActorRow[]>`
104
+ SELECT
105
+ sa.id AS service_account_id,
106
+ sa.name AS service_account_name,
107
+ sa.kind AS service_account_kind,
108
+ sa.status AS service_account_status,
109
+ sa.delegated_user_id,
110
+ sa.app_id,
111
+ sa.resource_type,
112
+ sa.resource_id,
113
+ sa.created_by AS service_account_created_by,
114
+ sa.created_at AS service_account_created_at,
115
+ ${userProjectionSql(groupsAdmin)}
116
+ FROM oauth.clients c
117
+ JOIN auth.service_accounts sa ON sa.id = ${serviceAccountId}::uuid AND sa.status = 'active'
118
+ LEFT JOIN auth.users u ON u.id = sa.delegated_user_id
119
+ LEFT JOIN auth.user_ipa_data ui ON ui.user_id = u.id
120
+ WHERE c.client_id = ${clientId}
121
+ `;
122
+ const row = rows[0];
123
+ if (!row) return null;
124
+ const serviceAccount = mapServiceAccount(row);
125
+ const delegatedUser = serviceAccount.delegatedUserId ? buildProjectedUser(row) : null;
126
+ if (serviceAccount.kind === "user_delegated" && !delegatedUser) return null;
127
+ if (delegatedUser && isAccountExpired(delegatedUser.accountExpires)) return null;
128
+ if (delegatedUser && !(await isAccountCategoryAllowed(delegatedUser, query))) return null;
129
+ return { kind: "service_account", payload, serviceAccount, delegatedUser, scopes };
130
+ }
131
+
132
+ const rawUserId = getStringClaim(payload, "id");
133
+ const userId = rawUserId ? uuidClaim(payload, "id") : null;
134
+ if (rawUserId && !userId) return null;
135
+ const uid = getStringClaim(payload, "uid") ?? (typeof payload.sub === "string" ? payload.sub : null);
136
+ if (!userId && !uid) return null;
137
+ const predicate = userId ? sql`u.id = ${userId}::uuid` : sql`u.uid = ${uid}`;
138
+ const rows = await query<Record<string, unknown>[]>`
139
+ SELECT ${userProjectionSql(groupsAdmin)}
140
+ FROM oauth.clients c
141
+ JOIN auth.users u ON ${predicate}
142
+ LEFT JOIN auth.user_ipa_data ui ON ui.user_id = u.id
143
+ WHERE c.client_id = ${clientId}
144
+ `;
145
+ const user = rows[0] ? buildProjectedUser(rows[0]) : null;
146
+ if (!user || isAccountExpired(user.accountExpires)) return null;
147
+ if (!(await isAccountCategoryAllowed(user, query))) return null;
148
+ return { kind: "user", payload, user, scopes };
149
+ };
150
+
151
+ export const verifyAccessToken = async (
152
+ token: string,
153
+ expectedAudience: string | string[] = "cloud",
154
+ options: {
155
+ issuer?: string;
156
+ key?: JWTVerifyGetKey;
157
+ jwksUrl?: URL;
158
+ groupsAdmin?: string[];
159
+ query?: typeof sql;
160
+ } = {},
161
+ ): Promise<AuthenticatedOAuthToken | null> => {
162
+ let remote: { url: URL; state: RemoteSetState } | null = null;
163
+ try {
164
+ const header = decodeProtectedHeader(token);
165
+ if (header.alg !== "RS256" || typeof header.kid !== "string" || header.kid.length === 0 || header.kid.length > 200) return null;
166
+
167
+ const completeOverride = Boolean(options.issuer && (options.key || options.jwksUrl) && options.groupsAdmin);
168
+ const runtime = completeOverride ? null : await getIdentityRuntimeConfig();
169
+ const issuer = options.issuer ?? runtime!.issuer;
170
+ const jwksUrl = options.jwksUrl ?? runtime?.oauthJwksUrl;
171
+ const groupsAdmin = options.groupsAdmin ?? runtime!.groupsAdmin;
172
+ const state = options.key || !jwksUrl ? null : remoteSet(jwksUrl);
173
+ if (state && jwksUrl) remote = { url: jwksUrl, state };
174
+ let key = options.key ?? state?.key;
175
+ if (!key) return null;
176
+
177
+ const verify = (candidate: JWTVerifyGetKey) =>
178
+ jwtVerify(token, candidate, {
179
+ algorithms: ["RS256"],
180
+ issuer,
181
+ audience: expectedAudience,
182
+ });
183
+ let verified: Awaited<ReturnType<typeof verify>>;
184
+ try {
185
+ verified = await verify(key);
186
+ } catch (error) {
187
+ if (!(error instanceof errors.JWKSNoMatchingKey) || !remote) throw error;
188
+ const now = Date.now();
189
+ if (now - remote.state.lastForcedRefreshAt < 30_000) throw error;
190
+ remote.state.lastForcedRefreshAt = now;
191
+ key = createRemoteSet(remote.url);
192
+ remote.state.key = key;
193
+ verified = await verify(key);
194
+ }
195
+
196
+ if (verified.payload.token_use !== "access") return null;
197
+ return resolveOAuthTokenActor(verified.payload, groupsAdmin, options.query);
198
+ } catch {
199
+ return null;
200
+ }
201
+ };
202
+
203
+ export const clearOAuthVerifierCachesForTest = (): void => {
204
+ remoteSets.clear();
205
+ };
206
+
207
+ export const oauthTokens = {
208
+ verifyAccessToken,
209
+ };