offrouter-core 0.2.1 → 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 +7 -2
  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
package/src/config.ts DELETED
@@ -1,505 +0,0 @@
1
- /**
2
- * OffRouter V1 user/project config loader.
3
- * Loads TOML from OFFROUTER_HOME (or ~/.offrouter) with Zod validation.
4
- * Project .offrouter/config.toml is ignored unless the workspace is trusted.
5
- */
6
- import { readdir, readFile } from "node:fs/promises";
7
- import { homedir as osHomedir } from "node:os";
8
- import { join, resolve } from "node:path";
9
- import { parse as parseToml } from "smol-toml";
10
- import { z } from "zod";
11
- import type { PolicyConfig } from "./policy.js";
12
-
13
- /** Defaults applied when user config omits policy knobs. */
14
- export interface PolicyDefaults {
15
- subscriptionFirst: boolean;
16
- allowApiKeyFallback: boolean;
17
- }
18
-
19
- /** Declared provider entry from config (no secrets here). */
20
- export interface ConfiguredProvider {
21
- id: string;
22
- enabled: boolean;
23
- accounts: ConfiguredAccount[];
24
- }
25
-
26
- /** Named profile overlay loaded from profiles/*.toml. */
27
- export interface ConfiguredProfile {
28
- id: string;
29
- deniedProviders?: string[];
30
- allowApiKeyFallback?: boolean;
31
- subscriptionFirst?: boolean;
32
- }
33
-
34
- /** Single configured account within a provider. */
35
- export interface ConfiguredAccount {
36
- id: string;
37
- label?: string;
38
- }
39
-
40
- export interface ConfigWarning {
41
- filePath: string;
42
- path: string;
43
- message: string;
44
- }
45
-
46
- export interface OffRouterConfig {
47
- homeDir: string;
48
- sources: string[];
49
- policy: PolicyConfig;
50
- policyDefaults: PolicyDefaults;
51
- providers: Record<string, ConfiguredProvider>;
52
- profiles: Record<string, ConfiguredProfile>;
53
- warnings: ConfigWarning[];
54
- }
55
-
56
- export interface LoadConfigOptions {
57
- /** Override process.env (for tests). */
58
- env?: NodeJS.ProcessEnv;
59
- /** Override os.homedir (for tests). */
60
- homedir?: () => string;
61
- /** Workspace directory that may contain .offrouter/config.toml. */
62
- workspaceDir?: string;
63
- /**
64
- * Whether the workspace is trusted. Project config is ignored when false
65
- * or when workspaceDir is unset.
66
- */
67
- workspaceTrusted?: boolean;
68
- }
69
-
70
- export interface ResolveHomeOptions {
71
- env?: NodeJS.ProcessEnv;
72
- homedir?: () => string;
73
- }
74
-
75
- export class ConfigError extends Error {
76
- readonly filePath: string;
77
- readonly issues: string[];
78
-
79
- constructor(filePath: string, issues: string[], cause?: unknown) {
80
- const detail =
81
- issues.length > 0 ? issues.join("; ") : "unknown validation error";
82
- super(`Invalid OffRouter config in ${filePath}: ${detail}`, {
83
- cause,
84
- });
85
- this.name = "ConfigError";
86
- this.filePath = filePath;
87
- this.issues = issues;
88
- }
89
- }
90
-
91
- type AllowedNode = AllowedShape | "leaf" | "record";
92
- interface AllowedShape {
93
- [key: string]: AllowedNode;
94
- }
95
-
96
- function allowedFromZodShape(shape: z.ZodRawShape): AllowedShape {
97
- return Object.fromEntries(
98
- Object.keys(shape).map((key) => [key, "leaf" as const]),
99
- );
100
- }
101
-
102
- const ProviderConfigShape = {
103
- enabled: z.boolean().optional(),
104
- accounts: z
105
- .array(z.object({ id: z.string().min(1), label: z.string().optional() }).strict())
106
- .optional(),
107
- } satisfies z.ZodRawShape;
108
-
109
- const PolicySectionShape = {
110
- subscription_first: z.boolean().optional(),
111
- allow_api_key_fallback: z.boolean().optional(),
112
- allow_third_party_subscription_adapters: z.boolean().optional(),
113
- denied_providers: z.array(z.string().min(1)).optional(),
114
- near_limit_threshold: z.number().min(0).max(1).optional(),
115
- } satisfies z.ZodRawShape;
116
-
117
- const RootConfigShape = {
118
- allowlisted_profiles: z.array(z.string().min(1)).optional(),
119
- denied_profile_patterns: z.array(z.string().min(1)).optional(),
120
- policy: z.object(PolicySectionShape).strict().optional(),
121
- providers: z
122
- .record(z.string().min(1), z.object(ProviderConfigShape).strict())
123
- .optional(),
124
- } satisfies z.ZodRawShape;
125
-
126
- const ProfileFileShape = {
127
- id: z.string().min(1).optional(),
128
- denied_providers: z.array(z.string().min(1)).optional(),
129
- allow_api_key_fallback: z.boolean().optional(),
130
- subscription_first: z.boolean().optional(),
131
- } satisfies z.ZodRawShape;
132
-
133
- const RootConfigSchema = z.object(RootConfigShape).strict();
134
- const ProfileFileSchema = z.object(ProfileFileShape).strict();
135
-
136
- const PROVIDER_ALLOWED: AllowedShape = {
137
- ...allowedFromZodShape(ProviderConfigShape),
138
- accounts: "leaf",
139
- };
140
- const ROOT_ALLOWED: AllowedShape = {
141
- ...allowedFromZodShape(RootConfigShape),
142
- policy: allowedFromZodShape(PolicySectionShape),
143
- providers: "record",
144
- };
145
- const PROFILE_ALLOWED: AllowedShape = allowedFromZodShape(ProfileFileShape);
146
-
147
- type RootConfig = z.infer<typeof RootConfigSchema>;
148
- type ProfileFile = z.infer<typeof ProfileFileSchema>;
149
-
150
- const DEFAULT_DENIED_PROFILE_PATTERNS = ["*-work"] as const;
151
-
152
- /**
153
- * Safe default config: work profiles denied, nothing allowlisted, no providers.
154
- */
155
- export function defaultConfig(homeDir = ""): OffRouterConfig {
156
- return {
157
- homeDir,
158
- sources: [],
159
- policy: {
160
- allowlistedProfiles: [],
161
- deniedProfilePatterns: [...DEFAULT_DENIED_PROFILE_PATTERNS],
162
- allowThirdPartySubscriptionAdapters: false,
163
- deniedProviders: [],
164
- },
165
- policyDefaults: {
166
- subscriptionFirst: true,
167
- allowApiKeyFallback: false,
168
- },
169
- providers: {},
170
- profiles: {},
171
- warnings: [],
172
- };
173
- }
174
-
175
- export function resolveOffRouterHome(
176
- options: ResolveHomeOptions = {},
177
- ): string {
178
- const env = options.env ?? process.env;
179
- const home = options.homedir ?? osHomedir;
180
- const override = env.OFFROUTER_HOME?.trim();
181
- if (override) {
182
- return resolve(override);
183
- }
184
- return resolve(join(home(), ".offrouter"));
185
- }
186
-
187
- function isPlainObject(value: unknown): value is Record<string, unknown> {
188
- return typeof value === "object" && value !== null && !Array.isArray(value);
189
- }
190
-
191
- function formatZodIssues(error: z.ZodError): string[] {
192
- return error.issues.map((issue) => {
193
- const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
194
- return `${path}: ${issue.message}`;
195
- });
196
- }
197
-
198
- /**
199
- * Non-Zod path walker that flags keys absent from a schema tree.
200
- * Uses Zod internals only as type-shape guidance via a parallel known-keys model.
201
- */
202
- function collectUnknownKeyWarnings(
203
- value: unknown,
204
- allowed: AllowedShape,
205
- filePath: string,
206
- pathPrefix: string,
207
- ): ConfigWarning[] {
208
- if (!isPlainObject(value)) {
209
- return [];
210
- }
211
-
212
- const warnings: ConfigWarning[] = [];
213
- for (const key of Object.keys(value)) {
214
- const keyPath = pathPrefix ? `${pathPrefix}.${key}` : key;
215
- const child = allowed[key];
216
- if (child === undefined) {
217
- warnings.push({
218
- filePath,
219
- path: keyPath,
220
- message: `Unknown config key "${keyPath}" in ${filePath}`,
221
- });
222
- continue;
223
- }
224
- if (child === "leaf") {
225
- continue;
226
- }
227
- if (child === "record") {
228
- const recordValue = value[key];
229
- if (isPlainObject(recordValue)) {
230
- for (const nestedKey of Object.keys(recordValue)) {
231
- const nestedPath = `${keyPath}.${nestedKey}`;
232
- warnings.push(
233
- ...collectUnknownKeyWarnings(
234
- recordValue[nestedKey],
235
- PROVIDER_ALLOWED,
236
- filePath,
237
- nestedPath,
238
- ),
239
- );
240
- }
241
- }
242
- continue;
243
- }
244
- warnings.push(
245
- ...collectUnknownKeyWarnings(value[key], child, filePath, keyPath),
246
- );
247
- }
248
- return warnings;
249
- }
250
-
251
- function parseTomlFile(content: string, filePath: string): unknown {
252
- try {
253
- return parseToml(content);
254
- } catch (err) {
255
- const message = err instanceof Error ? err.message : String(err);
256
- throw new ConfigError(filePath, [`parse error: ${message}`], err);
257
- }
258
- }
259
-
260
- function validateRoot(
261
- raw: unknown,
262
- filePath: string,
263
- ): { data: RootConfig; warnings: ConfigWarning[] } {
264
- const warnings = collectUnknownKeyWarnings(raw, ROOT_ALLOWED, filePath, "");
265
- // Strip unknown keys before strict Zod parse so warnings ≠ hard failure.
266
- const stripped = stripUnknown(raw, ROOT_ALLOWED);
267
- const parsed = RootConfigSchema.safeParse(stripped);
268
- if (!parsed.success) {
269
- throw new ConfigError(filePath, formatZodIssues(parsed.error), parsed.error);
270
- }
271
- return { data: parsed.data, warnings };
272
- }
273
-
274
- function validateProfile(
275
- raw: unknown,
276
- filePath: string,
277
- fallbackId: string,
278
- ): { data: ConfiguredProfile; warnings: ConfigWarning[] } {
279
- const warnings = collectUnknownKeyWarnings(
280
- raw,
281
- PROFILE_ALLOWED,
282
- filePath,
283
- "",
284
- );
285
- const stripped = stripUnknown(raw, PROFILE_ALLOWED);
286
- const parsed = ProfileFileSchema.safeParse(stripped);
287
- if (!parsed.success) {
288
- throw new ConfigError(filePath, formatZodIssues(parsed.error), parsed.error);
289
- }
290
- const profile: ProfileFile = parsed.data;
291
- const data: ConfiguredProfile = {
292
- id: profile.id ?? fallbackId,
293
- };
294
- if (profile.denied_providers !== undefined) {
295
- data.deniedProviders = profile.denied_providers;
296
- }
297
- if (profile.allow_api_key_fallback !== undefined) {
298
- data.allowApiKeyFallback = profile.allow_api_key_fallback;
299
- }
300
- if (profile.subscription_first !== undefined) {
301
- data.subscriptionFirst = profile.subscription_first;
302
- }
303
-
304
- return {
305
- data,
306
- warnings,
307
- };
308
- }
309
-
310
- function stripUnknown(value: unknown, allowed: AllowedShape): unknown {
311
- if (!isPlainObject(value)) {
312
- return value;
313
- }
314
- const out: Record<string, unknown> = {};
315
- for (const [key, child] of Object.entries(allowed)) {
316
- if (!(key in value)) continue;
317
- const v = value[key];
318
- if (child === "leaf") {
319
- out[key] = v;
320
- } else if (child === "record") {
321
- if (isPlainObject(v)) {
322
- const recordOut: Record<string, unknown> = {};
323
- for (const [nestedKey, nestedVal] of Object.entries(v)) {
324
- recordOut[nestedKey] = stripUnknown(nestedVal, PROVIDER_ALLOWED);
325
- }
326
- out[key] = recordOut;
327
- } else {
328
- out[key] = v;
329
- }
330
- } else {
331
- out[key] = stripUnknown(v, child);
332
- }
333
- }
334
- return out;
335
- }
336
-
337
- function mergeRootInto(
338
- target: OffRouterConfig,
339
- root: RootConfig,
340
- sourcePath: string,
341
- ): void {
342
- target.sources.push(sourcePath);
343
-
344
- if (root.allowlisted_profiles) {
345
- target.policy.allowlistedProfiles = uniqueStrings([
346
- ...target.policy.allowlistedProfiles,
347
- ...root.allowlisted_profiles,
348
- ]);
349
- }
350
-
351
- if (root.denied_profile_patterns) {
352
- target.policy.deniedProfilePatterns = uniqueStrings([
353
- ...(target.policy.deniedProfilePatterns ?? [
354
- ...DEFAULT_DENIED_PROFILE_PATTERNS,
355
- ]),
356
- ...root.denied_profile_patterns,
357
- ]);
358
- }
359
-
360
- if (root.policy) {
361
- if (root.policy.subscription_first !== undefined) {
362
- target.policyDefaults.subscriptionFirst = root.policy.subscription_first;
363
- }
364
- if (root.policy.allow_api_key_fallback !== undefined) {
365
- target.policyDefaults.allowApiKeyFallback =
366
- root.policy.allow_api_key_fallback;
367
- }
368
- if (root.policy.allow_third_party_subscription_adapters !== undefined) {
369
- target.policy.allowThirdPartySubscriptionAdapters =
370
- root.policy.allow_third_party_subscription_adapters;
371
- }
372
- if (root.policy.near_limit_threshold !== undefined) {
373
- target.policy.nearLimitThreshold = root.policy.near_limit_threshold;
374
- }
375
- if (root.policy.denied_providers) {
376
- target.policy.deniedProviders = uniqueStrings([
377
- ...(target.policy.deniedProviders ?? []),
378
- ...root.policy.denied_providers,
379
- ]);
380
- }
381
- }
382
-
383
- if (root.providers) {
384
- for (const [id, provider] of Object.entries(root.providers)) {
385
- const accounts: ConfiguredAccount[] = provider.accounts
386
- ? provider.accounts.map((a: { id: string; label?: string }) => ({ id: a.id, label: a.label }))
387
- : [{ id }];
388
- target.providers[id] = {
389
- id,
390
- enabled: provider.enabled ?? true,
391
- accounts,
392
- };
393
- }
394
- }
395
- }
396
-
397
- function uniqueStrings(values: string[]): string[] {
398
- return [...new Set(values)];
399
- }
400
-
401
- async function readOptionalFile(path: string): Promise<string | null> {
402
- try {
403
- return await readFile(path, "utf8");
404
- } catch (err) {
405
- if (
406
- err &&
407
- typeof err === "object" &&
408
- "code" in err &&
409
- (err as { code?: string }).code === "ENOENT"
410
- ) {
411
- return null;
412
- }
413
- throw err;
414
- }
415
- }
416
-
417
- async function loadRootFile(
418
- filePath: string,
419
- target: OffRouterConfig,
420
- ): Promise<void> {
421
- const content = await readOptionalFile(filePath);
422
- if (content === null) {
423
- return;
424
- }
425
- // Empty file is valid and means no overrides.
426
- if (content.trim() === "") {
427
- target.sources.push(filePath);
428
- return;
429
- }
430
- const raw = parseTomlFile(content, filePath);
431
- const { data, warnings } = validateRoot(raw, filePath);
432
- target.warnings.push(...warnings);
433
- mergeRootInto(target, data, filePath);
434
- }
435
-
436
- async function loadProfileDir(
437
- dirPath: string,
438
- target: OffRouterConfig,
439
- ): Promise<void> {
440
- let entries: string[];
441
- try {
442
- entries = await readdir(dirPath);
443
- } catch (err) {
444
- if (
445
- err &&
446
- typeof err === "object" &&
447
- "code" in err &&
448
- (err as { code?: string }).code === "ENOENT"
449
- ) {
450
- return;
451
- }
452
- throw err;
453
- }
454
-
455
- const tomlFiles = entries
456
- .filter((name) => name.endsWith(".toml"))
457
- .sort((a, b) => a.localeCompare(b));
458
-
459
- for (const name of tomlFiles) {
460
- const filePath = join(dirPath, name);
461
- const content = await readFile(filePath, "utf8");
462
- const raw =
463
- content.trim() === "" ? {} : parseTomlFile(content, filePath);
464
- const fallbackId = name.replace(/\.toml$/i, "");
465
- const { data, warnings } = validateProfile(raw, filePath, fallbackId);
466
- target.warnings.push(...warnings);
467
- target.sources.push(filePath);
468
- if (target.profiles[data.id]) {
469
- target.warnings.push({
470
- filePath,
471
- path: `profiles.${data.id}`,
472
- message: `Duplicate profile id "${data.id}" in ${filePath}; later profile file overwrote an earlier definition.`,
473
- });
474
- }
475
- target.profiles[data.id] = data;
476
- }
477
- }
478
-
479
- /**
480
- * Load OffRouter configuration.
481
- *
482
- * Resolution order:
483
- * 1. defaults (deny *-work, empty allowlist, no providers)
484
- * 2. `$OFFROUTER_HOME/config.toml` or `~/.offrouter/config.toml`
485
- * 3. `$OFFROUTER_HOME/profiles/*.toml`
486
- * 4. `<workspace>/.offrouter/config.toml` only when workspaceTrusted
487
- */
488
- export async function loadConfig(
489
- options: LoadConfigOptions = {},
490
- ): Promise<OffRouterConfig> {
491
- const homeDir = resolveOffRouterHome(options);
492
- const config = defaultConfig(homeDir);
493
-
494
- await loadRootFile(join(homeDir, "config.toml"), config);
495
- await loadProfileDir(join(homeDir, "profiles"), config);
496
-
497
- if (options.workspaceDir && options.workspaceTrusted) {
498
- await loadRootFile(
499
- join(options.workspaceDir, ".offrouter", "config.toml"),
500
- config,
501
- );
502
- }
503
-
504
- return config;
505
- }