@byok-sdk/keys 0.2.1 → 0.3.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/README.md CHANGED
@@ -105,6 +105,13 @@ dependency and the workspace release floor.
105
105
  | `TruthStoreProviderProfileStore` | Node 22.22+ plus an injected tenant-bound `TruthStore` | stale CAS or malformed/hash-mismatched authority fails closed; never falls back to SQLite |
106
106
  | `byok-pi-provider-launcher` | Node 22.5+ (`node:sqlite`) and macOS/Windows for authenticated profiles | fails closed; `auth_mode: none` does not require a credential backend |
107
107
 
108
+ On macOS, an isolated host may select one explicit credential authority with
109
+ `MacOsKeychainSecretStore({ keychainPath: '/absolute/path/to/keychain-db' })`
110
+ or the launcher's `--macos-keychain-path` flag. The path must be absolute and
111
+ single-line. Once selected, availability and every credential operation use
112
+ that keychain only; the store never also searches the user default keychain.
113
+ The launcher rejects this macOS-only flag on other platforms.
114
+
108
115
  Only on-disk profile persistence needs the newer runtime. `node:sqlite` shipped
109
116
  in Node 22.5 and spent part of the 22.x line behind `--experimental-sqlite`, so
110
117
  a version-number comparison would be wrong in both directions; call
@@ -2,7 +2,7 @@
2
2
  import { spawn } from 'child_process';
3
3
  import { promises, mkdirSync, existsSync, chmodSync } from 'fs';
4
4
  import os from 'os';
5
- import path, { dirname } from 'path';
5
+ import path2, { dirname } from 'path';
6
6
  import { z } from 'zod';
7
7
  import { createRequire } from 'module';
8
8
 
