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,33 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm'
|
|
2
|
+
import { db } from '#devpush/server/db'
|
|
3
|
+
import type { AtomicStore } from '#devpush/server/db/atomic'
|
|
4
|
+
|
|
5
|
+
export const API_AUDIT_RETENTION_MS = 30 * 86400_000
|
|
6
|
+
const batchSize = 500
|
|
7
|
+
|
|
8
|
+
// Expired idempotency records may remain longer than the guaranteed replay
|
|
9
|
+
// window until maintenance runs. A maintenance run always does bounded work.
|
|
10
|
+
export async function pruneApiHistory(
|
|
11
|
+
store: AtomicStore = db,
|
|
12
|
+
now = Date.now(),
|
|
13
|
+
) {
|
|
14
|
+
const auditBefore = now - API_AUDIT_RETENTION_MS
|
|
15
|
+
const [idempotency, audit, accessTokens, refreshTokens] = await store.batch([
|
|
16
|
+
store.all<{ id: string }>(sql`delete from api_idempotency where id in
|
|
17
|
+
(select id from api_idempotency where expires_at <= ${now} order by expires_at limit ${batchSize}) returning id`),
|
|
18
|
+
store.all<{ id: string }>(sql`delete from api_audit where id in
|
|
19
|
+
(select id from api_audit where created_at < ${auditBefore} order by created_at limit ${batchSize}) returning id`),
|
|
20
|
+
store.all<{ id: string }>(sql`delete from oauth_access_token where id in
|
|
21
|
+
(select id from oauth_access_token where expires_at <= ${now} order by expires_at limit ${batchSize}) returning id`),
|
|
22
|
+
// Preserve revoked/rotated refresh rows until expiry: their families still
|
|
23
|
+
// need reuse detection while any token in that family can be presented.
|
|
24
|
+
store.all<{ id: string }>(sql`delete from oauth_refresh_token where id in
|
|
25
|
+
(select id from oauth_refresh_token where expires_at <= ${now} order by expires_at limit ${batchSize}) returning id`),
|
|
26
|
+
])
|
|
27
|
+
return {
|
|
28
|
+
idempotency: idempotency.length,
|
|
29
|
+
audit: audit.length,
|
|
30
|
+
accessTokens: accessTokens.length,
|
|
31
|
+
refreshTokens: refreshTokens.length,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { eq, sql } from 'drizzle-orm'
|
|
2
|
+
import { isValidAllowlistPattern } from '#devpush/lib/allowlist-schema'
|
|
3
|
+
|
|
4
|
+
import { db } from '#devpush/server/db'
|
|
5
|
+
import { allowlist, user } from '#devpush/server/db/schema'
|
|
6
|
+
|
|
7
|
+
export { isValidAllowlistPattern } from '#devpush/lib/allowlist-schema'
|
|
8
|
+
|
|
9
|
+
export async function assertEmailCanAuthenticate(
|
|
10
|
+
email: string | null | undefined,
|
|
11
|
+
) {
|
|
12
|
+
const normalizedEmail = normalizeEmail(email)
|
|
13
|
+
|
|
14
|
+
if (!normalizedEmail) {
|
|
15
|
+
throw new Error('Email address is required.')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const [existingUsers, allowlisted] = await Promise.all([
|
|
19
|
+
db
|
|
20
|
+
.select({ status: user.status })
|
|
21
|
+
.from(user)
|
|
22
|
+
.where(sql`lower(${user.email}) = ${normalizedEmail}`)
|
|
23
|
+
.limit(1),
|
|
24
|
+
isEmailAllowlisted(normalizedEmail),
|
|
25
|
+
])
|
|
26
|
+
const existingUser = existingUsers.at(0)
|
|
27
|
+
|
|
28
|
+
if (existingUser?.status === 'banned') {
|
|
29
|
+
throw new Error('This account has been banned.')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (existingUser?.status === 'deleted') {
|
|
33
|
+
throw new Error('This account has been deleted.')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!allowlisted) {
|
|
37
|
+
throw new Error('This email address is not allowed to sign in.')
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function assertUserCanCreateSession(userId: string) {
|
|
42
|
+
const [currentUsers, entries] = await Promise.all([
|
|
43
|
+
db
|
|
44
|
+
.select({ email: user.email, status: user.status })
|
|
45
|
+
.from(user)
|
|
46
|
+
.where(eq(user.id, userId))
|
|
47
|
+
.limit(1),
|
|
48
|
+
listAllowlistEntries(),
|
|
49
|
+
])
|
|
50
|
+
const currentUser = currentUsers.at(0)
|
|
51
|
+
|
|
52
|
+
if (!currentUser || currentUser.status !== 'active') {
|
|
53
|
+
throw new Error('This account cannot sign in.')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!isEmailAllowedByEntries(currentUser.email, entries)) {
|
|
57
|
+
throw new Error('This email address is not allowed to sign in.')
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function isEmailAllowlisted(email: string) {
|
|
62
|
+
const normalizedEmail = normalizeEmail(email)
|
|
63
|
+
if (!normalizedEmail) return false
|
|
64
|
+
|
|
65
|
+
return isEmailAllowedByEntries(normalizedEmail, await listAllowlistEntries())
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function listAllowlistEntries() {
|
|
69
|
+
return db
|
|
70
|
+
.select({ type: allowlist.type, value: allowlist.value })
|
|
71
|
+
.from(allowlist)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isEmailAllowedByEntries(
|
|
75
|
+
email: string,
|
|
76
|
+
entries: Array<{ type: 'domain' | 'email' | 'pattern'; value: string }>,
|
|
77
|
+
) {
|
|
78
|
+
const normalizedEmail = normalizeEmail(email)
|
|
79
|
+
if (!normalizedEmail) return false
|
|
80
|
+
|
|
81
|
+
if (entries.length === 0) {
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const domain = normalizedEmail.split('@').at(1) ?? ''
|
|
86
|
+
|
|
87
|
+
return entries.some((entry) => {
|
|
88
|
+
const value = entry.value.trim().toLowerCase()
|
|
89
|
+
|
|
90
|
+
if (entry.type === 'email') {
|
|
91
|
+
return value === normalizedEmail
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (entry.type === 'domain') {
|
|
95
|
+
return value.replace(/^@/, '') === domain
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
if (!isValidAllowlistPattern(entry.value)) return false
|
|
100
|
+
return new RegExp(`^(?:${entry.value})$`, 'i').test(normalizedEmail)
|
|
101
|
+
} catch {
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function normalizeEmail(email: string | null | undefined) {
|
|
108
|
+
return email?.trim().toLowerCase() || null
|
|
109
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { config } from '#devpush/server/config'
|
|
2
|
+
|
|
3
|
+
export type AuthMethods = {
|
|
4
|
+
email: boolean
|
|
5
|
+
github: boolean
|
|
6
|
+
google: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getGitHubOAuthCredentials() {
|
|
10
|
+
const clientId = config.GITHUB_APP_CLIENT_ID
|
|
11
|
+
const clientSecret = config.GITHUB_APP_CLIENT_SECRET
|
|
12
|
+
|
|
13
|
+
if (!clientId || !clientSecret) {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return { clientId, clientSecret }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getAuthMethods(): AuthMethods {
|
|
21
|
+
const githubCredentials = getGitHubOAuthCredentials()
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
email: Boolean(config.EMAIL_FROM && config.RESEND_API_KEY),
|
|
25
|
+
github: Boolean(githubCredentials),
|
|
26
|
+
google: Boolean(config.GOOGLE_CLIENT_ID && config.GOOGLE_CLIENT_SECRET),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { fetchGitHub, readGitHubJson } from '#devpush/server/github/http'
|
|
2
|
+
import { readProviderToken, storeProviderToken } from './provider-tokens'
|
|
3
|
+
import { drizzleAdapter } from '@better-auth/drizzle-adapter'
|
|
4
|
+
import { betterAuth } from 'better-auth'
|
|
5
|
+
import { emailOTP } from 'better-auth/plugins/email-otp'
|
|
6
|
+
import { tanstackStartCookies } from 'better-auth/tanstack-start'
|
|
7
|
+
import { and, eq, gt, sql } from 'drizzle-orm'
|
|
8
|
+
import { decodeJwt } from 'jose'
|
|
9
|
+
import type { GithubProfile, GoogleProfile } from 'better-auth/social-providers'
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
getAuthMethods,
|
|
13
|
+
getGitHubOAuthCredentials,
|
|
14
|
+
} from '#devpush/server/auth/auth-methods'
|
|
15
|
+
import { db } from '#devpush/server/db'
|
|
16
|
+
import { replacePendingIdentity } from '#devpush/server/auth/provider-identity'
|
|
17
|
+
import * as schema from '#devpush/server/db/schema'
|
|
18
|
+
import {
|
|
19
|
+
assertEmailCanAuthenticate,
|
|
20
|
+
assertUserCanCreateSession,
|
|
21
|
+
} from '#devpush/server/auth/access'
|
|
22
|
+
import {
|
|
23
|
+
sendEmailChangeEmail,
|
|
24
|
+
sendEmailVerificationEmail,
|
|
25
|
+
sendOtpEmail,
|
|
26
|
+
} from '#devpush/server/email/service'
|
|
27
|
+
import { config } from '#devpush/server/config'
|
|
28
|
+
import { requireAuthEmailDelivery } from '#devpush/server/auth/email-delivery'
|
|
29
|
+
import {
|
|
30
|
+
assertAllowedAuthEmailChange,
|
|
31
|
+
requireAllowedEmailChanges,
|
|
32
|
+
} from '#devpush/server/auth/email-change'
|
|
33
|
+
import { ensureUserBootstrap } from '#devpush/server/teams/service'
|
|
34
|
+
import { decryptSecret, encryptSecret } from '#devpush/server/secrets/crypto'
|
|
35
|
+
import { mcpOAuthPlugins } from '#devpush/server/mcp/oauth-provider'
|
|
36
|
+
|
|
37
|
+
export const authMethods = getAuthMethods()
|
|
38
|
+
const githubCredentials = getGitHubOAuthCredentials()
|
|
39
|
+
|
|
40
|
+
type ProviderIdentity = {
|
|
41
|
+
name?: string | null
|
|
42
|
+
email?: string | null
|
|
43
|
+
username?: string | null
|
|
44
|
+
image?: string | null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type OAuthTokens = {
|
|
48
|
+
accessToken?: string
|
|
49
|
+
idToken?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type GitHubProfile = GithubProfile
|
|
53
|
+
|
|
54
|
+
type GitHubEmail = {
|
|
55
|
+
email: string
|
|
56
|
+
primary: boolean
|
|
57
|
+
verified: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const pendingProviderIdentityTtlMs = 5 * 60 * 1000
|
|
61
|
+
|
|
62
|
+
function createAuth() {
|
|
63
|
+
return betterAuth({
|
|
64
|
+
appName: 'devpush',
|
|
65
|
+
secret: config.BETTER_AUTH_SECRET,
|
|
66
|
+
baseURL: config.BETTER_AUTH_URL,
|
|
67
|
+
basePath: '/api/auth',
|
|
68
|
+
// This server-only bridge is called directly by the MCP handler. OAuth
|
|
69
|
+
// protocol endpoints remain served by Better Auth.
|
|
70
|
+
disabledPaths: ['/mcp/verify', '/token', '/update-user'],
|
|
71
|
+
advanced: {
|
|
72
|
+
cookiePrefix: 'devpush',
|
|
73
|
+
},
|
|
74
|
+
database: drizzleAdapter(db, {
|
|
75
|
+
provider: 'sqlite',
|
|
76
|
+
schema,
|
|
77
|
+
}),
|
|
78
|
+
session: {
|
|
79
|
+
cookieCache: {
|
|
80
|
+
enabled: true,
|
|
81
|
+
maxAge: 60,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
user: {
|
|
85
|
+
additionalFields: {
|
|
86
|
+
defaultTeamId: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
required: false,
|
|
89
|
+
input: false,
|
|
90
|
+
},
|
|
91
|
+
username: {
|
|
92
|
+
type: 'string',
|
|
93
|
+
required: false,
|
|
94
|
+
input: false,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
changeEmail: {
|
|
98
|
+
enabled: true,
|
|
99
|
+
sendChangeEmailConfirmation: async ({ newEmail, url, user }) => {
|
|
100
|
+
await sendEmailChangeEmail({
|
|
101
|
+
currentEmail: user.email,
|
|
102
|
+
newEmail,
|
|
103
|
+
verifyUrl: url,
|
|
104
|
+
})
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
emailVerification: {
|
|
109
|
+
sendVerificationEmail: async ({ url, user }) => {
|
|
110
|
+
await sendEmailVerificationEmail({
|
|
111
|
+
email: user.email,
|
|
112
|
+
verifyUrl: url,
|
|
113
|
+
})
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
databaseHooks: {
|
|
117
|
+
account: {
|
|
118
|
+
update: {
|
|
119
|
+
before: async (updatedAccount) => ({
|
|
120
|
+
data: {
|
|
121
|
+
...updatedAccount,
|
|
122
|
+
...(updatedAccount.idToken
|
|
123
|
+
? { idToken: await storeProviderToken(updatedAccount.idToken) }
|
|
124
|
+
: {}),
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
},
|
|
128
|
+
create: {
|
|
129
|
+
before: async (createdAccount) => {
|
|
130
|
+
const identity = await readPendingProviderIdentity(
|
|
131
|
+
getProviderIdentityKey(
|
|
132
|
+
createdAccount.providerId,
|
|
133
|
+
createdAccount.accountId,
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const idToken = createdAccount.idToken
|
|
138
|
+
? await storeProviderToken(createdAccount.idToken)
|
|
139
|
+
: createdAccount.idToken
|
|
140
|
+
if (!identity) return { data: { idToken } }
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
data: {
|
|
144
|
+
idToken,
|
|
145
|
+
providerAccountName: identity.name ?? undefined,
|
|
146
|
+
providerAccountEmail: identity.email ?? undefined,
|
|
147
|
+
providerAccountUsername: identity.username ?? undefined,
|
|
148
|
+
providerAccountImage: identity.image ?? undefined,
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
after: async (createdAccount) => {
|
|
153
|
+
await clearPendingProviderIdentity(
|
|
154
|
+
getProviderIdentityKey(
|
|
155
|
+
createdAccount.providerId,
|
|
156
|
+
createdAccount.accountId,
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
session: {
|
|
163
|
+
create: {
|
|
164
|
+
before: async (createdSession) => {
|
|
165
|
+
await assertUserCanCreateSession(createdSession.userId)
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
user: {
|
|
170
|
+
update: {
|
|
171
|
+
before: async (updatedUser) => {
|
|
172
|
+
if (updatedUser.email)
|
|
173
|
+
await assertAllowedAuthEmailChange(updatedUser.email)
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
create: {
|
|
177
|
+
before: async (createdUser) => {
|
|
178
|
+
await assertEmailCanAuthenticate(createdUser.email)
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
data: {
|
|
182
|
+
username: await getAvailableUsername(
|
|
183
|
+
createdUser.name || createdUser.email,
|
|
184
|
+
),
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
after: async (createdUser) => {
|
|
189
|
+
try {
|
|
190
|
+
await ensureUserBootstrap(createdUser.id)
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error('Failed to bootstrap user', error)
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
socialProviders: {
|
|
199
|
+
...(githubCredentials
|
|
200
|
+
? {
|
|
201
|
+
github: {
|
|
202
|
+
clientId: githubCredentials.clientId,
|
|
203
|
+
clientSecret: githubCredentials.clientSecret,
|
|
204
|
+
getUserInfo: getGitHubUserInfo,
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
: {}),
|
|
208
|
+
...(authMethods.google
|
|
209
|
+
? {
|
|
210
|
+
google: {
|
|
211
|
+
clientId: config.GOOGLE_CLIENT_ID as string,
|
|
212
|
+
clientSecret: config.GOOGLE_CLIENT_SECRET as string,
|
|
213
|
+
getUserInfo: getGoogleUserInfo,
|
|
214
|
+
},
|
|
215
|
+
}
|
|
216
|
+
: {}),
|
|
217
|
+
},
|
|
218
|
+
account: {
|
|
219
|
+
encryptOAuthTokens: true,
|
|
220
|
+
additionalFields: {
|
|
221
|
+
providerAccountName: {
|
|
222
|
+
type: 'string',
|
|
223
|
+
required: false,
|
|
224
|
+
input: false,
|
|
225
|
+
},
|
|
226
|
+
providerAccountEmail: {
|
|
227
|
+
type: 'string',
|
|
228
|
+
required: false,
|
|
229
|
+
input: false,
|
|
230
|
+
},
|
|
231
|
+
providerAccountUsername: {
|
|
232
|
+
type: 'string',
|
|
233
|
+
required: false,
|
|
234
|
+
input: false,
|
|
235
|
+
},
|
|
236
|
+
providerAccountImage: {
|
|
237
|
+
type: 'string',
|
|
238
|
+
required: false,
|
|
239
|
+
input: false,
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
accountLinking: {
|
|
243
|
+
allowDifferentEmails: false,
|
|
244
|
+
enabled: true,
|
|
245
|
+
trustedProviders: ['github', 'google'],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
plugins: [
|
|
249
|
+
...mcpOAuthPlugins(),
|
|
250
|
+
requireAllowedEmailChanges(),
|
|
251
|
+
requireAuthEmailDelivery(),
|
|
252
|
+
...(authMethods.email
|
|
253
|
+
? [
|
|
254
|
+
emailOTP({
|
|
255
|
+
allowedAttempts: 5,
|
|
256
|
+
expiresIn: 300,
|
|
257
|
+
resendStrategy: 'reuse',
|
|
258
|
+
storeOTP: 'encrypted',
|
|
259
|
+
sendVerificationOTP: async ({ email, otp, type }) => {
|
|
260
|
+
if (type !== 'sign-in') {
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
await assertEmailCanAuthenticate(email)
|
|
265
|
+
await sendOtpEmail(email, otp)
|
|
266
|
+
},
|
|
267
|
+
}),
|
|
268
|
+
]
|
|
269
|
+
: []),
|
|
270
|
+
tanstackStartCookies(),
|
|
271
|
+
],
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
type Auth = ReturnType<typeof createAuth>
|
|
276
|
+
let initializedAuth: Auth | undefined
|
|
277
|
+
// Better Auth plugin initialization may seed provider resources. Defer that I/O
|
|
278
|
+
// until a request uses the auth API; Workers cannot perform it at module scope.
|
|
279
|
+
export const auth = new Proxy({} as Auth, {
|
|
280
|
+
get(_target, property) {
|
|
281
|
+
const instance = (initializedAuth ??= createAuth())
|
|
282
|
+
const value: unknown = Reflect.get(instance, property, instance)
|
|
283
|
+
return typeof value === 'function' ? value.bind(instance) : value
|
|
284
|
+
},
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
function getProviderIdentityKey(providerId: string, accountId: string) {
|
|
288
|
+
return `${providerId}:${accountId}`
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function rememberProviderIdentity({
|
|
292
|
+
accountId,
|
|
293
|
+
providerId,
|
|
294
|
+
identity,
|
|
295
|
+
}: {
|
|
296
|
+
accountId: string
|
|
297
|
+
providerId: string
|
|
298
|
+
identity: ProviderIdentity
|
|
299
|
+
}) {
|
|
300
|
+
const key = getProviderIdentityKey(providerId, accountId)
|
|
301
|
+
const identifier = pendingProviderIdentityIdentifier(key)
|
|
302
|
+
|
|
303
|
+
await replacePendingIdentity(db, {
|
|
304
|
+
identifier,
|
|
305
|
+
value: encryptSecret(JSON.stringify(identity)),
|
|
306
|
+
expiresAt: new Date(Date.now() + pendingProviderIdentityTtlMs),
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function readPendingProviderIdentity(key: string) {
|
|
311
|
+
const identifier = pendingProviderIdentityIdentifier(key)
|
|
312
|
+
const rows = await db
|
|
313
|
+
.select({ value: schema.verification.value })
|
|
314
|
+
.from(schema.verification)
|
|
315
|
+
.where(
|
|
316
|
+
and(
|
|
317
|
+
eq(schema.verification.identifier, identifier),
|
|
318
|
+
gt(schema.verification.expiresAt, new Date()),
|
|
319
|
+
),
|
|
320
|
+
)
|
|
321
|
+
.limit(1)
|
|
322
|
+
const value = rows[0]?.value
|
|
323
|
+
|
|
324
|
+
if (!value) {
|
|
325
|
+
return null
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const identity = JSON.parse(decryptSecret(value)) as unknown
|
|
330
|
+
return isProviderIdentity(identity) ? identity : null
|
|
331
|
+
} catch {
|
|
332
|
+
await clearPendingProviderIdentity(key)
|
|
333
|
+
return null
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function clearPendingProviderIdentity(key: string) {
|
|
338
|
+
await db
|
|
339
|
+
.delete(schema.verification)
|
|
340
|
+
.where(
|
|
341
|
+
eq(
|
|
342
|
+
schema.verification.identifier,
|
|
343
|
+
pendingProviderIdentityIdentifier(key),
|
|
344
|
+
),
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function pendingProviderIdentityIdentifier(key: string) {
|
|
349
|
+
return `devpush:provider-identity:${key}`
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function isProviderIdentity(value: unknown): value is ProviderIdentity {
|
|
353
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
354
|
+
return false
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return ['name', 'email', 'username', 'image'].every((field) => {
|
|
358
|
+
const item = (value as Record<string, unknown>)[field]
|
|
359
|
+
return item === undefined || item === null || typeof item === 'string'
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export async function getGitHubUserInfo(token: OAuthTokens) {
|
|
364
|
+
if (!token.accessToken) {
|
|
365
|
+
return null
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const headers = {
|
|
369
|
+
Accept: 'application/vnd.github+json',
|
|
370
|
+
Authorization: `Bearer ${token.accessToken}`,
|
|
371
|
+
'User-Agent': 'devpush',
|
|
372
|
+
}
|
|
373
|
+
const [userResponse, emailsResponse] = await Promise.all([
|
|
374
|
+
fetchGitHub('https://api.github.com/user', { headers }),
|
|
375
|
+
fetchGitHub('https://api.github.com/user/emails', { headers }),
|
|
376
|
+
])
|
|
377
|
+
|
|
378
|
+
if (!userResponse.ok) {
|
|
379
|
+
return null
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const profile = await readGitHubJson<GitHubProfile>(userResponse)
|
|
383
|
+
if (!emailsResponse.ok) return null
|
|
384
|
+
const emails = await readGitHubJson<Array<GitHubEmail | null>>(emailsResponse)
|
|
385
|
+
if (!Array.isArray(emails)) return null
|
|
386
|
+
const verified = emails.filter(
|
|
387
|
+
(candidate): candidate is GitHubEmail =>
|
|
388
|
+
candidate?.verified === true && typeof candidate.email === 'string',
|
|
389
|
+
)
|
|
390
|
+
const email = (
|
|
391
|
+
verified.find((candidate) => candidate.primary === true) ??
|
|
392
|
+
verified.find((candidate) => candidate.email === profile.email) ??
|
|
393
|
+
verified.at(0)
|
|
394
|
+
)?.email
|
|
395
|
+
if (!email) return null
|
|
396
|
+
|
|
397
|
+
await assertEmailCanAuthenticate(email)
|
|
398
|
+
|
|
399
|
+
const accountId = String(profile.id)
|
|
400
|
+
|
|
401
|
+
await rememberProviderIdentity({
|
|
402
|
+
accountId,
|
|
403
|
+
providerId: 'github',
|
|
404
|
+
identity: {
|
|
405
|
+
name: profile.name,
|
|
406
|
+
email,
|
|
407
|
+
username: profile.login,
|
|
408
|
+
image: profile.avatar_url,
|
|
409
|
+
},
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
user: {
|
|
414
|
+
name: profile.name || profile.login || '',
|
|
415
|
+
email,
|
|
416
|
+
image: profile.avatar_url,
|
|
417
|
+
emailVerified: true,
|
|
418
|
+
},
|
|
419
|
+
data: profile,
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export async function getGoogleUserInfo(token: OAuthTokens) {
|
|
424
|
+
if (!token.idToken) {
|
|
425
|
+
return null
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const claims = decodeJwt(await readProviderToken(token.idToken))
|
|
429
|
+
const accountId = getStringClaim(claims.sub)
|
|
430
|
+
const email = getStringClaim(claims.email)
|
|
431
|
+
const name = getStringClaim(claims.name)
|
|
432
|
+
const image = getStringClaim(claims.picture)
|
|
433
|
+
|
|
434
|
+
if (!accountId || !email || claims.email_verified !== true) {
|
|
435
|
+
return null
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
await assertEmailCanAuthenticate(email)
|
|
439
|
+
|
|
440
|
+
await rememberProviderIdentity({
|
|
441
|
+
accountId,
|
|
442
|
+
providerId: 'google',
|
|
443
|
+
identity: {
|
|
444
|
+
name,
|
|
445
|
+
email,
|
|
446
|
+
username: email,
|
|
447
|
+
image,
|
|
448
|
+
},
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
user: {
|
|
453
|
+
name,
|
|
454
|
+
email,
|
|
455
|
+
image,
|
|
456
|
+
emailVerified: true,
|
|
457
|
+
},
|
|
458
|
+
data: claims as unknown as GoogleProfile,
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function getStringClaim(value: unknown) {
|
|
463
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function getAvailableUsername(value: string) {
|
|
467
|
+
const base = normalizeUsername(value)
|
|
468
|
+
const existingRows = await db
|
|
469
|
+
.select({ username: schema.user.username })
|
|
470
|
+
.from(schema.user)
|
|
471
|
+
.where(
|
|
472
|
+
and(
|
|
473
|
+
sql`lower(${schema.user.username}) = ${base}`,
|
|
474
|
+
eq(schema.user.status, 'active'),
|
|
475
|
+
),
|
|
476
|
+
)
|
|
477
|
+
.limit(1)
|
|
478
|
+
|
|
479
|
+
if (existingRows.length === 0) {
|
|
480
|
+
return base
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
484
|
+
const suffix = crypto.randomUUID().slice(0, 8)
|
|
485
|
+
const candidate = `${base.slice(0, 41)}-${suffix}`
|
|
486
|
+
const rows = await db
|
|
487
|
+
.select({ id: schema.user.id })
|
|
488
|
+
.from(schema.user)
|
|
489
|
+
.where(
|
|
490
|
+
and(
|
|
491
|
+
eq(schema.user.username, candidate),
|
|
492
|
+
eq(schema.user.status, 'active'),
|
|
493
|
+
),
|
|
494
|
+
)
|
|
495
|
+
.limit(1)
|
|
496
|
+
|
|
497
|
+
if (rows.length === 0) {
|
|
498
|
+
return candidate
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return `user-${crypto.randomUUID().slice(0, 12)}`
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function normalizeUsername(value: string) {
|
|
506
|
+
const username = value
|
|
507
|
+
.split('@')[0]
|
|
508
|
+
?.toLowerCase()
|
|
509
|
+
.replace(/[\s]+/g, '-')
|
|
510
|
+
.replace(/[^a-z0-9._-]/g, '')
|
|
511
|
+
.replace(/^[._-]+|[._-]+$/g, '')
|
|
512
|
+
.slice(0, 50)
|
|
513
|
+
.replace(/[._-]+$/g, '')
|
|
514
|
+
|
|
515
|
+
return username && username.length >= 2
|
|
516
|
+
? username
|
|
517
|
+
: `user-${crypto.randomUUID().slice(0, 12)}`
|
|
518
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
2
|
+
import { getAuthMethods } from '#devpush/server/auth/auth-methods'
|
|
3
|
+
import { getAuthSession } from '#devpush/server/auth/session.server'
|
|
4
|
+
import { getUserContext } from '#devpush/server/teams/service'
|
|
5
|
+
|
|
6
|
+
export type AuthUserContext = Awaited<ReturnType<typeof getUserContext>>
|
|
7
|
+
export const getLoginContext = createServerFn({ method: 'GET' }).handler(
|
|
8
|
+
async () => {
|
|
9
|
+
const session = await getAuthSession()
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
authMethods: getAuthMethods(),
|
|
13
|
+
userContext: session ? await getUserContext(session.user.id) : null,
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
export const getCurrentUserContext = createServerFn({ method: 'GET' }).handler(
|
|
19
|
+
async () => {
|
|
20
|
+
const session = await getAuthSession()
|
|
21
|
+
|
|
22
|
+
if (!session) {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return getUserContext(session.user.id)
|
|
27
|
+
},
|
|
28
|
+
)
|