@byok-sdk/keys 0.3.9 → 0.4.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.
@@ -4,6 +4,7 @@ import { promises, mkdirSync, existsSync, chmodSync } from 'fs';
4
4
  import os from 'os';
5
5
  import path2, { dirname } from 'path';
6
6
  import { z } from 'zod';
7
+ import { createHash } from 'crypto';
7
8
  import { createRequire } from 'module';
8
9
 
9
10
  // src/errors.ts
@@ -71,16 +72,291 @@ function assertSecretNamespace(value) {
71
72
  return normalized;
72
73
  }
73
74
 
75
+ // src/provider-catalog.ts
76
+ function openAiCompatible(display_name, base_url, api_key_env) {
77
+ return {
78
+ display_name,
79
+ base_url,
80
+ adapter: "openai_compatible",
81
+ auth_mode: "bearer",
82
+ api_key_env
83
+ };
84
+ }
85
+ function anthropicMessages(display_name, base_url, api_key_env) {
86
+ return {
87
+ display_name,
88
+ base_url,
89
+ adapter: "anthropic",
90
+ auth_mode: "x_api_key",
91
+ api_key_env
92
+ };
93
+ }
94
+ var MODEL_PROVIDER_VENDORS = {
95
+ "ant-ling": openAiCompatible("Ant Ling", "https://api.ant-ling.com/v1", "ANT_LING_API_KEY"),
96
+ baseten: openAiCompatible("Baseten", "https://inference.baseten.co/v1", "BASETEN_API_KEY"),
97
+ cerebras: openAiCompatible("Cerebras", "https://api.cerebras.ai/v1", "CEREBRAS_API_KEY"),
98
+ deepseek: openAiCompatible("DeepSeek", "https://api.deepseek.com", "DEEPSEEK_API_KEY"),
99
+ groq: openAiCompatible("Groq", "https://api.groq.com/openai/v1", "GROQ_API_KEY"),
100
+ huggingface: openAiCompatible("Hugging Face", "https://router.huggingface.co/v1", "HF_TOKEN"),
101
+ moonshotai: openAiCompatible("Moonshot AI", "https://api.moonshot.ai/v1", "MOONSHOT_API_KEY"),
102
+ "moonshotai-cn": openAiCompatible("Moonshot AI CN", "https://api.moonshot.cn/v1", "MOONSHOT_API_KEY"),
103
+ nvidia: openAiCompatible("NVIDIA", "https://integrate.api.nvidia.com/v1", "NVIDIA_API_KEY"),
104
+ openai: openAiCompatible("OpenAI", "https://api.openai.com/v1", "OPENAI_API_KEY"),
105
+ openrouter: openAiCompatible("OpenRouter", "https://openrouter.ai/api/v1", "OPENROUTER_API_KEY"),
106
+ "qwen-token-plan": openAiCompatible(
107
+ "Qwen Token Plan",
108
+ "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
109
+ "QWEN_TOKEN_PLAN_API_KEY"
110
+ ),
111
+ "qwen-token-plan-cn": openAiCompatible(
112
+ "Qwen Token Plan CN",
113
+ "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1",
114
+ "QWEN_TOKEN_PLAN_CN_API_KEY"
115
+ ),
116
+ together: openAiCompatible("Together", "https://api.together.ai/v1", "TOGETHER_API_KEY"),
117
+ xai: openAiCompatible("xAI", "https://api.x.ai/v1", "XAI_API_KEY"),
118
+ xiaomi: openAiCompatible("Xiaomi", "https://api.xiaomimimo.com/v1", "XIAOMI_API_KEY"),
119
+ "xiaomi-token-plan-ams": openAiCompatible(
120
+ "Xiaomi Token Plan AMS",
121
+ "https://token-plan-ams.xiaomimimo.com/v1",
122
+ "XIAOMI_TOKEN_PLAN_AMS_API_KEY"
123
+ ),
124
+ "xiaomi-token-plan-cn": openAiCompatible(
125
+ "Xiaomi Token Plan CN",
126
+ "https://token-plan-cn.xiaomimimo.com/v1",
127
+ "XIAOMI_TOKEN_PLAN_CN_API_KEY"
128
+ ),
129
+ "xiaomi-token-plan-sgp": openAiCompatible(
130
+ "Xiaomi Token Plan SGP",
131
+ "https://token-plan-sgp.xiaomimimo.com/v1",
132
+ "XIAOMI_TOKEN_PLAN_SGP_API_KEY"
133
+ ),
134
+ zai: openAiCompatible("Z.AI", "https://api.z.ai/api/coding/paas/v4", "ZAI_API_KEY"),
135
+ "zai-coding-cn": openAiCompatible(
136
+ "Z.AI Coding CN",
137
+ "https://open.bigmodel.cn/api/coding/paas/v4",
138
+ "ZAI_CODING_CN_API_KEY"
139
+ ),
140
+ anthropic: anthropicMessages("Anthropic", "https://api.anthropic.com/v1", "ANTHROPIC_API_KEY"),
141
+ fireworks: anthropicMessages("Fireworks", "https://api.fireworks.ai/inference/v1", "FIREWORKS_API_KEY"),
142
+ "kimi-coding": anthropicMessages("Kimi For Coding", "https://api.kimi.com/coding/v1", "KIMI_API_KEY"),
143
+ minimax: anthropicMessages("MiniMax", "https://api.minimax.io/anthropic/v1", "MINIMAX_API_KEY"),
144
+ "minimax-cn": anthropicMessages("MiniMax CN", "https://api.minimaxi.com/anthropic/v1", "MINIMAX_CN_API_KEY"),
145
+ "vercel-ai-gateway": anthropicMessages(
146
+ "Vercel AI Gateway",
147
+ "https://ai-gateway.vercel.sh/v1",
148
+ "AI_GATEWAY_API_KEY"
149
+ )
150
+ };
151
+ var MODEL_PROVIDER_VENDOR_IDS = Object.keys(
152
+ MODEL_PROVIDER_VENDORS
153
+ );
154
+ function modelProviderVendor(kind) {
155
+ return Object.hasOwn(MODEL_PROVIDER_VENDORS, kind) ? MODEL_PROVIDER_VENDORS[kind] : void 0;
156
+ }
157
+
158
+ // src/url.ts
159
+ function normalizeProviderUrl(value) {
160
+ let url;
161
+ try {
162
+ url = new URL(value);
163
+ } catch {
164
+ throw new ByokKeysError(
165
+ "PROVIDER_URL_INVALID",
166
+ "Provider base URL must be absolute"
167
+ );
168
+ }
169
+ if (url.username || url.password || url.hash || url.search || url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname))) {
170
+ throw new ByokKeysError(
171
+ "PROVIDER_URL_INVALID",
172
+ "Provider URL requires HTTPS; HTTP is allowed only for localhost"
173
+ );
174
+ }
175
+ if (isPrivateNetworkLiteral(url.hostname) && !isLoopbackHost(url.hostname)) {
176
+ throw new ByokKeysError(
177
+ "PROVIDER_URL_INVALID",
178
+ "Private-network provider IPs are not allowed"
179
+ );
180
+ }
181
+ return url.toString().replace(/\/$/u, "");
182
+ }
183
+ function isLoopbackHost(hostname) {
184
+ const value = hostname.toLowerCase().replace(/^\[|\]$/gu, "");
185
+ return value === "localhost" || value === "127.0.0.1" || value === "::1";
186
+ }
187
+ function isPrivateNetworkLiteral(hostname) {
188
+ const value = hostname.toLowerCase().replace(/^\[|\]$/gu, "");
189
+ if (value.includes(":")) return true;
190
+ const parts = value.split(".").map(Number);
191
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
192
+ return false;
193
+ }
194
+ 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;
195
+ }
196
+
197
+ // src/provider-profile.ts
198
+ var PROVIDER_PROFILE_REF_PATTERN = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/u;
199
+ var ProviderProfileRefSchema = z.string().min(1).max(64).regex(
200
+ PROVIDER_PROFILE_REF_PATTERN,
201
+ "provider profile refs must be lowercase portable identifiers"
202
+ );
203
+ var MODEL_PROVIDER_KINDS = [
204
+ ...MODEL_PROVIDER_VENDOR_IDS,
205
+ "custom"
206
+ ];
207
+ var PROVIDER_MODEL_CAPABILITIES = ["image-input"];
208
+ var ProviderModelCapabilitySchema = z.enum(PROVIDER_MODEL_CAPABILITIES);
209
+ var PROVIDER_AUTH_MODES = ["bearer", "x_api_key", "none"];
210
+ var MODEL_PROVIDER_ADAPTERS = ["openai_compatible", "anthropic"];
211
+ function boundedString(field, maximumLength) {
212
+ return z.string().superRefine((value, ctx) => {
213
+ if (value.trim().length === 0 || value.length > maximumLength || /[\u0000\r\n]/u.test(value)) {
214
+ ctx.addIssue({
215
+ code: "custom",
216
+ message: `Provider ${field} is invalid`
217
+ });
218
+ }
219
+ }).transform((value) => value.trim());
220
+ }
221
+ function isoTimestamp(field) {
222
+ return boundedString(field, 64).superRefine((value, ctx) => {
223
+ if (!Number.isFinite(Date.parse(value))) {
224
+ ctx.addIssue({
225
+ code: "custom",
226
+ message: `Provider ${field} must be an ISO timestamp`
227
+ });
228
+ }
229
+ });
230
+ }
231
+ var providerBaseUrl = z.string().superRefine((value, ctx) => {
232
+ try {
233
+ normalizeProviderUrl(value);
234
+ } catch (error) {
235
+ ctx.addIssue({
236
+ code: "custom",
237
+ message: error instanceof Error ? error.message : "Provider base URL is invalid",
238
+ params: {
239
+ byokCode: error instanceof ByokKeysError ? error.code : "PROVIDER_URL_INVALID"
240
+ }
241
+ });
242
+ }
243
+ }).transform((value) => normalizeProviderUrl(value));
244
+ var ModelProviderProfileSchema = z.object({
245
+ adapter: z.enum(MODEL_PROVIDER_ADAPTERS),
246
+ auth_mode: z.enum(PROVIDER_AUTH_MODES),
247
+ base_url: providerBaseUrl,
248
+ capabilities: z.array(ProviderModelCapabilitySchema).max(8),
249
+ created_at: isoTimestamp("created_at"),
250
+ display_name: boundedString("display_name", 100),
251
+ enabled: z.boolean(),
252
+ kind: z.literal("model"),
253
+ model: boundedString("model", 160),
254
+ profile_ref: ProviderProfileRefSchema,
255
+ provider_kind: z.enum(MODEL_PROVIDER_KINDS),
256
+ updated_at: isoTimestamp("updated_at")
257
+ }).superRefine((profile, ctx) => {
258
+ if (new Set(profile.capabilities).size !== profile.capabilities.length) {
259
+ ctx.addIssue({
260
+ code: "custom",
261
+ message: "Provider capabilities cannot repeat",
262
+ path: ["capabilities"]
263
+ });
264
+ }
265
+ if (profile.adapter === "anthropic" && profile.auth_mode !== "x_api_key") {
266
+ ctx.addIssue({
267
+ code: "custom",
268
+ message: "Anthropic requires x_api_key authentication",
269
+ path: ["auth_mode"]
270
+ });
271
+ }
272
+ if (profile.adapter === "openai_compatible" && profile.auth_mode === "x_api_key") {
273
+ ctx.addIssue({
274
+ code: "custom",
275
+ message: "OpenAI-compatible providers support bearer or no authentication",
276
+ path: ["auth_mode"]
277
+ });
278
+ }
279
+ const vendor = modelProviderVendor(profile.provider_kind);
280
+ if (vendor !== void 0 && vendor.adapter !== profile.adapter) {
281
+ ctx.addIssue({
282
+ code: "custom",
283
+ message: `Provider kind ${profile.provider_kind} speaks the ${vendor.adapter} adapter`,
284
+ path: ["adapter"]
285
+ });
286
+ }
287
+ if (Date.parse(profile.updated_at) < Date.parse(profile.created_at)) {
288
+ ctx.addIssue({
289
+ code: "custom",
290
+ message: "Provider updated_at cannot precede created_at",
291
+ path: ["updated_at"]
292
+ });
293
+ }
294
+ });
295
+ function exactProviderProfileBinding(profileInput, requiredCapabilities = profileInput.capabilities) {
296
+ const profile = parseModelProviderProfile(profileInput);
297
+ const revision = Date.parse(profile.updated_at);
298
+ if (!Number.isSafeInteger(revision) || revision < 0) {
299
+ throw new ByokKeysError(
300
+ "PROVIDER_PROFILE_INVALID",
301
+ "Provider updated_at cannot be represented as a canonical revision"
302
+ );
303
+ }
304
+ const normalizedCapabilities = [...profile.capabilities].sort();
305
+ const canonical = JSON.stringify({
306
+ adapter: profile.adapter,
307
+ auth_mode: profile.auth_mode,
308
+ base_url: profile.base_url,
309
+ capabilities: normalizedCapabilities,
310
+ kind: profile.kind,
311
+ model: profile.model,
312
+ profile_ref: profile.profile_ref,
313
+ provider_kind: profile.provider_kind
314
+ });
315
+ return {
316
+ profileRef: profile.profile_ref,
317
+ profileRevision: String(revision),
318
+ profileHash: `sha256:${createHash("sha256").update(canonical).digest("hex")}`,
319
+ modelId: profile.model,
320
+ requiredCapabilities: [...requiredCapabilities]
321
+ };
322
+ }
323
+ function assertExactProviderProfileBinding(profile, expected) {
324
+ if (new Set(expected.requiredCapabilities).size !== expected.requiredCapabilities.length) {
325
+ throw new Error("provider profile required capabilities must be unique");
326
+ }
327
+ const actual = exactProviderProfileBinding(profile, expected.requiredCapabilities);
328
+ if (actual.profileRef !== expected.profileRef) throw new Error("provider profile ref mismatch");
329
+ if (actual.profileRevision !== expected.profileRevision) throw new Error("provider profile revision mismatch");
330
+ if (actual.profileHash !== expected.profileHash) throw new Error("provider profile hash mismatch");
331
+ if (actual.modelId !== expected.modelId) throw new Error("provider profile model mismatch");
332
+ const supported = new Set(profile.capabilities);
333
+ const unsupported = expected.requiredCapabilities.find((capability) => !supported.has(capability));
334
+ if (unsupported !== void 0) throw new Error(`provider profile does not support required capability ${unsupported}`);
335
+ }
336
+ function parseModelProviderProfile(value) {
337
+ const result = ModelProviderProfileSchema.safeParse(value);
338
+ if (result.success) return result.data;
339
+ const issue = result.error.issues[0];
340
+ const params = issue?.params;
341
+ throw new ByokKeysError(
342
+ params?.byokCode ?? "PROVIDER_PROFILE_INVALID",
343
+ issue?.message ?? "Provider profile is invalid",
344
+ { cause: result.error }
345
+ );
346
+ }
347
+
74
348
  // src/secret-store.ts
