{"version":3,"file":"index.cjs","names":["buildSuggestedFix","collectClientReachableModuleIds","getAllTaintedModules","buildImportChainToModule","DEFAULT_ENV_PREFIXES","scanModuleSource","createDebouncedAnalysisScheduler","hasTaintedModules","buildImportChainViaImporters","formatViolationForOverlay","formatViolationForTerminal","collectServerEnvVarValues","scanOutputChunk","formatBundleLeakForTerminal"],"sources":["../src/index.ts"],"sourcesContent":["import type { SourceMapInput } from '@jridgewell/trace-mapping'\nimport type { Plugin, ResolvedConfig } from 'vite'\nimport { createFilter, resolveEnvPrefix } from 'vite'\n\nimport { scanOutputChunk, collectServerEnvVarValues } from '#bundle-scan'\nimport { DEFAULT_ENV_PREFIXES } from '#env-classification'\nimport { collectClientReachableModuleIds, buildImportChainToModule, buildImportChainViaImporters } from '#graph'\nimport type { GetImportedModuleIds, GetImporterModuleIds } from '#graph'\nimport {\n  registerTaintedModule,\n  clearTaintedModule,\n  clearAllTaintedModules,\n  getAllTaintedModules,\n  hasTaintedModules,\n} from '#registry'\nimport {\n  buildSuggestedFix,\n  formatViolationForTerminal,\n  formatBundleLeakForTerminal,\n  formatViolationForOverlay,\n} from '#reporter'\nimport { scanModuleSource } from '#scanner'\nimport type { SafeEnvOptions, ResolvedSafeEnvOptions, BlockOnMode, TaintedModule, Violation } from '#types'\nimport { createDebouncedAnalysisScheduler } from '#worker'\nimport type { DebouncedAnalysisScheduler } from '#worker'\n\nexport type { SafeEnvOptions, BlockOnMode } from '#types'\n\nconst DEFAULT_INCLUDE = ['**/*.{ts,tsx,js,jsx,vue,svelte}']\nconst DEFAULT_EXCLUDE = ['node_modules/**', 'dist/**']\nconst DEV_ANALYSIS_QUIET_PERIOD_MS = 150\n\n/**\n * Merges user-supplied options with plugin defaults.\n *\n * @param userOptions - The optional user-supplied configuration.\n * @returns A complete options object with all defaults applied.\n */\nfunction resolvePluginOptions(userOptions: SafeEnvOptions | undefined): ResolvedSafeEnvOptions {\n  return {\n    allowClientAccess: userOptions?.allowClientAccess ?? [],\n    blockOn: userOptions?.blockOn ?? 'production',\n    overlay: userOptions?.overlay ?? true,\n  }\n}\n\n/**\n * Returns `true` if the plugin should abort the build based on the configured\n * `blockOn` mode and the current `NODE_ENV`.\n *\n * @param blockOnMode - The configured blocking behaviour.\n * @returns `true` if the plugin should treat violations as fatal errors.\n */\nfunction shouldBlockBuild(blockOnMode: BlockOnMode): boolean {\n  if (blockOnMode === 'never') {\n    return false\n  }\n  if (blockOnMode === 'always') {\n    return true\n  }\n  return process.env['NODE_ENV'] === 'production'\n}\n\n/**\n * Resolves the client entry point module IDs from the Vite resolved config.\n * Falls back to `<root>/index.html` when no explicit rollup input is configured.\n *\n * @param resolvedViteConfig - The fully resolved Vite configuration object.\n * @returns An array of absolute module IDs representing client entry points.\n */\nfunction resolveClientEntryModuleIds(resolvedViteConfig: ResolvedConfig): string[] {\n  const rollupInput = resolvedViteConfig.build.rollupOptions?.input\n\n  if (!rollupInput) {\n    return [`${resolvedViteConfig.root}/index.html`]\n  }\n  if (typeof rollupInput === 'string') {\n    return [rollupInput]\n  }\n  if (Array.isArray(rollupInput)) {\n    return rollupInput\n  }\n\n  return Object.values(rollupInput)\n}\n\n/**\n * Constructs a `Violation` for every env-var access inside the given tainted\n * module and forwards each one to `onViolation`. Shared by the dev-mode and\n * build-mode paths so that the `Violation` shape is defined in exactly one place.\n *\n * @param taintedModule - The module whose accesses will be emitted as violations.\n * @param importChain - The pre-computed client entry to module import chain.\n * @param onViolation - Called once for each constructed `Violation`.\n */\nfunction emitViolationsForTaintedModule(\n  taintedModule: TaintedModule,\n  importChain: string[],\n  onViolation: (violation: Violation) => void\n): void {\n  for (const envAccess of taintedModule.accesses) {\n    onViolation({\n      moduleId: taintedModule.moduleId,\n      envVarName: envAccess.envVarName,\n      line: envAccess.line,\n      column: envAccess.column,\n      importChain,\n      suggestedFix: buildSuggestedFix(envAccess.envVarName, importChain),\n    })\n  }\n}\n\n/**\n * Walks every tainted module reachable from the client entry points and emits\n * a `Violation` for each env-var access via `onViolation`. The import chain is\n * computed once per tainted module, not once per access.\n *\n * @param clientEntryModuleIds - Entry points used as BFS roots.\n * @param graphAdapter - Returns the direct imported module IDs for a given module.\n * @param onViolation - Called once for each constructed `Violation`.\n */\nfunction reportClientReachableViolations(\n  clientEntryModuleIds: string[],\n  graphAdapter: GetImportedModuleIds,\n  onViolation: (violation: Violation) => void\n): void {\n  const reachableModuleIds = collectClientReachableModuleIds(clientEntryModuleIds, graphAdapter)\n\n  for (const taintedModule of getAllTaintedModules()) {\n    if (!reachableModuleIds.has(taintedModule.moduleId)) {\n      continue\n    }\n\n    const importChain = buildImportChainToModule(taintedModule.moduleId, clientEntryModuleIds, graphAdapter)\n    emitViolationsForTaintedModule(taintedModule, importChain, onViolation)\n  }\n}\n\n/**\n * A Vite plugin that prevents server-only environment variables from leaking\n * into client-side bundles through two coordinated detection phases.\n *\n * Phase 1 uses the `transform` hook to detect env var accesses in source modules,\n * then walks the module graph in `buildEnd` to identify client-reachable leaks.\n *\n * Phase 2 uses `generateBundle` to scan compiled output chunks for literal env var\n * values, with sourcemap resolution for precise attribution.\n *\n * @param userOptions - Optional {@link SafeEnvOptions}. All fields default to safe, non-intrusive values.\n * @returns A Vite plugin object ready to be added to the `plugins` array.\n *\n * @example\n * ```ts\n * import safeEnv from 'vite-plugin-safe-env'\n *\n * export default defineConfig({\n *   plugins: [safeEnv()],\n * })\n * ```\n */\nexport default function safeEnv(userOptions?: SafeEnvOptions): Plugin {\n  const resolvedOptions = resolvePluginOptions(userOptions)\n  const moduleFilter = createFilter(userOptions?.include ?? DEFAULT_INCLUDE, userOptions?.exclude ?? DEFAULT_EXCLUDE)\n  const allowClientAccessSet = new Set(resolvedOptions.allowClientAccess)\n  let resolvedViteConfig: ResolvedConfig\n  let envPrefixes = DEFAULT_ENV_PREFIXES\n  let devAnalysisScheduler: DebouncedAnalysisScheduler | null = null\n\n  return {\n    name: 'vite-plugin-safe-env',\n    // Must run before Vite's built-in define plugin, which replaces process.env.X\n    // member expressions with inlined literals before a 'post' transform hook would run.\n    enforce: 'pre',\n\n    configResolved(config: ResolvedConfig): void {\n      resolvedViteConfig = config\n      // Mirror the prefixes Vite itself exposes through `import.meta.env`, so a\n      // custom `envPrefix` is respected instead of assuming the `VITE_` default.\n      envPrefixes = resolveEnvPrefix({ envPrefix: config.envPrefix })\n      clearAllTaintedModules()\n    },\n\n    transform(sourceCode: string, moduleId: string): null {\n      if (!moduleFilter(moduleId)) {\n        return null\n      }\n\n      const envAccesses = scanModuleSource(sourceCode, allowClientAccessSet, envPrefixes)\n\n      if (envAccesses.length > 0) {\n        registerTaintedModule({ moduleId, accesses: envAccesses })\n        devAnalysisScheduler?.schedule()\n      }\n\n      return null\n    },\n\n    configureServer(server) {\n      // The dev-mode analysis traverses `importers` upward from each tainted module\n      // rather than `importedModules` downward from the HTML entry, because Vite's\n      // dev `moduleGraph` does not populate imports on the HTML node. Any module\n      // present in the dev graph is client-reachable by construction: the browser\n      // requested it.\n      const devImporterAdapter: GetImporterModuleIds = (moduleId) => {\n        const moduleNode = server.moduleGraph.getModuleById(moduleId)\n        return [...(moduleNode?.importers ?? [])].flatMap((importerNode) => (importerNode.id ? [importerNode.id] : []))\n      }\n\n      devAnalysisScheduler = createDebouncedAnalysisScheduler(async () => {\n        if (!hasTaintedModules()) {\n          return\n        }\n\n        for (const taintedModule of getAllTaintedModules()) {\n          if (!server.moduleGraph.getModuleById(taintedModule.moduleId)) {\n            continue\n          }\n\n          const importChain = buildImportChainViaImporters(taintedModule.moduleId, devImporterAdapter)\n\n          emitViolationsForTaintedModule(taintedModule, importChain, (violation) => {\n            if (resolvedOptions.overlay) {\n              server.ws.send(formatViolationForOverlay(violation))\n            }\n          })\n        }\n      }, DEV_ANALYSIS_QUIET_PERIOD_MS)\n    },\n\n    handleHotUpdate({ file }) {\n      clearTaintedModule(file)\n      devAnalysisScheduler?.schedule()\n    },\n\n    closeWatcher(): void {\n      devAnalysisScheduler?.cancel()\n      devAnalysisScheduler = null\n    },\n\n    buildEnd(): void {\n      if (!hasTaintedModules()) {\n        return\n      }\n\n      const clientEntryModuleIds = resolveClientEntryModuleIds(resolvedViteConfig)\n\n      const buildGraphAdapter: GetImportedModuleIds = (moduleId) => {\n        const moduleInfo = this.getModuleInfo(moduleId)\n        return moduleInfo ? [...moduleInfo.importedIds, ...moduleInfo.dynamicallyImportedIds] : []\n      }\n\n      const shouldBlock = shouldBlockBuild(resolvedOptions.blockOn)\n\n      reportClientReachableViolations(clientEntryModuleIds, buildGraphAdapter, (violation) => {\n        const message = formatViolationForTerminal(violation)\n        if (shouldBlock) {\n          this.error(message)\n        } else {\n          this.warn(message)\n        }\n      })\n    },\n\n    generateBundle(_outputOptions, outputBundle): void {\n      const serverEnvVarValues = collectServerEnvVarValues(resolvedOptions.allowClientAccess, envPrefixes)\n      if (serverEnvVarValues.size === 0) {\n        return\n      }\n\n      const shouldBlock = shouldBlockBuild(resolvedOptions.blockOn)\n\n      for (const [chunkFileName, outputChunk] of Object.entries(outputBundle)) {\n        if (outputChunk.type !== 'chunk') {\n          continue\n        }\n\n        const bundleLeaks = scanOutputChunk({\n          chunkCode: outputChunk.code,\n          chunkFileName,\n          knownSecrets: serverEnvVarValues,\n          // Rollup's SourceMap satisfies TraceMap's constructor; cast resolves optional-field structural mismatch.\n          sourceMap: (outputChunk.map ?? null) as SourceMapInput | null,\n        })\n\n        for (const bundleLeak of bundleLeaks) {\n          const message = formatBundleLeakForTerminal(bundleLeak)\n          if (shouldBlock) {\n            this.error(message)\n          } else {\n            this.warn(message)\n          }\n        }\n      }\n    },\n  }\n}\n"],"mappings":"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"}