import { kody, packageStorage } from 'kody:runtime' import { originFetch } from './http.ts' import { type OriginInstallation } from './types.ts' const APP_ID_VALUE = 'originAppId' const INSTALLATION_ID_VALUE = 'originInstallationId' const PRIVATE_KEY_SECRET = 'originAppPrivateKey' const APP_JWT_AUDIENCE = 'origin-apps' const APP_JWT_LIFETIME_SECONDS = 300 export async function readNamedSetting(name: string): Promise { const storage = packageStorage() const stored = await storage.get(name) if (typeof stored === 'string' && stored.trim()) return stored.trim() 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() { const copied: Array = [] const appId = await readNamedSetting(APP_ID_VALUE) if (appId) copied.push(APP_ID_VALUE) const installationId = await readNamedSetting(INSTALLATION_ID_VALUE) if (installationId) copied.push(INSTALLATION_ID_VALUE) return { copied, hasAppId: Boolean(appId), hasInstallationId: Boolean(installationId), } } export async function resolveOriginAppId(): Promise { const appId = await readNamedSetting(APP_ID_VALUE) if (!appId) { throw new Error( 'Save originAppId in this package\'s storage (the Origin App id, app_01…). See coding_guide_get({ guide: "provider_origin" }).', ) } return appId } export async function signOriginAppJwt(): Promise { const appId = await resolveOriginAppId() const now = Math.floor(Date.now() / 1000) const signed = await signOriginAppJwtWithHost({ private_key_secret_name: PRIVATE_KEY_SECRET, 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( explicitId?: string, ): Promise { const requested = explicitId?.trim() if (requested) return requested const saved = await readNamedSetting(INSTALLATION_ID_VALUE) if (saved) return saved const { items } = await listOriginInstallations() const first = items[0] if (!first) { throw new Error( 'No Origin App installations found. Install the app into a codebase and save originInstallationId in this package\'s storage. See coding_guide_get({ guide: "provider_origin" }).', ) } return first.id } export async function listOriginInstallations(): Promise<{ items: Array }> { const result = await originFetch({ path: '/app/installations', bearerToken: await signOriginAppJwt(), }) 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: { installationId?: string scopes?: Array } = {}): Promise<{ token: string installationId: string expiresAt: string | null }> { const installationId = await resolveOriginInstallationId(input.installationId) const result = await originFetch({ path: `/app/installations/${encodeURIComponent(installationId)}/access_tokens`, method: 'POST', bearerToken: await signOriginAppJwt(), 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 claims: Record }): Promise<{ jwt: string }> { const sign = kody.secretJwtSign as (args: { private_key_secret_name: string algorithm: 'RS256' | 'EdDSA' header?: Record claims: Record }) => Promise<{ jwt: string }> return sign(input) } function slimInstallation(item: Record): 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 ? { slug: stringField(target.slug) ?? '', id: stringField(target.id) ?? '', } : null, repoSelectionMode: stringField(item.repoSelectionMode), scopes, } } function assertOk(status: number, body: unknown, action: string) { if (status >= 200 && status < 300) return const message = isRecord(body) && typeof body.message === 'string' ? body.message : `HTTP ${String(status)}` throw new Error(`Failed to ${action}: ${message}`) } function asRecord(value: unknown): Record { return isRecord(value) ? value : {} } function isRecord(value: unknown): value is Record { return value !== null && typeof value === 'object' && !Array.isArray(value) } function stringField(value: unknown): string | null { return typeof value === 'string' && value.length > 0 ? value : null }