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.

- 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.
Screen: /legal
- 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 onto404.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) insideMarketingLayout.
Screen: /legal
- URL:
/legal— imprint and privacy./legal.htmlpermanently redirects here. - What the user sees: Legal Notice (Switzerland, [email protected]) 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 genericlightning:, or copy the LNURL. The page polls/auth/sessionuntil authenticated, then stays on/loginand shows the signed-in card (role, linking key, Lightning Address form, log out). There is no client redirect to/. - Calls:
LoginCard(includesLightningAddressFormwhen signed in),useLnurlLogin,startLnurlAuth,pollSession,walletOfSatoshiHref,walletOfSatoshiIntentHref,uppercaseLnurl,QrCode,useAuthStore.
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.
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 asGET. - Inputs: Incoming
Requeston 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/loginand/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
nullwhen logged out. - Used by:
LoginCardsigned-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
useLnurlLoginanduseAuthStore. Rehydrates vialoadSession+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), optionallabel. - Returns / side effects: React element.
- Used by:
LoginCardandDonateForm.
Function: RootLayout
- Purpose: Root HTML shell:
lang=en, global CSS, metadata (title, icons, Open Graph, Twitter). - Inputs:
childrenReact 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 (
windowundefined). - Used by:
useAuthStore.clearAuthandLoginCardwhen session hydration gets 401.
Function: confirmLightningAddressVerification
- Purpose: POST
/me/lightning-address/verification/confirmwith 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
/mewith the bearer session. - Inputs:
sessionToken. - Returns / side effects:
Accountornullon 401. - Used by:
LoginCardsession hydration.
Function: formatMsatAsSats
- Purpose: Formats millisatoshis as a sat string for the donate UI.
- Inputs:
msatnumber. - Returns / side effects: Decimal string in sats.
- Used by:
DonateFormamount-range error (minSendable–maxSendable).
Function: getApiUrl
- Purpose: Reads
NEXT_PUBLIC_API_URLvia 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:
userAgentstring. - Returns / side effects:
trueiff/Android/imatches. - Used by:
LoginCardQrView.
Function: loadSession
- Purpose: Reads the bearer token from
localStorage. - Inputs: None.
- Returns / side effects: Token string or
null. SSR-safe. - Used by:
LoginCardon mount.
Function: pollSession
- Purpose: GET
/auth/sessionwithX-Poll-Token. - Inputs:
pollToken. - Returns / side effects:
SessionResult(pending/authenticated/expired/used). - Used by:
useLnurlLogin.
Function: requestDonateInvoice
- Purpose: GET an LNURL-pay callback with
amountmillisatoshis 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:
DonateFormbefore paying.
Function: satsToMsat
- Purpose: Converts whole sats to millisatoshis.
- Inputs:
satsnumber. - Returns / side effects:
sats * 1000. - Used by:
DonateForm(converts sats before callingrequestDonateInvoice).
Function: saveSession
- Purpose: Writes the bearer token to
localStorage. - Inputs:
tokenstring. - 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
Accountwith address cleared. - Used by:
LightningAddressForm.
Function: uppercaseLnurl
- Purpose: Uppercases a bech32 LNURL (LUD-01).
- Inputs:
lnurlstring. - 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-initlocalStorage). - Inputs: Hook. Methods
setAuth,setAccount,clearAuth. - Returns / side effects: Auth state object.
- Used by:
LoginCard,useLnurlLogin,LightningAddressFormon/login(not/).
Function: useLnurlLogin
- Purpose: Hook: start LNURL-auth, poll until authenticated or expired. Returns
{ status, lnurl, start }— there is no separateerrorfield (errors are astatusof'error'). - Inputs: None.
- Returns / side effects:
UseLnurlLoginstatus machine. - Used by:
LoginCard.
Function: walletOfSatoshiHref
- Purpose: iOS/desktop WoS deep link.
- Inputs:
lnurl. - Returns / side effects:
walletofsatoshi:lightning:+ uppercase LNURL. - Used by:
LoginCardprimary 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:
LoginCardprimary CTA on Android.
Function: DELETE
- Purpose: App Router DELETE export on
/me/lightning-address(re-export ofproxyMeLightningAddressDelete). - 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,apiPathbeginning 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:
Requestwith 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:
RequestwithX-Poll-Token. - Returns / side effects: Upstream
Response. - Used by: Route GET
/auth/session.
Function: proxyLightningAddressGet
- Purpose: Proxies GET
/lightning-address. - Inputs:
Requestwithaddressquery. - Returns / side effects: Upstream
Response. - Used by: Route GET
/lightning-address.
Function: proxyMeGet
- Purpose: Proxies GET
/me. - Inputs:
Requestwith 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:
Requestwith 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:
Requestwith 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' }fromsrc/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:
startLnurlAuthin 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
linkingKeydomain is whatever host the encoded callback uses (set on the api asPUBLIC_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:
resolveLightningAddresson/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:
createAppdefault store; all auth/me routes.
Function: InMemoryLnAddressCache
- Purpose: TTL cache for successful LUD-16 metadata resolves.
- Inputs:
get(address, now),put(entry, now). TTL fromLN_ADDRESS_CACHE_TTL_MS. - Returns / side effects:
getreturnsCachedLnAddressornull. - 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
createAppinvoicePayer.
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:
createAppat/.
Function: claimSession
- Purpose: Consumes an authenticated challenge and issues a session.
- Inputs:
store,now,pollToken. - Returns / side effects:
SessionResultpending/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.tsand by tests viaapp.request(). - Used by: Boot path and every HTTP test.
Function: encodeLnurl
- Purpose: bech32-encodes an HTTPS URL as
lnurl1…(LUD-01). - Inputs:
urlstring. - 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:
LightningAddressRouteDepscache, 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:
eventstring, optionalLogFields. - Returns / side effects: void.
- Used by: Auth, me, lightning-address, requestLog.
Function: meRoutes
- Purpose: Authenticated account routes.
- Inputs:
MeRouteDepsstore, now, payer, fetchImpl. - Returns / side effects: Hono at
/me. - Used by:
createApp.
Function: normalizeLightningAddress
- Purpose: Lowercases and validates
local@domainLUD-16 shape. - Inputs:
rawstring. - 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 500Server auth is not configured. - Used by:
startChallengevia auth routes.
Function: parseBindAddr
- Purpose: Parses
host:portbind spec. - Inputs:
addrstring. - Returns / side effects:
{ host, port }. Throws on garbage. - Used by:
index.tsboot.
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:
BrandFileNameand optionalroot(defaultprocess.cwd()). - Returns / side effects:
Uint8Arrayornullif missing. Does not change the process cwd. - Used by: Default
brandRoutesreader.
Function: requestLog
- Purpose: Hono middleware:
http.requestJSON after the handler. Skips/healthzand 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
amountand optionalcommentquery 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_ORIGINSor the built-in apex, transitional app-subdomain, and localhost origins. - Inputs:
envrecord. - Returns / side effects: string[] of origins.
- Used by:
createAppCORS.
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/localand 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:
Accountornull. - 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,keyhex. - Returns / side effects:
trueiff 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:
unlinkLightningAddressin 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.pngis 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' }ifPUBLIC_BASE_URLis 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.icois 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.svgis 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 feb78c9f0369