import { redactUnknown } from './redact.ts'
import {
accessKeySetupUrl,
isDryRun,
resolveAccessKeySecretName,
resolveSecretKeySecretName,
secretKeySetupUrl,
type AwsAuthOptions,
} from './validation.ts'
export class AwsApiError extends Error {
readonly status: number
readonly code: string | null
readonly nextStep: string
readonly details: Record<string, unknown>
constructor(
status: number,
details: Record<string, unknown>,
nextStep: string,
) {
const code = typeof details.code === 'string' ? details.code : null
const message =
typeof details.message === 'string' && details.message.length > 0
? details.message
: 'AWS API request failed'
super(
'AWS API failed (' +
status +
(code ? '/' + code : '') +
'): ' +
message +
'. Next step: ' +
nextStep,
)
this.name = 'AwsApiError'
this.status = status
this.code = code
this.nextStep = nextStep
this.details = details
}
}
export function nextStepFor(
status: number,
message: string,
accessKeySecret: string,
secretKeySecret: string,
host: string,
): string {
const accessUrl = accessKeySetupUrl(accessKeySecret)
const secretUrl = secretKeySetupUrl(secretKeySecret)
const lower = message.toLowerCase()
if (
status === 401 ||
status === 403 ||
lower.includes('invalidclienttokenid') ||
lower.includes('signaturedoesnotmatch') ||
lower.includes('incomplete') ||
lower.includes('expired')
) {
return (
'Save ' +
accessKeySecret +
' and ' +
secretKeySecret +
' (optional awsSessionToken for temporary credentials) and approve host ' +
host +
' plus the regional service hosts you call. Access key: ' +
accessUrl +
' Secret key: ' +
secretUrl
)
}
if (status === 404) {
return (
'AWS returned 404. Confirm the region, bucket, and resource exist, and that host ' +
host +
' is approved for both secrets.'
)
}
return (
'Confirm both secrets are saved and host ' +
host +
' is approved. Access key: ' +
accessUrl +
' Secret key: ' +
secretUrl
)
}
export type MutationPreview = {
dryRun: true
method: string
service: string
region: string
host: string
path: string
body: Record<string, unknown> | null
accessKeySecret: string
secretKeySecret: string
}
export function mutationPreview(
input: AwsAuthOptions & { dryRun?: unknown; confirm?: unknown },
spec: {
method: string
service: string
region: string
host: string
path: string
body?: Record<string, unknown> | null
},
): MutationPreview | null {
if (!isDryRun(input)) return null
return {
dryRun: true,
method: spec.method,
service: spec.service,
region: spec.region,
host: spec.host,
path: spec.path,
body: spec.body ?? null,
accessKeySecret: resolveAccessKeySecretName(input),
secretKeySecret: resolveSecretKeySecretName(input),
}
}
export function slimErrorBody(text: string): Record<string, unknown> {
const redacted = redactUnknown(text)
if (typeof redacted !== 'string') return { message: 'AWS request failed' }
const codeMatch = redacted.match(/<Code>([\s\S]*?)<\/Code>/i)
const messageMatch = redacted.match(/<Message>([\s\S]*?)<\/Message>/i)
try {
const parsed = JSON.parse(redacted) as Record<string, unknown>
return {
code:
typeof parsed.__type === 'string'
? parsed.__type
: typeof parsed.code === 'string'
? parsed.code
: null,
message:
typeof parsed.message === 'string'
? parsed.message
: redacted.slice(0, 300),
}
} catch {
return {
code: codeMatch?.[1] ?? null,
message: messageMatch?.[1] ?? redacted.slice(0, 300),
}
}
}