Skip to content

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

Package listing

@kody/origin

src/token.ts

275 lines · 8.7 KB · TypeScript
import { kody, packageStorage } from 'kody:runtime'
import {
	asRecord,
	assertOk,
	isRecord,
	originFetch,
	stringField,
} from './http.ts'
import {
	type OriginAccountInput,
	type OriginAuthLaneInfo,
	type OriginInstallation,
	type OriginResolvedAccount,
} from './types.ts'

export const DEFAULT_ORIGIN_ACCOUNT = 'origin'
export const DEFAULT_ORIGIN_SECRET_NAME = 'originAppPrivateKey'
export const APP_ID_VALUE = 'originAppId'
export const INSTALLATION_ID_VALUE = 'originInstallationId'
export const ORIGIN_API_HOST = 'api.cursor.com'
export const APP_JWT_AUDIENCE = 'origin-apps'
export const APP_JWT_LIFETIME_SECONDS = 300

export const ORIGIN_SECRET_SETUP_URL =
	'https://kody.codes/account/secrets/new?name=originAppPrivateKey&description=Origin%20App%20Ed25519%20PKCS%238%20private%20key&allowedHosts=api.cursor.com&allowedCapabilities=secret_jwt_sign&scope=user'

export const ORIGIN_APPS_DASHBOARD_URL =
	'https://cursor.com/codebase/settings/apps'

export const ORIGIN_PUBLIC_URL = 'https://kody.codes/@kody/origin'

export function resolveOriginAccount(
	input: OriginAccountInput = {},
): OriginResolvedAccount {
	const secretName = trimToUndefined(input.secretName)
	const account =
		trimToUndefined(input.integrationName) ??
		trimToUndefined(input.account) ??
		DEFAULT_ORIGIN_ACCOUNT
	if (secretName) {
		return {
			account,
			secretName,
			appIdKey: storageKey(APP_ID_VALUE, account),
			installationIdKey: storageKey(INSTALLATION_ID_VALUE, account),
		}
	}
	return {
		account,
		secretName: secretNameForAccount(account),
		appIdKey: storageKey(APP_ID_VALUE, account),
		installationIdKey: storageKey(INSTALLATION_ID_VALUE, account),
	}
}

export function secretSetupUrlForAccount(account: string): string {
	const secretName = secretNameForAccount(account)
	const params = new URLSearchParams({
		name: secretName,
		description: 'Origin App Ed25519 PKCS#8 private key',
		allowedHosts: ORIGIN_API_HOST,
		allowedCapabilities: 'secret_jwt_sign',
		scope: 'user',
	})
	return `https://kody.codes/account/secrets/new?${params.toString()}`
}

export function accounts(): readonly OriginAuthLaneInfo[] {
	return [
		{
			lane: 'origin-app',
			default: true,
			account: DEFAULT_ORIGIN_ACCOUNT,
			secretName: DEFAULT_ORIGIN_SECRET_NAME,
			secretSetupUrl: ORIGIN_SECRET_SETUP_URL,
			useWhen:
				'Default Origin App. Extra apps use a distinct account/integrationName (for example origin-work) and matching secret/storage keys.',
			avoidWhen:
				'You only have a Cloud Agents API key. Origin rejects crsr_ / cursorApiKey credentials.',
			mutationGuidance:
				'Mutating helpers and non-GET originRequest calls require dryRun: true to preview, then confirm: true to apply. Jobs are not enabled.',
		},
	]
}

export async function readNamedSetting(name: string): Promise<string> {
	const storage = packageStorage()
	const stored = await storage.get(name)
	if (typeof stored === 'string' && stored.trim()) return stored.trim()
	try {
		const entry = await kody.value_get({ name, scope: 'user' })
		const value = typeof entry?.value === 'string' ? entry.value.trim() : ''
		if (value) {
			await storage.set(name, value)
			return value
		}
	} catch {
		// Leftover user values may already be gone.
	}
	return ''
}

export async function writeNamedSetting(name: string, value: string) {
	const trimmed = value.trim()
	if (!trimmed) throw new Error(`${name} is required.`)
	await packageStorage().set(name, trimmed)
	return trimmed
}

export async function migrateOriginIdsFromValues(
	input: OriginAccountInput = {},
) {
	const resolved = resolveOriginAccount(input)
	const copied: Array<string> = []
	const appId = await readNamedSetting(resolved.appIdKey)
	if (appId) copied.push(resolved.appIdKey)
	const installationId = await readNamedSetting(resolved.installationIdKey)
	if (installationId) copied.push(resolved.installationIdKey)
	return {
		account: resolved.account,
		copied,
		hasAppId: Boolean(appId),
		hasInstallationId: Boolean(installationId),
	}
}

export async function resolveOriginAppId(
	input: OriginAccountInput = {},
): Promise<string> {
	const explicit = trimToUndefined(input.appId)
	if (explicit) return explicit
	const resolved = resolveOriginAccount(input)
	const appId = await readNamedSetting(resolved.appIdKey)
	if (!appId) {
		throw new Error(
			`Save ${resolved.appIdKey} in this package's storage (the Origin App id, app_01…) or pass appId. See ${ORIGIN_PUBLIC_URL} and coding_guide_get({ guide: "provider_origin" }).`,
		)
	}
	return appId
}

