Website

21.gifts website handbook

This handbook is mandatory. Every HTML screen in this repository must have a ## Screen: section. CI (npm run handbook:check) fails otherwise.

There is no application src/ here (static HTML). Screens are /, /legal, /404.

The rendered handbook for website, app, and api is published at handbook.21.gifts (DEV: dev-handbook.21.gifts).

Screens

Screen: /

  • URL: https://21.gifts/ (production) and the Cloudflare Pages develop alias.
  • What the user sees: Marketing landing: peer-to-peer Lightning gifts, NOSTR mention, CTA toward the app, Open Graph preview image.

21.gifts landing Open Graph preview

  • Actions: Read the pitch, follow links into the app (app.21.gifts), the handbook (handbook.21.gifts), and the legal page.
  • Assets: /favicon.ico, /apple-touch-icon.png, assets/favicon.svg, assets/og.png.
  • URL: /legal.html (sitemap and canonical use this path; there is no in-repo rewrite to /legal).
  • What the user sees: Imprint plus privacy (FADP), hosting, and app-data notes for 21.gifts.
  • Actions: Read the legal text. Follow header links to the landing, the handbook (handbook.21.gifts), and the app.
  • Assets: Same favicon and OG tags as the landing.

Screen: /404

  • URL: /404.html (static asset). This repo does not rewrite unknown paths onto 404.html; that is hosting configuration.
  • What the user sees: Not-found page with a link home. Icons only (no Open Graph block).
  • Actions: Navigate back to /, or follow header links to the handbook (handbook.21.gifts) and the app.
  • Assets: /favicon.ico, SVG icon, apple-touch-icon.

App

21.gifts app handbook

This handbook is mandatory. Every exported function, every UI screen, and every HTTP endpoint in this repository must have a section here. CI (npm run handbook:check) fails the PR if a heading is missing or the section is a stub.

  • Screens: ## Screen: /path
  • Functions: ## Function: name
  • Endpoints: ## Endpoint: METHOD /path

Do not merge a PR that adds a screen, export, or endpoint without updating this handbook in the same PR. Undeclared gaps are rejected, not discussed.

See screens.md, functions.md, and endpoints.md.

Screens

Screen: /

  • URL: / — public marketing landing (no auth gate).
  • What the user sees: Dark 21.gifts header, headline about peer-to-peer Lightning gifts, How it works (LNURL-auth + Lightning Address) / Why / FAQ, CTAs Ask for help (/login) and Send help (/donate).
  • Actions: Read the pitch, open login or donate, jump to in-page sections, open Legal & Privacy.
  • Calls: Home (src/app/(marketing)/page.tsx) inside MarketingLayout.
  • URL: /legal — imprint and privacy. /legal.html permanently redirects here.
  • What the user sees: Legal Notice (Switzerland, info@21.gifts) and Privacy Policy (no cookies/analytics, session in localStorage, Cloudflare TLS, LNURL-auth on this origin).
  • Actions: Read-only. Header Log in goes to /login.
  • Calls: LegalPage.

Screen: /login

  • URL: /login — LNURL-auth challenge.
  • What the user sees: Idle: page heading Log in to 21.gifts, card heading Sign in to 21.gifts, and button Log in with your Lightning wallet. After start: QR of the uppercase LNURL, primary Open Wallet of Satoshi, secondary Open default Lightning wallet, Copy login code, plus expiry/error/signed-in states.
  • Actions: Scan the QR, tap WoS (walletofsatoshi:lightning:LNURL1… / Android Intent), tap generic lightning:, or copy the LNURL. The page polls /auth/session until authenticated, then stays on /login and shows the signed-in card (role, linking key, Lightning Address form, log out). There is no client redirect to /.
  • Calls: LoginCard (includes LightningAddressForm when signed in), useLnurlLogin, startLnurlAuth, pollSession, walletOfSatoshiHref, walletOfSatoshiIntentHref, uppercaseLnurl, QrCode, useAuthStore.

21.gifts login

