Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/planetscale

src/setup.ts

98 lines · 3.4 KB · TypeScript
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`
}