devpush 0.0.0-stage → 1.0.0-beta.3

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 (456) hide show
  1. package/README.md +6 -2
  2. package/agent-release.json +37 -0
  3. package/dist/package/LICENSE.md +21 -0
  4. package/dist/package/migrate.mjs +182 -0
  5. package/dist/package/plugins.mjs +125 -0
  6. package/dist/package/vite.mjs +439 -0
  7. package/package.json +170 -4
  8. package/public/apple-touch-icon.png +0 -0
  9. package/public/favicon.svg +1 -0
  10. package/public/logo-email.png +0 -0
  11. package/public/robots.txt +3 -0
  12. package/public/social.png +0 -0
  13. package/src/cloudflare-runtime.d.ts +78 -0
  14. package/src/components/about-dialog.tsx +70 -0
  15. package/src/components/copy-button.tsx +74 -0
  16. package/src/components/logo-mark.tsx +20 -0
  17. package/src/components/pending-collection.tsx +103 -0
  18. package/src/components/resource-avatar.tsx +65 -0
  19. package/src/components/route-error-state.tsx +95 -0
  20. package/src/components/section-navigation.tsx +39 -0
  21. package/src/components/settings-layout.tsx +111 -0
  22. package/src/components/storage-capabilities.tsx +11 -0
  23. package/src/components/table-pager.tsx +101 -0
  24. package/src/components/ui/alert-dialog.tsx +180 -0
  25. package/src/components/ui/alert.tsx +78 -0
  26. package/src/components/ui/avatar.tsx +58 -0
  27. package/src/components/ui/badge.tsx +49 -0
  28. package/src/components/ui/button-group.tsx +38 -0
  29. package/src/components/ui/button.tsx +67 -0
  30. package/src/components/ui/card.tsx +82 -0
  31. package/src/components/ui/chart.tsx +307 -0
  32. package/src/components/ui/checkbox.tsx +30 -0
  33. package/src/components/ui/combobox.tsx +256 -0
  34. package/src/components/ui/dialog.tsx +163 -0
  35. package/src/components/ui/dropdown-menu.tsx +110 -0
  36. package/src/components/ui/empty.tsx +65 -0
  37. package/src/components/ui/field.tsx +192 -0
  38. package/src/components/ui/input-group.tsx +118 -0
  39. package/src/components/ui/input-otp.tsx +70 -0
  40. package/src/components/ui/input.tsx +19 -0
  41. package/src/components/ui/label.tsx +24 -0
  42. package/src/components/ui/pagination.tsx +34 -0
  43. package/src/components/ui/popover.tsx +86 -0
  44. package/src/components/ui/select.tsx +145 -0
  45. package/src/components/ui/skeleton.tsx +19 -0
  46. package/src/components/ui/sonner.tsx +47 -0
  47. package/src/components/ui/switch.tsx +31 -0
  48. package/src/components/ui/table.tsx +81 -0
  49. package/src/components/ui/textarea.tsx +18 -0
  50. package/src/components/ui/tooltip.tsx +55 -0
  51. package/src/features/admin/admin-settings.tsx +1048 -0
  52. package/src/features/auth/current-user-context-query.ts +11 -0
  53. package/src/features/auth/login-form.tsx +442 -0
  54. package/src/features/auth/sign-out-menu-item.tsx +42 -0
  55. package/src/features/deployments/deployment-actions.tsx +336 -0
  56. package/src/features/deployments/deployment-alias-badge.tsx +35 -0
  57. package/src/features/deployments/deployment-detail.tsx +596 -0
  58. package/src/features/deployments/deployment-filters.ts +66 -0
  59. package/src/features/deployments/deployment-list.tsx +465 -0
  60. package/src/features/deployments/deployment-visit-links.tsx +104 -0
  61. package/src/features/deployments/deployments-page.tsx +376 -0
  62. package/src/features/logs/log-scroll-area.tsx +69 -0
  63. package/src/features/notifications/email-preferences.tsx +232 -0
  64. package/src/features/onboarding/profile-onboarding.tsx +162 -0
  65. package/src/features/plugins/contributions.tsx +118 -0
  66. package/src/features/projects/backup-job-logs.tsx +143 -0
  67. package/src/features/projects/backup-policy-delete-dialog.tsx +82 -0
  68. package/src/features/projects/backup-restore-actions.tsx +573 -0
  69. package/src/features/projects/backup-status-icon.tsx +33 -0
  70. package/src/features/projects/backup-target-list.tsx +222 -0
  71. package/src/features/projects/backup-timestamp.tsx +32 -0
  72. package/src/features/projects/backup-volume-picker.tsx +158 -0
  73. package/src/features/projects/compose-service-input.tsx +64 -0
  74. package/src/features/projects/deploy-readiness.ts +28 -0
  75. package/src/features/projects/environment-dot.tsx +91 -0
  76. package/src/features/projects/log-keyword-filter.tsx +48 -0
  77. package/src/features/projects/manual-deploy-dialog.tsx +563 -0
  78. package/src/features/projects/pending-section.ts +26 -0
  79. package/src/features/projects/project-backup-details-pending.tsx +305 -0
  80. package/src/features/projects/project-backup-details.tsx +936 -0
  81. package/src/features/projects/project-backup-policy.tsx +644 -0
  82. package/src/features/projects/project-backup-utils.ts +190 -0
  83. package/src/features/projects/project-backups-pending.tsx +136 -0
  84. package/src/features/projects/project-backups.tsx +876 -0
  85. package/src/features/projects/project-environment-settings.tsx +1128 -0
  86. package/src/features/projects/project-heading.tsx +68 -0
  87. package/src/features/projects/project-layout.tsx +188 -0
  88. package/src/features/projects/project-logs.tsx +461 -0
  89. package/src/features/projects/project-overview.tsx +280 -0
  90. package/src/features/projects/project-route-pending.tsx +1064 -0
  91. package/src/features/projects/project-routing-settings.tsx +663 -0
  92. package/src/features/projects/project-settings-section.tsx +64 -0
  93. package/src/features/projects/project-settings-utils.ts +101 -0
  94. package/src/features/projects/project-settings.tsx +1472 -0
  95. package/src/features/projects/repository-picker-dialog.tsx +404 -0
  96. package/src/features/repositories/new-project-dialog-launcher.tsx +44 -0
  97. package/src/features/repositories/new-project-dialog.tsx +1204 -0
  98. package/src/features/repositories/new-project-pending.tsx +28 -0
  99. package/src/features/repositories/repository-list-pending.tsx +59 -0
  100. package/src/features/servers/server-availability-alert.tsx +44 -0
  101. package/src/features/servers/server-availability.ts +29 -0
  102. package/src/features/servers/server-avatar.tsx +52 -0
  103. package/src/features/servers/server-detail-pending.tsx +315 -0
  104. package/src/features/servers/server-detail.tsx +855 -0
  105. package/src/features/servers/server-metric-chart-pending.tsx +38 -0
  106. package/src/features/servers/server-metric-chart.tsx +182 -0
  107. package/src/features/servers/server-metric-format.ts +50 -0
  108. package/src/features/servers/server-metrics-pending.tsx +75 -0
  109. package/src/features/servers/server-metrics.tsx +136 -0
  110. package/src/features/servers/team-servers.tsx +432 -0
  111. package/src/features/settings/settings-route-pending.tsx +406 -0
  112. package/src/features/teams/backup-storage-settings.tsx +483 -0
  113. package/src/features/teams/navigation.ts +51 -0
  114. package/src/features/teams/new-team-dialog.tsx +132 -0
  115. package/src/features/teams/pending-section.ts +19 -0
  116. package/src/features/teams/team-header.tsx +1286 -0
  117. package/src/features/teams/team-heading.tsx +57 -0
  118. package/src/features/teams/team-home.tsx +391 -0
  119. package/src/features/teams/team-invite.tsx +155 -0
  120. package/src/features/teams/team-route-pending.tsx +505 -0
  121. package/src/features/teams/team-settings.tsx +1555 -0
  122. package/src/features/teams/use-workspace-updates.ts +126 -0
  123. package/src/features/users/api-keys-settings.tsx +544 -0
  124. package/src/features/users/api-scope-selection.tsx +269 -0
  125. package/src/features/users/connected-apps-settings.tsx +449 -0
  126. package/src/features/users/mcp-consent.tsx +220 -0
  127. package/src/features/users/profile-picture.tsx +171 -0
  128. package/src/features/users/user-settings.tsx +1064 -0
  129. package/src/lib/account-settings-schema.ts +78 -0
  130. package/src/lib/action-feedback.ts +81 -0
  131. package/src/lib/admin-search-schema.ts +8 -0
  132. package/src/lib/agent-release.d.mts +21 -0
  133. package/src/lib/agent-release.mjs +90 -0
  134. package/src/lib/allowlist-schema.ts +79 -0
  135. package/src/lib/api-key-schema.ts +176 -0
  136. package/src/lib/auth/client.ts +20 -0
  137. package/src/lib/avatar-schema.ts +11 -0
  138. package/src/lib/backup-cron.ts +118 -0
  139. package/src/lib/backup-database.ts +41 -0
  140. package/src/lib/backup-feature.ts +2 -0
  141. package/src/lib/backup-paths.ts +7 -0
  142. package/src/lib/backup-policy-schema.ts +85 -0
  143. package/src/lib/backup-resource-schema.ts +12 -0
  144. package/src/lib/backup-restore-schema.ts +15 -0
  145. package/src/lib/backup-storage-schema.ts +55 -0
  146. package/src/lib/deferred-module.ts +38 -0
  147. package/src/lib/deployment-logs-query.ts +63 -0
  148. package/src/lib/display-name.ts +7 -0
  149. package/src/lib/error-message.ts +119 -0
  150. package/src/lib/form-errors.ts +10 -0
  151. package/src/lib/internal-redirect.ts +25 -0
  152. package/src/lib/manual-deploy-options-query.ts +21 -0
  153. package/src/lib/notification-preferences.ts +43 -0
  154. package/src/lib/onboarding.ts +11 -0
  155. package/src/lib/operation-logs-query.ts +51 -0
  156. package/src/lib/page-head.ts +14 -0
  157. package/src/lib/project-context-query.ts +257 -0
  158. package/src/lib/project-creation-schema.ts +64 -0
  159. package/src/lib/project-env-schema.ts +23 -0
  160. package/src/lib/project-environments-schema.ts +56 -0
  161. package/src/lib/project-routing-schema.ts +94 -0
  162. package/src/lib/project-runtime-config.ts +131 -0
  163. package/src/lib/project-settings-schema.ts +75 -0
  164. package/src/lib/public-api-schema.ts +128 -0
  165. package/src/lib/public-error.ts +337 -0
  166. package/src/lib/query.ts +13 -0
  167. package/src/lib/repository-selection-query.ts +11 -0
  168. package/src/lib/resource-cache.ts +30 -0
  169. package/src/lib/runtime-id.ts +19 -0
  170. package/src/lib/search.ts +14 -0
  171. package/src/lib/server-context-query.ts +43 -0
  172. package/src/lib/server-name-schema.ts +11 -0
  173. package/src/lib/settings-conflict.ts +45 -0
  174. package/src/lib/settings-version.ts +7 -0
  175. package/src/lib/team-context-query.ts +126 -0
  176. package/src/lib/team-context-schema.ts +8 -0
  177. package/src/lib/team-forms-schema.ts +45 -0
  178. package/src/lib/team-role.ts +13 -0
  179. package/src/lib/team-settings-schema.ts +48 -0
  180. package/src/lib/urls.ts +3 -0
  181. package/src/lib/use-avatar-input.ts +61 -0
  182. package/src/lib/use-refresh-error.ts +18 -0
  183. package/src/lib/use-settings-version.ts +23 -0
  184. package/src/lib/utils.ts +6 -0
  185. package/src/lib/validation.ts +5 -0
  186. package/src/lib/workspace-updates.ts +53 -0
  187. package/src/package/config.ts +10 -0
  188. package/src/package/env.mjs +54 -0
  189. package/src/package/migrate-api.d.ts +5 -0
  190. package/src/package/migrate.ts +56 -0
  191. package/src/package/plugins.ts +141 -0
  192. package/src/package/sqlite.mjs +2 -0
  193. package/src/package/vite-api.d.ts +5 -0
  194. package/src/package/vite.ts +324 -0
  195. package/src/package/watch-guard.cjs +26 -0
  196. package/src/plugins/client.ts +57 -0
  197. package/src/plugins/contract.ts +188 -0
  198. package/src/plugins/default.server.ts +4 -0
  199. package/src/plugins/default.ui.ts +4 -0
  200. package/src/plugins/operations.ts +45 -0
  201. package/src/plugins/server.ts +197 -0
  202. package/src/plugins/version.ts +1 -0
  203. package/src/routeTree.gen.ts +1275 -0
  204. package/src/router.tsx +46 -0
  205. package/src/routes/$teamSlug.index.tsx +53 -0
  206. package/src/routes/$teamSlug.new-project.tsx +13 -0
  207. package/src/routes/$teamSlug.projects.$projectName.backups.$backupId.tsx +40 -0
  208. package/src/routes/$teamSlug.projects.$projectName.backups.tsx +82 -0
  209. package/src/routes/$teamSlug.projects.$projectName.deployments.$deploymentId.tsx +52 -0
  210. package/src/routes/$teamSlug.projects.$projectName.deployments.tsx +86 -0
  211. package/src/routes/$teamSlug.projects.$projectName.logs.tsx +55 -0
  212. package/src/routes/$teamSlug.projects.$projectName.settings.tsx +51 -0
  213. package/src/routes/$teamSlug.projects.$projectName.tsx +106 -0
  214. package/src/routes/$teamSlug.projects.tsx +40 -0
  215. package/src/routes/$teamSlug.servers.$serverId.tsx +31 -0
  216. package/src/routes/$teamSlug.servers.index.tsx +59 -0
  217. package/src/routes/$teamSlug.servers.tsx +7 -0
  218. package/src/routes/$teamSlug.settings.tsx +56 -0
  219. package/src/routes/$teamSlug.tsx +86 -0
  220. package/src/routes/[.]well-known.oauth-authorization-server.api.auth.ts +13 -0
  221. package/src/routes/[.]well-known.oauth-protected-resource.mcp.ts +6 -0
  222. package/src/routes/__root.tsx +155 -0
  223. package/src/routes/admin.tsx +55 -0
  224. package/src/routes/api/agents/certificates.ts +21 -0
  225. package/src/routes/api/agents/credentials/rotate.ts +22 -0
  226. package/src/routes/api/agents/heartbeat.ts +42 -0
  227. package/src/routes/api/agents/job-logs.ts +22 -0
  228. package/src/routes/api/agents/jobs/complete.ts +22 -0
  229. package/src/routes/api/agents/jobs/next.ts +29 -0
  230. package/src/routes/api/agents/jobs/progress.ts +32 -0
  231. package/src/routes/api/agents/jobs/status.ts +22 -0
  232. package/src/routes/api/agents/register.ts +45 -0
  233. package/src/routes/api/agents/runtime-logs.ts +22 -0
  234. package/src/routes/api/agents/switch.ts +25 -0
  235. package/src/routes/api/auth/$.ts +38 -0
  236. package/src/routes/api/cron/backups.ts +33 -0
  237. package/src/routes/api/cron/maintenance.ts +30 -0
  238. package/src/routes/api/github/install.callback.ts +51 -0
  239. package/src/routes/api/github/install.ts +41 -0
  240. package/src/routes/api/github/webhook.ts +11 -0
  241. package/src/routes/api/health.ts +19 -0
  242. package/src/routes/api/install/agent.$token.ts +50 -0
  243. package/src/routes/api/install/agent.binary.ts +83 -0
  244. package/src/routes/api/install/agent.checksum.ts +45 -0
  245. package/src/routes/api/install/agent.ts +26 -0
  246. package/src/routes/api/plugins/$.ts +11 -0
  247. package/src/routes/api/v1/$.ts +15 -0
  248. package/src/routes/api/workspaces/updates.ts +8 -0
  249. package/src/routes/auth.login.tsx +50 -0
  250. package/src/routes/deployments.$deploymentId.unavailable.tsx +21 -0
  251. package/src/routes/index.tsx +23 -0
  252. package/src/routes/invites.$token.tsx +35 -0
  253. package/src/routes/mcp.ts +13 -0
  254. package/src/routes/oauth.consent.tsx +54 -0
  255. package/src/routes/onboarding.tsx +37 -0
  256. package/src/routes/onboarding.workspace.tsx +44 -0
  257. package/src/routes/settings.tsx +30 -0
  258. package/src/routes/uploads/avatars/$fileName.ts +44 -0
  259. package/src/server/admin/access.ts +67 -0
  260. package/src/server/admin/functions.ts +122 -0
  261. package/src/server/admin/service.ts +260 -0
  262. package/src/server/agents/backup-state.ts +210 -0
  263. package/src/server/agents/certificates.ts +96 -0
  264. package/src/server/agents/claims.ts +116 -0
  265. package/src/server/agents/cleanup.ts +52 -0
  266. package/src/server/agents/compatibility.ts +116 -0
  267. package/src/server/agents/completion.ts +83 -0
  268. package/src/server/agents/container-cleanup-state.ts +51 -0
  269. package/src/server/agents/deployment-state.ts +111 -0
  270. package/src/server/agents/job-payloads.ts +159 -0
  271. package/src/server/agents/jobs.ts +1293 -0
  272. package/src/server/agents/protocol.ts +400 -0
  273. package/src/server/agents/reconciliation.ts +43 -0
  274. package/src/server/agents/runtime-logs.ts +337 -0
  275. package/src/server/agents/runtime-state.ts +125 -0
  276. package/src/server/api/admission.ts +12 -0
  277. package/src/server/api/authorization.ts +115 -0
  278. package/src/server/api/credentials.ts +569 -0
  279. package/src/server/api/cursors.ts +72 -0
  280. package/src/server/api/errors.ts +24 -0
  281. package/src/server/api/functions.ts +60 -0
  282. package/src/server/api/handler.ts +274 -0
  283. package/src/server/api/mutation-operations.ts +913 -0
  284. package/src/server/api/operation-context.ts +139 -0
  285. package/src/server/api/operation-contract.ts +1099 -0
  286. package/src/server/api/operations.ts +64 -0
  287. package/src/server/api/read-operations.ts +891 -0
  288. package/src/server/api/retention.ts +33 -0
  289. package/src/server/auth/access.ts +109 -0
  290. package/src/server/auth/auth-methods.ts +28 -0
  291. package/src/server/auth/better-auth.ts +518 -0
  292. package/src/server/auth/context.ts +28 -0
  293. package/src/server/auth/email-change.ts +47 -0
  294. package/src/server/auth/email-delivery.ts +51 -0
  295. package/src/server/auth/provider-identity.ts +19 -0
  296. package/src/server/auth/provider-tokens.ts +27 -0
  297. package/src/server/auth/session.server.ts +19 -0
  298. package/src/server/backups/database-commands.ts +40 -0
  299. package/src/server/backups/destination.ts +12 -0
  300. package/src/server/backups/feature.ts +6 -0
  301. package/src/server/backups/policy-validation.ts +51 -0
  302. package/src/server/backups/queue-state.ts +137 -0
  303. package/src/server/backups/service.ts +918 -0
  304. package/src/server/backups/storage-functions.ts +42 -0
  305. package/src/server/backups/storage-state.ts +67 -0
  306. package/src/server/backups/storage.ts +237 -0
  307. package/src/server/config.ts +274 -0
  308. package/src/server/db/atomic.ts +16 -0
  309. package/src/server/db/d1-connection.ts +83 -0
  310. package/src/server/db/errors.ts +17 -0
  311. package/src/server/db/index.cloudflare.ts +15 -0
  312. package/src/server/db/index.ts +35 -0
  313. package/src/server/db/libsql-connection.ts +72 -0
  314. package/src/server/db/log-schema.ts +102 -0
  315. package/src/server/db/migrate-libsql.ts +29 -0
  316. package/src/server/db/migrate-sqlite.ts +47 -0
  317. package/src/server/db/migration-history.ts +12 -0
  318. package/src/server/db/migrations-logs/0000_initial.sql +48 -0
  319. package/src/server/db/migrations-logs/0001_runtime_log_order.sql +2 -0
  320. package/src/server/db/migrations-logs/meta/0000_snapshot.json +327 -0
  321. package/src/server/db/migrations-logs/meta/0001_snapshot.json +329 -0
  322. package/src/server/db/migrations-logs/meta/_journal.json +20 -0
  323. package/src/server/db/migrations-sqlite/0000_initial.sql +894 -0
  324. package/src/server/db/migrations-sqlite/0001_agent_indexes.sql +5 -0
  325. package/src/server/db/migrations-sqlite/meta/0000_snapshot.json +5816 -0
  326. package/src/server/db/migrations-sqlite/meta/0001_snapshot.json +5841 -0
  327. package/src/server/db/migrations-sqlite/meta/_journal.json +20 -0
  328. package/src/server/db/oauth-schema.ts +216 -0
  329. package/src/server/db/remote-config.ts +47 -0
  330. package/src/server/db/schema.ts +1218 -0
  331. package/src/server/db/sqlite-connection.ts +94 -0
  332. package/src/server/db/sqlite-files.ts +27 -0
  333. package/src/server/db/sqlite-params.ts +19 -0
  334. package/src/server/db/worker-connection.ts +47 -0
  335. package/src/server/deleted-resources.ts +133 -0
  336. package/src/server/deletion-recovery.ts +272 -0
  337. package/src/server/email/layout.tsx +85 -0
  338. package/src/server/email/notification-template.tsx +62 -0
  339. package/src/server/email/otp-template.tsx +48 -0
  340. package/src/server/email/service.ts +251 -0
  341. package/src/server/email/team-invite-template.tsx +59 -0
  342. package/src/server/email/theme.ts +10 -0
  343. package/src/server/github/http.ts +14 -0
  344. package/src/server/github/install-state.ts +48 -0
  345. package/src/server/github/service.ts +831 -0
  346. package/src/server/github/webhook-handler.ts +198 -0
  347. package/src/server/github/webhooks.ts +205 -0
  348. package/src/server/http/auth.ts +11 -0
  349. package/src/server/http/error-diagnostics.ts +66 -0
  350. package/src/server/http/plugin-request.ts +15 -0
  351. package/src/server/http/proxy-request.ts +53 -0
  352. package/src/server/http/public-errors.ts +48 -0
  353. package/src/server/http/rate-limit.ts +59 -0
  354. package/src/server/http/request-body.ts +54 -0
  355. package/src/server/http/request-ip.ts +54 -0
  356. package/src/server/http/security-headers.ts +36 -0
  357. package/src/server/install/agent-binary.ts +230 -0
  358. package/src/server/install/agent-script.ts +565 -0
  359. package/src/server/install/release-binary.ts +65 -0
  360. package/src/server/logs/budget.ts +72 -0
  361. package/src/server/logs/cleanup.ts +64 -0
  362. package/src/server/logs/deployments.ts +98 -0
  363. package/src/server/logs/operations.ts +145 -0
  364. package/src/server/logs/retention.ts +39 -0
  365. package/src/server/maintenance.ts +61 -0
  366. package/src/server/mcp/connections.ts +358 -0
  367. package/src/server/mcp/functions.ts +63 -0
  368. package/src/server/mcp/handler.ts +295 -0
  369. package/src/server/mcp/oauth-discovery.cloudflare.ts +6 -0
  370. package/src/server/mcp/oauth-discovery.ts +8 -0
  371. package/src/server/mcp/oauth-provider.ts +220 -0
  372. package/src/server/mcp/urls.ts +20 -0
  373. package/src/server/notifications/functions.ts +51 -0
  374. package/src/server/notifications/observe.ts +303 -0
  375. package/src/server/notifications/outbox.ts +335 -0
  376. package/src/server/notifications/preferences.ts +131 -0
  377. package/src/server/notifications/query.ts +16 -0
  378. package/src/server/plugins/scope.ts +17 -0
  379. package/src/server/plugins/server-context.ts +157 -0
  380. package/src/server/plugins/services.server.ts +10 -0
  381. package/src/server/plugins/workspace-access.ts +74 -0
  382. package/src/server/projects/backup-policy-state.ts +68 -0
  383. package/src/server/projects/backups.ts +219 -0
  384. package/src/server/projects/compose-metadata.ts +174 -0
  385. package/src/server/projects/configuration-state.ts +90 -0
  386. package/src/server/projects/context.ts +909 -0
  387. package/src/server/projects/creation-state.ts +46 -0
  388. package/src/server/projects/deletion-state.ts +69 -0
  389. package/src/server/projects/deployment-config.ts +139 -0
  390. package/src/server/projects/deployment-logs.ts +97 -0
  391. package/src/server/projects/deployments.ts +1123 -0
  392. package/src/server/projects/domain-state.ts +71 -0
  393. package/src/server/projects/environment-branches.ts +75 -0
  394. package/src/server/projects/environment-state.ts +158 -0
  395. package/src/server/projects/environments.ts +424 -0
  396. package/src/server/projects/functions.ts +748 -0
  397. package/src/server/projects/generated-hostname.ts +77 -0
  398. package/src/server/projects/hostnames.ts +244 -0
  399. package/src/server/projects/mappers.ts +457 -0
  400. package/src/server/projects/names.ts +41 -0
  401. package/src/server/projects/project-env-vars.ts +28 -0
  402. package/src/server/projects/queue-state.ts +118 -0
  403. package/src/server/projects/repository.ts +368 -0
  404. package/src/server/projects/route-state.ts +37 -0
  405. package/src/server/projects/runtime-logs.ts +97 -0
  406. package/src/server/projects/settings-context.ts +58 -0
  407. package/src/server/projects/settings.ts +738 -0
  408. package/src/server/projects/types.ts +355 -0
  409. package/src/server/realtime/agent-events.ts +32 -0
  410. package/src/server/realtime/node-broadcaster.ts +71 -0
  411. package/src/server/realtime/node-handler.ts +55 -0
  412. package/src/server/realtime/service.ts +125 -0
  413. package/src/server/realtime/workspace-object.cloudflare.ts +62 -0
  414. package/src/server/realtime/workspace-object.ts +3 -0
  415. package/src/server/resources/color.ts +13 -0
  416. package/src/server/runtime/background.ts +21 -0
  417. package/src/server/runtime/node-asset-cache-plugin.ts +12 -0
  418. package/src/server/runtime/node-scheduler-plugin.ts +17 -0
  419. package/src/server/runtime/node-scheduler.ts +51 -0
  420. package/src/server/runtime/platform-env.cloudflare.ts +17 -0
  421. package/src/server/runtime/platform-env.ts +19 -0
  422. package/src/server/runtime/scheduled-task-error.ts +9 -0
  423. package/src/server/scheduled-tasks.ts +28 -0
  424. package/src/server/secrets/crypto.ts +56 -0
  425. package/src/server/servers/functions.ts +233 -0
  426. package/src/server/servers/lifecycle.ts +122 -0
  427. package/src/server/servers/metric-history-schema.ts +24 -0
  428. package/src/server/servers/metric-history.ts +55 -0
  429. package/src/server/servers/monitoring.ts +592 -0
  430. package/src/server/servers/service.ts +1211 -0
  431. package/src/server/servers/status.ts +32 -0
  432. package/src/server/servers/switch.ts +96 -0
  433. package/src/server/settings-version.ts +43 -0
  434. package/src/server/storage/capabilities.ts +6 -0
  435. package/src/server/storage/local-object-store.cloudflare.ts +7 -0
  436. package/src/server/storage/local-object-store.ts +50 -0
  437. package/src/server/storage/object-store.ts +160 -0
  438. package/src/server/teams/creation.ts +51 -0
  439. package/src/server/teams/delete-policy.ts +13 -0
  440. package/src/server/teams/deletion-state.ts +80 -0
  441. package/src/server/teams/functions.ts +324 -0
  442. package/src/server/teams/invitations.ts +38 -0
  443. package/src/server/teams/membership.ts +86 -0
  444. package/src/server/teams/queries.ts +261 -0
  445. package/src/server/teams/service.ts +1127 -0
  446. package/src/server/teams/types.ts +101 -0
  447. package/src/server/uploads/avatar.ts +133 -0
  448. package/src/server/users/deletion-state.ts +93 -0
  449. package/src/server/users/functions.ts +160 -0
  450. package/src/server/users/onboarding.ts +17 -0
  451. package/src/server/users/service.ts +644 -0
  452. package/src/server.ts +55 -0
  453. package/src/styles/shadcn.css +653 -0
  454. package/src/styles/theme.css +76 -0
  455. package/src/styles.css +177 -0
  456. package/wrangler.template.jsonc +37 -0
