n8n/packages/@n8n/instance-ai/evaluations/credentials/seeder.ts
José Braulio González Valido 560f300716
test: Add Instance AI workflow evals CI pipeline (no-changelog) (#28366)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 14:15:41 +00:00

162 lines
4.5 KiB
TypeScript

// ---------------------------------------------------------------------------
// Credential seeding and cleanup for evaluation runs
//
// External service credentials (Slack, GitHub, etc.) require real tokens
// via environment variables. If the env var is not set, the credential
// is skipped. Generic HTTP credentials use placeholder values and are
// always seeded.
//
// POST /rest/credentials takes raw values -- n8n encrypts them server-side.
// ---------------------------------------------------------------------------
import type { N8nClient } from '../clients/n8n-client';
import type { EvalLogger } from '../harness/logger';
// ---------------------------------------------------------------------------
// Config types
// ---------------------------------------------------------------------------
interface CredentialConfig {
envVar: string;
type: string;
name: string;
dataBuilder: (token: string) => Record<string, unknown>;
}
interface GenericCredentialConfig {
type: string;
name: string;
data: Record<string, unknown>;
}
// ---------------------------------------------------------------------------
// Credential definitions
// ---------------------------------------------------------------------------
const CREDENTIAL_CONFIGS: CredentialConfig[] = [
{
envVar: 'EVAL_SLACK_ACCESS_TOKEN',
type: 'slackApi',
name: '[eval] Slack',
dataBuilder: (token) => ({ accessToken: token }),
},
{
envVar: 'EVAL_NOTION_API_KEY',
type: 'notionApi',
name: '[eval] Notion',
dataBuilder: (key) => ({ apiKey: key }),
},
{
envVar: 'EVAL_GITHUB_ACCESS_TOKEN',
type: 'githubApi',
name: '[eval] GitHub',
dataBuilder: (token) => ({ accessToken: token }),
},
{
envVar: 'EVAL_GMAIL_ACCESS_TOKEN',
type: 'gmailOAuth2Api',
name: '[eval] Gmail',
dataBuilder: (token) => ({ oauthTokenData: { access_token: token } }),
},
{
envVar: 'EVAL_TEAMS_ACCESS_TOKEN',
type: 'microsoftTeamsOAuth2Api',
name: '[eval] Teams',
dataBuilder: (token) => ({
scope: 'openid',
oauthTokenData: { access_token: token },
}),
},
];
const GENERIC_CREDENTIALS: GenericCredentialConfig[] = [
{
type: 'httpHeaderAuth',
name: '[eval] HTTP Header',
data: { name: 'Authorization', value: 'Bearer eval-placeholder' },
},
{
type: 'httpBasicAuth',
name: '[eval] HTTP Basic',
data: { user: 'eval-user', password: 'eval-pass' },
},
];
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
export interface SeedResult {
credentialIds: string[];
seededTypes: string[];
}
/**
* Seed credentials into the n8n instance for evaluation runs.
*
* For env-var-based configs, the credential is skipped if the env var is not
* set. Generic credentials (HTTP Header, HTTP Basic) are always seeded.
*
* When `requiredTypes` is provided, only credentials matching those types are
* seeded. When omitted, all available credentials are seeded.
*/
export async function seedCredentials(
client: N8nClient,
requiredTypes?: string[],
logger?: EvalLogger,
): Promise<SeedResult> {
const credentialIds: string[] = [];
const seededTypes: string[] = [];
const typeFilter = requiredTypes ? new Set(requiredTypes) : undefined;
// Seed env-var-based credentials
for (const config of CREDENTIAL_CONFIGS) {
if (typeFilter && !typeFilter.has(config.type)) continue;
const token = process.env[config.envVar];
if (!token) {
logger?.verbose(` Skipping ${config.name}: ${config.envVar} not set`);
continue;
}
try {
const data = config.dataBuilder(token);
const { id } = await client.createCredential(config.name, config.type, data);
credentialIds.push(id);
seededTypes.push(config.type);
} catch {
// Non-fatal -- credential type may not exist on this n8n version
}
}
// Always seed generic credentials
for (const generic of GENERIC_CREDENTIALS) {
if (typeFilter && !typeFilter.has(generic.type)) continue;
try {
const { id } = await client.createCredential(generic.name, generic.type, generic.data);
credentialIds.push(id);
seededTypes.push(generic.type);
} catch {
// Non-fatal
}
}
return { credentialIds, seededTypes };
}
/**
* Best-effort cleanup of seeded credentials after an evaluation run.
*/
export async function cleanupCredentials(
client: N8nClient,
credentialIds: string[],
): Promise<void> {
for (const id of credentialIds) {
try {
await client.deleteCredential(id);
} catch {
// Best-effort cleanup
}
}
}