offrouter-core 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/audit.d.ts +137 -0
  2. package/dist/audit.d.ts.map +1 -0
  3. package/dist/audit.js +320 -0
  4. package/dist/audit.js.map +1 -0
  5. package/dist/auth/credential-chain.d.ts +84 -0
  6. package/dist/auth/credential-chain.d.ts.map +1 -0
  7. package/dist/auth/credential-chain.js +150 -0
  8. package/dist/auth/credential-chain.js.map +1 -0
  9. package/dist/auth/index.d.ts +10 -0
  10. package/dist/auth/index.d.ts.map +1 -0
  11. package/dist/auth/index.js +7 -0
  12. package/dist/auth/index.js.map +1 -0
  13. package/dist/auth/keychain.d.ts +60 -0
  14. package/dist/auth/keychain.d.ts.map +1 -0
  15. package/dist/auth/keychain.js +104 -0
  16. package/dist/auth/keychain.js.map +1 -0
  17. package/dist/auth/oauth-pkce.d.ts +138 -0
  18. package/dist/auth/oauth-pkce.d.ts.map +1 -0
  19. package/dist/auth/oauth-pkce.js +375 -0
  20. package/dist/auth/oauth-pkce.js.map +1 -0
  21. package/dist/auth/oauth-server.d.ts +36 -0
  22. package/dist/auth/oauth-server.d.ts.map +1 -0
  23. package/dist/auth/oauth-server.js +210 -0
  24. package/dist/auth/oauth-server.js.map +1 -0
  25. package/dist/config.d.ts +76 -0
  26. package/dist/config.d.ts.map +1 -0
  27. package/dist/config.js +355 -0
  28. package/dist/config.js.map +1 -0
  29. package/dist/delegation.d.ts +123 -0
  30. package/dist/delegation.d.ts.map +1 -0
  31. package/dist/delegation.js +455 -0
  32. package/dist/delegation.js.map +1 -0
  33. package/dist/index.d.ts +41 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +26 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/policy.d.ts +45 -0
  38. package/dist/policy.d.ts.map +1 -0
  39. package/dist/policy.js +318 -0
  40. package/dist/policy.js.map +1 -0
  41. package/{src/protocol.ts → dist/protocol.d.ts} +2 -2
  42. package/dist/protocol.d.ts.map +1 -0
  43. package/dist/protocol.js +6 -0
  44. package/dist/protocol.js.map +1 -0
  45. package/dist/providers/adapter.d.ts +137 -0
  46. package/dist/providers/adapter.d.ts.map +1 -0
  47. package/dist/providers/adapter.js +163 -0
  48. package/dist/providers/adapter.js.map +1 -0
  49. package/dist/providers/catalog-data.d.ts +128 -0
  50. package/dist/providers/catalog-data.d.ts.map +1 -0
  51. package/dist/providers/catalog-data.js +397 -0
  52. package/dist/providers/catalog-data.js.map +1 -0
  53. package/dist/providers/catalog.d.ts +63 -0
  54. package/dist/providers/catalog.d.ts.map +1 -0
  55. package/dist/providers/catalog.js +394 -0
  56. package/dist/providers/catalog.js.map +1 -0
  57. package/dist/providers/fake.d.ts +46 -0
  58. package/dist/providers/fake.d.ts.map +1 -0
  59. package/dist/providers/fake.js +234 -0
  60. package/dist/providers/fake.js.map +1 -0
  61. package/dist/providers/openai-compatible.d.ts +65 -0
  62. package/dist/providers/openai-compatible.d.ts.map +1 -0
  63. package/dist/providers/openai-compatible.js +434 -0
  64. package/dist/providers/openai-compatible.js.map +1 -0
  65. package/dist/proxy/responses-mapper.d.ts +366 -0
  66. package/dist/proxy/responses-mapper.d.ts.map +1 -0
  67. package/dist/proxy/responses-mapper.js +517 -0
  68. package/dist/proxy/responses-mapper.js.map +1 -0
  69. package/dist/proxy/responses-server.d.ts +29 -0
  70. package/dist/proxy/responses-server.d.ts.map +1 -0
  71. package/dist/proxy/responses-server.js +360 -0
  72. package/dist/proxy/responses-server.js.map +1 -0
  73. package/dist/router.d.ts +18 -0
  74. package/dist/router.d.ts.map +1 -0
  75. package/dist/router.js +296 -0
  76. package/dist/router.js.map +1 -0
  77. package/dist/schemas.d.ts +560 -0
  78. package/dist/schemas.d.ts.map +1 -0
  79. package/{src/schemas.ts → dist/schemas.js} +83 -103
  80. package/dist/schemas.js.map +1 -0
  81. package/dist/secrets.d.ts +112 -0
  82. package/dist/secrets.d.ts.map +1 -0
  83. package/dist/secrets.js +326 -0
  84. package/dist/secrets.js.map +1 -0
  85. package/dist/types.d.ts +117 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +6 -0
  88. package/dist/types.js.map +1 -0
  89. package/dist/usage-file.d.ts +59 -0
  90. package/dist/usage-file.d.ts.map +1 -0
  91. package/dist/usage-file.js +316 -0
  92. package/dist/usage-file.js.map +1 -0
  93. package/dist/usage.d.ts +235 -0
  94. package/dist/usage.d.ts.map +1 -0
  95. package/dist/usage.js +517 -0
  96. package/dist/usage.js.map +1 -0
  97. package/package.json +9 -4
  98. package/src/audit.test.ts +0 -302
  99. package/src/audit.ts +0 -553
  100. package/src/auth/credential-chain.test.ts +0 -326
  101. package/src/auth/credential-chain.ts +0 -235
  102. package/src/auth/index.ts +0 -45
  103. package/src/auth/keychain.test.ts +0 -265
  104. package/src/auth/keychain.ts +0 -168
  105. package/src/auth/oauth-pkce.test.ts +0 -329
  106. package/src/auth/oauth-pkce.ts +0 -571
  107. package/src/auth/oauth-server.test.ts +0 -83
  108. package/src/auth/oauth-server.ts +0 -296
  109. package/src/config.test.ts +0 -479
  110. package/src/config.ts +0 -505
  111. package/src/delegation.test.ts +0 -368
  112. package/src/delegation.ts +0 -605
  113. package/src/index.ts +0 -280
  114. package/src/policy.test.ts +0 -634
  115. package/src/policy.ts +0 -420
  116. package/src/providers/adapter.test.ts +0 -293
  117. package/src/providers/adapter.ts +0 -328
  118. package/src/providers/catalog-data.test.ts +0 -258
  119. package/src/providers/catalog-data.ts +0 -498
  120. package/src/providers/catalog.test.ts +0 -84
  121. package/src/providers/catalog.ts +0 -483
  122. package/src/providers/fake.ts +0 -312
  123. package/src/providers/openai-compatible.test.ts +0 -366
  124. package/src/providers/openai-compatible.ts +0 -554
  125. package/src/proxy/responses-mapper.test.ts +0 -290
  126. package/src/proxy/responses-mapper.ts +0 -736
  127. package/src/proxy/responses-server.test.ts +0 -322
  128. package/src/proxy/responses-server.ts +0 -469
  129. package/src/router.test.ts +0 -699
  130. package/src/router.ts +0 -352
  131. package/src/schemas.test.ts +0 -291
  132. package/src/secrets.test.ts +0 -271
  133. package/src/secrets.ts +0 -461
  134. package/src/types.ts +0 -173
  135. package/src/usage-file.test.ts +0 -243
  136. package/src/usage-file.ts +0 -435
  137. package/src/usage.test.ts +0 -335
  138. package/src/usage.ts +0 -859
  139. package/tsconfig.json +0 -9
