offrouter-core 0.0.0

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 (41) hide show
  1. package/package.json +21 -0
  2. package/src/audit.test.ts +302 -0
  3. package/src/audit.ts +553 -0
  4. package/src/auth/credential-chain.test.ts +202 -0
  5. package/src/auth/credential-chain.ts +181 -0
  6. package/src/auth/index.ts +32 -0
  7. package/src/auth/keychain.test.ts +75 -0
  8. package/src/auth/keychain.ts +39 -0
  9. package/src/auth/oauth-pkce.test.ts +184 -0
  10. package/src/auth/oauth-pkce.ts +409 -0
  11. package/src/config.test.ts +415 -0
  12. package/src/config.ts +484 -0
  13. package/src/delegation.test.ts +368 -0
  14. package/src/delegation.ts +605 -0
  15. package/src/index.ts +261 -0
  16. package/src/policy.test.ts +634 -0
  17. package/src/policy.ts +418 -0
  18. package/src/protocol.ts +7 -0
  19. package/src/providers/adapter.test.ts +293 -0
  20. package/src/providers/adapter.ts +324 -0
  21. package/src/providers/catalog-data.test.ts +258 -0
  22. package/src/providers/catalog-data.ts +498 -0
  23. package/src/providers/catalog.test.ts +84 -0
  24. package/src/providers/catalog.ts +483 -0
  25. package/src/providers/fake.ts +312 -0
  26. package/src/providers/openai-compatible.test.ts +366 -0
  27. package/src/providers/openai-compatible.ts +554 -0
  28. package/src/proxy/responses-mapper.test.ts +290 -0
  29. package/src/proxy/responses-mapper.ts +736 -0
  30. package/src/proxy/responses-server.test.ts +322 -0
  31. package/src/proxy/responses-server.ts +469 -0
  32. package/src/router.test.ts +562 -0
  33. package/src/router.ts +323 -0
  34. package/src/schemas.test.ts +240 -0
  35. package/src/schemas.ts +203 -0
  36. package/src/secrets.test.ts +271 -0
  37. package/src/secrets.ts +461 -0
  38. package/src/types.ts +167 -0
  39. package/src/usage.test.ts +283 -0
  40. package/src/usage.ts +669 -0
  41. package/tsconfig.json +9 -0
