import { describe, expect, it } from "vitest"; import { DEV_AUTH_SECRET_PLACEHOLDER, DEV_ENCRYPTION_KEY_PLACEHOLDER, DEV_SCREEN_PROXY_SECRET_PLACEHOLDER, DEV_SUPERVISOR_TOKEN_PLACEHOLDER, hasValidBearerToken, resolveAuthSecret, resolveEncryptionKey, resolveScreenProxySecret, resolveSupervisorToken, resolveUpdaterToken, timingSafeStringEqual, } from "./secrets-guard.js"; describe("secrets-guard", () => { it("allows placeholders in test mode", () => { expect(resolveAuthSecret({ NODE_ENV: "test" })).toBe(DEV_AUTH_SECRET_PLACEHOLDER); expect(resolveEncryptionKey({ NODE_ENV: "test" })).toBe(DEV_ENCRYPTION_KEY_PLACEHOLDER); expect(resolveSupervisorToken({ NODE_ENV: "test" })).toBe(DEV_SUPERVISOR_TOKEN_PLACEHOLDER); expect(resolveScreenProxySecret({ NODE_ENV: "test" })).toBe( DEV_SCREEN_PROXY_SECRET_PLACEHOLDER, ); }); it("rejects missing secrets outside local/test", () => { expect(() => resolveAuthSecret({ NODE_ENV: "production" })).toThrow(/BETTER_AUTH_SECRET/); expect(() => resolveEncryptionKey({ NODE_ENV: "production" })).toThrow(/ENCRYPTION_KEY/); expect(() => resolveSupervisorToken({ NODE_ENV: "production" })).toThrow( /SANDBOX_SUPERVISOR_TOKEN/, ); expect(() => resolveScreenProxySecret({ NODE_ENV: "production" })).toThrow( /SCREEN_PROXY_SECRET/, ); }); it("rejects placeholder values outside local/test", () => { expect(() => resolveAuthSecret({ NODE_ENV: "production", BETTER_AUTH_SECRET: DEV_AUTH_SECRET_PLACEHOLDER, }), ).toThrow(/BETTER_AUTH_SECRET/); expect(() => resolveEncryptionKey({ NODE_ENV: "production", ENCRYPTION_KEY: DEV_ENCRYPTION_KEY_PLACEHOLDER, }), ).toThrow(/ENCRYPTION_KEY/); expect(() => resolveSupervisorToken({ NODE_ENV: "production", SANDBOX_SUPERVISOR_TOKEN: DEV_SUPERVISOR_TOKEN_PLACEHOLDER, }), ).toThrow(/SANDBOX_SUPERVISOR_TOKEN/); expect(() => resolveSupervisorToken({ NODE_ENV: "production", SANDBOX_SUPERVISOR_TOKEN: "replace-with-32-plus-character-supervisor-token", }), ).toThrow(/SANDBOX_SUPERVISOR_TOKEN/); expect(() => resolveScreenProxySecret({ NODE_ENV: "production", SCREEN_PROXY_SECRET: DEV_SCREEN_PROXY_SECRET_PLACEHOLDER, }), ).toThrow(/SCREEN_PROXY_SECRET/); expect(() => resolveScreenProxySecret({ NODE_ENV: "production", SCREEN_PROXY_SECRET: "replace-with-32-plus-character-screen-proxy-secret", }), ).toThrow(/SCREEN_PROXY_SECRET/); }); it("rejects blank and whitespace-padded placeholder secrets in production", () => { expect(() => resolveAuthSecret({ NODE_ENV: "production", BETTER_AUTH_SECRET: " " })).toThrow( /BETTER_AUTH_SECRET/, ); expect(() => resolveEncryptionKey({ NODE_ENV: "production", ENCRYPTION_KEY: "\t" })).toThrow( /ENCRYPTION_KEY/, ); expect(() => resolveAuthSecret({ NODE_ENV: "production", BETTER_AUTH_SECRET: ` ${DEV_AUTH_SECRET_PLACEHOLDER}\n`, }), ).toThrow(/BETTER_AUTH_SECRET/); expect(() => resolveEncryptionKey({ NODE_ENV: "production", ENCRYPTION_KEY: `${DEV_ENCRYPTION_KEY_PLACEHOLDER} `, }), ).toThrow(/ENCRYPTION_KEY/); }); it("does not treat disabled Vitest flags as a test environment", () => { expect(() => resolveAuthSecret({ NODE_ENV: "production", VITEST: "0" })).toThrow( /BETTER_AUTH_SECRET/, ); expect(() => resolveEncryptionKey({ NODE_ENV: "production", VITEST: "false" })).toThrow( /ENCRYPTION_KEY/, ); }); it("accepts real dedicated credentials in production", () => { expect( resolveSupervisorToken({ NODE_ENV: "production", SANDBOX_SUPERVISOR_TOKEN: "prod-supervisor-token-with-enough-length", }), ).toBe("prod-supervisor-token-with-enough-length"); expect( resolveScreenProxySecret({ NODE_ENV: "production", SCREEN_PROXY_SECRET: "prod-screen-proxy-secret-with-enough-length", SANDBOX_SUPERVISOR_TOKEN: "prod-supervisor-token-with-enough-length", }), ).toBe("prod-screen-proxy-secret-with-enough-length"); }); it("accepts real secrets in production", () => { expect( resolveAuthSecret({ NODE_ENV: "production", BETTER_AUTH_SECRET: "prod-secret-with-enough-entropy-here", }), ).toBe("prod-secret-with-enough-entropy-here"); expect( resolveEncryptionKey({ NODE_ENV: "production", ENCRYPTION_KEY: "prod-encryption-key-with-enough-entropy", }), ).toBe("prod-encryption-key-with-enough-entropy"); }); it("keeps an existing non-placeholder encryption key usable during upgrades", () => { expect(resolveEncryptionKey({ NODE_ENV: "production", ENCRYPTION_KEY: "existing-key" })).toBe( "existing-key", ); }); it("requires dedicated supervisor and screen-proxy credentials", () => { expect( resolveSupervisorToken({ NODE_ENV: "test", SANDBOX_SUPERVISOR_TOKEN: "supervisor-only", BETTER_AUTH_SECRET: "custom-auth", }), ).toBe("supervisor-only"); expect(() => resolveSupervisorToken({ NODE_ENV: "test", SANDBOX_SUPERVISOR_TOKEN: "custom-auth", BETTER_AUTH_SECRET: "custom-auth", }), ).toThrow(/must differ/); expect( resolveScreenProxySecret({ NODE_ENV: "test", SCREEN_PROXY_SECRET: "screen-only", SANDBOX_SUPERVISOR_TOKEN: "supervisor-only", BETTER_AUTH_SECRET: "custom-auth", }), ).toBe("screen-only"); expect(() => resolveScreenProxySecret({ NODE_ENV: "test", SCREEN_PROXY_SECRET: "supervisor-only", SANDBOX_SUPERVISOR_TOKEN: "supervisor-only", }), ).toThrow(/must differ/); expect(() => resolveSupervisorToken({ NODE_ENV: "production", SANDBOX_SUPERVISOR_TOKEN: "too-short", }), ).toThrow(/at least 32 characters/); }); it("requires the updater to use a dedicated token", () => { expect(() => resolveUpdaterToken({ NODE_ENV: "test", BETTER_AUTH_SECRET: "custom-auth" }), ).toThrow(/RAKAZO_UPDATER_TOKEN/); expect( resolveUpdaterToken({ NODE_ENV: "test", RAKAZO_UPDATER_TOKEN: "updater-only", SANDBOX_SUPERVISOR_TOKEN: "supervisor-only", BETTER_AUTH_SECRET: "custom-auth", }), ).toBe("updater-only"); expect(() => resolveUpdaterToken({ NODE_ENV: "test", RAKAZO_UPDATER_TOKEN: "custom-auth", BETTER_AUTH_SECRET: "custom-auth", }), ).toThrow(/must differ/); expect(() => resolveUpdaterToken({ NODE_ENV: "test", RAKAZO_UPDATER_TOKEN: "supervisor-only", SANDBOX_SUPERVISOR_TOKEN: "supervisor-only", }), ).toThrow(/must differ/); expect(() => resolveUpdaterToken({ NODE_ENV: "test", RAKAZO_UPDATER_TOKEN: "screen-only", SCREEN_PROXY_SECRET: "screen-only", }), ).toThrow(/must differ/); expect(() => resolveUpdaterToken({ NODE_ENV: "production", RAKAZO_UPDATER_TOKEN: "too-short", }), ).toThrow(/at least 32 characters/); }); it("compares raw strings in constant time", () => { expect(timingSafeStringEqual("secret-token", "secret-token")).toBe(true); expect(timingSafeStringEqual("secret-token-longer", "secret-token")).toBe(false); expect(timingSafeStringEqual("secret-toke", "secret-token")).toBe(false); expect(timingSafeStringEqual("Secret-token", "secret-token")).toBe(false); expect(timingSafeStringEqual(undefined, "secret-token")).toBe(false); expect(timingSafeStringEqual("", "")).toBe(true); }); it("compares bearer tokens without leaking a length or a prefix match", () => { expect(hasValidBearerToken("Bearer secret-token", "secret-token")).toBe(true); expect(hasValidBearerToken("Bearer secret-token-longer", "secret-token")).toBe(false); expect(hasValidBearerToken("Bearer secret-toke", "secret-token")).toBe(false); expect(hasValidBearerToken("secret-token", "secret-token")).toBe(false); expect(hasValidBearerToken("Basic secret-token", "secret-token")).toBe(false); expect(hasValidBearerToken(undefined, "secret-token")).toBe(false); }); });