Deno and Bun
A signup handler for Deno.serve and one for Bun.serve. Both use the built-in fetch, so there is nothing to install.
Browser
<script src="https://nolgard.com/v1/client.js" data-site-key="SITE_KEY" defer></script>
<script>
document.querySelector('#signup').addEventListener('submit', async (event) => {
event.preventDefault();
let token; // stays undefined when the script is blocked: the server takes the missing-token path
try {
await Nolgard.ready;
token = await Nolgard.execute({ action: 'signup' });
} catch {}
const form = event.target;
const response = await fetch('/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, email: form.elements.email.value })
});
// Show your own success or error message here.
});
</script>Deno
// main.ts (Deno 2): deno run --allow-net --allow-env main.ts
type Verify = { success: boolean; error?: string; decision?: string };
type Account = { email: string };
// Your own function. confirmEmailFirst holds the trial until the email is confirmed.
declare function createAccount(email: string, options?: { trialDays?: number; confirmEmailFirst?: boolean }): Promise<void>;
async function verify(token: string, remoteIp: string, action: string, account: Account): Promise<Verify> {
const response = await fetch('https://nolgard.com/v1/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ secret: Deno.env.get('NOLGARD_SECRET'), token, remoteIp, action, account }),
signal: AbortSignal.timeout(3000),
});
if (response.status >= 500) throw new Error('nolgard unavailable');
return response.json();
}
Deno.serve({ port: 3000 }, async (req, info) => {
if (req.method !== 'POST' || new URL(req.url).pathname !== '/signup') {
return new Response('Not found', { status: 404 });
}
const { token = '', email = '' } = await req.json().catch(() => ({}));
// Behind a proxy you control, read the visitor address it sets instead.
const remoteIp = info.remoteAddr.hostname;
let result: Verify;
try {
result = await verify(token, remoteIp, 'signup', { email });
} catch {
result = { success: false, error: 'unavailable' }; // timeout or 5xx
}
if (!result.success) {
switch (result.error) {
case 'invalid-secret':
case 'invalid-input': // config error: fail closed and alert yourself
return Response.json({ error: 'try-again' }, { status: 503 });
case 'missing-token': // script blocked: not a bot verdict
await createAccount(email, { trialDays: 3, confirmEmailFirst: true });
return Response.json({ ok: true, confirmEmail: true });
case 'over-quota':
case 'unavailable': // fail open with a reduced grant
await createAccount(email, { trialDays: 3 });
return Response.json({ ok: true });
default: // invalid-token, expired, replayed, site-mismatch, action-mismatch
return Response.json({ error: 'retry-token' }, { status: 409 }); // the form retries once
}
}
switch (result.decision) {
case 'allow':
await createAccount(email);
return Response.json({ ok: true });
case 'limit':
await createAccount(email, { trialDays: 3 });
return Response.json({ ok: true });
case 'shadow':
return Response.json({ ok: true }); // same response, nothing created
case 'challenge':
return Response.json({ error: 'confirm-email' }, { status: 403 });
default:
return Response.json({ error: 'rejected' }, { status: 403 });
}
});Run with --allow-net and --allow-env so the handler can read NOLGARD_SECRET and call the API. Supply your own createAccount function; confirmEmailFirst holds the trial until the user clicks your confirmation link. Without a token (ad blocker, Brave Shields, no JavaScript) the user goes through email confirmation instead of a rejection. info.remoteAddr is the direct peer, so behind a proxy use the address set by your trusted proxy. Do not trust unchecked forwarding headers.
Bun
// server.ts (Bun 1.1+): bun run server.ts
type Verify = { success: boolean; error?: string; decision?: string };
type Account = { email: string };
// Your own function. confirmEmailFirst holds the trial until the email is confirmed.
declare function createAccount(email: string, options?: { trialDays?: number; confirmEmailFirst?: boolean }): Promise<void>;
async function verify(token: string, remoteIp: string, action: string, account: Account): Promise<Verify> {
const response = await fetch('https://nolgard.com/v1/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ secret: Bun.env.NOLGARD_SECRET, token, remoteIp, action, account }),
signal: AbortSignal.timeout(3000),
});
if (response.status >= 500) throw new Error('nolgard unavailable');
return response.json() as Promise<Verify>;
}
Bun.serve({
port: 3000,
async fetch(req, server) {
if (req.method !== 'POST' || new URL(req.url).pathname !== '/signup') {
return new Response('Not found', { status: 404 });
}
const { token = '', email = '' } = await req.json().catch(() => ({}));
// Behind a proxy you control, read the visitor address it sets instead.
const remoteIp = server.requestIP(req)?.address ?? '';
let result: Verify;
try {
result = await verify(token, remoteIp, 'signup', { email });
} catch {
result = { success: false, error: 'unavailable' }; // timeout or 5xx
}
if (!result.success) {
switch (result.error) {
case 'invalid-secret':
case 'invalid-input': // config error: fail closed and alert yourself
return Response.json({ error: 'try-again' }, { status: 503 });
case 'missing-token': // script blocked: not a bot verdict
await createAccount(email, { trialDays: 3, confirmEmailFirst: true });
return Response.json({ ok: true, confirmEmail: true });
case 'over-quota':
case 'unavailable': // fail open with a reduced grant
await createAccount(email, { trialDays: 3 });
return Response.json({ ok: true });
default: // invalid-token, expired, replayed, site-mismatch, action-mismatch
return Response.json({ error: 'retry-token' }, { status: 409 }); // the form retries once
}
}
switch (result.decision) {
case 'allow':
await createAccount(email);
return Response.json({ ok: true });
case 'limit':
await createAccount(email, { trialDays: 3 });
return Response.json({ ok: true });
case 'shadow':
return Response.json({ ok: true }); // same response, nothing created
case 'challenge':
return Response.json({ error: 'confirm-email' }, { status: 403 });
default:
return Response.json({ error: 'rejected' }, { status: 403 });
}
},
});Bun reads NOLGARD_SECRET from the environment or a .env file. server.requestIP(req) is the direct peer, so behind a proxy use the address set by your trusted proxy. Supply your own createAccount function, as in the Deno example.
Cloudflare Workers and Hono
Cloudflare Workers and Hono use the same fetch call to /v1/verify and the same branches. In a Worker, read the secret from env.NOLGARD_SECRET instead of Deno.env or Bun.env, and the visitor address from the CF-Connecting-IP header.