@ait-co/devtools 0.1.143 → 0.2.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.
Files changed (223) hide show
  1. package/README.en.md +65 -163
  2. package/README.md +65 -192
  3. package/dist/in-app/auto.d.ts +1 -138
  4. package/dist/in-app/auto.js +28 -1102
  5. package/dist/in-app/auto.js.map +1 -1
  6. package/dist/in-app/index.d.ts +38 -547
  7. package/dist/in-app/index.d.ts.map +1 -1
  8. package/dist/in-app/index.js +62 -939
  9. package/dist/in-app/index.js.map +1 -1
  10. package/dist/mcp/cli.d.ts +1 -54
  11. package/dist/mcp/cli.js +33 -9720
  12. package/dist/mcp/cli.js.map +1 -1
  13. package/dist/mcp/server.d.ts +1 -88
  14. package/dist/mcp/server.js +36 -1076
  15. package/dist/mcp/server.js.map +1 -1
  16. package/dist/mock/index.d.ts +19 -20
  17. package/dist/mock/index.d.ts.map +1 -1
  18. package/dist/mock/index.js.map +1 -1
  19. package/dist/optional-peers-CDEFhlhJ.cjs +131 -0
  20. package/dist/optional-peers-CDEFhlhJ.cjs.map +1 -0
  21. package/dist/optional-peers-FdVbUJst.js +96 -0
  22. package/dist/optional-peers-FdVbUJst.js.map +1 -0
  23. package/dist/panel/index.js +1 -103
  24. package/dist/panel/index.js.map +1 -1
  25. package/dist/relay-url-store-CPZAn-T5.js +107 -0
  26. package/dist/relay-url-store-CPZAn-T5.js.map +1 -0
  27. package/dist/relay-url-store-DLjlvMSA.cjs +108 -0
  28. package/dist/relay-url-store-DLjlvMSA.cjs.map +1 -0
  29. package/dist/stubs/bin-devtools-mcp.js +58 -0
  30. package/dist/stubs/bin-devtools-mcp.js.map +1 -0
  31. package/dist/stubs/bin-devtools-test.d.ts +2 -0
  32. package/dist/stubs/bin-devtools-test.js +55 -0
  33. package/dist/stubs/bin-devtools-test.js.map +1 -0
  34. package/dist/test-runner/config.d.ts +1 -231
  35. package/dist/test-runner/config.js +41 -45
  36. package/dist/test-runner/config.js.map +1 -1
  37. package/dist/{tunnel-BVSMXctM.cjs → tunnel-BKZkOyQp.cjs} +27 -75
  38. package/dist/tunnel-BKZkOyQp.cjs.map +1 -0
  39. package/dist/{tunnel-D7xkimBu.js → tunnel-CqSCIrdU.js} +27 -75
  40. package/dist/tunnel-CqSCIrdU.js.map +1 -0
  41. package/dist/unplugin/index.cjs +17 -30
  42. package/dist/unplugin/index.cjs.map +1 -1
  43. package/dist/unplugin/index.d.cts +34 -5
  44. package/dist/unplugin/index.d.cts.map +1 -1
  45. package/dist/unplugin/index.d.ts +35 -6
  46. package/dist/unplugin/index.d.ts.map +1 -1
  47. package/dist/unplugin/index.js +17 -30
  48. package/dist/unplugin/index.js.map +1 -1
  49. package/dist/unplugin/tunnel.cjs +61 -74
  50. package/dist/unplugin/tunnel.cjs.map +1 -1
  51. package/dist/unplugin/tunnel.d.cts +22 -16
  52. package/dist/unplugin/tunnel.d.cts.map +1 -1
  53. package/dist/unplugin/tunnel.d.ts +22 -16
  54. package/dist/unplugin/tunnel.d.ts.map +1 -1
  55. package/dist/unplugin/tunnel.js +61 -74
  56. package/dist/unplugin/tunnel.js.map +1 -1
  57. package/package.json +17 -22
  58. package/dist/attach-orchestrator-0F0m_UqQ.js +0 -1845
  59. package/dist/attach-orchestrator-0F0m_UqQ.js.map +0 -1
  60. package/dist/attach-orchestrator-D65KxFy_.js +0 -1831
  61. package/dist/attach-orchestrator-D65KxFy_.js.map +0 -1
  62. package/dist/attach-orchestrator-DL3NQ9ca.js +0 -1846
  63. package/dist/attach-orchestrator-DL3NQ9ca.js.map +0 -1
  64. package/dist/bundle-C796JIwG.d.ts +0 -159
  65. package/dist/bundle-C796JIwG.d.ts.map +0 -1
  66. package/dist/capture-DsP525OZ.d.ts +0 -58
  67. package/dist/capture-DsP525OZ.d.ts.map +0 -1
  68. package/dist/cdp-connection-rP1WdnH5.d.ts +0 -287
  69. package/dist/cdp-connection-rP1WdnH5.d.ts.map +0 -1
  70. package/dist/cell-BaLvusOl.js +0 -68
  71. package/dist/cell-BaLvusOl.js.map +0 -1
  72. package/dist/cell-CBUS3-nT.js +0 -274
  73. package/dist/cell-CBUS3-nT.js.map +0 -1
  74. package/dist/cell-EBKKpAAT.js +0 -307
  75. package/dist/cell-EBKKpAAT.js.map +0 -1
  76. package/dist/chii-relay-BZ3HqWL5.js +0 -304
  77. package/dist/chii-relay-BZ3HqWL5.js.map +0 -1
  78. package/dist/chii-relay-D7eK2acz.cjs +0 -304
  79. package/dist/chii-relay-D7eK2acz.cjs.map +0 -1
  80. package/dist/debug-server-B3ABDrRI.js +0 -456
  81. package/dist/debug-server-B3ABDrRI.js.map +0 -1
  82. package/dist/debug-server-BWhwrVXa.js +0 -1158
  83. package/dist/debug-server-BWhwrVXa.js.map +0 -1
  84. package/dist/debug-server-CfQNxxGW.js +0 -600
  85. package/dist/debug-server-CfQNxxGW.js.map +0 -1
  86. package/dist/deeplink-B5-Hxu0Q.js +0 -62
  87. package/dist/deeplink-B5-Hxu0Q.js.map +0 -1
  88. package/dist/deeplink-BpO9qc-D.js +0 -62
  89. package/dist/deeplink-BpO9qc-D.js.map +0 -1
  90. package/dist/deeplink-BzdbA1gV.cjs +0 -62
  91. package/dist/deeplink-BzdbA1gV.cjs.map +0 -1
  92. package/dist/deeplink-DCScMYcp.cjs +0 -62
  93. package/dist/deeplink-DCScMYcp.cjs.map +0 -1
  94. package/dist/devtools-opener-3Drge_RJ.js +0 -75
  95. package/dist/devtools-opener-3Drge_RJ.js.map +0 -1
  96. package/dist/devtools-opener-B8nxrxqu.js +0 -71
  97. package/dist/devtools-opener-B8nxrxqu.js.map +0 -1
  98. package/dist/devtools-opener-BDY0w3_0.cjs +0 -68
  99. package/dist/devtools-opener-BDY0w3_0.cjs.map +0 -1
  100. package/dist/devtools-opener-BTl5A6Cd.js +0 -71
  101. package/dist/devtools-opener-BTl5A6Cd.js.map +0 -1
  102. package/dist/devtools-opener-CJpEsXXQ.js +0 -76
  103. package/dist/devtools-opener-CJpEsXXQ.js.map +0 -1
  104. package/dist/devtools-opener-CxtryS8c.js +0 -75
  105. package/dist/devtools-opener-CxtryS8c.js.map +0 -1
  106. package/dist/devtools-opener-iv1OwfJN.cjs +0 -68
  107. package/dist/devtools-opener-iv1OwfJN.cjs.map +0 -1
  108. package/dist/in-app/auto.d.ts.map +0 -1
  109. package/dist/mcp/cli.d.ts.map +0 -1
  110. package/dist/mcp/server.d.ts.map +0 -1
  111. package/dist/pool-DcaaOwUq.d.ts +0 -14761
  112. package/dist/pool-DcaaOwUq.d.ts.map +0 -1
  113. package/dist/qr-http-server--gl2-WKc.cjs +0 -1644
  114. package/dist/qr-http-server--gl2-WKc.cjs.map +0 -1
  115. package/dist/qr-http-server-Bb7lMeqi.js +0 -1644
  116. package/dist/qr-http-server-Bb7lMeqi.js.map +0 -1
  117. package/dist/qr-http-server-C_lqOrgc.js +0 -1644
  118. package/dist/qr-http-server-C_lqOrgc.js.map +0 -1
  119. package/dist/qr-http-server-CopuMbub.js +0 -1644
  120. package/dist/qr-http-server-CopuMbub.js.map +0 -1
  121. package/dist/qr-http-server-D-Off6K1.cjs +0 -1644
  122. package/dist/qr-http-server-D-Off6K1.cjs.map +0 -1
  123. package/dist/qr-http-server-DrbIVDjO.js +0 -1645
  124. package/dist/qr-http-server-DrbIVDjO.js.map +0 -1
  125. package/dist/qr-http-server-n1twN18z.js +0 -1644
  126. package/dist/qr-http-server-n1twN18z.js.map +0 -1
  127. package/dist/relay-factory-N9QobQxG.js +0 -206
  128. package/dist/relay-factory-N9QobQxG.js.map +0 -1
  129. package/dist/relay-secret-store-BPhN1upr.js +0 -240
  130. package/dist/relay-secret-store-BPhN1upr.js.map +0 -1
  131. package/dist/relay-secret-store-Bmyleu0A.js +0 -154
  132. package/dist/relay-secret-store-Bmyleu0A.js.map +0 -1
  133. package/dist/relay-secret-store-CQenfcSL.js +0 -154
  134. package/dist/relay-secret-store-CQenfcSL.js.map +0 -1
  135. package/dist/relay-secret-store-DKxs7zwq.js +0 -153
  136. package/dist/relay-secret-store-DKxs7zwq.js.map +0 -1
  137. package/dist/relay-secret-store-DWKdV-eY.cjs +0 -241
  138. package/dist/relay-secret-store-DWKdV-eY.cjs.map +0 -1
  139. package/dist/relay-secret-store-WJ8EGkIl.js +0 -153
  140. package/dist/relay-secret-store-WJ8EGkIl.js.map +0 -1
  141. package/dist/relay-url-store-1FGuSYAn.cjs +0 -115
  142. package/dist/relay-url-store-1FGuSYAn.cjs.map +0 -1
  143. package/dist/relay-url-store-BR2XodiO.js +0 -123
  144. package/dist/relay-url-store-BR2XodiO.js.map +0 -1
  145. package/dist/relay-url-store-Bskcyeg8.js +0 -114
  146. package/dist/relay-url-store-Bskcyeg8.js.map +0 -1
  147. package/dist/relay-url-store-CH63fVCm.js +0 -122
  148. package/dist/relay-url-store-CH63fVCm.js.map +0 -1
  149. package/dist/relay-url-store-DaY1QPes.js +0 -123
  150. package/dist/relay-url-store-DaY1QPes.js.map +0 -1
  151. package/dist/relay-url-store-xmUuTjXA.js +0 -122
  152. package/dist/relay-url-store-xmUuTjXA.js.map +0 -1
  153. package/dist/relay-worker-B5HKkGUY.js +0 -832
  154. package/dist/relay-worker-B5HKkGUY.js.map +0 -1
  155. package/dist/relay-worker-YdlpZQl9.d.ts +0 -214
  156. package/dist/relay-worker-YdlpZQl9.d.ts.map +0 -1
  157. package/dist/rolldown-runtime-DGkTqVfb.js +0 -15
  158. package/dist/rolldown-runtime-DUslC3ob.js +0 -14
  159. package/dist/runtime-kn9DxOeg.d.ts +0 -249
  160. package/dist/runtime-kn9DxOeg.d.ts.map +0 -1
  161. package/dist/test-runner/bin.js +0 -2584
  162. package/dist/test-runner/bin.js.map +0 -1
  163. package/dist/test-runner/bridge-stub.d.ts +0 -125
  164. package/dist/test-runner/bridge-stub.d.ts.map +0 -1
  165. package/dist/test-runner/bridge-stub.js +0 -92
  166. package/dist/test-runner/bridge-stub.js.map +0 -1
  167. package/dist/test-runner/bundle.d.ts +0 -2
  168. package/dist/test-runner/bundle.js +0 -439
  169. package/dist/test-runner/bundle.js.map +0 -1
  170. package/dist/test-runner/capture.d.ts +0 -2
  171. package/dist/test-runner/capture.js +0 -44
  172. package/dist/test-runner/capture.js.map +0 -1
  173. package/dist/test-runner/config.d.ts.map +0 -1
  174. package/dist/test-runner/method-pace.d.ts +0 -82
  175. package/dist/test-runner/method-pace.d.ts.map +0 -1
  176. package/dist/test-runner/method-pace.js +0 -120
  177. package/dist/test-runner/method-pace.js.map +0 -1
  178. package/dist/test-runner/pool.d.ts +0 -2
  179. package/dist/test-runner/pool.js +0 -136
  180. package/dist/test-runner/pool.js.map +0 -1
  181. package/dist/test-runner/relay-factory.d.ts +0 -11245
  182. package/dist/test-runner/relay-factory.d.ts.map +0 -1
  183. package/dist/test-runner/relay-factory.js +0 -206
  184. package/dist/test-runner/relay-factory.js.map +0 -1
  185. package/dist/test-runner/relay-worker.d.ts +0 -2
  186. package/dist/test-runner/relay-worker.js +0 -2
  187. package/dist/test-runner/report.d.ts +0 -163
  188. package/dist/test-runner/report.d.ts.map +0 -1
  189. package/dist/test-runner/report.js +0 -198
  190. package/dist/test-runner/report.js.map +0 -1
  191. package/dist/test-runner/rpc.d.ts +0 -56
  192. package/dist/test-runner/rpc.d.ts.map +0 -1
  193. package/dist/test-runner/rpc.js +0 -98
  194. package/dist/test-runner/rpc.js.map +0 -1
  195. package/dist/test-runner/runtime.d.ts +0 -2
  196. package/dist/test-runner/runtime.js +0 -659
  197. package/dist/test-runner/runtime.js.map +0 -1
  198. package/dist/test-runner/task-graph.d.ts +0 -38
  199. package/dist/test-runner/task-graph.d.ts.map +0 -1
  200. package/dist/test-runner/task-graph.js +0 -182
  201. package/dist/test-runner/task-graph.js.map +0 -1
  202. package/dist/throttle-DKKzX1qC.js +0 -59
  203. package/dist/throttle-DKKzX1qC.js.map +0 -1
  204. package/dist/totp-95OAa20j.js +0 -64
  205. package/dist/totp-95OAa20j.js.map +0 -1
  206. package/dist/totp-BjtoQNfu.cjs +0 -64
  207. package/dist/totp-BjtoQNfu.cjs.map +0 -1
  208. package/dist/totp-CZLLKfOC.js +0 -200
  209. package/dist/totp-CZLLKfOC.js.map +0 -1
  210. package/dist/totp-DAxys-r0.js +0 -199
  211. package/dist/totp-DAxys-r0.js.map +0 -1
  212. package/dist/totp-DIbrZtI7.js +0 -189
  213. package/dist/totp-DIbrZtI7.js.map +0 -1
  214. package/dist/totp-Df252ZdA.cjs +0 -192
  215. package/dist/totp-Df252ZdA.cjs.map +0 -1
  216. package/dist/totp-DfekTBk3.js +0 -211
  217. package/dist/totp-DfekTBk3.js.map +0 -1
  218. package/dist/totp-Dwft0Kz7.js +0 -3
  219. package/dist/totp-WY6l0ysP.js +0 -190
  220. package/dist/totp-WY6l0ysP.js.map +0 -1
  221. package/dist/tunnel-BVSMXctM.cjs.map +0 -1
  222. package/dist/tunnel-D7xkimBu.js.map +0 -1
  223. /package/dist/{test-runner/bin.d.ts → stubs/bin-devtools-mcp.d.ts} +0 -0
