Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/aws

src/request.ts

101 lines · 2.5 KB · TypeScript
import { awsSignedFetch } from './client.ts'
import { mutationPreview } from './helpers.ts'
import { redactUnknown, looksLikeCredentialText } from './redact.ts'
import {
	assertService,
	inputRecord,
	optionalString,
	requiredString,
	type AwsAuthOptions,
} from './validation.ts'

const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])

/**
 * Escape hatch for a generic SigV4-signed AWS REST call.
 *
 * Writes default to dry-run and require `confirm: true`. Responses never
 * include object bodies that look like credentials.
 *
 * @example
 * import { awsRequest } from 'kody:@kody/aws/request'
 * const result = await awsRequest({
 *   service: 'sts',
 *   method: 'POST',
 *   path: '/',
 *   body: 'Action=GetCallerIdentity&Version=2011-06-15',
 * })
 */
export async function awsRequest(
	params: AwsAuthOptions & {
		service: string
		method?: string
		path?: string
		host?: string
		headers?: Record<string, string>
		query?: Record<string, string>
		body?: string
		dryRun?: boolean
		confirm?: boolean
	},
) {
	const input = inputRecord(params)
	const service = assertService(requiredString(input, 'service'))
	const method = (optionalString(input, 'method') ?? 'GET').toUpperCase()
	const path = optionalString(input, 'path') ?? '/'
	const host = optionalString(input, 'host')
	const body = optionalString(input, 'body')
	const mutating = WRITE_METHODS.has(method)

	if (mutating) {
		const preview = mutationPreview(input, {
			method,
			service,
			region: optionalString(input, 'region') ?? 'us-east-1',
			host: host ?? service + '.amazonaws.com',
			path,
			body: body ? { bodyLength: body.length } : null,
		})
		if (preview) return preview
	}

	const result = await awsSignedFetch({
		...params,
		service,
		method,
		path,
		host,
		headers: (input.headers as Record<string, string> | undefined) ?? {},
		query: (input.query as Record<string, string> | undefined) ?? {},
		body,
	})

	const looksJson = result.text.trim().startsWith('{')
	const looksXml = result.text.trim().startsWith('<')
	let parsed: unknown = null
	if (looksJson) {
		try {
			parsed = redactUnknown(JSON.parse(result.text))
		} catch {
			parsed = null
		}
	}

	const credentialLike = looksLikeCredentialText(result.text)
	return {
		ok: true,
		status: result.status,
		service,
		region: result.region,
		host: result.host,
		method,
		path,
		body:
			credentialLike || (!looksJson && !looksXml)
				? null
				: parsed ?? redactUnknown(result.text.slice(0, 4000)),
		bodyOmitted: credentialLike || (!looksJson && !looksXml && result.text.length > 0),
	}
}

export default awsRequest