@byok-sdk/keys 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/anthropic-client.d.ts +37 -0
- package/dist/command-runner.d.ts +22 -0
- package/dist/errors.d.ts +78 -0
- package/dist/headers.d.ts +21 -0
- package/dist/http.d.ts +44 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +1462 -0
- package/dist/index.js.map +1 -0
- package/dist/macos-keychain.d.ts +47 -0
- package/dist/openai-client.d.ts +55 -0
- package/dist/profile-store.d.ts +61 -0
- package/dist/provider-profile.d.ts +59 -0
- package/dist/registry.d.ts +103 -0
- package/dist/secret-name.d.ts +30 -0
- package/dist/secret-scope.d.ts +59 -0
- package/dist/secret-store.d.ts +103 -0
- package/dist/sqlite-profile-store.d.ts +35 -0
- package/dist/sqlite-support.d.ts +50 -0
- package/dist/url.d.ts +21 -0
- package/dist/windows-credential-manager.d.ts +31 -0
- package/package.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1462 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
import { mkdirSync, existsSync, chmodSync } from 'fs';
|
|
5
|
+
import { createRequire } from 'module';
|
|
6
|
+
import { dirname } from 'path';
|
|
7
|
+
|
|
8
|
+
// src/errors.ts
|
|
9
|
+
var ByokKeysError = class extends Error {
|
|
10
|
+
code;
|
|
11
|
+
httpStatus;
|
|
12
|
+
constructor(code, message, options) {
|
|
13
|
+
super(message, options);
|
|
14
|
+
this.name = "ByokKeysError";
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.httpStatus = options?.httpStatus;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var BYOK_KEYS_ERROR_CODES = {
|
|
20
|
+
CREDENTIAL_MANAGER_DELETE_FAILED: "CREDENTIAL_MANAGER_DELETE_FAILED",
|
|
21
|
+
CREDENTIAL_MANAGER_READ_FAILED: "CREDENTIAL_MANAGER_READ_FAILED",
|
|
22
|
+
CREDENTIAL_MANAGER_SECRET_INVALID: "CREDENTIAL_MANAGER_SECRET_INVALID",
|
|
23
|
+
CREDENTIAL_MANAGER_UNAVAILABLE: "CREDENTIAL_MANAGER_UNAVAILABLE",
|
|
24
|
+
CREDENTIAL_MANAGER_WRITE_FAILED: "CREDENTIAL_MANAGER_WRITE_FAILED",
|
|
25
|
+
KEYCHAIN_ARGUMENT_INVALID: "KEYCHAIN_ARGUMENT_INVALID",
|
|
26
|
+
KEYCHAIN_DELETE_FAILED: "KEYCHAIN_DELETE_FAILED",
|
|
27
|
+
KEYCHAIN_READ_FAILED: "KEYCHAIN_READ_FAILED",
|
|
28
|
+
KEYCHAIN_SECRET_DECODE_FAILED: "KEYCHAIN_SECRET_DECODE_FAILED",
|
|
29
|
+
KEYCHAIN_SECRET_INVALID: "KEYCHAIN_SECRET_INVALID",
|
|
30
|
+
KEYCHAIN_UNAVAILABLE: "KEYCHAIN_UNAVAILABLE",
|
|
31
|
+
KEYCHAIN_WRITE_FAILED: "KEYCHAIN_WRITE_FAILED",
|
|
32
|
+
LOCAL_ACCOUNT_SCOPE_INVALID: "LOCAL_ACCOUNT_SCOPE_INVALID",
|
|
33
|
+
MODEL_PROVIDER_AUTH_FAILED: "MODEL_PROVIDER_AUTH_FAILED",
|
|
34
|
+
MODEL_PROVIDER_BALANCE_INSUFFICIENT: "MODEL_PROVIDER_BALANCE_INSUFFICIENT",
|
|
35
|
+
MODEL_PROVIDER_HTTP_ERROR: "MODEL_PROVIDER_HTTP_ERROR",
|
|
36
|
+
MODEL_PROVIDER_MODEL_NOT_FOUND: "MODEL_PROVIDER_MODEL_NOT_FOUND",
|
|
37
|
+
MODEL_PROVIDER_RATE_LIMITED: "MODEL_PROVIDER_RATE_LIMITED",
|
|
38
|
+
MODEL_RESPONSE_INVALID: "MODEL_RESPONSE_INVALID",
|
|
39
|
+
PROVIDER_NOT_CONFIGURED: "PROVIDER_NOT_CONFIGURED",
|
|
40
|
+
PROVIDER_PROFILE_INVALID: "PROVIDER_PROFILE_INVALID",
|
|
41
|
+
PROVIDER_REQUEST_TIMEOUT: "PROVIDER_REQUEST_TIMEOUT",
|
|
42
|
+
PROVIDER_RESPONSE_INVALID: "PROVIDER_RESPONSE_INVALID",
|
|
43
|
+
PROVIDER_RESPONSE_TOO_LARGE: "PROVIDER_RESPONSE_TOO_LARGE",
|
|
44
|
+
PROVIDER_SECRET_EMPTY: "PROVIDER_SECRET_EMPTY",
|
|
45
|
+
PROVIDER_SECRET_MISSING: "PROVIDER_SECRET_MISSING",
|
|
46
|
+
PROVIDER_SECRET_NOT_ALLOWED: "PROVIDER_SECRET_NOT_ALLOWED",
|
|
47
|
+
PROVIDER_STORE_UNAVAILABLE: "PROVIDER_STORE_UNAVAILABLE",
|
|
48
|
+
PROVIDER_URL_INVALID: "PROVIDER_URL_INVALID",
|
|
49
|
+
SECRET_ENVELOPE_INVALID: "SECRET_ENVELOPE_INVALID",
|
|
50
|
+
SECRET_NAME_INVALID: "SECRET_NAME_INVALID",
|
|
51
|
+
SECRET_NAMESPACE_INVALID: "SECRET_NAMESPACE_INVALID",
|
|
52
|
+
SECRET_VALUE_INVALID: "SECRET_VALUE_INVALID"
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// src/url.ts
|
|
56
|
+
function normalizeProviderUrl(value) {
|
|
57
|
+
let url;
|
|
58
|
+
try {
|
|
59
|
+
url = new URL(value);
|
|
60
|
+
} catch {
|
|
61
|
+
throw new ByokKeysError(
|
|
62
|
+
"PROVIDER_URL_INVALID",
|
|
63
|
+
"Provider base URL must be absolute"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (url.username || url.password || url.hash || url.search || url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname))) {
|
|
67
|
+
throw new ByokKeysError(
|
|
68
|
+
"PROVIDER_URL_INVALID",
|
|
69
|
+
"Provider URL requires HTTPS; HTTP is allowed only for localhost"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (isPrivateNetworkLiteral(url.hostname) && !isLoopbackHost(url.hostname)) {
|
|
73
|
+
throw new ByokKeysError(
|
|
74
|
+
"PROVIDER_URL_INVALID",
|
|
75
|
+
"Private-network provider IPs are not allowed"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return url.toString().replace(/\/$/u, "");
|
|
79
|
+
}
|
|
80
|
+
function isLoopbackProviderUrl(value) {
|
|
81
|
+
return isLoopbackHost(new URL(value).hostname);
|
|
82
|
+
}
|
|
83
|
+
function isLoopbackHost(hostname) {
|
|
84
|
+
const value = hostname.toLowerCase().replace(/^\[|\]$/gu, "");
|
|
85
|
+
return value === "localhost" || value === "127.0.0.1" || value === "::1";
|
|
86
|
+
}
|
|
87
|
+
function isPrivateNetworkLiteral(hostname) {
|
|
88
|
+
const value = hostname.toLowerCase().replace(/^\[|\]$/gu, "");
|
|
89
|
+
if (value.includes(":")) return true;
|
|
90
|
+
const parts = value.split(".").map(Number);
|
|
91
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return parts[0] === 10 || parts[0] === 127 || parts[0] === 169 && parts[1] === 254 || parts[0] === 172 && (parts[1] ?? 0) >= 16 && (parts[1] ?? 0) <= 31 || parts[0] === 192 && parts[1] === 168;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/provider-profile.ts
|
|
98
|
+
var MODEL_PROVIDER_IDS = [
|
|
99
|
+
"openai",
|
|
100
|
+
"deepseek",
|
|
101
|
+
"anthropic",
|
|
102
|
+
"custom"
|
|
103
|
+
];
|
|
104
|
+
var PROVIDER_AUTH_MODES = ["bearer", "x_api_key", "none"];
|
|
105
|
+
var MODEL_PROVIDER_ADAPTERS = ["openai_compatible", "anthropic"];
|
|
106
|
+
function boundedString(field, maximumLength) {
|
|
107
|
+
return z.string().superRefine((value, ctx) => {
|
|
108
|
+
if (value.trim().length === 0 || value.length > maximumLength || /[\u0000\r\n]/u.test(value)) {
|
|
109
|
+
ctx.addIssue({
|
|
110
|
+
code: "custom",
|
|
111
|
+
message: `Provider ${field} is invalid`
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}).transform((value) => value.trim());
|
|
115
|
+
}
|
|
116
|
+
function isoTimestamp(field) {
|
|
117
|
+
return boundedString(field, 64).superRefine((value, ctx) => {
|
|
118
|
+
if (!Number.isFinite(Date.parse(value))) {
|
|
119
|
+
ctx.addIssue({
|
|
120
|
+
code: "custom",
|
|
121
|
+
message: `Provider ${field} must be an ISO timestamp`
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
var providerBaseUrl = z.string().superRefine((value, ctx) => {
|
|
127
|
+
try {
|
|
128
|
+
normalizeProviderUrl(value);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
ctx.addIssue({
|
|
131
|
+
code: "custom",
|
|
132
|
+
message: error instanceof Error ? error.message : "Provider base URL is invalid",
|
|
133
|
+
params: {
|
|
134
|
+
byokCode: error instanceof ByokKeysError ? error.code : "PROVIDER_URL_INVALID"
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}).transform((value) => normalizeProviderUrl(value));
|
|
139
|
+
var ModelProviderProfileSchema = z.object({
|
|
140
|
+
adapter: z.enum(MODEL_PROVIDER_ADAPTERS),
|
|
141
|
+
auth_mode: z.enum(PROVIDER_AUTH_MODES),
|
|
142
|
+
base_url: providerBaseUrl,
|
|
143
|
+
created_at: isoTimestamp("created_at"),
|
|
144
|
+
display_name: boundedString("display_name", 100),
|
|
145
|
+
enabled: z.boolean(),
|
|
146
|
+
kind: z.literal("model"),
|
|
147
|
+
model: boundedString("model", 160),
|
|
148
|
+
provider_id: z.enum(MODEL_PROVIDER_IDS),
|
|
149
|
+
updated_at: isoTimestamp("updated_at")
|
|
150
|
+
}).superRefine((profile, ctx) => {
|
|
151
|
+
if (profile.adapter === "anthropic" && profile.auth_mode !== "x_api_key") {
|
|
152
|
+
ctx.addIssue({
|
|
153
|
+
code: "custom",
|
|
154
|
+
message: "Anthropic requires x_api_key authentication",
|
|
155
|
+
path: ["auth_mode"]
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (profile.adapter === "openai_compatible" && profile.auth_mode === "x_api_key") {
|
|
159
|
+
ctx.addIssue({
|
|
160
|
+
code: "custom",
|
|
161
|
+
message: "OpenAI-compatible providers support bearer or no authentication",
|
|
162
|
+
path: ["auth_mode"]
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
if (Date.parse(profile.updated_at) < Date.parse(profile.created_at)) {
|
|
166
|
+
ctx.addIssue({
|
|
167
|
+
code: "custom",
|
|
168
|
+
message: "Provider updated_at cannot precede created_at",
|
|
169
|
+
path: ["updated_at"]
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
function parseModelProviderProfile(value) {
|
|
174
|
+
const result = ModelProviderProfileSchema.safeParse(value);
|
|
175
|
+
if (result.success) return result.data;
|
|
176
|
+
const issue = result.error.issues[0];
|
|
177
|
+
const params = issue?.params;
|
|
178
|
+
throw new ByokKeysError(
|
|
179
|
+
params?.byokCode ?? "PROVIDER_PROFILE_INVALID",
|
|
180
|
+
issue?.message ?? "Provider profile is invalid",
|
|
181
|
+
{ cause: result.error }
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/headers.ts
|
|
186
|
+
function requiredProviderSecret(profile, secret) {
|
|
187
|
+
if ((profile.auth_mode === "bearer" || profile.auth_mode === "x_api_key") && !secret) {
|
|
188
|
+
throw new ByokKeysError(
|
|
189
|
+
"PROVIDER_SECRET_MISSING",
|
|
190
|
+
`${profile.kind} provider requires a secret in the operating-system credential store`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return secret;
|
|
194
|
+
}
|
|
195
|
+
function providerHeaders(profile, secret) {
|
|
196
|
+
return {
|
|
197
|
+
accept: "application/json",
|
|
198
|
+
"content-type": "application/json",
|
|
199
|
+
...profile.auth_mode === "bearer" ? { authorization: `Bearer ${requiredProviderSecret(profile, secret)}` } : {},
|
|
200
|
+
...profile.auth_mode === "x_api_key" ? {
|
|
201
|
+
"anthropic-version": "2023-06-01",
|
|
202
|
+
"x-api-key": requiredProviderSecret(profile, secret)
|
|
203
|
+
} : {}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/http.ts
|
|
208
|
+
var PROVIDER_RESPONSE_MAX_BYTES = 2 * 1024 * 1024;
|
|
209
|
+
var PROVIDER_TIMEOUT_MS = 15e3;
|
|
210
|
+
async function fetchWithProviderGuards(fetchImpl, url, init, signal) {
|
|
211
|
+
normalizeProviderUrl(url);
|
|
212
|
+
const controller = new AbortController();
|
|
213
|
+
const onAbort = () => controller.abort(signal.reason);
|
|
214
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
215
|
+
if (signal.aborted) onAbort();
|
|
216
|
+
const timeout = setTimeout(
|
|
217
|
+
() => controller.abort("provider_timeout"),
|
|
218
|
+
PROVIDER_TIMEOUT_MS
|
|
219
|
+
);
|
|
220
|
+
try {
|
|
221
|
+
return await fetchImpl(url, { ...init, signal: controller.signal });
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (!signal.aborted && controller.signal.aborted && controller.signal.reason === "provider_timeout") {
|
|
224
|
+
throw new ByokKeysError(
|
|
225
|
+
"PROVIDER_REQUEST_TIMEOUT",
|
|
226
|
+
"Provider request timed out"
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
throw error;
|
|
230
|
+
} finally {
|
|
231
|
+
clearTimeout(timeout);
|
|
232
|
+
signal.removeEventListener("abort", onAbort);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function parseBoundedJsonResponse(response) {
|
|
236
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
237
|
+
if (Number.isFinite(contentLength) && contentLength > PROVIDER_RESPONSE_MAX_BYTES) {
|
|
238
|
+
throw new ByokKeysError(
|
|
239
|
+
"PROVIDER_RESPONSE_TOO_LARGE",
|
|
240
|
+
"Provider response exceeds the local safety limit"
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
const text = await response.text();
|
|
244
|
+
if (new TextEncoder().encode(text).byteLength > PROVIDER_RESPONSE_MAX_BYTES) {
|
|
245
|
+
throw new ByokKeysError(
|
|
246
|
+
"PROVIDER_RESPONSE_TOO_LARGE",
|
|
247
|
+
"Provider response exceeds the local safety limit"
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
return JSON.parse(text);
|
|
252
|
+
} catch {
|
|
253
|
+
throw new ByokKeysError(
|
|
254
|
+
"PROVIDER_RESPONSE_INVALID",
|
|
255
|
+
"Provider returned invalid JSON"
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function readModelProviderResponse(response) {
|
|
260
|
+
let payload;
|
|
261
|
+
try {
|
|
262
|
+
payload = await parseBoundedJsonResponse(response);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (!response.ok && error instanceof ByokKeysError && error.code === "PROVIDER_RESPONSE_INVALID") {
|
|
265
|
+
throw modelProviderHttpError(response.status, void 0);
|
|
266
|
+
}
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
if (!response.ok) {
|
|
270
|
+
throw modelProviderHttpError(response.status, payload);
|
|
271
|
+
}
|
|
272
|
+
return payload;
|
|
273
|
+
}
|
|
274
|
+
function modelProviderHttpError(status, payload) {
|
|
275
|
+
return new ByokKeysError(
|
|
276
|
+
classifyModelProviderHttpError(status, payload),
|
|
277
|
+
`Model provider request failed with HTTP ${status}`,
|
|
278
|
+
{ httpStatus: status }
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
function classifyModelProviderHttpError(status, payload) {
|
|
282
|
+
const detail = safeProviderErrorText(payload);
|
|
283
|
+
if (status === 402 || /(?:insufficient[_ -]?(?:quota|balance|credit)|quota[_ -]?exceeded|billing|payment required|余额不足|额度不足|欠费|充值)/iu.test(
|
|
284
|
+
detail
|
|
285
|
+
)) {
|
|
286
|
+
return "MODEL_PROVIDER_BALANCE_INSUFFICIENT";
|
|
287
|
+
}
|
|
288
|
+
if (status === 401 || status === 403 || /(?:invalid[_ -]?api[_ -]?key|authentication|unauthori[sz]ed|forbidden|鉴权失败|密钥无效|令牌无效)/iu.test(
|
|
289
|
+
detail
|
|
290
|
+
)) {
|
|
291
|
+
return "MODEL_PROVIDER_AUTH_FAILED";
|
|
292
|
+
}
|
|
293
|
+
if (status === 404 || /(?:model[_ -]?not[_ -]?found|model does not exist|unknown model|模型不存在|无权访问模型)/iu.test(
|
|
294
|
+
detail
|
|
295
|
+
)) {
|
|
296
|
+
return "MODEL_PROVIDER_MODEL_NOT_FOUND";
|
|
297
|
+
}
|
|
298
|
+
if (status === 429) return "MODEL_PROVIDER_RATE_LIMITED";
|
|
299
|
+
return "MODEL_PROVIDER_HTTP_ERROR";
|
|
300
|
+
}
|
|
301
|
+
function safeProviderErrorText(payload) {
|
|
302
|
+
try {
|
|
303
|
+
return JSON.stringify(payload ?? "").slice(0, 8e3).toLowerCase();
|
|
304
|
+
} catch {
|
|
305
|
+
return "";
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function modelApiUrl(baseUrl, suffix) {
|
|
309
|
+
const normalized = normalizeProviderUrl(baseUrl);
|
|
310
|
+
if (normalized.endsWith(`/${suffix}`)) return normalized;
|
|
311
|
+
return `${normalized}/${suffix}`;
|
|
312
|
+
}
|
|
313
|
+
function modelMessageText(value) {
|
|
314
|
+
if (typeof value === "string") return value;
|
|
315
|
+
if (Array.isArray(value)) {
|
|
316
|
+
return value.map(objectValue).filter(
|
|
317
|
+
(item) => item !== void 0 && typeof item.text === "string"
|
|
318
|
+
).map((item) => item.text).join("");
|
|
319
|
+
}
|
|
320
|
+
throw new ByokKeysError(
|
|
321
|
+
"MODEL_RESPONSE_INVALID",
|
|
322
|
+
"Model response did not contain text"
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
function objectValue(value) {
|
|
326
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
327
|
+
}
|
|
328
|
+
function assertLiveModelResponse(value) {
|
|
329
|
+
if (value.trim().length === 0) {
|
|
330
|
+
throw new ByokKeysError(
|
|
331
|
+
"MODEL_RESPONSE_INVALID",
|
|
332
|
+
"Model provider returned an empty completion during connection validation"
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/openai-client.ts
|
|
338
|
+
var OpenAiCompatibleChatClient = class {
|
|
339
|
+
model;
|
|
340
|
+
/** False only when the provider is loopback, i.e. no data leaves the machine. */
|
|
341
|
+
remoteDataTransfer;
|
|
342
|
+
#fetch;
|
|
343
|
+
#profile;
|
|
344
|
+
#secret;
|
|
345
|
+
constructor(options) {
|
|
346
|
+
this.#fetch = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
347
|
+
this.#profile = parseModelProviderProfile(options.profile);
|
|
348
|
+
this.#secret = requiredProviderSecret(this.#profile, options.secret);
|
|
349
|
+
this.model = this.#profile.model;
|
|
350
|
+
this.remoteDataTransfer = !isLoopbackProviderUrl(this.#profile.base_url);
|
|
351
|
+
}
|
|
352
|
+
/** POST `<base_url>/chat/completions`; returns the parsed response object. */
|
|
353
|
+
async createChatCompletion(request, signal = new AbortController().signal) {
|
|
354
|
+
const response = await fetchWithProviderGuards(
|
|
355
|
+
this.#fetch,
|
|
356
|
+
modelApiUrl(this.#profile.base_url, "chat/completions"),
|
|
357
|
+
{
|
|
358
|
+
body: JSON.stringify({ ...request, model: this.#profile.model }),
|
|
359
|
+
headers: providerHeaders(this.#profile, this.#secret),
|
|
360
|
+
method: "POST",
|
|
361
|
+
redirect: "error"
|
|
362
|
+
},
|
|
363
|
+
signal
|
|
364
|
+
);
|
|
365
|
+
const payload = objectValue(await readModelProviderResponse(response));
|
|
366
|
+
if (payload === void 0) {
|
|
367
|
+
throw new ByokKeysError(
|
|
368
|
+
"MODEL_RESPONSE_INVALID",
|
|
369
|
+
"Model provider returned an invalid response"
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return payload;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Round-trip the configured key against the provider
|
|
376
|
+
* (`providers.ts:797-828`). An empty completion fails, so a provider that
|
|
377
|
+
* accepts anything cannot be mistaken for a working configuration.
|
|
378
|
+
*/
|
|
379
|
+
async testConnection(signal = new AbortController().signal) {
|
|
380
|
+
const payload = await this.createChatCompletion(
|
|
381
|
+
{
|
|
382
|
+
max_tokens: 32,
|
|
383
|
+
messages: [
|
|
384
|
+
{
|
|
385
|
+
content: 'Return one JSON object with exactly this shape: {"ok":true}.',
|
|
386
|
+
role: "user"
|
|
387
|
+
}
|
|
388
|
+
],
|
|
389
|
+
response_format: { type: "json_object" },
|
|
390
|
+
temperature: 0
|
|
391
|
+
},
|
|
392
|
+
signal
|
|
393
|
+
);
|
|
394
|
+
assertLiveModelResponse(chatCompletionText(payload));
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
function chatCompletionText(payload) {
|
|
398
|
+
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
399
|
+
const message = objectValue(objectValue(choices[0])?.message);
|
|
400
|
+
return modelMessageText(message?.content);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/anthropic-client.ts
|
|
404
|
+
var AnthropicMessagesClient = class {
|
|
405
|
+
model;
|
|
406
|
+
/** False only when the provider is loopback, i.e. no data leaves the machine. */
|
|
407
|
+
remoteDataTransfer;
|
|
408
|
+
#fetch;
|
|
409
|
+
#profile;
|
|
410
|
+
#secret;
|
|
411
|
+
constructor(options) {
|
|
412
|
+
this.#fetch = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
413
|
+
this.#profile = parseModelProviderProfile(options.profile);
|
|
414
|
+
if (this.#profile.adapter !== "anthropic") {
|
|
415
|
+
throw new ByokKeysError(
|
|
416
|
+
"PROVIDER_PROFILE_INVALID",
|
|
417
|
+
"Anthropic provider requires the anthropic adapter"
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
this.#secret = requiredProviderSecret(
|
|
421
|
+
this.#profile,
|
|
422
|
+
options.secret
|
|
423
|
+
);
|
|
424
|
+
this.model = this.#profile.model;
|
|
425
|
+
this.remoteDataTransfer = !isLoopbackProviderUrl(this.#profile.base_url);
|
|
426
|
+
}
|
|
427
|
+
/** POST `<base_url>/messages`; returns the parsed response object. */
|
|
428
|
+
async createMessage(request, signal = new AbortController().signal) {
|
|
429
|
+
const response = await fetchWithProviderGuards(
|
|
430
|
+
this.#fetch,
|
|
431
|
+
modelApiUrl(this.#profile.base_url, "messages"),
|
|
432
|
+
{
|
|
433
|
+
body: JSON.stringify({ ...request, model: this.#profile.model }),
|
|
434
|
+
headers: providerHeaders(this.#profile, this.#secret),
|
|
435
|
+
method: "POST",
|
|
436
|
+
redirect: "error"
|
|
437
|
+
},
|
|
438
|
+
signal
|
|
439
|
+
);
|
|
440
|
+
const payload = objectValue(await readModelProviderResponse(response));
|
|
441
|
+
if (payload === void 0) {
|
|
442
|
+
throw new ByokKeysError(
|
|
443
|
+
"MODEL_RESPONSE_INVALID",
|
|
444
|
+
"Anthropic provider returned an invalid response"
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
return payload;
|
|
448
|
+
}
|
|
449
|
+
/** Round-trip the configured key against the provider (`providers.ts:1037-1054`). */
|
|
450
|
+
async testConnection(signal = new AbortController().signal) {
|
|
451
|
+
const payload = await this.createMessage(
|
|
452
|
+
{
|
|
453
|
+
max_tokens: 32,
|
|
454
|
+
messages: [
|
|
455
|
+
{
|
|
456
|
+
content: 'Return one JSON object with exactly this shape: {"ok":true}.',
|
|
457
|
+
role: "user"
|
|
458
|
+
}
|
|
459
|
+
],
|
|
460
|
+
temperature: 0
|
|
461
|
+
},
|
|
462
|
+
signal
|
|
463
|
+
);
|
|
464
|
+
assertLiveModelResponse(anthropicMessageText(payload));
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
function anthropicMessageText(payload) {
|
|
468
|
+
return modelMessageText(payload.content);
|
|
469
|
+
}
|
|
470
|
+
async function runCommand(executable, args, stdin) {
|
|
471
|
+
return new Promise((resolveResult) => {
|
|
472
|
+
const child = spawn(executable, args, {
|
|
473
|
+
env: process.env,
|
|
474
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
475
|
+
});
|
|
476
|
+
let stdout = "";
|
|
477
|
+
let stderr = "";
|
|
478
|
+
child.stdout.setEncoding("utf8");
|
|
479
|
+
child.stderr.setEncoding("utf8");
|
|
480
|
+
child.stdout.on("data", (chunk) => {
|
|
481
|
+
stdout += chunk;
|
|
482
|
+
});
|
|
483
|
+
child.stderr.on("data", (chunk) => {
|
|
484
|
+
stderr += chunk;
|
|
485
|
+
});
|
|
486
|
+
child.on("error", () => {
|
|
487
|
+
resolveResult({
|
|
488
|
+
exitCode: 127,
|
|
489
|
+
stderr: "command unavailable",
|
|
490
|
+
stdout: ""
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
child.on("close", (code) => {
|
|
494
|
+
resolveResult({ exitCode: code ?? 1, stderr, stdout });
|
|
495
|
+
});
|
|
496
|
+
if (stdin === void 0) child.stdin.end();
|
|
497
|
+
else child.stdin.end(stdin);
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/secret-name.ts
|
|
502
|
+
var SECRET_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{2,95}$/u;
|
|
503
|
+
var SECRET_NAMESPACE_PATTERN = /^[a-z0-9][a-z0-9_-]{7,95}$/u;
|
|
504
|
+
function assertSecretName(name) {
|
|
505
|
+
if (!SECRET_NAME_PATTERN.test(name)) {
|
|
506
|
+
throw new ByokKeysError(
|
|
507
|
+
"SECRET_NAME_INVALID",
|
|
508
|
+
"Secret name must be 3 to 96 characters of [a-z0-9_-] starting with a letter or digit"
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
return name;
|
|
512
|
+
}
|
|
513
|
+
function assertSecretNamespace(value) {
|
|
514
|
+
const normalized = value.trim();
|
|
515
|
+
if (!SECRET_NAMESPACE_PATTERN.test(normalized)) {
|
|
516
|
+
throw new ByokKeysError(
|
|
517
|
+
"SECRET_NAMESPACE_INVALID",
|
|
518
|
+
"Secret namespace must be 8 to 96 characters of [a-z0-9_-] starting with a letter or digit"
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
return normalized;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/secret-store.ts
|
|
525
|
+
var DEFAULT_SECRET_SERVICE_PREFIX = "com.byok.keys";
|
|
526
|
+
var MODEL_PROVIDER_SECRET_NAMES = {
|
|
527
|
+
anthropic: "model-anthropic-api-key",
|
|
528
|
+
custom: "model-custom-api-key",
|
|
529
|
+
deepseek: "model-deepseek-api-key",
|
|
530
|
+
openai: "model-openai-api-key"
|
|
531
|
+
};
|
|
532
|
+
function modelProviderSecretName(providerId) {
|
|
533
|
+
return MODEL_PROVIDER_SECRET_NAMES[providerId];
|
|
534
|
+
}
|
|
535
|
+
function assertSharedSecretValue(secret) {
|
|
536
|
+
if (secret.length === 0 || /[\u0000\r\n]/u.test(secret)) {
|
|
537
|
+
throw new ByokKeysError(
|
|
538
|
+
"SECRET_VALUE_INVALID",
|
|
539
|
+
"Secret must be a non-empty string without null or newline characters"
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function decodeStrictBase64Utf8(encoded) {
|
|
544
|
+
if (encoded.length % 4 !== 0) return void 0;
|
|
545
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/u.test(encoded)) return void 0;
|
|
546
|
+
const bytes = Buffer.from(encoded, "base64");
|
|
547
|
+
if (bytes.toString("base64") !== encoded) return void 0;
|
|
548
|
+
const text = bytes.toString("utf8");
|
|
549
|
+
if (!Buffer.from(text, "utf8").equals(bytes)) return void 0;
|
|
550
|
+
return text;
|
|
551
|
+
}
|
|
552
|
+
var InMemorySecretStore = class _InMemorySecretStore {
|
|
553
|
+
providerLabel = "in-memory";
|
|
554
|
+
#entries;
|
|
555
|
+
#servicePrefix;
|
|
556
|
+
constructor(options = {}) {
|
|
557
|
+
this.#entries = options.entries ?? /* @__PURE__ */ new Map();
|
|
558
|
+
this.#servicePrefix = options.servicePrefix ?? DEFAULT_SECRET_SERVICE_PREFIX;
|
|
559
|
+
}
|
|
560
|
+
available() {
|
|
561
|
+
return Promise.resolve(true);
|
|
562
|
+
}
|
|
563
|
+
async delete(name) {
|
|
564
|
+
return this.#entries.delete(this.#service(name));
|
|
565
|
+
}
|
|
566
|
+
async get(name) {
|
|
567
|
+
return this.#entries.get(this.#service(name));
|
|
568
|
+
}
|
|
569
|
+
async has(name) {
|
|
570
|
+
return await this.get(name) !== void 0;
|
|
571
|
+
}
|
|
572
|
+
scope(namespace) {
|
|
573
|
+
return new _InMemorySecretStore({
|
|
574
|
+
entries: this.#entries,
|
|
575
|
+
servicePrefix: `${this.#servicePrefix}.scope.${assertSecretNamespace(namespace)}`
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
async set(name, secret) {
|
|
579
|
+
const service = this.#service(name);
|
|
580
|
+
assertSharedSecretValue(secret);
|
|
581
|
+
this.#entries.set(service, secret);
|
|
582
|
+
}
|
|
583
|
+
#service(name) {
|
|
584
|
+
return `${this.#servicePrefix}.${assertSecretName(name)}`;
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
// src/macos-keychain.ts
|
|
589
|
+
var DEFAULT_KEYCHAIN_SECRET_STORAGE_PREFIX = "byok-b64-v1:";
|
|
590
|
+
var SECURITY_ITEM_NOT_FOUND = 44;
|
|
591
|
+
var MacOsKeychainSecretStore = class _MacOsKeychainSecretStore {
|
|
592
|
+
providerLabel = "macOS Keychain";
|
|
593
|
+
#account;
|
|
594
|
+
#allowUnprefixedRead;
|
|
595
|
+
#commandRunner;
|
|
596
|
+
#platform;
|
|
597
|
+
#servicePrefix;
|
|
598
|
+
#storagePrefix;
|
|
599
|
+
constructor(options = {}) {
|
|
600
|
+
this.#account = options.account ?? "local-device";
|
|
601
|
+
this.#allowUnprefixedRead = options.allowUnprefixedRead ?? false;
|
|
602
|
+
this.#commandRunner = options.commandRunner ?? runCommand;
|
|
603
|
+
this.#platform = options.platform ?? process.platform;
|
|
604
|
+
this.#servicePrefix = options.servicePrefix ?? DEFAULT_SECRET_SERVICE_PREFIX;
|
|
605
|
+
this.#storagePrefix = options.storagePrefix ?? DEFAULT_KEYCHAIN_SECRET_STORAGE_PREFIX;
|
|
606
|
+
}
|
|
607
|
+
async available() {
|
|
608
|
+
if (this.#platform !== "darwin") return false;
|
|
609
|
+
const result = await this.#commandRunner("/usr/bin/security", [
|
|
610
|
+
"default-keychain",
|
|
611
|
+
"-d",
|
|
612
|
+
"user"
|
|
613
|
+
]);
|
|
614
|
+
return result.exitCode === 0;
|
|
615
|
+
}
|
|
616
|
+
async delete(name) {
|
|
617
|
+
const service = this.#service(name);
|
|
618
|
+
this.#assertMacOs();
|
|
619
|
+
const result = await this.#commandRunner("/usr/bin/security", [
|
|
620
|
+
"delete-generic-password",
|
|
621
|
+
"-a",
|
|
622
|
+
this.#account,
|
|
623
|
+
"-s",
|
|
624
|
+
service
|
|
625
|
+
]);
|
|
626
|
+
if (result.exitCode === SECURITY_ITEM_NOT_FOUND) return false;
|
|
627
|
+
if (result.exitCode !== 0) {
|
|
628
|
+
throw new ByokKeysError(
|
|
629
|
+
"KEYCHAIN_DELETE_FAILED",
|
|
630
|
+
"macOS Keychain could not delete the requested secret"
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
async get(name) {
|
|
636
|
+
const service = this.#service(name);
|
|
637
|
+
this.#assertMacOs();
|
|
638
|
+
const result = await this.#commandRunner("/usr/bin/security", [
|
|
639
|
+
"find-generic-password",
|
|
640
|
+
"-a",
|
|
641
|
+
this.#account,
|
|
642
|
+
"-s",
|
|
643
|
+
service,
|
|
644
|
+
"-w"
|
|
645
|
+
]);
|
|
646
|
+
if (result.exitCode === SECURITY_ITEM_NOT_FOUND) return void 0;
|
|
647
|
+
if (result.exitCode !== 0) {
|
|
648
|
+
throw new ByokKeysError(
|
|
649
|
+
"KEYCHAIN_READ_FAILED",
|
|
650
|
+
"macOS Keychain could not read the requested secret"
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
return this.#decode(result.stdout.replace(/\r?\n$/u, ""));
|
|
654
|
+
}
|
|
655
|
+
async has(name) {
|
|
656
|
+
return await this.get(name) !== void 0;
|
|
657
|
+
}
|
|
658
|
+
scope(namespace) {
|
|
659
|
+
return new _MacOsKeychainSecretStore({
|
|
660
|
+
account: this.#account,
|
|
661
|
+
allowUnprefixedRead: this.#allowUnprefixedRead,
|
|
662
|
+
commandRunner: this.#commandRunner,
|
|
663
|
+
platform: this.#platform,
|
|
664
|
+
servicePrefix: `${this.#servicePrefix}.scope.${assertSecretNamespace(namespace)}`,
|
|
665
|
+
storagePrefix: this.#storagePrefix
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
async set(name, secret) {
|
|
669
|
+
const service = this.#service(name);
|
|
670
|
+
this.#assertMacOs();
|
|
671
|
+
if (secret.length === 0 || secret.length > 16384 || /[\u0000\r\n]/u.test(secret)) {
|
|
672
|
+
throw new ByokKeysError(
|
|
673
|
+
"KEYCHAIN_SECRET_INVALID",
|
|
674
|
+
"Secret must contain 1 to 16384 non-newline characters"
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
const storedSecret = `${this.#storagePrefix}${Buffer.from(secret, "utf8").toString("base64")}`;
|
|
678
|
+
const command = [
|
|
679
|
+
"add-generic-password",
|
|
680
|
+
"-U",
|
|
681
|
+
"-a",
|
|
682
|
+
quoteSecurityInteractiveArgument(this.#account),
|
|
683
|
+
"-s",
|
|
684
|
+
quoteSecurityInteractiveArgument(service),
|
|
685
|
+
"-w",
|
|
686
|
+
quoteSecurityInteractiveArgument(storedSecret)
|
|
687
|
+
].join(" ");
|
|
688
|
+
const result = await this.#commandRunner(
|
|
689
|
+
"/usr/bin/security",
|
|
690
|
+
["-i"],
|
|
691
|
+
`${command}
|
|
692
|
+
`
|
|
693
|
+
);
|
|
694
|
+
if (result.exitCode !== 0) {
|
|
695
|
+
throw new ByokKeysError(
|
|
696
|
+
"KEYCHAIN_WRITE_FAILED",
|
|
697
|
+
"macOS Keychain could not store the requested secret"
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
#assertMacOs() {
|
|
702
|
+
if (this.#platform !== "darwin") {
|
|
703
|
+
throw new ByokKeysError(
|
|
704
|
+
"KEYCHAIN_UNAVAILABLE",
|
|
705
|
+
"macOS Keychain is required; plaintext fallback is disabled"
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Fail-closed inverse of the storage encoding. The error message never
|
|
711
|
+
* echoes the undecodable value — it may still be somebody's live credential.
|
|
712
|
+
*/
|
|
713
|
+
#decode(storedSecret) {
|
|
714
|
+
if (!storedSecret.startsWith(this.#storagePrefix)) {
|
|
715
|
+
if (this.#allowUnprefixedRead) return storedSecret;
|
|
716
|
+
throw new ByokKeysError(
|
|
717
|
+
"KEYCHAIN_SECRET_DECODE_FAILED",
|
|
718
|
+
"macOS Keychain returned a value without this store's storage prefix"
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
const decoded = decodeStrictBase64Utf8(
|
|
722
|
+
storedSecret.slice(this.#storagePrefix.length)
|
|
723
|
+
);
|
|
724
|
+
if (decoded === void 0 || decoded.length === 0) {
|
|
725
|
+
throw new ByokKeysError(
|
|
726
|
+
"KEYCHAIN_SECRET_DECODE_FAILED",
|
|
727
|
+
"macOS Keychain returned a secret that is not valid base64-encoded UTF-8"
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
return decoded;
|
|
731
|
+
}
|
|
732
|
+
#service(name) {
|
|
733
|
+
return `${this.#servicePrefix}.${assertSecretName(name)}`;
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
function quoteSecurityInteractiveArgument(value) {
|
|
737
|
+
if (/[\u0000\r\n]/u.test(value)) {
|
|
738
|
+
throw new ByokKeysError(
|
|
739
|
+
"KEYCHAIN_ARGUMENT_INVALID",
|
|
740
|
+
"macOS Keychain arguments cannot contain null or newline characters"
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/windows-credential-manager.ts
|
|
747
|
+
var CREDENTIAL_NOT_FOUND = 44;
|
|
748
|
+
var WINDOWS_CREDENTIAL_MANAGER_SCRIPT_BASE64 = Buffer.from(
|
|
749
|
+
String.raw`
|
|
750
|
+
Add-Type -TypeDefinition @"
|
|
751
|
+
using System;
|
|
752
|
+
using System.ComponentModel;
|
|
753
|
+
using System.Runtime.InteropServices;
|
|
754
|
+
using System.Runtime.InteropServices.ComTypes;
|
|
755
|
+
|
|
756
|
+
namespace Byok {
|
|
757
|
+
public static class CredentialManager {
|
|
758
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
759
|
+
private struct Credential {
|
|
760
|
+
public UInt32 Flags;
|
|
761
|
+
public UInt32 Type;
|
|
762
|
+
[MarshalAs(UnmanagedType.LPWStr)] public string TargetName;
|
|
763
|
+
[MarshalAs(UnmanagedType.LPWStr)] public string Comment;
|
|
764
|
+
public FILETIME LastWritten;
|
|
765
|
+
public UInt32 CredentialBlobSize;
|
|
766
|
+
public IntPtr CredentialBlob;
|
|
767
|
+
public UInt32 Persist;
|
|
768
|
+
public UInt32 AttributeCount;
|
|
769
|
+
public IntPtr Attributes;
|
|
770
|
+
[MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias;
|
|
771
|
+
[MarshalAs(UnmanagedType.LPWStr)] public string UserName;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
[DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
775
|
+
private static extern bool CredWrite([In] ref Credential credential, UInt32 flags);
|
|
776
|
+
|
|
777
|
+
[DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
778
|
+
private static extern bool CredRead(string target, UInt32 type, UInt32 flags, out IntPtr credential);
|
|
779
|
+
|
|
780
|
+
[DllImport("Advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
781
|
+
private static extern bool CredDelete(string target, UInt32 type, UInt32 flags);
|
|
782
|
+
|
|
783
|
+
[DllImport("Advapi32.dll", SetLastError = false)]
|
|
784
|
+
private static extern void CredFree(IntPtr buffer);
|
|
785
|
+
|
|
786
|
+
public static void Write(string target, string username, byte[] secret) {
|
|
787
|
+
IntPtr blob = IntPtr.Zero;
|
|
788
|
+
try {
|
|
789
|
+
blob = Marshal.AllocHGlobal(secret.Length);
|
|
790
|
+
Marshal.Copy(secret, 0, blob, secret.Length);
|
|
791
|
+
Credential credential = new Credential {
|
|
792
|
+
Type = 1,
|
|
793
|
+
TargetName = target,
|
|
794
|
+
CredentialBlobSize = (UInt32)secret.Length,
|
|
795
|
+
CredentialBlob = blob,
|
|
796
|
+
Persist = 2,
|
|
797
|
+
UserName = username
|
|
798
|
+
};
|
|
799
|
+
if (!CredWrite(ref credential, 0)) {
|
|
800
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
801
|
+
}
|
|
802
|
+
} finally {
|
|
803
|
+
if (blob != IntPtr.Zero) {
|
|
804
|
+
Marshal.Copy(new byte[secret.Length], 0, blob, secret.Length);
|
|
805
|
+
Marshal.FreeHGlobal(blob);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
public static byte[] Read(string target) {
|
|
811
|
+
IntPtr pointer;
|
|
812
|
+
if (!CredRead(target, 1, 0, out pointer)) {
|
|
813
|
+
int error = Marshal.GetLastWin32Error();
|
|
814
|
+
if (error == 1168) return null;
|
|
815
|
+
throw new Win32Exception(error);
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
Credential credential = (Credential)Marshal.PtrToStructure(pointer, typeof(Credential));
|
|
819
|
+
byte[] secret = new byte[credential.CredentialBlobSize];
|
|
820
|
+
if (secret.Length > 0) Marshal.Copy(credential.CredentialBlob, secret, 0, secret.Length);
|
|
821
|
+
return secret;
|
|
822
|
+
} finally {
|
|
823
|
+
CredFree(pointer);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
public static bool Delete(string target) {
|
|
828
|
+
if (CredDelete(target, 1, 0)) return true;
|
|
829
|
+
int error = Marshal.GetLastWin32Error();
|
|
830
|
+
if (error == 1168) return false;
|
|
831
|
+
throw new Win32Exception(error);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
"@
|
|
836
|
+
|
|
837
|
+
try {
|
|
838
|
+
$request = ([Console]::In.ReadToEnd() | ConvertFrom-Json)
|
|
839
|
+
if ($request.operation -eq "set") {
|
|
840
|
+
[Byok.CredentialManager]::Write(
|
|
841
|
+
[string]$request.target,
|
|
842
|
+
[string]$request.username,
|
|
843
|
+
[Convert]::FromBase64String([string]$request.secret_base64)
|
|
844
|
+
)
|
|
845
|
+
exit 0
|
|
846
|
+
}
|
|
847
|
+
if ($request.operation -eq "get") {
|
|
848
|
+
$secret = [Byok.CredentialManager]::Read([string]$request.target)
|
|
849
|
+
if ($null -eq $secret) { exit 44 }
|
|
850
|
+
[Console]::Out.Write([Convert]::ToBase64String($secret))
|
|
851
|
+
exit 0
|
|
852
|
+
}
|
|
853
|
+
if ($request.operation -eq "delete") {
|
|
854
|
+
if ([Byok.CredentialManager]::Delete([string]$request.target)) { exit 0 }
|
|
855
|
+
exit 44
|
|
856
|
+
}
|
|
857
|
+
exit 2
|
|
858
|
+
} catch {
|
|
859
|
+
[Console]::Error.Write("credential operation failed")
|
|
860
|
+
exit 1
|
|
861
|
+
}
|
|
862
|
+
`,
|
|
863
|
+
"utf16le"
|
|
864
|
+
).toString("base64");
|
|
865
|
+
var WindowsCredentialManagerSecretStore = class _WindowsCredentialManagerSecretStore {
|
|
866
|
+
providerLabel = "Windows Credential Manager";
|
|
867
|
+
#account;
|
|
868
|
+
#commandRunner;
|
|
869
|
+
#platform;
|
|
870
|
+
#servicePrefix;
|
|
871
|
+
constructor(options = {}) {
|
|
872
|
+
this.#account = options.account ?? "local-device";
|
|
873
|
+
this.#commandRunner = options.commandRunner ?? runCommand;
|
|
874
|
+
this.#platform = options.platform ?? process.platform;
|
|
875
|
+
this.#servicePrefix = options.servicePrefix ?? DEFAULT_SECRET_SERVICE_PREFIX;
|
|
876
|
+
}
|
|
877
|
+
async available() {
|
|
878
|
+
if (this.#platform !== "win32") return false;
|
|
879
|
+
const result = await this.#commandRunner("powershell.exe", [
|
|
880
|
+
"-NoLogo",
|
|
881
|
+
"-NoProfile",
|
|
882
|
+
"-NonInteractive",
|
|
883
|
+
"-Command",
|
|
884
|
+
"$PSVersionTable.PSVersion.Major"
|
|
885
|
+
]);
|
|
886
|
+
return result.exitCode === 0;
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Delete, then read back. The source verifies rather than trusting the
|
|
890
|
+
* delete's exit code (`index.ts:651-679`): a credential that survives a
|
|
891
|
+
* "successful" delete is a security failure, so it is reported as one instead
|
|
892
|
+
* of being returned as `true`.
|
|
893
|
+
*/
|
|
894
|
+
async delete(name) {
|
|
895
|
+
const target = this.#service(name);
|
|
896
|
+
this.#assertWindows();
|
|
897
|
+
const result = await this.#invoke({
|
|
898
|
+
operation: "delete",
|
|
899
|
+
target,
|
|
900
|
+
username: this.#account
|
|
901
|
+
});
|
|
902
|
+
if (result.exitCode === CREDENTIAL_NOT_FOUND) return false;
|
|
903
|
+
if (result.exitCode !== 0) {
|
|
904
|
+
throw new ByokKeysError(
|
|
905
|
+
"CREDENTIAL_MANAGER_DELETE_FAILED",
|
|
906
|
+
"Windows Credential Manager could not delete the requested secret"
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
const verification = await this.#invoke({
|
|
910
|
+
operation: "get",
|
|
911
|
+
target,
|
|
912
|
+
username: this.#account
|
|
913
|
+
});
|
|
914
|
+
if (verification.exitCode === CREDENTIAL_NOT_FOUND) return true;
|
|
915
|
+
if (verification.exitCode !== 0) {
|
|
916
|
+
throw new ByokKeysError(
|
|
917
|
+
"CREDENTIAL_MANAGER_DELETE_FAILED",
|
|
918
|
+
"Windows Credential Manager could not verify secret deletion"
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
throw new ByokKeysError(
|
|
922
|
+
"CREDENTIAL_MANAGER_DELETE_FAILED",
|
|
923
|
+
"Windows Credential Manager reported deletion but the secret remains"
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
async get(name) {
|
|
927
|
+
const target = this.#service(name);
|
|
928
|
+
this.#assertWindows();
|
|
929
|
+
const result = await this.#invoke({
|
|
930
|
+
operation: "get",
|
|
931
|
+
target,
|
|
932
|
+
username: this.#account
|
|
933
|
+
});
|
|
934
|
+
if (result.exitCode === CREDENTIAL_NOT_FOUND) return void 0;
|
|
935
|
+
if (result.exitCode !== 0) {
|
|
936
|
+
throw new ByokKeysError(
|
|
937
|
+
"CREDENTIAL_MANAGER_READ_FAILED",
|
|
938
|
+
"Windows Credential Manager could not read the requested secret"
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
const secret = decodeStrictBase64Utf8(result.stdout.trim());
|
|
942
|
+
if (secret === void 0 || secret.length === 0) {
|
|
943
|
+
throw new ByokKeysError(
|
|
944
|
+
"CREDENTIAL_MANAGER_READ_FAILED",
|
|
945
|
+
"Windows Credential Manager returned an invalid secret"
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
return secret;
|
|
949
|
+
}
|
|
950
|
+
async has(name) {
|
|
951
|
+
return await this.get(name) !== void 0;
|
|
952
|
+
}
|
|
953
|
+
scope(namespace) {
|
|
954
|
+
return new _WindowsCredentialManagerSecretStore({
|
|
955
|
+
account: this.#account,
|
|
956
|
+
commandRunner: this.#commandRunner,
|
|
957
|
+
platform: this.#platform,
|
|
958
|
+
servicePrefix: `${this.#servicePrefix}.scope.${assertSecretNamespace(namespace)}`
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
async set(name, secret) {
|
|
962
|
+
const target = this.#service(name);
|
|
963
|
+
this.#assertWindows();
|
|
964
|
+
assertWindowsCredentialSecret(secret);
|
|
965
|
+
const result = await this.#invoke({
|
|
966
|
+
operation: "set",
|
|
967
|
+
secret_base64: Buffer.from(secret, "utf8").toString("base64"),
|
|
968
|
+
target,
|
|
969
|
+
username: this.#account
|
|
970
|
+
});
|
|
971
|
+
if (result.exitCode !== 0) {
|
|
972
|
+
throw new ByokKeysError(
|
|
973
|
+
"CREDENTIAL_MANAGER_WRITE_FAILED",
|
|
974
|
+
"Windows Credential Manager could not store the requested secret"
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
#assertWindows() {
|
|
979
|
+
if (this.#platform !== "win32") {
|
|
980
|
+
throw new ByokKeysError(
|
|
981
|
+
"CREDENTIAL_MANAGER_UNAVAILABLE",
|
|
982
|
+
"Windows Credential Manager is required; plaintext fallback is disabled"
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
async #invoke(request) {
|
|
987
|
+
return this.#commandRunner(
|
|
988
|
+
"powershell.exe",
|
|
989
|
+
[
|
|
990
|
+
"-NoLogo",
|
|
991
|
+
"-NoProfile",
|
|
992
|
+
"-NonInteractive",
|
|
993
|
+
"-EncodedCommand",
|
|
994
|
+
WINDOWS_CREDENTIAL_MANAGER_SCRIPT_BASE64
|
|
995
|
+
],
|
|
996
|
+
JSON.stringify(request)
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
#service(name) {
|
|
1000
|
+
return `${this.#servicePrefix}.${assertSecretName(name)}`;
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
function assertWindowsCredentialSecret(secret) {
|
|
1004
|
+
const bytes = Buffer.byteLength(secret, "utf8");
|
|
1005
|
+
if (secret.length === 0 || bytes > 2560 || /[\u0000\r\n]/u.test(secret)) {
|
|
1006
|
+
throw new ByokKeysError(
|
|
1007
|
+
"CREDENTIAL_MANAGER_SECRET_INVALID",
|
|
1008
|
+
"Secret must contain 1 to 2560 UTF-8 bytes without newline characters"
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
var DEFAULT_SECRET_ENVELOPE_PREFIX = "byok-scoped-secrets-v1:";
|
|
1013
|
+
function secretScopeId(scope) {
|
|
1014
|
+
const normalized = normalizeSecretScope(scope);
|
|
1015
|
+
return `acct_${createHash("sha256").update(`${normalized.account_id}
|
|
1016
|
+
${normalized.workspace_id}`, "utf8").digest("hex")}`;
|
|
1017
|
+
}
|
|
1018
|
+
function scopeSecretStore(store, scope) {
|
|
1019
|
+
return store.scope(secretScopeId(scope));
|
|
1020
|
+
}
|
|
1021
|
+
var EnvelopeScopedSecretStore = class _EnvelopeScopedSecretStore {
|
|
1022
|
+
providerLabel;
|
|
1023
|
+
#envelopePrefix;
|
|
1024
|
+
#scopeId;
|
|
1025
|
+
#store;
|
|
1026
|
+
constructor(store, scopeId, options = {}) {
|
|
1027
|
+
this.#store = store;
|
|
1028
|
+
this.#scopeId = assertScopeSlotKey(scopeId);
|
|
1029
|
+
this.#envelopePrefix = options.envelopePrefix ?? DEFAULT_SECRET_ENVELOPE_PREFIX;
|
|
1030
|
+
this.providerLabel = store.providerLabel;
|
|
1031
|
+
}
|
|
1032
|
+
available() {
|
|
1033
|
+
return this.#store.available();
|
|
1034
|
+
}
|
|
1035
|
+
async delete(name) {
|
|
1036
|
+
const envelope = await this.#readEnvelope(name);
|
|
1037
|
+
if (envelope === void 0 || !Object.hasOwn(envelope, this.#scopeId)) {
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
delete envelope[this.#scopeId];
|
|
1041
|
+
if (Object.keys(envelope).length === 0) return this.#store.delete(name);
|
|
1042
|
+
await this.#store.set(name, this.#serialize(envelope));
|
|
1043
|
+
return true;
|
|
1044
|
+
}
|
|
1045
|
+
async get(name) {
|
|
1046
|
+
return (await this.#readEnvelope(name))?.[this.#scopeId];
|
|
1047
|
+
}
|
|
1048
|
+
async has(name) {
|
|
1049
|
+
return await this.get(name) !== void 0;
|
|
1050
|
+
}
|
|
1051
|
+
scope(namespace) {
|
|
1052
|
+
return new _EnvelopeScopedSecretStore(
|
|
1053
|
+
this.#store,
|
|
1054
|
+
`${this.#scopeId}.${assertSecretNamespace(namespace)}`,
|
|
1055
|
+
{ envelopePrefix: this.#envelopePrefix }
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
async set(name, secret) {
|
|
1059
|
+
assertSharedSecretValue(secret);
|
|
1060
|
+
const envelope = await this.#readEnvelope(name) ?? /* @__PURE__ */ Object.create(null);
|
|
1061
|
+
envelope[this.#scopeId] = secret;
|
|
1062
|
+
await this.#store.set(name, this.#serialize(envelope));
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* `undefined` means the entry is absent; a malformed entry throws.
|
|
1066
|
+
*
|
|
1067
|
+
* The returned map has a null prototype. A slot key is attacker-adjacent
|
|
1068
|
+
* data — `constructor` is a legal namespace (`toString` and `__proto__` are
|
|
1069
|
+
* rejected by the pattern, but this defense does not depend on the pattern's
|
|
1070
|
+
* shape) — so a plain object would answer `envelope[scopeId]` from
|
|
1071
|
+
* `Object.prototype` and hand a Function back as a scope's secret.
|
|
1072
|
+
*/
|
|
1073
|
+
async #readEnvelope(name) {
|
|
1074
|
+
const stored = await this.#store.get(assertSecretName(name));
|
|
1075
|
+
if (stored === void 0) return void 0;
|
|
1076
|
+
if (!stored.startsWith(this.#envelopePrefix)) throw envelopeInvalid();
|
|
1077
|
+
const decoded = decodeBase64Url(stored.slice(this.#envelopePrefix.length));
|
|
1078
|
+
if (decoded === void 0) throw envelopeInvalid();
|
|
1079
|
+
let parsed;
|
|
1080
|
+
try {
|
|
1081
|
+
parsed = JSON.parse(decoded);
|
|
1082
|
+
} catch {
|
|
1083
|
+
throw envelopeInvalid();
|
|
1084
|
+
}
|
|
1085
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || Object.values(parsed).some((value) => typeof value !== "string")) {
|
|
1086
|
+
throw envelopeInvalid();
|
|
1087
|
+
}
|
|
1088
|
+
return Object.assign(
|
|
1089
|
+
/* @__PURE__ */ Object.create(null),
|
|
1090
|
+
parsed
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
#serialize(value) {
|
|
1094
|
+
return `${this.#envelopePrefix}${Buffer.from(
|
|
1095
|
+
JSON.stringify(value),
|
|
1096
|
+
"utf8"
|
|
1097
|
+
).toString("base64url")}`;
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
function assertScopeSlotKey(value) {
|
|
1101
|
+
return value.trim().split(".").map((segment) => assertSecretNamespace(segment)).join(".");
|
|
1102
|
+
}
|
|
1103
|
+
function envelopeInvalid() {
|
|
1104
|
+
return new ByokKeysError(
|
|
1105
|
+
"SECRET_ENVELOPE_INVALID",
|
|
1106
|
+
"The stored value is not a well-formed scoped-secret envelope"
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
function decodeBase64Url(encoded) {
|
|
1110
|
+
if (!/^[A-Za-z0-9_-]*$/u.test(encoded)) return void 0;
|
|
1111
|
+
const bytes = Buffer.from(encoded, "base64url");
|
|
1112
|
+
if (bytes.toString("base64url") !== encoded) return void 0;
|
|
1113
|
+
const text = bytes.toString("utf8");
|
|
1114
|
+
if (!Buffer.from(text, "utf8").equals(bytes)) return void 0;
|
|
1115
|
+
return text;
|
|
1116
|
+
}
|
|
1117
|
+
function normalizeSecretScope(scope) {
|
|
1118
|
+
const accountId = scope.account_id.trim();
|
|
1119
|
+
const workspaceId = scope.workspace_id.trim();
|
|
1120
|
+
if (!accountId || !workspaceId || accountId.length > 160 || workspaceId.length > 160 || /[\u0000\r\n]/u.test(accountId) || /[\u0000\r\n]/u.test(workspaceId)) {
|
|
1121
|
+
throw new ByokKeysError(
|
|
1122
|
+
"LOCAL_ACCOUNT_SCOPE_INVALID",
|
|
1123
|
+
"The local account data scope is invalid"
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
return { account_id: accountId, workspace_id: workspaceId };
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// src/profile-store.ts
|
|
1130
|
+
function providerNotConfigured(providerId) {
|
|
1131
|
+
return new ByokKeysError(
|
|
1132
|
+
"PROVIDER_NOT_CONFIGURED",
|
|
1133
|
+
`${providerId} model provider is not configured`
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
var InMemoryProviderProfileStore = class {
|
|
1137
|
+
#profiles = /* @__PURE__ */ new Map();
|
|
1138
|
+
close() {
|
|
1139
|
+
this.#profiles.clear();
|
|
1140
|
+
}
|
|
1141
|
+
delete(providerId) {
|
|
1142
|
+
return this.#profiles.delete(providerId);
|
|
1143
|
+
}
|
|
1144
|
+
get(providerId) {
|
|
1145
|
+
return this.#profiles.get(providerId);
|
|
1146
|
+
}
|
|
1147
|
+
getEnabled() {
|
|
1148
|
+
return this.list().find((profile) => profile.enabled);
|
|
1149
|
+
}
|
|
1150
|
+
list() {
|
|
1151
|
+
return [...this.#profiles.values()].sort(
|
|
1152
|
+
(left, right) => left.provider_id.localeCompare(right.provider_id)
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
save(profile) {
|
|
1156
|
+
const validated = parseModelProviderProfile({
|
|
1157
|
+
...profile,
|
|
1158
|
+
created_at: this.#profiles.get(profile.provider_id)?.created_at ?? profile.created_at
|
|
1159
|
+
});
|
|
1160
|
+
if (validated.enabled) {
|
|
1161
|
+
for (const [providerId, existing] of this.#profiles) {
|
|
1162
|
+
if (providerId !== validated.provider_id && existing.enabled) {
|
|
1163
|
+
this.#profiles.set(providerId, { ...existing, enabled: false });
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
this.#profiles.set(validated.provider_id, validated);
|
|
1168
|
+
return validated;
|
|
1169
|
+
}
|
|
1170
|
+
setEnabled(providerId) {
|
|
1171
|
+
const existing = this.#profiles.get(providerId);
|
|
1172
|
+
if (existing === void 0) throw providerNotConfigured(providerId);
|
|
1173
|
+
return this.save({ ...existing, enabled: true });
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
var SECURE_DIR_MODE = 448;
|
|
1177
|
+
var SECURE_FILE_MODE = 384;
|
|
1178
|
+
var DEFAULT_BUSY_TIMEOUT_MS = 5e3;
|
|
1179
|
+
function loadSqliteModule() {
|
|
1180
|
+
try {
|
|
1181
|
+
return createRequire(import.meta.url)("node:sqlite");
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
throw new ByokKeysError(
|
|
1184
|
+
"PROVIDER_STORE_UNAVAILABLE",
|
|
1185
|
+
"node:sqlite is unavailable in this Node.js runtime. SqliteProviderProfileStore requires Node.js 22.5+ with the built-in `node:sqlite` module (no native dependency is used or allowed here). Upgrade Node.js, or use InMemoryProviderProfileStore instead.",
|
|
1186
|
+
{ cause: error }
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
function isSqliteAvailable() {
|
|
1191
|
+
try {
|
|
1192
|
+
loadSqliteModule();
|
|
1193
|
+
return true;
|
|
1194
|
+
} catch {
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
function openSqliteDatabase(path, options) {
|
|
1199
|
+
const { DatabaseSync } = loadSqliteModule();
|
|
1200
|
+
if (path !== ":memory:") {
|
|
1201
|
+
mkdirSync(dirname(path), { mode: SECURE_DIR_MODE, recursive: true });
|
|
1202
|
+
}
|
|
1203
|
+
const database = new DatabaseSync(path, {
|
|
1204
|
+
timeout: DEFAULT_BUSY_TIMEOUT_MS,
|
|
1205
|
+
...options
|
|
1206
|
+
});
|
|
1207
|
+
if (path !== ":memory:") {
|
|
1208
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
1209
|
+
database.exec("PRAGMA synchronous = FULL");
|
|
1210
|
+
}
|
|
1211
|
+
return database;
|
|
1212
|
+
}
|
|
1213
|
+
function secureSqliteFilePermissions(databasePath) {
|
|
1214
|
+
if (databasePath === ":memory:") return;
|
|
1215
|
+
for (const candidate of [
|
|
1216
|
+
databasePath,
|
|
1217
|
+
`${databasePath}-wal`,
|
|
1218
|
+
`${databasePath}-shm`
|
|
1219
|
+
]) {
|
|
1220
|
+
if (existsSync(candidate)) {
|
|
1221
|
+
chmodSync(candidate, SECURE_FILE_MODE);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// src/sqlite-profile-store.ts
|
|
1227
|
+
var SCHEMA = `
|
|
1228
|
+
CREATE TABLE IF NOT EXISTS provider_profile (
|
|
1229
|
+
provider_id TEXT PRIMARY KEY CHECK (provider_id IN ('openai', 'deepseek', 'anthropic', 'custom')),
|
|
1230
|
+
kind TEXT NOT NULL CHECK (kind = 'model'),
|
|
1231
|
+
adapter TEXT NOT NULL CHECK (adapter IN ('openai_compatible', 'anthropic')),
|
|
1232
|
+
display_name TEXT NOT NULL,
|
|
1233
|
+
base_url TEXT NOT NULL,
|
|
1234
|
+
auth_mode TEXT NOT NULL CHECK (auth_mode IN ('bearer', 'x_api_key', 'none')),
|
|
1235
|
+
model TEXT NOT NULL,
|
|
1236
|
+
enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
|
|
1237
|
+
created_at TEXT NOT NULL,
|
|
1238
|
+
updated_at TEXT NOT NULL
|
|
1239
|
+
);
|
|
1240
|
+
`;
|
|
1241
|
+
var ENABLED_INDEX = `
|
|
1242
|
+
CREATE UNIQUE INDEX IF NOT EXISTS provider_profile_one_enabled
|
|
1243
|
+
ON provider_profile(kind)
|
|
1244
|
+
WHERE enabled = 1;
|
|
1245
|
+
`;
|
|
1246
|
+
var SqliteProviderProfileStore = class {
|
|
1247
|
+
#database;
|
|
1248
|
+
#closed = false;
|
|
1249
|
+
constructor(options) {
|
|
1250
|
+
this.#database = openSqliteDatabase(options.path);
|
|
1251
|
+
this.#database.exec(SCHEMA);
|
|
1252
|
+
this.#database.exec(ENABLED_INDEX);
|
|
1253
|
+
secureSqliteFilePermissions(options.path);
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Idempotent, as {@link ProviderProfileStore.close} requires: `node:sqlite`
|
|
1257
|
+
* throws "database is not open" on a second `close()`, and a store is
|
|
1258
|
+
* routinely closed both by the code that finished with it and by a test's
|
|
1259
|
+
* teardown.
|
|
1260
|
+
*/
|
|
1261
|
+
close() {
|
|
1262
|
+
if (this.#closed) return;
|
|
1263
|
+
this.#closed = true;
|
|
1264
|
+
this.#database.close();
|
|
1265
|
+
}
|
|
1266
|
+
delete(providerId) {
|
|
1267
|
+
const result = this.#database.prepare("DELETE FROM provider_profile WHERE provider_id = ?").run(providerId);
|
|
1268
|
+
return Number(result.changes) === 1;
|
|
1269
|
+
}
|
|
1270
|
+
get(providerId) {
|
|
1271
|
+
const row = this.#database.prepare("SELECT * FROM provider_profile WHERE provider_id = ?").get(providerId);
|
|
1272
|
+
return row === void 0 ? void 0 : parseRow(row);
|
|
1273
|
+
}
|
|
1274
|
+
getEnabled() {
|
|
1275
|
+
const row = this.#database.prepare("SELECT * FROM provider_profile WHERE enabled = 1").get();
|
|
1276
|
+
return row === void 0 ? void 0 : parseRow(row);
|
|
1277
|
+
}
|
|
1278
|
+
list() {
|
|
1279
|
+
const rows = this.#database.prepare("SELECT * FROM provider_profile ORDER BY provider_id ASC").all();
|
|
1280
|
+
return rows.map(parseRow);
|
|
1281
|
+
}
|
|
1282
|
+
save(profile) {
|
|
1283
|
+
const validated = parseModelProviderProfile({
|
|
1284
|
+
...profile,
|
|
1285
|
+
created_at: this.get(profile.provider_id)?.created_at ?? profile.created_at
|
|
1286
|
+
});
|
|
1287
|
+
this.#transaction(() => {
|
|
1288
|
+
if (validated.enabled) {
|
|
1289
|
+
this.#database.prepare(
|
|
1290
|
+
"UPDATE provider_profile SET enabled = 0 WHERE provider_id <> ?"
|
|
1291
|
+
).run(validated.provider_id);
|
|
1292
|
+
}
|
|
1293
|
+
this.#database.prepare(
|
|
1294
|
+
`INSERT INTO provider_profile (
|
|
1295
|
+
provider_id, kind, adapter, display_name, base_url,
|
|
1296
|
+
auth_mode, model, enabled, created_at, updated_at
|
|
1297
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1298
|
+
ON CONFLICT(provider_id) DO UPDATE SET
|
|
1299
|
+
adapter = excluded.adapter,
|
|
1300
|
+
display_name = excluded.display_name,
|
|
1301
|
+
base_url = excluded.base_url,
|
|
1302
|
+
auth_mode = excluded.auth_mode,
|
|
1303
|
+
model = excluded.model,
|
|
1304
|
+
enabled = excluded.enabled,
|
|
1305
|
+
updated_at = excluded.updated_at`
|
|
1306
|
+
).run(
|
|
1307
|
+
validated.provider_id,
|
|
1308
|
+
validated.kind,
|
|
1309
|
+
validated.adapter,
|
|
1310
|
+
validated.display_name,
|
|
1311
|
+
validated.base_url,
|
|
1312
|
+
validated.auth_mode,
|
|
1313
|
+
validated.model,
|
|
1314
|
+
validated.enabled ? 1 : 0,
|
|
1315
|
+
validated.created_at,
|
|
1316
|
+
validated.updated_at
|
|
1317
|
+
);
|
|
1318
|
+
});
|
|
1319
|
+
return this.get(validated.provider_id);
|
|
1320
|
+
}
|
|
1321
|
+
setEnabled(providerId) {
|
|
1322
|
+
const existing = this.get(providerId);
|
|
1323
|
+
if (existing === void 0) throw providerNotConfigured(providerId);
|
|
1324
|
+
return this.save({ ...existing, enabled: true });
|
|
1325
|
+
}
|
|
1326
|
+
/** `BEGIN IMMEDIATE` / `COMMIT` / `ROLLBACK`, per `providers.ts:1252-1263`. */
|
|
1327
|
+
#transaction(body) {
|
|
1328
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
1329
|
+
try {
|
|
1330
|
+
body();
|
|
1331
|
+
this.#database.exec("COMMIT");
|
|
1332
|
+
} catch (error) {
|
|
1333
|
+
this.#database.exec("ROLLBACK");
|
|
1334
|
+
throw error;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
function parseRow(row) {
|
|
1339
|
+
return parseModelProviderProfile({
|
|
1340
|
+
...row,
|
|
1341
|
+
enabled: row.enabled === 1
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// src/registry.ts
|
|
1346
|
+
var ProviderRegistry = class {
|
|
1347
|
+
#fetch;
|
|
1348
|
+
#now;
|
|
1349
|
+
#profiles;
|
|
1350
|
+
#secrets;
|
|
1351
|
+
constructor(options) {
|
|
1352
|
+
this.#fetch = options.fetchImpl;
|
|
1353
|
+
this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1354
|
+
this.#profiles = options.profileStore;
|
|
1355
|
+
this.#secrets = options.secretStore;
|
|
1356
|
+
}
|
|
1357
|
+
close() {
|
|
1358
|
+
this.#profiles.close();
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Persist a provider's profile and, when supplied, its secret
|
|
1362
|
+
* (`providers.ts:1180-1229`).
|
|
1363
|
+
*
|
|
1364
|
+
* Order matters and is the source's: write the secret first, then require
|
|
1365
|
+
* that an authenticating profile actually has one, and only then save the
|
|
1366
|
+
* profile. A profile is therefore never persisted in a state that claims
|
|
1367
|
+
* authentication it cannot perform.
|
|
1368
|
+
*/
|
|
1369
|
+
async configure(configuration, secret) {
|
|
1370
|
+
const timestamp = this.#now().toISOString();
|
|
1371
|
+
const previous = this.#profiles.get(configuration.provider_id);
|
|
1372
|
+
const profile = {
|
|
1373
|
+
...configuration,
|
|
1374
|
+
created_at: previous?.created_at ?? timestamp,
|
|
1375
|
+
enabled: configuration.enabled ?? true,
|
|
1376
|
+
kind: "model",
|
|
1377
|
+
updated_at: timestamp
|
|
1378
|
+
};
|
|
1379
|
+
const secretName = modelProviderSecretName(configuration.provider_id);
|
|
1380
|
+
if (configuration.auth_mode === "none" && secret !== void 0) {
|
|
1381
|
+
throw new ByokKeysError(
|
|
1382
|
+
"PROVIDER_SECRET_NOT_ALLOWED",
|
|
1383
|
+
"A provider without authentication cannot accept a secret"
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
if (secret !== void 0) {
|
|
1387
|
+
if (secret.length === 0) {
|
|
1388
|
+
throw new ByokKeysError(
|
|
1389
|
+
"PROVIDER_SECRET_EMPTY",
|
|
1390
|
+
"Provider secret cannot be empty"
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
await this.#secrets.set(secretName, secret);
|
|
1394
|
+
}
|
|
1395
|
+
if (configuration.auth_mode !== "none" && !await this.#secrets.has(secretName)) {
|
|
1396
|
+
throw new ByokKeysError(
|
|
1397
|
+
"PROVIDER_SECRET_MISSING",
|
|
1398
|
+
"Provider authentication requires a secret in the operating-system credential store"
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
const saved = this.#profiles.save(profile);
|
|
1402
|
+
if (saved.auth_mode === "none") {
|
|
1403
|
+
await this.#secrets.delete(secretName);
|
|
1404
|
+
}
|
|
1405
|
+
return this.#status(saved);
|
|
1406
|
+
}
|
|
1407
|
+
/** Remove a provider's profile and its secret together. */
|
|
1408
|
+
async delete(providerId) {
|
|
1409
|
+
const removed = this.#profiles.delete(providerId);
|
|
1410
|
+
await this.#secrets.delete(modelProviderSecretName(providerId));
|
|
1411
|
+
return removed;
|
|
1412
|
+
}
|
|
1413
|
+
async get(providerId) {
|
|
1414
|
+
const profile = this.#profiles.get(providerId);
|
|
1415
|
+
return profile === void 0 ? void 0 : this.#status(profile);
|
|
1416
|
+
}
|
|
1417
|
+
async list() {
|
|
1418
|
+
return Promise.all(
|
|
1419
|
+
this.#profiles.list().map((profile) => this.#status(profile))
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Build a client for the one enabled provider (`providers.ts:1331-1354`).
|
|
1424
|
+
*
|
|
1425
|
+
* `undefined` means "nothing is configured", which is a legitimate state a
|
|
1426
|
+
* caller must handle. A configured-but-broken provider throws instead — a
|
|
1427
|
+
* missing secret or an unusable profile is a fault, not an absence.
|
|
1428
|
+
*/
|
|
1429
|
+
async resolveDefaultModelProvider() {
|
|
1430
|
+
const profile = this.#profiles.getEnabled();
|
|
1431
|
+
if (profile === void 0) return void 0;
|
|
1432
|
+
const secret = await this.#secrets.get(
|
|
1433
|
+
modelProviderSecretName(profile.provider_id)
|
|
1434
|
+
);
|
|
1435
|
+
const options = { fetchImpl: this.#fetch, profile, secret };
|
|
1436
|
+
return profile.adapter === "anthropic" ? new AnthropicMessagesClient(options) : new OpenAiCompatibleChatClient(options);
|
|
1437
|
+
}
|
|
1438
|
+
/** Switch which configured provider is the default. */
|
|
1439
|
+
async setDefaultModelProvider(providerId) {
|
|
1440
|
+
return this.#status(this.#profiles.setEnabled(providerId));
|
|
1441
|
+
}
|
|
1442
|
+
async #status(profile) {
|
|
1443
|
+
return {
|
|
1444
|
+
adapter: profile.adapter,
|
|
1445
|
+
auth_mode: profile.auth_mode,
|
|
1446
|
+
base_url: profile.base_url,
|
|
1447
|
+
created_at: profile.created_at,
|
|
1448
|
+
display_name: profile.display_name,
|
|
1449
|
+
enabled: profile.enabled,
|
|
1450
|
+
model: profile.model,
|
|
1451
|
+
provider_id: profile.provider_id,
|
|
1452
|
+
secret_configured: await this.#secrets.has(
|
|
1453
|
+
modelProviderSecretName(profile.provider_id)
|
|
1454
|
+
),
|
|
1455
|
+
updated_at: profile.updated_at
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
export { AnthropicMessagesClient, BYOK_KEYS_ERROR_CODES, ByokKeysError, DEFAULT_KEYCHAIN_SECRET_STORAGE_PREFIX, DEFAULT_SECRET_ENVELOPE_PREFIX, DEFAULT_SECRET_SERVICE_PREFIX, EnvelopeScopedSecretStore, InMemoryProviderProfileStore, InMemorySecretStore, MODEL_PROVIDER_ADAPTERS, MODEL_PROVIDER_IDS, MODEL_PROVIDER_SECRET_NAMES, MacOsKeychainSecretStore, ModelProviderProfileSchema, OpenAiCompatibleChatClient, PROVIDER_AUTH_MODES, PROVIDER_RESPONSE_MAX_BYTES, PROVIDER_TIMEOUT_MS, ProviderRegistry, SECRET_NAMESPACE_PATTERN, SECRET_NAME_PATTERN, SqliteProviderProfileStore, WindowsCredentialManagerSecretStore, anthropicMessageText, assertLiveModelResponse, assertSecretName, assertSecretNamespace, assertSharedSecretValue, chatCompletionText, classifyModelProviderHttpError, decodeStrictBase64Utf8, fetchWithProviderGuards, isLoopbackHost, isLoopbackProviderUrl, isPrivateNetworkLiteral, isSqliteAvailable, loadSqliteModule, modelApiUrl, modelMessageText, modelProviderSecretName, normalizeProviderUrl, objectValue, openSqliteDatabase, parseBoundedJsonResponse, parseModelProviderProfile, providerHeaders, readModelProviderResponse, requiredProviderSecret, runCommand, scopeSecretStore, secretScopeId, secureSqliteFilePermissions };
|
|
1461
|
+
//# sourceMappingURL=index.js.map
|
|
1462
|
+
//# sourceMappingURL=index.js.map
|