File size: 10,704 Bytes
5448d8b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**

 * T-04 fetch-interceptor contract tests.

 *

 * Covers `createOmniRouteFetchInterceptor` (URL-prefix gating, header merge,

 * Content-Type defaulting, input-shape polymorphism) plus the loader

 * integration that wires it into the AuthHook return shape.

 *

 * Strategy: replace `globalThis.fetch` with a closure-based recorder for the

 * duration of each test (saved-and-restored in try/finally β€” node:test has

 * no built-in spy/restore lifecycle). The recorder captures `(input, init)`

 * as observed by the wrapped global call so we can assert on what was

 * forwarded after header injection.

 */
import test from "node:test";
import assert from "node:assert/strict";
import { createOmniRouteAuthHook, createOmniRouteFetchInterceptor } from "../src/index.js";

type FetchCall = { input: Parameters<typeof fetch>[0]; init?: RequestInit };

function installFetchRecorder(response: Response = new Response("ok")) {
  const calls: FetchCall[] = [];
  const original = globalThis.fetch;
  globalThis.fetch = (async (input: any, init?: any) => {
    calls.push({ input, init });
    return response;
  }) as typeof fetch;
  const restore = () => {
    globalThis.fetch = original;
  };
  return { calls, restore };
}

const BASE = "https://or.example.com/v1";
const KEY = "sk-test-fetch";

test("createOmniRouteFetchInterceptor: targets baseURL β†’ Authorization header injected", async () => {
  const { calls, restore } = installFetchRecorder();
  try {
    const f = createOmniRouteFetchInterceptor({ apiKey: KEY, baseURL: BASE });
    await f(`${BASE}/chat/completions`, {
      method: "POST",
      body: JSON.stringify({ x: 1 }),
    });
    assert.equal(calls.length, 1);
    const sent = calls[0]!;
    const sentHeaders = new Headers((sent.init as RequestInit).headers);
    assert.equal(sentHeaders.get("Authorization"), `Bearer ${KEY}`);
  } finally {
    restore();
  }
});

test("createOmniRouteFetchInterceptor: targets baseURL β†’ Authorization OVERRIDES caller-supplied Bearer", async () => {
  const { calls, restore } = installFetchRecorder();
  try {
    const f = createOmniRouteFetchInterceptor({ apiKey: KEY, baseURL: BASE });
    await f(`${BASE}/chat/completions`, {
      method: "POST",
      body: "{}",
      headers: { Authorization: "Bearer attacker-key" },
    });
    const sent = calls[0]!;
    const sentHeaders = new Headers((sent.init as RequestInit).headers);
    // We own the apiKey for this provider β€” caller-supplied Bearer must lose.
    assert.equal(sentHeaders.get("Authorization"), `Bearer ${KEY}`);
  } finally {
    restore();
  }
});

test("createOmniRouteFetchInterceptor: targets baseURL + body β†’ Content-Type defaults to application/json", async () => {
  const { calls, restore } = installFetchRecorder();
  try {
    const f = createOmniRouteFetchInterceptor({ apiKey: KEY, baseURL: BASE });
    await f(`${BASE}/chat/completions`, {
      method: "POST",
      body: JSON.stringify({ m: "x" }),
    });
    const sent = calls[0]!;
    const sentHeaders = new Headers((sent.init as RequestInit).headers);
    assert.equal(sentHeaders.get("Content-Type"), "application/json");
  } finally {
    restore();
  }
});

test("createOmniRouteFetchInterceptor: caller-set Content-Type is NOT overwritten", async () => {
  const { calls, restore } = installFetchRecorder();
  try {
    const f = createOmniRouteFetchInterceptor({ apiKey: KEY, baseURL: BASE });
    await f(`${BASE}/v2/whatever`, {
      method: "POST",
      body: "raw",
      headers: { "Content-Type": "text/plain; charset=utf-8" },
    });
    const sent = calls[0]!;
    const sentHeaders = new Headers((sent.init as RequestInit).headers);
    assert.equal(sentHeaders.get("Content-Type"), "text/plain; charset=utf-8");
  } finally {
    restore();
  }
});

test("createOmniRouteFetchInterceptor: non-baseURL host β†’ passthrough, no Authorization injected", async () => {
  const { calls, restore } = installFetchRecorder();
  try {
    const f = createOmniRouteFetchInterceptor({ apiKey: KEY, baseURL: BASE });
    await f("https://third-party.example.org/v1/chat", {
      method: "POST",
      body: "{}",
      headers: { "X-Caller": "yes" },
    });
    const sent = calls[0]!;
    // Init forwarded verbatim β€” no header injection.
    const sentHeaders = new Headers((sent.init as RequestInit | undefined)?.headers);
    assert.equal(sentHeaders.get("Authorization"), null, "MUST NOT leak apiKey");
    assert.equal(sentHeaders.get("X-Caller"), "yes");
  } finally {
    restore();
  }
});

test("createOmniRouteFetchInterceptor: refuses suffix-spoof β€” `${base}-attacker.evil` does NOT match baseURL", async () => {
  const { calls, restore } = installFetchRecorder();
  try {
    const f = createOmniRouteFetchInterceptor({ apiKey: KEY, baseURL: BASE });
    // baseURL is `https://or.example.com/v1`. A spoofed
    // `https://or.example.com/v1-attacker.evil/chat` shares the literal prefix
    // but is NOT under our origin path β€” must be treated as passthrough.
    await f("https://or.example.com/v1-attacker.evil/chat", {
      method: "POST",
      body: "{}",
    });
    const sent = calls[0]!;
    const sentHeaders = new Headers((sent.init as RequestInit | undefined)?.headers);
    assert.equal(sentHeaders.get("Authorization"), null);
  } finally {
    restore();
  }
});