Screen: /donate

  • URL: /donate — guest LNURL-pay gift. No login required.
  • What the user sees: Heading Send a gift, Lightning Address field, sat amount (no comment field), Create invoice, then a QR and lightning: invoice link.
  • Actions: Enter a LUD-16 address and amount, create an invoice, pay from any Lightning wallet.
  • Calls: DonateForm, resolveLightningAddress, requestDonateInvoice, satsToMsat, formatMsatAsSats, QrCode.

21.gifts donate

Functions

Function: DonateForm

  • Purpose: Renders the guest donate form (Lightning Address and sat amount only; no comment) and, after success, the invoice QR.
  • Inputs: Form state: address and whole-sat amount.
  • Returns / side effects: React element. Side effects: HTTP to the api then GET the payee LNURL-pay callback.
  • Used by: Screen /donate.

Function: DonatePage

  • Purpose: Next.js page for /donate.
  • Inputs: None.
  • Returns / side effects: The donate screen wrapped in the root layout.
  • Used by: Route /donate.

Function: GET

  • Purpose: Shared export name for App Router GET handlers. Healthz uses export function GET; same-origin api proxies re-export unique functions as GET.
  • Inputs: Incoming Request on proxy routes; none on healthz.
  • Returns / side effects: Response. Healthz is { status: 'ok' } 200; proxies return the upstream api response.
  • Used by: Container probes, browser/wallet same-origin calls.

Function: Home

  • Purpose: Next.js page for /. Marketing landing: pitch, how it works, why, FAQ, CTAs to /login and /donate.
  • Inputs: None.
  • Returns / side effects: The home screen element.
  • Used by: Route /.

Function: LightningAddressForm

  • Purpose: Logged-in form to claim, verify, or unlink a LUD-16 Lightning Address.
  • Inputs: Reads useAuthStore. User input: address string, verification confirm.
  • Returns / side effects: React element or null when logged out.
  • Used by: LoginCard signed-in view on screen /login (not on /).

Function: LoginCard

  • Purpose: LNURL-auth UI: hydrate session, start challenge, QR, WoS deep link, copy, poll, expiry, then signed-in view with LightningAddressForm.
  • Inputs: Uses useLnurlLogin and useAuthStore. Rehydrates via loadSession + fetchMe.
  • Returns / side effects: React element covering idle/waiting/expired/error/signed-in. Does not navigate away from /login.
  • Used by: Screen /login.

Function: LoginPage

  • Purpose: Next.js page for /login.
  • Inputs: None.
  • Returns / side effects: Renders LoginCard.
  • Used by: Route /login.

Function: QrCode

  • Purpose: SVG QR for a string (LNURL or bolt11).
  • Inputs: value (required), optional label.
  • Returns / side effects: React element.
  • Used by: LoginCard and DonateForm.

Function: RootLayout

  • Purpose: Root HTML shell: lang=en, global CSS, metadata (title, icons, Open Graph, Twitter).
  • Inputs: children React nodes.
  • Returns / side effects: The document wrapper for every route.
  • Used by: All screens.

Function: clearSession

  • Purpose: Removes the bearer token from localStorage.
  • Inputs: None.
  • Returns / side effects: void. No-op during SSR (window undefined).
  • Used by: useAuthStore.clearAuth and LoginCard when session hydration gets 401.

Function: confirmLightningAddressVerification

  • Purpose: POST /me/lightning-address/verification/confirm with the nonce after the 1-sat invoice is paid.
  • Inputs: sessionToken, nonce.
  • Returns / side effects: Updated Account, or throws on 4xx/5xx.
  • Used by: LightningAddressForm.

Function: fetchMe

  • Purpose: GET /me with the bearer session.
  • Inputs: sessionToken.
  • Returns / side effects: Account or null on 401.
  • Used by: LoginCard session hydration.

Function: formatMsatAsSats

  • Purpose: Formats millisatoshis as a sat string for the donate UI.
  • Inputs: msat number.
  • Returns / side effects: Decimal string in sats.
  • Used by: DonateForm amount-range error (minSendablemaxSendable).

Function: getApiUrl

  • Purpose: Reads NEXT_PUBLIC_API_URL via the typed config accessor.
  • Inputs: None.
  • Returns / side effects: Origin string. Throws if unset/empty (entrypoint must substitute).
  • Used by: proxyApiRequest (server-side upstream origin).

