devpush 0.0.0-stage → 1.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -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 +170 -4
- 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,569 @@
|
|
|
1
|
+
import { withWorkspaceAccess } from '#devpush/server/plugins/workspace-access'
|
|
2
|
+
import { sql } from 'drizzle-orm'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import type { SQL } from 'drizzle-orm'
|
|
5
|
+
import {
|
|
6
|
+
createApiKeySchema,
|
|
7
|
+
apiRoleRank,
|
|
8
|
+
apiScopeDefinitions,
|
|
9
|
+
apiKeySettingsSchema,
|
|
10
|
+
apiKeyTeamsSchema,
|
|
11
|
+
apiKeyProjectsSchema,
|
|
12
|
+
apiKeyMaxExpirationDays,
|
|
13
|
+
scopesForApiPreset,
|
|
14
|
+
} from '#devpush/lib/api-key-schema'
|
|
15
|
+
import { PublicError } from '#devpush/lib/public-error'
|
|
16
|
+
import type {
|
|
17
|
+
ApiKeySummary,
|
|
18
|
+
ApiKeyTeamOption,
|
|
19
|
+
ApiKeyProjectOption,
|
|
20
|
+
ApiScope,
|
|
21
|
+
} from '#devpush/lib/api-key-schema'
|
|
22
|
+
import type { TeamRole } from '#devpush/lib/team-role'
|
|
23
|
+
import { db } from '#devpush/server/db'
|
|
24
|
+
import type { AtomicStore } from '#devpush/server/db/atomic'
|
|
25
|
+
import {
|
|
26
|
+
apiGrants,
|
|
27
|
+
apiKeys,
|
|
28
|
+
projects,
|
|
29
|
+
teamMembers,
|
|
30
|
+
teams,
|
|
31
|
+
user,
|
|
32
|
+
} from '#devpush/server/db/schema'
|
|
33
|
+
import type { ApiPrincipal } from './authorization'
|
|
34
|
+
import { apiAuthorizationCondition } from './authorization'
|
|
35
|
+
import { getResourceColor } from '#devpush/server/resources/color'
|
|
36
|
+
|
|
37
|
+
type CredentialRow = {
|
|
38
|
+
id: string
|
|
39
|
+
userId: string
|
|
40
|
+
teamId: string
|
|
41
|
+
membershipId: string
|
|
42
|
+
roleCeiling: TeamRole
|
|
43
|
+
projectIds: string | null
|
|
44
|
+
scopes: string
|
|
45
|
+
expiresAt: number | null
|
|
46
|
+
revokedAt: number | null
|
|
47
|
+
createdAt: number
|
|
48
|
+
providerGrantId?: string | null
|
|
49
|
+
}
|
|
50
|
+
type KeyRow = CredentialRow & {
|
|
51
|
+
name: string
|
|
52
|
+
prefix: string
|
|
53
|
+
preset: ApiKeySummary['preset']
|
|
54
|
+
lastUsedAt: number | null
|
|
55
|
+
teamName: string
|
|
56
|
+
}
|
|
57
|
+
type Member = { membershipId: string; role: TeamRole }
|
|
58
|
+
|
|
59
|
+
const credentialColumnNames = [
|
|
60
|
+
'id',
|
|
61
|
+
'userId',
|
|
62
|
+
'teamId',
|
|
63
|
+
'membershipId',
|
|
64
|
+
'roleCeiling',
|
|
65
|
+
'projectIds',
|
|
66
|
+
'scopes',
|
|
67
|
+
'expiresAt',
|
|
68
|
+
'revokedAt',
|
|
69
|
+
'createdAt',
|
|
70
|
+
] as const
|
|
71
|
+
const keyColumnNames = [
|
|
72
|
+
...credentialColumnNames,
|
|
73
|
+
'name',
|
|
74
|
+
'prefix',
|
|
75
|
+
'preset',
|
|
76
|
+
'lastUsedAt',
|
|
77
|
+
'teamName',
|
|
78
|
+
] as const
|
|
79
|
+
|
|
80
|
+
async function readRows<T>(
|
|
81
|
+
store: AtomicStore,
|
|
82
|
+
query: SQL,
|
|
83
|
+
columns: readonly string[],
|
|
84
|
+
): Promise<T[]> {
|
|
85
|
+
const rows = await store.all(query)
|
|
86
|
+
// sqlite-proxy preserves positional rows; D1's raw SQL API returns records.
|
|
87
|
+
return rows.map(
|
|
88
|
+
(row) =>
|
|
89
|
+
(Array.isArray(row)
|
|
90
|
+
? Object.fromEntries(
|
|
91
|
+
columns.map((column, index) => [column, row[index]]),
|
|
92
|
+
)
|
|
93
|
+
: row) as T,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const credentialColumns = sql`credential.id, credential.user_id as userId, credential.team_id as teamId,
|
|
98
|
+
credential.membership_id as membershipId, credential.role_ceiling as roleCeiling,
|
|
99
|
+
credential.project_ids as projectIds, credential.scopes, credential.expires_at as expiresAt,
|
|
100
|
+
credential.revoked_at as revokedAt, credential.created_at as createdAt`
|
|
101
|
+
|
|
102
|
+
function activeMemberGuard(
|
|
103
|
+
userId: string,
|
|
104
|
+
teamId: string,
|
|
105
|
+
membershipId: string,
|
|
106
|
+
role?: TeamRole,
|
|
107
|
+
) {
|
|
108
|
+
return sql`exists(select 1 from ${teamMembers} as member
|
|
109
|
+
join ${user} as actor on actor.id = member.user_id
|
|
110
|
+
join ${teams} as member_team on member_team.id = member.team_id
|
|
111
|
+
where member.id = ${membershipId} and member.user_id = ${userId} and member.team_id = ${teamId}
|
|
112
|
+
and actor.status = 'active' and member_team.status = 'active'
|
|
113
|
+
${role ? sql`and member.role = ${role}` : sql``})`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function requireMember(
|
|
117
|
+
store: AtomicStore,
|
|
118
|
+
userId: string,
|
|
119
|
+
teamId: string,
|
|
120
|
+
): Promise<Member> {
|
|
121
|
+
const [member] = await readRows<Member>(
|
|
122
|
+
store,
|
|
123
|
+
sql`select member.id as membershipId, member.role
|
|
124
|
+
from ${teamMembers} as member join ${user} as actor on actor.id = member.user_id
|
|
125
|
+
join ${teams} as member_team on member_team.id = member.team_id
|
|
126
|
+
where member.user_id = ${userId} and member.team_id = ${teamId}
|
|
127
|
+
and actor.status = 'active' and member_team.status = 'active' limit 1`,
|
|
128
|
+
['membershipId', 'role'],
|
|
129
|
+
)
|
|
130
|
+
if (!member) throw new PublicError('FORBIDDEN')
|
|
131
|
+
return member
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function requireActiveUser(store: AtomicStore, userId: string) {
|
|
135
|
+
const rows = await store.all(
|
|
136
|
+
sql`select id from ${user} where id = ${userId} and status = 'active' limit 1`,
|
|
137
|
+
)
|
|
138
|
+
if (rows.length !== 1) throw new PublicError('UNAUTHORIZED')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function projectSelectionGuard(teamId: string, projectIds: string[] | null) {
|
|
142
|
+
return projectIds === null
|
|
143
|
+
? sql`1`
|
|
144
|
+
: sql`(select count(*) from ${projects} where team_id = ${teamId} and status = 'active'
|
|
145
|
+
and id in (${sql.join(
|
|
146
|
+
projectIds.map((id) => sql`${id}`),
|
|
147
|
+
sql`, `,
|
|
148
|
+
)})) = ${projectIds.length}`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function principalFrom(
|
|
152
|
+
row: CredentialRow,
|
|
153
|
+
currentRole: TeamRole,
|
|
154
|
+
credentialKind: 'api_key' | 'oauth',
|
|
155
|
+
): ApiPrincipal {
|
|
156
|
+
const roleCeiling =
|
|
157
|
+
apiRoleRank[currentRole] < apiRoleRank[row.roleCeiling]
|
|
158
|
+
? currentRole
|
|
159
|
+
: row.roleCeiling
|
|
160
|
+
const storedScopes = JSON.parse(row.scopes) as string[]
|
|
161
|
+
return {
|
|
162
|
+
userId: row.userId,
|
|
163
|
+
teamId: row.teamId,
|
|
164
|
+
membershipId: row.membershipId,
|
|
165
|
+
roleCeiling,
|
|
166
|
+
projectIds: row.projectIds === null ? null : JSON.parse(row.projectIds),
|
|
167
|
+
scopes: storedScopes.filter(
|
|
168
|
+
(scope): scope is ApiScope =>
|
|
169
|
+
scope in apiScopeDefinitions &&
|
|
170
|
+
apiRoleRank[roleCeiling] >=
|
|
171
|
+
apiRoleRank[apiScopeDefinitions[scope as ApiScope].minimumRole],
|
|
172
|
+
),
|
|
173
|
+
credentialId: row.id,
|
|
174
|
+
credentialKind,
|
|
175
|
+
providerGrantId: row.providerGrantId,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function resolveCredential(
|
|
180
|
+
store: AtomicStore,
|
|
181
|
+
row: CredentialRow | undefined,
|
|
182
|
+
credentialKind: 'api_key' | 'oauth',
|
|
183
|
+
) {
|
|
184
|
+
if (
|
|
185
|
+
!row ||
|
|
186
|
+
row.revokedAt !== null ||
|
|
187
|
+
(row.expiresAt === null
|
|
188
|
+
? credentialKind === 'oauth'
|
|
189
|
+
: row.expiresAt <= Date.now())
|
|
190
|
+
)
|
|
191
|
+
throw new PublicError('UNAUTHORIZED')
|
|
192
|
+
let member: Member
|
|
193
|
+
try {
|
|
194
|
+
member = await requireMember(store, row.userId, row.teamId)
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (error instanceof PublicError) throw new PublicError('UNAUTHORIZED')
|
|
197
|
+
throw error
|
|
198
|
+
}
|
|
199
|
+
if (member.membershipId !== row.membershipId)
|
|
200
|
+
throw new PublicError('UNAUTHORIZED')
|
|
201
|
+
return principalFrom(row, member.role, credentialKind)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function hashApiSecret(secret: string) {
|
|
205
|
+
const digest = await crypto.subtle.digest(
|
|
206
|
+
'SHA-256',
|
|
207
|
+
new TextEncoder().encode(secret),
|
|
208
|
+
)
|
|
209
|
+
return Array.from(new Uint8Array(digest), (byte) =>
|
|
210
|
+
byte.toString(16).padStart(2, '0'),
|
|
211
|
+
).join('')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function newSecret() {
|
|
215
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32))
|
|
216
|
+
return `dp_${Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')}`
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Public bearer authentication intentionally ignores browser cookies. */
|
|
220
|
+
export async function authenticateApiKey(
|
|
221
|
+
request: Request,
|
|
222
|
+
store: AtomicStore = db,
|
|
223
|
+
): Promise<ApiPrincipal> {
|
|
224
|
+
const match = request.headers
|
|
225
|
+
.get('authorization')
|
|
226
|
+
?.match(/^Bearer (dp_[a-f0-9]{64})$/i)
|
|
227
|
+
if (!match) throw new PublicError('UNAUTHORIZED')
|
|
228
|
+
const keyHash = await hashApiSecret(match[1]!)
|
|
229
|
+
const [row] = await readRows<CredentialRow>(
|
|
230
|
+
store,
|
|
231
|
+
sql`select ${credentialColumns} from ${apiKeys} as credential where key_hash = ${keyHash} limit 1`,
|
|
232
|
+
credentialColumnNames,
|
|
233
|
+
)
|
|
234
|
+
const principal = await resolveCredential(store, row, 'api_key')
|
|
235
|
+
const rows =
|
|
236
|
+
await store.all(sql`update ${apiKeys} set last_used_at = ${Date.now()} where id = ${principal.credentialId}
|
|
237
|
+
and ${apiAuthorizationCondition(principal)} returning id`)
|
|
238
|
+
if (rows.length !== 1) throw new PublicError('UNAUTHORIZED')
|
|
239
|
+
return principal
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function resolveApiGrant(
|
|
243
|
+
grantId: string,
|
|
244
|
+
store: AtomicStore = db,
|
|
245
|
+
): Promise<ApiPrincipal> {
|
|
246
|
+
const [row] = await readRows<CredentialRow>(
|
|
247
|
+
store,
|
|
248
|
+
sql`select ${credentialColumns}, credential.provider_grant_id as providerGrantId from ${apiGrants} as credential where id = ${grantId} limit 1`,
|
|
249
|
+
[...credentialColumnNames, 'providerGrantId'],
|
|
250
|
+
)
|
|
251
|
+
const principal = await resolveCredential(store, row, 'oauth')
|
|
252
|
+
const valid = await store.all(
|
|
253
|
+
sql`select 1 where ${apiAuthorizationCondition(principal)}`,
|
|
254
|
+
)
|
|
255
|
+
if (valid.length !== 1) throw new PublicError('UNAUTHORIZED')
|
|
256
|
+
return principal
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function keySummary(row: KeyRow, available: boolean): ApiKeySummary {
|
|
260
|
+
return {
|
|
261
|
+
id: row.id,
|
|
262
|
+
name: row.name,
|
|
263
|
+
prefix: row.prefix,
|
|
264
|
+
teamId: row.teamId,
|
|
265
|
+
teamName: row.teamName,
|
|
266
|
+
projectIds: row.projectIds === null ? null : JSON.parse(row.projectIds),
|
|
267
|
+
preset: row.preset,
|
|
268
|
+
scopes: JSON.parse(row.scopes),
|
|
269
|
+
status:
|
|
270
|
+
row.revokedAt !== null
|
|
271
|
+
? 'revoked'
|
|
272
|
+
: row.expiresAt !== null && row.expiresAt <= Date.now()
|
|
273
|
+
? 'expired'
|
|
274
|
+
: available
|
|
275
|
+
? 'active'
|
|
276
|
+
: 'unavailable',
|
|
277
|
+
createdAt: new Date(row.createdAt).toISOString(),
|
|
278
|
+
expiresAt:
|
|
279
|
+
row.expiresAt === null ? null : new Date(row.expiresAt).toISOString(),
|
|
280
|
+
lastUsedAt:
|
|
281
|
+
row.lastUsedAt === null ? null : new Date(row.lastUsedAt).toISOString(),
|
|
282
|
+
revokedAt:
|
|
283
|
+
row.revokedAt === null ? null : new Date(row.revokedAt).toISOString(),
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function readKey(store: AtomicStore, userId: string, id: string) {
|
|
288
|
+
const [row] = await readRows<KeyRow>(
|
|
289
|
+
store,
|
|
290
|
+
sql`select ${credentialColumns}, credential.name, credential.prefix, credential.preset,
|
|
291
|
+
credential.last_used_at as lastUsedAt, member_team.name as teamName
|
|
292
|
+
from ${apiKeys} as credential join ${teams} as member_team on member_team.id = credential.team_id
|
|
293
|
+
where credential.id = ${id} and credential.user_id = ${userId} limit 1`,
|
|
294
|
+
keyColumnNames,
|
|
295
|
+
)
|
|
296
|
+
if (!row) throw new PublicError('FORBIDDEN')
|
|
297
|
+
return row
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export async function getApiKeySettings(
|
|
301
|
+
userId: string,
|
|
302
|
+
store: AtomicStore = db,
|
|
303
|
+
options: { page?: number } = {},
|
|
304
|
+
): Promise<{
|
|
305
|
+
keys: ApiKeySummary[]
|
|
306
|
+
teams: ApiKeyTeamOption[]
|
|
307
|
+
pagination: {
|
|
308
|
+
page: number
|
|
309
|
+
pageSize: number
|
|
310
|
+
total: number
|
|
311
|
+
totalPages: number
|
|
312
|
+
}
|
|
313
|
+
}> {
|
|
314
|
+
await requireActiveUser(store, userId)
|
|
315
|
+
const requested = apiKeySettingsSchema.parse(options)
|
|
316
|
+
const [count] = await readRows<{ total: number }>(
|
|
317
|
+
store,
|
|
318
|
+
sql`select count(*) as total from ${apiKeys} where user_id = ${userId}`,
|
|
319
|
+
['total'],
|
|
320
|
+
)
|
|
321
|
+
const pageSize = 25
|
|
322
|
+
const total = Number(count?.total ?? 0)
|
|
323
|
+
const totalPages = Math.max(1, Math.ceil(total / pageSize))
|
|
324
|
+
const page = Math.min(requested.page, totalPages)
|
|
325
|
+
const teamOptions = await getApiKeyTeams(userId, {}, store)
|
|
326
|
+
const keyRows = await readRows<KeyRow>(
|
|
327
|
+
store,
|
|
328
|
+
sql`select ${credentialColumns}, credential.name, credential.prefix, credential.preset,
|
|
329
|
+
credential.last_used_at as lastUsedAt, member_team.name as teamName
|
|
330
|
+
from ${apiKeys} as credential join ${teams} as member_team on member_team.id = credential.team_id
|
|
331
|
+
where credential.user_id = ${userId}
|
|
332
|
+
order by credential.revoked_at is not null, (credential.expires_at is not null and credential.expires_at <= ${Date.now()}), credential.created_at desc, credential.id
|
|
333
|
+
limit ${pageSize} offset ${(page - 1) * pageSize}`,
|
|
334
|
+
keyColumnNames,
|
|
335
|
+
)
|
|
336
|
+
const keys = keyRows.map((row) => keySummary(row, true))
|
|
337
|
+
return {
|
|
338
|
+
keys,
|
|
339
|
+
pagination: { page, pageSize, total, totalPages },
|
|
340
|
+
teams: teamOptions.items,
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function searchPattern(query: string) {
|
|
345
|
+
return `%${query.replace(/[\\%_]/g, '\\$&')}%`
|
|
346
|
+
}
|
|
347
|
+
function pageInfo(total: number, requested: number) {
|
|
348
|
+
const pageSize = 25
|
|
349
|
+
const totalPages = Math.max(1, Math.ceil(total / pageSize))
|
|
350
|
+
return { page: Math.min(requested, totalPages), pageSize, total, totalPages }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export async function getApiKeyTeams(
|
|
354
|
+
userId: string,
|
|
355
|
+
input: unknown = {},
|
|
356
|
+
store: AtomicStore = db,
|
|
357
|
+
) {
|
|
358
|
+
await requireActiveUser(store, userId)
|
|
359
|
+
const data = apiKeyTeamsSchema.parse(input)
|
|
360
|
+
const filter = sql`member.user_id = ${userId} and member_team.status = 'active'
|
|
361
|
+
and (member_team.name like ${searchPattern(data.query)} escape '\\' or member_team.slug like ${searchPattern(data.query)} escape '\\')`
|
|
362
|
+
const [count] = await readRows<{ total: number }>(
|
|
363
|
+
store,
|
|
364
|
+
sql`select count(*) as total from ${teamMembers} as member
|
|
365
|
+
join ${teams} as member_team on member_team.id = member.team_id where ${filter}`,
|
|
366
|
+
['total'],
|
|
367
|
+
)
|
|
368
|
+
const pagination = pageInfo(Number(count?.total ?? 0), data.page)
|
|
369
|
+
const rows = await readRows<Omit<ApiKeyTeamOption, 'projects' | 'color'>>(
|
|
370
|
+
store,
|
|
371
|
+
sql`select member_team.id, member_team.name, member_team.slug, member.role, member_team.avatar_path as image
|
|
372
|
+
from ${teamMembers} as member join ${teams} as member_team on member_team.id = member.team_id
|
|
373
|
+
where ${filter} order by member_team.name, member_team.id limit ${pagination.pageSize} offset ${(pagination.page - 1) * pagination.pageSize}`,
|
|
374
|
+
['id', 'name', 'slug', 'role', 'image'],
|
|
375
|
+
)
|
|
376
|
+
return {
|
|
377
|
+
items: rows.map((row) => ({
|
|
378
|
+
...row,
|
|
379
|
+
color: getResourceColor(row.id),
|
|
380
|
+
projects: [],
|
|
381
|
+
})),
|
|
382
|
+
pagination,
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export async function getApiKeyProjects(
|
|
387
|
+
userId: string,
|
|
388
|
+
input: unknown,
|
|
389
|
+
store: AtomicStore = db,
|
|
390
|
+
) {
|
|
391
|
+
const data = apiKeyProjectsSchema.parse(input)
|
|
392
|
+
await requireMember(store, userId, data.teamId)
|
|
393
|
+
const filter = sql`${projects.teamId} = ${data.teamId} and ${projects.status} = 'active'
|
|
394
|
+
and (${projects.name} like ${searchPattern(data.query)} escape '\\' or ${projects.slug} like ${searchPattern(data.query)} escape '\\')`
|
|
395
|
+
const [count] = await readRows<{ total: number }>(
|
|
396
|
+
store,
|
|
397
|
+
sql`select count(*) as total from ${projects} where ${filter}`,
|
|
398
|
+
['total'],
|
|
399
|
+
)
|
|
400
|
+
const pagination = pageInfo(Number(count?.total ?? 0), data.page)
|
|
401
|
+
const rows = await readRows<Omit<ApiKeyProjectOption, 'color'>>(
|
|
402
|
+
store,
|
|
403
|
+
sql`select ${projects.id} as id,${projects.name} as name,${projects.slug} as slug,${projects.avatarPath} as avatarUrl
|
|
404
|
+
from ${projects} where ${filter} order by ${projects.name}, ${projects.id} limit ${pagination.pageSize} offset ${(pagination.page - 1) * pagination.pageSize}`,
|
|
405
|
+
['id', 'name', 'slug', 'avatarUrl'],
|
|
406
|
+
)
|
|
407
|
+
return {
|
|
408
|
+
items: rows.map((row) => ({ ...row, color: getResourceColor(row.id) })),
|
|
409
|
+
pagination,
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export async function createApiKey(
|
|
414
|
+
userId: string,
|
|
415
|
+
input: unknown,
|
|
416
|
+
store: AtomicStore = db,
|
|
417
|
+
) {
|
|
418
|
+
const data = createApiKeySchema.parse(input)
|
|
419
|
+
const member = await requireMember(store, userId, data.teamId)
|
|
420
|
+
return withWorkspaceAccess(
|
|
421
|
+
data.teamId,
|
|
422
|
+
'credential.create',
|
|
423
|
+
async () => {
|
|
424
|
+
const scopes = scopesForApiPreset(
|
|
425
|
+
data.preset,
|
|
426
|
+
member.role,
|
|
427
|
+
data.includeLogs,
|
|
428
|
+
)
|
|
429
|
+
const id = crypto.randomUUID()
|
|
430
|
+
const secret = newSecret()
|
|
431
|
+
const keyHash = await hashApiSecret(secret)
|
|
432
|
+
const now = Date.now()
|
|
433
|
+
const expiresAt =
|
|
434
|
+
data.expiresInDays === null
|
|
435
|
+
? null
|
|
436
|
+
: now + data.expiresInDays * 86_400_000
|
|
437
|
+
const inserted =
|
|
438
|
+
await store.all(sql`insert into ${apiKeys} (id,user_id,team_id,membership_id,name,prefix,key_hash,role_ceiling,preset,project_ids,scopes,expires_at,created_at)
|
|
439
|
+
select ${id},${userId},${data.teamId},${member.membershipId},${data.name},${secret.slice(0, 11)},${keyHash},${member.role},${data.preset},${data.projectIds === null ? null : JSON.stringify(data.projectIds)},${JSON.stringify(scopes)},${expiresAt},${now}
|
|
440
|
+
where ${activeMemberGuard(userId, data.teamId, member.membershipId, member.role)}
|
|
441
|
+
and ${projectSelectionGuard(data.teamId, data.projectIds)}
|
|
442
|
+
and (select count(*) from ${apiKeys} where user_id = ${userId} and revoked_at is null
|
|
443
|
+
and (expires_at is null or expires_at > cast(unixepoch('subsec') * 1000 as integer))) < 100
|
|
444
|
+
returning id`)
|
|
445
|
+
if (inserted.length !== 1)
|
|
446
|
+
throw new PublicError('API_KEY_CREATION_UNAVAILABLE')
|
|
447
|
+
return { key: keySummary(await readKey(store, userId, id), true), secret }
|
|
448
|
+
},
|
|
449
|
+
{ kind: 'user', userId: userId },
|
|
450
|
+
)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export async function revokeApiKey(
|
|
454
|
+
userId: string,
|
|
455
|
+
id: string,
|
|
456
|
+
store: AtomicStore = db,
|
|
457
|
+
) {
|
|
458
|
+
await requireActiveUser(store, userId)
|
|
459
|
+
const result =
|
|
460
|
+
await store.all(sql`update ${apiKeys} set revoked_at = coalesce(revoked_at,${Date.now()})
|
|
461
|
+
where id = ${id} and user_id = ${userId} and exists(select 1 from ${user} where id = ${userId} and status = 'active') returning id`)
|
|
462
|
+
if (result.length !== 1) throw new PublicError('FORBIDDEN')
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export async function rotateApiKey(
|
|
466
|
+
userId: string,
|
|
467
|
+
id: string,
|
|
468
|
+
store: AtomicStore = db,
|
|
469
|
+
) {
|
|
470
|
+
const old = await readKey(store, userId, id)
|
|
471
|
+
if (old.revokedAt !== null) throw new PublicError('API_KEY_REVOKED')
|
|
472
|
+
if (old.expiresAt !== null && old.expiresAt <= Date.now())
|
|
473
|
+
throw new PublicError('API_KEY_EXPIRED')
|
|
474
|
+
const member = await requireMember(store, userId, old.teamId)
|
|
475
|
+
return withWorkspaceAccess(
|
|
476
|
+
old.teamId,
|
|
477
|
+
'credential.rotate',
|
|
478
|
+
async () => {
|
|
479
|
+
if (member.membershipId !== old.membershipId)
|
|
480
|
+
throw new PublicError('FORBIDDEN')
|
|
481
|
+
const principal = principalFrom(old, member.role, 'api_key')
|
|
482
|
+
const newId = crypto.randomUUID()
|
|
483
|
+
const secret = newSecret()
|
|
484
|
+
const keyHash = await hashApiSecret(secret)
|
|
485
|
+
const now = Date.now()
|
|
486
|
+
const expiresAt =
|
|
487
|
+
old.expiresAt === null
|
|
488
|
+
? null
|
|
489
|
+
: now +
|
|
490
|
+
Math.min(
|
|
491
|
+
apiKeyMaxExpirationDays * 86_400_000,
|
|
492
|
+
Math.max(86_400_000, old.expiresAt - old.createdAt),
|
|
493
|
+
)
|
|
494
|
+
const result = await store.batch([
|
|
495
|
+
store.all(sql`insert into ${apiKeys} (id,user_id,team_id,membership_id,name,prefix,key_hash,role_ceiling,preset,project_ids,scopes,expires_at,created_at)
|
|
496
|
+
select ${newId},user_id,team_id,membership_id,name,${secret.slice(0, 11)},${keyHash},${principal.roleCeiling},preset,project_ids,${JSON.stringify(principal.scopes)},${expiresAt},${now}
|
|
497
|
+
from ${apiKeys} where id = ${id} and user_id = ${userId} and revoked_at is null
|
|
498
|
+
and (expires_at is null or expires_at > cast(unixepoch('subsec') * 1000 as integer))
|
|
499
|
+
and (select count(*) from ${apiKeys} where user_id = ${userId} and revoked_at is null
|
|
500
|
+
and (expires_at is null or expires_at > cast(unixepoch('subsec') * 1000 as integer))) <= 100
|
|
501
|
+
and ${activeMemberGuard(userId, old.teamId, old.membershipId, member.role)}
|
|
502
|
+
and ${projectSelectionGuard(old.teamId, principal.projectIds)} returning id`),
|
|
503
|
+
store.run(
|
|
504
|
+
sql`update ${apiKeys} set revoked_at = ${now} where id = ${id} and exists(select 1 from ${apiKeys} where id = ${newId})`,
|
|
505
|
+
),
|
|
506
|
+
])
|
|
507
|
+
if (result[0].length !== 1) throw new PublicError('FORBIDDEN')
|
|
508
|
+
return {
|
|
509
|
+
key: keySummary(await readKey(store, userId, newId), true),
|
|
510
|
+
secret,
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
{ kind: 'user', userId: userId },
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export async function createApiGrant(
|
|
518
|
+
input: {
|
|
519
|
+
userId: string
|
|
520
|
+
teamId: string
|
|
521
|
+
projectIds: string[] | null
|
|
522
|
+
preset: 'read' | 'deploy' | 'manage'
|
|
523
|
+
expiresInDays: number
|
|
524
|
+
includeLogs?: boolean
|
|
525
|
+
providerGrantId?: string
|
|
526
|
+
},
|
|
527
|
+
store: AtomicStore = db,
|
|
528
|
+
) {
|
|
529
|
+
const data = createApiKeySchema
|
|
530
|
+
.safeExtend({ expiresInDays: z.number().int().min(1).max(90) })
|
|
531
|
+
.parse({
|
|
532
|
+
name: 'MCP connection',
|
|
533
|
+
teamId: input.teamId,
|
|
534
|
+
projectIds: input.projectIds,
|
|
535
|
+
preset: input.preset,
|
|
536
|
+
expiresInDays: input.expiresInDays,
|
|
537
|
+
includeLogs: input.includeLogs ?? false,
|
|
538
|
+
})
|
|
539
|
+
const member = await requireMember(store, input.userId, data.teamId)
|
|
540
|
+
return withWorkspaceAccess(
|
|
541
|
+
data.teamId,
|
|
542
|
+
'credential.create',
|
|
543
|
+
async () => {
|
|
544
|
+
const id = crypto.randomUUID()
|
|
545
|
+
const now = Date.now()
|
|
546
|
+
const rows =
|
|
547
|
+
await store.all(sql`insert into ${apiGrants} (id,user_id,team_id,membership_id,role_ceiling,project_ids,scopes,provider_grant_id,expires_at,created_at)
|
|
548
|
+
select ${id},${input.userId},${data.teamId},${member.membershipId},${member.role},${data.projectIds === null ? null : JSON.stringify(data.projectIds)},${JSON.stringify(scopesForApiPreset(data.preset, member.role, data.includeLogs))},${input.providerGrantId ?? null},${now + data.expiresInDays * 86_400_000},${now}
|
|
549
|
+
where ${activeMemberGuard(input.userId, data.teamId, member.membershipId, member.role)} and ${projectSelectionGuard(data.teamId, data.projectIds)}
|
|
550
|
+
and (select count(*) from ${apiGrants} where user_id = ${input.userId} and revoked_at is null and expires_at > ${now}) < 100 returning id`)
|
|
551
|
+
if (rows.length !== 1)
|
|
552
|
+
throw new PublicError('MCP_CONNECTION_CREATION_UNAVAILABLE')
|
|
553
|
+
return resolveApiGrant(id, store)
|
|
554
|
+
},
|
|
555
|
+
{ kind: 'user', userId: input.userId },
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export async function revokeApiGrant(
|
|
560
|
+
userId: string,
|
|
561
|
+
grantId: string,
|
|
562
|
+
store: AtomicStore = db,
|
|
563
|
+
) {
|
|
564
|
+
await requireActiveUser(store, userId)
|
|
565
|
+
const rows =
|
|
566
|
+
await store.all(sql`update ${apiGrants} set revoked_at = coalesce(revoked_at,${Date.now()}) where id = ${grantId} and user_id = ${userId}
|
|
567
|
+
and exists(select 1 from ${user} where id = ${userId} and status = 'active') returning id`)
|
|
568
|
+
if (rows.length !== 1) throw new PublicError('FORBIDDEN')
|
|
569
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { config } from '#devpush/server/config'
|
|
4
|
+
import { ApiError } from './errors'
|
|
5
|
+
|
|
6
|
+
const cursorSchema = z.strictObject({
|
|
7
|
+
version: z.literal(1),
|
|
8
|
+
context: z.string(),
|
|
9
|
+
position: z.number().int().nonnegative(),
|
|
10
|
+
id: z.string().max(256),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export function encodeApiCursor(context: string, position: number, id = '') {
|
|
14
|
+
const payload = Buffer.from(
|
|
15
|
+
JSON.stringify({ version: 1, context, position, id }),
|
|
16
|
+
).toString('base64url')
|
|
17
|
+
return `${payload}.${signature(payload)}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function decodeApiCursor(cursor: string | undefined, context: string) {
|
|
21
|
+
if (!cursor) return null
|
|
22
|
+
try {
|
|
23
|
+
if (cursor.length > 2048) throw new Error('length')
|
|
24
|
+
const [payload, signed, extra] = cursor.split('.')
|
|
25
|
+
if (!payload || !signed || extra) throw new Error('format')
|
|
26
|
+
const expected = Buffer.from(signature(payload))
|
|
27
|
+
const actual = Buffer.from(signed)
|
|
28
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected))
|
|
29
|
+
throw new Error('signature')
|
|
30
|
+
const value = cursorSchema.parse(
|
|
31
|
+
JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')),
|
|
32
|
+
)
|
|
33
|
+
if (value.context !== context) throw new Error('context')
|
|
34
|
+
return value
|
|
35
|
+
} catch {
|
|
36
|
+
throw new ApiError('INVALID_CURSOR', 400)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function apiFingerprint(value: unknown) {
|
|
41
|
+
return createHmac('sha256', fingerprintKey())
|
|
42
|
+
.update('devpush-api-fingerprint-v1\0')
|
|
43
|
+
.update(JSON.stringify(canonical(value)))
|
|
44
|
+
.digest('hex')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function signature(value: string) {
|
|
48
|
+
return createHmac('sha256', fingerprintKey())
|
|
49
|
+
.update('devpush-api-cursor-v1\0')
|
|
50
|
+
.update(value)
|
|
51
|
+
.digest('base64url')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function fingerprintKey() {
|
|
55
|
+
const value = config.DEVPUSH_SECRET_ENCRYPTION_KEY
|
|
56
|
+
if (!value)
|
|
57
|
+
throw new Error(
|
|
58
|
+
'DEVPUSH_SECRET_ENCRYPTION_KEY is required for API cursor and replay protection.',
|
|
59
|
+
)
|
|
60
|
+
return value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function canonical(value: unknown): unknown {
|
|
64
|
+
if (Array.isArray(value)) return value.map(canonical)
|
|
65
|
+
if (value && typeof value === 'object')
|
|
66
|
+
return Object.fromEntries(
|
|
67
|
+
Object.entries(value)
|
|
68
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
69
|
+
.map(([key, item]) => [key, canonical(item)]),
|
|
70
|
+
)
|
|
71
|
+
return value
|
|
72
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const apiErrorMessages = {
|
|
2
|
+
NOT_FOUND: 'The resource was not found.',
|
|
3
|
+
IDEMPOTENCY_REQUIRED: 'Provide an Idempotency-Key for this operation.',
|
|
4
|
+
IDEMPOTENCY_CONFLICT:
|
|
5
|
+
'This idempotency key was already used with different input.',
|
|
6
|
+
SETTINGS_CONFLICT:
|
|
7
|
+
'The resource settings changed. Read its current settings version and try again.',
|
|
8
|
+
OPERATION_CONFLICT:
|
|
9
|
+
'The operation could not be admitted in the current resource state.',
|
|
10
|
+
METHOD_NOT_ALLOWED: 'This HTTP method is not supported for this resource.',
|
|
11
|
+
INVALID_CURSOR: 'The cursor is invalid or belongs to a different query.',
|
|
12
|
+
UNSUPPORTED_MEDIA_TYPE: 'Use application/json for the request body.',
|
|
13
|
+
REQUEST_TOO_LARGE: 'The request body is too large.',
|
|
14
|
+
} as const
|
|
15
|
+
|
|
16
|
+
export class ApiError extends Error {
|
|
17
|
+
constructor(
|
|
18
|
+
readonly code: keyof typeof apiErrorMessages,
|
|
19
|
+
readonly status: number,
|
|
20
|
+
) {
|
|
21
|
+
super(apiErrorMessages[code])
|
|
22
|
+
this.name = 'ApiError'
|
|
23
|
+
}
|
|
24
|
+
}
|