@cocorograph/hub-agent 0.5.24 → 0.5.26

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.5.24",
3
+ "version": "0.5.26",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -214,6 +214,183 @@ ensure_npm_user_prefix() {
214
214
  done
215
215
  }
216
216
 
217
+ # =============================================================================
218
+ # Node TLS 環境の自動修復
219
+ #
220
+ # 背景: npm install で `UNABLE_TO_GET_ISSUER_CERT_LOCALLY` が出る環境がある。
221
+ # 原因は「node は OS の信頼ストアを使わず、自分にコンパイル時に焼き込まれた
222
+ # CA リストだけで TLS 検証する」設計にある:
223
+ #
224
+ # - curl は macOS Keychain (or /etc/ssl/certs) を使う → 通る
225
+ # - node はバンドル CA のみ → 通らない
226
+ #
227
+ # テナント環境では以下のいずれかで MITM 的な証明書差し替えが起きうる:
228
+ # - ウィルス対策ソフト (Sophos / Trend Micro / Norton / Symantec / Kaspersky 等)
229
+ # - 企業の SSL インスペクション proxy (ZScaler / Cloudflare WARP for Teams 等)
230
+ # - VPN クライアントによるトラフィック検査
231
+ # - 広告ブロッカー (NextDNS / AdGuard / 1.1.1.1 等)
232
+ # - 親会社配布のセキュリティアプリ
233
+ #
234
+ # どれもユーザー操作なしには検出しにくく、ユーザー自身も気づいていないことが多い。
235
+ #
236
+ # 対処: ユーザーが既に OS で信頼している CA バンドルを node にも渡せば、curl と
237
+ # node の信頼ストアの乖離が解消する。`NODE_EXTRA_CA_CERTS` 環境変数を使えば
238
+ # node のバンドル CA に「追加で」信頼する証明書を渡せる (バンドル CA を置き換える
239
+ # わけではないので、通常環境への副作用はない)。
240
+ #
241
+ # 1. pre-flight check で node の TLS が通るかテスト
242
+ # 2. 失敗時のみ、OS の信頼ストアを PEM に書き出して NODE_EXTRA_CA_CERTS にセット
243
+ # 3. シェル profile (.zprofile / .bash_profile) にも追記して永続化
244
+ # 4. 再テスト → ダメなら明確な日本語エラーガイドで終了
245
+ # =============================================================================
246
+
247
+ # node 側 TLS で npm registry に到達できるかテスト。成功で 0、失敗で 1。
248
+ # stderr の最後の行を grep 用に echo するので、呼び出し側で原因種別を判定できる。
249
+ _test_node_tls() {
250
+ if ! present node; then
251
+ return 1
252
+ fi
253
+ # node -e で出る ERR_TLS_CERT_ALTNAME_INVALID 等の他種別エラーは別途扱う必要が
254
+ # あるため戻り値だけで判定。stdout/stderr は呼び出し側で破棄してよい。
255
+ node -e "require('https').get('https://registry.npmjs.org/', r => { process.exit(r.statusCode >= 400 ? 1 : 0); }).on('error', e => { console.error(e.code || e.message); process.exit(1); });" 2>&1
256
+ }
257
+
258
+ # OS の信頼ストア (system / login keychain or /etc/ssl/certs) を PEM に書き出す。
259
+ # 返り値: 成功なら 0 + ファイルパスを stdout に echo / 失敗なら 1。
260
+ _export_os_ca_bundle() {
261
+ local out_pem="$HOME/.hub-agent-ca.pem"
262
+ case "$(uname -s)" in
263
+ Darwin)
264
+ # System.keychain には企業配布 CA や手動追加 CA、SystemRootCertificates.keychain
265
+ # には Apple 配布の標準 root CA が入っている。両方をマージ。
266
+ # `-p` で PEM 形式、`-a` で全件出力。pemcat に近い操作。
267
+ {
268
+ security find-certificate -a -p /Library/Keychains/System.keychain 2>/dev/null || true
269
+ security find-certificate -a -p /System/Library/Keychains/SystemRootCertificates.keychain 2>/dev/null || true
270
+ } > "$out_pem"
271
+ ;;
272
+ Linux)
273
+ # Debian/Ubuntu 系
274
+ if [[ -r /etc/ssl/certs/ca-certificates.crt ]]; then
275
+ cp /etc/ssl/certs/ca-certificates.crt "$out_pem"
276
+ # RHEL/CentOS/Fedora 系
277
+ elif [[ -r /etc/pki/tls/certs/ca-bundle.crt ]]; then
278
+ cp /etc/pki/tls/certs/ca-bundle.crt "$out_pem"
279
+ # SUSE 系
280
+ elif [[ -r /var/lib/ca-certificates/ca-bundle.pem ]]; then
281
+ cp /var/lib/ca-certificates/ca-bundle.pem "$out_pem"
282
+ else
283
+ return 1
284
+ fi
285
+ ;;
286
+ *)
287
+ return 1
288
+ ;;
289
+ esac
290
+ if [[ ! -s "$out_pem" ]]; then
291
+ rm -f "$out_pem"
292
+ return 1
293
+ fi
294
+ echo "$out_pem"
295
+ }
296
+
297
+ # シェル profile に `export NODE_EXTRA_CA_CERTS=...` を追記する。
298
+ # 既に同じパス指定の export 行があればスキップ (idempotent)。
299
+ _persist_node_extra_ca_certs() {
300
+ local ca_path="$1"
301
+ local snippet="export NODE_EXTRA_CA_CERTS=\"$ca_path\""
302
+ local marker="# >>> hub-agent: NODE_EXTRA_CA_CERTS (TLS fallback) >>>"
303
+ local end_marker="# <<< hub-agent: NODE_EXTRA_CA_CERTS <<<"
304
+ local target
305
+ for target in "$HOME/.zprofile" "$HOME/.bash_profile"; do
306
+ [[ -e "$target" ]] || touch "$target"
307
+ if grep -Fq "$snippet" "$target" 2>/dev/null; then
308
+ continue
309
+ fi
310
+ {
311
+ printf '\n%s\n' "$marker"
312
+ printf '%s\n' "$snippet"
313
+ printf '%s\n' "$end_marker"
314
+ } >> "$target"
315
+ color_ok "$target に NODE_EXTRA_CA_CERTS を追記"
316
+ done
317
+ }
318
+
319
+ # 「UNABLE_TO_GET_ISSUER_CERT_LOCALLY が出た時に何を確認すべきか」のガイド表示。
320
+ # フォールバックも効かなかった最終手段ケース用。
321
+ _print_tls_failure_guidance() {
322
+ # ヘッダーだけ printf で色付け。本文は cat <<EOF で複数行を読みやすく出す。
323
+ printf '\n\033[1;31m✗ node の TLS 検証が修復できませんでした。\033[0m\n'
324
+ cat <<EOF
325
+
326
+ curl は通るのに node だけ落ちる場合、お使いの Mac/PC に
327
+ HTTPS 通信を検査しているソフトが入っている可能性が高いです。
328
+ 典型例:
329
+ - ウィルス対策ソフト (Sophos / Norton / Trend Micro / Symantec 等)
330
+ - 親会社配布のセキュリティアプリ
331
+ - SSL インスペクション機能つきの VPN クライアント
332
+ - 広告ブロッカー (NextDNS / 1.1.1.1 for Families / AdGuard 等)
333
+
334
+ 以下のコマンドで「実際の証明書発行者」が見えます。
335
+ ここに表示される \`issuer\` が \`Let's Encrypt\` や \`DigiCert\` ではなく、
336
+ 特定のソフト名 (ZScaler / Cocorograph / 製品名 等) なら、それが原因です:
337
+
338
+ node -e 'const t=require("tls");const s=t.connect(443,"registry.npmjs.org",{servername:"registry.npmjs.org",rejectUnauthorized:false},()=>{let c=s.getPeerCertificate(true);while(c&&Object.keys(c).length){console.log("issuer:",JSON.stringify(c.issuer));if(!c.issuerCertificate||c.issuerCertificate===c)break;c=c.issuerCertificate;}s.end();});'
339
+
340
+ 暫定的に install を進めたい場合(自分の環境を信頼している前提):
341
+
342
+ npm install -g $PACKAGE_NAME --strict-ssl=false
343
+ npm install -g $CLAUDE_CODE_PACKAGE --strict-ssl=false
344
+ hub-agent enroll <token> --hub-url <hub-url>
345
+ hub-agent install-service
346
+
347
+ 詳細サポートは Hub の cockpit チャネルへ。
348
+
349
+ EOF
350
+ }
351
+
352
+ # pre-flight TLS 検査 + 自動 fallback の本体。
353
+ # main() から ensure_npm_user_prefix の直後で呼ぶ想定。
354
+ ensure_node_tls_works() {
355
+ # Linux の中には node を持たない経路 (本スクリプトより前に node が入る) もあるので
356
+ # node が無ければスキップ (後段の ensure_global_install で別エラーになる)。
357
+ present node || return 0
358
+
359
+ color_step "node の TLS 検証を pre-flight check"
360
+
361
+ # ステップ 0: brew link 直後はシェル command hash に古い node/npm パスが残る
362
+ # ことがあるので、念のためクリアしてから検査する。
363
+ hash -r 2>/dev/null || true
364
+
365
+ if _test_node_tls >/dev/null 2>&1; then
366
+ color_ok "node TLS 検証 OK (registry.npmjs.org に到達可能)"
367
+ return 0
368
+ fi
369
+
370
+ color_warn "node TLS 検証失敗 → OS 信頼ストアから CA をエクスポートして再試行"
371
+
372
+ local ca_pem
373
+ if ! ca_pem=$(_export_os_ca_bundle); then
374
+ color_err "OS の信頼ストア (macOS Keychain / Linux ca-certificates) からの CA エクスポートに失敗"
375
+ _print_tls_failure_guidance
376
+ exit 1
377
+ fi
378
+ color_ok "$ca_pem に OS 信頼ストアの CA を書き出し ($(wc -l < "$ca_pem" | tr -d ' ') 行)"
379
+
380
+ export NODE_EXTRA_CA_CERTS="$ca_pem"
381
+ _persist_node_extra_ca_certs "$ca_pem"
382
+
383
+ # 再テスト
384
+ if _test_node_tls >/dev/null 2>&1; then
385
+ color_ok "node TLS 検証 OK (NODE_EXTRA_CA_CERTS=$ca_pem 経由)"
386
+ return 0
387
+ fi
388
+
389
+ color_err "OS 信頼ストアを渡しても node TLS 検証が通りません"
390
+ _print_tls_failure_guidance
391
+ exit 1
392
+ }
393
+
217
394
  ensure_pkg() {
218
395
  local cmd="$1"
219
396
  local brew_pkg="$2"
@@ -371,6 +548,11 @@ main() {
371
548
  ensure_pkg tmux tmux tmux
372
549
  ensure_node_version
373
550
  ensure_npm_user_prefix
551
+ # ensure_node_version 直後だと brew link でシェルの command hash がズレている
552
+ # 場合があるため、prefix 設定後にまとめて TLS pre-flight check + 自動 fallback
553
+ # を行う。失敗時はガイダンス付きで exit するので、後段の npm install で TLS
554
+ # エラーを再度浴びる経路はカットされる。
555
+ ensure_node_tls_works
374
556
  ensure_global_install
375
557
  ensure_claude_code
376
558
  do_enroll
@@ -41,11 +41,43 @@ async function readTemplate(name) {
41
41
  return fs.readFile(p, "utf-8")
42
42
  }
43
43
 
44
+ function escapeXmlText(s) {
45
+ // plist の <string> 中身を埋めるための最小エスケープ。
46
+ // パスに XML 特殊文字が入っているケースは現実にはほぼ無いが、念のため。
47
+ return String(s)
48
+ .replaceAll("&", "&amp;")
49
+ .replaceAll("<", "&lt;")
50
+ .replaceAll(">", "&gt;")
51
+ }
52
+
44
53
  function expandTemplate(text, hubAgentBin) {
54
+ // hub-agent デーモンの runtime 環境に必須な追加 env を、各プラットフォームの
55
+ // 起動ファイル形式 (plist / systemd) に合わせて差し込むためのプレースホルダ展開。
56
+ //
57
+ // NODE_EXTRA_CA_CERTS: install.sh の TLS pre-flight で OS Keychain → PEM
58
+ // エクスポート経由で設定された場合 (e.g. MITM 環境)、シェル profile の export
59
+ // だけだと launchd / systemd には届かない。install-service 実行時の env に
60
+ // 残っている値を plist/unit に永続化する。未 set なら関連行を空に置換するので
61
+ // 通常環境への副作用はゼロ。
62
+ const nodeExtraCa = process.env.NODE_EXTRA_CA_CERTS || ""
63
+ let plistEntry = ""
64
+ let systemdLine = ""
65
+ if (nodeExtraCa) {
66
+ // plist 側: EnvironmentVariables dict の inside に <key>...</key><string>...</string>
67
+ // 2 行を追加 (インデント 4 スペースは既存 PATH/HOME と揃える)。末尾に \n を付けて
68
+ // テンプレートの "__...__\n" を綺麗に消費する。
69
+ plistEntry =
70
+ ` <key>NODE_EXTRA_CA_CERTS</key>\n` +
71
+ ` <string>${escapeXmlText(nodeExtraCa)}</string>\n`
72
+ // systemd 側: Environment=KEY=VALUE の 1 行。末尾 \n でテンプレ "__...__\n" を吸収。
73
+ systemdLine = `Environment=NODE_EXTRA_CA_CERTS=${nodeExtraCa}\n`
74
+ }
45
75
  return text
46
76
  .replaceAll("__HUB_AGENT_BIN__", hubAgentBin)
47
77
  .replaceAll("__HOME__", os.homedir())
48
78
  .replaceAll("__PATH__", process.env.PATH || "/usr/local/bin:/usr/bin:/bin")
79
+ .replaceAll("__NODE_EXTRA_CA_CERTS_PLIST_ENTRY__", plistEntry)
80
+ .replaceAll("__NODE_EXTRA_CA_CERTS_SYSTEMD_LINE__", systemdLine)
49
81
  }
50
82
 
51
83
  async function ensureDir(p) {
@@ -31,13 +31,21 @@
31
31
  <key>WorkingDirectory</key>
32
32
  <string>__HOME__</string>
33
33
 
34
+ <!--
35
+ EnvironmentVariables は launchd 起動時の env を完全に置き換える (ログイン
36
+ シェルや .zprofile の export を継承しない)。シェル profile に export した
37
+ cert 系 env 等を agent デーモンに届けるには plist に明示する必要がある。
38
+ 下の placeholder 行は install-service 実行時に expandTemplate() が「env が
39
+ set されていれば NODE_EXTRA_CA_CERTS entry に、未 set なら空文字列に」展開する。
40
+ 未 set 時は当該行ごと消えるので、通常環境への副作用はゼロ。
41
+ -->
34
42
  <key>EnvironmentVariables</key>
35
43
  <dict>
36
44
  <key>PATH</key>
37
45
  <string>__PATH__</string>
38
46
  <key>HOME</key>
39
47
  <string>__HOME__</string>
40
- </dict>
48
+ __NODE_EXTRA_CA_CERTS_PLIST_ENTRY__ </dict>
41
49
 
42
50
  <!-- KeepAlive で過剰再起動した時に 10 秒スロットルする -->
43
51
  <key>ThrottleInterval</key>
@@ -5,9 +5,14 @@ Wants=network-online.target
5
5
 
6
6
  [Service]
7
7
  Type=simple
8
+ # Environment は systemd ユニット起動時の env を明示指定する (ログインシェルや
9
+ # .bashrc / .profile の export を継承しないため、agent デーモンに必要な env は
10
+ # ここに書く必要がある)。下の placeholder 行は install-service 実行時に
11
+ # expandTemplate() が「env が set されていれば Environment=NODE_EXTRA_CA_CERTS=...
12
+ # 行に、未 set なら空文字列に」展開する。
8
13
  Environment=PATH=__PATH__
9
14
  Environment=HOME=__HOME__
10
- WorkingDirectory=__HOME__
15
+ __NODE_EXTRA_CA_CERTS_SYSTEMD_LINE__WorkingDirectory=__HOME__
11
16
  ExecStart=__HUB_AGENT_BIN__ start
12
17
  Restart=always
13
18
  RestartSec=10