import { packageSecrets } from 'kody:runtime' import { AwsApiError, nextStepFor, slimErrorBody } from './helpers.ts' import { redactUnknown } from './redact.ts' import { assertAmazonAwsHost, serviceHost, signAwsRequest } from './sign.ts' import { DEFAULT_REGION, assertRegion, assertService, inputRecord, optionalString, resolveAccessKeySecretName, resolveSecretKeySecretName, resolveSessionTokenSecretName, type AwsAuthOptions, } from './validation.ts' import { resolveRegion } from './config.ts' type MountedSecrets = { get?: (alias: string) => unknown [alias: string]: unknown } | ((alias: string) => unknown) async function readMountedSecret(alias: string): Promise { try { const mounts = packageSecrets as MountedSecrets | null if (typeof mounts === 'function') { const value = await (mounts as (name: string) => unknown)(alias) if (typeof value === 'string' && value.trim()) return value.trim() } if (mounts && typeof mounts === 'object') { if (typeof mounts.get === 'function') { const value = await mounts.get(alias) if (typeof value === 'string' && value.trim()) return value.trim() } const direct = mounts[alias] if (typeof direct === 'string' && direct.trim()) return direct.trim() if (direct && typeof direct === 'object' && 'value' in direct) { const value = (direct as { value?: unknown }).value if (typeof value === 'string' && value.trim()) return value.trim() } } const env = ( globalThis as { process?: { env?: Record } } ).process?.env const envValue = env?.[alias] ?? env?.[alias.toUpperCase()] if (typeof envValue === 'string' && envValue.trim()) return envValue.trim() } catch { return null } return null } export async function resolveAwsCredentials(input: AwsAuthOptions = {}) { const accessKeySecret = resolveAccessKeySecretName(input) const secretKeySecret = resolveSecretKeySecretName(input) const sessionTokenSecret = resolveSessionTokenSecretName(input) const accessKeyId = await readMountedSecret(accessKeySecret) const secretAccessKey = await readMountedSecret(secretKeySecret) const sessionToken = (await readMountedSecret(sessionTokenSecret)) ?? undefined if (!accessKeyId || !secretAccessKey) { throw new Error( 'AWS credentials are not mounted in this runtime. Save awsAccessKeyId and awsSecretAccessKey, then call this export from the package runtime (or after forking) so secret mounts resolve. Setup: ' + 'https://kody.codes/account/secrets/new?name=awsAccessKeyId&scope=user', ) } return { accessKeyId, secretAccessKey, sessionToken, accessKeySecret, secretKeySecret, sessionTokenSecret, } } export async function resolveAwsRegion( input: AwsAuthOptions = {}, ): Promise { const fromInput = optionalString(inputRecord(input), 'region') if (fromInput) return assertRegion(fromInput) return resolveRegion() } export type AwsRequestInput = AwsAuthOptions & { service: string method?: string path?: string query?: Record headers?: Record body?: string host?: string unsignedPayload?: boolean } export async function awsSignedFetch(input: AwsRequestInput) { const service = assertService(input.service) const region = await resolveAwsRegion(input) const credentials = await resolveAwsCredentials(input) const host = input.host?.trim() || serviceHost(service, region) assertAmazonAwsHost(host) const url = new URL('https://' + host + (input.path || '/')) for (const [key, value] of Object.entries(input.query ?? {})) { if (value === undefined || value === '') continue url.searchParams.set(key, value) } const method = (input.method ?? 'GET').toUpperCase() const signed = await signAwsRequest({ method, url: url.toString(), region, service, accessKeyId: credentials.accessKeyId, secretAccessKey: credentials.secretAccessKey, sessionToken: credentials.sessionToken, headers: input.headers, body: input.body, unsignedPayload: input.unsignedPayload ?? service === 's3', }) const response = await fetch(signed.url, { method: signed.method, headers: signed.headers, body: signed.body, }) const text = await response.text() if (!response.ok) { const details = slimErrorBody(text) throw new AwsApiError( response.status, details, nextStepFor( response.status, typeof details.message === 'string' ? details.message : '', credentials.accessKeySecret, credentials.secretKeySecret, host, ), ) } return { status: response.status, headers: Object.fromEntries(response.headers.entries()), text, region, host, service, } } export function parseJsonBody(text: string): unknown { if (!text.trim()) return {} return redactUnknown(JSON.parse(text)) }