/** * Tests for plugin-declared credential key pattern contributions. * * A plugin may declare `credentialKeyPatterns` in its `package.json` manifest; * the plugin lifecycle registers the declared patterns into the secret-pattern * registry when the plugin activates and removes them on every teardown path * (disable, uninstall, eviction). Assertions read the registry via * {@link getPluginSecretPatterns} — end-to-end detection through the ingress / * scanner / redaction consumers is covered by those consumers' own suites. * * Two lifecycle layers are exercised, mirroring the injector-contribution * suite (`plugin-injector-contribution.test.ts`) and the mtime-cache suite * (`mtime-cache.test.ts`): * * 1. User plugins — synthetic plugin directories driven through boot * discovery and the imperative source-versions reconcile (install, * disable, uninstall applied exactly the way production applies them). * 2. Default/registered plugins — `bootstrapPlugins()` registering the * manifest declaration and rolling it back when `init()` fails. * * All token shapes in this file are synthetic; no real credential formats. */ import { mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll, beforeAll, beforeEach, describe, expect, test, } from "bun:test"; import { bootstrapPlugins } from "../daemon/external-plugins-bootstrap.js"; import { getUserHooksFor } from "../hooks/registry.js"; import { populateCacheAtBoot, reconcilePluginSourcesNow, resetPluginCacheForTests, } from "../plugins/mtime-cache.js"; import { registerPlugin, resetPluginRegistryForTests, } from "../plugins/registry.js"; import { getSourceVersionsPath } from "../plugins/source-versions.js"; import type { PluginCredentialKeyPattern } from "../plugins/types.js"; import { getPluginSecretPatterns } from "../security/plugin-secret-patterns.js"; // ─── Test fixtures ─────────────────────────────────────────────────────────── // realpath so the reconcile's symlink-resolving allowed-roots check matches // on platforms where the tempdir is itself behind a symlink (macOS /var). const ROOT = join( realpathSync(tmpdir()), `vellum-plugin-secret-pattern-contrib-test-${process.pid}-${Date.now()}`, ); const PLUGINS_DIR = join(ROOT, "plugins"); const VIRLO_PATTERN: PluginCredentialKeyPattern = { label: "Virlo API Key", pattern: "virlo_tkn_[A-Za-z0-9_-]{20,}", }; /** Fails the registry's literal-prefix rule (leading `.` is a wildcard). */ const INVALID_PATTERN: PluginCredentialKeyPattern = { label: "Broad Matcher", pattern: ".*secret", }; function ensurePluginsDir(): void { rmSync(PLUGINS_DIR, { recursive: true, force: true }); mkdirSync(PLUGINS_DIR, { recursive: true }); } function writePluginDir( name: string, credentialKeyPatterns: PluginCredentialKeyPattern[], ): string { const dir = join(PLUGINS_DIR, name); rmSync(dir, { recursive: true, force: true }); mkdirSync(dir, { recursive: true }); writeFileSync( join(dir, "package.json"), JSON.stringify( { name, version: "1.0.0", peerDependencies: { "@vellumai/plugin-api": "*" }, credentialKeyPatterns, }, null, 2, ), ); return dir; } function writeHook(dir: string, hookName: string, body: string): void { const hooksDir = join(dir, "hooks"); mkdirSync(hooksDir, { recursive: true }); writeFileSync(join(hooksDir, `${hookName}.ts`), body); } /** Registered-pattern labels are namespaced as `