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,337 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
import { and, desc, eq, inArray, lt, sql } from 'drizzle-orm'
|
|
4
|
+
|
|
5
|
+
import { db, logsDb } from '#devpush/server/db'
|
|
6
|
+
import { runtimeLogs } from '#devpush/server/db/log-schema'
|
|
7
|
+
import { deployments, environments, projects } from '#devpush/server/db/schema'
|
|
8
|
+
import { getPluginServices } from '#devpush/server/plugins/services.server'
|
|
9
|
+
import { admitRuntimeLogs, reserveLogBudget } from '#devpush/server/logs/budget'
|
|
10
|
+
import { requireAgentServer } from '#devpush/server/servers/service'
|
|
11
|
+
import {
|
|
12
|
+
runtimeLogRetentionMs,
|
|
13
|
+
runtimeLogPruneBatchSize,
|
|
14
|
+
} from '#devpush/server/logs/retention'
|
|
15
|
+
|
|
16
|
+
export type RuntimeLogLine = {
|
|
17
|
+
runtimeId: string
|
|
18
|
+
service: string
|
|
19
|
+
observedAt: string
|
|
20
|
+
content: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const maxRuntimeLogLines = 500
|
|
24
|
+
const maxRuntimeLogContentLength = 16_384
|
|
25
|
+
const maxRuntimeLogRowsPerRuntime = 10_000
|
|
26
|
+
|
|
27
|
+
export async function recordRuntimeLogs({
|
|
28
|
+
serverId,
|
|
29
|
+
token,
|
|
30
|
+
logs,
|
|
31
|
+
}: {
|
|
32
|
+
serverId: string
|
|
33
|
+
token: string
|
|
34
|
+
logs: Array<RuntimeLogLine>
|
|
35
|
+
}) {
|
|
36
|
+
const server = await requireAgentServer({ serverId, token })
|
|
37
|
+
|
|
38
|
+
const normalizedLogs = normalizeRuntimeLogs(logs)
|
|
39
|
+
|
|
40
|
+
if (normalizedLogs.length === 0) {
|
|
41
|
+
return { ok: true, accepted: 0, inserted: 0 }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const assignments = await listAllowedRuntimes({
|
|
45
|
+
serverId,
|
|
46
|
+
runtimeIds: Array.from(new Set(normalizedLogs.map((log) => log.runtimeId))),
|
|
47
|
+
})
|
|
48
|
+
let rows = normalizedLogs.flatMap((log) => {
|
|
49
|
+
const assignment = assignments.get(log.runtimeId)
|
|
50
|
+
if (!assignment) return []
|
|
51
|
+
return [
|
|
52
|
+
{
|
|
53
|
+
id: randomUUID(),
|
|
54
|
+
projectId: assignment.projectId,
|
|
55
|
+
environmentId: assignment.environmentId,
|
|
56
|
+
serverId,
|
|
57
|
+
runtimeId: log.runtimeId,
|
|
58
|
+
service: log.service,
|
|
59
|
+
observedAt: new Date(Math.min(log.observedAt.getTime(), Date.now())),
|
|
60
|
+
content: log.content,
|
|
61
|
+
searchContent: log.content.toLowerCase(),
|
|
62
|
+
contentHash: hashRuntimeLogLine(log),
|
|
63
|
+
},
|
|
64
|
+
]
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
if (rows.length === 0) {
|
|
68
|
+
return { ok: true, accepted: 0, inserted: 0 }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const acknowledged = rows.length
|
|
72
|
+
let dropped = 0
|
|
73
|
+
const limits = getPluginServices().runtimeLogLimits?.(server.teamId)
|
|
74
|
+
if (limits) {
|
|
75
|
+
// Replayed batches must not consume another allowance after their rows were stored.
|
|
76
|
+
const known = new Set<string>()
|
|
77
|
+
for (let start = 0; start < rows.length; start += 80) {
|
|
78
|
+
const existing = await logsDb
|
|
79
|
+
.select({ hash: runtimeLogs.contentHash })
|
|
80
|
+
.from(runtimeLogs)
|
|
81
|
+
.where(
|
|
82
|
+
and(
|
|
83
|
+
eq(runtimeLogs.serverId, serverId),
|
|
84
|
+
inArray(
|
|
85
|
+
runtimeLogs.contentHash,
|
|
86
|
+
rows.slice(start, start + 80).map((row) => row.contentHash),
|
|
87
|
+
),
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
for (const row of existing) known.add(row.hash)
|
|
91
|
+
}
|
|
92
|
+
rows = rows.filter((row) => {
|
|
93
|
+
if (known.has(row.contentHash)) return false
|
|
94
|
+
known.add(row.contentHash)
|
|
95
|
+
return true
|
|
96
|
+
})
|
|
97
|
+
if (rows.length && !(await admitRuntimeLogs(server.teamId, limits, rows))) {
|
|
98
|
+
dropped = rows.length
|
|
99
|
+
const first = rows[0]!
|
|
100
|
+
rows = []
|
|
101
|
+
// One visible notice per workspace/day. Never block agent delivery on a quota refusal.
|
|
102
|
+
if (
|
|
103
|
+
await reserveLogBudget({
|
|
104
|
+
teamId: server.teamId,
|
|
105
|
+
kind: 'notice',
|
|
106
|
+
amount: 1,
|
|
107
|
+
limit: 1,
|
|
108
|
+
windowMs: 86_400_000,
|
|
109
|
+
})
|
|
110
|
+
) {
|
|
111
|
+
const content =
|
|
112
|
+
'Runtime log limit reached. Some logs were dropped; collection resumes when the allowance resets. Applications and deployments are unaffected.'
|
|
113
|
+
rows.push({
|
|
114
|
+
...first,
|
|
115
|
+
id: randomUUID(),
|
|
116
|
+
service: 'devpush',
|
|
117
|
+
content,
|
|
118
|
+
searchContent: content.toLowerCase(),
|
|
119
|
+
observedAt: new Date(),
|
|
120
|
+
contentHash: createHash('sha256')
|
|
121
|
+
.update(
|
|
122
|
+
`log-limit:${server.teamId}:${Math.floor(Date.now() / 86_400_000)}`,
|
|
123
|
+
)
|
|
124
|
+
.digest('hex'),
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Keep each statement below D1's parameter limit. A partially completed
|
|
131
|
+
// request is safe to replay because each line has a stable content hash.
|
|
132
|
+
let inserted = 0
|
|
133
|
+
for (let offset = 0; offset < rows.length; offset += 32) {
|
|
134
|
+
const statements = []
|
|
135
|
+
for (
|
|
136
|
+
let start = offset;
|
|
137
|
+
start < Math.min(offset + 32, rows.length);
|
|
138
|
+
start += 8
|
|
139
|
+
) {
|
|
140
|
+
statements.push(
|
|
141
|
+
logsDb
|
|
142
|
+
.insert(runtimeLogs)
|
|
143
|
+
.values(rows.slice(start, Math.min(start + 8, rows.length)))
|
|
144
|
+
.onConflictDoNothing({
|
|
145
|
+
target: [
|
|
146
|
+
runtimeLogs.serverId,
|
|
147
|
+
runtimeLogs.runtimeId,
|
|
148
|
+
runtimeLogs.contentHash,
|
|
149
|
+
],
|
|
150
|
+
})
|
|
151
|
+
.returning({ id: runtimeLogs.id }),
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
const [first, ...rest] = statements
|
|
155
|
+
if (first) {
|
|
156
|
+
const batch = await logsDb.batch([first, ...rest])
|
|
157
|
+
inserted += batch.reduce((count, result) => count + result.length, 0)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await pruneRuntimeLogs({
|
|
162
|
+
serverId,
|
|
163
|
+
runtimeIds: Array.from(new Set(rows.map((row) => row.runtimeId))),
|
|
164
|
+
now: new Date(),
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
ok: true,
|
|
169
|
+
accepted: acknowledged,
|
|
170
|
+
inserted,
|
|
171
|
+
...(dropped ? { dropped } : {}),
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizeRuntimeLogs(logs: Array<RuntimeLogLine>) {
|
|
176
|
+
return logs
|
|
177
|
+
.slice(0, maxRuntimeLogLines)
|
|
178
|
+
.map((log) => {
|
|
179
|
+
const runtimeId = log.runtimeId.trim()
|
|
180
|
+
const service = log.service.trim() || 'runtime'
|
|
181
|
+
const content = log.content.slice(0, maxRuntimeLogContentLength)
|
|
182
|
+
const observedAt = new Date(log.observedAt)
|
|
183
|
+
|
|
184
|
+
if (
|
|
185
|
+
runtimeId.length === 0 ||
|
|
186
|
+
runtimeId.length > 128 ||
|
|
187
|
+
service.length > 128 ||
|
|
188
|
+
content.length === 0 ||
|
|
189
|
+
Number.isNaN(observedAt.getTime())
|
|
190
|
+
) {
|
|
191
|
+
return null
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
runtimeId,
|
|
196
|
+
service,
|
|
197
|
+
observedAt,
|
|
198
|
+
content,
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
.filter((log): log is NonNullable<typeof log> => log !== null)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function listAllowedRuntimes({
|
|
205
|
+
serverId,
|
|
206
|
+
runtimeIds,
|
|
207
|
+
}: {
|
|
208
|
+
serverId: string
|
|
209
|
+
runtimeIds: Array<string>
|
|
210
|
+
}) {
|
|
211
|
+
const assignments = new Map<
|
|
212
|
+
string,
|
|
213
|
+
{ projectId: string; environmentId: string }
|
|
214
|
+
>()
|
|
215
|
+
for (let offset = 0; offset < runtimeIds.length; offset += 80) {
|
|
216
|
+
const rows = await db
|
|
217
|
+
.select({
|
|
218
|
+
runtimeId: environments.runtimeId,
|
|
219
|
+
projectId: projects.id,
|
|
220
|
+
environmentId: environments.id,
|
|
221
|
+
})
|
|
222
|
+
.from(environments)
|
|
223
|
+
.innerJoin(projects, eq(projects.id, environments.projectId))
|
|
224
|
+
.where(
|
|
225
|
+
and(
|
|
226
|
+
eq(
|
|
227
|
+
sql`coalesce(${environments.serverId}, ${projects.defaultServerId})`,
|
|
228
|
+
serverId,
|
|
229
|
+
),
|
|
230
|
+
eq(environments.status, 'active'),
|
|
231
|
+
eq(projects.status, 'active'),
|
|
232
|
+
inArray(
|
|
233
|
+
environments.runtimeId,
|
|
234
|
+
runtimeIds.slice(offset, offset + 80),
|
|
235
|
+
),
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
for (const row of rows) assignments.set(row.runtimeId, row)
|
|
239
|
+
// A durable capture can arrive after replacement or project deletion.
|
|
240
|
+
// Historical deployment ownership, never the agent's claim alone, permits
|
|
241
|
+
// delivery from the original server. Existing display/retention guards stay.
|
|
242
|
+
const historical = await db
|
|
243
|
+
.selectDistinct({
|
|
244
|
+
runtimeId: deployments.runtimeId,
|
|
245
|
+
projectId: projects.id,
|
|
246
|
+
environmentId: environments.id,
|
|
247
|
+
})
|
|
248
|
+
.from(deployments)
|
|
249
|
+
.innerJoin(environments, eq(environments.id, deployments.environmentId))
|
|
250
|
+
.innerJoin(projects, eq(projects.id, environments.projectId))
|
|
251
|
+
.where(
|
|
252
|
+
and(
|
|
253
|
+
eq(deployments.serverId, serverId),
|
|
254
|
+
inArray(deployments.runtimeId, runtimeIds.slice(offset, offset + 80)),
|
|
255
|
+
),
|
|
256
|
+
)
|
|
257
|
+
for (const row of historical) {
|
|
258
|
+
if (!assignments.has(row.runtimeId)) assignments.set(row.runtimeId, row)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return assignments
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function pruneRuntimeLogs({
|
|
265
|
+
serverId,
|
|
266
|
+
runtimeIds,
|
|
267
|
+
now,
|
|
268
|
+
}: {
|
|
269
|
+
serverId: string
|
|
270
|
+
runtimeIds: Array<string>
|
|
271
|
+
now: Date
|
|
272
|
+
}) {
|
|
273
|
+
if (runtimeIds.length === 0) {
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const cutoff = new Date(now.getTime() - runtimeLogRetentionMs)
|
|
278
|
+
|
|
279
|
+
for (const runtimeId of runtimeIds) {
|
|
280
|
+
const scope = and(
|
|
281
|
+
eq(runtimeLogs.serverId, serverId),
|
|
282
|
+
eq(runtimeLogs.runtimeId, runtimeId),
|
|
283
|
+
)
|
|
284
|
+
// Bound cleanup work independently of how long the agent was disconnected.
|
|
285
|
+
await logsDb.delete(runtimeLogs).where(
|
|
286
|
+
inArray(
|
|
287
|
+
runtimeLogs.id,
|
|
288
|
+
logsDb
|
|
289
|
+
.select({ id: runtimeLogs.id })
|
|
290
|
+
.from(runtimeLogs)
|
|
291
|
+
.where(and(scope, lt(runtimeLogs.observedAt, cutoff)))
|
|
292
|
+
.orderBy(runtimeLogs.observedAt, runtimeLogs.id)
|
|
293
|
+
.limit(runtimeLogPruneBatchSize),
|
|
294
|
+
),
|
|
295
|
+
)
|
|
296
|
+
await logsDb
|
|
297
|
+
.delete(runtimeLogs)
|
|
298
|
+
.where(
|
|
299
|
+
inArray(
|
|
300
|
+
runtimeLogs.id,
|
|
301
|
+
logsDb
|
|
302
|
+
.select({ id: runtimeLogs.id })
|
|
303
|
+
.from(runtimeLogs)
|
|
304
|
+
.where(scope)
|
|
305
|
+
.orderBy(
|
|
306
|
+
desc(runtimeLogs.observedAt),
|
|
307
|
+
desc(runtimeLogs.createdAt),
|
|
308
|
+
desc(runtimeLogs.id),
|
|
309
|
+
)
|
|
310
|
+
.offset(maxRuntimeLogRowsPerRuntime)
|
|
311
|
+
.limit(runtimeLogPruneBatchSize),
|
|
312
|
+
),
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function hashRuntimeLogLine({
|
|
318
|
+
runtimeId,
|
|
319
|
+
service,
|
|
320
|
+
observedAt,
|
|
321
|
+
content,
|
|
322
|
+
}: {
|
|
323
|
+
runtimeId: string
|
|
324
|
+
service: string
|
|
325
|
+
observedAt: Date
|
|
326
|
+
content: string
|
|
327
|
+
}) {
|
|
328
|
+
return createHash('sha256')
|
|
329
|
+
.update(runtimeId)
|
|
330
|
+
.update('\0')
|
|
331
|
+
.update(service)
|
|
332
|
+
.update('\0')
|
|
333
|
+
.update(observedAt.toISOString())
|
|
334
|
+
.update('\0')
|
|
335
|
+
.update(content)
|
|
336
|
+
.digest('hex')
|
|
337
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm'
|
|
2
|
+
import type { SQL } from 'drizzle-orm'
|
|
3
|
+
import type { AtomicStore } from '#devpush/server/db/atomic'
|
|
4
|
+
import {
|
|
5
|
+
agentJobs,
|
|
6
|
+
agentRuntimeObservations,
|
|
7
|
+
deployments,
|
|
8
|
+
environments,
|
|
9
|
+
projects,
|
|
10
|
+
} from '#devpush/server/db/schema'
|
|
11
|
+
import { cleanupVolumeRetentionMs } from './cleanup'
|
|
12
|
+
import type { ReportedRuntime } from './reconciliation'
|
|
13
|
+
|
|
14
|
+
export async function reconcileRuntimeState(
|
|
15
|
+
store: AtomicStore,
|
|
16
|
+
input: {
|
|
17
|
+
serverId: string
|
|
18
|
+
runtimeIds: string[]
|
|
19
|
+
runtimes: ReportedRuntime[]
|
|
20
|
+
now: Date
|
|
21
|
+
},
|
|
22
|
+
) {
|
|
23
|
+
const now = input.now.getTime()
|
|
24
|
+
const services = new Map(
|
|
25
|
+
input.runtimes.map((runtime) => [runtime.runtimeId, runtime.services]),
|
|
26
|
+
)
|
|
27
|
+
const reportedServices = JSON.stringify([...services.keys()])
|
|
28
|
+
const observations = []
|
|
29
|
+
for (let offset = 0; offset < input.runtimeIds.length; offset += 8) {
|
|
30
|
+
const rows = input.runtimeIds.slice(offset, offset + 8).map(
|
|
31
|
+
(runtimeId) => sql`(
|
|
32
|
+
${crypto.randomUUID()}, ${input.serverId}, ${runtimeId},
|
|
33
|
+
${services.has(runtimeId) ? JSON.stringify(services.get(runtimeId)) : null}, ${now}, ${now}, ${now}, ${now})`,
|
|
34
|
+
)
|
|
35
|
+
observations.push(
|
|
36
|
+
store.run(sql`insert into ${agentRuntimeObservations}
|
|
37
|
+
(id, server_id, runtime_id, services, first_seen_at, last_seen_at, created_at, updated_at)
|
|
38
|
+
values ${sql.join(rows, sql`, `)} on conflict(server_id, runtime_id) do update set
|
|
39
|
+
last_seen_at = excluded.last_seen_at, updated_at = excluded.updated_at,
|
|
40
|
+
services = case when runtime_id in (select value from json_each(${reportedServices}))
|
|
41
|
+
then excluded.services else services end`),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
// Decide whether a runtime is still needed inside the same write batch that
|
|
45
|
+
// enqueues cleanup. Assignment or deployment changes cannot race this check.
|
|
46
|
+
const needed = sql`exists(select 1 from ${environments} as env join ${projects} as project on project.id = env.project_id
|
|
47
|
+
where env.runtime_id = ob.runtime_id and env.status = 'active'
|
|
48
|
+
and coalesce(env.server_id, project.default_server_id) = ${input.serverId})
|
|
49
|
+
or exists(select 1 from ${deployments} as deployment
|
|
50
|
+
where deployment.runtime_id = ob.runtime_id and deployment.server_id = ${input.serverId}
|
|
51
|
+
and deployment.status in ('queued', 'running'))`
|
|
52
|
+
const orphan = sql`substr(ob.runtime_id, 1, 3) = 'dp_'
|
|
53
|
+
and not (${needed})
|
|
54
|
+
and not exists(select 1 from ${agentJobs} as job
|
|
55
|
+
where job.server_id = ${input.serverId} and job.type in ('deprovision', 'purge')
|
|
56
|
+
and job.status in ('queued', 'running') and json_type(job.payload, '$.runtimeIds') = 'array'
|
|
57
|
+
and exists(select 1 from json_each(job.payload, '$.runtimeIds') as runtime
|
|
58
|
+
where runtime.type = 'text' and runtime.value = ob.runtime_id))`
|
|
59
|
+
const cleanupId = crypto.randomUUID()
|
|
60
|
+
const purgeId = crypto.randomUUID()
|
|
61
|
+
const queue = (
|
|
62
|
+
id: string,
|
|
63
|
+
type: 'deprovision' | 'purge',
|
|
64
|
+
eligible: SQL,
|
|
65
|
+
reason: string,
|
|
66
|
+
) =>
|
|
67
|
+
store.run(sql`
|
|
68
|
+
insert into ${agentJobs} (id, type, status, server_id, payload, max_attempts, available_at, created_at, updated_at)
|
|
69
|
+
select ${id}, ${type}, 'queued', ${input.serverId},
|
|
70
|
+
json_object('schemaVersion', 1, 'runtimeIds', json_group_array(runtime_id),
|
|
71
|
+
'removeVolumes', json(${type === 'purge' ? 'true' : 'false'}), 'reason', ${reason}),
|
|
72
|
+
10, ${now}, ${now}, ${now}
|
|
73
|
+
from (select ob.runtime_id from ${agentRuntimeObservations} as ob
|
|
74
|
+
where ob.server_id = ${input.serverId} and ${eligible} and ${orphan} order by ob.runtime_id limit 200)
|
|
75
|
+
having count(*) > 0`)
|
|
76
|
+
const cleanup = queue(
|
|
77
|
+
cleanupId,
|
|
78
|
+
'deprovision',
|
|
79
|
+
sql`ob.cleanup_queued_at is null and ob.first_seen_at <= ${now - 10 * 60 * 1000}
|
|
80
|
+
and ob.runtime_id in (select value from json_each(${JSON.stringify(input.runtimeIds)}))`,
|
|
81
|
+
'devpush detected a Compose runtime that is no longer assigned.',
|
|
82
|
+
)
|
|
83
|
+
const markCleanup =
|
|
84
|
+
store.all(sql`update ${agentRuntimeObservations} set cleanup_queued_at = ${now}, updated_at = ${now}
|
|
85
|
+
where server_id = ${input.serverId} and runtime_id in (
|
|
86
|
+
select runtime.value from ${agentJobs} as job, json_each(job.payload, '$.runtimeIds') as runtime where job.id = ${cleanupId})
|
|
87
|
+
returning runtime_id`)
|
|
88
|
+
const purge = queue(
|
|
89
|
+
purgeId,
|
|
90
|
+
'purge',
|
|
91
|
+
sql`ob.cleanup_queued_at <= ${now - cleanupVolumeRetentionMs} and ob.purge_queued_at is null`,
|
|
92
|
+
'devpush detected retained volumes for a Compose runtime that is no longer assigned.',
|
|
93
|
+
)
|
|
94
|
+
const markPurge =
|
|
95
|
+
store.all(sql`update ${agentRuntimeObservations} set purge_queued_at = ${now}, updated_at = ${now}
|
|
96
|
+
where server_id = ${input.serverId} and runtime_id in (
|
|
97
|
+
select runtime.value from ${agentJobs} as job, json_each(job.payload, '$.runtimeIds') as runtime where job.id = ${purgeId})
|
|
98
|
+
returning runtime_id`)
|
|
99
|
+
// A reused runtime starts a new orphan lifecycle when it is detached again.
|
|
100
|
+
// Pending cleanup alone is not ownership and must not reset the grace period.
|
|
101
|
+
const resetOwned = store.run(sql`update ${agentRuntimeObservations} as ob
|
|
102
|
+
set cleanup_queued_at = null, purge_queued_at = null, first_seen_at = ${now}, updated_at = ${now}
|
|
103
|
+
where ob.server_id = ${input.serverId} and (${needed})`)
|
|
104
|
+
const tail = [resetOwned, cleanup, markCleanup, purge, markPurge] as const
|
|
105
|
+
const [first, ...rest] = observations
|
|
106
|
+
const results = first
|
|
107
|
+
? await store.batch([first, ...rest, ...tail])
|
|
108
|
+
: await store.batch(tail)
|
|
109
|
+
const runtimeIds: string[] = []
|
|
110
|
+
for (const result of [results.at(-3), results.at(-1)]) {
|
|
111
|
+
if (!Array.isArray(result))
|
|
112
|
+
throw new Error('Invalid runtime reconciliation result.')
|
|
113
|
+
for (const row of result as unknown[]) {
|
|
114
|
+
const id = Array.isArray(row)
|
|
115
|
+
? row[0]
|
|
116
|
+
: typeof row === 'object' && row !== null && 'runtime_id' in row
|
|
117
|
+
? row.runtime_id
|
|
118
|
+
: undefined
|
|
119
|
+
if (typeof id !== 'string')
|
|
120
|
+
throw new Error('Invalid runtime reconciliation result.')
|
|
121
|
+
runtimeIds.push(id)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return runtimeIds
|
|
125
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SQL } from 'drizzle-orm'
|
|
2
|
+
import type { AtomicStore } from '#devpush/server/db/atomic'
|
|
3
|
+
|
|
4
|
+
// Domain services commit these statements in the same transaction as their
|
|
5
|
+
// effect. The guard is checked by the effect itself, after any provider reads.
|
|
6
|
+
export type ApiMutationAdmission = {
|
|
7
|
+
guard: SQL
|
|
8
|
+
statements: ReturnType<AtomicStore['run']>[]
|
|
9
|
+
resourceId: string
|
|
10
|
+
secondaryResourceId?: string
|
|
11
|
+
idempotencyId?: string
|
|
12
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm'
|
|
2
|
+
import { apiRoleRank, apiScopeDefinitions } from '#devpush/lib/api-key-schema'
|
|
3
|
+
import { PublicError } from '#devpush/lib/public-error'
|
|
4
|
+
import type { ApiScope } from '#devpush/lib/api-key-schema'
|
|
5
|
+
import type { TeamRole } from '#devpush/lib/team-role'
|
|
6
|
+
import {
|
|
7
|
+
apiGrants,
|
|
8
|
+
apiKeys,
|
|
9
|
+
projects,
|
|
10
|
+
teamMembers,
|
|
11
|
+
teams,
|
|
12
|
+
user,
|
|
13
|
+
} from '#devpush/server/db/schema'
|
|
14
|
+
|
|
15
|
+
export type ApiPrincipal = {
|
|
16
|
+
userId: string
|
|
17
|
+
teamId: string
|
|
18
|
+
membershipId: string
|
|
19
|
+
roleCeiling: TeamRole
|
|
20
|
+
projectIds: string[] | null
|
|
21
|
+
scopes: string[]
|
|
22
|
+
credentialId: string
|
|
23
|
+
credentialKind?: 'api_key' | 'oauth'
|
|
24
|
+
providerGrantId?: string | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function requireApiScope(
|
|
28
|
+
principal: ApiPrincipal,
|
|
29
|
+
scope: string,
|
|
30
|
+
minimumRole: TeamRole = 'member',
|
|
31
|
+
) {
|
|
32
|
+
const definition = Object.hasOwn(apiScopeDefinitions, scope)
|
|
33
|
+
? apiScopeDefinitions[scope as ApiScope]
|
|
34
|
+
: undefined
|
|
35
|
+
if (
|
|
36
|
+
!definition ||
|
|
37
|
+
!principal.scopes.includes(scope) ||
|
|
38
|
+
apiRoleRank[principal.roleCeiling] <
|
|
39
|
+
Math.max(apiRoleRank[definition.minimumRole], apiRoleRank[minimumRole])
|
|
40
|
+
) {
|
|
41
|
+
throw new PublicError('FORBIDDEN')
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function requireApiProject(principal: ApiPrincipal, projectId: string) {
|
|
46
|
+
if (
|
|
47
|
+
principal.projectIds !== null &&
|
|
48
|
+
!principal.projectIds.includes(projectId)
|
|
49
|
+
) {
|
|
50
|
+
throw new PublicError('FORBIDDEN')
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Recheck live credentials and membership at the SQL admission point. */
|
|
55
|
+
export function apiAuthorizationCondition(
|
|
56
|
+
principal: ApiPrincipal,
|
|
57
|
+
options: {
|
|
58
|
+
scope?: string
|
|
59
|
+
projectId?: string
|
|
60
|
+
teamId?: string
|
|
61
|
+
minimumRole?: TeamRole
|
|
62
|
+
allowDeletedProject?: boolean
|
|
63
|
+
} = {},
|
|
64
|
+
) {
|
|
65
|
+
if (options.teamId && options.teamId !== principal.teamId) return sql`0`
|
|
66
|
+
if (
|
|
67
|
+
options.projectId &&
|
|
68
|
+
principal.projectIds !== null &&
|
|
69
|
+
!principal.projectIds.includes(options.projectId)
|
|
70
|
+
)
|
|
71
|
+
return sql`0`
|
|
72
|
+
const definition =
|
|
73
|
+
options.scope && Object.hasOwn(apiScopeDefinitions, options.scope)
|
|
74
|
+
? apiScopeDefinitions[options.scope as ApiScope]
|
|
75
|
+
: undefined
|
|
76
|
+
if (
|
|
77
|
+
options.scope &&
|
|
78
|
+
(!definition || !principal.scopes.includes(options.scope))
|
|
79
|
+
)
|
|
80
|
+
return sql`0`
|
|
81
|
+
const rank = Math.max(
|
|
82
|
+
apiRoleRank[options.minimumRole ?? 'member'],
|
|
83
|
+
apiRoleRank[definition?.minimumRole ?? 'member'],
|
|
84
|
+
)
|
|
85
|
+
if (apiRoleRank[principal.roleCeiling] < rank) return sql`0`
|
|
86
|
+
const credential = principal.credentialKind === 'oauth' ? apiGrants : apiKeys
|
|
87
|
+
const expirationGuard =
|
|
88
|
+
principal.credentialKind === 'oauth'
|
|
89
|
+
? sql`credential.expires_at > cast(unixepoch('subsec') * 1000 as integer)`
|
|
90
|
+
: sql`(credential.expires_at is null or credential.expires_at > cast(unixepoch('subsec') * 1000 as integer))`
|
|
91
|
+
const scopeGuard = options.scope
|
|
92
|
+
? sql`exists(select 1 from json_each(credential.scopes) where value = ${options.scope})`
|
|
93
|
+
: sql`1`
|
|
94
|
+
const projectGuard = options.projectId
|
|
95
|
+
? sql`exists(select 1 from ${projects} as granted_project where granted_project.id = ${options.projectId} and granted_project.team_id = ${principal.teamId} and ${options.allowDeletedProject ? sql`granted_project.status in ('active','deleted')` : sql`granted_project.status = 'active'`})
|
|
96
|
+
and (credential.project_ids is null or exists(select 1 from json_each(credential.project_ids) where value = ${options.projectId}))`
|
|
97
|
+
: sql`1`
|
|
98
|
+
const roleRank = (column: ReturnType<typeof sql>) =>
|
|
99
|
+
sql`case ${column} when 'owner' then 2 when 'admin' then 1 else 0 end`
|
|
100
|
+
return sql`exists (
|
|
101
|
+
select 1 from ${credential} as credential
|
|
102
|
+
join ${teamMembers} as membership on membership.id = credential.membership_id
|
|
103
|
+
join ${user} as actor on actor.id = credential.user_id
|
|
104
|
+
join ${teams} as actor_team on actor_team.id = credential.team_id
|
|
105
|
+
where credential.id = ${principal.credentialId}
|
|
106
|
+
and credential.user_id = ${principal.userId} and credential.team_id = ${principal.teamId}
|
|
107
|
+
and credential.membership_id = ${principal.membershipId}
|
|
108
|
+
and membership.user_id = credential.user_id and membership.team_id = credential.team_id
|
|
109
|
+
and actor.status = 'active' and actor_team.status = 'active'
|
|
110
|
+
and credential.revoked_at is null and ${expirationGuard}
|
|
111
|
+
and ${roleRank(sql`membership.role`)} >= ${rank}
|
|
112
|
+
and ${roleRank(sql`credential.role_ceiling`)} >= ${rank}
|
|
113
|
+
and ${scopeGuard} and ${projectGuard}
|
|
114
|
+
)`
|
|
115
|
+
}
|