diff --git a/docs/reference/secretref-user-supplied-credentials-matrix.json b/docs/reference/secretref-user-supplied-credentials-matrix.json index f8b6d27115e..40449f76ae8 100644 --- a/docs/reference/secretref-user-supplied-credentials-matrix.json +++ b/docs/reference/secretref-user-supplied-credentials-matrix.json @@ -9,10 +9,10 @@ "hooks.gmail.pushToken", "hooks.mappings[].sessionKey", "auth-profiles.oauth.*", - "channels.discord.threadBindings.webhookToken", "channels.discord.accounts.*.threadBindings.webhookToken", - "channels.whatsapp.creds.json", - "channels.whatsapp.accounts.*.creds.json" + "channels.discord.threadBindings.webhookToken", + "channels.whatsapp.accounts.*.creds.json", + "channels.whatsapp.creds.json" ], "entries": [ { diff --git a/scripts/generate-bundled-channel-config-metadata.ts b/scripts/generate-bundled-channel-config-metadata.ts index 37ba1468e71..64cdc4cb9df 100644 --- a/scripts/generate-bundled-channel-config-metadata.ts +++ b/scripts/generate-bundled-channel-config-metadata.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; +import { loadBundledPluginPublicArtifactModuleSync } from "../src/plugins/public-surface-loader.js"; import { loadChannelConfigSurfaceModule } from "./load-channel-config-surface.ts"; const GENERATED_BY = "scripts/generate-bundled-channel-config-metadata.ts"; @@ -63,6 +64,11 @@ type BundledChannelConfigMetadata = { description?: string; schema: Record; uiHints?: Record; + unsupportedSecretRefSurfacePatterns?: readonly string[]; +}; + +type BundledChannelSecuritySurface = { + unsupportedSecretRefSurfacePatterns?: readonly string[]; }; function resolveChannelConfigSchemaModulePath(rootDir: string): string | null { @@ -131,6 +137,34 @@ function formatTypeScriptModule(source: string, outputPath: string, repoRoot: st }); } +function resolveChannelUnsupportedSecretRefSurfacePatterns( + source: BundledPluginSource, + channelId: string, +): string[] { + try { + const surface = loadBundledPluginPublicArtifactModuleSync({ + dirName: source.dirName, + artifactBasename: "security-contract-api.js", + }); + const prefix = `channels.${channelId}.`; + return [ + ...new Set( + (surface.unsupportedSecretRefSurfacePatterns ?? []).filter( + (pattern): pattern is string => typeof pattern === "string" && pattern.startsWith(prefix), + ), + ), + ].toSorted((left, right) => left.localeCompare(right)); + } catch (error) { + if ( + error instanceof Error && + error.message.startsWith("Unable to resolve bundled plugin public surface ") + ) { + return []; + } + throw error; + } +} + export async function collectBundledChannelConfigMetadata(params?: { repoRoot?: string }) { const repoRoot = path.resolve(params?.repoRoot ?? process.cwd()); const sources = collectBundledPluginSources({ repoRoot, requirePackageJson: true }); @@ -156,6 +190,10 @@ export async function collectBundledChannelConfigMetadata(params?: { repoRoot?: for (const channelId of channelIds) { const label = resolveRootLabel(source, channelId); const description = resolveRootDescription(source, channelId); + const unsupportedSecretRefSurfacePatterns = resolveChannelUnsupportedSecretRefSurfacePatterns( + source, + channelId, + ); entries.push({ pluginId: source.manifest.id, channelId, @@ -163,6 +201,9 @@ export async function collectBundledChannelConfigMetadata(params?: { repoRoot?: ...(description ? { description } : {}), schema: surface.schema, ...(Object.keys(surface.uiHints ?? {}).length > 0 ? { uiHints: surface.uiHints } : {}), + ...(unsupportedSecretRefSurfacePatterns.length > 0 + ? { unsupportedSecretRefSurfacePatterns } + : {}), }); } } diff --git a/src/config/bundled-channel-config-metadata.generated.ts b/src/config/bundled-channel-config-metadata.generated.ts index bad407fa1ab..2ac3ef1cb63 100644 --- a/src/config/bundled-channel-config-metadata.generated.ts +++ b/src/config/bundled-channel-config-metadata.generated.ts @@ -3129,6 +3129,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [ sensitive: true, }, }, + unsupportedSecretRefSurfacePatterns: [ + "channels.discord.accounts.*.threadBindings.webhookToken", + "channels.discord.threadBindings.webhookToken", + ], }, { pluginId: "feishu", @@ -15432,6 +15436,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [ help: "Allow WhatsApp to write config in response to channel events/commands (default: true).", }, }, + unsupportedSecretRefSurfacePatterns: [ + "channels.whatsapp.accounts.*.creds.json", + "channels.whatsapp.creds.json", + ], }, { pluginId: "zalo", diff --git a/src/secrets/unsupported-surface-policy.test.ts b/src/secrets/unsupported-surface-policy.test.ts index 2216ad07f27..47019137d80 100644 --- a/src/secrets/unsupported-surface-policy.test.ts +++ b/src/secrets/unsupported-surface-policy.test.ts @@ -1,88 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; - -const { loadBundledChannelSecurityContractApiMock, loadPluginManifestRegistryMock } = vi.hoisted( - () => ({ - loadBundledChannelSecurityContractApiMock: vi.fn((channelId: string) => { - if (channelId === "discord") { - return { - unsupportedSecretRefSurfacePatterns: [ - "channels.discord.threadBindings.webhookToken", - "channels.discord.accounts.*.threadBindings.webhookToken", - ], - collectUnsupportedSecretRefConfigCandidates: (raw: Record) => { - const discord = (raw.channels as Record | undefined)?.discord as - | Record - | undefined; - const candidates: Array<{ path: string; value: unknown }> = []; - const threadBindings = discord?.threadBindings as Record | undefined; - candidates.push({ - path: "channels.discord.threadBindings.webhookToken", - value: threadBindings?.webhookToken, - }); - const accounts = discord?.accounts as Record | undefined; - for (const [accountId, account] of Object.entries(accounts ?? {})) { - const accountThreadBindings = (account as Record).threadBindings as - | Record - | undefined; - candidates.push({ - path: `channels.discord.accounts.${accountId}.threadBindings.webhookToken`, - value: accountThreadBindings?.webhookToken, - }); - } - return candidates; - }, - }; - } - if (channelId === "whatsapp") { - return { - unsupportedSecretRefSurfacePatterns: [ - "channels.whatsapp.creds.json", - "channels.whatsapp.accounts.*.creds.json", - ], - collectUnsupportedSecretRefConfigCandidates: (raw: Record) => { - const whatsapp = (raw.channels as Record | undefined)?.whatsapp as - | Record - | undefined; - const candidates: Array<{ path: string; value: unknown }> = []; - const creds = whatsapp?.creds as Record | undefined; - candidates.push({ - path: "channels.whatsapp.creds.json", - value: creds?.json, - }); - const accounts = whatsapp?.accounts as Record | undefined; - for (const [accountId, account] of Object.entries(accounts ?? {})) { - const accountCreds = (account as Record).creds as - | Record - | undefined; - candidates.push({ - path: `channels.whatsapp.accounts.${accountId}.creds.json`, - value: accountCreds?.json, - }); - } - return candidates; - }, - }; - } - return undefined; - }), - loadPluginManifestRegistryMock: vi.fn(() => ({ - plugins: [ - { id: "discord", origin: "bundled", channels: ["discord"] }, - { id: "whatsapp", origin: "bundled", channels: ["whatsapp"] }, - ], - diagnostics: [], - })), - }), -); - -vi.mock("../plugins/manifest-registry.js", () => ({ - loadPluginManifestRegistry: loadPluginManifestRegistryMock, -})); - -vi.mock("./channel-contract-api.js", () => ({ - loadBundledChannelSecurityContractApi: loadBundledChannelSecurityContractApiMock, -})); - +import { describe, expect, it } from "vitest"; import { collectUnsupportedSecretRefConfigCandidates, getUnsupportedSecretRefSurfacePatterns, @@ -90,17 +6,19 @@ import { describe("unsupported SecretRef surface policy metadata", () => { it("exposes the canonical unsupported surface patterns", () => { - expect(getUnsupportedSecretRefSurfacePatterns()).toEqual([ - "commands.ownerDisplaySecret", - "hooks.token", - "hooks.gmail.pushToken", - "hooks.mappings[].sessionKey", - "auth-profiles.oauth.*", - "channels.discord.threadBindings.webhookToken", - "channels.discord.accounts.*.threadBindings.webhookToken", - "channels.whatsapp.creds.json", - "channels.whatsapp.accounts.*.creds.json", - ]); + expect(getUnsupportedSecretRefSurfacePatterns().toSorted()).toEqual( + [ + "commands.ownerDisplaySecret", + "hooks.token", + "hooks.gmail.pushToken", + "hooks.mappings[].sessionKey", + "auth-profiles.oauth.*", + "channels.discord.threadBindings.webhookToken", + "channels.discord.accounts.*.threadBindings.webhookToken", + "channels.whatsapp.creds.json", + "channels.whatsapp.accounts.*.creds.json", + ].toSorted(), + ); }); it("discovers concrete config candidates for unsupported mutable surfaces", () => { diff --git a/src/secrets/unsupported-surface-policy.ts b/src/secrets/unsupported-surface-policy.ts index 358653f46ef..ef540df0009 100644 --- a/src/secrets/unsupported-surface-policy.ts +++ b/src/secrets/unsupported-surface-policy.ts @@ -1,6 +1,5 @@ -import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js"; +import { GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA } from "../config/bundled-channel-config-metadata.generated.js"; import { isRecord } from "../utils.js"; -import { loadBundledChannelSecurityContractApi } from "./channel-contract-api.js"; const CORE_UNSUPPORTED_SECRETREF_SURFACE_PATTERNS = [ "commands.ownerDisplaySecret", @@ -10,33 +9,149 @@ const CORE_UNSUPPORTED_SECRETREF_SURFACE_PATTERNS = [ "auth-profiles.oauth.*", ] as const; -function listBundledChannelIds(): string[] { - return [ - ...new Set( - loadPluginManifestRegistry({}) - .plugins.filter((entry) => entry.origin === "bundled") - .flatMap((entry) => entry.channels), +const CORE_UNSUPPORTED_SECRETREF_CONFIG_CANDIDATE_PATTERNS = [ + "commands.ownerDisplaySecret", + "hooks.token", + "hooks.gmail.pushToken", + "hooks.mappings[].sessionKey", +] as const; + +type PatternToken = + | { kind: "key"; key: string } + | { kind: "array"; key: string } + | { kind: "wildcard" }; + +const bundledChannelUnsupportedSecretRefSurfacePatterns = [ + ...new Set( + GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA.flatMap((entry) => + "unsupportedSecretRefSurfacePatterns" in entry + ? (entry.unsupportedSecretRefSurfacePatterns ?? []) + : [], ), - ].toSorted((left, right) => left.localeCompare(right)); -} + ), +]; -function collectChannelUnsupportedSecretRefSurfacePatterns(): string[] { - const patterns: string[] = []; - for (const channelId of listBundledChannelIds()) { - const contract = loadBundledChannelSecurityContractApi(channelId); - patterns.push(...(contract?.unsupportedSecretRefSurfacePatterns ?? [])); +const unsupportedSecretRefSurfacePatterns = [ + ...CORE_UNSUPPORTED_SECRETREF_SURFACE_PATTERNS, + ...bundledChannelUnsupportedSecretRefSurfacePatterns, +]; + +const unsupportedSecretRefConfigCandidatePatterns = [ + ...CORE_UNSUPPORTED_SECRETREF_CONFIG_CANDIDATE_PATTERNS, + ...bundledChannelUnsupportedSecretRefSurfacePatterns, +]; + +const parsedPatternCache = new Map(); + +function parseUnsupportedSecretRefSurfacePattern(pattern: string): PatternToken[] { + const cached = parsedPatternCache.get(pattern); + if (cached) { + return cached; } - return patterns; + const parsed = pattern + .split(".") + .filter((segment) => segment.length > 0) + .map((segment) => { + if (segment === "*") { + return { kind: "wildcard" }; + } + if (segment.endsWith("[]")) { + return { + kind: "array", + key: segment.slice(0, -2), + }; + } + return { + kind: "key", + key: segment, + }; + }); + parsedPatternCache.set(pattern, parsed); + return parsed; } -let cachedUnsupportedSecretRefSurfacePatterns: string[] | null = null; +function collectPatternCandidates(params: { + current: unknown; + tokens: readonly PatternToken[]; + tokenIndex: number; + pathSegments: string[]; + candidates: UnsupportedSecretRefConfigCandidate[]; +}): void { + if (params.tokenIndex >= params.tokens.length) { + params.candidates.push({ + path: params.pathSegments.join("."), + value: params.current, + }); + return; + } + + const token = params.tokens[params.tokenIndex]; + if (!token) { + return; + } + + if (token.kind === "wildcard") { + if (Array.isArray(params.current)) { + for (const [index, value] of params.current.entries()) { + collectPatternCandidates({ + ...params, + current: value, + tokenIndex: params.tokenIndex + 1, + pathSegments: [...params.pathSegments, String(index)], + }); + } + return; + } + if (!isRecord(params.current)) { + return; + } + for (const [key, value] of Object.entries(params.current)) { + collectPatternCandidates({ + ...params, + current: value, + tokenIndex: params.tokenIndex + 1, + pathSegments: [...params.pathSegments, key], + }); + } + return; + } + + if (!isRecord(params.current)) { + return; + } + + if (token.kind === "array") { + if (!Object.hasOwn(params.current, token.key)) { + return; + } + const value = params.current[token.key]; + if (!Array.isArray(value)) { + return; + } + for (const [index, entry] of value.entries()) { + collectPatternCandidates({ + ...params, + current: entry, + tokenIndex: params.tokenIndex + 1, + pathSegments: [...params.pathSegments, token.key, String(index)], + }); + } + return; + } + + if (!Object.hasOwn(params.current, token.key)) { + return; + } + collectPatternCandidates({ + ...params, + current: params.current[token.key], + tokenIndex: params.tokenIndex + 1, + pathSegments: [...params.pathSegments, token.key], + }); +} export function getUnsupportedSecretRefSurfacePatterns(): string[] { - cachedUnsupportedSecretRefSurfacePatterns ??= [ - ...CORE_UNSUPPORTED_SECRETREF_SURFACE_PATTERNS, - ...collectChannelUnsupportedSecretRefSurfacePatterns(), - ]; - return cachedUnsupportedSecretRefSurfacePatterns; + return [...unsupportedSecretRefSurfacePatterns]; } export type UnsupportedSecretRefConfigCandidate = { @@ -52,51 +167,14 @@ export function collectUnsupportedSecretRefConfigCandidates( } const candidates: UnsupportedSecretRefConfigCandidate[] = []; - - const commands = isRecord(raw.commands) ? raw.commands : null; - if (commands) { - candidates.push({ - path: "commands.ownerDisplaySecret", - value: commands.ownerDisplaySecret, + for (const pattern of unsupportedSecretRefConfigCandidatePatterns) { + collectPatternCandidates({ + current: raw, + tokens: parseUnsupportedSecretRefSurfacePattern(pattern), + tokenIndex: 0, + pathSegments: [], + candidates, }); } - - const hooks = isRecord(raw.hooks) ? raw.hooks : null; - if (hooks) { - candidates.push({ path: "hooks.token", value: hooks.token }); - - const gmail = isRecord(hooks.gmail) ? hooks.gmail : null; - if (gmail) { - candidates.push({ - path: "hooks.gmail.pushToken", - value: gmail.pushToken, - }); - } - - const mappings = hooks.mappings; - if (Array.isArray(mappings)) { - for (const [index, mapping] of mappings.entries()) { - if (!isRecord(mapping)) { - continue; - } - candidates.push({ - path: `hooks.mappings.${index}.sessionKey`, - value: mapping.sessionKey, - }); - } - } - } - - if (isRecord(raw.channels)) { - for (const channelId of Object.keys(raw.channels)) { - const contract = loadBundledChannelSecurityContractApi(channelId); - const channelCandidates = contract?.collectUnsupportedSecretRefConfigCandidates?.(raw); - if (!channelCandidates?.length) { - continue; - } - candidates.push(...channelCandidates); - } - } - return candidates; }