import { requireString } from './types.ts'
export const PLANETSCALE_API_BASE_URL = 'https://api.planetscale.com/v1'
export const PLANETSCALE_API_HOST = 'api.planetscale.com'
export const PLANETSCALE_APP_URL = 'https://app.planetscale.com'
export const PLANETSCALE_TOKEN_DOCS = 'https://planetscale.com/docs/api/service-tokens'
export const DEFAULT_API_TOKEN_SECRET = 'planetscaleApiToken'
export const DEFAULT_WEBHOOK_SECRET = 'planetscaleWebhookSecret'
export type PlanetscaleOperation =
| 'organizations.read'
| 'databases.read'
| 'branches.read'
| 'insights.read'
| 'webhooks.write'
| 'unknownRead'
| 'unknownMutation'
export function apiTokenSetupUrl(secretName: string = DEFAULT_API_TOKEN_SECRET): string {
const name = requireString(secretName, 'secretName')
const params = new URLSearchParams({
name,
description:
'PlanetScale service token as SERVICE_TOKEN_ID:SERVICE_TOKEN for api.planetscale.com',
allowedHosts: PLANETSCALE_API_HOST,
scope: 'user',
})
return `https://kody.codes/account/secrets/new?${params.toString()}`
}
export function webhookSecretSetupUrl(secretName: string = DEFAULT_WEBHOOK_SECRET): string {
const name = requireString(secretName, 'secretName')
const params = new URLSearchParams({
name,
description: 'PlanetScale webhook HMAC secret (X-PlanetScale-Signature)',
allowedHosts: PLANETSCALE_API_HOST,
scope: 'user',
})
return `https://kody.codes/account/secrets/new?${params.toString()}`
}
export function missingCredentialsMessage(options: { secretName: string }): string {
return [
'PlanetScale credentials are missing.',
`Create a service token at ${PLANETSCALE_TOKEN_DOCS}`,
'with at least read_organization and read_database (write_database if you will create the Insights webhook).',
'Save the value as SERVICE_TOKEN_ID:SERVICE_TOKEN (do not paste it in chat):',
apiTokenSetupUrl(options.secretName),
`Required API host: ${PLANETSCALE_API_HOST}.`,
].join(' ')
}
export function isMutatingMethod(method: string): boolean {
const normalized = method.toUpperCase()
return (
normalized === 'POST' ||
normalized === 'PUT' ||
normalized === 'PATCH' ||
normalized === 'DELETE'
)
}
export function inferOperationFromPath(
method: string,
path: string,
explicit?: PlanetscaleOperation,
): PlanetscaleOperation {
if (explicit) return explicit
const mutating = isMutatingMethod(method)
if (/\/insights(?:\/|$)/.test(path)) {
return mutating ? 'unknownMutation' : 'insights.read'
}
if (/\/webhooks(?:\/|$)/.test(path)) {
return mutating ? 'webhooks.write' : 'unknownRead'
}
if (/\/branches(?:\/|$)/.test(path)) {
return mutating ? 'unknownMutation' : 'branches.read'
}
if (/\/databases(?:\/|$)/.test(path)) {
return mutating ? 'unknownMutation' : 'databases.read'
}
if (/\/organizations(?:\/|$)/.test(path)) {
return mutating ? 'unknownMutation' : 'organizations.read'
}
return mutating ? 'unknownMutation' : 'unknownRead'
}
export function nextStepForAuthFailure(secretName: string): string {
return [
'PlanetScale rejected the service token (401/403). Confirm the secret is',
'SERVICE_TOKEN_ID:SERVICE_TOKEN and that host api.planetscale.com is approved.',
apiTokenSetupUrl(secretName),
].join(' ')
}
export function insightsUrl(organization: string, database: string, branch: string): string {
return `${PLANETSCALE_APP_URL}/${encodeURIComponent(organization)}/${encodeURIComponent(database)}/${encodeURIComponent(branch)}/insights`
}