From 6911316fe31cc37f0c21d2f690e8ee8ad6928a23 Mon Sep 17 00:00:00 2001 From: Jill Regan Date: Wed, 18 Feb 2026 17:24:55 -0500 Subject: [PATCH] Add secret ref validation --- src/utils.test.ts | 54 +++++++++++++++++++++++++++++++++++++++++++++-- src/utils.ts | 29 ++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/src/utils.test.ts b/src/utils.test.ts index 7b066e5..d981470 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -1,7 +1,7 @@ import * as core from "@actions/core"; import * as exec from "@actions/exec"; import { read, setClientInfo } from "@1password/op-js"; -import { createClient } from "@1password/sdk"; +import { createClient, Secrets } from "@1password/sdk"; import { extractSecret, loadSecrets, @@ -25,6 +25,9 @@ jest.mock("@actions/exec", () => ({ jest.mock("@1password/op-js"); jest.mock("@1password/sdk", () => ({ createClient: jest.fn(), + Secrets: { + validateSecretReference: jest.fn(), + }, })); beforeEach(() => { @@ -170,11 +173,24 @@ describe("loadSecrets when using Connect", () => { expect(core.exportVariable).not.toHaveBeenCalled(); }); + it("fails with clear message when a secret reference is invalid", async () => { + (Secrets.validateSecretReference as jest.Mock).mockImplementationOnce( + () => { + throw new Error("invalid reference format"); + }, + ); + process.env.MOCK_SECRET = "op://bad/invalid-ref"; + + await expect(loadSecrets(true)).rejects.toThrow( + "Invalid secret reference(s): MOCK_SECRET", + ); + }); + describe("core.exportVariable", () => { it("is called when shouldExportEnv is true", async () => { await loadSecrets(true); - expect(core.exportVariable).toHaveBeenCalledTimes(1); + expect(core.exportVariable).toHaveBeenCalledTimes(2); }); it("is not called when shouldExportEnv is false", async () => { @@ -334,6 +350,40 @@ describe("loadSecrets when using Service Account", () => { expect(core.setSecret).toHaveBeenCalledTimes(3); }); }); + + describe("secret reference validation", () => { + it("fails with clear message when a secret reference is invalid", async () => { + process.env.MY_SECRET = "op://invalid/ref/form"; + (Secrets.validateSecretReference as jest.Mock).mockImplementationOnce( + () => { + throw new Error("invalid reference format"); + }, + ); + + await expect(loadSecrets(true)).rejects.toThrow( + "Invalid secret reference(s): MY_SECRET", + ); + expect(mockResolve).not.toHaveBeenCalled(); + }); + + it("validates all refs before resolving any secrets", async () => { + process.env.MY_SECRET = "op://vault/item/field"; + process.env.OTHER = "op://vault/other/item"; + (Secrets.validateSecretReference as jest.Mock).mockImplementation( + (ref: string) => { + if (ref === "op://vault/other/item") { + throw new Error("invalid"); + } + }, + ); + mockResolve.mockResolvedValue("value1"); + + await expect(loadSecrets(false)).rejects.toThrow( + "Invalid secret reference(s): OTHER", + ); + expect(mockResolve).not.toHaveBeenCalled(); + }); + }); }); describe("unsetPrevious", () => { diff --git a/src/utils.ts b/src/utils.ts index 383e103..9653956 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,7 @@ import * as core from "@actions/core"; import * as exec from "@actions/exec"; import { read, setClientInfo, semverToInt } from "@1password/op-js"; -import { createClient } from "@1password/sdk"; +import { createClient, Secrets } from "@1password/sdk"; import { version } from "../package.json"; import { authErr, @@ -37,6 +37,29 @@ export const getEnvVarNamesWithSecretRefs = (): string[] => process.env[key]?.startsWith("op://"), ); +const validateSecretRefs = (envNames: string[]): void => { + const invalid: string[] = []; + + for (const envName of envNames) { + const ref = process.env[envName]; + if (!ref) { + continue; + } + + try { + Secrets.validateSecretReference(ref); + } catch { + invalid.push(envName); + } + } + + // Throw an error if any secret references are invalid + if (invalid.length > 0) { + const names = invalid.join(", "); + throw new Error(`Invalid secret reference(s): ${names}`); + } +}; + const setResolvedSecret = ( envName: string, secretValue: string, @@ -102,6 +125,8 @@ const loadSecretsViaConnect = async ( } const envs = res.stdout.replace(/\n+$/g, "").split(/\r?\n/); + validateSecretRefs(envs); + for (const envName of envs) { extractSecret(envName, shouldExportEnv); } @@ -119,6 +144,8 @@ const loadSecretsViaServiceAccount = async ( return; } + validateSecretRefs(envs); + const token = process.env[envServiceAccountToken]; if (!token) { throw new Error(authErr);