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.
- package/dist/audit.d.ts +137 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +320 -0
- package/dist/audit.js.map +1 -0
- package/dist/auth/credential-chain.d.ts +84 -0
- package/dist/auth/credential-chain.d.ts.map +1 -0
- package/dist/auth/credential-chain.js +150 -0
- package/dist/auth/credential-chain.js.map +1 -0
- package/dist/auth/index.d.ts +10 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +7 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/keychain.d.ts +60 -0
- package/dist/auth/keychain.d.ts.map +1 -0
- package/dist/auth/keychain.js +104 -0
- package/dist/auth/keychain.js.map +1 -0
- package/dist/auth/oauth-pkce.d.ts +138 -0
- package/dist/auth/oauth-pkce.d.ts.map +1 -0
- package/dist/auth/oauth-pkce.js +375 -0
- package/dist/auth/oauth-pkce.js.map +1 -0
- package/dist/auth/oauth-server.d.ts +36 -0
- package/dist/auth/oauth-server.d.ts.map +1 -0
- package/dist/auth/oauth-server.js +210 -0
- package/dist/auth/oauth-server.js.map +1 -0
- package/dist/config.d.ts +76 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +355 -0
- package/dist/config.js.map +1 -0
- package/dist/delegation.d.ts +123 -0
- package/dist/delegation.d.ts.map +1 -0
- package/dist/delegation.js +455 -0
- package/dist/delegation.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/policy.d.ts +45 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +318 -0
- package/dist/policy.js.map +1 -0
- package/{src/protocol.ts → dist/protocol.d.ts} +2 -2
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +6 -0
- package/dist/protocol.js.map +1 -0
- package/dist/providers/adapter.d.ts +137 -0
- package/dist/providers/adapter.d.ts.map +1 -0
- package/dist/providers/adapter.js +163 -0
- package/dist/providers/adapter.js.map +1 -0
- package/dist/providers/catalog-data.d.ts +128 -0
- package/dist/providers/catalog-data.d.ts.map +1 -0
- package/dist/providers/catalog-data.js +397 -0
- package/dist/providers/catalog-data.js.map +1 -0
- package/dist/providers/catalog.d.ts +63 -0
- package/dist/providers/catalog.d.ts.map +1 -0
- package/dist/providers/catalog.js +394 -0
- package/dist/providers/catalog.js.map +1 -0
- package/dist/providers/fake.d.ts +46 -0
- package/dist/providers/fake.d.ts.map +1 -0
- package/dist/providers/fake.js +234 -0
- package/dist/providers/fake.js.map +1 -0
- package/dist/providers/openai-compatible.d.ts +65 -0
- package/dist/providers/openai-compatible.d.ts.map +1 -0
- package/dist/providers/openai-compatible.js +434 -0
- package/dist/providers/openai-compatible.js.map +1 -0
- package/dist/proxy/responses-mapper.d.ts +366 -0
- package/dist/proxy/responses-mapper.d.ts.map +1 -0
- package/dist/proxy/responses-mapper.js +517 -0
- package/dist/proxy/responses-mapper.js.map +1 -0
- package/dist/proxy/responses-server.d.ts +29 -0
- package/dist/proxy/responses-server.d.ts.map +1 -0
- package/dist/proxy/responses-server.js +360 -0
- package/dist/proxy/responses-server.js.map +1 -0
- package/dist/router.d.ts +18 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +296 -0
- package/dist/router.js.map +1 -0
- package/dist/schemas.d.ts +560 -0
- package/dist/schemas.d.ts.map +1 -0
- package/{src/schemas.ts → dist/schemas.js} +83 -103
- package/dist/schemas.js.map +1 -0
- package/dist/secrets.d.ts +112 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +326 -0
- package/dist/secrets.js.map +1 -0
- package/dist/types.d.ts +117 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/usage-file.d.ts +59 -0
- package/dist/usage-file.d.ts.map +1 -0
- package/dist/usage-file.js +316 -0
- package/dist/usage-file.js.map +1 -0
- package/dist/usage.d.ts +235 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +517 -0
- package/dist/usage.js.map +1 -0
- package/package.json +9 -4
- package/src/audit.test.ts +0 -302
- package/src/audit.ts +0 -553
- package/src/auth/credential-chain.test.ts +0 -326
- package/src/auth/credential-chain.ts +0 -235
- package/src/auth/index.ts +0 -45
- package/src/auth/keychain.test.ts +0 -265
- package/src/auth/keychain.ts +0 -168
- package/src/auth/oauth-pkce.test.ts +0 -329
- package/src/auth/oauth-pkce.ts +0 -571
- package/src/auth/oauth-server.test.ts +0 -83
- package/src/auth/oauth-server.ts +0 -296
- package/src/config.test.ts +0 -479
- package/src/config.ts +0 -505
- package/src/delegation.test.ts +0 -368
- package/src/delegation.ts +0 -605
- package/src/index.ts +0 -280
- package/src/policy.test.ts +0 -634
- package/src/policy.ts +0 -420
- package/src/providers/adapter.test.ts +0 -293
- package/src/providers/adapter.ts +0 -328
- package/src/providers/catalog-data.test.ts +0 -258
- package/src/providers/catalog-data.ts +0 -498
- package/src/providers/catalog.test.ts +0 -84
- package/src/providers/catalog.ts +0 -483
- package/src/providers/fake.ts +0 -312
- package/src/providers/openai-compatible.test.ts +0 -366
- package/src/providers/openai-compatible.ts +0 -554
- package/src/proxy/responses-mapper.test.ts +0 -290
- package/src/proxy/responses-mapper.ts +0 -736
- package/src/proxy/responses-server.test.ts +0 -322
- package/src/proxy/responses-server.ts +0 -469
- package/src/router.test.ts +0 -699
- package/src/router.ts +0 -352
- package/src/schemas.test.ts +0 -291
- package/src/secrets.test.ts +0 -271
- package/src/secrets.ts +0 -461
- package/src/types.ts +0 -173
- package/src/usage-file.test.ts +0 -243
- package/src/usage-file.ts +0 -435
- package/src/usage.test.ts +0 -335
- package/src/usage.ts +0 -859
- package/tsconfig.json +0 -9
package/src/config.test.ts
DELETED
|
@@ -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
|
-
});
|