Skip to content

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

Package listing

@kody/stripe

src/smoke-test.ts

139 lines · 3.9 KB · TypeScript
import { kody } from 'kody:runtime'
import { getAccount, getBalance } from './account.ts'
import { listCustomers } from './customers.ts'
import { createRefund } from './payments.ts'
import {
	API_KEY_SETUP_URL,
	DEFAULT_API_KEY_SECRET,
	resolveApiKeySecretName,
	secretSetupUrl,
	WEBHOOK_SECRET_SETUP_URL,
	type StripeAuthOptions,
} from './stripe-core.ts'
import { createTestSignatureHeader, verifyWebhookSignature } from './webhooks.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<Set<string>> {
	try {
		const listed = await kody.secret_list({ scope: 'user' })
		const names = new Set<string>()
		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 an optional live account/balance read.
 *
 * Always verifies webhook HMAC helpers with a throwaway test secret. When
 * `stripeApiKey` (or the selected account secret) exists, also reads the
 * Stripe account profile without mutating anything.
 *
 * @example
 * import smokeTest from 'kody:@kody/stripe/smoke-test'
 * const result = await smokeTest()
 */
export async function smokeTest(input: StripeAuthOptions = {}) {
	const payload = JSON.stringify({
		id: 'evt_kody_smoke',
		object: 'event',
		type: 'charge.succeeded',
		livemode: false,
		data: { object: { id: 'ch_kody_smoke', object: 'charge' } },
	})
	const webhookSecret = 'whsec_kody_smoke_test'
	const nowSeconds = 1_700_000_000
	const signatureHeader = await createTestSignatureHeader({
		payload,
		webhookSecret,
		timestamp: nowSeconds,
	})
	const verified = await verifyWebhookSignature({
		payload,
		signatureHeader,
		webhookSecret,
		nowSeconds,
	})
	if (!verified.ok || verified.eventId !== 'evt_kody_smoke') {
		throw new Error('Local Stripe webhook HMAC self-check failed.')
	}

	const refundPreview = await createRefund({
		chargeId: 'ch_kody_smoke',
		amount: 100,
		dryRun: true,
	})
	if (!('dryRun' in refundPreview) || refundPreview.dryRun !== true) {
		throw new Error('createRefund dryRun self-check failed.')
	}

	const secretName = resolveApiKeySecretName(input)
	const names = await listUserSecretNames()
	const hasApiKey = names.has(secretName)
	const hasWebhookSecret = names.has(
		secretName === DEFAULT_API_KEY_SECRET
			? 'stripeWebhookSecret'
			: secretName.replace(/^stripeApiKey/, 'stripeWebhookSecret'),
	)

	if (!hasApiKey) {
		return {
			ok: true,
			live: false,
			selfCheck: { webhookSignature: true, dryRunRefund: true },
			secretName,
			setup: {
				auth: 'api-key',
				hosts: ['api.stripe.com', 'files.stripe.com'],
				nextSteps: [
					'Save a Stripe secret key (sk_...) or restricted key (rk_...) as ' + secretName + '.',
					secretSetupUrl(secretName),
					'Optional webhook signing secret: ' + WEBHOOK_SECRET_SETUP_URL,
				],
			},
		}
	}

	const [account, balance, customers] = await Promise.all([
		getAccount(input),
		getBalance(input),
		listCustomers({ ...input, maxItems: 1 }),
	])

	return {
		ok: true,
		live: true,
		selfCheck: { webhookSignature: true, dryRunRefund: true },
		secretName,
		account: {
			id: account.id,
			displayName: account.displayName,
			country: account.country,
			chargesEnabled: account.chargesEnabled,
			livemode: account.livemode,
		},
		balance: {
			livemode: balance.livemode,
			availableCurrencies: balance.available.map((entry) => entry.currency),
		},
		customerSampleCount: customers.customers.length,
		setup: {
			auth: 'api-key',
			hosts: ['api.stripe.com', 'files.stripe.com'],
			webhookSecretSaved: hasWebhookSecret,
			apiKeyUrl: hasApiKey ? null : API_KEY_SETUP_URL,
		},
	}
}

export default smokeTest