Skip to content

src/secrets.ts

85 lines · 2.7 KB · TypeScript
Raw
/**
 * Package secrets for the Audible tokens. Code writes values once with
 * `secretSet` and afterwards handles only opaque `{{secret:…}}` refs, which
 * Kody fills in on the way out to approved hosts.
 */

import { kody, packageSecrets } from 'kody:runtime'
import { NotConnectedError } from './device.ts'

export async function saveSecret(input: {
	name: string
	value: string
	description: string
	expiresAt?: string | null
}): Promise<{ packageId: string | null }> {
	const saved = (await kody.secretSet({
		name: input.name,
		value: input.value,
		description: input.description,
		scope: 'package',
		...(input.expiresAt !== undefined ? { expires_at: input.expiresAt } : {}),
	})) as { package_id?: unknown } | null
	return { packageId: typeof saved?.package_id === 'string' ? saved.package_id : null }
}

/** Delete a package secret. Returns false when Kody refuses or it is already gone. */
export async function deleteSecret(name: string): Promise<boolean> {
	try {
		const result = (await kody.secretDelete({ name, scope: 'package' })) as { deleted?: unknown } | null
		return result?.deleted === true
	} catch {
		return false
	}
}

/** Opaque placeholder for a package secret. Never a value. */
export async function secretRef(name: string): Promise<string> {
	let ref: unknown
	try {
		ref = await packageSecrets.get(name)
	} catch {
		ref = null
	}
	if (typeof ref !== 'string' || !ref.startsWith('{{secret')) {
		throw new NotConnectedError()
	}
	return ref
}

export type SecretState = {
	exists: boolean
	hosts: string[]
	expiresAt: string | null
	packageId: string | null
}

/**
 * Metadata only: which hosts may receive each secret. Returns null when the
 * runtime does not list package secrets (the hosted app surface does not).
 */
export async function secretStates(names: string[]): Promise<Record<string, SecretState> | null> {
	const states: Record<string, SecretState> = {}
	for (const name of names) states[name] = { exists: false, hosts: [], expiresAt: null, packageId: null }
	let found = false
	try {
		const listed = (await kody.secretList({ scope: 'package' })) as
			| Array<Record<string, unknown>>
			| { secrets?: Array<Record<string, unknown>> }
		const rows = Array.isArray(listed) ? listed : (listed?.secrets ?? [])
		for (const row of rows) {
			const name = String(row.name ?? '')
			if (!(name in states)) continue
			found = true
			states[name] = {
				exists: true,
				hosts: Array.isArray(row.allowed_hosts) ? row.allowed_hosts.map(String) : [],
				expiresAt: typeof row.expires_at === 'string' ? row.expires_at : null,
				packageId: typeof row.package_id === 'string' ? row.package_id : null,
			}
		}
	} catch {
		return null
	}
	return found ? states : null
}