@ait-co/devtools 0.1.110 → 0.1.111

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.
@@ -255,7 +255,7 @@ const aitDevtoolsPlugin = (0, unplugin.createUnplugin)((options) => {
255
255
  let relayHttpUrl;
256
256
  let relayLocalHttpUrl;
257
257
  if (tunnelConfig.cdp) try {
258
- const { ensureRelaySecret } = await Promise.resolve().then(() => require("../relay-secret-store-CPBBlV3J.cjs"));
258
+ const { ensureRelaySecret } = await Promise.resolve().then(() => require("../relay-secret-store-DWKdV-eY.cjs"));
259
259
  await ensureRelaySecret({ projectRoot: server.config.root });
260
260
  const { assertRelayAuthConfigured, buildRelayVerifyAuth } = await Promise.resolve().then(() => require("../totp-Df252ZdA.cjs"));
261
261
  assertRelayAuthConfigured();
@@ -297,7 +297,7 @@ const aitDevtoolsPlugin = (0, unplugin.createUnplugin)((options) => {
297
297
  navBarTransparent,
298
298
  navBarTheme
299
299
  });
300
- const { writeRelayUrls, deleteRelayUrls } = await Promise.resolve().then(() => require("../relay-url-store-C9QLhB2p.cjs"));
300
+ const { writeRelayUrls, deleteRelayUrls } = await Promise.resolve().then(() => require("../relay-url-store-1FGuSYAn.cjs"));
301
301
  await writeRelayUrls({
302
302
  projectRoot: server.config.root,
303
303
  tunnelBaseUrl: t.url,
@@ -251,7 +251,7 @@ const aitDevtoolsPlugin = createUnplugin((options) => {
251
251
  let relayHttpUrl;
252
252
  let relayLocalHttpUrl;
253
253
  if (tunnelConfig.cdp) try {
254
- const { ensureRelaySecret } = await import("../relay-secret-store-DBwzoCXQ.js");
254
+ const { ensureRelaySecret } = await import("../relay-secret-store-BPhN1upr.js");
255
255
  await ensureRelaySecret({ projectRoot: server.config.root });
256
256
  const { assertRelayAuthConfigured, buildRelayVerifyAuth } = await import("../totp-DIbrZtI7.js");
257
257
  assertRelayAuthConfigured();
@@ -293,7 +293,7 @@ const aitDevtoolsPlugin = createUnplugin((options) => {
293
293
  navBarTransparent,
294
294
  navBarTheme
295
295
  });
296
- const { writeRelayUrls, deleteRelayUrls } = await import("../relay-url-store-j16TRTiJ.js");
296
+ const { writeRelayUrls, deleteRelayUrls } = await import("../relay-url-store-Bskcyeg8.js");
297
297
  await writeRelayUrls({
298
298
  projectRoot: server.config.root,
299
299
  tunnelBaseUrl: t.url,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ait-co/devtools",
3
- "version": "0.1.110",
3
+ "version": "0.1.111",
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": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"relay-secret-store-CPBBlV3J.cjs","names":[],"sources":["../src/mcp/relay-secret-store.ts"],"sourcesContent":["/**\n * Project-local relay TOTP secret store (#394 first-run auto-mint, #396 moved to\n * a project-local single file `.ait_relay`).\n *\n * Two surfaces, intentionally split by who is allowed to write:\n *\n * - {@link ensureRelaySecret} — WRITE path, called ONLY from the unplugin\n * (env-2 relay boot). Mints a fresh secret on first run and persists it to\n * `<projectRoot>/.ait_relay` (0600). A single file — no directory is created.\n *\n * - {@link loadRelaySecretReadOnly} — READ-ONLY path, called from the MCP\n * daemon when switching into a relay environment. It NEVER mints, chmods, or\n * creates anything: it only reads an already-existing `.ait_relay` and injects\n * its value into `env`. A daemon that minted would defeat the #250 fail-fast\n * (the daemon is the verifier side — a self-minted secret would let a leaked\n * tunnel URL attach unauthenticated), so the daemon stays read-only.\n *\n * Why a per-session `projectRoot` instead of `process.cwd()`: the daemon cannot\n * trust its own cwd — agent-plugin spawns it via `npx` without `cwd`, so cwd is\n * frozen at Claude Code launch and a cwd-walk stops at the monorepo workspace\n * root (which always has a package.json). So the project root is supplied\n * per-debug-session through `start_debug`.\n *\n * SECRET-HANDLING: this module handles AIT_DEBUG_TOTP_SECRET — the raw value and\n * its length MUST NOT appear in any log, error message, stdout, stderr, or\n * assertion output. Only boolean pass/fail signals are safe to surface, and the\n * discovered file path is never logged either. The persist file is written mode\n * 0600.\n */\n\nimport { dirname, join } from 'node:path';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Project-local secret file name (single file, not a directory). */\nexport const RELAY_SECRET_FILE_NAME = '.ait_relay';\n\n// ---------------------------------------------------------------------------\n// Dependency injection surface\n// ---------------------------------------------------------------------------\n\n/** Minimal fs subset needed by {@link ensureRelaySecret} — injectable for tests. */\nexport interface RelaySecretFs {\n writeFileSync(path: string, data: string, options: { mode: number; flag: string }): void;\n readFileSync(path: string, encoding: BufferEncoding): string;\n chmodSync(path: string, mode: number): void;\n existsSync(path: string): boolean;\n}\n\n/**\n * Minimal fs subset needed by {@link loadRelaySecretReadOnly} — strictly the two\n * read-only operations. Deliberately omits writeFileSync/mkdirSync/chmodSync so\n * the daemon path cannot mutate the filesystem even by accident (the type\n * forbids it).\n */\nexport interface RelaySecretReadOnlyFs {\n existsSync(path: string): boolean;\n readFileSync(path: string, encoding: BufferEncoding): string;\n}\n\nexport interface RelaySecretDeps {\n /**\n * Project root (typically Vite `server.config.root`). The `.ait_relay` file is\n * resolved against the nearest `package.json` directory at or above this path.\n * When omitted, the current working directory is used as the start point —\n * retained for back-compat/tests; the unplugin always passes it.\n */\n projectRoot?: string;\n /** Process environment to read from and inject into. Defaults to process.env. */\n env?: NodeJS.ProcessEnv;\n /** Cryptographically secure random bytes. Defaults to node:crypto randomBytes. */\n randomBytes?: (n: number) => Buffer;\n /** Filesystem operations. Defaults to node:fs synchronous functions. */\n fs?: RelaySecretFs;\n /** existsSync used to resolve the nearest package.json directory. Defaults to node:fs. */\n existsSync?: (path: string) => boolean;\n /** Current working directory resolver (used only when `projectRoot` is omitted). */\n cwd?: () => string;\n /** Log function for first-mint announcement. Defaults to process.stderr.write. */\n log?: (msg: string) => void;\n}\n\nexport interface RelaySecretReadOnlyDeps {\n /**\n * Project root supplied per-debug-session via `start_debug`. The daemon reads\n * `<nearest package.json dir from projectRoot>/.ait_relay`. When omitted, the\n * loader is a no-op (the daemon has no anchor to read from).\n */\n projectRoot?: string;\n /** Process environment to read from and inject into. Defaults to process.env. */\n env?: NodeJS.ProcessEnv;\n /** Read-only filesystem operations. Defaults to node:fs (existsSync + readFileSync). */\n fs?: RelaySecretReadOnlyFs;\n /** existsSync used to resolve the nearest package.json directory. Defaults to node:fs. */\n existsSync?: (path: string) => boolean;\n /** Optional log sink — never receives the secret value, length, or file path. */\n log?: (msg: string) => void;\n}\n\n// ---------------------------------------------------------------------------\n// Path helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Walks upward from `start` and returns the nearest directory that contains a\n * `package.json`. Falls back to `start` itself when none is found (so a write\n * still lands somewhere deterministic).\n *\n * The write (unplugin) and read (daemon) sides use the SAME anchor so a secret\n * minted by `pnpm dev` is found by the daemon: real mini-apps keep\n * `vite.config.ts` and `package.json` in the same directory, so\n * `server.config.root === package.json-dir`. In a monorepo subdir the anchor is\n * the package's own directory — the one the daemon can also reach via the\n * per-session projectRoot.\n *\n * @param start - Directory to start the upward walk from.\n * @param existsSyncFn - Injectable existence check (defaults to node:fs).\n */\nexport function nearestPackageJsonDir(\n start: string,\n existsSyncFn: (path: string) => boolean,\n): string {\n let dir = start;\n // Stop at the filesystem root (dirname of root === root).\n while (true) {\n if (existsSyncFn(join(dir, 'package.json'))) {\n return dir;\n }\n const parent = dirname(dir);\n if (parent === dir) {\n // Reached the filesystem root without finding a package.json — fall back\n // to the original start directory.\n return start;\n }\n dir = parent;\n }\n}\n\n/**\n * Absolute path to the project-local `.ait_relay` file for a given start\n * directory (resolved against the nearest package.json directory).\n *\n * Exported so tests can compute the expected path without duplicating the\n * resolution logic.\n */\nexport function relaySecretFilePath(\n start: string,\n existsSyncFn: (path: string) => boolean,\n): string {\n return join(nearestPackageJsonDir(start, existsSyncFn), RELAY_SECRET_FILE_NAME);\n}\n\n// ---------------------------------------------------------------------------\n// WRITE path (unplugin only) — mint + persist\n// ---------------------------------------------------------------------------\n\n/**\n * Ensures `env.AIT_DEBUG_TOTP_SECRET` is set to a valid relay TOTP secret,\n * persisting a freshly-minted one to `<projectRoot>/.ait_relay` (0600) on first\n * run and loading it silently on subsequent runs.\n *\n * Writes a SINGLE file into the already-existing project directory — it never\n * creates a directory (so no `mkdirSync`/dir `chmod`). The file is created with\n * `O_EXCL` (`flag: 'wx'`) so a concurrent process cannot be clobbered; on the\n * EEXIST race the winner's value is read instead.\n *\n * Called ONLY from the unplugin (env-2 relay boot). The MCP daemon uses\n * {@link loadRelaySecretReadOnly} (read-only) — it must never mint.\n *\n * @param deps - Optional dependency overrides for testing.\n */\nexport async function ensureRelaySecret(deps?: RelaySecretDeps): Promise<void> {\n const {\n projectRoot,\n env = process.env,\n randomBytes: randomBytesFn,\n fs: fsDep,\n existsSync: existsSyncDep,\n cwd: cwdFn,\n log,\n } = deps ?? {};\n\n const logFn: (msg: string) => void = log ?? ((msg: string) => process.stderr.write(msg));\n\n // Lazily import isValidRelayAuthSecret to avoid pulling in node:crypto at\n // module-load time (keeps the import side-effect free).\n const { isValidRelayAuthSecret } = await import('./totp.js');\n\n // 1. Already configured — no-op (operator export or earlier run wins).\n // But first check for a divergence between the env value and .ait_relay —\n // if they differ the relay will verify against the env value while QR/\n // deep-links carry codes derived from the file, causing silent 401s (#620).\n if (isValidRelayAuthSecret(env.AIT_DEBUG_TOTP_SECRET)) {\n // We need fs to compare — resolve deps early just for the divergence check.\n // This mirrors the lazy-resolve block below but is hoisted here so we can\n // still early-return after the (possibly-emitted) warning.\n const fsEarly: RelaySecretFs = fsDep ?? (await import('node:fs'));\n const existsSyncEarly: (path: string) => boolean = existsSyncDep ?? fsEarly.existsSync;\n const startEarly = projectRoot ?? (cwdFn ?? (() => process.cwd()))();\n const secretPathEarly = relaySecretFilePath(startEarly, existsSyncEarly);\n warnIfEnvDiffersFromFile(\n env.AIT_DEBUG_TOTP_SECRET,\n secretPathEarly,\n fsEarly,\n isValidRelayAuthSecret,\n logFn,\n );\n return;\n }\n\n // Resolve injected or real dependencies lazily to keep the import graph clean.\n const rb: (n: number) => Buffer = randomBytesFn ?? (await import('node:crypto')).randomBytes;\n const fs: RelaySecretFs = fsDep ?? (await import('node:fs'));\n const existsSyncFn: (path: string) => boolean = existsSyncDep ?? fs.existsSync;\n\n const start = projectRoot ?? (cwdFn ?? (() => process.cwd()))();\n const secretPath = relaySecretFilePath(start, existsSyncFn);\n\n // 2. Persist file exists — read and inject (silent reload).\n if (fs.existsSync(secretPath)) {\n return readAndInject(secretPath, fs, env, logFn, isValidRelayAuthSecret, rb);\n }\n\n // 3. Mint a fresh secret.\n return mintAndPersist(secretPath, fs, env, rb, logFn, isValidRelayAuthSecret);\n}\n\n// ---------------------------------------------------------------------------\n// READ-ONLY path (daemon only) — never mints, chmods, or creates anything\n// ---------------------------------------------------------------------------\n\n/**\n * Reads an already-existing `<projectRoot>/.ait_relay` and, if its contents are a\n * valid relay TOTP secret, injects them into `env.AIT_DEBUG_TOTP_SECRET`.\n *\n * Strictly READ-ONLY: it uses only `existsSync` + `readFileSync` and NEVER mints,\n * chmods, or creates files/directories. The daemon must not mint because it is\n * the relay verifier side — a self-minted secret would defeat the #250 fail-fast\n * (a leaked tunnel URL could then attach unauthenticated). If no valid secret is\n * found the function leaves `env` untouched and returns without throwing, so the\n * downstream `assertRelayAuthConfigured()` stays the single fail-fast.\n *\n * Resolution order:\n * 1. `env.AIT_DEBUG_TOTP_SECRET` already valid → no-op (operator export wins).\n * 2. `projectRoot` given → read `<nearest package.json dir>/.ait_relay`; inject\n * iff the contents pass {@link isValidRelayAuthSecret}.\n * 3. Otherwise (no projectRoot, file absent, or invalid) → silent no-op.\n *\n * SECRET-HANDLING: the read value is passed ONLY to the boolean predicate before\n * assignment; its value, length, and the discovered file path are never logged.\n *\n * @param deps - Optional dependency overrides for testing.\n */\nexport async function loadRelaySecretReadOnly(deps?: RelaySecretReadOnlyDeps): Promise<void> {\n const { projectRoot, env = process.env, fs: fsDep, existsSync: existsSyncDep, log } = deps ?? {};\n\n const logFn: (msg: string) => void = log ?? ((msg: string) => process.stderr.write(msg));\n\n const { isValidRelayAuthSecret } = await import('./totp.js');\n\n // 1. Already configured — no-op (operator export or unplugin run wins).\n // But first check for a divergence between the env value and .ait_relay —\n // if they differ the relay will verify against the env value while QR/\n // deep-links carry codes derived from the file, causing silent 401s (#620).\n if (isValidRelayAuthSecret(env.AIT_DEBUG_TOTP_SECRET)) {\n if (projectRoot !== undefined) {\n const fsEarly: RelaySecretReadOnlyFs = fsDep ?? (await import('node:fs'));\n const existsSyncEarly: (path: string) => boolean = existsSyncDep ?? fsEarly.existsSync;\n const secretPathEarly = relaySecretFilePath(projectRoot, existsSyncEarly);\n warnIfEnvDiffersFromFile(\n env.AIT_DEBUG_TOTP_SECRET,\n secretPathEarly,\n fsEarly,\n isValidRelayAuthSecret,\n logFn,\n );\n }\n return;\n }\n\n // 2. No anchor → nothing to read.\n if (projectRoot === undefined) {\n return;\n }\n\n const fs: RelaySecretReadOnlyFs = fsDep ?? (await import('node:fs'));\n const existsSyncFn: (path: string) => boolean = existsSyncDep ?? fs.existsSync;\n\n const secretPath = relaySecretFilePath(projectRoot, existsSyncFn);\n if (!fs.existsSync(secretPath)) {\n return;\n }\n\n let stored: string;\n try {\n stored = fs.readFileSync(secretPath, 'utf8').trim();\n } catch {\n // Unreadable file (permissions, transient FS error) — stay silent and let\n // the downstream assert be the single fail-fast. SECRET-HANDLING: the error\n // and path are not surfaced.\n return;\n }\n\n // SECRET-HANDLING: the value flows only through the boolean predicate.\n if (!isValidRelayAuthSecret(stored)) {\n return;\n }\n\n env.AIT_DEBUG_TOTP_SECRET = stored;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers (not exported — single-use extracted for readability)\n// ---------------------------------------------------------------------------\n\n/**\n * Compares `envSecret` against the contents of `secretPath` (if the file\n * exists and contains a valid secret) and emits a single warning via `logFn`\n * when they differ.\n *\n * SECRET-HANDLING (hard rules — do NOT relax):\n * - The warning MUST NOT include either secret value, its length, a hash of\n * it, or the resolved file path.\n * - Only an inequality boolean drives the warning; no secret-derived data\n * enters the log message.\n * - If the file is absent, unreadable, or its contents are invalid the\n * function returns silently — no spurious noise.\n *\n * This helper is intentionally synchronous-like (reads via the injected fs)\n * so it can be called from within the async early-return guards without\n * introducing additional async hops.\n *\n * @param envSecret - The validated env value (caller must have confirmed it is\n * valid before calling).\n * @param secretPath - Absolute path to `.ait_relay` to read for comparison.\n * @param fsDep - Injectable fs subset (at minimum `existsSync` + `readFileSync`).\n * @param isValidRelayAuthSecret - Injectable predicate from totp.ts.\n * @param logFn - Injectable log sink; never receives a secret value.\n */\nfunction warnIfEnvDiffersFromFile(\n envSecret: string,\n secretPath: string,\n fsDep: RelaySecretReadOnlyFs,\n isValidRelayAuthSecret: (s: string | undefined) => s is string,\n logFn: (msg: string) => void,\n): void {\n // File absent → nothing to compare.\n if (!fsDep.existsSync(secretPath)) {\n return;\n }\n\n let stored: string;\n try {\n stored = fsDep.readFileSync(secretPath, 'utf8').trim();\n } catch {\n // Unreadable — skip silently. SECRET-HANDLING: error and path not surfaced.\n return;\n }\n\n // Invalid stored contents → skip silently (no spurious noise).\n if (!isValidRelayAuthSecret(stored)) {\n return;\n }\n\n // Compare by equality only. Neither value nor path enters the log message.\n if (envSecret !== stored) {\n logFn(\n `[@ait-co/devtools] AIT_DEBUG_TOTP_SECRET (from environment) differs from the project-local relay secret; ` +\n `the relay will verify against the environment value. ` +\n `Remove .env/.env.local/exported AIT_DEBUG_TOTP_SECRET, or sync the file, ` +\n `so QR/deep-links and the relay agree.\\n`,\n );\n }\n}\n\nasync function readAndInject(\n secretPath: string,\n fs: RelaySecretFs,\n env: NodeJS.ProcessEnv,\n logFn: (msg: string) => void,\n isValidRelayAuthSecret: (s: string | undefined) => s is string,\n rb: (n: number) => Buffer,\n): Promise<void> {\n let stored: string;\n try {\n stored = fs.readFileSync(secretPath, 'utf8').trim();\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`[@ait-co/devtools] relay 시크릿 파일 읽기 실패: ${msg}`);\n }\n\n if (!isValidRelayAuthSecret(stored)) {\n // Stored value is corrupt — re-mint over the same path.\n logFn('[@ait-co/devtools] relay 시크릿 파일의 값이 유효하지 않습니다. 재생성합니다.\\n');\n return mintAndPersist(secretPath, fs, env, rb, logFn, isValidRelayAuthSecret, true);\n }\n\n // Inject into env — silent path (no log on successful reload).\n env.AIT_DEBUG_TOTP_SECRET = stored;\n}\n\nasync function mintAndPersist(\n secretPath: string,\n fs: RelaySecretFs,\n env: NodeJS.ProcessEnv,\n rb: (n: number) => Buffer,\n logFn: (msg: string) => void,\n isValidRelayAuthSecret: (s: string | undefined) => s is string,\n /** When re-minting over a corrupt file, the existing file must be overwritten. */\n overwrite = false,\n): Promise<void> {\n // SECRET-HANDLING: the raw bytes are never written to any log or string other\n // than the persist file and the env variable.\n const secret = rb(32).toString('hex'); // 64 hex chars = 256 bits\n\n // Self-consistency guard: our own minted secret must pass validation.\n if (!isValidRelayAuthSecret(secret)) {\n throw new Error(\n '[@ait-co/devtools] 내부 오류: mint된 시크릿이 유효성 검사를 통과하지 못했습니다.',\n );\n }\n\n // Write a SINGLE file into the already-existing project directory — no\n // directory is created. `O_EXCL` (flag 'wx') makes the create exclusive so a\n // concurrent process cannot be clobbered; on EEXIST we read the winner's value.\n // (When re-minting over a corrupt file we must overwrite, so use 'w'.)\n const flag = overwrite ? 'w' : 'wx';\n try {\n fs.writeFileSync(secretPath, secret, { mode: 0o600, flag });\n // Belt-and-suspenders: apply chmod after write in case umask relaxed the mode.\n fs.chmodSync(secretPath, 0o600);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'EEXIST') {\n // Race: another process already wrote the file — read their value.\n let stored: string;\n try {\n stored = fs.readFileSync(secretPath, 'utf8').trim();\n } catch (readErr) {\n const msg = readErr instanceof Error ? readErr.message : String(readErr);\n throw new Error(`[@ait-co/devtools] relay 시크릿 파일 읽기 실패(경합): ${msg}`);\n }\n if (!isValidRelayAuthSecret(stored)) {\n throw new Error('[@ait-co/devtools] relay 시크릿 파일이 경합 후에도 유효하지 않습니다.');\n }\n env.AIT_DEBUG_TOTP_SECRET = stored;\n return;\n }\n throw err;\n }\n\n // Inject into the current process env so the immediately following\n // assertRelayAuthConfigured() / buildRelayVerifyAuth() calls see the value.\n env.AIT_DEBUG_TOTP_SECRET = secret;\n\n // First-mint announcement (value never included — SECRET-HANDLING). The file\n // name is fixed (`.ait_relay`); we do not echo the resolved directory either.\n logFn(\n `[@ait-co/devtools] relay 인증 시크릿을 생성해 프로젝트의 ${RELAY_SECRET_FILE_NAME} 파일에 저장했습니다 (권한 0600).\\n` +\n `다음 실행부터 자동으로 사용됩니다. 직접 export할 필요 없습니다.\\n` +\n `팀이 같은 relay를 공유하려면 이 파일을 repo에 커밋하세요(비공개 repo 권장).\\n` +\n `자세히: https://docs.aitc.dev/guides/relay-auth-totp\\n`,\n );\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"relay-secret-store-DBwzoCXQ.js","names":[],"sources":["../src/mcp/relay-secret-store.ts"],"sourcesContent":["/**\n * Project-local relay TOTP secret store (#394 first-run auto-mint, #396 moved to\n * a project-local single file `.ait_relay`).\n *\n * Two surfaces, intentionally split by who is allowed to write:\n *\n * - {@link ensureRelaySecret} — WRITE path, called ONLY from the unplugin\n * (env-2 relay boot). Mints a fresh secret on first run and persists it to\n * `<projectRoot>/.ait_relay` (0600). A single file — no directory is created.\n *\n * - {@link loadRelaySecretReadOnly} — READ-ONLY path, called from the MCP\n * daemon when switching into a relay environment. It NEVER mints, chmods, or\n * creates anything: it only reads an already-existing `.ait_relay` and injects\n * its value into `env`. A daemon that minted would defeat the #250 fail-fast\n * (the daemon is the verifier side — a self-minted secret would let a leaked\n * tunnel URL attach unauthenticated), so the daemon stays read-only.\n *\n * Why a per-session `projectRoot` instead of `process.cwd()`: the daemon cannot\n * trust its own cwd — agent-plugin spawns it via `npx` without `cwd`, so cwd is\n * frozen at Claude Code launch and a cwd-walk stops at the monorepo workspace\n * root (which always has a package.json). So the project root is supplied\n * per-debug-session through `start_debug`.\n *\n * SECRET-HANDLING: this module handles AIT_DEBUG_TOTP_SECRET — the raw value and\n * its length MUST NOT appear in any log, error message, stdout, stderr, or\n * assertion output. Only boolean pass/fail signals are safe to surface, and the\n * discovered file path is never logged either. The persist file is written mode\n * 0600.\n */\n\nimport { dirname, join } from 'node:path';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Project-local secret file name (single file, not a directory). */\nexport const RELAY_SECRET_FILE_NAME = '.ait_relay';\n\n// ---------------------------------------------------------------------------\n// Dependency injection surface\n// ---------------------------------------------------------------------------\n\n/** Minimal fs subset needed by {@link ensureRelaySecret} — injectable for tests. */\nexport interface RelaySecretFs {\n writeFileSync(path: string, data: string, options: { mode: number; flag: string }): void;\n readFileSync(path: string, encoding: BufferEncoding): string;\n chmodSync(path: string, mode: number): void;\n existsSync(path: string): boolean;\n}\n\n/**\n * Minimal fs subset needed by {@link loadRelaySecretReadOnly} — strictly the two\n * read-only operations. Deliberately omits writeFileSync/mkdirSync/chmodSync so\n * the daemon path cannot mutate the filesystem even by accident (the type\n * forbids it).\n */\nexport interface RelaySecretReadOnlyFs {\n existsSync(path: string): boolean;\n readFileSync(path: string, encoding: BufferEncoding): string;\n}\n\nexport interface RelaySecretDeps {\n /**\n * Project root (typically Vite `server.config.root`). The `.ait_relay` file is\n * resolved against the nearest `package.json` directory at or above this path.\n * When omitted, the current working directory is used as the start point —\n * retained for back-compat/tests; the unplugin always passes it.\n */\n projectRoot?: string;\n /** Process environment to read from and inject into. Defaults to process.env. */\n env?: NodeJS.ProcessEnv;\n /** Cryptographically secure random bytes. Defaults to node:crypto randomBytes. */\n randomBytes?: (n: number) => Buffer;\n /** Filesystem operations. Defaults to node:fs synchronous functions. */\n fs?: RelaySecretFs;\n /** existsSync used to resolve the nearest package.json directory. Defaults to node:fs. */\n existsSync?: (path: string) => boolean;\n /** Current working directory resolver (used only when `projectRoot` is omitted). */\n cwd?: () => string;\n /** Log function for first-mint announcement. Defaults to process.stderr.write. */\n log?: (msg: string) => void;\n}\n\nexport interface RelaySecretReadOnlyDeps {\n /**\n * Project root supplied per-debug-session via `start_debug`. The daemon reads\n * `<nearest package.json dir from projectRoot>/.ait_relay`. When omitted, the\n * loader is a no-op (the daemon has no anchor to read from).\n */\n projectRoot?: string;\n /** Process environment to read from and inject into. Defaults to process.env. */\n env?: NodeJS.ProcessEnv;\n /** Read-only filesystem operations. Defaults to node:fs (existsSync + readFileSync). */\n fs?: RelaySecretReadOnlyFs;\n /** existsSync used to resolve the nearest package.json directory. Defaults to node:fs. */\n existsSync?: (path: string) => boolean;\n /** Optional log sink — never receives the secret value, length, or file path. */\n log?: (msg: string) => void;\n}\n\n// ---------------------------------------------------------------------------\n// Path helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Walks upward from `start` and returns the nearest directory that contains a\n * `package.json`. Falls back to `start` itself when none is found (so a write\n * still lands somewhere deterministic).\n *\n * The write (unplugin) and read (daemon) sides use the SAME anchor so a secret\n * minted by `pnpm dev` is found by the daemon: real mini-apps keep\n * `vite.config.ts` and `package.json` in the same directory, so\n * `server.config.root === package.json-dir`. In a monorepo subdir the anchor is\n * the package's own directory — the one the daemon can also reach via the\n * per-session projectRoot.\n *\n * @param start - Directory to start the upward walk from.\n * @param existsSyncFn - Injectable existence check (defaults to node:fs).\n */\nexport function nearestPackageJsonDir(\n start: string,\n existsSyncFn: (path: string) => boolean,\n): string {\n let dir = start;\n // Stop at the filesystem root (dirname of root === root).\n while (true) {\n if (existsSyncFn(join(dir, 'package.json'))) {\n return dir;\n }\n const parent = dirname(dir);\n if (parent === dir) {\n // Reached the filesystem root without finding a package.json — fall back\n // to the original start directory.\n return start;\n }\n dir = parent;\n }\n}\n\n/**\n * Absolute path to the project-local `.ait_relay` file for a given start\n * directory (resolved against the nearest package.json directory).\n *\n * Exported so tests can compute the expected path without duplicating the\n * resolution logic.\n */\nexport function relaySecretFilePath(\n start: string,\n existsSyncFn: (path: string) => boolean,\n): string {\n return join(nearestPackageJsonDir(start, existsSyncFn), RELAY_SECRET_FILE_NAME);\n}\n\n// ---------------------------------------------------------------------------\n// WRITE path (unplugin only) — mint + persist\n// ---------------------------------------------------------------------------\n\n/**\n * Ensures `env.AIT_DEBUG_TOTP_SECRET` is set to a valid relay TOTP secret,\n * persisting a freshly-minted one to `<projectRoot>/.ait_relay` (0600) on first\n * run and loading it silently on subsequent runs.\n *\n * Writes a SINGLE file into the already-existing project directory — it never\n * creates a directory (so no `mkdirSync`/dir `chmod`). The file is created with\n * `O_EXCL` (`flag: 'wx'`) so a concurrent process cannot be clobbered; on the\n * EEXIST race the winner's value is read instead.\n *\n * Called ONLY from the unplugin (env-2 relay boot). The MCP daemon uses\n * {@link loadRelaySecretReadOnly} (read-only) — it must never mint.\n *\n * @param deps - Optional dependency overrides for testing.\n */\nexport async function ensureRelaySecret(deps?: RelaySecretDeps): Promise<void> {\n const {\n projectRoot,\n env = process.env,\n randomBytes: randomBytesFn,\n fs: fsDep,\n existsSync: existsSyncDep,\n cwd: cwdFn,\n log,\n } = deps ?? {};\n\n const logFn: (msg: string) => void = log ?? ((msg: string) => process.stderr.write(msg));\n\n // Lazily import isValidRelayAuthSecret to avoid pulling in node:crypto at\n // module-load time (keeps the import side-effect free).\n const { isValidRelayAuthSecret } = await import('./totp.js');\n\n // 1. Already configured — no-op (operator export or earlier run wins).\n // But first check for a divergence between the env value and .ait_relay —\n // if they differ the relay will verify against the env value while QR/\n // deep-links carry codes derived from the file, causing silent 401s (#620).\n if (isValidRelayAuthSecret(env.AIT_DEBUG_TOTP_SECRET)) {\n // We need fs to compare — resolve deps early just for the divergence check.\n // This mirrors the lazy-resolve block below but is hoisted here so we can\n // still early-return after the (possibly-emitted) warning.\n const fsEarly: RelaySecretFs = fsDep ?? (await import('node:fs'));\n const existsSyncEarly: (path: string) => boolean = existsSyncDep ?? fsEarly.existsSync;\n const startEarly = projectRoot ?? (cwdFn ?? (() => process.cwd()))();\n const secretPathEarly = relaySecretFilePath(startEarly, existsSyncEarly);\n warnIfEnvDiffersFromFile(\n env.AIT_DEBUG_TOTP_SECRET,\n secretPathEarly,\n fsEarly,\n isValidRelayAuthSecret,\n logFn,\n );\n return;\n }\n\n // Resolve injected or real dependencies lazily to keep the import graph clean.\n const rb: (n: number) => Buffer = randomBytesFn ?? (await import('node:crypto')).randomBytes;\n const fs: RelaySecretFs = fsDep ?? (await import('node:fs'));\n const existsSyncFn: (path: string) => boolean = existsSyncDep ?? fs.existsSync;\n\n const start = projectRoot ?? (cwdFn ?? (() => process.cwd()))();\n const secretPath = relaySecretFilePath(start, existsSyncFn);\n\n // 2. Persist file exists — read and inject (silent reload).\n if (fs.existsSync(secretPath)) {\n return readAndInject(secretPath, fs, env, logFn, isValidRelayAuthSecret, rb);\n }\n\n // 3. Mint a fresh secret.\n return mintAndPersist(secretPath, fs, env, rb, logFn, isValidRelayAuthSecret);\n}\n\n// ---------------------------------------------------------------------------\n// READ-ONLY path (daemon only) — never mints, chmods, or creates anything\n// ---------------------------------------------------------------------------\n\n/**\n * Reads an already-existing `<projectRoot>/.ait_relay` and, if its contents are a\n * valid relay TOTP secret, injects them into `env.AIT_DEBUG_TOTP_SECRET`.\n *\n * Strictly READ-ONLY: it uses only `existsSync` + `readFileSync` and NEVER mints,\n * chmods, or creates files/directories. The daemon must not mint because it is\n * the relay verifier side — a self-minted secret would defeat the #250 fail-fast\n * (a leaked tunnel URL could then attach unauthenticated). If no valid secret is\n * found the function leaves `env` untouched and returns without throwing, so the\n * downstream `assertRelayAuthConfigured()` stays the single fail-fast.\n *\n * Resolution order:\n * 1. `env.AIT_DEBUG_TOTP_SECRET` already valid → no-op (operator export wins).\n * 2. `projectRoot` given → read `<nearest package.json dir>/.ait_relay`; inject\n * iff the contents pass {@link isValidRelayAuthSecret}.\n * 3. Otherwise (no projectRoot, file absent, or invalid) → silent no-op.\n *\n * SECRET-HANDLING: the read value is passed ONLY to the boolean predicate before\n * assignment; its value, length, and the discovered file path are never logged.\n *\n * @param deps - Optional dependency overrides for testing.\n */\nexport async function loadRelaySecretReadOnly(deps?: RelaySecretReadOnlyDeps): Promise<void> {\n const { projectRoot, env = process.env, fs: fsDep, existsSync: existsSyncDep, log } = deps ?? {};\n\n const logFn: (msg: string) => void = log ?? ((msg: string) => process.stderr.write(msg));\n\n const { isValidRelayAuthSecret } = await import('./totp.js');\n\n // 1. Already configured — no-op (operator export or unplugin run wins).\n // But first check for a divergence between the env value and .ait_relay —\n // if they differ the relay will verify against the env value while QR/\n // deep-links carry codes derived from the file, causing silent 401s (#620).\n if (isValidRelayAuthSecret(env.AIT_DEBUG_TOTP_SECRET)) {\n if (projectRoot !== undefined) {\n const fsEarly: RelaySecretReadOnlyFs = fsDep ?? (await import('node:fs'));\n const existsSyncEarly: (path: string) => boolean = existsSyncDep ?? fsEarly.existsSync;\n const secretPathEarly = relaySecretFilePath(projectRoot, existsSyncEarly);\n warnIfEnvDiffersFromFile(\n env.AIT_DEBUG_TOTP_SECRET,\n secretPathEarly,\n fsEarly,\n isValidRelayAuthSecret,\n logFn,\n );\n }\n return;\n }\n\n // 2. No anchor → nothing to read.\n if (projectRoot === undefined) {\n return;\n }\n\n const fs: RelaySecretReadOnlyFs = fsDep ?? (await import('node:fs'));\n const existsSyncFn: (path: string) => boolean = existsSyncDep ?? fs.existsSync;\n\n const secretPath = relaySecretFilePath(projectRoot, existsSyncFn);\n if (!fs.existsSync(secretPath)) {\n return;\n }\n\n let stored: string;\n try {\n stored = fs.readFileSync(secretPath, 'utf8').trim();\n } catch {\n // Unreadable file (permissions, transient FS error) — stay silent and let\n // the downstream assert be the single fail-fast. SECRET-HANDLING: the error\n // and path are not surfaced.\n return;\n }\n\n // SECRET-HANDLING: the value flows only through the boolean predicate.\n if (!isValidRelayAuthSecret(stored)) {\n return;\n }\n\n env.AIT_DEBUG_TOTP_SECRET = stored;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers (not exported — single-use extracted for readability)\n// ---------------------------------------------------------------------------\n\n/**\n * Compares `envSecret` against the contents of `secretPath` (if the file\n * exists and contains a valid secret) and emits a single warning via `logFn`\n * when they differ.\n *\n * SECRET-HANDLING (hard rules — do NOT relax):\n * - The warning MUST NOT include either secret value, its length, a hash of\n * it, or the resolved file path.\n * - Only an inequality boolean drives the warning; no secret-derived data\n * enters the log message.\n * - If the file is absent, unreadable, or its contents are invalid the\n * function returns silently — no spurious noise.\n *\n * This helper is intentionally synchronous-like (reads via the injected fs)\n * so it can be called from within the async early-return guards without\n * introducing additional async hops.\n *\n * @param envSecret - The validated env value (caller must have confirmed it is\n * valid before calling).\n * @param secretPath - Absolute path to `.ait_relay` to read for comparison.\n * @param fsDep - Injectable fs subset (at minimum `existsSync` + `readFileSync`).\n * @param isValidRelayAuthSecret - Injectable predicate from totp.ts.\n * @param logFn - Injectable log sink; never receives a secret value.\n */\nfunction warnIfEnvDiffersFromFile(\n envSecret: string,\n secretPath: string,\n fsDep: RelaySecretReadOnlyFs,\n isValidRelayAuthSecret: (s: string | undefined) => s is string,\n logFn: (msg: string) => void,\n): void {\n // File absent → nothing to compare.\n if (!fsDep.existsSync(secretPath)) {\n return;\n }\n\n let stored: string;\n try {\n stored = fsDep.readFileSync(secretPath, 'utf8').trim();\n } catch {\n // Unreadable — skip silently. SECRET-HANDLING: error and path not surfaced.\n return;\n }\n\n // Invalid stored contents → skip silently (no spurious noise).\n if (!isValidRelayAuthSecret(stored)) {\n return;\n }\n\n // Compare by equality only. Neither value nor path enters the log message.\n if (envSecret !== stored) {\n logFn(\n `[@ait-co/devtools] AIT_DEBUG_TOTP_SECRET (from environment) differs from the project-local relay secret; ` +\n `the relay will verify against the environment value. ` +\n `Remove .env/.env.local/exported AIT_DEBUG_TOTP_SECRET, or sync the file, ` +\n `so QR/deep-links and the relay agree.\\n`,\n );\n }\n}\n\nasync function readAndInject(\n secretPath: string,\n fs: RelaySecretFs,\n env: NodeJS.ProcessEnv,\n logFn: (msg: string) => void,\n isValidRelayAuthSecret: (s: string | undefined) => s is string,\n rb: (n: number) => Buffer,\n): Promise<void> {\n let stored: string;\n try {\n stored = fs.readFileSync(secretPath, 'utf8').trim();\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`[@ait-co/devtools] relay 시크릿 파일 읽기 실패: ${msg}`);\n }\n\n if (!isValidRelayAuthSecret(stored)) {\n // Stored value is corrupt — re-mint over the same path.\n logFn('[@ait-co/devtools] relay 시크릿 파일의 값이 유효하지 않습니다. 재생성합니다.\\n');\n return mintAndPersist(secretPath, fs, env, rb, logFn, isValidRelayAuthSecret, true);\n }\n\n // Inject into env — silent path (no log on successful reload).\n env.AIT_DEBUG_TOTP_SECRET = stored;\n}\n\nasync function mintAndPersist(\n secretPath: string,\n fs: RelaySecretFs,\n env: NodeJS.ProcessEnv,\n rb: (n: number) => Buffer,\n logFn: (msg: string) => void,\n isValidRelayAuthSecret: (s: string | undefined) => s is string,\n /** When re-minting over a corrupt file, the existing file must be overwritten. */\n overwrite = false,\n): Promise<void> {\n // SECRET-HANDLING: the raw bytes are never written to any log or string other\n // than the persist file and the env variable.\n const secret = rb(32).toString('hex'); // 64 hex chars = 256 bits\n\n // Self-consistency guard: our own minted secret must pass validation.\n if (!isValidRelayAuthSecret(secret)) {\n throw new Error(\n '[@ait-co/devtools] 내부 오류: mint된 시크릿이 유효성 검사를 통과하지 못했습니다.',\n );\n }\n\n // Write a SINGLE file into the already-existing project directory — no\n // directory is created. `O_EXCL` (flag 'wx') makes the create exclusive so a\n // concurrent process cannot be clobbered; on EEXIST we read the winner's value.\n // (When re-minting over a corrupt file we must overwrite, so use 'w'.)\n const flag = overwrite ? 'w' : 'wx';\n try {\n fs.writeFileSync(secretPath, secret, { mode: 0o600, flag });\n // Belt-and-suspenders: apply chmod after write in case umask relaxed the mode.\n fs.chmodSync(secretPath, 0o600);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'EEXIST') {\n // Race: another process already wrote the file — read their value.\n let stored: string;\n try {\n stored = fs.readFileSync(secretPath, 'utf8').trim();\n } catch (readErr) {\n const msg = readErr instanceof Error ? readErr.message : String(readErr);\n throw new Error(`[@ait-co/devtools] relay 시크릿 파일 읽기 실패(경합): ${msg}`);\n }\n if (!isValidRelayAuthSecret(stored)) {\n throw new Error('[@ait-co/devtools] relay 시크릿 파일이 경합 후에도 유효하지 않습니다.');\n }\n env.AIT_DEBUG_TOTP_SECRET = stored;\n return;\n }\n throw err;\n }\n\n // Inject into the current process env so the immediately following\n // assertRelayAuthConfigured() / buildRelayVerifyAuth() calls see the value.\n env.AIT_DEBUG_TOTP_SECRET = secret;\n\n // First-mint announcement (value never included — SECRET-HANDLING). The file\n // name is fixed (`.ait_relay`); we do not echo the resolved directory either.\n logFn(\n `[@ait-co/devtools] relay 인증 시크릿을 생성해 프로젝트의 ${RELAY_SECRET_FILE_NAME} 파일에 저장했습니다 (권한 0600).\\n` +\n `다음 실행부터 자동으로 사용됩니다. 직접 export할 필요 없습니다.\\n` +\n `팀이 같은 relay를 공유하려면 이 파일을 repo에 커밋하세요(비공개 repo 권장).\\n` +\n `자세히: https://docs.aitc.dev/guides/relay-auth-totp\\n`,\n );\n}\n"],"mappings":"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"}