@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,125 @@
1
+ import type { Context, MiddlewareHandler } from "hono";
2
+ import { createMiddleware } from "hono/factory";
3
+ import type { MessageResponse } from "../../contracts/shared";
4
+ import * as settings from "../../services/settings";
5
+ import { ratelimit } from "../ratelimit";
6
+ import { type AuthContext, auth } from "./auth";
7
+
8
+ export type RateLimitRouteOverride = {
9
+ method?: string;
10
+ path: string | RegExp;
11
+ limitPerSecond?: number;
12
+ disabled?: boolean;
13
+ };
14
+
15
+ export type RateLimitConfig = {
16
+ limitPerSecond?: number;
17
+ windowSecs?: number;
18
+ keyBy?: "auto" | "ip" | "user";
19
+ routes?: RateLimitRouteOverride[];
20
+ };
21
+
22
+ type ResolvedRateLimit = {
23
+ disabled: boolean;
24
+ limitPerSecond: number;
25
+ windowSecs: number;
26
+ };
27
+
28
+ const DEFAULT_WINDOW_SECS = 1;
29
+ const LIMITER_PREFIX = "cloud:rate-limit";
30
+ const limiterCache = new Map<string, ReturnType<typeof ratelimit>>();
31
+
32
+ const getClientIp = (c: Context): string =>
33
+ c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ?? c.req.header("x-real-ip") ?? "unknown";
34
+
35
+ const pathMatches = (requestPath: string, matcher: string | RegExp): boolean => {
36
+ if (matcher instanceof RegExp) return matcher.test(requestPath);
37
+ return requestPath === matcher || requestPath.startsWith(`${matcher}/`);
38
+ };
39
+
40
+ const resolveRouteOverride = (c: Context, overrides: RateLimitRouteOverride[] | undefined): RateLimitRouteOverride | undefined => {
41
+ if (!overrides || overrides.length === 0) return undefined;
42
+ return overrides.find((override) => {
43
+ if (override.method && override.method.toUpperCase() !== c.req.method.toUpperCase()) {
44
+ return false;
45
+ }
46
+ return pathMatches(c.req.path, override.path);
47
+ });
48
+ };
49
+
50
+ const resolveConfig = async (c: Context, config: RateLimitConfig): Promise<ResolvedRateLimit> => {
51
+ const override = resolveRouteOverride(c, config.routes);
52
+ if (override?.disabled) {
53
+ return {
54
+ disabled: true,
55
+ limitPerSecond: 0,
56
+ windowSecs: config.windowSecs ?? DEFAULT_WINDOW_SECS,
57
+ };
58
+ }
59
+
60
+ const configuredLimit = override?.limitPerSecond ?? config.limitPerSecond ?? null;
61
+ const limitPerSecond = configuredLimit ?? (await settings.get<number>("security.rate_limit_per_second"));
62
+
63
+ return {
64
+ disabled: false,
65
+ limitPerSecond: Math.max(1, Math.floor(limitPerSecond)),
66
+ windowSecs: Math.max(1, Math.floor(config.windowSecs ?? DEFAULT_WINDOW_SECS)),
67
+ };
68
+ };
69
+
70
+ const getLimiter = (limitPerSecond: number, windowSecs: number) => {
71
+ const cacheKey = `${limitPerSecond}:${windowSecs}`;
72
+ const cached = limiterCache.get(cacheKey);
73
+ if (cached) return cached;
74
+
75
+ const limiter = ratelimit({
76
+ id: cacheKey,
77
+ limit: limitPerSecond,
78
+ windowSecs,
79
+ prefix: LIMITER_PREFIX,
80
+ });
81
+ limiterCache.set(cacheKey, limiter);
82
+ return limiter;
83
+ };
84
+
85
+ const resolveIdentifier = async (c: Context<AuthContext>, keyBy: RateLimitConfig["keyBy"]): Promise<string> => {
86
+ if (keyBy === "ip") return `ip:${getClientIp(c)}`;
87
+
88
+ const token = auth.session.getToken(c);
89
+ if (!token) return `ip:${getClientIp(c)}`;
90
+
91
+ const userId = await auth.session.getRequestSubject(c, token);
92
+ if (!userId) return `ip:${getClientIp(c)}`;
93
+
94
+ return `user:${userId}`;
95
+ };
96
+
97
+ /**
98
+ * Stateless per-route rate limiting middleware backed by the Cloud Redis.
99
+ * Keying defaults to user ID (when session exists), otherwise client IP.
100
+ */
101
+ export const rateLimit = (config: RateLimitConfig = {}): MiddlewareHandler<AuthContext> =>
102
+ createMiddleware<AuthContext>(async (c, next) => {
103
+ const resolved = await resolveConfig(c, config);
104
+ if (resolved.disabled) {
105
+ await next();
106
+ return;
107
+ }
108
+
109
+ const keyBy = config.keyBy ?? "auto";
110
+ const identifier = await resolveIdentifier(c, keyBy);
111
+ const limiter = getLimiter(resolved.limitPerSecond, resolved.windowSecs);
112
+ const result = await limiter.check(identifier);
113
+ const resetInSeconds = Math.max(1, Math.ceil(result.resetIn / 1000));
114
+
115
+ c.header("X-RateLimit-Limit", String(resolved.limitPerSecond));
116
+ c.header("X-RateLimit-Remaining", String(result.remaining));
117
+ c.header("X-RateLimit-Reset", String(resetInSeconds));
118
+
119
+ if (result.limited) {
120
+ c.header("Retry-After", String(resetInSeconds));
121
+ return c.json({ message: "Rate limit exceeded" } as MessageResponse, 429);
122
+ }
123
+
124
+ await next();
125
+ });
@@ -0,0 +1,41 @@
1
+ import { createMiddleware } from "hono/factory";
2
+ import { logger } from "../../services/logging";
3
+ import type { AuthContext } from "./auth";
4
+
5
+ const log = logger("http");
6
+
7
+ const SKIP_PREFIXES = ["/public/", "/_ssr/", "/favicon", "/branding/"];
8
+
9
+ /**
10
+ * HTTP request logging middleware.
11
+ * Logs to DB based on response status:
12
+ * - 5xx → error (server errors)
13
+ * - 429 → warn (rate limiting)
14
+ * - 401/403 → info (auth flows)
15
+ * - Everything else (2xx, 3xx, 400, 404) → not logged (too noisy)
16
+ */
17
+ export const requestLogger = createMiddleware<AuthContext>(async (c, next) => {
18
+ const path = c.req.path;
19
+ if (SKIP_PREFIXES.some((p) => path.startsWith(p))) return next();
20
+
21
+ const start = Date.now();
22
+ await next();
23
+ const status = c.res.status;
24
+ const duration = Date.now() - start;
25
+
26
+ const meta = {
27
+ method: c.req.method,
28
+ path,
29
+ status,
30
+ duration,
31
+ userId: c.get("user")?.id ?? null,
32
+ };
33
+
34
+ if (status >= 500) {
35
+ log.error(`${c.req.method} ${path} ${status}`, meta);
36
+ } else if (status === 429) {
37
+ log.warn(`${c.req.method} ${path} 429 rate-limited`, meta);
38
+ } else if (status === 401 || status === 403) {
39
+ log.info(`${c.req.method} ${path} ${status}`, meta);
40
+ }
41
+ });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Publishes the app's matched Hono route template to the gateway.
3
+ *
4
+ * Gateway telemetry only knows the registry prefix it routed on
5
+ * (`/api/mail`), so every endpoint of an app collapses into one bucket —
6
+ * enough to see that an app is failing, never enough to see which
7
+ * endpoint. The app itself already knows the exact answer: after the
8
+ * handler ran, `routePath(c)` resolves to the registered pattern
9
+ * (`/api/mail/mailboxes/:mailboxId`).
10
+ *
11
+ * We hand it to the gateway in a response header. The gateway reads it
12
+ * off the upstream response next to status and duration — which it
13
+ * already sources the same way — and strips it before the client sees
14
+ * it. No second telemetry pipeline, no path guessing.
15
+ *
16
+ * Templates only: the request path itself never leaves the app, so ids
17
+ * and query strings stay out of telemetry by construction.
18
+ */
19
+ import { createMiddleware } from "hono/factory";
20
+ import { routePath } from "hono/route";
21
+ import { ROUTE_TEMPLATE_HEADER } from "../../services/gateway";
22
+
23
+ /** Hono's placeholder when nothing matched (404) — carries no information. */
24
+ const UNMATCHED = "/*";
25
+
26
+ const MAX_TEMPLATE_LENGTH = 200;
27
+
28
+ /**
29
+ * Hono allows a regex constraint on a param (`:filename{.+\.js$}`). It is part
30
+ * of the pattern but noise in an ops table, and it can carry characters that
31
+ * are awkward in a header value, so only the param name is reported.
32
+ */
33
+ const stripParamConstraints = (template: string): string => template.replace(/\{[^}]*\}/g, "");
34
+
35
+ export const routeTemplate = createMiddleware(async (c, next) => {
36
+ await next();
37
+
38
+ // Best-effort: telemetry enrichment must never break a response.
39
+ try {
40
+ const template = routePath(c);
41
+ if (!template || template === UNMATCHED) return;
42
+ c.res.headers.set(ROUTE_TEMPLATE_HEADER, stripParamConstraints(template).slice(0, MAX_TEMPLATE_LENGTH));
43
+ } catch {
44
+ // Immutable response headers or an unrouted context — nothing to report.
45
+ }
46
+ });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Per-request middleware that exposes the live cluster registry on
3
+ * `c.get("runtime")`. The first call to this middleware kicks off the
4
+ * Redis registry watcher (idempotent); subsequent requests read from
5
+ * the in-memory snapshot.
6
+ *
7
+ * Required by anything that renders the framework's `<Layout>`,
8
+ * `<Sidebar>`, dashboard widget aggregator, or `Cmd+K` global search.
9
+ */
10
+ import { createMiddleware } from "hono/factory";
11
+ import { ensureRuntimeWatcher, getCurrentRuntime } from "../../_internal/runtime-watcher";
12
+ import { routeTemplate } from "./route-template";
13
+
14
+ export const runtime = () =>
15
+ createMiddleware(async (c, next) => {
16
+ await ensureRuntimeWatcher();
17
+ (c as unknown as { set: (k: string, v: unknown) => void }).set("runtime", getCurrentRuntime());
18
+ // Piggybacks on the middleware every proxied app already installs, so
19
+ // gateway telemetry gets per-endpoint route templates without any app
20
+ // having to opt in. See `middleware.observability()` for the rest.
21
+ await routeTemplate(c, next);
22
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Per-request middleware that exposes a frozen settings snapshot on
3
+ * `c.get("settings")`. Backed by Redis cache-aside (5-minute TTL),
4
+ * so the per-request cost is a single Redis read at most.
5
+ *
6
+ * Required by anything that reads typed settings inside an HTTP
7
+ * handler via `c.get("settings").<key>`.
8
+ *
9
+ * `skipPrefixes` defaults to `["/public/", "/_ssr/", "/branding/", "/favicon"]`
10
+ * — those paths never read settings, so skipping the snapshot load
11
+ * keeps static-asset requests free. Apps that mount settings only on
12
+ * /api or /app can avoid the option entirely by scoping the
13
+ * `.use()` path instead.
14
+ */
15
+ import { createMiddleware } from "hono/factory";
16
+ import {
17
+ type ActiveAnnouncementsResponse,
18
+ type AnnouncementCookieState,
19
+ parseAnnouncementCookieHeader,
20
+ } from "../../contracts/announcements";
21
+ import { announcements } from "../../services/announcements";
22
+ import { logger } from "../../services/logging";
23
+ import { loadSnapshot } from "../../services/settings/snapshot";
24
+
25
+ const DEFAULT_SKIP = ["/public/", "/_ssr/", "/branding/", "/favicon"] as const;
26
+ const ANNOUNCEMENT_SKIP = ["/api/", "/public/", "/_ssr/", "/branding/", "/favicon"] as const;
27
+ const log = logger("middleware:settings");
28
+
29
+ export type LayoutAnnouncementsState = ActiveAnnouncementsResponse & {
30
+ cookieState: AnnouncementCookieState;
31
+ };
32
+
33
+ const shouldLoadAnnouncements = (path: string, cookieHeader: string | null): boolean =>
34
+ Boolean(cookieHeader?.match(/(?:^|;\s*)session_token=/)) && !ANNOUNCEMENT_SKIP.some((prefix) => path.startsWith(prefix));
35
+
36
+ export const settings = (opts?: { skipPrefixes?: readonly string[] }) => {
37
+ const skip = opts?.skipPrefixes ?? DEFAULT_SKIP;
38
+ return createMiddleware(async (c, next) => {
39
+ const path = c.req.path;
40
+ if (!skip.some((p) => path.startsWith(p))) {
41
+ (c as unknown as { set: (k: string, v: unknown) => void }).set("settings", await loadSnapshot());
42
+ }
43
+ const cookieHeader = c.req.header("Cookie") ?? null;
44
+ if (shouldLoadAnnouncements(path, cookieHeader)) {
45
+ const cookieState = parseAnnouncementCookieHeader(cookieHeader);
46
+ try {
47
+ const active = await announcements.active.forState({ state: cookieState });
48
+ (c as unknown as { set: (k: string, v: unknown) => void }).set("announcements", { ...active, cookieState });
49
+ } catch (error) {
50
+ log.warn("Failed to preload announcements", { error: error instanceof Error ? error.message : String(error) });
51
+ }
52
+ }
53
+ await next();
54
+ });
55
+ };
@@ -0,0 +1,49 @@
1
+ import type { Context, ValidationTargets } from "hono";
2
+ import { validator as honoValidator } from "hono-openapi";
3
+ import type { ZodType } from "zod";
4
+
5
+ export type ValidatorError = Readonly<{
6
+ code: string;
7
+ message: string;
8
+ }>;
9
+
10
+ export type ValidatorErrorResolver = (context: Context) => ValidatorError;
11
+
12
+ /**
13
+ * Zod validator middleware with pretty error messages and OpenAPI support.
14
+ * Uses hono-openapi validator for automatic OpenAPI schema generation.
15
+ *
16
+ * @param target - Where to validate: "json", "query", "param", "header", "cookie", or "form"
17
+ * @param schema - Zod schema to validate against
18
+ * @param resolveError - Optional request-scoped application error resolver
19
+ * @returns Hono middleware that validates request data and returns 400 on failure
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * app.post("/users", v("json", CreateUserSchema), async (c) => {
24
+ * const data = c.req.valid("json"); // Fully typed!
25
+ * // ...
26
+ * });
27
+ * ```
28
+ */
29
+ export const validator = <Target extends keyof ValidationTargets, T extends ZodType>(
30
+ target: Target,
31
+ schema: T,
32
+ resolveError?: ValidatorErrorResolver,
33
+ ) =>
34
+ honoValidator(target, schema, (result, c: Context) => {
35
+ if (!result.success) {
36
+ if (resolveError) return c.json(resolveError(c), 400);
37
+
38
+ // Standard Schema returns issues array on failure
39
+ const errorMessage = result.error
40
+ ?.map((issue) => {
41
+ const path = issue.path?.map((p) => (typeof p === "object" && "key" in p ? String(p.key) : String(p))).join(".");
42
+ return path ? `${path}: ${issue.message}` : issue.message;
43
+ })
44
+ .join(", ");
45
+ return c.json({ message: errorMessage || "Validation failed" }, 400);
46
+ }
47
+ });
48
+
49
+ export const v = validator;
@@ -0,0 +1,22 @@
1
+ import type { Context, MiddlewareHandler } from "hono";
2
+ import { createMiddleware } from "hono/factory";
3
+ import { hasReservedWorkloadScope } from "../../services/identity/workload-auth";
4
+ import type { AuthContext } from "./auth";
5
+
6
+ /** Reserved app workload keys are valid only at their dedicated Core authority routes. */
7
+ export const isReservedWorkloadCredential = (c: Context<AuthContext>): boolean => {
8
+ const actor = c.get("actor");
9
+ return (
10
+ c.get("credentialKind") === "api_key" &&
11
+ actor?.kind === "service_account" &&
12
+ actor.serviceAccount.kind === "resource_bound" &&
13
+ hasReservedWorkloadScope(c.get("credentialScopes") ?? [])
14
+ );
15
+ };
16
+
17
+ const reservedWorkloadResponse = (c: Context<AuthContext>): Response =>
18
+ c.json({ code: "FORBIDDEN", message: "App workload credentials require a dedicated Core authority route" }, 403);
19
+
20
+ export const rejectReservedWorkloadCredential: MiddlewareHandler<AuthContext> = createMiddleware<AuthContext>(async (c, next) =>
21
+ isReservedWorkloadCredential(c) ? reservedWorkloadResponse(c) : await next(),
22
+ );
@@ -0,0 +1,151 @@
1
+ import { createHash } from "node:crypto";
2
+ import { redis } from "bun";
3
+
4
+ /**
5
+ * Sliding-window rate limiter on the Cloud Redis.
6
+ *
7
+ * Ported 1:1 from @k2b/sync v5 (which dropped it in v6): two fixed-window
8
+ * counters per identifier, the previous one weighted by the remaining share
9
+ * of the current window. Key layout and semantics are unchanged so existing
10
+ * counters keep working across the cutover.
11
+ */
12
+
13
+ const DEFAULT_PREFIX = "cloud:ratelimit";
14
+ const DEFAULT_WINDOW_SECS = 1;
15
+ const MAX_IDENTIFIER_LENGTH = 128;
16
+
17
+ const RATE_LIMIT_SCRIPT = `
18
+ local currentKey = KEYS[1]
19
+ local previousKey = KEYS[2]
20
+ local windowSecs = tonumber(ARGV[1])
21
+ local limit = tonumber(ARGV[2])
22
+ local elapsedRatio = tonumber(ARGV[3])
23
+
24
+ local previousCount = tonumber(redis.call("GET", previousKey) or "0")
25
+
26
+ local currentCount = redis.call("INCR", currentKey)
27
+ if currentCount == 1 then
28
+ redis.call("EXPIRE", currentKey, windowSecs * 2)
29
+ end
30
+
31
+ local weightedCount = previousCount * (1 - elapsedRatio) + currentCount
32
+
33
+ -- Redis converts Lua numbers returned directly to integers. Return the
34
+ -- weighted value as text so fractional carry-over is not truncated.
35
+ return {currentCount, previousCount, tostring(weightedCount)}
36
+ `;
37
+
38
+ const normalizeIdentifier = (identifier: string): string => {
39
+ if (identifier.length <= MAX_IDENTIFIER_LENGTH) return identifier;
40
+ const hash = createHash("sha256").update(identifier).digest("hex");
41
+ return `hash:${hash}`;
42
+ };
43
+
44
+ // ==========================
45
+ // Types
46
+ // ==========================
47
+
48
+ export type RateLimitResult = {
49
+ limited: boolean;
50
+ remaining: number;
51
+ /** Milliseconds until the current window resets. */
52
+ resetIn: number;
53
+ };
54
+
55
+ export type RateLimiterConfig = {
56
+ id: string;
57
+ limit: number;
58
+ windowSecs?: number;
59
+ prefix?: string;
60
+ };
61
+
62
+ export type RateLimiter = {
63
+ id: string;
64
+ check(identifier: string): Promise<RateLimitResult>;
65
+ checkOrThrow(identifier: string): Promise<RateLimitResult>;
66
+ };
67
+
68
+ // ==========================
69
+ // Rate Limit Error
70
+ // ==========================
71
+
72
+ export class RateLimitError extends Error {
73
+ readonly remaining: number;
74
+ readonly resetIn: number;
75
+
76
+ constructor(result: RateLimitResult) {
77
+ super("Rate limit exceeded");
78
+ this.name = "RateLimitError";
79
+ this.remaining = result.remaining;
80
+ this.resetIn = result.resetIn;
81
+ }
82
+ }
83
+
84
+ // ==========================
85
+ // Rate Limiter Factory
86
+ // ==========================
87
+
88
+ export const ratelimit = (config: RateLimiterConfig): RateLimiter => {
89
+ // windowSecs: 0 — e.g. Number(process.env.RL_WINDOW) on an unset var — made
90
+ // both window keys Infinity and collapse to one, elapsedRatio NaN, and
91
+ // `EXPIRE key 0` delete the counter on every first increment, so the
92
+ // weighted count never exceeded the limit and the limiter allowed
93
+ // everything. A fractional windowSecs made EXPIRE reject exactly the first
94
+ // request of each window. Fail loudly at construction instead.
95
+ if (!Number.isFinite(config.limit) || config.limit <= 0) {
96
+ throw new Error("limit must be > 0");
97
+ }
98
+ if (
99
+ config.windowSecs !== undefined &&
100
+ (!Number.isSafeInteger(config.windowSecs) || config.windowSecs <= 0 || config.windowSecs > Math.floor(Number.MAX_SAFE_INTEGER / 2_000))
101
+ ) {
102
+ throw new Error("windowSecs must be a positive integer number of seconds");
103
+ }
104
+
105
+ const prefix = config.prefix ?? DEFAULT_PREFIX;
106
+ const windowSecs = config.windowSecs ?? DEFAULT_WINDOW_SECS;
107
+ const { limit } = config;
108
+
109
+ const check = async (identifier: string): Promise<RateLimitResult> => {
110
+ const safeIdentifier = normalizeIdentifier(identifier);
111
+ const now = Date.now();
112
+ const windowMs = windowSecs * 1000;
113
+ const currentWindow = Math.floor(now / windowMs);
114
+ const previousWindow = currentWindow - 1;
115
+ const elapsedInWindow = now % windowMs;
116
+ const elapsedRatio = elapsedInWindow / windowMs;
117
+
118
+ const currentKey = `${prefix}:${config.id}:${safeIdentifier}:${currentWindow}`;
119
+ const previousKey = `${prefix}:${config.id}:${safeIdentifier}:${previousWindow}`;
120
+
121
+ const result = (await redis.send("EVAL", [
122
+ RATE_LIMIT_SCRIPT,
123
+ "2",
124
+ currentKey,
125
+ previousKey,
126
+ windowSecs.toString(),
127
+ limit.toString(),
128
+ elapsedRatio.toString(),
129
+ ])) as [number, number, string];
130
+
131
+ const weightedCount = Number(result[2]);
132
+
133
+ // Fail closed on a non-comparable count: an abuse-control primitive must
134
+ // never allow everything because arithmetic went sideways.
135
+ const limited = !Number.isFinite(weightedCount) || weightedCount > limit;
136
+ const remaining = Math.max(0, Math.floor(limit - weightedCount));
137
+ const resetIn = windowMs - elapsedInWindow;
138
+
139
+ return { limited, remaining, resetIn };
140
+ };
141
+
142
+ const checkOrThrow = async (identifier: string): Promise<RateLimitResult> => {
143
+ const result = await check(identifier);
144
+ if (result.limited) {
145
+ throw new RateLimitError(result);
146
+ }
147
+ return result;
148
+ };
149
+
150
+ return { id: config.id, check, checkOrThrow };
151
+ };
@@ -0,0 +1,18 @@
1
+ /** One cookie value from raw request headers, URL-decoded when possible. */
2
+ export const readCookie = (headers: Headers, name: string): string | undefined => {
3
+ const cookie = headers.get("Cookie");
4
+ if (!cookie) return undefined;
5
+
6
+ for (const part of cookie.split(";")) {
7
+ const [rawName, ...rawValue] = part.trim().split("=");
8
+ if (rawName !== name) continue;
9
+ const value = rawValue.join("=");
10
+ try {
11
+ return decodeURIComponent(value);
12
+ } catch {
13
+ return value;
14
+ }
15
+ }
16
+
17
+ return undefined;
18
+ };