@@ -100,6 +100,7 @@ var MacOsKeychainSecretStore = class _MacOsKeychainSecretStore {
100
100
  #account;
101
101
  #allowUnprefixedRead;
102
102
  #commandRunner;
103
+ #keychainPath;
103
104
  #platform;
104
105
  #servicePrefix;
105
106
  #storagePrefix;
@@ -107,6 +108,7 @@ var MacOsKeychainSecretStore = class _MacOsKeychainSecretStore {
107
108
  this.#account = options.account ?? "local-device";
108
109
  this.#allowUnprefixedRead = options.allowUnprefixedRead ?? false;
109
110
  this.#commandRunner = options.commandRunner ?? runCommand;
111
+ this.#keychainPath = assertKeychainPath(options.keychainPath);
110
112
  this.#platform = options.platform ?? process.platform;
111
113
  this.#servicePrefix = options.servicePrefix ?? DEFAULT_SECRET_SERVICE_PREFIX;
112
114
  this.#storagePrefix = options.storagePrefix ?? DEFAULT_KEYCHAIN_SECRET_STORAGE_PREFIX;
@@ -114,9 +116,7 @@ var MacOsKeychainSecretStore = class _MacOsKeychainSecretStore {
114
116
  async available() {
115
117
  if (this.#platform !== "darwin") return false;
116
118
  const result = await this.#commandRunner("/usr/bin/security", [
117
- "default-keychain",
118
- "-d",
119
- "user"
119
+ ...this.#keychainPath === void 0 ? ["default-keychain", "-d", "user"] : ["show-keychain-info", this.#keychainPath]
120
120
  ]);
121
121
  return result.exitCode === 0;
122
122
  }
@@ -128,7 +128,8 @@ var MacOsKeychainSecretStore = class _MacOsKeychainSecretStore {
128
128
  "-a",
129
129
  this.#account,
130
130
  "-s",
131
- service
131
+ service,
132
+ ...this.#keychainPath === void 0 ? [] : [this.#keychainPath]
132
133
  ]);
133
134
  if (result.exitCode === SECURITY_ITEM_NOT_FOUND) return false;
134
135
  if (result.exitCode !== 0) {
@@ -148,7 +149,8 @@ var MacOsKeychainSecretStore = class _MacOsKeychainSecretStore {
148
149
  this.#account,
149
150
  "-s",
150
151
  service,
151
- "-w"
152
+ "-w",
153
+ ...this.#keychainPath === void 0 ? [] : [this.#keychainPath]
152
154
  ]);
153
155
  if (result.exitCode === SECURITY_ITEM_NOT_FOUND) return void 0;
154
156
  if (result.exitCode !== 0) {
@@ -167,6 +169,7 @@ var MacOsKeychainSecretStore = class _MacOsKeychainSecretStore {
167
169
  account: this.#account,
168
170
  allowUnprefixedRead: this.#allowUnprefixedRead,
169
171
  commandRunner: this.#commandRunner,
172
+ keychainPath: this.#keychainPath,
170
173
  platform: this.#platform,
171
174
  servicePrefix: `${this.#servicePrefix}.scope.${assertSecretNamespace(namespace)}`,
172
175
  storagePrefix: this.#storagePrefix
@@ -190,7 +193,8 @@ var MacOsKeychainSecretStore = class _MacOsKeychainSecretStore {
190
193
  "-s",
191
194
  quoteSecurityInteractiveArgument(service),
192
195
  "-w",
193
- quoteSecurityInteractiveArgument(storedSecret)
196
+ quoteSecurityInteractiveArgument(storedSecret),
197
+ ...this.#keychainPath === void 0 ? [] : [quoteSecurityInteractiveArgument(this.#keychainPath)]
194
198
  ].join(" ");
195
199
  const result = await this.#commandRunner(
196
200
  "/usr/bin/security",
@@ -249,6 +253,16 @@ function quoteSecurityInteractiveArgument(value) {
249
253
  }
250
254
  return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
251
255
  }
256
+ function assertKeychainPath(keychainPath) {
257
+ if (keychainPath === void 0) return void 0;
258
+ if (keychainPath.length === 0 || !path2.posix.isAbsolute(keychainPath) || /[\u0000\r\n]/u.test(keychainPath)) {
259
+ throw new ByokKeysError(
260
+ "KEYCHAIN_ARGUMENT_INVALID",
261
+ "macOS keychain path must be a non-empty absolute single-line path"
262
+ );
263
+ }
264
+ return keychainPath;
265
+ }
252
266
 
253
267
  // src/pi-provider-projection.ts
254
268
  var PI_PROJECTED_KEY_ENV = "PI_PROVIDER_API_KEY";
@@ -476,7 +490,8 @@ function parsePiProviderLauncherOptions(args) {
476
490
  "--provider",
477
491
  "--model",
478
492
  "--session-dir",
479
- "--secret-service-prefix"
493
+ "--secret-service-prefix",
494
+ "--macos-keychain-path"
480
495
  ]);
481
496
  const values = /* @__PURE__ */ new Map();
482
497
  for (let index = 0; index < ownArgs.length; index += 2) {
@@ -505,10 +520,14 @@ function parsePiProviderLauncherOptions(args) {
505
520
  if (modelId.length > 160) throw new Error("--model exceeds 160 characters");
506
521
  const profileDbPath = required("--profile-db");
507
522
  const sessionDir = required("--session-dir");
508
- if (!path.isAbsolute(profileDbPath) || !path.isAbsolute(sessionDir)) {
523
+ if (!path2.isAbsolute(profileDbPath) || !path2.isAbsolute(sessionDir)) {
509
524
  throw new Error("launcher profile database and session directory must be absolute paths");
510
525
  }
511
526
  const secretServicePrefix = values.get("--secret-service-prefix");
527
+ const macosKeychainPath = values.get("--macos-keychain-path");
528
+ if (macosKeychainPath !== void 0 && !path2.posix.isAbsolute(macosKeychainPath)) {
529
+ throw new Error("--macos-keychain-path must be an absolute path");
530
+ }
512
531
  return {
513
532
  piBin: required("--pi-bin"),
514
533
  profileDbPath,
@@ -516,6 +535,7 @@ function parsePiProviderLauncherOptions(args) {
516
535
  modelId,
517
536
  sessionDir,
518
537
  ...secretServicePrefix ? { secretServicePrefix } : {},
538
+ ...macosKeychainPath !== void 0 ? { macosKeychainPath } : {},
519
539
  piArgs
520
540
  };
521
541
  }
@@ -604,19 +624,19 @@ function loadSqliteModule() {
604
624
  );
605
625
  }
606
626
  }
607
- function openSqliteDatabase(path3, options, faults) {
627
+ function openSqliteDatabase(path4, options, faults) {
608
628
  const { DatabaseSync } = loadSqliteModule();
609
629
  const readOnly = options?.readOnly === true;
610
- if (path3 !== ":memory:" && !readOnly) {
611
- mkdirSync(dirname(path3), { mode: SECURE_DIR_MODE, recursive: true });
630
+ if (path4 !== ":memory:" && !readOnly) {
631
+ mkdirSync(dirname(path4), { mode: SECURE_DIR_MODE, recursive: true });
612
632
  }
613
- const database = new DatabaseSync(path3, {
633
+ const database = new DatabaseSync(path4, {
614
634
  timeout: DEFAULT_BUSY_TIMEOUT_MS,
615
635
  ...options
616
636
  });
617
637
  try {
618
638
  faults?.onStep?.("after-open");
619
- if (path3 !== ":memory:" && !readOnly) {
639
+ if (path4 !== ":memory:" && !readOnly) {
620
640
  database.exec("PRAGMA journal_mode = WAL");
621
641
  faults?.onStep?.("after-wal");
622
642
  database.exec("PRAGMA synchronous = FULL");
@@ -1045,10 +1065,19 @@ function assertWindowsCredentialSecret(secret) {
1045
1065
  }
1046
1066
 
1047
1067
  // src/bin/pi-provider-launcher.ts
1048
- function createSecretStore(servicePrefix) {
1068
+ function createSecretStore(servicePrefix, macosKeychainPath) {
1069
+ if (macosKeychainPath !== void 0 && process.platform !== "darwin") {
1070
+ throw new ByokKeysError(
1071
+ "KEYCHAIN_UNAVAILABLE",
1072
+ "macOS keychain path is only supported on darwin"
1073
+ );
1074
+ }
1049
1075
  switch (process.platform) {
1050
1076
  case "darwin":
1051
- return new MacOsKeychainSecretStore({ servicePrefix });
1077
+ return new MacOsKeychainSecretStore({
1078
+ keychainPath: macosKeychainPath,
1079
+ servicePrefix
1080
+ });
1052
1081
  case "win32":
1053
1082
  return new WindowsCredentialManagerSecretStore({ servicePrefix });
1054
1083
  default:
@@ -1059,6 +1088,12 @@ function createSecretStore(servicePrefix) {
1059
1088
  }
1060
1089
  }
1061
1090
  async function run(options) {
1091
+ if (options.macosKeychainPath !== void 0 && process.platform !== "darwin") {
1092
+ throw new ByokKeysError(
1093
+ "KEYCHAIN_UNAVAILABLE",
1094
+ "macOS keychain path is only supported on darwin"
1095
+ );
1096
+ }
1062
1097
  const profiles = new SqliteProviderProfileStore({
1063
1098
  path: options.profileDbPath,
1064
1099
  readOnly: true
@@ -1076,14 +1111,17 @@ async function run(options) {
1076
1111
  }
1077
1112
  const secret = await resolvePiProviderSecret(
1078
1113
  profile,
1079
- () => createSecretStore(options.secretServicePrefix)
1114
+ () => createSecretStore(
1115
+ options.secretServicePrefix,
1116
+ options.macosKeychainPath
1117
+ )
1080
1118
  );
1081
- projectionDir = await promises.mkdtemp(path.join(os.tmpdir(), "byok-pi-provider-"));
1119
+ projectionDir = await promises.mkdtemp(path2.join(os.tmpdir(), "byok-pi-provider-"));
1082
1120
  await promises.chmod(projectionDir, 448).catch(() => {
1083
1121
  });
1084
1122
  await ensurePiSessionDirectory(options.sessionDir);
1085
1123
  await promises.writeFile(
1086
- path.join(projectionDir, "models.json"),
1124
+ path2.join(projectionDir, "models.json"),
1087
1125
  `${JSON.stringify(buildPiProviderProjection(profile))}
1088
1126
  `,
1089
1127
  { mode: 384 }