Function: isAndroidUserAgent

  • Purpose: Detects Android so the WoS CTA can use an Intent URL.
  • Inputs: userAgent string.
  • Returns / side effects: true iff /Android/i matches.
  • Used by: LoginCard QrView.

Function: loadSession

  • Purpose: Reads the bearer token from localStorage.
  • Inputs: None.
  • Returns / side effects: Token string or null. SSR-safe.
  • Used by: LoginCard on mount.

Function: pollSession

  • Purpose: GET /auth/session with X-Poll-Token.
  • Inputs: pollToken.
  • Returns / side effects: SessionResult (pending / authenticated / expired / used).
  • Used by: useLnurlLogin.

Function: requestDonateInvoice

  • Purpose: GET an LNURL-pay callback with amount millisatoshis and return the bolt11 string.
  • Inputs: { callback, amountMsat, fetchImpl? }. Does not resolve a Lightning Address.
  • Returns / side effects: bolt11 string, or throws.
  • Used by: DonateForm.

Function: resolveLightningAddress

  • Purpose: GET /lightning-address?address= on the 21.gifts api.
  • Inputs: address.
  • Returns / side effects: Resolved LNURL-pay metadata (callback, min/max).
  • Used by: DonateForm before paying.

Function: satsToMsat

  • Purpose: Converts whole sats to millisatoshis.
  • Inputs: sats number.
  • Returns / side effects: sats * 1000.
  • Used by: DonateForm (converts sats before calling requestDonateInvoice).

Function: saveSession

  • Purpose: Writes the bearer token to localStorage.
  • Inputs: token string.
  • Returns / side effects: void. SSR no-op.
  • Used by: useAuthStore.setAuth.

Function: setLightningAddress

  • Purpose: POST /me/lightning-address.
  • Inputs: sessionToken, address.
  • Returns / side effects: Updated Account.
  • Used by: LightningAddressForm.

Function: startLightningAddressVerification

  • Purpose: POST /me/lightning-address/verification — api pays ~1 sat to the linked address. The nonce is not in the JSON; the user reads it from the wallet payment comment.
  • Inputs: sessionToken.
  • Returns / side effects: { status: 'sent', expiresInSeconds, sats }.
  • Used by: LightningAddressForm.

Function: startLnurlAuth

  • Purpose: GET /auth/lnurl — creates k1 + LNURL + poll token.
  • Inputs: None.
  • Returns / side effects: StartChallenge.
  • Used by: useLnurlLogin.

Function: unlinkLightningAddress

  • Purpose: DELETE /me/lightning-address.
  • Inputs: sessionToken.
  • Returns / side effects: Updated Account with address cleared.
  • Used by: LightningAddressForm.

Function: uppercaseLnurl

  • Purpose: Uppercases a bech32 LNURL (LUD-01).
  • Inputs: lnurl string.
  • Returns / side effects: Uppercase string.
  • Used by: QR value, lightning: href, WoS hrefs.

Function: useAuthStore

  • Purpose: Zustand store for session + account. Hydration is explicit (no module-init localStorage).
  • Inputs: Hook. Methods setAuth, setAccount, clearAuth.
  • Returns / side effects: Auth state object.
  • Used by: LoginCard, useLnurlLogin, LightningAddressForm on /login (not /).

Function: useLnurlLogin

  • Purpose: Hook: start LNURL-auth, poll until authenticated or expired. Returns { status, lnurl, start } — there is no separate error field (errors are a status of 'error').
  • Inputs: None.
  • Returns / side effects: UseLnurlLogin status machine.
  • Used by: LoginCard.

Function: walletOfSatoshiHref

  • Purpose: iOS/desktop WoS deep link.
  • Inputs: lnurl.
  • Returns / side effects: walletofsatoshi:lightning: + uppercase LNURL.
  • Used by: LoginCard primary CTA when not Android.

Function: walletOfSatoshiIntentHref

  • Purpose: Android Chrome Intent pinning the WoS package.
  • Inputs: lnurl.
  • Returns / side effects: intent:lightning:LNURL…#Intent;scheme=walletofsatoshi;package=com.livingroomofsatoshi.wallet;…;end.
  • Used by: LoginCard primary CTA on Android.

