| 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as Redacted from "effect/Redacted"; |
| 3 | import * as Schema from "effect/Schema"; |
| 4 | import { AuthError } from "../../Auth/AuthProvider.ts"; |
| 5 | import { profileCommandHint } from "../../Util/interactive.ts"; |
| 6 | |
| 7 | /** Typed values stored in a Cloudflare provider profile document. */ |
| 8 | export const CloudflareAuthConfigSchema = Schema.Union([ |
| 9 | Schema.Struct({ |
| 10 | method: Schema.Literal("stored"), |
| 11 | credentialType: Schema.Literal("apiToken"), |
| 12 | apiToken: Schema.String, |
| 13 | accountId: Schema.String, |
| 14 | }), |
| 15 | Schema.Struct({ |
| 16 | method: Schema.Literal("stored"), |
| 17 | credentialType: Schema.Literal("apiKey"), |
| 18 | apiKey: Schema.String, |
| 19 | email: Schema.String, |
| 20 | accountId: Schema.String, |
| 21 | }), |
| 22 | Schema.Struct({ |
| 23 | method: Schema.Literal("oauth"), |
| 24 | scopes: Schema.mutable(Schema.Array(Schema.String)), |
| 25 | accountId: Schema.String, |
| 26 | clientId: Schema.optional(Schema.String), |
| 27 | access: Schema.String, |
| 28 | refresh: Schema.String, |
| 29 | expires: Schema.Number, |
| 30 | }), |
| 31 | // Released v0 OAuth grants cannot be reused, but retaining the method lets |
| 32 | // `profile refresh` restart OAuth at scope selection. |
| 33 | Schema.Struct({ |
| 34 | method: Schema.Literal("oauth"), |
| 35 | }), |
| 36 | ]); |
| 37 | export type CloudflareAuthConfig = typeof CloudflareAuthConfigSchema.Type; |
| 38 | |
| 39 | export type CloudflareResolvedCredentials = |
| 40 | | { |
| 41 | type: "apiToken"; |
| 42 | apiToken: Redacted.Redacted<string>; |
| 43 | accountId: string; |
| 44 | source: { |
| 45 | type: CloudflareAuthConfig["method"] | "env"; |
| 46 | details?: string; |
| 47 | }; |
| 48 | } |
| 49 | | { |
| 50 | type: "apiKey"; |
| 51 | apiKey: Redacted.Redacted<string>; |
| 52 | email: Redacted.Redacted<string>; |
| 53 | accountId: string; |
| 54 | source: { |
| 55 | type: CloudflareAuthConfig["method"] | "env"; |
| 56 | details?: string; |
| 57 | }; |
| 58 | } |
| 59 | | { |
| 60 | type: "oauth"; |
| 61 | accessToken: Redacted.Redacted<string>; |
| 62 | expires: number; |
| 63 | accountId: string; |
| 64 | source: { |
| 65 | type: CloudflareAuthConfig["method"] | "env"; |
| 66 | details?: string; |
| 67 | }; |
| 68 | }; |
| 69 | |
| 70 | export const CLOUDFLARE_AUTH_PROVIDER_NAME = "Cloudflare"; |
| 71 | |
| 72 | /** |
| 73 | * Cloudflare account IDs are 32 lowercase hex characters. Placeholder |
| 74 | * values ("", "-", "dummy", …) end up interpolated into API paths and |
| 75 | * surface as baffling `InvalidRoute: Could not route to |
| 76 | * /accounts/<value>/...` errors, so reject them up front with an |
| 77 | * actionable message instead. |
| 78 | */ |
| 79 | const ACCOUNT_ID_PATTERN = /^[0-9a-f]{32}$/i; |
| 80 | |
| 81 | export const validateAccountId = ( |
| 82 | accountId: string | undefined, |
| 83 | source: string, |
| 84 | ): Effect.Effect<string, AuthError> => |
| 85 | Effect.gen(function* () { |
| 86 | const trimmed = accountId?.trim() ?? ""; |
| 87 | const command = yield* profileCommandHint( |
| 88 | "alchemy profile edit --reconfigure Cloudflare", |
| 89 | ); |
| 90 | if (trimmed.length === 0) { |
| 91 | return yield* Effect.fail( |
| 92 | new AuthError({ |
| 93 | message: |
| 94 | `Cloudflare account ID is missing (${source}). ` + |
| 95 | `Re-run \`${command}\` and provide your account ID ` + |
| 96 | "(found in the Cloudflare dashboard under Workers & Pages → Account details).", |
| 97 | }), |
| 98 | ); |
| 99 | } |
| 100 | if (!ACCOUNT_ID_PATTERN.test(trimmed)) { |
| 101 | return yield* Effect.fail( |
| 102 | new AuthError({ |
| 103 | message: |
| 104 | `'${trimmed}' is not a valid Cloudflare account ID (${source}) — expected 32 hex characters. ` + |
| 105 | "Copy the account ID from the Cloudflare dashboard (Workers & Pages → Account details) " + |
| 106 | `and re-run \`${command}\`.`, |
| 107 | }), |
| 108 | ); |
| 109 | } |
| 110 | return trimmed.toLowerCase(); |
| 111 | }); |
| 112 | |
| 113 | /** Field-level validator reusing {@link ACCOUNT_ID_PATTERN}. */ |
| 114 | export const validateAccountIdField = (v: string): string | undefined => |
| 115 | ACCOUNT_ID_PATTERN.test(v.trim()) |
| 116 | ? undefined |
| 117 | : "Expected a 32-character hex account ID (Workers & Pages → Account details)"; |