import { kody } from 'kody:runtime'
import { getAccounts } from './accounts.ts'
import { listInstitutions } from './institutions.ts'
import { getItem } from './item.ts'
import { createLinkToken } from './link.ts'
import {
ACCESS_TOKEN_SETUP_URL,
CLIENT_ID_SETUP_URL,
DASHBOARD_KEYS_URL,
PRODUCTION_API_HOST,
SANDBOX_API_HOST,
SECRET_SETUP_URL,
accessTokenSetupUrl,
clientIdSetupUrl,
resolveAccessTokenSecretName,
resolveClientIdSecretName,
resolveSecretSecretName,
secretSetupUrl,
type PlaidAuthInput,
} from './plaid-core.ts'
import { createSandboxPublicToken } from './sandbox.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 dry-run self-check plus an optional live institutions read.
*
* Without saved client credentials this still returns `{ ok: true, live: false }`
* and the prefilled setup URLs. It never creates Items, never asks for a bank
* password, and never writes Plaid state.
*
* @example
* import plaid from 'kody:@kody/plaid'
* const result = await plaid({ action: 'smoke-test' })
*/
export async function smokeTest(input: PlaidAuthInput = {}) {
const linkPreview = await createLinkToken({
...input,
clientName: 'Kody',
clientUserId: 'kody-plaid-smoke',
dryRun: true,
})
if (!('dryRun' in linkPreview) || linkPreview.dryRun !== true) {
throw new Error('createLinkToken dryRun self-check failed.')
}
const sandboxPreview = await createSandboxPublicToken({
...input,
dryRun: true,
})
if (!('dryRun' in sandboxPreview) || sandboxPreview.dryRun !== true) {
throw new Error('createSandboxPublicToken dryRun self-check failed.')
}
const clientIdSecret = resolveClientIdSecretName(input)
const secretSecret = resolveSecretSecretName(input)
const accessTokenSecret = resolveAccessTokenSecretName(input)
const names = await listUserSecretNames()
const hasClientId = names.has(clientIdSecret)
const hasSecret = names.has(secretSecret)
const hasAccessToken = names.has(accessTokenSecret)
if (!hasClientId || !hasSecret) {
return {
ok: true,
live: false,
selfCheck: { dryRunCreateLinkToken: true, dryRunCreateSandboxPublicToken: true },
clientIdSecret,
secretSecret,
accessTokenSecret,
setup: {
auth: 'client-id-secret',
hosts: [SANDBOX_API_HOST, PRODUCTION_API_HOST],
dashboard: DASHBOARD_KEYS_URL,
nextSteps: [
'Create a Plaid account and copy the sandbox client_id and secret from ' +
DASHBOARD_KEYS_URL +
' (dashboard fields PLAID_CLIENT_ID / PLAID_SECRET).',
'Save the client id: ' + clientIdSetupUrl(clientIdSecret),
'Save the secret: ' + secretSetupUrl(secretSecret),
'Approve hosts ' + SANDBOX_API_HOST + ' and ' + PRODUCTION_API_HOST + ' on both secrets.',
'Optional Item access_token (not a bank password): ' + accessTokenSetupUrl(accessTokenSecret),
'Never paste a bank username or password into chat.',
],
},
}
}
const institutions = await listInstitutions({ ...input, count: 3 })
const itemRead = hasAccessToken
? await getItem(input)
.then((item) =>
getAccounts(input).then((accounts) => ({
item: item.item,
accountCount: accounts.accounts.length,
accounts: accounts.accounts,
})),
)
.catch((error: unknown) => ({
skipped: true,
reason: error instanceof Error ? error.message : String(error),
}))
: {
skipped: true,
reason: 'Save an Item access_token as ' + accessTokenSecret + ' to exercise item/account reads.',
}
return {
ok: true,
live: true,
selfCheck: { dryRunCreateLinkToken: true, dryRunCreateSandboxPublicToken: true },
clientIdSecret,
institutionSample: {
total: institutions.total,
sampleCount: institutions.institutions.length,
sample: institutions.institutions,
},
itemRead,
setup: {
auth: 'client-id-secret',
hosts: [SANDBOX_API_HOST, PRODUCTION_API_HOST],
clientIdUrl: hasClientId ? null : CLIENT_ID_SETUP_URL,
secretUrl: hasSecret ? null : SECRET_SETUP_URL,
accessTokenUrl: hasAccessToken ? null : ACCESS_TOKEN_SETUP_URL,
},
}
}
export default smokeTest