Function: DELETE

  • Purpose: App Router DELETE export on /me/lightning-address (re-export of proxyMeLightningAddressDelete).
  • Inputs: Incoming Request.
  • Returns / side effects: Upstream api Response.
  • Used by: Same-origin unlinkLightningAddress.

Function: LegalPage

  • Purpose: Next.js page for /legal (imprint and privacy).
  • Inputs: None.
  • Returns / side effects: The legal screen.
  • Used by: Route /legal.

Function: MarketingFooter

  • Purpose: Footer for marketing pages: wordmark, section links, legal, GitHub.
  • Inputs: None.
  • Returns / side effects: Footer element.
  • Used by: MarketingLayout, NotFound.

Function: MarketingHeader

  • Purpose: Sticky marketing header with wordmark, section nav, login CTA, mobile menu.
  • Inputs: None (internal open state).
  • Returns / side effects: Header element; toggles nav on small screens.
  • Used by: MarketingLayout, NotFound.

Function: MarketingLayout

  • Purpose: Dark full-page shell for / and /legal.
  • Inputs: children.
  • Returns / side effects: Wrapper div with header, page, footer.
  • Used by: Marketing route group.

Function: NotFound

  • Purpose: App-wide 404 screen with marketing chrome and a link home.
  • Inputs: None.
  • Returns / side effects: 404 element.
  • Used by: Next.js not-found.tsx.

Function: POST

  • Purpose: App Router POST export on lightning-address write/verify routes (re-exports of the proxy*Post functions).
  • Inputs: Incoming Request.
  • Returns / side effects: Upstream api Response.
  • Used by: Same-origin address link and verification.

Function: proxyApiRequest

  • Purpose: Forwards an App Router request to getApiUrl() + path, copying query, body, and authorization / content-type / poll-token / user-agent headers.
  • Inputs: request, apiPath beginning with /.
  • Returns / side effects: Upstream Response, or 502 JSON if fetch throws.
  • Used by: All same-origin api proxy route handlers.

Function: proxyAuthLnurlCallbackGet

  • Purpose: Proxies GET /auth/lnurl/callback (wallet LUD-04).
  • Inputs: Request with k1/sig/key query.
  • Returns / side effects: Upstream Response.
  • Used by: Route GET /auth/lnurl/callback.

Function: proxyAuthLnurlGet

  • Purpose: Proxies GET /auth/lnurl.
  • Inputs: Request.
  • Returns / side effects: Upstream Response.
  • Used by: Route GET /auth/lnurl.

Function: proxyAuthSessionGet

  • Purpose: Proxies GET /auth/session.
  • Inputs: Request with X-Poll-Token.
  • Returns / side effects: Upstream Response.
  • Used by: Route GET /auth/session.

Function: proxyLightningAddressGet

  • Purpose: Proxies GET /lightning-address.
  • Inputs: Request with address query.
  • Returns / side effects: Upstream Response.
  • Used by: Route GET /lightning-address.

Function: proxyMeGet

  • Purpose: Proxies GET /me.
  • Inputs: Request with Bearer token.
  • Returns / side effects: Upstream Response.
  • Used by: Route GET /me.

Function: proxyMeLightningAddressDelete

  • Purpose: Proxies DELETE /me/lightning-address.
  • Inputs: Request.
  • Returns / side effects: Upstream Response.
  • Used by: Route DELETE /me/lightning-address.

Function: proxyMeLightningAddressPost

  • Purpose: Proxies POST /me/lightning-address.
  • Inputs: Request with JSON body.
  • Returns / side effects: Upstream Response.
  • Used by: Route POST /me/lightning-address.

Function: proxyMeLightningAddressVerificationConfirmPost

  • Purpose: Proxies POST /me/lightning-address/verification/confirm.
  • Inputs: Request with nonce JSON.
  • Returns / side effects: Upstream Response.
  • Used by: Route POST /me/lightning-address/verification/confirm.