@@ -1,189 +0,0 @@
1
- import { createHmac, timingSafeEqual } from "node:crypto";
2
- //#region src/mcp/totp.ts
3
- /**
4
- * RFC 6238 TOTP implementation (Node.js, node:crypto only).
5
- *
6
- * External TOTP libraries (otplib, speakeasy, …) are intentionally NOT used
7
- * to keep the dependency surface minimal. This hand-roll is ~30 lines and
8
- * covers exactly what relay-side auth needs.
9
- *
10
- * Algorithm summary (RFC 6238 + RFC 4226):
11
- * T = floor(now / 30) — 30-second time step counter
12
- * K = Buffer.from(secret, 'hex') — shared secret (raw bytes, hex-encoded)
13
- * MAC = HMAC-SHA1(K, T as 8-byte big-endian uint64)
14
- * offset = MAC[19] & 0x0f
15
- * code = (MAC[offset..offset+4] & 0x7fffffff) % 10^6 — 6 digits
16
- *
17
- * Security note (keep this comment accurate):
18
- * The baked-in secret in a dog-food build is extractable from the bundle by a
19
- * determined reverse engineer. This mechanism raises the bar from
20
- * "anyone with the URL" to "URL + bundle extraction + live TOTP calculation".
21
- * Casual URL leaks (Slack paste, QR screenshot, shoulder-surfing) are
22
- * blocked; deliberate reverse engineering is not. See threat model in
23
- * src/mcp/chii-relay.ts and umbrella CLAUDE.md §4.
24
- *
25
- * SECRET-HANDLING: secret values and computed codes MUST NOT appear in any
26
- * log, error message, or string visible outside this module. Only boolean
27
- * pass/fail and reason enum values are safe to surface.
28
- */
29
- /** Time step window in seconds (RFC 6238 default). */
30
- const TIME_STEP = 30;
31
- /** Number of digits in the generated code. */
32
- const DIGITS = 6;
33
- /**
34
- * Derives a 6-digit TOTP code from a hex-encoded secret at the given wall-
35
- * clock time.
36
- *
37
- * @param secret - The shared secret as a hex string (e.g. 64 hex chars = 32
38
- * bytes). Must be the output of `generateAttachToken()` or compatible.
39
- * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
40
- * @returns A zero-padded 6-digit decimal string, e.g. `"042193"`.
41
- */
42
- function generateTotp(secret, when = Date.now()) {
43
- const key = Buffer.from(secret, "hex");
44
- const counter = Math.max(0, Math.floor(when / 1e3 / TIME_STEP));
45
- const counterBuf = Buffer.alloc(8);
46
- const hi = Math.floor(counter / 4294967296);
47
- const lo = counter >>> 0;
48
- counterBuf.writeUInt32BE(hi, 0);
49
- counterBuf.writeUInt32BE(lo, 4);
50
- const mac = createHmac("sha1", key).update(counterBuf).digest();
51
- const offset = mac[19] & 15;
52
- return (((mac[offset] & 127) << 24 | (mac[offset + 1] & 255) << 16 | (mac[offset + 2] & 255) << 8 | mac[offset + 3] & 255) % 10 ** DIGITS).toString().padStart(DIGITS, "0");
53
- }
54
- /**
55
- * Verifies a TOTP code against the secret, accepting ±`skew` time steps to
56
- * tolerate clock drift between the relay host and the client device.
57
- *
58
- * Uses `timingSafeEqual` for constant-time comparison to prevent timing
59
- * side-channel attacks.
60
- *
61
- * @param secret - Hex-encoded shared secret.
62
- * @param code - The 6-digit code to verify (string or numeric).
63
- * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
64
- * @param skew - Number of adjacent steps to accept on either side. Default 1
65
- * (accepts T-1, T, T+1 — a 90-second acceptance window).
66
- * @returns `true` if the code matches any accepted step, `false` otherwise.
67
- */
68
- function verifyTotp(secret, code, when = Date.now(), skew = 1) {
69
- const normalised = String(code).padStart(DIGITS, "0");
70
- if (normalised.length !== DIGITS || !/^\d{6}$/.test(normalised)) return false;
71
- const candidateBuf = Buffer.from(normalised, "utf8");
72
- for (let delta = -skew; delta <= skew; delta++) {
73
- const expected = generateTotp(secret, when + delta * TIME_STEP * 1e3);
74
- if (timingSafeEqual(Buffer.from(expected, "utf8"), candidateBuf)) return true;
75
- }
76
- return false;
77
- }
78
- /**
79
- * Minimum length (in hex characters) accepted for `AIT_DEBUG_TOTP_SECRET`.
80
- *
81
- * The secret is hex-encoded (see {@link generateTotp} — `Buffer.from(secret,
82
- * 'hex')`). 32 hex chars = 16 bytes = 128 bits, the floor for an HMAC-SHA1 key
83
- * we are willing to gate a public relay behind. `generateAttachToken()` emits
84
- * 64 hex chars (32 bytes), comfortably above this bar.
85
- */
86
- const MIN_SECRET_HEX_CHARS = 32;
87
- /** Hex string: one or more hex digits, case-insensitive (RFC 4648 base16). */
88
- const HEX_RE = /^[0-9a-fA-F]+$/;
89
- /**
90
- * Human-facing guidance printed when {@link assertRelayAuthConfigured} fails.
91
- *
92
- * SECRET-HANDLING: this message states only the REQUIREMENT (≥32 hex chars) and
93
- * how to mint one. It NEVER echoes the configured value, its length, or any
94
- * fragment derived from it — see {@link assertRelayAuthConfigured}.
95
- *
96
- * Note on encoding: the secret is hex (base16), not base32 — `generateTotp`
97
- * decodes it with `Buffer.from(secret, 'hex')`. A base32 string would be
98
- * silently mis-decoded and every TOTP code would fail to match, so the minting
99
- * command emits hex.
100
- */
101
- const RELAY_AUTH_SECRET_MISSING_MESSAGE = [
102
- "[ait-debug] AIT_DEBUG_TOTP_SECRET이 필수입니다. 32자 이상 16진수(hex) 문자열을 설정하세요.",
103
- "발급: openssl rand -hex 32",
104
- "데몬은 start_debug의 projectRoot 인자로 받은 디렉토리에서 .ait_relay 파일을 읽어 이 시크릿을 채웁니다.",
105
- "프로젝트에서 pnpm dev:phone:cdp를 한 번 띄우면 unplugin이 .ait_relay를 자동 생성하니(tunnel.cdp 옵션 필요), projectRoot를 전달하세요.",
106
- "자세히: https://docs.aitc.dev/guides/relay-auth-totp"
107
- ].join("\n");
108
- /**
109
- * Whether `secret` is a well-formed relay-auth TOTP secret: a hex string of at
110
- * least {@link MIN_SECRET_HEX_CHARS} characters with an even length (an odd
111
- * length would have its trailing nibble silently dropped by `Buffer.from(...,
112
- * 'hex')`, weakening the key without warning).
113
- *
114
- * Pure predicate so callers can test the validation independently of the
115
- * fail-fast side effect in {@link assertRelayAuthConfigured}.
116
- *
117
- * SECRET-HANDLING: returns only a boolean — the input value is never returned,
118
- * logged, or echoed.
119
- */
120
- function isValidRelayAuthSecret(secret) {
121
- if (secret === void 0 || secret === "") return false;
122
- if (secret.length < MIN_SECRET_HEX_CHARS) return false;
123
- if (secret.length % 2 !== 0) return false;
124
- return HEX_RE.test(secret);
125
- }
126
- /**
127
- * Fail-fast guard enforcing that a relay-auth TOTP secret is configured before
128
- * a public-internet-exposed relay is booted (issue #250).
129
- *
130
- * Relay-auth (the §4 Layer C TOTP gate) is the only fail-fast layer that closes
131
- * the real gap: a leaked `wss://…trycloudflare.com` URL otherwise lets a third
132
- * party attach a debugger to a dog-food/live mini-app. Without a secret the relay
133
- * comes up unauthenticated, so this guard is called at every relay-boot site —
134
- * `bootRelayFamily` (intoss env 3/4) and `bootExternalRelayFamily` (env-2 PWA),
135
- * both eager and lazy. Local-only sessions never boot a relay and so never reach
136
- * this guard, matching the issue's exemption for non-relay debugging.
137
- *
138
- * Throws when the secret is unset, empty, too short, or not a valid hex string.
139
- * The thrown message is the bin entry's fatal stderr (see `cli.ts` `main().catch`)
140
- * — the same fatal model as the missing-`AIT_RELAY_BASE_URL` path.
141
- *
142
- * SECRET-HANDLING: the env value is read once, passed ONLY to the boolean
143
- * predicate, and never logged. The thrown message names the requirement, never
144
- * the value, its length, or any derived fragment.
145
- *
146
- * @param env - Environment to read from. Defaults to `process.env`; injectable
147
- * for tests so they never mutate the real process environment.
148
- */
149
- function assertRelayAuthConfigured(env = process.env) {
150
- if (!isValidRelayAuthSecret(env.AIT_DEBUG_TOTP_SECRET)) throw new Error(RELAY_AUTH_SECRET_MISSING_MESSAGE);
151
- }
152
- /**
153
- * Reads `AIT_DEBUG_TOTP_SECRET` from `process.env` at runtime and builds a
154
- * `verifyAuth` predicate for the Chii relay's WebSocket upgrade gate.
155
- *
156
- * The predicate checks the `at` query parameter against the current and
157
- * adjacent TOTP time steps (±{@link RELAY_VERIFY_SKEW_STEPS} skew) using
158
- * {@link verifyTotp}. This gives the issued code a minimum validity of ~3
159
- * minutes, which is enough to cover the QR-scan → launcher-attach flow even
160
- * when the launcher PWA needs to load or reinstall (#490).
161
- *
162
- * Returns `undefined` when the env var is not set — callers treat that as
163
- * "auth disabled" (no predicate registered on the relay). Note that since
164
- * issue #250 the secret is MANDATORY at every relay-boot site (enforced by
165
- * {@link assertRelayAuthConfigured} BEFORE the relay starts), so in production
166
- * this never returns `undefined` for a relay that actually boots; the
167
- * `undefined` branch only matters for the no-relay local path and tests.
168
- *
169
- * Lives here (not in the MCP server) so the unplugin's env-2 relay can wire the
170
- * same gate without importing the heavy MCP server module graph. Re-exported
171
- * from `debug-server.ts` for back-compat.
172
- *
173
- * SECRET-HANDLING: The secret value read from env is captured in a closure and
174
- * is NEVER written to any log, error message, or process output.
175
- */
176
- function buildRelayVerifyAuth(env = process.env) {
177
- const secret = env.AIT_DEBUG_TOTP_SECRET;
178
- if (!secret) return void 0;
179
- return (req) => {
180
- const rawUrl = req.url ?? "";
181
- const qIndex = rawUrl.indexOf("?");
182
- const queryStr = qIndex === -1 ? "" : rawUrl.slice(qIndex + 1);
183
- return verifyTotp(secret, new URLSearchParams(queryStr).get("at") ?? "", void 0, 6);
184
- };
185
- }
186
- //#endregion
187
- export { assertRelayAuthConfigured, buildRelayVerifyAuth, generateTotp, isValidRelayAuthSecret };
188
-
189
- //# sourceMappingURL=totp-DIbrZtI7.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"totp-DIbrZtI7.js","names":[],"sources":["../src/mcp/totp.ts"],"sourcesContent":["/**\n * RFC 6238 TOTP implementation (Node.js, node:crypto only).\n *\n * External TOTP libraries (otplib, speakeasy, …) are intentionally NOT used\n * to keep the dependency surface minimal. This hand-roll is ~30 lines and\n * covers exactly what relay-side auth needs.\n *\n * Algorithm summary (RFC 6238 + RFC 4226):\n * T = floor(now / 30) — 30-second time step counter\n * K = Buffer.from(secret, 'hex') — shared secret (raw bytes, hex-encoded)\n * MAC = HMAC-SHA1(K, T as 8-byte big-endian uint64)\n * offset = MAC[19] & 0x0f\n * code = (MAC[offset..offset+4] & 0x7fffffff) % 10^6 — 6 digits\n *\n * Security note (keep this comment accurate):\n * The baked-in secret in a dog-food build is extractable from the bundle by a\n * determined reverse engineer. This mechanism raises the bar from\n * \"anyone with the URL\" to \"URL + bundle extraction + live TOTP calculation\".\n * Casual URL leaks (Slack paste, QR screenshot, shoulder-surfing) are\n * blocked; deliberate reverse engineering is not. See threat model in\n * src/mcp/chii-relay.ts and umbrella CLAUDE.md §4.\n *\n * SECRET-HANDLING: secret values and computed codes MUST NOT appear in any\n * log, error message, or string visible outside this module. Only boolean\n * pass/fail and reason enum values are safe to surface.\n */\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\n\n/** Time step window in seconds (RFC 6238 default). */\nconst TIME_STEP = 30;\n\n/** Number of digits in the generated code. */\nconst DIGITS = 6;\n\n/**\n * Derives a 6-digit TOTP code from a hex-encoded secret at the given wall-\n * clock time.\n *\n * @param secret - The shared secret as a hex string (e.g. 64 hex chars = 32\n * bytes). Must be the output of `generateAttachToken()` or compatible.\n * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.\n * @returns A zero-padded 6-digit decimal string, e.g. `\"042193\"`.\n */\nexport function generateTotp(secret: string, when: number = Date.now()): string {\n const key = Buffer.from(secret, 'hex');\n // Clamp to 0 so negative timestamps (e.g. in ±skew checks near epoch) do not\n // produce a negative counter, which would cause writeUInt32BE to throw.\n const counter = Math.max(0, Math.floor(when / 1000 / TIME_STEP));\n\n // Encode counter as 8-byte big-endian unsigned integer.\n const counterBuf = Buffer.alloc(8);\n // JavaScript numbers are safe integers up to 2^53; counter is ~7.5×10^10 at\n // year 9999 — well within safe range so standard bitwise ops are fine.\n const hi = Math.floor(counter / 0x100000000);\n const lo = counter >>> 0;\n counterBuf.writeUInt32BE(hi, 0);\n counterBuf.writeUInt32BE(lo, 4);\n\n const mac = createHmac('sha1', key).update(counterBuf).digest();\n\n // Dynamic truncation (RFC 4226 §5.4).\n const offset = mac[19] & 0x0f;\n const binCode =\n ((mac[offset] & 0x7f) << 24) |\n ((mac[offset + 1] & 0xff) << 16) |\n ((mac[offset + 2] & 0xff) << 8) |\n (mac[offset + 3] & 0xff);\n\n const otp = binCode % 10 ** DIGITS;\n return otp.toString().padStart(DIGITS, '0');\n}\n\n/**\n * Verifies a TOTP code against the secret, accepting ±`skew` time steps to\n * tolerate clock drift between the relay host and the client device.\n *\n * Uses `timingSafeEqual` for constant-time comparison to prevent timing\n * side-channel attacks.\n *\n * @param secret - Hex-encoded shared secret.\n * @param code - The 6-digit code to verify (string or numeric).\n * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.\n * @param skew - Number of adjacent steps to accept on either side. Default 1\n * (accepts T-1, T, T+1 — a 90-second acceptance window).\n * @returns `true` if the code matches any accepted step, `false` otherwise.\n */\nexport function verifyTotp(\n secret: string,\n code: string,\n when: number = Date.now(),\n skew: number = 1,\n): boolean {\n const normalised = String(code).padStart(DIGITS, '0');\n if (normalised.length !== DIGITS || !/^\\d{6}$/.test(normalised)) {\n return false;\n }\n\n const candidateBuf = Buffer.from(normalised, 'utf8');\n\n for (let delta = -skew; delta <= skew; delta++) {\n const stepWhen = when + delta * TIME_STEP * 1000;\n const expected = generateTotp(secret, stepWhen);\n const expectedBuf = Buffer.from(expected, 'utf8');\n if (timingSafeEqual(expectedBuf, candidateBuf)) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Minimum length (in hex characters) accepted for `AIT_DEBUG_TOTP_SECRET`.\n *\n * The secret is hex-encoded (see {@link generateTotp} — `Buffer.from(secret,\n * 'hex')`). 32 hex chars = 16 bytes = 128 bits, the floor for an HMAC-SHA1 key\n * we are willing to gate a public relay behind. `generateAttachToken()` emits\n * 64 hex chars (32 bytes), comfortably above this bar.\n */\nconst MIN_SECRET_HEX_CHARS = 32;\n\n/** Hex string: one or more hex digits, case-insensitive (RFC 4648 base16). */\nconst HEX_RE = /^[0-9a-fA-F]+$/;\n\n/**\n * Human-facing guidance printed when {@link assertRelayAuthConfigured} fails.\n *\n * SECRET-HANDLING: this message states only the REQUIREMENT (≥32 hex chars) and\n * how to mint one. It NEVER echoes the configured value, its length, or any\n * fragment derived from it — see {@link assertRelayAuthConfigured}.\n *\n * Note on encoding: the secret is hex (base16), not base32 — `generateTotp`\n * decodes it with `Buffer.from(secret, 'hex')`. A base32 string would be\n * silently mis-decoded and every TOTP code would fail to match, so the minting\n * command emits hex.\n */\nexport const RELAY_AUTH_SECRET_MISSING_MESSAGE = [\n '[ait-debug] AIT_DEBUG_TOTP_SECRET이 필수입니다. 32자 이상 16진수(hex) 문자열을 설정하세요.',\n '발급: openssl rand -hex 32',\n '데몬은 start_debug의 projectRoot 인자로 받은 디렉토리에서 .ait_relay 파일을 읽어 이 시크릿을 채웁니다.',\n '프로젝트에서 pnpm dev:phone:cdp를 한 번 띄우면 unplugin이 .ait_relay를 자동 생성하니(tunnel.cdp 옵션 필요), projectRoot를 전달하세요.',\n '자세히: https://docs.aitc.dev/guides/relay-auth-totp',\n].join('\\n');\n\n/**\n * Whether `secret` is a well-formed relay-auth TOTP secret: a hex string of at\n * least {@link MIN_SECRET_HEX_CHARS} characters with an even length (an odd\n * length would have its trailing nibble silently dropped by `Buffer.from(...,\n * 'hex')`, weakening the key without warning).\n *\n * Pure predicate so callers can test the validation independently of the\n * fail-fast side effect in {@link assertRelayAuthConfigured}.\n *\n * SECRET-HANDLING: returns only a boolean — the input value is never returned,\n * logged, or echoed.\n */\nexport function isValidRelayAuthSecret(secret: string | undefined): secret is string {\n if (secret === undefined || secret === '') return false;\n if (secret.length < MIN_SECRET_HEX_CHARS) return false;\n if (secret.length % 2 !== 0) return false;\n return HEX_RE.test(secret);\n}\n\n/**\n * Fail-fast guard enforcing that a relay-auth TOTP secret is configured before\n * a public-internet-exposed relay is booted (issue #250).\n *\n * Relay-auth (the §4 Layer C TOTP gate) is the only fail-fast layer that closes\n * the real gap: a leaked `wss://…trycloudflare.com` URL otherwise lets a third\n * party attach a debugger to a dog-food/live mini-app. Without a secret the relay\n * comes up unauthenticated, so this guard is called at every relay-boot site —\n * `bootRelayFamily` (intoss env 3/4) and `bootExternalRelayFamily` (env-2 PWA),\n * both eager and lazy. Local-only sessions never boot a relay and so never reach\n * this guard, matching the issue's exemption for non-relay debugging.\n *\n * Throws when the secret is unset, empty, too short, or not a valid hex string.\n * The thrown message is the bin entry's fatal stderr (see `cli.ts` `main().catch`)\n * — the same fatal model as the missing-`AIT_RELAY_BASE_URL` path.\n *\n * SECRET-HANDLING: the env value is read once, passed ONLY to the boolean\n * predicate, and never logged. The thrown message names the requirement, never\n * the value, its length, or any derived fragment.\n *\n * @param env - Environment to read from. Defaults to `process.env`; injectable\n * for tests so they never mutate the real process environment.\n */\nexport function assertRelayAuthConfigured(env: NodeJS.ProcessEnv = process.env): void {\n if (!isValidRelayAuthSecret(env.AIT_DEBUG_TOTP_SECRET)) {\n throw new Error(RELAY_AUTH_SECRET_MISSING_MESSAGE);\n }\n}\n\n/**\n * Gate-specific skew for the relay WebSocket upgrade TOTP check.\n *\n * Rationale (why 6, not the RFC default of 1):\n * - Each step is 30 s, so ±6 steps = past 6 steps accepted = 180–210 s of\n * backwards acceptance. This means a code generated at issuance time is\n * guaranteed valid for at least 3 minutes (180 s) after it was minted.\n * - The real-world attach flow (QR issued on desktop → developer picks up\n * phone → camera scan → launcher PWA loads → attach) routinely exceeds\n * the 90 s window of the RFC default (skew=1), especially when the launcher\n * PWA needs to reinstall or when the phone is not immediately at hand.\n * - Expanding to ~3.5 min reachability is acceptable under the §4 threat\n * model: the adversary we guard against is \"someone who got the URL but\n * does NOT have the secret\". Without the secret they cannot compute a TOTP\n * code regardless of the window size — security theater is explicitly\n * forbidden by the project principle. An attacker WITH the secret (bundle\n * extractor) is out of scope per CLAUDE.md §4.\n *\n * `verifyTotp`'s own default (skew=1) is deliberately left unchanged — it is\n * the RFC primitive. Only this relay-gate call site is widened.\n */\nexport const RELAY_VERIFY_SKEW_STEPS = 6;\n\n/**\n * Reads `AIT_DEBUG_TOTP_SECRET` from `process.env` at runtime and builds a\n * `verifyAuth` predicate for the Chii relay's WebSocket upgrade gate.\n *\n * The predicate checks the `at` query parameter against the current and\n * adjacent TOTP time steps (±{@link RELAY_VERIFY_SKEW_STEPS} skew) using\n * {@link verifyTotp}. This gives the issued code a minimum validity of ~3\n * minutes, which is enough to cover the QR-scan → launcher-attach flow even\n * when the launcher PWA needs to load or reinstall (#490).\n *\n * Returns `undefined` when the env var is not set — callers treat that as\n * \"auth disabled\" (no predicate registered on the relay). Note that since\n * issue #250 the secret is MANDATORY at every relay-boot site (enforced by\n * {@link assertRelayAuthConfigured} BEFORE the relay starts), so in production\n * this never returns `undefined` for a relay that actually boots; the\n * `undefined` branch only matters for the no-relay local path and tests.\n *\n * Lives here (not in the MCP server) so the unplugin's env-2 relay can wire the\n * same gate without importing the heavy MCP server module graph. Re-exported\n * from `debug-server.ts` for back-compat.\n *\n * SECRET-HANDLING: The secret value read from env is captured in a closure and\n * is NEVER written to any log, error message, or process output.\n */\nexport function buildRelayVerifyAuth(\n env: NodeJS.ProcessEnv = process.env,\n): ((req: import('node:http').IncomingMessage) => boolean) | undefined {\n const secret = env.AIT_DEBUG_TOTP_SECRET;\n if (!secret) return undefined;\n\n return (req) => {\n // Parse the `at` query param from the upgrade request URL.\n // req.url is the raw request path + query, e.g. `/client/id?target=…&at=123456`\n const rawUrl = req.url ?? '';\n const qIndex = rawUrl.indexOf('?');\n const queryStr = qIndex === -1 ? '' : rawUrl.slice(qIndex + 1);\n const params = new URLSearchParams(queryStr);\n const code = params.get('at') ?? '';\n\n // Do NOT log `code`, `secret`, or any derived value here.\n // Use RELAY_VERIFY_SKEW_STEPS (±6) for a ~3-minute acceptance window (#490).\n // verifyTotp's own default (skew=1) is unchanged — only this call site is widened.\n return verifyTotp(secret, code, undefined, RELAY_VERIFY_SKEW_STEPS);\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAM,YAAY;;AAGlB,MAAM,SAAS;;;;;;;;;;AAWf,SAAgB,aAAa,QAAgB,OAAe,KAAK,KAAK,EAAU;CAC9E,MAAM,MAAM,OAAO,KAAK,QAAQ,MAAM;CAGtC,MAAM,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,MAAO,UAAU,CAAC;CAGhE,MAAM,aAAa,OAAO,MAAM,EAAE;CAGlC,MAAM,KAAK,KAAK,MAAM,UAAU,WAAY;CAC5C,MAAM,KAAK,YAAY;AACvB,YAAW,cAAc,IAAI,EAAE;AAC/B,YAAW,cAAc,IAAI,EAAE;CAE/B,MAAM,MAAM,WAAW,QAAQ,IAAI,CAAC,OAAO,WAAW,CAAC,QAAQ;CAG/D,MAAM,SAAS,IAAI,MAAM;AAQzB,WANI,IAAI,UAAU,QAAS,MACvB,IAAI,SAAS,KAAK,QAAS,MAC3B,IAAI,SAAS,KAAK,QAAS,IAC5B,IAAI,SAAS,KAAK,OAEC,MAAM,QACjB,UAAU,CAAC,SAAS,QAAQ,IAAI;;;;;;;;;;;;;;;;AAiB7C,SAAgB,WACd,QACA,MACA,OAAe,KAAK,KAAK,EACzB,OAAe,GACN;CACT,MAAM,aAAa,OAAO,KAAK,CAAC,SAAS,QAAQ,IAAI;AACrD,KAAI,WAAW,WAAW,UAAU,CAAC,UAAU,KAAK,WAAW,CAC7D,QAAO;CAGT,MAAM,eAAe,OAAO,KAAK,YAAY,OAAO;AAEpD,MAAK,IAAI,QAAQ,CAAC,MAAM,SAAS,MAAM,SAAS;EAE9C,MAAM,WAAW,aAAa,QADb,OAAO,QAAQ,YAAY,IACG;AAE/C,MAAI,gBADgB,OAAO,KAAK,UAAU,OAAO,EAChB,aAAa,CAC5C,QAAO;;AAIX,QAAO;;;;;;;;;;AAWT,MAAM,uBAAuB;;AAG7B,MAAM,SAAS;;;;;;;;;;;;;AAcf,MAAa,oCAAoC;CAC/C;CACA;CACA;CACA;CACA;CACD,CAAC,KAAK,KAAK;;;;;;;;;;;;;AAcZ,SAAgB,uBAAuB,QAA8C;AACnF,KAAI,WAAW,KAAA,KAAa,WAAW,GAAI,QAAO;AAClD,KAAI,OAAO,SAAS,qBAAsB,QAAO;AACjD,KAAI,OAAO,SAAS,MAAM,EAAG,QAAO;AACpC,QAAO,OAAO,KAAK,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;AA0B5B,SAAgB,0BAA0B,MAAyB,QAAQ,KAAW;AACpF,KAAI,CAAC,uBAAuB,IAAI,sBAAsB,CACpD,OAAM,IAAI,MAAM,kCAAkC;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDtD,SAAgB,qBACd,MAAyB,QAAQ,KACoC;CACrE,MAAM,SAAS,IAAI;AACnB,KAAI,CAAC,OAAQ,QAAO,KAAA;AAEpB,SAAQ,QAAQ;EAGd,MAAM,SAAS,IAAI,OAAO;EAC1B,MAAM,SAAS,OAAO,QAAQ,IAAI;EAClC,MAAM,WAAW,WAAW,KAAK,KAAK,OAAO,MAAM,SAAS,EAAE;AAO9D,SAAO,WAAW,QANH,IAAI,gBAAgB,SAAS,CACxB,IAAI,KAAK,IAAI,IAKD,KAAA,GAAA,EAAmC"}
@@ -1,192 +0,0 @@
1
- let node_crypto = require("node:crypto");
2
- //#region src/mcp/totp.ts
3
- /**
4
- * RFC 6238 TOTP implementation (Node.js, node:crypto only).
5
- *
6
- * External TOTP libraries (otplib, speakeasy, …) are intentionally NOT used
7
- * to keep the dependency surface minimal. This hand-roll is ~30 lines and
8
- * covers exactly what relay-side auth needs.
9
- *
10
- * Algorithm summary (RFC 6238 + RFC 4226):
11
- * T = floor(now / 30) — 30-second time step counter
12
- * K = Buffer.from(secret, 'hex') — shared secret (raw bytes, hex-encoded)
13
- * MAC = HMAC-SHA1(K, T as 8-byte big-endian uint64)
14
- * offset = MAC[19] & 0x0f
15
- * code = (MAC[offset..offset+4] & 0x7fffffff) % 10^6 — 6 digits
16
- *
17
- * Security note (keep this comment accurate):
18
- * The baked-in secret in a dog-food build is extractable from the bundle by a
19
- * determined reverse engineer. This mechanism raises the bar from
20
- * "anyone with the URL" to "URL + bundle extraction + live TOTP calculation".
21
- * Casual URL leaks (Slack paste, QR screenshot, shoulder-surfing) are
22
- * blocked; deliberate reverse engineering is not. See threat model in
23
- * src/mcp/chii-relay.ts and umbrella CLAUDE.md §4.
24
- *
25
- * SECRET-HANDLING: secret values and computed codes MUST NOT appear in any
26
- * log, error message, or string visible outside this module. Only boolean
27
- * pass/fail and reason enum values are safe to surface.
28
- */
29
- /** Time step window in seconds (RFC 6238 default). */
30
- const TIME_STEP = 30;
31
- /** Number of digits in the generated code. */
32
- const DIGITS = 6;
33
- /**
34
- * Derives a 6-digit TOTP code from a hex-encoded secret at the given wall-
35
- * clock time.
36
- *
37
- * @param secret - The shared secret as a hex string (e.g. 64 hex chars = 32
38
- * bytes). Must be the output of `generateAttachToken()` or compatible.
39
- * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
40
- * @returns A zero-padded 6-digit decimal string, e.g. `"042193"`.
41
- */
42
- function generateTotp(secret, when = Date.now()) {
43
- const key = Buffer.from(secret, "hex");
44
- const counter = Math.max(0, Math.floor(when / 1e3 / TIME_STEP));
45
- const counterBuf = Buffer.alloc(8);
46
- const hi = Math.floor(counter / 4294967296);
47
- const lo = counter >>> 0;
48
- counterBuf.writeUInt32BE(hi, 0);
49
- counterBuf.writeUInt32BE(lo, 4);
50
- const mac = (0, node_crypto.createHmac)("sha1", key).update(counterBuf).digest();
51
- const offset = mac[19] & 15;
52
- return (((mac[offset] & 127) << 24 | (mac[offset + 1] & 255) << 16 | (mac[offset + 2] & 255) << 8 | mac[offset + 3] & 255) % 10 ** DIGITS).toString().padStart(DIGITS, "0");
53
- }
54
- /**
55
- * Verifies a TOTP code against the secret, accepting ±`skew` time steps to
56
- * tolerate clock drift between the relay host and the client device.
57
- *
58
- * Uses `timingSafeEqual` for constant-time comparison to prevent timing
59
- * side-channel attacks.
60
- *
61
- * @param secret - Hex-encoded shared secret.
62
- * @param code - The 6-digit code to verify (string or numeric).
63
- * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
64
- * @param skew - Number of adjacent steps to accept on either side. Default 1
65
- * (accepts T-1, T, T+1 — a 90-second acceptance window).
66
- * @returns `true` if the code matches any accepted step, `false` otherwise.
67
- */
68
- function verifyTotp(secret, code, when = Date.now(), skew = 1) {
69
- const normalised = String(code).padStart(DIGITS, "0");
70
- if (normalised.length !== DIGITS || !/^\d{6}$/.test(normalised)) return false;
71
- const candidateBuf = Buffer.from(normalised, "utf8");
72
- for (let delta = -skew; delta <= skew; delta++) {
73
- const expected = generateTotp(secret, when + delta * TIME_STEP * 1e3);
74
- if ((0, node_crypto.timingSafeEqual)(Buffer.from(expected, "utf8"), candidateBuf)) return true;
75
- }
76
- return false;
77
- }
78
- /**
79
- * Minimum length (in hex characters) accepted for `AIT_DEBUG_TOTP_SECRET`.
80
- *
81
- * The secret is hex-encoded (see {@link generateTotp} — `Buffer.from(secret,
82
- * 'hex')`). 32 hex chars = 16 bytes = 128 bits, the floor for an HMAC-SHA1 key
83
- * we are willing to gate a public relay behind. `generateAttachToken()` emits
84
- * 64 hex chars (32 bytes), comfortably above this bar.
85
- */
86
- const MIN_SECRET_HEX_CHARS = 32;
87
- /** Hex string: one or more hex digits, case-insensitive (RFC 4648 base16). */
88
- const HEX_RE = /^[0-9a-fA-F]+$/;
89
- /**
90
- * Human-facing guidance printed when {@link assertRelayAuthConfigured} fails.
91
- *
92
- * SECRET-HANDLING: this message states only the REQUIREMENT (≥32 hex chars) and
93
- * how to mint one. It NEVER echoes the configured value, its length, or any
94
- * fragment derived from it — see {@link assertRelayAuthConfigured}.
95
- *
96
- * Note on encoding: the secret is hex (base16), not base32 — `generateTotp`
97
- * decodes it with `Buffer.from(secret, 'hex')`. A base32 string would be
98
- * silently mis-decoded and every TOTP code would fail to match, so the minting
99
- * command emits hex.
100
- */
101
- const RELAY_AUTH_SECRET_MISSING_MESSAGE = [
102
- "[ait-debug] AIT_DEBUG_TOTP_SECRET이 필수입니다. 32자 이상 16진수(hex) 문자열을 설정하세요.",
103
- "발급: openssl rand -hex 32",
104
- "데몬은 start_debug의 projectRoot 인자로 받은 디렉토리에서 .ait_relay 파일을 읽어 이 시크릿을 채웁니다.",
105
- "프로젝트에서 pnpm dev:phone:cdp를 한 번 띄우면 unplugin이 .ait_relay를 자동 생성하니(tunnel.cdp 옵션 필요), projectRoot를 전달하세요.",
106
- "자세히: https://docs.aitc.dev/guides/relay-auth-totp"
107
- ].join("\n");
108
- /**
109
- * Whether `secret` is a well-formed relay-auth TOTP secret: a hex string of at
110
- * least {@link MIN_SECRET_HEX_CHARS} characters with an even length (an odd
111
- * length would have its trailing nibble silently dropped by `Buffer.from(...,
112
- * 'hex')`, weakening the key without warning).
113
- *
114
- * Pure predicate so callers can test the validation independently of the
115
- * fail-fast side effect in {@link assertRelayAuthConfigured}.
116
- *
117
- * SECRET-HANDLING: returns only a boolean — the input value is never returned,
118
- * logged, or echoed.
119
- */
120
- function isValidRelayAuthSecret(secret) {
121
- if (secret === void 0 || secret === "") return false;
122
- if (secret.length < MIN_SECRET_HEX_CHARS) return false;
123
- if (secret.length % 2 !== 0) return false;
124
- return HEX_RE.test(secret);
125
- }
126
- /**
127
- * Fail-fast guard enforcing that a relay-auth TOTP secret is configured before
128
- * a public-internet-exposed relay is booted (issue #250).
129
- *
130
- * Relay-auth (the §4 Layer C TOTP gate) is the only fail-fast layer that closes
131
- * the real gap: a leaked `wss://…trycloudflare.com` URL otherwise lets a third
132
- * party attach a debugger to a dog-food/live mini-app. Without a secret the relay
133
- * comes up unauthenticated, so this guard is called at every relay-boot site —
134
- * `bootRelayFamily` (intoss env 3/4) and `bootExternalRelayFamily` (env-2 PWA),
135
- * both eager and lazy. Local-only sessions never boot a relay and so never reach
136
- * this guard, matching the issue's exemption for non-relay debugging.
137
- *
138
- * Throws when the secret is unset, empty, too short, or not a valid hex string.
139
- * The thrown message is the bin entry's fatal stderr (see `cli.ts` `main().catch`)
140
- * — the same fatal model as the missing-`AIT_RELAY_BASE_URL` path.
141
- *
142
- * SECRET-HANDLING: the env value is read once, passed ONLY to the boolean
143
- * predicate, and never logged. The thrown message names the requirement, never
144
- * the value, its length, or any derived fragment.
145
- *
146
- * @param env - Environment to read from. Defaults to `process.env`; injectable
147
- * for tests so they never mutate the real process environment.
148
- */
149
- function assertRelayAuthConfigured(env = process.env) {
150
- if (!isValidRelayAuthSecret(env.AIT_DEBUG_TOTP_SECRET)) throw new Error(RELAY_AUTH_SECRET_MISSING_MESSAGE);
151
- }
152
- /**
153
- * Reads `AIT_DEBUG_TOTP_SECRET` from `process.env` at runtime and builds a
154
- * `verifyAuth` predicate for the Chii relay's WebSocket upgrade gate.
155
- *
156
- * The predicate checks the `at` query parameter against the current and
157
- * adjacent TOTP time steps (±{@link RELAY_VERIFY_SKEW_STEPS} skew) using
158
- * {@link verifyTotp}. This gives the issued code a minimum validity of ~3
159
- * minutes, which is enough to cover the QR-scan → launcher-attach flow even
160
- * when the launcher PWA needs to load or reinstall (#490).
161
- *
162
- * Returns `undefined` when the env var is not set — callers treat that as
163
- * "auth disabled" (no predicate registered on the relay). Note that since
164
- * issue #250 the secret is MANDATORY at every relay-boot site (enforced by
165
- * {@link assertRelayAuthConfigured} BEFORE the relay starts), so in production
166
- * this never returns `undefined` for a relay that actually boots; the
167
- * `undefined` branch only matters for the no-relay local path and tests.
168
- *
169
- * Lives here (not in the MCP server) so the unplugin's env-2 relay can wire the
170
- * same gate without importing the heavy MCP server module graph. Re-exported
171
- * from `debug-server.ts` for back-compat.
172
- *
173
- * SECRET-HANDLING: The secret value read from env is captured in a closure and
174
- * is NEVER written to any log, error message, or process output.
175
- */
176
- function buildRelayVerifyAuth(env = process.env) {
177
- const secret = env.AIT_DEBUG_TOTP_SECRET;
178
- if (!secret) return void 0;
179
- return (req) => {
180
- const rawUrl = req.url ?? "";
181
- const qIndex = rawUrl.indexOf("?");
182
- const queryStr = qIndex === -1 ? "" : rawUrl.slice(qIndex + 1);
183
- return verifyTotp(secret, new URLSearchParams(queryStr).get("at") ?? "", void 0, 6);
184
- };
185
- }
186
- //#endregion
187
- exports.assertRelayAuthConfigured = assertRelayAuthConfigured;
188
- exports.buildRelayVerifyAuth = buildRelayVerifyAuth;
189
- exports.generateTotp = generateTotp;
190
- exports.isValidRelayAuthSecret = isValidRelayAuthSecret;
191
-
192
- //# sourceMappingURL=totp-Df252ZdA.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"totp-Df252ZdA.cjs","names":[],"sources":["../src/mcp/totp.ts"],"sourcesContent":["/**\n * RFC 6238 TOTP implementation (Node.js, node:crypto only).\n *\n * External TOTP libraries (otplib, speakeasy, …) are intentionally NOT used\n * to keep the dependency surface minimal. This hand-roll is ~30 lines and\n * covers exactly what relay-side auth needs.\n *\n * Algorithm summary (RFC 6238 + RFC 4226):\n * T = floor(now / 30) — 30-second time step counter\n * K = Buffer.from(secret, 'hex') — shared secret (raw bytes, hex-encoded)\n * MAC = HMAC-SHA1(K, T as 8-byte big-endian uint64)\n * offset = MAC[19] & 0x0f\n * code = (MAC[offset..offset+4] & 0x7fffffff) % 10^6 — 6 digits\n *\n * Security note (keep this comment accurate):\n * The baked-in secret in a dog-food build is extractable from the bundle by a\n * determined reverse engineer. This mechanism raises the bar from\n * \"anyone with the URL\" to \"URL + bundle extraction + live TOTP calculation\".\n * Casual URL leaks (Slack paste, QR screenshot, shoulder-surfing) are\n * blocked; deliberate reverse engineering is not. See threat model in\n * src/mcp/chii-relay.ts and umbrella CLAUDE.md §4.\n *\n * SECRET-HANDLING: secret values and computed codes MUST NOT appear in any\n * log, error message, or string visible outside this module. Only boolean\n * pass/fail and reason enum values are safe to surface.\n */\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\n\n/** Time step window in seconds (RFC 6238 default). */\nconst TIME_STEP = 30;\n\n/** Number of digits in the generated code. */\nconst DIGITS = 6;\n\n/**\n * Derives a 6-digit TOTP code from a hex-encoded secret at the given wall-\n * clock time.\n *\n * @param secret - The shared secret as a hex string (e.g. 64 hex chars = 32\n * bytes). Must be the output of `generateAttachToken()` or compatible.\n * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.\n * @returns A zero-padded 6-digit decimal string, e.g. `\"042193\"`.\n */\nexport function generateTotp(secret: string, when: number = Date.now()): string {\n const key = Buffer.from(secret, 'hex');\n // Clamp to 0 so negative timestamps (e.g. in ±skew checks near epoch) do not\n // produce a negative counter, which would cause writeUInt32BE to throw.\n const counter = Math.max(0, Math.floor(when / 1000 / TIME_STEP));\n\n // Encode counter as 8-byte big-endian unsigned integer.\n const counterBuf = Buffer.alloc(8);\n // JavaScript numbers are safe integers up to 2^53; counter is ~7.5×10^10 at\n // year 9999 — well within safe range so standard bitwise ops are fine.\n const hi = Math.floor(counter / 0x100000000);\n const lo = counter >>> 0;\n counterBuf.writeUInt32BE(hi, 0);\n counterBuf.writeUInt32BE(lo, 4);\n\n const mac = createHmac('sha1', key).update(counterBuf).digest();\n\n // Dynamic truncation (RFC 4226 §5.4).\n const offset = mac[19] & 0x0f;\n const binCode =\n ((mac[offset] & 0x7f) << 24) |\n ((mac[offset + 1] & 0xff) << 16) |\n ((mac[offset + 2] & 0xff) << 8) |\n (mac[offset + 3] & 0xff);\n\n const otp = binCode % 10 ** DIGITS;\n return otp.toString().padStart(DIGITS, '0');\n}\n\n/**\n * Verifies a TOTP code against the secret, accepting ±`skew` time steps to\n * tolerate clock drift between the relay host and the client device.\n *\n * Uses `timingSafeEqual` for constant-time comparison to prevent timing\n * side-channel attacks.\n *\n * @param secret - Hex-encoded shared secret.\n * @param code - The 6-digit code to verify (string or numeric).\n * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.\n * @param skew - Number of adjacent steps to accept on either side. Default 1\n * (accepts T-1, T, T+1 — a 90-second acceptance window).\n * @returns `true` if the code matches any accepted step, `false` otherwise.\n */\nexport function verifyTotp(\n secret: string,\n code: string,\n when: number = Date.now(),\n skew: number = 1,\n): boolean {\n const normalised = String(code).padStart(DIGITS, '0');\n if (normalised.length !== DIGITS || !/^\\d{6}$/.test(normalised)) {\n return false;\n }\n\n const candidateBuf = Buffer.from(normalised, 'utf8');\n\n for (let delta = -skew; delta <= skew; delta++) {\n const stepWhen = when + delta * TIME_STEP * 1000;\n const expected = generateTotp(secret, stepWhen);\n const expectedBuf = Buffer.from(expected, 'utf8');\n if (timingSafeEqual(expectedBuf, candidateBuf)) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Minimum length (in hex characters) accepted for `AIT_DEBUG_TOTP_SECRET`.\n *\n * The secret is hex-encoded (see {@link generateTotp} — `Buffer.from(secret,\n * 'hex')`). 32 hex chars = 16 bytes = 128 bits, the floor for an HMAC-SHA1 key\n * we are willing to gate a public relay behind. `generateAttachToken()` emits\n * 64 hex chars (32 bytes), comfortably above this bar.\n */\nconst MIN_SECRET_HEX_CHARS = 32;\n\n/** Hex string: one or more hex digits, case-insensitive (RFC 4648 base16). */\nconst HEX_RE = /^[0-9a-fA-F]+$/;\n\n/**\n * Human-facing guidance printed when {@link assertRelayAuthConfigured} fails.\n *\n * SECRET-HANDLING: this message states only the REQUIREMENT (≥32 hex chars) and\n * how to mint one. It NEVER echoes the configured value, its length, or any\n * fragment derived from it — see {@link assertRelayAuthConfigured}.\n *\n * Note on encoding: the secret is hex (base16), not base32 — `generateTotp`\n * decodes it with `Buffer.from(secret, 'hex')`. A base32 string would be\n * silently mis-decoded and every TOTP code would fail to match, so the minting\n * command emits hex.\n */\nexport const RELAY_AUTH_SECRET_MISSING_MESSAGE = [\n '[ait-debug] AIT_DEBUG_TOTP_SECRET이 필수입니다. 32자 이상 16진수(hex) 문자열을 설정하세요.',\n '발급: openssl rand -hex 32',\n '데몬은 start_debug의 projectRoot 인자로 받은 디렉토리에서 .ait_relay 파일을 읽어 이 시크릿을 채웁니다.',\n '프로젝트에서 pnpm dev:phone:cdp를 한 번 띄우면 unplugin이 .ait_relay를 자동 생성하니(tunnel.cdp 옵션 필요), projectRoot를 전달하세요.',\n '자세히: https://docs.aitc.dev/guides/relay-auth-totp',\n].join('\\n');\n\n/**\n * Whether `secret` is a well-formed relay-auth TOTP secret: a hex string of at\n * least {@link MIN_SECRET_HEX_CHARS} characters with an even length (an odd\n * length would have its trailing nibble silently dropped by `Buffer.from(...,\n * 'hex')`, weakening the key without warning).\n *\n * Pure predicate so callers can test the validation independently of the\n * fail-fast side effect in {@link assertRelayAuthConfigured}.\n *\n * SECRET-HANDLING: returns only a boolean — the input value is never returned,\n * logged, or echoed.\n */\nexport function isValidRelayAuthSecret(secret: string | undefined): secret is string {\n if (secret === undefined || secret === '') return false;\n if (secret.length < MIN_SECRET_HEX_CHARS) return false;\n if (secret.length % 2 !== 0) return false;\n return HEX_RE.test(secret);\n}\n\n/**\n * Fail-fast guard enforcing that a relay-auth TOTP secret is configured before\n * a public-internet-exposed relay is booted (issue #250).\n *\n * Relay-auth (the §4 Layer C TOTP gate) is the only fail-fast layer that closes\n * the real gap: a leaked `wss://…trycloudflare.com` URL otherwise lets a third\n * party attach a debugger to a dog-food/live mini-app. Without a secret the relay\n * comes up unauthenticated, so this guard is called at every relay-boot site —\n * `bootRelayFamily` (intoss env 3/4) and `bootExternalRelayFamily` (env-2 PWA),\n * both eager and lazy. Local-only sessions never boot a relay and so never reach\n * this guard, matching the issue's exemption for non-relay debugging.\n *\n * Throws when the secret is unset, empty, too short, or not a valid hex string.\n * The thrown message is the bin entry's fatal stderr (see `cli.ts` `main().catch`)\n * — the same fatal model as the missing-`AIT_RELAY_BASE_URL` path.\n *\n * SECRET-HANDLING: the env value is read once, passed ONLY to the boolean\n * predicate, and never logged. The thrown message names the requirement, never\n * the value, its length, or any derived fragment.\n *\n * @param env - Environment to read from. Defaults to `process.env`; injectable\n * for tests so they never mutate the real process environment.\n */\nexport function assertRelayAuthConfigured(env: NodeJS.ProcessEnv = process.env): void {\n if (!isValidRelayAuthSecret(env.AIT_DEBUG_TOTP_SECRET)) {\n throw new Error(RELAY_AUTH_SECRET_MISSING_MESSAGE);\n }\n}\n\n/**\n * Gate-specific skew for the relay WebSocket upgrade TOTP check.\n *\n * Rationale (why 6, not the RFC default of 1):\n * - Each step is 30 s, so ±6 steps = past 6 steps accepted = 180–210 s of\n * backwards acceptance. This means a code generated at issuance time is\n * guaranteed valid for at least 3 minutes (180 s) after it was minted.\n * - The real-world attach flow (QR issued on desktop → developer picks up\n * phone → camera scan → launcher PWA loads → attach) routinely exceeds\n * the 90 s window of the RFC default (skew=1), especially when the launcher\n * PWA needs to reinstall or when the phone is not immediately at hand.\n * - Expanding to ~3.5 min reachability is acceptable under the §4 threat\n * model: the adversary we guard against is \"someone who got the URL but\n * does NOT have the secret\". Without the secret they cannot compute a TOTP\n * code regardless of the window size — security theater is explicitly\n * forbidden by the project principle. An attacker WITH the secret (bundle\n * extractor) is out of scope per CLAUDE.md §4.\n *\n * `verifyTotp`'s own default (skew=1) is deliberately left unchanged — it is\n * the RFC primitive. Only this relay-gate call site is widened.\n */\nexport const RELAY_VERIFY_SKEW_STEPS = 6;\n\n/**\n * Reads `AIT_DEBUG_TOTP_SECRET` from `process.env` at runtime and builds a\n * `verifyAuth` predicate for the Chii relay's WebSocket upgrade gate.\n *\n * The predicate checks the `at` query parameter against the current and\n * adjacent TOTP time steps (±{@link RELAY_VERIFY_SKEW_STEPS} skew) using\n * {@link verifyTotp}. This gives the issued code a minimum validity of ~3\n * minutes, which is enough to cover the QR-scan → launcher-attach flow even\n * when the launcher PWA needs to load or reinstall (#490).\n *\n * Returns `undefined` when the env var is not set — callers treat that as\n * \"auth disabled\" (no predicate registered on the relay). Note that since\n * issue #250 the secret is MANDATORY at every relay-boot site (enforced by\n * {@link assertRelayAuthConfigured} BEFORE the relay starts), so in production\n * this never returns `undefined` for a relay that actually boots; the\n * `undefined` branch only matters for the no-relay local path and tests.\n *\n * Lives here (not in the MCP server) so the unplugin's env-2 relay can wire the\n * same gate without importing the heavy MCP server module graph. Re-exported\n * from `debug-server.ts` for back-compat.\n *\n * SECRET-HANDLING: The secret value read from env is captured in a closure and\n * is NEVER written to any log, error message, or process output.\n */\nexport function buildRelayVerifyAuth(\n env: NodeJS.ProcessEnv = process.env,\n): ((req: import('node:http').IncomingMessage) => boolean) | undefined {\n const secret = env.AIT_DEBUG_TOTP_SECRET;\n if (!secret) return undefined;\n\n return (req) => {\n // Parse the `at` query param from the upgrade request URL.\n // req.url is the raw request path + query, e.g. `/client/id?target=…&at=123456`\n const rawUrl = req.url ?? '';\n const qIndex = rawUrl.indexOf('?');\n const queryStr = qIndex === -1 ? '' : rawUrl.slice(qIndex + 1);\n const params = new URLSearchParams(queryStr);\n const code = params.get('at') ?? '';\n\n // Do NOT log `code`, `secret`, or any derived value here.\n // Use RELAY_VERIFY_SKEW_STEPS (±6) for a ~3-minute acceptance window (#490).\n // verifyTotp's own default (skew=1) is unchanged — only this call site is widened.\n return verifyTotp(secret, code, undefined, RELAY_VERIFY_SKEW_STEPS);\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAM,YAAY;;AAGlB,MAAM,SAAS;;;;;;;;;;AAWf,SAAgB,aAAa,QAAgB,OAAe,KAAK,KAAK,EAAU;CAC9E,MAAM,MAAM,OAAO,KAAK,QAAQ,MAAM;CAGtC,MAAM,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,MAAO,UAAU,CAAC;CAGhE,MAAM,aAAa,OAAO,MAAM,EAAE;CAGlC,MAAM,KAAK,KAAK,MAAM,UAAU,WAAY;CAC5C,MAAM,KAAK,YAAY;AACvB,YAAW,cAAc,IAAI,EAAE;AAC/B,YAAW,cAAc,IAAI,EAAE;CAE/B,MAAM,OAAA,GAAA,YAAA,YAAiB,QAAQ,IAAI,CAAC,OAAO,WAAW,CAAC,QAAQ;CAG/D,MAAM,SAAS,IAAI,MAAM;AAQzB,WANI,IAAI,UAAU,QAAS,MACvB,IAAI,SAAS,KAAK,QAAS,MAC3B,IAAI,SAAS,KAAK,QAAS,IAC5B,IAAI,SAAS,KAAK,OAEC,MAAM,QACjB,UAAU,CAAC,SAAS,QAAQ,IAAI;;;;;;;;;;;;;;;;AAiB7C,SAAgB,WACd,QACA,MACA,OAAe,KAAK,KAAK,EACzB,OAAe,GACN;CACT,MAAM,aAAa,OAAO,KAAK,CAAC,SAAS,QAAQ,IAAI;AACrD,KAAI,WAAW,WAAW,UAAU,CAAC,UAAU,KAAK,WAAW,CAC7D,QAAO;CAGT,MAAM,eAAe,OAAO,KAAK,YAAY,OAAO;AAEpD,MAAK,IAAI,QAAQ,CAAC,MAAM,SAAS,MAAM,SAAS;EAE9C,MAAM,WAAW,aAAa,QADb,OAAO,QAAQ,YAAY,IACG;AAE/C,OAAA,GAAA,YAAA,iBADoB,OAAO,KAAK,UAAU,OAAO,EAChB,aAAa,CAC5C,QAAO;;AAIX,QAAO;;;;;;;;;;AAWT,MAAM,uBAAuB;;AAG7B,MAAM,SAAS;;;;;;;;;;;;;AAcf,MAAa,oCAAoC;CAC/C;CACA;CACA;CACA;CACA;CACD,CAAC,KAAK,KAAK;;;;;;;;;;;;;AAcZ,SAAgB,uBAAuB,QAA8C;AACnF,KAAI,WAAW,KAAA,KAAa,WAAW,GAAI,QAAO;AAClD,KAAI,OAAO,SAAS,qBAAsB,QAAO;AACjD,KAAI,OAAO,SAAS,MAAM,EAAG,QAAO;AACpC,QAAO,OAAO,KAAK,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;AA0B5B,SAAgB,0BAA0B,MAAyB,QAAQ,KAAW;AACpF,KAAI,CAAC,uBAAuB,IAAI,sBAAsB,CACpD,OAAM,IAAI,MAAM,kCAAkC;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDtD,SAAgB,qBACd,MAAyB,QAAQ,KACoC;CACrE,MAAM,SAAS,IAAI;AACnB,KAAI,CAAC,OAAQ,QAAO,KAAA;AAEpB,SAAQ,QAAQ;EAGd,MAAM,SAAS,IAAI,OAAO;EAC1B,MAAM,SAAS,OAAO,QAAQ,IAAI;EAClC,MAAM,WAAW,WAAW,KAAK,KAAK,OAAO,MAAM,SAAS,EAAE;AAO9D,SAAO,WAAW,QANH,IAAI,gBAAgB,SAAS,CACxB,IAAI,KAAK,IAAI,IAKD,KAAA,GAAA,EAAmC"}
@@ -1,211 +0,0 @@
1
- import "node:module";
2
- import { createHmac, timingSafeEqual } from "node:crypto";
3
- //#region \0rolldown/runtime.js
4
- var __defProp = Object.defineProperty;
5
- var __exportAll = (all, no_symbols) => {
6
- let target = {};
7
- for (var name in all) __defProp(target, name, {
8
- get: all[name],
9
- enumerable: true
10
- });
11
- if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
12
- return target;
13
- };
14
- //#endregion
15
- //#region src/mcp/totp.ts
16
- /**
17
- * RFC 6238 TOTP implementation (Node.js, node:crypto only).
18
- *
19
- * External TOTP libraries (otplib, speakeasy, …) are intentionally NOT used
20
- * to keep the dependency surface minimal. This hand-roll is ~30 lines and
21
- * covers exactly what relay-side auth needs.
22
- *
23
- * Algorithm summary (RFC 6238 + RFC 4226):
24
- * T = floor(now / 30) — 30-second time step counter
25
- * K = Buffer.from(secret, 'hex') — shared secret (raw bytes, hex-encoded)
26
- * MAC = HMAC-SHA1(K, T as 8-byte big-endian uint64)
27
- * offset = MAC[19] & 0x0f
28
- * code = (MAC[offset..offset+4] & 0x7fffffff) % 10^6 — 6 digits
29
- *
30
- * Security note (keep this comment accurate):
31
- * The baked-in secret in a dog-food build is extractable from the bundle by a
32
- * determined reverse engineer. This mechanism raises the bar from
33
- * "anyone with the URL" to "URL + bundle extraction + live TOTP calculation".
34
- * Casual URL leaks (Slack paste, QR screenshot, shoulder-surfing) are
35
- * blocked; deliberate reverse engineering is not. See threat model in
36
- * src/mcp/chii-relay.ts and umbrella CLAUDE.md §4.
37
- *
38
- * SECRET-HANDLING: secret values and computed codes MUST NOT appear in any
39
- * log, error message, or string visible outside this module. Only boolean
40
- * pass/fail and reason enum values are safe to surface.
41
- */
42
- var totp_exports = /* @__PURE__ */ __exportAll({
43
- RELAY_AUTH_SECRET_MISSING_MESSAGE: () => RELAY_AUTH_SECRET_MISSING_MESSAGE,
44
- RELAY_VERIFY_SKEW_STEPS: () => 6,
45
- assertRelayAuthConfigured: () => assertRelayAuthConfigured,
46
- buildRelayVerifyAuth: () => buildRelayVerifyAuth,
47
- generateTotp: () => generateTotp,
48
- isValidRelayAuthSecret: () => isValidRelayAuthSecret,
49
- verifyTotp: () => verifyTotp
50
- });
51
- /** Time step window in seconds (RFC 6238 default). */
52
- const TIME_STEP = 30;
53
- /** Number of digits in the generated code. */
54
- const DIGITS = 6;
55
- /**
56
- * Derives a 6-digit TOTP code from a hex-encoded secret at the given wall-
57
- * clock time.
58
- *
59
- * @param secret - The shared secret as a hex string (e.g. 64 hex chars = 32
60
- * bytes). Must be the output of `generateAttachToken()` or compatible.
61
- * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
62
- * @returns A zero-padded 6-digit decimal string, e.g. `"042193"`.
63
- */
64
- function generateTotp(secret, when = Date.now()) {
65
- const key = Buffer.from(secret, "hex");
66
- const counter = Math.max(0, Math.floor(when / 1e3 / TIME_STEP));
67
- const counterBuf = Buffer.alloc(8);
68
- const hi = Math.floor(counter / 4294967296);
69
- const lo = counter >>> 0;
70
- counterBuf.writeUInt32BE(hi, 0);
71
- counterBuf.writeUInt32BE(lo, 4);
72
- const mac = createHmac("sha1", key).update(counterBuf).digest();
73
- const offset = mac[19] & 15;
74
- return (((mac[offset] & 127) << 24 | (mac[offset + 1] & 255) << 16 | (mac[offset + 2] & 255) << 8 | mac[offset + 3] & 255) % 10 ** DIGITS).toString().padStart(DIGITS, "0");
75
- }
76
- /**
77
- * Verifies a TOTP code against the secret, accepting ±`skew` time steps to
78
- * tolerate clock drift between the relay host and the client device.
79
- *
80
- * Uses `timingSafeEqual` for constant-time comparison to prevent timing
81
- * side-channel attacks.
82
- *
83
- * @param secret - Hex-encoded shared secret.
84
- * @param code - The 6-digit code to verify (string or numeric).
85
- * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
86
- * @param skew - Number of adjacent steps to accept on either side. Default 1
87
- * (accepts T-1, T, T+1 — a 90-second acceptance window).
88
- * @returns `true` if the code matches any accepted step, `false` otherwise.
89
- */
90
- function verifyTotp(secret, code, when = Date.now(), skew = 1) {
91
- const normalised = String(code).padStart(DIGITS, "0");
92
- if (normalised.length !== DIGITS || !/^\d{6}$/.test(normalised)) return false;
93
- const candidateBuf = Buffer.from(normalised, "utf8");
94
- for (let delta = -skew; delta <= skew; delta++) {
95
- const expected = generateTotp(secret, when + delta * TIME_STEP * 1e3);
96
- if (timingSafeEqual(Buffer.from(expected, "utf8"), candidateBuf)) return true;
97
- }
98
- return false;
99
- }
100
- /**
101
- * Minimum length (in hex characters) accepted for `AIT_DEBUG_TOTP_SECRET`.
102
- *
103
- * The secret is hex-encoded (see {@link generateTotp} — `Buffer.from(secret,
104
- * 'hex')`). 32 hex chars = 16 bytes = 128 bits, the floor for an HMAC-SHA1 key
105
- * we are willing to gate a public relay behind. `generateAttachToken()` emits
106
- * 64 hex chars (32 bytes), comfortably above this bar.
107
- */
108
- const MIN_SECRET_HEX_CHARS = 32;
109
- /** Hex string: one or more hex digits, case-insensitive (RFC 4648 base16). */
110
- const HEX_RE = /^[0-9a-fA-F]+$/;
111
- /**
112
- * Human-facing guidance printed when {@link assertRelayAuthConfigured} fails.
113
- *
114
- * SECRET-HANDLING: this message states only the REQUIREMENT (≥32 hex chars) and
115
- * how to mint one. It NEVER echoes the configured value, its length, or any
116
- * fragment derived from it — see {@link assertRelayAuthConfigured}.
117
- *
118
- * Note on encoding: the secret is hex (base16), not base32 — `generateTotp`
119
- * decodes it with `Buffer.from(secret, 'hex')`. A base32 string would be
120
- * silently mis-decoded and every TOTP code would fail to match, so the minting
121
- * command emits hex.
122
- */
123
- const RELAY_AUTH_SECRET_MISSING_MESSAGE = [
124
- "[ait-debug] AIT_DEBUG_TOTP_SECRET이 필수입니다. 32자 이상 16진수(hex) 문자열을 설정하세요.",
125
- "발급: openssl rand -hex 32",
126
- "데몬은 start_debug의 projectRoot 인자로 받은 디렉토리에서 .ait_relay 파일을 읽어 이 시크릿을 채웁니다.",
127
- "프로젝트에서 pnpm dev:phone:cdp를 한 번 띄우면 unplugin이 .ait_relay를 자동 생성하니(tunnel.cdp 옵션 필요), projectRoot를 전달하세요.",
128
- "자세히: https://docs.aitc.dev/guides/relay-auth-totp"
129
- ].join("\n");
130
- /**
131
- * Whether `secret` is a well-formed relay-auth TOTP secret: a hex string of at
132
- * least {@link MIN_SECRET_HEX_CHARS} characters with an even length (an odd
133
- * length would have its trailing nibble silently dropped by `Buffer.from(...,
134
- * 'hex')`, weakening the key without warning).
135
- *
136
- * Pure predicate so callers can test the validation independently of the
137
- * fail-fast side effect in {@link assertRelayAuthConfigured}.
138
- *
139
- * SECRET-HANDLING: returns only a boolean — the input value is never returned,
140
- * logged, or echoed.
141
- */
142
- function isValidRelayAuthSecret(secret) {
143
- if (secret === void 0 || secret === "") return false;
144
- if (secret.length < MIN_SECRET_HEX_CHARS) return false;
145
- if (secret.length % 2 !== 0) return false;
146
- return HEX_RE.test(secret);
147
- }
148
- /**
149
- * Fail-fast guard enforcing that a relay-auth TOTP secret is configured before
150
- * a public-internet-exposed relay is booted (issue #250).
151
- *
152
- * Relay-auth (the §4 Layer C TOTP gate) is the only fail-fast layer that closes
153
- * the real gap: a leaked `wss://…trycloudflare.com` URL otherwise lets a third
154
- * party attach a debugger to a dog-food/live mini-app. Without a secret the relay
155
- * comes up unauthenticated, so this guard is called at every relay-boot site —
156
- * `bootRelayFamily` (intoss env 3/4) and `bootExternalRelayFamily` (env-2 PWA),
157
- * both eager and lazy. Local-only sessions never boot a relay and so never reach
158
- * this guard, matching the issue's exemption for non-relay debugging.
159
- *
160
- * Throws when the secret is unset, empty, too short, or not a valid hex string.
161
- * The thrown message is the bin entry's fatal stderr (see `cli.ts` `main().catch`)
162
- * — the same fatal model as the missing-`AIT_RELAY_BASE_URL` path.
163
- *
164
- * SECRET-HANDLING: the env value is read once, passed ONLY to the boolean
165
- * predicate, and never logged. The thrown message names the requirement, never
166
- * the value, its length, or any derived fragment.
167
- *
168
- * @param env - Environment to read from. Defaults to `process.env`; injectable
169
- * for tests so they never mutate the real process environment.
170
- */
171
- function assertRelayAuthConfigured(env = process.env) {
172
- if (!isValidRelayAuthSecret(env.AIT_DEBUG_TOTP_SECRET)) throw new Error(RELAY_AUTH_SECRET_MISSING_MESSAGE);
173
- }
174
- /**
175
- * Reads `AIT_DEBUG_TOTP_SECRET` from `process.env` at runtime and builds a
176
- * `verifyAuth` predicate for the Chii relay's WebSocket upgrade gate.
177
- *
178
- * The predicate checks the `at` query parameter against the current and
179
- * adjacent TOTP time steps (±{@link RELAY_VERIFY_SKEW_STEPS} skew) using
180
- * {@link verifyTotp}. This gives the issued code a minimum validity of ~3
181
- * minutes, which is enough to cover the QR-scan → launcher-attach flow even
182
- * when the launcher PWA needs to load or reinstall (#490).
183
- *
184
- * Returns `undefined` when the env var is not set — callers treat that as
185
- * "auth disabled" (no predicate registered on the relay). Note that since
186
- * issue #250 the secret is MANDATORY at every relay-boot site (enforced by
187
- * {@link assertRelayAuthConfigured} BEFORE the relay starts), so in production
188
- * this never returns `undefined` for a relay that actually boots; the
189
- * `undefined` branch only matters for the no-relay local path and tests.
190
- *
191
- * Lives here (not in the MCP server) so the unplugin's env-2 relay can wire the
192
- * same gate without importing the heavy MCP server module graph. Re-exported
193
- * from `debug-server.ts` for back-compat.
194
- *
195
- * SECRET-HANDLING: The secret value read from env is captured in a closure and
196
- * is NEVER written to any log, error message, or process output.
197
- */
198
- function buildRelayVerifyAuth(env = process.env) {
199
- const secret = env.AIT_DEBUG_TOTP_SECRET;
200
- if (!secret) return void 0;
201
- return (req) => {
202
- const rawUrl = req.url ?? "";
203
- const qIndex = rawUrl.indexOf("?");
204
- const queryStr = qIndex === -1 ? "" : rawUrl.slice(qIndex + 1);
205
- return verifyTotp(secret, new URLSearchParams(queryStr).get("at") ?? "", void 0, 6);
206
- };
207
- }
208
- //#endregion
209
- export { totp_exports as i, buildRelayVerifyAuth as n, generateTotp as r, assertRelayAuthConfigured as t };
210
-
211
- //# sourceMappingURL=totp-DfekTBk3.js.map