@@ -0,0 +1,415 @@
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 },
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("loads named profile overlays from profiles/*.toml", async () => {
172
+ const home = await trackDir(makeHome);
173
+ await writeFile(
174
+ join(home, "config.toml"),
175
+ `allowlisted_profiles = ["claude-personal"]
176
+ `,
177
+ "utf8",
178
+ );
179
+ await mkdir(join(home, "profiles"), { recursive: true });
180
+ await writeFile(
181
+ join(home, "profiles", "claude-personal.toml"),
182
+ `
183
+ id = "claude-personal"
184
+ denied_providers = ["openai"]
185
+ `,
186
+ "utf8",
187
+ );
188
+
189
+ const config = await loadConfig({ env: { OFFROUTER_HOME: home } });
190
+
191
+ expect(config.sources).toContain(
192
+ join(home, "profiles", "claude-personal.toml"),
193
+ );
194
+ expect(config.profiles["claude-personal"]).toEqual({
195
+ id: "claude-personal",
196
+ deniedProviders: ["openai"],
197
+ });
198
+ });
199
+
200
+ it("warns when profile files declare duplicate ids", async () => {
201
+ const home = await trackDir(makeHome);
202
+ await mkdir(join(home, "profiles"), { recursive: true });
203
+ await writeFile(
204
+ join(home, "profiles", "one.toml"),
205
+ `
206
+ id = "claude-personal"
207
+ denied_providers = ["first"]
208
+ `,
209
+ "utf8",
210
+ );
211
+ await writeFile(
212
+ join(home, "profiles", "two.toml"),
213
+ `
214
+ id = "claude-personal"
215
+ denied_providers = ["second"]
216
+ `,
217
+ "utf8",
218
+ );
219
+
220
+ const config = await loadConfig({ env: { OFFROUTER_HOME: home } });
221
+
222
+ expect(config.profiles["claude-personal"]).toEqual({
223
+ id: "claude-personal",
224
+ deniedProviders: ["second"],
225
+ });
226
+ expect(config.warnings).toContainEqual(
227
+ expect.objectContaining({
228
+ path: "profiles.claude-personal",
229
+ }),
230
+ );
231
+ });
232
+
233
+ it("reports file path and validation issue for invalid config", async () => {
234
+ const home = await trackDir(makeHome);
235
+ const configPath = join(home, "config.toml");
236
+ await writeFile(
237
+ configPath,
238
+ `
239
+ allowlisted_profiles = "not-an-array"
240
+ `,
241
+ "utf8",
242
+ );
243
+
244
+ await expect(
245
+ loadConfig({ env: { OFFROUTER_HOME: home } }),
246
+ ).rejects.toSatisfy((err: unknown) => {
247
+ expect(err).toBeInstanceOf(ConfigError);
248
+ const ce = err as ConfigError;
249
+ expect(ce.filePath).toBe(configPath);
250
+ expect(ce.message).toContain(configPath);
251
+ expect(ce.issues.length).toBeGreaterThan(0);
252
+ expect(ce.issues.some((issue) => /allowlisted_profiles|array/i.test(issue))).toBe(
253
+ true,
254
+ );
255
+ return true;
256
+ });
257
+ });
258
+
259
+ it("reports invalid profile file path and validation issue", async () => {
260
+ const home = await trackDir(makeHome);
261
+ await writeFile(join(home, "config.toml"), "", "utf8");
262
+ await mkdir(join(home, "profiles"), { recursive: true });
263
+ const profilePath = join(home, "profiles", "broken.toml");
264
+ await writeFile(
265
+ profilePath,
266
+ `
267
+ denied_providers = 42
268
+ `,
269
+ "utf8",
270
+ );
271
+
272
+ await expect(
273
+ loadConfig({ env: { OFFROUTER_HOME: home } }),
274
+ ).rejects.toSatisfy((err: unknown) => {
275
+ expect(err).toBeInstanceOf(ConfigError);
276
+ const ce = err as ConfigError;
277
+ expect(ce.filePath).toBe(profilePath);
278
+ expect(ce.message).toContain(profilePath);
279
+ expect(ce.issues.length).toBeGreaterThan(0);
280
+ return true;
281
+ });
282
+ });
283
+
284
+ it("ignores project .offrouter/config.toml unless workspace is trusted", async () => {
285
+ const home = await trackDir(makeHome);
286
+ await writeFile(
287
+ join(home, "config.toml"),
288
+ `allowlisted_profiles = ["claude-personal"]
289
+ `,
290
+ "utf8",
291
+ );
292
+
293
+ const project = await trackDir(makeProject);
294
+ await mkdir(join(project, ".offrouter"), { recursive: true });
295
+ await writeFile(
296
+ join(project, ".offrouter", "config.toml"),
297
+ `
298
+ allowlisted_profiles = ["claude-personal", "project-enabled"]
299
+ [providers.project-agent]
300
+ enabled = true
301
+ `,
302
+ "utf8",
303
+ );
304
+
305
+ const untrusted = await loadConfig({
306
+ env: { OFFROUTER_HOME: home },
307
+ workspaceDir: project,
308
+ workspaceTrusted: false,
309
+ });
310
+ expect(untrusted.policy.allowlistedProfiles).toEqual(["claude-personal"]);
311
+ expect(untrusted.providers).toEqual({});
312
+ expect(untrusted.sources).not.toContain(
313
+ join(project, ".offrouter", "config.toml"),
314
+ );
315
+
316
+ const trusted = await loadConfig({
317
+ env: { OFFROUTER_HOME: home },
318
+ workspaceDir: project,
319
+ workspaceTrusted: true,
320
+ });
321
+ expect(trusted.policy.allowlistedProfiles).toEqual([
322
+ "claude-personal",
323
+ "project-enabled",
324
+ ]);
325
+ expect(trusted.providers).toEqual({
326
+ "project-agent": { id: "project-agent", enabled: true },
327
+ });
328
+ expect(trusted.sources).toContain(
329
+ join(project, ".offrouter", "config.toml"),
330
+ );
331
+ });
332
+
333
+ it("accumulates denied profile patterns across user and trusted project config", async () => {
334
+ const home = await trackDir(makeHome);
335
+ await writeFile(
336
+ join(home, "config.toml"),
337
+ `
338
+ denied_profile_patterns = ["secret-*"]
339
+ `,
340
+ "utf8",
341
+ );
342
+
343
+ const project = await trackDir(makeProject);
344
+ await mkdir(join(project, ".offrouter"), { recursive: true });
345
+ await writeFile(
346
+ join(project, ".offrouter", "config.toml"),
347
+ `
348
+ denied_profile_patterns = ["client-*"]
349
+ `,
350
+ "utf8",
351
+ );
352
+
353
+ const config = await loadConfig({
354
+ env: { OFFROUTER_HOME: home },
355
+ workspaceDir: project,
356
+ workspaceTrusted: true,
357
+ });
358
+
359
+ expect(config.policy.deniedProfilePatterns).toEqual([
360
+ "*-work",
361
+ "secret-*",
362
+ "client-*",
363
+ ]);
364
+ });
365
+
366
+ it("emits strict unknown-key warnings without failing load", async () => {
367
+ const home = await trackDir(makeHome);
368
+ await writeFile(
369
+ join(home, "config.toml"),
370
+ `
371
+ allowlisted_profiles = ["claude-personal"]
372
+ mystery_knob = true
373
+
374
+ [policy]
375
+ subscription_first = true
376
+ secret_backdoor = "nope"
377
+ `,
378
+ "utf8",
379
+ );
380
+ await mkdir(join(home, "profiles"), { recursive: true });
381
+ await writeFile(
382
+ join(home, "profiles", "claude-personal.toml"),
383
+ `
384
+ id = "claude-personal"
385
+ weird_flag = 1
386
+ `,
387
+ "utf8",
388
+ );
389
+
390
+ const config = await loadConfig({ env: { OFFROUTER_HOME: home } });
391
+
392
+ expect(config.policy.allowlistedProfiles).toEqual(["claude-personal"]);
393
+ expect(config.warnings.length).toBeGreaterThanOrEqual(3);
394
+
395
+ const messages = config.warnings.map((w) => w.message).join("\n");
396
+ expect(messages).toMatch(/mystery_knob/);
397
+ expect(messages).toMatch(/secret_backdoor/);
398
+ expect(messages).toMatch(/weird_flag/);
399
+
400
+ for (const warning of config.warnings) {
401
+ expect(warning.path.length).toBeGreaterThan(0);
402
+ }
403
+ });
404
+
405
+ it("uses defaults when config.toml is missing", async () => {
406
+ const home = await trackDir(makeHome);
407
+ const config = await loadConfig({ env: { OFFROUTER_HOME: home } });
408
+
409
+ expect(config.policy.deniedProfilePatterns).toContain("*-work");
410
+ expect(config.policy.allowlistedProfiles).toEqual([]);
411
+ expect(config.providers).toEqual({});
412
+ expect(config.sources).toEqual([]);
413
+ expect(config.warnings).toEqual([]);
414
+ });
415
+ });