export async function signOriginAppJwt(
	input: OriginAccountInput = {},
): Promise<string> {
	const resolved = resolveOriginAccount(input)
	const appId = await resolveOriginAppId(input)
	const now = Math.floor(Date.now() / 1000)
	const signed = await signOriginAppJwtWithHost({
		private_key_secret_name: resolved.secretName,
		algorithm: 'EdDSA',
		header: { kid: appId },
		claims: {
			iss: appId,
			aud: APP_JWT_AUDIENCE,
			iat: now,
			exp: now + APP_JWT_LIFETIME_SECONDS,
		},
	})
	return signed.jwt
}

export async function resolveOriginInstallationId(
	input: OriginAccountInput = {},
): Promise<string> {
	const requested = trimToUndefined(input.installationId)
	if (requested) return requested
	const resolved = resolveOriginAccount(input)
	const saved = await readNamedSetting(resolved.installationIdKey)
	if (saved) return saved
	const { items } = await listOriginInstallations(input)
	if (items.length === 1 && items[0]) return items[0].id
	if (items.length === 0) {
		throw new Error(
			`No Origin App installations found. Install the app at ${ORIGIN_APPS_DASHBOARD_URL} and save ${resolved.installationIdKey} (or pass installationId). See ${ORIGIN_PUBLIC_URL}.`,
		)
	}
	const ids = items.map((item) => item.id).filter(Boolean).join(', ')
	throw new Error(
		`Multiple Origin installations found (${ids}). Pass installationId or save ${resolved.installationIdKey} in this package's storage. Do not pick one implicitly.`,
	)
}

export async function listOriginInstallations(
	input: OriginAccountInput = {},
): Promise<{ items: Array<OriginInstallation> }> {
	const result = await originFetch({
		path: '/app/installations',
		bearerToken: await signOriginAppJwt(input),
	})
	assertOk(result.status, result.body, 'list Origin installations')
	const payload = asRecord(result.body)
	const raw = Array.isArray(payload.installations) ? payload.installations : []
	return {
		items: raw.map((item) => slimInstallation(asRecord(item))),
	}
}

export async function mintInstallationAccessTokenInternal(
	input: OriginAccountInput & { scopes?: Array<string> } = {},
): Promise<{
	token: string
	installationId: string
	expiresAt: string | null
}> {
	const installationId = await resolveOriginInstallationId(input)
	const result = await originFetch({
		path: `/app/installations/${encodeURIComponent(installationId)}/access_tokens`,
		method: 'POST',
		bearerToken: await signOriginAppJwt(input),
		body: input.scopes && input.scopes.length > 0 ? { scopes: input.scopes } : {},
	})
	assertOk(result.status, result.body, 'mint Origin installation token')
	const payload = asRecord(result.body)
	const token = typeof payload.token === 'string' ? payload.token : ''
	if (!token) {
		throw new Error('Origin installation token response did not include token.')
	}
	return {
		token,
		installationId,
		expiresAt: typeof payload.expiresAt === 'string' ? payload.expiresAt : null,
	}
}

async function signOriginAppJwtWithHost(input: {
	private_key_secret_name: string
	algorithm: 'EdDSA'
	header: Record<string, unknown>
	claims: Record<string, unknown>
}): Promise<{ jwt: string }> {
	const sign = kody.secret_jwt_sign as (args: {
		private_key_secret_name: string
		algorithm: 'RS256' | 'EdDSA'
		header?: Record<string, unknown>
		claims: Record<string, unknown>
	}) => Promise<{ jwt: string }>
	return sign(input)
}

function slimInstallation(item: Record<string, unknown>): OriginInstallation {
	const target = isRecord(item.target) ? item.target : null
	const scopes = Array.isArray(item.scopes)
		? item.scopes.filter((scope): scope is string => typeof scope === 'string')
		: []
	return {
		id: stringField(item.id) ?? '',
		appId: stringField(item.appId),
		target: target ? slimOwner(target) : null,
		repoSelectionMode: stringField(item.repoSelectionMode),
		scopes,
	}
}

export function slimOwner(item: Record<string, unknown>) {
	return {
		slug: stringField(item.slug) ?? '',
		id: stringField(item.id) ?? '',
		type: stringField(item.type),
	}
}

function secretNameForAccount(account: string): string {
	if (account === DEFAULT_ORIGIN_ACCOUNT) return DEFAULT_ORIGIN_SECRET_NAME
	return `${DEFAULT_ORIGIN_SECRET_NAME}-${account}`
}

function storageKey(base: string, account: string): string {
	if (account === DEFAULT_ORIGIN_ACCOUNT) return base
	return `${base}-${account}`
}

function trimToUndefined(value: string | undefined): string | undefined {
	const trimmed = value?.trim()
	return trimmed ? trimmed : undefined
}