Function: proxyMeLightningAddressVerificationPost

  • Purpose: Proxies POST /me/lightning-address/verification.
  • Inputs: Request.
  • Returns / side effects: Upstream Response.
  • Used by: Route POST /me/lightning-address/verification.

HTTP endpoints (Next.js route handlers)

Endpoint: GET /healthz

  • Purpose: Liveness JSON { status: 'ok' } from src/app/healthz/route.ts.
  • Errors: None if the process is up (always 200).
  • Used by: Container probes and Playwright smoke.
  • Auth: Public.

Endpoint: GET /auth/lnurl

  • Purpose: Same-origin proxy of api GET /auth/lnurl (issue LNURL-auth challenge).
  • Errors: Upstream status, or 502 if the api is unreachable.
  • Used by: startLnurlAuth in the browser.
  • Auth: Public.

Endpoint: GET /auth/lnurl/callback

  • Purpose: Same-origin proxy of the wallet-facing LUD-04 callback: forwards query string to the upstream api. The wallet linkingKey domain is whatever host the encoded callback uses (set on the api as PUBLIC_BASE_URL).
  • Errors: Upstream LUD-04 JSON, or 502 if the api is unreachable.
  • Used by: Lightning wallets after scanning the login QR.
  • Auth: Public (wallet signature in query).

Endpoint: GET /auth/session

  • Purpose: Same-origin proxy of api session poll.
  • Errors: Upstream status, or 502 if the api is unreachable.
  • Used by: pollSession.
  • Auth: X-Poll-Token.

Endpoint: GET /lightning-address

  • Purpose: Same-origin proxy of public LUD-16 resolve.
  • Errors: Upstream 400/502, or 502 if the api is unreachable.
  • Used by: resolveLightningAddress on /donate.
  • Auth: Public.

Endpoint: GET /me

  • Purpose: Same-origin proxy of the signed-in account.
  • Errors: Upstream 401, or 502 if the api is unreachable.
  • Used by: fetchMe.
  • Auth: Bearer.

Endpoint: POST /me/lightning-address

  • Purpose: Same-origin proxy to link or replace a Lightning Address.
  • Errors: Upstream 400, or 502 if the api is unreachable.
  • Used by: setLightningAddress.
  • Auth: Bearer.

Endpoint: DELETE /me/lightning-address

  • Purpose: Same-origin proxy to unlink a Lightning Address.
  • Errors: Upstream status, or 502 if the api is unreachable.
  • Used by: unlinkLightningAddress.
  • Auth: Bearer.

Endpoint: POST /me/lightning-address/verification

  • Purpose: Same-origin proxy to start address proof-of-control.
  • Errors: Upstream 400/409/502/503, or 502 if the api is unreachable.
  • Used by: startLightningAddressVerification.
  • Auth: Bearer.

Endpoint: POST /me/lightning-address/verification/confirm

  • Purpose: Same-origin proxy to confirm the verification nonce.
  • Errors: Upstream 400/409/502/503, or 502 if the api is unreachable.
  • Used by: confirmLightningAddressVerification.
  • Auth: Bearer.

API

21.gifts api handbook

This handbook is mandatory. Every exported function/class and every HTTP endpoint must have a section. CI (bun run handbook:check / node scripts/check-handbook.mjs) fails the PR otherwise.

  • Functions: ## Function: name
  • Endpoints: ## Endpoint: METHOD /path

This repo has no UI screens. UI screens live in 21gifts/app and 21gifts/website. A PR that adds an export or route without a handbook section is rejected.

Functions

Function: InMemoryAuthStore

  • Purpose: Process-local AuthStore: challenges, accounts, sessions, verifications. Evicts expired challenges/sessions on write.
  • Inputs: Constructor none. Methods take domain objects (Challenge, Account, Session, AddressVerification).
  • Returns / side effects: Lookups return the object or undefined. Writes are void.
  • Used by: createApp default store; all auth/me routes.

Function: InMemoryLnAddressCache

  • Purpose: TTL cache for successful LUD-16 metadata resolves.
  • Inputs: get(address, now), put(entry, now). TTL from LN_ADDRESS_CACHE_TTL_MS.
  • Returns / side effects: get returns CachedLnAddress or null.
  • Used by: lightningAddressRoutes.

