1import { makeOAuthClient } from "../Auth/OAuthFlow.ts";
2import * as Redacted from "effect/Redacted";
3
4export {
5 OAuthCredentials,
6 OAuthError,
7 type Authorization,
8} from "../Auth/OAuthFlow.ts";
9
10/**
11 * Registered PlanetScale OAuth application credentials.
12 *
13 * Unlike Cloudflare, PlanetScale OAuth has **no public-client flow** — the
14 * token endpoint requires client authentication for every grant (PKCE is
15 * advertised in their discovery doc but does not lift that requirement), so
16 * exchanging the authorization code (and refreshing the token) requires the
17 * application's `client_secret`. There is no way to keep that secret out of
18 * a distributed CLI, so it ships here: the exposure is the same posture as a
19 * public `client_id` (a stolen refresh token is usable, exactly like
20 * Cloudflare's secret-less refresh), and it can be rotated by cutting a new
21 * release. PlanetScale's own CLI ships its OAuth `client_secret` in source
22 * the same way:
23 * https://github.com/planetscale/cli/blob/main/internal/auth/authenticator.go
24 *
25 * Registered at https://app.planetscale.com with redirect URI
26 * {@link OAUTH_REDIRECT_URI}. Scopes are configured on the application
27 * itself, not requested per-authorization (so `authorize()` is called with
28 * no scopes). Rotate by registering a new secret and cutting a release.
29 */
30export const OAUTH_CLIENT_ID = "pscale_app_aa12e3938baebb788aac443f66e422da";
31export const OAUTH_CLIENT_SECRET = Redacted.make(
32 "pscale_app_secret_yyZ3Q8oe99GP9_yA5wrA5er6RuN6Lz9dC66Bj1OJzpg",
33);
34
35export const OAUTH_REDIRECT_URI = "https://alchemy.run/auth/callback";
36export const OAUTH_LOCAL_CALLBACK_URI = "http://localhost:9976/auth/callback";
37export const OAUTH_ENDPOINTS = {
38 // PlanetScale's own .well-known OAuth discovery doc declares this as
39 // the authorization_endpoint — NOT auth.planetscale.com/oauth/authorize
40 // (which their public docs cite). The auth.planetscale.com alias does
41 // render a consent screen but emits codes whose resulting tokens lack
42 // a `sub` claim, so the resource API at api.planetscale.com rejects
43 // them as invalid. Use the canonical endpoint.
44 authorize: "https://app.planetscale.com/oauth/authorize",
45 token: "https://auth.planetscale.com/oauth/token",
46};
47
48const client = makeOAuthClient({
49 clientId: OAUTH_CLIENT_ID,
50 endpoints: OAUTH_ENDPOINTS,
51 redirectUri: OAUTH_REDIRECT_URI,
52 localCallbackUri: OAUTH_LOCAL_CALLBACK_URI,
53 auth: { kind: "clientSecret", clientSecret: OAUTH_CLIENT_SECRET },
54 // PlanetScale's docs show the token endpoint with all parameters in the
55 // query string (https://planetscale.com/docs/api/reference/oauth). Their
56 // .well-known discovery doc advertises client_secret_basic /
57 // client_secret_post instead, but both behave identically to the
58 // query-string form in practice, so we follow the public docs literally.
59 tokenTransport: "query",
60});
61
62export const {
63 authorize,
64 callback,
65 exchange,
66 exchangeCallbackInput,
67 refresh,
68 usesCurrentClient,
69} = client;