@harnessforce/cli 0.1.0 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +13 -6
  2. package/dist/bin.js +343 -75
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -11,8 +11,8 @@ then, install it from the [local marketplace build](https://github.com/lambda-sc
11
11
  ## Requirements
12
12
 
13
13
  - Node.js 18 or later (the same Node.js the plugin hook uses).
14
- - An OS keychain: macOS Keychain, the Secret Service on Linux, or the Windows Credential Manager.
15
- Keys and tokens are never written to a plain file.
14
+ - An OS keychain: macOS Keychain, the Windows Credential Manager, or on Linux the Secret Service or the
15
+ kernel keyring (keyutils). Keys and tokens are never written to a plain file.
16
16
 
17
17
  ## Install
18
18
 
@@ -104,9 +104,13 @@ tokens are sent only to the origins pinned by `harnessforce init`.
104
104
 
105
105
  `harnessforce init [--url <base URL>] [--port <port>] [--send-content]` connects this machine to a Harnessforce Workspace:
106
106
 
107
- 1. It checks the OS keychain first: macOS Keychain, the Secret Service on Linux, or the Windows
108
- Credential Manager (through [`@napi-rs/keyring`](https://github.com/Brooooooklyn/keyring-node)).
109
- Without one it stops. Keys are never written to a plain file.
107
+ 1. It checks the OS keychain first: macOS Keychain or the Windows Credential Manager (through
108
+ [`@napi-rs/keyring`](https://github.com/Brooooooklyn/keyring-node)). On Linux it writes and deletes
109
+ a non-secret `availability-probe` item in the Secret Service (GNOME Keyring or KWallet), waiting at
110
+ most 60 seconds for you to unlock it, and saves there when that works. Otherwise, as on a machine
111
+ reached only over SSH, it saves to the kernel's persistent keyring, which lives in memory and is
112
+ cleared on reboot or after a period of disuse; run `harnessforce init` again then. Without either
113
+ store it stops. Keys are never written to a plain file.
110
114
  2. It logs in with the browser: OAuth 2.0 authorization code with PKCE, a `127.0.0.1:8080` loopback
111
115
  redirect (`--port <port>` changes the port), the endpoint discovered from
112
116
  `/.well-known/oauth-authorization-server`, and a 5 minute wait. When the port is already in use it
@@ -120,7 +124,10 @@ tokens are sent only to the origins pinned by `harnessforce init`.
120
124
  `<workspace_id>:api-token` (a JSON object with the access token, the refresh token and their
121
125
  expiries), and pins the ingest endpoint origin as `<workspace_id>:ingest-origin`
122
126
  and the base URL origin as `<workspace_id>:url-origin`. It deletes both origins first and writes
123
- them last, so a failure part way never pairs an old origin with a new key or token.
127
+ them last, so a failure part way never pairs an old origin with a new key or token. After saving to
128
+ the Secret Service it deletes the same Workspace's items from the kernel keyring. Later commands read
129
+ a Workspace from the kernel keyring when it holds the ingest key there, and from the Secret Service
130
+ otherwise.
124
131
  5. It updates the Claude Code user settings (`~/.claude/settings.json`, or `$CLAUDE_CONFIG_DIR`),
125
132
  changing only these keys:
126
133
  - `env`: the OTel exporter variables, `HARNESSFORCE_URL`, `HARNESSFORCE_ENDPOINT` and
package/dist/bin.js CHANGED
@@ -414,7 +414,52 @@ function takeStashedRuntimeEnv(env, platform) {
414
414
  return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string" && isRuntimeVariable(entry[0], platform)));
415
415
  }
416
416
  //#endregion
417
+ //#region src/shared/messages.ts
418
+ const INIT_MESSAGES = {
419
+ success: "Harnessforceに接続しました。Claude Codeを再起動すると設定が反映されます",
420
+ invalidUrl: "接続先のURLが不正です",
421
+ keychainUnavailable: "OSのキーチェーンを利用できないため、送信キーを保存できません",
422
+ linuxKeychainUnavailable: "Secret Service(GNOME KeyringまたはKWallet)とkernelのkeyringのどちらも使えません。コンテナやCIでは、Workspace用の送信キーをmanaged settingsで配るようOwnerかAdminに依頼してください",
423
+ waitingForSecretService: "Secret Serviceの解錠の確認を待っています(最大60秒)",
424
+ savedToKernelKeyring: "Secret Serviceを使えないため、送信キーをkernelのkeyringに保存しました。再起動するか、一定期間使わないと消えます。消えたら`harnessforce init`を再実行してください",
425
+ tooManyKeys: "keychainにある送信キーが多すぎるため、`harnessforce init`を実行できません",
426
+ tooManyCredentials: "keychainにある送信キーまたはログインの情報が多すぎるため、`harnessforce init`を実行できません",
427
+ settingsUnreadable: "Claude Codeのuser settingsを読めません",
428
+ network: "Harnessforceとの通信に失敗しました。もう一度`harnessforce init`を実行してください",
429
+ listenFailed: "ログインの待ち受けを開始できませんでした",
430
+ timeout: "ログインが時間内に完了しませんでした。もう一度`harnessforce init`を実行してください",
431
+ accessDenied: "ログインが拒否されました",
432
+ noWorkspace: "参加しているWorkspaceがありません。Workspaceを作成するか招待を受諾してから、もう一度`harnessforce init`を実行してください",
433
+ loginFailed: "ログインに失敗しました。もう一度`harnessforce init`を実行してください",
434
+ viewer: "閲覧のみのロールでは送信キーを発行できません。Workspaceの管理者に連絡してください",
435
+ limitOwnerAdmin: "有効な送信キーの数が上限に達しました。送信キーの画面で使っていないキーを失効してください",
436
+ limitMember: "送信キーの上限に達しています。Workspaceの管理者に連絡してください",
437
+ gateUnavailable: "受信側へ反映できませんでした。変更はしていません。もう一度お試しください",
438
+ ingestUnavailable: "この環境はテレメトリを受信しないため、送信キーを発行できません",
439
+ saveFailed: "送信キーを保存できませんでした。もう一度`harnessforce init`を実行してください",
440
+ codexSaveFailed: "Codexの設定を書き込めませんでした。もう一度`harnessforce init`を実行してください",
441
+ contentEnabled: "入力した指示の本文を送る設定を有効にしました。パターンに一致した秘密値と個人情報を伏せ字にしますが、すべてを検出できるとは限りません。保存した本文は、Workspaceのメンバーの中ではあなただけが読め、本文データの保持期間(最大30日)が過ぎると削除されます",
442
+ contentNotOptedIn: "このWorkspaceは本文データをopt-inしていないため、本文を送る設定は有効にしませんでした。Workspaceの設定のデータの保持でopt-inしてから、もう一度`harnessforce init --send-content`を実行してください"
443
+ };
444
+ const codexNotes = (configPath) => [
445
+ `Codexの設定を書き込みました(${configPath})。Codexを再起動すると反映されます`,
446
+ "Codexのhookは、Codexの/hooksで内容をtrustするまで実行されません。/hooksでharnessforceのSessionStartのhookをtrustしてください",
447
+ "HarnessforceのMCPを使う場合は、`codex mcp login harnessforce`でログインしてください"
448
+ ].join("\n");
449
+ const listenFailedMessage = (port) => `${INIT_MESSAGES.listenFailed}。ポート ${port}を使っているプロセスを終えるか、\`harnessforce init --port\`で別のポートを指定してください`;
450
+ const keychainUnavailableMessage = (keychain, message = INIT_MESSAGES.keychainUnavailable) => keychain.unavailableHint ? `${message}。${keychain.unavailableHint}` : message;
451
+ //#endregion
417
452
  //#region src/credentials/keychain.ts
453
+ async function openSingleStore(store) {
454
+ if (!await store.isAvailable()) return void 0;
455
+ return {
456
+ isKernelKeyring: false,
457
+ items: await store.list(),
458
+ set: (account, secret) => store.set(account, secret),
459
+ delete: (account) => store.delete(account),
460
+ deleteElsewhere: async () => {}
461
+ };
462
+ }
418
463
  const KEYCHAIN_SERVICE = "harnessforce";
419
464
  const INGEST_KEY_SUFFIX = ":ingest-key";
420
465
  const API_TOKEN_SUFFIX = ":api-token";
@@ -422,27 +467,139 @@ const ingestKeyAccount = (workspaceId) => `${workspaceId}${INGEST_KEY_SUFFIX}`;
422
467
  const apiTokenAccount = (workspaceId) => `${workspaceId}${API_TOKEN_SUFFIX}`;
423
468
  const ingestOriginAccount = (workspaceId) => `${workspaceId}:ingest-origin`;
424
469
  const urlOriginAccount = (workspaceId) => `${workspaceId}:url-origin`;
470
+ const workspaceAccounts = (workspaceId) => [
471
+ ingestKeyAccount(workspaceId),
472
+ apiTokenAccount(workspaceId),
473
+ ingestOriginAccount(workspaceId),
474
+ urlOriginAccount(workspaceId)
475
+ ];
425
476
  const isIngestKeyAccount = (account) => account.endsWith(INGEST_KEY_SUFFIX);
426
477
  const apiTokenWorkspaceId = (account) => account.endsWith(API_TOKEN_SUFFIX) ? account.slice(0, -10) : void 0;
427
478
  //#endregion
479
+ //#region src/credentials/linux-keychain.ts
480
+ const SECRET_SERVICE_TIMEOUT_MS = 6e4;
481
+ const WAITING_NOTICE_DELAY_MS = 2e3;
482
+ const PROBE_ACCOUNT$1 = "availability-probe";
483
+ const PROBE_VALUE = "harnessforce";
484
+ const succeeds = (operation) => operation().then(() => true, () => false);
485
+ const workspaceOf = (account) => account.slice(0, account.lastIndexOf(":"));
486
+ function createLinuxKeychain(options) {
487
+ return build(options, SECRET_SERVICE_TIMEOUT_MS);
488
+ }
489
+ function build(options, timeoutMs) {
490
+ const { kernel, secretService } = options;
491
+ const locations = /* @__PURE__ */ new Map();
492
+ const locate = (workspaceId) => {
493
+ let located = locations.get(workspaceId);
494
+ if (!located) {
495
+ located = kernel.get(ingestKeyAccount(workspaceId), timeoutMs).then((key) => ({
496
+ location: key ? "kernel" : "secret-service",
497
+ isKernelBroken: false
498
+ }), () => ({
499
+ location: "secret-service",
500
+ isKernelBroken: true
501
+ }));
502
+ locations.set(workspaceId, located);
503
+ }
504
+ return located;
505
+ };
506
+ const storeAt = (location) => location === "kernel" ? kernel : secretService;
507
+ return {
508
+ unavailableHint: options.unavailableHint,
509
+ async isAvailable() {
510
+ return await succeeds(() => kernel.get(PROBE_ACCOUNT$1, timeoutMs)) || await succeeds(() => secretService.get(PROBE_ACCOUNT$1, timeoutMs));
511
+ },
512
+ async get(account) {
513
+ const { location, isKernelBroken } = await locate(workspaceOf(account));
514
+ if (location === "kernel") return kernel.get(account, timeoutMs);
515
+ try {
516
+ return await secretService.get(account, timeoutMs);
517
+ } catch (error) {
518
+ if (isKernelBroken) throw error;
519
+ return;
520
+ }
521
+ },
522
+ async set(account, secret) {
523
+ const { location } = await locate(workspaceOf(account));
524
+ await storeAt(location).set(account, secret, timeoutMs);
525
+ },
526
+ async delete(account) {
527
+ const { location } = await locate(workspaceOf(account));
528
+ await storeAt(location).delete(account, timeoutMs);
529
+ },
530
+ withTimeout: (ms) => build(options, ms),
531
+ openForInit: (init) => openForInit(options, timeoutMs, init)
532
+ };
533
+ }
534
+ async function openForInit({ kernel, secretService }, timeoutMs, { previousWorkspaceId, onWaiting }) {
535
+ const canWrite = (store) => succeeds(async () => {
536
+ await store.set(PROBE_ACCOUNT$1, PROBE_VALUE, timeoutMs);
537
+ await store.delete(PROBE_ACCOUNT$1, timeoutMs);
538
+ });
539
+ const notice = setTimeout(onWaiting, WAITING_NOTICE_DELAY_MS);
540
+ const usesSecretService = await canWrite(secretService).finally(() => clearTimeout(notice));
541
+ const readKernel = async () => {
542
+ if (!previousWorkspaceId) return [];
543
+ const accounts = [ingestKeyAccount(previousWorkspaceId), apiTokenAccount(previousWorkspaceId)];
544
+ const items = [];
545
+ for (const account of accounts) {
546
+ const secret = await kernel.get(account, timeoutMs);
547
+ if (secret !== void 0) items.push({
548
+ account,
549
+ secret
550
+ });
551
+ }
552
+ return items;
553
+ };
554
+ if (usesSecretService) return {
555
+ isKernelKeyring: false,
556
+ items: [...await secretService.list(timeoutMs), ...await readKernel().catch(() => [])],
557
+ set: (account, secret) => secretService.set(account, secret, timeoutMs),
558
+ delete: (account) => secretService.delete(account, timeoutMs),
559
+ async deleteElsewhere(workspaceId) {
560
+ for (const account of workspaceAccounts(workspaceId)) await kernel.delete(account, timeoutMs).catch(() => {});
561
+ }
562
+ };
563
+ if (!await canWrite(kernel)) return void 0;
564
+ return {
565
+ isKernelKeyring: true,
566
+ items: await readKernel(),
567
+ set: (account, secret) => kernel.set(account, secret, timeoutMs),
568
+ delete: (account) => kernel.delete(account, timeoutMs),
569
+ deleteElsewhere: async () => {}
570
+ };
571
+ }
572
+ //#endregion
428
573
  //#region src/credentials/os-keychain.ts
429
- const SUPPORTED_PLATFORMS = /* @__PURE__ */ new Set([
430
- "darwin",
431
- "linux",
432
- "win32"
433
- ]);
434
- const ENTRY_OPTIONS = { linux: { store: "secret-service" } };
435
574
  const PROBE_ACCOUNT = "availability-probe";
436
- function createOsKeychain({ platform, load }) {
575
+ function createOsKeychain({ platform, load, secretService }) {
437
576
  let loaded;
438
577
  const keyring = () => {
439
578
  loaded ??= load();
440
579
  return loaded;
441
580
  };
442
- const entry = async (account) => new (await (keyring())).Entry(KEYCHAIN_SERVICE, account, ENTRY_OPTIONS);
581
+ if (platform === "linux") return createLinuxKeychain({
582
+ kernel: kernelKeyring(keyring),
583
+ secretService,
584
+ unavailableHint: INIT_MESSAGES.linuxKeychainUnavailable
585
+ });
586
+ if (platform === "darwin" || platform === "win32") return singleStore(keyring);
587
+ return unsupported;
588
+ }
589
+ function kernelKeyring(keyring) {
590
+ const entry = async (account) => new (await (keyring())).Entry(KEYCHAIN_SERVICE, account, { linux: { store: "keyutils" } });
443
591
  return {
592
+ get: async (account) => (await entry(account)).getPassword() ?? void 0,
593
+ set: async (account, secret) => (await entry(account)).setPassword(secret),
594
+ delete: async (account) => {
595
+ (await entry(account)).deleteCredential();
596
+ }
597
+ };
598
+ }
599
+ function singleStore(keyring) {
600
+ const entry = async (account) => new (await (keyring())).Entry(KEYCHAIN_SERVICE, account);
601
+ const keychain = {
444
602
  async isAvailable() {
445
- if (!SUPPORTED_PLATFORMS.has(platform)) return false;
446
603
  try {
447
604
  (await entry(PROBE_ACCOUNT)).getPassword();
448
605
  return true;
@@ -450,17 +607,121 @@ function createOsKeychain({ platform, load }) {
450
607
  return false;
451
608
  }
452
609
  },
453
- async get(account) {
454
- return (await entry(account)).getPassword() ?? void 0;
610
+ get: async (account) => (await entry(account)).getPassword() ?? void 0,
611
+ set: async (account, secret) => (await entry(account)).setPassword(secret),
612
+ delete: async (account) => {
613
+ (await entry(account)).deleteCredential();
455
614
  },
456
- async set(account, secret) {
457
- (await entry(account)).setPassword(secret);
615
+ withTimeout: (ms) => limitedSingleStore(keychain, keyring, ms),
616
+ openForInit: () => openSingleStore({
617
+ ...keychain,
618
+ list: async () => (await keyring()).findCredentials(KEYCHAIN_SERVICE).map(({ account, password }) => ({
619
+ account,
620
+ secret: password
621
+ }))
622
+ })
623
+ };
624
+ return keychain;
625
+ }
626
+ function limitedSingleStore(base, keyring, ms) {
627
+ const within = (operation) => async (account) => {
628
+ const entry = new (await (keyring())).AsyncEntry(KEYCHAIN_SERVICE, account);
629
+ let timer;
630
+ const timeout = new Promise((_, reject) => {
631
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("keychain timed out")), ms);
632
+ });
633
+ return Promise.race([operation(entry), timeout]).finally(() => clearTimeout(timer));
634
+ };
635
+ return {
636
+ ...base,
637
+ get: async (account) => await within((entry) => entry.getPassword())(account) ?? void 0,
638
+ set: (account, secret) => within((entry) => entry.setPassword(secret))(account),
639
+ delete: async (account) => {
640
+ await within((entry) => entry.deleteCredential())(account);
641
+ }
642
+ };
643
+ }
644
+ const unsupportedError = async () => {
645
+ throw new Error("no keychain on this platform");
646
+ };
647
+ const unsupported = {
648
+ isAvailable: async () => false,
649
+ get: unsupportedError,
650
+ set: unsupportedError,
651
+ delete: unsupportedError,
652
+ withTimeout: () => unsupported,
653
+ openForInit: async () => void 0
654
+ };
655
+ //#endregion
656
+ //#region src/credentials/secret-service-process.ts
657
+ const CHILD_SCRIPT = `
658
+ const [op, service, account, timeoutMs, modulePath] = process.argv.slice(1);
659
+ setTimeout(() => process.kill(process.pid, "SIGKILL"), Number(timeoutMs));
660
+ const chunks = [];
661
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
662
+ process.stdin.on("end", async () => {
663
+ try {
664
+ const keyring = require(modulePath);
665
+ const options = { linux: { store: "secret-service" } };
666
+ let result = null;
667
+ if (op === "list") {
668
+ result = await keyring.findCredentialsAsync(service);
669
+ } else {
670
+ const entry = new keyring.AsyncEntry(service, account, options);
671
+ if (op === "get") result = (await entry.getPassword()) ?? null;
672
+ if (op === "set") await entry.setPassword(Buffer.concat(chunks).toString("utf8"));
673
+ if (op === "delete") await entry.deleteCredential();
674
+ }
675
+ process.stdout.write(JSON.stringify({ result }), () => process.exit(0));
676
+ } catch {
677
+ process.exit(1);
678
+ }
679
+ });
680
+ `;
681
+ function createSecretServiceProcess({ execPath, resolveKeyring }) {
682
+ const run = (op, account, timeoutMs, input = "") => new Promise((resolve, reject) => {
683
+ const child = spawn(execPath, [
684
+ "-e",
685
+ CHILD_SCRIPT,
686
+ op,
687
+ KEYCHAIN_SERVICE,
688
+ account,
689
+ String(timeoutMs),
690
+ resolveKeyring()
691
+ ], { stdio: [
692
+ "pipe",
693
+ "pipe",
694
+ "ignore"
695
+ ] });
696
+ const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
697
+ const chunks = [];
698
+ child.stdout.on("data", (chunk) => chunks.push(chunk));
699
+ child.on("error", reject);
700
+ child.on("close", (code) => {
701
+ clearTimeout(timer);
702
+ if (code !== 0) return reject(/* @__PURE__ */ new Error(`Secret Service ${op} failed`));
703
+ try {
704
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")).result);
705
+ } catch (error) {
706
+ reject(error);
707
+ }
708
+ });
709
+ child.stdin.on("error", () => {});
710
+ child.stdin.end(input);
711
+ });
712
+ return {
713
+ async get(account, timeoutMs) {
714
+ const secret = await run("get", account, timeoutMs);
715
+ return typeof secret === "string" ? secret : void 0;
458
716
  },
459
- async delete(account) {
460
- (await entry(account)).deleteCredential();
717
+ async set(account, secret, timeoutMs) {
718
+ await run("set", account, timeoutMs, secret);
461
719
  },
462
- async list() {
463
- return (await keyring()).findCredentials(KEYCHAIN_SERVICE).map(({ account, password }) => ({
720
+ async delete(account, timeoutMs) {
721
+ await run("delete", account, timeoutMs);
722
+ },
723
+ async list(timeoutMs) {
724
+ return (await run("list", "", timeoutMs)).map(({ account, password }) => ({
464
725
  account,
465
726
  secret: password
466
727
  }));
@@ -874,6 +1135,7 @@ async function resolveUserDestination(deps) {
874
1135
  const HOOK_USAGE = "Usage: harnessforce hook session-start\n";
875
1136
  const MAX_INPUT_BYTES$1 = 1048576;
876
1137
  const REGISTERING_SOURCES = /* @__PURE__ */ new Set(["startup", "clear"]);
1138
+ const KEYCHAIN_READ_TIMEOUT_MS = 1e3;
877
1139
  const REVOKED_MESSAGE = "送信キーが失効しています。`harnessforce init`を実行してください";
878
1140
  function parseInput(raw) {
879
1141
  let fields;
@@ -892,8 +1154,21 @@ function parseInput(raw) {
892
1154
  };
893
1155
  }
894
1156
  const report$1 = (deps, detail) => deps.stderr(`harnessforce: session registration ${detail}\n`);
1157
+ function resolveWithinLimit(deps) {
1158
+ let timer;
1159
+ const slow = new Promise((resolve) => {
1160
+ timer = setTimeout(() => resolve({
1161
+ kind: "skipped",
1162
+ reason: "no ingest key"
1163
+ }), KEYCHAIN_READ_TIMEOUT_MS);
1164
+ });
1165
+ return Promise.race([resolveUserDestination({
1166
+ ...deps,
1167
+ keychain: deps.keychain.withTimeout(KEYCHAIN_READ_TIMEOUT_MS)
1168
+ }), slow]).finally(() => clearTimeout(timer));
1169
+ }
895
1170
  async function resolveDestination(deps) {
896
- const resolved = await resolveUserDestination(deps);
1171
+ const resolved = await resolveWithinLimit(deps);
897
1172
  if (resolved.kind === "ok") return { destination: resolved.destination };
898
1173
  if (resolved.kind === "none") {
899
1174
  report$1(deps, "skipped (not configured)");
@@ -998,7 +1273,7 @@ function parseStoredApiToken(secret, workspaceId) {
998
1273
  async function verifyCliAccess(deps, messages) {
999
1274
  const { keychain } = deps;
1000
1275
  const stop = (message) => stopWith(messages[message]);
1001
- if (!await keychain.isAvailable().catch(() => false)) stop("keychainUnavailable");
1276
+ if (!await keychain.isAvailable().catch(() => false)) stopWith(keychainUnavailableMessage(keychain, messages.keychainUnavailable));
1002
1277
  const read = (account) => keychain.get(account).catch(() => stop("keychainUnavailable"));
1003
1278
  const destinations = await resolveCliDestinations(deps.env, userSettingsPath(deps.env, deps.homeDir), deps.defaultUrl);
1004
1279
  const workspaceId = destinations.workspaceId ?? stop("initRequired");
@@ -1099,6 +1374,7 @@ async function acquireTokenLock(path, deps) {
1099
1374
  //#endregion
1100
1375
  //#region src/credentials/api-token-session.ts
1101
1376
  const CLIENT_ID$1 = "harnessforce-cli";
1377
+ const LOCKED_KEYCHAIN_TIMEOUT_MS = 1e4;
1102
1378
  const isPast = (instant, now) => now >= Date.parse(instant);
1103
1379
  async function requestRefresh(endpoint, refreshToken, workspaceId, fetchImpl) {
1104
1380
  try {
@@ -1127,11 +1403,11 @@ async function requestRefresh(endpoint, refreshToken, workspaceId, fetchImpl) {
1127
1403
  return { kind: "failed" };
1128
1404
  }
1129
1405
  }
1130
- function tokenStore(deps) {
1406
+ function tokenStore(deps, keychain) {
1131
1407
  const account = apiTokenAccount(deps.workspaceId);
1132
1408
  return {
1133
- read: async () => parseStoredApiToken(await deps.keychain.get(account).catch(() => void 0), deps.workspaceId),
1134
- save: (token) => deps.keychain.set(account, serializeApiToken(token)).then(() => true).catch(() => false)
1409
+ read: async () => parseStoredApiToken(await keychain.get(account).catch(() => void 0), deps.workspaceId),
1410
+ save: (token) => keychain.set(account, serializeApiToken(token)).then(() => true).catch(() => false)
1135
1411
  };
1136
1412
  }
1137
1413
  async function exchange(stored, deps, store) {
@@ -1144,7 +1420,8 @@ async function exchange(stored, deps, store) {
1144
1420
  const reread = await store.read();
1145
1421
  return reread && reread.refreshToken !== stored.refreshToken ? reread : void 0;
1146
1422
  }
1147
- async function refreshUnderLock(used, deps, store) {
1423
+ async function refreshUnderLock(used, deps) {
1424
+ const store = tokenStore(deps, deps.keychain.withTimeout(LOCKED_KEYCHAIN_TIMEOUT_MS));
1148
1425
  const release = await acquireTokenLock(tokenLockPath(deps.homeDir), {
1149
1426
  now: deps.now,
1150
1427
  sleep: deps.sleep
@@ -1159,8 +1436,8 @@ async function refreshUnderLock(used, deps, store) {
1159
1436
  }
1160
1437
  }
1161
1438
  function createApiTokenSession(deps) {
1162
- const store = tokenStore(deps);
1163
- const refresh = (used) => refreshUnderLock(used, deps, store);
1439
+ const store = tokenStore(deps, deps.keychain);
1440
+ const refresh = (used) => refreshUnderLock(used, deps);
1164
1441
  let current;
1165
1442
  async function withToken(request, isUnauthorized) {
1166
1443
  let token = current ?? await store.read();
@@ -1206,37 +1483,6 @@ function createApiTokenSession(deps) {
1206
1483
  };
1207
1484
  }
1208
1485
  //#endregion
1209
- //#region src/shared/messages.ts
1210
- const INIT_MESSAGES = {
1211
- success: "Harnessforceに接続しました。Claude Codeを再起動すると設定が反映されます",
1212
- invalidUrl: "接続先のURLが不正です",
1213
- keychainUnavailable: "OSのキーチェーンを利用できないため、送信キーを保存できません",
1214
- tooManyKeys: "keychainにある送信キーが多すぎるため、`harnessforce init`を実行できません",
1215
- tooManyCredentials: "keychainにある送信キーまたはログインの情報が多すぎるため、`harnessforce init`を実行できません",
1216
- settingsUnreadable: "Claude Codeのuser settingsを読めません",
1217
- network: "Harnessforceとの通信に失敗しました。もう一度`harnessforce init`を実行してください",
1218
- listenFailed: "ログインの待ち受けを開始できませんでした",
1219
- timeout: "ログインが時間内に完了しませんでした。もう一度`harnessforce init`を実行してください",
1220
- accessDenied: "ログインが拒否されました",
1221
- noWorkspace: "参加しているWorkspaceがありません。Workspaceを作成するか招待を受諾してから、もう一度`harnessforce init`を実行してください",
1222
- loginFailed: "ログインに失敗しました。もう一度`harnessforce init`を実行してください",
1223
- viewer: "閲覧のみのロールでは送信キーを発行できません。Workspaceの管理者に連絡してください",
1224
- limitOwnerAdmin: "有効な送信キーの数が上限に達しました。送信キーの画面で使っていないキーを失効してください",
1225
- limitMember: "送信キーの上限に達しています。Workspaceの管理者に連絡してください",
1226
- gateUnavailable: "受信側へ反映できませんでした。変更はしていません。もう一度お試しください",
1227
- ingestUnavailable: "この環境はテレメトリを受信しないため、送信キーを発行できません",
1228
- saveFailed: "送信キーを保存できませんでした。もう一度`harnessforce init`を実行してください",
1229
- codexSaveFailed: "Codexの設定を書き込めませんでした。もう一度`harnessforce init`を実行してください",
1230
- contentEnabled: "入力した指示の本文を送る設定を有効にしました。パターンに一致した秘密値と個人情報を伏せ字にしますが、すべてを検出できるとは限りません。保存した本文は、Workspaceのメンバーの中ではあなただけが読め、本文データの保持期間(最大30日)が過ぎると削除されます",
1231
- contentNotOptedIn: "このWorkspaceは本文データをopt-inしていないため、本文を送る設定は有効にしませんでした。Workspaceの設定のデータの保持でopt-inしてから、もう一度`harnessforce init --send-content`を実行してください"
1232
- };
1233
- const codexNotes = (configPath) => [
1234
- `Codexの設定を書き込みました(${configPath})。Codexを再起動すると反映されます`,
1235
- "Codexのhookは、Codexの/hooksで内容をtrustするまで実行されません。/hooksでharnessforceのSessionStartのhookをtrustしてください",
1236
- "HarnessforceのMCPを使う場合は、`codex mcp login harnessforce`でログインしてください"
1237
- ].join("\n");
1238
- const listenFailedMessage = (port) => `${INIT_MESSAGES.listenFailed}。ポート ${port}を使っているプロセスを終えるか、\`harnessforce init --port\`で別のポートを指定してください`;
1239
- //#endregion
1240
1486
  //#region src/import/read-api.ts
1241
1487
  const FAILED$1 = { kind: "failed" };
1242
1488
  async function getJson(url, apiToken, fetchImpl) {
@@ -8898,8 +9144,13 @@ const stop$1 = (message) => stopWith(INIT_MESSAGES[message]);
8898
9144
  const sha256Hex = (value) => createHash("sha256").update(value, "utf8").digest("hex");
8899
9145
  function init(args, deps) {
8900
9146
  return runUntilStop(async () => {
8901
- const codexPath = await runInit(args, deps);
8902
- deps.stdout(`${INIT_MESSAGES.success}\n${codexNotes(codexPath)}\n`);
9147
+ const { codexPath, isKernelKeyring } = await runInit(args, deps);
9148
+ const lines = [
9149
+ INIT_MESSAGES.success,
9150
+ ...isKernelKeyring ? [INIT_MESSAGES.savedToKernelKeyring] : [],
9151
+ codexNotes(codexPath)
9152
+ ];
9153
+ deps.stdout(`${lines.join("\n")}\n`);
8903
9154
  return 0;
8904
9155
  }, deps.stderr);
8905
9156
  }
@@ -8907,8 +9158,9 @@ async function runInit(args, deps) {
8907
9158
  const port = args.port ?? DEFAULT_PORT;
8908
9159
  const base = parseAllowedUrl(args.url ?? deps.defaultUrl) ?? stop$1("invalidUrl");
8909
9160
  const recordedUrl = withoutExtras(base);
8910
- const revoke = await readRevokeHashes(deps.keychain);
8911
9161
  const settingsPath = userSettingsPath(deps.env, deps.homeDir);
9162
+ const store = await openKeychain(deps, (await readSettingsEnv$1(settingsPath)).HARNESSFORCE_WORKSPACE_ID);
9163
+ const revoke = revokeHashes(store);
8912
9164
  if ((await readUserSettings(settingsPath)).kind === "invalid") stop$1("settingsUnreadable");
8913
9165
  const authorizationEndpoint = await discoverAuthorizationEndpoint(base, deps.fetch) ?? stop$1("network");
8914
9166
  const secrets = createLoginSecrets();
@@ -8921,18 +9173,25 @@ async function runInit(args, deps) {
8921
9173
  revoke_key_hashes: revoke.keyHashes,
8922
9174
  revoke_api_token_hashes: revoke.apiTokenHashes
8923
9175
  }, deps.fetch));
8924
- await save(issued, recordedUrl, settingsPath, args.sendContent, deps).catch((error) => {
9176
+ await save(issued, recordedUrl, settingsPath, args.sendContent, store).catch((error) => {
8925
9177
  if (isStop(error)) throw error;
8926
9178
  return stop$1("saveFailed");
8927
9179
  });
8928
9180
  const codexPath = codexConfigPath(deps.env, deps.homeDir);
8929
9181
  await saveCodexConfig(issued, recordedUrl, codexPath, args.sendContent).catch(() => stop$1("codexSaveFailed"));
8930
9182
  if (args.sendContent) deps.stdout(`${issued.contentOptIn ? INIT_MESSAGES.contentEnabled : INIT_MESSAGES.contentNotOptedIn}\n`);
8931
- return codexPath;
9183
+ return {
9184
+ codexPath,
9185
+ isKernelKeyring: store.isKernelKeyring
9186
+ };
8932
9187
  }
8933
- async function readRevokeHashes(keychain) {
8934
- if (!await keychain.isAvailable()) stop$1("keychainUnavailable");
8935
- const items = await keychain.list().catch(() => void 0) ?? stop$1("keychainUnavailable");
9188
+ async function openKeychain(deps, previousWorkspaceId) {
9189
+ return await deps.keychain.openForInit({
9190
+ previousWorkspaceId,
9191
+ onWaiting: () => deps.stderr(`${INIT_MESSAGES.waitingForSecretService}\n`)
9192
+ }).catch(() => stop$1("keychainUnavailable")) ?? stopWith(keychainUnavailableMessage(deps.keychain));
9193
+ }
9194
+ function revokeHashes({ items }) {
8936
9195
  const keyHashes = items.filter((item) => isIngestKeyAccount(item.account)).map((item) => sha256Hex(item.secret));
8937
9196
  if (keyHashes.length > MAX_REVOKE_HASHES) stop$1("tooManyKeys");
8938
9197
  const apiTokenHashes = items.flatMap((item) => {
@@ -8985,13 +9244,14 @@ function issuedOrStop(outcome) {
8985
9244
  case "unexpected": return stop$1("network");
8986
9245
  }
8987
9246
  }
8988
- async function save(issued, connection, settingsPath, sendContent, deps) {
8989
- await deps.keychain.delete(ingestOriginAccount(issued.workspaceId));
8990
- await deps.keychain.delete(urlOriginAccount(issued.workspaceId));
8991
- await deps.keychain.set(ingestKeyAccount(issued.workspaceId), issued.ingestKey);
8992
- await deps.keychain.set(apiTokenAccount(issued.workspaceId), serializeApiToken(issued.apiToken));
8993
- await deps.keychain.set(ingestOriginAccount(issued.workspaceId), new URL(issued.ingestEndpoint).origin);
8994
- await deps.keychain.set(urlOriginAccount(issued.workspaceId), new URL(connection).origin);
9247
+ async function save(issued, connection, settingsPath, sendContent, store) {
9248
+ await store.delete(ingestOriginAccount(issued.workspaceId));
9249
+ await store.delete(urlOriginAccount(issued.workspaceId));
9250
+ await store.set(ingestKeyAccount(issued.workspaceId), issued.ingestKey);
9251
+ await store.set(apiTokenAccount(issued.workspaceId), serializeApiToken(issued.apiToken));
9252
+ await store.set(ingestOriginAccount(issued.workspaceId), new URL(issued.ingestEndpoint).origin);
9253
+ await store.set(urlOriginAccount(issued.workspaceId), new URL(connection).origin);
9254
+ await store.deleteElsewhere(issued.workspaceId);
8995
9255
  const current = await readUserSettings(settingsPath);
8996
9256
  if (current.kind === "invalid") return stop$1("saveFailed");
8997
9257
  const env = {
@@ -9035,14 +9295,17 @@ async function otelHeaders(env, managedDir, keychain, stdout, stderr) {
9035
9295
  stdout(`${JSON.stringify({ Authorization: `Bearer ${selection.key}` })}\n`);
9036
9296
  return 0;
9037
9297
  }
9038
- async function selectKey(env, managedDir, keychain) {
9298
+ const HOOK_READ_TIMEOUT_MS = 1e3;
9299
+ async function selectKey(env, managedDir, unlimited) {
9039
9300
  const managed = await readManagedEnv(managedDir, ["HARNESSFORCE_INGEST_KEY"]);
9040
9301
  if (managed.HARNESSFORCE_INGEST_KEY) return {
9041
9302
  kind: "key",
9042
9303
  key: managed.HARNESSFORCE_INGEST_KEY
9043
9304
  };
9044
9305
  const workspaceId = env.HARNESSFORCE_WORKSPACE_ID;
9045
- if (!workspaceId || !await keychain.isAvailable()) return { kind: "none" };
9306
+ if (!workspaceId) return { kind: "none" };
9307
+ const keychain = unlimited.withTimeout(HOOK_READ_TIMEOUT_MS);
9308
+ if (!await keychain.isAvailable()) return { kind: "none" };
9046
9309
  const key = await keychain.get(ingestKeyAccount(workspaceId));
9047
9310
  if (!key) return { kind: "none" };
9048
9311
  const pinnedOrigin = await keychain.get(ingestOriginAccount(workspaceId));
@@ -12772,7 +13035,8 @@ function spawnUsageSender(payload) {
12772
13035
  }
12773
13036
  //#endregion
12774
13037
  //#region src/bin.ts
12775
- const { url: defaultUrl } = createRequire(import.meta.url)("./build-config.json");
13038
+ const require = createRequire(import.meta.url);
13039
+ const { url: defaultUrl } = require("./build-config.json");
12776
13040
  const CALLBACK_TIMEOUT_MS = 3e5;
12777
13041
  const argv = process.argv.slice(2);
12778
13042
  const restoredEnv = takeStashedRuntimeEnv(process.env, process.platform);
@@ -12787,7 +13051,11 @@ process.exitCode = relaunched ?? await run(argv, {
12787
13051
  env: process.env,
12788
13052
  keychain: createOsKeychain({
12789
13053
  platform: process.platform,
12790
- load: () => import("@napi-rs/keyring")
13054
+ load: () => import("@napi-rs/keyring"),
13055
+ secretService: createSecretServiceProcess({
13056
+ execPath: process.execPath,
13057
+ resolveKeyring: () => require.resolve("@napi-rs/keyring")
13058
+ })
12791
13059
  }),
12792
13060
  stdout: (text) => process.stdout.write(text),
12793
13061
  stderr: (text) => process.stderr.write(text),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harnessforce/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Harnessforce command line (harnessforce)",
5
5
  "keywords": [
6
6
  "harnessforce",