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
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for keychain-backed token helpers.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
5
|
-
import {
|
|
6
|
-
deleteOAuthTokens,
|
|
7
|
-
getOAuthTokens,
|
|
8
|
-
getProviderToken,
|
|
9
|
-
isOAuthTokenExpired,
|
|
10
|
-
setOAuthTokens,
|
|
11
|
-
setProviderToken,
|
|
12
|
-
type OAuthStoredTokens,
|
|
13
|
-
} from "./keychain.js";
|
|
14
|
-
import { FileSecretStore } from "../secrets.js";
|
|
15
|
-
import { tmpdir } from "node:os";
|
|
16
|
-
import { join } from "node:path";
|
|
17
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
18
|
-
|
|
19
|
-
describe("getProviderToken", () => {
|
|
20
|
-
it("returns undefined when no token is stored", async () => {
|
|
21
|
-
const dir = mkdtempSync(join(tmpdir(), "offrouter-keychain-test-"));
|
|
22
|
-
try {
|
|
23
|
-
const store = new FileSecretStore({ filePath: join(dir, "secrets.json") });
|
|
24
|
-
const result = await getProviderToken("anthropic", store);
|
|
25
|
-
expect(result).toBeUndefined();
|
|
26
|
-
} finally {
|
|
27
|
-
rmSync(dir, { recursive: true, force: true });
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("returns the token when stored", async () => {
|
|
32
|
-
const dir = mkdtempSync(join(tmpdir(), "offrouter-keychain-test-"));
|
|
33
|
-
try {
|
|
34
|
-
const store = new FileSecretStore({ filePath: join(dir, "secrets.json") });
|
|
35
|
-
await store.set("anthropic", "sk-ant-stored-value");
|
|
36
|
-
const result = await getProviderToken("anthropic", store);
|
|
37
|
-
expect(result).not.toBeUndefined();
|
|
38
|
-
expect(result!.rawValue).toBe("sk-ant-stored-value");
|
|
39
|
-
expect(JSON.stringify(result)).toBe('"[REDACTED]"');
|
|
40
|
-
} finally {
|
|
41
|
-
rmSync(dir, { recursive: true, force: true });
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("returns undefined for nonexistent provider", async () => {
|
|
46
|
-
const dir = mkdtempSync(join(tmpdir(), "offrouter-keychain-test-"));
|
|
47
|
-
try {
|
|
48
|
-
const store = new FileSecretStore({ filePath: join(dir, "secrets.json") });
|
|
49
|
-
const result = await getProviderToken("nonexistent", store);
|
|
50
|
-
expect(result).toBeUndefined();
|
|
51
|
-
} finally {
|
|
52
|
-
rmSync(dir, { recursive: true, force: true });
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe("setProviderToken", () => {
|
|
58
|
-
it("stores a token and retrieves it", async () => {
|
|
59
|
-
const dir = mkdtempSync(join(tmpdir(), "offrouter-keychain-test-"));
|
|
60
|
-
try {
|
|
61
|
-
const store = new FileSecretStore({ filePath: join(dir, "secrets.json") });
|
|
62
|
-
await setProviderToken("openai", "sk-openai-test", store);
|
|
63
|
-
const token = await getProviderToken("openai", store);
|
|
64
|
-
expect(token).not.toBeUndefined();
|
|
65
|
-
expect(token!.rawValue).toBe("sk-openai-test");
|
|
66
|
-
} finally {
|
|
67
|
-
rmSync(dir, { recursive: true, force: true });
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("overwrites existing token", async () => {
|
|
72
|
-
const dir = mkdtempSync(join(tmpdir(), "offrouter-keychain-test-"));
|
|
73
|
-
try {
|
|
74
|
-
const store = new FileSecretStore({ filePath: join(dir, "secrets.json") });
|
|
75
|
-
await setProviderToken("test", "first-value", store);
|
|
76
|
-
await setProviderToken("test", "second-value", store);
|
|
77
|
-
const token = await getProviderToken("test", store);
|
|
78
|
-
expect(token!.rawValue).toBe("second-value");
|
|
79
|
-
} finally {
|
|
80
|
-
rmSync(dir, { recursive: true, force: true });
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe("multi-account tokens", () => {
|
|
86
|
-
it("stores and retrieves per-account tokens with backward compatibility", async () => {
|
|
87
|
-
const dir = mkdtempSync(join(tmpdir(), "offrouter-keychain-test-"));
|
|
88
|
-
try {
|
|
89
|
-
const store = new FileSecretStore({ filePath: join(dir, "secrets.json") });
|
|
90
|
-
await setProviderToken("anthropic", "tok-legacy", store);
|
|
91
|
-
await setProviderToken("anthropic", "tok-2", store, "anthropic-2");
|
|
92
|
-
|
|
93
|
-
const legacy = await getProviderToken("anthropic", store);
|
|
94
|
-
expect(legacy!.rawValue).toBe("tok-legacy");
|
|
95
|
-
|
|
96
|
-
const acct2 = await getProviderToken("anthropic", store, "anthropic-2");
|
|
97
|
-
expect(acct2!.rawValue).toBe("tok-2");
|
|
98
|
-
|
|
99
|
-
// accountId === providerId uses legacy key
|
|
100
|
-
const explicitDefault = await getProviderToken("anthropic", store, "anthropic");
|
|
101
|
-
expect(explicitDefault!.rawValue).toBe("tok-legacy");
|
|
102
|
-
} finally {
|
|
103
|
-
rmSync(dir, { recursive: true, force: true });
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("setProviderToken with accountId matching providerId overwrites legacy slot", async () => {
|
|
108
|
-
const dir = mkdtempSync(join(tmpdir(), "offrouter-keychain-test-"));
|
|
109
|
-
try {
|
|
110
|
-
const store = new FileSecretStore({ filePath: join(dir, "secrets.json") });
|
|
111
|
-
await setProviderToken("anthropic", "tok-legacy", store);
|
|
112
|
-
await setProviderToken("anthropic", "tok-implicit", store, "anthropic");
|
|
113
|
-
|
|
114
|
-
const result = await getProviderToken("anthropic", store);
|
|
115
|
-
expect(result!.rawValue).toBe("tok-implicit");
|
|
116
|
-
} finally {
|
|
117
|
-
rmSync(dir, { recursive: true, force: true });
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
describe("OAuth token storage", () => {
|
|
123
|
-
function freshStore() {
|
|
124
|
-
const dir = mkdtempSync(join(tmpdir(), "offrouter-oauth-test-"));
|
|
125
|
-
const store = new FileSecretStore({ filePath: join(dir, "secrets.json") });
|
|
126
|
-
return { store, dir };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
it("getOAuthTokens returns undefined when nothing is stored", async () => {
|
|
130
|
-
const { store, dir } = freshStore();
|
|
131
|
-
try {
|
|
132
|
-
const result = await getOAuthTokens("anthropic", store);
|
|
133
|
-
expect(result).toBeUndefined();
|
|
134
|
-
} finally {
|
|
135
|
-
rmSync(dir, { recursive: true, force: true });
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("stores and retrieves structured OAuth tokens", async () => {
|
|
140
|
-
const { store, dir } = freshStore();
|
|
141
|
-
try {
|
|
142
|
-
const tokens: OAuthStoredTokens = {
|
|
143
|
-
accessToken: "access-abc",
|
|
144
|
-
refreshToken: "refresh-xyz",
|
|
145
|
-
expiresAt: 9_999_999_999_000,
|
|
146
|
-
scope: "api offline_access",
|
|
147
|
-
obtainedAt: 1_000,
|
|
148
|
-
};
|
|
149
|
-
await setOAuthTokens("anthropic", tokens, store);
|
|
150
|
-
const result = await getOAuthTokens("anthropic", store);
|
|
151
|
-
expect(result).toEqual(tokens);
|
|
152
|
-
} finally {
|
|
153
|
-
rmSync(dir, { recursive: true, force: true });
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("stores tokens under a separate key from plain API-key tokens", async () => {
|
|
158
|
-
const { store, dir } = freshStore();
|
|
159
|
-
try {
|
|
160
|
-
await setProviderToken("anthropic", "sk-ant-apikey", store);
|
|
161
|
-
await setOAuthTokens(
|
|
162
|
-
"anthropic",
|
|
163
|
-
{ accessToken: "access-oauth", obtainedAt: 1 },
|
|
164
|
-
store,
|
|
165
|
-
);
|
|
166
|
-
const apiToken = await getProviderToken("anthropic", store);
|
|
167
|
-
const oauthTokens = await getOAuthTokens("anthropic", store);
|
|
168
|
-
expect(apiToken!.rawValue).toBe("sk-ant-apikey");
|
|
169
|
-
expect(oauthTokens!.accessToken).toBe("access-oauth");
|
|
170
|
-
} finally {
|
|
171
|
-
rmSync(dir, { recursive: true, force: true });
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it("deleteOAuthTokens removes only the OAuth entry", async () => {
|
|
176
|
-
const { store, dir } = freshStore();
|
|
177
|
-
try {
|
|
178
|
-
await setOAuthTokens(
|
|
179
|
-
"openai",
|
|
180
|
-
{ accessToken: "a", obtainedAt: 1 },
|
|
181
|
-
store,
|
|
182
|
-
);
|
|
183
|
-
await setProviderToken("openai", "sk-openai", store);
|
|
184
|
-
await deleteOAuthTokens("openai", store);
|
|
185
|
-
expect(await getOAuthTokens("openai", store)).toBeUndefined();
|
|
186
|
-
expect((await getProviderToken("openai", store))!.rawValue).toBe(
|
|
187
|
-
"sk-openai",
|
|
188
|
-
);
|
|
189
|
-
} finally {
|
|
190
|
-
rmSync(dir, { recursive: true, force: true });
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("supports per-account OAuth token storage", async () => {
|
|
195
|
-
const { store, dir } = freshStore();
|
|
196
|
-
try {
|
|
197
|
-
await setOAuthTokens(
|
|
198
|
-
"anthropic",
|
|
199
|
-
{ accessToken: "default", obtainedAt: 1 },
|
|
200
|
-
store,
|
|
201
|
-
);
|
|
202
|
-
await setOAuthTokens(
|
|
203
|
-
"anthropic",
|
|
204
|
-
{ accessToken: "acct2", obtainedAt: 1 },
|
|
205
|
-
store,
|
|
206
|
-
"anthropic-2",
|
|
207
|
-
);
|
|
208
|
-
const def = await getOAuthTokens("anthropic", store);
|
|
209
|
-
const acct2 = await getOAuthTokens("anthropic", store, "anthropic-2");
|
|
210
|
-
expect(def!.accessToken).toBe("default");
|
|
211
|
-
expect(acct2!.accessToken).toBe("acct2");
|
|
212
|
-
} finally {
|
|
213
|
-
rmSync(dir, { recursive: true, force: true });
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it("returns undefined for corrupt stored JSON", async () => {
|
|
218
|
-
const { store, dir } = freshStore();
|
|
219
|
-
try {
|
|
220
|
-
await store.set("anthropic:oauth", "{not json");
|
|
221
|
-
const result = await getOAuthTokens("anthropic", store);
|
|
222
|
-
expect(result).toBeUndefined();
|
|
223
|
-
} finally {
|
|
224
|
-
rmSync(dir, { recursive: true, force: true });
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
describe("isOAuthTokenExpired", () => {
|
|
230
|
-
it("treats tokens without expiresAt as not expired", () => {
|
|
231
|
-
expect(
|
|
232
|
-
isOAuthTokenExpired({ accessToken: "a", obtainedAt: 1 }),
|
|
233
|
-
).toBe(false);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it("treats future expiry as valid", () => {
|
|
237
|
-
expect(
|
|
238
|
-
isOAuthTokenExpired(
|
|
239
|
-
{ accessToken: "a", expiresAt: 10_000, obtainedAt: 1 },
|
|
240
|
-
5_000,
|
|
241
|
-
1_000,
|
|
242
|
-
),
|
|
243
|
-
).toBe(false);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it("treats tokens within the skew window as expired", () => {
|
|
247
|
-
// now=5000, skew=1000 => anything expiring <= 6000 is expired
|
|
248
|
-
expect(
|
|
249
|
-
isOAuthTokenExpired(
|
|
250
|
-
{ accessToken: "a", expiresAt: 6_000, obtainedAt: 1 },
|
|
251
|
-
5_000,
|
|
252
|
-
1_000,
|
|
253
|
-
),
|
|
254
|
-
).toBe(true);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it("treats past expiry as expired", () => {
|
|
258
|
-
expect(
|
|
259
|
-
isOAuthTokenExpired(
|
|
260
|
-
{ accessToken: "a", expiresAt: 1_000, obtainedAt: 0 },
|
|
261
|
-
5_000,
|
|
262
|
-
),
|
|
263
|
-
).toBe(true);
|
|
264
|
-
});
|
|
265
|
-
});
|
package/src/auth/keychain.ts
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Keychain-backed token helpers for OffRouter.
|
|
3
|
-
*
|
|
4
|
-
* Provides getProviderToken/setProviderToken that use the SecretStore
|
|
5
|
-
* abstraction (file or OS keychain). Never scrapes other CLI credential stores.
|
|
6
|
-
*
|
|
7
|
-
* Per-account token storage: when accountId is provided and differs from
|
|
8
|
-
* providerId, the storage key is "${providerId}:${accountId}". Otherwise the
|
|
9
|
-
* plain providerId is used for backward compatibility with existing tokens.
|
|
10
|
-
*/
|
|
11
|
-
import type { SecretStore } from "../secrets.js";
|
|
12
|
-
import { Redacted } from "./credential-chain.js";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Compute the storage key for a provider+account combination.
|
|
16
|
-
* Backward compatible: when accountId is absent or equals providerId, returns
|
|
17
|
-
* the plain providerId so existing tokens resolve.
|
|
18
|
-
*/
|
|
19
|
-
export function accountStorageKey(providerId: string, accountId?: string): string {
|
|
20
|
-
if (!accountId || accountId === providerId) return providerId;
|
|
21
|
-
return `${providerId}:${accountId}`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Retrieve a provider token from the secret store.
|
|
26
|
-
* Returns undefined if no token is stored or the store is unavailable.
|
|
27
|
-
*/
|
|
28
|
-
export async function getProviderToken(
|
|
29
|
-
providerId: string,
|
|
30
|
-
store: SecretStore,
|
|
31
|
-
accountId?: string,
|
|
32
|
-
): Promise<Redacted<string> | undefined> {
|
|
33
|
-
try {
|
|
34
|
-
const key = accountStorageKey(providerId, accountId);
|
|
35
|
-
const value = await store.get(key);
|
|
36
|
-
if (typeof value === "string" && value.length > 0) {
|
|
37
|
-
return new Redacted(value);
|
|
38
|
-
}
|
|
39
|
-
} catch {
|
|
40
|
-
// KeychainUnavailableError or other errors: treat as missing.
|
|
41
|
-
}
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Store a provider token in the secret store.
|
|
47
|
-
* Throws if the store is unavailable (e.g., KeychainUnavailableError).
|
|
48
|
-
*/
|
|
49
|
-
export async function setProviderToken(
|
|
50
|
-
providerId: string,
|
|
51
|
-
token: string,
|
|
52
|
-
store: SecretStore,
|
|
53
|
-
accountId?: string,
|
|
54
|
-
): Promise<void> {
|
|
55
|
-
const key = accountStorageKey(providerId, accountId);
|
|
56
|
-
await store.set(key, token);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// OAuth token storage (structured, separate from API-key tokens)
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
/** Suffix that namespaces structured OAuth token blobs in the secret store. */
|
|
64
|
-
const OAUTH_SUFFIX = ":oauth";
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Structured OAuth token material stored under "${providerId}[:account]:oauth".
|
|
68
|
-
* accessToken is the bearer used for provider calls; refreshToken drives
|
|
69
|
-
* silent refresh; expiresAt/obtainedAt are epoch milliseconds. Stored as JSON
|
|
70
|
-
* so the whole token set moves atomically and stays separate from API keys.
|
|
71
|
-
*/
|
|
72
|
-
export interface OAuthStoredTokens {
|
|
73
|
-
accessToken: string;
|
|
74
|
-
refreshToken?: string;
|
|
75
|
-
expiresAt?: number;
|
|
76
|
-
scope?: string;
|
|
77
|
-
obtainedAt: number;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Compute the secret-store key for a provider+account OAuth token set. */
|
|
81
|
-
export function oauthStorageKey(
|
|
82
|
-
providerId: string,
|
|
83
|
-
accountId?: string,
|
|
84
|
-
): string {
|
|
85
|
-
return `${accountStorageKey(providerId, accountId)}${OAUTH_SUFFIX}`;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Store structured OAuth tokens (access, optional refresh, expiry, scope).
|
|
90
|
-
*/
|
|
91
|
-
export async function setOAuthTokens(
|
|
92
|
-
providerId: string,
|
|
93
|
-
tokens: OAuthStoredTokens,
|
|
94
|
-
store: SecretStore,
|
|
95
|
-
accountId?: string,
|
|
96
|
-
): Promise<void> {
|
|
97
|
-
const key = oauthStorageKey(providerId, accountId);
|
|
98
|
-
await store.set(key, JSON.stringify(tokens));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Retrieve structured OAuth tokens. Returns undefined if nothing is stored,
|
|
103
|
-
* the store is unavailable, or the stored payload is corrupt.
|
|
104
|
-
*/
|
|
105
|
-
export async function getOAuthTokens(
|
|
106
|
-
providerId: string,
|
|
107
|
-
store: SecretStore,
|
|
108
|
-
accountId?: string,
|
|
109
|
-
): Promise<OAuthStoredTokens | undefined> {
|
|
110
|
-
try {
|
|
111
|
-
const key = oauthStorageKey(providerId, accountId);
|
|
112
|
-
const raw = await store.get(key);
|
|
113
|
-
if (typeof raw !== "string" || raw.length === 0) {
|
|
114
|
-
return undefined;
|
|
115
|
-
}
|
|
116
|
-
const parsed = JSON.parse(raw) as Partial<OAuthStoredTokens>;
|
|
117
|
-
if (
|
|
118
|
-
typeof parsed.accessToken !== "string" ||
|
|
119
|
-
parsed.accessToken.length === 0
|
|
120
|
-
) {
|
|
121
|
-
return undefined;
|
|
122
|
-
}
|
|
123
|
-
return {
|
|
124
|
-
accessToken: parsed.accessToken,
|
|
125
|
-
refreshToken:
|
|
126
|
-
typeof parsed.refreshToken === "string" && parsed.refreshToken.length > 0
|
|
127
|
-
? parsed.refreshToken
|
|
128
|
-
: undefined,
|
|
129
|
-
expiresAt:
|
|
130
|
-
typeof parsed.expiresAt === "number" ? parsed.expiresAt : undefined,
|
|
131
|
-
scope: typeof parsed.scope === "string" ? parsed.scope : undefined,
|
|
132
|
-
obtainedAt:
|
|
133
|
-
typeof parsed.obtainedAt === "number" ? parsed.obtainedAt : Date.now(),
|
|
134
|
-
};
|
|
135
|
-
} catch {
|
|
136
|
-
// Corrupt payload or unavailable store: treat as missing.
|
|
137
|
-
return undefined;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** Remove stored OAuth tokens for a provider+account. */
|
|
142
|
-
export async function deleteOAuthTokens(
|
|
143
|
-
providerId: string,
|
|
144
|
-
store: SecretStore,
|
|
145
|
-
accountId?: string,
|
|
146
|
-
): Promise<void> {
|
|
147
|
-
try {
|
|
148
|
-
const key = oauthStorageKey(providerId, accountId);
|
|
149
|
-
await store.delete(key);
|
|
150
|
-
} catch {
|
|
151
|
-
// Unavailable store: nothing to delete.
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Returns true when the access token is expired or within the skew window.
|
|
157
|
-
* Tokens without an expiresAt are treated as not expired (no expiry known).
|
|
158
|
-
*/
|
|
159
|
-
export function isOAuthTokenExpired(
|
|
160
|
-
tokens: OAuthStoredTokens,
|
|
161
|
-
now: number = Date.now(),
|
|
162
|
-
skewMs = 60_000,
|
|
163
|
-
): boolean {
|
|
164
|
-
if (typeof tokens.expiresAt !== "number") {
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
return now + skewMs >= tokens.expiresAt;
|
|
168
|
-
}
|