# Knock docs > Let one real human in, once. Add a Knock button to your site with door-sdk, and check each knock with a verifier you run. Knock lets a site let in a real person, once, with no account, form or captcha. The person owns a **Wedgie**: a small USB device with a screen and two buttons, whose key is bound to their World ID on World Chain and named in ENS. When they knock on your site, the Wedgie shows your site's address, they press green, and your verifier checks what the Wedgie signed against ENS and World Chain. You add two things to your site: - **A Knock button**, with [`@knock-dev/door-sdk`](reference/door-sdk.md). Its one function, `knock()`, opens Knock's Door and hands back what the Wedgie signed. - **A verifier**, which gives out challenges and checks each knock, eight checks in order: [`@knock-dev/verify`](guides/verify-in-your-server.md) inside your server, if it runs JavaScript, or [`knock-verifier`](guides/verifier.md), a small Rust service beside your site. If your server is in Rust, you can embed the same checks with the [`knock-verify`](reference/knock-verify.md) crate instead. ## What a knock tells you A knock that passes your verifier tells you that, just now, on your site: - someone pressed green on a Wedgie whose key is in ENS under a name, such as `wedgie.itzraj.eth`; - the Wedgie signed your challenge, for your origin, with a counter higher than in any knock of its you've seen; - the name is switched on, so its owner hasn't revoked it or killed it; - that key is bound on World Chain to one human, proven with World ID; - if you asked, this human hasn't done this action on your site before. It doesn't tell you who the person is. You get the Wedgie's name and the human's **nullifier**: a number that is the same for one human everywhere Knock is used, and that says nothing else about them. [How a knock works](how-it-works.md) goes through each step. ## The parts | Part | What it does | In the repo | | --- | --- | --- | | `@knock-dev/door-sdk` | The browser side. `knock()` opens the Door and resolves with the knock. One TypeScript file, no dependencies. | [`packages/door-sdk`](https://github.com/getknock/knock/blob/main/packages/door-sdk/src/index.ts) | | `@knock-dev/verify` | The checks as a TypeScript package, for a server that runs JavaScript: Node, Bun, Deno, serverless. | [`packages/verify`](https://github.com/getknock/knock/blob/main/packages/verify/src/index.ts) | | `knock-verifier` | Your verifier: `POST /challenge`, `POST /verify` and `GET /health`, with SQLite for what's been used. | [`server/verifier`](https://github.com/getknock/knock/blob/main/server/verifier/src/main.rs) | | `knock-verify` | The checks themselves, as a Rust library, for your own server. | [`crates/knock-verify`](https://github.com/getknock/knock/blob/main/crates/knock-verify/src/lib.rs) | | The Door | Knock's popup, at `https://door.getknock.dev/knock`. It talks to the Wedgie over USB, from the browser. | [`apps/door`](https://github.com/getknock/knock/blob/main/apps/door/app/knock/Door.tsx) | | The Knock Console | Where a person sets up their Wedgie, at `https://getknock.dev`: its name, its binding, and its records in ENS; and the kill switch. Sites never call it. | [`apps/door`](https://github.com/getknock/knock/blob/main/apps/door/app/console/Console.tsx) | | knockd | Relays a Wedgie's binding to World Chain, and signs World ID requests, while a person sets it up. Sites never call it. | [`server/knockd`](https://github.com/getknock/knock/blob/main/server/knockd/src/main.rs) | | `@knock-dev/protocol` | The messages a Wedgie signs (EIP-712), key helpers, and test vectors. | [`packages/protocol`](https://github.com/getknock/knock/blob/main/packages/protocol/src/index.ts) | | The Human Club | A demo site: petitions and fundraisers where each human signs once, checked with `@knock-dev/verify`. | [`apps/bouncer`](https://github.com/getknock/knock/blob/main/apps/bouncer/lib/club.ts) | Every part is open source, in [`getknock/knock`](https://github.com/getknock/knock), and you can [host any of them yourself](self-host/overview.md). ## Where Knock is today Knock is new. Read this before you plan around it: - **Nothing is on a registry yet.** Every package is at 0.1.0: door-sdk and `@knock-dev/verify` aren't on npm, and knock-verify isn't on crates.io. The [quickstart](quickstart.md) uses a checkout of [`getknock/knock`](https://github.com/getknock/knock). - **Knocking needs a desktop Chromium browser**, such as Chrome, Edge, Brave or Arc. The Door reaches the Wedgie over Web Serial, which Firefox, Safari and phones don't have. - **Only one kind of binding counts**: those made on KnockBinding v3's production deployment on World Chain. Bindings from World ID's Simulator (staging) never count. - **One mode.** Mode 1, Bound, takes the human from the Wedgie's binding. Mode 2, Fresh, a new World ID proof with each knock, isn't offered. - **The nullifier is public.** It sits in the name's ENS records, so two sites can tell that the same human knocked on both. [Security](security.md) says what follows from that. ## Start here - New to Knock? The [quickstart](quickstart.md) gets a button and a server that checks knocks working in about fifteen minutes. - Adding Knock to a real site? Read [Add a Knock button](guides/knock-button.md), then [Check knocks in your server](guides/verify-in-your-server.md) or [Run a verifier](guides/verifier.md). - Running Knock's parts yourself? [Host Knock yourself](self-host/overview.md). - Want each human in once? [One human, once](guides/one-human-once.md). - A knock bounced? [Troubleshooting](troubleshooting.md) and [Checks and refusals](reference/checks.md). - Using a coding agent? Point it at [`SKILL.md`](agents.md). # Quickstart > A Knock button on a page, a server that checks each knock with @knock-dev/verify, and one human let in, in about fifteen minutes. By the end, you'll have a page with a Knock button, a small server that checks each knock with `@knock-dev/verify`, and a Wedgie that gets in once and is turned away the second time. ## Before you start - **Bun** 1.4 or later. - **A checkout of [`getknock/knock`](https://github.com/getknock/knock)**, with `bun install` run in it. The packages aren't on npm yet, so you'll import them from the checkout, working in a folder beside it. - **To knock:** a Wedgie that's set up in the [Knock Console](https://getknock.dev), meaning named and bound to a human, and Chrome or another Chromium browser on a desktop. ```bash git clone https://github.com/getknock/knock cd knock && bun install && cd .. ``` > [!NOTE] > You can build all of this without a Wedgie. You need one only at step 3, to press green. [Test your integration](guides/testing.md) covers what you can check without one. ## 1. Check knocks in your server Your server checks each knock with `@knock-dev/verify`, Knock's eight checks as a TypeScript package. It gives out a challenge before each knock, then checks what the Wedgie signed against ENS and World Chain. Make a folder beside your checkout: ```bash mkdir knock-quickstart && cd knock-quickstart ``` Then the server, in one file. It serves your page and answers two routes: one gives out challenges, the other checks knocks. ```ts title="server.ts" import { Refusal, Verifier, memoryStore } from '../knock/packages/verify/src/index.ts' import page from './index.html' // Checks knocks for this page's origin. It keeps what it has seen in memory, which is fine for one process. const verifier = new Verifier({ origin: 'http://localhost:5173', store: memoryStore() }) // A refusal as JSON, with the HTTP status knock-verifier would give it function refused(e: unknown) { if (e instanceof Refusal) return Response.json(e, { status: e.status }) throw e } Bun.serve({ port: 5173, routes: { '/': page, '/api/challenge': { POST: async () => { try { const { challenge } = await verifier.challenge() return Response.json({ challenge }) } catch (e) { return refused(e) } }, }, '/api/knock': { POST: async (req) => { const { assertion } = await req.json() try { // 'enter' lets each human in once. Leave the action out to let them in every time. const k = await verifier.verify(assertion, { action: 'enter' }) return Response.json({ ok: true, name: k.name }) } catch (e) { return refused(e) } }, }, }, }) console.log('Open http://localhost:5173') ``` The package isn't on npm yet, so the server imports it from your checkout. The verifier checks knocks for one origin: the scheme, host and port your page is served from. Here that's `http://localhost:5173`, where this server serves the page. It reads both chains through public RPCs unless you give it your own, and it needs no keys. `memoryStore()` keeps the challenges the verifier gave out, each Wedgie's counter and each human's entries in memory, and forgets them when the server stops. A real site keeps them in a database: [Check knocks in your server](guides/verify-in-your-server.md) shows how, with every setting. If your server isn't JavaScript, run [`knock-verifier`](guides/verifier.md) beside it instead. It runs the same checks as a small Rust service, and your server asks it over HTTP. ## 2. Add the button The page is two files beside `server.ts`. It imports door-sdk, the browser side of Knock, from your checkout too. door-sdk opens Knock's Door, at `https://door.getknock.dev`: the popup that talks to the Wedgie over USB. There's nothing to run for it. ```html title="index.html" Knock