@@ -1,329 +0,0 @@
1
- /**
2
- * Tests for OAuth PKCE flow helpers.
3
- */
4
- import { describe, expect, it, vi } from "vitest";
5
- import {
6
- generateCodeVerifier,
7
- generateCodeChallenge,
8
- PKCE_CONFIGS,
9
- createOAuthState,
10
- isOAuthProvider,
11
- defaultCallbackPort,
12
- resolveClientId,
13
- runOAuthLogin,
14
- } from "./oauth-pkce.js";
15
- import { FileSecretStore } from "../secrets.js";
16
- import { tmpdir } from "node:os";
17
- import { join } from "node:path";
18
- import { mkdtempSync, rmSync } from "node:fs";
19
- import { getOAuthTokens } from "./keychain.js";
20
-
21
- describe("PKCE code verifier and challenge", () => {
22
- it("generates a code verifier of correct length", () => {
23
- const verifier = generateCodeVerifier();
24
- expect(verifier).toBeTruthy();
25
- expect(verifier.length).toBeGreaterThanOrEqual(43);
26
- expect(verifier.length).toBeLessThanOrEqual(128);
27
- });
28
-
29
- it("generates a non-empty code challenge from verifier", async () => {
30
- const verifier = generateCodeVerifier();
31
- const challenge = await generateCodeChallenge(verifier);
32
- expect(challenge).toBeTruthy();
33
- expect(typeof challenge).toBe("string");
34
- });
35
-
36
- it("generates unique verifiers on each call", () => {
37
- const v1 = generateCodeVerifier();
38
- const v2 = generateCodeVerifier();
39
- expect(v1).not.toBe(v2);
40
- });
41
- });
42
-
43
- describe("PKCE_CONFIGS", () => {
44
- it("has config for anthropic", () => {
45
- const config = PKCE_CONFIGS["anthropic"];
46
- expect(config).toBeDefined();
47
- expect(config.authorizeUrl).toContain("console.anthropic.com");
48
- expect(config.scopes).toContain("offline_access");
49
- });
50
-
51
- it("has config for openai", () => {
52
- const config = PKCE_CONFIGS["openai"];
53
- expect(config).toBeDefined();
54
- expect(config.authorizeUrl).toContain("auth.openai.com");
55
- expect(config.scopes).toContain("offline_access");
56
- });
57
-
58
- it("has no config for gemini", () => {
59
- expect(PKCE_CONFIGS["gemini"]).toBeUndefined();
60
- });
61
-
62
- it("has no config for xai", () => {
63
- expect(PKCE_CONFIGS["xai"]).toBeUndefined();
64
- });
65
- });
66
-
67
- describe("createOAuthState", () => {
68
- it("creates a state string with metadata", () => {
69
- const state = createOAuthState("anthropic");
70
- const parsed = JSON.parse(state);
71
- expect(parsed.provider).toBe("anthropic");
72
- expect(parsed.nonce).toBeTruthy();
73
- expect(typeof parsed.nonce).toBe("string");
74
- });
75
- });
76
-
77
- describe("isOAuthProvider", () => {
78
- it("returns true for anthropic", () => {
79
- expect(isOAuthProvider("anthropic")).toBe(true);
80
- });
81
-
82
- it("returns true for openai", () => {
83
- expect(isOAuthProvider("openai")).toBe(true);
84
- });
85
-
86
- it("returns false for gemini", () => {
87
- expect(isOAuthProvider("gemini")).toBe(false);
88
- });
89
-
90
- it("returns false for xai", () => {
91
- expect(isOAuthProvider("xai")).toBe(false);
92
- });
93
- });
94
-
95
- describe("startOAuthFlow", () => {
96
- it("returns an authorization URL for anthropic", async () => {
97
- const { startOAuthFlow } = await import("./oauth-pkce.js");
98
- const result = await startOAuthFlow("anthropic", {
99
- redirectUri: "http://127.0.0.1:8765/callback",
100
- clientId: "test-client-id-anthropic",
101
- });
102
- expect(result).toBeDefined();
103
- expect(result.authorizeUrl).toContain("console.anthropic.com/oauth/authorize");
104
- expect(result.authorizeUrl).toContain("response_type=code");
105
- expect(result.authorizeUrl).toContain("code_challenge_method=S256");
106
- expect(result.state).toBeTruthy();
107
- });
108
-
109
- it("returns an authorization URL for openai", async () => {
110
- const { startOAuthFlow } = await import("./oauth-pkce.js");
111
- const result = await startOAuthFlow("openai", {
112
- redirectUri: "http://127.0.0.1:8765/callback",
113
- clientId: "test-client-id-openai",
114
- });
115
- expect(result).toBeDefined();
116
- expect(result.authorizeUrl).toContain("auth.openai.com/oauth/authorize");
117
- expect(result.authorizeUrl).toContain("response_type=code");
118
- });
119
-
120
- it("throws for unsupported provider", async () => {
121
- const { startOAuthFlow } = await import("./oauth-pkce.js");
122
- await expect(
123
- startOAuthFlow("gemini", {
124
- redirectUri: "http://127.0.0.1:8765/callback",
125
- }),
126
- ).rejects.toThrow("OAuth not supported for provider: gemini");
127
- });
128
- });
129
-
130
- describe("OAuth session store", () => {
131
- it("state string does NOT contain codeVerifier or redirectUri", async () => {
132
- const { startOAuthFlow } = await import("./oauth-pkce.js");
133
- const result = await startOAuthFlow("anthropic", {
134
- redirectUri: "http://127.0.0.1:8765/callback",
135
- clientId: "test-client-id-anthropic",
136
- });
137
- const parsed = JSON.parse(result.state);
138
- expect(parsed).not.toHaveProperty("codeVerifier");
139
- expect(parsed).not.toHaveProperty("redirectUri");
140
- expect(parsed).toHaveProperty("provider", "anthropic");
141
- expect(parsed).toHaveProperty("nonce");
142
- });
143
-
144
- it("consumeOAuthSession is one-shot: second call returns null", async () => {
145
- const { startOAuthFlow, consumeOAuthSession } = await import(
146
- "./oauth-pkce.js"
147
- );
148
- const result = await startOAuthFlow("anthropic", {
149
- redirectUri: "http://127.0.0.1:8765/callback",
150
- clientId: "test-client-id-anthropic",
151
- });
152
- const parsed = JSON.parse(result.state);
153
-
154
- const first = consumeOAuthSession(parsed.nonce);
155
- expect(first).not.toBeNull();
156
- expect(first!.codeVerifier).toBeTruthy();
157
- expect(first!.redirectUri).toBe("http://127.0.0.1:8765/callback");
158
-
159
- const second = consumeOAuthSession(parsed.nonce);
160
- expect(second).toBeNull();
161
- });
162
-
163
- it("cancelOAuthSession removes the session", async () => {
164
- const { startOAuthFlow, cancelOAuthSession, consumeOAuthSession } =
165
- await import("./oauth-pkce.js");
166
- const result = await startOAuthFlow("anthropic", {
167
- redirectUri: "http://127.0.0.1:8765/callback",
168
- clientId: "test-client-id-anthropic",
169
- });
170
- const parsed = JSON.parse(result.state);
171
-
172
- cancelOAuthSession(parsed.nonce);
173
- const session = consumeOAuthSession(parsed.nonce);
174
- expect(session).toBeNull();
175
- });
176
-
177
- it("consumeOAuthSession returns null for unknown nonce", async () => {
178
- const { consumeOAuthSession } = await import("./oauth-pkce.js");
179
- const result = consumeOAuthSession("nonexistent-nonce");
180
- expect(result).toBeNull();
181
- });
182
- });
183
-
184
- describe("refreshTokens", () => {
185
- it("throws when no store is provided", async () => {
186
- const { refreshTokens } = await import("./oauth-pkce.js");
187
- await expect(
188
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
189
- refreshTokens("anthropic", "old-refresh-token", undefined as any),
190
- ).rejects.toThrow("SecretStore is required");
191
- });
192
- });
193
-
194
- describe("defaultCallbackPort", () => {
195
- it("returns 1456 for anthropic", () => {
196
- expect(defaultCallbackPort("anthropic")).toBe(1456);
197
- });
198
-
199
- it("returns 1455 for openai", () => {
200
- expect(defaultCallbackPort("openai")).toBe(1455);
201
- });
202
- });
203
-
204
- describe("resolveClientId", () => {
205
- it("reads from the right env var for anthropic", () => {
206
- const id = resolveClientId("anthropic", {
207
- OFFROUTER_ANTHROPIC_CLIENT_ID: "ant-id-from-env",
208
- } as NodeJS.ProcessEnv);
209
- expect(id).toBe("ant-id-from-env");
210
- });
211
-
212
- it("reads from the right env var for openai", () => {
213
- const id = resolveClientId("openai", {
214
- OFFROUTER_OPENAI_CLIENT_ID: "oa-id-from-env",
215
- } as NodeJS.ProcessEnv);
216
- expect(id).toBe("oa-id-from-env");
217
- });
218
-
219
- it("returns undefined when env var is not set", () => {
220
- expect(resolveClientId("anthropic", {} as NodeJS.ProcessEnv)).toBeUndefined();
221
- });
222
- });
223
-
224
- describe("runOAuthLogin", () => {
225
- it("stores structured OAuth tokens in the keychain after the full flow", async () => {
226
- const dir = mkdtempSync(join(tmpdir(), "offrouter-run-oauth-"));
227
- try {
228
- const store = new FileSecretStore({ filePath: join(dir, "secrets.json") });
229
- const openBrowser = vi.fn();
230
- let capturedAuthorizeUrl = "";
231
-
232
- // Mock fetch: return a fake token response on the token exchange.
233
- const mockFetch = vi.fn(async (_url: string, init?: RequestInit) => {
234
- const body = typeof init?.body === "string" ? init.body : "";
235
- if (body.includes("grant_type=authorization_code")) {
236
- return new Response(
237
- JSON.stringify({
238
- access_token: "ant-access-token-mock",
239
- refresh_token: "ant-refresh-token-mock",
240
- expires_in: 3600,
241
- scope: "api offline_access",
242
- }),
243
- {
244
- status: 200,
245
- headers: { "content-type": "application/json" },
246
- },
247
- );
248
- }
249
- return new Response("ok", { status: 200 });
250
- });
251
-
252
- // Start login; it will block on waitForCode.
253
- const loginPromise = runOAuthLogin({
254
- provider: "anthropic",
255
- store,
256
- clientId: "test-client-id",
257
- port: 0,
258
- timeoutMs: 5000,
259
- openBrowser,
260
- onAuthorizeUrl: (url) => { capturedAuthorizeUrl = url; },
261
- fetch: mockFetch as unknown as typeof globalThis.fetch,
262
- });
263
-
264
- // Wait until the authorize URL is emitted (server is ready).
265
- await vi.waitFor(() => {
266
- expect(capturedAuthorizeUrl).toBeTruthy();
267
- }, { timeout: 2000, interval: 50 });
268
-
269
- // Parse url to get redirect_uri and state.
270
- const parsed = new URL(capturedAuthorizeUrl);
271
- const state = parsed.searchParams.get("state");
272
- const redirectUri = parsed.searchParams.get("redirect_uri");
273
- expect(state).toBeTruthy();
274
- expect(redirectUri).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/callback$/);
275
-
276
- // Emulate the provider callback.
277
- const cbRes = await fetch(
278
- `${redirectUri}?code=TESTCODE&state=${encodeURIComponent(state!)}`,
279
- { redirect: "manual" },
280
- );
281
- expect(cbRes.status).toBe(200);
282
- const body = await cbRes.text();
283
- expect(body).toMatch(/success|complete|authorized/i);
284
-
285
- // Await the login result.
286
- const result = await loginPromise;
287
- expect(result.provider).toBe("anthropic");
288
- expect(result.redirectUri).toBe(redirectUri);
289
- expect(result.expiresAt).toBeGreaterThan(Date.now());
290
-
291
- // Verify tokens stored in keychain.
292
- const stored = await getOAuthTokens("anthropic", store);
293
- expect(stored).toBeDefined();
294
- expect(stored!.accessToken).toBe("ant-access-token-mock");
295
- expect(stored!.refreshToken).toBe("ant-refresh-token-mock");
296
- expect(stored!.expiresAt).toBeGreaterThan(Date.now());
297
- expect(stored!.scope).toBe("api offline_access");
298
-
299
- // Verify openBrowser was called.
300
- expect(openBrowser).toHaveBeenCalledWith(capturedAuthorizeUrl);
301
- } finally {
302
- rmSync(dir, { recursive: true, force: true });
303
- }
304
- });
305
-
306
- it("throws for unsupported provider", async () => {
307
- const dir = mkdtempSync(join(tmpdir(), "offrouter-oauth-bad-"));
308
- try {
309
- const store = new FileSecretStore({ filePath: join(dir, "secrets.json") });
310
- await expect(
311
- runOAuthLogin({ provider: "gemini", store }),
312
- ).rejects.toThrow(/not supported/i);
313
- } finally {
314
- rmSync(dir, { recursive: true, force: true });
315
- }
316
- });
317
-
318
- it("throws if client id cannot be resolved", async () => {
319
- const dir = mkdtempSync(join(tmpdir(), "offrouter-oauth-noid-"));
320
- try {
321
- const store = new FileSecretStore({ filePath: join(dir, "secrets.json") });
322
- await expect(
323
- runOAuthLogin({ provider: "anthropic", store, port: 0 }),
324
- ).rejects.toThrow(/client id required|client.*id/i);
325
- } finally {
326
- rmSync(dir, { recursive: true, force: true });
327
- }
328
- });
329
- });