--- name: qid-connect description: Add "Sign in with qID" to a website or dApp - post-quantum wallet login for BTX where the user's address IS the account, with no email, no password, no relay server and no third-party service. Covers the server SDK (@qid/connect-server), the button widget, the account model, and a live checker that catches the misconfigurations that silently break sign-in. Use this whenever someone wants wallet login, wallet connect, address verification, "prove they own this address", passwordless or email-less auth, or account linking for a BTX or bonuz app, and also when they are debugging an existing qID integration (bad_origin, nonce_unknown, a QR that flips to expired, sign-in that works on desktop but not mobile). Reach for it even if the person says "web3 login" or "connect wallet" without naming qID, as long as BTX, bonuz or qID is anywhere in the picture. license: MIT --- # Integrating qID Connect ## What you are building, and the one invariant A user proves control of a post-quantum BTX address by signing a one-time, origin-bound challenge. Your server verifies the signature and issues a session. That address is the account: first successful sign-in creates it, there is nothing else to collect. The invariant that shapes every decision below: **a qID login signature is domain-separated from transactions and can never move funds.** There is no transaction endpoint in this surface, so the worst case for a fully compromised integrator site is stolen sessions on that site, never stolen coins. Keep that line intact and you cannot build a catastrophic bug here. ## Get oriented first Ask, or work out from the codebase, three things before writing code: 1. **The runtime.** Express, Next.js, or a static site with a backend elsewhere. Each has a reference file below; read only the one you need. 2. **The exact origin users land on.** Not the domain, the origin: scheme, host, and port after every redirect. This is the single most common cause of a dead integration, and it is worth two minutes of certainty now. 3. **Whether more than one server instance will serve this**, now or later. Serverless counts. If yes, the default in-memory stores are wrong and you need a shared store from the start. Then follow the four steps. They are ordered so you always have something verifiable before you add the next piece. A developer who knows their own codebase should be through all four in well under an hour; most of that is reading, not typing. ## Step 0: get the SDK `@qid/connect-server` is **not on npm**. It ships in the release pack, which is a public download: ```sh curl -LO https://qid.dev/connect/qid-connect-latest.zip && unzip qid-connect-latest.zip ``` Inside, `packages/server` is the SDK. Add it as a workspace dependency, or copy it into your app and install its three small dependencies (`@noble/post-quantum`, `@noble/hashes`, `@scure/base`). The pack also carries the runnable examples, the reference signer, and this skill. The button needs nothing installed at all: it imports from qid.dev at runtime. So if you only want address verification on a page you already serve, you can skip ahead, wire the two endpoints your framework needs, and come back. ## Step 1: the server ```js import { createQidConnect } from "@qid/connect-server"; import { qidMiddleware, requireQidSession } from "@qid/connect-server/express"; const qid = createQidConnect({ origin: "https://www.yourapp.com", // exact, no path, no trailing slash sessionSecret: process.env.QID_SESSION_SECRET, // 32+ random chars, from a secret store }); app.use(express.json()); app.use("/qid", qidMiddleware(qid)); // the entire login system app.get("/api/me", requireQidSession(qid), (req, res) => res.json(req.qidSession)); ``` That middleware mounts six routes. You rarely call them directly, but knowing them makes debugging obvious rather than mysterious: | route | purpose | |---|---| | `POST /qid/challenge` | issue a one-time challenge bound to your origin, plus the browser's poll secret | | `POST /qid/verify` | verify a proof pasted in this browser, create or find the account, set the session cookie | | `POST /qid/proof` | a phone wallet submits its proof here after scanning the QR | | `GET /qid/poll` | the browser that issued the challenge polls with its poll secret. Once a phone proof lands it answers `{ status: "confirm", address }` and mints nothing; after the user accepts that address the browser calls again with `&confirm=1` and that claims the session (1.7.0+, the QR fixation fix) | | `GET /qid/session` | `{ address, account, expiresAt }`, or 401 | | `POST /qid/logout` | clear the cookie | Framework specifics, including the one Next.js setting that silently breaks QR sign-in, are in `references/express.md`, `references/nextjs.md` and `references/static-site.md`. ## Step 2: the button ```html
``` The hosted import is the default worth recommending: wallet-list and UX updates reach users without a redeploy, and the signed protocol underneath is frozen v1, so an update cannot break login. Pin `widget-.js` or vendor `packages/widget/src/` when the project has a policy about third-party script origins. Either way, keep the button and dialog stock. Sameness across BTX apps is what makes the flow trustworthy to users, so restyling it is a real cost. `mountQidAccount` renders nothing while signed out, so mount it unconditionally. **If the site sends a Content-Security-Policy, add qid.dev to `script-src` before you debug anything else.** The hosted import is a cross-origin module, so a strict policy blocks it and the button simply never appears, with the only clue in the browser console. qID's own sites run `script-src 'self' https://qid.dev` for exactly this reason. Vendoring the widget instead of importing it avoids the question entirely, which is the right call when a policy forbids third-party script origins outright. **The same policy needs `style-src 'self' 'unsafe-inline'`.** The widget injects its stylesheet at runtime, so a `style-src` (or a bare `default-src` fallback) without `'unsafe-inline'` renders the dialog as raw unstyled HTML while every script check passes. Do not hash-pin the widget's style: it is hosted and unpinned, so the hash changes every release, and a nonce or hash source in the directive makes browsers ignore `'unsafe-inline'` entirely. This broke a live site once; the checker now fails on it. ## Step 3: the account model, and the one trap Your accounts table needs exactly one required column: `address TEXT PRIMARY KEY`. Registration and login are the same event. Do not add an email column for identity. If the project already has email accounts, `docs/MIGRATION-from-email.md` in the pack covers unbolting that. Now the trap, because it is the one place an integrator can create an account takeover while following the shape of the docs: **Never look accounts up by `recovery_leaf_hash`, merge on it, or gate one-per-wallet rules on it.** A verified proof establishes control of the login key only. The recovery leaf hash is a value the signer *chose*, not a key they proved: anyone can pair a leaf hash they saw with a login key they generated and produce a perfectly valid proof for a different address carrying that same hash. It is also public by design, since a P2MR spend reveals the sibling hash in its control block. Treat it as an attribute of the address and nothing more. Rotation-aware identity is the legitimate version of what people reach for that field to do, and it is not available yet. Details and the design that survives the transition are in `references/account-model.md`. Read it before designing anything that links two addresses to one human. ## Step 4: verify it works Run the checker against your running app. It needs no wallet, no keys and no dependencies: ```sh bun scripts/check-integration.mjs http://localhost:3000 # or: node scripts/... bun scripts/check-integration.mjs https://www.yourapp.com --api /api/qid ``` It exercises the real HTTP surface and reports pass/fail per check: origin mismatches, a `proof_url` your phone would 404 against, an unshared store across instances, a CSP that blocks the widget, anything answering on the SDK's behalf, and a session endpoint that leaks 200 to anonymous callers. It separates failures that stop sign-in from ones merely worth tightening. Fix the blocking ones before touching a wallet: from the UI they are indistinguishable, because the dialog simply never completes. Then do one real signed round trip. From inside the release pack you can sign without any wallet installed: ```sh bun tools/signer/btx-sign-ownership.mjs --random \ --origin http://localhost:3000 --nonce --ts ``` Take `nonce` and `ts` from the dialog's **Desktop wallet** tab, not the QR tab. The QR rotates about every 105 seconds and the server burns the superseded nonce, so a proof signed against a QR you left sitting will correctly be rejected. Paste the printed proof into that same tab's step 2, or POST it to `/qid/proof` to simulate a phone. ## Before it goes live - `origin` is the exact final origin, after apex/www and http/https redirects. Open the deployed site and read `location.origin` in the console; that string is your value. Getting this wrong rejects every proof with `bad_origin`. - Durable, shared stores if anything other than a single long-lived process serves traffic. Use `SqliteNonceStore` / `SqliteAccounts`, or implement the two interfaces in `stores.js` on Redis or your SQL. The package warns under `NODE_ENV=production` when it is still on the in-memory defaults. - HTTPS, so the session cookie is `Secure` as well as `HttpOnly` and `SameSite=Lax`. - `sessionSecret` from a secret manager, 32+ random characters. Rotating it signs everyone out, which makes it your kill switch. - A same-origin `/qid/*` proxy is fine and is required when your backend is on another host. The rule is narrow: never let the proxy change the origin the wallet sees versus the one the user visits. ## When something is broken `references/troubleshooting.md` maps symptoms to causes, including the failures that produce no error at all. Start there rather than reading the SDK source; nearly every real-world failure is one of six configuration mistakes, and the symptom identifies which.