``` A knock is two requests to your server with one press in between: a challenge before, and the check after. ```ts title="main.ts" import { KnockError, knock } from '../knock/packages/door-sdk/src/index.ts' const button = document.querySelector('#knock')! const said = document.querySelector('#said')! /** A fresh challenge from your server: single use, good for 2 minutes. */ async function challenge(): Promise<`0x${string}`> { const r = await fetch('/api/challenge', { method: 'POST' }) const j = await r.json() if (!r.ok) throw new Error(j.why) return j.challenge } button.addEventListener('click', async () => { button.disabled = true said.textContent = 'Waiting for your Wedgie…' try { // Call knock() in the click itself: browsers allow a popup only there. // It opens the Door at once and sends the challenge when it arrives. const assertion = await knock({ challenge: challenge() }) said.textContent = 'Checking…' const r = await fetch('/api/knock', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ assertion }), }) const v = await r.json() said.textContent = v.ok ? `In: ${v.name}` : `Bounced at check ${v.check}: ${v.why}` } catch (e) { said.textContent = e instanceof KnockError ? `No knock: ${e.message}` : String(e) } finally { button.disabled = false } }) ``` The page only shows what the server decided. Your server is what lets someone in, counts a vote or starts a session, so it never takes the page's word for who got in. ## 3. Knock Start the server: ```bash bun server.ts ``` Open `http://localhost:5173` and press the button. 1. The Door opens as a popup, from `door.getknock.dev`. The first time, it asks you to connect the Wedgie, and the browser asks which serial device to use. 2. The Wedgie shows **KNOCK**: the name it knocks as, and `http://localhost:5173`, the origin the Door saw. 3. Press green. The Door closes, and your page says `In: wedgie.itzraj.eth`, or whichever name your Wedgie has. 4. Press the button again and press green again. This time your server turns the knock away at check 8: `this human has done enter here already`. That second answer is `action: 'enter'` at work: the verifier keeps one entry per human for each action. Leave `action` out, and the same human can come in as often as they knock. ## What you have - A challenge per knock, so a knock can't be replayed or used on another site. - A server that read the name's records from ENS and the binding from World Chain, and checked the Wedgie's signature, before letting anyone in. - One entry per human for `enter`. ## Next - [Add a Knock button](guides/knock-button.md): the button for a real site, every error, and React. - [Check knocks in your server](guides/verify-in-your-server.md): a database for what the verifier remembers, serverless functions, and every setting. - [Run a verifier](guides/verifier.md): the same checks as a service, for a server in another language. - [Checks and refusals](reference/checks.md): what each check means, and what to tell people. # How a knock works > Who does what in a knock, from the click to the verifier's answer, and why your verifier needn't trust the browser, the Door or Knock. ## Who takes part - **The person and their Wedgie.** The Wedgie makes its key inside its secure chip, an OPTIGA Trust M, and the key never leaves it. The Wedgie shows each request on its own screen and signs only after a press of its green button. - **Your page**, with door-sdk. - **The Door**: Knock's popup. It talks to the Wedgie over USB, through the browser's Web Serial, and hands the knock to the page that opened it. It holds no key, so it can carry a knock but never make one. - **Your verifier**: your own service. It reads two public chains and trusts nothing it's handed. - **ENS, on Ethereum mainnet**: the Wedgie's name, and the Knock records on it. - **World Chain**: the binding. It's a transaction on Knock's KnockBinding contract, which checked a World ID proof and then tied the Wedgie's key to one human and to the name. ## Once: setting up a Wedgie Before a Wedgie can knock, its owner sets it up in the Knock Console, in this order: 1. **The key.** The Wedgie makes it on the chip, after a green press. 2. **The binding.** The person proves with World ID that they're a human, and the Wedgie signs a request to bind its key. KnockBinding checks the proof on World Chain and logs `Bound(nullifier, keyHash, node, expiresAtMin)`: this key, this human, this name. The nullifier is the human's number for Knock's World ID action: the same for every Wedgie they bind. 3. **The name.** The owner's wallet sets the name's Knock records in ENS. `knock.poh` points at the binding's transaction, so the binding comes first. | Record | Holds | | --- | --- | | `knock.v` | `1`, the version of these records | | `knock.key` | The Wedgie's public key: compressed P-256, 33 bytes in hex | | `knock.status` | `active`, or `revoked` when its owner turns it off | | `knock.nullifier` | The human's nullifier, as a 32-byte word in hex | | `knock.poh` | The binding: `eip155:480:tx:` and its transaction hash | The owner can kill the name with the Console's kill switch, from the wallet that holds it: the name leaves ENS, and its knocks bounce at once. The Wedgie needn't be there, so a lost or stolen one can be stopped too. If it is there, it forgets the name and its binding, and can be set up again from scratch. The owner can also revoke the name in the [ENS app](https://app.ens.domains), by setting `knock.status` to `revoked`. One human can bind several Wedgies, each under its own name. ## A knock, step by step ```text your page the Door the Wedgie your verifier ENS, World Chain | challenge, please ------------------------------>| | open ------->| | | challenge -->| request ------->| | | | KNOCK, green | | |<------- signed | | |<-- the knock | | | check it ---------------------------------------->| records, binding ->| |<------------------------------------- in, or why not | ``` 1. **A challenge.** On the click, your page asks your verifier for a challenge: 32 random bytes for your origin, good once, for 120 seconds. 2. **The Door opens.** In the same click, `knock()` opens the Door as a popup. The Door says it's ready, and door-sdk sends it the challenge, addressed to the Door's origin only. 3. **Your origin, from the browser.** The Door reads your page's origin from the browser, as the origin of the message that carried the challenge. Your page never states it, so it can't claim to be another site. The Door takes `https://` origins only, and `http://localhost` or `http://127.0.0.1` for development. 4. **The Wedgie asks its owner.** Its KNOCK page shows the name it knocks as, and your origin, with *unverified: the Door says so* beneath. The Wedgie can't see the browser, so it shows whose word the origin is. 5. **The press.** On green, the Wedgie raises its counter and signs a `KnockPass` (EIP-712) over the challenge, the origin, its name's node, the counter, the mode and the human's nullifier. Red means no knock. 6. **The Door hands it over.** The Door checks the signature against the Wedgie's key, then posts the knock to your page's origin only, and closes. 7. **Your verifier checks it.** The knock goes to your verifier's `POST /verify`. It runs eight checks in order, and the first that fails gives the answer. If all pass, it uses up the challenge, records the counter and, if you named an action, the entry, all in one transaction. Then it answers `{ok: true, knock}`. ## The eight checks | | Check | It reads | | --- | --- | --- | | 1 | The knock is whole and well formed | The knock | | 2 | Its challenge is one this verifier gave, unused, under 2 minutes old, for this origin | Its own store | | 3 | The name has Knock's records, switched on, and unchanged for the last 3 blocks | ENS on mainnet | | 4 | The Wedgie whose key ENS holds signed it, for this site and challenge | ENS's key, and the knock | | 5 | Its counter is higher than in any knock of this Wedgie's seen before | Its own store | | 6 | The binding names this key, this human and this name, on KnockBinding v3, and isn't too old | World Chain | | 7 | Its mode is one this verifier offers: mode 1 | The knock | | 8 | This human hasn't done this action here before, when there is one | Its own store | Check 7 runs just before check 6, and keeps its number. [Checks and refusals](reference/checks.md) has every answer each check can give. ## What your verifier trusts It trusts four things: - **Ethereum mainnet**, for ENS, through the RPC you give it. - **World Chain**, for the binding, through the RPC you give it. - **KnockBinding v3**, whose code checks a World ID proof before it logs a `Bound`. - **The Wedgie**: its chip keeps the key in, and its firmware signs only after a green press. It trusts nothing else: - **Not the browser, and not your page.** The signature covers the challenge and the origin. The key, the human and the binding come from ENS and World Chain, never from the knock. - **Not the Door.** It holds no key, so a knock it made up fails check 4. - **Not Knock.** No Knock server takes part in a knock. Your verifier reads public chains only, so it keeps working as long as ENS and World Chain do. ## What each side learns - **Your site** learns the name, the Wedgie's key, the human's nullifier, the Wedgie's counter and the binding's transaction. It doesn't learn who the person is. - **Knock's servers** learn nothing about knocks. knockd, Knock's signer, takes part only when a Wedgie is bound. - **Anyone** can read each name's records in ENS, including its key and nullifier. Only the sites a Wedgie knocks on see its knocks. Because the nullifier is the same for one human on every site, sites that compare notes can tell it's the same human. [Security](security.md) covers this and the rest of the model. # Add a Knock button > The browser side of Knock. Call knock() from a click, hand it a challenge, and turn every answer into words people can act on. The button does three things: it gets a challenge from your verifier, calls `knock()`, and sends the knock it gets back to be checked. ## Call knock() from the click `knock()` opens the Door as a popup. Browsers allow a popup only while they're handling a click, and that permission doesn't last long. So call `knock()` straight away in the click handler, before any `await`, and hand it the challenge as a promise: ```ts button.addEventListener('click', () => { // The Door opens now, and the challenge goes to it when your verifier answers const knocked = knock({ challenge: challenge() }) }) ``` Don't wait for the challenge first. If your verifier is slow, the click's permission can run out, and the browser blocks the Door: ```ts button.addEventListener('click', async () => { const c = await challenge() // the click's permission to open a popup may be gone by now await knock({ challenge: c }) // may reject with KnockError('blocked') }) ``` The challenge must be the one your verifier gave, as `0x` and 64 hex digits. A challenge you make up yourself fails check 2. ## Check what comes back `knock()` resolves with an `Assertion`: the knock as the Wedgie signed it. It isn't a decision yet. Nothing in it can be trusted until your verifier has checked it, so send it on: ```ts const assertion = await knocked const r = await fetch('/api/knock', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ assertion }), }) ``` Here `/api/knock` is your server, which asks your verifier and acts on its answer. [Run a verifier](verifier.md#verify-from-your-server) shows that side. ## When there's no knock When no knock happens, `knock()` rejects with a `KnockError`. Its `code` says what happened, and its `message` says it in words you can show people as they are. ```ts try { const assertion = await knock({ challenge: challenge() }) // … } catch (e) { if (!(e instanceof KnockError)) throw e if (e.code === 'rejected' || e.code === 'closed') return // they said no, or changed their mind show(e.message) } ``` ### door-sdk's own codes | Code | What happened | What to show or do | | --- | --- | --- | | `blocked` | The browser didn't open the Door. | Ask them to allow popups for your site. Check that `knock()` runs in the click itself. | | `closed` | The Door was closed before the Wedgie knocked. | Nothing. Let them knock again. | | `timeout` | No knock within `timeoutMs`, 3 minutes by default. The Door is closed. | "No knock in time. Try again." | | `challenge` | Your challenge failed: the promise rejected, or it isn't `0x` and 64 hex digits. | Your verifier is down or busy. Say Knock is unavailable for now, and log it. | | `refused` | The Door said no without a code. | Show the message. | ### The Door's codes The Door answers with these, and door-sdk passes them on as they are. | Code | What happened | What to show or do | | --- | --- | --- | | `rejected` | Red on the Wedgie. | Nothing: they said no. | | `timeout` | Nobody pressed on the Wedgie within 2 minutes. | "No press on your Wedgie. Try again." | | `not_enrolled` | The Wedgie has no name yet, or isn't bound to a human. | The message: it says to set the Wedgie up in the Knock Console. | | `not_ready` | The Door couldn't talk to the Wedgie: unplugged, or gone mid-knock. | Ask them to check the Wedgie is plugged in, then knock again. | | `bad_origin` | Your page isn't on `https://`, or on `localhost` or `127.0.0.1` for development. | Serve your page over https. | | `bad_request` | The challenge or mode was wrong, or the Wedgie couldn't read the request. | Pass your verifier's challenge as it is, and leave `mode` out. | | `bad_signature` | The Wedgie's knock didn't hold for your site and challenge, so the Door sent nothing. | Rare. Let them knock again. | The Wedgie's own codes also come through the Door as they are, such as `no_key` when the Wedgie has no Knock key yet. Show the message. ### When the Door waits instead Some trouble stays in the Door's window, and your page hears nothing until the person closes it (`closed`) or the time runs out (`timeout`): - **Another tab has the Wedgie**, such as the Knock Console. Only one tab can talk to it at a time, so the Door asks them to disconnect it there. - **The Wedgie hasn't been connected to the Door before.** The Door shows a button, and the browser asks which serial device to use. - **The Door is framed.** It never works inside another page, since a frame could cover what it says. ## Keep it to one knock at a time Disable the button while a knock is going, and say what's happening. Each `knock()` opens the popup window named `knock-door`, so a second knock while the first is open takes over its window. | While | Say | | --- | --- | | The Door is open | "Waiting for your Wedgie…" | | Your verifier is checking | "Checking…" | | It passed | Who's in, by name | | It bounced | The verifier's `why`, such as "wedgie.itzraj.eth is revoked: its owner turned it off" | ## Show it only where knocking works The Door reaches the Wedgie over Web Serial, which only desktop Chromium browsers have. The Door runs in the same browser as your page, so your page can tell: ```ts const canKnock = typeof navigator !== 'undefined' && 'serial' in navigator ``` Where `canKnock` is false, show your usual way in, or a line saying Knock needs Chrome, Edge or another Chromium browser on a computer. ## A React component This one asks your server for the challenge and the check, so the browser never decides who's in. ```tsx title="KnockButton.tsx" 'use client' import { KnockError, knock } from '@knock-dev/door-sdk' import { useState } from 'react' type Said = { busy: boolean; text: string | null } async function challenge(): Promise<`0x${string}`> { const r = await fetch('/api/knock/challenge', { method: 'POST' }) const j = await r.json() if (!r.ok) throw new Error(j.why ?? 'Knock is unavailable right now') return j.challenge } export function KnockButton({ onIn }: { onIn: (name: string) => void }) { const [said, setSaid] = useState({ busy: false, text: null }) function enter() { setSaid({ busy: true, text: 'Waiting for your Wedgie…' }) knock({ challenge: challenge() }) .then(async (assertion) => { setSaid({ busy: true, text: 'Checking…' }) const r = await fetch('/api/knock', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ assertion }), }) const v = await r.json() if (v.ok) { setSaid({ busy: false, text: null }) onIn(v.name) } else setSaid({ busy: false, text: v.why }) }) .catch((e) => { const quiet = e instanceof KnockError && (e.code === 'rejected' || e.code === 'closed') setSaid({ busy: false, text: quiet ? null : e instanceof Error ? e.message : String(e) }) }) } return (

{said.text}

) } ``` The Human Club does the same in [`apps/bouncer/app/Knock.tsx`](https://github.com/getknock/knock/blob/main/apps/bouncer/app/Knock.tsx), and ticks off the checks as they pass. ## Options | Option | Type | Default | What it does | | --- | --- | --- | --- | | `challenge` | `` `0x${string}` `` or a promise of one | Required | Your verifier's challenge. A promise lets the Door open at once. | | `mode` | `1` | `1` | Bound: the human comes from the Wedgie's binding. Mode 2 isn't offered. | | `door` | `string` | `DOOR`, `https://door.getknock.dev` | Where the Door is. | | `timeoutMs` | `number` | `180000` | How long the whole knock may take, presses included. | The [door-sdk reference](../reference/door-sdk.md) has every type, and the messages door-sdk and the Door send each other. # Check knocks in your server > @knock-dev/verify runs the eight checks inside your own server: Node, Bun, Deno, or serverless and edge functions. No service beside your site. `@knock-dev/verify` is the verifier as a package. It runs the same eight checks as [`knock-verifier`](verifier.md), in the same order, with the same codes and words, but inside your own server, so there's no second service to run. It works wherever modern JavaScript runs on a server: Node 20 or later, Bun, Deno, and serverless or edge functions. > [!IMPORTANT] > Run it on your server, never in the visitor's browser. A check that runs in the browser is one the person being checked can skip, and what makes a knock count once (used challenges, counters, entries) would be theirs to clear. ## Add it It isn't on npm yet. Use it from a checkout of `getknock/knock`, in one of two ways. In a workspace with Knock's packages, as the Human Club does: ```json title="package.json" { "dependencies": { "@knock-dev/verify": "workspace:*", "@libsql/client": "^0.18.0" } } ``` Anywhere else, copy its file and the protocol's into your project, point one at the other, and add their two dependencies: ```bash mkdir -p src/knock cp ../knock/packages/protocol/src/index.ts src/knock/protocol.ts cp ../knock/packages/verify/src/index.ts src/knock/verify.ts sed -i.bak "s#'@knock-dev/protocol'#'./protocol'#" src/knock/verify.ts && rm src/knock/verify.ts.bak bun add viem @noble/curves @libsql/client ``` Then import from `./knock/verify` instead of `@knock-dev/verify`. ## Make your verifier One verifier checks knocks for one origin, your site's, and keeps what it has seen in a store. ```ts title="knock.ts" import { createClient } from '@libsql/client' import { Verifier, libsqlStore, rpcChain } from '@knock-dev/verify' export const verifier = new Verifier({ // Your site's origin, exactly as browsers send it origin: 'https://club.example', // What it remembers between knocks: a SQLite file here, or Turso store: libsqlStore( createClient({ url: process.env.KNOCK_DB_URL ?? 'file:knock.db', authToken: process.env.KNOCK_DB_TOKEN }), ), // Your own RPCs. Public ones are used if you leave these out. chain: rpcChain({ eth: process.env.ETH_RPC, world: process.env.WORLD_RPC }), }) ``` ## Give out challenges Your page asks your server for a challenge just before it knocks. Each one is good for one knock, for two minutes. ```ts title="challenge.ts" import { Refusal } from '@knock-dev/verify' import { verifier } from './knock' export async function POST() { try { const { challenge } = await verifier.challenge() return Response.json({ challenge }) } catch (e) { if (e instanceof Refusal) return Response.json(e, { status: e.status }) throw e } } ``` A `Refusal` turns into `{ ok: false, check, error, why }` as JSON, the same shape `knock-verifier` answers with, and its `status` is the HTTP status the service would give. ## Check a knock The page hands you what door-sdk's `knock()` resolved with. `verify()` runs V1 to V8, then writes what the knock used up, all of it or none: ```ts title="verify.ts" import { Refusal } from '@knock-dev/verify' import { verifier } from './knock' export async function POST(req: Request) { const { assertion } = await req.json() try { // `action` lets each human do it once (V8). Leave it out to let them in every time. const k = await verifier.verify(assertion, { action: 'enter' }) // In: one human, bound on World Chain. Their nullifier is the same on every Knock site. await letIn({ human: k.nullifier, name: k.name }) return Response.json({ ok: true, name: k.name }) } catch (e) { if (e instanceof Refusal) return Response.json(e, { status: e.status }) throw e } } ``` These are web-standard handlers, so they fit Next.js route handlers, Bun, Deno, Hono and the like as they are. Express and Fastify take the same calls with their own request and response. The page side doesn't change: [Add a Knock button](knock-button.md) shows `knock()` with a challenge from your server, then sending the assertion back. ## Settings Everything is an option to `new Verifier()`. Only `origin` and `store` are needed. | Option | Default | What it does | | --- | --- | --- | | `origin` | none | Your site's origin, exactly as browsers send it: `https://host` or `https://host:port`, or `http://localhost` or `http://127.0.0.1`, with or without a port, for development. Knocks for any other origin fail check 2. | | `store` | none | Where it keeps challenges, counters, revoked keys and entries. See below. | | `chain` | public RPCs | Where it reads ENS on mainnet and bindings on World Chain: `rpcChain({ eth, world })`, each a URL or a viem client. | | `maxBindingDays` | `365` | How old a binding's World ID proof may be, in days. Older bindings fail check 6 as `binding_expired`. | | `strict` | `'off'` | Whether a binding must be final on World Chain first: `'off'`, `'safe'` or `'finalized'`. [Strict mode](strict-mode.md) explains the choice. `strictFrom()` reads `on`, `true` and the like from an environment variable. | | `contract` | Knock's KnockBinding | The KnockBinding whose bindings count. Leave it alone unless you take knocks from [a network of your own](../self-host/own-network.md). | The defaults are the service's. The public RPCs are fine for trying Knock out, but rate-limited, and they're what the checks believe: [Its RPCs](verifier.md#its-rpcs) says why your own matter on a real site. ## Where it remembers A verifier remembers four things between knocks: the challenges it gave out, the highest counter it has seen from each Wedgie, the keys it has seen revoked, and who has done each action. Every instance of your server has to reach the same store, or a challenge given by one instance won't be found by the next, and a knock could be replayed against another. - **`libsqlStore(client)`** keeps them in a libSQL database: a SQLite file (`file:knock.db`), [Turso](https://turso.tech) (`libsql://…` with its token), or memory. It makes its four tables on first use, each named `knock_…`, so they can sit beside your own. On edge runtimes, make the client with `@libsql/client/web`. - **`memoryStore()`** keeps them in the process. It's for tests, and for a site that's one long-lived process. Serverless functions don't share memory, so don't use it there. - **Your own**: anything that implements [`Store`](../reference/verify.md#store). Its `commit()` must check and write the challenge, the counter and the entry in one transaction. The tables mean what the service's do, and losing them costs the same: [Its database](verifier.md#its-database). ## Serverless and edge functions - Use `libsqlStore` with Turso, or your own store over a database the functions share. - Set `origin` from your configuration, never from the request's `Host` header: the origin is what the Wedgie signed for, and a proxy can change the header. - A check makes three to five RPC calls. Allow your function at least ten seconds. - Each instance keeps a name's records for a minute, as the service does. A new instance reads them again, which costs a call and nothing else. ## Writing what a knock uses up yourself `verify()` keeps its own tables. If your site needs to write what a knock uses up in its own transaction, together with what the knock was for, use the checks on their own: `parse()` for V1, then `check()` for V3 to V7. The Human Club does this: a signature, the human and the Wedgie's counter go in one transaction, so a knock that's turned away writes nothing. ```ts title="own-records.ts" import { type Memory, check, parse } from '@knock-dev/verify' declare const memory: Memory declare function takeChallenge(challenge: string): Promise export async function knocked(body: { assertion: unknown }) { const a = parse(body.assertion) // V1 if (!(await takeChallenge(a.challenge))) throw new Error('not our challenge') // V2: yours, used once const k = await check({ origin: 'https://club.example', memory }, a) // V3–V7, writing nothing but a revoked key // Then, in one transaction: the counter, only if it rose (V5 again, for two knocks racing), your V8, your act return k } ``` `memory` answers the counter for a key and remembers revoked keys. [`apps/bouncer/lib/club.ts`](https://github.com/getknock/knock/blob/main/apps/bouncer/lib/club.ts) is a whole example, with libSQL. ## The package, the service or the crate All three run the same checks, held to the same test vectors. - **`@knock-dev/verify`**, when your server runs JavaScript or TypeScript: nothing extra to run. - **[`knock-verifier`](verifier.md)**, the Rust service, when your server is in another language: it answers over HTTP. - **[`knock-verify`](../reference/knock-verify.md)**, the Rust crate, when your server is in Rust. The package measures time in milliseconds, as JavaScript does; the service and the crate count seconds. # Run a verifier > knock-verifier is the service that decides whether a knock gets in. Its settings, where to run it, and how your server asks it. `knock-verifier` is a small Rust service you run beside your site. It gives out challenges and checks knocks for one origin, your site's, and keeps what it has seen in a SQLite file. It reads ENS on Ethereum mainnet and bindings on World Chain through RPCs, needs no keys, and never calls Knock. If your server runs JavaScript or TypeScript, you don't need the service: [`@knock-dev/verify`](verify-in-your-server.md) runs the same checks inside your server. ## Build it From the root of a checkout of `getknock/knock`: ```bash cargo build --release -p knock-verifier ./target/release/knock-verifier ``` It's one binary, with SQLite built in. ## Settings Everything is an environment variable, and only `VERIFIER_ORIGIN` usually needs setting. | Variable | Default | What it is | | --- | --- | --- | | `VERIFIER_ORIGIN` | `http://localhost:3001` | Your site's origin, exactly as browsers send it: `https://host` or `https://host:port`, or `http://localhost` or `http://127.0.0.1`, with or without a port, for development. Knocks for any other origin fail check 2, and CORS allows only this one. | | `VERIFIER_ADDR` | `127.0.0.1:8788` | Where it listens. | | `VERIFIER_DB` | `knock-verifier.db` | Its SQLite file. | | `VERIFIER_ETH_RPC` | `https://ethereum-rpc.publicnode.com` | An Ethereum mainnet RPC, for ENS. | | `VERIFIER_WORLD_RPC` | `https://worldchain-mainnet.g.alchemy.com/public` | A World Chain RPC, for bindings. | | `VERIFIER_MAX_BINDING_DAYS` | `365` | How old a binding's World ID proof may be, in days. Older bindings fail check 6 as `binding_expired`. | | `VERIFIER_STRICT` | `off` | Whether a binding must be final on World Chain first: `off`, `on` (or `safe`), or `finalized`. [Strict mode](strict-mode.md) explains the choice. | | `VERIFIER_KNOCK_BINDING` | `knock` | The KnockBinding whose bindings count: `knock` for Knock's, or the address of [a network of your own](../self-host/own-network.md)'s. | The default origin is `http://localhost:3001`, for development, so set yours: ```bash VERIFIER_ORIGIN=https://club.example \ VERIFIER_DB=/var/lib/knock/verifier.db \ VERIFIER_ETH_RPC=https://your-mainnet-rpc.example \ VERIFIER_WORLD_RPC=https://your-worldchain-rpc.example \ ./target/release/knock-verifier ``` ## What it checks before it starts Before it takes a single knock, the verifier makes sure of where it reads from, and stops if anything is off: - `VERIFIER_ORIGIN` is an origin it can check knocks for. - `VERIFIER_ETH_RPC` is chain 1, Ethereum mainnet, and ENS's Universal Resolver is there. - `VERIFIER_WORLD_RPC` is chain 480, World Chain, and the KnockBinding it accepts reports version 3. The KnockBinding it accepts is Knock's production v3 on World Chain, `0x7748D2787DcB6E6024968edC19fcA13ccAbA1699`, built in from [`contracts/deployments/480.json`](https://github.com/getknock/knock/blob/main/contracts/deployments/480.json), unless `VERIFIER_KNOCK_BINDING` names [a network of your own](../self-host/own-network.md)'s. Bindings on any other KnockBinding, including staging's and older versions', never count. ## Verify from your server Your page gets the knock, but your server should decide what it means. If the browser asks the verifier and then tells your server "they're in", your server is taking the browser's word for it. Instead, have the browser hand the knock to your server, and your server ask the verifier: ```text browser -- the knock --> your server -- POST /verify --> verifier browser <-- in, or why not -- your server <-- the answer -- ``` Keep the verifier on `127.0.0.1`, where only your server can reach it, and give the browser two routes on your own origin. In Next.js: ```ts title="app/api/knock/challenge/route.ts" const VERIFIER = process.env.KNOCK_VERIFIER ?? 'http://127.0.0.1:8788' // A challenge for the page, passed through as it is export async function POST() { const r = await fetch(`${VERIFIER}/challenge`, { method: 'POST' }) return new Response(r.body, { status: r.status, headers: { 'content-type': 'application/json' } }) } ``` ```ts title="app/api/knock/route.ts" const VERIFIER = process.env.KNOCK_VERIFIER ?? 'http://127.0.0.1:8788' export async function POST(req: Request) { const { assertion } = await req.json() const r = await fetch(`${VERIFIER}/verify`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ assertion, action: 'enter' }), }) const v = await r.json() if (!v.ok) return Response.json({ ok: false, check: v.check, error: v.error, why: v.why }, { status: r.status }) // In: one human, bound on World Chain. Their nullifier is the same on every Knock site. const { name, nullifier } = v.knock await letIn({ human: nullifier, name }) // your session, vote, claim… return Response.json({ ok: true, name }) } ``` Check 2 compares the origin inside the knock, the one the Door saw, with `VERIFIER_ORIGIN`. Where the request to `/verify` comes from doesn't matter, so your server can make it. ### From the browser, for trying things out A page can ask the verifier itself, because the verifier allows CORS from `VERIFIER_ORIGIN`. It's fine for a demo, or when nothing on your server depends on the answer. Then the verifier must be reachable from the browser, behind your TLS proxy, at an address such as `https://knock.club.example`. ## One verifier per origin A verifier checks knocks for one origin, because the origin is part of what the Wedgie signs. Run one for each: - `https://club.example` and `https://www.club.example` are two origins, so two verifiers, or one canonical host. - Staging at `https://staging.club.example` gets its own verifier and its own database. - For development, run one with `VERIFIER_ORIGIN=http://localhost:5173` or wherever your dev server is. ## Its database The SQLite file at `VERIFIER_DB` holds four tables: | Table | Holds | If you lose it | | --- | --- | --- | | `challenges` | Each challenge, its origin, when it was given out and whether it's been used. Expired ones are cleared as new ones are given out. | Knocks under way fail check 2. People knock again. | | `counters` | The highest counter seen for each Wedgie's key. | No harm: old knocks still fail check 2, since their challenges are gone. | | `revoked` | Keys it has seen revoked in ENS, which stay refused even if their records come back. | A revoked key could get in again if its owner turned it back on. | | `entries` | For each action, the humans who've done it, with the name they knocked as and when. | Every human can do every action once more. | Back it up if `entries` matter to you. It's one SQLite file, so one verifier process uses it at a time. ## Its RPCs The default RPCs are free public endpoints. They're good for trying Knock out, but they limit how often you can call them, and the verifier makes several calls for each knock: the mainnet head, the name's five Knock records at the latest block and again 3 blocks deep (unless it read the same ones in the last minute), and the binding's receipt on World Chain. For a real site, use your own endpoints or a paid provider. Your RPCs are also what the verifier believes. An RPC that lies could show records or a binding that aren't on chain. Use providers you trust, or your own nodes. The verifier never logs an RPC's URL, since URLs often carry keys. When an RPC can't be reached, the verifier refuses the knock as `unavailable`, with status 503. It never lets a knock in on a guess. ## Limits | Limit | Value | When it's hit | | --- | --- | --- | | Request body | 16 KiB | Refused at check 1, status 400. A knock is under 1 KiB. | | Challenges | 600 an hour, per process | `busy`, status 429 | | Checks | 600 an hour, per process | `busy`, status 429 | | Challenges outstanding | 10,000 at once | `busy` at check 2, status 429 | ## Health and logs `GET /health` says what the verifier checks for: ```json { "binding": "0x7748d2787dcb6e6024968edc19fca13ccaba1699", "maxBindingDays": 365, "ok": true, "origin": "https://club.example", "strict": "off" } ``` It logs each answer, who got in or which check said no, and nothing about who asked: ```text INFO let in wedgie.itzraj.eth (counter 12) INFO refused at V8: already_entered ``` ## In your own Rust server `knock-verifier` is a thin HTTP layer over the [`knock-verify`](../reference/knock-verify.md) crate. If your server is in Rust, you can call the checks directly and skip the extra service. # One human, once > Actions let each human do something on your site once, such as enter, vote or claim, however many Wedgies they own. A knock that passes tells you a real human is there. An **action** tells you whether that human has done something on your site before. Name an action when you send a knock to be checked, and check 8 lets each human do it once. ```json { "assertion": { "…": "the knock" }, "action": "enter" } ``` ## How check 8 counts The verifier counts humans by the human's **nullifier**, not by Wedgie or by name. One human gets one nullifier from Knock's World ID action, and every Wedgie they bind carries it. So a human who owns three Wedgies, under three names, still does each action once. When every other check has passed, the verifier records the action and nullifier in the same transaction that uses up the challenge. A knock that's refused at any check uses up nothing, and doesn't count as the human's turn. The second time the same human knocks for the same action, check 8 refuses it: ```json { "ok": false, "check": 8, "error": "already_entered", "why": "this human has done enter here already" } ``` ## Naming actions An action is 1 to 32 characters of `a`–`z`, `0`–`9` and `-`. Anything else is refused at check 1. Pick names for what people do, and keep them stable: | Action | Once means | | --- | --- | | `enter` | One entry per human, ever | | `vote-2026-budget` | One vote per human in this poll | | `claim-launch-drop` | One claim per human for this drop | | `signup` | One account per human | Actions are counted per verifier, so per origin. `enter` on your site and `enter` on someone else's are separate. To start an action over, say for a new poll, use a new name. There's no way to clear an action through the API. ## Without an action Leave `action` out, and check 8 doesn't run. The same human can come in as often as they knock. Each knock still needs its own challenge and a higher counter, so none can be replayed. Use this when you want to know that a human is here now, and not whether they've been here before. For example: - before posting a comment, or sending a message; - to slow down sign-ups, alongside your own limits; - to step up an existing session before something sensitive. ## Keeping track yourself The verifier answers with the knock that got in, including `nullifier`: ```json { "ok": true, "knock": { "name": "wedgie.itzraj.eth", "key": "0x02…", "nullifier": "0x1f…", "counter": 12, "origin": "https://club.example", "binding": { "tx": "0x…", "block": 37301500, "provedAt": 1793860000 } } } ``` To tie your own records to a human, such as an account or an allowance, key them by `nullifier`. Don't key them by `name` or `key`: one human can have several Wedgies, and can kill a name and set up the Wedgie again under another. > [!IMPORTANT] > The nullifier is public. It's in the name's ENS records, and it's the same on every site that uses Knock. Treat it as an identifier for a human that others can see, and don't show it next to anything you'd keep private. [Security](../security.md) says more. ## What once doesn't stop - **A human who lends their Wedgie.** Knock knows a Wedgie is bound to a human, and that someone pressed it. It can't know who pressed. - **Your other verifiers.** Each one keeps its own entries, so staging and production, or two origins, count separately. - **A lost database.** Entries live in the verifier's SQLite file. [Run a verifier](verifier.md#its-database) says what to back up. # Strict mode > Whether a binding must be final on World Chain before its knocks count. Off by default; what it costs to turn on, and when it's worth it. A knock stands on its Wedgie's binding: a transaction on World Chain. By default, a verifier counts the binding as soon as that transaction is in a block. A **strict** verifier waits until World Chain has posted the block to Ethereum, or until that post is final there. Strict mode only affects people who knock within minutes of binding their Wedgie. After that, every binding is final anyway. ## How World Chain settles a block World Chain is an OP Stack chain: 1. **In a block.** Its sequencer puts the binding in a block within a few seconds: World Chain makes one every 2 seconds. This is its latest head. 2. **Safe.** Its batcher posts the block's data to Ethereum. Once that post is in an Ethereum block, the block is at or below World Chain's safe head. The batcher posts every few minutes. 3. **Finalized.** Once the Ethereum block holding that post is finalized, about two epochs or 13 minutes later, the World Chain block is at or below the finalized head. Measured on 2026-11-06: World Chain's batcher posted to Ethereum 40 times in four hours, every 4.6 to 7.4 minutes, 6.2 minutes apart in the middle. Between posts the safe head stands still, so it falls up to 8.5 minutes behind the latest block before it jumps. One binding mined at 05:14:39 UTC became safe at 05:21:12, 6.5 minutes later. The finalized head runs about 20 to 25 minutes behind. ## The three settings | `VERIFIER_STRICT` | A binding counts once | Someone who just bound waits | | --- | --- | --- | | `off` (default) | Its transaction is in a block | Nothing | | `on`, or `safe` | World Chain has posted its block to Ethereum | Up to ~9 minutes, ~6 typically | | `finalized` | That post is final on Ethereum | About 20 to 25 minutes | `on`, `true`, `1`, `yes` and `safe` all mean safe. `off`, `false`, `0`, `no` and an empty value all mean off. Anything else stops the verifier at start. `GET /health` reports the setting as `off`, `safe` or `finalized`. ## What a strict verifier says A knock whose binding isn't final yet fails check 6, as `binding_not_final`, with the wait in words: | Setting | `why` | | --- | --- | | Safe | this site is strict: it takes a binding once World Chain has posted it to Ethereum, which it does every ~6 minutes. Try again in a few minutes | | Finalized | this site is strict: it takes a binding once World Chain's post of it to Ethereum is final, ~20–25 minutes after binding. Try again later | Nothing is used up, so the same person can knock again a few minutes later and get in. Show them the `why`. ## What a verifier that isn't strict risks The risk is a binding that World Chain drops, or replaces, before its batch reaches Ethereum. A human could then knock on a binding that, in the end, never happened. For that to happen, World Chain's sequencer would have to misbehave, or its batches would have to stop reaching Ethereum for hours. Even then, the knock is still signed by the Wedgie after a press, and its name is still checked in ENS. Only the World ID proof behind the binding would be in doubt. ## Which to choose - **Off**, for most sites. People can knock as soon as they've set up their Wedgie, and the risk above is remote. - **Safe**, when a knock is worth something that can't be taken back, such as a payout or a one-time claim, and a few minutes' wait for new Wedgies is fine. - **Finalized**, rarely: when you want only what Ethereum itself has settled, and a new Wedgie can wait half an hour. ## Setting it For `knock-verifier`: ```bash VERIFIER_STRICT=on ./target/release/knock-verifier ``` With the `knock-verify` crate: ```rust use knock_verify::{Strict, Verifier}; let verifier = Verifier::new(chain, store, origin, contract, 365 * 86_400).strict(Strict::Safe); ``` A verifier that isn't strict never asks World Chain for its safe or finalized head, so it makes one call fewer for each knock. # Test your integration > What to test without a Wedgie, the knocks to try with one, and the Human Club and its bot as a working reference. Most of a Knock integration can be tested without a Wedgie: what your page does with each answer, and what your server does with each verdict. You need a Wedgie, and a person to press it, only for the knocks that get in. ## Without a Wedgie ### Your page's answers Keep the code that calls `knock()` apart from the code that shows the result, so a test can hand it any answer. Here `enter` takes `knock` and `fetch` as arguments: ```ts title="enter.ts" import { type Assertion, KnockError, type KnockOptions, knock as realKnock } from '@knock-dev/door-sdk' export type Said = { in: string } | { out: string } | { quiet: true } type Post = (url: string, init: RequestInit) => Promise export async function enter( challenge: () => Promise<`0x${string}`>, knock: (o: KnockOptions) => Promise = realKnock, post: Post = fetch, ): Promise { try { const assertion = await knock({ challenge: challenge() }) const v = await (await post('/api/knock', { method: 'POST', body: JSON.stringify({ assertion }) })).json() return v.ok ? { in: v.name } : { out: v.why } } catch (e) { if (e instanceof KnockError && (e.code === 'rejected' || e.code === 'closed')) return { quiet: true } return { out: e instanceof Error ? e.message : String(e) } } } ``` ```ts title="enter.test.ts" import { expect, test } from 'bun:test' import { type Assertion, KnockError } from '@knock-dev/door-sdk' import { enter } from './enter' const challenge = async () => `0x${'ab'.repeat(32)}` as const test('red on the Wedgie says nothing', async () => { const red = () => Promise.reject(new KnockError('rejected', 'Red on the Wedgie: no knock.')) expect(await enter(challenge, red)).toEqual({ quiet: true }) }) test('a bounce shows the verifier’s why', async () => { const knocked = async () => ({}) as Assertion const bounced = async () => Response.json({ ok: false, check: 3, error: 'revoked', why: 'wedgie.itzraj.eth is revoked: its owner turned it off' }) expect(await enter(challenge, knocked, bounced)).toEqual({ out: 'wedgie.itzraj.eth is revoked: its owner turned it off', }) }) ``` Cover each code in [Add a Knock button](knock-button.md#when-theres-no-knock), and a verifier that can't be reached. ### Your server's verdicts Your server route acts on the verifier's answer, so test it against each kind: `{ok: true}`, a refusal from each check, `busy` with status 429 and `unavailable` with status 503. Stub the verifier's HTTP answers, or run a real verifier and send it knocks that bounce. The Human Club's bot sends a real verifier the knocks a bot would try, for the club's origin, `http://localhost:3001`. It signs them with a key of its own, so none should get in, and it says where each one bounced: ```bash cd apps/bouncer bun run bot wedgie.itzraj.eth http://localhost:8788 ``` ```text The Human Club's bot tries to get in as wedgie.itzraj.eth, without its Wedgie (verifier http://localhost:8788): its own key, a fresh challenge bounced at check 4: not signed by wedgie.itzraj.eth's key, for this site and challenge a knock signed for another site bounced at check 2: the knock is for https://evil.example, not http://localhost:3001 … Nothing got in. ``` It exits with 1 if anything got in. Each try bounces at the first check that catches it, so what it prints depends on the name you give it, and on the verifier: a name with no Knock records bounces at check 3, and a verifier for another origin turns most tries away at check 2. [`apps/bouncer/scripts/bot.ts`](https://github.com/getknock/knock/blob/main/apps/bouncer/scripts/bot.ts) is a starting point for a bot of your own. ### The protocol [`spec/vectors/v1.json`](https://github.com/getknock/knock/blob/main/spec/vectors/v1.json) holds test vectors: a published test key, and the digests and signatures of each message Knock signs. Don't use that key for anything real. `@knock-dev/protocol`'s tests check the TypeScript side against them, and `knock-verify`'s tests take the vectors' signed knock through all eight checks with a stand-in chain: ```bash bun run --filter @knock-dev/protocol test cargo test -p knock-verify ``` ## With a Wedgie Run your page and a verifier for your page's origin, then try each of these. Each row is one knock. | Do this | Expect | | --- | --- | | Press green | In | | Knock again, with the same action | Check 8: `already_entered` | | Knock again, with no action | In | | Press red | `KnockError` `rejected` | | Close the Door before pressing | `KnockError` `closed` | | Leave the KNOCK screen for 2 minutes | `KnockError` `timeout`, from the Wedgie | | Stop your verifier, then knock | `KnockError` `challenge` | | Keep the Wedgie connected in the Console's tab, then knock | The Door asks you to disconnect it there | | Set the name's `knock.status` to `revoked` in the ENS app, then knock | Check 3: `revoked` | | Kill the name in the Console, then knock | Check 3: `no_name` | | Name a Wedgie, then knock within a minute | Check 3: `records_changing`, then in a minute later | | With `VERIFIER_STRICT=on`, knock within minutes of binding | Check 6: `binding_not_final` | | Run a verifier for another origin, then knock from your page | Check 2: `wrong_origin` | Revoking and killing change the name for every site, so use a Wedgie set up for testing. ## The Human Club The Human Club is a whole Knock site: petitions and fundraisers where each human signs once, with the checks ticked off as they pass. It checks knocks in its own functions with `@knock-dev/verify`, so it needs no verifier beside it. It's in [`apps/bouncer`](https://github.com/getknock/knock/blob/main/apps/bouncer/lib/club.ts), and runs on port 3001: ```bash # two terminals, from the root of the checkout cd apps/door && bun run dev # the Door, on 3000 cd apps/bouncer && bun run dev # the club, on 3001 ``` Its records go in `apps/bouncer/human-club.db`. Delete the file to start again. [Host the Human Club](../self-host/human-club.md) has its settings. # door-sdk > Every export of @knock-dev/door-sdk, the knock it returns field by field, and the messages it and the Door send each other. `@knock-dev/door-sdk` is the browser side of Knock: one TypeScript file with no dependencies, in [`packages/door-sdk/src/index.ts`](https://github.com/getknock/knock/blob/main/packages/door-sdk/src/index.ts). It needs a browser, since it opens a window. ```ts import { DOOR, type Assertion, type Hex, KnockError, type KnockOptions, knock } from '@knock-dev/door-sdk' ``` ## Get it It isn't on npm yet. Use it from a checkout of `getknock/knock`, in one of three ways: - **Import it from the checkout**, as the [quickstart](../quickstart.md) does: `from '../knock/packages/door-sdk/src/index.ts'`. - **Copy the file** into your project, such as `src/door-sdk.ts`, and import from it. - **In a workspace with Knock's packages**, add `"@knock-dev/door-sdk": "workspace:*"` to your dependencies. With Next.js, also add `transpilePackages: ['@knock-dev/door-sdk']` to `next.config.ts`, since the package ships TypeScript. ## knock() ```ts function knock(options: KnockOptions): Promise ``` Opens the Door and resolves with the knock the Wedgie signed, or rejects with a `KnockError`. Call it from a click: it opens the Door straight away, and browsers allow that only while they're handling a click. In order, `knock()`: 1. opens `/knock` in a popup window named `knock-door`, 420 by 680 pixels, or rejects with `blocked`; 2. waits for the Door's `knock/ready`, and for your challenge, whichever comes last; 3. checks the challenge is `0x` and 64 hex digits, or rejects with `challenge`; 4. posts `knock/request` to the Door, addressed to the Door's origin only; 5. resolves with the `knock/result`'s assertion, or rejects with its error. It rejects with `closed` if the popup is closed first, and with `timeout`, closing the popup, if `timeoutMs` passes. It takes messages only from the popup it opened, at the Door's origin. ## KnockOptions ```ts type KnockOptions = { challenge: Hex | Promise mode?: 1 door?: string timeoutMs?: number } ``` | Field | Default | What it is | | --- | --- | --- | | `challenge` | Required | Your verifier's challenge from `POST /challenge`, or a promise of it. With a promise, the Door opens at once and gets the challenge when it arrives. | | `mode` | `1` | Bound: the human comes from the Wedgie's binding. Mode 2, Fresh, isn't offered. | | `door` | `DOOR` | The Door's URL. Only its origin is used: the popup opens at `/knock` there. | | `timeoutMs` | `180000` | How long the whole knock may take, presses included. The Wedgie itself gives up after 2 minutes without a press. | ## Assertion The knock, as the Wedgie signed it and the Door handed it over. Send it to your verifier as it is, and trust none of it until the verifier says `ok`. ```ts type Assertion = { v: 1 name: string challenge: Hex origin: string deviceNode: Hex counter: number mode: 1 nullifier: Hex humanRef: Hex r: Hex s: Hex } ``` | Field | What it is | | --- | --- | | `v` | The format's version: `1`. | | `name` | The ENS name the Wedgie knocks as, such as `wedgie.itzraj.eth`. | | `challenge` | Your verifier's challenge, 32 bytes. | | `origin` | Your page's origin, as the Door read it from the browser. | | `deviceNode` | The name's ENS namehash. | | `counter` | The Wedgie's knock counter. It rises with each knock, on any site. | | `mode` | `1`, Bound. | | `nullifier` | The human's nullifier, 32 bytes, as the Wedgie holds it. Check 6 compares it with the name's ENS records. | | `humanRef` | 32 zero bytes in mode 1. | | `r`, `s` | The Wedgie's P-256 signature over the knock's `KnockPass` digest, `s` low. | The [protocol](protocol.md#knockpass) page says how the digest is made. ## KnockError ```ts class KnockError extends Error { readonly code: string } ``` Why there was no knock. `code` is for your code, and `message` is a sentence you can show people. | `code` | From | Meaning | | --- | --- | --- | | `blocked` | door-sdk | The browser didn't open the popup. | | `closed` | door-sdk | The popup was closed before a knock. | | `timeout` | door-sdk | `timeoutMs` passed. | | `challenge` | door-sdk | The challenge's promise rejected, or it isn't `0x` and 64 hex digits. | | `refused` | door-sdk | The Door said no without a code. | | `rejected` | The Door | Red on the Wedgie. | | `timeout` | The Door | Nobody pressed on the Wedgie within 2 minutes. | | `not_enrolled` | The Door | The Wedgie has no name, or isn't bound to a human. | | `not_ready` | The Door | The Door couldn't talk to the Wedgie. | | `bad_origin` | The Door | The page isn't on `https://`, or `localhost` or `127.0.0.1` for development. | | `bad_request` | The Door | The challenge or mode was wrong, or the Wedgie couldn't read the request. | | `bad_signature` | The Door | The Wedgie's knock didn't hold for this site and challenge, so nothing was sent. | | `no_key` | The Wedgie, through the Door | The Wedgie has no Knock key yet. | The Door passes the Wedgie's other codes on as they are. [Add a Knock button](../guides/knock-button.md#when-theres-no-knock) says what to show for each. ## DOOR ```ts const DOOR = 'https://door.getknock.dev' ``` The Door that door-sdk opens unless you pass `door`: Knock's own. To use [one you serve yourself](../self-host/door.md), pass its address as `door`. ## Hex ```ts type Hex = `0x${string}` ``` ## The messages door-sdk and the Door talk with `postMessage`. You only need these if you write your own client. | Message | From, to | Sent to | Body | | --- | --- | --- | --- | | `knock/ready` | Door to your page | Any origin | `{ type: 'knock/ready' }`. It says only that the Door is up. | | `knock/request` | Your page to the Door | The Door's origin | `{ type: 'knock/request', challenge, mode: 1 }` | | `knock/result` | Door to your page | Your page's origin | `{ type: 'knock/result', ok: true, assertion }`, or `{ type: 'knock/result', ok: false, error, why }` | The Door: - takes a request only from the window that opened it, and reads that window's origin from the browser, as the message's `origin`; - answers once, and only to that origin; - takes one request, and closes itself a moment after a knock; - refuses to work inside a frame. # @knock-dev/verify > Every export of the verifier package, the eight checks for a site's own server, and what each takes and gives back. `@knock-dev/verify` is one TypeScript file, [`packages/verify/src/index.ts`](https://github.com/getknock/knock/blob/main/packages/verify/src/index.ts), with two dependencies: `@knock-dev/protocol` and `viem`. [Check knocks in your server](../guides/verify-in-your-server.md) shows it in use. Times are in milliseconds unless a name says otherwise. ## Verifier ```ts new Verifier(site: Site & { store: Store }) ``` A verifier for one origin. It throws if `origin` isn't one browsers write. - **`challenge(now?)`** gives out a challenge and keeps it in the store: `Promise`. It refuses with `busy` at check 2 when 10,000 are outstanding. - **`verify(assertion, { action?, now? })`** runs V1 to V8 on what door-sdk's `knock()` resolved with, then commits what the knock used up: its challenge, its Wedgie's counter, and, with an `action`, the human's entry for it. All of it is written, or none. It resolves with `Knocked`, or rejects with a `Refusal`. - **`site`** is the options it was made with. ### Site What the checks need to know about your site. `Verifier` takes it with a `store`, and `check()` with a `memory`. | Field | Default | | | --- | --- | --- | | `origin` | none | Your site's origin: `https://host[:port]`, or `http://localhost[:port]` or `http://127.0.0.1[:port]` for development. | | `chain` | `rpcChain()`, public RPCs | A `Chain`. | | `contract` | `KNOCK_BINDING` | The KnockBinding whose `Bound` logs count. | | `maxBindingDays` | `MAX_BINDING_DAYS`, 365 | How old a binding's proof may be. | | `strict` | `'off'` | A `Strict`: how final a binding must be. | ### Challenge `{ challenge, origin, expiresAt }`: the challenge for door-sdk's `knock()`, as `0x` and 64 hex digits, your origin, and when it expires. ### Knocked A knock that got in: | Field | | | --- | --- | | `name` | The Wedgie's ENS name. | | `key` | Its compressed P-256 key, as `knock.key` holds it, in lowercase. | | `keyHash` | `keccak256(x ‖ y)` of that key, as `Bound` names it. | | `nullifier` | The human: a 32-byte word in lowercase hex, the same on every site that takes Knock. | | `counter` | The Wedgie's counter in this knock. | | `origin` | Your origin. | | `binding` | A `Binding`: `{ tx, block, provedAt }`, the World Chain transaction and block, and about when its World ID proof was made, in unix seconds. | ## Refusal ```ts class Refusal extends Error { check: number; error: string; why: string; status: number } ``` A no: the check that said it, 1 to 8 (0 for the verifier's own trouble), its code, and why, in words a person can read. Its codes and words are the service's: [Checks and refusals](checks.md) lists them. `toJSON()` gives `{ ok: false, check, error, why }`, and `status` the HTTP status `knock-verifier` answers with: 400 for check 1, 403 for checks 2 to 8, 429 for `busy`, 503 for `unavailable`, 500 for check 0. ## The checks - **`parse(x)`**, V1: `x` as an `Assertion`, whole and well formed, its `s` low. Anything else is refused at check 1 with `bad_assertion`. - **`check(site, assertion, now?)`**, V3 to V7, for a knock that passed V1 and whose challenge you've checked yourself. It needs `site.memory`, a `Memory`, and writes nothing but a key seen revoked. Store the counter and anything V8 asks in your own transaction. It resolves with `Knocked`. - **`checkRecords(name, deep, latest)`**, V3: the name's `Records` at the latest block and 3 blocks deep. It gives back a `Valid`: `{ key, keyHash, nullifier, poh }`. - **`checkBinding(receipt, wanted)`**, V6: a `Receipt` and what's `Wanted` of it, `{ tx, contract, nullifier, keyHash, node, final?, now, maxAge }`, with `now` in unix seconds and `maxAge` in seconds. It gives back a `Binding`. - **`okName(name)`** and **`okOrigin(origin)`**: whether Knock takes this name and this origin. - **`parseKey(text)`**: a `knock.key` record as `{ key, keyHash }`, or `null` if it isn't a compressed P-256 key. ### Assertion What door-sdk's `knock()` resolves with: `{ v, name, challenge, origin, deviceNode, counter, mode, nullifier, humanRef, r, s }`. [The protocol](protocol.md#knockpass) says what each field is. ### Strict `'off' | 'safe' | 'finalized'`. **`strictFrom(text)`** reads it as people write it: `off` (or nothing, `false`, `0`, `no`), `on` (`true`, `1`, `yes`, `safe`) or `finalized`. Anything else throws. ## Chain Where the checks read the chains: ```ts interface Chain { head(): Promise // mainnet's latest block records(name: string, block: bigint): Promise // the name's Knock records at a block receipt(tx: Hex): Promise // a World Chain receipt finalBlock(strict: 'safe' | 'finalized'): Promise // World Chain's safe or finalized head } ``` **`rpcChain({ eth?, world? })`** makes one over JSON-RPC with viem. Give each an RPC URL or a viem client, a mainnet one made with `chain: mainnet`. Left out, they're `ETH_RPC` and `WORLD_RPC`, public endpoints. ENS is read through its Universal Resolver, at the block asked for. - **`Records`**: `{ v, key, status, nullifier, poh }`, the five `knock.*` text records, each `null` when empty. **`recordsOf(texts)`** makes one from the texts in `RECORDS`' order. - **`Receipt`**: `{ ok, block, logs }`, where each log is `{ address, topics, data }`. A chain that throws is a chain that couldn't be read: the knock is refused as `unavailable`, and the error's URLs are left out of `why`. ## Store What a verifier remembers between knocks. Every instance of your server must reach the same one. ```ts interface Store extends Memory { issue(challenge: Hex, origin: string, now: number): Promise // false: too many out challenge(challenge: Hex): Promise // { origin, issuedAt, used } entered(action: string, nullifier: Hex): Promise commit(c: Commit): Promise<'challenge' | 'counter' | 'entry' | null> } ``` **`commit()`** takes a `Commit`, `{ challenge, origin, key, counter, entry, now }`, where `entry` is `{ action, nullifier, name }` or `null`. In one transaction it marks the challenge used (if it's this origin's, unused and unexpired), raises the key's counter (only upward), and makes the entry (if it's new). Each is checked again as it's written, so of two knocks racing for one challenge, counter or entry, one gets it. It answers with what the other lost, or `null`, and writes nothing when it loses. ### Memory What `check()` needs from a store: ```ts interface Memory { counter(key: Hex): Promise // the key's highest counter taken revoke(key: Hex, now: number): Promise isRevoked(key: Hex): Promise // a key seen revoked stays revoked } ``` ### The stores here - **`memoryStore()`**: in the process, for tests and a site that's one long-lived process. - **`libsqlStore(client)`**: in a libSQL database, from `@libsql/client` (or `@libsql/client/web` on edge runtimes): a SQLite file, Turso, or memory. Any client with `execute`, `executeMultiple` and `transaction('write')` will do: that's `SqlClient`. It runs `SCHEMA` on first use: four tables, `knock_challenges`, `knock_counters`, `knock_revoked` and `knock_entries`. ## Constants | Constant | Value | | | --- | --- | --- | | `WORLD_CHAIN` | `480` | World Chain's chain id. | | `CHALLENGE_MS` | `120_000` | How long a challenge lasts. | | `DEPTH` | `3n` | How many blocks deep `knock.status=active` must be. | | `CACHE_MS` | `60_000` | How long a name's deep records are kept. | | `MAX_BINDING_DAYS` | `365` | The oldest binding that counts, by default. | | `RECORDS` | | `knock.v`, `knock.key`, `knock.status`, `knock.nullifier`, `knock.poh`. | | `KNOCK_BINDING` | `0x7748D2787DcB6E6024968edC19fcA13ccAbA1699` | KnockBinding v3, production, on World Chain. | | `ETH_RPC` | `https://ethereum-rpc.publicnode.com` | The public mainnet RPC used by default. | | `WORLD_RPC` | `https://worldchain-mainnet.g.alchemy.com/public` | The public World Chain RPC used by default. | # Verifier HTTP API > knock-verifier's three routes, their requests and answers, status codes, CORS and limits. `knock-verifier` listens on `VERIFIER_ADDR`, `127.0.0.1:8788` by default, and takes JSON. Every refusal has the same shape, whichever route gives it. ## POST /challenge A challenge for one knock: 32 random bytes, for the verifier's origin, good once, for 120 seconds. It takes no body. ```bash curl -s -X POST http://127.0.0.1:8788/challenge ``` ```json { "challenge": "0x6c1f…e2a9", "origin": "https://club.example", "expiresAt": 1793860320 } ``` | Field | What it is | | --- | --- | | `challenge` | 32 bytes in hex. Pass it to `knock()` as it is. | | `origin` | The origin knocks must be for: `VERIFIER_ORIGIN`. | | `expiresAt` | When it expires, in unix seconds. | It can refuse with `busy` and status 429, at check 0 when the hour's limit is reached, or at check 2 when 10,000 challenges are outstanding. ## POST /verify Checks a knock, V1 to V8 in order, and if all pass, uses up what it used in one transaction. ```json title="Request" { "assertion": { "v": 1, "name": "wedgie.itzraj.eth", "challenge": "0x6c1f…e2a9", "origin": "https://club.example", "deviceNode": "0x…", "counter": 12, "mode": 1, "nullifier": "0x…", "humanRef": "0x0000000000000000000000000000000000000000000000000000000000000000", "r": "0x…", "s": "0x…" }, "action": "enter" } ``` | Field | What it is | | --- | --- | | `assertion` | The knock from `knock()`, as it is. Unknown fields are refused. | | `action` | Optional. Lets each human do this once: 1 to 32 characters of `a`–`z`, `0`–`9` and `-`. [One human, once](../guides/one-human-once.md). | The body may have no other fields. ### When it passes Status 200: ```json { "ok": true, "knock": { "name": "wedgie.itzraj.eth", "key": "0x02…", "nullifier": "0x…", "counter": 12, "origin": "https://club.example", "binding": { "tx": "0x…", "block": 37301500, "provedAt": 1793860000 } } } ``` | Field | What it is | | --- | --- | | `name` | The ENS name it knocked as. | | `key` | The Wedgie's public key, from ENS: compressed P-256, 33 bytes in hex. | | `nullifier` | The human's nullifier, the same on every Knock site. Key your records by it. | | `counter` | The Wedgie's counter for this knock. | | `origin` | Your origin. | | `binding.tx` | The binding's World Chain transaction, from `knock.poh`. | | `binding.block` | The World Chain block it's in. | | `binding.provedAt` | About when its World ID proof was made, in unix seconds. Bindings older than `VERIFIER_MAX_BINDING_DAYS` are refused. | ### When it's refused ```json { "ok": false, "check": 3, "error": "revoked", "why": "wedgie.itzraj.eth is revoked: its owner turned it off" } ``` | Field | What it is | | --- | --- | | `check` | The check that said no, 1 to 8, or 0 for the verifier's own trouble. Every check before it passed, and none after it ran. | | `error` | A code your code can act on. [Checks and refusals](checks.md) lists them all. | | `why` | The reason in words, for people. | A refused knock uses up nothing: its challenge, the counter and the entry are all as they were. ## GET /health What the verifier checks knocks for. ```json { "binding": "0x7748d2787dcb6e6024968edc19fca13ccaba1699", "maxBindingDays": 365, "ok": true, "origin": "https://club.example", "strict": "off" } ``` | Field | What it is | | --- | --- | | `binding` | The KnockBinding whose bindings count: Knock's v3, production, on World Chain, or the one `VERIFIER_KNOCK_BINDING` names. | | `maxBindingDays` | `VERIFIER_MAX_BINDING_DAYS`. | | `origin` | `VERIFIER_ORIGIN`. | | `strict` | `off`, `safe` or `finalized`. [Strict mode](../guides/strict-mode.md). | ## Status codes | Status | When | | --- | --- | | 200 | A challenge, a knock that passed, or health. | | 400 | Check 1: the body or the knock isn't well formed. | | 403 | Checks 2 to 8 said no. | | 429 | `busy`: the verifier's limits. Try again later. | | 500 | `internal`: the verifier's own store failed. | | 503 | `unavailable`: a chain couldn't be read. Try again. | ## CORS The verifier allows exactly one origin, `VERIFIER_ORIGIN`, with `GET`, `POST` and the `content-type` header. Browsers on any other origin can't read its answers. Requests from your server aren't affected, since CORS is between browsers and sites. ## Limits | Limit | Value | | --- | --- | | Body | 16 KiB | | `POST /challenge` | 600 an hour, per process | | `POST /verify` | 600 an hour, per process | | Outstanding challenges | 10,000 | | A challenge's life | 120 seconds, single use | ## TypeScript types ```ts title="verifier.ts" export type Challenge = { challenge: `0x${string}`; origin: string; expiresAt: number } export type Knocked = { name: string key: `0x${string}` nullifier: `0x${string}` counter: number origin: string binding: { tx: `0x${string}`; block: number; provedAt: number } } export type Verdict = { ok: true; knock: Knocked } | { ok: false; check: number; error: string; why: string } ``` # knock-verify (Rust) > The verifier's checks as a Rust library, for a server that checks knocks itself. Verifier, the Chain trait, Store, and every public item. `knock-verify` is what `knock-verifier` runs: V1 to V8 in order, failing closed, and a SQLite store for what's been used. Use it when your server is in Rust and you'd rather not run a second service. Its source is [`crates/knock-verify/src/lib.rs`](https://github.com/getknock/knock/blob/main/crates/knock-verify/src/lib.rs). ## Add it It isn't on crates.io, so depend on the repo: ```toml title="Cargo.toml" [dependencies] knock-verify = { git = "https://github.com/getknock/knock" } alloy = { version = "2.5", default-features = false, features = ["std"] } ``` Its types are alloy's (`Address`, `B256`, `U256`), so use the same alloy, 2.5. Its Rust is edition 2024: Rust 1.88 or later. ## Use it ```rust title="src/knock.rs" use knock_verify::{Store, Strict, Verifier, accepted_binding}; fn now() -> u64 { std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() } pub async fn example(chain: MyChain, assertion: serde_json::Value) -> anyhow::Result<()> { let verifier = Verifier::new( chain, // your Chain: mainnet and World Chain Store::open("knock.db")?, // or ":memory:" for tests "https://club.example".to_string(), // the origin knocks must be for accepted_binding().map_err(anyhow::Error::msg)?, // KnockBinding v3, production 365 * 86_400, // the oldest binding it takes, in seconds ) .strict(Strict::Off); // For the page: pass challenge.challenge to knock() let challenge = verifier.challenge(now()).map_err(|r| anyhow::anyhow!(r.why))?; // The knock the page sent back match verifier.verify(&assertion, Some("enter"), now()).await { Ok(knocked) => println!("in: {} (human {})", knocked.name, knocked.nullifier), Err(no) => println!("refused at check {}: {} ({})", no.check, no.why, no.error), } Ok(()) } ``` Share one `Verifier` across requests, in an `Arc`. It's `Send` and `Sync` when your `Chain` is. ## Verifier ```rust pub struct Verifier { pub chain: C, pub store: Store, pub origin: String, pub contract: Address, pub max_binding_age: u64, pub strict: Strict, /* a cache of deep ENS reads */ } ``` | Method | What it does | | --- | --- | | `Verifier::new(chain, store, origin, contract, max_binding_age)` | A verifier for one origin. Not strict. | | `.strict(strict)` | The same verifier, strict: it takes a binding only once World Chain's safe or finalized head has reached it. | | `.challenge(now)` | A new `Challenge` for the page, good once, for `CHALLENGE_TTL` seconds. Refuses with `busy` at check 2 when 10,000 are outstanding. | | `.verify(&assertion, action, now).await` | V1 to V8, then one commit. `assertion` is the knock's JSON as a `serde_json::Value`. `action` is the site's action for V8, or `None`. Returns `Knocked`, or the first `Refusal`. | `now` is unix seconds. The store's commit happens only after every check has passed, so a refused knock uses up nothing. ## The Chain trait How the verifier reads the chains. You implement it over your RPCs: ```rust pub trait Chain: Sync { fn head(&self) -> impl Future> + Send; fn records(&self, name: &str, block: u64) -> impl Future> + Send; fn receipt(&self, tx: B256) -> impl Future, Unavailable>> + Send; fn final_block(&self, strict: Strict) -> impl Future> + Send; } ``` | Method | Returns | | --- | --- | | `head` | Ethereum mainnet's latest block number. | | `records` | The name's five `RECORDS` texts at a mainnet block, through ENS's Universal Resolver. An empty text is `None`, and so is a name with no resolver. | | `receipt` | A World Chain transaction's receipt, or `None` if there's none. | | `final_block` | World Chain's safe head for `Strict::Safe`, or its finalized head for `Strict::Finalized`. Never called when the verifier isn't strict. | Return `Unavailable` when a chain can't be read. The verifier then refuses the knock as `unavailable`, at the check that was reading, and never guesses. `async fn` in your `impl` is fine: ```rust impl Chain for MyChain { async fn head(&self) -> Result { self.eth.get_block_number().await.map_err(|e| Unavailable(format!("mainnet: {e}"))) } // records, receipt, final_block … } ``` `knock-verifier`'s `Rpc`, in [`server/verifier/src/main.rs`](https://github.com/getknock/knock/blob/main/server/verifier/src/main.rs), is a complete implementation with alloy, Universal Resolver calls included. Copy it. ## Store The SQLite store: challenges, counters, keys seen revoked, and entries. `Verifier` calls it for you. | Method | What it does | | --- | --- | | `Store::open(path)` | Opens or creates the database. `":memory:"` lasts as long as the process. | | `issue(origin, now)` | A new challenge, after clearing expired ones. | | `fresh(challenge, ours, knocked_for, now)` | V2: ours, unused, unexpired, and for our origin. | | `counter(key)` | The highest counter seen for a key. | | `revoke(key, now)`, `is_revoked(key)` | Keys seen revoked, for good. | | `entered(action, nullifier)` | Whether a human has done an action. | | `commit(challenge, origin, key, counter, entry, now)` | Uses the challenge, raises the counter and records the entry, all or none. | ## Types | Type | What it is | | --- | --- | | `Assertion` | A knock as the Door hands it over (door-sdk's `Assertion`), from `parse`. | | `Challenge` | `{ challenge, origin, expires_at }`, serialized in camelCase. | | `Knocked` | A knock that passed: `{ name, key, nullifier, counter, origin, binding }`. | | `Binding` | `{ tx, block, proved_at }`: the binding a knock stands on. | | `Refusal` | `{ check, error, why }`: the first check that said no, a code, and words. | | `Unavailable` | A chain couldn't be read: `Unavailable(String)`. | | `Strict` | `Off` (the default), `Safe` or `Finalized`. Parses from `off`, `on`, `safe`, `finalized` and the like; `as_str()` gives `off`, `safe` or `finalized`. | | `Records` | A name's Knock records at one block. `Records::from_texts` builds one from the texts of `RECORDS`, in order. | | `Valid` | Records that passed V3: the key, its `VerifyingKey`, its `key_hash`, the nullifier and the binding's transaction. | | `Receipt`, `Log` | A World Chain receipt, as `Chain::receipt` returns it. | | `Wanted` | What V6 asks of a binding: transaction, contract, nullifier, key hash, name's node, final block, now, oldest age. | ## Constants | Constant | Value | What it is | | --- | --- | --- | | `WORLD_CHAIN` | `480` | World Chain's chain id. | | `CHALLENGE_TTL` | `120` | A challenge's life, in seconds. | | `DEPTH` | `3` | How many blocks deep V3 reads a name's records. | | `CACHE_TTL` | `60` | How long a deep read is kept, in seconds. Revocation is always read from the latest block. | | `RECORDS` | `knock.v`, `knock.key`, `knock.status`, `knock.nullifier`, `knock.poh` | The records V3 reads. | ## Functions Each check is a function you can call on its own, for tests or for a verifier of your own design. | Function | What it does | | --- | --- | | `parse(&Value)` | V1: a knock, whole and well formed, `s` low. | | `ok_name(&str)` | A name V1 takes: lowercase labels of `a`–`z`, `0`–`9` and `-`, at least two. | | `ok_origin(&str)` | An origin V1 takes: `https://host[:port]`, or `http://localhost` or `http://127.0.0.1`, with or without a port. | | `pass_digest(challenge, origin, node, counter, mode, nullifier, human_ref)` | The `KnockPass` digest, as the Wedgie makes it. | | `signed_by(&VerifyingKey, digest, r, s)` | V4: the signature is the key's, over the digest, `s` low. | | `parse_key(&str)` | A `knock.key`: 33 bytes, compressed P-256, as hex. Returns the bytes, the key and its hash. | | `check_records(name, &deep, &latest)` | V3, given the records 3 blocks deep and at the latest block. | | `check_binding(Option<&Receipt>, &Wanted)` | V6, given the binding's receipt. | | `accepted_binding()` | The KnockBinding whose bindings count: v3, production, from the repo's deployments. | ## Testing with a stand-in chain The crate's own tests run every check against a `Chain` that answers from memory, in [`crates/knock-verify/src/tests.rs`](https://github.com/getknock/knock/blob/main/crates/knock-verify/src/tests.rs). Do the same in yours: return fixed `Records` and a `Receipt`, open the store with `":memory:"`, and feed it the signed knock from [`spec/vectors/v1.json`](https://github.com/getknock/knock/blob/main/spec/vectors/v1.json). # Checks and refusals > The eight checks a verifier runs, in order, and every refusal each can give, with its status code and what to tell the person knocking. A verifier runs eight checks, in order, and the first one that fails gives the answer. Every check before it passed, and none after it ran. A refused knock uses up nothing: its challenge, the Wedgie's counter and the entry stay as they were. Each refusal has a `check`, an `error` code for your code, and a `why` for people. The `why`s below are exact, with the parts that vary in angle brackets. ## Check 1: well formed The knock is whole and well formed, before anything is read from a chain. | `error` | ``why`` | Means | | --- | --- | --- | | `bad_assertion` | `not a knock:
` | Missing or extra fields, or wrong types. | | `bad_assertion` | `version : this verifier reads 1` | A format this verifier doesn't read. | | `bad_assertion` | `name: a-z, 0-9 and "-", with dots between` | Not a name a Wedgie can have. | | `bad_assertion` | `deviceNode isn't the name's namehash` | The name and its node don't match. | | `bad_assertion` | `origin: https://host[:port], or http://localhost` | Not an origin a Wedgie knocks for. | | `bad_assertion` | `counter: from 1 up` | A counter of 0, or too large. | | `bad_assertion` | `mode: 1 (Bound) or 2 (Fresh)` | No such mode. | | `bad_assertion` | `signature: r and s, s low` | A zero, or a high `s`: every Wedgie signs with a low `s`. | | `bad_assertion` | `the body is {assertion, action?}` | `knock-verifier` only: the request body isn't that, or is over 16 KiB. | | `bad_assertion` | `action: a-z, 0-9 and "-", up to 32` | `knock-verifier` only: not an action name. | Status 400. These come from your code, or from someone sending made-up knocks, never from a Wedgie and the Door. Log them, and show "Something went wrong. Try again." ## Check 2: for this site, just now The challenge is one this verifier gave out, unused, under 120 seconds old, and for its origin. The knock is for its origin too. | `error` | ``why`` | Means | | --- | --- | --- | | `bad_challenge` | `not a challenge this verifier gave out` | Made up, from another verifier, or the verifier's database was replaced. | | `bad_challenge` | `that challenge was used already` | A knock with this challenge already got in. | | `bad_challenge` | `that challenge expired: knock again` | More than 2 minutes passed between the challenge and the check. | | `bad_challenge` | `that challenge was used or expired meanwhile` | Two checks of the same knock at once, and the other one won. | | `wrong_origin` | `the knock is for , not ` | The page that knocked isn't at `VERIFIER_ORIGIN`. | | `wrong_origin` | `a challenge for another origin` | The challenge came from a verifier for another origin, sharing this database. | | `busy` | `too many challenges out at once: try again in a minute` | From `POST /challenge`: 10,000 challenges are outstanding. Status 429. | Status 403, except `busy`. Tell people to knock again. If it's `wrong_origin`, check `VERIFIER_ORIGIN` against the address bar, port included: `localhost` and `127.0.0.1` are different origins. ## Check 3: named, and switched on The name has Knock's records in ENS on mainnet, with `knock.status` set to `active`, and they're the same 3 blocks deep as at the latest block. A revoke counts at the latest block. | `error` | ``why`` | Means | | --- | --- | --- | | `no_name` | ` has no Knock records in ENS: never named, or killed` | No records, or the name expired. | | `revoked` | ` is revoked: its owner turned it off` | `knock.status` is `revoked`. | | `revoked` | `this Wedgie's key was seen revoked, and stays revoked` | This verifier saw the key revoked before. Turning it back on doesn't help here. | | `not_knock` | ` isn't a Knock name (knock.v isn't 1)` | The name has records, but not Knock's. | | `records_changing` | `'s Knock records changed in the last few blocks: try again in a minute` | The name was set up or changed less than about 36 seconds ago. | | `not_active` | `'s knock.status isn't active` | Any status but `active` or `revoked`. | | `bad_records` | `'s knock.key isn't a compressed P-256 key` | | | `bad_records` | `'s knock.nullifier isn't a word` | Not `0x` and 64 hex digits. | | `bad_records` | `'s knock.poh isn't eip155:480:tx:` | | | `unavailable` | `couldn't read the chain, try again: mainnet: ` | The mainnet RPC failed. Status 503. | Status 403, except `unavailable`. Show the `why`: it names the name, and says what its owner can do. ## Check 4: signed by its Wedgie The Wedgie whose key is in `knock.key` signed this knock, for this verifier's origin, this challenge and this name. The verifier makes the digest from its own origin and the knock's fields, so a knock signed for another site fails here even if its `origin` field was changed. | `error` | ``why`` | Means | | --- | --- | --- | | `bad_signature` | `not signed by 's key, for this site and challenge` | Made up, changed on the way, or signed by another key. | Status 403. A real Wedgie and the Door never give this, since the Door checks the signature before it sends a knock. Treat it as an attempt, and show "This knock didn't come from that Wedgie." ## Check 5: not seen before The Wedgie's counter is higher than in any knock of this key's that this verifier has let in. | `error` | ``why`` | Means | | --- | --- | --- | | `replayed` | `the Wedgie's counter didn't rise: a knock seen before` | A knock sent again, or an older one. | Status 403. Tell people to knock again: a new knock has a higher counter. ## Check 6: one human, bound The knock's human is the name's, and the binding `knock.poh` names is a successful transaction on KnockBinding v3, production, with a `Bound` log for this key, this human and this name. Its World ID proof is no older than `VERIFIER_MAX_BINDING_DAYS`, and for a strict verifier, its block is final enough. | `error` | ``why`` | Means | | --- | --- | --- | | `bad_assertion` | `humanRef: zero in mode 1` | A mode 1 knock with a `humanRef` that isn't zero. | | `wrong_human` | `the knock's human isn't 's` | The knock's nullifier isn't the one in `knock.nullifier`. | | `no_binding` | `World Chain has no transaction ` | `knock.poh` names a transaction World Chain doesn't have. | | `no_binding` | `the binding's transaction failed` | It reverted. | | `no_binding` | `transaction holds no Bound from Knock's KnockBinding for this key, human and name` | Another contract, an older KnockBinding, staging's, or a binding for another key, human or name. | | `binding_not_final` | `this site is strict: it takes a binding once World Chain has posted it to Ethereum, which it does every ~6 minutes. Try again in a few minutes` | Strict, safe: the binding is newer than World Chain's safe head. | | `binding_not_final` | `this site is strict: it takes a binding once World Chain's post of it to Ethereum is final, ~20–25 minutes after binding. Try again later` | Strict, finalized. | | `binding_expired` | `the binding is more than days old: bind the Wedgie again` | Its World ID proof is older than the verifier allows. | | `unavailable` | `couldn't read the chain, try again: World Chain: ` | The World Chain RPC failed. Status 503. | Status 403, except `unavailable`. For `binding_not_final` and `binding_expired`, show the `why`. For the others, the Wedgie's owner should set it up again in the Knock Console. ## Check 7: a mode it offers | `error` | ``why`` | Means | | --- | --- | --- | | `mode_unsupported` | `Fresh (mode 2) isn't offered yet` | A mode 2 knock. | Status 403. Check 7 runs just before check 6, so a mode 2 knock gets this answer whatever its binding. ## Check 8: first time Only when the request names an `action`: this human hasn't done it here before. | `error` | ``why`` | Means | | --- | --- | --- | | `already_entered` | `this human has done here already` | The human's nullifier is in this action's entries, from any of their Wedgies. | Status 403. Tell them they've already done it. [One human, once](../guides/one-human-once.md) covers actions. ## The verifier's own trouble | `check` | `error` | `why` | Status | | --- | --- | --- | --- | | 0 | `busy` | `the verifier's limit for now: try again later` | 429 | | 0 | `internal` | `the verifier's store: ` | 500 | `busy` comes from the verifier's hourly limits, 600 challenges and 600 checks per process. `internal` means its SQLite store failed. Neither says anything about the knock. ## Every code | `error` | Checks | Status | | --- | --- | --- | | `bad_assertion` | 1, 6 | 400 at check 1, 403 at check 6 | | `bad_challenge` | 2 | 403 | | `wrong_origin` | 2 | 403 | | `busy` | 0, 2 | 429 | | `no_name` | 3 | 403 | | `revoked` | 3 | 403 | | `not_knock` | 3 | 403 | | `records_changing` | 3 | 403 | | `not_active` | 3 | 403 | | `bad_records` | 3 | 403 | | `bad_signature` | 4 | 403 | | `replayed` | 5 | 403 | | `wrong_human` | 6 | 403 | | `no_binding` | 6 | 403 | | `binding_not_final` | 6 | 403 | | `binding_expired` | 6 | 403 | | `mode_unsupported` | 7 | 403 | | `already_entered` | 8 | 403 | | `unavailable` | 3, 6 | 503 | | `internal` | 0 | 500 | # Protocol > What a Wedgie signs, how a knock's digest is made, the ENS records and the World Chain binding a verifier reads, and the test vectors. You don't need this page to add Knock to a site. It's for writing a verifier of your own, checking one, or knowing exactly what a knock commits to. ## Signed messages Every signature a Wedgie makes is over an EIP-712 digest it builds itself, from fields it shows or holds. It never signs bytes a computer hands it. The domain is the same for every message, with no chain and no contract: ```ts const DOMAIN = { name: 'Knock', version: '1' } ``` The digest is EIP-712's: `keccak256(0x1901 ‖ domainSeparator ‖ hashStruct(message))`. | Message | Signed when | Fields | | --- | --- | --- | | `KnockHello` | The Console checks a Wedgie's key | `bytes32 challenge` | | `KnockEnroll` | A Wedgie is named | `bytes32 nonce`, `string name`, `bytes key` | | `KnockBind` | A Wedgie asks to be bound to a human | `uint256 chainId`, `address registry`, `uint256 nullifier`, `uint64 expiresAtMin`, `bytes key` | | `KnockPass` | A knock | See below | ### KnockPass ```solidity struct KnockPass { bytes32 challenge; string origin; bytes32 deviceNode; uint64 counter; uint8 mode; uint256 nullifier; bytes32 humanRef; } ``` | Field | What the Wedgie puts in it | | --- | --- | | `challenge` | The site's challenge, from the Door. | | `origin` | The site's origin, from the Door, as the browser gave it. The Wedgie shows it on screen first. | | `deviceNode` | The ENS namehash of the name it knocks as, after ENSIP-15 normalization. | | `counter` | Its knock counter, raised for this knock. It starts at 1 and rises with every knock, on any site. | | `mode` | `1`, Bound. `2`, Fresh, is defined but not offered. | | `nullifier` | The human's nullifier, from its binding. | | `humanRef` | 32 zero bytes in mode 1. | A verifier rebuilds the digest from its own origin and challenge, and the knock's other fields. It never takes the digest from the knock. ## Signatures and keys - **Curve:** P-256 (secp256r1), ECDSA over the 32-byte digest as it is, with no second hash. - **Signatures:** `r` and `s`, 32 bytes each, with `s` at most half the curve order. A high `s` is refused. - **Keys:** `knock.key` is the 33-byte compressed point, `0x02` or `0x03` and then X, in lowercase hex. - **Key hash:** `keccak256(X ‖ Y)` over the 64-byte uncompressed coordinates. This is how a `Bound` log names a key. The key is made on the Wedgie's OPTIGA Trust M chip, and stays there. ## ENS records A Wedgie's name, such as `wedgie.itzraj.eth`, holds these records on Ethereum mainnet. Verifiers read the first five, through ENS's Universal Resolver. | Record | Value | Read by verifiers | | --- | --- | --- | | `knock.v` | `1` | Yes | | `knock.key` | The compressed P-256 key, `0x` and 66 hex digits | Yes | | `knock.status` | `active` or `revoked` | Yes | | `knock.nullifier` | The human's nullifier, `0x` and 64 hex digits | Yes | | `knock.poh` | The binding: `eip155:480:tx:0x` and the transaction hash | Yes | | `knock.sc` | The Wedgie's security class: `S1` when any app installed on the Wedgie can use its chip | No | | `addr` | The owner's wallet | No | `knock.att`, the chip maker's attestation of the key, is reserved and not written yet. A verifier takes records that are `active` and the same 3 blocks deep as at the latest block, and reads `revoked` at the latest block. An expired name has no resolver, so its records read as empty. ## The binding A binding is a successful transaction on KnockBinding v3 on World Chain (chain 480). The contract checks a World ID proof whose signal commits to the Wedgie's key and its name's node, and then logs: ```solidity event Bound(uint256 indexed nullifier, bytes32 indexed keyHash, bytes32 indexed node, uint64 expiresAtMin); ``` | | Production | Staging | | --- | --- | --- | | KnockBinding v3 | `0x7748D2787DcB6E6024968edC19fcA13ccAbA1699` | `0x5248D9EE577311D8f0e87bF3Ca2EE990a7ce46E7` | | Takes proofs from | World App | World ID's Simulator | | Counts for verifiers | Yes | No | A verifier takes a binding when the transaction succeeded, and a log from the production address has `Bound`'s signature and exactly this nullifier, key hash and node as its topics. `expiresAtMin` tells it roughly when the proof was made, which it holds to `VERIFIER_MAX_BINDING_DAYS`. Bindings from KnockBinding v1 and v2 never count. [`contracts/deployments/480.json`](https://github.com/getknock/knock/blob/main/contracts/deployments/480.json) lists every deployment. ## Names and origins | | Taken | Refused | | --- | --- | --- | | Names | Two or more labels of `a`–`z`, `0`–`9` and `-`, up to 63 characters each, 255 in all | Capitals, `_`, empty labels, `--` in the third and fourth characters | | Origins | `https://host` or `https://host:port`, lowercase. `http://localhost` and `http://127.0.0.1`, with or without a port. | Paths, `user@`, other `http://` hosts, capitals | ## Test vectors [`spec/vectors/v1.json`](https://github.com/getknock/knock/blob/main/spec/vectors/v1.json) has a published test key, and every message above with its type string, struct hash, digest and signature, including a whole knock. Both the TypeScript and Rust sides are tested against it. The key is public, so never use it for anything real. ## @knock-dev/protocol The TypeScript side of all this, in [`packages/protocol`](https://github.com/getknock/knock/blob/main/packages/protocol/src/index.ts): | Export | What it is | | --- | --- | | `DOMAIN`, `TYPES`, `KINDS`, `DOMAIN_SEPARATOR` | The EIP-712 domain and types. | | `digest(kind, message)`, `structHash(kind, message)`, `typeString(kind)` | A message's digest, struct hash and type string. | | `deviceNode(name)` | The namehash of a name, after normalization. | | `compressKey(x, y)`, `uncompressKey(key)`, `keyHash(x, y)` | Key formats, and the key hash `Bound` uses. | | `verifySignature(digest, r, s, key)` | A Wedgie's signature over a digest, `s` low. | | `signDigest(digest, secretKey)`, `publicKeyOf(secretKey)` | Signing in software, for tests and vectors. Wedgies sign in their chip. | | `hashToField(bytes)`, `signalHash(signal)`, `bindSignal(key, node)` | World ID's hashing, and a binding's signal. | | `MODE`, `NO_HUMAN_REF`, `P256_N` | `{ bound: 1, fresh: 2 }`, 32 zero bytes, and P-256's order. | | `ACTION`, `ACTION_HASH` | The World ID action in the test vectors, `knock-bind`, and its hash. Each KnockBinding names its own action in the deployments. | # Host Knock yourself > Every part of Knock can run on your own machines. What each part is, what it needs, and the one thing that belongs to whoever runs a network. Nothing in Knock has to run on Knock's servers. Every part is open source and builds from [`getknock/knock`](https://github.com/getknock/knock), and most of them are static files or a package. ## The parts | Part | What it is | Who runs it | To host it | | --- | --- | --- | --- | | [The Door](door.md) | The popup that talks to the Wedgie over USB | Knock, or any site | Static files on HTTPS | | [The Console](console.md) | Where a person names and binds their Wedgie | Knock, or anyone | Static files on HTTPS, and a knockd | | [knockd](knockd.md) | Relays bindings to World Chain, answers lookups, and signs World ID requests | Knock, or anyone | A small Rust server, a World Chain RPC and a little ETH there | | A verifier | Checks knocks for one site | Each site | [`@knock-dev/verify`](../guides/verify-in-your-server.md) in your server, or [`knock-verifier`](../guides/verifier.md) beside it | | [The Human Club](human-club.md) | The demo site: petitions and fundraisers for humans | Knock, or anyone | Vercel and Turso, or any Node host | | These docs | This site | Knock, or anyone | Static files: `bun run build` in [`getknock/docs`](https://github.com/getknock/docs), then serve `out/` | | The Knock app | Runs on the Wedgie | Its owner | Installed from a checkout of [`getknock/wedgie`](https://github.com/getknock/wedgie) with `wedgie.py install .`, or from wedgie.dev as an app from that repo | ## What doesn't need Knock at all A Wedgie that's already set up never needs Knock's servers. Its records are in ENS on Ethereum mainnet, its binding is on World Chain, and its knocks go from the Door to your verifier, which reads both chains through RPCs of your choosing. If Knock's machines all went away tomorrow, every bound Wedgie would keep knocking, on every site that runs a verifier, through any copy of the Door, until its binding is older than a site allows (a year, by default). What does go through a server is setting a Wedgie up: binding it to a human needs a World ID proof, and a World ID proof request has to be signed by the app that asks for it. ## One RP key per network World ID only answers requests signed by an app's RP key, the key World's registry on World Chain holds for that app. For Knock's network, that's Knock's key, and only Knock's knockd holds it. So: - **On Knock's network** you can run everything yourself, a knockd included, except the signature on World ID requests. Your Console gets those from Knock's knockd, as it does unless you set `NEXT_PUBLIC_KNOCKD_RP`, and does the rest with yours: [Run knockd](knockd.md) says how. - **On a network of your own** you hold the RP key: your own World ID app, your own KnockBinding, your own knockd. [Run a network of your own](own-network.md) walks through it. Sites take your bindings only if they choose to, and your humans' nullifiers differ from their nullifiers on Knock's network. ## Choosing what to host - **A site that takes knocks** runs a verifier. That's all it needs. It can also host its own copy of [the Door](door.md), if it wants nothing of Knock's in the knock's path. - **A person who doesn't want to depend on Knock's Console** hosts [the Console](console.md) and, to relay their own bindings, [knockd](knockd.md). - **A community that wants its own network** of humans, separate from Knock's, follows [Run a network of your own](own-network.md). # Host the Door > The Door is static files. Build it, serve it on HTTPS, and point door-sdk at it with one option. The Door is the popup door-sdk opens when someone knocks. It talks to the Wedgie over Web Serial, hands it your challenge and your origin, and hands back what the Wedgie signed. It calls no server and holds no keys, so any static host can serve it. Knock's is at `https://door.getknock.dev`, and door-sdk opens it unless told otherwise. You might host your own to keep Knock's machines out of the path of your knocks, or to serve it from your own domain. ## Build it From a checkout of `getknock/knock`: ```bash bun install cd apps/door bun run build ``` `apps/door/out/` is now the whole site: the Door at `/knock`, and the [Console](console.md) at `/`, from one build. Serve both or only the Door: the Door doesn't need the Console. ## Serve it Put `out/` on any static host that serves HTTPS: - **Vercel**: import your copy of `getknock/knock` and set the project's Root Directory to `apps/door`. [`apps/door/vercel.json`](https://github.com/getknock/knock/blob/main/apps/door/vercel.json) sets the rest: the install at the repo's root, then `bun run build`. - **Netlify or Cloudflare Pages**: set the project's root to `apps/door`, the build command to `bun run build`, and the output folder to `out`. - Each of them serves `/knock` from `knock.html` as it is. - **Your own server**: copy `out/` there and have `/knock` serve `knock.html`. With nginx, `try_files $uri $uri.html $uri/ =404;` in the site's `location /`. It must be HTTPS, because browsers only offer Web Serial to secure pages. `http://localhost` counts as secure, for development. Knock serves its Door and its Console from one Vercel project with two domains, `door.getknock.dev` and `getknock.dev`. `vercel.json` sends `door.getknock.dev/` to the Console, so each address shows one thing. ## Point door-sdk at it door-sdk opens `/knock` at the address you give it as `door`: ```ts title="knock-button.ts" import { knock } from '@knock-dev/door-sdk' declare const challenge: `0x${string}` export const assertion = await knock({ challenge, door: 'https://door.club.example' }) ``` That's the only change. door-sdk sends its request to that origin only, and listens to the popup it opened, at that origin, and to nothing else. ## What changes for people who knock - **Their browser asks again for the Wedgie's port.** Web Serial permission is per site, so the first knock through a new Door asks them to pick the Wedgie, as the first knock through any Door does. - **Their knocks are the same.** The Wedgie signs your origin, not the Door's, and your verifier never learns which Door was used. ## Serve the Door as it's built The Door tells the Wedgie which site is asking. Knock's Door takes that from the browser, the origin of the page that opened it, never from anything the page says. A Door changed to lie could ask a Wedgie to knock for another site, and the only thing that would stop it is the person reading the Wedgie's screen before pressing green. So serve the Door built from Knock's code, unchanged, and say whose Door it is: people should know whose page they're giving the Wedgie's port to. The [security](../security.md) page has more on what the Wedgie's screen is for. # Host the Console > The Console is static files, built with the knockds it talks to. Its settings, and what it needs from the person using it. The Knock Console is where a person sets up their Wedgie: they choose its name, bind it to themselves with World ID, name it in ENS, and kill it if it's lost. It runs in the browser. Everything it writes, it writes through the person's own wallet or through a knockd. It holds no keys of its own. ## Build it The Console and the [Door](door.md) are one app. Set the Console's settings as you build, from a checkout of `getknock/knock`: ```bash bun install cd apps/door NEXT_PUBLIC_KNOCKD=https://knockd.example \ NEXT_PUBLIC_ETH_RPC=https://your-mainnet-rpc.example \ bun run build ``` Then serve `apps/door/out/` on HTTPS, as [Host the Door](door.md#serve-it) says. The Console is at `/`. On Vercel, set these settings in the project's environment variables, for Production, before it builds. With no settings at all, it's a Console on Knock's network that uses Knock's knockd for everything. That example relays bindings through your own knockd and still gets its World ID requests signed by Knock's, which is the only one that can sign them. ## Settings They're read when it's built, so build again to change one. | Setting | Default | What it does | | --- | --- | --- | | `NEXT_PUBLIC_KNOCKD` | Knock's, `https://knockd.getknock.dev`; on localhost, `http://127.0.0.1:8787` | The knockd the Console relays bindings through and looks them up with: Knock's, or [your own](knockd.md). | | `NEXT_PUBLIC_KNOCKD_RP` | Knock's, `https://knockd.getknock.dev`; on localhost, `http://127.0.0.1:8787` | The knockd that signs World ID requests. On Knock's network that's Knock's, the only one that holds Knock's World ID key. On [a network of your own](own-network.md), yours. | | `NEXT_PUBLIC_ETH_RPC` | `https://ethereum-rpc.publicnode.com` | A mainnet RPC, for reading ENS when no wallet is connected, and for simulating transactions the wallet's own RPC won't. | | `NEXT_PUBLIC_DOCS` | these docs | Where its links to the docs go. | | `NEXT_PUBLIC_CLUB` | the Human Club | Where its link to the Human Club goes. | Each knockd the Console calls has to let the Console's origin in: its `KNOCKD_ORIGINS` lists it, or is `*`, as Knock's is. The KnockBindings it binds with, staging's and production's, come from `contracts/deployments/480.json` in the checkout it's built from. Knock's are there. A network of your own puts its own there before building. ## What the person needs - **A desktop Chromium browser**, such as Chrome, Edge, Brave or Arc, for Web Serial. - **Their Wedgie**, running the Knock app, plugged in. - **World App** on their phone, to prove they're a human. The Console's Simulator option is for testing, on staging. - **A wallet in the browser** that holds, or can make, the ENS name: an extension that announces itself (EIP-6963). Naming is mainnet transactions, signed and paid for in their wallet. ## What it calls | What | Why | | --- | --- | | The person's wallet | ENS reads and the naming and kill-switch transactions, on mainnet. | | `NEXT_PUBLIC_ETH_RPC` | ENS reads with no wallet connected, and simulations. | | `NEXT_PUBLIC_KNOCKD_RP` | `/rp-context`: World ID requests, signed. | | `NEXT_PUBLIC_KNOCKD` | `/health`, `/bind`, `/bound`, `/binding` and `/human`. | | World App, through World's relay | The World ID proof, after the person scans the QR on the Wedgie. | Nothing else: no analytics, and nothing of Knock's beyond the knockds you name. # Run knockd > knockd relays bindings to World Chain and answers the Console's lookups. Build it, give it a World Chain RPC and a funded key, and point a Console at it. knockd is the small server the [Console](console.md) needs while a person sets up a Wedgie. It does three things: - **It relays bindings.** When a Wedgie says yes to being bound to a human, knockd checks the Wedgie's signature and the human's World ID proof, then sends the binding to KnockBinding on World Chain and pays its gas. - **It answers lookups.** It reads a binding back from its transaction, finds a Wedgie's latest binding by its key, and finds a returning human's World ID session. - **It signs World ID requests**, if it holds its network's RP key. Only one knockd per network does, as [Host Knock yourself](overview.md#one-rp-key-per-network) explains. It never sees a knock. Knocks go from the Door to a site's verifier, and knockd isn't in that path. ## What you need - **Rust**, stable, and a checkout of [`getknock/knock`](https://github.com/getknock/knock). - **A World Chain RPC URL.** A public one works for trying it out. A provider's is better for anything people use: knockd reads every binding so far when it starts, in many requests at once, and public RPCs limit those. - **A gas key**: a private key whose address holds a little ETH on World Chain (chain 480). It pays for every binding knockd sends. Make a new key for this and use it for nothing else. - **HTTPS in front of it.** Browsers won't let a Console on HTTPS call a knockd on plain HTTP, except on localhost. ## Build it From the repo's root: ```bash cargo build --release -p knockd ``` The binary is `target/release/knockd`. It reads its settings from the environment and from a `.env` file in the folder it runs in, so run it from the folder that holds that file. ## Set it up Make a `.env` beside where it runs: ```bash title=".env" WORLD_RPC=https://worldchain-mainnet.g.alchemy.com/public RELAYER_WALLET_PRIV_KEY=0x… WORLD_RP_KEY_FILE=none KNOCKD_ORIGINS=https://console.example KNOCKD_ADDR=127.0.0.1:8787 ``` `WORLD_RP_KEY_FILE=none` makes a knockd that relays and looks up, and signs nothing: the right one for Knock's network, whose RP key is Knock's. Keep `.env` out of git, and readable only by the user knockd runs as (`chmod 600 .env`). Then start it: ```bash ./target/release/knockd ``` Before it serves anything, it checks World Chain: that `WORLD_RPC` is chain 480, and that each KnockBinding it will send to is version 3, proves the action its deployments say, and answers to World ID's verifier. If anything's off, it says what and stops. When it's up, it logs `knockd on http://127.0.0.1:8787` and reads World Chain's bindings so far in the background. Check it from another terminal: ```bash curl -s http://127.0.0.1:8787/health ``` ```json {"KnockBinding":{"production":"0x…","staging":"0x…"},"action":{"production":"knock-human","staging":"knock-human"},"chainId":480,"ok":true,"signs":false} ``` `signs` is `false` for a knockd without an RP key. ## Fund the gas key knockd pays for each binding it sends: about 415,500 gas, sent only after a simulation shows it will succeed, so a binding that would fail costs nothing. Gas on World Chain is cheap. When this was written, a binding cost under a millionth of an ETH, so a little ETH lasts a long time. To fund it, send ETH on World Chain to the gas key's address, from an exchange that withdraws to World Chain or over a bridge. Check the balance now and then. When it runs out, bindings fail as `chain`, "sending failed", and knockd's log says why. ## Settings | Setting | Default | What it does | | --- | --- | --- | | `WORLD_RPC` | none, needed | A World Chain RPC URL. It's never logged, and knockd leaves URLs out of the errors it sends back, since a provider's URL often carries its key. | | `RELAYER_WALLET_PRIV_KEY` | none, needed | The gas key, `0x` and 64 hex digits. It pays for the bindings knockd sends. | | `WORLD_RP_KEY_FILE` | `knock-worldchain-key.json` | The World ID RP key's file, or `none`. If the default file isn't there, knockd starts without signing, and says so. If a file you named isn't there, it stops. | | `WORLD_RP_KEY` | none | The RP key itself, `0x` and 64 hex digits, for hosts that hand secrets over as environment variables, such as Fly.io. Set this or `WORLD_RP_KEY_FILE`, not both. | | `WORLD_APP_ID` | none | The World ID app's id, `app_…`. Needed only with an RP key. | | `WORLD_RP_ID` | none | The app's RP id, `rp_` and 16 hex digits. Needed only with an RP key. | | `KNOCKD_ORIGINS` | `http://localhost:3000,http://127.0.0.1:3000` | The Consoles that may call it from a browser, separated by commas, each exactly as browsers send it. `*` lets any Console call it. | | `KNOCKD_ADDR` | `127.0.0.1:8787` | Where it listens. | | `KNOCKD_ENV` | `.env` | The file it reads settings from. If it isn't there, knockd uses the environment alone. | | `KNOCK_DEPLOYMENTS` | the one built in | A deployments file: the KnockBindings it sends to and looks up in. Only [a network of your own](own-network.md) needs it. | | `KNOCKD_SKIP_CHECKS` | not set | Set to anything to skip the checks on World Chain at start. For development only. | The RP key file is JSON with one field, `{"privateKey": "0x…"}`. Keep it beside `.env`, never in git, readable only by knockd's user. ## Put it behind HTTPS knockd speaks plain HTTP on the address it listens on. Keep that address on `127.0.0.1` and put a proxy that serves HTTPS in front of it. With Caddy, which gets the certificate itself: ```text title="Caddyfile" knockd.example { reverse_proxy 127.0.0.1:8787 } ``` With nginx, use your certificate and `proxy_pass http://127.0.0.1:8787;` in the site's `location /`. knockd answers CORS itself, for the origins in `KNOCKD_ORIGINS`. Don't add CORS headers in the proxy. ## Keep it running knockd is one process with nothing on disk: what it knows about bindings, it reads again from World Chain when it starts. Run it with whatever keeps your services up. With systemd: ```ini title="/etc/systemd/system/knockd.service" [Unit] Description=knockd After=network-online.target Wants=network-online.target [Service] User=knockd WorkingDirectory=/opt/knockd ExecStart=/opt/knockd/knockd Restart=on-failure [Install] WantedBy=multi-user.target ``` Put the binary, `.env` and, if it has one, the RP key file in `/opt/knockd`, owned by the `knockd` user. Then `systemctl enable --now knockd`. ## On Fly.io [Fly.io](https://fly.io) runs knockd from the repo's [`fly.toml`](https://github.com/getknock/knock/blob/main/fly.toml) and [its Dockerfile](https://github.com/getknock/knock/blob/main/server/knockd/Dockerfile), and serves it on HTTPS itself, so there's no proxy to set up. With [flyctl](https://fly.io/docs/flyctl/install/) installed and signed in, work from the root of your checkout: the image builds from the whole Rust workspace. 1. **Make the app.** Pick a name, put it in `fly.toml` as `app`, and create it: ```bash fly apps create your-knockd ``` `fly.toml` puts it in Singapore (`primary_region = "sin"`). Change that to the [region](https://fly.io/docs/reference/regions/) nearest the people who'll use your Console. 2. **Give it its secrets.** Fly keeps them encrypted and hands them to knockd as environment variables. To keep them out of your shell's history, import them from the `.env` you made above: ```bash grep -E '^(WORLD_RPC|RELAYER_WALLET_PRIV_KEY)=' .env | fly secrets import ``` A knockd that signs for a network also needs its RP key and its app's ids. Import those too, the key as `WORLD_RP_KEY`: ```bash grep -E '^(WORLD_APP_ID|WORLD_RP_ID)=' .env | fly secrets import printf 'WORLD_RP_KEY=%s\n' "$(jq -r .privateKey our-rp-key.json)" | fly secrets import ``` 3. **Set who may call it.** `fly.toml` sets `KNOCKD_ORIGINS` to `*`, which suits a knockd that signs for its network, since every Console on it asks for signatures there. For a knockd only your own Console uses, set your Console's origin instead. 4. **Deploy it**, as one machine: ```bash fly deploy --ha=false ``` One machine is enough, and keeps one count of the hourly limits. The root's `.dockerignore` lets only the Rust workspace into the build, so `.env` and key files stay on your machine. 5. **Check it:** ```bash curl -s https://your-knockd.fly.dev/health ``` `signs` is `true` if you gave it an RP key. `fly logs` shows its log. 6. **Give it your domain**, if you like: `fly certs add knockd.example`, then add the DNS records it prints, such as a `CNAME` to `your-knockd.fly.dev`. On Cloudflare, leave that record unproxied, so Fly can get its certificate. To change a setting later, `fly secrets set` or `fly secrets import` again: the machine restarts with it. To update knockd, pull and `fly deploy --ha=false` again. ## Point a Console at it Build the [Console](console.md) with `NEXT_PUBLIC_KNOCKD` set to your knockd's address, and `NEXT_PUBLIC_KNOCKD_RP` set to the knockd that signs for your network. Add the Console's origin to `KNOCKD_ORIGINS`. ## What it answers Every answer is JSON. A no is `{"error", "why"}` with an HTTP status, `why` in words a person can read. | Route | What it does | | --- | --- | | `GET /health` | Whether it's up, whether it `signs`, and the KnockBindings and actions it uses. | | `POST /rp-context` | A signed World ID request: `{ environment, session }` in, `{ app_id, action, rp_context }` out. Without an RP key it answers 501, `no_rp_key`. | | `POST /bind` | Checks a binding and sends it: the Wedgie's signature, then the human's proof in a simulation, then the transaction. | | `POST /bound` | A binding as World Chain has it, from its transaction. | | `POST /binding` | A Wedgie's latest binding, by its key. | | `POST /human` | A human's World ID session, found by their nullifier, a session, a Wedgie's key or a name. | `environment` is `production` (World App) or `staging` (World's Simulator, for testing). ### Its limits So a busy or hostile caller can't spend its gas or its RPC, knockd allows, across everyone, in any hour: | What | How many | | --- | --- | | Signed requests | 120 | | Bindings simulated | 120 | | Bindings sent | 12 | | Lookups | 600 | Past a limit it answers 429, `busy`, until the hour's oldest has aged out. Requests can be at most 16 KiB. ## What it keeps Nothing about who asked: it logs no IP addresses, and keeps no records on disk. It logs what it did, such as a request signed or a binding sent with its transaction, and errors with their URLs left out. The gas key, the RP key and the RPC URL are never logged or sent back. # Run a network of your own > Your own World ID app, your own KnockBinding on World Chain, and your own knockd, for humans who bind on your network instead of Knock's. On Knock's network, Knock's World ID app proves who's human, and only Knock's knockd can sign its requests. A network of your own replaces that part with your own World ID app, KnockBinding and knockd, which holds your app's key. Everything else is the same code, and the Wedgie needs nothing new. Most people don't need this. To keep Knock's servers out of the path, [host the parts](overview.md) on Knock's network. Run a network when you want humans to be proved by an app you control, for example a community that won't depend on Knock for setting up a Wedgie. ## What changes, and what it costs - **Sites choose to take it.** A verifier takes Knock's KnockBinding unless it's told to take yours. A Wedgie bound on your network is refused at check 6 by sites that haven't opted in. - **A human has a different nullifier there.** World ID gives each app and action its own nullifier for a person, so nobody can link a human on your network to the same human on Knock's. A site that takes both networks could let one human in once on each. - **A Wedgie knocks on one network at a time.** Its name's records point at one binding, so it's on Knock's network or on yours, not both. - **You run the signing knockd**, keep its RP key safe, and pay for bindings in ETH on World Chain. ## What you need - A checkout of [`getknock/knock`](https://github.com/getknock/knock), with [Rust](https://rustup.rs), [Bun](https://bun.sh) and [Foundry](https://getfoundry.sh). - A World Chain RPC URL, and a key with a little ETH on World Chain to deploy with and to pay for bindings, as in [Run knockd](knockd.md#fund-the-gas-key). - An account in [World's Developer Portal](https://developer.world.org). ## 1. Make a World ID app In the Developer Portal: 1. **Create an app.** Note its id, `app_…`. 2. **Set up World ID 4.0 for it with a managed RP.** The portal registers your app's RP on World Chain, in World's production registry and its staging registry, and gives you the RP's signing key once. Note the RP's id, `rp_` and 16 hex digits, and save the key in a file of its own: ```json title="our-rp-key.json" { "privateKey": "0x…" } ``` Keep this file out of git and readable only by you (`chmod 600 our-rp-key.json`). It's the key World ID trusts for your app: whoever holds it can ask World App for proofs in your app's name. 3. **Create the action first bindings prove**, in production, and in staging too to test with World's Simulator. Give it a name of its own, such as `our-human`. A human proves it once, when their first Wedgie is bound. After that, their Wedgies are bound with the World ID session that proof started. ## 2. Deploy your KnockBinding KnockBinding is the contract on World Chain that holds your network's bindings. It has no owner and can't be upgraded: once deployed, nobody can change it, you included. Each one names your RP and your action, and answers to one of World's verifiers, production's or staging's, so you deploy two. Put your settings in `.env` at the repo's root: ```bash title=".env" WORLD_RPC=https://worldchain-mainnet.g.alchemy.com/public RELAYER_WALLET_PRIV_KEY=0x… WORLD_APP_ID=app_… WORLD_RP_ID=rp_… ``` Then, from `contracts/`: ```bash cd contracts forge soldeer install set -a; . ../.env; set +a KNOCK_ACTION=our-human forge script script/DeployKnockBinding.s.sol --rpc-url "$WORLD_RPC" ``` Without `--broadcast` that only simulates: it checks your settings and shows what it would deploy. When it looks right, deploy production and then staging: ```bash KNOCK_ACTION=our-human forge script script/DeployKnockBinding.s.sol --rpc-url "$WORLD_RPC" --broadcast KNOCK_ACTION=our-human KNOCK_STAGING=true forge script script/DeployKnockBinding.s.sol --rpc-url "$WORLD_RPC" --broadcast ``` Each costs about 843,500 gas, paid by `RELAYER_WALLET_PRIV_KEY`. forge prints each contract's address and the block it went into. Note both, for each. ## 3. Write your deployments Your network's parts find your KnockBindings in a deployments file. Copy Knock's, `contracts/deployments/480.json`, to `our-deployments.json`, and in both `production` and `staging` change three fields: | Field | Set it to | | --- | --- | | `KnockBinding` | Your KnockBinding's address there. | | `block` | The block it went into. knockd reads bindings from there on. | | `action` | Your action, `our-human`. | Leave `WorldIDVerifier`, `RpRegistry` and `version` as they are: they're World's contracts, and KnockBinding's version. Set `retired` to `[]`: it lists Knock's old KnockBindings. The Console reads `contracts/deployments/480.json` when it's built, so for a checkout that builds your Console, put your file in its place. ## 4. Run your knockd [Run knockd](knockd.md) as that guide says, with the RP key: in its `.env`, besides the settings above, ```bash title=".env" WORLD_RP_KEY_FILE=our-rp-key.json KNOCK_DEPLOYMENTS=our-deployments.json KNOCKD_ORIGINS=https://console.example ``` As it starts, it checks World Chain: each KnockBinding is version 3, proves your action and carries your RP, and World's registries have your RP key as your RP's signer. If the portal hasn't finished registering your RP, the last check fails: wait a few minutes and start it again. `/health` then says `"signs": true`. ## 5. Build your Console Build [the Console](console.md) from the checkout with your deployments file, pointing both of its knockd settings at yours: ```bash cd apps/door NEXT_PUBLIC_KNOCKD=https://knockd.example \ NEXT_PUBLIC_KNOCKD_RP=https://knockd.example \ bun run build ``` Serve it, and try a binding on staging first, with the Console's Simulator option. Then one with World App, on production. ## 6. Have sites take it Tell sites your production KnockBinding's address. Each verifier takes it in its own way: | Verifier | Setting | | --- | --- | | [`@knock-dev/verify`](../guides/verify-in-your-server.md) | `contract: '0x…'` in `new Verifier()` or `check()` | | [`knock-verifier`](../guides/verifier.md) | `VERIFIER_KNOCK_BINDING=0x…` | | [`knock-verify`](../reference/knock-verify.md) | the `contract` it's made with | | The Human Club | `CLUB_KNOCK_BINDING=0x…` | A verifier takes one network. A site that takes both runs a verifier for each, and should remember that the same human can come in once through each. Never give sites your staging KnockBinding. Staging takes World's Simulator, whose humans anyone can make. # Host the Human Club > The Human Club is a Next.js app with a libSQL database. Run it on Vercel with Turso, or on any server that runs Node. The Human Club is Knock's demo site: petitions and fundraisers for humans only, where each human signs a petition once. It's [`apps/bouncer`](https://github.com/getknock/knock/tree/main/apps/bouncer) in `getknock/knock`, a Next.js app that checks knocks in its own functions with [`@knock-dev/verify`](../guides/verify-in-your-server.md) and keeps its records in a libSQL database. A copy of your own is a whole club of its own, with its own petitions, fundraisers and members. ## What you need - A checkout of [`getknock/knock`](https://github.com/getknock/knock) and [Bun](https://bun.sh). - **A database**: a [Turso](https://turso.tech) database for Vercel or any host that runs more than one copy of the club, or a SQLite file on a server that runs one. - **An origin** for it, such as `https://club.example`, served on HTTPS. ## Try it on your machine ```bash bun install cd apps/bouncer bun run dev ``` The club is at `http://localhost:3001` and keeps its records in `human-club.db` beside it. In development, knocks go through the Door at `http://localhost:3000`: run the Console and Door's dev server too (`bun run dev` in `apps/door`), or set `NEXT_PUBLIC_DOOR=https://door.getknock.dev` to use Knock's. ## On Vercel, with Turso 1. **Make the database.** With the [Turso CLI](https://docs.turso.tech/cli/installation), signed in: ```bash turso db create human-club turso db show human-club --url # libsql://… turso db tokens create human-club # its token ``` The club makes its tables itself, the first time it opens the database. 2. **Make the project.** In Vercel, import your copy of `getknock/knock` and set the Root Directory to `apps/bouncer`. [`apps/bouncer/vercel.json`](https://github.com/getknock/knock/blob/main/apps/bouncer/vercel.json) sets the rest: the install runs at the repo's root, because the club uses Knock's packages from the same workspace, then `bun run build`. 3. **Set its environment variables**, for Production: `TURSO_DATABASE_URL` and `TURSO_AUTH_TOKEN` from step 1, and `CLUB_ORIGIN` set to the club's address. The rest are in [Settings](#settings). 4. **Deploy**, then add your domain to the project. If the domain changes, change `CLUB_ORIGIN` with it and deploy again. Preview deployments have addresses of their own, and the club takes knocks only for `CLUB_ORIGIN`. To try one, give previews a domain in Vercel, set `CLUB_ORIGIN` to it for the Preview environment, and give them a database of their own, so tests stay out of the real club. ## On your own server ```bash bun install cd apps/bouncer NEXT_PUBLIC_DOOR=https://door.example bun run build CLUB_ORIGIN=https://club.example \ TURSO_DATABASE_URL=file:/var/lib/human-club/club.db \ PORT=3001 \ bun run start ``` Put it behind a proxy that serves HTTPS, as for [knockd](knockd.md#put-it-behind-https), and keep it running with systemd or the like. A SQLite file works for one copy of the club. If you run more than one, they must share a database: use Turso, with `libsql://…` and its token. ## Settings `NEXT_PUBLIC_` settings are read when the club is built. The rest are read when it runs. | Setting | Default | What it does | | --- | --- | --- | | `CLUB_ORIGIN` | none: needed in production | The club's origin, exactly as browsers send it, such as `https://club.example`. Wedgies sign for it, and the club takes no knock signed for another. In development, the address the request came to. | | `TURSO_DATABASE_URL` | `file:human-club.db` | The database: `libsql://…` for Turso, or `file:…` for a SQLite file. Needed on Vercel. | | `TURSO_AUTH_TOKEN` | none | Turso's token for that database. | | `NEXT_PUBLIC_DOOR` | Knock's, `https://door.getknock.dev`; in development, `http://localhost:3000` | The Door knocks go through: Knock's, or [your own](door.md). | | `CLUB_ETH_RPC` | `https://ethereum-rpc.publicnode.com` | A mainnet RPC: for ENS, and to check gifts. | | `CLUB_WORLD_RPC` | `https://worldchain-mainnet.g.alchemy.com/public` | A World Chain RPC: for bindings. | | `CLUB_STRICT` | `off` | Whether a binding must be final first: `off`, `on` (safe) or `finalized`. [Strict mode](../guides/strict-mode.md) explains. | | `CLUB_MAX_BINDING_DAYS` | `365` | How old a binding's World ID proof may be, in days. | | `CLUB_KNOCK_BINDING` | Knock's | The KnockBinding whose bindings count. Only for [a network of your own](own-network.md). | Use your own RPCs for a club people use: the public ones are rate-limited, and the checks believe what they say. [Its RPCs](../guides/verifier.md#its-rpcs) says more. ## What it keeps For each human who has knocked: their nullifier, their Wedgie's name, and when they came. Then what they started, signed and gave, with their name beside it only where they chose to show it, and the gifts' transactions. A session is a random token in a cookie, which the club keeps only as a hash, for two weeks. Nothing else: no email addresses, no IP addresses, no analytics. Whoever holds the database can see which nullifier did what, so keep its token as you'd keep a password. # Security model > What a knock proves and what it doesn't, what's public, and how Knock handles replays, revocation and chain trouble. ## What a knock proves When your verifier lets a knock in, it has checked that: - the Wedgie whose key is in ENS under the knock's name signed it, which its firmware does only after a green press; - it signed your challenge, for your origin, less than 2 minutes after your verifier made the challenge; - the name is active at the latest mainnet block, and its records have held for 3 blocks; - the key is bound to a human on KnockBinding v3, production, for this name, by a World ID proof no older than you allow; - the Wedgie's counter rose, so this knock is new; - if you named an action, this human hasn't done it on your site before. ## What it doesn't prove - **Who pressed.** The Wedgie has no PIN, so anyone holding it can knock as its owner. If it's lost or stolen, its owner kills its name with the Knock Console's kill switch, which needs only the wallet that holds the name, not the Wedgie. Or they revoke the name in the [ENS app](https://app.ens.domains), by setting its `knock.status` record to `revoked`. Until then, it knocks for whoever has it. - **That one human is one person at a keyboard.** World ID proves the binding was made by a unique human. It can't stop a human from binding a Wedgie and handing it to someone else, or being paid to. - **That the origin on screen is right.** The Wedgie shows the origin the Door read from the browser, labelled *unverified: the Door says so*, since the Wedgie can't see the browser itself. On a computer that's been compromised, the Door could be made to ask for a knock for another site. The screen still shows that site, so people should read it before pressing. - **That nothing else on the Wedgie can use its key.** When a Wedgie's `knock.sc` record is `S1`, any app installed on the Wedgie can use its chip. Install apps you trust. - **That the Wedgie is a hardware wallet.** It's a small hackable device, and its Knock app hasn't been audited. ## What's public Knock trades privacy between sites for a binding anyone can check: - **In ENS**, anyone can read each name's key, status, nullifier, binding transaction and owner's wallet. - **The nullifier is one per human.** It's the same on every Wedgie a human binds and on every site they knock on. Sites that compare their records can tell the same human visited each, and anyone can look up which names carry a nullifier. - **The counter rises with every knock, on any site.** A site can see roughly how many times a Wedgie knocked elsewhere since it last knocked there. - **On World Chain**, each binding's `Bound` log shows the nullifier, the key's hash and the name's node. No one can see where a Wedgie knocks, apart from the sites it knocks on. Mode 2, Fresh, a new World ID proof with each knock and a nullifier for each site, would make humans unlinkable between sites. It isn't offered yet. ## Replays and relays - **Challenges** are 32 random bytes from your verifier, for your origin, used once and good for 120 seconds. A knock can't be used twice, or saved for later. - **The origin is signed.** The Door reads it from the browser, and the Wedgie signs it. A site that gets a knock for itself can't use it on yours: your verifier refuses it at check 2, or at check 4 if its `origin` was changed. - **A site can't borrow your challenge.** If another site gets a challenge from your verifier and asks for a knock with it, the knock is signed for that site's origin, and your verifier refuses it. - **Counters** must rise with each knock from a key, so a knock sent again fails check 5. - **All or nothing.** A knock that passes uses up its challenge, its counter and its entry in one SQLite transaction. Two checks of one knock at the same time can't both pass. ## Revocation - **Revoked:** when a name's `knock.status` is `revoked`, every verifier refuses its knocks at once, since check 3 reads the status at the latest block. A verifier that has seen a key revoked keeps refusing it, even if the name is switched back on. The owner sets the record in the [ENS app](https://app.ens.domains); the Console has no revoke. - **Killed:** the Console's kill switch unregisters the name and unlinks its records, from the owner's wallet, with or without the Wedgie. Knocks then fail check 3 as `no_name`. - **Expired:** an ENS name that expires reads as empty, so it fails the same way. - **Old bindings:** a binding whose World ID proof is older than `VERIFIER_MAX_BINDING_DAYS`, 365 by default, fails check 6. ## Failing closed - **A chain that can't be read** means a refusal, `unavailable` with status 503, never a guess. - **Records that are changing** mean a refusal until they've held for 3 blocks. - **Only one KnockBinding counts**: v3, production. Staging's takes proofs from World ID's Simulator, so its bindings never count, nor do older versions'. - **Finality is your choice.** By default a binding counts once it's in a World Chain block. [Strict mode](guides/strict-mode.md) waits for Ethereum. ## What your verifier trusts Your verifier believes its RPCs. One that lies could show records or a binding that aren't on chain. Use providers you trust, or run your own nodes. It never calls Knock: no Knock server takes part in a knock. ## Your verifier's data - **`entries`** links humans' nullifiers to the actions they took on your site, with names and times. Treat it as personal data. - **Logs** say who got in and which check refused. They hold no IP addresses, and no RPC URLs, which often carry keys. ## The Door - It holds no key, so it can't make a knock, only carry one. A knock it made up fails check 4. - It reads your origin from the browser, never from your page. - It answers only the window that opened it, at that window's origin, and only once. - It refuses to work inside a frame, where another page could cover what it says. - Knock's is at `https://door.getknock.dev`. A site can [serve its own](self-host/door.md) instead. ## Reporting a problem If you find a way past a check, report it to the maintainers privately, not in a public issue. # Troubleshooting > What to do when the Door won't open, the Wedgie won't answer, or your verifier turns a knock away. By symptom, and by refusal. Find what you see, below. For the full list of refusals, see [Checks and refusals](reference/checks.md). ## The Door ### The Door doesn't open `knock()` rejected with `blocked`: the browser stopped the popup. - Call `knock()` in the click handler itself, before any `await`. Pass the challenge as a promise. [Add a Knock button](guides/knock-button.md#call-knock-from-the-click) shows how. - If it still happens, the person has blocked popups for your site. Ask them to allow them. ### The Door says to connect the Wedgie The browser hasn't been given the Wedgie for the Door's origin yet. Press the Door's button, and choose the Wedgie from the browser's list of serial devices. The browser remembers it after that. If the list is empty, check the cable: some USB cables carry power only. ### "Another tab has the Wedgie" Only one tab can talk to a Wedgie at a time. The Knock Console or another Door has it. Disconnect it there, or close that tab, then knock again. Your page hears nothing until the Door is closed (`closed`) or the knock times out (`timeout`). ### Knocking doesn't work in Firefox, Safari or on a phone The Door reaches the Wedgie over Web Serial, which only desktop Chromium browsers have: Chrome, Edge, Brave, Arc and others. Check for it with `'serial' in navigator` and show another way in where it's missing. ### The Door says it can't be used in a frame The Door never works inside another page. Let door-sdk open it as a popup. ### `bad_origin` Your page is on `http://` at a host other than `localhost` or `127.0.0.1`. Serve it over https. ## The Wedgie ### `not_enrolled` The Wedgie has no name, or isn't bound to a human. Its owner sets it up in the Knock Console: the binding, then the name. ### `no_key` The Wedgie has no Knock key yet. Its owner makes one in the Knock Console. ### `timeout` Nobody pressed on the Wedgie within 2 minutes, or the whole knock took longer than `timeoutMs`. Knock again. ### `rejected` Red on the Wedgie: its owner said no. Show nothing, and let them knock again. ### `not_ready` The Door couldn't talk to the Wedgie: it was unplugged, or went away during the knock. Plug it in, and knock again. ## Your verifier ### The verifier won't start It stops with a reason when something it depends on isn't right: | It says | Do this | | --- | --- | | `VERIFIER_ORIGIN: https://host[:port], or http://localhost[:port]` | Set the origin as the browser shows it, with no path and no trailing `/`. | | `VERIFIER_ETH_RPC is chain , not mainnet` | Point it at an Ethereum mainnet RPC. | | `VERIFIER_WORLD_RPC is chain , not World Chain` | Point it at a World Chain mainnet RPC, chain 480. | | `the accepted KnockBinding says it's v, not v3` | Your World Chain RPC is wrong, or out of date. | | `VERIFIER_STRICT: strict: off, on (safe) or finalized, not ` | Use `off`, `on` or `finalized`. | ### The browser says CORS blocked the request The verifier only answers pages at `VERIFIER_ORIGIN`, exactly. `http://localhost:5173` and `http://127.0.0.1:5173` are different origins, and so are different ports. Set `VERIFIER_ORIGIN` to what's in the address bar, or ask the verifier from your server instead. ### `challenge` door-sdk couldn't get a challenge: your verifier didn't answer, refused, or answered with something else. Check that it's running, that the page can reach it, and that it isn't `busy`. ## Refusals, by check ### Check 2: `that challenge expired: knock again` More than 2 minutes passed between your verifier making the challenge and checking the knock. This happens when someone waits on the KNOCK screen, or connects the Wedgie for the first time. Knock again. ### Check 2: `wrong_origin` The page that knocked isn't at `VERIFIER_ORIGIN`. Compare it with the address bar, port and all. Each origin needs its own verifier. ### Check 3: `records_changing` The name was set up or changed less than a minute ago. The verifier waits until its records have held for 3 mainnet blocks, about 36 seconds. Knock again in a minute. ### Check 3: `no_name` The name has no Knock records: it was never named, its owner killed it, or it expired. Its owner sets it up again in the Knock Console. ### Check 3: `revoked` Its owner revoked it. If this verifier ever saw the key revoked, it refuses that key for good, even after the name is switched back on. That's on purpose: a revoke is meant to stick. ### Check 4: `bad_signature` The knock wasn't signed by the name's key for your site and challenge. Knocks from a Wedgie through the Door don't do this, so it's someone sending made-up knocks, or a knock changed on the way. ### Check 5: `replayed` A knock this verifier has seen, or an older one, was sent again. Knock again: a new knock has a higher counter. ### Check 6: `binding_not_final` Your verifier is strict, and the Wedgie was bound minutes ago. Wait a few minutes, or 20 to 25 for `finalized`, then knock again. Or turn strict mode off: [Strict mode](guides/strict-mode.md) explains the trade. ### Check 6: `no_binding`, `wrong_human` The binding in `knock.poh` isn't a v3, production binding of this key, for this human and this name. This happens when the binding was made with World ID's Simulator, on an older KnockBinding, or for another name. Its owner binds the Wedgie again in the Knock Console, then names it. ### Check 6: `binding_expired` The binding's World ID proof is older than `VERIFIER_MAX_BINDING_DAYS`. Its owner binds the Wedgie again. ### Check 8: `already_entered` This human has done this action on your site. That's the action working. To let them do something again, use a new action name. ### `unavailable` The verifier couldn't read mainnet (check 3) or World Chain (check 6). Public RPCs limit how often you can call them, so use your own. Knock again. ### `busy` The verifier's limits: 600 challenges and 600 checks an hour per process, and 10,000 challenges outstanding at once. They're fixed for now, so wait and knock again. Don't spread one origin over several verifiers with their own databases: a challenge from one fails at another, and each would count entries on its own. # For AI agents > These docs as Markdown for assistants and coding agents, and a SKILL.md that teaches an agent to add Knock to a site. Every page here is a Markdown file, and the site serves each one as it is, for assistants and coding agents to read. ## The Knock skill [`SKILL.md`](../skills/knock/SKILL.md) is an [Agent Skill](https://agentskills.io): instructions a coding agent loads when it's asked to add Knock to a site. It covers the button, a verifier, checking knocks from the server, actions, and tests, and it points the agent at the right reference when it needs one. | File | What's in it | | --- | --- | | [`skills/knock/SKILL.md`](../skills/knock/SKILL.md) | The steps, the rules that matter, and what to check when done. | | [`skills/knock/reference/api.md`](../skills/knock/reference/api.md) | door-sdk, `@knock-dev/verify`, and the verifier's HTTP API, in brief. | | [`skills/knock/reference/refusals.md`](../skills/knock/reference/refusals.md) | Every `KnockError` code and refusal, and what to show for each. | ### Install it For Claude Code, put the folder in your project's `.claude/skills/`, and the agent finds it when a task calls for it: ```bash for f in SKILL.md reference/api.md reference/refusals.md; do curl -fsS --create-dirs -o ".claude/skills/knock/$f" "https://docs.getknock.dev/skills/knock/$f" done ``` Put it in `~/.claude/skills/knock` instead to have it in every project, or copy `skills/knock` from a checkout of `getknock/docs`. Then ask for what you want, such as "Add a Knock button to the sign-up page, and let each human sign up once." Other agents that read skills take the same folder. For one that doesn't, give it the skill's address, `https://docs.getknock.dev/SKILL.md`, or paste the file in. ## The docs, for reading | Address | What it is | | --- | --- | | [`docs.getknock.dev/llms.txt`](https://docs.getknock.dev/llms.txt) | What Knock is, and a link to every page as Markdown, in reading order. | | [`docs.getknock.dev/llms-full.txt`](https://docs.getknock.dev/llms-full.txt) | Every page in one file. | | `docs.getknock.dev/raw/.md` | One page as Markdown, such as [`/raw/quickstart.md`](https://docs.getknock.dev/raw/quickstart.md). The home page is [`/raw/index.md`](https://docs.getknock.dev/raw/index.md). | | [`docs.getknock.dev/SKILL.md`](https://docs.getknock.dev/SKILL.md) | The skill's `SKILL.md`. Its reference files are under `docs.getknock.dev/skills/knock/`. | Each page also has **Copy page** at the top, which copies it as Markdown, ready to paste into a chat. The same files are in the `getknock/docs` repo, under `docs/` and `skills/`.