Start in Observe on one route. Use the deployment URL and installed package version supplied for your workspace. Historical host examples and source capabilities do not establish a live service or published release.
Installation
Warning
Wire-name status: _mndt, x-md-vq, x-md-w / x-md-h, and
md_<seed> purpose cookies are the canonical SDK baseline in this repository,
but are undeployed. Legacy names remain accepted as documented.
Add the MANDATE script to your HTML:
<script async src="https://gate.x-lock.dev/g.js?k=YOUR_SITE_KEY"></script>Script Parameters
| Parameter | Required | Description |
|---|---|---|
k (query param) | Yes | Your site key from the MANDATE dashboard |
async (attribute) | Recommended | Prevents render blocking |
Info
The script is lightweight, loads asynchronously, and does not block rendering. There is no user-facing interaction — verification happens entirely in the background.
Same-Origin Collector Alias
High-value deployments can route collector traffic through the customer app or edge hostname instead of exposing gate collector paths directly in browser network logs. Configure the site key with a dedicated collector alias:
{
"collector": {
"origin": "https://app.example.com",
"path_prefix": "/__mndt",
"proxy_metadata": {
"mode": "monitor",
"scope": "customer_alias",
"kill_switch": false
}
}
}Then proxy the bundle-selected app routes to the matching gate route without rewriting the body. Gate-owned non-alias routes use a daily rotated /rx/<daily>/... prefix by default, while customer alias paths keep your configured stable prefix and proxy to the stable selected gate route:
| Customer route | Gate route |
|---|---|
/__mndt/<seed>/a | /r/<seed>/a |
/__mndt/<seed>/b | /r/<seed>/b |
/__mndt/<seed>/c?k=YOUR_SITE_KEY | /r/<seed>/c?k=YOUR_SITE_KEY |
/__mndt/a/<seed> | /rt/a/<seed> |
/__mndt/b/<seed> | /rt/b/<seed> |
/__mndt/c/<seed>?k=YOUR_SITE_KEY | /rt/c/<seed>?k=YOUR_SITE_KEY |
/__mndt/<seed>/e/a | /rx/<seed>/e/a |
/__mndt/<seed>/e/b | /rx/<seed>/e/b |
/__mndt/<seed>/e/c?k=YOUR_SITE_KEY | /rx/<seed>/e/c?k=YOUR_SITE_KEY |
Keep the prefix dedicated to MANDATE, forward normal request metadata such as User-Agent, Origin, Referer, and X-Forwarded-*, and do not expose proxied collector payloads to page scripts.
For production aliases, sign the proxy metadata so MANDATE can distinguish a real customer proxy from a browser that spoofed X-Mndt-Collector-Path. The proxy should preserve the encrypted request body and add only bounded routing metadata:
import { createHash, createHmac } from "node:crypto";
function signingSecret(siteSecret) {
const hash = createHash("sha256").update(siteSecret).digest("hex");
return Buffer.from(`mandate-token-v1:${hash}`, "utf8");
}
function proxySignature({ method, gatePath, collectorPath, timestamp, proxyId, siteSecret }) {
const base = ["v1", method.toUpperCase(), gatePath, collectorPath, timestamp, proxyId].join("\n");
return `sha256=${createHmac("sha256", signingSecret(siteSecret)).update(base).digest("hex")}`;
}
function requestShapeSignature({ method, gatePath, protocol, h2Settings, headerOrder, timestamp, siteSecret }) {
const base = ["v1", method.toUpperCase(), gatePath, protocol || "", h2Settings || "", headerOrder || "", timestamp].join("\n");
return `sha256=${createHmac("sha256", signingSecret(siteSecret)).update(base).digest("hex")}`;
}Forward these headers with the proxied collector request:
| Header | Value |
|---|---|
X-Mndt-Collector-Path | The customer route, for example /__mndt/<seed>/a or /__mndt/a/<seed> |
X-Mndt-Proxy | v1 |
X-Mndt-Proxy-ID | Stable deployment label such as edge-us-east |
X-Mndt-Proxy-Ts | Unix timestamp in seconds |
X-Mndt-Proxy-Signature | HMAC over v1\nMETHOD\nGATE_PATH\nCUSTOMER_PATH\nTIMESTAMP\nPROXY_ID |
X-Mndt-Protocol | Optional trusted edge protocol hint (h2, h3, or http/1.1) |
X-Mndt-H2-Settings | Optional bounded HTTP/2 settings-order hint when available |
X-Mndt-Header-Order | Optional bounded request-header order hint when available |
X-Mndt-Request-Shape | v1 when signing request-shape hints |
X-Mndt-Request-Shape-Ts | Unix timestamp in seconds |
X-Mndt-Request-Shape-Signature | HMAC over v1\nMETHOD\nGATE_PATH\nPROTOCOL\nH2_SETTINGS\nHEADER_ORDER\nTIMESTAMP |
Tip
If you run Cloudflare in front of your app, you do not have to hand-roll this.
The ready-made MANDATE Cloudflare connector
(connectors/cloudflare-worker) maps the /__mndt/* routes to the gate, signs
the proxy and request-shape metadata for you, and can optionally verify X-Mndt
at the edge.
Tip
For the golden Next.js App Router path, use the first-party collector
reference at
It signs proxy metadata, forwards opaque bodies, and re-appends multi-value
Set-Cookie (including HttpOnly _mndt; legacy Mndt-Sess remains accepted) on the customer origin. Treat it
as a reference recipe — not a published managed connector.
The gate verifies this metadata for dashboard health reporting by default. Customer Integrations and staff Operations show whether recent customer-alias traffic is ready for a stricter requirement. To link enforcement, the site must set collector.proxy_metadata.mode to enforce and MANDATE operations must set MANDATE_PROXY_METADATA_ENFORCEMENT=true; either MANDATE_PROXY_METADATA_KILL_SWITCH=true or site-level collector.proxy_metadata.kill_switch=true returns the deployment to report-only. Once linked, collector verify/delta traffic for that site must carry valid signed proxy metadata, so direct unsigned calls to canonical /v or /v/delta are blocked instead of treated as fallback traffic. MANDATE still decrypts, scores, and enforces policy on the gate; customer proxy code should not inspect collector payloads or make allow/block decisions.
Token Delivery
MANDATE keeps verification tokens private to the browser-verification runtime by default. Public g.js does not create a token meta tag, dispatch a token event, or expose a stable global token getter.
The production frontend contract is first-party credential attachment: after verification through a customer alias, the gate sets the HttpOnly _mndt credential and same-origin protected requests carry it automatically. The legacy Mndt-Sess name remains accepted. This baseline is in-repository and undeployed; the page does not receive raw token material.
The runtime may also attach x-md-vq plus x-md-w/x-md-h. Backend SDKs
forward the latter as work_proof/integrity_proof, and app code does nothing.
After hosted validation, API responses may rotate HttpOnly md_<seed> purpose
cookies through the SDK; app code does nothing.
In non-production header or dual compatibility mode, the client can automatically add the opaque X-Mndt header to same-origin fetch requests and same-origin non-GET/HEAD XMLHttpRequest requests. Same-origin requests made before verification completes are held until the proof is ready.
await fetch("/api/action", {
method: "POST",
body: JSON.stringify({ intent: "submit" }),
});Production protected routes enforce the cookie credential. Header-mode compatibility backends read and verify:
X-Mndt: <opaque-token>Tip
For cross-origin protected APIs, route browser calls through a same-origin BFF/proxy or serve the protected API from the same origin. Do not scrape page-visible token state from g.js internals.
Legacy Compatibility Bridge
The old page-visible bridge can be restored only outside production for migration shims or trusted diagnostics with MANDATE_TOKEN_BRIDGE=legacy or MANDATE_TOKEN_BRIDGE=diagnostic on the gate. In that mode, g.js publishes meta[name="mndt-token"] and mandate:token events for compatibility.
Warning
The legacy bridge makes the token readable to any script or browser automation running on the page. Keep it disabled for normal production traffic.
CAPTCHA migration shims (/compat/*) require the legacy bridge while they are in use. They never scrape polymorphic g.js internals.
SPA Integration
import { useEffect, useState } from "react";
function App() {
const [ready, setReady] = useState(false);
useEffect(() => {
const script = document.createElement("script");
script.src = "https://gate.x-lock.dev/g.js?k=YOUR_SITE_KEY";
script.async = true;
script.onload = () => setReady(true);
document.head.appendChild(script);
return () => {
document.head.removeChild(script);
};
}, []);
async function handleSubmit() {
await fetch("/api/action", { method: "POST" });
}
return <button onClick={handleSubmit} disabled={!ready}>Submit</button>;
}<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const ready = ref(false);
let scriptEl;
onMounted(() => {
scriptEl = document.createElement("script");
scriptEl.src = "https://gate.x-lock.dev/g.js?k=YOUR_SITE_KEY";
scriptEl.async = true;
scriptEl.onload = () => (ready.value = true);
document.head.appendChild(scriptEl);
});
onUnmounted(() => {
if (scriptEl) document.head.removeChild(scriptEl);
});
async function handleSubmit() {
await fetch("/api/action", { method: "POST" });
}
</script>
<template>
<button @click="handleSubmit" :disabled="!ready">Submit</button>
</template>"use client";
import Script from "next/script";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<Script
src="https://gate.x-lock.dev/g.js?k=YOUR_SITE_KEY"
strategy="afterInteractive"
/>
{children}
</>
);
}Content Security Policy
If your site uses a Content Security Policy, add the following directives:
script-src 'self' 'wasm-unsafe-eval' https://gate.x-lock.dev;
connect-src 'self' https://gate.x-lock.dev;
worker-src 'self' blob:;If you use a same-origin collector alias, keep https://gate.x-lock.dev in script-src for /g.js; collector connect-src can stay on 'self' when /__mndt/* is served by the app origin.
Warning
MANDATE verification instantiates WebAssembly, so script-src requires 'wasm-unsafe-eval'. Blob-backed workers also require blob: in worker-src. Do not add 'unsafe-inline' or 'unsafe-eval'; neither is required.
Token Lifecycle
- Tokens are issued after successful background verification
- TTL: 15 minutes from issuance
- The script automatically refreshes tokens before expiry on long-lived pages
- Tokens are single-use (recommended server-side enforcement)
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Missing X-Mndt on protected request | Request is cross-origin, GET/HEAD XHR, or sent before g.js loaded | Use same-origin fetch/mutation XHR and load g.js before protected actions |
| Token is stale / expired | Page open > 15 min without activity | Client auto-refreshes; check CSP connect-src |
| CSP violation | Missing directive | Add https://gate.x-lock.dev to script-src and connect-src |
CSP violation with blockedURI wasm-eval | WebAssembly compilation is blocked | Add 'wasm-unsafe-eval' to script-src |