noidme.js
v0.3.1
Published
Fail-closed, network-level consent enforcement SDK — the CMP that actually blocks. Intercepts every outgoing browser request and allows/blocks/quarantines it by consent, region and purpose.
Downloads
544
Maintainers
Readme
noidme.js
Fail-closed, network-level consent enforcement for the browser — the CMP that actually blocks.
Most consent tools gate known tags. noidme.js patches the browser's networking
primitives directly, so it can allow, block, or quarantine every outgoing request
— fetch, XHR, sendBeacon, WebSocket, EventSource, and <script>/<img>/<iframe>/
<link>/<form> — based on the visitor's consent, region, and the request's purpose.
By default, unknown third-party flows are denied (fail-closed), while first-party and
payment/fraud ("Essential") flows always pass so checkout never breaks.
Status: v0.3.0 — Phase 1 + 2 SDK complete (131 tests). Implemented: the synchronous decision engine; multi-vector Layer-1 patchers (fetch/XHR/beacon/WS/ES/DOM/setAttribute/ WebRTC/worklet/
<a ping>) + MutationObserver sweep; Layer-2 service worker; the watchdog; quarantine + replay; consent state/banner/persistence; GPC + Google Consent Mode v2; TCF 2.2 + GPP API routers; hash-chained consent ledger + evidence; signed-config loader; migration importers (OneTrust/Cookiebot/Ketch); sGTM consent propagation; SPA soft-nav; drift/CNAME classification; debug console. APIs may still change before 1.0.
Install
npm install noidme.jsimport { DecisionEngine, installPatchers } from 'noidme.js';A pre-bundled browser build (dist/noidme.js, ESM) is also published for direct <script type="module"> use. In production you should load it first, before any other script that can make a request.
Quick start
import { DecisionEngine, installPatchers } from 'noidme.js';
import type { Policy } from 'noidme.js';
// A policy maps third-party hosts to a tracking purpose + what to do without consent.
// (In production this arrives as a signed bundle from the edge — see "Signed config".)
const policy: Policy = {
version: 1,
hosts: new Map([
['google-analytics.com', { purpose: 'analytics', denyAction: 'quarantine' }],
['doubleclick.net', { purpose: 'advertising', denyAction: 'block' }],
]),
blocklist: new Set(['known-bad-tracker.com']),
essentialSuffixes: ['stripe.com', 'adyen.com'], // never blocked
essentialPatterns: [/(^|\.)3ds[.-]/], // 3-D Secure issuer origins
};
const engine = new DecisionEngine(policy);
// Wire every supported request vector to the engine. Returns an uninstall().
const uninstall = installPatchers(engine, (url) => ({
firstPartyHost: location.hostname,
consent: getConsentFromYourUI(), // e.g. { analytics: true, advertising: false }
region: 'EU', // injected at the edge from IP in production
mode: 'fail-closed',
}));After installPatchers, a call to fetch('https://doubleclick.net/ad') with no advertising
consent never reaches the network; fetch('https://api.stripe.com/...') always does.
How a decision is made
DecisionEngine.decide(ctx) is synchronous and runs on every request (p99 ≈ 0.0005 ms,
~400× under our 0.2 ms budget). Evaluation order:
| Step | Condition | Result |
|---|---|---|
| 1 | Unparseable absolute URL | block (allow only when mode: 'off') |
| 2 | Non-network scheme (data:/blob:/javascript:) or empty host | allow |
| 3 | First-party / same-origin | allow (Essential) |
| 4 | Essential suffix/pattern (payment, fraud, 3DS) | allow (never blocked) |
| 5 | mode: 'off' → allow · mode: 'degraded' → blocklist-only (never fail-open) | — |
| 6 | Known host with consent | allow |
| 6 | Known host without consent | its denyAction (block/quarantine) |
| 6 | Unknown third-party | block in EU/UK (interim), else quarantine |
Enforcement modes
| Mode | Unknown third-party | When |
|---|---|---|
| fail-closed | quarantine (EU: block) | steady state |
| degraded | blocklist-only | watchdog fallback — never unconditional fail-open |
| monitor | allow + record intended action | onboarding / dry-run |
| off | allow | kill-switch |
API
new DecisionEngine(policy)·.decide(ctx)→Decision·.setPolicy(p)·.versioninstallPatchers(engine, getCtx, opts?)→uninstall()— wires all vectors; targets are injectable for testing.patchFetch(engine, getCtx, hooks?, target?)— just the fetch vector (withonDecision/onQuarantinehooks).installNetworkPatchers(decide, target)/installDomPatchers(decide, target)— individual layers.verifyBundle(signed, key, state)/importVerifyKey(jwkX)— verify a signed policy bundle (see below).- Types:
Policy,PurposeRule,RequestCtx,Decision,Action,Mode,Region,SignedBundle,VerifyResult, …
Full types ship with the package (dist/noidme.d.ts).
Signed config (supply-chain integrity)
A compromised consent script is site-wide XSS, so policy is delivered as an Ed25519-signed
bundle the SDK verifies before applying — with anti-rollback (monotonic version) and
freshness (TTL) checks. The verifier (verifyBundle) is browser-portable WebCrypto;
signing is server-side only and never shipped to the browser.
import { verifyBundle, importVerifyKey } from 'noidme.js';
const key = await importVerifyKey(PUBLIC_JWK_X);
const result = await verifyBundle(signedBundle, key, { minVersion: lastApplied, nowMs: Date.now() });
if (result.ok) applyPolicy(result.payload.config);Not done yet (honest roadmap)
The client SDK is feature-complete for Phase 1 + 2. What remains is backend / infra / certification, not SDK code:
- Hosted edge config service + IP→region injection (client loader ships; the CDN/edge
service is infra). The first-party serving recipe is in
../edge/DEPLOYMENT.md. - ML tracker-classifier + scan pipeline (the SDK telemetry schema + drift monitor + leak oracle ship; the classifier/KB is a backend service).
- IAB conformance: the
__tcfapi/__gpprouters ship, but conformant binary TC/GPP strings need GVL + TCF CMP registration (external). - Admin portal, billing, DSAR/data-mapping suite, SOC 2 / pentest (GA gates).
See ../docs/BUILD-PLAN.md, ../sdk/COVERAGE.md, and the GitHub issues.
Design notes
- The engine is intentionally synchronous and allocation-light (it runs on the hot path).
- Patchers capture pristine natives and restore them exactly on
uninstall(). - Source is type-strippable TypeScript (no enums / namespaces / parameter properties) so it runs directly on Node ≥ 24 for zero-install dev/test, and bundles cleanly for the browser.
License
MIT © NoidMe
