From 6e760da93bb47a427d2c284e907473fa52860d58 Mon Sep 17 00:00:00 2001 From: Artur Spatari Date: Mon, 7 Sep 2026 22:47:43 +0300 Subject: [PATCH 1/2] fix(vault): expand Infisical secret references when listing secrets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A secret in Infisical can reference another one (`${env.folder.KEY}`), which is the only way to keep a value in one place and read it from several folders. The Infisical client fetches `/api/v3/secrets/raw` without `expandSecretReferences`, and that endpoint leaves references untouched by default. The referencing secret therefore arrives as the literal `${...}` string, is written to the generated `.env`, and the deployment still reports success — the service just receives a placeholder instead of its value. This is easy to miss because the single-secret endpoint (`/raw/{name}`) does expand by default, so the Infisical UI and API show the resolved value while only Dokploy sees the literal. Adds the parameter and the first tests for this client: the list request now asserts the flag, a missing secret and an auth failure. Without the fix the new test fails with `expected null to be 'true'`. --- apps/dokploy/__test__/env/vault.test.ts | 53 ++++++++++++++++++++ packages/server/src/utils/vault/infisical.ts | 6 +++ 2 files changed, 59 insertions(+) diff --git a/apps/dokploy/__test__/env/vault.test.ts b/apps/dokploy/__test__/env/vault.test.ts index ea57b6800..81fba1f53 100644 --- a/apps/dokploy/__test__/env/vault.test.ts +++ b/apps/dokploy/__test__/env/vault.test.ts @@ -20,6 +20,7 @@ import { import { azureClient } from "@dokploy/server/utils/vault/azure"; import { dopplerClient } from "@dokploy/server/utils/vault/doppler"; import { hashicorpClient } from "@dokploy/server/utils/vault/hashicorp"; +import { infisicalClient } from "@dokploy/server/utils/vault/infisical"; import { phaseClient } from "@dokploy/server/utils/vault/phase"; import { scalewayClient } from "@dokploy/server/utils/vault/scaleway"; @@ -431,6 +432,58 @@ describe("azure client", () => { }); }); +describe("infisical client", () => { + const config = { + providerType: "infisical" as const, + siteUrl: "https://app.infisical.com", + clientId: "client-1", + clientSecret: "client-secret", + projectId: "workspace-1", + environmentSlug: "prod", + secretPath: "/frontend", + }; + + const loginResponse = () => jsonResponse({ accessToken: "token-1" }); + + it("asks the list endpoint to expand secret references", async () => { + mockFetch + .mockResolvedValueOnce(loginResponse()) + .mockResolvedValueOnce( + jsonResponse({ + secrets: [{ secretKey: "DB_URL", secretValue: "postgres://real" }], + }), + ); + + const result = await infisicalClient.getSecrets(config, ["DB_URL"]); + + expect(result).toEqual({ DB_URL: "postgres://real" }); + const [listUrl] = mockFetch.mock.calls[1] as [string]; + const params = new URL(listUrl).searchParams; + expect(params.get("expandSecretReferences")).toBe("true"); + expect(params.get("workspaceId")).toBe("workspace-1"); + expect(params.get("environment")).toBe("prod"); + expect(params.get("secretPath")).toBe("/frontend"); + }); + + it("throws a clear error for a missing secret", async () => { + mockFetch + .mockResolvedValueOnce(loginResponse()) + .mockResolvedValueOnce(jsonResponse({ secrets: [] })); + + await expect( + infisicalClient.getSecrets(config, ["ABSENT"]), + ).rejects.toThrow('secret "ABSENT" not found in environment "prod"'); + }); + + it("propagates authentication failures with the status code", async () => { + mockFetch.mockResolvedValueOnce(jsonResponse({}, false, 401)); + + await expect(infisicalClient.getSecrets(config, ["DB_URL"])).rejects.toThrow( + "authentication failed (status 401)", + ); + }); +}); + describe("doppler client", () => { it("propagates auth errors with the status code", async () => { mockFetch.mockResolvedValue(jsonResponse({}, false, 401)); diff --git a/packages/server/src/utils/vault/infisical.ts b/packages/server/src/utils/vault/infisical.ts index a32852c05..ad3339ff7 100644 --- a/packages/server/src/utils/vault/infisical.ts +++ b/packages/server/src/utils/vault/infisical.ts @@ -38,6 +38,12 @@ const fetchSecrets = async (config: InfisicalConfig) => { workspaceId: config.projectId, environment: config.environmentSlug, secretPath: config.secretPath, + // Infisical's list endpoint leaves secret references (`${env.folder.KEY}`) + // unexpanded unless asked, so without this a referencing secret arrives as + // the literal `${...}` string, lands in the generated .env and the deploy + // still reports success. Single secrets read via /raw/{name} expand by + // default, which makes the difference easy to miss in the UI. + expandSecretReferences: "true", }); const response = await vaultFetch( `${baseUrl(config)}/api/v3/secrets/raw?${params.toString()}`, From df4e29954544ee1aa8c379a9462724faae3167ce Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:43:47 +0000 Subject: [PATCH 2/2] [autofix.ci] apply automated fixes --- apps/dokploy/__test__/env/vault.test.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/apps/dokploy/__test__/env/vault.test.ts b/apps/dokploy/__test__/env/vault.test.ts index 81fba1f53..871fe5c57 100644 --- a/apps/dokploy/__test__/env/vault.test.ts +++ b/apps/dokploy/__test__/env/vault.test.ts @@ -446,13 +446,11 @@ describe("infisical client", () => { const loginResponse = () => jsonResponse({ accessToken: "token-1" }); it("asks the list endpoint to expand secret references", async () => { - mockFetch - .mockResolvedValueOnce(loginResponse()) - .mockResolvedValueOnce( - jsonResponse({ - secrets: [{ secretKey: "DB_URL", secretValue: "postgres://real" }], - }), - ); + mockFetch.mockResolvedValueOnce(loginResponse()).mockResolvedValueOnce( + jsonResponse({ + secrets: [{ secretKey: "DB_URL", secretValue: "postgres://real" }], + }), + ); const result = await infisicalClient.getSecrets(config, ["DB_URL"]); @@ -478,9 +476,9 @@ describe("infisical client", () => { it("propagates authentication failures with the status code", async () => { mockFetch.mockResolvedValueOnce(jsonResponse({}, false, 401)); - await expect(infisicalClient.getSecrets(config, ["DB_URL"])).rejects.toThrow( - "authentication failed (status 401)", - ); + await expect( + infisicalClient.getSecrets(config, ["DB_URL"]), + ).rejects.toThrow("authentication failed (status 401)"); }); });