feat: add cross-plugin secret references gated by the provider's RBAC permission

This commit is contained in:
LukeGus committed 2026-09-21 17:09:28 -05:00
1 parent 2d6c1cdecb
commit 9db66d5780
13 files changed
+1782 -10

No files matched your search

+110
View File
@@ -150,6 +150,116 @@ describe("plugin manifest validation", () => {
});
});
describe("providesSecret / requiresSecret", () => {
const permissionGroup = {
group: "testplugin",
permissions: ["testplugin.secrets.share"],
};
it("accepts a well-formed pair", () => {
expect(
validateManifest(
validManifest({
providesSecret: [
{ key: "api-key", permission: "testplugin.secrets.share" },
],
requiresSecret: [
{ plugin: "ai-assistant", key: "api-key", optional: true },
],
contributes: { permissionGroup },
}),
),
).toEqual([]);
});
it("rejects a dotted secret key", () => {
// The owning plugin is already its own field, so a dotted key would only
// encode the owner twice and invite the two disagreeing.
const errors = validateManifest(
validManifest({
providesSecret: [
{ key: "ai.api-key", permission: "testplugin.secrets.share" },
],
}),
);
expect(errors.some((e) => e.includes("providesSecret[0].key"))).toBe(
true,
);
});
it("rejects a duplicate offer of the same key", () => {
const errors = validateManifest(
validManifest({
providesSecret: [
{ key: "api-key", permission: "testplugin.secrets.share" },
{ key: "api-key", permission: "testplugin.secrets.share" },
],
}),
);
expect(errors.some((e) => e.includes("duplicate"))).toBe(true);
});
it("requires a permission on an offer", () => {
const errors = validateManifest(
validManifest({ providesSecret: [{ key: "api-key" }] }),
);
expect(
errors.some((e) => e.includes("providesSecret[0].permission")),
).toBe(true);
});
it("rejects a reference with a bad plugin id", () => {
const errors = validateManifest(
validManifest({
requiresSecret: [{ plugin: "Bad_Id", key: "api-key" }],
}),
);
expect(errors.some((e) => e.includes("requiresSecret[0].plugin"))).toBe(
true,
);
});
it("rejects a duplicate reference to the same plugin and key", () => {
const errors = validateManifest(
validManifest({
requiresSecret: [
{ plugin: "ai-assistant", key: "api-key" },
{ plugin: "ai-assistant", key: "api-key" },
],
}),
);
expect(errors.some((e) => e.includes("duplicate"))).toBe(true);
});
it("refuses a shared secret gated by a permission the group never declares", () => {
// Same rule as a service: sharing rides on an ordinary role permission,
// so it has to be one an admin can actually grant.
const { manifest, errors } = parseManifest(
validManifest({
providesSecret: [
{ key: "api-key", permission: "testplugin.undeclared" },
],
contributes: { permissionGroup },
}),
);
expect(manifest).toBeUndefined();
expect(errors[0]).toContain("testplugin.undeclared");
expect(errors[0]).toContain("permissionGroup");
});
it("refuses a plugin borrowing from itself", () => {
const { manifest, errors } = parseManifest(
validManifest({
requiresSecret: [{ plugin: "sample-plugin", key: "api-key" }],
}),
);
expect(manifest).toBeUndefined();
expect(errors[0]).toContain("names this plugin itself");
});
});
it("rejects an unknown permission", () => {
const errors = validateManifest(
validManifest({ permissions: ["credentials.read"] }),
@@ -0,0 +1,394 @@
/**
* Two real plugins on disk, one offering a secret and one borrowing it, driven
* through the actual PluginLoader.
*
* Same borrowed-id trick as service-contract.test.ts: the in-process allowlist
* is hardcoded and deliberately unfakeable, so the fixtures use real
* first-party ids to reach that tier. The secret ("api-key") and the
* permission gating it ("testplugin.secrets.share") are fixture-owned, so
* nothing about the shipped ssh-terminal or docker plugins is under test.
*
* What the unit test cannot show, and this does: that the manifest declaration
* is enforced by the real ctx, and that a genuine uninstall through
* loader.deactivate() leaves the borrower with null rather than a handle onto
* a resolver whose plugin is gone.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import fs from "node:fs";
import path from "node:path";
import type { Fixture } from "./fixture-plugin.js";
const state = vi.hoisted(() => ({
/** Permissions the acting user currently holds. Mutated mid-test. */
granted: new Set<string>(),
auditEntries: [] as Record<string, unknown>[],
}));
vi.mock("../../utils/logger.js", () => ({
pluginLogger: {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
success: vi.fn(),
},
}));
vi.mock("../../utils/audit-logger.js", () => ({
logAudit: vi.fn(async (params: Record<string, unknown>) => {
state.auditEntries.push(params);
}),
getAuditUsername: vi.fn(async (userId: string) => `user:${userId}`),
}));
vi.mock("../../utils/permission-manager.js", () => ({
PermissionManager: {
getInstance: () => ({
hasPermission: async (_userId: string, permission: string) =>
state.granted.has(permission),
invalidateUserPermissionCache: vi.fn(),
}),
},
}));
const { PluginLoader } = await import("../../plugins/loader.js");
const { TRANSPORT_OWNER_CAPABILITY } =
await import("../../plugins/first-party.js");
const {
clearSecretRegistry,
getSecretRegistration,
PluginSecretPermissionError,
PluginSecretUndeclaredError,
} = await import("../../plugins/secret-registry.js");
const { pluginEvents } = await import("../../plugins/events.js");
const { createFixturePlugin } = await import("./fixture-plugin.js");
const PROVIDER_ID = "ssh-terminal";
const CONSUMER_ID = "docker";
const KEY = "api-key";
const PERMISSION = "testplugin.secrets.share";
const SECRET = "sk-fixture-value";
/**
* The provider offers a resolver reading the current value from a file, so the
* test can rotate or clear it from outside without restarting the plugin. A
* module-level variable would not work: the plugin is imported by URL and
* lives in its own ESM module instance.
*/
function providerSource(valueFile: string): string {
const literal = JSON.stringify(valueFile);
return `
import fs from "node:fs";
export async function activate(ctx) {
ctx.secrets.offer(${JSON.stringify(KEY)}, async () => {
if (!fs.existsSync(${literal})) return null;
const value = fs.readFileSync(${literal}, "utf8");
return value.length > 0 ? value : null;
});
}
`;
}
/**
* The consumer stashes a bound read on globalThis so the test can invoke it as
* a specific user afterwards. Note it stashes a FUNCTION, not a value: there
* is no handle to hold, which is the property under test.
*/
const CONSUMER_SOURCE = `
export async function activate(ctx) {
globalThis.__borrow = (userId) =>
ctx.secrets.getShared(${JSON.stringify(PROVIDER_ID)}, ${JSON.stringify(KEY)}, { userId });
globalThis.__borrowUndeclared = (userId) =>
ctx.secrets.getShared(${JSON.stringify(PROVIDER_ID)}, "not-declared", { userId });
}
`;
function providerFixture(): Fixture {
return createFixturePlugin({
id: PROVIDER_ID,
permissions: [TRANSPORT_OWNER_CAPABILITY],
manifestOverrides: {
category: "Terminal",
providesSecret: [{ key: KEY, permission: PERMISSION }],
contributes: {
permissionGroup: { group: "testplugin", permissions: [PERMISSION] },
},
},
});
}
function consumerFixture(
requiresSecret: Array<Record<string, unknown>> = [
{ plugin: PROVIDER_ID, key: KEY, optional: true },
],
): Fixture {
return createFixturePlugin({
id: CONSUMER_ID,
permissions: [TRANSPORT_OWNER_CAPABILITY],
backendSource: CONSUMER_SOURCE,
manifestOverrides: { category: "Infrastructure", requiresSecret },
});
}
function borrow(userId = "user-1"): Promise<string | null> {
const fn = (globalThis as Record<string, unknown>).__borrow as (
id: string,
) => Promise<string | null>;
return fn(userId);
}
function borrowUndeclared(userId = "user-1"): Promise<string | null> {
const fn = (globalThis as Record<string, unknown>).__borrowUndeclared as (
id: string,
) => Promise<string | null>;
return fn(userId);
}
describe("cross-plugin secret references", () => {
let provider: Fixture | null = null;
let consumer: Fixture | null = null;
let loader: InstanceType<typeof PluginLoader> | null = null;
let valueFile = "";
beforeEach(() => {
state.granted = new Set([PERMISSION]);
state.auditEntries = [];
clearSecretRegistry();
pluginEvents.clear();
});
afterEach(async () => {
await loader?.shutdown();
loader = null;
provider?.cleanup();
consumer?.cleanup();
provider = null;
consumer = null;
delete (globalThis as Record<string, unknown>).__borrow;
delete (globalThis as Record<string, unknown>).__borrowUndeclared;
clearSecretRegistry();
pluginEvents.clear();
});
async function activateProvider() {
provider = providerFixture();
valueFile = path.join(provider.root, "secret.txt");
fs.writeFileSync(valueFile, SECRET);
fs.writeFileSync(
path.join(provider.dir, "backend", "index.mjs"),
providerSource(valueFile),
);
loader ??= new PluginLoader();
const plugin = await loader.load(provider.dir);
await loader.activate(plugin.id);
return plugin;
}
async function activateConsumer(
requiresSecret?: Array<Record<string, unknown>>,
) {
consumer = requiresSecret
? consumerFixture(requiresSecret)
: consumerFixture();
loader ??= new PluginLoader();
const plugin = await loader.load(consumer.dir);
await loader.activate(plugin.id);
return plugin;
}
async function activateBoth() {
loader = new PluginLoader();
const providerPlugin = await activateProvider();
const consumerPlugin = await activateConsumer();
return { providerPlugin, consumerPlugin };
}
it("registers the offer when the provider activates", async () => {
const { providerPlugin, consumerPlugin } = await activateBoth();
expect(providerPlugin.state).toBe("active");
expect(consumerPlugin.state).toBe("active");
const registration = getSecretRegistration(PROVIDER_ID, KEY);
expect(registration?.pluginId).toBe(PROVIDER_ID);
expect(registration?.permission).toBe(PERMISSION);
});
it("resolves for a user who holds the permission", async () => {
await activateBoth();
await expect(borrow()).resolves.toBe(SECRET);
});
it("denies a user without the permission, though the secret is set", async () => {
await activateBoth();
state.granted = new Set();
await expect(borrow("user-2")).rejects.toBeInstanceOf(
PluginSecretPermissionError,
);
// Still installed, still set: the difference is purely this user's role.
expect(getSecretRegistration(PROVIDER_ID, KEY)).toBeDefined();
expect(fs.readFileSync(valueFile, "utf8")).toBe(SECRET);
});
it("lets one user borrow while another is denied, with no reinstall", async () => {
await activateBoth();
// The grant is per-user, so both answers come from the same live install.
await expect(borrow("allowed-user")).resolves.toBe(SECRET);
state.granted = new Set();
await expect(borrow("denied-user")).rejects.toBeInstanceOf(
PluginSecretPermissionError,
);
state.granted = new Set([PERMISSION]);
await expect(borrow("allowed-user")).resolves.toBe(SECRET);
});
it("returns null when the provider is installed but the key is not set", async () => {
await activateBoth();
fs.writeFileSync(valueFile, "");
await expect(borrow()).resolves.toBeNull();
});
it("returns null when the providing plugin was never installed", async () => {
loader = new PluginLoader();
await activateConsumer();
// The consumer activates regardless: the reference is resolved per call.
await expect(borrow()).resolves.toBeNull();
});
it("activates the consumer even when a non-optional reference is absent", async () => {
loader = new PluginLoader();
const plugin = await activateConsumer([{ plugin: PROVIDER_ID, key: KEY }]);
// Unlike a service requirement, an absent secret provider is recoverable
// by installing it later, so it must not be a hard ordering dependency.
expect(plugin.state).toBe("active");
await expect(borrow()).resolves.toBeNull();
});
it("refuses a read the consumer's manifest never declared", async () => {
await activateBoth();
await expect(borrowUndeclared()).rejects.toBeInstanceOf(
PluginSecretUndeclaredError,
);
});
it("refuses to offer a secret the provider's manifest never declared", async () => {
provider = createFixturePlugin({
id: PROVIDER_ID,
permissions: [TRANSPORT_OWNER_CAPABILITY],
backendSource: `
export async function activate(ctx) {
ctx.secrets.offer("undeclared-key", () => "nope");
}
`,
manifestOverrides: {
category: "Terminal",
providesSecret: [{ key: KEY, permission: PERMISSION }],
contributes: {
permissionGroup: { group: "testplugin", permissions: [PERMISSION] },
},
},
});
loader = new PluginLoader();
const plugin = await loader.load(provider.dir);
await loader.activate(plugin.id).catch(() => undefined);
expect(plugin.state).not.toBe("active");
expect(
getSecretRegistration(PROVIDER_ID, "undeclared-key"),
).toBeUndefined();
});
describe("the provider stays in control", () => {
it("serves a rotated value on the next borrow", async () => {
await activateBoth();
await expect(borrow()).resolves.toBe(SECRET);
fs.writeFileSync(valueFile, "sk-fixture-rotated");
// Nothing to invalidate: the borrower never held a copy to go stale.
await expect(borrow()).resolves.toBe("sk-fixture-rotated");
});
it("nulls the borrow out when the provider clears the secret", async () => {
await activateBoth();
await expect(borrow()).resolves.toBe(SECRET);
fs.rmSync(valueFile);
await expect(borrow()).resolves.toBeNull();
});
});
describe("uninstalling the provider", () => {
it("nulls out the reference rather than leaving a dangling handle", async () => {
const { providerPlugin } = await activateBoth();
await expect(borrow()).resolves.toBe(SECRET);
await loader!.deactivate(providerPlugin.id);
// The borrower is untouched: still active, still permitted, still
// declaring the reference. It just gets null now, and does not throw.
expect(getSecretRegistration(PROVIDER_ID, KEY)).toBeUndefined();
await expect(borrow()).resolves.toBeNull();
});
it("keeps serving nothing even though the underlying value still exists", async () => {
const { providerPlugin } = await activateBoth();
await loader!.deactivate(providerPlugin.id);
// The file is still on disk. Access went away with the plugin that
// mediated it, which is the point: there was never a copy to fall back on.
expect(fs.readFileSync(valueFile, "utf8")).toBe(SECRET);
await expect(borrow()).resolves.toBeNull();
});
it("resumes when the provider is reinstalled, with no borrower restart", async () => {
const { providerPlugin } = await activateBoth();
await loader!.deactivate(providerPlugin.id);
await expect(borrow()).resolves.toBeNull();
await loader!.activate(providerPlugin.id);
await expect(borrow()).resolves.toBe(SECRET);
});
});
describe("audit", () => {
it("records a borrow without the value", async () => {
await activateBoth();
await borrow("user-9");
const entry = state.auditEntries.at(-1);
expect(entry).toMatchObject({
userId: "user-9",
username: `plugin:${CONSUMER_ID}`,
action: "plugin_secret_shared_read",
resourceId: PROVIDER_ID,
success: true,
});
expect(JSON.stringify(state.auditEntries)).not.toContain(SECRET);
});
it("records a denial without the value", async () => {
await activateBoth();
state.granted = new Set();
await borrow().catch(() => undefined);
expect(state.auditEntries.at(-1)).toMatchObject({ success: false });
expect(JSON.stringify(state.auditEntries)).not.toContain(SECRET);
});
});
});
@@ -0,0 +1,424 @@
/**
* Cross-plugin secret references: the permission gate, the two null cases, and
* what an uninstall does to a borrower still pointing at the provider.
*
* The distinction under test throughout is between a denial (an error, because
* the caller asked for something it may not have) and an absence (null, because
* the thing legitimately is not there). A consumer that cannot tell those apart
* would read "you may not" as "not configured" and fall back to something
* weaker, so every case below asserts which of the two it got.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../utils/logger.js", () => ({
pluginLogger: {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
success: vi.fn(),
},
}));
const {
clearSecretRegistry,
getSecretRegistration,
listSecrets,
offerSecret,
readSharedSecret,
resolveSecretRequirements,
withdrawAllForPlugin,
withdrawSecret,
PluginSecretActorError,
PluginSecretPermissionError,
PluginSecretUndeclaredError,
} = await import("../../plugins/secret-registry.js");
const PROVIDER = "ai-assistant";
const KEY = "api-key";
const PERMISSION = "ai.secrets.share";
const SECRET = "sk-live-value";
/** Permissions the acting user currently holds. Mutated mid-test. */
let granted = new Set<string>();
let auditEntries: Array<Record<string, unknown>> = [];
function offer(overrides: Partial<Parameters<typeof offerSecret>[0]> = {}) {
return offerSecret({
pluginId: PROVIDER,
pluginName: "AI Assistant",
key: KEY,
permission: PERMISSION,
resolve: () => SECRET,
...overrides,
});
}
/** A consumer manifest declaring the reference, as the real one would. */
function consumer(
requiresSecret: Array<{ plugin: string; key: string; optional?: boolean }> = [
{ plugin: PROVIDER, key: KEY, optional: true },
],
) {
return { id: "reporting-plugin", requiresSecret } as never;
}
function context(userId: string | undefined) {
return {
resolveUserId: () => userId,
hasPermission: async (_userId: string, permission: string) =>
granted.has(permission),
audit: (entry: Record<string, unknown>) => {
auditEntries.push(entry);
},
} as never;
}
function read(
manifest = consumer(),
userId: string | undefined = "user-1",
providerPluginId = PROVIDER,
key = KEY,
) {
return readSharedSecret(manifest, providerPluginId, key, context(userId));
}
/**
* A read with no acting user. Separate from read() on purpose: a default
* parameter cannot express this, since passing undefined explicitly would
* just fall back to the default and quietly test the wrong thing.
*/
function readAnonymously(manifest = consumer()) {
return readSharedSecret(manifest, PROVIDER, KEY, context(undefined));
}
describe("shared secret registry", () => {
beforeEach(() => {
clearSecretRegistry();
granted = new Set([PERMISSION]);
auditEntries = [];
});
afterEach(() => clearSecretRegistry());
describe("registration", () => {
it("round-trips an offer", () => {
const registration = offer();
expect(getSecretRegistration(PROVIDER, KEY)).toBe(registration);
expect(listSecrets()).toHaveLength(1);
expect(registration.generation).toBeGreaterThan(0);
});
it("keys offers by plugin as well as name", () => {
offer();
offer({ pluginId: "other-plugin", resolve: () => "other-value" });
expect(listSecrets()).toHaveLength(2);
expect(getSecretRegistration("other-plugin", KEY)?.pluginId).toBe(
"other-plugin",
);
});
it("only withdraws the offer that is actually current", () => {
const first = offer();
const second = offer();
// A crashed-and-restarted provider must not withdraw the replacement its
// own restart installed.
expect(withdrawSecret(PROVIDER, KEY, first)).toBe(false);
expect(getSecretRegistration(PROVIDER, KEY)).toBe(second);
expect(withdrawSecret(PROVIDER, KEY, second)).toBe(true);
expect(getSecretRegistration(PROVIDER, KEY)).toBeUndefined();
});
it("reports a withdraw of something never offered", () => {
expect(withdrawSecret(PROVIDER, "nothing")).toBe(false);
});
});
describe("reading", () => {
it("resolves for a user who holds the permission", async () => {
offer();
await expect(read()).resolves.toBe(SECRET);
});
it("denies a user without the permission, though the secret exists", async () => {
offer();
granted = new Set();
// The whole point of the per-call check: the plugin is installed, the
// key is set, and this user still gets nothing.
await expect(read()).rejects.toBeInstanceOf(PluginSecretPermissionError);
expect(getSecretRegistration(PROVIDER, KEY)).toBeDefined();
});
it("denies with the 403 shape a route can pass straight through", async () => {
offer();
granted = new Set();
const error = await read().catch((caught) => caught);
expect(error.status).toBe(403);
expect(error.body).toEqual({
error: "Insufficient permissions",
required: PERMISSION,
});
});
it("never runs the provider's resolver for a denied read", async () => {
const resolve = vi.fn(() => SECRET);
offer({ resolve });
granted = new Set();
await expect(read()).rejects.toBeInstanceOf(PluginSecretPermissionError);
expect(resolve).not.toHaveBeenCalled();
});
it("returns null when the provider is installed but the key is not set", async () => {
offer({ resolve: () => null });
// Absence, not denial: the user may read it, there is just nothing there.
await expect(read()).resolves.toBeNull();
});
it("treats an empty string as not set", async () => {
offer({ resolve: () => "" });
await expect(read()).resolves.toBeNull();
});
it("returns null when the providing plugin is not installed at all", async () => {
await expect(read()).resolves.toBeNull();
});
it("gives the same answer for absent and unset, so neither leaks install state", async () => {
const unset = await read();
offer({ resolve: () => null });
const absent = await read();
expect(unset).toBeNull();
expect(absent).toBeNull();
});
it("refuses a plugin reading something its manifest never declared", async () => {
offer();
await expect(read(consumer([]), "user-1")).rejects.toBeInstanceOf(
PluginSecretUndeclaredError,
);
});
it("refuses a declared reference to a different key on the same plugin", async () => {
offer();
await expect(
read(consumer([{ plugin: PROVIDER, key: "other-key" }])),
).rejects.toBeInstanceOf(PluginSecretUndeclaredError);
});
it("refuses a read that cannot be attributed to a user", async () => {
offer();
await expect(readAnonymously()).rejects.toBeInstanceOf(
PluginSecretActorError,
);
});
it("returns null for an anonymous read of an absent provider", async () => {
// Absence is settled before the actor is, so a borrower whose provider
// is gone gets the same null whether or not it had a user to act as.
await expect(readAnonymously()).resolves.toBeNull();
});
it("passes the acting user to the resolver, so per-user keys stay per-user", async () => {
const resolve = vi.fn((userId: string) => `key-for-${userId}`);
offer({ resolve });
await expect(read(consumer(), "user-7")).resolves.toBe("key-for-user-7");
expect(resolve).toHaveBeenCalledWith("user-7");
});
it("propagates a resolver failure rather than reporting it as unset", async () => {
offer({
resolve: () => {
throw new Error("vault unreachable");
},
});
// Reported as null, a consumer would fall back to a weaker path on what
// is actually a transient provider fault.
await expect(read()).rejects.toThrow("vault unreachable");
});
});
describe("revocation takes effect immediately", () => {
it("stops resolving the moment the permission is revoked", async () => {
offer();
await expect(read()).resolves.toBe(SECRET);
granted = new Set();
// No restart, no reinstall: the next call is already denied.
await expect(read()).rejects.toBeInstanceOf(PluginSecretPermissionError);
});
it("serves the rotated value on the next read, with nothing to invalidate", async () => {
let current = SECRET;
offer({ resolve: () => current });
await expect(read()).resolves.toBe(SECRET);
current = "sk-live-rotated";
await expect(read()).resolves.toBe("sk-live-rotated");
});
it("nulls out a borrower's reference when the provider clears the secret", async () => {
let current: string | null = SECRET;
offer({ resolve: () => current });
await expect(read()).resolves.toBe(SECRET);
current = null;
await expect(read()).resolves.toBeNull();
});
});
describe("uninstalling the provider", () => {
it("nulls out the reference rather than leaving a dangling handle", async () => {
offer();
await expect(read()).resolves.toBe(SECRET);
expect(withdrawAllForPlugin(PROVIDER)).toBe(1);
// The borrower is still installed, still declares the reference, and
// still holds the permission. It simply gets null now, the same answer
// as a provider that was never installed -- no throw, no stale value.
await expect(read()).resolves.toBeNull();
expect(getSecretRegistration(PROVIDER, KEY)).toBeUndefined();
});
it("keeps no copy of the value behind after an uninstall", async () => {
const resolve = vi.fn(() => SECRET);
offer({ resolve });
await read();
resolve.mockClear();
withdrawAllForPlugin(PROVIDER);
// Nothing cached the earlier read: the answer comes from the absence of
// a registration, not from a remembered value.
await expect(read()).resolves.toBeNull();
expect(resolve).not.toHaveBeenCalled();
});
it("withdraws every key the plugin offered, not just the one read", () => {
offer();
offer({ key: "webhook-secret" });
offer({ pluginId: "unrelated-plugin" });
expect(withdrawAllForPlugin(PROVIDER)).toBe(2);
expect(listSecrets()).toHaveLength(1);
expect(getSecretRegistration("unrelated-plugin", KEY)).toBeDefined();
});
it("starts working again if the provider is reinstalled, with no borrower restart", async () => {
offer();
withdrawAllForPlugin(PROVIDER);
await expect(read()).resolves.toBeNull();
offer({ resolve: () => "sk-live-reinstalled" });
await expect(read()).resolves.toBe("sk-live-reinstalled");
});
});
describe("requirement resolution", () => {
it("reports an offered reference as available", () => {
offer();
const resolution = resolveSecretRequirements(consumer());
expect(resolution.available).toEqual([`${PROVIDER}:${KEY}`]);
expect(resolution.unavailable).toEqual([]);
expect(resolution.errors).toEqual([]);
});
it("reports an absent optional reference without an error", () => {
const resolution = resolveSecretRequirements(consumer());
expect(resolution.unavailable).toEqual([`${PROVIDER}:${KEY}`]);
expect(resolution.errors).toEqual([]);
});
it("reports an absent required reference as an error", () => {
const resolution = resolveSecretRequirements(
consumer([{ plugin: PROVIDER, key: KEY }]),
);
expect(resolution.errors).toHaveLength(1);
expect(resolution.errors[0]).toContain(PROVIDER);
});
it("ignores any user's permissions, because activation is per-instance", () => {
offer();
granted = new Set();
// A user without the grant must get a clear denial from the call, not a
// plugin that mysteriously refuses to start.
expect(resolveSecretRequirements(consumer()).available).toHaveLength(1);
});
it("is empty for a manifest that borrows nothing", () => {
const resolution = resolveSecretRequirements({ id: "plain" } as never);
expect(resolution.available).toEqual([]);
expect(resolution.unavailable).toEqual([]);
expect(resolution.errors).toEqual([]);
});
});
describe("audit", () => {
it("records an allowed read without the value", async () => {
offer();
await read();
expect(auditEntries).toHaveLength(1);
expect(auditEntries[0]).toMatchObject({
userId: "user-1",
consumerPluginId: "reporting-plugin",
providerPluginId: PROVIDER,
key: KEY,
success: true,
resolved: true,
});
expect(JSON.stringify(auditEntries[0])).not.toContain(SECRET);
});
it("records a denial", async () => {
offer();
granted = new Set();
await read().catch(() => undefined);
expect(auditEntries[0]).toMatchObject({
success: false,
resolved: false,
});
});
it("distinguishes an allowed read that found nothing", async () => {
offer({ resolve: () => null });
await read();
expect(auditEntries[0]).toMatchObject({ success: true, resolved: false });
});
it("writes nothing when the provider is not installed", async () => {
await read();
// There is no provider to attribute the read to, and no decision was
// made about the user -- an audit line here would be noise.
expect(auditEntries).toEqual([]);
});
});
});