Fix Forgejo token secret ref validation
This commit is contained in:
parent
fc50e74989
commit
d95caa1cb9
5 changed files with 136 additions and 2 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import type { ForgejoPluginConfig } from "./types.js";
|
||||
import { FORGEJO_TOKEN_REF_FORMAT_MESSAGE, isPaperclipSecretRef } from "./secret-ref.js";
|
||||
|
||||
function optionalString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
||||
|
|
@ -39,6 +40,10 @@ export function validateConfig(raw: Record<string, unknown>): { ok: boolean; err
|
|||
errors.push("forgejoBaseUrl, forgejoTokenRef, forgejoOwner, and forgejoRepo must all be configured together.");
|
||||
}
|
||||
|
||||
if (config.forgejoTokenRef && !isPaperclipSecretRef(config.forgejoTokenRef)) {
|
||||
errors.push(FORGEJO_TOKEN_REF_FORMAT_MESSAGE);
|
||||
}
|
||||
|
||||
if (!config.defaultCompanyId) {
|
||||
warnings.push("defaultCompanyId is not set; the reconciliation job cannot backfill unsynced issues across a company.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { PluginContext } from "@paperclipai/plugin-sdk";
|
||||
import { readConfig } from "./config.js";
|
||||
import { assertForgejoTokenRef } from "./secret-ref.js";
|
||||
import type { ForgejoIssuePayload, ForgejoIssueRecord } from "./types.js";
|
||||
|
||||
function joinUrl(baseUrl: string, path: string): string {
|
||||
|
|
@ -15,7 +16,17 @@ export async function createForgejoIssue(
|
|||
throw new Error("Forgejo outbound sync is not fully configured.");
|
||||
}
|
||||
|
||||
const token = await ctx.secrets.resolve(config.forgejoTokenRef);
|
||||
const secretRef = assertForgejoTokenRef(config.forgejoTokenRef);
|
||||
let token: string;
|
||||
try {
|
||||
token = await ctx.secrets.resolve(secretRef);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
"Failed to resolve forgejoTokenRef. Use a Paperclip company secret UUID and re-save the field through the secret picker if it currently contains a visible secret name.",
|
||||
{ cause: error }
|
||||
);
|
||||
}
|
||||
|
||||
const response = await ctx.http.fetch(
|
||||
joinUrl(config.forgejoBaseUrl, `/api/v1/repos/${encodeURIComponent(config.forgejoOwner)}/${encodeURIComponent(config.forgejoRepo)}/issues`),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const manifest: PaperclipPluginManifestV1 = {
|
|||
type: "string",
|
||||
title: "Forgejo Token Secret Ref",
|
||||
format: "secret-ref",
|
||||
description: "Secret reference for outbound Forgejo API authentication."
|
||||
description: "Paperclip company secret reference UUID for outbound Forgejo API authentication."
|
||||
},
|
||||
forgejoOwner: {
|
||||
type: "string",
|
||||
|
|
|
|||
17
src/secret-ref.ts
Normal file
17
src/secret-ref.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
const PAPERCLIP_SECRET_REF_RE =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
export const FORGEJO_TOKEN_REF_FORMAT_MESSAGE =
|
||||
"forgejoTokenRef must be a Paperclip secret reference UUID. Re-save the field through the Paperclip secret picker or paste the secret UUID, not the visible secret name.";
|
||||
|
||||
export function isPaperclipSecretRef(value: string): boolean {
|
||||
return PAPERCLIP_SECRET_REF_RE.test(value.trim());
|
||||
}
|
||||
|
||||
export function assertForgejoTokenRef(secretRef: string | undefined): string {
|
||||
if (!secretRef || !isPaperclipSecretRef(secretRef)) {
|
||||
throw new Error(FORGEJO_TOKEN_REF_FORMAT_MESSAGE);
|
||||
}
|
||||
|
||||
return secretRef.trim();
|
||||
}
|
||||
101
tests/forgejo-client.spec.ts
Normal file
101
tests/forgejo-client.spec.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { validateConfig } from "../src/config.js";
|
||||
import { createForgejoIssue } from "../src/forgejo-client.js";
|
||||
|
||||
describe("forgejo client", () => {
|
||||
it("resolves a Paperclip-managed secret ref and sends the Forgejo token", async () => {
|
||||
const secretRef = "11111111-1111-4111-8111-111111111111";
|
||||
const resolveSecret = vi.fn(async () => "forgejo-token");
|
||||
const fetch = vi.fn(async () => ({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
id: 42,
|
||||
number: 7,
|
||||
html_url: "https://forgejo.example/acme/repo/issues/7",
|
||||
url: "https://forgejo.example/api/v1/repos/acme/repo/issues/7"
|
||||
})
|
||||
}));
|
||||
|
||||
const result = await createForgejoIssue(
|
||||
{
|
||||
config: {
|
||||
get: async () => ({
|
||||
forgejoBaseUrl: "https://forgejo.example",
|
||||
forgejoTokenRef: secretRef,
|
||||
forgejoOwner: "acme",
|
||||
forgejoRepo: "repo"
|
||||
})
|
||||
},
|
||||
secrets: { resolve: resolveSecret },
|
||||
http: { fetch }
|
||||
} as never,
|
||||
{
|
||||
title: "Example issue",
|
||||
body: "body"
|
||||
}
|
||||
);
|
||||
|
||||
expect(resolveSecret).toHaveBeenCalledWith(secretRef);
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"https://forgejo.example/api/v1/repos/acme/repo/issues",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: expect.objectContaining({
|
||||
authorization: "token forgejo-token"
|
||||
})
|
||||
})
|
||||
);
|
||||
expect(result).toEqual({
|
||||
id: 42,
|
||||
number: 7,
|
||||
url: "https://forgejo.example/acme/repo/issues/7",
|
||||
apiUrl: "https://forgejo.example/api/v1/repos/acme/repo/issues/7"
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects visible secret names with an actionable, sanitized error", async () => {
|
||||
const invalidRef = "forgejo-ake-paperclip-forgejo-issue-plugin-issues";
|
||||
const resolveSecret = vi.fn();
|
||||
const fetch = vi.fn();
|
||||
|
||||
expect(validateConfig({
|
||||
forgejoBaseUrl: "https://forgejo.example",
|
||||
forgejoTokenRef: invalidRef,
|
||||
forgejoOwner: "acme",
|
||||
forgejoRepo: "repo"
|
||||
})).toEqual({
|
||||
ok: false,
|
||||
errors: [
|
||||
"forgejoTokenRef must be a Paperclip secret reference UUID. Re-save the field through the Paperclip secret picker or paste the secret UUID, not the visible secret name."
|
||||
],
|
||||
warnings: [
|
||||
"defaultCompanyId is not set; the reconciliation job cannot backfill unsynced issues across a company.",
|
||||
"syncIssueLabel is not set; the plugin will use the default label \"forgejo-sync\"."
|
||||
]
|
||||
});
|
||||
|
||||
await expect(createForgejoIssue(
|
||||
{
|
||||
config: {
|
||||
get: async () => ({
|
||||
forgejoBaseUrl: "https://forgejo.example",
|
||||
forgejoTokenRef: invalidRef,
|
||||
forgejoOwner: "acme",
|
||||
forgejoRepo: "repo"
|
||||
})
|
||||
},
|
||||
secrets: { resolve: resolveSecret },
|
||||
http: { fetch }
|
||||
} as never,
|
||||
{
|
||||
title: "Example issue",
|
||||
body: "body"
|
||||
}
|
||||
)).rejects.toThrow(
|
||||
"forgejoTokenRef must be a Paperclip secret reference UUID. Re-save the field through the Paperclip secret picker or paste the secret UUID, not the visible secret name."
|
||||
);
|
||||
|
||||
expect(resolveSecret).not.toHaveBeenCalled();
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue