File size: 10,578 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
/**
 * Issue #2257 β€” clientApi policy behavior when an invalid Bearer is sent and
 * REQUIRE_API_KEY=false.
 *
 * The existing `client-api-policy.test.ts` shares a DB-backed setup via
 * `resetStorage()` and `apiKeysDb` that has SQLite migration races on this
 * branch. This standalone file mocks `validateApiKey` to test the policy's
 * fallback branch in isolation β€” no DB, no migration runner.
 */

import test from "node:test";
import assert from "node:assert/strict";
import Module from "node:module";

// ─── Mock validateApiKey via require interception (so the dynamic import in
// the policy module returns our stub instead of hitting the real DB module) ─

type ValidateFn = (key: string) => boolean | Promise<boolean>;
let mockValidateApiKey: ValidateFn = () => false;

const originalResolve = (Module as unknown as { _resolveFilename: typeof Module._resolveFilename })
  ._resolveFilename;

// Intercept require() / import() resolution for the apiKeys DB module and
// substitute it for our stub. This runs only for the exact path the policy
// imports β€” production code paths are unaffected.
const POLICY_IMPORT_TARGET = "src/lib/db/apiKeys";

(Module as unknown as { _resolveFilename: typeof Module._resolveFilename })._resolveFilename =
  function patched(this: unknown, request: string, ...rest: unknown[]) {
    if (request.includes(POLICY_IMPORT_TARGET)) {
      // Resolve to a stub file we create below
      const stubPath = new URL("./__stub_apiKeys.mjs", import.meta.url).pathname;
      // @ts-expect-error - rest spread to original
      return originalResolve.call(this, stubPath, ...rest);
    }
    // @ts-expect-error - rest spread to original
    return originalResolve.call(this, request, ...rest);
  };

// Write the stub file ad-hoc (Node's loader needs a real file)
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";

const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omr-clientapi-policy-fallback-"));
process.env.DATA_DIR = TEST_DATA_DIR;

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const STUB_PATH = path.join(__dirname, "__stub_apiKeys.mjs");
fs.writeFileSync(
  STUB_PATH,
  `export const validateApiKey = (key) => globalThis.__mockValidateApiKey(key);\n`
);

// Wire the stub to our local variable
(globalThis as unknown as { __mockValidateApiKey: ValidateFn }).__mockValidateApiKey = (key) =>
  mockValidateApiKey(key);

test.after(() => {
  try {
    fs.unlinkSync(STUB_PATH);
  } catch {
    /* ignore */
  }
  fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
  if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR;
  else process.env.DATA_DIR = ORIGINAL_DATA_DIR;
});

// ─── Load policy fresh (after the interceptor is in place) ────────────────

async function loadPolicy() {
  const mod = await import(`../../../src/server/authz/policies/clientApi.ts?ts=${Date.now()}`);
  return mod.clientApiPolicy;
}

function ctx(headers: Headers, normalizedPath = "/api/v1/chat/completions") {
  return {
    request: { method: "POST", headers, url: `http://localhost${normalizedPath}` },
    classification: {
      routeClass: "CLIENT_API" as const,
      reason: "client_api_v1" as const,
      normalizedPath,
    },
    requestId: "req_test",
  };
}

// ─── Tests ────────────────────────────────────────────────────────────────

test.beforeEach(() => {
  // Default to "every key fails" β€” individual tests override as needed.
  mockValidateApiKey = () => false;
  delete process.env.REQUIRE_API_KEY;
});

test("#2257 β€” invalid bearer + REQUIRE_API_KEY=true β†’ 401", async () => {
  process.env.REQUIRE_API_KEY = "true";
  const policy = await loadPolicy();
  const headers = new Headers({ authorization: "Bearer sk-stub-bogus" });
  const out = await policy.evaluate(ctx(headers));
  assert.equal(out.allow, false);
  if (!out.allow) {
    assert.equal(out.status, 401);
    assert.equal(out.code, "AUTH_002");
  }
});

test("#2257 β€” invalid bearer + REQUIRE_API_KEY=false β†’ anonymous (with warning log)", async () => {
  const originalWarn = console.warn;
  const warnings: string[] = [];
  console.warn = (msg: string) => warnings.push(String(msg));
  try {
    const policy = await loadPolicy();
    const headers = new Headers({ authorization: "Bearer sk-stub-bogus" });
    const out = await policy.evaluate(ctx(headers));
    assert.equal(out.allow, true);
    if (out.allow) {
      assert.equal(out.subject.kind, "anonymous");
      assert.equal(out.subject.id, "local");
    }
    assert.ok(
      warnings.some((w) => w.includes("[clientApiPolicy]") && w.includes("REQUIRE_API_KEY=false")),
      "expected a warning about the fallback"
    );
  } finally {
    console.warn = originalWarn;
  }
});

test("#2257 β€” invalid x-api-key + REQUIRE_API_KEY=false β†’ anonymous (with warning log)", async () => {
  const originalWarn = console.warn;
  const warnings: string[] = [];
  console.warn = (msg: string) => warnings.push(String(msg));
  try {
    const policy = await loadPolicy();
    const headers = new Headers({ "x-api-key": "sk-stub-bogus" });
    const out = await policy.evaluate(ctx(headers));
    assert.equal(out.allow, true);
    if (out.allow) {
      assert.equal(out.subject.kind, "anonymous");
      assert.equal(out.subject.id, "local");
    }
    assert.ok(
      warnings.some((w) => w.includes("[clientApiPolicy]") && w.includes("REQUIRE_API_KEY=false")),
      "expected a warning about the fallback"
    );
  } finally {
    console.warn = originalWarn;
  }
});

