Skip to content
PolicyStack
V1
Esc
navigateopen⌘Jpreview
On this page

@policystack/vite/consent

Static AST detection of cookie writes and vendor scripts

PolicyStack V1 — current documentation. Supported capabilities and limitations.

Static cookie and vendor detection for Consent. Scans your source for known third-party scripts and the cookies they (or your code) set.

The scanner is a pure library. The Vite plugin (@policystack/vite) and the audit CLI (@policystack/cli) are built on top of it.

Install

bun add -D @policystack/vite

Usage

import { scan } from "@policystack/vite/consent";

const result = await scan({ cwd: process.cwd() });
console.log(result.cookies); // Cookie[]
console.log(result.vendors); // VendorHit[]
console.log(result.ungated); // Ungated[]

ScanOptions:

field default notes
cwd required the project root
include **/*.{js,jsx,ts,tsx,vue,svelte,mjs,cjs,mts,cts} tinyglobby patterns
exclude node_modules, dist, .next, .svelte-kit, coverage, *.d.ts tinyglobby patterns
rules every built-in rule override to run a subset
vendors bundled registry override or extend the vendor list
concurrency os.availableParallelism() per-file workers

What it detects

Rule Pattern
document-cookie document.cookie = "..."
js-cookie Cookies.set/... from js-cookie
cookies-next setCookie(...) from cookies-next or nookies
react-cookie the setter from useCookies()
next-headers cookies().set(...) from next/headers
set-cookie-header Set-Cookie in a Response / Headers / NextResponse
vendor-imports imports, dynamic imports, and global calls for the bundled vendor registry

The bundled vendor registry covers Google Analytics / Tag Manager, Meta Pixel, PostHog, Segment, Mixpanel, Hotjar, Intercom, LinkedIn Insight, Twitter/X Pixel, TikTok Pixel, Reddit Pixel, Sentry, Datadog, and Microsoft Clarity. Pass vendors: to extend or replace it.

Suppression comments

// consent-ignore-next-line
document.cookie = "experiment=on";
// consent-ignore-file
// — placed in the first 10 lines, suppresses all hits in this file.

The “ungated” heuristic

A hit is reported as ungated when the scanner cannot find evidence that it runs only after the user has consented. The heuristic is intentionally conservative — it favours false negatives over false positives.

A hit is treated as gated when any ancestor in its AST path is one of:

  • a JSX element named <ConsentGate>, or one imported from a PolicyStack package under any local name — both import { ConsentGate as Gate }<Gate> and import * as PS<PS.ConsentGate> are recognised
  • an if / ternary whose test contains a .has(...) call (matches both consent.has("analytics") and cookies.has("session"))
  • a function named acceptAll, acceptNecessary, or any name beginning with set (catches the common setSessionCookie / setLocaleCookie shape)

This is not a control-flow analysis. Treat the ungated array as a “please double-check” list, not a definitive enforcement signal — the runtime is what enforces consent. See Consent core for the runtime story.

Custom rules

import { defineRule, scan, defaultRules } from "@policystack/vite/consent";

const banPlausible = defineRule({
	name: "plausible-import",
	visit: (ctx) => {
		if (ctx.node.type !== "ImportDeclaration") return;
		const src = (ctx.node as { source?: { value?: string } }).source?.value;
		if (src === "plausible-tracker") {
			const { line, column } = ctx.position(ctx.node.start);
			ctx.report({
				file: ctx.file,
				line,
				column,
				vendor: "plausible",
				category: "analytics",
				via: "import",
			});
		}
	},
});

await scan({ cwd: process.cwd(), rules: [...defaultRules, banPlausible] });

Vendor registry contributions

The bundled list lives in src/registry.ts. Each entry has the shape:

{
	"vendor": "stripe",
	"category": "payments",
	"imports": ["@stripe/stripe-js", "stripe"],
	"globals": ["Stripe"],
	"scriptUrls": ["https://js.stripe.com/v3/"]
}

PRs that add or correct entries are welcome. Please include a one-liner about the categorisation in your commit message; the categories are deliberately informal and align loosely with the GDPR/CCPA buckets the runtime uses.

Tests

bun test (or vp test) runs:

  • per-rule fixtures under __fixtures__/rules/<rule>/
  • integrated synthetic projects under __fixtures__/projects/
  • hand-authored real-world snippets under __fixtures__/real-world/
  • a 1000-file synthetic perf assertion (must complete in under 2s)

OPENCOOKIES_REAL_WORLD=1 vp test additionally downloads pinned tarballs of Cal.com and Documenso into tests/real-world/.cache/ (gitignored) and runs the scanner against them, asserting zero false positives on a curated allowlist of known-clean files. The cache is reused across runs.

See also

  • @policystack/vite — Vite plugin that runs the scanner during dev and CI (recommended; you usually don’t call the scanner directly)
  • @policystack/cli — terminal entry point for one-off scans and config sync
  • @policystack/core/consent — runtime that actually enforces the consent decisions the scanner is checking for

License

Apache-2.0

Last updated on September 6, 2026