Function: UnconfiguredInvoicePayer

  • Purpose: InvoicePayer that always fails — process boots without a payer so verification returns 503 until wired.
  • Inputs: isConfigured() is always false. payInvoice(bolt11) is the pay method.
  • Returns / side effects: { ok: false, reason: 'not_configured' } — it does not throw.
  • Used by: Default createApp invoicePayer.

Function: authRoutes

  • Purpose: Hono sub-app for LNURL-auth.
  • Inputs: AuthRouteDeps: store, now, publicBaseUrl.
  • Returns / side effects: Hono app mounted at /auth.
  • Used by: createApp.

Function: bearerToken

  • Purpose: Parses Authorization: Bearer <token>.
  • Inputs: Header string or undefined.
  • Returns / side effects: Token or null.
  • Used by: meRoutes.

Function: brandRoutes

  • Purpose: Serves favicon.ico, favicon.svg, apple-touch-icon.png from public/.
  • Inputs: BrandRouteDeps.read.
  • Returns / side effects: Hono app with three GETs; 404 empty body if bytes missing.
  • Used by: createApp at /.

Function: claimSession

  • Purpose: Consumes an authenticated challenge and issues a session.
  • Inputs: store, now, pollToken.
  • Returns / side effects: SessionResult pending/authenticated/expired/used.
  • Used by: GET /auth/session.

Function: completeCallback

  • Purpose: Verifies wallet sig+key against k1, upserts account, marks challenge authenticated.
  • Inputs: store, now, { k1, sig, key }.
  • Returns / side effects: { ok: true, accountId, firstLogin } or { ok: false, reason }.
  • Used by: GET /auth/lnurl/callback.

Function: confirmVerification

  • Purpose: Checks the nonce the user read from the wallet payment comment (21gifts <hex>), not a nonce returned by startVerification.
  • Inputs: store, now, account, nonceRaw.
  • Returns / side effects: Success marks the address verified, or a ConfirmVerificationCode.
  • Used by: POST /me/lightning-address/verification/confirm.

Function: createApp

  • Purpose: Wires CORS, requestLog, brand, health, info, auth, me, lightning-address.
  • Inputs: Optional AppDeps (store, clock, payer, fetch, cache, readBrand, origins, publicBaseUrl).
  • Returns / side effects: Hono app. Used by Bun.serve in index.ts and by tests via app.request().
  • Used by: Boot path and every HTTP test.

Function: encodeLnurl

  • Purpose: bech32-encodes an HTTPS URL as lnurl1… (LUD-01).
  • Inputs: url string.
  • Returns / side effects: Bech32 LNURL.
  • Used by: startChallenge.

Function: healthRoute

  • Purpose: Hono app: GET /{ status: 'ok', service, version }.
  • Inputs: None.
  • Returns / side effects: Mounted at /healthz.
  • Used by: Probes.

Function: infoRoute

  • Purpose: Hono app: GET / → service name, version, description, repo.
  • Inputs: None.
  • Returns / side effects: Mounted at /info.
  • Used by: Service discovery.

Function: lightningAddressRoutes

  • Purpose: Public LUD-16 resolve with cache.
  • Inputs: LightningAddressRouteDeps cache, now, fetchImpl.
  • Returns / side effects: Hono GET /.
  • Used by: GET /lightning-address.

Function: logEvent

  • Purpose: One JSON line on console.warn (ts + event + fields). Never log secrets.
  • Inputs: event string, optional LogFields.
  • Returns / side effects: void.
  • Used by: Auth, me, lightning-address, requestLog.

Function: meRoutes

  • Purpose: Authenticated account routes.
  • Inputs: MeRouteDeps store, now, payer, fetchImpl.
  • Returns / side effects: Hono at /me.
  • Used by: createApp.

Function: normalizeLightningAddress

  • Purpose: Lowercases and validates local@domain LUD-16 shape.
  • Inputs: raw string.
  • Returns / side effects: Canonical address or null.
  • Used by: me lightning-address POST and public resolve.

