mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
fix(plugins): accept secret_ref bindings in string secret-ref config fields
Plugin manifests declare credential fields as `type: "string", format:
"secret-ref"`, while the settings form, the plugin secrets handler and the
SDK all use the `{ type: "secret_ref", secretId, version }` binding object
(and reject a bare secret id). Saving the binding failed schema validation
with "must be string", so no value could be saved for such a field.
Validate the binding's secret id in its place; stored config is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
faaab3ccca
commit
6d04feb54e
2 files changed
+72
-1
No files matched your search
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { JsonSchema } from "@paperclipai/shared";
|
||||
import { validateInstanceConfig } from "../services/plugin-config-validator.js";
|
||||
|
||||
const SECRET_ID = "11111111-1111-4111-8111-111111111111";
|
||||
const binding = { type: "secret_ref", secretId: SECRET_ID, version: "latest" };
|
||||
|
||||
// The shape plugin manifests use: a string field marked as a secret reference.
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
apiKey: { type: "string", title: "API key", format: "secret-ref" },
|
||||
region: { type: "string" },
|
||||
auth: {
|
||||
type: "object",
|
||||
properties: { token: { type: "string", format: "secret-ref" } },
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
required: ["apiKey"],
|
||||
additionalProperties: false,
|
||||
} as unknown as JsonSchema;
|
||||
|
||||
describe("validateInstanceConfig secret-ref fields", () => {
|
||||
it("accepts the secret_ref binding object the settings form saves for a string secret-ref field", () => {
|
||||
const configJson = { apiKey: binding, region: "us", auth: { token: { ...binding, version: 2 } } };
|
||||
expect(validateInstanceConfig(configJson, schema)).toEqual({ valid: true });
|
||||
// Validation does not rewrite what gets stored or sent to the worker.
|
||||
expect(configJson.apiKey).toEqual(binding);
|
||||
});
|
||||
|
||||
it("still enforces the rest of the schema around a binding", () => {
|
||||
expect(validateInstanceConfig({ apiKey: binding, region: 5 }, schema)).toMatchObject({
|
||||
valid: false,
|
||||
errors: [{ field: "/region" }],
|
||||
});
|
||||
expect(validateInstanceConfig({ region: "us" }, schema).valid).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects objects that are not valid secret_ref bindings and objects in ordinary string fields", () => {
|
||||
expect(validateInstanceConfig({ apiKey: { type: "secret_ref", secretId: "not-a-uuid" } }, schema)).toMatchObject({
|
||||
valid: false,
|
||||
errors: [{ field: "/apiKey", message: "must be string" }],
|
||||
});
|
||||
expect(validateInstanceConfig({ apiKey: binding, region: binding }, schema)).toMatchObject({
|
||||
valid: false,
|
||||
errors: [{ field: "/region", message: "must be string" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,12 +11,33 @@
|
||||
import Ajv, { type ErrorObject } from "ajv";
|
||||
import addFormats from "ajv-formats";
|
||||
import type { JsonSchema } from "@paperclipai/shared";
|
||||
import {
|
||||
collectSecretRefPaths,
|
||||
parseSecretRefBindingObject,
|
||||
readConfigValueAtPath,
|
||||
writeConfigValueAtPath,
|
||||
} from "./json-schema-secret-refs.js";
|
||||
|
||||
export interface ConfigValidationResult {
|
||||
valid: boolean;
|
||||
errors?: { field: string; message: string }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Secret-ref fields are declared `type: "string"`, but the settings form and the
|
||||
* plugin secrets API store the `{ type: "secret_ref", secretId, version }` binding
|
||||
* object there. Validate the binding's secret id in its place so the schema
|
||||
* checks the field without rejecting the only shape the secrets handler accepts.
|
||||
*/
|
||||
function secretRefsAsIds(configJson: Record<string, unknown>, schema: JsonSchema) {
|
||||
let candidate = configJson;
|
||||
for (const path of collectSecretRefPaths(schema as Record<string, unknown>)) {
|
||||
const binding = parseSecretRefBindingObject(readConfigValueAtPath(candidate, path));
|
||||
if (binding) candidate = writeConfigValueAtPath(candidate, path, binding.secretId);
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a config object against a JSON Schema.
|
||||
*
|
||||
@@ -39,7 +60,7 @@ export function validateInstanceConfig(
|
||||
// hint only — UUID validation happens in the secrets handler at resolve time.
|
||||
ajv.addFormat("secret-ref", { validate: () => true });
|
||||
const validate = ajv.compile(schema);
|
||||
const valid = validate(configJson);
|
||||
const valid = validate(secretRefsAsIds(configJson, schema));
|
||||
|
||||
if (valid) {
|
||||
return { valid: true };
|
||||
|
||||
Reference in new issue
Block a user