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<string> {
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<string> = []
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<string> {
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<string> {
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<string> {
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<OriginInstallation>
}> {
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<string>
} = {}): 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<string, unknown>
claims: Record<string, unknown>
}): Promise<{ jwt: string }> {
const sign = kody.secretJwtSign 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
? {
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<string, unknown> {
return isRecord(value) ? value : {}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value)
}
function stringField(value: unknown): string | null {
return typeof value === 'string' && value.length > 0 ? value : null
}