test("createOmniRouteFetchInterceptor: URL object input is handled", async () => {
  const { calls, restore } = installFetchRecorder();
  try {
    const f = createOmniRouteFetchInterceptor({ apiKey: KEY, baseURL: BASE });
    await f(new URL(`${BASE}/models`), {});
    const sent = calls[0]!;
    const sentHeaders = new Headers((sent.init as RequestInit).headers);
    assert.equal(sentHeaders.get("Authorization"), `Bearer ${KEY}`);
  } finally {
    restore();
  }
});

test("createOmniRouteFetchInterceptor: Request input is handled (reads .url)", async () => {
  const { calls, restore } = installFetchRecorder();
  try {
    const f = createOmniRouteFetchInterceptor({ apiKey: KEY, baseURL: BASE });
    const req = new Request(`${BASE}/chat/completions`, {
      method: "POST",
      body: JSON.stringify({ a: 1 }),
      headers: { "X-Caller": "preserved" },
    });
    await f(req);
    const sent = calls[0]!;
    // The interceptor forwards the original Request as `input` but layers our
    // headers into the `init`. We assert against the init view since fetch()
    // resolves headers from init first when both are present.
    const sentHeaders = new Headers((sent.init as RequestInit).headers);
    assert.equal(sentHeaders.get("Authorization"), `Bearer ${KEY}`);
    assert.equal(
      sentHeaders.get("X-Caller"),
      "preserved",
      "Request-attached headers must survive the merge"
    );
  } finally {
    restore();
  }
});

test("createOmniRouteFetchInterceptor: trailing slash in baseURL is normalized", async () => {
  const { calls, restore } = installFetchRecorder();
  try {
    const f = createOmniRouteFetchInterceptor({
      apiKey: KEY,
      baseURL: `${BASE}////`,
    });
    await f(`${BASE}/models`, {});
    const sent = calls[0]!;
    const sentHeaders = new Headers((sent.init as RequestInit).headers);
    assert.equal(sentHeaders.get("Authorization"), `Bearer ${KEY}`);
  } finally {
    restore();
  }
});

test("createOmniRouteFetchInterceptor: GET without body does NOT set Content-Type", async () => {
  const { calls, restore } = installFetchRecorder();
  try {
    const f = createOmniRouteFetchInterceptor({ apiKey: KEY, baseURL: BASE });
    await f(`${BASE}/models`); // no init at all
    const sent = calls[0]!;
    const sentHeaders = new Headers((sent.init as RequestInit).headers);
    assert.equal(sentHeaders.get("Authorization"), `Bearer ${KEY}`);
    assert.equal(
      sentHeaders.get("Content-Type"),
      null,
      "Content-Type should only default when a body exists"
    );
  } finally {
    restore();
  }
});

// ----------------------------------------------------------------------------
// loader integration
// ----------------------------------------------------------------------------

test("loader: returns fetch fn when apiKey + baseURL both present (via opts)", async () => {
  const hook = createOmniRouteAuthHook({ baseURL: BASE });
  const result = await hook.loader!(async () => ({ type: "api", key: KEY }) as never, {} as never);
  assert.equal((result as { apiKey: string }).apiKey, KEY);
  assert.equal((result as { baseURL: string }).baseURL, BASE);
  assert.equal(
    typeof (result as { fetch?: unknown }).fetch,
    "function",
    "loader must wire fetch interceptor when baseURL resolves"
  );
});

test("loader: returns fetch fn when baseURL is stashed on the auth credential", async () => {
  // Some auth backends attach baseURL alongside the key (post-/connect flow).
  // The loader should pick it up even when plugin opts.baseURL is unset.
  const hook = createOmniRouteAuthHook();
  const result = await hook.loader!(
    async () => ({ type: "api", key: KEY, baseURL: BASE }) as never,
    {} as never
  );
  assert.equal((result as { baseURL?: string }).baseURL, BASE);
  assert.equal(typeof (result as { fetch?: unknown }).fetch, "function");
});

test("loader: omits fetch fn when baseURL missing (apiKey-only return)", async () => {
  const hook = createOmniRouteAuthHook(); // no baseURL opt
  const result = await hook.loader!(async () => ({ type: "api", key: KEY }) as never, {} as never);
  // Interceptor needs a baseURL to gate-keep; without one, fall back to
  // apiKey-only and let the SDK use its default fetch.
  assert.deepEqual(result, { apiKey: KEY });
});

test("loader integration: wired interceptor actually injects Bearer when invoked", async () => {
  // End-to-end: pull the fetch fn out of the loader return and exercise it,
  // proving the wiring matches the standalone interceptor's contract.
  const { calls, restore } = installFetchRecorder();
  try {
    const hook = createOmniRouteAuthHook({ baseURL: BASE });
    const result = await hook.loader!(
      async () => ({ type: "api", key: KEY }) as never,
      {} as never
    );
    const wiredFetch = (result as { fetch: typeof fetch }).fetch;
    await wiredFetch(`${BASE}/v1/models`, {});
    assert.equal(calls.length, 1);
    const sentHeaders = new Headers((calls[0]!.init as RequestInit).headers);
    assert.equal(sentHeaders.get("Authorization"), `Bearer ${KEY}`);
  } finally {
    restore();
  }
});