import { kody } from 'kody:runtime' import { getCallerIdentity } from './identity.ts' import { deleteObject, putObject } from './s3.ts' import { COMMON_AWS_HOSTS, accessKeySetupUrl, resolveAccessKeySecretName, resolveSecretKeySecretName, resolveSessionTokenSecretName, secretKeySetupUrl, sessionTokenSetupUrl, type AwsAuthOptions, } from './validation.ts' function secretEntries( result: unknown, ): Array<{ name?: string; scope?: string }> { if ( result && typeof result === 'object' && Array.isArray((result as { secrets?: unknown }).secrets) ) { return (result as { secrets: Array<{ name?: string; scope?: string }> }) .secrets } return Array.isArray(result) ? result : [] } async function listUserSecretNames(): Promise> { try { const listed = await kody.secretList({ scope: 'user' }) const names = new Set() for (const entry of secretEntries(listed)) { if (entry?.name && (entry.scope === 'user' || !entry.scope)) { names.add(entry.name) } } return names } catch { return new Set() } } /** * Local helper checks, plus a live STS identity read when both required * secrets exist and mounts resolve. * * Without secrets this still returns `{ ok: true, live: false }` and the * prefilled setup URLs so the package is forkable. It never dumps object * bodies or secret values. * * @example * import smokeTest from 'kody:@kody/aws/smoke-test' * const result = await smokeTest() */ export async function smokeTest(params: AwsAuthOptions = {}) { const previewPut = await putObject({ ...params, bucket: 'example-bucket', key: 'kody-aws-dry-run.txt', body: 'dry-run', dryRun: true, }) const previewDelete = await deleteObject({ ...params, bucket: 'example-bucket', key: 'kody-aws-dry-run.txt', dryRun: true, }) if (!('dryRun' in previewPut) || previewPut.dryRun !== true) { throw new Error('putObject dry-run self-check failed.') } if (!('dryRun' in previewDelete) || previewDelete.dryRun !== true) { throw new Error('deleteObject dry-run self-check failed.') } const accessKeySecret = resolveAccessKeySecretName(params) const secretKeySecret = resolveSecretKeySecretName(params) const sessionTokenSecret = resolveSessionTokenSecretName(params) const names = await listUserSecretNames() const hasAccessKey = names.has(accessKeySecret) const hasSecretKey = names.has(secretKeySecret) const setup = { secrets: { [accessKeySecret]: hasAccessKey, [secretKeySecret]: hasSecretKey, [sessionTokenSecret]: names.has(sessionTokenSecret), }, nextSteps: [ 'Save the AWS access key id. Do not paste it into chat.', accessKeySetupUrl(accessKeySecret), 'Save the AWS secret access key. Do not paste it into chat.', secretKeySetupUrl(secretKeySecret), 'Optional temporary session token:', sessionTokenSetupUrl(sessionTokenSecret), 'Approve the amazonaws.com hosts listed on those forms (add regional hosts as needed).', ], hosts: [...COMMON_AWS_HOSTS], } if (!hasAccessKey || !hasSecretKey) { return { ok: true, live: false, selfCheck: { dryRunPut: true, dryRunDelete: true }, accessKeySecret, secretKeySecret, setup, } } try { const identity = await getCallerIdentity(params) return { ok: true, live: true, selfCheck: { dryRunPut: true, dryRunDelete: true }, accessKeySecret, secretKeySecret, identity: { account: identity.account, arn: identity.arn, userId: identity.userId, region: identity.region, }, setup: { secrets: { [accessKeySecret]: true, [secretKeySecret]: true, [sessionTokenSecret]: names.has(sessionTokenSecret), }, nextSteps: [], hosts: [...COMMON_AWS_HOSTS], }, } } catch (error) { const message = error instanceof Error ? error.message : 'identity failed' return { ok: true, live: false, selfCheck: { dryRunPut: true, dryRunDelete: true }, accessKeySecret, secretKeySecret, identityError: message.includes('not mounted') ? 'Secrets exist but are not mounted in this static-import runtime. Fork or invoke the package runtime for a live STS read.' : message.slice(0, 300), setup, } } } export default smokeTest