mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
## Thinking Path > - Paperclip is the open source control plane people use to manage AI-agent companies. > - Its secrets subsystem can resolve external references such as AWS Secrets Manager values without copying those values into Paperclip custody. > - Operators also need to rotate a referenced secret's value while preserving the same provider reference for consumers inside and outside Paperclip. > - Previously, external-reference rotation could only retarget metadata, and secret detail sheets were driven by local component state rather than shareable navigation state. > - This pull request adds an optional provider write capability, implements AWS Secrets Manager write-through rotation, and exposes capability-aware rotate modes in the UI. > - It also makes secret and each-user definition detail sheets URL-driven and adds a copy-link action. > - The benefit is that operators can update the canonical external value safely while keeping AWS rotation tracking intact, and they can share or navigate directly to secret details. ## Linked Issues or Issue Description ### Subsystem affected Cross-cutting (`server/`, `ui/`, and `packages/shared`). ### Problem or motivation External-reference secrets can follow a provider-managed value, but Paperclip could not write a replacement value back to providers that support it. Operators had to leave Paperclip, update the value separately, and then return without an auditable Paperclip rotation record. Secret detail sheets also could not be shared or restored through browser history because their selection lived only in component state. ### Proposed solution Add an optional `updateExternalSecretValue` provider capability and surface it as `supportsExternalValueWrites`. Implement AWS writes with `PutSecretValue` while leaving the resolution `versionId` unset so future reads continue following `AWSCURRENT`. Add write-value and retarget modes to the rotate dialog for capable providers. Drive secret detail selection from `?secret=` / `?definition=` query parameters and provide a copy-link action. ### Alternatives considered Converting an external reference into a Paperclip-managed secret would break consumers that depend on the existing provider reference. Pinning reads to the newly written AWS version would prevent later out-of-band rotations from flowing through. Keeping sheet selection only in React state would not support browser Back or shareable links. ### Roadmap alignment This extends the completed “Secrets Manager with per-agent access” roadmap capability; it does not duplicate a separate planned roadmap item. Public GitHub searches found no duplicate or closely related issue or PR. ### Additional context The PR includes focused provider, service, and UI render coverage. Cutter also generated previews for the deep-linked detail sheet and capability-aware rotate modes. ## What Changed - Added optional external-value write support to the secret provider contract and provider descriptors. - Implemented AWS Secrets Manager write-through with `PutSecretValue`, audit material, and compensation when persistence fails after the provider write. - Allowed `secretService.rotate()` value updates for external references while rejecting ambiguous value-plus-retarget combinations and unsupported providers. - Added capability-aware “Write new value” and “Change reference” rotate modes with updated custody and action copy. - Made secret and each-user definition detail sheets source their selection from URL query parameters, compose with folder paths, close through browser history, and expose a copy-link action. - Added provider, service, and UI render coverage for write-through, rollback, capability messaging, dialog modes, and deep links. ## Verification - `pnpm vitest run server/src/__tests__/aws-secrets-manager-provider.test.ts` — 18 passed. - `pnpm vitest run server/src/__tests__/secrets-service.test.ts` — 75 passed. - `pnpm vitest run ui/src/pages/Secrets.render.test.tsx` — 31 passed. - `pnpm --filter @paperclipai/shared typecheck` — passed. - `pnpm --filter @paperclipai/server typecheck` — passed. - `pnpm --filter @paperclipai/ui typecheck` — passed. - `pnpm check:token-gates` — passed with all gates clean. ## Risks - External value writes affect the canonical provider secret and therefore all consumers of that AWS secret; the UI explicitly labels this custody behavior. - A provider write can succeed before Paperclip persistence fails. The service records the written version and AWS support includes compensation coverage to restore the prior value where possible; unrecoverable failures return explicit audit-safe error details. - URL-driven sheet state changes navigation behavior; render tests cover deep links, Back/close behavior, and composition with folder query state. - No database migration or breaking API requirement is introduced; providers without the optional capability retain reference-only behavior. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex using exact model ID `gpt-5.6-sol`, high reasoning mode, Codex CLI `0.142.5`, with repository, shell, Git, GitHub CLI, and code-execution tools. The runtime did not expose a context-window size. - Earlier implementation commits were assisted by Anthropic `Claude Fable 5` as recorded in their commit trailers; the exact backend model ID and context-window size were not preserved in the workspace metadata. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes, or confirmed no documentation change is required - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Paperclip <noreply@paperclip.ing>
1078 lines
37 KiB
TypeScript
1078 lines
37 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { createAwsSecretsManagerProvider } from "../secrets/aws-secrets-manager-provider.js";
|
|
import { SecretProviderClientError } from "../secrets/types.js";
|
|
|
|
describe("awsSecretsManagerProvider", () => {
|
|
const previousEnv = {
|
|
PAPERCLIP_SECRETS_AWS_REGION: process.env.PAPERCLIP_SECRETS_AWS_REGION,
|
|
AWS_REGION: process.env.AWS_REGION,
|
|
AWS_DEFAULT_REGION: process.env.AWS_DEFAULT_REGION,
|
|
PAPERCLIP_SECRETS_AWS_DEPLOYMENT_ID: process.env.PAPERCLIP_SECRETS_AWS_DEPLOYMENT_ID,
|
|
PAPERCLIP_SECRETS_AWS_KMS_KEY_ID: process.env.PAPERCLIP_SECRETS_AWS_KMS_KEY_ID,
|
|
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID,
|
|
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY,
|
|
AWS_SESSION_TOKEN: process.env.AWS_SESSION_TOKEN,
|
|
AWS_PROFILE: process.env.AWS_PROFILE,
|
|
AWS_DEFAULT_PROFILE: process.env.AWS_DEFAULT_PROFILE,
|
|
AWS_CONFIG_FILE: process.env.AWS_CONFIG_FILE,
|
|
AWS_SHARED_CREDENTIALS_FILE: process.env.AWS_SHARED_CREDENTIALS_FILE,
|
|
AWS_SDK_LOAD_CONFIG: process.env.AWS_SDK_LOAD_CONFIG,
|
|
};
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
for (const [key, value] of Object.entries(previousEnv)) {
|
|
if (value === undefined) {
|
|
delete process.env[key];
|
|
} else {
|
|
process.env[key] = value;
|
|
}
|
|
}
|
|
});
|
|
|
|
it("creates Paperclip-managed AWS secrets without persisting plaintext in provider material", async () => {
|
|
const calls: Array<{ op: string; input: Record<string, unknown> }> = [];
|
|
const provider = createAwsSecretsManagerProvider({
|
|
config: {
|
|
region: "us-east-1",
|
|
endpoint: "https://secretsmanager.us-east-1.amazonaws.com",
|
|
deploymentId: "prod-use1",
|
|
prefix: "paperclip",
|
|
kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/test",
|
|
environmentTag: "production",
|
|
providerOwnerTag: "paperclip",
|
|
deleteRecoveryWindowDays: 30,
|
|
},
|
|
gateway: {
|
|
async createSecret(input) {
|
|
calls.push({ op: "createSecret", input });
|
|
return {
|
|
ARN: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-1/openai-api-key",
|
|
VersionId: "aws-version-1",
|
|
};
|
|
},
|
|
async putSecretValue(input) {
|
|
calls.push({ op: "putSecretValue", input });
|
|
return { ARN: String(input.SecretId), VersionId: "unused" };
|
|
},
|
|
async getSecretValue(input) {
|
|
calls.push({ op: "getSecretValue", input });
|
|
return { SecretString: "resolved-value", VersionId: "unused" };
|
|
},
|
|
async deleteSecret(input) {
|
|
calls.push({ op: "deleteSecret", input });
|
|
return {};
|
|
},
|
|
},
|
|
});
|
|
|
|
const prepared = await provider.createSecret({
|
|
value: "super-secret-value",
|
|
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/attacker",
|
|
context: {
|
|
companyId: "company-1",
|
|
secretKey: "openai-api-key",
|
|
secretName: "OpenAI API Key",
|
|
version: 1,
|
|
},
|
|
});
|
|
|
|
expect(calls).toEqual([
|
|
expect.objectContaining({
|
|
op: "createSecret",
|
|
input: expect.objectContaining({
|
|
Name: "paperclip/prod-use1/company-1/openai-api-key",
|
|
KmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/test",
|
|
}),
|
|
}),
|
|
]);
|
|
expect(JSON.stringify(prepared)).not.toContain("super-secret-value");
|
|
expect(prepared.externalRef).toContain("paperclip/prod-use1/company-1/openai-api-key");
|
|
expect(prepared.providerVersionRef).toBe("aws-version-1");
|
|
});
|
|
|
|
it("creates AWS secrets from selected provider vault config without deployment env fallback", async () => {
|
|
delete process.env.PAPERCLIP_SECRETS_AWS_REGION;
|
|
delete process.env.AWS_REGION;
|
|
delete process.env.AWS_DEFAULT_REGION;
|
|
delete process.env.PAPERCLIP_SECRETS_AWS_DEPLOYMENT_ID;
|
|
delete process.env.PAPERCLIP_SECRETS_AWS_KMS_KEY_ID;
|
|
delete process.env.AWS_ACCESS_KEY_ID;
|
|
delete process.env.AWS_SECRET_ACCESS_KEY;
|
|
delete process.env.AWS_SESSION_TOKEN;
|
|
|
|
const calls: Array<{ op: string; input: Record<string, unknown> }> = [];
|
|
const provider = createAwsSecretsManagerProvider({
|
|
gateway: {
|
|
async createSecret(input) {
|
|
calls.push({ op: "createSecret", input });
|
|
return {
|
|
ARN: "arn:aws:secretsmanager:us-west-2:123456789012:secret:clip/prod-us-west/company-1/openai-api-key",
|
|
VersionId: "aws-version-1",
|
|
};
|
|
},
|
|
async putSecretValue(input) {
|
|
calls.push({ op: "putSecretValue", input });
|
|
return { ARN: String(input.SecretId), VersionId: "unused" };
|
|
},
|
|
async getSecretValue(input) {
|
|
calls.push({ op: "getSecretValue", input });
|
|
return { SecretString: "resolved-value", VersionId: "unused" };
|
|
},
|
|
async deleteSecret(input) {
|
|
calls.push({ op: "deleteSecret", input });
|
|
return {};
|
|
},
|
|
},
|
|
});
|
|
|
|
const providerConfig = {
|
|
id: "vault-1",
|
|
provider: "aws_secrets_manager" as const,
|
|
status: "ready",
|
|
config: {
|
|
region: "us-west-2",
|
|
namespace: "prod-us-west",
|
|
secretNamePrefix: "clip",
|
|
ownerTag: "platform",
|
|
environmentTag: "production",
|
|
},
|
|
};
|
|
|
|
const health = await provider.healthCheck({ providerConfig });
|
|
const prepared = await provider.createSecret({
|
|
value: "super-secret-value",
|
|
providerConfig,
|
|
context: {
|
|
companyId: "company-1",
|
|
secretKey: "openai-api-key",
|
|
secretName: "OpenAI API Key",
|
|
version: 1,
|
|
},
|
|
});
|
|
|
|
expect(health.status).toBe("ok");
|
|
expect(health.details).toMatchObject({
|
|
region: "us-west-2",
|
|
prefix: "clip",
|
|
deploymentId: "prod-us-west",
|
|
kmsKeyConfigured: false,
|
|
});
|
|
expect(calls).toEqual([
|
|
expect.objectContaining({
|
|
op: "createSecret",
|
|
input: expect.objectContaining({
|
|
Name: "clip/prod-us-west/company-1/openai-api-key",
|
|
SecretString: "super-secret-value",
|
|
Tags: expect.arrayContaining([
|
|
{ Key: "paperclip:provider-owner", Value: "platform" },
|
|
{ Key: "paperclip:environment", Value: "production" },
|
|
]),
|
|
}),
|
|
}),
|
|
]);
|
|
expect(calls[0]?.input).not.toHaveProperty("KmsKeyId");
|
|
expect(JSON.stringify(prepared)).not.toContain("super-secret-value");
|
|
expect(prepared.externalRef).toContain("clip/prod-us-west/company-1/openai-api-key");
|
|
});
|
|
|
|
it("signs AWS Secrets Manager JSON requests with default runtime credentials", async () => {
|
|
delete process.env.AWS_PROFILE;
|
|
delete process.env.AWS_DEFAULT_PROFILE;
|
|
delete process.env.AWS_CONFIG_FILE;
|
|
delete process.env.AWS_SHARED_CREDENTIALS_FILE;
|
|
delete process.env.AWS_SDK_LOAD_CONFIG;
|
|
process.env.AWS_ACCESS_KEY_ID = "AKIA_TEST_ACCESS";
|
|
process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key";
|
|
process.env.AWS_SESSION_TOKEN = "test-session-token";
|
|
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(
|
|
JSON.stringify({
|
|
ARN: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod/company-1/openai-api-key",
|
|
VersionId: "aws-version-1",
|
|
}),
|
|
{ status: 200 },
|
|
),
|
|
);
|
|
const provider = createAwsSecretsManagerProvider({
|
|
config: {
|
|
region: "us-east-1",
|
|
endpoint: "https://secretsmanager.us-east-1.amazonaws.com",
|
|
deploymentId: "prod",
|
|
prefix: "paperclip",
|
|
kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/test",
|
|
environmentTag: "production",
|
|
providerOwnerTag: "paperclip",
|
|
deleteRecoveryWindowDays: 30,
|
|
},
|
|
});
|
|
|
|
await provider.createSecret({
|
|
value: "super-secret-value",
|
|
context: {
|
|
companyId: "company-1",
|
|
secretKey: "openai-api-key",
|
|
secretName: "OpenAI API Key",
|
|
version: 1,
|
|
},
|
|
});
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
const [url, init] = fetchMock.mock.calls[0]!;
|
|
const headers = init?.headers as Record<string, string>;
|
|
expect(String(url)).toBe("https://secretsmanager.us-east-1.amazonaws.com/");
|
|
expect(headers["x-amz-target"]).toBe("secretsmanager.CreateSecret");
|
|
expect(headers["x-amz-security-token"]).toBe("test-session-token");
|
|
expect(headers.authorization).toContain("Credential=AKIA_TEST_ACCESS/");
|
|
expect(headers.authorization).toContain("/us-east-1/secretsmanager/aws4_request");
|
|
expect(headers.authorization).toContain("SignedHeaders=");
|
|
expect(headers.authorization).toContain("Signature=");
|
|
expect(init?.signal).toBeInstanceOf(AbortSignal);
|
|
});
|
|
|
|
it("creates new AWS secret versions against a namespace-valid existing secret reference", async () => {
|
|
const calls: Array<{ op: string; input: Record<string, unknown> }> = [];
|
|
const provider = createAwsSecretsManagerProvider({
|
|
config: {
|
|
region: "us-east-1",
|
|
endpoint: "https://secretsmanager.us-east-1.amazonaws.com",
|
|
deploymentId: "prod-use1",
|
|
prefix: "paperclip",
|
|
kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/test",
|
|
environmentTag: "production",
|
|
providerOwnerTag: "paperclip",
|
|
deleteRecoveryWindowDays: 30,
|
|
},
|
|
gateway: {
|
|
async createSecret() {
|
|
throw new Error("not used");
|
|
},
|
|
async putSecretValue(input) {
|
|
calls.push({ op: "putSecretValue", input });
|
|
return {
|
|
ARN: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-1/openai-api-key",
|
|
VersionId: "aws-version-2",
|
|
};
|
|
},
|
|
async getSecretValue() {
|
|
throw new Error("not used");
|
|
},
|
|
async deleteSecret() {
|
|
throw new Error("not used");
|
|
},
|
|
},
|
|
});
|
|
|
|
const prepared = await provider.createVersion({
|
|
value: "rotated-secret-value",
|
|
externalRef:
|
|
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-1/openai-api-key",
|
|
context: {
|
|
companyId: "company-1",
|
|
secretKey: "openai-api-key",
|
|
secretName: "OpenAI API Key",
|
|
version: 2,
|
|
},
|
|
});
|
|
|
|
expect(calls).toEqual([
|
|
{
|
|
op: "putSecretValue",
|
|
input: {
|
|
SecretId:
|
|
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-1/openai-api-key",
|
|
SecretString: "rotated-secret-value",
|
|
VersionStages: ["PAPERCLIP_PENDING"],
|
|
},
|
|
},
|
|
]);
|
|
expect(JSON.stringify(prepared)).not.toContain("rotated-secret-value");
|
|
expect(prepared.providerVersionRef).toBe("aws-version-2");
|
|
});
|
|
|
|
it("rejects out-of-namespace refs for managed AWS secret version writes", async () => {
|
|
const calls: Array<{ op: string; input: Record<string, unknown> }> = [];
|
|
const provider = createAwsSecretsManagerProvider({
|
|
config: {
|
|
region: "us-east-1",
|
|
endpoint: "https://secretsmanager.us-east-1.amazonaws.com",
|
|
deploymentId: "prod-use1",
|
|
prefix: "paperclip",
|
|
kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/test",
|
|
environmentTag: "production",
|
|
providerOwnerTag: "paperclip",
|
|
deleteRecoveryWindowDays: 30,
|
|
},
|
|
gateway: {
|
|
async createSecret() {
|
|
throw new Error("not used");
|
|
},
|
|
async putSecretValue(input) {
|
|
calls.push({ op: "putSecretValue", input });
|
|
return { Name: String(input.SecretId), VersionId: "aws-version-2" };
|
|
},
|
|
async getSecretValue() {
|
|
throw new Error("not used");
|
|
},
|
|
async deleteSecret() {
|
|
throw new Error("not used");
|
|
},
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
provider.createVersion({
|
|
value: "rotated-secret-value",
|
|
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/attacker",
|
|
context: {
|
|
companyId: "company-1",
|
|
secretKey: "openai-api-key",
|
|
secretName: "OpenAI API Key",
|
|
version: 2,
|
|
},
|
|
}),
|
|
).rejects.toThrow(/drifted outside the derived deployment\/company scope/i);
|
|
|
|
expect(calls).toEqual([]);
|
|
});
|
|
|
|
it("stores linked external references as metadata-only provider material", async () => {
|
|
const provider = createAwsSecretsManagerProvider({
|
|
config: {
|
|
region: "us-east-1",
|
|
endpoint: "https://secretsmanager.us-east-1.amazonaws.com",
|
|
deploymentId: "prod-use1",
|
|
prefix: "paperclip",
|
|
kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/test",
|
|
environmentTag: "production",
|
|
providerOwnerTag: "paperclip",
|
|
deleteRecoveryWindowDays: 30,
|
|
},
|
|
});
|
|
|
|
const prepared = await provider.linkExternalSecret({
|
|
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/external",
|
|
providerVersionRef: "linked-version-7",
|
|
});
|
|
|
|
expect(prepared.externalRef).toBe(
|
|
"arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/external",
|
|
);
|
|
expect(prepared.providerVersionRef).toBe("linked-version-7");
|
|
expect(prepared.valueSha256).toBeTruthy();
|
|
});
|
|
|
|
it("rejects linked external references under the Paperclip-managed namespace", async () => {
|
|
const provider = createAwsSecretsManagerProvider({
|
|
config: {
|
|
region: "us-east-1",
|
|
endpoint: "https://secretsmanager.us-east-1.amazonaws.com",
|
|
deploymentId: "prod-use1",
|
|
prefix: "paperclip",
|
|
kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/test",
|
|
environmentTag: "production",
|
|
providerOwnerTag: "paperclip",
|
|
deleteRecoveryWindowDays: 30,
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
provider.linkExternalSecret({
|
|
externalRef:
|
|
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-2/openai-api-key",
|
|
providerVersionRef: "linked-version-7",
|
|
}),
|
|
).rejects.toThrow(/Paperclip-managed namespace/i);
|
|
});
|
|
|
|
it("writes new values through to externally referenced AWS secrets as AWSCURRENT", async () => {
|
|
const calls: Array<{ op: string; input: Record<string, unknown> }> = [];
|
|
const provider = createAwsSecretsManagerProvider({
|
|
config: {
|
|
region: "us-east-1",
|
|
endpoint: "https://secretsmanager.us-east-1.amazonaws.com",
|
|
deploymentId: "prod-use1",
|
|
prefix: "paperclip",
|
|
kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/test",
|
|
environmentTag: "production",
|
|
providerOwnerTag: "paperclip",
|
|
deleteRecoveryWindowDays: 30,
|
|
},
|
|
gateway: {
|
|
async createSecret() {
|
|
throw new Error("not used");
|
|
},
|
|
async putSecretValue(input) {
|
|
calls.push({ op: "putSecretValue", input });
|
|
return {
|
|
ARN: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/external",
|
|
VersionId: "aws-version-9",
|
|
};
|
|
},
|
|
async getSecretValue(input) {
|
|
calls.push({ op: "getSecretValue", input });
|
|
return { VersionId: "aws-version-8", SecretString: "previous-value" };
|
|
},
|
|
async deleteSecret() {
|
|
throw new Error("not used");
|
|
},
|
|
},
|
|
});
|
|
|
|
const prepared = await provider.updateExternalSecretValue!({
|
|
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/external",
|
|
value: "written-through-value",
|
|
context: {
|
|
companyId: "company-1",
|
|
secretKey: "neon-admin-api-key",
|
|
secretName: "paperclip-cloud/prod/provider/neon/admin-api-key",
|
|
version: 2,
|
|
},
|
|
});
|
|
|
|
// No VersionStages: the write must become AWSCURRENT for all consumers.
|
|
expect(calls).toEqual([
|
|
{
|
|
op: "getSecretValue",
|
|
input: {
|
|
SecretId: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/external",
|
|
VersionStage: "AWSCURRENT",
|
|
},
|
|
},
|
|
{
|
|
op: "putSecretValue",
|
|
input: {
|
|
SecretId: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/external",
|
|
SecretString: "written-through-value",
|
|
},
|
|
},
|
|
]);
|
|
expect(JSON.stringify(prepared.material)).not.toContain("written-through-value");
|
|
expect(prepared.externalRef).toBe(
|
|
"arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/external",
|
|
);
|
|
// Resolution keeps tracking AWSCURRENT rather than pinning the written version.
|
|
expect(prepared.providerVersionRef).toBeNull();
|
|
expect(prepared.material.versionId).toBeNull();
|
|
expect(prepared.material.lastWrittenVersionId).toBe("aws-version-9");
|
|
expect(prepared.material.previousCurrentVersionId).toBe("aws-version-8");
|
|
expect(prepared.material.source).toBe("external_reference");
|
|
});
|
|
|
|
it("rejects external value writes under the Paperclip-managed namespace", async () => {
|
|
const provider = createAwsSecretsManagerProvider({
|
|
config: {
|
|
region: "us-east-1",
|
|
endpoint: "https://secretsmanager.us-east-1.amazonaws.com",
|
|
deploymentId: "prod-use1",
|
|
prefix: "paperclip",
|
|
kmsKeyId: null,
|
|
environmentTag: "production",
|
|
providerOwnerTag: "paperclip",
|
|
deleteRecoveryWindowDays: 30,
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
provider.updateExternalSecretValue!({
|
|
externalRef:
|
|
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-2/openai-api-key",
|
|
value: "new-value",
|
|
}),
|
|
).rejects.toThrow(/Paperclip-managed namespace/i);
|
|
});
|
|
|
|
it("restores the previous AWSCURRENT version when an external value write is rolled back", async () => {
|
|
const calls: Array<{ op: string; input: Record<string, unknown> }> = [];
|
|
const provider = createAwsSecretsManagerProvider({
|
|
config: {
|
|
region: "us-east-1",
|
|
endpoint: "https://secretsmanager.us-east-1.amazonaws.com",
|
|
deploymentId: "prod-use1",
|
|
prefix: "paperclip",
|
|
kmsKeyId: null,
|
|
environmentTag: "production",
|
|
providerOwnerTag: "paperclip",
|
|
deleteRecoveryWindowDays: 30,
|
|
},
|
|
gateway: {
|
|
async createSecret() { throw new Error("not used"); },
|
|
async putSecretValue() { throw new Error("not used"); },
|
|
async getSecretValue() { throw new Error("not used"); },
|
|
async deleteSecret() { throw new Error("not used"); },
|
|
async updateSecretVersionStage(input) {
|
|
calls.push({ op: "updateSecretVersionStage", input });
|
|
},
|
|
},
|
|
});
|
|
|
|
await provider.deleteOrArchive({
|
|
material: {
|
|
scheme: "aws_secrets_manager_v1",
|
|
secretId: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/external",
|
|
versionId: null,
|
|
source: "external_reference",
|
|
lastWrittenVersionId: "aws-version-9",
|
|
previousCurrentVersionId: "aws-version-8",
|
|
},
|
|
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/external",
|
|
mode: "archive",
|
|
});
|
|
|
|
expect(calls).toEqual([
|
|
{
|
|
op: "updateSecretVersionStage",
|
|
input: {
|
|
SecretId: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/external",
|
|
VersionStage: "AWSCURRENT",
|
|
MoveToVersionId: "aws-version-8",
|
|
RemoveFromVersionId: "aws-version-9",
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("lists remote AWS secrets with metadata only and never resolves plaintext", async () => {
|
|
const calls: Array<{ op: string; input: Record<string, unknown> }> = [];
|
|
const provider = createAwsSecretsManagerProvider({
|
|
config: {
|
|
region: "us-east-1",
|
|
endpoint: "https://secretsmanager.us-east-1.amazonaws.com",
|
|
deploymentId: "prod-use1",
|
|
prefix: "paperclip",
|
|
kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/test",
|
|
environmentTag: "production",
|
|
providerOwnerTag: "paperclip",
|
|
deleteRecoveryWindowDays: 30,
|
|
},
|
|
gateway: {
|
|
async createSecret() {
|
|
throw new Error("not used");
|
|
},
|
|
async putSecretValue() {
|
|
throw new Error("not used");
|
|
},
|
|
async getSecretValue() {
|
|
throw new Error("GetSecretValue must not be used for remote import preview");
|
|
},
|
|
async deleteSecret() {
|
|
throw new Error("not used");
|
|
},
|
|
async listSecrets(input) {
|
|
calls.push({ op: "listSecrets", input });
|
|
return {
|
|
NextToken: "token-2",
|
|
SecretList: [
|
|
{
|
|
ARN: "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/openai",
|
|
Name: "prod/openai",
|
|
Description: "OpenAI API key",
|
|
CreatedDate: new Date("2026-05-06T00:00:00.000Z"),
|
|
Tags: [{ Key: "team", Value: "platform" }],
|
|
},
|
|
],
|
|
};
|
|
},
|
|
},
|
|
});
|
|
|
|
const listed = await provider.listRemoteSecrets?.({
|
|
query: "openai",
|
|
nextToken: "token-1",
|
|
pageSize: 25,
|
|
});
|
|
|
|
expect(calls).toEqual([
|
|
{
|
|
op: "listSecrets",
|
|
input: {
|
|
MaxResults: 25,
|
|
NextToken: "token-1",
|
|
IncludePlannedDeletion: false,
|
|
Filters: [{ Key: "all", Values: ["openai"] }],
|
|
},
|
|
},
|
|
]);
|
|
expect(listed).toEqual({
|
|
nextToken: "token-2",
|
|
secrets: [
|
|
{
|
|
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/openai",
|
|
name: "prod/openai",
|
|
providerVersionRef: null,
|
|
metadata: expect.objectContaining({
|
|
createdDate: "2026-05-06T00:00:00.000Z",
|
|
hasDescription: true,
|
|
tagCount: 1,
|
|
}),
|
|
},
|
|
],
|
|
});
|
|
expect(JSON.stringify(listed)).not.toContain("SecretString");
|
|
expect(JSON.stringify(listed)).not.toContain("OpenAI API key");
|
|
expect(JSON.stringify(listed)).not.toContain("team");
|
|
});
|
|
|
|
it("discovers AWS provider vault prefill candidates from metadata without reading values", async () => {
|
|
const calls: Array<{ op: string; input: Record<string, unknown> }> = [];
|
|
const provider = createAwsSecretsManagerProvider({
|
|
gateway: {
|
|
async createSecret() {
|
|
throw new Error("not used");
|
|
},
|
|
async putSecretValue() {
|
|
throw new Error("not used");
|
|
},
|
|
async getSecretValue() {
|
|
throw new Error("GetSecretValue must not be used for provider vault discovery");
|
|
},
|
|
async deleteSecret() {
|
|
throw new Error("not used");
|
|
},
|
|
async listSecrets(input) {
|
|
calls.push({ op: "listSecrets", input });
|
|
return {
|
|
NextToken: "next-page",
|
|
SecretList: [
|
|
{
|
|
ARN: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-1/openai",
|
|
Name: "paperclip/prod-use1/company-1/openai",
|
|
KmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/prod",
|
|
Tags: [
|
|
{ Key: "paperclip:managed-by", Value: "paperclip" },
|
|
{ Key: "paperclip:deployment-id", Value: "prod-use1" },
|
|
{ Key: "paperclip:company-id", Value: "company-1" },
|
|
{ Key: "paperclip:environment", Value: "production" },
|
|
{ Key: "paperclip:provider-owner", Value: "platform" },
|
|
],
|
|
},
|
|
{
|
|
ARN: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-2/stripe",
|
|
Name: "paperclip/prod-use1/company-2/stripe",
|
|
Tags: [
|
|
{ Key: "paperclip:managed-by", Value: "paperclip" },
|
|
{ Key: "paperclip:company-id", Value: "company-2" },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
},
|
|
},
|
|
});
|
|
|
|
const preview = await provider.discoverProviderConfigs?.({
|
|
companyId: "company-1",
|
|
providerConfig: {
|
|
id: "draft",
|
|
provider: "aws_secrets_manager",
|
|
status: "ready",
|
|
config: { region: "us-east-1" },
|
|
},
|
|
query: "paperclip",
|
|
pageSize: 25,
|
|
});
|
|
|
|
expect(calls).toEqual([
|
|
{
|
|
op: "listSecrets",
|
|
input: {
|
|
MaxResults: 25,
|
|
NextToken: undefined,
|
|
IncludePlannedDeletion: false,
|
|
Filters: [{ Key: "all", Values: ["paperclip"] }],
|
|
},
|
|
},
|
|
]);
|
|
expect(preview).toMatchObject({
|
|
provider: "aws_secrets_manager",
|
|
nextToken: "next-page",
|
|
sampledSecretCount: 1,
|
|
skippedForeignPaperclipSampleCount: 1,
|
|
candidates: [
|
|
expect.objectContaining({
|
|
displayName: "AWS production",
|
|
config: expect.objectContaining({
|
|
region: "us-east-1",
|
|
namespace: "prod-use1",
|
|
secretNamePrefix: "paperclip",
|
|
kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/prod",
|
|
ownerTag: "platform",
|
|
environmentTag: "production",
|
|
}),
|
|
signals: expect.objectContaining({
|
|
paperclipManagedSampleCount: 1,
|
|
skippedForeignPaperclipSampleCount: 1,
|
|
}),
|
|
}),
|
|
],
|
|
});
|
|
expect(JSON.stringify(preview)).not.toContain("SecretString");
|
|
expect(JSON.stringify(preview)).not.toContain("company-2/stripe");
|
|
});
|
|
|
|
it("redacts AWS provider exception text when remote listing fails", async () => {
|
|
const rawProviderMessage =
|
|
"AccessDeniedException: User: arn:aws:sts::123456789012:assumed-role/prod/Paperclip is not authorized to perform secretsmanager:ListSecrets on arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/openai";
|
|
const provider = createAwsSecretsManagerProvider({
|
|
config: {
|
|
region: "us-east-1",
|
|
endpoint: "https://secretsmanager.us-east-1.amazonaws.com",
|
|
deploymentId: "prod-use1",
|
|
prefix: "paperclip",
|
|
kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/test",
|
|
environmentTag: "production",
|
|
providerOwnerTag: "paperclip",
|
|
deleteRecoveryWindowDays: 30,
|
|
},
|
|
gateway: {
|
|
async createSecret() {
|
|
throw new Error("not used");
|
|
},
|
|
async putSecretValue() {
|
|
throw new Error("not used");
|
|
},
|
|
async getSecretValue() {
|
|
throw new Error("not used");
|
|
},
|
|
async deleteSecret() {
|
|
throw new Error("not used");
|
|
},
|
|
async listSecrets() {
|
|
throw new Error(rawProviderMessage);
|
|
},
|
|
},
|
|
});
|
|
|
|
let thrown: unknown;
|
|
try {
|
|
await provider.listRemoteSecrets?.({});
|
|
} catch (error) {
|
|
thrown = error;
|
|
}
|
|
|
|
expect(thrown).toBeInstanceOf(SecretProviderClientError);
|
|
expect(thrown).toMatchObject({
|
|
code: "access_denied",
|
|
status: 403,
|
|
message: "AWS Secrets Manager denied the request. Check IAM permissions for this provider vault.",
|
|
rawMessage: rawProviderMessage,
|
|
});
|
|
expect(thrown instanceof Error ? thrown.message : String(thrown)).not.toContain("arn:aws");
|
|
expect(thrown instanceof Error ? thrown.message : String(thrown)).not.toContain("123456789012");
|
|
});
|
|
|
|
it("resolves AWS secret values by provider version reference", async () => {
|
|
const calls: Array<{ op: string; input: Record<string, unknown> }> = [];
|
|
const provider = createAwsSecretsManagerProvider({
|
|
config: {
|
|
region: "us-east-1",
|
|
endpoint: "https://secretsmanager.us-east-1.amazonaws.com",
|
|
deploymentId: "prod-use1",
|
|
prefix: "paperclip",
|
|
kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/test",
|
|
environmentTag: "production",
|
|
providerOwnerTag: "paperclip",
|
|
deleteRecoveryWindowDays: 30,
|
|
},
|
|
gateway: {
|
|
async createSecret() {
|
|
throw new Error("not used");
|
|
},
|
|
async putSecretValue() {
|
|
throw new Error("not used");
|
|
},
|
|
async getSecretValue(input) {
|
|
calls.push({ op: "getSecretValue", input });
|
|
return { SecretString: "resolved-secret-value", VersionId: "aws-version-2" };
|
|
},
|
|
async deleteSecret() {
|
|
throw new Error("not used");
|
|
},
|
|
},
|
|
});
|
|
|
|
const resolved = await provider.resolveVersion({
|
|
material: {
|
|
scheme: "aws_secrets_manager_v1",
|
|
secretId: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-1/openai-api-key",
|
|
versionId: "aws-version-2",
|
|
source: "managed",
|
|
},
|
|
externalRef:
|
|
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-1/openai-api-key",
|
|
providerVersionRef: "aws-version-2",
|
|
context: {
|
|
companyId: "company-1",
|
|
secretId: "secret-1",
|
|
secretKey: "openai-api-key",
|
|
version: 2,
|
|
},
|
|
});
|
|
|
|
expect(resolved).toBe("resolved-secret-value");
|
|
expect(calls).toEqual([
|
|
{
|
|
op: "getSecretValue",
|
|
input: {
|
|
SecretId:
|
|
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-1/openai-api-key",
|
|
VersionId: "aws-version-2",
|
|
VersionStage: undefined,
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("rejects managed resolve attempts when stored refs drift outside the derived scope", async () => {
|
|
const provider = createAwsSecretsManagerProvider({
|
|
config: {
|
|
region: "us-east-1",
|
|
endpoint: "https://secretsmanager.us-east-1.amazonaws.com",
|
|
deploymentId: "prod-use1",
|
|
prefix: "paperclip",
|
|
kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/test",
|
|
environmentTag: "production",
|
|
providerOwnerTag: "paperclip",
|
|
deleteRecoveryWindowDays: 30,
|
|
},
|
|
gateway: {
|
|
async createSecret() {
|
|
throw new Error("not used");
|
|
},
|
|
async putSecretValue() {
|
|
throw new Error("not used");
|
|
},
|
|
async getSecretValue() {
|
|
throw new Error("should not be called");
|
|
},
|
|
async deleteSecret() {
|
|
throw new Error("not used");
|
|
},
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
provider.resolveVersion({
|
|
material: {
|
|
scheme: "aws_secrets_manager_v1",
|
|
secretId:
|
|
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-2/openai-api-key",
|
|
versionId: "aws-version-2",
|
|
source: "managed",
|
|
},
|
|
externalRef:
|
|
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-2/openai-api-key",
|
|
providerVersionRef: "aws-version-2",
|
|
context: {
|
|
companyId: "company-1",
|
|
secretId: "secret-1",
|
|
secretKey: "openai-api-key",
|
|
version: 2,
|
|
},
|
|
}),
|
|
).rejects.toThrow(/drifted outside the derived deployment\/company scope/i);
|
|
});
|
|
|
|
it("warns when AWS provider configuration is incomplete and blocks managed writes", async () => {
|
|
delete process.env.PAPERCLIP_SECRETS_AWS_REGION;
|
|
delete process.env.AWS_REGION;
|
|
delete process.env.AWS_DEFAULT_REGION;
|
|
delete process.env.PAPERCLIP_SECRETS_AWS_DEPLOYMENT_ID;
|
|
delete process.env.PAPERCLIP_SECRETS_AWS_KMS_KEY_ID;
|
|
|
|
const provider = createAwsSecretsManagerProvider();
|
|
const health = await provider.healthCheck();
|
|
|
|
expect(health.status).toBe("warn");
|
|
expect(health.message).toContain("missing PAPERCLIP_SECRETS_AWS_REGION");
|
|
expect(health.warnings).toEqual(
|
|
expect.arrayContaining([
|
|
expect.stringContaining("Missing required non-secret AWS provider config"),
|
|
expect.stringContaining("AWS bootstrap credentials must be available"),
|
|
expect.stringContaining("Do not store AWS root credentials"),
|
|
]),
|
|
);
|
|
expect(health.details).toMatchObject({
|
|
missingConfig: [
|
|
"PAPERCLIP_SECRETS_AWS_REGION or AWS_REGION/AWS_DEFAULT_REGION",
|
|
"PAPERCLIP_SECRETS_AWS_DEPLOYMENT_ID",
|
|
"PAPERCLIP_SECRETS_AWS_KMS_KEY_ID",
|
|
],
|
|
credentialSource: "AWS SDK default credential provider chain",
|
|
});
|
|
await expect(
|
|
provider.createSecret({
|
|
value: "super-secret-value",
|
|
context: {
|
|
companyId: "company-1",
|
|
secretKey: "openai-api-key",
|
|
secretName: "OpenAI API Key",
|
|
version: 1,
|
|
},
|
|
}),
|
|
).rejects.toThrow(/PAPERCLIP_SECRETS_AWS_REGION|AWS_REGION/i);
|
|
});
|
|
|
|
it("deletes only Paperclip-managed AWS secrets", async () => {
|
|
const calls: Array<{ op: string; input: Record<string, unknown> }> = [];
|
|
const provider = createAwsSecretsManagerProvider({
|
|
config: {
|
|
region: "us-east-1",
|
|
endpoint: "https://secretsmanager.us-east-1.amazonaws.com",
|
|
deploymentId: "prod-use1",
|
|
prefix: "paperclip",
|
|
kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/test",
|
|
environmentTag: "production",
|
|
providerOwnerTag: "paperclip",
|
|
deleteRecoveryWindowDays: 30,
|
|
},
|
|
gateway: {
|
|
async createSecret() {
|
|
throw new Error("not used");
|
|
},
|
|
async putSecretValue() {
|
|
throw new Error("not used");
|
|
},
|
|
async getSecretValue() {
|
|
throw new Error("not used");
|
|
},
|
|
async deleteSecret(input) {
|
|
calls.push({ op: "deleteSecret", input });
|
|
return {};
|
|
},
|
|
},
|
|
});
|
|
|
|
await provider.deleteOrArchive({
|
|
mode: "delete",
|
|
externalRef:
|
|
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-1/openai-api-key",
|
|
material: {
|
|
scheme: "aws_secrets_manager_v1",
|
|
secretId:
|
|
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-1/openai-api-key",
|
|
versionId: null,
|
|
source: "managed",
|
|
},
|
|
context: {
|
|
companyId: "company-1",
|
|
secretKey: "openai-api-key",
|
|
secretName: "OpenAI API Key",
|
|
version: 2,
|
|
},
|
|
});
|
|
await expect(
|
|
provider.deleteOrArchive({
|
|
mode: "delete",
|
|
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/attacker",
|
|
material: {
|
|
scheme: "aws_secrets_manager_v1",
|
|
secretId: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/attacker",
|
|
versionId: null,
|
|
source: "managed",
|
|
},
|
|
context: {
|
|
companyId: "company-1",
|
|
secretKey: "openai-api-key",
|
|
secretName: "OpenAI API Key",
|
|
version: 2,
|
|
},
|
|
}),
|
|
).rejects.toThrow(/drifted outside the derived deployment\/company scope/i);
|
|
await provider.deleteOrArchive({
|
|
mode: "delete",
|
|
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/external",
|
|
material: {
|
|
scheme: "aws_secrets_manager_v1",
|
|
secretId: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/external",
|
|
versionId: "linked-version-7",
|
|
source: "external_reference",
|
|
},
|
|
context: {
|
|
companyId: "company-1",
|
|
secretKey: "openai-api-key",
|
|
secretName: "OpenAI API Key",
|
|
version: 2,
|
|
},
|
|
});
|
|
|
|
expect(calls).toEqual([
|
|
{
|
|
op: "deleteSecret",
|
|
input: {
|
|
SecretId:
|
|
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-1/openai-api-key",
|
|
RecoveryWindowInDays: 30,
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("archives pending Paperclip-managed AWS versions without deleting the secret", async () => {
|
|
const calls: Array<{ op: string; input: Record<string, unknown> }> = [];
|
|
const provider = createAwsSecretsManagerProvider({
|
|
config: {
|
|
region: "us-east-1",
|
|
endpoint: "https://secretsmanager.us-east-1.amazonaws.com",
|
|
deploymentId: "prod-use1",
|
|
prefix: "paperclip",
|
|
kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/test",
|
|
environmentTag: "production",
|
|
providerOwnerTag: "paperclip",
|
|
deleteRecoveryWindowDays: 30,
|
|
},
|
|
gateway: {
|
|
async createSecret() {
|
|
throw new Error("not used");
|
|
},
|
|
async putSecretValue() {
|
|
throw new Error("not used");
|
|
},
|
|
async getSecretValue() {
|
|
throw new Error("not used");
|
|
},
|
|
async deleteSecret(input) {
|
|
calls.push({ op: "deleteSecret", input });
|
|
return {};
|
|
},
|
|
async updateSecretVersionStage(input) {
|
|
calls.push({ op: "updateSecretVersionStage", input });
|
|
return {};
|
|
},
|
|
},
|
|
});
|
|
|
|
await provider.deleteOrArchive({
|
|
mode: "archive",
|
|
externalRef:
|
|
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-1/openai-api-key",
|
|
material: {
|
|
scheme: "aws_secrets_manager_v1",
|
|
secretId:
|
|
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-1/openai-api-key",
|
|
versionId: "aws-version-2",
|
|
source: "managed",
|
|
},
|
|
context: {
|
|
companyId: "company-1",
|
|
secretKey: "openai-api-key",
|
|
secretName: "OpenAI API Key",
|
|
version: 2,
|
|
},
|
|
});
|
|
|
|
expect(calls).toEqual([
|
|
{
|
|
op: "updateSecretVersionStage",
|
|
input: {
|
|
SecretId:
|
|
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-1/openai-api-key",
|
|
VersionStage: "PAPERCLIP_PENDING",
|
|
RemoveFromVersionId: "aws-version-2",
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
});
|