@@ -0,0 +1,78 @@
1
+ import { settingsVersionSchema } from '#devpush/lib/settings-version'
2
+ import { z } from 'zod'
3
+ import { avatarDataUrlSchema } from '#devpush/lib/avatar-schema'
4
+
5
+ export const accountSettingsSchema = z.object({
6
+ settingsVersion: settingsVersionSchema,
7
+ name: z
8
+ .string()
9
+ .trim()
10
+ .min(1, 'Enter your name.')
11
+ .max(256, 'Name must be 256 characters or fewer.'),
12
+ username: z
13
+ .string()
14
+ .trim()
15
+ .min(1, 'Enter a username.')
16
+ .max(50, 'Username must be 50 characters or fewer.')
17
+ .regex(
18
+ /^[A-Za-z0-9][A-Za-z0-9._-]*[A-Za-z0-9]$/,
19
+ 'Username must start and end with a letter or number and only use letters, numbers, dots, underscores, or hyphens.',
20
+ ),
21
+ avatarDataUrl: avatarDataUrlSchema,
22
+ deleteAvatar: z.boolean().optional(),
23
+ })
24
+ export type AccountSettingsErrors = Partial<
25
+ Record<'name' | 'username' | 'avatarDataUrl' | 'form', string>
26
+ >
27
+
28
+ export function accountValidationErrors(
29
+ issues: Array<{ path: PropertyKey[]; message: string }>,
30
+ ): AccountSettingsErrors {
31
+ const errors: AccountSettingsErrors = {}
32
+ for (const issue of issues) {
33
+ const field = issue.path[0]
34
+ const key =
35
+ field === 'name' || field === 'username' || field === 'avatarDataUrl'
36
+ ? field
37
+ : 'form'
38
+ errors[key] ??= issue.message
39
+ }
40
+ return errors
41
+ }
42
+
43
+ export function accountServiceErrors(
44
+ error: unknown,
45
+ ): AccountSettingsErrors | null {
46
+ if (!(error instanceof Error)) return null
47
+ if (error.message === 'Name is required.') return { name: 'Enter your name.' }
48
+ if (
49
+ [
50
+ 'Username is already taken.',
51
+ 'Username is required.',
52
+ 'Username must be 50 characters or fewer.',
53
+ 'Username must start and end with a letter or number and only use letters, numbers, dots, underscores hyphens.',
54
+ ].includes(error.message)
55
+ )
56
+ return { username: error.message }
57
+ if (
58
+ [
59
+ 'Upload a PNG, JPEG, or WebP image.',
60
+ 'Avatars must be 2 MB or smaller.',
61
+ 'The uploaded file does not match its image type.',
62
+ ].includes(error.message)
63
+ ) {
64
+ return { avatarDataUrl: error.message }
65
+ }
66
+ return null
67
+ }
68
+
69
+ export const accountEmailSchema = z.object({
70
+ settingsVersion: settingsVersionSchema,
71
+ newEmail: z
72
+ .string()
73
+ .trim()
74
+ .min(1, 'Enter an email address.')
75
+ .email('Enter a valid email address.')
76
+ .max(320, 'Email addresses must be 320 characters or fewer.')
77
+ .transform((email) => email.toLowerCase()),
78
+ })
@@ -0,0 +1,81 @@
1
+ import { getPolicyRecoveryUrl } from './public-error'
2
+ import { toast } from 'sonner'
3
+
4
+ export async function copyText(text: string, label: string) {
5
+ try {
6
+ await navigator.clipboard.writeText(text)
7
+ toast.success(`${label} copied.`)
8
+ return true
9
+ } catch {
10
+ toast.error(
11
+ `Unable to copy ${label.toLowerCase()}. Please select and copy it manually.`,
12
+ )
13
+ return false
14
+ }
15
+ }
16
+
17
+ // A committed mutation must not be reported as failed when its follow-up fails:
18
+ // retrying a create/queue operation could otherwise produce duplicates.
19
+ export async function afterMutation(
20
+ task: () => Promise<unknown>,
21
+ message: string,
22
+ ) {
23
+ try {
24
+ await task()
25
+ return true
26
+ } catch {
27
+ toast.warning(message)
28
+ return false
29
+ }
30
+ }
31
+
32
+ export async function refreshView(task: () => Promise<unknown>, id: string) {
33
+ try {
34
+ await task()
35
+ toast.dismiss(id)
36
+ } catch {
37
+ toast.error(
38
+ 'Unable to refresh. Displayed information may be out of date. Please reload.',
39
+ { id },
40
+ )
41
+ }
42
+ }
43
+
44
+ export async function navigateFilters(task: () => Promise<unknown>) {
45
+ try {
46
+ await task()
47
+ toast.dismiss('filter-navigation')
48
+ } catch {
49
+ toast.error('Unable to apply filters. Please try again.', {
50
+ id: 'filter-navigation',
51
+ })
52
+ }
53
+ }
54
+
55
+ export function mutationErrorOptions(
56
+ error: unknown,
57
+ ):
58
+ | []
59
+ | [
60
+ {
61
+ duration: number
62
+ closeButton: boolean
63
+ action: { label: string; onClick: () => void }
64
+ },
65
+ ] {
66
+ const url = getPolicyRecoveryUrl(error)
67
+ return url
68
+ ? [
69
+ {
70
+ duration: Infinity,
71
+ closeButton: true,
72
+ action: {
73
+ label: 'Review settings',
74
+ onClick: () => {
75
+ window.location.assign(url)
76
+ },
77
+ },
78
+ },
79
+ ]
80
+ : []
81
+ }
@@ -0,0 +1,8 @@
1
+ import { z } from 'zod'
2
+
3
+ export const adminSearchSchema = z.object({
4
+ userQuery: z.string().trim().max(255).catch(''),
5
+ userPage: z.coerce.number().int().min(1).max(1_000_000).catch(1),
6
+ allowlistQuery: z.string().trim().max(255).catch(''),
7
+ allowlistPage: z.coerce.number().int().min(1).max(1_000_000).catch(1),
8
+ })
@@ -0,0 +1,21 @@
1
+ export type AgentReleaseAsset = Readonly<{
2
+ os: 'linux'
3
+ arch: 'amd64' | 'arm64'
4
+ filename: string
5
+ url: string
6
+ sha256: string
7
+ size: number
8
+ }>
9
+ export type AgentRelease = Readonly<{
10
+ schemaVersion: 1
11
+ repository: string
12
+ version: string
13
+ goVersion: string
14
+ protocolVersion: number
15
+ capabilities: readonly string[]
16
+ assets: readonly AgentReleaseAsset[]
17
+ }>
18
+ export const agentReleasePlatforms: readonly Readonly<
19
+ Pick<AgentReleaseAsset, 'os' | 'arch' | 'filename'>
20
+ >[]
21
+ export function validateAgentRelease(value: unknown): AgentRelease
@@ -0,0 +1,90 @@
1
+ export const agentReleasePlatforms = Object.freeze([
2
+ Object.freeze({
3
+ os: 'linux',
4
+ arch: 'amd64',
5
+ filename: 'devpush-agent-linux-amd64',
6
+ }),
7
+ Object.freeze({
8
+ os: 'linux',
9
+ arch: 'arm64',
10
+ filename: 'devpush-agent-linux-arm64',
11
+ }),
12
+ ])
13
+ const maxAgentBinaryBytes = 32 * 1024 * 1024
14
+
15
+ export function validateAgentRelease(value) {
16
+ const fail = () => {
17
+ throw new Error('Invalid agent release manifest.')
18
+ }
19
+ if (!value || typeof value !== 'object' || value.schemaVersion !== 1) fail()
20
+ if (
21
+ typeof value.version !== 'string' ||
22
+ !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(value.version)
23
+ )
24
+ fail()
25
+ if (
26
+ typeof value.repository !== 'string' ||
27
+ !/^[A-Za-z0-9][A-Za-z0-9_.-]*\/[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(
28
+ value.repository,
29
+ )
30
+ )
31
+ fail()
32
+ if (
33
+ typeof value.goVersion !== 'string' ||
34
+ !/^go\d+\.\d+\.\d+$/.test(value.goVersion)
35
+ )
36
+ fail()
37
+ if (!Number.isInteger(value.protocolVersion) || value.protocolVersion < 1)
38
+ fail()
39
+ if (
40
+ !Array.isArray(value.capabilities) ||
41
+ !value.capabilities.length ||
42
+ value.capabilities.some(
43
+ (item) => typeof item !== 'string' || !/^[a-z][a-z0-9.-]+$/.test(item),
44
+ ) ||
45
+ new Set(value.capabilities).size !== value.capabilities.length
46
+ )
47
+ fail()
48
+ if (
49
+ !Array.isArray(value.assets) ||
50
+ value.assets.length !== agentReleasePlatforms.length
51
+ )
52
+ fail()
53
+ const assets = agentReleasePlatforms.map((platform) => {
54
+ const matches = value.assets.filter(
55
+ (asset) =>
56
+ asset && asset.os === platform.os && asset.arch === platform.arch,
57
+ )
58
+ if (matches.length !== 1) fail()
59
+ const asset = matches[0]
60
+ if (
61
+ asset.filename !== platform.filename ||
62
+ typeof asset.sha256 !== 'string' ||
63
+ !/^[a-f0-9]{64}$/.test(asset.sha256)
64
+ )
65
+ fail()
66
+ if (
67
+ !Number.isSafeInteger(asset.size) ||
68
+ asset.size < 1 ||
69
+ asset.size > maxAgentBinaryBytes
70
+ )
71
+ fail()
72
+ const url = `https://github.com/${value.repository}/releases/download/v${value.version}/${platform.filename}`
73
+ if (asset.url !== url) fail()
74
+ return Object.freeze({
75
+ ...platform,
76
+ url,
77
+ sha256: asset.sha256,
78
+ size: asset.size,
79
+ })
80
+ })
81
+ return Object.freeze({
82
+ schemaVersion: 1,
83
+ repository: value.repository,
84
+ version: value.version,
85
+ goVersion: value.goVersion,
86
+ protocolVersion: value.protocolVersion,
87
+ capabilities: Object.freeze([...value.capabilities]),
88
+ assets: Object.freeze(assets),
89
+ })
90
+ }
@@ -0,0 +1,79 @@
1
+ import { z } from 'zod'
2
+ import safeRegex from 'safe-regex2'
3
+
4
+ const emailSchema = z.string().email().max(320)
5
+ export function isValidAllowlistPattern(value: string) {
6
+ return value.length <= 255 && safeRegex(value)
7
+ }
8
+ export const allowlistRuleSchema = z
9
+ .object({
10
+ type: z.enum(['email', 'domain', 'pattern']),
11
+ value: z
12
+ .string()
13
+ .trim()
14
+ .min(1, 'Enter an allowlist rule.')
15
+ .max(255, 'Rules must be 255 characters or fewer.'),
16
+ })
17
+ .superRefine(({ type, value }, ctx) => {
18
+ if (!value) return
19
+ const valid =
20
+ type === 'email'
21
+ ? emailSchema.safeParse(value).success
22
+ : type === 'domain'
23
+ ? emailSchema.safeParse(`member@${value.replace(/^@/, '')}`).success
24
+ : isValidAllowlistPattern(value)
25
+ if (!valid)
26
+ ctx.addIssue({
27
+ code: 'custom',
28
+ path: ['value'],
29
+ message:
30
+ type === 'email'
31
+ ? 'Enter a valid email address.'
32
+ : type === 'domain'
33
+ ? 'Enter a domain such as example.com, without a URL or path.'
34
+ : 'Enter a valid regular expression without excessive repetition.',
35
+ })
36
+ })
37
+ .transform(({ type, value }) => ({
38
+ type,
39
+ value:
40
+ type === 'pattern'
41
+ ? value
42
+ : type === 'domain'
43
+ ? value.toLowerCase().replace(/^@/, '')
44
+ : value.toLowerCase(),
45
+ }))
46
+
47
+ export function parseAllowlistEmails(value: string) {
48
+ return [
49
+ ...new Set(
50
+ value
51
+ .split(/[\s,]+/)
52
+ .map((email) => email.trim().toLowerCase())
53
+ .filter(Boolean),
54
+ ),
55
+ ]
56
+ }
57
+ export const allowlistImportSchema = z.object({
58
+ emails: z
59
+ .string()
60
+ .trim()
61
+ .min(1, 'Enter at least one email address.')
62
+ .max(20_000, 'Import at most 20,000 characters at a time.')
63
+ .superRefine((value, ctx) => {
64
+ const emails = parseAllowlistEmails(value)
65
+ if (!emails.length)
66
+ ctx.addIssue({
67
+ code: 'custom',
68
+ message: 'Enter at least one email address.',
69
+ })
70
+ const invalid = emails.find(
71
+ (email) => !emailSchema.safeParse(email).success,
72
+ )
73
+ if (invalid)
74
+ ctx.addIssue({
75
+ code: 'custom',
76
+ message: `Invalid email address: ${invalid}.`,
77
+ })
78
+ }),
79
+ })
@@ -0,0 +1,176 @@
1
+ import { z } from 'zod'
2
+ import type { TeamRole } from '#devpush/lib/team-role'
3
+ import type { ResourceColor } from '#devpush/server/resources/color'
4
+
5
+ export const apiScopeDefinitions = {
6
+ 'projects:read': { minimumRole: 'member', label: 'Read projects' },
7
+ 'servers:read': { minimumRole: 'member', label: 'Read servers' },
8
+ 'deployments:read': { minimumRole: 'member', label: 'Read deployments' },
9
+ 'logs:read': {
10
+ minimumRole: 'member',
11
+ label: 'Read deployment and operation logs',
12
+ },
13
+ 'backups:read': { minimumRole: 'member', label: 'Read backups' },
14
+ 'deployments:write': {
15
+ minimumRole: 'member',
16
+ label: 'Deploy, redeploy and roll back',
17
+ },
18
+ 'projects:write': {
19
+ minimumRole: 'member',
20
+ label: 'Manage project configuration',
21
+ },
22
+ 'secrets:write': {
23
+ minimumRole: 'member',
24
+ label: 'Write environment variables',
25
+ },
26
+ 'backups:write': {
27
+ minimumRole: 'admin',
28
+ label: 'Run and configure backups',
29
+ },
30
+ 'servers:write': { minimumRole: 'admin', label: 'Manage servers' },
31
+ 'backups:restore': { minimumRole: 'admin', label: 'Restore backups' },
32
+ 'projects:host-commands': {
33
+ minimumRole: 'admin',
34
+ label: 'Manage host lifecycle commands',
35
+ },
36
+ } as const satisfies Record<string, { minimumRole: TeamRole; label: string }>
37
+
38
+ export type ApiScope = keyof typeof apiScopeDefinitions
39
+ export const apiKeyPresetSchema = z.enum(['read', 'deploy', 'manage'])
40
+ export type ApiKeyPreset = z.infer<typeof apiKeyPresetSchema>
41
+
42
+ export const apiKeyExpirationOptions = [
43
+ { value: '1', label: '1 day', days: 1 },
44
+ { value: '7', label: '7 days', days: 7 },
45
+ { value: '30', label: '30 days', days: 30 },
46
+ { value: '90', label: '90 days', days: 90 },
47
+ { value: '180', label: '180 days', days: 180 },
48
+ { value: '365', label: '1 year', days: 365 },
49
+ { value: 'never', label: 'Never expires', days: null },
50
+ ] as const
51
+ export const apiKeyMaxExpirationDays = Math.max(
52
+ ...apiKeyExpirationOptions.map(({ days }) => days ?? 0),
53
+ )
54
+
55
+ const readScopes = Object.keys(apiScopeDefinitions).filter(
56
+ (scope) => scope.endsWith(':read') && scope !== 'logs:read',
57
+ ) as ApiScope[]
58
+ export const apiKeyPresets: Record<
59
+ ApiKeyPreset,
60
+ { label: string; description: string; scopes: ApiScope[] }
61
+ > = {
62
+ read: {
63
+ label: 'Read only',
64
+ description: 'Read projects, servers, deployments and backups.',
65
+ scopes: readScopes,
66
+ },
67
+ deploy: {
68
+ label: 'Deploy',
69
+ description: 'Read resources and deploy, redeploy or roll back projects.',
70
+ scopes: [...readScopes, 'deployments:write'],
71
+ },
72
+ manage: {
73
+ label: 'Manage',
74
+ description: 'Manage resources within your current team permissions.',
75
+ scopes: Object.keys(apiScopeDefinitions).filter(
76
+ (scope) => scope !== 'logs:read',
77
+ ) as ApiScope[],
78
+ },
79
+ }
80
+
81
+ export const apiRoleRank: Record<TeamRole, number> = {
82
+ member: 0,
83
+ admin: 1,
84
+ owner: 2,
85
+ }
86
+
87
+ export function scopesForApiPreset(
88
+ preset: ApiKeyPreset,
89
+ role: TeamRole,
90
+ includeLogs = false,
91
+ ): ApiScope[] {
92
+ return [
93
+ ...apiKeyPresets[preset].scopes,
94
+ ...(includeLogs ? ['logs:read' as const] : []),
95
+ ].filter(
96
+ (scope) =>
97
+ apiRoleRank[role] >= apiRoleRank[apiScopeDefinitions[scope].minimumRole],
98
+ )
99
+ }
100
+
101
+ export const createApiKeySchema = z
102
+ .object({
103
+ name: z.string().trim().min(1).max(80),
104
+ teamId: z.string().min(1).max(128),
105
+ projectIds: z
106
+ .array(z.string().min(1).max(128))
107
+ .min(1)
108
+ .max(100)
109
+ .nullable()
110
+ .default(null),
111
+ preset: apiKeyPresetSchema.default('read'),
112
+ includeLogs: z.boolean().default(false),
113
+ expiresInDays: z
114
+ .number()
115
+ .int()
116
+ .min(1)
117
+ .max(apiKeyMaxExpirationDays)
118
+ .nullable()
119
+ .default(30),
120
+ })
121
+ .strict()
122
+ .refine(
123
+ (input) =>
124
+ input.projectIds === null ||
125
+ new Set(input.projectIds).size === input.projectIds.length,
126
+ { path: ['projectIds'], message: 'Select each project only once.' },
127
+ )
128
+
129
+ export const apiKeyIdSchema = z
130
+ .object({ id: z.string().min(1).max(128) })
131
+ .strict()
132
+
133
+ export const apiKeySettingsSchema = z
134
+ .object({ page: z.number().int().min(1).max(1_000_000).default(1) })
135
+ .strict()
136
+
137
+ export const apiKeyTeamsSchema = apiKeySettingsSchema.extend({
138
+ query: z.string().trim().max(100).default(''),
139
+ })
140
+ export const apiKeyProjectsSchema = apiKeyTeamsSchema.extend({
141
+ teamId: z.string().min(1).max(128),
142
+ })
143
+
144
+ export type ApiKeySummary = {
145
+ id: string
146
+ name: string
147
+ prefix: string
148
+ teamId: string
149
+ teamName: string
150
+ projectIds: string[] | null
151
+ preset: ApiKeyPreset
152
+ scopes: ApiScope[]
153
+ status: 'active' | 'expired' | 'revoked' | 'unavailable'
154
+ createdAt: string
155
+ expiresAt: string | null
156
+ lastUsedAt: string | null
157
+ revokedAt: string | null
158
+ }
159
+
160
+ export type ApiKeyTeamOption = {
161
+ id: string
162
+ name: string
163
+ slug: string
164
+ role: TeamRole
165
+ image: string | null
166
+ color: ResourceColor
167
+ projects: ApiKeyProjectOption[]
168
+ }
169
+
170
+ export type ApiKeyProjectOption = {
171
+ id: string
172
+ name: string
173
+ slug: string
174
+ avatarUrl: string | null
175
+ color: ResourceColor
176
+ }
@@ -0,0 +1,20 @@
1
+ import { createAuthClient } from 'better-auth/react'
2
+ import { emailOTPClient } from 'better-auth/client/plugins'
3
+
4
+ function getAuthBaseURL() {
5
+ if (typeof window !== 'undefined') {
6
+ return new URL('/api/auth', window.location.origin).toString()
7
+ }
8
+
9
+ return new URL(
10
+ '/api/auth',
11
+ process.env.BETTER_AUTH_URL?.trim() ||
12
+ process.env.DEVPUSH_BASE_URL?.trim() ||
13
+ 'http://localhost:7429',
14
+ ).toString()
15
+ }
16
+
17
+ export const authClient = createAuthClient({
18
+ baseURL: getAuthBaseURL(),
19
+ plugins: [emailOTPClient()],
20
+ })
@@ -0,0 +1,11 @@
1
+ import { z } from 'zod'
2
+
3
+ export const avatarDataUrlSchema = z
4
+ .string()
5
+ .max(3_000_000)
6
+ .regex(
7
+ /^data:image\/(?:png|jpeg|webp);base64,/,
8
+ 'Upload a PNG, JPEG, or WebP image.',
9
+ )
10
+ .nullable()
11
+ .optional()
@@ -0,0 +1,118 @@
1
+ export function isCronDue(cron: string, now: Date) {
2
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = cron.trim().split(/\s+/)
3
+
4
+ if (
5
+ minute === undefined ||
6
+ hour === undefined ||
7
+ dayOfMonth === undefined ||
8
+ month === undefined ||
9
+ dayOfWeek === undefined
10
+ )
11
+ return false
12
+
13
+ const minuteMatches = matchesCronPart(minute, now.getUTCMinutes(), 0, 59)
14
+ const hourMatches = matchesCronPart(hour, now.getUTCHours(), 0, 23)
15
+ const monthMatches = matchesCronPart(month, now.getUTCMonth() + 1, 1, 12)
16
+ const dayOfMonthMatches = matchesCronPart(dayOfMonth, now.getUTCDate(), 1, 31)
17
+ const dayOfWeekMatches = matchesCronPart(dayOfWeek, now.getUTCDay(), 0, 7)
18
+ const dayMatches =
19
+ dayOfMonth === '*'
20
+ ? dayOfWeekMatches
21
+ : dayOfWeek === '*'
22
+ ? dayOfMonthMatches
23
+ : dayOfMonthMatches || dayOfWeekMatches
24
+
25
+ return minuteMatches && hourMatches && monthMatches && dayMatches
26
+ }
27
+
28
+ export function isValidCron(cron: string) {
29
+ const parts = cron.trim().split(/\s+/)
30
+ const ranges = [
31
+ [0, 59],
32
+ [0, 23],
33
+ [1, 31],
34
+ [1, 12],
35
+ [0, 7],
36
+ ] as const
37
+
38
+ return (
39
+ parts.length === 5 &&
40
+ ranges.every(([min, max], index) => {
41
+ const part = parts[index]
42
+ return part !== undefined && isValidCronPart(part, min, max)
43
+ })
44
+ )
45
+ }
46
+
47
+ function isValidCronPart(part: string, min: number, max: number) {
48
+ return part.split(',').every((token) => {
49
+ const segments = token.split('/')
50
+ const base = segments[0]
51
+ const hasStep = segments.length === 2
52
+ if (segments.length > 2 || !base) return false
53
+
54
+ if (hasStep) {
55
+ const step = Number(segments[1])
56
+ if (!Number.isInteger(step) || step < 1 || step > max - min + 1) {
57
+ return false
58
+ }
59
+ }
60
+
61
+ if (base === '*') return true
62
+
63
+ if (base.includes('-')) {
64
+ const range = base.split('-')
65
+ if (range.length !== 2) return false
66
+ const start = Number(range[0])
67
+ const end = Number(range[1])
68
+ return (
69
+ Number.isInteger(start) &&
70
+ Number.isInteger(end) &&
71
+ start >= min &&
72
+ end <= max &&
73
+ start <= end
74
+ )
75
+ }
76
+
77
+ const numeric = Number(base)
78
+ return (
79
+ !hasStep && Number.isInteger(numeric) && numeric >= min && numeric <= max
80
+ )
81
+ })
82
+ }
83
+
84
+ function matchesCronPart(
85
+ part: string,
86
+ value: number,
87
+ min: number,
88
+ max: number,
89
+ ) {
90
+ for (const token of part.split(',')) {
91
+ const [base = '', rawStep] = token.split('/')
92
+ const step = rawStep ? Number(rawStep) : 1
93
+ let start = min
94
+ let end = max
95
+
96
+ if (base !== '*') {
97
+ if (base.includes('-')) {
98
+ const [rawStart, rawEnd] = base.split('-', 2)
99
+ start = Number(rawStart)
100
+ end = Number(rawEnd)
101
+ } else {
102
+ start = Number(base)
103
+ end = start
104
+ }
105
+ }
106
+
107
+ const normalizedValue = max === 7 && value === 0 && end === 7 ? 7 : value
108
+ if (
109
+ normalizedValue >= start &&
110
+ normalizedValue <= end &&
111
+ (normalizedValue - start) % step === 0
112
+ ) {
113
+ return true
114
+ }
115
+ }
116
+
117
+ return false
118
+ }