fix(secrets): preserve external rotation selectors

Reject empty rotation requests while still allowing external-reference rotations that intentionally supply only provider version or provider config selectors.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-01 21:05:47 +00:00
1 parent 7a5738b6dc
commit b04069d14d
3 files changed
+23 -10

No files matched your search

@@ -63,12 +63,15 @@ describe("secret validators", () => {
});
});
it("requires secret rotation payloads to include new material", () => {
expect(() => rotateSecretSchema.parse({})).toThrow(/requires value or externalRef/);
expect(() => rotateUserSecretValueSchema.parse({})).toThrow(/requires value or externalRef/);
it("requires secret rotation payloads to include rotation input", () => {
expect(() => rotateSecretSchema.parse({})).toThrow(/requires value, externalRef/);
expect(() => rotateUserSecretValueSchema.parse({})).toThrow(/requires value, externalRef/);
expect(rotateUserSecretValueSchema.parse({ value: "new-secret" })).toEqual({
value: "new-secret",
});
expect(rotateUserSecretValueSchema.parse({ providerVersionRef: "version-2" })).toEqual({
providerVersionRef: "version-2",
});
});
it("rejects externalRef on managed secrets", () => {
+16 -6
View File
@@ -79,15 +79,25 @@ export const createSecretSchema = z.object({
export type CreateSecret = z.infer<typeof createSecretSchema>;
function requireSecretRotationMaterial(
value: { value?: string | null; externalRef?: string | null },
function requireSecretRotationInput(
value: {
value?: string | null;
externalRef?: string | null;
providerVersionRef?: string | null;
providerConfigId?: string | null;
},
ctx: z.RefinementCtx,
) {
if (!value.value?.trim() && !value.externalRef?.trim()) {
if (
!value.value?.trim() &&
!value.externalRef?.trim() &&
value.providerVersionRef === undefined &&
value.providerConfigId === undefined
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["value"],
message: "Secret rotation requires value or externalRef",
message: "Secret rotation requires value, externalRef, providerVersionRef, or providerConfigId",
});
}
}
@@ -97,7 +107,7 @@ export const rotateSecretSchema = z.object({
externalRef: z.string().optional().nullable(),
providerVersionRef: z.string().optional().nullable(),
providerConfigId: z.string().uuid().optional().nullable(),
}).superRefine(requireSecretRotationMaterial);
}).superRefine(requireSecretRotationInput);
export type RotateSecret = z.infer<typeof rotateSecretSchema>;
@@ -194,7 +204,7 @@ export const rotateUserSecretValueSchema = z.object({
externalRef: z.string().optional().nullable(),
providerVersionRef: z.string().optional().nullable(),
providerConfigId: z.string().uuid().optional().nullable(),
}).superRefine(requireSecretRotationMaterial);
}).superRefine(requireSecretRotationInput);
export type RotateUserSecretValue = z.infer<typeof rotateUserSecretValueSchema>;
+1 -1
View File
@@ -162,7 +162,7 @@ describe("secret routes", () => {
.send({});
expect(res.status).toBe(400);
expect(JSON.stringify(res.body)).toMatch(/requires value or externalRef/);
expect(JSON.stringify(res.body)).toMatch(/requires value, externalRef/);
expect(mockSecretService.rotateCurrentUserSecretValue).not.toHaveBeenCalled();
});