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,479 +0,0 @@
1
- import { afterEach, describe, expect, it } from "vitest";
2
- import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join, resolve } from "node:path";
5
- import {
6
- ConfigError,
7
- defaultConfig,
8
- loadConfig,
9
- resolveOffRouterHome,
10
- } from "./config.js";
11
- import { evaluatePolicy } from "./policy.js";
12
- import type { ProviderCandidate, RouteRequest } from "./types.js";
13
-
14
- async function makeHome(): Promise<string> {
15
- return mkdtemp(join(tmpdir(), "offrouter-home-"));
16
- }
17
-
18
- async function makeProject(): Promise<string> {
19
- return mkdtemp(join(tmpdir(), "offrouter-project-"));
20
- }
21
-
22
- const tempDirs: string[] = [];
23
-
24
- afterEach(async () => {
25
- await Promise.all(
26
- tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })),
27
- );
28
- });
29
-
30
- async function trackDir(maker: () => Promise<string>): Promise<string> {
31
- const dir = await maker();
32
- tempDirs.push(dir);
33
- return dir;
34
- }
35
-
36
- function baseRequest(
37
- profile: string,
38
- workspace: { cwd: string; trusted: boolean },
39
- ): RouteRequest {
40
- return {
41
- protocolVersion: "offrouter.route.v1",
42
- requestId: "req_config",
43
- harness: { kind: "claude", profile },
44
- task: {
45
- promptPreview: "explain this",
46
- promptDigest: "sha256:config",
47
- kind: "explain",
48
- risk: "low",
49
- },
50
- workspace,
51
- constraints: {
52
- subscriptionFirst: true,
53
- allowApiKeyFallback: false,
54
- },
55
- };
56
- }
57
-
58
- function subscriptionCandidate(): ProviderCandidate {
59
- return {
60
- providerId: "claude",
61
- modelId: "sonnet",
62
- authTier: "subscription",
63
- authScope: "first-party",
64
- subscriptionStatus: "active",
65
- health: "healthy",
66
- supportsTools: true,
67
- supportsStreaming: true,
68
- supportsJson: true,
69
- supportsImages: false,
70
- };
71
- }
72
-
73
- describe("defaultConfig", () => {
74
- it("denies *-work profiles and has no providers by default", () => {
75
- const config = defaultConfig();
76
-
77
- expect(config.policy.deniedProfilePatterns).toContain("*-work");
78
- expect(config.policy.allowlistedProfiles).toEqual([]);
79
- expect(config.providers).toEqual({});
80
- expect(Object.keys(config.profiles)).toEqual([]);
81
-
82
- const decision = evaluatePolicy(
83
- baseRequest("claude-work", { cwd: "/tmp/p", trusted: true }),
84
- [subscriptionCandidate()],
85
- config.policy,
86
- );
87
- expect(decision.allowed).toEqual([]);
88
- expect(decision.denials.some((d) => d.code === "work_profile_denied")).toBe(
89
- true,
90
- );
91
- });
92
- });
93
-
94
- describe("resolveOffRouterHome", () => {
95
- it("prefers OFFROUTER_HOME over ~/.offrouter", () => {
96
- const home = resolveOffRouterHome({
97
- env: { OFFROUTER_HOME: "/custom/offrouter-home" },
98
- homedir: () => "/Users/example",
99
- });
100
- expect(home).toBe("/custom/offrouter-home");
101
- });
102
-
103
- it("normalizes relative OFFROUTER_HOME values", () => {
104
- const home = resolveOffRouterHome({
105
- env: { OFFROUTER_HOME: "relative-offrouter-home" },
106
- homedir: () => "/Users/example",
107
- });
108
- expect(home).toBe(resolve("relative-offrouter-home"));
109
- });
110
-
111
- it("falls back to ~/.offrouter when OFFROUTER_HOME is unset", () => {
112
- const home = resolveOffRouterHome({
113
- env: {},
114
- homedir: () => "/Users/example",
115
- });
116
- expect(home).toBe(join("/Users/example", ".offrouter"));
117
- });
118
- });
119
-
120
- describe("loadConfig", () => {
121
- it("loads user config from temp OFFROUTER_HOME", async () => {
122
- const home = await trackDir(makeHome);
123
- await writeFile(
124
- join(home, "config.toml"),
125
- `
126
- allowlisted_profiles = ["claude-personal", "codex-personal"]
127
- denied_profile_patterns = ["*-work", "secret-*"]
128
-
129
- [policy]
130
- subscription_first = true
131
- allow_api_key_fallback = true
132
- allow_third_party_subscription_adapters = false
133
- denied_providers = ["shadow"]
134
-
135
- [providers.claude]
136
- enabled = true
137
- `,
138
- "utf8",
139
- );
140
-
141
- const config = await loadConfig({
142
- env: { OFFROUTER_HOME: home },
143
- });
144
-
145
- expect(config.homeDir).toBe(home);
146
- expect(config.sources).toContain(join(home, "config.toml"));
147
- expect(config.policy.allowlistedProfiles).toEqual([
148
- "claude-personal",
149
- "codex-personal",
150
- ]);
151
- expect(config.policy.deniedProfilePatterns).toEqual([
152
- "*-work",
153
- "secret-*",
154
- ]);
155
- expect(config.policy.allowThirdPartySubscriptionAdapters).toBe(false);
156
- expect(config.policy.deniedProviders).toEqual(["shadow"]);
157
- expect(config.policyDefaults.subscriptionFirst).toBe(true);
158
- expect(config.policyDefaults.allowApiKeyFallback).toBe(true);
159
- expect(config.providers).toEqual({
160
- claude: { id: "claude", enabled: true, accounts: [{ id: "claude" }] },
161
- });
162
-
163
- const allowed = evaluatePolicy(
164
- baseRequest("claude-personal", { cwd: "/tmp/p", trusted: true }),
165
- [subscriptionCandidate()],
166
- config.policy,
167
- );
168
- expect(allowed.allowed).toHaveLength(1);
169
- });
170
-
171
- it("parses policy.near_limit_threshold into policy.nearLimitThreshold", async () => {
172
- const home = await trackDir(makeHome);
173
- await writeFile(
174
- join(home, "config.toml"),
175
- `allowlisted_profiles = ["claude-personal"]
176
-
177
- [policy]
178
- near_limit_threshold = 0.15
179
- `,
180
- "utf8",
181
- );
182
-
183
- const config = await loadConfig({ env: { OFFROUTER_HOME: home } });
184
- expect(config.policy.nearLimitThreshold).toBe(0.15);
185
- });
186
-
187
- it("loads named profile overlays from profiles/*.toml", async () => {
188
- const home = await trackDir(makeHome);
189
- await writeFile(
190
- join(home, "config.toml"),
191
- `allowlisted_profiles = ["claude-personal"]
192
- `,
193
- "utf8",
194
- );
195
- await mkdir(join(home, "profiles"), { recursive: true });
196
- await writeFile(
197
- join(home, "profiles", "claude-personal.toml"),
198
- `
199
- id = "claude-personal"
200
- denied_providers = ["openai"]
201
- `,
202
- "utf8",
203
- );
204
-
205
- const config = await loadConfig({ env: { OFFROUTER_HOME: home } });
206
-
207
- expect(config.sources).toContain(
208
- join(home, "profiles", "claude-personal.toml"),
209
- );
210
- expect(config.profiles["claude-personal"]).toEqual({
211
- id: "claude-personal",
212
- deniedProviders: ["openai"],
213
- });
214
- });
215
-
216
- it("warns when profile files declare duplicate ids", async () => {
217
- const home = await trackDir(makeHome);
218
- await mkdir(join(home, "profiles"), { recursive: true });
219
- await writeFile(
220
- join(home, "profiles", "one.toml"),
221
- `
222
- id = "claude-personal"
223
- denied_providers = ["first"]
224
- `,
225
- "utf8",
226
- );
227
- await writeFile(
228
- join(home, "profiles", "two.toml"),
229
- `
230
- id = "claude-personal"
231
- denied_providers = ["second"]
232
- `,
233
- "utf8",
234
- );
235
-
236
- const config = await loadConfig({ env: { OFFROUTER_HOME: home } });
237
-
238
- expect(config.profiles["claude-personal"]).toEqual({
239
- id: "claude-personal",
240
- deniedProviders: ["second"],
241
- });
242
- expect(config.warnings).toContainEqual(
243
- expect.objectContaining({
244
- path: "profiles.claude-personal",
245
- }),
246
- );
247
- });
248
-
249
- it("reports file path and validation issue for invalid config", async () => {
250
- const home = await trackDir(makeHome);
251
- const configPath = join(home, "config.toml");
252
- await writeFile(
253
- configPath,
254
- `
255
- allowlisted_profiles = "not-an-array"
256
- `,
257
- "utf8",
258
- );
259
-
260
- await expect(
261
- loadConfig({ env: { OFFROUTER_HOME: home } }),
262
- ).rejects.toSatisfy((err: unknown) => {
263
- expect(err).toBeInstanceOf(ConfigError);
264
- const ce = err as ConfigError;
265
- expect(ce.filePath).toBe(configPath);
266
- expect(ce.message).toContain(configPath);
267
- expect(ce.issues.length).toBeGreaterThan(0);
268
- expect(ce.issues.some((issue) => /allowlisted_profiles|array/i.test(issue))).toBe(
269
- true,
270
- );
271
- return true;
272
- });
273
- });
274
-
275
- it("reports invalid profile file path and validation issue", async () => {
276
- const home = await trackDir(makeHome);
277
- await writeFile(join(home, "config.toml"), "", "utf8");
278
- await mkdir(join(home, "profiles"), { recursive: true });
279
- const profilePath = join(home, "profiles", "broken.toml");
280
- await writeFile(
281
- profilePath,
282
- `
283
- denied_providers = 42
284
- `,
285
- "utf8",
286
- );
287
-
288
- await expect(
289
- loadConfig({ env: { OFFROUTER_HOME: home } }),
290
- ).rejects.toSatisfy((err: unknown) => {
291
- expect(err).toBeInstanceOf(ConfigError);
292
- const ce = err as ConfigError;
293
- expect(ce.filePath).toBe(profilePath);
294
- expect(ce.message).toContain(profilePath);
295
- expect(ce.issues.length).toBeGreaterThan(0);
296
- return true;
297
- });
298
- });
299
-
300
- it("ignores project .offrouter/config.toml unless workspace is trusted", async () => {
301
- const home = await trackDir(makeHome);
302
- await writeFile(
303
- join(home, "config.toml"),
304
- `allowlisted_profiles = ["claude-personal"]
305
- `,
306
- "utf8",
307
- );
308
-
309
- const project = await trackDir(makeProject);
310
- await mkdir(join(project, ".offrouter"), { recursive: true });
311
- await writeFile(
312
- join(project, ".offrouter", "config.toml"),
313
- `
314
- allowlisted_profiles = ["claude-personal", "project-enabled"]
315
- [providers.project-agent]
316
- enabled = true
317
- `,
318
- "utf8",
319
- );
320
-
321
- const untrusted = await loadConfig({
322
- env: { OFFROUTER_HOME: home },
323
- workspaceDir: project,
324
- workspaceTrusted: false,
325
- });
326
- expect(untrusted.policy.allowlistedProfiles).toEqual(["claude-personal"]);
327
- expect(untrusted.providers).toEqual({});
328
- expect(untrusted.sources).not.toContain(
329
- join(project, ".offrouter", "config.toml"),
330
- );
331
-
332
- const trusted = await loadConfig({
333
- env: { OFFROUTER_HOME: home },
334
- workspaceDir: project,
335
- workspaceTrusted: true,
336
- });
337
- expect(trusted.policy.allowlistedProfiles).toEqual([
338
- "claude-personal",
339
- "project-enabled",
340
- ]);
341
- expect(trusted.providers).toEqual({
342
- "project-agent": { id: "project-agent", enabled: true, accounts: [{ id: "project-agent" }] },
343
- });
344
- expect(trusted.sources).toContain(
345
- join(project, ".offrouter", "config.toml"),
346
- );
347
- });
348
-
349
- it("accumulates denied profile patterns across user and trusted project config", async () => {
350
- const home = await trackDir(makeHome);
351
- await writeFile(
352
- join(home, "config.toml"),
353
- `
354
- denied_profile_patterns = ["secret-*"]
355
- `,
356
- "utf8",
357
- );
358
-
359
- const project = await trackDir(makeProject);
360
- await mkdir(join(project, ".offrouter"), { recursive: true });
361
- await writeFile(
362
- join(project, ".offrouter", "config.toml"),
363
- `
364
- denied_profile_patterns = ["client-*"]
365
- `,
366
- "utf8",
367
- );
368
-
369
- const config = await loadConfig({
370
- env: { OFFROUTER_HOME: home },
371
- workspaceDir: project,
372
- workspaceTrusted: true,
373
- });
374
-
375
- expect(config.policy.deniedProfilePatterns).toEqual([
376
- "*-work",
377
- "secret-*",
378
- "client-*",
379
- ]);
380
- });
381
-
382
- it("emits strict unknown-key warnings without failing load", async () => {
383
- const home = await trackDir(makeHome);
384
- await writeFile(
385
- join(home, "config.toml"),
386
- `
387
- allowlisted_profiles = ["claude-personal"]
388
- mystery_knob = true
389
-
390
- [policy]
391
- subscription_first = true
392
- secret_backdoor = "nope"
393
- `,
394
- "utf8",
395
- );
396
- await mkdir(join(home, "profiles"), { recursive: true });
397
- await writeFile(
398
- join(home, "profiles", "claude-personal.toml"),
399
- `
400
- id = "claude-personal"
401
- weird_flag = 1
402
- `,
403
- "utf8",
404
- );
405
-
406
- const config = await loadConfig({ env: { OFFROUTER_HOME: home } });
407
-
408
- expect(config.policy.allowlistedProfiles).toEqual(["claude-personal"]);
409
- expect(config.warnings.length).toBeGreaterThanOrEqual(3);
410
-
411
- const messages = config.warnings.map((w) => w.message).join("\n");
412
- expect(messages).toMatch(/mystery_knob/);
413
- expect(messages).toMatch(/secret_backdoor/);
414
- expect(messages).toMatch(/weird_flag/);
415
-
416
- for (const warning of config.warnings) {
417
- expect(warning.path.length).toBeGreaterThan(0);
418
- }
419
- });
420
-
421
- it("uses defaults when config.toml is missing", async () => {
422
- const home = await trackDir(makeHome);
423
- const config = await loadConfig({ env: { OFFROUTER_HOME: home } });
424
-
425
- expect(config.policy.deniedProfilePatterns).toContain("*-work");
426
- expect(config.policy.allowlistedProfiles).toEqual([]);
427
- expect(config.providers).toEqual({});
428
- expect(config.sources).toEqual([]);
429
- expect(config.warnings).toEqual([]);
430
- });
431
-
432
- describe("multi-account provider config", () => {
433
- it("parses TOML with explicit accounts array", async () => {
434
- const home = await trackDir(makeHome);
435
- await writeFile(
436
- join(home, "config.toml"),
437
- `
438
- allowlisted_profiles = ["claude-personal"]
439
-
440
- [[providers.anthropic.accounts]]
441
- id = "anthropic-1"
442
- label = "Work Account"
443
-
444
- [[providers.anthropic.accounts]]
445
- id = "anthropic-2"
446
- label = "Personal Account"
447
- `,
448
- "utf8",
449
- );
450
-
451
- const config = await loadConfig({ env: { OFFROUTER_HOME: home } });
452
- expect(config.providers.anthropic).toBeDefined();
453
- expect(config.providers.anthropic.accounts).toEqual([
454
- { id: "anthropic-1", label: "Work Account" },
455
- { id: "anthropic-2", label: "Personal Account" },
456
- ]);
457
- expect(config.providers.anthropic.enabled).toBe(true);
458
- });
459
-
460
- it("creates implicit account when no accounts array is present", async () => {
461
- const home = await trackDir(makeHome);
462
- await writeFile(
463
- join(home, "config.toml"),
464
- `
465
- allowlisted_profiles = ["codex-personal"]
466
-
467
- [providers.openai]
468
- enabled = true
469
- `,
470
- "utf8",
471
- );
472
-
473
- const config = await loadConfig({ env: { OFFROUTER_HOME: home } });
474
- expect(config.providers.openai).toBeDefined();
475
- expect(config.providers.openai.accounts).toEqual([{ id: "openai" }]);
476
- expect(config.providers.openai.enabled).toBe(true);
477
- });
478
- });
479
- });