Function: normalizePublicBaseUrl

  • Purpose: Trims trailing slash; rejects empty.
  • Inputs: raw env string or undefined.
  • Returns / side effects: Base URL or null. Auth routes then respond HTTP 500 Server auth is not configured.
  • Used by: startChallenge via auth routes.

Function: parseBindAddr

  • Purpose: Parses host:port bind spec.
  • Inputs: addr string.
  • Returns / side effects: { host, port }. Throws on garbage.
  • Used by: index.ts boot.

Function: randomHex

  • Purpose: CSPRNG hex for k1 / poll tokens / session tokens / nonces.
  • Inputs: byteLength.
  • Returns / side effects: Lowercase hex.
  • Used by: Auth challenge + session + verification.

Function: readPublicBrandFile

  • Purpose: Reads public/<name> relative to a root directory.
  • Inputs: BrandFileName and optional root (default process.cwd()).
  • Returns / side effects: Uint8Array or null if missing. Does not change the process cwd.
  • Used by: Default brandRoutes reader.

Function: requestLog

  • Purpose: Hono middleware: http.request JSON after the handler. Skips /healthz and OPTIONS. Never logs the query string.
  • Inputs: None.
  • Returns / side effects: MiddlewareHandler.
  • Used by: createApp.

Function: requestPayInvoice

  • Purpose: LNURL-pay: fetch metadata, then GET the callback with amount and optional comment query params (LUD-06), return bolt11.
  • Inputs: RequestPayInvoiceArgs.
  • Returns / side effects: LnurlPayResult.
  • Used by: Verification payer path when a real InvoicePayer is wired; app donate uses the browser equivalent.

Function: resolveAllowedOrigins

  • Purpose: CORS allow-list from CORS_ALLOWED_ORIGINS or the built-in app origins.
  • Inputs: env record.
  • Returns / side effects: string[] of origins.
  • Used by: createApp CORS.

Function: resolveBindAddr

  • Purpose: BIND_ADDR from env with default 0.0.0.0:3000.
  • Inputs: optional override, env.
  • Returns / side effects: Address string.
  • Used by: index.ts.

Function: resolveLnurlp

  • Purpose: GET https://domain/.well-known/lnurlp/local and parse metadata.
  • Inputs: address + fetchImpl.
  • Returns / side effects: Callback URL + min/max sendable or error.
  • Used by: lightningAddressRoutes, requestPayInvoice.

Function: resolveSession

  • Purpose: Looks up a bearer session; rejects expired.
  • Inputs: store, now, token.
  • Returns / side effects: Account or null.
  • Used by: meRoutes.

Function: startChallenge

  • Purpose: Mints k1, poll token, LNURL pointing at {base}/auth/lnurl/callback.
  • Inputs: store, now, baseUrl.
  • Returns / side effects: StartChallengeResult.
  • Used by: GET /auth/lnurl.

Function: startVerification

  • Purpose: Pays a 1-sat LNURL-pay invoice to the linked address and stores a nonce.
  • Inputs: StartVerificationArgs (store, payer, fetch, accountId, now).
  • Returns / side effects: Sent result or a StartVerificationCode (no address, payer down, …).
  • Used by: POST /me/lightning-address/verification.

Function: verifyAuthSig

  • Purpose: secp256k1 verify: DER sig of k1 by linkingKey.
  • Inputs: k1, sig, key hex.
  • Returns / side effects: true iff the wallet signed this challenge.
  • Used by: completeCallback.

HTTP endpoints

Endpoint: DELETE /me/lightning-address

  • Purpose: Bearer required. Clears the account Lightning Address.
  • Errors: 401 without session.
  • Used by: unlinkLightningAddress in the app.
  • Auth: See Purpose — Bearer where stated, else public.

Endpoint: GET /apple-touch-icon.png

  • Purpose: PNG brand mark (apple-touch). Cache-Control: public, max-age=86400.
  • Errors: 404 empty body when public/apple-touch-icon.png is missing.
  • Used by: iOS home-screen icon crawlers.
  • Auth: See Purpose — Bearer where stated, else public.

Endpoint: GET /auth/lnurl

  • Purpose: Creates a login challenge. JSON: lnurl, k1, pollToken.
  • Errors: HTTP 500 { error: 'Server auth is not configured' } if PUBLIC_BASE_URL is unset.
  • Used by: App startLnurlAuth.
  • Auth: See Purpose — Bearer where stated, else public.