75
349
  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];
350
+ function modelProviderSecretName(profileRef) {
351
+ const parsed = ProviderProfileRefSchema.safeParse(profileRef);
352
+ if (!parsed.success) {
353
+ throw new ByokKeysError(
354
+ "PROVIDER_PROFILE_INVALID",
355
+ "Provider profile ref must be a lowercase portable identifier",
356
+ { cause: parsed.error }
357
+ );
358
+ }
359
+ return assertSecretName(`model-${parsed.data}-api-key`);
84
360
  }
85
361
  function decodeStrictBase64Utf8(encoded) {
86
362
  if (encoded.length % 4 !== 0) return void 0;
@@ -266,11 +542,11 @@ function assertKeychainPath(keychainPath) {
266
542
 
267
543
  // src/pi-provider-projection.ts
268
544
  var PI_PROJECTED_KEY_ENV = "PI_PROVIDER_API_KEY";
269
- function piProjectionProviderId(profileProviderId) {
270
- return `byok-sdk-${profileProviderId}`;
545
+ function piProjectionProviderId(profileRef) {
546
+ return `byok-sdk-${profileRef}`;
271
547
  }
272
548
  function buildPiProviderProjection(profile) {
273
- const projectedProviderId = piProjectionProviderId(profile.provider_id);
549
+ const projectedProviderId = piProjectionProviderId(profile.profile_ref);
274
550
  return {
275
551
  providers: {
276
552
  [projectedProviderId]: {
@@ -281,7 +557,11 @@ function buildPiProviderProjection(profile) {
281
557
  models: [
282
558
  {
283
559
  id: profile.model,
284
- name: profile.display_name
560
+ name: profile.display_name,
561
+ input: [
562
+ "text",
563
+ ...profile.capabilities.includes("image-input") ? ["image"] : []
564
+ ]
285
565
  }
286
566
  ]
287
567
  }
@@ -314,139 +594,12 @@ function buildPiProviderArgs(profile, delegatedArgs) {
314
594
  return [
315
595
  ...delegatedArgs,
316
596
  "--provider",
317
- piProjectionProviderId(profile.provider_id),
597
+ piProjectionProviderId(profile.profile_ref),
318
598
  "--model",
319
599
  profile.model
320
600
  ];
321
601
  }
322
602
 
323
- // src/url.ts
324
- function normalizeProviderUrl(value) {
325
- let url;
326
- try {
327
- url = new URL(value);
328
- } catch {
329
- throw new ByokKeysError(
330
- "PROVIDER_URL_INVALID",
331
- "Provider base URL must be absolute"
332
- );
333
- }
334
- if (url.username || url.password || url.hash || url.search || url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname))) {
335
- throw new ByokKeysError(
336
- "PROVIDER_URL_INVALID",
337
- "Provider URL requires HTTPS; HTTP is allowed only for localhost"
338
- );
339
- }
340
- if (isPrivateNetworkLiteral(url.hostname) && !isLoopbackHost(url.hostname)) {
341
- throw new ByokKeysError(
342
- "PROVIDER_URL_INVALID",
343
- "Private-network provider IPs are not allowed"
344
- );
345
- }
346
- return url.toString().replace(/\/$/u, "");
347
- }
348
- function isLoopbackHost(hostname) {
349
- const value = hostname.toLowerCase().replace(/^\[|\]$/gu, "");
350
- return value === "localhost" || value === "127.0.0.1" || value === "::1";
351
- }
352
- function isPrivateNetworkLiteral(hostname) {
353
- const value = hostname.toLowerCase().replace(/^\[|\]$/gu, "");
354
- if (value.includes(":")) return true;
355
- const parts = value.split(".").map(Number);
356
- if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
357
- return false;
358
- }
359
- 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;
360
- }
361
-
362
- // src/provider-profile.ts
363
- var MODEL_PROVIDER_IDS = [
364
- "openai",
365
- "deepseek",
366
- "anthropic",
367
- "custom"
368
- ];
369
- var PROVIDER_AUTH_MODES = ["bearer", "x_api_key", "none"];
370
- var MODEL_PROVIDER_ADAPTERS = ["openai_compatible", "anthropic"];
371
- function boundedString(field, maximumLength) {
372
- return z.string().superRefine((value, ctx) => {
373
- if (value.trim().length === 0 || value.length > maximumLength || /[\u0000\r\n]/u.test(value)) {
374
- ctx.addIssue({
375
- code: "custom",
376
- message: `Provider ${field} is invalid`
377
- });
378
- }
379
- }).transform((value) => value.trim());
380
- }
381
- function isoTimestamp(field) {
382
- return boundedString(field, 64).superRefine((value, ctx) => {
383
- if (!Number.isFinite(Date.parse(value))) {
384
- ctx.addIssue({
385
- code: "custom",
386
- message: `Provider ${field} must be an ISO timestamp`
387
- });
388
- }
389
- });
390
- }
391
- var providerBaseUrl = z.string().superRefine((value, ctx) => {
392
- try {
393
- normalizeProviderUrl(value);
394
- } catch (error) {
395
- ctx.addIssue({
396
- code: "custom",
397
- message: error instanceof Error ? error.message : "Provider base URL is invalid",
398
- params: {
399
- byokCode: error instanceof ByokKeysError ? error.code : "PROVIDER_URL_INVALID"
400
- }
401
- });
402
- }
403
- }).transform((value) => normalizeProviderUrl(value));
404
- var ModelProviderProfileSchema = z.object({
405
- adapter: z.enum(MODEL_PROVIDER_ADAPTERS),
406
- auth_mode: z.enum(PROVIDER_AUTH_MODES),
407
- base_url: providerBaseUrl,
408
- created_at: isoTimestamp("created_at"),
409
- display_name: boundedString("display_name", 100),
410
- enabled: z.boolean(),
411
- kind: z.literal("model"),
412
- model: boundedString("model", 160),
413
- provider_id: z.enum(MODEL_PROVIDER_IDS),
414
- updated_at: isoTimestamp("updated_at")
415
- }).superRefine((profile, ctx) => {
416
- if (profile.adapter === "anthropic" && profile.auth_mode !== "x_api_key") {
417
- ctx.addIssue({
418
- code: "custom",
419
- message: "Anthropic requires x_api_key authentication",
420
- path: ["auth_mode"]
421
- });
422
- }
423
- if (profile.adapter === "openai_compatible" && profile.auth_mode === "x_api_key") {
424
- ctx.addIssue({
425
- code: "custom",
426
- message: "OpenAI-compatible providers support bearer or no authentication",
427
- path: ["auth_mode"]
428
- });
429
- }
430
- if (Date.parse(profile.updated_at) < Date.parse(profile.created_at)) {
431
- ctx.addIssue({
432
- code: "custom",
433
- message: "Provider updated_at cannot precede created_at",
434
- path: ["updated_at"]
435
- });
436
- }
437
- });
438
- function parseModelProviderProfile(value) {
439
- const result = ModelProviderProfileSchema.safeParse(value);
440
- if (result.success) return result.data;
441
- const issue = result.error.issues[0];
442
- const params = issue?.params;
443
- throw new ByokKeysError(
444
- params?.byokCode ?? "PROVIDER_PROFILE_INVALID",
445
- issue?.message ?? "Provider profile is invalid",
446
- { cause: result.error }
447
- );
448
- }
449
-
450
603
  // src/pi-provider-launcher-core.ts
451
604
  var PI_CHILD_BASE_ENV_NAMES = [
452
605
  "PATH",
@@ -480,15 +633,17 @@ var PI_CHILD_WINDOWS_ENV_NAMES = [
480
633
  ];
481
634
  function parsePiProviderLauncherOptions(args) {
482
635
  const separator = args.indexOf("--");
483
- if (separator < 0) throw new Error("launcher arguments must end with -- <pi args>");
484
- const ownArgs = args.slice(0, separator);
485
- const piArgs = args.slice(separator + 1);
486
- if (piArgs.length === 0) throw new Error("launcher requires Pi arguments after --");
636
+ const ownArgs = separator < 0 ? args : args.slice(0, separator);
637
+ const piArgs = separator < 0 ? [] : args.slice(separator + 1);
487
638
  const allowedFlags = /* @__PURE__ */ new Set([
488
639
  "--pi-bin",
489
640
  "--profile-db",
490
641
  "--provider",
491
642
  "--model",
643
+ "--profile-revision",
644
+ "--profile-hash",
645
+ "--required-capabilities",
646
+ "--validate-only",
492
647
  "--session-dir",
493
648
  "--secret-service-prefix",
494
649
  "--macos-keychain-path"
@@ -512,9 +667,10 @@ function parsePiProviderLauncherOptions(args) {
512
667
  if (value === void 0) throw new Error(`${flag} requires a value`);
513
668
  return value;
514
669
  };
515
- const rawProviderId = required("--provider");
516
- if (!MODEL_PROVIDER_IDS.includes(rawProviderId)) {
517
- throw new Error(`provider ${rawProviderId} is not configured by @byok-sdk/keys`);
670
+ const rawProfileRef = required("--provider");
671
+ const profileRef = ProviderProfileRefSchema.safeParse(rawProfileRef);
672
+ if (!profileRef.success) {
673
+ throw new Error(`provider profile ref ${rawProfileRef} is not a valid @byok-sdk/keys identifier`);
518
674
  }
519
675
  const modelId = required("--model");
520
676
  if (modelId.length > 160) throw new Error("--model exceeds 160 characters");
@@ -528,11 +684,54 @@ function parsePiProviderLauncherOptions(args) {
528
684
  if (macosKeychainPath !== void 0 && !path2.posix.isAbsolute(macosKeychainPath)) {
529
685
  throw new Error("--macos-keychain-path must be an absolute path");
530
686
  }
687
+ const validateOnly = values.get("--validate-only") === "true";
688
+ if (values.has("--validate-only") && !["true", "false"].includes(values.get("--validate-only"))) {
689
+ throw new Error("--validate-only must be true or false");
690
+ }
691
+ if (!validateOnly && piArgs.length === 0) {
692
+ throw new Error("launcher requires Pi arguments after --");
693
+ }
694
+ const exactValues = [
695
+ values.get("--profile-revision"),
696
+ values.get("--profile-hash"),
697
+ values.get("--required-capabilities")
698
+ ];
699
+ if (exactValues.some((value) => value !== void 0) && exactValues.some((value) => value === void 0)) {
700
+ throw new Error("exact provider binding requires revision, hash, and required capabilities together");
701
+ }
702
+ let expectedBinding;
703
+ if (exactValues[0] !== void 0) {
704
+ if (!/^(?:0|[1-9][0-9]{0,19})$/u.test(exactValues[0])) {
705
+ throw new Error("--profile-revision must be canonical decimal");
706
+ }
707
+ if (!/^sha256:[0-9a-f]{64}$/u.test(exactValues[1])) {
708
+ throw new Error("--profile-hash must be lowercase sha256");
709
+ }
710
+ let capabilities;
711
+ try {
712
+ capabilities = JSON.parse(exactValues[2]);
713
+ } catch {
714
+ throw new Error("--required-capabilities must be a JSON array");
715
+ }
716
+ const parsedCapabilities = ProviderModelCapabilitySchema.array().max(8).safeParse(capabilities);
717
+ if (!parsedCapabilities.success || new Set(parsedCapabilities.data).size !== parsedCapabilities.data.length) {
718
+ throw new Error("--required-capabilities must contain unique supported capabilities");
719
+ }
720
+ expectedBinding = {
721
+ profileRef: profileRef.data,
722
+ profileRevision: exactValues[0],
723
+ profileHash: exactValues[1],
724
+ modelId,
725
+ requiredCapabilities: parsedCapabilities.data
726
+ };
727
+ }
531
728
  return {
532
729
  piBin: required("--pi-bin"),
533
730
  profileDbPath,
534
- providerId: rawProviderId,
731
+ profileRef: profileRef.data,
535
732
  modelId,
733
+ ...expectedBinding === void 0 ? {} : { expectedBinding },
734
+ validateOnly,
536
735
  sessionDir,
537
736
  ...secretServicePrefix ? { secretServicePrefix } : {},
538
737
  ...macosKeychainPath !== void 0 ? { macosKeychainPath } : {},
@@ -548,11 +747,11 @@ async function resolvePiProviderSecret(profile, createStore) {
548
747
  `${secrets.providerLabel} is unavailable`
549
748
  );
550
749
  }
551
- const secret = await secrets.get(modelProviderSecretName(profile.provider_id));
750
+ const secret = await secrets.get(modelProviderSecretName(profile.profile_ref));
552
751
  if (!secret) {
553
752
  throw new ByokKeysError(
554
753
  "PROVIDER_SECRET_MISSING",
555
- `${profile.provider_id} provider requires a secret in ${secrets.providerLabel}`
754
+ `${profile.profile_ref} provider profile requires a secret in ${secrets.providerLabel}`
556
755
  );
557
756
  }
558
757
  return secret;
@@ -596,10 +795,10 @@ async function ensurePiSessionDirectory(sessionDir) {
596
795
  }
597
796
 
598
797
  // src/profile-store.ts
599
- function providerNotConfigured(providerId) {
798
+ function providerNotConfigured(profileRef) {
600
799
  return new ByokKeysError(
601
800
  "PROVIDER_NOT_CONFIGURED",
602
- `${providerId} model provider is not configured`
801
+ `${profileRef} model provider is not configured`
603
802
  );
604
803
  }
605
804
  function closeSqliteDatabaseAfterInitializationFailure(database, initializationError, message, close = (handle) => handle.close()) {
@@ -666,20 +865,40 @@ function secureSqliteFilePermissions(databasePath) {
666
865
  }
667
866
 
668
867
  // src/sqlite-profile-store.ts
868
+ function sqlList(values) {
869
+ return values.map((value) => `'${value}'`).join(", ");
870
+ }
669
871
  var SCHEMA = `
670
872
  CREATE TABLE IF NOT EXISTS provider_profile (
671
- provider_id TEXT PRIMARY KEY CHECK (provider_id IN ('openai', 'deepseek', 'anthropic', 'custom')),
672
- kind TEXT NOT NULL CHECK (kind = 'model'),
673
- adapter TEXT NOT NULL CHECK (adapter IN ('openai_compatible', 'anthropic')),
674
- display_name TEXT NOT NULL,
675
- base_url TEXT NOT NULL,
676
- auth_mode TEXT NOT NULL CHECK (auth_mode IN ('bearer', 'x_api_key', 'none')),
677
- model TEXT NOT NULL,
678
- enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
679
- created_at TEXT NOT NULL,
680
- updated_at TEXT NOT NULL
873
+ profile_ref TEXT PRIMARY KEY,
874
+ provider_kind TEXT NOT NULL CHECK (provider_kind IN (${sqlList(MODEL_PROVIDER_KINDS)})),
875
+ kind TEXT NOT NULL CHECK (kind = 'model'),
876
+ adapter TEXT NOT NULL CHECK (adapter IN (${sqlList(MODEL_PROVIDER_ADAPTERS)})),
877
+ display_name TEXT NOT NULL,
878
+ base_url TEXT NOT NULL,
879
+ auth_mode TEXT NOT NULL CHECK (auth_mode IN (${sqlList(PROVIDER_AUTH_MODES)})),
880
+ model TEXT NOT NULL,
881
+ capabilities TEXT NOT NULL,
882
+ enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
883
+ created_at TEXT NOT NULL,
884
+ updated_at TEXT NOT NULL
681
885
  );
682
886
  `;
887
+ function normalizeTableDdl(sql) {
888
+ return sql.replace(/\bIF\s+NOT\s+EXISTS\b/giu, "").replace(/\s+/gu, " ").trim().replace(/;$/u, "").trim();
889
+ }
890
+ function assertProviderProfileSchemaIsCurrent(database, path4) {
891
+ const row = database.prepare(
892
+ "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'provider_profile'"
893
+ ).get();
894
+ const stored = row?.sql;
895
+ if (typeof stored !== "string") return;
896
+ if (normalizeTableDdl(stored) === normalizeTableDdl(SCHEMA)) return;
897
+ throw new ByokKeysError(
898
+ "PROVIDER_STORE_SCHEMA_STALE",
899
+ `Provider profile store at ${path4} was created by a different @byok-sdk/keys schema; recreate the store file to continue`
900
+ );
901
+ }
683
902
  var ENABLED_INDEX = `
684
903
  CREATE UNIQUE INDEX IF NOT EXISTS provider_profile_one_enabled
685
904
  ON provider_profile(kind)
@@ -696,6 +915,7 @@ var SqliteProviderProfileStore = class {
696
915
  try {
697
916
  this.#database.exec(SCHEMA);
698
917
  this.#database.exec(ENABLED_INDEX);
918
+ assertProviderProfileSchemaIsCurrent(this.#database, options.path);
699
919
  secureSqliteFilePermissions(options.path);
700
920
  } catch (error) {
701
921
  closeSqliteDatabaseAfterInitializationFailure(
@@ -704,6 +924,16 @@ var SqliteProviderProfileStore = class {
704
924
  "SqliteProviderProfileStore initialization failed and its native handle could not be closed"
705
925
  );
706
926
  }
927
+ } else {
928
+ try {
929
+ assertProviderProfileSchemaIsCurrent(this.#database, options.path);
930
+ } catch (error) {
931
+ closeSqliteDatabaseAfterInitializationFailure(
932
+ this.#database,
933
+ error,
934
+ "SqliteProviderProfileStore read-only schema check failed and its native handle could not be closed"
935
+ );
936
+ }
707
937
  }
708
938
  }
709
939
  /**
@@ -717,12 +947,12 @@ var SqliteProviderProfileStore = class {
717
947
  this.#closed = true;
718
948
  this.#database.close();
719
949
  }
720
- async delete(providerId) {
721
- const result = this.#database.prepare("DELETE FROM provider_profile WHERE provider_id = ?").run(providerId);
950
+ async delete(profileRef) {
951
+ const result = this.#database.prepare("DELETE FROM provider_profile WHERE profile_ref = ?").run(profileRef);
722
952
  return Number(result.changes) === 1;
723
953
  }
724
- async get(providerId) {
725
- const row = this.#database.prepare("SELECT * FROM provider_profile WHERE provider_id = ?").get(providerId);
954
+ async get(profileRef) {
955
+ const row = this.#database.prepare("SELECT * FROM provider_profile WHERE profile_ref = ?").get(profileRef);
726
956
  return row === void 0 ? void 0 : parseRow(row);
727
957
  }
728
958
  async getEnabled() {
@@ -730,11 +960,11 @@ var SqliteProviderProfileStore = class {
730
960
  return row === void 0 ? void 0 : parseRow(row);
731
961
  }
732
962
  async list() {
733
- const rows = this.#database.prepare("SELECT * FROM provider_profile ORDER BY provider_id ASC").all();
963
+ const rows = this.#database.prepare("SELECT * FROM provider_profile ORDER BY profile_ref ASC").all();
734
964
  return rows.map(parseRow);
735
965
  }
736
966
  async save(profile) {
737
- const existing = await this.get(profile.provider_id);
967
+ const existing = await this.get(profile.profile_ref);
738
968
  const validated = parseModelProviderProfile({
739
969
  ...profile,
740
970
  created_at: existing?.created_at ?? profile.created_at
@@ -742,40 +972,44 @@ var SqliteProviderProfileStore = class {
742
972
  this.#transaction(() => {
743
973
  if (validated.enabled) {
744
974
  this.#database.prepare(
745
- "UPDATE provider_profile SET enabled = 0 WHERE provider_id <> ?"
746
- ).run(validated.provider_id);
975
+ "UPDATE provider_profile SET enabled = 0 WHERE profile_ref <> ?"
976
+ ).run(validated.profile_ref);
747
977
  }
748
978
  this.#database.prepare(
749
979
  `INSERT INTO provider_profile (
750
- provider_id, kind, adapter, display_name, base_url,
751
- auth_mode, model, enabled, created_at, updated_at
752
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
753
- ON CONFLICT(provider_id) DO UPDATE SET
980
+ profile_ref, provider_kind, kind, adapter, display_name, base_url,
981
+ auth_mode, model, capabilities, enabled, created_at, updated_at
982
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
983
+ ON CONFLICT(profile_ref) DO UPDATE SET
984
+ provider_kind = excluded.provider_kind,
754
985
  adapter = excluded.adapter,
755
986
  display_name = excluded.display_name,
756
987
  base_url = excluded.base_url,
757
988
  auth_mode = excluded.auth_mode,
758
989
  model = excluded.model,
990
+ capabilities = excluded.capabilities,
759
991
  enabled = excluded.enabled,
760
992
  updated_at = excluded.updated_at`
761
993
  ).run(
762
- validated.provider_id,
994
+ validated.profile_ref,
995
+ validated.provider_kind,
763
996
  validated.kind,
764
997
  validated.adapter,
765
998
  validated.display_name,
766
999
  validated.base_url,
767
1000
  validated.auth_mode,
768
1001
  validated.model,
1002
+ JSON.stringify(validated.capabilities),
769
1003
  validated.enabled ? 1 : 0,
770
1004
  validated.created_at,
771
1005
  validated.updated_at
772
1006
  );
773
1007
  });
774
- return await this.get(validated.provider_id);
1008
+ return await this.get(validated.profile_ref);
775
1009
  }
776
- async setEnabled(providerId) {
777
- const existing = await this.get(providerId);
778
- if (existing === void 0) throw providerNotConfigured(providerId);
1010
+ async setEnabled(profileRef) {
1011
+ const existing = await this.get(profileRef);
1012
+ if (existing === void 0) throw providerNotConfigured(profileRef);
779
1013
  return this.save({ ...existing, enabled: true });
780
1014
  }
781
1015
  /** `BEGIN IMMEDIATE` / `COMMIT` / `ROLLBACK`, per `providers.ts:1252-1263`. */
@@ -791,8 +1025,19 @@ var SqliteProviderProfileStore = class {
791
1025
  }
792
1026
  };
793
1027
  function parseRow(row) {
1028
+ let capabilities;
1029
+ try {
1030
+ capabilities = JSON.parse(row.capabilities);
1031
+ } catch (cause) {
1032
+ throw new ByokKeysError(
1033
+ "PROVIDER_PROFILE_INVALID",
1034
+ "Provider profile capabilities column is not valid JSON",
1035
+ { cause }
1036
+ );
1037
+ }
794
1038
  return parseModelProviderProfile({
795
1039
  ...row,
1040
+ capabilities,
796
1041
  enabled: row.enabled === 1
797
1042
  });
798
1043
  }
@@ -1100,15 +1345,19 @@ async function run(options) {
1100
1345
  });
1101
1346
  let projectionDir;
1102
1347
  try {
1103
- const profile = await profiles.get(options.providerId);
1348
+ const profile = await profiles.get(options.profileRef);
1104
1349
  if (profile === void 0) {
1105
- throw new Error(`provider ${options.providerId} is not configured`);
1350
+ throw new Error(`provider profile ${options.profileRef} is not configured`);
1106
1351
  }
1107
1352
  if (profile.model !== options.modelId) {
1108
1353
  throw new Error(
1109
1354
  `selected model ${options.modelId} does not match configured provider model ${profile.model}`
1110
1355
  );
1111
1356
  }
1357
+ if (options.expectedBinding !== void 0) {
1358
+ assertExactProviderProfileBinding(profile, options.expectedBinding);
1359
+ }
1360
+ if (options.validateOnly) return 0;
1112
1361
  const secret = await resolvePiProviderSecret(
1113
1362
  profile,
1114
1363
  () => createSecretStore(