alepha 0.21.2 → 0.23.0
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 +0 -1
- package/dist/api/audits/index.browser.js.map +1 -1
- package/dist/api/audits/index.d.ts +393 -403
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +25 -56
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.browser.js +31 -1
- package/dist/api/files/index.browser.js.map +1 -1
- package/dist/api/files/index.d.ts +313 -208
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +152 -42
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.browser.js +2 -2
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +282 -285
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +39 -33
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +217 -222
- package/dist/api/keys/index.d.ts.map +1 -1
- package/dist/api/keys/index.js.map +1 -1
- package/dist/api/notifications/index.browser.js.map +1 -1
- package/dist/api/notifications/index.d.ts +188 -195
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/oauth/index.d.ts +71 -76
- package/dist/api/oauth/index.d.ts.map +1 -1
- package/dist/api/oauth/index.js.map +1 -1
- package/dist/api/organizations/index.browser.js.map +1 -1
- package/dist/api/organizations/index.d.ts +104 -109
- package/dist/api/organizations/index.d.ts.map +1 -1
- package/dist/api/organizations/index.js.map +1 -1
- package/dist/api/parameters/index.browser.js +43 -16
- package/dist/api/parameters/index.browser.js.map +1 -1
- package/dist/api/parameters/index.d.ts +488 -344
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +175 -35
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/payments/index.d.ts +396 -402
- package/dist/api/payments/index.d.ts.map +1 -1
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/subscriptions/index.d.ts +644 -652
- package/dist/api/subscriptions/index.d.ts.map +1 -1
- package/dist/api/subscriptions/index.js +1 -1
- package/dist/api/subscriptions/index.js.map +1 -1
- package/dist/api/users/index.browser.js +7 -0
- package/dist/api/users/index.browser.js.map +1 -1
- package/dist/api/users/index.d.ts +1106 -1005
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +307 -64
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.browser.js.map +1 -1
- package/dist/api/verifications/index.d.ts +137 -143
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/background/index.d.ts +95 -0
- package/dist/background/index.d.ts.map +1 -0
- package/dist/background/index.js +121 -0
- package/dist/background/index.js.map +1 -0
- package/dist/background/index.workerd.js +110 -0
- package/dist/background/index.workerd.js.map +1 -0
- package/dist/batch/index.d.ts +5 -7
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/batch/index.js.map +1 -1
- package/dist/bin/index.js.map +1 -1
- package/dist/bucket/index.d.ts +76 -54
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +58 -11
- package/dist/bucket/index.js.map +1 -1
- package/dist/bucket/index.workerd.js +200 -5
- package/dist/bucket/index.workerd.js.map +1 -1
- package/dist/cache/core/index.d.ts +7 -10
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/core/index.workerd.js.map +1 -1
- package/dist/cache/database/index.d.ts +22 -26
- package/dist/cache/database/index.d.ts.map +1 -1
- package/dist/cache/database/index.js.map +1 -1
- package/dist/cache/redis/index.d.ts +4 -7
- package/dist/cache/redis/index.d.ts.map +1 -1
- package/dist/cache/redis/index.js.map +1 -1
- package/dist/captcha/index.d.ts +3 -6
- package/dist/captcha/index.d.ts.map +1 -1
- package/dist/captcha/index.js.map +1 -1
- package/dist/cli/config/index.d.ts.map +1 -1
- package/dist/cli/config/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +458 -249
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +372 -660
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.d.ts +3 -5
- package/dist/cli/devtools/index.d.ts.map +1 -1
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/i18n/index.d.ts +20 -17
- package/dist/cli/i18n/index.d.ts.map +1 -1
- package/dist/cli/i18n/index.js +45 -11
- package/dist/cli/i18n/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +126 -1342
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +136 -2374
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/platform-lib/index.d.ts +1472 -0
- package/dist/cli/platform-lib/index.d.ts.map +1 -0
- package/dist/cli/platform-lib/index.js +2660 -0
- package/dist/cli/platform-lib/index.js.map +1 -0
- package/dist/cli/vendor/index.d.ts +17 -21
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/command/index.d.ts +20 -19
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +39 -10
- package/dist/command/index.js.map +1 -1
- package/dist/{containers → container}/core/index.d.ts +13 -15
- package/dist/container/core/index.d.ts.map +1 -0
- package/dist/{containers → container}/core/index.js +23 -14
- package/dist/container/core/index.js.map +1 -0
- package/dist/{containers → container}/core/index.workerd.js +37 -22
- package/dist/container/core/index.workerd.js.map +1 -0
- package/dist/core/index.browser.js +27 -1
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +48 -24
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +27 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +27 -1
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +27 -1
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/crypto/index.browser.js.map +1 -1
- package/dist/crypto/index.d.ts +5 -8
- package/dist/crypto/index.d.ts.map +1 -1
- package/dist/crypto/index.js.map +1 -1
- package/dist/datetime/index.d.ts +3 -4
- package/dist/datetime/index.d.ts.map +1 -1
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/brevo/index.d.ts +2 -4
- package/dist/email/brevo/index.d.ts.map +1 -1
- package/dist/email/brevo/index.js.map +1 -1
- package/dist/email/cloudflare/index.d.ts +20 -7
- package/dist/email/cloudflare/index.d.ts.map +1 -1
- package/dist/email/cloudflare/index.js +46 -9
- package/dist/email/cloudflare/index.js.map +1 -1
- package/dist/email/core/index.d.ts +6 -9
- package/dist/email/core/index.d.ts.map +1 -1
- package/dist/email/core/index.js.map +1 -1
- package/dist/email/core/index.workerd.js.map +1 -1
- package/dist/email/smtp/index.d.ts +10 -13
- package/dist/email/smtp/index.d.ts.map +1 -1
- package/dist/email/smtp/index.js +107 -32
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/fake/index.d.ts +1 -2
- package/dist/fake/index.d.ts.map +1 -1
- package/dist/fake/index.js.map +1 -1
- package/dist/lock/core/index.d.ts +9 -14
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/core/index.js.map +1 -1
- package/dist/lock/redis/index.d.ts +2 -4
- package/dist/lock/redis/index.d.ts.map +1 -1
- package/dist/lock/redis/index.js.map +1 -1
- package/dist/logger/index.d.ts +105 -76
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js +196 -174
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.d.ts +25 -20
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +23 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +19 -1
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +76 -62
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +20 -2
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.d.ts +28 -20
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/orm/postgres/index.js.map +1 -1
- package/dist/queue/core/index.d.ts +12 -15
- package/dist/queue/core/index.d.ts.map +1 -1
- package/dist/queue/core/index.js.map +1 -1
- package/dist/queue/core/index.workerd.js.map +1 -1
- package/dist/queue/redis/index.d.ts +3 -5
- package/dist/queue/redis/index.d.ts.map +1 -1
- package/dist/queue/redis/index.js.map +1 -1
- package/dist/react/auth/index.browser.js +9 -2
- package/dist/react/auth/index.browser.js.map +1 -1
- package/dist/react/auth/index.d.ts +14 -9
- package/dist/react/auth/index.d.ts.map +1 -1
- package/dist/react/auth/index.js +9 -2
- package/dist/react/auth/index.js.map +1 -1
- package/dist/react/core/index.d.ts +7 -8
- package/dist/react/core/index.d.ts.map +1 -1
- package/dist/react/core/index.js +6 -3
- package/dist/react/core/index.js.map +1 -1
- package/dist/react/form/index.d.ts +2 -5
- package/dist/react/form/index.d.ts.map +1 -1
- package/dist/react/form/index.js +16 -15
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/head/index.browser.js.map +1 -1
- package/dist/react/head/index.d.ts +2 -4
- package/dist/react/head/index.d.ts.map +1 -1
- package/dist/react/head/index.js.map +1 -1
- package/dist/react/i18n/index.d.ts +90 -11
- package/dist/react/i18n/index.d.ts.map +1 -1
- package/dist/react/i18n/index.js +147 -11
- package/dist/react/i18n/index.js.map +1 -1
- package/dist/react/intro/index.d.ts +1 -2
- package/dist/react/intro/index.d.ts.map +1 -1
- package/dist/react/intro/index.js +2 -2
- package/dist/react/intro/index.js.map +1 -1
- package/dist/react/router/index.browser.js +193 -24
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +434 -222
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +249 -35
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/sitemap/index.browser.js +35 -0
- package/dist/react/sitemap/index.browser.js.map +1 -0
- package/dist/react/sitemap/index.d.ts +92 -0
- package/dist/react/sitemap/index.d.ts.map +1 -0
- package/dist/react/sitemap/index.js +131 -0
- package/dist/react/sitemap/index.js.map +1 -0
- package/dist/react/testing/index.d.ts +1 -2
- package/dist/react/testing/index.d.ts.map +1 -1
- package/dist/react/testing/index.js +16 -17
- package/dist/react/testing/index.js.map +1 -1
- package/dist/react/ui/index.d.ts +20 -25
- package/dist/react/ui/index.d.ts.map +1 -1
- package/dist/react/ui/index.js.map +1 -1
- package/dist/redis/index.bun.js.map +1 -1
- package/dist/redis/index.d.ts +17 -19
- package/dist/redis/index.d.ts.map +1 -1
- package/dist/redis/index.js.map +1 -1
- package/dist/retry/index.d.ts +2 -4
- package/dist/retry/index.d.ts.map +1 -1
- package/dist/retry/index.js.map +1 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +10 -13
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/index.workerd.js.map +1 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.d.ts +45 -48
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.browser.js.map +1 -1
- package/dist/server/auth/index.d.ts +272 -173
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +1608 -15
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cookies/index.browser.js.map +1 -1
- package/dist/server/cookies/index.d.ts +20 -7
- package/dist/server/cookies/index.d.ts.map +1 -1
- package/dist/server/cookies/index.js +22 -3
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.browser.js.map +1 -1
- package/dist/server/core/index.d.ts +106 -73
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +44 -0
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.d.ts +11 -14
- package/dist/server/cors/index.d.ts.map +1 -1
- package/dist/server/cors/index.js.map +1 -1
- package/dist/server/etag/index.d.ts +6 -9
- package/dist/server/etag/index.d.ts.map +1 -1
- package/dist/server/etag/index.js.map +1 -1
- package/dist/server/health/index.d.ts +18 -21
- package/dist/server/health/index.d.ts.map +1 -1
- package/dist/server/health/index.js.map +1 -1
- package/dist/server/links/index.browser.js +2 -0
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.d.ts +63 -67
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/links/index.js +2 -0
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.d.ts +5 -7
- package/dist/server/metrics/index.d.ts.map +1 -1
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/proxy/index.d.ts +3 -5
- package/dist/server/proxy/index.d.ts.map +1 -1
- package/dist/server/proxy/index.js.map +1 -1
- package/dist/server/rate-limit/index.d.ts +10 -13
- package/dist/server/rate-limit/index.d.ts.map +1 -1
- package/dist/server/rate-limit/index.js.map +1 -1
- package/dist/server/static/index.d.ts +3 -5
- package/dist/server/static/index.d.ts.map +1 -1
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts +5 -8
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.d.ts +3 -5
- package/dist/sms/index.d.ts.map +1 -1
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js.map +1 -1
- package/dist/system/index.d.ts +2 -4
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js.map +1 -1
- package/dist/system/index.workerd.js.map +1 -1
- package/dist/topic/core/index.d.ts +4 -6
- package/dist/topic/core/index.d.ts.map +1 -1
- package/dist/topic/core/index.js.map +1 -1
- package/dist/topic/redis/index.d.ts +5 -8
- package/dist/topic/redis/index.d.ts.map +1 -1
- package/dist/topic/redis/index.js.map +1 -1
- package/package.json +59 -23
- package/src/api/audits/__tests__/AuditService.spec.ts +18 -110
- package/src/api/audits/controllers/AdminAuditController.ts +14 -0
- package/src/api/audits/services/AuditService.ts +21 -88
- package/src/api/files/__tests__/FileService.spec.ts +207 -2
- package/src/api/files/index.ts +3 -0
- package/src/api/files/schemas/fileCreatorSummarySchema.ts +22 -0
- package/src/api/files/schemas/fileResourceSchema.ts +10 -1
- package/src/api/files/services/FileService.ts +170 -72
- package/src/api/jobs/__tests__/$job.spec.ts +24 -1
- package/src/api/jobs/index.ts +4 -3
- package/src/api/jobs/primitives/$job.ts +7 -3
- package/src/api/jobs/providers/DirectJobDispatcher.ts +17 -36
- package/src/api/jobs/providers/JobProvider.ts +53 -24
- package/src/api/jobs/schemas/jobConfigAtom.ts +1 -1
- package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +4 -1
- package/src/api/keys/schemas/adminApiKeyResourceSchema.ts +3 -1
- package/src/api/parameters/__tests__/$parameter.spec.ts +19 -2
- package/src/api/parameters/audits/ParameterAudits.ts +17 -0
- package/src/api/parameters/controllers/AdminParameterController.ts +95 -19
- package/src/api/parameters/index.ts +3 -0
- package/src/api/parameters/schemas/activateParameterBodySchema.ts +3 -3
- package/src/api/parameters/schemas/createParameterVersionBodySchema.ts +3 -2
- package/src/api/parameters/schemas/parameterCreatorSummarySchema.ts +25 -0
- package/src/api/parameters/schemas/parameterResponseSchema.ts +5 -0
- package/src/api/parameters/schemas/rollbackParameterBodySchema.ts +4 -2
- package/src/api/parameters/services/ParameterProvider.ts +69 -6
- package/src/api/subscriptions/jobs/SubscriptionJobs.ts +1 -1
- package/src/api/users/__tests__/AdminSessionController.spec.ts +37 -0
- package/src/api/users/audits/SessionAudits.ts +33 -0
- package/src/api/users/audits/UserAudits.ts +19 -43
- package/src/api/users/controllers/AdminUserController.ts +66 -1
- package/src/api/users/controllers/RealmController.ts +1 -0
- package/src/api/users/entities/sessions.ts +6 -0
- package/src/api/users/entities/users.ts +2 -0
- package/src/api/users/index.ts +9 -1
- package/src/api/users/primitives/$realm.ts +29 -0
- package/src/api/users/providers/RealmProvider.ts +15 -0
- package/src/api/users/schemas/realmConfigSchema.ts +14 -0
- package/src/api/users/schemas/sessionResourceSchema.ts +16 -0
- package/src/api/users/schemas/updateUserSchema.ts +1 -8
- package/src/api/users/schemas/userQuerySchema.ts +7 -0
- package/src/api/users/services/CredentialService.ts +15 -6
- package/src/api/users/services/IdentityService.ts +2 -1
- package/src/api/users/services/RegistrationService.ts +2 -1
- package/src/api/users/services/SessionCrudService.ts +19 -2
- package/src/api/users/services/SessionService.ts +39 -19
- package/src/api/users/services/UserService.ts +106 -8
- package/src/background/__tests__/BackgroundTaskProvider.spec.ts +96 -0
- package/src/background/index.ts +37 -0
- package/src/background/index.workerd.ts +28 -0
- package/src/background/providers/BackgroundTaskProvider.ts +70 -0
- package/src/background/providers/WorkerdBackgroundTaskProvider.ts +43 -0
- package/src/bucket/__tests__/$bucket.spec.ts +18 -0
- package/src/bucket/__tests__/LocalFileStorageProvider.spec.ts +5 -0
- package/src/bucket/__tests__/MemoryFileStorageProvider.spec.ts +5 -0
- package/src/bucket/__tests__/NodeS3BucketProvider.spec.ts +23 -4
- package/src/bucket/__tests__/shared.ts +30 -0
- package/src/bucket/index.ts +5 -5
- package/src/bucket/index.workerd.ts +11 -4
- package/src/bucket/primitives/$bucket.ts +27 -0
- package/src/bucket/providers/FileStorageProvider.ts +13 -0
- package/src/bucket/providers/LocalFileStorageProvider.ts +17 -1
- package/src/bucket/providers/MemoryFileStorageProvider.ts +7 -0
- package/src/bucket/providers/{CloudflareR2Provider.ts → R2FileStorageProvider.ts} +10 -1
- package/src/bucket/providers/{NodeS3BucketProvider.ts → S3FileStorageProvider.ts} +27 -5
- package/src/cli/core/__tests__/BuildDockerTask.spec.ts +25 -1
- package/src/cli/core/__tests__/init.spec.ts +0 -219
- package/src/cli/core/atoms/buildOptions.ts +0 -12
- package/src/cli/core/commands/__tests__/BuildCommand.spec.ts +43 -0
- package/src/cli/core/commands/build.ts +105 -37
- package/src/cli/core/commands/init.ts +0 -12
- package/src/cli/core/commands/pack.ts +133 -0
- package/src/cli/core/index.ts +3 -3
- package/src/cli/core/providers/ViteDevServerProvider.ts +40 -16
- package/src/cli/core/services/PackageManagerUtils.ts +0 -16
- package/src/cli/core/services/ProjectScaffolder.ts +29 -291
- package/src/cli/core/tasks/BuildCloudflareTask.ts +382 -56
- package/src/cli/core/tasks/BuildDockerTask.ts +33 -3
- package/src/cli/core/tasks/BuildPrerenderTask.ts +44 -7
- package/src/cli/core/tasks/BuildTask.ts +34 -0
- package/src/cli/core/templates/apiIndexTs.ts +1 -22
- package/src/cli/core/templates/mainCss.ts +0 -1
- package/src/cli/core/templates/webAppRouterTs.ts +0 -99
- package/src/cli/core/templates/webIndexTs.ts +1 -22
- package/src/cli/i18n/__tests__/I18nCheckService.spec.ts +48 -0
- package/src/cli/i18n/services/I18nCheckService.ts +65 -11
- package/src/cli/platform/__tests__/SecretsCommand.spec.ts +5 -3
- package/src/cli/platform/commands/SecretsCommand.ts +8 -6
- package/src/cli/platform/commands/platform.ts +192 -46
- package/src/cli/platform/index.ts +12 -52
- package/src/cli/{platform → platform-lib}/__tests__/CloudflareAdapter.spec.ts +426 -169
- package/src/cli/{platform → platform-lib}/__tests__/NamingService.spec.ts +91 -4
- package/src/cli/{platform → platform-lib}/__tests__/VercelAdapter.spec.ts +56 -85
- package/src/cli/{platform → platform-lib}/adapters/CloudflareAdapter.ts +519 -190
- package/src/cli/{platform → platform-lib}/adapters/PlatformAdapter.ts +62 -35
- package/src/cli/{platform → platform-lib}/adapters/VercelAdapter.ts +6 -10
- package/src/cli/{platform → platform-lib}/atoms/platformOptions.ts +34 -1
- package/src/cli/platform-lib/index.ts +67 -0
- package/src/cli/platform-lib/services/NamingService.ts +136 -0
- package/src/cli/{platform → platform-lib}/services/PlatformInspector.ts +60 -13
- package/src/cli/{platform → platform-lib}/services/PlatformOrchestrator.ts +54 -43
- package/src/cli/{platform → platform-lib}/services/WranglerApi.ts +4 -2
- package/src/command/__tests__/Runner.spec.ts +20 -0
- package/src/command/helpers/EnvUtils.ts +19 -3
- package/src/command/helpers/Runner.ts +12 -2
- package/src/command/providers/CliProvider.ts +34 -1
- package/src/{containers → container}/core/__tests__/$container.spec.ts +5 -5
- package/src/{containers → container}/core/index.ts +4 -4
- package/src/{containers → container}/core/index.workerd.ts +19 -3
- package/src/{containers → container}/core/primitives/$container.ts +1 -1
- package/src/{containers → container}/core/providers/CloudflareContainerProvider.ts +17 -19
- package/src/{containers → container}/core/providers/ContainerProvider.ts +16 -2
- package/src/{containers → container}/core/providers/MockContainerProvider.ts +1 -1
- package/src/core/Alepha.ts +49 -1
- package/src/core/__tests__/$env.spec.ts +42 -0
- package/src/core/__tests__/dump.spec.ts +47 -0
- package/src/email/cloudflare/__tests__/CloudflareEmailProvider.spec.ts +42 -10
- package/src/email/cloudflare/index.ts +14 -5
- package/src/email/cloudflare/providers/CloudflareEmailProvider.ts +54 -9
- package/src/logger/__tests__/Logger.spec.ts +55 -0
- package/src/logger/index.ts +13 -0
- package/src/logger/services/Logger.ts +31 -1
- package/src/mcp/__tests__/McpServerProvider.spec.ts +71 -0
- package/src/mcp/providers/McpServerProvider.ts +55 -0
- package/src/orm/__tests__/orm-showcase-tests.ts +27 -0
- package/src/orm/__tests__/orm-showcase.spec.ts +12 -0
- package/src/orm/core/interfaces/PgQuery.ts +4 -1
- package/src/orm/core/services/Repository.ts +27 -11
- package/src/react/auth/hooks/useAuth.ts +10 -5
- package/src/react/core/__tests__/useQuery.browser.spec.tsx +25 -0
- package/src/react/core/hooks/useAction.ts +14 -3
- package/src/react/core/hooks/useQuery.ts +24 -4
- package/src/react/form/__tests__/FormModel-submit-loading.spec.ts +71 -0
- package/src/react/form/__tests__/form-submitting-reactive.browser.spec.tsx +96 -0
- package/src/react/form/services/FormModel.ts +57 -39
- package/src/react/i18n/__tests__/I18nProvider.spec.ts +89 -0
- package/src/react/i18n/__tests__/locale-routing.spec.ts +107 -0
- package/src/react/i18n/components/Translate.tsx +47 -0
- package/src/react/i18n/index.ts +2 -0
- package/src/react/i18n/providers/I18nProvider.ts +171 -12
- package/src/react/intro/components/GettingStartedAdminSlide.tsx +2 -2
- package/src/react/router/__tests__/$page.spec.tsx +3 -2
- package/src/react/router/__tests__/RouterLocaleProvider.spec.ts +127 -0
- package/src/react/router/__tests__/page-can.spec.ts +18 -13
- package/src/react/router/hooks/useQueryParams.ts +114 -14
- package/src/react/router/index.browser.ts +4 -0
- package/src/react/router/index.shared.ts +1 -0
- package/src/react/router/index.ts +9 -0
- package/src/react/router/primitives/$page.ts +85 -4
- package/src/react/router/providers/ReactBrowserRouterProvider.ts +18 -8
- package/src/react/router/providers/ReactPageProvider.ts +12 -1
- package/src/react/router/providers/ReactServerProvider.ts +96 -14
- package/src/react/router/providers/RootComponentsProvider.ts +13 -0
- package/src/react/router/providers/RouterLocaleProvider.ts +125 -0
- package/src/react/router/providers/__tests__/RootComponentsProvider.spec.ts +15 -0
- package/src/react/router/providers/__tests__/rootComponents.ssr.browser.spec.tsx +67 -0
- package/src/react/sitemap/__tests__/$sitemap.spec.ts +131 -0
- package/src/react/sitemap/index.browser.ts +21 -0
- package/src/react/sitemap/index.ts +25 -0
- package/src/react/sitemap/primitives/$sitemap.browser.ts +26 -0
- package/src/react/sitemap/primitives/$sitemap.ts +196 -0
- package/src/react/ui/services/SchemaControl.ts +3 -4
- package/src/server/auth/__tests__/appleClientSecret.spec.ts +34 -0
- package/src/server/auth/__tests__/authFederationClient.spec.ts +40 -0
- package/src/server/auth/__tests__/federationAssertion.spec.ts +146 -0
- package/src/server/auth/__tests__/federationRedirectReplay.spec.ts +44 -0
- package/src/server/auth/helpers/appleClientSecret.ts +24 -0
- package/src/server/auth/helpers/federationAssertion.ts +74 -0
- package/src/server/auth/helpers/jtiReplayGuard.ts +41 -0
- package/src/server/auth/helpers/safeRedirectPath.ts +19 -0
- package/src/server/auth/index.ts +4 -0
- package/src/server/auth/primitives/$authFederationBroker.ts +273 -0
- package/src/server/auth/primitives/$authFederationClient.ts +89 -0
- package/src/server/auth/providers/ServerAuthProvider.ts +18 -4
- package/src/server/cookies/__tests__/ServerCookiesProvider.spec.ts +70 -0
- package/src/server/cookies/providers/ServerCookiesProvider.ts +23 -3
- package/src/server/core/interfaces/ServerRequest.ts +8 -0
- package/src/server/core/primitives/$route.ts +27 -0
- package/src/server/core/providers/ServerMultipartProvider.ts +19 -0
- package/src/server/links/providers/LinkProvider.ts +10 -0
- package/dist/containers/core/index.d.ts.map +0 -1
- package/dist/containers/core/index.js.map +0 -1
- package/dist/containers/core/index.workerd.js.map +0 -1
- package/src/cli/core/tasks/BuildSitemapTask.ts +0 -130
- package/src/cli/core/templates/componentsJsonTs.ts +0 -39
- package/src/cli/core/templates/saasAdminLayoutTsx.ts +0 -77
- package/src/cli/core/templates/saasAdminPagesTsx.ts +0 -26
- package/src/cli/core/templates/saasAuthLayoutTsx.ts +0 -22
- package/src/cli/core/templates/saasAuthPagesTsx.ts +0 -62
- package/src/cli/core/templates/saasRealmProviderTs.ts +0 -52
- package/src/cli/platform/services/NamingService.ts +0 -54
- /package/dist/orm/core/{chunk-o8xxKEmq.js → chunk-B4FMCO8f.js} +0 -0
- /package/dist/react/testing/{chunk-6Ep1yQYe.js → chunk-BpyX8vjI.js} +0 -0
- /package/src/cli/{platform → platform-lib}/__tests__/GitHubSecretStore.spec.ts +0 -0
- /package/src/cli/{platform → platform-lib}/__tests__/PlatformCacheProvider.spec.ts +0 -0
- /package/src/cli/{platform → platform-lib}/__tests__/PlatformInspector.spec.ts +0 -0
- /package/src/cli/{platform → platform-lib}/__tests__/PlatformOrchestrator.spec.ts +0 -0
- /package/src/cli/{platform → platform-lib}/__tests__/SecretFilterService.spec.ts +0 -0
- /package/src/cli/{platform → platform-lib}/__tests__/detectResources.spec.ts +0 -0
- /package/src/cli/{platform → platform-lib}/providers/GitHubSecretStore.ts +0 -0
- /package/src/cli/{platform → platform-lib}/providers/MemorySecretStore.ts +0 -0
- /package/src/cli/{platform → platform-lib}/providers/PlatformCacheProvider.ts +0 -0
- /package/src/cli/{platform → platform-lib}/providers/SecretStoreProvider.ts +0 -0
- /package/src/cli/{platform → platform-lib}/schemas/cloudflare.ts +0 -0
- /package/src/cli/{platform → platform-lib}/schemas/platform.ts +0 -0
- /package/src/cli/{platform → platform-lib}/schemas/vercel.ts +0 -0
- /package/src/cli/{platform → platform-lib}/services/CloudflareApi.ts +0 -0
- /package/src/cli/{platform → platform-lib}/services/SecretFilterService.ts +0 -0
- /package/src/cli/{platform → platform-lib}/services/VercelApi.ts +0 -0
- /package/src/cli/{platform → platform-lib}/services/VercelCli.ts +0 -0
- /package/src/{containers → container}/core/interfaces/ContainerOptions.ts +0 -0
- /package/src/{containers → container}/core/providers/NodeContainerProvider.ts +0 -0
|
@@ -0,0 +1,2660 @@
|
|
|
1
|
+
import { $atom, $inject, $module, $state, Alepha, AlephaError, t } from "alepha";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { AlephaCliUtils, BuildCloudflareTask, PackageManagerUtils } from "alepha/cli";
|
|
6
|
+
import { Asker, EnvUtils, Runner } from "alepha/command";
|
|
7
|
+
import { $logger, ConsoleColorProvider } from "alepha/logger";
|
|
8
|
+
import { FileSystemProvider, ShellProvider } from "alepha/system";
|
|
9
|
+
import { S3mini } from "s3mini";
|
|
10
|
+
import { DateTimeProvider } from "alepha/datetime";
|
|
11
|
+
import { homedir, platform } from "node:os";
|
|
12
|
+
//#region ../../src/cli/platform-lib/atoms/platformOptions.ts
|
|
13
|
+
/**
|
|
14
|
+
* Platform deployment configuration atom.
|
|
15
|
+
*
|
|
16
|
+
* Filled from the `platform` section of `alepha.config.ts`.
|
|
17
|
+
* Read by `PlatformCommand` to resolve environments and adapters.
|
|
18
|
+
*/
|
|
19
|
+
const platformOptions = $atom({
|
|
20
|
+
name: "alepha.cli.platform.options",
|
|
21
|
+
description: "Platform deployment configuration",
|
|
22
|
+
schema: t.optional(t.object({
|
|
23
|
+
/**
|
|
24
|
+
* Project name override. Defaults to root package.json "name".
|
|
25
|
+
*/
|
|
26
|
+
name: t.optional(t.text()),
|
|
27
|
+
/**
|
|
28
|
+
* Default environment when --env is omitted.
|
|
29
|
+
*
|
|
30
|
+
* @default "production"
|
|
31
|
+
*/
|
|
32
|
+
default: t.optional(t.text()),
|
|
33
|
+
/**
|
|
34
|
+
* Multi-tenancy mode — controls whether `--tenant <slug>` is
|
|
35
|
+
* accepted/required and how it shapes resource names + the domain.
|
|
36
|
+
*
|
|
37
|
+
* - `none` (default): single instance. `--tenant` is rejected.
|
|
38
|
+
* - `required`: every deploy needs `--tenant`; resources are named
|
|
39
|
+
* `<tenant>-<project>-<env>` and the host becomes
|
|
40
|
+
* `<tenant>.<domain>`. Omitting it errors.
|
|
41
|
+
* - `optional`: a base instance (no `--tenant`) plus per-tenant
|
|
42
|
+
* instances coexist.
|
|
43
|
+
*
|
|
44
|
+
* @default "none"
|
|
45
|
+
*/
|
|
46
|
+
tenancy: t.optional(t.enum([
|
|
47
|
+
"none",
|
|
48
|
+
"optional",
|
|
49
|
+
"required"
|
|
50
|
+
])),
|
|
51
|
+
/**
|
|
52
|
+
* Secret store configuration for syncing .env secrets
|
|
53
|
+
* to external providers (e.g. GitHub Actions environments).
|
|
54
|
+
*/
|
|
55
|
+
secrets: t.optional(t.object({
|
|
56
|
+
/**
|
|
57
|
+
* Explicit override of the worker secret-key allowlist.
|
|
58
|
+
*
|
|
59
|
+
* By default the deploy `secrets` step uses the build manifest's
|
|
60
|
+
* `env` list (every key the app declares via `$env`, captured at
|
|
61
|
+
* build time) as the allowlist, resolving each value from
|
|
62
|
+
* `.env.<env>[.local]` first, then `process.env`. This lets CI
|
|
63
|
+
* deliver secrets via the job environment (no `.env` file on the
|
|
64
|
+
* runner) while only ever pushing declared keys — ambient runner
|
|
65
|
+
* vars (PATH, GITHUB_*, …) can never leak.
|
|
66
|
+
*
|
|
67
|
+
* Set `keys` to override that auto-detected list (e.g. to narrow it,
|
|
68
|
+
* or to add a key read via `process.env` rather than `$env`). When
|
|
69
|
+
* neither this nor a manifest is present, the `.env.<env>` file is
|
|
70
|
+
* itself the allowlist (legacy fallback).
|
|
71
|
+
*/
|
|
72
|
+
keys: t.optional(t.array(t.text())),
|
|
73
|
+
/**
|
|
74
|
+
* Secret store backend.
|
|
75
|
+
*/
|
|
76
|
+
store: t.optional(t.enum(["github"])),
|
|
77
|
+
/**
|
|
78
|
+
* Pattern for resolving environment names in the store.
|
|
79
|
+
* Placeholders: {project}, {env}.
|
|
80
|
+
*
|
|
81
|
+
* @default "{project}-{env}"
|
|
82
|
+
*/
|
|
83
|
+
environmentPattern: t.optional(t.text())
|
|
84
|
+
})),
|
|
85
|
+
/**
|
|
86
|
+
* Named environments with their adapter and configuration.
|
|
87
|
+
*/
|
|
88
|
+
environments: t.record(t.text({ description: "Environment name (e.g. 'production', 'staging', 'preview'). Used in resource naming and selected via --env." }), t.object({
|
|
89
|
+
adapter: t.enum(["cloudflare", "vercel"]),
|
|
90
|
+
/**
|
|
91
|
+
* Custom domain for the deployed worker (e.g. "api.example.com").
|
|
92
|
+
*
|
|
93
|
+
* On Cloudflare this is attached as a custom-domain route.
|
|
94
|
+
* Omit to use the adapter's default `*.workers.dev` / preview URL.
|
|
95
|
+
*
|
|
96
|
+
* Wildcards are supported for multi-tenant SaaS apps:
|
|
97
|
+
* `"*.club.alepha.dev"` routes every subdomain to the worker.
|
|
98
|
+
* Wildcard patterns require `zone` to be set, and the wildcard DNS
|
|
99
|
+
* record must already exist (proxied) in the Cloudflare zone.
|
|
100
|
+
*/
|
|
101
|
+
domain: t.optional(t.text()),
|
|
102
|
+
/**
|
|
103
|
+
* Cloudflare zone name (e.g. "alepha.dev") that owns `domain`.
|
|
104
|
+
*
|
|
105
|
+
* Required when `domain` contains a wildcard (`*`). Ignored for
|
|
106
|
+
* plain custom domains, which Cloudflare resolves automatically.
|
|
107
|
+
*/
|
|
108
|
+
zone: t.optional(t.text()),
|
|
109
|
+
/**
|
|
110
|
+
* Cloudflare data jurisdiction for R2 buckets and D1 databases.
|
|
111
|
+
* - "eu": data stays within the EU
|
|
112
|
+
* - "fedramp": FedRAMP-authorized regions
|
|
113
|
+
*
|
|
114
|
+
* Omit for the default (global) jurisdiction.
|
|
115
|
+
*/
|
|
116
|
+
jurisdiction: t.optional(t.enum(["eu", "fedramp"])),
|
|
117
|
+
/**
|
|
118
|
+
* Cloudflare account ID to deploy into.
|
|
119
|
+
*
|
|
120
|
+
* Falls back to `CLOUDFLARE_ACCOUNT_ID` env var, then to the
|
|
121
|
+
* token's account when the token is scoped to exactly one.
|
|
122
|
+
* Required when the token has access to multiple accounts.
|
|
123
|
+
*/
|
|
124
|
+
accountId: t.optional(t.text())
|
|
125
|
+
}))
|
|
126
|
+
}))
|
|
127
|
+
});
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region ../../src/cli/platform-lib/providers/PlatformCacheProvider.ts
|
|
130
|
+
/**
|
|
131
|
+
* Caches cloud provider login state to avoid slow auth checks.
|
|
132
|
+
*
|
|
133
|
+
* Stored in node_modules/.alepha/platform.json (gitignored, project-scoped).
|
|
134
|
+
* TTL: 4 hours.
|
|
135
|
+
*/
|
|
136
|
+
var PlatformCacheProvider = class PlatformCacheProvider {
|
|
137
|
+
static TTL_MS = 14400 * 1e3;
|
|
138
|
+
fs = $inject(FileSystemProvider);
|
|
139
|
+
dateTime = $inject(DateTimeProvider);
|
|
140
|
+
cachePath(root) {
|
|
141
|
+
return this.fs.join(root, "node_modules", ".alepha", "platform.json");
|
|
142
|
+
}
|
|
143
|
+
async isLoginFresh(root, adapter) {
|
|
144
|
+
const entry = (await this.readCache(root))[adapter];
|
|
145
|
+
if (!entry) return false;
|
|
146
|
+
return this.dateTime.nowMillis() - entry.lastLoginCheck < PlatformCacheProvider.TTL_MS;
|
|
147
|
+
}
|
|
148
|
+
async getAccountId(root, adapter) {
|
|
149
|
+
return (await this.readCache(root))[adapter]?.accountId;
|
|
150
|
+
}
|
|
151
|
+
async recordLogin(root, adapter, accountId) {
|
|
152
|
+
const cache = await this.readCache(root);
|
|
153
|
+
cache[adapter] = {
|
|
154
|
+
lastLoginCheck: this.dateTime.nowMillis(),
|
|
155
|
+
accountId
|
|
156
|
+
};
|
|
157
|
+
await this.writeCache(root, cache);
|
|
158
|
+
}
|
|
159
|
+
async readCache(root) {
|
|
160
|
+
const path = this.cachePath(root);
|
|
161
|
+
try {
|
|
162
|
+
return await this.fs.readJsonFile(path);
|
|
163
|
+
} catch {
|
|
164
|
+
return {};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async writeCache(root, cache) {
|
|
168
|
+
const path = this.cachePath(root);
|
|
169
|
+
const lastSlash = path.lastIndexOf("/");
|
|
170
|
+
const dir = lastSlash > 0 ? path.slice(0, lastSlash) : path;
|
|
171
|
+
await this.fs.mkdir(dir, { recursive: true }).catch(() => null);
|
|
172
|
+
await this.fs.writeFile(path, JSON.stringify(cache, null, 2));
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region ../../src/cli/platform-lib/schemas/cloudflare.ts
|
|
177
|
+
const cloudflareAccountSchema = t.object({
|
|
178
|
+
id: t.string(),
|
|
179
|
+
name: t.string()
|
|
180
|
+
});
|
|
181
|
+
const cloudflareD1Schema = t.object({
|
|
182
|
+
uuid: t.string(),
|
|
183
|
+
name: t.string()
|
|
184
|
+
});
|
|
185
|
+
const cloudflareKVSchema = t.object({
|
|
186
|
+
id: t.string(),
|
|
187
|
+
title: t.string()
|
|
188
|
+
});
|
|
189
|
+
const cloudflareR2Schema = t.object({
|
|
190
|
+
name: t.string(),
|
|
191
|
+
creation_date: t.optional(t.string())
|
|
192
|
+
});
|
|
193
|
+
const cloudflareR2ListSchema = t.object({ buckets: t.array(cloudflareR2Schema) });
|
|
194
|
+
const cloudflareQueueSchema = t.object({
|
|
195
|
+
queue_id: t.string(),
|
|
196
|
+
queue_name: t.string()
|
|
197
|
+
});
|
|
198
|
+
const cloudflareQueueConsumerSchema = t.object({
|
|
199
|
+
consumer_id: t.string(),
|
|
200
|
+
service: t.string(),
|
|
201
|
+
environment: t.optional(t.string())
|
|
202
|
+
});
|
|
203
|
+
const cloudflareHyperdriveOriginSchema = t.object({ host: t.string() });
|
|
204
|
+
const cloudflareHyperdriveSchema = t.object({
|
|
205
|
+
id: t.string(),
|
|
206
|
+
name: t.string(),
|
|
207
|
+
origin: cloudflareHyperdriveOriginSchema
|
|
208
|
+
});
|
|
209
|
+
const cloudflareWorkerSchema = t.object({
|
|
210
|
+
id: t.string(),
|
|
211
|
+
created_on: t.string(),
|
|
212
|
+
modified_on: t.string()
|
|
213
|
+
});
|
|
214
|
+
const cloudflareDeploymentVersionSchema = t.object({
|
|
215
|
+
version_id: t.string(),
|
|
216
|
+
percentage: t.number()
|
|
217
|
+
});
|
|
218
|
+
const cloudflareDeploymentSchema = t.object({
|
|
219
|
+
id: t.string(),
|
|
220
|
+
versions: t.array(cloudflareDeploymentVersionSchema),
|
|
221
|
+
created_on: t.string()
|
|
222
|
+
});
|
|
223
|
+
const cloudflareDeploymentListSchema = t.object({ deployments: t.array(cloudflareDeploymentSchema) });
|
|
224
|
+
const cloudflareVersionSchema = t.object({
|
|
225
|
+
id: t.string(),
|
|
226
|
+
metadata: t.object({ created_on: t.string() }),
|
|
227
|
+
annotations: t.optional(t.record(t.string(), t.string()))
|
|
228
|
+
});
|
|
229
|
+
const cloudflareVersionListSchema = t.object({ items: t.array(cloudflareVersionSchema) });
|
|
230
|
+
const cloudflareSecretSchema = t.object({
|
|
231
|
+
name: t.string(),
|
|
232
|
+
type: t.string()
|
|
233
|
+
});
|
|
234
|
+
const createD1BodySchema = t.object({
|
|
235
|
+
name: t.string(),
|
|
236
|
+
primary_location_hint: t.optional(t.string()),
|
|
237
|
+
jurisdiction: t.optional(t.string())
|
|
238
|
+
});
|
|
239
|
+
const createKVBodySchema = t.object({ title: t.string() });
|
|
240
|
+
const createR2BodySchema = t.object({ name: t.string() });
|
|
241
|
+
const cloudflareR2TokenSchema = t.object({
|
|
242
|
+
id: t.string(),
|
|
243
|
+
accessKeyId: t.string(),
|
|
244
|
+
secretAccessKey: t.string()
|
|
245
|
+
});
|
|
246
|
+
const createR2TokenBodySchema = t.object({
|
|
247
|
+
name: t.string(),
|
|
248
|
+
policies: t.array(t.object({
|
|
249
|
+
effect: t.string(),
|
|
250
|
+
permissions: t.array(t.string()),
|
|
251
|
+
buckets: t.optional(t.array(t.string()))
|
|
252
|
+
}))
|
|
253
|
+
});
|
|
254
|
+
const createQueueBodySchema = t.object({ queue_name: t.string() });
|
|
255
|
+
const createHyperdriveOriginSchema = t.object({
|
|
256
|
+
scheme: t.string(),
|
|
257
|
+
host: t.string(),
|
|
258
|
+
port: t.number(),
|
|
259
|
+
database: t.string(),
|
|
260
|
+
user: t.string(),
|
|
261
|
+
password: t.string()
|
|
262
|
+
});
|
|
263
|
+
const createHyperdriveBodySchema = t.object({
|
|
264
|
+
name: t.string(),
|
|
265
|
+
origin: createHyperdriveOriginSchema
|
|
266
|
+
});
|
|
267
|
+
const putSecretBodySchema = t.object({
|
|
268
|
+
name: t.string(),
|
|
269
|
+
text: t.string(),
|
|
270
|
+
type: t.string()
|
|
271
|
+
});
|
|
272
|
+
const cloudflareApiErrorSchema = t.object({
|
|
273
|
+
code: t.number(),
|
|
274
|
+
message: t.string()
|
|
275
|
+
});
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region ../../src/cli/platform-lib/services/WranglerApi.ts
|
|
278
|
+
/**
|
|
279
|
+
* Wraps wrangler CLI commands that are kept as shell-outs.
|
|
280
|
+
*
|
|
281
|
+
* Only used for operations where wrangler provides value
|
|
282
|
+
* beyond a raw API call: OAuth login, worker deploy (bundling/upload),
|
|
283
|
+
* D1 migrations, and secret bulk push.
|
|
284
|
+
*/
|
|
285
|
+
var WranglerApi = class {
|
|
286
|
+
log = $logger();
|
|
287
|
+
shell = $inject(ShellProvider);
|
|
288
|
+
utils = $inject(AlephaCliUtils);
|
|
289
|
+
pm = $inject(PackageManagerUtils);
|
|
290
|
+
runner = $inject(Runner);
|
|
291
|
+
async runShell(command, options = {}) {
|
|
292
|
+
const capture = options.capture;
|
|
293
|
+
const output = await this.shell.run(command, {
|
|
294
|
+
...options,
|
|
295
|
+
capture: capture ?? this.runner.useDynamicLogger
|
|
296
|
+
});
|
|
297
|
+
if (capture && !this.runner.useDynamicLogger) this.log.info(output);
|
|
298
|
+
return output;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Ensure wrangler is installed in the project.
|
|
302
|
+
*/
|
|
303
|
+
async ensureInstalled(root, run) {
|
|
304
|
+
await this.pm.ensureDependency(root, "wrangler", {
|
|
305
|
+
dev: true,
|
|
306
|
+
exec: async (cmd, opts) => {
|
|
307
|
+
run.pause();
|
|
308
|
+
try {
|
|
309
|
+
await this.utils.exec(cmd, opts);
|
|
310
|
+
} finally {
|
|
311
|
+
run.resume();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Check if the user is authenticated. Returns the whoami output.
|
|
318
|
+
*/
|
|
319
|
+
async whoami() {
|
|
320
|
+
return await this.runShell("wrangler whoami", {
|
|
321
|
+
resolve: true,
|
|
322
|
+
capture: true
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Open the browser-based OAuth login flow.
|
|
327
|
+
*/
|
|
328
|
+
async login() {
|
|
329
|
+
await this.runShell("wrangler login", { resolve: true });
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Get the current auth token from wrangler (auto-refreshes if expired).
|
|
333
|
+
*/
|
|
334
|
+
async getAuthToken() {
|
|
335
|
+
const output = await this.shell.run("wrangler auth token --json", {
|
|
336
|
+
resolve: true,
|
|
337
|
+
capture: true
|
|
338
|
+
});
|
|
339
|
+
return JSON.parse(output).token;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Deploy a worker via wrangler (handles bundling and upload).
|
|
343
|
+
*
|
|
344
|
+
* Returns the workers.dev URL if found in the output.
|
|
345
|
+
*/
|
|
346
|
+
async deploy(workerName, configPath, root) {
|
|
347
|
+
return (await this.runShell(`wrangler deploy --name=${workerName} --no-bundle --config=${configPath}`, {
|
|
348
|
+
resolve: true,
|
|
349
|
+
capture: true,
|
|
350
|
+
root
|
|
351
|
+
})).match(/https:\/\/[^\s]*\.workers\.dev/)?.[0];
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Apply D1 migrations remotely.
|
|
355
|
+
*/
|
|
356
|
+
async d1MigrationsApply(dbName, configPath, root) {
|
|
357
|
+
await this.runShell(`wrangler d1 migrations apply ${dbName} --remote --config=${configPath}`, {
|
|
358
|
+
resolve: true,
|
|
359
|
+
env: { CI: "1" },
|
|
360
|
+
root
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
//#endregion
|
|
365
|
+
//#region ../../src/cli/platform-lib/services/CloudflareApi.ts
|
|
366
|
+
/**
|
|
367
|
+
* Thin wrapper over the Cloudflare REST API.
|
|
368
|
+
*
|
|
369
|
+
* Uses `wrangler auth token` to obtain credentials,
|
|
370
|
+
* then calls `fetch()` directly for all CRUD operations.
|
|
371
|
+
*/
|
|
372
|
+
var CloudflareApi = class CloudflareApi {
|
|
373
|
+
static BASE = "https://api.cloudflare.com/client/v4";
|
|
374
|
+
log = $logger();
|
|
375
|
+
alepha = $inject(Alepha);
|
|
376
|
+
wrangler = $inject(WranglerApi);
|
|
377
|
+
token;
|
|
378
|
+
accountId;
|
|
379
|
+
jurisdiction;
|
|
380
|
+
/**
|
|
381
|
+
* Set the Cloudflare data jurisdiction for R2 and D1 resources.
|
|
382
|
+
*
|
|
383
|
+
* R2 buckets and D1 databases created under a jurisdiction live in a
|
|
384
|
+
* separate namespace — every R2 API call (list/create/delete) must include
|
|
385
|
+
* the `cf-r2-jurisdiction` header, and D1 create must include the field
|
|
386
|
+
* in the request body. Omit / pass `undefined` for the default (global).
|
|
387
|
+
*/
|
|
388
|
+
setJurisdiction(jurisdiction) {
|
|
389
|
+
this.jurisdiction = jurisdiction;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Override the Cloudflare account ID (from platform config).
|
|
393
|
+
*
|
|
394
|
+
* When unset, `resolveAccountId` falls back to `CLOUDFLARE_ACCOUNT_ID` env
|
|
395
|
+
* var or the token's single account.
|
|
396
|
+
*/
|
|
397
|
+
setAccountId(accountId) {
|
|
398
|
+
this.accountId = accountId;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Obtain the current auth token from wrangler.
|
|
402
|
+
*/
|
|
403
|
+
async resolveToken() {
|
|
404
|
+
if (this.token) return this.token;
|
|
405
|
+
this.token = await this.wrangler.getAuthToken();
|
|
406
|
+
return this.token;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Resolve the Cloudflare account ID.
|
|
410
|
+
*
|
|
411
|
+
* Calls /accounts and picks the first one. Cached after first call.
|
|
412
|
+
*/
|
|
413
|
+
async resolveAccountId() {
|
|
414
|
+
if (this.accountId) return this.accountId;
|
|
415
|
+
const fromEnv = process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
416
|
+
if (fromEnv) {
|
|
417
|
+
this.accountId = fromEnv;
|
|
418
|
+
return this.accountId;
|
|
419
|
+
}
|
|
420
|
+
const res = await this.fetch("/accounts", { schema: t.array(cloudflareAccountSchema) });
|
|
421
|
+
if (res.length === 0) throw new AlephaError("No Cloudflare accounts found for this token.");
|
|
422
|
+
if (res.length > 1) {
|
|
423
|
+
const list = res.map((a) => ` - ${a.id} ${a.name}`).join("\n");
|
|
424
|
+
throw new AlephaError(`Cloudflare token has access to ${res.length} accounts; set \`CLOUDFLARE_ACCOUNT_ID\` or the \`accountId\` field in your platform config to pick one:\n${list}`);
|
|
425
|
+
}
|
|
426
|
+
this.accountId = res[0].id;
|
|
427
|
+
return this.accountId;
|
|
428
|
+
}
|
|
429
|
+
async listD1() {
|
|
430
|
+
const accountId = await this.resolveAccountId();
|
|
431
|
+
return await this.paginate(`/accounts/${accountId}/d1/database`, cloudflareD1Schema);
|
|
432
|
+
}
|
|
433
|
+
async createD1(name, location = "weur") {
|
|
434
|
+
const accountId = await this.resolveAccountId();
|
|
435
|
+
const body = { name };
|
|
436
|
+
if (this.jurisdiction) body.jurisdiction = this.jurisdiction;
|
|
437
|
+
else body.primary_location_hint = location;
|
|
438
|
+
return await this.fetch(`/accounts/${accountId}/d1/database`, {
|
|
439
|
+
method: "POST",
|
|
440
|
+
body,
|
|
441
|
+
bodySchema: createD1BodySchema,
|
|
442
|
+
schema: cloudflareD1Schema
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
async deleteD1(databaseId) {
|
|
446
|
+
const accountId = await this.resolveAccountId();
|
|
447
|
+
await this.fetch(`/accounts/${accountId}/d1/database/${databaseId}`, { method: "DELETE" });
|
|
448
|
+
}
|
|
449
|
+
async listKV() {
|
|
450
|
+
const accountId = await this.resolveAccountId();
|
|
451
|
+
return await this.paginate(`/accounts/${accountId}/storage/kv/namespaces`, cloudflareKVSchema, 100);
|
|
452
|
+
}
|
|
453
|
+
async createKV(title) {
|
|
454
|
+
const accountId = await this.resolveAccountId();
|
|
455
|
+
return await this.fetch(`/accounts/${accountId}/storage/kv/namespaces`, {
|
|
456
|
+
method: "POST",
|
|
457
|
+
body: { title },
|
|
458
|
+
bodySchema: createKVBodySchema,
|
|
459
|
+
schema: cloudflareKVSchema
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
async deleteKV(namespaceId) {
|
|
463
|
+
const accountId = await this.resolveAccountId();
|
|
464
|
+
await this.fetch(`/accounts/${accountId}/storage/kv/namespaces/${namespaceId}`, { method: "DELETE" });
|
|
465
|
+
}
|
|
466
|
+
async listR2() {
|
|
467
|
+
const accountId = await this.resolveAccountId();
|
|
468
|
+
return await this.paginateCursor(`/accounts/${accountId}/r2/buckets`, "buckets", cloudflareR2Schema);
|
|
469
|
+
}
|
|
470
|
+
async createR2(name) {
|
|
471
|
+
const accountId = await this.resolveAccountId();
|
|
472
|
+
await this.fetch(`/accounts/${accountId}/r2/buckets`, {
|
|
473
|
+
method: "POST",
|
|
474
|
+
body: { name },
|
|
475
|
+
bodySchema: createR2BodySchema
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
async deleteR2(name) {
|
|
479
|
+
const accountId = await this.resolveAccountId();
|
|
480
|
+
await this.fetch(`/accounts/${accountId}/r2/buckets/${name}`, { method: "DELETE" });
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Mint a bucket-scoped R2 API token (S3 access key + secret) using the
|
|
484
|
+
* current bearer token. Used by teardown to wipe a bucket over the S3
|
|
485
|
+
* protocol without requiring users to pre-create R2 access keys.
|
|
486
|
+
*
|
|
487
|
+
* The returned token should be revoked with `deleteR2Token` as soon as the
|
|
488
|
+
* wipe is done.
|
|
489
|
+
*/
|
|
490
|
+
async createR2Token(name, bucket) {
|
|
491
|
+
const accountId = await this.resolveAccountId();
|
|
492
|
+
return await this.fetch(`/accounts/${accountId}/r2/api-tokens`, {
|
|
493
|
+
method: "POST",
|
|
494
|
+
body: {
|
|
495
|
+
name,
|
|
496
|
+
policies: [{
|
|
497
|
+
effect: "allow",
|
|
498
|
+
permissions: ["admin-read-write"],
|
|
499
|
+
buckets: [bucket]
|
|
500
|
+
}]
|
|
501
|
+
},
|
|
502
|
+
bodySchema: createR2TokenBodySchema,
|
|
503
|
+
schema: cloudflareR2TokenSchema
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
async deleteR2Token(tokenId) {
|
|
507
|
+
const accountId = await this.resolveAccountId();
|
|
508
|
+
await this.fetch(`/accounts/${accountId}/r2/api-tokens/${tokenId}`, { method: "DELETE" });
|
|
509
|
+
}
|
|
510
|
+
async listQueues() {
|
|
511
|
+
const accountId = await this.resolveAccountId();
|
|
512
|
+
return await this.paginate(`/accounts/${accountId}/queues`, cloudflareQueueSchema);
|
|
513
|
+
}
|
|
514
|
+
async createQueue(name) {
|
|
515
|
+
const accountId = await this.resolveAccountId();
|
|
516
|
+
return await this.fetch(`/accounts/${accountId}/queues`, {
|
|
517
|
+
method: "POST",
|
|
518
|
+
body: { queue_name: name },
|
|
519
|
+
bodySchema: createQueueBodySchema,
|
|
520
|
+
schema: cloudflareQueueSchema
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
async deleteQueue(queueId) {
|
|
524
|
+
const accountId = await this.resolveAccountId();
|
|
525
|
+
await this.fetch(`/accounts/${accountId}/queues/${queueId}`, { method: "DELETE" });
|
|
526
|
+
}
|
|
527
|
+
async listQueueConsumers(queueId) {
|
|
528
|
+
const accountId = await this.resolveAccountId();
|
|
529
|
+
return await this.paginate(`/accounts/${accountId}/queues/${queueId}/consumers`, cloudflareQueueConsumerSchema);
|
|
530
|
+
}
|
|
531
|
+
async deleteQueueConsumer(queueId, consumerService) {
|
|
532
|
+
const accountId = await this.resolveAccountId();
|
|
533
|
+
const consumer = (await this.listQueueConsumers(queueId)).find((c) => c.service === consumerService);
|
|
534
|
+
if (!consumer) return;
|
|
535
|
+
await this.fetch(`/accounts/${accountId}/queues/${queueId}/consumers/${consumer.consumer_id}`, { method: "DELETE" });
|
|
536
|
+
}
|
|
537
|
+
async listHyperdrive() {
|
|
538
|
+
const accountId = await this.resolveAccountId();
|
|
539
|
+
return await this.paginate(`/accounts/${accountId}/hyperdrive/configs`, cloudflareHyperdriveSchema);
|
|
540
|
+
}
|
|
541
|
+
async createHyperdrive(name, connectionString) {
|
|
542
|
+
const accountId = await this.resolveAccountId();
|
|
543
|
+
return await this.fetch(`/accounts/${accountId}/hyperdrive/configs`, {
|
|
544
|
+
method: "POST",
|
|
545
|
+
body: {
|
|
546
|
+
name,
|
|
547
|
+
origin: this.parseConnectionString(connectionString)
|
|
548
|
+
},
|
|
549
|
+
bodySchema: createHyperdriveBodySchema,
|
|
550
|
+
schema: cloudflareHyperdriveSchema
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
async deleteHyperdrive(configId) {
|
|
554
|
+
const accountId = await this.resolveAccountId();
|
|
555
|
+
await this.fetch(`/accounts/${accountId}/hyperdrive/configs/${configId}`, { method: "DELETE" });
|
|
556
|
+
}
|
|
557
|
+
async getWorker(scriptName) {
|
|
558
|
+
const accountId = await this.resolveAccountId();
|
|
559
|
+
try {
|
|
560
|
+
return await this.fetch(`/accounts/${accountId}/workers/scripts/${scriptName}`, { schema: cloudflareWorkerSchema });
|
|
561
|
+
} catch {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async deleteWorker(scriptName) {
|
|
566
|
+
const accountId = await this.resolveAccountId();
|
|
567
|
+
await this.fetch(`/accounts/${accountId}/workers/scripts/${scriptName}`, {
|
|
568
|
+
method: "DELETE",
|
|
569
|
+
query: { force: "true" }
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
async listDeployments(scriptName) {
|
|
573
|
+
const accountId = await this.resolveAccountId();
|
|
574
|
+
return (await this.fetch(`/accounts/${accountId}/workers/scripts/${scriptName}/deployments`, {
|
|
575
|
+
schema: cloudflareDeploymentListSchema,
|
|
576
|
+
query: { per_page: "100" }
|
|
577
|
+
})).deployments;
|
|
578
|
+
}
|
|
579
|
+
async listVersions(scriptName) {
|
|
580
|
+
const accountId = await this.resolveAccountId();
|
|
581
|
+
return await this.paginateCursor(`/accounts/${accountId}/workers/scripts/${scriptName}/versions`, "items", cloudflareVersionSchema);
|
|
582
|
+
}
|
|
583
|
+
async listSecrets(scriptName) {
|
|
584
|
+
const accountId = await this.resolveAccountId();
|
|
585
|
+
return await this.fetch(`/accounts/${accountId}/workers/scripts/${scriptName}/secrets`, { schema: t.array(cloudflareSecretSchema) });
|
|
586
|
+
}
|
|
587
|
+
async putSecret(scriptName, name, value) {
|
|
588
|
+
const accountId = await this.resolveAccountId();
|
|
589
|
+
await this.fetch(`/accounts/${accountId}/workers/scripts/${scriptName}/secrets`, {
|
|
590
|
+
method: "PUT",
|
|
591
|
+
body: {
|
|
592
|
+
name,
|
|
593
|
+
text: value,
|
|
594
|
+
type: "secret_text"
|
|
595
|
+
},
|
|
596
|
+
bodySchema: putSecretBodySchema
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Fetch the current worker bindings via the script-settings endpoint.
|
|
601
|
+
* Used to merge new secrets into the existing binding set in one PATCH
|
|
602
|
+
* (avoids the per-secret `putSecret` calls, each of which creates a
|
|
603
|
+
* Cloudflare deployment — pushing 7 secrets meant 7 deployment rows).
|
|
604
|
+
*
|
|
605
|
+
* Secret bindings come back with `name` + `type` but no `text` (they're
|
|
606
|
+
* write-only on Cloudflare's side); to preserve them across a PATCH we
|
|
607
|
+
* forward each one as `{ type, name }` and Cloudflare keeps the stored
|
|
608
|
+
* value.
|
|
609
|
+
*/
|
|
610
|
+
async getWorkerSettings(scriptName) {
|
|
611
|
+
const accountId = await this.resolveAccountId();
|
|
612
|
+
return await this.fetch(`/accounts/${accountId}/workers/scripts/${scriptName}/settings`);
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Replace the worker's binding set in one call (= one Cloudflare
|
|
616
|
+
* deployment, regardless of how many secrets are being updated).
|
|
617
|
+
*
|
|
618
|
+
* The endpoint expects multipart FormData with a `settings` field whose
|
|
619
|
+
* value is a JSON-encoded `{ bindings: [...] }` — the `fetch` helper
|
|
620
|
+
* above is JSON-only, so this one bypasses it and calls `globalThis.fetch`
|
|
621
|
+
* directly. Mirrors what `wrangler secret bulk` does internally.
|
|
622
|
+
*/
|
|
623
|
+
async patchWorkerBindings(scriptName, bindings) {
|
|
624
|
+
const accountId = await this.resolveAccountId();
|
|
625
|
+
const token = await this.resolveToken();
|
|
626
|
+
const url = `${CloudflareApi.BASE}/accounts/${accountId}/workers/scripts/${scriptName}/settings`;
|
|
627
|
+
const form = new FormData();
|
|
628
|
+
form.set("settings", JSON.stringify({ bindings }));
|
|
629
|
+
const response = await globalThis.fetch(url, {
|
|
630
|
+
method: "PATCH",
|
|
631
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
632
|
+
body: form
|
|
633
|
+
});
|
|
634
|
+
const json = await response.json().catch(() => null);
|
|
635
|
+
if (!response.ok || !json?.success) {
|
|
636
|
+
const messages = json?.errors?.map((e) => e.message).join(", ");
|
|
637
|
+
throw new AlephaError(`Cloudflare API error (PATCH /workers/scripts/${scriptName}/settings): ${messages ?? response.statusText}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async fetch(path, options = {}) {
|
|
641
|
+
const token = await this.resolveToken();
|
|
642
|
+
const { method = "GET", body, query } = options;
|
|
643
|
+
let url = `${CloudflareApi.BASE}${path}`;
|
|
644
|
+
if (query) {
|
|
645
|
+
const params = new URLSearchParams(query);
|
|
646
|
+
url += `?${params.toString()}`;
|
|
647
|
+
}
|
|
648
|
+
const headers = { Authorization: `Bearer ${token}` };
|
|
649
|
+
if (this.jurisdiction && /\/r2\//.test(path)) headers["cf-r2-jurisdiction"] = this.jurisdiction;
|
|
650
|
+
const init = {
|
|
651
|
+
method,
|
|
652
|
+
headers
|
|
653
|
+
};
|
|
654
|
+
if (body) {
|
|
655
|
+
headers["Content-Type"] = "application/json";
|
|
656
|
+
const validated = options.bodySchema ? this.alepha.codec.validate(options.bodySchema, body) : body;
|
|
657
|
+
init.body = JSON.stringify(validated);
|
|
658
|
+
}
|
|
659
|
+
const json = await (await globalThis.fetch(url, init)).json();
|
|
660
|
+
if (!json.success) throw new AlephaError(`Cloudflare API error (${method} ${path}): ${json.errors.map((e) => e.message).join(", ")}`);
|
|
661
|
+
if (options.schema) return this.alepha.codec.validate(options.schema, json.result);
|
|
662
|
+
return json.result;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Paginate a page-based list endpoint (`result_info.total_pages`).
|
|
666
|
+
*
|
|
667
|
+
* Cloudflare defaults to `per_page=20`; we push it to 1000 (max on most
|
|
668
|
+
* list endpoints) and loop if more pages exist. Each page is validated
|
|
669
|
+
* against the item schema.
|
|
670
|
+
*/
|
|
671
|
+
async paginate(path, itemSchema, perPage = 1e3) {
|
|
672
|
+
const results = [];
|
|
673
|
+
let page = 1;
|
|
674
|
+
while (true) {
|
|
675
|
+
const token = await this.resolveToken();
|
|
676
|
+
const url = `${CloudflareApi.BASE}${path}?per_page=${perPage}&page=${page}`;
|
|
677
|
+
const headers = { Authorization: `Bearer ${token}` };
|
|
678
|
+
if (this.jurisdiction && /\/r2\//.test(path)) headers["cf-r2-jurisdiction"] = this.jurisdiction;
|
|
679
|
+
const json = await (await globalThis.fetch(url, {
|
|
680
|
+
method: "GET",
|
|
681
|
+
headers
|
|
682
|
+
})).json();
|
|
683
|
+
if (!json.success) throw new AlephaError(`Cloudflare API error (GET ${path}): ${json.errors.map((e) => e.message).join(", ")}`);
|
|
684
|
+
const validated = this.alepha.codec.validate(t.array(itemSchema), json.result);
|
|
685
|
+
results.push(...validated);
|
|
686
|
+
const totalPages = json.result_info?.total_pages;
|
|
687
|
+
if (!totalPages || page >= totalPages || validated.length === 0) break;
|
|
688
|
+
page++;
|
|
689
|
+
}
|
|
690
|
+
return results;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Paginate a cursor-based list endpoint where `result` is an object
|
|
694
|
+
* containing both the items array and a `cursor` field (R2 buckets,
|
|
695
|
+
* Workers versions). Returns the flattened item array.
|
|
696
|
+
*/
|
|
697
|
+
async paginateCursor(path, itemsKey, itemSchema, perPage = 1e3) {
|
|
698
|
+
const results = [];
|
|
699
|
+
let cursor;
|
|
700
|
+
while (true) {
|
|
701
|
+
const query = { per_page: String(perPage) };
|
|
702
|
+
if (cursor) query.cursor = cursor;
|
|
703
|
+
const res = await this.fetch(path, { query });
|
|
704
|
+
const items = res[itemsKey] ?? [];
|
|
705
|
+
const validated = this.alepha.codec.validate(t.array(itemSchema), items);
|
|
706
|
+
results.push(...validated);
|
|
707
|
+
const nextCursor = res.cursor;
|
|
708
|
+
if (!nextCursor || validated.length === 0) break;
|
|
709
|
+
cursor = nextCursor;
|
|
710
|
+
}
|
|
711
|
+
return results;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Parse a postgres:// connection string into Hyperdrive origin fields.
|
|
715
|
+
*/
|
|
716
|
+
parseConnectionString(connectionString) {
|
|
717
|
+
const url = new URL(connectionString);
|
|
718
|
+
return {
|
|
719
|
+
scheme: "postgres",
|
|
720
|
+
host: url.hostname,
|
|
721
|
+
port: Number(url.port) || 5432,
|
|
722
|
+
database: url.pathname.slice(1),
|
|
723
|
+
user: decodeURIComponent(url.username),
|
|
724
|
+
password: decodeURIComponent(url.password)
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
//#endregion
|
|
729
|
+
//#region ../../src/cli/platform-lib/services/NamingService.ts
|
|
730
|
+
/**
|
|
731
|
+
* Validate a `--tenant` value against the app's declared `tenancy` and
|
|
732
|
+
* return the effective tenant (or `undefined` for a base/non-tenanted
|
|
733
|
+
* deploy). Throws on any matrix violation so a misconfigured deploy fails
|
|
734
|
+
* fast instead of landing on the wrong resource names / host.
|
|
735
|
+
*/
|
|
736
|
+
function resolveTenant(tenancy, tenant) {
|
|
737
|
+
const mode = tenancy ?? "none";
|
|
738
|
+
if (tenant !== void 0 && !/^[a-z0-9][a-z0-9-]*$/.test(tenant)) throw new AlephaError(`Invalid --tenant "${tenant}": must be a slug (lowercase alphanumeric + dashes, e.g. "b14").`);
|
|
739
|
+
if (mode === "none") {
|
|
740
|
+
if (tenant !== void 0) throw new AlephaError(`This app is not tenanted (tenancy: "none") but --tenant "${tenant}" was given. Set tenancy: "optional" | "required" in platform() to deploy per-tenant.`);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (mode === "required" && tenant === void 0) throw new AlephaError(`This app requires a tenant (tenancy: "required"). Pass --tenant <slug>.`);
|
|
744
|
+
return tenant;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Resolve the host a deploy is served on.
|
|
748
|
+
*
|
|
749
|
+
* - `override` (V2 custom domains, e.g. `reservation.club-b14.fr`) wins
|
|
750
|
+
* outright when supplied — not wired to a flag today, but the single
|
|
751
|
+
* seam a future `--domain` / Rocket `config.hostname` plugs into.
|
|
752
|
+
* - else a tenant becomes the leftmost DNS label: `b14` + `alepha.club`
|
|
753
|
+
* → `b14.alepha.club`.
|
|
754
|
+
* - else the base domain is used as-is.
|
|
755
|
+
*/
|
|
756
|
+
function tenantDomain(base, tenant, override) {
|
|
757
|
+
if (override) return override;
|
|
758
|
+
if (!base) return base;
|
|
759
|
+
return tenant ? `${tenant}.${base}` : base;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Generates deterministic resource names for cloud deployments.
|
|
763
|
+
*
|
|
764
|
+
* Pattern: `<tenant>-<project>-<env>` (tenant segment omitted when the
|
|
765
|
+
* deploy isn't tenanted).
|
|
766
|
+
*
|
|
767
|
+
* All segments are slugified (lowercase, alphanumeric + dashes, max 63
|
|
768
|
+
* chars). One app per workspace — see `alepha platform`.
|
|
769
|
+
*/
|
|
770
|
+
var NamingService = class {
|
|
771
|
+
forContext(project, env, tenant) {
|
|
772
|
+
return new NamingContext([
|
|
773
|
+
tenant,
|
|
774
|
+
project,
|
|
775
|
+
env
|
|
776
|
+
].filter((segment) => !!segment).map((segment) => this.slugify(segment)).join("-"));
|
|
777
|
+
}
|
|
778
|
+
slugify(name) {
|
|
779
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
var NamingContext = class {
|
|
783
|
+
prefix;
|
|
784
|
+
constructor(prefix) {
|
|
785
|
+
this.prefix = prefix;
|
|
786
|
+
}
|
|
787
|
+
worker() {
|
|
788
|
+
return this.prefix;
|
|
789
|
+
}
|
|
790
|
+
d1() {
|
|
791
|
+
return this.prefix;
|
|
792
|
+
}
|
|
793
|
+
hyperdrive() {
|
|
794
|
+
return this.prefix;
|
|
795
|
+
}
|
|
796
|
+
r2() {
|
|
797
|
+
return this.prefix;
|
|
798
|
+
}
|
|
799
|
+
kv() {
|
|
800
|
+
return this.prefix;
|
|
801
|
+
}
|
|
802
|
+
queue() {
|
|
803
|
+
return this.prefix;
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
//#endregion
|
|
807
|
+
//#region ../../src/cli/platform-lib/adapters/PlatformAdapter.ts
|
|
808
|
+
/**
|
|
809
|
+
* Abstract platform adapter.
|
|
810
|
+
*
|
|
811
|
+
* Each cloud provider (Cloudflare, AKS, docker-compose) implements this.
|
|
812
|
+
* The PlatformOrchestrator calls these methods in the correct order.
|
|
813
|
+
*/
|
|
814
|
+
var PlatformAdapter = class {
|
|
815
|
+
/**
|
|
816
|
+
* Create/ensure cloud resources exist (DB, buckets, queues).
|
|
817
|
+
* Not all adapters provision -- AKS defers to Helm.
|
|
818
|
+
*/
|
|
819
|
+
async provision(_ctx, _run) {}
|
|
820
|
+
/**
|
|
821
|
+
* Run database migrations.
|
|
822
|
+
*/
|
|
823
|
+
async migrate(_ctx, _run) {}
|
|
824
|
+
/**
|
|
825
|
+
* Export the deployed database to a local file — the remote → local dev
|
|
826
|
+
* snapshot workflow. Adapter/dialect specific; the default refuses.
|
|
827
|
+
*/
|
|
828
|
+
async exportDb(_ctx, _run, _options = {}) {
|
|
829
|
+
throw new AlephaError(`Database export is not supported by the '${this.constructor.name}' adapter.`);
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Push runtime secrets to the deployed worker(s).
|
|
833
|
+
*
|
|
834
|
+
* Reads secrets from `.env.{env}` files (parsed, not from process.env),
|
|
835
|
+
* filters out vars already handled by bindings (DATABASE_URL, R2, etc.),
|
|
836
|
+
* and pushes the rest via the platform's secret management.
|
|
837
|
+
*/
|
|
838
|
+
async secrets(_ctx, _run) {}
|
|
839
|
+
};
|
|
840
|
+
//#endregion
|
|
841
|
+
//#region ../../src/cli/platform-lib/adapters/CloudflareAdapter.ts
|
|
842
|
+
/**
|
|
843
|
+
* Cloudflare Workers adapter.
|
|
844
|
+
*
|
|
845
|
+
* Uses the Cloudflare REST API (via CloudflareApi) for resource provisioning
|
|
846
|
+
* and teardown, and wrangler CLI (via WranglerApi) for login, deploy,
|
|
847
|
+
* D1 migrations, and secret bulk push.
|
|
848
|
+
*/
|
|
849
|
+
var CloudflareAdapter = class CloudflareAdapter extends PlatformAdapter {
|
|
850
|
+
log = $logger();
|
|
851
|
+
fs = $inject(FileSystemProvider);
|
|
852
|
+
shell = $inject(ShellProvider);
|
|
853
|
+
cache = $inject(PlatformCacheProvider);
|
|
854
|
+
alepha = $inject(Alepha);
|
|
855
|
+
envUtils = $inject(EnvUtils);
|
|
856
|
+
api = $inject(CloudflareApi);
|
|
857
|
+
wrangler = $inject(WranglerApi);
|
|
858
|
+
runner = $inject(Runner);
|
|
859
|
+
buildTask = $inject(BuildCloudflareTask);
|
|
860
|
+
options = $state(platformOptions);
|
|
861
|
+
provisionedD1Id;
|
|
862
|
+
provisionedHyperdriveId;
|
|
863
|
+
provisionedKVIds = /* @__PURE__ */ new Map();
|
|
864
|
+
/**
|
|
865
|
+
* Check if the user's DATABASE_URL points to an external Postgres database.
|
|
866
|
+
* If so, we use Hyperdrive instead of D1.
|
|
867
|
+
*
|
|
868
|
+
* Reads from `.env.{env}` first, falls back to `process.env`.
|
|
869
|
+
*/
|
|
870
|
+
async isPostgres(ctx) {
|
|
871
|
+
return !!((await this.envUtils.parseEnv(ctx.root, [`.env.${ctx.env}`])).DATABASE_URL ?? process.env.DATABASE_URL)?.startsWith("postgres:");
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Propagate the environment's data-jurisdiction setting to the API client.
|
|
875
|
+
*
|
|
876
|
+
* Must be invoked at the top of every entry point (authenticate, build,
|
|
877
|
+
* deploy, secrets, provision, migrate, inspect, teardown) because
|
|
878
|
+
* CloudflareApi is a singleton reused across env invocations.
|
|
879
|
+
*/
|
|
880
|
+
configureApi(ctx) {
|
|
881
|
+
this.api.setJurisdiction(ctx.envConfig.jurisdiction);
|
|
882
|
+
this.api.setAccountId(ctx.envConfig.accountId);
|
|
883
|
+
}
|
|
884
|
+
async runShell(command, options = {}) {
|
|
885
|
+
const capture = options.capture;
|
|
886
|
+
const output = await this.shell.run(command, {
|
|
887
|
+
...options,
|
|
888
|
+
capture: capture ?? this.runner.useDynamicLogger
|
|
889
|
+
});
|
|
890
|
+
if (capture && !this.runner.useDynamicLogger) this.log.info(output);
|
|
891
|
+
return output;
|
|
892
|
+
}
|
|
893
|
+
async authenticate(ctx, run) {
|
|
894
|
+
this.configureApi(ctx);
|
|
895
|
+
await run({
|
|
896
|
+
name: "authenticate",
|
|
897
|
+
handler: async () => {
|
|
898
|
+
await this.wrangler.ensureInstalled(ctx.root, run);
|
|
899
|
+
let needsLogin = false;
|
|
900
|
+
try {
|
|
901
|
+
await this.wrangler.getAuthToken();
|
|
902
|
+
} catch {
|
|
903
|
+
needsLogin = true;
|
|
904
|
+
}
|
|
905
|
+
if (needsLogin) {
|
|
906
|
+
run.pause();
|
|
907
|
+
await this.wrangler.login();
|
|
908
|
+
run.resume();
|
|
909
|
+
}
|
|
910
|
+
if (await this.cache.isLoginFresh(ctx.root, "cloudflare")) return;
|
|
911
|
+
try {
|
|
912
|
+
const accountId = await this.api.resolveAccountId();
|
|
913
|
+
await this.cache.recordLogin(ctx.root, "cloudflare", accountId);
|
|
914
|
+
} catch {
|
|
915
|
+
await this.cache.recordLogin(ctx.root, "cloudflare");
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
async build(ctx, run) {
|
|
921
|
+
this.configureApi(ctx);
|
|
922
|
+
const appDir = ctx.root;
|
|
923
|
+
const env = {};
|
|
924
|
+
if (ctx.resources.hasDatabase) {
|
|
925
|
+
if (this.provisionedHyperdriveId) {
|
|
926
|
+
env.HYPERDRIVE_ID = this.provisionedHyperdriveId;
|
|
927
|
+
const pgSchema = (await this.envUtils.parseEnv(ctx.root, [`.env.${ctx.env}`])).POSTGRES_SCHEMA ?? process.env.POSTGRES_SCHEMA;
|
|
928
|
+
if (pgSchema) env.POSTGRES_SCHEMA = pgSchema;
|
|
929
|
+
} else if (this.provisionedD1Id) env.DATABASE_URL = `d1://${ctx.naming.d1()}:${this.provisionedD1Id}`;
|
|
930
|
+
}
|
|
931
|
+
if (ctx.resources.hasBucket) env.R2_BUCKET_NAME = ctx.naming.r2();
|
|
932
|
+
if (ctx.resources.hasKV) {
|
|
933
|
+
const kvName = ctx.naming.kv();
|
|
934
|
+
env.CLOUDFLARE_KV_NAME = kvName;
|
|
935
|
+
const kvId = this.provisionedKVIds.get(kvName);
|
|
936
|
+
if (kvId) env.CLOUDFLARE_KV_ID = kvId;
|
|
937
|
+
}
|
|
938
|
+
if (ctx.resources.hasQueue) env.CLOUDFLARE_QUEUE_NAME = ctx.naming.queue();
|
|
939
|
+
const host = tenantDomain(ctx.envConfig.domain, ctx.tenant);
|
|
940
|
+
if (host) {
|
|
941
|
+
if (host.includes("*") && !ctx.envConfig.zone) throw new AlephaError(`Wildcard domain "${host}" requires "zone" to be set in the environment config (the Cloudflare zone name, e.g. "alepha.dev").`);
|
|
942
|
+
env.CLOUDFLARE_DOMAIN = host;
|
|
943
|
+
if (ctx.envConfig.zone) env.CLOUDFLARE_ZONE = ctx.envConfig.zone;
|
|
944
|
+
}
|
|
945
|
+
if (ctx.envConfig.jurisdiction) env.CLOUDFLARE_JURISDICTION = ctx.envConfig.jurisdiction;
|
|
946
|
+
if (ctx.prebuilt) {
|
|
947
|
+
await run({
|
|
948
|
+
name: "alepha build -t cloudflare --prebuilt (in-process)",
|
|
949
|
+
handler: async () => {
|
|
950
|
+
await this.runBuildInProcess(appDir, env);
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
const cmd = "alepha build -t cloudflare";
|
|
956
|
+
await run({
|
|
957
|
+
name: cmd,
|
|
958
|
+
handler: async () => {
|
|
959
|
+
await this.runShell(cmd, {
|
|
960
|
+
root: appDir,
|
|
961
|
+
env
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Library-embed of `alepha build -t cloudflare --prebuilt`. Loads the
|
|
968
|
+
* pre-built `dist/manifest.json`, sets the per-tenant env vars on
|
|
969
|
+
* `process.env` for the duration of the call (the task's enhance*
|
|
970
|
+
* methods read them directly), then runs `BuildCloudflareTask`
|
|
971
|
+
* against a synthetic context.
|
|
972
|
+
*
|
|
973
|
+
* `ctx.alepha` is intentionally null — in manifest mode the task
|
|
974
|
+
* reads resources/crons/containers from `ctx.manifest` and never
|
|
975
|
+
* dereferences `ctx.alepha`. Same for `entry` and `hasClient`:
|
|
976
|
+
* prebuilt mode skips the bundle tasks; only the wrangler.jsonc /
|
|
977
|
+
* worker-entrypoint emission runs.
|
|
978
|
+
*/
|
|
979
|
+
async runBuildInProcess(root, env) {
|
|
980
|
+
const manifestPath = join(root, "dist", "manifest.json");
|
|
981
|
+
let manifest;
|
|
982
|
+
try {
|
|
983
|
+
manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
|
|
984
|
+
} catch (err) {
|
|
985
|
+
throw new AlephaError(`Cannot read ${manifestPath}: ${err.message}. Prebuilt deploys require dist/manifest.json (emitted by \`alepha build -t cloudflare\`).`);
|
|
986
|
+
}
|
|
987
|
+
const ctx = {
|
|
988
|
+
alepha: null,
|
|
989
|
+
options: {
|
|
990
|
+
target: "cloudflare",
|
|
991
|
+
output: {
|
|
992
|
+
dist: "dist",
|
|
993
|
+
public: "public"
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
run: this.runner.run,
|
|
997
|
+
root,
|
|
998
|
+
entry: {
|
|
999
|
+
root,
|
|
1000
|
+
server: ""
|
|
1001
|
+
},
|
|
1002
|
+
hasClient: false,
|
|
1003
|
+
manifest,
|
|
1004
|
+
platformOptions: null,
|
|
1005
|
+
flags: { prebuilt: true }
|
|
1006
|
+
};
|
|
1007
|
+
const previous = {};
|
|
1008
|
+
for (const [k, v] of Object.entries(env)) {
|
|
1009
|
+
previous[k] = process.env[k];
|
|
1010
|
+
process.env[k] = v;
|
|
1011
|
+
}
|
|
1012
|
+
try {
|
|
1013
|
+
await this.buildTask.run(ctx);
|
|
1014
|
+
} finally {
|
|
1015
|
+
for (const [k, prev] of Object.entries(previous)) if (prev === void 0) delete process.env[k];
|
|
1016
|
+
else process.env[k] = prev;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async deploy(ctx, run) {
|
|
1020
|
+
this.configureApi(ctx);
|
|
1021
|
+
const workerName = ctx.naming.worker();
|
|
1022
|
+
const distDir = this.fs.join(ctx.root, "dist");
|
|
1023
|
+
let url;
|
|
1024
|
+
await run({
|
|
1025
|
+
name: `deploy worker ${ctx.project}`,
|
|
1026
|
+
handler: async () => {
|
|
1027
|
+
url = await this.wrangler.deploy(workerName, `${distDir}/wrangler.jsonc`, ctx.root);
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
return url;
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Vars that are handled by wrangler bindings or build config.
|
|
1034
|
+
* These should not be pushed as secrets.
|
|
1035
|
+
*/
|
|
1036
|
+
static EXCLUDED_SECRET_KEYS = new Set([
|
|
1037
|
+
"DATABASE_URL",
|
|
1038
|
+
"R2_BUCKET_NAME",
|
|
1039
|
+
"CLOUDFLARE_DOMAIN",
|
|
1040
|
+
"CLOUDFLARE_ZONE",
|
|
1041
|
+
"CLOUDFLARE_JURISDICTION",
|
|
1042
|
+
"HYPERDRIVE_ID",
|
|
1043
|
+
"POSTGRES_SCHEMA",
|
|
1044
|
+
"NODE_ENV",
|
|
1045
|
+
"LOG_LEVEL",
|
|
1046
|
+
"LOG_FORMAT",
|
|
1047
|
+
"SERVER_HOST",
|
|
1048
|
+
"SERVER_PORT",
|
|
1049
|
+
"TRUST_PROXY",
|
|
1050
|
+
"REACT_SSR_ENABLED",
|
|
1051
|
+
"DATABASE_SYNC",
|
|
1052
|
+
"DEBUG"
|
|
1053
|
+
]);
|
|
1054
|
+
/**
|
|
1055
|
+
* Read the build manifest's `env` list (every key the app declares via
|
|
1056
|
+
* `$env`) from `dist/manifest.json`. Used as the default worker-secret
|
|
1057
|
+
* allowlist. Returns `undefined` when the manifest is absent or predates
|
|
1058
|
+
* the `env` field, so the caller falls back to the `.env` file keys.
|
|
1059
|
+
*/
|
|
1060
|
+
async readManifestEnvKeys(root) {
|
|
1061
|
+
try {
|
|
1062
|
+
const manifest = await this.fs.readJsonFile(this.fs.join(root, "dist", "manifest.json"));
|
|
1063
|
+
return Array.isArray(manifest.env) ? manifest.env : void 0;
|
|
1064
|
+
} catch {
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
async secrets(ctx, run) {
|
|
1069
|
+
this.configureApi(ctx);
|
|
1070
|
+
const envVars = await this.envUtils.parseEnv(ctx.root, [`.env.${ctx.env}`]);
|
|
1071
|
+
const declaredKeys = this.options?.secrets?.keys;
|
|
1072
|
+
const manifestKeys = await this.readManifestEnvKeys(ctx.root);
|
|
1073
|
+
const localKeys = Object.keys(await this.envUtils.parseEnv(ctx.root, [`.env.${ctx.env}.local`]));
|
|
1074
|
+
const keys = declaredKeys ?? Array.from(new Set([...manifestKeys ?? Object.keys(envVars), ...localKeys]));
|
|
1075
|
+
const secrets = {};
|
|
1076
|
+
for (const key of keys) {
|
|
1077
|
+
if (CloudflareAdapter.EXCLUDED_SECRET_KEYS.has(key)) continue;
|
|
1078
|
+
if (key.startsWith("VITE_")) continue;
|
|
1079
|
+
const value = envVars[key] ?? process.env[key];
|
|
1080
|
+
if (!value) continue;
|
|
1081
|
+
secrets[key] = value;
|
|
1082
|
+
}
|
|
1083
|
+
if (!secrets.PUBLIC_URL) {
|
|
1084
|
+
const url = this.publicUrl(ctx);
|
|
1085
|
+
if (url) secrets.PUBLIC_URL = url;
|
|
1086
|
+
}
|
|
1087
|
+
if (Object.keys(secrets).length === 0) return;
|
|
1088
|
+
const hash = computeSecretsHash(secrets);
|
|
1089
|
+
{
|
|
1090
|
+
const workerName = ctx.naming.worker();
|
|
1091
|
+
await run({
|
|
1092
|
+
name: `push secrets to ${workerName} (bulk)`,
|
|
1093
|
+
handler: async () => {
|
|
1094
|
+
const existingBindings = (await this.api.getWorkerSettings(workerName)).bindings ?? [];
|
|
1095
|
+
if (existingBindings.find((b) => b.type === "plain_text" && b.name === CloudflareAdapter.SECRETS_HASH_BINDING)?.text === hash) {
|
|
1096
|
+
this.log.info(`Secrets for ${workerName} unchanged (hash ${hash.slice(0, 8)}…), skipping push.`);
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
const overwriting = new Set(Object.keys(secrets));
|
|
1100
|
+
const inherit = existingBindings.filter((b) => !(b.type === "plain_text" && b.name === CloudflareAdapter.SECRETS_HASH_BINDING) && (b.type !== "secret_text" || !overwriting.has(b.name))).map((b) => ({
|
|
1101
|
+
type: b.type,
|
|
1102
|
+
name: b.name
|
|
1103
|
+
}));
|
|
1104
|
+
const upsert = Object.entries(secrets).map(([name, text]) => ({
|
|
1105
|
+
type: "secret_text",
|
|
1106
|
+
name,
|
|
1107
|
+
text
|
|
1108
|
+
}));
|
|
1109
|
+
await this.api.patchWorkerBindings(workerName, [
|
|
1110
|
+
...inherit,
|
|
1111
|
+
...upsert,
|
|
1112
|
+
{
|
|
1113
|
+
type: "plain_text",
|
|
1114
|
+
name: CloudflareAdapter.SECRETS_HASH_BINDING,
|
|
1115
|
+
text: hash
|
|
1116
|
+
}
|
|
1117
|
+
]);
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Public base URL for this deploy, derived from the configured domain
|
|
1124
|
+
* (honoring tenant subdomains). Returns undefined when no domain is set or
|
|
1125
|
+
* the host is a wildcard — there's no single resolvable origin to point at.
|
|
1126
|
+
*/
|
|
1127
|
+
publicUrl(ctx) {
|
|
1128
|
+
const host = tenantDomain(ctx.envConfig.domain, ctx.tenant);
|
|
1129
|
+
if (!host || host.includes("*")) return;
|
|
1130
|
+
return `https://${host}`;
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Plain-text binding used to fingerprint the deployed secret set so the
|
|
1134
|
+
* next `up` can skip the PATCH when nothing has changed.
|
|
1135
|
+
*/
|
|
1136
|
+
static SECRETS_HASH_BINDING = "ALEPHA_SECRETS_HASH";
|
|
1137
|
+
async provision(ctx, run) {
|
|
1138
|
+
this.configureApi(ctx);
|
|
1139
|
+
const needsDB = ctx.resources.hasDatabase;
|
|
1140
|
+
const needsBucket = ctx.resources.hasBucket;
|
|
1141
|
+
const postgres = needsDB && await this.isPostgres(ctx);
|
|
1142
|
+
const tasks = [];
|
|
1143
|
+
if (needsDB) if (postgres) {
|
|
1144
|
+
const hdName = ctx.naming.hyperdrive();
|
|
1145
|
+
const dbUrl = (await this.envUtils.parseEnv(ctx.root, [`.env.${ctx.env}`])).DATABASE_URL ?? process.env.DATABASE_URL;
|
|
1146
|
+
tasks.push({
|
|
1147
|
+
name: `provision hyperdrive (${hdName})`,
|
|
1148
|
+
handler: async () => {
|
|
1149
|
+
this.provisionedHyperdriveId = await this.ensureHyperdrive(hdName, dbUrl);
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
} else {
|
|
1153
|
+
const dbName = ctx.naming.d1();
|
|
1154
|
+
tasks.push({
|
|
1155
|
+
name: `provision d1 (${dbName})`,
|
|
1156
|
+
handler: async () => {
|
|
1157
|
+
this.provisionedD1Id = await this.ensureD1(dbName);
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
if (needsBucket) {
|
|
1162
|
+
const bucketName = ctx.naming.r2();
|
|
1163
|
+
tasks.push({
|
|
1164
|
+
name: `provision r2 (${bucketName})`,
|
|
1165
|
+
handler: async () => {
|
|
1166
|
+
await this.ensureR2(bucketName);
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
if (ctx.resources.hasKV) {
|
|
1171
|
+
const kvName = ctx.naming.kv();
|
|
1172
|
+
tasks.push({
|
|
1173
|
+
name: `provision kv (${kvName})`,
|
|
1174
|
+
handler: async () => {
|
|
1175
|
+
this.provisionedKVIds.set(kvName, await this.ensureKV(kvName));
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
if (ctx.resources.hasQueue) {
|
|
1180
|
+
const queueName = ctx.naming.queue();
|
|
1181
|
+
tasks.push({
|
|
1182
|
+
name: `provision queue (${queueName})`,
|
|
1183
|
+
handler: async () => {
|
|
1184
|
+
await this.ensureQueue(queueName);
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
await run(tasks);
|
|
1189
|
+
}
|
|
1190
|
+
async migrate(ctx, run) {
|
|
1191
|
+
this.configureApi(ctx);
|
|
1192
|
+
if (!ctx.resources.hasDatabase) return;
|
|
1193
|
+
if (await this.isPostgres(ctx)) await this.migratePostgres(ctx, run);
|
|
1194
|
+
else await this.migrateD1(ctx, run);
|
|
1195
|
+
}
|
|
1196
|
+
async exportDb(ctx, run, options = {}) {
|
|
1197
|
+
this.configureApi(ctx);
|
|
1198
|
+
if (!ctx.resources.hasDatabase) throw new AlephaError("No database detected for this app — nothing to export.");
|
|
1199
|
+
if (await this.isPostgres(ctx)) throw new AlephaError("Database export currently supports Cloudflare D1 only — Postgres/Hyperdrive export (pg_dump) is not implemented yet.");
|
|
1200
|
+
const dbName = ctx.naming.d1();
|
|
1201
|
+
const tmpDir = this.fs.join(ctx.root, "node_modules", ".alepha");
|
|
1202
|
+
const sqlPath = this.fs.join(tmpDir, `${dbName}.sql`);
|
|
1203
|
+
const dbPath = options.output ?? this.fs.join(tmpDir, "sqlite.db");
|
|
1204
|
+
await this.fs.mkdir(tmpDir, { recursive: true });
|
|
1205
|
+
await run(`wrangler d1 export ${dbName} --remote --output=${sqlPath}`, { alias: `export D1 ${dbName} → ${sqlPath}` });
|
|
1206
|
+
await this.fs.rm(dbPath, { force: true });
|
|
1207
|
+
await run(`sh -c "sqlite3 '${dbPath}' < '${sqlPath}'"`, { alias: `import dump → ${dbPath}` });
|
|
1208
|
+
if (!options.keepSql) await this.fs.rm(sqlPath, { force: true });
|
|
1209
|
+
}
|
|
1210
|
+
async migrateD1(ctx, run) {
|
|
1211
|
+
const dbName = ctx.naming.d1();
|
|
1212
|
+
await run({
|
|
1213
|
+
name: "migrate d1",
|
|
1214
|
+
handler: async () => {
|
|
1215
|
+
const migrationsDir = this.fs.join(ctx.root, "migrations", "sqlite");
|
|
1216
|
+
const env = { DATABASE_URL: this.provisionedD1Id ? `d1://${dbName}:${this.provisionedD1Id}` : `d1://${dbName}` };
|
|
1217
|
+
if (!ctx.prebuilt) if (await this.fs.exists(migrationsDir)) await this.runShell(`alepha db migrations check --mode ${ctx.env}`, {
|
|
1218
|
+
resolve: true,
|
|
1219
|
+
env
|
|
1220
|
+
});
|
|
1221
|
+
else await this.runShell(`alepha db migrations create --mode ${ctx.env}`, {
|
|
1222
|
+
resolve: true,
|
|
1223
|
+
env
|
|
1224
|
+
});
|
|
1225
|
+
const distMigrations = this.fs.join(ctx.root, "dist", "migrations");
|
|
1226
|
+
await this.fs.cp(migrationsDir, distMigrations);
|
|
1227
|
+
await this.wrangler.d1MigrationsApply(dbName, "dist/wrangler.jsonc", ctx.root);
|
|
1228
|
+
await this.fs.rm(distMigrations, { recursive: true });
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
async migratePostgres(ctx, run) {
|
|
1233
|
+
if (ctx.prebuilt) throw new AlephaError("Postgres migrations are not yet supported in prebuilt mode. Use the `alepha platform up` CLI for now.");
|
|
1234
|
+
await run({
|
|
1235
|
+
name: "migrate postgres",
|
|
1236
|
+
handler: async () => {
|
|
1237
|
+
const envVars = await this.envUtils.parseEnv(ctx.root, [`.env.${ctx.env}`]);
|
|
1238
|
+
const env = { DATABASE_URL: envVars.DATABASE_URL ?? process.env.DATABASE_URL };
|
|
1239
|
+
if (envVars.POSTGRES_SCHEMA ?? process.env.POSTGRES_SCHEMA) env.POSTGRES_SCHEMA = envVars.POSTGRES_SCHEMA ?? process.env.POSTGRES_SCHEMA;
|
|
1240
|
+
await this.runShell(`alepha db migrations apply --mode ${ctx.env}`, {
|
|
1241
|
+
resolve: true,
|
|
1242
|
+
env
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
async inspect(ctx, run) {
|
|
1248
|
+
this.configureApi(ctx);
|
|
1249
|
+
const state = {
|
|
1250
|
+
workers: [],
|
|
1251
|
+
databases: [],
|
|
1252
|
+
buckets: [],
|
|
1253
|
+
kvNamespaces: [],
|
|
1254
|
+
queues: [],
|
|
1255
|
+
secrets: []
|
|
1256
|
+
};
|
|
1257
|
+
const tasks = [];
|
|
1258
|
+
{
|
|
1259
|
+
const name = ctx.naming.worker();
|
|
1260
|
+
tasks.push({
|
|
1261
|
+
name: `inspect worker (${name})`,
|
|
1262
|
+
handler: async () => {
|
|
1263
|
+
try {
|
|
1264
|
+
const deployment = await this.getActiveDeployment(name);
|
|
1265
|
+
if (deployment) state.workers.push({
|
|
1266
|
+
name,
|
|
1267
|
+
exists: true,
|
|
1268
|
+
version: deployment.versionId,
|
|
1269
|
+
tag: deployment.tag,
|
|
1270
|
+
createdAt: deployment.createdAt
|
|
1271
|
+
});
|
|
1272
|
+
else state.workers.push({
|
|
1273
|
+
name,
|
|
1274
|
+
exists: false
|
|
1275
|
+
});
|
|
1276
|
+
} catch {
|
|
1277
|
+
state.workers.push({
|
|
1278
|
+
name,
|
|
1279
|
+
exists: false
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
if (ctx.resources.hasDatabase) if (await this.isPostgres(ctx)) {
|
|
1286
|
+
const hdName = ctx.naming.hyperdrive();
|
|
1287
|
+
tasks.push({
|
|
1288
|
+
name: `inspect hyperdrive (${hdName})`,
|
|
1289
|
+
handler: async () => {
|
|
1290
|
+
const existing = (await this.api.listHyperdrive()).find((c) => c.name === hdName);
|
|
1291
|
+
state.databases.push({
|
|
1292
|
+
name: hdName,
|
|
1293
|
+
exists: !!existing,
|
|
1294
|
+
id: existing?.id,
|
|
1295
|
+
detail: existing?.origin.host
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
} else {
|
|
1300
|
+
const dbName = ctx.naming.d1();
|
|
1301
|
+
tasks.push({
|
|
1302
|
+
name: `inspect d1 (${dbName})`,
|
|
1303
|
+
handler: async () => {
|
|
1304
|
+
const existing = (await this.api.listD1()).find((db) => db.name === dbName);
|
|
1305
|
+
state.databases.push({
|
|
1306
|
+
name: dbName,
|
|
1307
|
+
exists: !!existing,
|
|
1308
|
+
id: existing?.uuid
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
if (ctx.resources.hasBucket) {
|
|
1314
|
+
const bucketName = ctx.naming.r2();
|
|
1315
|
+
tasks.push({
|
|
1316
|
+
name: `inspect r2 (${bucketName})`,
|
|
1317
|
+
handler: async () => {
|
|
1318
|
+
const existing = (await this.api.listR2()).find((b) => b.name === bucketName);
|
|
1319
|
+
state.buckets.push({
|
|
1320
|
+
name: bucketName,
|
|
1321
|
+
exists: !!existing,
|
|
1322
|
+
id: existing?.creation_date
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
if (ctx.resources.hasKV) {
|
|
1328
|
+
const kvName = ctx.naming.kv();
|
|
1329
|
+
tasks.push({
|
|
1330
|
+
name: `inspect kv (${kvName})`,
|
|
1331
|
+
handler: async () => {
|
|
1332
|
+
const existing = (await this.api.listKV()).find((ns) => ns.title === kvName);
|
|
1333
|
+
state.kvNamespaces.push({
|
|
1334
|
+
name: kvName,
|
|
1335
|
+
exists: !!existing,
|
|
1336
|
+
id: existing?.id
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
if (ctx.resources.hasQueue) {
|
|
1342
|
+
const queueName = ctx.naming.queue();
|
|
1343
|
+
tasks.push({
|
|
1344
|
+
name: `inspect queue (${queueName})`,
|
|
1345
|
+
handler: async () => {
|
|
1346
|
+
const existing = (await this.api.listQueues()).find((q) => q.queue_name === queueName);
|
|
1347
|
+
state.queues.push({
|
|
1348
|
+
name: queueName,
|
|
1349
|
+
exists: !!existing,
|
|
1350
|
+
id: existing?.queue_id
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
const envVars = await this.envUtils.parseEnv(ctx.root, [`.env.${ctx.env}`]);
|
|
1356
|
+
const expectedSecrets = Object.keys(envVars).filter((key) => envVars[key] && !CloudflareAdapter.EXCLUDED_SECRET_KEYS.has(key) && !key.startsWith("VITE_"));
|
|
1357
|
+
if (expectedSecrets.length > 0) {
|
|
1358
|
+
const workerName = ctx.naming.worker();
|
|
1359
|
+
tasks.push({
|
|
1360
|
+
name: "inspect secrets",
|
|
1361
|
+
handler: async () => {
|
|
1362
|
+
try {
|
|
1363
|
+
const deployed = await this.api.listSecrets(workerName);
|
|
1364
|
+
const deployedNames = new Set(deployed.map((s) => s.name));
|
|
1365
|
+
for (const key of expectedSecrets) state.secrets.push({
|
|
1366
|
+
name: key,
|
|
1367
|
+
deployed: deployedNames.has(key)
|
|
1368
|
+
});
|
|
1369
|
+
} catch {
|
|
1370
|
+
for (const key of expectedSecrets) state.secrets.push({
|
|
1371
|
+
name: key,
|
|
1372
|
+
deployed: false
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
await run(tasks);
|
|
1379
|
+
return state;
|
|
1380
|
+
}
|
|
1381
|
+
async teardown(ctx, run) {
|
|
1382
|
+
this.configureApi(ctx);
|
|
1383
|
+
if (ctx.resources.hasQueue) {
|
|
1384
|
+
const workerName = ctx.naming.worker();
|
|
1385
|
+
const queueName = ctx.naming.queue();
|
|
1386
|
+
await run({
|
|
1387
|
+
name: `unbind queue consumer ${queueName}`,
|
|
1388
|
+
handler: async () => {
|
|
1389
|
+
try {
|
|
1390
|
+
const queue = (await this.api.listQueues()).find((q) => q.queue_name === queueName);
|
|
1391
|
+
if (queue) await this.api.deleteQueueConsumer(queue.queue_id, workerName);
|
|
1392
|
+
} catch (error) {
|
|
1393
|
+
this.log.warn(`Failed to unbind queue consumer: ${String(error.message || "")}`);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
{
|
|
1399
|
+
const name = ctx.naming.worker();
|
|
1400
|
+
await run({
|
|
1401
|
+
name: `delete worker ${name}`,
|
|
1402
|
+
handler: async () => {
|
|
1403
|
+
try {
|
|
1404
|
+
await this.api.deleteWorker(name);
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
this.log.warn(`Failed to delete worker ${name}: ${String(error.message || "")}`);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
if (ctx.resources.hasQueue) {
|
|
1412
|
+
const name = ctx.naming.queue();
|
|
1413
|
+
await run({
|
|
1414
|
+
name: `delete queue ${name}`,
|
|
1415
|
+
handler: async () => {
|
|
1416
|
+
try {
|
|
1417
|
+
const queue = (await this.api.listQueues()).find((q) => q.queue_name === name);
|
|
1418
|
+
if (!queue) {
|
|
1419
|
+
this.log.debug(`Queue ${name} not found — skipping.`);
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
await this.api.deleteQueue(queue.queue_id);
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
this.log.warn(`Failed to delete queue ${name}: ${String(error.message || "")}`);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
if (ctx.resources.hasKV) {
|
|
1430
|
+
const name = ctx.naming.kv();
|
|
1431
|
+
await run({
|
|
1432
|
+
name: `delete kv ${name}`,
|
|
1433
|
+
handler: async () => {
|
|
1434
|
+
try {
|
|
1435
|
+
const existing = (await this.api.listKV()).find((ns) => ns.title === name);
|
|
1436
|
+
if (!existing) {
|
|
1437
|
+
this.log.debug(`KV namespace ${name} not found — skipping.`);
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
await this.api.deleteKV(existing.id);
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
this.log.warn(`Failed to delete kv ${name}: ${String(error.message || "")}`);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
if (ctx.resources.hasBucket) {
|
|
1448
|
+
const name = ctx.naming.r2();
|
|
1449
|
+
await run({
|
|
1450
|
+
name: `delete r2 ${name}`,
|
|
1451
|
+
handler: async () => {
|
|
1452
|
+
try {
|
|
1453
|
+
await this.deleteR2Bucket(name, ctx);
|
|
1454
|
+
} catch (error) {
|
|
1455
|
+
const msg = String(error.message || "");
|
|
1456
|
+
if (this.isMissingBucketError(msg)) this.log.debug(`Bucket ${name} not found — skipping.`);
|
|
1457
|
+
else this.log.warn(`Failed to delete r2 ${name}: ${msg}`);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
if (ctx.resources.hasDatabase) if (await this.isPostgres(ctx)) {
|
|
1463
|
+
const name = ctx.naming.hyperdrive();
|
|
1464
|
+
await run({
|
|
1465
|
+
name: `delete hyperdrive ${name}`,
|
|
1466
|
+
handler: async () => {
|
|
1467
|
+
try {
|
|
1468
|
+
const existing = (await this.api.listHyperdrive()).find((c) => c.name === name);
|
|
1469
|
+
if (!existing) {
|
|
1470
|
+
this.log.debug(`Hyperdrive ${name} not found — skipping.`);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
await this.api.deleteHyperdrive(existing.id);
|
|
1474
|
+
} catch (error) {
|
|
1475
|
+
this.log.warn(`Failed to delete hyperdrive ${name}: ${String(error.message || "")}`);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
});
|
|
1479
|
+
} else {
|
|
1480
|
+
const name = ctx.naming.d1();
|
|
1481
|
+
await run({
|
|
1482
|
+
name: `delete d1 ${name}`,
|
|
1483
|
+
handler: async () => {
|
|
1484
|
+
try {
|
|
1485
|
+
const existing = (await this.api.listD1()).find((db) => db.name === name);
|
|
1486
|
+
if (!existing) {
|
|
1487
|
+
this.log.debug(`D1 database ${name} not found — skipping.`);
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
await this.api.deleteD1(existing.uuid);
|
|
1491
|
+
} catch (error) {
|
|
1492
|
+
this.log.warn(`Failed to delete d1 ${name}: ${String(error.message || "")}`);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
async ensureD1(name) {
|
|
1499
|
+
const existing = (await this.api.listD1()).find((db) => db.name === name);
|
|
1500
|
+
if (existing) return existing.uuid;
|
|
1501
|
+
return (await this.api.createD1(name)).uuid;
|
|
1502
|
+
}
|
|
1503
|
+
async ensureHyperdrive(name, connectionString) {
|
|
1504
|
+
const existing = (await this.api.listHyperdrive()).find((c) => c.name === name);
|
|
1505
|
+
if (existing) return existing.id;
|
|
1506
|
+
return (await this.api.createHyperdrive(name, connectionString)).id;
|
|
1507
|
+
}
|
|
1508
|
+
async ensureR2(name) {
|
|
1509
|
+
if ((await this.api.listR2()).find((b) => b.name === name)) return;
|
|
1510
|
+
await this.api.createR2(name);
|
|
1511
|
+
}
|
|
1512
|
+
/** Whether a Cloudflare error message indicates the bucket is already gone. */
|
|
1513
|
+
isMissingBucketError(msg) {
|
|
1514
|
+
return msg.includes("does not exist") || msg.includes("NoSuchBucket") || msg.includes("bucket not found");
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Resolve S3 credentials for wiping an R2 bucket over the S3 protocol.
|
|
1518
|
+
*
|
|
1519
|
+
* Prefers the account's R2 S3 credentials from the environment
|
|
1520
|
+
* (`S3_ACCESS_KEY_ID` / `S3_SECRET_ACCESS_KEY`) — these are already
|
|
1521
|
+
* provisioned for the deploy (artifact registry) and are account-scoped,
|
|
1522
|
+
* so they can empty any bucket without minting anything. Returns `null`
|
|
1523
|
+
* when not configured, letting the caller fall back to token minting.
|
|
1524
|
+
*/
|
|
1525
|
+
resolveR2Credentials() {
|
|
1526
|
+
const accessKeyId = process.env.S3_ACCESS_KEY_ID;
|
|
1527
|
+
const secretAccessKey = process.env.S3_SECRET_ACCESS_KEY;
|
|
1528
|
+
if (accessKeyId && secretAccessKey) return {
|
|
1529
|
+
accessKeyId,
|
|
1530
|
+
secretAccessKey
|
|
1531
|
+
};
|
|
1532
|
+
return null;
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Delete an R2 bucket, emptying it first only when necessary.
|
|
1536
|
+
*
|
|
1537
|
+
* Cloudflare's REST `DELETE /r2/buckets/:name` succeeds on an empty bucket
|
|
1538
|
+
* but rejects a non-empty one. So we attempt the delete directly (the
|
|
1539
|
+
* common teardown case — no objects, no creds needed), and only on failure
|
|
1540
|
+
* empty the bucket over the S3 protocol and retry. A missing bucket is a
|
|
1541
|
+
* no-op, so teardown is idempotent.
|
|
1542
|
+
*/
|
|
1543
|
+
async deleteR2Bucket(name, ctx) {
|
|
1544
|
+
try {
|
|
1545
|
+
await this.api.deleteR2(name);
|
|
1546
|
+
return;
|
|
1547
|
+
} catch (error) {
|
|
1548
|
+
const msg = String(error.message || "");
|
|
1549
|
+
if (this.isMissingBucketError(msg)) return;
|
|
1550
|
+
this.log.debug(`Direct delete of r2 ${name} failed (${msg}); emptying then retrying.`);
|
|
1551
|
+
}
|
|
1552
|
+
await this.wipeR2Bucket(name, ctx);
|
|
1553
|
+
try {
|
|
1554
|
+
await this.api.deleteR2(name);
|
|
1555
|
+
} catch (error) {
|
|
1556
|
+
const msg = String(error.message || "");
|
|
1557
|
+
if (this.isMissingBucketError(msg)) return;
|
|
1558
|
+
throw error;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* Empty an R2 bucket via the S3-compatible API.
|
|
1563
|
+
*
|
|
1564
|
+
* Cloudflare's REST API has no object-level endpoints — objects must be
|
|
1565
|
+
* listed and deleted over the S3 protocol. We use the account's R2 S3
|
|
1566
|
+
* credentials (`S3_ACCESS_KEY_ID` / `S3_SECRET_ACCESS_KEY`) when present;
|
|
1567
|
+
* otherwise we fall back to minting a short-lived bucket-scoped token via
|
|
1568
|
+
* the CF API (requires a user-scoped `CLOUDFLARE_API_TOKEN`) and revoke it
|
|
1569
|
+
* after. When neither is available the wipe is skipped with a warning —
|
|
1570
|
+
* the caller still attempts the delete, which succeeds for empty buckets.
|
|
1571
|
+
*
|
|
1572
|
+
* Also aborts any pending multipart uploads — those count as bucket
|
|
1573
|
+
* contents from R2's perspective and would otherwise block the delete.
|
|
1574
|
+
*/
|
|
1575
|
+
async wipeR2Bucket(bucketName, ctx) {
|
|
1576
|
+
let creds = this.resolveR2Credentials();
|
|
1577
|
+
let mintedTokenId;
|
|
1578
|
+
if (!creds) {
|
|
1579
|
+
if (!process.env.CLOUDFLARE_API_TOKEN) {
|
|
1580
|
+
this.log.warn(`Skipping R2 wipe for ${bucketName}: no S3 credentials (S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY) and no CLOUDFLARE_API_TOKEN to mint a bucket-scoped token. A non-empty bucket must be emptied manually in the Cloudflare dashboard.`);
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
try {
|
|
1584
|
+
const tokenName = `alepha-teardown-${bucketName}-${Date.now()}`;
|
|
1585
|
+
const token = await this.api.createR2Token(tokenName, bucketName);
|
|
1586
|
+
mintedTokenId = token.id;
|
|
1587
|
+
creds = {
|
|
1588
|
+
accessKeyId: token.accessKeyId,
|
|
1589
|
+
secretAccessKey: token.secretAccessKey
|
|
1590
|
+
};
|
|
1591
|
+
} catch (error) {
|
|
1592
|
+
this.log.warn(`Skipping R2 wipe for ${bucketName}: could not mint an R2 token (${String(error.message || "")}). Set S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY for reliable teardown.`);
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
try {
|
|
1597
|
+
const accountId = await this.api.resolveAccountId();
|
|
1598
|
+
const jur = ctx.envConfig.jurisdiction;
|
|
1599
|
+
const host = jur ? `${accountId}.${jur}.r2.cloudflarestorage.com` : `${accountId}.r2.cloudflarestorage.com`;
|
|
1600
|
+
const client = new S3mini({
|
|
1601
|
+
accessKeyId: creds.accessKeyId,
|
|
1602
|
+
secretAccessKey: creds.secretAccessKey,
|
|
1603
|
+
region: "auto",
|
|
1604
|
+
endpoint: `https://${host}/${bucketName}`
|
|
1605
|
+
});
|
|
1606
|
+
try {
|
|
1607
|
+
const mp = await client.listMultipartUploads();
|
|
1608
|
+
if ("listMultipartUploadsResult" in mp) {
|
|
1609
|
+
const uploads = mp.listMultipartUploadsResult.uploads ?? [];
|
|
1610
|
+
for (const upload of uploads) {
|
|
1611
|
+
const u = upload;
|
|
1612
|
+
const key = u.Key ?? u.key;
|
|
1613
|
+
const uploadId = u.UploadId ?? u.uploadId;
|
|
1614
|
+
if (key && uploadId) await client.abortMultipartUpload(key, uploadId);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
} catch (error) {
|
|
1618
|
+
this.log.debug(`listMultipartUploads on ${bucketName} failed: ${String(error.message || "")}`);
|
|
1619
|
+
}
|
|
1620
|
+
let cursor;
|
|
1621
|
+
let total = 0;
|
|
1622
|
+
while (true) {
|
|
1623
|
+
const page = await client.listObjectsPaged(void 0, void 0, 1e3, cursor);
|
|
1624
|
+
const objects = page?.objects ?? [];
|
|
1625
|
+
if (objects.length === 0) break;
|
|
1626
|
+
await client.deleteObjects(objects.map((o) => o.Key));
|
|
1627
|
+
total += objects.length;
|
|
1628
|
+
cursor = page?.nextContinuationToken;
|
|
1629
|
+
if (!cursor) break;
|
|
1630
|
+
}
|
|
1631
|
+
if (total > 0) this.log.info(`Emptied ${total} object(s) from bucket ${bucketName}.`);
|
|
1632
|
+
} finally {
|
|
1633
|
+
if (mintedTokenId) try {
|
|
1634
|
+
await this.api.deleteR2Token(mintedTokenId);
|
|
1635
|
+
} catch (error) {
|
|
1636
|
+
this.log.warn(`Failed to revoke ephemeral R2 token ${mintedTokenId}: ${String(error.message || "")}`);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
async ensureKV(name) {
|
|
1641
|
+
const existing = (await this.api.listKV()).find((ns) => ns.title === name);
|
|
1642
|
+
if (existing) return existing.id;
|
|
1643
|
+
return (await this.api.createKV(name)).id;
|
|
1644
|
+
}
|
|
1645
|
+
async ensureQueue(name) {
|
|
1646
|
+
if ((await this.api.listQueues()).find((q) => q.queue_name === name)) return;
|
|
1647
|
+
await this.api.createQueue(name);
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Get the currently active deployment for a worker.
|
|
1651
|
+
*/
|
|
1652
|
+
async getActiveDeployment(workerName) {
|
|
1653
|
+
const latest = [...await this.api.listDeployments(workerName)].sort((a, b) => b.created_on.localeCompare(a.created_on))[0];
|
|
1654
|
+
if (!latest?.versions?.[0]) return;
|
|
1655
|
+
const activeVersionId = latest.versions[0].version_id;
|
|
1656
|
+
const version = (await this.api.listVersions(workerName)).find((v) => v.id === activeVersionId);
|
|
1657
|
+
return {
|
|
1658
|
+
versionId: activeVersionId,
|
|
1659
|
+
tag: version?.annotations?.["workers/tag"],
|
|
1660
|
+
createdAt: version?.metadata.created_on
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
/**
|
|
1665
|
+
* Stable SHA-256 of the secret set. Keys are sorted so reordering `.env`
|
|
1666
|
+
* lines does not invalidate the cache. Used as a fingerprint by
|
|
1667
|
+
* `CloudflareAdapter.secrets` — see the comment block there.
|
|
1668
|
+
*/
|
|
1669
|
+
function computeSecretsHash(secrets) {
|
|
1670
|
+
const sorted = Object.keys(secrets).sort().map((k) => `${k}=${secrets[k]}`).join("\n");
|
|
1671
|
+
return createHash("sha256").update(sorted).digest("hex");
|
|
1672
|
+
}
|
|
1673
|
+
//#endregion
|
|
1674
|
+
//#region ../../src/cli/platform-lib/schemas/vercel.ts
|
|
1675
|
+
const vercelProjectSchema = t.object({
|
|
1676
|
+
id: t.string(),
|
|
1677
|
+
name: t.string(),
|
|
1678
|
+
accountId: t.string()
|
|
1679
|
+
});
|
|
1680
|
+
const createProjectBodySchema = t.object({
|
|
1681
|
+
name: t.string(),
|
|
1682
|
+
framework: t.optional(t.null())
|
|
1683
|
+
});
|
|
1684
|
+
const vercelDeploymentSchema = t.object({
|
|
1685
|
+
uid: t.string(),
|
|
1686
|
+
name: t.string(),
|
|
1687
|
+
url: t.string(),
|
|
1688
|
+
state: t.optional(t.string()),
|
|
1689
|
+
readyState: t.optional(t.string()),
|
|
1690
|
+
created: t.optional(t.number()),
|
|
1691
|
+
target: t.optional(t.string()),
|
|
1692
|
+
alias: t.optional(t.array(t.string()))
|
|
1693
|
+
});
|
|
1694
|
+
const vercelEnvVarSchema = t.object({
|
|
1695
|
+
id: t.string(),
|
|
1696
|
+
key: t.string(),
|
|
1697
|
+
value: t.optional(t.string()),
|
|
1698
|
+
type: t.string(),
|
|
1699
|
+
target: t.array(t.string())
|
|
1700
|
+
});
|
|
1701
|
+
const createEnvVarBodySchema = t.object({
|
|
1702
|
+
key: t.string(),
|
|
1703
|
+
value: t.string(),
|
|
1704
|
+
type: t.string(),
|
|
1705
|
+
target: t.array(t.string())
|
|
1706
|
+
});
|
|
1707
|
+
//#endregion
|
|
1708
|
+
//#region ../../src/cli/platform-lib/services/VercelCli.ts
|
|
1709
|
+
/**
|
|
1710
|
+
* Wraps Vercel CLI commands and token management.
|
|
1711
|
+
*
|
|
1712
|
+
* Used for operations where the Vercel CLI provides value:
|
|
1713
|
+
* OAuth login, prebuilt deploy, and auth token extraction.
|
|
1714
|
+
*/
|
|
1715
|
+
var VercelCli = class {
|
|
1716
|
+
log = $logger();
|
|
1717
|
+
shell = $inject(ShellProvider);
|
|
1718
|
+
fs = $inject(FileSystemProvider);
|
|
1719
|
+
utils = $inject(AlephaCliUtils);
|
|
1720
|
+
pm = $inject(PackageManagerUtils);
|
|
1721
|
+
runner = $inject(Runner);
|
|
1722
|
+
async runShell(command, options = {}) {
|
|
1723
|
+
const capture = options.capture;
|
|
1724
|
+
const output = await this.shell.run(command, {
|
|
1725
|
+
...options,
|
|
1726
|
+
capture: capture ?? this.runner.useDynamicLogger
|
|
1727
|
+
});
|
|
1728
|
+
if (capture && !this.runner.useDynamicLogger) this.log.info(output);
|
|
1729
|
+
return output;
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Ensure vercel CLI is installed in the project.
|
|
1733
|
+
*/
|
|
1734
|
+
async ensureInstalled(root, run) {
|
|
1735
|
+
await this.pm.ensureDependency(root, "vercel", {
|
|
1736
|
+
dev: true,
|
|
1737
|
+
exec: async (cmd, opts) => {
|
|
1738
|
+
run.pause();
|
|
1739
|
+
try {
|
|
1740
|
+
await this.utils.exec(cmd, opts);
|
|
1741
|
+
} finally {
|
|
1742
|
+
run.resume();
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Get the Vercel auth token.
|
|
1749
|
+
*
|
|
1750
|
+
* Priority:
|
|
1751
|
+
* 1. VERCEL_TOKEN environment variable (CI/CD)
|
|
1752
|
+
* 2. Vercel CLI auth.json file (local dev)
|
|
1753
|
+
*/
|
|
1754
|
+
async getAuthToken() {
|
|
1755
|
+
const envToken = process.env.VERCEL_TOKEN;
|
|
1756
|
+
if (envToken) return envToken;
|
|
1757
|
+
const authPath = this.getAuthFilePath();
|
|
1758
|
+
if (!await this.fs.exists(authPath)) throw new AlephaError("Vercel auth token not found. Run `vercel login` or set VERCEL_TOKEN.");
|
|
1759
|
+
const content = await this.fs.readFile(authPath);
|
|
1760
|
+
const parsed = JSON.parse(content.toString());
|
|
1761
|
+
if (!parsed.token) throw new AlephaError("Vercel auth.json exists but contains no token. Run `vercel login`.");
|
|
1762
|
+
return parsed.token;
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Validate the current auth token.
|
|
1766
|
+
*/
|
|
1767
|
+
async whoami() {
|
|
1768
|
+
return await this.runShell("vercel whoami", {
|
|
1769
|
+
resolve: true,
|
|
1770
|
+
capture: true
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Open the browser-based login flow.
|
|
1775
|
+
*/
|
|
1776
|
+
async login() {
|
|
1777
|
+
await this.runShell("vercel login", { resolve: true });
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Deploy a prebuilt .vercel/output/ directory.
|
|
1781
|
+
*
|
|
1782
|
+
* Returns the deployment URL.
|
|
1783
|
+
*/
|
|
1784
|
+
async deploy(distDir, options) {
|
|
1785
|
+
const args = [
|
|
1786
|
+
"vercel",
|
|
1787
|
+
"deploy",
|
|
1788
|
+
"--prebuilt"
|
|
1789
|
+
];
|
|
1790
|
+
if (options.prod) args.push("--prod");
|
|
1791
|
+
if (options.token) args.push(`--token=${options.token}`);
|
|
1792
|
+
return (await this.runShell(args.join(" "), {
|
|
1793
|
+
resolve: true,
|
|
1794
|
+
capture: true,
|
|
1795
|
+
root: distDir
|
|
1796
|
+
})).trim().split("\n").reverse().find((line) => line.trim().startsWith("https://"))?.trim();
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Resolve the path to Vercel CLI auth.json.
|
|
1800
|
+
*/
|
|
1801
|
+
getAuthFilePath() {
|
|
1802
|
+
const os = platform();
|
|
1803
|
+
if (os === "darwin") return join(homedir(), "Library", "Application Support", "com.vercel.cli", "auth.json");
|
|
1804
|
+
if (os === "win32") return join(homedir(), "AppData", "Roaming", "xdg.data", "com.vercel.cli", "auth.json");
|
|
1805
|
+
return join(homedir(), ".local", "share", "com.vercel.cli", "auth.json");
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
//#endregion
|
|
1809
|
+
//#region ../../src/cli/platform-lib/services/VercelApi.ts
|
|
1810
|
+
/**
|
|
1811
|
+
* Thin wrapper over the Vercel REST API.
|
|
1812
|
+
*
|
|
1813
|
+
* Uses the auth token from VercelCli for all requests.
|
|
1814
|
+
*/
|
|
1815
|
+
var VercelApi = class VercelApi {
|
|
1816
|
+
static BASE = "https://api.vercel.com";
|
|
1817
|
+
log = $logger();
|
|
1818
|
+
alepha = $inject(Alepha);
|
|
1819
|
+
vercelCli = $inject(VercelCli);
|
|
1820
|
+
token;
|
|
1821
|
+
/**
|
|
1822
|
+
* Obtain the current auth token from the Vercel CLI.
|
|
1823
|
+
*/
|
|
1824
|
+
async resolveToken() {
|
|
1825
|
+
if (this.token) return this.token;
|
|
1826
|
+
this.token = await this.vercelCli.getAuthToken();
|
|
1827
|
+
return this.token;
|
|
1828
|
+
}
|
|
1829
|
+
async listProjects() {
|
|
1830
|
+
return (await this.fetch("/v10/projects", { schema: t.object({ projects: t.array(vercelProjectSchema) }) })).projects;
|
|
1831
|
+
}
|
|
1832
|
+
async getProject(nameOrId) {
|
|
1833
|
+
try {
|
|
1834
|
+
return await this.fetch(`/v9/projects/${encodeURIComponent(nameOrId)}`, { schema: vercelProjectSchema });
|
|
1835
|
+
} catch {
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
async createProject(name) {
|
|
1840
|
+
return await this.fetch("/v11/projects", {
|
|
1841
|
+
method: "POST",
|
|
1842
|
+
body: {
|
|
1843
|
+
name,
|
|
1844
|
+
framework: null
|
|
1845
|
+
},
|
|
1846
|
+
bodySchema: createProjectBodySchema,
|
|
1847
|
+
schema: vercelProjectSchema
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
async updateProject(nameOrId, settings) {
|
|
1851
|
+
await this.fetch(`/v9/projects/${encodeURIComponent(nameOrId)}`, {
|
|
1852
|
+
method: "PATCH",
|
|
1853
|
+
body: settings
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
async deleteProject(nameOrId) {
|
|
1857
|
+
await this.fetch(`/v9/projects/${encodeURIComponent(nameOrId)}`, { method: "DELETE" });
|
|
1858
|
+
}
|
|
1859
|
+
async listDeployments(projectId, options) {
|
|
1860
|
+
const query = { projectId };
|
|
1861
|
+
if (options?.limit) query.limit = String(options.limit);
|
|
1862
|
+
if (options?.target) query.target = options.target;
|
|
1863
|
+
return (await this.fetch("/v6/deployments", {
|
|
1864
|
+
query,
|
|
1865
|
+
schema: t.object({ deployments: t.array(vercelDeploymentSchema) })
|
|
1866
|
+
})).deployments;
|
|
1867
|
+
}
|
|
1868
|
+
async listEnvVars(projectId) {
|
|
1869
|
+
return (await this.fetch(`/v10/projects/${encodeURIComponent(projectId)}/env`, {
|
|
1870
|
+
query: { decrypt: "true" },
|
|
1871
|
+
schema: t.object({ envs: t.array(vercelEnvVarSchema) })
|
|
1872
|
+
})).envs;
|
|
1873
|
+
}
|
|
1874
|
+
async upsertEnvVars(projectId, vars) {
|
|
1875
|
+
for (const v of vars) await this.fetch(`/v10/projects/${encodeURIComponent(projectId)}/env`, {
|
|
1876
|
+
method: "POST",
|
|
1877
|
+
query: { upsert: "true" },
|
|
1878
|
+
body: {
|
|
1879
|
+
key: v.key,
|
|
1880
|
+
value: v.value,
|
|
1881
|
+
type: "encrypted",
|
|
1882
|
+
target: v.target
|
|
1883
|
+
},
|
|
1884
|
+
bodySchema: createEnvVarBodySchema
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
async deleteEnvVar(projectId, envVarId) {
|
|
1888
|
+
await this.fetch(`/v9/projects/${encodeURIComponent(projectId)}/env/${envVarId}`, { method: "DELETE" });
|
|
1889
|
+
}
|
|
1890
|
+
async fetch(path, options = {}) {
|
|
1891
|
+
const token = await this.resolveToken();
|
|
1892
|
+
const { method = "GET", body, query } = options;
|
|
1893
|
+
let url = `${VercelApi.BASE}${path}`;
|
|
1894
|
+
if (query) {
|
|
1895
|
+
const params = new URLSearchParams(query);
|
|
1896
|
+
url += `?${params.toString()}`;
|
|
1897
|
+
}
|
|
1898
|
+
const headers = { Authorization: `Bearer ${token}` };
|
|
1899
|
+
const init = {
|
|
1900
|
+
method,
|
|
1901
|
+
headers
|
|
1902
|
+
};
|
|
1903
|
+
if (body) {
|
|
1904
|
+
headers["Content-Type"] = "application/json";
|
|
1905
|
+
const validated = options.bodySchema ? this.alepha.codec.validate(options.bodySchema, body) : body;
|
|
1906
|
+
init.body = JSON.stringify(validated);
|
|
1907
|
+
}
|
|
1908
|
+
const response = await globalThis.fetch(url, init);
|
|
1909
|
+
if (response.status === 204) return;
|
|
1910
|
+
const json = await response.json();
|
|
1911
|
+
if (json.error) throw new AlephaError(`Vercel API error (${method} ${path}): ${json.error.message ?? JSON.stringify(json.error)}`);
|
|
1912
|
+
if (!response.ok) throw new AlephaError(`Vercel API error (${method} ${path}): HTTP ${response.status}`);
|
|
1913
|
+
if (options.schema) return this.alepha.codec.validate(options.schema, json);
|
|
1914
|
+
return json;
|
|
1915
|
+
}
|
|
1916
|
+
};
|
|
1917
|
+
//#endregion
|
|
1918
|
+
//#region ../../src/cli/platform-lib/adapters/VercelAdapter.ts
|
|
1919
|
+
/**
|
|
1920
|
+
* Vercel platform adapter.
|
|
1921
|
+
*
|
|
1922
|
+
* Uses the Vercel CLI for login and deploy (--prebuilt),
|
|
1923
|
+
* and the Vercel REST API for project management, env vars, and inspection.
|
|
1924
|
+
*
|
|
1925
|
+
* v1 scope: deploy pipeline only. No DB/storage/KV provisioning.
|
|
1926
|
+
*/
|
|
1927
|
+
var VercelAdapter = class VercelAdapter extends PlatformAdapter {
|
|
1928
|
+
log = $logger();
|
|
1929
|
+
fs = $inject(FileSystemProvider);
|
|
1930
|
+
shell = $inject(ShellProvider);
|
|
1931
|
+
utils = $inject(AlephaCliUtils);
|
|
1932
|
+
cache = $inject(PlatformCacheProvider);
|
|
1933
|
+
alepha = $inject(Alepha);
|
|
1934
|
+
envUtils = $inject(EnvUtils);
|
|
1935
|
+
api = $inject(VercelApi);
|
|
1936
|
+
vercelCli = $inject(VercelCli);
|
|
1937
|
+
runner = $inject(Runner);
|
|
1938
|
+
/**
|
|
1939
|
+
* Vars that should not be pushed as env vars.
|
|
1940
|
+
* These are either handled by the build or are internal.
|
|
1941
|
+
*/
|
|
1942
|
+
static EXCLUDED_SECRET_KEYS = new Set(["NODE_ENV"]);
|
|
1943
|
+
async runShell(command, options = {}) {
|
|
1944
|
+
const capture = options.capture;
|
|
1945
|
+
const output = await this.shell.run(command, {
|
|
1946
|
+
...options,
|
|
1947
|
+
capture: capture ?? this.runner.useDynamicLogger
|
|
1948
|
+
});
|
|
1949
|
+
if (capture && !this.runner.useDynamicLogger) this.log.info(output);
|
|
1950
|
+
return output;
|
|
1951
|
+
}
|
|
1952
|
+
async authenticate(ctx, run) {
|
|
1953
|
+
await run({
|
|
1954
|
+
name: "authenticate",
|
|
1955
|
+
handler: async () => {
|
|
1956
|
+
await this.vercelCli.ensureInstalled(ctx.root, run);
|
|
1957
|
+
let needsLogin = false;
|
|
1958
|
+
try {
|
|
1959
|
+
await this.vercelCli.getAuthToken();
|
|
1960
|
+
await this.vercelCli.whoami();
|
|
1961
|
+
} catch {
|
|
1962
|
+
needsLogin = true;
|
|
1963
|
+
}
|
|
1964
|
+
if (needsLogin) {
|
|
1965
|
+
run.pause();
|
|
1966
|
+
await this.vercelCli.login();
|
|
1967
|
+
run.resume();
|
|
1968
|
+
}
|
|
1969
|
+
if (await this.cache.isLoginFresh(ctx.root, "vercel")) return;
|
|
1970
|
+
await this.cache.recordLogin(ctx.root, "vercel");
|
|
1971
|
+
}
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
async build(ctx, run) {
|
|
1975
|
+
const appDir = ctx.root;
|
|
1976
|
+
await run({
|
|
1977
|
+
name: "alepha build -t vercel",
|
|
1978
|
+
handler: async () => {
|
|
1979
|
+
await this.runShell("alepha build -t vercel", { root: appDir });
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
async deploy(ctx, run) {
|
|
1984
|
+
const distDir = this.fs.join(ctx.root, "dist");
|
|
1985
|
+
const projectName = ctx.naming.worker();
|
|
1986
|
+
let url;
|
|
1987
|
+
await run({
|
|
1988
|
+
name: `deploy ${ctx.project}`,
|
|
1989
|
+
handler: async () => {
|
|
1990
|
+
let project = await this.api.getProject(projectName);
|
|
1991
|
+
if (!project) project = await this.api.createProject(projectName);
|
|
1992
|
+
await this.api.updateProject(projectName, { framework: null });
|
|
1993
|
+
const vercelDir = this.fs.join(distDir, ".vercel");
|
|
1994
|
+
await this.fs.mkdir(vercelDir);
|
|
1995
|
+
await this.fs.writeFile(this.fs.join(vercelDir, "project.json"), JSON.stringify({
|
|
1996
|
+
projectId: project.id,
|
|
1997
|
+
orgId: project.accountId
|
|
1998
|
+
}, null, 2));
|
|
1999
|
+
const token = process.env.VERCEL_TOKEN;
|
|
2000
|
+
await this.vercelCli.deploy(distDir, {
|
|
2001
|
+
prod: true,
|
|
2002
|
+
token
|
|
2003
|
+
});
|
|
2004
|
+
const latest = (await this.api.listDeployments(project.id, {
|
|
2005
|
+
limit: 1,
|
|
2006
|
+
target: "production"
|
|
2007
|
+
}))[0];
|
|
2008
|
+
url = latest?.alias?.[0] ? `https://${latest.alias[0]}` : `https://${projectName}.vercel.app`;
|
|
2009
|
+
}
|
|
2010
|
+
});
|
|
2011
|
+
return url;
|
|
2012
|
+
}
|
|
2013
|
+
async secrets(ctx, run) {
|
|
2014
|
+
const envVars = await this.envUtils.parseEnv(ctx.root, [`.env.${ctx.env}`]);
|
|
2015
|
+
const vars = [];
|
|
2016
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
2017
|
+
if (!value) continue;
|
|
2018
|
+
if (VercelAdapter.EXCLUDED_SECRET_KEYS.has(key)) continue;
|
|
2019
|
+
if (key.startsWith("VITE_")) continue;
|
|
2020
|
+
vars.push({
|
|
2021
|
+
key,
|
|
2022
|
+
value,
|
|
2023
|
+
target: ["production", "preview"]
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
if (vars.length === 0) return;
|
|
2027
|
+
{
|
|
2028
|
+
const projectName = ctx.naming.worker();
|
|
2029
|
+
await run({
|
|
2030
|
+
name: `push env vars to ${projectName}`,
|
|
2031
|
+
handler: async () => {
|
|
2032
|
+
await this.api.upsertEnvVars(projectName, vars);
|
|
2033
|
+
}
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
async inspect(ctx, run) {
|
|
2038
|
+
const state = {
|
|
2039
|
+
workers: [],
|
|
2040
|
+
databases: [],
|
|
2041
|
+
buckets: [],
|
|
2042
|
+
kvNamespaces: [],
|
|
2043
|
+
queues: [],
|
|
2044
|
+
secrets: []
|
|
2045
|
+
};
|
|
2046
|
+
const tasks = [];
|
|
2047
|
+
{
|
|
2048
|
+
const projectName = ctx.naming.worker();
|
|
2049
|
+
tasks.push({
|
|
2050
|
+
name: `inspect project (${projectName})`,
|
|
2051
|
+
handler: async () => {
|
|
2052
|
+
const project = await this.api.getProject(projectName);
|
|
2053
|
+
if (!project) {
|
|
2054
|
+
state.workers.push({
|
|
2055
|
+
name: projectName,
|
|
2056
|
+
exists: false
|
|
2057
|
+
});
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
const latest = (await this.api.listDeployments(project.id, { limit: 1 }))[0];
|
|
2061
|
+
state.workers.push({
|
|
2062
|
+
name: projectName,
|
|
2063
|
+
exists: true,
|
|
2064
|
+
version: latest?.uid,
|
|
2065
|
+
createdAt: latest?.created ? new Date(latest.created).toISOString() : void 0
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
const envVars = await this.envUtils.parseEnv(ctx.root, [`.env.${ctx.env}`]);
|
|
2071
|
+
const expectedVars = Object.keys(envVars).filter((key) => envVars[key] && !VercelAdapter.EXCLUDED_SECRET_KEYS.has(key) && !key.startsWith("VITE_"));
|
|
2072
|
+
if (expectedVars.length > 0) {
|
|
2073
|
+
const projectName = ctx.naming.worker();
|
|
2074
|
+
tasks.push({
|
|
2075
|
+
name: "inspect env vars",
|
|
2076
|
+
handler: async () => {
|
|
2077
|
+
try {
|
|
2078
|
+
const deployed = await this.api.listEnvVars(projectName);
|
|
2079
|
+
const deployedKeys = new Set(deployed.map((v) => v.key));
|
|
2080
|
+
for (const key of expectedVars) state.secrets.push({
|
|
2081
|
+
name: key,
|
|
2082
|
+
deployed: deployedKeys.has(key)
|
|
2083
|
+
});
|
|
2084
|
+
} catch {
|
|
2085
|
+
for (const key of expectedVars) state.secrets.push({
|
|
2086
|
+
name: key,
|
|
2087
|
+
deployed: false
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
await run(tasks);
|
|
2094
|
+
return state;
|
|
2095
|
+
}
|
|
2096
|
+
async teardown(ctx, run) {
|
|
2097
|
+
{
|
|
2098
|
+
const projectName = ctx.naming.worker();
|
|
2099
|
+
await run({
|
|
2100
|
+
name: `delete project ${projectName}`,
|
|
2101
|
+
handler: async () => {
|
|
2102
|
+
try {
|
|
2103
|
+
await this.api.deleteProject(projectName);
|
|
2104
|
+
} catch (error) {
|
|
2105
|
+
this.log.warn(`Failed to delete project ${projectName}: ${String(error.message || "")}`);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
};
|
|
2112
|
+
//#endregion
|
|
2113
|
+
//#region ../../src/cli/platform-lib/providers/GitHubSecretStore.ts
|
|
2114
|
+
/**
|
|
2115
|
+
* GitHub Actions secret store backed by the `gh` CLI.
|
|
2116
|
+
*
|
|
2117
|
+
* Requires the GitHub CLI (`gh`) to be installed and authenticated.
|
|
2118
|
+
* Pushes secrets into GitHub Actions environments.
|
|
2119
|
+
*/
|
|
2120
|
+
var GitHubSecretStore = class {
|
|
2121
|
+
log = $logger();
|
|
2122
|
+
shell = $inject(ShellProvider);
|
|
2123
|
+
fs = $inject(FileSystemProvider);
|
|
2124
|
+
/**
|
|
2125
|
+
* Verify that `gh` is installed and authenticated.
|
|
2126
|
+
*/
|
|
2127
|
+
async ensureAvailable() {
|
|
2128
|
+
if (!await this.shell.isInstalled("gh")) throw new AlephaError("GitHub CLI (gh) is not installed. Install it from https://cli.github.com");
|
|
2129
|
+
try {
|
|
2130
|
+
await this.shell.run("gh auth status", { capture: true });
|
|
2131
|
+
} catch {
|
|
2132
|
+
throw new AlephaError("GitHub CLI is not authenticated. Run `gh auth login` first.");
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* Create the GitHub Actions environment if it doesn't exist.
|
|
2137
|
+
*/
|
|
2138
|
+
async ensureEnvironment(environment) {
|
|
2139
|
+
await this.shell.run(`gh api --method PUT /repos/{owner}/{repo}/environments/${environment} --silent`, { capture: true });
|
|
2140
|
+
this.log.debug(`Ensured environment "${environment}" exists`);
|
|
2141
|
+
}
|
|
2142
|
+
/**
|
|
2143
|
+
* List all secrets in a GitHub Actions environment.
|
|
2144
|
+
*/
|
|
2145
|
+
async list(environment) {
|
|
2146
|
+
try {
|
|
2147
|
+
const output = await this.shell.run(`gh secret list --env ${environment} --json name,updatedAt`, { capture: true });
|
|
2148
|
+
return JSON.parse(output || "[]").map((s) => ({
|
|
2149
|
+
name: s.name,
|
|
2150
|
+
updatedAt: s.updatedAt
|
|
2151
|
+
}));
|
|
2152
|
+
} catch (error) {
|
|
2153
|
+
this.log.debug("Failed to list secrets", {
|
|
2154
|
+
environment,
|
|
2155
|
+
error
|
|
2156
|
+
});
|
|
2157
|
+
return [];
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Set a secret in a GitHub Actions environment.
|
|
2162
|
+
*
|
|
2163
|
+
* Writes a dotenv-formatted file and uses `gh secret set --env-file` to
|
|
2164
|
+
* avoid shell pipe issues with NodeShellProvider escaping the `|` character.
|
|
2165
|
+
*/
|
|
2166
|
+
async set(environment, key, value) {
|
|
2167
|
+
const tmpFile = `/tmp/alepha-secret-${key}-${Date.now()}`;
|
|
2168
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
|
|
2169
|
+
await this.fs.writeFile(tmpFile, `${key}="${escaped}"\n`);
|
|
2170
|
+
try {
|
|
2171
|
+
const output = await this.shell.run(`gh secret set -f ${tmpFile} --env ${environment}`, { capture: true });
|
|
2172
|
+
this.log.debug(`Secret set: ${key}`, { output });
|
|
2173
|
+
} finally {
|
|
2174
|
+
await this.fs.rm(tmpFile);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
/**
|
|
2178
|
+
* Delete a secret from a GitHub Actions environment.
|
|
2179
|
+
*/
|
|
2180
|
+
async delete(environment, key) {
|
|
2181
|
+
await this.shell.run(`gh secret delete ${key} --env ${environment}`, { capture: true });
|
|
2182
|
+
}
|
|
2183
|
+
};
|
|
2184
|
+
//#endregion
|
|
2185
|
+
//#region ../../src/cli/platform-lib/providers/MemorySecretStore.ts
|
|
2186
|
+
/**
|
|
2187
|
+
* In-memory implementation of SecretStoreProvider for testing.
|
|
2188
|
+
* Records all operations and stores secrets in a nested Map.
|
|
2189
|
+
*/
|
|
2190
|
+
var MemorySecretStore = class {
|
|
2191
|
+
/**
|
|
2192
|
+
* Secrets keyed by environment, then by key.
|
|
2193
|
+
*/
|
|
2194
|
+
secrets = /* @__PURE__ */ new Map();
|
|
2195
|
+
/**
|
|
2196
|
+
* All recorded operations.
|
|
2197
|
+
*/
|
|
2198
|
+
calls = [];
|
|
2199
|
+
/**
|
|
2200
|
+
* When set, ensureAvailable() will throw with this message.
|
|
2201
|
+
*/
|
|
2202
|
+
availableError = null;
|
|
2203
|
+
async ensureAvailable() {
|
|
2204
|
+
this.calls.push({ method: "ensureAvailable" });
|
|
2205
|
+
if (this.availableError) throw new AlephaError(this.availableError);
|
|
2206
|
+
}
|
|
2207
|
+
async ensureEnvironment(environment) {
|
|
2208
|
+
this.calls.push({
|
|
2209
|
+
method: "ensureEnvironment",
|
|
2210
|
+
environment
|
|
2211
|
+
});
|
|
2212
|
+
if (!this.secrets.has(environment)) this.secrets.set(environment, /* @__PURE__ */ new Map());
|
|
2213
|
+
}
|
|
2214
|
+
async list(environment) {
|
|
2215
|
+
this.calls.push({
|
|
2216
|
+
method: "list",
|
|
2217
|
+
environment
|
|
2218
|
+
});
|
|
2219
|
+
const envSecrets = this.secrets.get(environment);
|
|
2220
|
+
if (!envSecrets) return [];
|
|
2221
|
+
return Array.from(envSecrets.keys()).map((name) => ({ name }));
|
|
2222
|
+
}
|
|
2223
|
+
async set(environment, key, value) {
|
|
2224
|
+
this.calls.push({
|
|
2225
|
+
method: "set",
|
|
2226
|
+
environment,
|
|
2227
|
+
key,
|
|
2228
|
+
value
|
|
2229
|
+
});
|
|
2230
|
+
let envSecrets = this.secrets.get(environment);
|
|
2231
|
+
if (!envSecrets) {
|
|
2232
|
+
envSecrets = /* @__PURE__ */ new Map();
|
|
2233
|
+
this.secrets.set(environment, envSecrets);
|
|
2234
|
+
}
|
|
2235
|
+
envSecrets.set(key, value);
|
|
2236
|
+
}
|
|
2237
|
+
async delete(environment, key) {
|
|
2238
|
+
this.calls.push({
|
|
2239
|
+
method: "delete",
|
|
2240
|
+
environment,
|
|
2241
|
+
key
|
|
2242
|
+
});
|
|
2243
|
+
this.secrets.get(environment)?.delete(key);
|
|
2244
|
+
}
|
|
2245
|
+
/**
|
|
2246
|
+
* Check if set() was called for a given environment and key.
|
|
2247
|
+
*/
|
|
2248
|
+
wasSet(environment, key) {
|
|
2249
|
+
return this.calls.some((c) => c.method === "set" && c.environment === environment && c.key === key);
|
|
2250
|
+
}
|
|
2251
|
+
/**
|
|
2252
|
+
* Check if delete() was called for a given environment and key.
|
|
2253
|
+
*/
|
|
2254
|
+
wasDeleted(environment, key) {
|
|
2255
|
+
return this.calls.some((c) => c.method === "delete" && c.environment === environment && c.key === key);
|
|
2256
|
+
}
|
|
2257
|
+
/**
|
|
2258
|
+
* Get all set() calls for a given environment.
|
|
2259
|
+
*/
|
|
2260
|
+
getSetCalls(environment) {
|
|
2261
|
+
return this.calls.filter((c) => c.method === "set" && c.environment === environment).map((c) => ({
|
|
2262
|
+
key: c.key,
|
|
2263
|
+
value: c.value
|
|
2264
|
+
}));
|
|
2265
|
+
}
|
|
2266
|
+
/**
|
|
2267
|
+
* Reset all state.
|
|
2268
|
+
*/
|
|
2269
|
+
reset() {
|
|
2270
|
+
this.secrets.clear();
|
|
2271
|
+
this.calls = [];
|
|
2272
|
+
this.availableError = null;
|
|
2273
|
+
}
|
|
2274
|
+
};
|
|
2275
|
+
//#endregion
|
|
2276
|
+
//#region ../../src/cli/platform-lib/services/PlatformInspector.ts
|
|
2277
|
+
/**
|
|
2278
|
+
* Reads platform config and resolves project topology.
|
|
2279
|
+
*
|
|
2280
|
+
* Validates project name and environment configuration. Does NOT
|
|
2281
|
+
* introspect app code for resources — that happens at deploy time via
|
|
2282
|
+
* ViteBuildProvider.
|
|
2283
|
+
*
|
|
2284
|
+
* Each app self-declares its platform topology via its own
|
|
2285
|
+
* `alepha.config.ts`. Run `alepha platform <op>` from the app's
|
|
2286
|
+
* directory; no monorepo-root orchestration here.
|
|
2287
|
+
*/
|
|
2288
|
+
var PlatformInspector = class {
|
|
2289
|
+
log = $logger();
|
|
2290
|
+
alepha = $inject(Alepha);
|
|
2291
|
+
fs = $inject(FileSystemProvider);
|
|
2292
|
+
asker = $inject(Asker);
|
|
2293
|
+
options = $state(platformOptions);
|
|
2294
|
+
naming = $inject(NamingService);
|
|
2295
|
+
/**
|
|
2296
|
+
* Resolve and validate the full platform configuration.
|
|
2297
|
+
*
|
|
2298
|
+
* Source priority:
|
|
2299
|
+
* 1. `platformOptions` atom (set by `alepha.config.ts` during the
|
|
2300
|
+
* configure hook) — local dev / from-source deploys.
|
|
2301
|
+
* 2. `dist/manifest.json` (written by `alepha build`) — pre-built
|
|
2302
|
+
* deploys via Alepha Rocket or any `--prebuilt` consumer that
|
|
2303
|
+
* ships only the build artifact without `alepha.config.ts`.
|
|
2304
|
+
*/
|
|
2305
|
+
async resolveConfig(root) {
|
|
2306
|
+
if (this.options) {
|
|
2307
|
+
const opts = this.options;
|
|
2308
|
+
const project = await this.resolveProjectName(root, opts.name);
|
|
2309
|
+
return {
|
|
2310
|
+
project: this.naming.slugify(project),
|
|
2311
|
+
defaultEnv: opts.default ?? "production",
|
|
2312
|
+
tenancy: opts.tenancy,
|
|
2313
|
+
environments: opts.environments
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
const manifest = await this.readManifest(root);
|
|
2317
|
+
if (manifest) return {
|
|
2318
|
+
project: this.naming.slugify(manifest.project),
|
|
2319
|
+
defaultEnv: manifest.defaultEnv ?? "production",
|
|
2320
|
+
tenancy: manifest.tenancy,
|
|
2321
|
+
environments: manifest.environments
|
|
2322
|
+
};
|
|
2323
|
+
this.log.warn(` alepha.config.ts not found or missing platform config.
|
|
2324
|
+
|
|
2325
|
+
Please add a "platform" section to alepha.config.ts:
|
|
2326
|
+
|
|
2327
|
+
export default defineConfig({
|
|
2328
|
+
platform: {
|
|
2329
|
+
environments: {
|
|
2330
|
+
production: { adapter: "cloudflare" },
|
|
2331
|
+
},
|
|
2332
|
+
},
|
|
2333
|
+
});
|
|
2334
|
+
`);
|
|
2335
|
+
throw new AlephaError("Missing platform configuration.");
|
|
2336
|
+
}
|
|
2337
|
+
/**
|
|
2338
|
+
* Read `dist/manifest.json` if present. Returns null on any error so
|
|
2339
|
+
* callers fall back to the strict alepha.config.ts path.
|
|
2340
|
+
*/
|
|
2341
|
+
async readManifest(root) {
|
|
2342
|
+
try {
|
|
2343
|
+
const fs = await import("node:fs/promises");
|
|
2344
|
+
const path = await import("node:path");
|
|
2345
|
+
const raw = await fs.readFile(path.join(root, "dist", "manifest.json"), "utf-8");
|
|
2346
|
+
return JSON.parse(raw);
|
|
2347
|
+
} catch {
|
|
2348
|
+
return null;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
/**
|
|
2352
|
+
* Resolve a specific environment, validating it exists.
|
|
2353
|
+
*/
|
|
2354
|
+
async resolveEnvironment(root, envName) {
|
|
2355
|
+
const config = await this.resolveConfig(root);
|
|
2356
|
+
const envConfig = config.environments[envName];
|
|
2357
|
+
if (!envConfig) throw new AlephaError(`Unknown environment "${envName}". Available: ${Object.keys(config.environments).join(", ")}`);
|
|
2358
|
+
return envConfig;
|
|
2359
|
+
}
|
|
2360
|
+
async resolveProjectName(root, configName) {
|
|
2361
|
+
if (configName) return configName;
|
|
2362
|
+
try {
|
|
2363
|
+
const pkgPath = this.fs.join(root, "package.json");
|
|
2364
|
+
const pkg = await this.fs.readJsonFile(pkgPath);
|
|
2365
|
+
if (pkg.name) return pkg.name;
|
|
2366
|
+
} catch {}
|
|
2367
|
+
throw new AlephaError("Missing project name. Set \"name\" in alepha.config.ts or add a \"name\" field to package.json.");
|
|
2368
|
+
}
|
|
2369
|
+
};
|
|
2370
|
+
//#endregion
|
|
2371
|
+
//#region ../../src/cli/platform-lib/services/PlatformOrchestrator.ts
|
|
2372
|
+
/**
|
|
2373
|
+
* Orchestrates platform lifecycle operations.
|
|
2374
|
+
*
|
|
2375
|
+
* Coordinates adapter calls in the correct order for
|
|
2376
|
+
* up (build -> migrate -> deploy), down, plan, and status.
|
|
2377
|
+
*/
|
|
2378
|
+
var PlatformOrchestrator = class {
|
|
2379
|
+
log = $logger();
|
|
2380
|
+
color = $inject(ConsoleColorProvider);
|
|
2381
|
+
inspector = $inject(PlatformInspector);
|
|
2382
|
+
naming = $inject(NamingService);
|
|
2383
|
+
cloudflareAdapter = $inject(CloudflareAdapter);
|
|
2384
|
+
vercelAdapter = $inject(VercelAdapter);
|
|
2385
|
+
alepha = $inject(Alepha);
|
|
2386
|
+
resolveAdapter(adapterName) {
|
|
2387
|
+
switch (adapterName) {
|
|
2388
|
+
case "cloudflare": return this.cloudflareAdapter;
|
|
2389
|
+
case "vercel": return this.vercelAdapter;
|
|
2390
|
+
default: throw new AlephaError(`Unknown adapter: "${adapterName}"`);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
async up(options) {
|
|
2394
|
+
const { root, env, entry, resources, run, prebuilt } = options;
|
|
2395
|
+
const envConfig = await this.inspector.resolveEnvironment(root, env);
|
|
2396
|
+
const config = await this.inspector.resolveConfig(root);
|
|
2397
|
+
const adapter = this.resolveAdapter(envConfig.adapter);
|
|
2398
|
+
const tenant = resolveTenant(config.tenancy, options.tenant);
|
|
2399
|
+
const namingCtx = this.naming.forContext(config.project, env, tenant);
|
|
2400
|
+
const ctx = {
|
|
2401
|
+
project: config.project,
|
|
2402
|
+
env,
|
|
2403
|
+
envConfig,
|
|
2404
|
+
root,
|
|
2405
|
+
entry,
|
|
2406
|
+
resources,
|
|
2407
|
+
naming: namingCtx,
|
|
2408
|
+
tenant,
|
|
2409
|
+
prebuilt
|
|
2410
|
+
};
|
|
2411
|
+
await adapter.authenticate(ctx, run);
|
|
2412
|
+
await adapter.provision(ctx, run);
|
|
2413
|
+
await adapter.build(ctx, run);
|
|
2414
|
+
await adapter.migrate(ctx, run);
|
|
2415
|
+
const url = await adapter.deploy(ctx, run);
|
|
2416
|
+
await adapter.secrets(ctx, run);
|
|
2417
|
+
run.end();
|
|
2418
|
+
return {
|
|
2419
|
+
urls: url ? [url] : [],
|
|
2420
|
+
domain: tenantDomain(envConfig.domain, tenant)
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
/**
|
|
2424
|
+
* Pretty-print the `up()` result to stdout. Matches the formatting the
|
|
2425
|
+
* orchestrator used to emit inline; split out so callers that want
|
|
2426
|
+
* JSON output can skip this branch.
|
|
2427
|
+
*/
|
|
2428
|
+
printUpSummary(result) {
|
|
2429
|
+
const c = this.color;
|
|
2430
|
+
if (result.domain) {
|
|
2431
|
+
this.log.info("");
|
|
2432
|
+
const display = result.domain.includes("*") ? `https://${result.domain} (wildcard route)` : `https://${result.domain}`;
|
|
2433
|
+
this.log.info(` ${c.set("GREEN", "→")} ${c.set("CYAN", display)}`);
|
|
2434
|
+
this.log.info("");
|
|
2435
|
+
} else for (const url of result.urls) {
|
|
2436
|
+
this.log.info("");
|
|
2437
|
+
this.log.info(` ${c.set("GREEN", "→")} ${c.set("CYAN", url)}`);
|
|
2438
|
+
this.log.info("");
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
async down(options) {
|
|
2442
|
+
const { root, env, entry, resources, run, confirm } = options;
|
|
2443
|
+
const envConfig = await this.inspector.resolveEnvironment(root, env);
|
|
2444
|
+
const config = await this.inspector.resolveConfig(root);
|
|
2445
|
+
const adapter = this.resolveAdapter(envConfig.adapter);
|
|
2446
|
+
const tenant = resolveTenant(config.tenancy, options.tenant);
|
|
2447
|
+
const namingCtx = this.naming.forContext(config.project, env, tenant);
|
|
2448
|
+
const ctx = {
|
|
2449
|
+
project: config.project,
|
|
2450
|
+
env,
|
|
2451
|
+
envConfig,
|
|
2452
|
+
root,
|
|
2453
|
+
entry,
|
|
2454
|
+
resources,
|
|
2455
|
+
naming: namingCtx,
|
|
2456
|
+
tenant
|
|
2457
|
+
};
|
|
2458
|
+
if (!this.isTmpEnv(env)) {
|
|
2459
|
+
if (await confirm(`Type "${env}" to confirm teardown:`) !== env) {
|
|
2460
|
+
this.log.info("Aborted.");
|
|
2461
|
+
return false;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
await adapter.authenticate(ctx, run);
|
|
2465
|
+
await adapter.teardown(ctx, run);
|
|
2466
|
+
run.end();
|
|
2467
|
+
return true;
|
|
2468
|
+
}
|
|
2469
|
+
async plan(options) {
|
|
2470
|
+
const { root, env, resources } = options;
|
|
2471
|
+
const config = await this.inspector.resolveConfig(root);
|
|
2472
|
+
const tenant = resolveTenant(config.tenancy, options.tenant);
|
|
2473
|
+
return {
|
|
2474
|
+
config,
|
|
2475
|
+
naming: this.naming.forContext(config.project, env, tenant),
|
|
2476
|
+
resources
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
async status(options) {
|
|
2480
|
+
const { root, env, entry, resources, run } = options;
|
|
2481
|
+
const envConfig = await this.inspector.resolveEnvironment(root, env);
|
|
2482
|
+
const config = await this.inspector.resolveConfig(root);
|
|
2483
|
+
const adapter = this.resolveAdapter(envConfig.adapter);
|
|
2484
|
+
const tenant = resolveTenant(config.tenancy, options.tenant);
|
|
2485
|
+
const namingCtx = this.naming.forContext(config.project, env, tenant);
|
|
2486
|
+
const ctx = {
|
|
2487
|
+
project: config.project,
|
|
2488
|
+
env,
|
|
2489
|
+
envConfig,
|
|
2490
|
+
root,
|
|
2491
|
+
entry,
|
|
2492
|
+
resources,
|
|
2493
|
+
naming: namingCtx,
|
|
2494
|
+
tenant
|
|
2495
|
+
};
|
|
2496
|
+
await adapter.authenticate(ctx, run);
|
|
2497
|
+
return {
|
|
2498
|
+
config,
|
|
2499
|
+
state: await adapter.inspect(ctx, run)
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
isTmpEnv(env) {
|
|
2503
|
+
return env.startsWith("tmp");
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
//#endregion
|
|
2507
|
+
//#region ../../src/cli/platform-lib/services/SecretFilterService.ts
|
|
2508
|
+
/**
|
|
2509
|
+
* Filters environment variables for secret store syncing.
|
|
2510
|
+
*
|
|
2511
|
+
* Excludes platform-managed vars (NODE_ENV), build-time vars (VITE_*),
|
|
2512
|
+
* and empty values. Keeps everything else — including DATABASE_URL
|
|
2513
|
+
* and POSTGRES_SCHEMA which GitHub Actions needs.
|
|
2514
|
+
*
|
|
2515
|
+
* Also handles renaming GITHUB_* keys since GitHub Actions rejects
|
|
2516
|
+
* secret names starting with GITHUB_.
|
|
2517
|
+
*/
|
|
2518
|
+
var SecretFilterService = class SecretFilterService {
|
|
2519
|
+
static EXCLUDED_KEYS = new Set(["NODE_ENV"]);
|
|
2520
|
+
static GITHUB_PREFIX = "GITHUB_";
|
|
2521
|
+
static REMOTE_PREFIX = "APP_GITHUB_";
|
|
2522
|
+
/**
|
|
2523
|
+
* Return only the entries that should be pushed to a secret store.
|
|
2524
|
+
*/
|
|
2525
|
+
filter(envVars) {
|
|
2526
|
+
const result = {};
|
|
2527
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
2528
|
+
if (!value) continue;
|
|
2529
|
+
if (SecretFilterService.EXCLUDED_KEYS.has(key)) continue;
|
|
2530
|
+
if (key.startsWith("VITE_")) continue;
|
|
2531
|
+
result[key] = value;
|
|
2532
|
+
}
|
|
2533
|
+
return result;
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* Convert a local env key to a remote secret name.
|
|
2537
|
+
*
|
|
2538
|
+
* GITHUB_* keys are prefixed with APP_ since GitHub Actions rejects
|
|
2539
|
+
* secret names starting with GITHUB_.
|
|
2540
|
+
*/
|
|
2541
|
+
toRemoteName(key) {
|
|
2542
|
+
if (key.startsWith(SecretFilterService.GITHUB_PREFIX)) return `${SecretFilterService.REMOTE_PREFIX}${key.slice(SecretFilterService.GITHUB_PREFIX.length)}`;
|
|
2543
|
+
return key;
|
|
2544
|
+
}
|
|
2545
|
+
/**
|
|
2546
|
+
* Convert a remote secret name back to the local env key.
|
|
2547
|
+
*/
|
|
2548
|
+
toLocalName(remoteName) {
|
|
2549
|
+
if (remoteName.startsWith(SecretFilterService.REMOTE_PREFIX)) return `${SecretFilterService.GITHUB_PREFIX}${remoteName.slice(SecretFilterService.REMOTE_PREFIX.length)}`;
|
|
2550
|
+
return remoteName;
|
|
2551
|
+
}
|
|
2552
|
+
};
|
|
2553
|
+
//#endregion
|
|
2554
|
+
//#region ../../src/cli/platform-lib/providers/SecretStoreProvider.ts
|
|
2555
|
+
/**
|
|
2556
|
+
* Abstract provider for managing secrets in an external store.
|
|
2557
|
+
*
|
|
2558
|
+
* Implementations: GitHubSecretStore, MemorySecretStore
|
|
2559
|
+
*/
|
|
2560
|
+
var SecretStoreProvider = class {};
|
|
2561
|
+
//#endregion
|
|
2562
|
+
//#region ../../src/cli/platform-lib/schemas/platform.ts
|
|
2563
|
+
const platformStatusWorkerSchema = t.object({
|
|
2564
|
+
name: t.string(),
|
|
2565
|
+
exists: t.boolean(),
|
|
2566
|
+
id: t.optional(t.string()),
|
|
2567
|
+
detail: t.optional(t.string()),
|
|
2568
|
+
version: t.optional(t.string()),
|
|
2569
|
+
tag: t.optional(t.string()),
|
|
2570
|
+
createdAt: t.optional(t.string())
|
|
2571
|
+
});
|
|
2572
|
+
const platformStatusResourceSchema = t.object({
|
|
2573
|
+
name: t.string(),
|
|
2574
|
+
exists: t.boolean(),
|
|
2575
|
+
id: t.optional(t.string()),
|
|
2576
|
+
detail: t.optional(t.string())
|
|
2577
|
+
});
|
|
2578
|
+
const platformStatusSecretSchema = t.object({
|
|
2579
|
+
name: t.string(),
|
|
2580
|
+
deployed: t.boolean()
|
|
2581
|
+
});
|
|
2582
|
+
const platformStatusSchema = t.object({
|
|
2583
|
+
project: t.string(),
|
|
2584
|
+
env: t.string(),
|
|
2585
|
+
adapter: t.string(),
|
|
2586
|
+
workers: t.array(platformStatusWorkerSchema),
|
|
2587
|
+
databases: t.array(platformStatusResourceSchema),
|
|
2588
|
+
buckets: t.array(platformStatusResourceSchema),
|
|
2589
|
+
kvNamespaces: t.array(platformStatusResourceSchema),
|
|
2590
|
+
queues: t.array(platformStatusResourceSchema),
|
|
2591
|
+
secrets: t.array(platformStatusSecretSchema)
|
|
2592
|
+
});
|
|
2593
|
+
const platformPlanAppResourcesSchema = t.object({
|
|
2594
|
+
hasDatabase: t.boolean(),
|
|
2595
|
+
hasBucket: t.boolean(),
|
|
2596
|
+
hasKV: t.boolean(),
|
|
2597
|
+
hasQueue: t.boolean(),
|
|
2598
|
+
hasCron: t.boolean()
|
|
2599
|
+
});
|
|
2600
|
+
const platformPlanAppSchema = t.object({
|
|
2601
|
+
name: t.string(),
|
|
2602
|
+
path: t.string(),
|
|
2603
|
+
resources: platformPlanAppResourcesSchema
|
|
2604
|
+
});
|
|
2605
|
+
const platformPlanEnvironmentSchema = t.object({
|
|
2606
|
+
adapter: t.string(),
|
|
2607
|
+
domain: t.optional(t.string()),
|
|
2608
|
+
zone: t.optional(t.string())
|
|
2609
|
+
});
|
|
2610
|
+
const platformPlanResourceSchema = t.object({
|
|
2611
|
+
label: t.string(),
|
|
2612
|
+
value: t.string()
|
|
2613
|
+
});
|
|
2614
|
+
const platformPlanSchema = t.object({
|
|
2615
|
+
project: t.string(),
|
|
2616
|
+
env: t.string(),
|
|
2617
|
+
mode: t.enum(["monorepo", "standalone"]),
|
|
2618
|
+
apps: t.array(platformPlanAppSchema),
|
|
2619
|
+
environments: t.record(t.string(), platformPlanEnvironmentSchema),
|
|
2620
|
+
resources: t.array(platformPlanResourceSchema),
|
|
2621
|
+
secretCount: t.number()
|
|
2622
|
+
});
|
|
2623
|
+
//#endregion
|
|
2624
|
+
//#region ../../src/cli/platform-lib/index.ts
|
|
2625
|
+
/**
|
|
2626
|
+
* Framework-agnostic platform deploy services.
|
|
2627
|
+
*
|
|
2628
|
+
* Exports `PlatformOrchestrator` + adapters + secret stores + the
|
|
2629
|
+
* `platformOptions` atom — everything needed to drive a deploy
|
|
2630
|
+
* programmatically. **No `$command` instances** and **no
|
|
2631
|
+
* `AppEntryProvider` / `ViteBuildProvider` dependency** — so consumers
|
|
2632
|
+
* importing this subpath don't pull in the CLI argv-parser or Vite.
|
|
2633
|
+
*
|
|
2634
|
+
* Used by Alepha Rocket (and other non-CLI deploy orchestrators) to
|
|
2635
|
+
* call `orchestrator.up({ ... })` directly. For CLI usage
|
|
2636
|
+
* (`alepha platform up`), import `AlephaCliPlatformPlugin` from
|
|
2637
|
+
* `alepha/cli/platform` — that one adds the command layer on top.
|
|
2638
|
+
*/
|
|
2639
|
+
const AlephaPlatformLibPlugin = $module({
|
|
2640
|
+
name: "alepha.cli.platform-lib",
|
|
2641
|
+
services: [
|
|
2642
|
+
CloudflareAdapter,
|
|
2643
|
+
CloudflareApi,
|
|
2644
|
+
VercelAdapter,
|
|
2645
|
+
VercelApi,
|
|
2646
|
+
VercelCli,
|
|
2647
|
+
WranglerApi,
|
|
2648
|
+
PlatformCacheProvider,
|
|
2649
|
+
GitHubSecretStore,
|
|
2650
|
+
MemorySecretStore,
|
|
2651
|
+
NamingService,
|
|
2652
|
+
SecretFilterService,
|
|
2653
|
+
PlatformInspector,
|
|
2654
|
+
PlatformOrchestrator
|
|
2655
|
+
]
|
|
2656
|
+
});
|
|
2657
|
+
//#endregion
|
|
2658
|
+
export { AlephaPlatformLibPlugin, CloudflareAdapter, CloudflareApi, GitHubSecretStore, MemorySecretStore, NamingContext, NamingService, PlatformAdapter, PlatformCacheProvider, PlatformInspector, PlatformOrchestrator, SecretFilterService, SecretStoreProvider, VercelAdapter, VercelApi, VercelCli, WranglerApi, cloudflareAccountSchema, cloudflareApiErrorSchema, cloudflareD1Schema, cloudflareDeploymentListSchema, cloudflareDeploymentSchema, cloudflareDeploymentVersionSchema, cloudflareHyperdriveOriginSchema, cloudflareHyperdriveSchema, cloudflareKVSchema, cloudflareQueueConsumerSchema, cloudflareQueueSchema, cloudflareR2ListSchema, cloudflareR2Schema, cloudflareR2TokenSchema, cloudflareSecretSchema, cloudflareVersionListSchema, cloudflareVersionSchema, cloudflareWorkerSchema, createD1BodySchema, createEnvVarBodySchema, createHyperdriveBodySchema, createHyperdriveOriginSchema, createKVBodySchema, createProjectBodySchema, createQueueBodySchema, createR2BodySchema, createR2TokenBodySchema, platformOptions, platformPlanAppResourcesSchema, platformPlanAppSchema, platformPlanEnvironmentSchema, platformPlanResourceSchema, platformPlanSchema, platformStatusResourceSchema, platformStatusSchema, platformStatusSecretSchema, platformStatusWorkerSchema, putSecretBodySchema, resolveTenant, tenantDomain, vercelDeploymentSchema, vercelEnvVarSchema, vercelProjectSchema };
|
|
2659
|
+
|
|
2660
|
+
//# sourceMappingURL=index.js.map
|