@ait-co/devtools 0.1.132 → 0.1.134

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 (85) hide show
  1. package/dist/{attach-orchestrator-ca34r46y.js → attach-orchestrator-CE0S09YU.js} +110 -5
  2. package/dist/attach-orchestrator-CE0S09YU.js.map +1 -0
  3. package/dist/{attach-orchestrator-BiaYiNOr.js → attach-orchestrator-CMoDuG2A.js} +110 -5
  4. package/dist/attach-orchestrator-CMoDuG2A.js.map +1 -0
  5. package/dist/{cdp-connection-D7AYQck2.d.ts → cdp-connection-BYE9meXe.d.ts} +12 -2
  6. package/dist/{cdp-connection-D7AYQck2.d.ts.map → cdp-connection-BYE9meXe.d.ts.map} +1 -1
  7. package/dist/cell-BIdSQb9T.js +201 -0
  8. package/dist/cell-BIdSQb9T.js.map +1 -0
  9. package/dist/{cell-Cw4fVOl2.js → cell-D1y4shoV.js} +1 -21
  10. package/dist/cell-D1y4shoV.js.map +1 -0
  11. package/dist/cell-DHA578lX.js +200 -0
  12. package/dist/cell-DHA578lX.js.map +1 -0
  13. package/dist/{debug-server-Ch5p0ryv.js → debug-server-BcSBrajf.js} +19 -9
  14. package/dist/debug-server-BcSBrajf.js.map +1 -0
  15. package/dist/{debug-server-DoWAtTNj.js → debug-server-D9SmprUI.js} +7 -8
  16. package/dist/{debug-server-DoWAtTNj.js.map → debug-server-D9SmprUI.js.map} +1 -1
  17. package/dist/{debug-server-BBUzxZSf.js → debug-server-DnNCoNiR.js} +7 -7
  18. package/dist/{debug-server-BBUzxZSf.js.map → debug-server-DnNCoNiR.js.map} +1 -1
  19. package/dist/mcp/cli.js +149 -16
  20. package/dist/mcp/cli.js.map +1 -1
  21. package/dist/mcp/server.js +1 -1
  22. package/dist/panel/index.js +1 -1
  23. package/dist/{pool-BtLh7U1y.d.ts → pool-C6TgrcyW.d.ts} +3 -3
  24. package/dist/{pool-BtLh7U1y.d.ts.map → pool-C6TgrcyW.d.ts.map} +1 -1
  25. package/dist/{qr-http-server-KB147M-S.js → qr-http-server-C_DTp9WU.js} +1 -1
  26. package/dist/{qr-http-server-KB147M-S.js.map → qr-http-server-C_DTp9WU.js.map} +1 -1
  27. package/dist/{relay-factory-cNgPfrWg.js → relay-factory-CsVO9o25.js} +6 -6
  28. package/dist/{relay-factory-cNgPfrWg.js.map → relay-factory-CsVO9o25.js.map} +1 -1
  29. package/dist/{relay-secret-store-CFc9n0OA.js → relay-secret-store-CLEGyHou.js} +2 -2
  30. package/dist/{relay-secret-store-CFc9n0OA.js.map → relay-secret-store-CLEGyHou.js.map} +1 -1
  31. package/dist/{relay-secret-store-DzxDsK6S.js → relay-secret-store-DGduVJhs.js} +2 -2
  32. package/dist/{relay-secret-store-DzxDsK6S.js.map → relay-secret-store-DGduVJhs.js.map} +1 -1
  33. package/dist/{relay-url-store-D3rY-GJJ.js → relay-url-store-B0X8TsGr.js} +2 -2
  34. package/dist/{relay-url-store-D3rY-GJJ.js.map → relay-url-store-B0X8TsGr.js.map} +1 -1
  35. package/dist/{relay-url-store-DGQ-HPQC.js → relay-url-store-CKW8RQzf.js} +2 -2
  36. package/dist/{relay-url-store-DGQ-HPQC.js.map → relay-url-store-CKW8RQzf.js.map} +1 -1
  37. package/dist/{relay-worker-BMSnlyls.d.ts → relay-worker-Dw3aIzIK.d.ts} +15 -3
  38. package/dist/relay-worker-Dw3aIzIK.d.ts.map +1 -0
  39. package/dist/{relay-worker-l34xNuci.js → relay-worker-UK0nBfDX.js} +25 -11
  40. package/dist/relay-worker-UK0nBfDX.js.map +1 -0
  41. package/dist/rolldown-runtime-DGkTqVfb.js +15 -0
  42. package/dist/rolldown-runtime-DUslC3ob.js +14 -0
  43. package/dist/{runtime-9xhN9pr8.d.ts → runtime-DfHHZms2.d.ts} +34 -5
  44. package/dist/runtime-DfHHZms2.d.ts.map +1 -0
  45. package/dist/test-runner/bin.js +47 -112
  46. package/dist/test-runner/bin.js.map +1 -1
  47. package/dist/test-runner/config.d.ts +1 -1
  48. package/dist/test-runner/config.js +1 -1
  49. package/dist/test-runner/pool.d.ts +1 -1
  50. package/dist/test-runner/pool.js +1 -1
  51. package/dist/test-runner/relay-factory.d.ts +12 -2
  52. package/dist/test-runner/relay-factory.d.ts.map +1 -1
  53. package/dist/test-runner/relay-factory.js +3 -3
  54. package/dist/test-runner/relay-worker.d.ts +1 -1
  55. package/dist/test-runner/relay-worker.js +1 -1
  56. package/dist/test-runner/report.d.ts +11 -2
  57. package/dist/test-runner/report.d.ts.map +1 -1
  58. package/dist/test-runner/report.js +1 -0
  59. package/dist/test-runner/report.js.map +1 -1
  60. package/dist/test-runner/rpc.d.ts +8 -5
  61. package/dist/test-runner/rpc.d.ts.map +1 -1
  62. package/dist/test-runner/rpc.js +15 -4
  63. package/dist/test-runner/rpc.js.map +1 -1
  64. package/dist/test-runner/runtime.d.ts +2 -2
  65. package/dist/test-runner/runtime.js +30 -4
  66. package/dist/test-runner/runtime.js.map +1 -1
  67. package/dist/test-runner/task-graph.d.ts +1 -1
  68. package/dist/{totp-DvOYkYim.js → totp-CZLLKfOC.js} +2 -14
  69. package/dist/{totp-DvOYkYim.js.map → totp-CZLLKfOC.js.map} +1 -1
  70. package/dist/totp-DAxys-r0.js +199 -0
  71. package/dist/totp-DAxys-r0.js.map +1 -0
  72. package/package.json +1 -1
  73. package/dist/attach-orchestrator-BiaYiNOr.js.map +0 -1
  74. package/dist/attach-orchestrator-ca34r46y.js.map +0 -1
  75. package/dist/cell-Bi8MAPA8.js +0 -88
  76. package/dist/cell-Bi8MAPA8.js.map +0 -1
  77. package/dist/cell-Cw4fVOl2.js.map +0 -1
  78. package/dist/cell-rr6gL9AT.js +0 -89
  79. package/dist/cell-rr6gL9AT.js.map +0 -1
  80. package/dist/debug-server-Ch5p0ryv.js.map +0 -1
  81. package/dist/log-CUik_tHp.js +0 -95
  82. package/dist/log-CUik_tHp.js.map +0 -1
  83. package/dist/relay-worker-BMSnlyls.d.ts.map +0 -1
  84. package/dist/relay-worker-l34xNuci.js.map +0 -1
  85. package/dist/runtime-9xhN9pr8.d.ts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"totp-DvOYkYim.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
