@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.
- package/README.md +13 -6
- package/dist/bin.js +343 -75
- 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
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
|
454
|
-
|
|
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
|
-
|
|
457
|
-
|
|
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
|
|
460
|
-
|
|
717
|
+
async set(account, secret, timeoutMs) {
|
|
718
|
+
await run("set", account, timeoutMs, secret);
|
|
461
719
|
},
|
|
462
|
-
async
|
|
463
|
-
|
|
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
|
|
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))
|
|
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
|
|
1134
|
-
save: (token) =>
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
9183
|
+
return {
|
|
9184
|
+
codexPath,
|
|
9185
|
+
isKernelKeyring: store.isKernelKeyring
|
|
9186
|
+
};
|
|
8932
9187
|
}
|
|
8933
|
-
async function
|
|
8934
|
-
|
|
8935
|
-
|
|
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,
|
|
8989
|
-
await
|
|
8990
|
-
await
|
|
8991
|
-
await
|
|
8992
|
-
await
|
|
8993
|
-
await
|
|
8994
|
-
await
|
|
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
|
-
|
|
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
|
|
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
|
|
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),
|