devpush 0.0.0-stage → 1.0.0-beta.4
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.
- package/README.md +22 -2
- package/agent-release.json +37 -0
- package/dist/package/LICENSE.md +21 -0
- package/dist/package/migrate.mjs +182 -0
- package/dist/package/plugins.mjs +125 -0
- package/dist/package/vite.mjs +439 -0
- package/package.json +168 -3
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon.svg +1 -0
- package/public/logo-email.png +0 -0
- package/public/robots.txt +3 -0
- package/public/social.png +0 -0
- package/src/cloudflare-runtime.d.ts +78 -0
- package/src/components/about-dialog.tsx +70 -0
- package/src/components/copy-button.tsx +74 -0
- package/src/components/logo-mark.tsx +20 -0
- package/src/components/pending-collection.tsx +103 -0
- package/src/components/resource-avatar.tsx +65 -0
- package/src/components/route-error-state.tsx +95 -0
- package/src/components/section-navigation.tsx +39 -0
- package/src/components/settings-layout.tsx +111 -0
- package/src/components/storage-capabilities.tsx +11 -0
- package/src/components/table-pager.tsx +101 -0
- package/src/components/ui/alert-dialog.tsx +180 -0
- package/src/components/ui/alert.tsx +78 -0
- package/src/components/ui/avatar.tsx +58 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/button-group.tsx +38 -0
- package/src/components/ui/button.tsx +67 -0
- package/src/components/ui/card.tsx +82 -0
- package/src/components/ui/chart.tsx +307 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/combobox.tsx +256 -0
- package/src/components/ui/dialog.tsx +163 -0
- package/src/components/ui/dropdown-menu.tsx +110 -0
- package/src/components/ui/empty.tsx +65 -0
- package/src/components/ui/field.tsx +192 -0
- package/src/components/ui/input-group.tsx +118 -0
- package/src/components/ui/input-otp.tsx +70 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/pagination.tsx +34 -0
- package/src/components/ui/popover.tsx +86 -0
- package/src/components/ui/select.tsx +145 -0
- package/src/components/ui/skeleton.tsx +19 -0
- package/src/components/ui/sonner.tsx +47 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +81 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +55 -0
- package/src/features/admin/admin-settings.tsx +1048 -0
- package/src/features/auth/current-user-context-query.ts +11 -0
- package/src/features/auth/login-form.tsx +442 -0
- package/src/features/auth/sign-out-menu-item.tsx +42 -0
- package/src/features/deployments/deployment-actions.tsx +336 -0
- package/src/features/deployments/deployment-alias-badge.tsx +35 -0
- package/src/features/deployments/deployment-detail.tsx +596 -0
- package/src/features/deployments/deployment-filters.ts +66 -0
- package/src/features/deployments/deployment-list.tsx +465 -0
- package/src/features/deployments/deployment-visit-links.tsx +104 -0
- package/src/features/deployments/deployments-page.tsx +376 -0
- package/src/features/logs/log-scroll-area.tsx +69 -0
- package/src/features/notifications/email-preferences.tsx +232 -0
- package/src/features/onboarding/profile-onboarding.tsx +162 -0
- package/src/features/plugins/contributions.tsx +118 -0
- package/src/features/projects/backup-job-logs.tsx +143 -0
- package/src/features/projects/backup-policy-delete-dialog.tsx +82 -0
- package/src/features/projects/backup-restore-actions.tsx +573 -0
- package/src/features/projects/backup-status-icon.tsx +33 -0
- package/src/features/projects/backup-target-list.tsx +222 -0
- package/src/features/projects/backup-timestamp.tsx +32 -0
- package/src/features/projects/backup-volume-picker.tsx +158 -0
- package/src/features/projects/compose-service-input.tsx +64 -0
- package/src/features/projects/deploy-readiness.ts +28 -0
- package/src/features/projects/environment-dot.tsx +91 -0
- package/src/features/projects/log-keyword-filter.tsx +48 -0
- package/src/features/projects/manual-deploy-dialog.tsx +563 -0
- package/src/features/projects/pending-section.ts +26 -0
- package/src/features/projects/project-backup-details-pending.tsx +305 -0
- package/src/features/projects/project-backup-details.tsx +936 -0
- package/src/features/projects/project-backup-policy.tsx +644 -0
- package/src/features/projects/project-backup-utils.ts +190 -0
- package/src/features/projects/project-backups-pending.tsx +136 -0
- package/src/features/projects/project-backups.tsx +876 -0
- package/src/features/projects/project-environment-settings.tsx +1128 -0
- package/src/features/projects/project-heading.tsx +68 -0
- package/src/features/projects/project-layout.tsx +188 -0
- package/src/features/projects/project-logs.tsx +461 -0
- package/src/features/projects/project-overview.tsx +280 -0
- package/src/features/projects/project-route-pending.tsx +1064 -0
- package/src/features/projects/project-routing-settings.tsx +663 -0
- package/src/features/projects/project-settings-section.tsx +64 -0
- package/src/features/projects/project-settings-utils.ts +101 -0
- package/src/features/projects/project-settings.tsx +1472 -0
- package/src/features/projects/repository-picker-dialog.tsx +404 -0
- package/src/features/repositories/new-project-dialog-launcher.tsx +44 -0
- package/src/features/repositories/new-project-dialog.tsx +1204 -0
- package/src/features/repositories/new-project-pending.tsx +28 -0
- package/src/features/repositories/repository-list-pending.tsx +59 -0
- package/src/features/servers/server-availability-alert.tsx +44 -0
- package/src/features/servers/server-availability.ts +29 -0
- package/src/features/servers/server-avatar.tsx +52 -0
- package/src/features/servers/server-detail-pending.tsx +315 -0
- package/src/features/servers/server-detail.tsx +855 -0
- package/src/features/servers/server-metric-chart-pending.tsx +38 -0
- package/src/features/servers/server-metric-chart.tsx +182 -0
- package/src/features/servers/server-metric-format.ts +50 -0
- package/src/features/servers/server-metrics-pending.tsx +75 -0
- package/src/features/servers/server-metrics.tsx +136 -0
- package/src/features/servers/team-servers.tsx +432 -0
- package/src/features/settings/settings-route-pending.tsx +406 -0
- package/src/features/teams/backup-storage-settings.tsx +483 -0
- package/src/features/teams/navigation.ts +51 -0
- package/src/features/teams/new-team-dialog.tsx +132 -0
- package/src/features/teams/pending-section.ts +19 -0
- package/src/features/teams/team-header.tsx +1286 -0
- package/src/features/teams/team-heading.tsx +57 -0
- package/src/features/teams/team-home.tsx +391 -0
- package/src/features/teams/team-invite.tsx +155 -0
- package/src/features/teams/team-route-pending.tsx +505 -0
- package/src/features/teams/team-settings.tsx +1555 -0
- package/src/features/teams/use-workspace-updates.ts +126 -0
- package/src/features/users/api-keys-settings.tsx +544 -0
- package/src/features/users/api-scope-selection.tsx +269 -0
- package/src/features/users/connected-apps-settings.tsx +449 -0
- package/src/features/users/mcp-consent.tsx +220 -0
- package/src/features/users/profile-picture.tsx +171 -0
- package/src/features/users/user-settings.tsx +1064 -0
- package/src/lib/account-settings-schema.ts +78 -0
- package/src/lib/action-feedback.ts +81 -0
- package/src/lib/admin-search-schema.ts +8 -0
- package/src/lib/agent-release.d.mts +21 -0
- package/src/lib/agent-release.mjs +90 -0
- package/src/lib/allowlist-schema.ts +79 -0
- package/src/lib/api-key-schema.ts +176 -0
- package/src/lib/auth/client.ts +20 -0
- package/src/lib/avatar-schema.ts +11 -0
- package/src/lib/backup-cron.ts +118 -0
- package/src/lib/backup-database.ts +41 -0
- package/src/lib/backup-feature.ts +2 -0
- package/src/lib/backup-paths.ts +7 -0
- package/src/lib/backup-policy-schema.ts +85 -0
- package/src/lib/backup-resource-schema.ts +12 -0
- package/src/lib/backup-restore-schema.ts +15 -0
- package/src/lib/backup-storage-schema.ts +55 -0
- package/src/lib/deferred-module.ts +38 -0
- package/src/lib/deployment-logs-query.ts +63 -0
- package/src/lib/display-name.ts +7 -0
- package/src/lib/error-message.ts +119 -0
- package/src/lib/form-errors.ts +10 -0
- package/src/lib/internal-redirect.ts +25 -0
- package/src/lib/manual-deploy-options-query.ts +21 -0
- package/src/lib/notification-preferences.ts +43 -0
- package/src/lib/onboarding.ts +11 -0
- package/src/lib/operation-logs-query.ts +51 -0
- package/src/lib/page-head.ts +14 -0
- package/src/lib/project-context-query.ts +257 -0
- package/src/lib/project-creation-schema.ts +64 -0
- package/src/lib/project-env-schema.ts +23 -0
- package/src/lib/project-environments-schema.ts +56 -0
- package/src/lib/project-routing-schema.ts +94 -0
- package/src/lib/project-runtime-config.ts +131 -0
- package/src/lib/project-settings-schema.ts +75 -0
- package/src/lib/public-api-schema.ts +128 -0
- package/src/lib/public-error.ts +337 -0
- package/src/lib/query.ts +13 -0
- package/src/lib/repository-selection-query.ts +11 -0
- package/src/lib/resource-cache.ts +30 -0
- package/src/lib/runtime-id.ts +19 -0
- package/src/lib/search.ts +14 -0
- package/src/lib/server-context-query.ts +43 -0
- package/src/lib/server-name-schema.ts +11 -0
- package/src/lib/settings-conflict.ts +45 -0
- package/src/lib/settings-version.ts +7 -0
- package/src/lib/team-context-query.ts +126 -0
- package/src/lib/team-context-schema.ts +8 -0
- package/src/lib/team-forms-schema.ts +45 -0
- package/src/lib/team-role.ts +13 -0
- package/src/lib/team-settings-schema.ts +48 -0
- package/src/lib/urls.ts +3 -0
- package/src/lib/use-avatar-input.ts +61 -0
- package/src/lib/use-refresh-error.ts +18 -0
- package/src/lib/use-settings-version.ts +23 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validation.ts +5 -0
- package/src/lib/workspace-updates.ts +53 -0
- package/src/package/config.ts +10 -0
- package/src/package/env.mjs +54 -0
- package/src/package/migrate-api.d.ts +5 -0
- package/src/package/migrate.ts +56 -0
- package/src/package/plugins.ts +141 -0
- package/src/package/sqlite.mjs +2 -0
- package/src/package/vite-api.d.ts +5 -0
- package/src/package/vite.ts +324 -0
- package/src/package/watch-guard.cjs +26 -0
- package/src/plugins/client.ts +57 -0
- package/src/plugins/contract.ts +188 -0
- package/src/plugins/default.server.ts +4 -0
- package/src/plugins/default.ui.ts +4 -0
- package/src/plugins/operations.ts +45 -0
- package/src/plugins/server.ts +197 -0
- package/src/plugins/version.ts +1 -0
- package/src/routeTree.gen.ts +1275 -0
- package/src/router.tsx +46 -0
- package/src/routes/$teamSlug.index.tsx +53 -0
- package/src/routes/$teamSlug.new-project.tsx +13 -0
- package/src/routes/$teamSlug.projects.$projectName.backups.$backupId.tsx +40 -0
- package/src/routes/$teamSlug.projects.$projectName.backups.tsx +82 -0
- package/src/routes/$teamSlug.projects.$projectName.deployments.$deploymentId.tsx +52 -0
- package/src/routes/$teamSlug.projects.$projectName.deployments.tsx +86 -0
- package/src/routes/$teamSlug.projects.$projectName.logs.tsx +55 -0
- package/src/routes/$teamSlug.projects.$projectName.settings.tsx +51 -0
- package/src/routes/$teamSlug.projects.$projectName.tsx +106 -0
- package/src/routes/$teamSlug.projects.tsx +40 -0
- package/src/routes/$teamSlug.servers.$serverId.tsx +31 -0
- package/src/routes/$teamSlug.servers.index.tsx +59 -0
- package/src/routes/$teamSlug.servers.tsx +7 -0
- package/src/routes/$teamSlug.settings.tsx +56 -0
- package/src/routes/$teamSlug.tsx +86 -0
- package/src/routes/[.]well-known.oauth-authorization-server.api.auth.ts +13 -0
- package/src/routes/[.]well-known.oauth-protected-resource.mcp.ts +6 -0
- package/src/routes/__root.tsx +155 -0
- package/src/routes/admin.tsx +55 -0
- package/src/routes/api/agents/certificates.ts +21 -0
- package/src/routes/api/agents/credentials/rotate.ts +22 -0
- package/src/routes/api/agents/heartbeat.ts +42 -0
- package/src/routes/api/agents/job-logs.ts +22 -0
- package/src/routes/api/agents/jobs/complete.ts +22 -0
- package/src/routes/api/agents/jobs/next.ts +29 -0
- package/src/routes/api/agents/jobs/progress.ts +32 -0
- package/src/routes/api/agents/jobs/status.ts +22 -0
- package/src/routes/api/agents/register.ts +45 -0
- package/src/routes/api/agents/runtime-logs.ts +22 -0
- package/src/routes/api/agents/switch.ts +25 -0
- package/src/routes/api/auth/$.ts +38 -0
- package/src/routes/api/cron/backups.ts +33 -0
- package/src/routes/api/cron/maintenance.ts +30 -0
- package/src/routes/api/github/install.callback.ts +51 -0
- package/src/routes/api/github/install.ts +41 -0
- package/src/routes/api/github/webhook.ts +11 -0
- package/src/routes/api/health.ts +19 -0
- package/src/routes/api/install/agent.$token.ts +50 -0
- package/src/routes/api/install/agent.binary.ts +83 -0
- package/src/routes/api/install/agent.checksum.ts +45 -0
- package/src/routes/api/install/agent.ts +26 -0
- package/src/routes/api/plugins/$.ts +11 -0
- package/src/routes/api/v1/$.ts +15 -0
- package/src/routes/api/workspaces/updates.ts +8 -0
- package/src/routes/auth.login.tsx +50 -0
- package/src/routes/deployments.$deploymentId.unavailable.tsx +21 -0
- package/src/routes/index.tsx +23 -0
- package/src/routes/invites.$token.tsx +35 -0
- package/src/routes/mcp.ts +13 -0
- package/src/routes/oauth.consent.tsx +54 -0
- package/src/routes/onboarding.tsx +37 -0
- package/src/routes/onboarding.workspace.tsx +44 -0
- package/src/routes/settings.tsx +30 -0
- package/src/routes/uploads/avatars/$fileName.ts +44 -0
- package/src/server/admin/access.ts +67 -0
- package/src/server/admin/functions.ts +122 -0
- package/src/server/admin/service.ts +260 -0
- package/src/server/agents/backup-state.ts +210 -0
- package/src/server/agents/certificates.ts +96 -0
- package/src/server/agents/claims.ts +116 -0
- package/src/server/agents/cleanup.ts +52 -0
- package/src/server/agents/compatibility.ts +116 -0
- package/src/server/agents/completion.ts +83 -0
- package/src/server/agents/container-cleanup-state.ts +51 -0
- package/src/server/agents/deployment-state.ts +111 -0
- package/src/server/agents/job-payloads.ts +159 -0
- package/src/server/agents/jobs.ts +1293 -0
- package/src/server/agents/protocol.ts +400 -0
- package/src/server/agents/reconciliation.ts +43 -0
- package/src/server/agents/runtime-logs.ts +337 -0
- package/src/server/agents/runtime-state.ts +125 -0
- package/src/server/api/admission.ts +12 -0
- package/src/server/api/authorization.ts +115 -0
- package/src/server/api/credentials.ts +569 -0
- package/src/server/api/cursors.ts +72 -0
- package/src/server/api/errors.ts +24 -0
- package/src/server/api/functions.ts +60 -0
- package/src/server/api/handler.ts +274 -0
- package/src/server/api/mutation-operations.ts +913 -0
- package/src/server/api/operation-context.ts +139 -0
- package/src/server/api/operation-contract.ts +1099 -0
- package/src/server/api/operations.ts +64 -0
- package/src/server/api/read-operations.ts +891 -0
- package/src/server/api/retention.ts +33 -0
- package/src/server/auth/access.ts +109 -0
- package/src/server/auth/auth-methods.ts +28 -0
- package/src/server/auth/better-auth.ts +518 -0
- package/src/server/auth/context.ts +28 -0
- package/src/server/auth/email-change.ts +47 -0
- package/src/server/auth/email-delivery.ts +51 -0
- package/src/server/auth/provider-identity.ts +19 -0
- package/src/server/auth/provider-tokens.ts +27 -0
- package/src/server/auth/session.server.ts +19 -0
- package/src/server/backups/database-commands.ts +40 -0
- package/src/server/backups/destination.ts +12 -0
- package/src/server/backups/feature.ts +6 -0
- package/src/server/backups/policy-validation.ts +51 -0
- package/src/server/backups/queue-state.ts +137 -0
- package/src/server/backups/service.ts +918 -0
- package/src/server/backups/storage-functions.ts +42 -0
- package/src/server/backups/storage-state.ts +67 -0
- package/src/server/backups/storage.ts +237 -0
- package/src/server/config.ts +274 -0
- package/src/server/db/atomic.ts +16 -0
- package/src/server/db/d1-connection.ts +83 -0
- package/src/server/db/errors.ts +17 -0
- package/src/server/db/index.cloudflare.ts +15 -0
- package/src/server/db/index.ts +35 -0
- package/src/server/db/libsql-connection.ts +72 -0
- package/src/server/db/log-schema.ts +102 -0
- package/src/server/db/migrate-libsql.ts +29 -0
- package/src/server/db/migrate-sqlite.ts +47 -0
- package/src/server/db/migration-history.ts +12 -0
- package/src/server/db/migrations-logs/0000_initial.sql +48 -0
- package/src/server/db/migrations-logs/0001_runtime_log_order.sql +2 -0
- package/src/server/db/migrations-logs/meta/0000_snapshot.json +327 -0
- package/src/server/db/migrations-logs/meta/0001_snapshot.json +329 -0
- package/src/server/db/migrations-logs/meta/_journal.json +20 -0
- package/src/server/db/migrations-sqlite/0000_initial.sql +894 -0
- package/src/server/db/migrations-sqlite/0001_agent_indexes.sql +5 -0
- package/src/server/db/migrations-sqlite/meta/0000_snapshot.json +5816 -0
- package/src/server/db/migrations-sqlite/meta/0001_snapshot.json +5841 -0
- package/src/server/db/migrations-sqlite/meta/_journal.json +20 -0
- package/src/server/db/oauth-schema.ts +216 -0
- package/src/server/db/remote-config.ts +47 -0
- package/src/server/db/schema.ts +1218 -0
- package/src/server/db/sqlite-connection.ts +94 -0
- package/src/server/db/sqlite-files.ts +27 -0
- package/src/server/db/sqlite-params.ts +19 -0
- package/src/server/db/worker-connection.ts +47 -0
- package/src/server/deleted-resources.ts +133 -0
- package/src/server/deletion-recovery.ts +272 -0
- package/src/server/email/layout.tsx +85 -0
- package/src/server/email/notification-template.tsx +62 -0
- package/src/server/email/otp-template.tsx +48 -0
- package/src/server/email/service.ts +251 -0
- package/src/server/email/team-invite-template.tsx +59 -0
- package/src/server/email/theme.ts +10 -0
- package/src/server/github/http.ts +14 -0
- package/src/server/github/install-state.ts +48 -0
- package/src/server/github/service.ts +831 -0
- package/src/server/github/webhook-handler.ts +198 -0
- package/src/server/github/webhooks.ts +205 -0
- package/src/server/http/auth.ts +11 -0
- package/src/server/http/error-diagnostics.ts +66 -0
- package/src/server/http/plugin-request.ts +15 -0
- package/src/server/http/proxy-request.ts +53 -0
- package/src/server/http/public-errors.ts +48 -0
- package/src/server/http/rate-limit.ts +59 -0
- package/src/server/http/request-body.ts +54 -0
- package/src/server/http/request-ip.ts +54 -0
- package/src/server/http/security-headers.ts +36 -0
- package/src/server/install/agent-binary.ts +230 -0
- package/src/server/install/agent-script.ts +565 -0
- package/src/server/install/release-binary.ts +65 -0
- package/src/server/logs/budget.ts +72 -0
- package/src/server/logs/cleanup.ts +64 -0
- package/src/server/logs/deployments.ts +98 -0
- package/src/server/logs/operations.ts +145 -0
- package/src/server/logs/retention.ts +39 -0
- package/src/server/maintenance.ts +61 -0
- package/src/server/mcp/connections.ts +358 -0
- package/src/server/mcp/functions.ts +63 -0
- package/src/server/mcp/handler.ts +295 -0
- package/src/server/mcp/oauth-discovery.cloudflare.ts +6 -0
- package/src/server/mcp/oauth-discovery.ts +8 -0
- package/src/server/mcp/oauth-provider.ts +220 -0
- package/src/server/mcp/urls.ts +20 -0
- package/src/server/notifications/functions.ts +51 -0
- package/src/server/notifications/observe.ts +303 -0
- package/src/server/notifications/outbox.ts +335 -0
- package/src/server/notifications/preferences.ts +131 -0
- package/src/server/notifications/query.ts +16 -0
- package/src/server/plugins/scope.ts +17 -0
- package/src/server/plugins/server-context.ts +157 -0
- package/src/server/plugins/services.server.ts +10 -0
- package/src/server/plugins/workspace-access.ts +74 -0
- package/src/server/projects/backup-policy-state.ts +68 -0
- package/src/server/projects/backups.ts +219 -0
- package/src/server/projects/compose-metadata.ts +174 -0
- package/src/server/projects/configuration-state.ts +90 -0
- package/src/server/projects/context.ts +909 -0
- package/src/server/projects/creation-state.ts +46 -0
- package/src/server/projects/deletion-state.ts +69 -0
- package/src/server/projects/deployment-config.ts +139 -0
- package/src/server/projects/deployment-logs.ts +97 -0
- package/src/server/projects/deployments.ts +1123 -0
- package/src/server/projects/domain-state.ts +71 -0
- package/src/server/projects/environment-branches.ts +75 -0
- package/src/server/projects/environment-state.ts +158 -0
- package/src/server/projects/environments.ts +424 -0
- package/src/server/projects/functions.ts +748 -0
- package/src/server/projects/generated-hostname.ts +77 -0
- package/src/server/projects/hostnames.ts +244 -0
- package/src/server/projects/mappers.ts +457 -0
- package/src/server/projects/names.ts +41 -0
- package/src/server/projects/project-env-vars.ts +28 -0
- package/src/server/projects/queue-state.ts +118 -0
- package/src/server/projects/repository.ts +368 -0
- package/src/server/projects/route-state.ts +37 -0
- package/src/server/projects/runtime-logs.ts +97 -0
- package/src/server/projects/settings-context.ts +58 -0
- package/src/server/projects/settings.ts +738 -0
- package/src/server/projects/types.ts +355 -0
- package/src/server/realtime/agent-events.ts +32 -0
- package/src/server/realtime/node-broadcaster.ts +71 -0
- package/src/server/realtime/node-handler.ts +55 -0
- package/src/server/realtime/service.ts +125 -0
- package/src/server/realtime/workspace-object.cloudflare.ts +62 -0
- package/src/server/realtime/workspace-object.ts +3 -0
- package/src/server/resources/color.ts +13 -0
- package/src/server/runtime/background.ts +21 -0
- package/src/server/runtime/node-asset-cache-plugin.ts +12 -0
- package/src/server/runtime/node-scheduler-plugin.ts +17 -0
- package/src/server/runtime/node-scheduler.ts +51 -0
- package/src/server/runtime/platform-env.cloudflare.ts +17 -0
- package/src/server/runtime/platform-env.ts +19 -0
- package/src/server/runtime/scheduled-task-error.ts +9 -0
- package/src/server/scheduled-tasks.ts +28 -0
- package/src/server/secrets/crypto.ts +56 -0
- package/src/server/servers/functions.ts +233 -0
- package/src/server/servers/lifecycle.ts +122 -0
- package/src/server/servers/metric-history-schema.ts +24 -0
- package/src/server/servers/metric-history.ts +55 -0
- package/src/server/servers/monitoring.ts +592 -0
- package/src/server/servers/service.ts +1211 -0
- package/src/server/servers/status.ts +32 -0
- package/src/server/servers/switch.ts +96 -0
- package/src/server/settings-version.ts +43 -0
- package/src/server/storage/capabilities.ts +6 -0
- package/src/server/storage/local-object-store.cloudflare.ts +7 -0
- package/src/server/storage/local-object-store.ts +50 -0
- package/src/server/storage/object-store.ts +160 -0
- package/src/server/teams/creation.ts +51 -0
- package/src/server/teams/delete-policy.ts +13 -0
- package/src/server/teams/deletion-state.ts +80 -0
- package/src/server/teams/functions.ts +324 -0
- package/src/server/teams/invitations.ts +38 -0
- package/src/server/teams/membership.ts +86 -0
- package/src/server/teams/queries.ts +261 -0
- package/src/server/teams/service.ts +1127 -0
- package/src/server/teams/types.ts +101 -0
- package/src/server/uploads/avatar.ts +133 -0
- package/src/server/users/deletion-state.ts +93 -0
- package/src/server/users/functions.ts +160 -0
- package/src/server/users/onboarding.ts +17 -0
- package/src/server/users/service.ts +644 -0
- package/src/server.ts +55 -0
- package/src/styles/shadcn.css +653 -0
- package/src/styles/theme.css +76 -0
- package/src/styles.css +177 -0
- package/wrangler.template.jsonc +37 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
import { mkdirSync } from 'node:fs'
|
|
3
|
+
import { dirname } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import Sqlite from 'better-sqlite3'
|
|
6
|
+
import { drizzle } from 'drizzle-orm/sqlite-proxy'
|
|
7
|
+
import type {
|
|
8
|
+
AsyncRemoteCallback,
|
|
9
|
+
SqliteRemoteDatabase,
|
|
10
|
+
} from 'drizzle-orm/sqlite-proxy'
|
|
11
|
+
|
|
12
|
+
import { sqliteParams } from './sqlite-params'
|
|
13
|
+
|
|
14
|
+
export function openSqlite<TSchema extends Record<string, unknown>>(
|
|
15
|
+
path: string,
|
|
16
|
+
schema: TSchema,
|
|
17
|
+
) {
|
|
18
|
+
if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true })
|
|
19
|
+
const connection = new Sqlite(path)
|
|
20
|
+
connection.pragma('foreign_keys = ON')
|
|
21
|
+
connection.pragma('journal_mode = WAL')
|
|
22
|
+
connection.pragma('synchronous = FULL')
|
|
23
|
+
connection.pragma('busy_timeout = 5000')
|
|
24
|
+
|
|
25
|
+
// Callbacks may await between statements. Serialize unrelated operations on
|
|
26
|
+
// this connection so they cannot join another request's uncommitted work.
|
|
27
|
+
const transactionScope = new AsyncLocalStorage<{ active: boolean }>()
|
|
28
|
+
let tail = Promise.resolve()
|
|
29
|
+
async function exclusive<T>(work: () => T | Promise<T>): Promise<T> {
|
|
30
|
+
if (transactionScope.getStore()?.active) return work()
|
|
31
|
+
const previous = tail
|
|
32
|
+
let release = () => {}
|
|
33
|
+
tail = new Promise<void>((resolve) => {
|
|
34
|
+
release = resolve
|
|
35
|
+
})
|
|
36
|
+
await previous
|
|
37
|
+
const scope = { active: true }
|
|
38
|
+
try {
|
|
39
|
+
return await transactionScope.run(scope, work)
|
|
40
|
+
} finally {
|
|
41
|
+
scope.active = false
|
|
42
|
+
release()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function query(
|
|
46
|
+
sql: string,
|
|
47
|
+
params: unknown[],
|
|
48
|
+
method: Parameters<AsyncRemoteCallback>[2],
|
|
49
|
+
batch = false,
|
|
50
|
+
) {
|
|
51
|
+
const statement = connection.prepare(sql)
|
|
52
|
+
const values = sqliteParams(params)
|
|
53
|
+
if (method === 'run') {
|
|
54
|
+
statement.run(...values)
|
|
55
|
+
return { rows: [] }
|
|
56
|
+
}
|
|
57
|
+
const rows = statement.raw(true).all(...values) as unknown[][]
|
|
58
|
+
// Drizzle's callback type requires an array even though get() uses an
|
|
59
|
+
// undefined first row to represent no match. Keep that library contract.
|
|
60
|
+
return { rows: method === 'get' && !batch ? (rows[0] as unknown[]) : rows }
|
|
61
|
+
}
|
|
62
|
+
const database = drizzle<TSchema>(
|
|
63
|
+
(sql, params: unknown[], method) =>
|
|
64
|
+
exclusive(() => query(sql, params, method)),
|
|
65
|
+
(queries) =>
|
|
66
|
+
exclusive(() =>
|
|
67
|
+
connection
|
|
68
|
+
.transaction(() =>
|
|
69
|
+
queries.map((entry) =>
|
|
70
|
+
query(entry.sql, entry.params, entry.method, true),
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
.immediate(),
|
|
74
|
+
),
|
|
75
|
+
{ schema },
|
|
76
|
+
)
|
|
77
|
+
const db = new Proxy(database, {
|
|
78
|
+
get(target, property) {
|
|
79
|
+
if (property === 'transaction') {
|
|
80
|
+
const transaction: SqliteRemoteDatabase<TSchema>['transaction'] = (
|
|
81
|
+
callback,
|
|
82
|
+
options,
|
|
83
|
+
) =>
|
|
84
|
+
exclusive(() =>
|
|
85
|
+
target.transaction(callback, { ...options, behavior: 'immediate' }),
|
|
86
|
+
)
|
|
87
|
+
return transaction
|
|
88
|
+
}
|
|
89
|
+
const value: unknown = Reflect.get(target, property, target)
|
|
90
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
return { db, connection, close: () => exclusive(() => connection.close()) }
|
|
94
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { existsSync, realpathSync, statSync } from 'node:fs'
|
|
2
|
+
import { basename, dirname, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export function assertSeparateSqliteFiles(mainPath: string, logPath: string) {
|
|
5
|
+
// Each :memory: connection owns an independent database.
|
|
6
|
+
if (mainPath === ':memory:' || logPath === ':memory:') return
|
|
7
|
+
let same = canonicalPath(mainPath) === canonicalPath(logPath)
|
|
8
|
+
if (!same && existsSync(mainPath) && existsSync(logPath)) {
|
|
9
|
+
const main = statSync(mainPath)
|
|
10
|
+
const logs = statSync(logPath)
|
|
11
|
+
same = main.dev === logs.dev && main.ino === logs.ino
|
|
12
|
+
}
|
|
13
|
+
if (same) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
'DEVPUSH_DATABASE_PATH and DEVPUSH_LOG_DATABASE_PATH must identify separate SQLite files.',
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Resolve existing parent symlinks even before the database files are created.
|
|
21
|
+
function canonicalPath(path: string): string {
|
|
22
|
+
const absolute = resolve(path)
|
|
23
|
+
if (existsSync(absolute)) return realpathSync(absolute)
|
|
24
|
+
const parent = dirname(absolute)
|
|
25
|
+
if (parent === absolute) return absolute
|
|
26
|
+
return resolve(canonicalPath(parent), basename(absolute))
|
|
27
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function sqliteParams(
|
|
2
|
+
params: unknown[],
|
|
3
|
+
): Array<string | number | bigint | Uint8Array | null> {
|
|
4
|
+
return params.map((value) => {
|
|
5
|
+
if (value instanceof Date) return value.getTime()
|
|
6
|
+
if (typeof value === 'boolean') return Number(value)
|
|
7
|
+
if (
|
|
8
|
+
value === null ||
|
|
9
|
+
typeof value === 'string' ||
|
|
10
|
+
typeof value === 'number' ||
|
|
11
|
+
typeof value === 'bigint' ||
|
|
12
|
+
value instanceof Uint8Array
|
|
13
|
+
)
|
|
14
|
+
return value
|
|
15
|
+
throw new Error(
|
|
16
|
+
'Unsupported SQLite bound parameter; encode values at the schema boundary.',
|
|
17
|
+
)
|
|
18
|
+
})
|
|
19
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createClient } from '@libsql/client/web'
|
|
2
|
+
import type { D1Database } from '@cloudflare/workers-types'
|
|
3
|
+
import { openD1 } from './d1-connection'
|
|
4
|
+
import { openLibsql } from './libsql-connection'
|
|
5
|
+
import { remoteDatabaseConfig } from './remote-config'
|
|
6
|
+
import * as schema from './schema'
|
|
7
|
+
import * as logSchema from './log-schema'
|
|
8
|
+
|
|
9
|
+
type DatabaseEnvironment = Parameters<typeof remoteDatabaseConfig>[0] & {
|
|
10
|
+
DEVPUSH_DATABASE?: D1Database
|
|
11
|
+
DEVPUSH_LOG_DATABASE?: D1Database
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function openWorkerDatabases(env: DatabaseEnvironment) {
|
|
15
|
+
const remote = remoteDatabaseConfig(env)
|
|
16
|
+
if (remote)
|
|
17
|
+
return {
|
|
18
|
+
db: openLibsql(() => createClient(remote.main), schema).db,
|
|
19
|
+
logsDb: openLibsql(() => createClient(remote.logs), logSchema).db,
|
|
20
|
+
}
|
|
21
|
+
if (!env.DEVPUSH_DATABASE || !env.DEVPUSH_LOG_DATABASE)
|
|
22
|
+
throw new Error('Configure both D1 bindings or both remote database URLs.')
|
|
23
|
+
return {
|
|
24
|
+
db: openD1(env.DEVPUSH_DATABASE, schema),
|
|
25
|
+
logsDb: openD1(env.DEVPUSH_LOG_DATABASE, logSchema),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Cloudflare's upload validation can evaluate modules before resource bindings
|
|
30
|
+
// are available. Resolve the stores when application code first uses them.
|
|
31
|
+
export function createWorkerDatabases(env: DatabaseEnvironment) {
|
|
32
|
+
let stores: ReturnType<typeof openWorkerDatabases> | undefined
|
|
33
|
+
const getStores = () => (stores ??= openWorkerDatabases(env))
|
|
34
|
+
function lazy<TStore extends object>(getStore: () => TStore): TStore {
|
|
35
|
+
return new Proxy({} as TStore, {
|
|
36
|
+
get(_target, property) {
|
|
37
|
+
const store = getStore()
|
|
38
|
+
const value: unknown = Reflect.get(store, property, store)
|
|
39
|
+
return typeof value === 'function' ? value.bind(store) : value
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
db: lazy(() => getStores().db),
|
|
45
|
+
logsDb: lazy(() => getStores().logsDb),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm'
|
|
2
|
+
import { removeOwnedAvatar } from '#devpush/server/uploads/avatar'
|
|
3
|
+
import { db } from '#devpush/server/db'
|
|
4
|
+
import type { AtomicStore } from '#devpush/server/db/atomic'
|
|
5
|
+
|
|
6
|
+
// Keep terminal job records long enough for an agent to retry a lost acknowledgement.
|
|
7
|
+
// Unresolved external resources are never treated as successfully cleaned up.
|
|
8
|
+
export const deletedResourceRetentionMs = 30 * 24 * 60 * 60 * 1000
|
|
9
|
+
|
|
10
|
+
const resolvedJob = sql`(status = 'succeeded' or (status = 'canceled' and type not in ('deprovision', 'purge') and (attempt = 0
|
|
11
|
+
or json_extract(case when json_valid(payload) then payload else '{}' end, '$.operatorResolution.kind') = 'confirmed-cleanup')))`
|
|
12
|
+
|
|
13
|
+
export async function pruneDeletedResources({
|
|
14
|
+
store = db,
|
|
15
|
+
now = new Date(),
|
|
16
|
+
limit = 100,
|
|
17
|
+
}: {
|
|
18
|
+
store?: AtomicStore
|
|
19
|
+
now?: Date
|
|
20
|
+
limit?: number
|
|
21
|
+
} = {}) {
|
|
22
|
+
const cutoff = now.getTime() - deletedResourceRetentionMs
|
|
23
|
+
const batchLimit = Math.max(1, Math.min(100, Math.trunc(limit)))
|
|
24
|
+
// Keep the owning row if object storage is unavailable, so a later sweep
|
|
25
|
+
// can retry rather than losing the only reference to an uploaded avatar.
|
|
26
|
+
for (const table of ['projects', 'teams'] as const) {
|
|
27
|
+
const rows =
|
|
28
|
+
await store.all(sql`select id, avatar_path from ${sql.raw(table)}
|
|
29
|
+
where status = 'deleted' and updated_at <= ${cutoff} and avatar_path is not null
|
|
30
|
+
order by id limit ${batchLimit}`)
|
|
31
|
+
for (const row of rows) {
|
|
32
|
+
const values = Array.isArray(row)
|
|
33
|
+
? row
|
|
34
|
+
: Object.values(row as Record<string, unknown>)
|
|
35
|
+
const [id, path] = values
|
|
36
|
+
if (typeof id !== 'string' || typeof path !== 'string')
|
|
37
|
+
throw new Error('Invalid deleted avatar row.')
|
|
38
|
+
try {
|
|
39
|
+
await removeOwnedAvatar(
|
|
40
|
+
path,
|
|
41
|
+
{
|
|
42
|
+
filePrefix: table === 'projects' ? 'project' : 'team',
|
|
43
|
+
resourceId: id,
|
|
44
|
+
},
|
|
45
|
+
{ strict: true },
|
|
46
|
+
)
|
|
47
|
+
await store.run(sql`update ${sql.raw(table)} set avatar_path = null where id = ${id}
|
|
48
|
+
and status = 'deleted' and avatar_path = ${path}`)
|
|
49
|
+
} catch {
|
|
50
|
+
console.error('Unable to remove deleted resource avatar.', {
|
|
51
|
+
resourceId: id,
|
|
52
|
+
table,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const unprovisionedProjects = sql`select id from projects where status = 'deleted'
|
|
58
|
+
and (exists(select 1 from aliases a where a.project_id = projects.id)
|
|
59
|
+
or exists(select 1 from domains d where d.project_id = projects.id))
|
|
60
|
+
and not exists(select 1 from environments e where e.project_id = projects.id and e.status <> 'deleted')
|
|
61
|
+
and not exists(select 1 from agent_jobs j where j.type = 'deprovision' and j.status <> 'succeeded'
|
|
62
|
+
and (j.project_id = projects.id or (j.team_id = projects.team_id and j.project_id is null)))
|
|
63
|
+
order by id limit ${batchLimit}`
|
|
64
|
+
await store.batch([
|
|
65
|
+
store.run(
|
|
66
|
+
sql`delete from aliases where project_id in (${unprovisionedProjects})`,
|
|
67
|
+
),
|
|
68
|
+
store.run(
|
|
69
|
+
sql`delete from domains where project_id in (${unprovisionedProjects})`,
|
|
70
|
+
),
|
|
71
|
+
])
|
|
72
|
+
const expiredJobs = sql`${resolvedJob} and coalesce(finished_at, updated_at) <= ${cutoff}`
|
|
73
|
+
const projectEligible = sql`avatar_path is null and status = 'deleted' and updated_at <= ${cutoff}
|
|
74
|
+
and not exists(select 1 from environments e where e.project_id = projects.id and e.status <> 'deleted')
|
|
75
|
+
and not exists(select 1 from backup_runs r where r.project_id = projects.id and (
|
|
76
|
+
r.status in ('queued', 'running') or r.cleanup_queued_at is not null
|
|
77
|
+
or exists(select 1 from backup_artifacts a where a.backup_run_id = r.id)))
|
|
78
|
+
and not exists(select 1 from agent_jobs j where
|
|
79
|
+
(j.project_id = projects.id or (j.team_id = projects.team_id and j.project_id is null))
|
|
80
|
+
and not (${expiredJobs}))`
|
|
81
|
+
const serverEligible = sql`status = 'deleted' and updated_at <= ${cutoff}
|
|
82
|
+
and not exists(select 1 from projects p where p.default_server_id = servers.id)
|
|
83
|
+
and not exists(select 1 from environments e where e.server_id = servers.id)
|
|
84
|
+
and not exists(select 1 from deployments d where d.server_id = servers.id)
|
|
85
|
+
and not exists(select 1 from backup_runs r where r.server_id = servers.id)
|
|
86
|
+
and not exists(select 1 from agent_jobs j where j.server_id = servers.id and not (${expiredJobs}))
|
|
87
|
+
and not exists(select 1 from agent_runtime_observations o where o.server_id = servers.id
|
|
88
|
+
and not exists(select 1 from agent_jobs j,
|
|
89
|
+
json_each(case when json_valid(j.payload) then j.payload else '{}' end, '$.runtimeIds') target
|
|
90
|
+
where j.server_id = servers.id and j.type = 'purge' and j.status = 'succeeded'
|
|
91
|
+
and j.finished_at >= o.last_seen_at and target.value = o.runtime_id))`
|
|
92
|
+
// No asynchronous reads authorize a delete. Each mutation repeats its guard in
|
|
93
|
+
// the atomic batch; foreign-key cascades then remove owned settings/credentials.
|
|
94
|
+
const projectIds = sql`select id from projects where ${projectEligible} order by id limit ${batchLimit}`
|
|
95
|
+
const serverIds = sql`select id from servers where ${serverEligible} order by id limit ${batchLimit}`
|
|
96
|
+
const teamEligible = sql`avatar_path is null and status = 'deleted' and updated_at <= ${cutoff}
|
|
97
|
+
and not exists(select 1 from projects p where p.team_id = teams.id)
|
|
98
|
+
and not exists(select 1 from servers s where s.team_id = teams.id)
|
|
99
|
+
and not exists(select 1 from agent_jobs j where j.team_id = teams.id and not (${expiredJobs}))`
|
|
100
|
+
const teamIds = sql`select id from teams where ${teamEligible} order by id limit ${batchLimit}`
|
|
101
|
+
const userEligible = sql`status = 'deleted' and updated_at <= ${cutoff}
|
|
102
|
+
and not exists(select 1 from team_members m where m.user_id = user.id)
|
|
103
|
+
and exists(select 1 from user_cleanup_jobs j where j.user_id = user.id and j.status = 'succeeded')
|
|
104
|
+
and not exists(select 1 from user_cleanup_jobs j where j.user_id = user.id and j.status <> 'succeeded')`
|
|
105
|
+
const results = await store.batch([
|
|
106
|
+
store.run(
|
|
107
|
+
sql`delete from agent_jobs where project_id in (${projectIds}) and ${expiredJobs}
|
|
108
|
+
and exists(select 1 from servers s where s.id = agent_jobs.server_id and s.status <> 'deleted')`,
|
|
109
|
+
),
|
|
110
|
+
store.all(
|
|
111
|
+
sql`delete from projects where id in (${projectIds}) returning id`,
|
|
112
|
+
),
|
|
113
|
+
store.run(
|
|
114
|
+
sql`delete from agent_runtime_observations where server_id in (${serverIds})`,
|
|
115
|
+
),
|
|
116
|
+
store.run(
|
|
117
|
+
sql`delete from agent_jobs where server_id in (${serverIds}) and ${expiredJobs}`,
|
|
118
|
+
),
|
|
119
|
+
store.all(sql`delete from servers where id in (${serverIds}) returning id`),
|
|
120
|
+
store.run(
|
|
121
|
+
sql`delete from agent_jobs where team_id in (${teamIds}) and ${expiredJobs}`,
|
|
122
|
+
),
|
|
123
|
+
store.all(sql`delete from teams where id in (${teamIds}) returning id`),
|
|
124
|
+
store.all(sql`delete from user where id in (select id from user where ${userEligible}
|
|
125
|
+
order by id limit ${batchLimit}) returning id`),
|
|
126
|
+
])
|
|
127
|
+
return {
|
|
128
|
+
projects: results[1].length,
|
|
129
|
+
servers: results[4].length,
|
|
130
|
+
teams: results[6].length,
|
|
131
|
+
users: results[7].length,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm'
|
|
2
|
+
import type { SQL } from 'drizzle-orm'
|
|
3
|
+
import type { AtomicStore } from './db/atomic'
|
|
4
|
+
import { runtimeCleanupInsert } from './agents/cleanup'
|
|
5
|
+
|
|
6
|
+
export type RecoveryAction =
|
|
7
|
+
| 'retry-job'
|
|
8
|
+
| 'confirm-job'
|
|
9
|
+
| 'cleanup-project'
|
|
10
|
+
| 'confirm-artifact'
|
|
11
|
+
export type RecoveryTarget = 'project' | 'server' | 'team' | 'job' | 'artifact'
|
|
12
|
+
type Row = Record<string, unknown>
|
|
13
|
+
|
|
14
|
+
export class RecoveryInputError extends Error {}
|
|
15
|
+
|
|
16
|
+
export function recoveryRows(rows: unknown[]): Row[] {
|
|
17
|
+
return rows.map((row) => {
|
|
18
|
+
const encoded = Array.isArray(row)
|
|
19
|
+
? row[0]
|
|
20
|
+
: row && typeof row === 'object' && 'record' in row
|
|
21
|
+
? row.record
|
|
22
|
+
: undefined
|
|
23
|
+
if (typeof encoded === 'string') return JSON.parse(encoded) as Row
|
|
24
|
+
if (!row || typeof row !== 'object' || Array.isArray(row))
|
|
25
|
+
throw new RecoveryInputError('Recovery requires named database results.')
|
|
26
|
+
return row as Row
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function listDeletedResourcesQuery() {
|
|
31
|
+
return jsonRows(
|
|
32
|
+
sql`select 'project' as target, id, name, updated_at from projects where status = 'deleted'
|
|
33
|
+
union all select 'team', id, name, updated_at from teams where status = 'deleted'
|
|
34
|
+
union all select 'server', id, name, updated_at from servers where status = 'deleted'
|
|
35
|
+
order by updated_at, target, id limit 101`,
|
|
36
|
+
['target', 'id', 'name', 'updated_at'],
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function recoveryTargetQuery(target: RecoveryTarget, id: string) {
|
|
41
|
+
const table = {
|
|
42
|
+
project: 'projects',
|
|
43
|
+
server: 'servers',
|
|
44
|
+
team: 'teams',
|
|
45
|
+
job: 'agent_jobs',
|
|
46
|
+
artifact: 'backup_artifacts',
|
|
47
|
+
}[target]
|
|
48
|
+
const fields =
|
|
49
|
+
target === 'job'
|
|
50
|
+
? [
|
|
51
|
+
'id',
|
|
52
|
+
'type',
|
|
53
|
+
'status',
|
|
54
|
+
'server_id',
|
|
55
|
+
'team_id',
|
|
56
|
+
'project_id',
|
|
57
|
+
'payload',
|
|
58
|
+
'attempt',
|
|
59
|
+
'max_attempts',
|
|
60
|
+
'updated_at',
|
|
61
|
+
'claimed_at',
|
|
62
|
+
'available_at',
|
|
63
|
+
]
|
|
64
|
+
: target === 'artifact'
|
|
65
|
+
? ['id', 'backup_run_id', 'storage', 'object_key', 'checksum_sha256']
|
|
66
|
+
: target === 'team'
|
|
67
|
+
? ['id', 'status', 'updated_at']
|
|
68
|
+
: ['id', 'status', 'updated_at', 'team_id']
|
|
69
|
+
return jsonRows(sql`select * from ${sql.raw(table)} where id = ${id}`, fields)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function jsonRows(query: SQL, fields: string[]) {
|
|
73
|
+
return sql`select json_object(${sql.join(
|
|
74
|
+
fields.flatMap((field) => [sql`${field}`, sql.raw(field)]),
|
|
75
|
+
sql`, `,
|
|
76
|
+
)}) as record from (${query})`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Whitelist output: job payloads and resource rows can contain credentials.
|
|
80
|
+
export async function inspectDeletion(
|
|
81
|
+
store: AtomicStore,
|
|
82
|
+
target: RecoveryTarget,
|
|
83
|
+
row: Row,
|
|
84
|
+
) {
|
|
85
|
+
const id = String(row.id)
|
|
86
|
+
const projectIds =
|
|
87
|
+
target === 'project'
|
|
88
|
+
? sql`select id from projects where id = ${id}`
|
|
89
|
+
: target === 'team'
|
|
90
|
+
? sql`select id from projects where team_id = ${id}`
|
|
91
|
+
: target === 'server'
|
|
92
|
+
? sql`select id from projects where default_server_id = ${id}
|
|
93
|
+
union select project_id from environments where server_id = ${id}
|
|
94
|
+
union select project_id from deployments where server_id = ${id}`
|
|
95
|
+
: sql`select null where 0`
|
|
96
|
+
const jobs = await store.all(
|
|
97
|
+
jsonRows(
|
|
98
|
+
sql`select id, type, status, attempt, max_attempts, server_id, project_id,
|
|
99
|
+
available_at, claimed_at, updated_at, json_extract(payload, '$.runtimeIds') as runtime_ids,
|
|
100
|
+
json_extract(payload, '$.backupRunIds') as backup_run_ids, json_extract(payload, '$.localObjectKeys') as local_object_keys,
|
|
101
|
+
json_extract(payload, '$.remoteObjectKeys') as remote_object_keys, json_extract(payload, '$.operatorResolution') as resolution from agent_jobs where
|
|
102
|
+
${target === 'job' ? sql`id = ${id}` : target === 'server' ? sql`server_id = ${id}` : target === 'team' ? sql`team_id = ${id} or project_id in (${projectIds})` : sql`project_id in (${projectIds}) or (project_id is null and team_id = ${row.team_id ?? null})`}
|
|
103
|
+
order by id limit 101`,
|
|
104
|
+
[
|
|
105
|
+
'id',
|
|
106
|
+
'type',
|
|
107
|
+
'status',
|
|
108
|
+
'attempt',
|
|
109
|
+
'max_attempts',
|
|
110
|
+
'server_id',
|
|
111
|
+
'project_id',
|
|
112
|
+
'available_at',
|
|
113
|
+
'claimed_at',
|
|
114
|
+
'updated_at',
|
|
115
|
+
'runtime_ids',
|
|
116
|
+
'backup_run_ids',
|
|
117
|
+
'local_object_keys',
|
|
118
|
+
'remote_object_keys',
|
|
119
|
+
'resolution',
|
|
120
|
+
],
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
const artifacts = await store.all(
|
|
124
|
+
jsonRows(
|
|
125
|
+
sql`select a.id, a.backup_run_id, a.storage, a.object_key, a.checksum_sha256,
|
|
126
|
+
a.size_bytes, r.project_id, r.server_id, json_extract(r.destination, '$.bucket') as bucket,
|
|
127
|
+
json_extract(r.destination, '$.storageId') as storage_id from backup_artifacts a join backup_runs r on r.id = a.backup_run_id
|
|
128
|
+
where ${target === 'artifact' ? sql`a.id = ${id}` : target === 'server' ? sql`r.server_id = ${id}` : sql`r.project_id in (${projectIds})`}
|
|
129
|
+
order by a.id limit 101`,
|
|
130
|
+
[
|
|
131
|
+
'id',
|
|
132
|
+
'backup_run_id',
|
|
133
|
+
'storage',
|
|
134
|
+
'object_key',
|
|
135
|
+
'checksum_sha256',
|
|
136
|
+
'size_bytes',
|
|
137
|
+
'project_id',
|
|
138
|
+
'server_id',
|
|
139
|
+
'bucket',
|
|
140
|
+
'storage_id',
|
|
141
|
+
],
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
const environments = await store.all(
|
|
145
|
+
jsonRows(
|
|
146
|
+
sql`select id, project_id, runtime_id, status from environments
|
|
147
|
+
where project_id in (${projectIds}) order by id limit 101`,
|
|
148
|
+
['id', 'project_id', 'runtime_id', 'status'],
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
return {
|
|
152
|
+
target,
|
|
153
|
+
id,
|
|
154
|
+
status: row.status,
|
|
155
|
+
jobs: recoveryRows(jobs),
|
|
156
|
+
artifacts: recoveryRows(artifacts),
|
|
157
|
+
environments: recoveryRows(environments),
|
|
158
|
+
note: 'Each list is limited to 101 rows. Inspect individual IDs; never treat this as a complete archive inventory.',
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function deletionRecoveryPlan(input: {
|
|
163
|
+
action: RecoveryAction
|
|
164
|
+
row: Row
|
|
165
|
+
operator: string
|
|
166
|
+
reason: string
|
|
167
|
+
now: Date
|
|
168
|
+
}): [SQL, ...SQL[]] {
|
|
169
|
+
const { action, row, now } = input
|
|
170
|
+
if (!input.operator.trim() || !input.reason.trim())
|
|
171
|
+
throw new RecoveryInputError('Operator and evidence/reason are required.')
|
|
172
|
+
const id = String(row.id),
|
|
173
|
+
time = now.getTime()
|
|
174
|
+
const resolution = JSON.stringify({
|
|
175
|
+
kind: 'confirmed-cleanup',
|
|
176
|
+
operator: input.operator,
|
|
177
|
+
reason: input.reason,
|
|
178
|
+
at: time,
|
|
179
|
+
})
|
|
180
|
+
const statements: SQL[] = []
|
|
181
|
+
let guard: SQL
|
|
182
|
+
let team: SQL
|
|
183
|
+
if (action === 'cleanup-project') {
|
|
184
|
+
if (row.status !== 'deleted')
|
|
185
|
+
throw new RecoveryInputError('Only deleted projects can be cleaned up.')
|
|
186
|
+
guard = sql`exists(select 1 from projects where id = ${id} and status = 'deleted' and updated_at = ${row.updated_at}
|
|
187
|
+
and exists(select 1 from environments where project_id = ${id} and status <> 'deleted'))`
|
|
188
|
+
team = sql`select team_id from projects where id = ${id}`
|
|
189
|
+
statements.push(
|
|
190
|
+
runtimeCleanupInsert({
|
|
191
|
+
projectIds: sql`select id from projects where id = ${id}`,
|
|
192
|
+
projectId: id,
|
|
193
|
+
guard,
|
|
194
|
+
reason: `Operator ${input.operator}: ${input.reason}`,
|
|
195
|
+
now,
|
|
196
|
+
}),
|
|
197
|
+
)
|
|
198
|
+
statements.push(sql`update environments set status = 'deleted', is_production = 0, updated_at = ${time}
|
|
199
|
+
where project_id = ${id} and ${guard}`)
|
|
200
|
+
} else if (action === 'confirm-artifact') {
|
|
201
|
+
guard = sql`exists(select 1 from backup_artifacts a join backup_runs r on r.id = a.backup_run_id
|
|
202
|
+
join projects p on p.id = r.project_id where a.id = ${id} and p.status = 'deleted'
|
|
203
|
+
and a.object_key = ${row.object_key} and a.checksum_sha256 = ${row.checksum_sha256}
|
|
204
|
+
and a.backup_run_id = ${row.backup_run_id} and a.storage = ${row.storage}
|
|
205
|
+
and r.status not in ('queued','running') and r.cleanup_queued_at is null)`
|
|
206
|
+
team = sql`select p.team_id from projects p join backup_runs r on r.project_id = p.id where r.id = ${row.backup_run_id}`
|
|
207
|
+
statements.push(sql`update backup_runs set manifest = json_set(coalesce(manifest, '{}'), '$.operatorRecovery', json(${resolution})),
|
|
208
|
+
updated_at = ${time} where id = ${row.backup_run_id} and ${guard}`)
|
|
209
|
+
statements.push(
|
|
210
|
+
sql`delete from backup_artifacts where id = ${id} and ${guard}`,
|
|
211
|
+
)
|
|
212
|
+
} else {
|
|
213
|
+
const cleanup = row.type === 'deprovision' || row.type === 'purge'
|
|
214
|
+
if (row.status === 'succeeded')
|
|
215
|
+
throw new RecoveryInputError('This job has already completed.')
|
|
216
|
+
const staleCleanup =
|
|
217
|
+
cleanup &&
|
|
218
|
+
row.status === 'running' &&
|
|
219
|
+
(row.claimed_at === null ||
|
|
220
|
+
Number(row.claimed_at) < time - 60 * 60 * 1000)
|
|
221
|
+
if (row.status === 'running' && !staleCleanup)
|
|
222
|
+
throw new RecoveryInputError(
|
|
223
|
+
'Wait for the running job to finish or become stale before recovery.',
|
|
224
|
+
)
|
|
225
|
+
if (action === 'retry-job' && !cleanup)
|
|
226
|
+
throw new RecoveryInputError(
|
|
227
|
+
'Only container cleanup jobs can be retried; do not replay deployments, backups or restores.',
|
|
228
|
+
)
|
|
229
|
+
if (
|
|
230
|
+
action === 'retry-job' &&
|
|
231
|
+
row.status !== 'failed' &&
|
|
232
|
+
row.status !== 'abandoned' &&
|
|
233
|
+
row.status !== 'canceled' &&
|
|
234
|
+
!staleCleanup
|
|
235
|
+
)
|
|
236
|
+
throw new RecoveryInputError(
|
|
237
|
+
'Only terminal unsuccessful cleanup jobs can be retried.',
|
|
238
|
+
)
|
|
239
|
+
guard = sql`exists(select 1 from agent_jobs j where j.id = ${id} and j.status = ${row.status}
|
|
240
|
+
and j.type = ${row.type} and j.server_id = ${row.server_id}
|
|
241
|
+
and j.project_id is ${row.project_id ?? null} and j.team_id is ${row.team_id ?? null}
|
|
242
|
+
and j.available_at = ${row.available_at} and j.attempt = ${row.attempt} and j.claimed_at is ${row.claimed_at ?? null} and j.updated_at = ${row.updated_at} and j.payload = ${row.payload}
|
|
243
|
+
and (exists(select 1 from projects p where p.id = j.project_id and p.status = 'deleted')
|
|
244
|
+
or exists(select 1 from servers s where s.id = j.server_id and s.status = 'deleted')
|
|
245
|
+
or exists(select 1 from teams t where t.id = j.team_id and t.status = 'deleted')))`
|
|
246
|
+
team = sql`select coalesce(j.team_id, p.team_id, s.team_id) from agent_jobs j
|
|
247
|
+
left join projects p on p.id = j.project_id join servers s on s.id = j.server_id where j.id = ${id}`
|
|
248
|
+
if (action === 'retry-job') {
|
|
249
|
+
statements.push(sql`update agent_jobs set status = 'queued', claimed_at = null, finished_at = null,
|
|
250
|
+
available_at = max(available_at, ${time}), updated_at = ${time}, max_attempts = max(max_attempts, attempt + 10),
|
|
251
|
+
error_message = null, payload = json_set(payload, '$.operatorRetry', json(${resolution})) where id = ${id} and ${guard}`)
|
|
252
|
+
} else {
|
|
253
|
+
if (row.type === 'backup_cleanup')
|
|
254
|
+
statements.push(sql`update backup_runs set cleanup_queued_at = null, cleanup_error_message = null, updated_at = ${time}
|
|
255
|
+
where project_id = ${row.project_id} and server_id = ${row.server_id}
|
|
256
|
+
and id in (select value from json_each(${String(row.payload)}, '$.backupRunIds')) and ${guard}`)
|
|
257
|
+
if (row.type === 'backup')
|
|
258
|
+
statements.push(sql`update backup_runs set status = 'canceled', finished_at = ${time}, updated_at = ${time}
|
|
259
|
+
where id = json_extract(${String(row.payload)}, '$.backupRunId') and project_id = ${row.project_id}
|
|
260
|
+
and server_id = ${row.server_id} and status in ('queued','running') and ${guard}`)
|
|
261
|
+
// A confirmed cleanup is a real success; failed executions remain canceled,
|
|
262
|
+
// with an explicit operator resolution instead of a fabricated backup success.
|
|
263
|
+
statements.push(sql`update agent_jobs set status = ${cleanup ? 'succeeded' : 'canceled'},
|
|
264
|
+
finished_at = ${time}, updated_at = ${time}, error_message = ${`Operator resolution: ${input.reason}`},
|
|
265
|
+
payload = json_set(payload, '$.operatorResolution', json(${resolution})) where id = ${id} and ${guard}`)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const audit = sql`insert into api_audit (id, team_id, user_id, credential_id, request_id, operation, resource_id, outcome, created_at)
|
|
269
|
+
select ${crypto.randomUUID()}, coalesce((${team}), 'installation'), ${input.operator}, 'operator-cli',
|
|
270
|
+
${crypto.randomUUID()}, ${`deletion.${action}`}, ${id}, 'succeeded', ${time} where ${guard} returning id`
|
|
271
|
+
return [audit, ...statements]
|
|
272
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Body,
|
|
4
|
+
Container,
|
|
5
|
+
Head,
|
|
6
|
+
Heading,
|
|
7
|
+
Html,
|
|
8
|
+
Img,
|
|
9
|
+
Link,
|
|
10
|
+
Preview,
|
|
11
|
+
Section,
|
|
12
|
+
Tailwind,
|
|
13
|
+
Text,
|
|
14
|
+
} from '@react-email/components'
|
|
15
|
+
import { config } from '#devpush/server/config'
|
|
16
|
+
import { emailTheme } from './theme'
|
|
17
|
+
|
|
18
|
+
export function EmailLayout({
|
|
19
|
+
title,
|
|
20
|
+
preview,
|
|
21
|
+
to,
|
|
22
|
+
children,
|
|
23
|
+
allowIgnore = true,
|
|
24
|
+
}: {
|
|
25
|
+
title: string
|
|
26
|
+
preview: string
|
|
27
|
+
to: string
|
|
28
|
+
children: ReactNode
|
|
29
|
+
allowIgnore?: boolean
|
|
30
|
+
}) {
|
|
31
|
+
return (
|
|
32
|
+
<Html>
|
|
33
|
+
<Head />
|
|
34
|
+
<Preview>{preview}</Preview>
|
|
35
|
+
<Tailwind>
|
|
36
|
+
<Body
|
|
37
|
+
className="my-auto mx-auto px-2 font-sans antialiased"
|
|
38
|
+
style={{
|
|
39
|
+
backgroundColor: emailTheme.background,
|
|
40
|
+
color: emailTheme.foreground,
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<Container className="my-[40px] mx-auto max-w-[465px] p-[20px]">
|
|
44
|
+
<Section className="mt-[24px]">
|
|
45
|
+
<Img
|
|
46
|
+
alt="devpush"
|
|
47
|
+
className="my-0 mx-auto"
|
|
48
|
+
width="32"
|
|
49
|
+
height="32"
|
|
50
|
+
src={new URL(
|
|
51
|
+
'/logo-email.png',
|
|
52
|
+
config.BETTER_AUTH_URL,
|
|
53
|
+
).toString()}
|
|
54
|
+
/>
|
|
55
|
+
</Section>
|
|
56
|
+
<Heading
|
|
57
|
+
className="my-[30px] mx-0 p-0 text-center text-[24px] font-semibold tracking-tight"
|
|
58
|
+
style={{ color: emailTheme.foreground }}
|
|
59
|
+
>
|
|
60
|
+
{title}
|
|
61
|
+
</Heading>
|
|
62
|
+
{children}
|
|
63
|
+
<Text
|
|
64
|
+
className="mt-[36px] text-[14px] leading-[24px]"
|
|
65
|
+
style={{ color: emailTheme.mutedForeground }}
|
|
66
|
+
>
|
|
67
|
+
This email was intended for{' '}
|
|
68
|
+
<Link
|
|
69
|
+
className="underline"
|
|
70
|
+
href={`mailto:${to}`}
|
|
71
|
+
style={{ color: emailTheme.mutedLink }}
|
|
72
|
+
>
|
|
73
|
+
{to}
|
|
74
|
+
</Link>
|
|
75
|
+
.
|
|
76
|
+
{allowIgnore
|
|
77
|
+
? ' If you think this is a mistake, you can safely ignore this email.'
|
|
78
|
+
: ''}
|
|
79
|
+
</Text>
|
|
80
|
+
</Container>
|
|
81
|
+
</Body>
|
|
82
|
+
</Tailwind>
|
|
83
|
+
</Html>
|
|
84
|
+
)
|
|
85
|
+
}
|