diff --git a/package.json b/package.json index b66e620..0b08b99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "notifkit", - "version": "0.1.5", + "version": "0.1.6", "description": "Self-hosted notification infrastructure. One call delivers to email, SMS, push, and webhook — routed by preference, quiet hours, and consent.", "license": "MIT", "author": "devkitshq", diff --git a/src/config/index.ts b/src/config/index.ts index c6c4bfe..4ea76c2 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -38,7 +38,11 @@ export const baseConfigSchema = z.object({ HOST: z.string().default("127.0.0.1"), REDIS_URL: z.string().url().default("redis://localhost:6379"), DATABASE_URL: z.string().url().default("postgres://platform:platform@localhost:5432/notifkit"), - ADMIN_API_KEY: z.string().optional(), + // Trimmed because the incoming bearer token is trimmed before comparison, so + // an untrimmed value here could never match it. `set KEY=value && cmd` on + // Windows puts a trailing space in the variable, which otherwise turns every + // admin request into a 401 that reads like a wrong key. + ADMIN_API_KEY: z.string().trim().optional(), WORKER_CONCURRENCY: z.coerce.number().int().min(1).default(10), QUEUE_MAX_LEN: z.coerce.number().int().min(1).default(10000000), DB_MAX_CONNECTIONS: z.coerce.number().int().min(1).default(2), diff --git a/tests/api.test.ts b/tests/api.test.ts index 7bc9297..7017ae8 100644 --- a/tests/api.test.ts +++ b/tests/api.test.ts @@ -58,6 +58,19 @@ describe("extractAuthToken", () => { expect(extractAuthToken(req as any)).toBe("my-token-with-spaces"); }); + // `set ADMIN_API_KEY=key && cmd` on Windows puts a trailing space in the + // variable. The token side is trimmed, so if the configured side were not, + // the two could never match and every admin request would 401. + it("matches a configured key that carries the whitespace a shell added", async () => { + const { readBaseConfig } = await import("@/config/index.js"); + + const config = readBaseConfig({ ADMIN_API_KEY: "supersecretkey " } as NodeJS.ProcessEnv); + const token = extractAuthToken({ headers: { authorization: "Bearer supersecretkey" } } as any); + + expect(config.ADMIN_API_KEY).toBe("supersecretkey"); + expect(token).toBe(config.ADMIN_API_KEY); + }); + it("prioritizes Bearer authorization header over x-api-key if both are present", () => { const req = { headers: {