@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,498 @@
1
+ import { createHash, randomBytes, randomInt, timingSafeEqual } from "node:crypto";
2
+ import { sql, type SQL } from "bun";
3
+ import {
4
+ APP_APPROVAL_LIMITS as limits,
5
+ APP_APPROVAL_PATH,
6
+ APP_APPROVAL_PROTOCOL,
7
+ AppDevicePublicKeySchema,
8
+ appDeviceProofMessage,
9
+ appPairingProofMessage,
10
+ type AppDevicePublicKey,
11
+ type AppDeviceRequest,
12
+ type AppDeviceView,
13
+ type AppPendingLogin,
14
+ } from "../contracts/app-approval";
15
+ import { accountCategory, type AccountCategory } from "../contracts/account-categories";
16
+ import { isAccountCategoryAllowed } from "./account-category-policy";
17
+ import { audit } from "./audit";
18
+ import { decryptValue } from "./settings/crypto";
19
+ import { publicCloudOrigin } from "../shared/app-url";
20
+ import { CORE_SETTINGS } from "./settings/core-settings";
21
+
22
+ export class AppApprovalError extends Error {
23
+ constructor(
24
+ public code: "UNAVAILABLE" | "FORBIDDEN" | "REAUTHENTICATE" | "INVALID_REQUEST" | "CONFLICT" | "LIMIT_REACHED",
25
+ public status: 400 | 403 | 404 | 409 | 429 | 503,
26
+ ) {
27
+ super(code);
28
+ }
29
+ }
30
+ const reject = (code: AppApprovalError["code"], status: AppApprovalError["status"]): never => {
31
+ throw new AppApprovalError(code, status);
32
+ };
33
+ const secret = () => randomBytes(32).toString("base64url");
34
+ const hash = (value: string) => createHash("sha256").update(value).digest("hex");
35
+ const matches = (value: string, expected: string) => timingSafeEqual(Buffer.from(hash(value), "hex"), Buffer.from(expected, "hex"));
36
+ const comparison = () => String(randomInt(0, 1_000_000)).padStart(6, "0");
37
+ const iso = (value: Date | string) => new Date(value).toISOString();
38
+ const future = (seconds: number) => new Date(Date.now() + seconds * 1000);
39
+
40
+ export type AppApprovalConfig = { issuer: string; appOrigin: string; enabled: boolean; adminPairing: boolean };
41
+ export const readAppApprovalConfig = async (db: SQL = sql, requireAppOrigin = true): Promise<AppApprovalConfig> => {
42
+ const rows = await db<
43
+ { key: string; value: string }[]
44
+ >`SELECT key, value FROM settings.entries WHERE key IN ('app.url','user.app_approval.enabled','user.app_approval.origin','user.app_approval.admin_pairing')`;
45
+ const values = new Map(await Promise.all(rows.map(async (row) => [row.key, await decryptValue(row.value)] as const)));
46
+ const enabled = values.get("user.app_approval.enabled") ?? false;
47
+ const adminPairing = values.get("user.app_approval.admin_pairing") ?? false;
48
+ if (typeof enabled !== "boolean" || typeof adminPairing !== "boolean") return reject("UNAVAILABLE", 503);
49
+ // The issuer is the operator's canonical URL, never the request Host header.
50
+ const rawIssuer = values.get("app.url") ?? process.env.APP_URL ?? "localhost:3000";
51
+ const rawOrigin = values.get("user.app_approval.origin") ?? CORE_SETTINGS["user.app_approval.origin"].default;
52
+ const origin = (value: unknown) => {
53
+ if (typeof value !== "string") return reject("UNAVAILABLE", 503);
54
+ try {
55
+ const url = new URL(value);
56
+ const development = process.env.NODE_ENV !== "production" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
57
+ if (
58
+ (url.protocol !== "https:" && !(url.protocol === "http:" && development)) ||
59
+ url.username ||
60
+ url.password ||
61
+ url.search ||
62
+ url.hash ||
63
+ url.pathname !== "/"
64
+ )
65
+ return reject("UNAVAILABLE", 503);
66
+ return url.origin;
67
+ } catch {
68
+ return reject("UNAVAILABLE", 503);
69
+ }
70
+ };
71
+ if (typeof rawIssuer !== "string") return reject("UNAVAILABLE", 503);
72
+ let issuer: string;
73
+ try {
74
+ issuer = origin(publicCloudOrigin(rawIssuer));
75
+ } catch {
76
+ return reject("UNAVAILABLE", 503);
77
+ }
78
+ if (!enabled) return { issuer, appOrigin: "", enabled: false, adminPairing };
79
+ let appOrigin = "";
80
+ try {
81
+ appOrigin = origin(rawOrigin);
82
+ } catch (error) {
83
+ // Missing setup must not prevent same-origin device inspection/revocation.
84
+ // Pairing, discovery and sign-in still require a valid trusted origin.
85
+ if (requireAppOrigin) throw error;
86
+ }
87
+ return { issuer, appOrigin, enabled, adminPairing };
88
+ };
89
+
90
+ type AccountRow = {
91
+ id: string;
92
+ uid: string;
93
+ provider: "local" | "ipa";
94
+ profile: "guest" | "user";
95
+ auth_epoch: string;
96
+ account_expires: Date | null;
97
+ };
98
+ type DeviceRow = {
99
+ id: string;
100
+ issuer: string;
101
+ user_id: string;
102
+ name: string;
103
+ public_key: unknown;
104
+ revoked_at: Date | null;
105
+ created_at: Date;
106
+ last_used_at: Date | null;
107
+ assisted: boolean;
108
+ };
109
+ type PairingRow = {
110
+ id: string;
111
+ user_id: string;
112
+ initiated_by: string;
113
+ initiator_sid: string;
114
+ assisted: boolean;
115
+ secret_hash: string;
116
+ state: string;
117
+ expires_at: Date;
118
+ public_key: unknown;
119
+ name: string | null;
120
+ device_id: string | null;
121
+ comparison: string | null;
122
+ };
123
+ type LoginRow = {
124
+ id: string;
125
+ user_id: string | null;
126
+ auth_epoch: string | null;
127
+ category: AccountCategory;
128
+ browser_hash: string;
129
+ state: string;
130
+ approved_by: string | null;
131
+ challenge: string;
132
+ comparison: string;
133
+ created_at: Date;
134
+ expires_at: Date;
135
+ };
136
+ export type AppApprovalActor = { userId: string; sid: string; admin: boolean };
137
+ export type AppDeviceEnrollmentNotice = { deviceId: string; userId: string; name: string; assisted: boolean };
138
+ const view = (row: DeviceRow): AppDeviceView => ({
139
+ id: row.id,
140
+ name: row.name,
141
+ createdAt: iso(row.created_at),
142
+ lastUsedAt: row.last_used_at ? iso(row.last_used_at) : null,
143
+ revokedAt: row.revoked_at ? iso(row.revoked_at) : null,
144
+ assisted: row.assisted,
145
+ });
146
+ const verify = async (key: AppDevicePublicKey, message: string, signature: string): Promise<boolean> => {
147
+ try {
148
+ const imported = await crypto.subtle.importKey("jwk", key, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
149
+ return crypto.subtle.verify(
150
+ { name: "ECDSA", hash: "SHA-256" },
151
+ imported,
152
+ Buffer.from(signature, "base64url"),
153
+ new TextEncoder().encode(message),
154
+ );
155
+ } catch {
156
+ return false;
157
+ }
158
+ };
159
+
160
+ /** One owner for production API and isolated HTTP consumers. Config is re-read
161
+ * per operation; SQL transactions serialize device decisions and revocation. */
162
+ export const createAppApprovalService = (
163
+ db: SQL = sql,
164
+ configuration = (requireEnabled = true) => readAppApprovalConfig(db, requireEnabled),
165
+ ) => {
166
+ const config = async (requireEnabled = true) => {
167
+ const value = await configuration(requireEnabled);
168
+ if (requireEnabled && (!value.enabled || !value.appOrigin)) return reject("UNAVAILABLE", 503);
169
+ return value;
170
+ };
171
+ const eligible = async (tx: SQL, id: string): Promise<AccountRow> => {
172
+ const [row] = await tx<
173
+ AccountRow[]
174
+ >`SELECT id, uid, provider, profile, auth_epoch, account_expires FROM auth.users WHERE id = ${id}::uuid`;
175
+ if (
176
+ !row ||
177
+ (row.account_expires && new Date(row.account_expires).getTime() <= Date.now()) ||
178
+ !(await isAccountCategoryAllowed(row, tx))
179
+ )
180
+ return reject("FORBIDDEN", 403);
181
+ return row;
182
+ };
183
+ const fresh = async (tx: SQL, actor: AppApprovalActor) => {
184
+ const [row] = await tx`SELECT sid FROM auth.session_families f JOIN auth.users u ON u.id = f.user_id
185
+ WHERE f.sid = ${actor.sid}::uuid AND f.user_id = ${actor.userId}::uuid AND f.revoked_at IS NULL
186
+ AND f.auth_epoch = u.auth_epoch AND f.expires_at > now()
187
+ AND f.issued_at > now() - ${limits.recentSessionSeconds} * interval '1 second'`;
188
+ if (!row) return reject("REAUTHENTICATE", 403);
189
+ await eligible(tx, actor.userId);
190
+ };
191
+ const record = (tx: SQL, action: string, userId: string, id: string, metadata: Record<string, unknown> = {}) =>
192
+ audit.record({ action: `auth.app.${action}`, outcome: "allowed", actor: { userId }, target: { type: "app_device", id }, metadata }, tx);
193
+ const cleanup = async () => {
194
+ // Bounded opportunistic maintenance: each accepted write retires up to 100
195
+ // expired rows per table, so bursts cannot trigger an unbounded sweep.
196
+ await db`DELETE FROM auth.app_pairings WHERE id IN (SELECT id FROM auth.app_pairings WHERE expires_at < now() ORDER BY expires_at LIMIT 100 FOR UPDATE SKIP LOCKED)`;
197
+ await db`DELETE FROM auth.app_logins WHERE id IN (SELECT id FROM auth.app_logins WHERE expires_at < now() ORDER BY expires_at LIMIT 100 FOR UPDATE SKIP LOCKED)`;
198
+ await db`DELETE FROM auth.app_device_proofs WHERE (device_id,jti) IN (SELECT device_id,jti FROM auth.app_device_proofs WHERE expires_at < now() ORDER BY expires_at LIMIT 100 FOR UPDATE SKIP LOCKED)`;
199
+ };
200
+ const authorizePairing = async (tx: SQL, actor: AppApprovalActor, pairing: PairingRow, cfg: AppApprovalConfig) => {
201
+ await fresh(tx, actor);
202
+ if (
203
+ pairing.initiated_by !== actor.userId ||
204
+ pairing.initiator_sid !== actor.sid ||
205
+ (pairing.assisted && (!actor.admin || !cfg.adminPairing))
206
+ )
207
+ return reject("FORBIDDEN", 403);
208
+ await eligible(tx, pairing.user_id);
209
+ };
210
+ const activeDevice = async (tx: SQL, id: string, issuer: string): Promise<DeviceRow> => {
211
+ const [device] = await tx<DeviceRow[]>`SELECT * FROM auth.app_devices WHERE id = ${id}::uuid AND issuer = ${issuer} FOR UPDATE`;
212
+ if (!device || device.revoked_at) return reject("FORBIDDEN", 403);
213
+ await eligible(tx, device.user_id);
214
+ return device;
215
+ };
216
+
217
+ return {
218
+ config,
219
+ cleanup,
220
+ maintain: async (notify: (notice: AppDeviceEnrollmentNotice) => Promise<unknown>, signal?: AbortSignal) => {
221
+ await cleanup();
222
+ const rows = await db<
223
+ DeviceRow[]
224
+ >`SELECT * FROM auth.app_devices WHERE notified_at IS NULL ORDER BY created_at LIMIT ${limits.pageSize}`;
225
+ for (const row of rows) {
226
+ signal?.throwIfAborted();
227
+ // The notification owner deduplicates by deviceId. A crash after send
228
+ // but before this marker is safe; no external effect precedes enrollment.
229
+ await notify({ deviceId: row.id, userId: row.user_id, name: row.name, assisted: row.assisted });
230
+ await db`UPDATE auth.app_devices SET notified_at=now() WHERE id=${row.id}::uuid AND notified_at IS NULL`;
231
+ }
232
+ },
233
+ info: async () => {
234
+ const cfg = await config();
235
+ return {
236
+ protocol: APP_APPROVAL_PROTOCOL,
237
+ issuer: cfg.issuer,
238
+ api: cfg.issuer + APP_APPROVAL_PATH,
239
+ appOrigin: cfg.appOrigin,
240
+ algorithm: "ES256",
241
+ limits,
242
+ };
243
+ },
244
+ startPairing: async (actor: AppApprovalActor, targetId = actor.userId) => {
245
+ const cfg = await config();
246
+ await cleanup();
247
+ const id = crypto.randomUUID(),
248
+ token = secret(),
249
+ expiresAt = future(limits.pairingSeconds);
250
+ return db.begin(async (tx) => {
251
+ await fresh(tx, actor);
252
+ if (targetId !== actor.userId && (!actor.admin || !cfg.adminPairing)) return reject("FORBIDDEN", 403);
253
+ await tx`SELECT id FROM auth.users WHERE id = ${targetId}::uuid FOR UPDATE`;
254
+ await eligible(tx, targetId);
255
+ const [count] = await tx<
256
+ { count: number }[]
257
+ >`SELECT count(*)::int AS count FROM auth.app_pairings WHERE issuer = ${cfg.issuer} AND user_id = ${targetId}::uuid AND expires_at > now() AND state IN ('pending','claimed')`;
258
+ if ((count?.count ?? 0) >= limits.pendingPerAccount) return reject("LIMIT_REACHED", 429);
259
+ await tx`INSERT INTO auth.app_pairings(id, issuer, user_id, initiated_by, initiator_sid, assisted, secret_hash, expires_at)
260
+ VALUES (${id}::uuid,${cfg.issuer},${targetId}::uuid,${actor.userId}::uuid,${actor.sid}::uuid,${targetId !== actor.userId},${hash(token)},${expiresAt})`;
261
+ await record(tx, "pairing.start", actor.userId, id, { targetUserId: targetId, assisted: targetId !== actor.userId });
262
+ return { protocol: APP_APPROVAL_PROTOCOL, issuer: cfg.issuer, pairingId: id, secret: token, expiresAt: iso(expiresAt) };
263
+ });
264
+ },
265
+ claimPairing: async (claim: { pairingId: string; secret: string; publicKey: AppDevicePublicKey; name: string; signature: string }) => {
266
+ const cfg = await config();
267
+ if (!(await verify(claim.publicKey, appPairingProofMessage(cfg.issuer, claim), claim.signature))) return reject("FORBIDDEN", 403);
268
+ return db.begin(async (tx) => {
269
+ const [p] = await tx<
270
+ PairingRow[]
271
+ >`SELECT * FROM auth.app_pairings WHERE id = ${claim.pairingId}::uuid AND issuer = ${cfg.issuer} FOR UPDATE`;
272
+ if (!p || !matches(claim.secret, p.secret_hash) || new Date(p.expires_at).getTime() <= Date.now()) return reject("FORBIDDEN", 403);
273
+ await eligible(tx, p.user_id);
274
+ if (p.state !== "pending") return reject("CONFLICT", 409);
275
+ const code = comparison(),
276
+ deviceId = crypto.randomUUID();
277
+ await tx`UPDATE auth.app_pairings SET state = 'claimed', public_key = ${JSON.stringify(claim.publicKey)}::text::jsonb, name = ${claim.name}, device_id = ${deviceId}::uuid, comparison = ${code} WHERE id = ${p.id}::uuid`;
278
+ return { deviceId, comparison: code, expiresAt: iso(p.expires_at) };
279
+ });
280
+ },
281
+ pairingResult: async (id: string, token: string, key: AppDevicePublicKey) => {
282
+ const cfg = await config();
283
+ const [p] = await db<PairingRow[]>`SELECT * FROM auth.app_pairings WHERE id = ${id}::uuid AND issuer = ${cfg.issuer}`;
284
+ if (!p || !matches(token, p.secret_hash) || new Date(p.expires_at).getTime() <= Date.now()) return reject("FORBIDDEN", 403);
285
+ if (p.public_key) {
286
+ const claimed = AppDevicePublicKeySchema.parse(p.public_key);
287
+ if (claimed.x !== key.x || claimed.y !== key.y) return reject("FORBIDDEN", 403);
288
+ }
289
+ return { state: p.state, deviceId: p.device_id, comparison: p.state === "claimed" ? p.comparison : null };
290
+ },
291
+ inspectPairing: async (actor: AppApprovalActor, id: string) => {
292
+ const cfg = await config();
293
+ return db.begin(async (tx) => {
294
+ const [p] = await tx<PairingRow[]>`SELECT * FROM auth.app_pairings WHERE id = ${id}::uuid AND issuer = ${cfg.issuer}`;
295
+ if (!p || new Date(p.expires_at).getTime() <= Date.now()) return reject("UNAVAILABLE", 404);
296
+ await authorizePairing(tx, actor, p, cfg);
297
+ return { state: p.state, name: p.name, comparison: p.comparison, deviceId: p.device_id, userId: p.user_id };
298
+ });
299
+ },
300
+ confirmPairing: async (actor: AppApprovalActor, id: string, code: string) => {
301
+ const cfg = await config();
302
+ return db.begin(async (tx) => {
303
+ const [p] = await tx<PairingRow[]>`SELECT * FROM auth.app_pairings WHERE id = ${id}::uuid AND issuer = ${cfg.issuer} FOR UPDATE`;
304
+ if (
305
+ !p ||
306
+ p.state !== "claimed" ||
307
+ new Date(p.expires_at).getTime() <= Date.now() ||
308
+ p.comparison !== code ||
309
+ !p.device_id ||
310
+ !p.name
311
+ )
312
+ return reject("CONFLICT", 409);
313
+ await authorizePairing(tx, actor, p, cfg);
314
+ await tx`SELECT id FROM auth.users WHERE id = ${p.user_id}::uuid FOR UPDATE`;
315
+ const [count] = await tx<
316
+ { count: number }[]
317
+ >`SELECT count(*)::int AS count FROM auth.app_devices WHERE issuer = ${cfg.issuer} AND user_id = ${p.user_id}::uuid AND revoked_at IS NULL`;
318
+ if ((count?.count ?? 0) >= limits.devicesPerAccount) return reject("LIMIT_REACHED", 429);
319
+ const key = AppDevicePublicKeySchema.parse(p.public_key);
320
+ const existing =
321
+ await tx`SELECT id FROM auth.app_devices WHERE issuer=${cfg.issuer} AND user_id=${p.user_id}::uuid AND public_key=${JSON.stringify(key)}::text::jsonb`;
322
+ if (existing.length) return reject("CONFLICT", 409);
323
+ await tx`INSERT INTO auth.app_devices(id,issuer,user_id,name,public_key,assisted,enrolled_by) VALUES (${p.device_id}::uuid,${cfg.issuer},${p.user_id}::uuid,${p.name},${JSON.stringify(key)}::text::jsonb,${p.assisted},${actor.userId}::uuid)`;
324
+ await tx`UPDATE auth.app_pairings SET state = 'confirmed' WHERE id = ${id}::uuid`;
325
+ await record(tx, "device.enroll", actor.userId, p.device_id, { targetUserId: p.user_id, assisted: p.assisted });
326
+ return { deviceId: p.device_id };
327
+ });
328
+ },
329
+ cancelPairing: async (actor: AppApprovalActor, id: string) => {
330
+ const cfg = await config();
331
+ return db.begin(async (tx) => {
332
+ const [p] = await tx<PairingRow[]>`SELECT * FROM auth.app_pairings WHERE id = ${id}::uuid AND issuer = ${cfg.issuer} FOR UPDATE`;
333
+ if (!p) return reject("UNAVAILABLE", 404);
334
+ await authorizePairing(tx, actor, p, cfg);
335
+ if (p.state === "confirmed") return reject("CONFLICT", 409);
336
+ await tx`UPDATE auth.app_pairings SET state='cancelled' WHERE id=${id}::uuid`;
337
+ await record(tx, "pairing.cancel", actor.userId, id);
338
+ });
339
+ },
340
+ listDevices: async (actor: AppApprovalActor, after?: string) => {
341
+ const cfg = await config(false);
342
+ const rows = await db<
343
+ DeviceRow[]
344
+ >`SELECT * FROM auth.app_devices WHERE issuer=${cfg.issuer} AND user_id=${actor.userId}::uuid AND (${after ?? null}::uuid IS NULL OR id > ${after ?? null}::uuid) ORDER BY id LIMIT ${limits.pageSize + 1}`;
345
+ return {
346
+ items: rows.slice(0, limits.pageSize).map(view),
347
+ nextCursor: rows.length > limits.pageSize ? rows[limits.pageSize - 1]!.id : null,
348
+ };
349
+ },
350
+ mutateDevice: async (actor: AppApprovalActor, id: string, name?: string) => {
351
+ const cfg = await config(false);
352
+ return db.begin(async (tx) => {
353
+ await fresh(tx, actor);
354
+ const [device] = await tx<
355
+ DeviceRow[]
356
+ >`SELECT * FROM auth.app_devices WHERE id=${id}::uuid AND issuer=${cfg.issuer} AND user_id=${actor.userId}::uuid FOR UPDATE`;
357
+ if (!device) return reject("UNAVAILABLE", 404);
358
+ if (name !== undefined) await tx`UPDATE auth.app_devices SET name=${name} WHERE id=${id}::uuid`;
359
+ else await tx`UPDATE auth.app_devices SET revoked_at=COALESCE(revoked_at,now()) WHERE id=${id}::uuid`;
360
+ await record(tx, name === undefined ? "device.revoke" : "device.rename", actor.userId, id);
361
+ });
362
+ },
363
+ startLogin: async (identifier: string, category: AccountCategory) => {
364
+ const cfg = await config();
365
+ await cleanup();
366
+ const id = crypto.randomUUID(),
367
+ browserSecret = secret(),
368
+ challenge = secret(),
369
+ code = comparison(),
370
+ expiresAt = future(limits.loginSeconds);
371
+ await db.begin(async (tx) => {
372
+ const candidates = await tx<
373
+ AccountRow[]
374
+ >`SELECT id,uid,provider,profile,auth_epoch,account_expires FROM auth.users WHERE lower(uid)=lower(${identifier}) OR lower(btrim(mail))=lower(${identifier}) LIMIT 2`;
375
+ let candidate = candidates.length === 1 ? candidates[0] : undefined;
376
+ if (
377
+ candidate &&
378
+ (accountCategory(candidate) !== category ||
379
+ (candidate.account_expires && new Date(candidate.account_expires).getTime() <= Date.now()) ||
380
+ !(await isAccountCategoryAllowed(candidate, tx)))
381
+ )
382
+ candidate = undefined;
383
+ if (candidate) {
384
+ await tx`SELECT id FROM auth.users WHERE id=${candidate.id}::uuid FOR UPDATE`;
385
+ const [count] = await tx<
386
+ { count: number }[]
387
+ >`SELECT count(*)::int AS count FROM auth.app_logins WHERE issuer=${cfg.issuer} AND user_id=${candidate.id}::uuid AND expires_at > now() AND state IN ('pending','approved')`;
388
+ if ((count?.count ?? 0) >= limits.pendingPerAccount) candidate = undefined;
389
+ }
390
+ // Unknown, ambiguous, disabled and throttled accounts have the identical
391
+ // public shape and a decoy pending transaction; no account enumeration.
392
+ await tx`INSERT INTO auth.app_logins(id,issuer,user_id,auth_epoch,category,browser_hash,challenge,comparison,expires_at)
393
+ VALUES (${id}::uuid,${cfg.issuer},${candidate?.id ?? null}::uuid,${candidate?.auth_epoch ?? null},${category},${hash(browserSecret)},${challenge},${code},${expiresAt})`;
394
+ });
395
+ return { requestId: id, browserSecret, comparison: code, expiresAt: iso(expiresAt), pollAfterSeconds: limits.pollSeconds };
396
+ },
397
+ deviceCommand: async (request: AppDeviceRequest) => {
398
+ const cfg = await config();
399
+ const { proof, signature } = request;
400
+ const now = Math.floor(Date.now() / 1000);
401
+ if (
402
+ proof.issuer !== cfg.issuer ||
403
+ proof.issuedAt > now + 5 ||
404
+ proof.issuedAt < now - limits.proofSeconds ||
405
+ proof.expiresAt <= now ||
406
+ proof.expiresAt <= proof.issuedAt ||
407
+ proof.expiresAt > proof.issuedAt + limits.proofSeconds
408
+ )
409
+ return reject("FORBIDDEN", 403);
410
+ await cleanup();
411
+ return db.begin(async (tx) => {
412
+ const device = await activeDevice(tx, proof.deviceId, cfg.issuer);
413
+ if (!(await verify(AppDevicePublicKeySchema.parse(device.public_key), appDeviceProofMessage(proof), signature)))
414
+ return reject("FORBIDDEN", 403);
415
+ const used =
416
+ await tx`INSERT INTO auth.app_device_proofs(device_id,jti,expires_at) VALUES (${device.id}::uuid,${proof.jti}::uuid,${new Date((proof.expiresAt + 5) * 1000)}) ON CONFLICT DO NOTHING RETURNING jti`;
417
+ if (!used.length) return reject("CONFLICT", 409);
418
+ await tx`UPDATE auth.app_devices SET last_used_at=now() WHERE id=${device.id}::uuid`;
419
+ const command = proof.command;
420
+ if (command.operation === "revoke") {
421
+ await tx`UPDATE auth.app_devices SET revoked_at=now() WHERE id=${device.id}::uuid`;
422
+ await record(tx, "device.revoke", device.user_id, device.id);
423
+ return { state: "revoked" as const };
424
+ }
425
+ if (command.operation === "pending") {
426
+ const user = await eligible(tx, device.user_id);
427
+ const rows = await tx<
428
+ LoginRow[]
429
+ >`SELECT * FROM auth.app_logins WHERE issuer=${cfg.issuer} AND user_id=${device.user_id}::uuid AND state='pending' AND expires_at>now() AND auth_epoch=${user.auth_epoch} AND category=${accountCategory(user)} ORDER BY created_at LIMIT ${limits.pendingPerAccount}`;
430
+ return {
431
+ requests: rows.map(
432
+ (row): AppPendingLogin => ({
433
+ requestId: row.id,
434
+ challenge: row.challenge,
435
+ comparison: row.comparison,
436
+ createdAt: iso(row.created_at),
437
+ expiresAt: iso(row.expires_at),
438
+ }),
439
+ ),
440
+ pollAfterSeconds: limits.pollSeconds,
441
+ };
442
+ }
443
+ const [row] = await tx<
444
+ LoginRow[]
445
+ >`SELECT * FROM auth.app_logins WHERE id=${command.requestId}::uuid AND issuer=${cfg.issuer} AND user_id=${device.user_id}::uuid FOR UPDATE`;
446
+ const user = await eligible(tx, device.user_id);
447
+ if (
448
+ !row ||
449
+ row.state !== "pending" ||
450
+ new Date(row.expires_at).getTime() <= Date.now() ||
451
+ row.challenge !== command.challenge ||
452
+ row.comparison !== command.comparison ||
453
+ row.auth_epoch !== user.auth_epoch ||
454
+ row.category !== accountCategory(user)
455
+ )
456
+ return reject("CONFLICT", 409);
457
+ const state = command.decision === "approve" ? "approved" : "denied";
458
+ await tx`UPDATE auth.app_logins SET state=${state},approved_by=${device.id}::uuid WHERE id=${row.id}::uuid`;
459
+ await record(tx, `login.${command.decision}`, device.user_id, device.id, { requestId: row.id });
460
+ return { state };
461
+ });
462
+ },
463
+ browserStatus: async (id: string, token: string) => {
464
+ const cfg = await config();
465
+ const [row] = await db<LoginRow[]>`SELECT * FROM auth.app_logins WHERE id=${id}::uuid AND issuer=${cfg.issuer}`;
466
+ if (!row || !matches(token, row.browser_hash)) return reject("UNAVAILABLE", 404);
467
+ return { state: new Date(row.expires_at).getTime() <= Date.now() ? "expired" : row.state, pollAfterSeconds: limits.pollSeconds };
468
+ },
469
+ consumeLogin: async (id: string, token: string) => {
470
+ const cfg = await config();
471
+ return db.begin(async (tx) => {
472
+ // Lock order is device -> request for both approval and completion.
473
+ const [snapshot] = await tx<LoginRow[]>`SELECT * FROM auth.app_logins WHERE id=${id}::uuid AND issuer=${cfg.issuer}`;
474
+ if (!snapshot || !snapshot.approved_by || !matches(token, snapshot.browser_hash)) return reject("CONFLICT", 409);
475
+ const device = await activeDevice(tx, snapshot.approved_by, cfg.issuer);
476
+ const [row] = await tx<LoginRow[]>`SELECT * FROM auth.app_logins WHERE id=${id}::uuid AND issuer=${cfg.issuer} FOR UPDATE`;
477
+ const user = await eligible(tx, device.user_id);
478
+ if (
479
+ !row ||
480
+ row.state !== "approved" ||
481
+ row.user_id !== device.user_id ||
482
+ row.approved_by !== device.id ||
483
+ new Date(row.expires_at).getTime() <= Date.now() ||
484
+ row.auth_epoch !== user.auth_epoch ||
485
+ row.category !== accountCategory(user)
486
+ )
487
+ return reject("CONFLICT", 409);
488
+ await tx`UPDATE auth.app_logins SET state='consumed' WHERE id=${id}::uuid`;
489
+ await record(tx, "login.consume", user.id, device.id, { requestId: id });
490
+ // This commit is the authorization boundary. A later issuance/network
491
+ // failure requires a new login, never replay of the consumed approval.
492
+ return user.id;
493
+ });
494
+ },
495
+ };
496
+ };
497
+
498
+ export const appApproval = createAppApprovalService();