diff --git a/packages/plugins/sdk/src/host-client-factory.ts b/packages/plugins/sdk/src/host-client-factory.ts index 94d0e1fa..ec58bf4e 100644 --- a/packages/plugins/sdk/src/host-client-factory.ts +++ b/packages/plugins/sdk/src/host-client-factory.ts @@ -627,16 +627,8 @@ export function createHostClientHandlers( return { // Config - "config.get": gated("config.get", async (params, context) => { - const scopedCompanyId = readNonEmptyString(context?.invocationScope?.companyId); - const explicitCompanyId = Object.prototype.hasOwnProperty.call(params ?? {}, "companyId") - ? params.companyId ?? null - : undefined; - return services.config.get( - explicitCompanyId === undefined - ? (scopedCompanyId ? { companyId: scopedCompanyId } : {}) - : { companyId: explicitCompanyId }, - ); + "config.get": gated("config.get", async (params) => { + return services.config.get(params); }), "localFolders.declarations": gated("localFolders.declarations", async (params) => { diff --git a/packages/plugins/sdk/tests/worker-rpc-host.test.ts b/packages/plugins/sdk/tests/worker-rpc-host.test.ts index e4f0b2ce..ef1eead9 100644 --- a/packages/plugins/sdk/tests/worker-rpc-host.test.ts +++ b/packages/plugins/sdk/tests/worker-rpc-host.test.ts @@ -514,5 +514,4 @@ describe("startWorkerRpcHost runtime company context", () => { stdout.destroy(); } }); - }); diff --git a/server/src/__tests__/plugin-config-bridge.test.ts b/server/src/__tests__/plugin-config-bridge.test.ts deleted file mode 100644 index 57d0a1e1..00000000 --- a/server/src/__tests__/plugin-config-bridge.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { createHostClientHandlers } from "../../../packages/plugins/sdk/src/host-client-factory.js"; - -describe("plugin config bridge scoping", () => { - it("falls back to the invocation company scope for config.get when the worker omits companyId", async () => { - const getConfig = vi.fn(async (params: { companyId?: string | null }) => ({ - scope: params.companyId ?? null, - })); - - const handlers = createHostClientHandlers({ - pluginId: "test.plugin", - capabilities: [], - services: { - config: { get: getConfig }, - } as never, - }); - - await expect( - handlers["config.get"]({}, { invocationScope: { companyId: "company-a" } }), - ).resolves.toEqual({ scope: "company-a" }); - expect(getConfig).toHaveBeenCalledWith({ companyId: "company-a" }); - }); - - it("preserves an explicit global config.get request even inside a company-scoped invocation", async () => { - const getConfig = vi.fn(async (params: { companyId?: string | null }) => ({ - scope: params.companyId ?? null, - })); - - const handlers = createHostClientHandlers({ - pluginId: "test.plugin", - capabilities: [], - services: { - config: { get: getConfig }, - } as never, - }); - - await expect( - handlers["config.get"]({ companyId: null }, { invocationScope: { companyId: "company-a" } }), - ).resolves.toEqual({ scope: null }); - expect(getConfig).toHaveBeenCalledWith({ companyId: null }); - }); -}); diff --git a/server/src/__tests__/plugin-host-services-config-scope.test.ts b/server/src/__tests__/plugin-host-services-config-scope.test.ts deleted file mode 100644 index 28518b4f..00000000 --- a/server/src/__tests__/plugin-host-services-config-scope.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mocks = vi.hoisted(() => ({ - getConfig: vi.fn(), - getConfigExactScope: vi.fn(), -})); - -vi.mock("../services/plugin-registry.js", () => ({ - pluginRegistryService: () => ({ - getConfig: mocks.getConfig, - getConfigExactScope: mocks.getConfigExactScope, - }), -})); - -import { buildHostServices } from "../services/plugin-host-services.js"; - -function createEventBusStub() { - return { - forPlugin() { - return { - emit: vi.fn(), - subscribe: vi.fn(), - clear: vi.fn(), - }; - }, - } as any; -} - -describe("plugin host services config scoping", () => { - beforeEach(() => { - mocks.getConfig.mockReset(); - mocks.getConfigExactScope.mockReset(); - }); - - it("does not fall back to legacy global config for company-scoped reads", async () => { - mocks.getConfigExactScope.mockResolvedValue(null); - mocks.getConfig.mockResolvedValue({ - configJson: { tokenRef: "global-token-ref" }, - }); - - const services = buildHostServices( - {} as never, - "plugin-record-id", - "paperclip.example", - createEventBusStub(), - ); - - await expect( - services.config.get({ companyId: "company-a" }), - ).resolves.toEqual({}); - expect(mocks.getConfigExactScope).toHaveBeenCalledWith("plugin-record-id", "company-a"); - expect(mocks.getConfig).not.toHaveBeenCalled(); - - services.dispose(); - }); - - it("still reads the exact global row for explicit global requests", async () => { - mocks.getConfigExactScope.mockResolvedValue({ - configJson: { tokenRef: "global-token-ref" }, - }); - - const services = buildHostServices( - {} as never, - "plugin-record-id", - "paperclip.example", - createEventBusStub(), - ); - - await expect( - services.config.get({ companyId: null }), - ).resolves.toEqual({ tokenRef: "global-token-ref" }); - expect(mocks.getConfigExactScope).toHaveBeenCalledWith("plugin-record-id", null); - - services.dispose(); - }); -}); diff --git a/server/src/__tests__/plugin-secrets-handler-runtime.test.ts b/server/src/__tests__/plugin-secrets-handler-runtime.test.ts index 504ad9e0..da152656 100644 --- a/server/src/__tests__/plugin-secrets-handler-runtime.test.ts +++ b/server/src/__tests__/plugin-secrets-handler-runtime.test.ts @@ -1,13 +1,13 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ - getConfigExactScope: vi.fn(), + getConfig: vi.fn(), resolveSecretValue: vi.fn(), })); vi.mock("../services/plugin-registry.js", () => ({ pluginRegistryService: () => ({ - getConfigExactScope: mocks.getConfigExactScope, + getConfig: mocks.getConfig, }), })); @@ -40,7 +40,7 @@ const manifest = { describe("createPluginSecretsHandler runtime company scoping", () => { beforeEach(() => { - mocks.getConfigExactScope.mockReset(); + mocks.getConfig.mockReset(); mocks.resolveSecretValue.mockReset(); }); @@ -54,12 +54,12 @@ describe("createPluginSecretsHandler runtime company scoping", () => { await expect(handler.resolve({ secretRef })).rejects.toThrow( PLUGIN_SECRET_REFS_REQUIRE_COMPANY_MESSAGE, ); - expect(mocks.getConfigExactScope).not.toHaveBeenCalled(); + expect(mocks.getConfig).not.toHaveBeenCalled(); expect(mocks.resolveSecretValue).not.toHaveBeenCalled(); }); it("rejects a secret ref that is not referenced by that company's plugin config", async () => { - mocks.getConfigExactScope.mockResolvedValue({ + mocks.getConfig.mockResolvedValue({ configJson: { apiKeyRef: "88888888-8888-4888-8888-888888888888", }, @@ -74,12 +74,12 @@ describe("createPluginSecretsHandler runtime company scoping", () => { await expect(handler.resolve({ secretRef, companyId })).rejects.toThrow( /not referenced by this company's plugin config/i, ); - expect(mocks.getConfigExactScope).toHaveBeenCalledWith(pluginId, companyId); + expect(mocks.getConfig).toHaveBeenCalledWith(pluginId, companyId); expect(mocks.resolveSecretValue).not.toHaveBeenCalled(); }); it("resolves only through the company plugin binding context from saved config", async () => { - mocks.getConfigExactScope.mockResolvedValue({ + mocks.getConfig.mockResolvedValue({ configJson: { apiKeyRef: secretRef, }, @@ -102,20 +102,4 @@ describe("createPluginSecretsHandler runtime company scoping", () => { pluginId, }); }); - - it("does not authorize a company-scoped secret resolution from legacy global config fallback", async () => { - mocks.getConfigExactScope.mockResolvedValue(null); - - const handler = createPluginSecretsHandler({ - db: {} as never, - pluginId, - manifest: manifest as never, - }); - - await expect(handler.resolve({ secretRef, companyId })).rejects.toThrow( - /not referenced by this company's plugin config/i, - ); - expect(mocks.getConfigExactScope).toHaveBeenCalledWith(pluginId, companyId); - expect(mocks.resolveSecretValue).not.toHaveBeenCalled(); - }); }); diff --git a/server/src/services/plugin-host-services.ts b/server/src/services/plugin-host-services.ts index e76151cf..4ea90454 100644 --- a/server/src/services/plugin-host-services.ts +++ b/server/src/services/plugin-host-services.ts @@ -1054,10 +1054,7 @@ export function buildHostServices( return { config: { async get(params) { - const companyId = Object.prototype.hasOwnProperty.call(params ?? {}, "companyId") - ? params.companyId ?? null - : null; - const configRow = await registry.getConfigExactScope(pluginId, companyId); + const configRow = await registry.getConfig(pluginId, params?.companyId ?? null); return configRow?.configJson ?? {}; }, }, diff --git a/server/src/services/plugin-registry.ts b/server/src/services/plugin-registry.ts index c80c0cf4..355717e3 100644 --- a/server/src/services/plugin-registry.ts +++ b/server/src/services/plugin-registry.ts @@ -287,14 +287,6 @@ export function pluginRegistryService(db: Db) { // ----- Config --------------------------------------------------------- - /** Retrieve a plugin's config at the exact requested scope. */ - getConfigExactScope: (pluginId: string, companyId?: string | null) => - db - .select() - .from(pluginConfig) - .where(pluginConfigExactScopeCondition(pluginId, companyId)) - .then((rows) => rows[0] ?? null), - /** Retrieve a plugin's company-scoped config, or the legacy global fallback. */ getConfig: async (pluginId: string, companyId?: string | null) => { if (companyId) { diff --git a/server/src/services/plugin-secrets-handler.ts b/server/src/services/plugin-secrets-handler.ts index 84d370d0..cadb33c7 100644 --- a/server/src/services/plugin-secrets-handler.ts +++ b/server/src/services/plugin-secrets-handler.ts @@ -242,10 +242,7 @@ export function createPluginSecretsHandler( throw new Error(PLUGIN_SECRET_REFS_REQUIRE_COMPANY_MESSAGE); } - // Authorize secret refs only from the exact company-scoped config row. - // Falling back to legacy/global config would let a live worker keep using - // stale global refs after a company-scoped invocation takes over. - const configRow = await registry.getConfigExactScope(pluginId, companyId); + const configRow = await registry.getConfig(pluginId, companyId); const refsBySecret = extractSecretRefPathsFromConfig( configRow?.configJson ?? {}, manifest?.instanceConfigSchema,