test("#2257 β€” fallback warning masks the x-api-key (only last-4 in log)", async () => {
  const originalWarn = console.warn;
  const warnings: string[] = [];
  console.warn = (msg: string) => warnings.push(String(msg));
  try {
    const policy = await loadPolicy();
    const headers = new Headers({ "x-api-key": "sk-secretprefix-secretmiddle-XYZW" });
    const out = await policy.evaluate(ctx(headers));
    assert.equal(out.allow, true);
    assert.ok(
      warnings.every((w) => !w.includes("secretprefix") && !w.includes("secretmiddle")),
      "warning leaked the full bearer; only masked key id should be logged"
    );
    assert.ok(
      warnings.some((w) => w.includes("key_XYZW")),
      "expected masked key id (last-4) in the warning"
    );
  } finally {
    console.warn = originalWarn;
  }
});

test("#2257 β€” fallback warning masks the bearer (only last-4 in log)", async () => {
  const originalWarn = console.warn;
  const warnings: string[] = [];
  console.warn = (msg: string) => warnings.push(String(msg));
  try {
    const policy = await loadPolicy();
    const headers = new Headers({ authorization: "Bearer sk-secretprefix-secretmiddle-XYZW" });
    const out = await policy.evaluate(ctx(headers));
    assert.equal(out.allow, true);
    assert.ok(
      warnings.every((w) => !w.includes("secretprefix") && !w.includes("secretmiddle")),
      "warning leaked the full bearer; only masked key id should be logged"
    );
    assert.ok(
      warnings.some((w) => w.includes("key_XYZW")),
      "expected masked key id (last-4) in the warning"
    );
  } finally {
    console.warn = originalWarn;
  }
});

test("#2257 β€” no bearer + REQUIRE_API_KEY=false β†’ anonymous (unchanged, no fallback warning)", async () => {
  const originalWarn = console.warn;
  const warnings: string[] = [];
  console.warn = (msg: string) => warnings.push(String(msg));
  try {
    const policy = await loadPolicy();
    const out = await policy.evaluate(ctx(new Headers()));
    assert.equal(out.allow, true);
    if (out.allow) {
      assert.equal(out.subject.kind, "anonymous");
    }
    // No warning should fire when no bearer is sent in the first place β€”
    // the warning is specifically for the "invalid-bearer-fell-through" case.
    assert.ok(
      warnings.every((w) => !w.includes("[clientApiPolicy]")),
      "no fallback warning expected when no bearer was sent"
    );
  } finally {
    console.warn = originalWarn;
  }
});

// ─── #3504 β€” non-usable Authorization must NOT short-circuit the URL path token ─
// VS Code Copilot sends its own (empty / non-OmniRoute) Authorization header even
// when the OmniRoute key lives in the URL path of a /vscode tokenized endpoint.
// A non-"Bearer <token>" Authorization must fall through to the URL token instead
// of returning null and 401'ing under REQUIRE_API_KEY=true.

// validateApiKey is the real (no-DB β†’ always-false) implementation here, so we
// distinguish "URL token was extracted" from "no token found" by the rejection
// MESSAGE: an extracted-but-unknown token β†’ "Invalid API key"; nothing extracted
// β†’ "Authentication required". On the pre-fix code a non-Bearer Authorization
// returned null, so these would all 401 with "Authentication required".

test("#3504 β€” empty 'Bearer ' Authorization falls through to the URL path token", async () => {
  process.env.REQUIRE_API_KEY = "true";
  const policy = await loadPolicy();
  const headers = new Headers({ authorization: "Bearer " });
  const out = await policy.evaluate(
    ctx(headers, "/api/v1/vscode/sk-url-token/chat/completions")
  );
  assert.equal(out.allow, false);
  if (!out.allow) {
    assert.equal(out.status, 401);
    assert.equal(
      out.message,
      "Invalid API key",
      "URL token must be extracted (β†’ 'Invalid API key'), not skipped (β†’ 'Authentication required')"
    );
  }
});

test("#3504 β€” a non-Bearer scheme (Basic) also falls through to the URL token", async () => {
  process.env.REQUIRE_API_KEY = "true";
  const policy = await loadPolicy();
  const headers = new Headers({ authorization: "Basic Zm9vOmJhcg==" });
  const out = await policy.evaluate(
    ctx(headers, "/api/v1/vscode/sk-url-token/chat/completions")
  );
  assert.equal(out.allow, false);
  if (!out.allow) assert.equal(out.message, "Invalid API key");
});

test("#3504 β€” non-Bearer Authorization with NO URL token still rejects as unauthenticated", async () => {
  process.env.REQUIRE_API_KEY = "true";
  const policy = await loadPolicy();
  const headers = new Headers({ authorization: "Bearer " });
  const out = await policy.evaluate(ctx(headers, "/api/v1/chat/completions"));
  assert.equal(out.allow, false);
  if (!out.allow) assert.equal(out.message, "Authentication required");
});