From 8e2ecd053fe86c0745b5f7bc77c6b608770a5ca4 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 6 Apr 2026 17:58:23 +0100 Subject: [PATCH] fix(secrets): restore source-mode contract loading --- extensions/bluebubbles/contract-api.ts | 4 + extensions/irc/contract-api.ts | 4 + extensions/msteams/contract-api.ts | 4 + extensions/nextcloud-talk/contract-api.ts | 4 + extensions/slack/secret-contract-api.ts | 4 + extensions/whatsapp/contract-api.ts | 54 +----------- extensions/whatsapp/security-contract-api.ts | 4 + extensions/whatsapp/src/security-contract.ts | 49 +++++++++++ src/secrets/channel-contract-api.ts | 82 +++++++++++++++++++ ...runtime-config-collectors-channels.test.ts | 73 +++++++++++++++++ .../runtime-config-collectors-channels.ts | 11 +-- src/secrets/runtime.coverage.test.ts | 48 ++++++++--- src/secrets/target-registry-data.ts | 15 ++-- src/secrets/target-registry.docs.test.ts | 37 ++------- src/secrets/unsupported-surface-policy.ts | 33 ++++++-- 15 files changed, 315 insertions(+), 111 deletions(-) create mode 100644 extensions/bluebubbles/contract-api.ts create mode 100644 extensions/irc/contract-api.ts create mode 100644 extensions/msteams/contract-api.ts create mode 100644 extensions/nextcloud-talk/contract-api.ts create mode 100644 extensions/slack/secret-contract-api.ts create mode 100644 extensions/whatsapp/security-contract-api.ts create mode 100644 extensions/whatsapp/src/security-contract.ts create mode 100644 src/secrets/channel-contract-api.ts create mode 100644 src/secrets/runtime-config-collectors-channels.test.ts diff --git a/extensions/bluebubbles/contract-api.ts b/extensions/bluebubbles/contract-api.ts new file mode 100644 index 00000000000..bc8f64f050f --- /dev/null +++ b/extensions/bluebubbles/contract-api.ts @@ -0,0 +1,4 @@ +export { + collectRuntimeConfigAssignments, + secretTargetRegistryEntries, +} from "./src/secret-contract.js"; diff --git a/extensions/irc/contract-api.ts b/extensions/irc/contract-api.ts new file mode 100644 index 00000000000..bc8f64f050f --- /dev/null +++ b/extensions/irc/contract-api.ts @@ -0,0 +1,4 @@ +export { + collectRuntimeConfigAssignments, + secretTargetRegistryEntries, +} from "./src/secret-contract.js"; diff --git a/extensions/msteams/contract-api.ts b/extensions/msteams/contract-api.ts new file mode 100644 index 00000000000..bc8f64f050f --- /dev/null +++ b/extensions/msteams/contract-api.ts @@ -0,0 +1,4 @@ +export { + collectRuntimeConfigAssignments, + secretTargetRegistryEntries, +} from "./src/secret-contract.js"; diff --git a/extensions/nextcloud-talk/contract-api.ts b/extensions/nextcloud-talk/contract-api.ts new file mode 100644 index 00000000000..bc8f64f050f --- /dev/null +++ b/extensions/nextcloud-talk/contract-api.ts @@ -0,0 +1,4 @@ +export { + collectRuntimeConfigAssignments, + secretTargetRegistryEntries, +} from "./src/secret-contract.js"; diff --git a/extensions/slack/secret-contract-api.ts b/extensions/slack/secret-contract-api.ts new file mode 100644 index 00000000000..bc8f64f050f --- /dev/null +++ b/extensions/slack/secret-contract-api.ts @@ -0,0 +1,4 @@ +export { + collectRuntimeConfigAssignments, + secretTargetRegistryEntries, +} from "./src/secret-contract.js"; diff --git a/extensions/whatsapp/contract-api.ts b/extensions/whatsapp/contract-api.ts index 01c9a4ffc9f..2e0138d49cf 100644 --- a/extensions/whatsapp/contract-api.ts +++ b/extensions/whatsapp/contract-api.ts @@ -1,17 +1,3 @@ -type UnsupportedSecretRefConfigCandidate = { - path: string; - value: unknown; -}; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} - -export const unsupportedSecretRefSurfacePatterns = [ - "channels.whatsapp.creds.json", - "channels.whatsapp.accounts.*.creds.json", -] as const; - import { whatsappCommandPolicy as whatsappCommandPolicyImpl } from "./src/command-policy.js"; import { resolveLegacyGroupSessionKey as resolveLegacyGroupSessionKeyImpl } from "./src/group-session-contract.js"; import { __testing as whatsappAccessControlTestingImpl } from "./src/inbound/access-control.js"; @@ -28,6 +14,10 @@ import { canonicalizeLegacySessionKey as canonicalizeLegacySessionKeyImpl, isLegacyGroupSessionKey as isLegacyGroupSessionKeyImpl, } from "./src/session-contract.js"; +export { + collectUnsupportedSecretRefConfigCandidates, + unsupportedSecretRefSurfacePatterns, +} from "./src/security-contract.js"; export const canonicalizeLegacySessionKey = canonicalizeLegacySessionKeyImpl; export const createWhatsAppPollFixture = createWhatsAppPollFixtureImpl; @@ -39,39 +29,3 @@ export const resolveLegacyGroupSessionKey = resolveLegacyGroupSessionKeyImpl; export const resolveWhatsAppRuntimeGroupPolicy = resolveWhatsAppRuntimeGroupPolicyImpl; export const whatsappAccessControlTesting = whatsappAccessControlTestingImpl; export const whatsappCommandPolicy = whatsappCommandPolicyImpl; - -export function collectUnsupportedSecretRefConfigCandidates( - raw: unknown, -): UnsupportedSecretRefConfigCandidate[] { - if (!isRecord(raw)) { - return []; - } - if (!isRecord(raw.channels) || !isRecord(raw.channels.whatsapp)) { - return []; - } - - const candidates: UnsupportedSecretRefConfigCandidate[] = []; - const whatsapp = raw.channels.whatsapp; - const creds = isRecord(whatsapp.creds) ? whatsapp.creds : null; - if (creds) { - candidates.push({ - path: "channels.whatsapp.creds.json", - value: creds.json, - }); - } - - const accounts = isRecord(whatsapp.accounts) ? whatsapp.accounts : null; - if (!accounts) { - return candidates; - } - for (const [accountId, account] of Object.entries(accounts)) { - if (!isRecord(account) || !isRecord(account.creds)) { - continue; - } - candidates.push({ - path: `channels.whatsapp.accounts.${accountId}.creds.json`, - value: account.creds.json, - }); - } - return candidates; -} diff --git a/extensions/whatsapp/security-contract-api.ts b/extensions/whatsapp/security-contract-api.ts new file mode 100644 index 00000000000..be23026c5a6 --- /dev/null +++ b/extensions/whatsapp/security-contract-api.ts @@ -0,0 +1,4 @@ +export { + collectUnsupportedSecretRefConfigCandidates, + unsupportedSecretRefSurfacePatterns, +} from "./src/security-contract.js"; diff --git a/extensions/whatsapp/src/security-contract.ts b/extensions/whatsapp/src/security-contract.ts new file mode 100644 index 00000000000..421fd8d6ac4 --- /dev/null +++ b/extensions/whatsapp/src/security-contract.ts @@ -0,0 +1,49 @@ +type UnsupportedSecretRefConfigCandidate = { + path: string; + value: unknown; +}; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +export const unsupportedSecretRefSurfacePatterns = [ + "channels.whatsapp.creds.json", + "channels.whatsapp.accounts.*.creds.json", +] as const; + +export function collectUnsupportedSecretRefConfigCandidates( + raw: unknown, +): UnsupportedSecretRefConfigCandidate[] { + if (!isRecord(raw)) { + return []; + } + if (!isRecord(raw.channels) || !isRecord(raw.channels.whatsapp)) { + return []; + } + + const candidates: UnsupportedSecretRefConfigCandidate[] = []; + const whatsapp = raw.channels.whatsapp; + const creds = isRecord(whatsapp.creds) ? whatsapp.creds : null; + if (creds) { + candidates.push({ + path: "channels.whatsapp.creds.json", + value: creds.json, + }); + } + + const accounts = isRecord(whatsapp.accounts) ? whatsapp.accounts : null; + if (!accounts) { + return candidates; + } + for (const [accountId, account] of Object.entries(accounts)) { + if (!isRecord(account) || !isRecord(account.creds)) { + continue; + } + candidates.push({ + path: `channels.whatsapp.accounts.${accountId}.creds.json`, + value: account.creds.json, + }); + } + return candidates; +} diff --git a/src/secrets/channel-contract-api.ts b/src/secrets/channel-contract-api.ts new file mode 100644 index 00000000000..a215c2c7d95 --- /dev/null +++ b/src/secrets/channel-contract-api.ts @@ -0,0 +1,82 @@ +import type { OpenClawConfig } from "../config/config.js"; +import { loadBundledPluginPublicSurfaceModuleSync } from "../plugin-sdk/facade-runtime.js"; +import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js"; +import type { ResolverContext, SecretDefaults } from "./runtime-shared.js"; +import type { SecretTargetRegistryEntry } from "./target-registry-types.js"; + +type UnsupportedSecretRefConfigCandidate = { + path: string; + value: unknown; +}; + +type BundledChannelContractApi = { + collectRuntimeConfigAssignments?: (params: { + config: OpenClawConfig; + defaults: SecretDefaults | undefined; + context: ResolverContext; + }) => void; + secretTargetRegistryEntries?: readonly SecretTargetRegistryEntry[]; + unsupportedSecretRefSurfacePatterns?: readonly string[]; + collectUnsupportedSecretRefConfigCandidates?: ( + raw: Record, + ) => UnsupportedSecretRefConfigCandidate[]; +}; + +function loadBundledChannelPublicArtifact( + channelId: string, + artifactBasenames: readonly string[], +): BundledChannelContractApi | undefined { + const metadata = listBundledPluginMetadata({ + includeChannelConfigs: false, + includeSyntheticChannelConfigs: false, + }).find((entry) => entry.manifest.channels?.includes(channelId)); + if (!metadata) { + return undefined; + } + + for (const artifactBasename of artifactBasenames) { + if (!metadata.publicSurfaceArtifacts?.includes(artifactBasename)) { + continue; + } + try { + return loadBundledPluginPublicSurfaceModuleSync({ + dirName: metadata.dirName, + artifactBasename, + }); + } catch (error) { + if (process.env.OPENCLAW_DEBUG_CHANNEL_CONTRACT_API === "1") { + const detail = error instanceof Error ? error.message : String(error); + process.stderr.write( + `[channel-contract-api] failed to load ${channelId} via ${metadata.dirName}/${artifactBasename}: ${detail}\n`, + ); + } + } + } + + return undefined; +} + +export type BundledChannelSecretContractApi = Pick< + BundledChannelContractApi, + "collectRuntimeConfigAssignments" | "secretTargetRegistryEntries" +>; + +export function loadBundledChannelSecretContractApi( + channelId: string, +): BundledChannelSecretContractApi | undefined { + return loadBundledChannelPublicArtifact(channelId, ["secret-contract-api.js", "contract-api.js"]); +} + +export type BundledChannelSecurityContractApi = Pick< + BundledChannelContractApi, + "unsupportedSecretRefSurfacePatterns" | "collectUnsupportedSecretRefConfigCandidates" +>; + +export function loadBundledChannelSecurityContractApi( + channelId: string, +): BundledChannelSecurityContractApi | undefined { + return loadBundledChannelPublicArtifact(channelId, [ + "security-contract-api.js", + "contract-api.js", + ]); +} diff --git a/src/secrets/runtime-config-collectors-channels.test.ts b/src/secrets/runtime-config-collectors-channels.test.ts new file mode 100644 index 00000000000..a44522603e8 --- /dev/null +++ b/src/secrets/runtime-config-collectors-channels.test.ts @@ -0,0 +1,73 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; +import type { ResolverContext } from "./runtime-shared.js"; + +const getBootstrapChannelSecrets = vi.fn(); +const loadBundledChannelSecretContractApi = vi.fn(); + +vi.mock("../channels/plugins/bootstrap-registry.js", () => ({ + getBootstrapChannelSecrets, +})); + +vi.mock("./channel-contract-api.js", () => ({ + loadBundledChannelSecretContractApi, +})); + +describe("runtime channel config collectors", () => { + beforeEach(() => { + getBootstrapChannelSecrets.mockReset(); + loadBundledChannelSecretContractApi.mockReset(); + }); + + it("uses the bundled channel contract-api collector when bootstrap secrets are unavailable", async () => { + const { collectChannelConfigAssignments } = + await import("./runtime-config-collectors-channels.js"); + const collectRuntimeConfigAssignments = vi.fn(); + loadBundledChannelSecretContractApi.mockReturnValue({ + collectRuntimeConfigAssignments, + }); + getBootstrapChannelSecrets.mockReturnValue(undefined); + + collectChannelConfigAssignments({ + config: { + channels: { + bluebubbles: { + accounts: { + ops: {}, + }, + }, + }, + } as OpenClawConfig, + defaults: undefined, + context: {} as ResolverContext, + }); + + expect(loadBundledChannelSecretContractApi).toHaveBeenCalledWith("bluebubbles"); + expect(collectRuntimeConfigAssignments).toHaveBeenCalledOnce(); + expect(getBootstrapChannelSecrets).not.toHaveBeenCalled(); + }); + + it("falls back to bootstrap secrets when no channel contract-api is published", async () => { + const { collectChannelConfigAssignments } = + await import("./runtime-config-collectors-channels.js"); + const collectRuntimeConfigAssignments = vi.fn(); + loadBundledChannelSecretContractApi.mockReturnValue(undefined); + getBootstrapChannelSecrets.mockReturnValue({ + collectRuntimeConfigAssignments, + }); + + collectChannelConfigAssignments({ + config: { + channels: { + legacy: {}, + }, + } as OpenClawConfig, + defaults: undefined, + context: {} as ResolverContext, + }); + + expect(loadBundledChannelSecretContractApi).toHaveBeenCalledWith("legacy"); + expect(getBootstrapChannelSecrets).toHaveBeenCalledWith("legacy"); + expect(collectRuntimeConfigAssignments).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/secrets/runtime-config-collectors-channels.ts b/src/secrets/runtime-config-collectors-channels.ts index faf87ac5240..c0159847c46 100644 --- a/src/secrets/runtime-config-collectors-channels.ts +++ b/src/secrets/runtime-config-collectors-channels.ts @@ -1,5 +1,6 @@ import { getBootstrapChannelSecrets } from "../channels/plugins/bootstrap-registry.js"; import type { OpenClawConfig } from "../config/config.js"; +import { loadBundledChannelSecretContractApi } from "./channel-contract-api.js"; import { type ResolverContext, type SecretDefaults } from "./runtime-shared.js"; export function collectChannelConfigAssignments(params: { @@ -12,10 +13,10 @@ export function collectChannelConfigAssignments(params: { return; } for (const channelId of channelIds) { - const secrets = getBootstrapChannelSecrets(channelId); - if (!secrets) { - continue; - } - secrets.collectRuntimeConfigAssignments?.(params); + const contract = loadBundledChannelSecretContractApi(channelId); + const collectRuntimeConfigAssignments = + contract?.collectRuntimeConfigAssignments ?? + getBootstrapChannelSecrets(channelId)?.collectRuntimeConfigAssignments; + collectRuntimeConfigAssignments?.(params); } } diff --git a/src/secrets/runtime.coverage.test.ts b/src/secrets/runtime.coverage.test.ts index 3fb484a38f7..da8094cb89f 100644 --- a/src/secrets/runtime.coverage.test.ts +++ b/src/secrets/runtime.coverage.test.ts @@ -53,9 +53,13 @@ const COVERAGE_REGISTRY_ENTRIES = loadCoverageRegistryEntries(); const DEBUG_COVERAGE_BATCHES = process.env.OPENCLAW_DEBUG_RUNTIME_COVERAGE === "1"; let applyResolvedAssignments: typeof import("./runtime-shared.js").applyResolvedAssignments; +let clearBootstrapChannelPluginCache: typeof import("../channels/plugins/bootstrap-registry.js").clearBootstrapChannelPluginCache; +let clearBundledPluginMetadataCache: typeof import("../plugins/bundled-plugin-metadata.js").clearBundledPluginMetadataCache; +let clearPluginManifestRegistryCache: typeof import("../plugins/manifest-registry.js").clearPluginManifestRegistryCache; let collectAuthStoreAssignments: typeof import("./runtime-auth-collectors.js").collectAuthStoreAssignments; let collectConfigAssignments: typeof import("./runtime-config-collectors.js").collectConfigAssignments; let createResolverContext: typeof import("./runtime-shared.js").createResolverContext; +let resetFacadeRuntimeStateForTest: typeof import("../plugin-sdk/facade-runtime.js").resetFacadeRuntimeStateForTest; let resolveSecretRefValues: typeof import("./resolve.js").resolveSecretRefValues; let resolveRuntimeWebTools: typeof import("./runtime-web-tools.js").resolveRuntimeWebTools; @@ -134,7 +138,11 @@ function resolveCoverageBatchKey(entry: SecretRegistryEntry): string { if ( field === "accessToken" || field === "password" || - (channelId === "slack" && field === "signingSecret") + (channelId === "slack" && + (field === "appToken" || + field === "botToken" || + field === "signingSecret" || + field === "userToken")) ) { return entry.id; } @@ -419,15 +427,31 @@ async function prepareCoverageSnapshot(params: { describe("secrets runtime target coverage", () => { beforeAll(async () => { - const [sharedRuntime, authCollectors, configCollectors, resolver, webTools] = await Promise.all( - [ - import("./runtime-shared.js"), - import("./runtime-auth-collectors.js"), - import("./runtime-config-collectors.js"), - import("./resolve.js"), - import("./runtime-web-tools.js"), - ], - ); + const [ + bootstrapRegistry, + facadeRuntime, + bundledPluginMetadata, + manifestRegistry, + sharedRuntime, + authCollectors, + configCollectors, + resolver, + webTools, + ] = await Promise.all([ + import("../channels/plugins/bootstrap-registry.js"), + import("../plugin-sdk/facade-runtime.js"), + import("../plugins/bundled-plugin-metadata.js"), + import("../plugins/manifest-registry.js"), + import("./runtime-shared.js"), + import("./runtime-auth-collectors.js"), + import("./runtime-config-collectors.js"), + import("./resolve.js"), + import("./runtime-web-tools.js"), + ]); + ({ clearBootstrapChannelPluginCache } = bootstrapRegistry); + ({ resetFacadeRuntimeStateForTest } = facadeRuntime); + ({ clearBundledPluginMetadataCache } = bundledPluginMetadata); + ({ clearPluginManifestRegistryCache } = manifestRegistry); ({ applyResolvedAssignments, createResolverContext } = sharedRuntime); ({ collectAuthStoreAssignments } = authCollectors); ({ collectConfigAssignments } = configCollectors); @@ -440,6 +464,10 @@ describe("secrets runtime target coverage", () => { (entry) => entry.configFile === "openclaw.json", ); for (const batch of buildCoverageBatches(entries)) { + clearBootstrapChannelPluginCache(); + clearBundledPluginMetadataCache(); + clearPluginManifestRegistryCache(); + resetFacadeRuntimeStateForTest(); logCoverageBatch("openclaw.json", batch); const config = {} as OpenClawConfig; const env: Record = {}; diff --git a/src/secrets/target-registry-data.ts b/src/secrets/target-registry-data.ts index c5e7d83a119..90f7f729db9 100644 --- a/src/secrets/target-registry-data.ts +++ b/src/secrets/target-registry-data.ts @@ -1,6 +1,6 @@ import { iterateBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js"; -import { loadBundledPluginPublicSurfaceModuleSync } from "../plugin-sdk/facade-runtime.js"; import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js"; +import { loadBundledChannelSecretContractApi } from "./channel-contract-api.js"; import type { SecretTargetRegistryEntry } from "./target-registry-types.js"; const SECRET_INPUT_SHAPE = "secret_input"; // pragma: allowlist secret @@ -19,16 +19,13 @@ function listChannelSecretTargetRegistryEntries(): SecretTargetRegistryEntry[] { continue; } if (!metadata.publicSurfaceArtifacts?.includes("contract-api.js")) { - continue; + if (!metadata.publicSurfaceArtifacts?.includes("secret-contract-api.js")) { + continue; + } } try { - const contractApi = loadBundledPluginPublicSurfaceModuleSync<{ - secretTargetRegistryEntries?: readonly SecretTargetRegistryEntry[]; - }>({ - dirName: metadata.dirName, - artifactBasename: "contract-api.js", - }); - entries.push(...(contractApi.secretTargetRegistryEntries ?? [])); + const contractApi = loadBundledChannelSecretContractApi(metadata.manifest.id); + entries.push(...(contractApi?.secretTargetRegistryEntries ?? [])); channelIds.forEach((channelId) => handledChannelIds.add(channelId)); } catch { // Fall back to the full bootstrap plugin surface for channels that do not diff --git a/src/secrets/target-registry.docs.test.ts b/src/secrets/target-registry.docs.test.ts index e2d1dc3786d..1ab785642eb 100644 --- a/src/secrets/target-registry.docs.test.ts +++ b/src/secrets/target-registry.docs.test.ts @@ -1,37 +1,10 @@ -import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import type { SecretRefCredentialMatrixDocument } from "./credential-matrix.js"; - -function buildSecretRefCredentialMatrixInSubprocess(): SecretRefCredentialMatrixDocument { - // Building the matrix pulls in bundled channel registry state. Keep that work out of the - // Vitest worker so this docs-sync check does not pay the full module-graph cost in-process. - const childEnv = { ...process.env }; - delete childEnv.NODE_OPTIONS; - delete childEnv.VITEST; - delete childEnv.VITEST_MODE; - delete childEnv.VITEST_POOL_ID; - delete childEnv.VITEST_WORKER_ID; - - const stdout = execFileSync( - process.execPath, - [ - "--import", - "tsx", - "--input-type=module", - "-e", - 'import { buildSecretRefCredentialMatrix } from "./src/secrets/credential-matrix.ts"; process.stdout.write(JSON.stringify(buildSecretRefCredentialMatrix()));', - ], - { - cwd: process.cwd(), - encoding: "utf8", - env: childEnv, - maxBuffer: 10 * 1024 * 1024, - }, - ); - return JSON.parse(stdout) as SecretRefCredentialMatrixDocument; -} +import { + buildSecretRefCredentialMatrix, + type SecretRefCredentialMatrixDocument, +} from "./credential-matrix.js"; describe("secret target registry docs", () => { it("stays in sync with docs/reference/secretref-user-supplied-credentials-matrix.json", () => { @@ -44,7 +17,7 @@ describe("secret target registry docs", () => { const raw = fs.readFileSync(pathname, "utf8"); const parsed = JSON.parse(raw) as unknown; - expect(parsed).toEqual(buildSecretRefCredentialMatrixInSubprocess()); + expect(parsed).toEqual(buildSecretRefCredentialMatrix()); }); it("stays in sync with docs/reference/secretref-credential-surface.md", () => { diff --git a/src/secrets/unsupported-surface-policy.ts b/src/secrets/unsupported-surface-policy.ts index b0540debc49..7e0716ec34e 100644 --- a/src/secrets/unsupported-surface-policy.ts +++ b/src/secrets/unsupported-surface-policy.ts @@ -1,5 +1,7 @@ -import { iterateBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js"; +import { getBootstrapChannelPlugin } from "../channels/plugins/bootstrap-registry.js"; +import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js"; import { isRecord } from "../utils.js"; +import { loadBundledChannelSecurityContractApi } from "./channel-contract-api.js"; const CORE_UNSUPPORTED_SECRETREF_SURFACE_PATTERNS = [ "commands.ownerDisplaySecret", @@ -9,10 +11,26 @@ const CORE_UNSUPPORTED_SECRETREF_SURFACE_PATTERNS = [ "auth-profiles.oauth.*", ] as const; +function listBundledChannelIds(): string[] { + return [ + ...new Set( + listBundledPluginMetadata({ + includeChannelConfigs: false, + includeSyntheticChannelConfigs: false, + }).flatMap((entry) => entry.manifest.channels ?? []), + ), + ].toSorted((left, right) => left.localeCompare(right)); +} + function collectChannelUnsupportedSecretRefSurfacePatterns(): string[] { const patterns: string[] = []; - for (const plugin of iterateBootstrapChannelPlugins()) { - patterns.push(...(plugin.secrets?.unsupportedSecretRefSurfacePatterns ?? [])); + for (const channelId of listBundledChannelIds()) { + const contract = loadBundledChannelSecurityContractApi(channelId); + patterns.push( + ...(contract?.unsupportedSecretRefSurfacePatterns ?? + getBootstrapChannelPlugin(channelId)?.secrets?.unsupportedSecretRefSurfacePatterns ?? + []), + ); } return patterns; } @@ -76,8 +94,13 @@ export function collectUnsupportedSecretRefConfigCandidates( } if (isRecord(raw.channels)) { - for (const plugin of iterateBootstrapChannelPlugins()) { - const channelCandidates = plugin.secrets?.collectUnsupportedSecretRefConfigCandidates?.(raw); + for (const channelId of Object.keys(raw.channels)) { + const contract = loadBundledChannelSecurityContractApi(channelId); + const channelCandidates = + contract?.collectUnsupportedSecretRefConfigCandidates?.(raw) ?? + getBootstrapChannelPlugin( + channelId, + )?.secrets?.collectUnsupportedSecretRefConfigCandidates?.(raw); if (!channelCandidates?.length) { continue; }