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.
- 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 +7 -2
- 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
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for credential-chain resolver.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, expect, it, vi } from "vitest";
|
|
5
|
-
import {
|
|
6
|
-
Redacted,
|
|
7
|
-
resolveCredential,
|
|
8
|
-
type CredentialSource,
|
|
9
|
-
} from "./credential-chain.js";
|
|
10
|
-
import type { SecretStore } from "../secrets.js";
|
|
11
|
-
|
|
12
|
-
function stubStore(entries: Record<string, string>): SecretStore {
|
|
13
|
-
return {
|
|
14
|
-
get: vi.fn(async (id: string) => entries[id]),
|
|
15
|
-
set: vi.fn(),
|
|
16
|
-
delete: vi.fn(),
|
|
17
|
-
listProviders: vi.fn(async () => Object.keys(entries)),
|
|
18
|
-
redact: vi.fn((v: string) => v),
|
|
19
|
-
toJSON: vi.fn(() => ({})),
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe("Redacted", () => {
|
|
24
|
-
it("wraps a value and hides it in toJSON", () => {
|
|
25
|
-
const r = new Redacted("sk-ant-test123");
|
|
26
|
-
expect(r.rawValue).toBe("sk-ant-test123");
|
|
27
|
-
expect(r.toJSON()).toBe("[REDACTED]");
|
|
28
|
-
expect(JSON.stringify(r)).toBe('"[REDACTED]"');
|
|
29
|
-
expect(String(r)).toBe("[REDACTED]");
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("returns a non-empty rawValue", () => {
|
|
33
|
-
const r = new Redacted("some-token");
|
|
34
|
-
expect(r.rawValue).toBe("some-token");
|
|
35
|
-
expect(r.rawValue.length).toBeGreaterThan(0);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("resolveCredential", () => {
|
|
40
|
-
it("returns null for empty sources array", async () => {
|
|
41
|
-
const result = await resolveCredential([], { store: stubStore({}) });
|
|
42
|
-
expect(result).toBeNull();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe("env source", () => {
|
|
46
|
-
it("returns value from process.env", async () => {
|
|
47
|
-
const prev = process.env["OFFROUTER_TEST_KEY"];
|
|
48
|
-
process.env["OFFROUTER_TEST_KEY"] = "env-value";
|
|
49
|
-
try {
|
|
50
|
-
const sources: CredentialSource[] = [
|
|
51
|
-
{ type: "env", name: "OFFROUTER_TEST_KEY" },
|
|
52
|
-
];
|
|
53
|
-
const result = await resolveCredential(sources, { store: stubStore({}) });
|
|
54
|
-
expect(result).not.toBeNull();
|
|
55
|
-
expect(result!.value.rawValue).toBe("env-value");
|
|
56
|
-
expect(result!.source).toBe("env");
|
|
57
|
-
} finally {
|
|
58
|
-
if (prev === undefined) {
|
|
59
|
-
delete process.env["OFFROUTER_TEST_KEY"];
|
|
60
|
-
} else {
|
|
61
|
-
process.env["OFFROUTER_TEST_KEY"] = prev;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("skips env when variable is undefined", async () => {
|
|
67
|
-
const sources: CredentialSource[] = [
|
|
68
|
-
{ type: "env", name: "OFFROUTER_DOES_NOT_EXIST_XYZ" },
|
|
69
|
-
];
|
|
70
|
-
const result = await resolveCredential(sources, { store: stubStore({}) });
|
|
71
|
-
expect(result).toBeNull();
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe("keychain source", () => {
|
|
76
|
-
it("returns value from secret store", async () => {
|
|
77
|
-
const store = stubStore({ "anthropic": "sk-ant-keychain-test" });
|
|
78
|
-
const sources: CredentialSource[] = [
|
|
79
|
-
{ type: "keychain", providerId: "anthropic" },
|
|
80
|
-
];
|
|
81
|
-
const result = await resolveCredential(sources, { store });
|
|
82
|
-
expect(result).not.toBeNull();
|
|
83
|
-
expect(result!.value.rawValue).toBe("sk-ant-keychain-test");
|
|
84
|
-
expect(result!.source).toBe("keychain");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("skips keychain when store returns undefined", async () => {
|
|
88
|
-
const store = stubStore({});
|
|
89
|
-
const sources: CredentialSource[] = [
|
|
90
|
-
{ type: "keychain", providerId: "unknown" },
|
|
91
|
-
];
|
|
92
|
-
const result = await resolveCredential(sources, { store });
|
|
93
|
-
expect(result).toBeNull();
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe("chain order", () => {
|
|
98
|
-
it("returns first non-empty source (env beats keychain)", async () => {
|
|
99
|
-
process.env["ANTHROPIC_API_KEY"] = "from-env";
|
|
100
|
-
try {
|
|
101
|
-
const store = stubStore({ "anthropic": "from-keychain" });
|
|
102
|
-
const sources: CredentialSource[] = [
|
|
103
|
-
{ type: "env", name: "ANTHROPIC_API_KEY" },
|
|
104
|
-
{ type: "keychain", providerId: "anthropic" },
|
|
105
|
-
];
|
|
106
|
-
const result = await resolveCredential(sources, { store });
|
|
107
|
-
expect(result).not.toBeNull();
|
|
108
|
-
expect(result!.value.rawValue).toBe("from-env");
|
|
109
|
-
expect(result!.source).toBe("env");
|
|
110
|
-
} finally {
|
|
111
|
-
delete process.env["ANTHROPIC_API_KEY"];
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("falls through to keychain when env is missing", async () => {
|
|
116
|
-
const store = stubStore({ "openai": "sk-from-keychain" });
|
|
117
|
-
const sources: CredentialSource[] = [
|
|
118
|
-
{ type: "env", name: "OPENAI_API_KEY_MISSING" },
|
|
119
|
-
{ type: "keychain", providerId: "openai" },
|
|
120
|
-
];
|
|
121
|
-
const result = await resolveCredential(sources, { store });
|
|
122
|
-
expect(result).not.toBeNull();
|
|
123
|
-
expect(result!.value.rawValue).toBe("sk-from-keychain");
|
|
124
|
-
expect(result!.source).toBe("keychain");
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("returns null when all sources fail", async () => {
|
|
128
|
-
const store = stubStore({});
|
|
129
|
-
const sources: CredentialSource[] = [
|
|
130
|
-
{ type: "env", name: "MISSING_VAR_1" },
|
|
131
|
-
{ type: "env", name: "MISSING_VAR_2" },
|
|
132
|
-
{ type: "keychain", providerId: "nonexistent" },
|
|
133
|
-
];
|
|
134
|
-
const result = await resolveCredential(sources, { store });
|
|
135
|
-
expect(result).toBeNull();
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
describe("config source (trusted config)", () => {
|
|
140
|
-
it("returns value from trusted config provider apiKey", async () => {
|
|
141
|
-
const store = stubStore({});
|
|
142
|
-
const sources: CredentialSource[] = [
|
|
143
|
-
{ type: "config", key: "providers.anthropic.apiKey" },
|
|
144
|
-
];
|
|
145
|
-
const result = await resolveCredential(sources, {
|
|
146
|
-
store,
|
|
147
|
-
config: { providers: { anthropic: { apiKey: "sk-ant-config" } } } as Record<string, unknown>,
|
|
148
|
-
});
|
|
149
|
-
expect(result).not.toBeNull();
|
|
150
|
-
expect(result!.value.rawValue).toBe("sk-ant-config");
|
|
151
|
-
expect(result!.source).toBe("config");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("returns null when config is not provided", async () => {
|
|
155
|
-
const store = stubStore({});
|
|
156
|
-
const sources: CredentialSource[] = [
|
|
157
|
-
{ type: "config", key: "providers.anthropic.apiKey" },
|
|
158
|
-
];
|
|
159
|
-
const result = await resolveCredential(sources, { store });
|
|
160
|
-
expect(result).toBeNull();
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("handles dot-notation path", async () => {
|
|
164
|
-
const store = stubStore({});
|
|
165
|
-
const sources: CredentialSource[] = [
|
|
166
|
-
{ type: "config", key: "providers.openai.apiKey" },
|
|
167
|
-
];
|
|
168
|
-
const result = await resolveCredential(sources, {
|
|
169
|
-
store,
|
|
170
|
-
config: {
|
|
171
|
-
providers: {
|
|
172
|
-
openai: { enabled: true },
|
|
173
|
-
anthropic: { apiKey: "wrong-key" },
|
|
174
|
-
},
|
|
175
|
-
} as Record<string, unknown>,
|
|
176
|
-
});
|
|
177
|
-
expect(result).toBeNull();
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
describe("credentialSource type narrowing", () => {
|
|
183
|
-
it("env source type is correct literal", () => {
|
|
184
|
-
const s: CredentialSource = { type: "env", name: "API_KEY" };
|
|
185
|
-
expect(s.type).toBe("env");
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it("keychain source type is correct literal", () => {
|
|
189
|
-
const s: CredentialSource = { type: "keychain", providerId: "anthropic" };
|
|
190
|
-
expect(s.type).toBe("keychain");
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("config source type is correct literal", () => {
|
|
194
|
-
const s: CredentialSource = { type: "config", key: "providers.anthropic.apiKey" };
|
|
195
|
-
expect(s.type).toBe("config");
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("oauth source type is correct literal", () => {
|
|
199
|
-
const s: CredentialSource = { type: "oauth", provider: "anthropic" };
|
|
200
|
-
expect(s.type).toBe("oauth");
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
describe("oauth credential source", () => {
|
|
205
|
-
function storeWithOAuth(
|
|
206
|
-
accessToken: string,
|
|
207
|
-
refreshToken?: string,
|
|
208
|
-
expiresAt?: number,
|
|
209
|
-
): SecretStore {
|
|
210
|
-
const tokens = {
|
|
211
|
-
accessToken,
|
|
212
|
-
refreshToken,
|
|
213
|
-
expiresAt,
|
|
214
|
-
obtainedAt: 1_000,
|
|
215
|
-
};
|
|
216
|
-
return {
|
|
217
|
-
get: vi.fn(async (id: string) =>
|
|
218
|
-
id === "anthropic:oauth" ? JSON.stringify(tokens) : undefined,
|
|
219
|
-
),
|
|
220
|
-
set: vi.fn(),
|
|
221
|
-
delete: vi.fn(),
|
|
222
|
-
listProviders: vi.fn(async () => ["anthropic:oauth"]),
|
|
223
|
-
redact: vi.fn((v: string) => v),
|
|
224
|
-
toJSON: vi.fn(() => ({})),
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
it("resolves a stored (non-expired) OAuth token", async () => {
|
|
229
|
-
const store = storeWithOAuth(
|
|
230
|
-
"ant-oauth-token",
|
|
231
|
-
"ant-refresh",
|
|
232
|
-
Date.now() + 3600_000,
|
|
233
|
-
);
|
|
234
|
-
const sources: CredentialSource[] = [
|
|
235
|
-
{ type: "oauth", provider: "anthropic" },
|
|
236
|
-
];
|
|
237
|
-
const result = await resolveCredential(sources, { store });
|
|
238
|
-
expect(result).not.toBeNull();
|
|
239
|
-
expect(result!.value.rawValue).toBe("ant-oauth-token");
|
|
240
|
-
expect(result!.source).toBe("oauth");
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it("returns null when no OAuth tokens are stored", async () => {
|
|
244
|
-
const store = stubStore({});
|
|
245
|
-
const sources: CredentialSource[] = [
|
|
246
|
-
{ type: "oauth", provider: "anthropic" },
|
|
247
|
-
];
|
|
248
|
-
const result = await resolveCredential(sources, { store });
|
|
249
|
-
expect(result).toBeNull();
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it("returns null for an expired token without a refresh callback", async () => {
|
|
253
|
-
const store = storeWithOAuth(
|
|
254
|
-
"ant-oauth-expired",
|
|
255
|
-
"ant-refresh",
|
|
256
|
-
Date.now() - 1000,
|
|
257
|
-
);
|
|
258
|
-
const sources: CredentialSource[] = [
|
|
259
|
-
{ type: "oauth", provider: "anthropic" },
|
|
260
|
-
];
|
|
261
|
-
const result = await resolveCredential(sources, { store });
|
|
262
|
-
expect(result).toBeNull();
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it("calls refreshOAuthToken for expired tokens when callback is provided", async () => {
|
|
266
|
-
const store = storeWithOAuth(
|
|
267
|
-
"ant-oauth-expired",
|
|
268
|
-
"ant-refresh",
|
|
269
|
-
Date.now() - 1000,
|
|
270
|
-
);
|
|
271
|
-
const refreshCallback = vi.fn(async (provider: string) => {
|
|
272
|
-
expect(provider).toBe("anthropic");
|
|
273
|
-
return "ant-oauth-refreshed";
|
|
274
|
-
});
|
|
275
|
-
const sources: CredentialSource[] = [
|
|
276
|
-
{ type: "oauth", provider: "anthropic" },
|
|
277
|
-
];
|
|
278
|
-
const result = await resolveCredential(sources, {
|
|
279
|
-
store,
|
|
280
|
-
refreshOAuthToken: refreshCallback,
|
|
281
|
-
});
|
|
282
|
-
expect(result).not.toBeNull();
|
|
283
|
-
expect(result!.value.rawValue).toBe("ant-oauth-refreshed");
|
|
284
|
-
expect(result!.source).toBe("oauth");
|
|
285
|
-
expect(refreshCallback).toHaveBeenCalledWith("anthropic");
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it("falls through when refreshOAuthToken returns null", async () => {
|
|
289
|
-
const store = storeWithOAuth(
|
|
290
|
-
"ant-oauth-expired",
|
|
291
|
-
"ant-refresh",
|
|
292
|
-
Date.now() - 1000,
|
|
293
|
-
);
|
|
294
|
-
const refreshCallback = vi.fn(async () => null);
|
|
295
|
-
const sources: CredentialSource[] = [
|
|
296
|
-
{ type: "oauth", provider: "anthropic" },
|
|
297
|
-
{ type: "keychain", providerId: "fallback" },
|
|
298
|
-
];
|
|
299
|
-
// No fallback token stored, so overall null.
|
|
300
|
-
const result = await resolveCredential(sources, {
|
|
301
|
-
store,
|
|
302
|
-
refreshOAuthToken: refreshCallback,
|
|
303
|
-
});
|
|
304
|
-
expect(result).toBeNull();
|
|
305
|
-
expect(refreshCallback).toHaveBeenCalledWith("anthropic");
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
it("falls through when refreshOAuthToken throws", async () => {
|
|
309
|
-
const store = storeWithOAuth(
|
|
310
|
-
"ant-oauth-expired",
|
|
311
|
-
"ant-refresh",
|
|
312
|
-
Date.now() - 1000,
|
|
313
|
-
);
|
|
314
|
-
const refreshCallback = vi.fn(async () => {
|
|
315
|
-
throw new Error("network error");
|
|
316
|
-
});
|
|
317
|
-
const sources: CredentialSource[] = [
|
|
318
|
-
{ type: "oauth", provider: "anthropic" },
|
|
319
|
-
];
|
|
320
|
-
const result = await resolveCredential(sources, {
|
|
321
|
-
store,
|
|
322
|
-
refreshOAuthToken: refreshCallback,
|
|
323
|
-
});
|
|
324
|
-
expect(result).toBeNull();
|
|
325
|
-
});
|
|
326
|
-
});
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Credential-chain resolution for OffRouter.
|
|
3
|
-
*
|
|
4
|
-
* Tries each source in order: env -> config -> keychain -> (oauth, opt-in).
|
|
5
|
-
* Never logs secret values. Uses Redacted<T> wrapper for safe output.
|
|
6
|
-
* Never scrapes other CLI credential stores.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { SecretStore } from "../secrets.js";
|
|
10
|
-
import {
|
|
11
|
-
accountStorageKey,
|
|
12
|
-
getOAuthTokens,
|
|
13
|
-
isOAuthTokenExpired,
|
|
14
|
-
} from "./keychain.js";
|
|
15
|
-
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// Redacted wrapper
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Wrapper that hides secret material from JSON serialization and string
|
|
22
|
-
* conversion. The raw value is accessible via `.rawValue` for use in
|
|
23
|
-
* Authorization headers, never for logging.
|
|
24
|
-
*/
|
|
25
|
-
export class Redacted<T extends string = string> {
|
|
26
|
-
readonly #value: T;
|
|
27
|
-
|
|
28
|
-
constructor(value: T) {
|
|
29
|
-
this.#value = value;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Access the raw secret value. Use only for provider API calls. */
|
|
33
|
-
get rawValue(): T {
|
|
34
|
-
return this.#value;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Returns a constant redacted string for JSON serialization. */
|
|
38
|
-
toJSON(): string {
|
|
39
|
-
return "[REDACTED]";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Returns a constant redacted string for debugging. */
|
|
43
|
-
toString(): string {
|
|
44
|
-
return "[REDACTED]";
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
// Credential source types
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* A single credential source in the chain.
|
|
54
|
-
* Each source is tried in order; the first non-empty value wins.
|
|
55
|
-
*/
|
|
56
|
-
export type CredentialSource =
|
|
57
|
-
| { type: "env"; name: string }
|
|
58
|
-
| { type: "config"; key: string }
|
|
59
|
-
| { type: "keychain"; providerId: string; accountId?: string }
|
|
60
|
-
| { type: "oauth"; provider: string };
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Result of a successful credential resolution.
|
|
64
|
-
*/
|
|
65
|
-
export interface CredentialResult {
|
|
66
|
-
/**
|
|
67
|
-
* The resolved credential value, wrapped in Redacted so it cannot be
|
|
68
|
-
* accidentally logged or serialized.
|
|
69
|
-
*/
|
|
70
|
-
value: Redacted<string>;
|
|
71
|
-
/** Which source type produced the value. */
|
|
72
|
-
source: "env" | "config" | "keychain" | "oauth";
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Options for resolveCredential.
|
|
77
|
-
*/
|
|
78
|
-
export interface ResolveCredentialOptions {
|
|
79
|
-
/** Secret store for keychain lookups. */
|
|
80
|
-
store: SecretStore;
|
|
81
|
-
/**
|
|
82
|
-
* Optional trusted config object. When provided, config sources can read
|
|
83
|
-
* provider-level apiKey values. Only use with trusted config from the
|
|
84
|
-
* user's OffRouter home directory.
|
|
85
|
-
*/
|
|
86
|
-
config?: Record<string, unknown>;
|
|
87
|
-
/**
|
|
88
|
-
* Injectable fetch for any network calls during credential resolution
|
|
89
|
-
* (e.g. OAuth token refresh). Defaults to globalThis.fetch.
|
|
90
|
-
*/
|
|
91
|
-
fetch?: typeof globalThis.fetch;
|
|
92
|
-
/**
|
|
93
|
-
* Optional OAuth token refresher. Given a provider id, refreshes the stored
|
|
94
|
-
* OAuth token (if possible) and returns the new access token, or null if
|
|
95
|
-
* refresh is not possible. When omitted, expired OAuth tokens resolve as
|
|
96
|
-
* null (no silent refresh during credential resolution).
|
|
97
|
-
*/
|
|
98
|
-
refreshOAuthToken?: (provider: string) => Promise<string | null>;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Resolve a credential by trying each source in order.
|
|
103
|
-
* Returns the first non-empty value, or null if all sources are empty.
|
|
104
|
-
*/
|
|
105
|
-
export async function resolveCredential(
|
|
106
|
-
sources: CredentialSource[],
|
|
107
|
-
opts: ResolveCredentialOptions,
|
|
108
|
-
): Promise<CredentialResult | null> {
|
|
109
|
-
for (const source of sources) {
|
|
110
|
-
const value = await trySource(source, opts);
|
|
111
|
-
if (value !== null) {
|
|
112
|
-
return { value: new Redacted(value), source: source.type };
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Try a single credential source.
|
|
120
|
-
* Returns the raw secret string or null if not found.
|
|
121
|
-
*/
|
|
122
|
-
async function trySource(
|
|
123
|
-
source: CredentialSource,
|
|
124
|
-
opts: ResolveCredentialOptions,
|
|
125
|
-
): Promise<string | null> {
|
|
126
|
-
switch (source.type) {
|
|
127
|
-
case "env": {
|
|
128
|
-
return tryEnv(source.name);
|
|
129
|
-
}
|
|
130
|
-
case "config": {
|
|
131
|
-
return tryConfig(source.key, opts.config);
|
|
132
|
-
}
|
|
133
|
-
case "keychain": {
|
|
134
|
-
return tryKeychain(source.providerId, opts.store, source.accountId);
|
|
135
|
-
}
|
|
136
|
-
case "oauth": {
|
|
137
|
-
// Resolve stored OAuth tokens. This does NOT trigger a login flow;
|
|
138
|
-
// it resolves tokens already obtained via the `login` command.
|
|
139
|
-
// If the token is expired and a refresher is wired, try refreshing.
|
|
140
|
-
return tryOAuth(
|
|
141
|
-
source.provider,
|
|
142
|
-
opts.store,
|
|
143
|
-
opts.refreshOAuthToken,
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Read a value from environment variables.
|
|
151
|
-
*/
|
|
152
|
-
function tryEnv(name: string): string | null {
|
|
153
|
-
const value = process.env[name];
|
|
154
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
155
|
-
return value.trim();
|
|
156
|
-
}
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Read a value from trusted config using dot-notation path.
|
|
162
|
-
* Only reads from config if provided (trusted source).
|
|
163
|
-
* Path format: "providers.{providerId}.apiKey"
|
|
164
|
-
*/
|
|
165
|
-
function tryConfig(key: string, config?: Record<string, unknown>): string | null {
|
|
166
|
-
if (!config) {
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
const parts = key.split(".");
|
|
170
|
-
let current: unknown = config;
|
|
171
|
-
for (const part of parts) {
|
|
172
|
-
if (typeof current !== "object" || current === null) {
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
const obj = current as Record<string, unknown>;
|
|
176
|
-
if (!(part in obj)) {
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
current = obj[part];
|
|
180
|
-
}
|
|
181
|
-
if (typeof current === "string" && current.trim().length > 0) {
|
|
182
|
-
return current.trim();
|
|
183
|
-
}
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Read a value from the secret store (keychain-backed).
|
|
189
|
-
*/
|
|
190
|
-
async function tryKeychain(
|
|
191
|
-
providerId: string,
|
|
192
|
-
store: SecretStore,
|
|
193
|
-
accountId?: string,
|
|
194
|
-
): Promise<string | null> {
|
|
195
|
-
try {
|
|
196
|
-
const key = accountStorageKey(providerId, accountId);
|
|
197
|
-
const value = await store.get(key);
|
|
198
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
199
|
-
return value.trim();
|
|
200
|
-
}
|
|
201
|
-
} catch {
|
|
202
|
-
// KeychainUnavailableError or other errors: treat as missing.
|
|
203
|
-
}
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Resolve a credential from stored OAuth tokens (no new login flow).
|
|
209
|
-
* Returns the access token if valid, triggers refresh if expired + wired,
|
|
210
|
-
* or returns null if no tokens exist or refresh fails.
|
|
211
|
-
*/
|
|
212
|
-
async function tryOAuth(
|
|
213
|
-
providerId: string,
|
|
214
|
-
store: SecretStore,
|
|
215
|
-
refresh?: (provider: string) => Promise<string | null>,
|
|
216
|
-
): Promise<string | null> {
|
|
217
|
-
try {
|
|
218
|
-
const tokens = await getOAuthTokens(providerId, store);
|
|
219
|
-
if (!tokens) return null;
|
|
220
|
-
|
|
221
|
-
if (!isOAuthTokenExpired(tokens)) {
|
|
222
|
-
return tokens.accessToken;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Expired: try refresh if a refresher is wired.
|
|
226
|
-
if (!refresh) return null;
|
|
227
|
-
try {
|
|
228
|
-
return await refresh(providerId);
|
|
229
|
-
} catch {
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
} catch {
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
}
|
package/src/auth/index.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auth module barrel export for OffRouter.
|
|
3
|
-
*/
|
|
4
|
-
export {
|
|
5
|
-
Redacted,
|
|
6
|
-
resolveCredential,
|
|
7
|
-
} from "./credential-chain.js";
|
|
8
|
-
export type {
|
|
9
|
-
CredentialResult,
|
|
10
|
-
CredentialSource,
|
|
11
|
-
ResolveCredentialOptions,
|
|
12
|
-
} from "./credential-chain.js";
|
|
13
|
-
|
|
14
|
-
export {
|
|
15
|
-
deleteOAuthTokens,
|
|
16
|
-
getOAuthTokens,
|
|
17
|
-
getProviderToken,
|
|
18
|
-
isOAuthTokenExpired,
|
|
19
|
-
setOAuthTokens,
|
|
20
|
-
setProviderToken,
|
|
21
|
-
} from "./keychain.js";
|
|
22
|
-
export type { OAuthStoredTokens } from "./keychain.js";
|
|
23
|
-
|
|
24
|
-
export {
|
|
25
|
-
defaultCallbackPort,
|
|
26
|
-
exchangeCodeForTokens,
|
|
27
|
-
generateCodeChallenge,
|
|
28
|
-
generateCodeVerifier,
|
|
29
|
-
isOAuthProvider,
|
|
30
|
-
PKCE_CONFIGS,
|
|
31
|
-
refreshTokens,
|
|
32
|
-
resolveClientId,
|
|
33
|
-
runOAuthLogin,
|
|
34
|
-
startOAuthFlow,
|
|
35
|
-
} from "./oauth-pkce.js";
|
|
36
|
-
export type {
|
|
37
|
-
OAuthFlowResult,
|
|
38
|
-
OAuthFlowState,
|
|
39
|
-
OAuthProviderConfig,
|
|
40
|
-
OAuthStartOptions,
|
|
41
|
-
OAuthTokenOptions,
|
|
42
|
-
OAuthTokenResponse,
|
|
43
|
-
RunOAuthLoginOptions,
|
|
44
|
-
RunOAuthLoginResult,
|
|
45
|
-
} from "./oauth-pkce.js";
|