+ {"version":3,"file":"totp-CZLLKfOC.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"}
@@ -0,0 +1,199 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-DUslC3ob.js";
2
+ import { createHmac, timingSafeEqual } from "node:crypto";
3
+ //#region src/mcp/totp.ts
4
+ /**
5
+ * RFC 6238 TOTP implementation (Node.js, node:crypto only).
6
+ *
7
+ * External TOTP libraries (otplib, speakeasy, …) are intentionally NOT used
8
+ * to keep the dependency surface minimal. This hand-roll is ~30 lines and
9
+ * covers exactly what relay-side auth needs.
10
+ *
11
+ * Algorithm summary (RFC 6238 + RFC 4226):
12
+ * T = floor(now / 30) — 30-second time step counter
13
+ * K = Buffer.from(secret, 'hex') — shared secret (raw bytes, hex-encoded)
14
+ * MAC = HMAC-SHA1(K, T as 8-byte big-endian uint64)
15
+ * offset = MAC[19] & 0x0f
16
+ * code = (MAC[offset..offset+4] & 0x7fffffff) % 10^6 — 6 digits
17
+ *
18
+ * Security note (keep this comment accurate):
19
+ * The baked-in secret in a dog-food build is extractable from the bundle by a
20
+ * determined reverse engineer. This mechanism raises the bar from
21
+ * "anyone with the URL" to "URL + bundle extraction + live TOTP calculation".
22
+ * Casual URL leaks (Slack paste, QR screenshot, shoulder-surfing) are
23
+ * blocked; deliberate reverse engineering is not. See threat model in
24
+ * src/mcp/chii-relay.ts and umbrella CLAUDE.md §4.
25
+ *
26
+ * SECRET-HANDLING: secret values and computed codes MUST NOT appear in any
27
+ * log, error message, or string visible outside this module. Only boolean
28
+ * pass/fail and reason enum values are safe to surface.
29
+ */
30
+ var totp_exports = /* @__PURE__ */ __exportAll({
31
+ RELAY_AUTH_SECRET_MISSING_MESSAGE: () => RELAY_AUTH_SECRET_MISSING_MESSAGE,
32
+ RELAY_VERIFY_SKEW_STEPS: () => 6,
33
+ assertRelayAuthConfigured: () => assertRelayAuthConfigured,
34
+ buildRelayVerifyAuth: () => buildRelayVerifyAuth,
35
+ generateTotp: () => generateTotp,
36
+ isValidRelayAuthSecret: () => isValidRelayAuthSecret,
37
+ verifyTotp: () => verifyTotp
38
+ });
39
+ /** Time step window in seconds (RFC 6238 default). */
40
+ const TIME_STEP = 30;
41
+ /** Number of digits in the generated code. */
42
+ const DIGITS = 6;
43
+ /**
44
+ * Derives a 6-digit TOTP code from a hex-encoded secret at the given wall-
45
+ * clock time.
46
+ *
47
+ * @param secret - The shared secret as a hex string (e.g. 64 hex chars = 32
48
+ * bytes). Must be the output of `generateAttachToken()` or compatible.
49
+ * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
50
+ * @returns A zero-padded 6-digit decimal string, e.g. `"042193"`.
51
+ */
52
+ function generateTotp(secret, when = Date.now()) {
53
+ const key = Buffer.from(secret, "hex");
54
+ const counter = Math.max(0, Math.floor(when / 1e3 / TIME_STEP));
55
+ const counterBuf = Buffer.alloc(8);
56
+ const hi = Math.floor(counter / 4294967296);
57
+ const lo = counter >>> 0;
58
+ counterBuf.writeUInt32BE(hi, 0);
59
+ counterBuf.writeUInt32BE(lo, 4);
60
+ const mac = createHmac("sha1", key).update(counterBuf).digest();
61
+ const offset = mac[19] & 15;
62
+ 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");
63
+ }
64
+ /**
65
+ * Verifies a TOTP code against the secret, accepting ±`skew` time steps to
66
+ * tolerate clock drift between the relay host and the client device.
67
+ *
68
+ * Uses `timingSafeEqual` for constant-time comparison to prevent timing
69
+ * side-channel attacks.
70
+ *
71
+ * @param secret - Hex-encoded shared secret.
72
+ * @param code - The 6-digit code to verify (string or numeric).
73
+ * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
74
+ * @param skew - Number of adjacent steps to accept on either side. Default 1
75
+ * (accepts T-1, T, T+1 — a 90-second acceptance window).
76
+ * @returns `true` if the code matches any accepted step, `false` otherwise.
77
+ */
78
+ function verifyTotp(secret, code, when = Date.now(), skew = 1) {
79
+ const normalised = String(code).padStart(DIGITS, "0");
80
+ if (normalised.length !== DIGITS || !/^\d{6}$/.test(normalised)) return false;
81
+ const candidateBuf = Buffer.from(normalised, "utf8");
82
+ for (let delta = -skew; delta <= skew; delta++) {
83
+ const expected = generateTotp(secret, when + delta * TIME_STEP * 1e3);
84
+ if (timingSafeEqual(Buffer.from(expected, "utf8"), candidateBuf)) return true;
85
+ }
86
+ return false;
87
+ }
88
+ /**
89
+ * Minimum length (in hex characters) accepted for `AIT_DEBUG_TOTP_SECRET`.
90
+ *
91
+ * The secret is hex-encoded (see {@link generateTotp} — `Buffer.from(secret,
92
+ * 'hex')`). 32 hex chars = 16 bytes = 128 bits, the floor for an HMAC-SHA1 key
93
+ * we are willing to gate a public relay behind. `generateAttachToken()` emits
94
+ * 64 hex chars (32 bytes), comfortably above this bar.
95
+ */
96
+ const MIN_SECRET_HEX_CHARS = 32;
97
+ /** Hex string: one or more hex digits, case-insensitive (RFC 4648 base16). */
98
+ const HEX_RE = /^[0-9a-fA-F]+$/;
99
+ /**
100
+ * Human-facing guidance printed when {@link assertRelayAuthConfigured} fails.
101
+ *
102
+ * SECRET-HANDLING: this message states only the REQUIREMENT (≥32 hex chars) and
103
+ * how to mint one. It NEVER echoes the configured value, its length, or any
104
+ * fragment derived from it — see {@link assertRelayAuthConfigured}.
105
+ *
106
+ * Note on encoding: the secret is hex (base16), not base32 — `generateTotp`
107
+ * decodes it with `Buffer.from(secret, 'hex')`. A base32 string would be
108
+ * silently mis-decoded and every TOTP code would fail to match, so the minting
109
+ * command emits hex.
110
+ */
111
+ const RELAY_AUTH_SECRET_MISSING_MESSAGE = [
112
+ "[ait-debug] AIT_DEBUG_TOTP_SECRET이 필수입니다. 32자 이상 16진수(hex) 문자열을 설정하세요.",
113
+ "발급: openssl rand -hex 32",
114
+ "데몬은 start_debug의 projectRoot 인자로 받은 디렉토리에서 .ait_relay 파일을 읽어 이 시크릿을 채웁니다.",
115
+ "프로젝트에서 pnpm dev:phone:cdp를 한 번 띄우면 unplugin이 .ait_relay를 자동 생성하니(tunnel.cdp 옵션 필요), projectRoot를 전달하세요.",
116
+ "자세히: https://docs.aitc.dev/guides/relay-auth-totp"
117
+ ].join("\n");
118
+ /**
119
+ * Whether `secret` is a well-formed relay-auth TOTP secret: a hex string of at
120
+ * least {@link MIN_SECRET_HEX_CHARS} characters with an even length (an odd
121
+ * length would have its trailing nibble silently dropped by `Buffer.from(...,
122
+ * 'hex')`, weakening the key without warning).
123
+ *
124
+ * Pure predicate so callers can test the validation independently of the
125
+ * fail-fast side effect in {@link assertRelayAuthConfigured}.
126
+ *
127
+ * SECRET-HANDLING: returns only a boolean — the input value is never returned,
128
+ * logged, or echoed.
129
+ */
130
+ function isValidRelayAuthSecret(secret) {
131
+ if (secret === void 0 || secret === "") return false;
132
+ if (secret.length < MIN_SECRET_HEX_CHARS) return false;
133
+ if (secret.length % 2 !== 0) return false;
134
+ return HEX_RE.test(secret);
135
+ }
136
+ /**
137
+ * Fail-fast guard enforcing that a relay-auth TOTP secret is configured before
138
+ * a public-internet-exposed relay is booted (issue #250).
139
+ *
140
+ * Relay-auth (the §4 Layer C TOTP gate) is the only fail-fast layer that closes
141
+ * the real gap: a leaked `wss://…trycloudflare.com` URL otherwise lets a third
142
+ * party attach a debugger to a dog-food/live mini-app. Without a secret the relay
143
+ * comes up unauthenticated, so this guard is called at every relay-boot site —
144
+ * `bootRelayFamily` (intoss env 3/4) and `bootExternalRelayFamily` (env-2 PWA),
145
+ * both eager and lazy. Local-only sessions never boot a relay and so never reach
146
+ * this guard, matching the issue's exemption for non-relay debugging.
147
+ *
148
+ * Throws when the secret is unset, empty, too short, or not a valid hex string.
149
+ * The thrown message is the bin entry's fatal stderr (see `cli.ts` `main().catch`)
150
+ * — the same fatal model as the missing-`AIT_RELAY_BASE_URL` path.
151
+ *
152
+ * SECRET-HANDLING: the env value is read once, passed ONLY to the boolean
153
+ * predicate, and never logged. The thrown message names the requirement, never
154
+ * the value, its length, or any derived fragment.
155
+ *
156
+ * @param env - Environment to read from. Defaults to `process.env`; injectable
157
+ * for tests so they never mutate the real process environment.
158
+ */
159
+ function assertRelayAuthConfigured(env = process.env) {
160
+ if (!isValidRelayAuthSecret(env.AIT_DEBUG_TOTP_SECRET)) throw new Error(RELAY_AUTH_SECRET_MISSING_MESSAGE);
161
+ }
162
+ /**
163
+ * Reads `AIT_DEBUG_TOTP_SECRET` from `process.env` at runtime and builds a
164
+ * `verifyAuth` predicate for the Chii relay's WebSocket upgrade gate.
165
+ *
166
+ * The predicate checks the `at` query parameter against the current and
167
+ * adjacent TOTP time steps (±{@link RELAY_VERIFY_SKEW_STEPS} skew) using
168
+ * {@link verifyTotp}. This gives the issued code a minimum validity of ~3
169
+ * minutes, which is enough to cover the QR-scan → launcher-attach flow even
170
+ * when the launcher PWA needs to load or reinstall (#490).
171
+ *
172
+ * Returns `undefined` when the env var is not set — callers treat that as
173
+ * "auth disabled" (no predicate registered on the relay). Note that since
174
+ * issue #250 the secret is MANDATORY at every relay-boot site (enforced by
175
+ * {@link assertRelayAuthConfigured} BEFORE the relay starts), so in production
176
+ * this never returns `undefined` for a relay that actually boots; the
177
+ * `undefined` branch only matters for the no-relay local path and tests.
178
+ *
179
+ * Lives here (not in the MCP server) so the unplugin's env-2 relay can wire the
180
+ * same gate without importing the heavy MCP server module graph. Re-exported
181
+ * from `debug-server.ts` for back-compat.
182
+ *
183
+ * SECRET-HANDLING: The secret value read from env is captured in a closure and
184
+ * is NEVER written to any log, error message, or process output.
185
+ */
186
+ function buildRelayVerifyAuth(env = process.env) {
187
+ const secret = env.AIT_DEBUG_TOTP_SECRET;
188
+ if (!secret) return void 0;
189
+ return (req) => {
190
+ const rawUrl = req.url ?? "";
191
+ const qIndex = rawUrl.indexOf("?");
192
+ const queryStr = qIndex === -1 ? "" : rawUrl.slice(qIndex + 1);
193
+ return verifyTotp(secret, new URLSearchParams(queryStr).get("at") ?? "", void 0, 6);
194
+ };
195
+ }
196
+ //#endregion
197
+ export { totp_exports as i, buildRelayVerifyAuth as n, generateTotp as r, assertRelayAuthConfigured as t };
198
+
199
+ //# sourceMappingURL=totp-DAxys-r0.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"totp-DAxys-r0.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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ait-co/devtools",
3
- "version": "0.1.132",
3
+ "version": "0.1.134",
4
4
  "description": "Development tools for Apps in Toss mini-apps — mock SDK, floating devtools panel, and universal bundler plugin",
5
5
  "type": "module",
6
6
  "engines": {