@byok-sdk/keys 0.1.0 → 0.2.1
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/README.md +34 -15
- package/dist/bin/pi-provider-launcher.d.ts +2 -0
- package/dist/bin/pi-provider-launcher.js +1137 -0
- package/dist/bin/pi-provider-launcher.js.map +1 -0
- package/dist/errors.d.ts +4 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +340 -40
- package/dist/index.js.map +1 -1
- package/dist/pi-provider-launcher-core.d.ts +34 -0
- package/dist/pi-provider-projection.d.ts +11 -0
- package/dist/profile-store.d.ts +16 -16
- package/dist/registry.d.ts +1 -1
- package/dist/sqlite-profile-store.d.ts +11 -9
- package/dist/sqlite-support.d.ts +11 -3
- package/dist/truth-profile-store.d.ts +27 -0
- package/package.json +14 -7
|
@@ -0,0 +1,1137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { promises, mkdirSync, existsSync, chmodSync } from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path, { dirname } from 'path';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { createRequire } from 'module';
|
|
8
|
+
|
|
9
|
+
// src/errors.ts
|
|
10
|
+
var ByokKeysError = class extends Error {
|
|
11
|
+
code;
|
|
12
|
+
httpStatus;
|
|
13
|
+
constructor(code, message, options) {
|
|
14
|
+
super(message, options);
|
|
15
|
+
this.name = "ByokKeysError";
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.httpStatus = options?.httpStatus;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
async function runCommand(executable, args, stdin) {
|
|
21
|
+
return new Promise((resolveResult) => {
|
|
22
|
+
const child = spawn(executable, args, {
|
|
23
|
+
env: process.env,
|
|
24
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
25
|
+
});
|
|
26
|
+
let stdout = "";
|
|
27
|
+
let stderr = "";
|
|
28
|
+
child.stdout.setEncoding("utf8");
|
|
29
|
+
child.stderr.setEncoding("utf8");
|
|
30
|
+
child.stdout.on("data", (chunk) => {
|
|
31
|
+
stdout += chunk;
|
|
32
|
+
});
|
|
33
|
+
child.stderr.on("data", (chunk) => {
|
|
34
|
+
stderr += chunk;
|
|
35
|
+
});
|
|
36
|
+
child.on("error", () => {
|
|
37
|
+
resolveResult({
|
|
38
|
+
exitCode: 127,
|
|
39
|
+
stderr: "command unavailable",
|
|
40
|
+
stdout: ""
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
child.on("close", (code) => {
|
|
44
|
+
resolveResult({ exitCode: code ?? 1, stderr, stdout });
|
|
45
|
+
});
|
|
46
|
+
if (stdin === void 0) child.stdin.end();
|
|
47
|
+
else child.stdin.end(stdin);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/secret-name.ts
|
|
52
|
+
var SECRET_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{2,95}$/u;
|
|
53
|
+
var SECRET_NAMESPACE_PATTERN = /^[a-z0-9][a-z0-9_-]{7,95}$/u;
|
|
54
|
+
function assertSecretName(name) {
|
|
55
|
+
if (!SECRET_NAME_PATTERN.test(name)) {
|
|
56
|
+
throw new ByokKeysError(
|
|
57
|
+
"SECRET_NAME_INVALID",
|
|
58
|
+
"Secret name must be 3 to 96 characters of [a-z0-9_-] starting with a letter or digit"
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return name;
|
|
62
|
+
}
|
|
63
|
+
function assertSecretNamespace(value) {
|
|
64
|
+
const normalized = value.trim();
|
|
65
|
+
if (!SECRET_NAMESPACE_PATTERN.test(normalized)) {
|
|
66
|
+
throw new ByokKeysError(
|
|
67
|
+
"SECRET_NAMESPACE_INVALID",
|
|
68
|
+
"Secret namespace must be 8 to 96 characters of [a-z0-9_-] starting with a letter or digit"
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return normalized;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/secret-store.ts
|
|
75
|
+
var DEFAULT_SECRET_SERVICE_PREFIX = "com.byok.keys";
|
|
76
|
+
var MODEL_PROVIDER_SECRET_NAMES = {
|
|
77
|
+
anthropic: "model-anthropic-api-key",
|
|
78
|
+
custom: "model-custom-api-key",
|
|
79
|
+
deepseek: "model-deepseek-api-key",
|
|
80
|
+
openai: "model-openai-api-key"
|
|
81
|
+
};
|
|
82
|
+
function modelProviderSecretName(providerId) {
|
|
83
|
+
return MODEL_PROVIDER_SECRET_NAMES[providerId];
|
|
84
|
+
}
|
|
85
|
+
function decodeStrictBase64Utf8(encoded) {
|
|
86
|
+
if (encoded.length % 4 !== 0) return void 0;
|
|
87
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/u.test(encoded)) return void 0;
|
|
88
|
+
const bytes = Buffer.from(encoded, "base64");
|
|
89
|
+
if (bytes.toString("base64") !== encoded) return void 0;
|
|
90
|
+
const text = bytes.toString("utf8");
|
|
91
|
+
if (!Buffer.from(text, "utf8").equals(bytes)) return void 0;
|
|
92
|
+
return text;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/macos-keychain.ts
|
|
96
|
+
var DEFAULT_KEYCHAIN_SECRET_STORAGE_PREFIX = "byok-b64-v1:";
|
|
97
|
+
var SECURITY_ITEM_NOT_FOUND = 44;
|
|
98
|
+
var MacOsKeychainSecretStore = class _MacOsKeychainSecretStore {
|
|
99
|
+
providerLabel = "macOS Keychain";
|
|
100
|
+
#account;
|
|
101
|
+
#allowUnprefixedRead;
|
|
102
|
+
#commandRunner;
|
|
103
|
+
#platform;
|
|
104
|
+
#servicePrefix;
|
|
105
|
+
#storagePrefix;
|
|
106
|
+
constructor(options = {}) {
|
|
107
|
+
this.#account = options.account ?? "local-device";
|
|
108
|
+
this.#allowUnprefixedRead = options.allowUnprefixedRead ?? false;
|
|
109
|
+
this.#commandRunner = options.commandRunner ?? runCommand;
|
|
110
|
+
this.#platform = options.platform ?? process.platform;
|
|
111
|
+
this.#servicePrefix = options.servicePrefix ?? DEFAULT_SECRET_SERVICE_PREFIX;
|
|
112
|
+
this.#storagePrefix = options.storagePrefix ?? DEFAULT_KEYCHAIN_SECRET_STORAGE_PREFIX;
|
|
113
|
+
}
|
|
114
|
+
async available() {
|
|
115
|
+
if (this.#platform !== "darwin") return false;
|
|
116
|
+
const result = await this.#commandRunner("/usr/bin/security", [
|
|
117
|
+
"default-keychain",
|
|
118
|
+
"-d",
|
|
119
|
+
"user"
|
|
120
|
+
]);
|
|
121
|
+
return result.exitCode === 0;
|
|
122
|
+
}
|
|
123
|
+
async delete(name) {
|
|
124
|
+
const service = this.#service(name);
|
|
125
|
+
this.#assertMacOs();
|
|
126
|
+
const result = await this.#commandRunner("/usr/bin/security", [
|
|
127
|
+
"delete-generic-password",
|
|
128
|
+
"-a",
|
|
129
|
+
this.#account,
|
|
130
|
+
"-s",
|
|
131
|
+
service
|
|
132
|
+
]);
|
|
133
|
+
if (result.exitCode === SECURITY_ITEM_NOT_FOUND) return false;
|
|
134
|
+
if (result.exitCode !== 0) {
|
|
135
|
+
throw new ByokKeysError(
|
|
136
|
+
"KEYCHAIN_DELETE_FAILED",
|
|
137
|
+
"macOS Keychain could not delete the requested secret"
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
async get(name) {
|
|
143
|
+
const service = this.#service(name);
|
|
144
|
+
this.#assertMacOs();
|
|
145
|
+
const result = await this.#commandRunner("/usr/bin/security", [
|
|
146
|
+
"find-generic-password",
|
|
147
|
+
"-a",
|
|
148
|
+
this.#account,
|
|
149
|
+
"-s",
|
|
150
|
+
service,
|
|
151
|
+
"-w"
|
|
152
|
+
]);
|
|
153
|
+
if (result.exitCode === SECURITY_ITEM_NOT_FOUND) return void 0;
|
|
154
|
+
if (result.exitCode !== 0) {
|
|
155
|
+
throw new ByokKeysError(
|
|
156
|
+
"KEYCHAIN_READ_FAILED",
|
|
157
|
+
"macOS Keychain could not read the requested secret"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return this.#decode(result.stdout.replace(/\r?\n$/u, ""));
|
|
161
|
+
}
|
|
162
|
+
async has(name) {
|
|
163
|
+
return await this.get(name) !== void 0;
|
|
164
|
+
}
|
|
165
|
+
scope(namespace) {
|
|
166
|
+
return new _MacOsKeychainSecretStore({
|
|
167
|
+
account: this.#account,
|
|
168
|
+
allowUnprefixedRead: this.#allowUnprefixedRead,
|
|
169
|
+
commandRunner: this.#commandRunner,
|
|
170
|
+
platform: this.#platform,
|
|
171
|
+
servicePrefix: `${this.#servicePrefix}.scope.${assertSecretNamespace(namespace)}`,
|
|
172
|
+
storagePrefix: this.#storagePrefix
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async set(name, secret) {
|
|
176
|
+
const service = this.#service(name);
|
|
177
|
+
this.#assertMacOs();
|
|
178
|
+
if (secret.length === 0 || secret.length > 16384 || /[\u0000\r\n]/u.test(secret)) {
|
|
179
|
+
throw new ByokKeysError(
|
|
180
|
+
"KEYCHAIN_SECRET_INVALID",
|
|
181
|
+
"Secret must contain 1 to 16384 non-newline characters"
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const storedSecret = `${this.#storagePrefix}${Buffer.from(secret, "utf8").toString("base64")}`;
|
|
185
|
+
const command = [
|
|
186
|
+
"add-generic-password",
|
|
187
|
+
"-U",
|
|
188
|
+
"-a",
|
|
189
|
+
quoteSecurityInteractiveArgument(this.#account),
|
|
190
|
+
"-s",
|
|
191
|
+
quoteSecurityInteractiveArgument(service),
|
|
192
|
+
"-w",
|
|
193
|
+
quoteSecurityInteractiveArgument(storedSecret)
|
|
194
|
+
].join(" ");
|
|
195
|
+
const result = await this.#commandRunner(
|
|
196
|
+
"/usr/bin/security",
|
|
197
|
+
["-i"],
|
|
198
|
+
`${command}
|
|
199
|
+
`
|
|
200
|
+
);
|
|
201
|
+
if (result.exitCode !== 0) {
|
|
202
|
+
throw new ByokKeysError(
|
|
203
|
+
"KEYCHAIN_WRITE_FAILED",
|
|
204
|
+
"macOS Keychain could not store the requested secret"
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
#assertMacOs() {
|
|
209
|
+
if (this.#platform !== "darwin") {
|
|
210
|
+
throw new ByokKeysError(
|
|
211
|
+
"KEYCHAIN_UNAVAILABLE",
|
|
212
|
+
"macOS Keychain is required; plaintext fallback is disabled"
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Fail-closed inverse of the storage encoding. The error message never
|
|
218
|
+
* echoes the undecodable value — it may still be somebody's live credential.
|
|
219
|
+
*/
|
|
220
|
+
#decode(storedSecret) {
|
|
221
|
+
if (!storedSecret.startsWith(this.#storagePrefix)) {
|
|
222
|
+
if (this.#allowUnprefixedRead) return storedSecret;
|
|
223
|
+
throw new ByokKeysError(
|
|
224
|
+
"KEYCHAIN_SECRET_DECODE_FAILED",
|
|
225
|
+
"macOS Keychain returned a value without this store's storage prefix"
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const decoded = decodeStrictBase64Utf8(
|
|
229
|
+
storedSecret.slice(this.#storagePrefix.length)
|
|
230
|
+
);
|
|
231
|
+
if (decoded === void 0 || decoded.length === 0) {
|
|
232
|
+
throw new ByokKeysError(
|
|
233
|
+
"KEYCHAIN_SECRET_DECODE_FAILED",
|
|
234
|
+
"macOS Keychain returned a secret that is not valid base64-encoded UTF-8"
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return decoded;
|
|
238
|
+
}
|
|
239
|
+
#service(name) {
|
|
240
|
+
return `${this.#servicePrefix}.${assertSecretName(name)}`;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
function quoteSecurityInteractiveArgument(value) {
|
|
244
|
+
if (/[\u0000\r\n]/u.test(value)) {
|
|
245
|
+
throw new ByokKeysError(
|
|
246
|
+
"KEYCHAIN_ARGUMENT_INVALID",
|
|
247
|
+
"macOS Keychain arguments cannot contain null or newline characters"
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/pi-provider-projection.ts
|
|
254
|
+
var PI_PROJECTED_KEY_ENV = "PI_PROVIDER_API_KEY";
|
|
255
|
+
function piProjectionProviderId(profileProviderId) {
|
|
256
|
+
return `byok-sdk-${profileProviderId}`;
|
|
257
|
+
}
|
|
258
|
+
function buildPiProviderProjection(profile) {
|
|
259
|
+
const projectedProviderId = piProjectionProviderId(profile.provider_id);
|
|
260
|
+
return {
|
|
261
|
+
providers: {
|
|
262
|
+
[projectedProviderId]: {
|
|
263
|
+
baseUrl: profile.base_url,
|
|
264
|
+
api: profile.adapter === "anthropic" ? "anthropic-messages" : "openai-completions",
|
|
265
|
+
...profile.auth_mode === "none" ? {} : { apiKey: `$${PI_PROJECTED_KEY_ENV}` },
|
|
266
|
+
...profile.auth_mode === "bearer" ? { authHeader: true } : {},
|
|
267
|
+
models: [
|
|
268
|
+
{
|
|
269
|
+
id: profile.model,
|
|
270
|
+
name: profile.display_name
|
|
271
|
+
}
|
|
272
|
+
]
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function buildPiProviderArgs(profile, delegatedArgs) {
|
|
278
|
+
let modeCount = 0;
|
|
279
|
+
for (let index = 0; index < delegatedArgs.length; index += 1) {
|
|
280
|
+
const flag = delegatedArgs[index];
|
|
281
|
+
if (flag === "--no-tools") continue;
|
|
282
|
+
if (flag === "--mode") {
|
|
283
|
+
modeCount += 1;
|
|
284
|
+
const value = delegatedArgs[index + 1];
|
|
285
|
+
if (value !== "rpc") throw new Error("Pi launcher requires --mode rpc");
|
|
286
|
+
index += 1;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (flag === "--session" || flag === "--tools" || flag === "--exclude-tools") {
|
|
290
|
+
const value = delegatedArgs[index + 1];
|
|
291
|
+
if (!value || value.startsWith("--")) {
|
|
292
|
+
throw new Error(`${flag} requires a value`);
|
|
293
|
+
}
|
|
294
|
+
index += 1;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
throw new Error(`Pi launcher does not allow delegated argument ${flag ?? "<missing>"}`);
|
|
298
|
+
}
|
|
299
|
+
if (modeCount !== 1) throw new Error("Pi launcher requires exactly one --mode rpc");
|
|
300
|
+
return [
|
|
301
|
+
...delegatedArgs,
|
|
302
|
+
"--provider",
|
|
303
|
+
piProjectionProviderId(profile.provider_id),
|
|
304
|
+
"--model",
|
|
305
|
+
profile.model
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/url.ts
|
|
310
|
+
function normalizeProviderUrl(value) {
|
|
311
|
+
let url;
|
|
312
|
+
try {
|
|
313
|
+
url = new URL(value);
|
|
314
|
+
} catch {
|
|
315
|
+
throw new ByokKeysError(
|
|
316
|
+
"PROVIDER_URL_INVALID",
|
|
317
|
+
"Provider base URL must be absolute"
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
if (url.username || url.password || url.hash || url.search || url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname))) {
|
|
321
|
+
throw new ByokKeysError(
|
|
322
|
+
"PROVIDER_URL_INVALID",
|
|
323
|
+
"Provider URL requires HTTPS; HTTP is allowed only for localhost"
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
if (isPrivateNetworkLiteral(url.hostname) && !isLoopbackHost(url.hostname)) {
|
|
327
|
+
throw new ByokKeysError(
|
|
328
|
+
"PROVIDER_URL_INVALID",
|
|
329
|
+
"Private-network provider IPs are not allowed"
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
return url.toString().replace(/\/$/u, "");
|
|
333
|
+
}
|
|
334
|
+
function isLoopbackHost(hostname) {
|
|
335
|
+
const value = hostname.toLowerCase().replace(/^\[|\]$/gu, "");
|
|
336
|
+
return value === "localhost" || value === "127.0.0.1" || value === "::1";
|
|
337
|
+
}
|
|
338
|
+
function isPrivateNetworkLiteral(hostname) {
|
|
339
|
+
const value = hostname.toLowerCase().replace(/^\[|\]$/gu, "");
|
|
340
|
+
if (value.includes(":")) return true;
|
|
341
|
+
const parts = value.split(".").map(Number);
|
|
342
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
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;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/provider-profile.ts
|
|
349
|
+
var MODEL_PROVIDER_IDS = [
|
|
350
|
+
"openai",
|
|
351
|
+
"deepseek",
|
|
352
|
+
"anthropic",
|
|
353
|
+
"custom"
|
|
354
|
+
];
|
|
355
|
+
var PROVIDER_AUTH_MODES = ["bearer", "x_api_key", "none"];
|
|
356
|
+
var MODEL_PROVIDER_ADAPTERS = ["openai_compatible", "anthropic"];
|
|
357
|
+
function boundedString(field, maximumLength) {
|
|
358
|
+
return z.string().superRefine((value, ctx) => {
|
|
359
|
+
if (value.trim().length === 0 || value.length > maximumLength || /[\u0000\r\n]/u.test(value)) {
|
|
360
|
+
ctx.addIssue({
|
|
361
|
+
code: "custom",
|
|
362
|
+
message: `Provider ${field} is invalid`
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}).transform((value) => value.trim());
|
|
366
|
+
}
|
|
367
|
+
function isoTimestamp(field) {
|
|
368
|
+
return boundedString(field, 64).superRefine((value, ctx) => {
|
|
369
|
+
if (!Number.isFinite(Date.parse(value))) {
|
|
370
|
+
ctx.addIssue({
|
|
371
|
+
code: "custom",
|
|
372
|
+
message: `Provider ${field} must be an ISO timestamp`
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
var providerBaseUrl = z.string().superRefine((value, ctx) => {
|
|
378
|
+
try {
|
|
379
|
+
normalizeProviderUrl(value);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
ctx.addIssue({
|
|
382
|
+
code: "custom",
|
|
383
|
+
message: error instanceof Error ? error.message : "Provider base URL is invalid",
|
|
384
|
+
params: {
|
|
385
|
+
byokCode: error instanceof ByokKeysError ? error.code : "PROVIDER_URL_INVALID"
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}).transform((value) => normalizeProviderUrl(value));
|
|
390
|
+
var ModelProviderProfileSchema = z.object({
|
|
391
|
+
adapter: z.enum(MODEL_PROVIDER_ADAPTERS),
|
|
392
|
+
auth_mode: z.enum(PROVIDER_AUTH_MODES),
|
|
393
|
+
base_url: providerBaseUrl,
|
|
394
|
+
created_at: isoTimestamp("created_at"),
|
|
395
|
+
display_name: boundedString("display_name", 100),
|
|
396
|
+
enabled: z.boolean(),
|
|
397
|
+
kind: z.literal("model"),
|
|
398
|
+
model: boundedString("model", 160),
|
|
399
|
+
provider_id: z.enum(MODEL_PROVIDER_IDS),
|
|
400
|
+
updated_at: isoTimestamp("updated_at")
|
|
401
|
+
}).superRefine((profile, ctx) => {
|
|
402
|
+
if (profile.adapter === "anthropic" && profile.auth_mode !== "x_api_key") {
|
|
403
|
+
ctx.addIssue({
|
|
404
|
+
code: "custom",
|
|
405
|
+
message: "Anthropic requires x_api_key authentication",
|
|
406
|
+
path: ["auth_mode"]
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
if (profile.adapter === "openai_compatible" && profile.auth_mode === "x_api_key") {
|
|
410
|
+
ctx.addIssue({
|
|
411
|
+
code: "custom",
|
|
412
|
+
message: "OpenAI-compatible providers support bearer or no authentication",
|
|
413
|
+
path: ["auth_mode"]
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
if (Date.parse(profile.updated_at) < Date.parse(profile.created_at)) {
|
|
417
|
+
ctx.addIssue({
|
|
418
|
+
code: "custom",
|
|
419
|
+
message: "Provider updated_at cannot precede created_at",
|
|
420
|
+
path: ["updated_at"]
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
function parseModelProviderProfile(value) {
|
|
425
|
+
const result = ModelProviderProfileSchema.safeParse(value);
|
|
426
|
+
if (result.success) return result.data;
|
|
427
|
+
const issue = result.error.issues[0];
|
|
428
|
+
const params = issue?.params;
|
|
429
|
+
throw new ByokKeysError(
|
|
430
|
+
params?.byokCode ?? "PROVIDER_PROFILE_INVALID",
|
|
431
|
+
issue?.message ?? "Provider profile is invalid",
|
|
432
|
+
{ cause: result.error }
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/pi-provider-launcher-core.ts
|
|
437
|
+
var PI_CHILD_BASE_ENV_NAMES = [
|
|
438
|
+
"PATH",
|
|
439
|
+
"HOME",
|
|
440
|
+
"USERPROFILE",
|
|
441
|
+
"TMPDIR",
|
|
442
|
+
"TEMP",
|
|
443
|
+
"TMP",
|
|
444
|
+
"LANG",
|
|
445
|
+
"TZ",
|
|
446
|
+
"TERM",
|
|
447
|
+
"SHELL",
|
|
448
|
+
"HTTP_PROXY",
|
|
449
|
+
"HTTPS_PROXY",
|
|
450
|
+
"NO_PROXY",
|
|
451
|
+
"ALL_PROXY",
|
|
452
|
+
"http_proxy",
|
|
453
|
+
"https_proxy",
|
|
454
|
+
"no_proxy",
|
|
455
|
+
"all_proxy"
|
|
456
|
+
];
|
|
457
|
+
var PI_CHILD_WINDOWS_ENV_NAMES = [
|
|
458
|
+
"SystemRoot",
|
|
459
|
+
"COMSPEC",
|
|
460
|
+
"PATHEXT",
|
|
461
|
+
"windir",
|
|
462
|
+
"SYSTEMDRIVE",
|
|
463
|
+
"PROGRAMFILES",
|
|
464
|
+
"APPDATA",
|
|
465
|
+
"LOCALAPPDATA"
|
|
466
|
+
];
|
|
467
|
+
function parsePiProviderLauncherOptions(args) {
|
|
468
|
+
const separator = args.indexOf("--");
|
|
469
|
+
if (separator < 0) throw new Error("launcher arguments must end with -- <pi args>");
|
|
470
|
+
const ownArgs = args.slice(0, separator);
|
|
471
|
+
const piArgs = args.slice(separator + 1);
|
|
472
|
+
if (piArgs.length === 0) throw new Error("launcher requires Pi arguments after --");
|
|
473
|
+
const allowedFlags = /* @__PURE__ */ new Set([
|
|
474
|
+
"--pi-bin",
|
|
475
|
+
"--profile-db",
|
|
476
|
+
"--provider",
|
|
477
|
+
"--model",
|
|
478
|
+
"--session-dir",
|
|
479
|
+
"--secret-service-prefix"
|
|
480
|
+
]);
|
|
481
|
+
const values = /* @__PURE__ */ new Map();
|
|
482
|
+
for (let index = 0; index < ownArgs.length; index += 2) {
|
|
483
|
+
const flag = ownArgs[index];
|
|
484
|
+
const value = ownArgs[index + 1];
|
|
485
|
+
if (!flag || !allowedFlags.has(flag)) {
|
|
486
|
+
throw new Error(`unknown launcher argument ${flag ?? "<missing>"}`);
|
|
487
|
+
}
|
|
488
|
+
if (values.has(flag)) throw new Error(`launcher argument ${flag} may only be provided once`);
|
|
489
|
+
if (!value || value.startsWith("--")) throw new Error(`${flag} requires a value`);
|
|
490
|
+
if (/[\u0000\r\n]/u.test(value)) {
|
|
491
|
+
throw new Error(`${flag} must be a single-line value`);
|
|
492
|
+
}
|
|
493
|
+
values.set(flag, value);
|
|
494
|
+
}
|
|
495
|
+
const required = (flag) => {
|
|
496
|
+
const value = values.get(flag);
|
|
497
|
+
if (value === void 0) throw new Error(`${flag} requires a value`);
|
|
498
|
+
return value;
|
|
499
|
+
};
|
|
500
|
+
const rawProviderId = required("--provider");
|
|
501
|
+
if (!MODEL_PROVIDER_IDS.includes(rawProviderId)) {
|
|
502
|
+
throw new Error(`provider ${rawProviderId} is not configured by @byok-sdk/keys`);
|
|
503
|
+
}
|
|
504
|
+
const modelId = required("--model");
|
|
505
|
+
if (modelId.length > 160) throw new Error("--model exceeds 160 characters");
|
|
506
|
+
const profileDbPath = required("--profile-db");
|
|
507
|
+
const sessionDir = required("--session-dir");
|
|
508
|
+
if (!path.isAbsolute(profileDbPath) || !path.isAbsolute(sessionDir)) {
|
|
509
|
+
throw new Error("launcher profile database and session directory must be absolute paths");
|
|
510
|
+
}
|
|
511
|
+
const secretServicePrefix = values.get("--secret-service-prefix");
|
|
512
|
+
return {
|
|
513
|
+
piBin: required("--pi-bin"),
|
|
514
|
+
profileDbPath,
|
|
515
|
+
providerId: rawProviderId,
|
|
516
|
+
modelId,
|
|
517
|
+
sessionDir,
|
|
518
|
+
...secretServicePrefix ? { secretServicePrefix } : {},
|
|
519
|
+
piArgs
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
async function resolvePiProviderSecret(profile, createStore) {
|
|
523
|
+
if (profile.auth_mode === "none") return void 0;
|
|
524
|
+
const secrets = createStore();
|
|
525
|
+
if (!await secrets.available()) {
|
|
526
|
+
throw new ByokKeysError(
|
|
527
|
+
"KEYCHAIN_UNAVAILABLE",
|
|
528
|
+
`${secrets.providerLabel} is unavailable`
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
const secret = await secrets.get(modelProviderSecretName(profile.provider_id));
|
|
532
|
+
if (!secret) {
|
|
533
|
+
throw new ByokKeysError(
|
|
534
|
+
"PROVIDER_SECRET_MISSING",
|
|
535
|
+
`${profile.provider_id} provider requires a secret in ${secrets.providerLabel}`
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
return secret;
|
|
539
|
+
}
|
|
540
|
+
function buildPiProviderChildEnvironment(options) {
|
|
541
|
+
const platform = options.platform ?? process.platform;
|
|
542
|
+
const exactNames = new Set([
|
|
543
|
+
...PI_CHILD_BASE_ENV_NAMES,
|
|
544
|
+
...platform === "win32" ? PI_CHILD_WINDOWS_ENV_NAMES : []
|
|
545
|
+
].map((name) => platform === "win32" ? name.toUpperCase() : name));
|
|
546
|
+
const result = {};
|
|
547
|
+
for (const [name, value] of Object.entries(options.ambient)) {
|
|
548
|
+
if (value === void 0) continue;
|
|
549
|
+
const platformName = platform === "win32" ? name.toUpperCase() : name;
|
|
550
|
+
const isExact = exactNames.has(platformName);
|
|
551
|
+
const isPrefixed = platform === "win32" ? platformName.startsWith("LC_") || platformName.startsWith("XDG_") : name.startsWith("LC_") || name.startsWith("XDG_");
|
|
552
|
+
if (isExact || isPrefixed) result[name] = value;
|
|
553
|
+
}
|
|
554
|
+
result.PI_CODING_AGENT_DIR = options.projectionDir;
|
|
555
|
+
result.PI_CODING_AGENT_SESSION_DIR = options.sessionDir;
|
|
556
|
+
if (options.secret !== void 0) {
|
|
557
|
+
result[PI_PROJECTED_KEY_ENV] = options.secret;
|
|
558
|
+
}
|
|
559
|
+
return result;
|
|
560
|
+
}
|
|
561
|
+
async function ensurePiSessionDirectory(sessionDir) {
|
|
562
|
+
const firstCreated = await promises.mkdir(sessionDir, { recursive: true, mode: 448 });
|
|
563
|
+
if (firstCreated !== void 0) {
|
|
564
|
+
await promises.chmod(sessionDir, 448).catch(() => {
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
const stat = await promises.stat(sessionDir);
|
|
568
|
+
if (!stat.isDirectory()) {
|
|
569
|
+
throw new Error("Pi session path must be a directory");
|
|
570
|
+
}
|
|
571
|
+
if (process.platform !== "win32" && (stat.mode & 63) !== 0) {
|
|
572
|
+
throw new Error(
|
|
573
|
+
"existing Pi session directory must already be owner-only; refusing to change host-owned permissions"
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/profile-store.ts
|
|
579
|
+
function providerNotConfigured(providerId) {
|
|
580
|
+
return new ByokKeysError(
|
|
581
|
+
"PROVIDER_NOT_CONFIGURED",
|
|
582
|
+
`${providerId} model provider is not configured`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
function closeSqliteDatabaseAfterInitializationFailure(database, initializationError, message, close = (handle) => handle.close()) {
|
|
586
|
+
try {
|
|
587
|
+
close(database);
|
|
588
|
+
} catch (closeError) {
|
|
589
|
+
throw new AggregateError([initializationError, closeError], message);
|
|
590
|
+
}
|
|
591
|
+
throw initializationError;
|
|
592
|
+
}
|
|
593
|
+
var SECURE_DIR_MODE = 448;
|
|
594
|
+
var SECURE_FILE_MODE = 384;
|
|
595
|
+
var DEFAULT_BUSY_TIMEOUT_MS = 5e3;
|
|
596
|
+
function loadSqliteModule() {
|
|
597
|
+
try {
|
|
598
|
+
return createRequire(import.meta.url)("node:sqlite");
|
|
599
|
+
} catch (error) {
|
|
600
|
+
throw new ByokKeysError(
|
|
601
|
+
"PROVIDER_STORE_UNAVAILABLE",
|
|
602
|
+
"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.",
|
|
603
|
+
{ cause: error }
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function openSqliteDatabase(path3, options, faults) {
|
|
608
|
+
const { DatabaseSync } = loadSqliteModule();
|
|
609
|
+
const readOnly = options?.readOnly === true;
|
|
610
|
+
if (path3 !== ":memory:" && !readOnly) {
|
|
611
|
+
mkdirSync(dirname(path3), { mode: SECURE_DIR_MODE, recursive: true });
|
|
612
|
+
}
|
|
613
|
+
const database = new DatabaseSync(path3, {
|
|
614
|
+
timeout: DEFAULT_BUSY_TIMEOUT_MS,
|
|
615
|
+
...options
|
|
616
|
+
});
|
|
617
|
+
try {
|
|
618
|
+
faults?.onStep?.("after-open");
|
|
619
|
+
if (path3 !== ":memory:" && !readOnly) {
|
|
620
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
621
|
+
faults?.onStep?.("after-wal");
|
|
622
|
+
database.exec("PRAGMA synchronous = FULL");
|
|
623
|
+
faults?.onStep?.("after-synchronous");
|
|
624
|
+
}
|
|
625
|
+
return database;
|
|
626
|
+
} catch (error) {
|
|
627
|
+
closeSqliteDatabaseAfterInitializationFailure(
|
|
628
|
+
database,
|
|
629
|
+
error,
|
|
630
|
+
"provider profile SQLite open initialization failed and its native handle could not be closed",
|
|
631
|
+
faults?.close
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
function secureSqliteFilePermissions(databasePath) {
|
|
636
|
+
if (databasePath === ":memory:") return;
|
|
637
|
+
for (const candidate of [
|
|
638
|
+
databasePath,
|
|
639
|
+
`${databasePath}-wal`,
|
|
640
|
+
`${databasePath}-shm`
|
|
641
|
+
]) {
|
|
642
|
+
if (existsSync(candidate)) {
|
|
643
|
+
chmodSync(candidate, SECURE_FILE_MODE);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// src/sqlite-profile-store.ts
|
|
649
|
+
var SCHEMA = `
|
|
650
|
+
CREATE TABLE IF NOT EXISTS provider_profile (
|
|
651
|
+
provider_id TEXT PRIMARY KEY CHECK (provider_id IN ('openai', 'deepseek', 'anthropic', 'custom')),
|
|
652
|
+
kind TEXT NOT NULL CHECK (kind = 'model'),
|
|
653
|
+
adapter TEXT NOT NULL CHECK (adapter IN ('openai_compatible', 'anthropic')),
|
|
654
|
+
display_name TEXT NOT NULL,
|
|
655
|
+
base_url TEXT NOT NULL,
|
|
656
|
+
auth_mode TEXT NOT NULL CHECK (auth_mode IN ('bearer', 'x_api_key', 'none')),
|
|
657
|
+
model TEXT NOT NULL,
|
|
658
|
+
enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
|
|
659
|
+
created_at TEXT NOT NULL,
|
|
660
|
+
updated_at TEXT NOT NULL
|
|
661
|
+
);
|
|
662
|
+
`;
|
|
663
|
+
var ENABLED_INDEX = `
|
|
664
|
+
CREATE UNIQUE INDEX IF NOT EXISTS provider_profile_one_enabled
|
|
665
|
+
ON provider_profile(kind)
|
|
666
|
+
WHERE enabled = 1;
|
|
667
|
+
`;
|
|
668
|
+
var SqliteProviderProfileStore = class {
|
|
669
|
+
#database;
|
|
670
|
+
#closed = false;
|
|
671
|
+
constructor(options) {
|
|
672
|
+
this.#database = openSqliteDatabase(options.path, {
|
|
673
|
+
readOnly: options.readOnly ?? false
|
|
674
|
+
});
|
|
675
|
+
if (!options.readOnly) {
|
|
676
|
+
try {
|
|
677
|
+
this.#database.exec(SCHEMA);
|
|
678
|
+
this.#database.exec(ENABLED_INDEX);
|
|
679
|
+
secureSqliteFilePermissions(options.path);
|
|
680
|
+
} catch (error) {
|
|
681
|
+
closeSqliteDatabaseAfterInitializationFailure(
|
|
682
|
+
this.#database,
|
|
683
|
+
error,
|
|
684
|
+
"SqliteProviderProfileStore initialization failed and its native handle could not be closed"
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Idempotent, as {@link ProviderProfileStore.close} requires: `node:sqlite`
|
|
691
|
+
* throws "database is not open" on a second `close()`, and a store is
|
|
692
|
+
* routinely closed both by the code that finished with it and by a test's
|
|
693
|
+
* teardown.
|
|
694
|
+
*/
|
|
695
|
+
async close() {
|
|
696
|
+
if (this.#closed) return;
|
|
697
|
+
this.#closed = true;
|
|
698
|
+
this.#database.close();
|
|
699
|
+
}
|
|
700
|
+
async delete(providerId) {
|
|
701
|
+
const result = this.#database.prepare("DELETE FROM provider_profile WHERE provider_id = ?").run(providerId);
|
|
702
|
+
return Number(result.changes) === 1;
|
|
703
|
+
}
|
|
704
|
+
async get(providerId) {
|
|
705
|
+
const row = this.#database.prepare("SELECT * FROM provider_profile WHERE provider_id = ?").get(providerId);
|
|
706
|
+
return row === void 0 ? void 0 : parseRow(row);
|
|
707
|
+
}
|
|
708
|
+
async getEnabled() {
|
|
709
|
+
const row = this.#database.prepare("SELECT * FROM provider_profile WHERE enabled = 1").get();
|
|
710
|
+
return row === void 0 ? void 0 : parseRow(row);
|
|
711
|
+
}
|
|
712
|
+
async list() {
|
|
713
|
+
const rows = this.#database.prepare("SELECT * FROM provider_profile ORDER BY provider_id ASC").all();
|
|
714
|
+
return rows.map(parseRow);
|
|
715
|
+
}
|
|
716
|
+
async save(profile) {
|
|
717
|
+
const existing = await this.get(profile.provider_id);
|
|
718
|
+
const validated = parseModelProviderProfile({
|
|
719
|
+
...profile,
|
|
720
|
+
created_at: existing?.created_at ?? profile.created_at
|
|
721
|
+
});
|
|
722
|
+
this.#transaction(() => {
|
|
723
|
+
if (validated.enabled) {
|
|
724
|
+
this.#database.prepare(
|
|
725
|
+
"UPDATE provider_profile SET enabled = 0 WHERE provider_id <> ?"
|
|
726
|
+
).run(validated.provider_id);
|
|
727
|
+
}
|
|
728
|
+
this.#database.prepare(
|
|
729
|
+
`INSERT INTO provider_profile (
|
|
730
|
+
provider_id, kind, adapter, display_name, base_url,
|
|
731
|
+
auth_mode, model, enabled, created_at, updated_at
|
|
732
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
733
|
+
ON CONFLICT(provider_id) DO UPDATE SET
|
|
734
|
+
adapter = excluded.adapter,
|
|
735
|
+
display_name = excluded.display_name,
|
|
736
|
+
base_url = excluded.base_url,
|
|
737
|
+
auth_mode = excluded.auth_mode,
|
|
738
|
+
model = excluded.model,
|
|
739
|
+
enabled = excluded.enabled,
|
|
740
|
+
updated_at = excluded.updated_at`
|
|
741
|
+
).run(
|
|
742
|
+
validated.provider_id,
|
|
743
|
+
validated.kind,
|
|
744
|
+
validated.adapter,
|
|
745
|
+
validated.display_name,
|
|
746
|
+
validated.base_url,
|
|
747
|
+
validated.auth_mode,
|
|
748
|
+
validated.model,
|
|
749
|
+
validated.enabled ? 1 : 0,
|
|
750
|
+
validated.created_at,
|
|
751
|
+
validated.updated_at
|
|
752
|
+
);
|
|
753
|
+
});
|
|
754
|
+
return await this.get(validated.provider_id);
|
|
755
|
+
}
|
|
756
|
+
async setEnabled(providerId) {
|
|
757
|
+
const existing = await this.get(providerId);
|
|
758
|
+
if (existing === void 0) throw providerNotConfigured(providerId);
|
|
759
|
+
return this.save({ ...existing, enabled: true });
|
|
760
|
+
}
|
|
761
|
+
/** `BEGIN IMMEDIATE` / `COMMIT` / `ROLLBACK`, per `providers.ts:1252-1263`. */
|
|
762
|
+
#transaction(body) {
|
|
763
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
764
|
+
try {
|
|
765
|
+
body();
|
|
766
|
+
this.#database.exec("COMMIT");
|
|
767
|
+
} catch (error) {
|
|
768
|
+
this.#database.exec("ROLLBACK");
|
|
769
|
+
throw error;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
function parseRow(row) {
|
|
774
|
+
return parseModelProviderProfile({
|
|
775
|
+
...row,
|
|
776
|
+
enabled: row.enabled === 1
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/windows-credential-manager.ts
|
|
781
|
+
var CREDENTIAL_NOT_FOUND = 44;
|
|
782
|
+
var WINDOWS_CREDENTIAL_MANAGER_SCRIPT_BASE64 = Buffer.from(
|
|
783
|
+
String.raw`
|
|
784
|
+
Add-Type -TypeDefinition @"
|
|
785
|
+
using System;
|
|
786
|
+
using System.ComponentModel;
|
|
787
|
+
using System.Runtime.InteropServices;
|
|
788
|
+
using System.Runtime.InteropServices.ComTypes;
|
|
789
|
+
|
|
790
|
+
namespace Byok {
|
|
791
|
+
public static class CredentialManager {
|
|
792
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
793
|
+
private struct Credential {
|
|
794
|
+
public UInt32 Flags;
|
|
795
|
+
public UInt32 Type;
|
|
796
|
+
[MarshalAs(UnmanagedType.LPWStr)] public string TargetName;
|
|
797
|
+
[MarshalAs(UnmanagedType.LPWStr)] public string Comment;
|
|
798
|
+
public FILETIME LastWritten;
|
|
799
|
+
public UInt32 CredentialBlobSize;
|
|
800
|
+
public IntPtr CredentialBlob;
|
|
801
|
+
public UInt32 Persist;
|
|
802
|
+
public UInt32 AttributeCount;
|
|
803
|
+
public IntPtr Attributes;
|
|
804
|
+
[MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias;
|
|
805
|
+
[MarshalAs(UnmanagedType.LPWStr)] public string UserName;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
[DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
809
|
+
private static extern bool CredWrite([In] ref Credential credential, UInt32 flags);
|
|
810
|
+
|
|
811
|
+
[DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
812
|
+
private static extern bool CredRead(string target, UInt32 type, UInt32 flags, out IntPtr credential);
|
|
813
|
+
|
|
814
|
+
[DllImport("Advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
815
|
+
private static extern bool CredDelete(string target, UInt32 type, UInt32 flags);
|
|
816
|
+
|
|
817
|
+
[DllImport("Advapi32.dll", SetLastError = false)]
|
|
818
|
+
private static extern void CredFree(IntPtr buffer);
|
|
819
|
+
|
|
820
|
+
public static void Write(string target, string username, byte[] secret) {
|
|
821
|
+
IntPtr blob = IntPtr.Zero;
|
|
822
|
+
try {
|
|
823
|
+
blob = Marshal.AllocHGlobal(secret.Length);
|
|
824
|
+
Marshal.Copy(secret, 0, blob, secret.Length);
|
|
825
|
+
Credential credential = new Credential {
|
|
826
|
+
Type = 1,
|
|
827
|
+
TargetName = target,
|
|
828
|
+
CredentialBlobSize = (UInt32)secret.Length,
|
|
829
|
+
CredentialBlob = blob,
|
|
830
|
+
Persist = 2,
|
|
831
|
+
UserName = username
|
|
832
|
+
};
|
|
833
|
+
if (!CredWrite(ref credential, 0)) {
|
|
834
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
835
|
+
}
|
|
836
|
+
} finally {
|
|
837
|
+
if (blob != IntPtr.Zero) {
|
|
838
|
+
Marshal.Copy(new byte[secret.Length], 0, blob, secret.Length);
|
|
839
|
+
Marshal.FreeHGlobal(blob);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
public static byte[] Read(string target) {
|
|
845
|
+
IntPtr pointer;
|
|
846
|
+
if (!CredRead(target, 1, 0, out pointer)) {
|
|
847
|
+
int error = Marshal.GetLastWin32Error();
|
|
848
|
+
if (error == 1168) return null;
|
|
849
|
+
throw new Win32Exception(error);
|
|
850
|
+
}
|
|
851
|
+
try {
|
|
852
|
+
Credential credential = (Credential)Marshal.PtrToStructure(pointer, typeof(Credential));
|
|
853
|
+
byte[] secret = new byte[credential.CredentialBlobSize];
|
|
854
|
+
if (secret.Length > 0) Marshal.Copy(credential.CredentialBlob, secret, 0, secret.Length);
|
|
855
|
+
return secret;
|
|
856
|
+
} finally {
|
|
857
|
+
CredFree(pointer);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
public static bool Delete(string target) {
|
|
862
|
+
if (CredDelete(target, 1, 0)) return true;
|
|
863
|
+
int error = Marshal.GetLastWin32Error();
|
|
864
|
+
if (error == 1168) return false;
|
|
865
|
+
throw new Win32Exception(error);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
"@
|
|
870
|
+
|
|
871
|
+
try {
|
|
872
|
+
$request = ([Console]::In.ReadToEnd() | ConvertFrom-Json)
|
|
873
|
+
if ($request.operation -eq "set") {
|
|
874
|
+
[Byok.CredentialManager]::Write(
|
|
875
|
+
[string]$request.target,
|
|
876
|
+
[string]$request.username,
|
|
877
|
+
[Convert]::FromBase64String([string]$request.secret_base64)
|
|
878
|
+
)
|
|
879
|
+
exit 0
|
|
880
|
+
}
|
|
881
|
+
if ($request.operation -eq "get") {
|
|
882
|
+
$secret = [Byok.CredentialManager]::Read([string]$request.target)
|
|
883
|
+
if ($null -eq $secret) { exit 44 }
|
|
884
|
+
[Console]::Out.Write([Convert]::ToBase64String($secret))
|
|
885
|
+
exit 0
|
|
886
|
+
}
|
|
887
|
+
if ($request.operation -eq "delete") {
|
|
888
|
+
if ([Byok.CredentialManager]::Delete([string]$request.target)) { exit 0 }
|
|
889
|
+
exit 44
|
|
890
|
+
}
|
|
891
|
+
exit 2
|
|
892
|
+
} catch {
|
|
893
|
+
[Console]::Error.Write("credential operation failed")
|
|
894
|
+
exit 1
|
|
895
|
+
}
|
|
896
|
+
`,
|
|
897
|
+
"utf16le"
|
|
898
|
+
).toString("base64");
|
|
899
|
+
var WindowsCredentialManagerSecretStore = class _WindowsCredentialManagerSecretStore {
|
|
900
|
+
providerLabel = "Windows Credential Manager";
|
|
901
|
+
#account;
|
|
902
|
+
#commandRunner;
|
|
903
|
+
#platform;
|
|
904
|
+
#servicePrefix;
|
|
905
|
+
constructor(options = {}) {
|
|
906
|
+
this.#account = options.account ?? "local-device";
|
|
907
|
+
this.#commandRunner = options.commandRunner ?? runCommand;
|
|
908
|
+
this.#platform = options.platform ?? process.platform;
|
|
909
|
+
this.#servicePrefix = options.servicePrefix ?? DEFAULT_SECRET_SERVICE_PREFIX;
|
|
910
|
+
}
|
|
911
|
+
async available() {
|
|
912
|
+
if (this.#platform !== "win32") return false;
|
|
913
|
+
const result = await this.#commandRunner("powershell.exe", [
|
|
914
|
+
"-NoLogo",
|
|
915
|
+
"-NoProfile",
|
|
916
|
+
"-NonInteractive",
|
|
917
|
+
"-Command",
|
|
918
|
+
"$PSVersionTable.PSVersion.Major"
|
|
919
|
+
]);
|
|
920
|
+
return result.exitCode === 0;
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Delete, then read back. The source verifies rather than trusting the
|
|
924
|
+
* delete's exit code (`index.ts:651-679`): a credential that survives a
|
|
925
|
+
* "successful" delete is a security failure, so it is reported as one instead
|
|
926
|
+
* of being returned as `true`.
|
|
927
|
+
*/
|
|
928
|
+
async delete(name) {
|
|
929
|
+
const target = this.#service(name);
|
|
930
|
+
this.#assertWindows();
|
|
931
|
+
const result = await this.#invoke({
|
|
932
|
+
operation: "delete",
|
|
933
|
+
target,
|
|
934
|
+
username: this.#account
|
|
935
|
+
});
|
|
936
|
+
if (result.exitCode === CREDENTIAL_NOT_FOUND) return false;
|
|
937
|
+
if (result.exitCode !== 0) {
|
|
938
|
+
throw new ByokKeysError(
|
|
939
|
+
"CREDENTIAL_MANAGER_DELETE_FAILED",
|
|
940
|
+
"Windows Credential Manager could not delete the requested secret"
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
const verification = await this.#invoke({
|
|
944
|
+
operation: "get",
|
|
945
|
+
target,
|
|
946
|
+
username: this.#account
|
|
947
|
+
});
|
|
948
|
+
if (verification.exitCode === CREDENTIAL_NOT_FOUND) return true;
|
|
949
|
+
if (verification.exitCode !== 0) {
|
|
950
|
+
throw new ByokKeysError(
|
|
951
|
+
"CREDENTIAL_MANAGER_DELETE_FAILED",
|
|
952
|
+
"Windows Credential Manager could not verify secret deletion"
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
throw new ByokKeysError(
|
|
956
|
+
"CREDENTIAL_MANAGER_DELETE_FAILED",
|
|
957
|
+
"Windows Credential Manager reported deletion but the secret remains"
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
async get(name) {
|
|
961
|
+
const target = this.#service(name);
|
|
962
|
+
this.#assertWindows();
|
|
963
|
+
const result = await this.#invoke({
|
|
964
|
+
operation: "get",
|
|
965
|
+
target,
|
|
966
|
+
username: this.#account
|
|
967
|
+
});
|
|
968
|
+
if (result.exitCode === CREDENTIAL_NOT_FOUND) return void 0;
|
|
969
|
+
if (result.exitCode !== 0) {
|
|
970
|
+
throw new ByokKeysError(
|
|
971
|
+
"CREDENTIAL_MANAGER_READ_FAILED",
|
|
972
|
+
"Windows Credential Manager could not read the requested secret"
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
const secret = decodeStrictBase64Utf8(result.stdout.trim());
|
|
976
|
+
if (secret === void 0 || secret.length === 0) {
|
|
977
|
+
throw new ByokKeysError(
|
|
978
|
+
"CREDENTIAL_MANAGER_READ_FAILED",
|
|
979
|
+
"Windows Credential Manager returned an invalid secret"
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
return secret;
|
|
983
|
+
}
|
|
984
|
+
async has(name) {
|
|
985
|
+
return await this.get(name) !== void 0;
|
|
986
|
+
}
|
|
987
|
+
scope(namespace) {
|
|
988
|
+
return new _WindowsCredentialManagerSecretStore({
|
|
989
|
+
account: this.#account,
|
|
990
|
+
commandRunner: this.#commandRunner,
|
|
991
|
+
platform: this.#platform,
|
|
992
|
+
servicePrefix: `${this.#servicePrefix}.scope.${assertSecretNamespace(namespace)}`
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
async set(name, secret) {
|
|
996
|
+
const target = this.#service(name);
|
|
997
|
+
this.#assertWindows();
|
|
998
|
+
assertWindowsCredentialSecret(secret);
|
|
999
|
+
const result = await this.#invoke({
|
|
1000
|
+
operation: "set",
|
|
1001
|
+
secret_base64: Buffer.from(secret, "utf8").toString("base64"),
|
|
1002
|
+
target,
|
|
1003
|
+
username: this.#account
|
|
1004
|
+
});
|
|
1005
|
+
if (result.exitCode !== 0) {
|
|
1006
|
+
throw new ByokKeysError(
|
|
1007
|
+
"CREDENTIAL_MANAGER_WRITE_FAILED",
|
|
1008
|
+
"Windows Credential Manager could not store the requested secret"
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
#assertWindows() {
|
|
1013
|
+
if (this.#platform !== "win32") {
|
|
1014
|
+
throw new ByokKeysError(
|
|
1015
|
+
"CREDENTIAL_MANAGER_UNAVAILABLE",
|
|
1016
|
+
"Windows Credential Manager is required; plaintext fallback is disabled"
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
async #invoke(request) {
|
|
1021
|
+
return this.#commandRunner(
|
|
1022
|
+
"powershell.exe",
|
|
1023
|
+
[
|
|
1024
|
+
"-NoLogo",
|
|
1025
|
+
"-NoProfile",
|
|
1026
|
+
"-NonInteractive",
|
|
1027
|
+
"-EncodedCommand",
|
|
1028
|
+
WINDOWS_CREDENTIAL_MANAGER_SCRIPT_BASE64
|
|
1029
|
+
],
|
|
1030
|
+
JSON.stringify(request)
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
#service(name) {
|
|
1034
|
+
return `${this.#servicePrefix}.${assertSecretName(name)}`;
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
function assertWindowsCredentialSecret(secret) {
|
|
1038
|
+
const bytes = Buffer.byteLength(secret, "utf8");
|
|
1039
|
+
if (secret.length === 0 || bytes > 2560 || /[\u0000\r\n]/u.test(secret)) {
|
|
1040
|
+
throw new ByokKeysError(
|
|
1041
|
+
"CREDENTIAL_MANAGER_SECRET_INVALID",
|
|
1042
|
+
"Secret must contain 1 to 2560 UTF-8 bytes without newline characters"
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// src/bin/pi-provider-launcher.ts
|
|
1048
|
+
function createSecretStore(servicePrefix) {
|
|
1049
|
+
switch (process.platform) {
|
|
1050
|
+
case "darwin":
|
|
1051
|
+
return new MacOsKeychainSecretStore({ servicePrefix });
|
|
1052
|
+
case "win32":
|
|
1053
|
+
return new WindowsCredentialManagerSecretStore({ servicePrefix });
|
|
1054
|
+
default:
|
|
1055
|
+
throw new ByokKeysError(
|
|
1056
|
+
"KEYCHAIN_UNAVAILABLE",
|
|
1057
|
+
`Pi BYOK credential launcher has no plaintext fallback on ${process.platform}`
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
async function run(options) {
|
|
1062
|
+
const profiles = new SqliteProviderProfileStore({
|
|
1063
|
+
path: options.profileDbPath,
|
|
1064
|
+
readOnly: true
|
|
1065
|
+
});
|
|
1066
|
+
let projectionDir;
|
|
1067
|
+
try {
|
|
1068
|
+
const profile = await profiles.get(options.providerId);
|
|
1069
|
+
if (profile === void 0) {
|
|
1070
|
+
throw new Error(`provider ${options.providerId} is not configured`);
|
|
1071
|
+
}
|
|
1072
|
+
if (profile.model !== options.modelId) {
|
|
1073
|
+
throw new Error(
|
|
1074
|
+
`selected model ${options.modelId} does not match configured provider model ${profile.model}`
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
const secret = await resolvePiProviderSecret(
|
|
1078
|
+
profile,
|
|
1079
|
+
() => createSecretStore(options.secretServicePrefix)
|
|
1080
|
+
);
|
|
1081
|
+
projectionDir = await promises.mkdtemp(path.join(os.tmpdir(), "byok-pi-provider-"));
|
|
1082
|
+
await promises.chmod(projectionDir, 448).catch(() => {
|
|
1083
|
+
});
|
|
1084
|
+
await ensurePiSessionDirectory(options.sessionDir);
|
|
1085
|
+
await promises.writeFile(
|
|
1086
|
+
path.join(projectionDir, "models.json"),
|
|
1087
|
+
`${JSON.stringify(buildPiProviderProjection(profile))}
|
|
1088
|
+
`,
|
|
1089
|
+
{ mode: 384 }
|
|
1090
|
+
);
|
|
1091
|
+
const child = spawn(options.piBin, buildPiProviderArgs(profile, options.piArgs), {
|
|
1092
|
+
env: buildPiProviderChildEnvironment({
|
|
1093
|
+
ambient: process.env,
|
|
1094
|
+
projectionDir,
|
|
1095
|
+
sessionDir: options.sessionDir,
|
|
1096
|
+
secret
|
|
1097
|
+
}),
|
|
1098
|
+
stdio: "inherit"
|
|
1099
|
+
});
|
|
1100
|
+
const forward = (signal) => {
|
|
1101
|
+
if (!child.killed) child.kill(signal);
|
|
1102
|
+
};
|
|
1103
|
+
const onSigint = () => forward("SIGINT");
|
|
1104
|
+
const onSigterm = () => forward("SIGTERM");
|
|
1105
|
+
process.on("SIGINT", onSigint);
|
|
1106
|
+
process.on("SIGTERM", onSigterm);
|
|
1107
|
+
try {
|
|
1108
|
+
return await new Promise((resolve, reject) => {
|
|
1109
|
+
child.once("error", reject);
|
|
1110
|
+
child.once("close", (code, signal) => {
|
|
1111
|
+
resolve(code ?? (signal ? 1 : 0));
|
|
1112
|
+
});
|
|
1113
|
+
});
|
|
1114
|
+
} finally {
|
|
1115
|
+
process.off("SIGINT", onSigint);
|
|
1116
|
+
process.off("SIGTERM", onSigterm);
|
|
1117
|
+
}
|
|
1118
|
+
} finally {
|
|
1119
|
+
await profiles.close();
|
|
1120
|
+
if (projectionDir) {
|
|
1121
|
+
await promises.rm(projectionDir, { recursive: true, force: true });
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
async function main() {
|
|
1126
|
+
try {
|
|
1127
|
+
process.exitCode = await run(parsePiProviderLauncherOptions(process.argv.slice(2)));
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1130
|
+
process.stderr.write(`pi provider launcher: ${message}
|
|
1131
|
+
`);
|
|
1132
|
+
process.exitCode = 1;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
void main();
|
|
1136
|
+
//# sourceMappingURL=pi-provider-launcher.js.map
|
|
1137
|
+
//# sourceMappingURL=pi-provider-launcher.js.map
|