@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 — bothimport { ConsentGate as Gate }→<Gate>andimport * as PS→<PS.ConsentGate>are recognised - an
if/ ternary whose test contains a.has(...)call (matches bothconsent.has("analytics")andcookies.has("session")) - a function named
acceptAll,acceptNecessary, or any name beginning withset(catches the commonsetSessionCookie/setLocaleCookieshape)
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