Endpoint: GET /auth/lnurl/callback

  • Purpose: Wallet hits this with k1, sig, key (LUD-04). Success body { status: 'OK' }. Query is never written to http.request logs.
  • Errors: HTTP 200 { status: 'ERROR', reason } on missing params or bad signature (LUD-04).
  • Used by: Wallet of Satoshi / any LNURL-auth wallet.
  • Auth: See Purpose — Bearer where stated, else public.

Endpoint: GET /auth/session

  • Purpose: Header X-Poll-Token. Returns pending until the callback succeeds, then { status: 'authenticated', token, account }.
  • Errors: expired/used after TTL or reuse.
  • Used by: App pollSession.
  • Auth: See Purpose — Bearer where stated, else public.

Endpoint: GET /favicon.ico

  • Purpose: Windows ICO (RGBA PNG-in-ICO) of the 21.gifts mark. Content-Type: image/x-icon, Cache-Control: public, max-age=86400.
  • Errors: 404 empty body when public/favicon.ico is missing.
  • Used by: Browsers opening api.21.gifts.
  • Auth: See Purpose — Bearer where stated, else public.

Endpoint: GET /favicon.svg

  • Purpose: SVG mark, orange 21 on black. Content-Type: image/svg+xml, Cache-Control: public, max-age=86400.
  • Errors: 404 empty body when public/favicon.svg is missing.
  • Used by: Modern browsers preferring SVG icons.
  • Auth: See Purpose — Bearer where stated, else public.

Endpoint: GET /healthz

  • Purpose: Liveness. { status: 'ok', service, version }. Not logged as http.request.
  • Errors: Always 200 if the process is up.
  • Used by: Orchestrators, e2e, Uptime checks.
  • Auth: See Purpose — Bearer where stated, else public.

Endpoint: GET /info

  • Purpose: Service metadata (name, version, description, repo).
  • Errors: 200 JSON.
  • Used by: Humans and service catalogs.
  • Auth: See Purpose — Bearer where stated, else public.

Endpoint: GET /lightning-address

  • Purpose: Query address=local@domain. Resolves LUD-16, cached 5 minutes on success.
  • Errors: 400 invalid, 502 unresolved.
  • Used by: App donate resolveLightningAddress.
  • Auth: See Purpose — Bearer where stated, else public.

Endpoint: GET /me

  • Purpose: Bearer session. Current account JSON (id, linkingKey prefix fields, lightning address, verified flag, role).
  • Errors: 401 if missing/expired.
  • Used by: App fetchMe.
  • Auth: See Purpose — Bearer where stated, else public.

Endpoint: POST /me/lightning-address

  • Purpose: Body { address }. Stores unverified LUD-16 on the account.
  • Errors: 401/400.
  • Used by: App setLightningAddress.
  • Auth: See Purpose — Bearer where stated, else public.

Endpoint: POST /me/lightning-address/verification

  • Purpose: Triggers the 1-sat proof-of-control payment. JSON { status: 'sent', expiresInSeconds, sats }. The nonce is not returned to the client; it is only in the LUD-12 wallet comment.
  • Errors: 401 { error: 'Unauthorized' }; 409 { error: 'No Lightning Address linked' } or { error: 'Lightning Address already verified' }; 502 { error: 'Lightning Address did not accept the verification payment' }; 503 { error: 'Verification payments are not configured' }.
  • Used by: App startLightningAddressVerification.
  • Auth: See Purpose — Bearer where stated, else public.

Endpoint: POST /me/lightning-address/verification/confirm

  • Purpose: Body { nonce }. Marks the address verified when the invoice was paid.
  • Errors: 401 { error: 'Unauthorized' }; 400 { error: 'Expected a JSON body with a "nonce" string' } or { error: 'Incorrect verification code' }; 409 { error: 'No verification in progress' } or { error: 'Verification expired' }.
  • Used by: App confirmLightningAddressVerification.
  • Auth: See Purpose — Bearer where stated, else public.

Stand 92d96b641ccd