feat(qr-device-login): Add QR code login feature documentation, update metadata in app manifest, and configure TypeScript for new functionality

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-04-08 23:51:18 -07:00
parent d6eb8cda9c
commit 41d6283f20
3 changed files with 142 additions and 0 deletions

97
qr-device-login/README.md Normal file
View file

@ -0,0 +1,97 @@
# @lilith/qr-device-login
Reusable QR-based cross-device session transfer. Enterprise-grade: mTLS admin
API, public scan landing, in-memory token store with TTL + DoS cap.
Follows the RFC 8628 device-authorization-grant shape adapted for same-user
session transfer between devices of an already-logged-in account.
## Why a service, not a library?
The previous implementation was a self-contained route module vendored into two
feature backends (`@features/my`, `@features/admin`). It worked, but any new
consumer had to own the code — which meant `qrcode` as a dep, in-memory state
per process, and drift between forks.
This app owns **only** token lifecycle, QR encoding, and scan landing.
Session minting stays with the consuming app — each consumer mints its own
cookie after exchanging a one-time code over mTLS.
## Architecture
```
Desktop (logged in) qr-auth.lilith.live Consuming app
│ │ │
│ 1. POST /api/device-link/ │ │
│ start ─────────────────────────────────────────────▶ createSession
│ │ ◀── mTLS ──────────────── (client SDK)
│ │ │
│ ◀── {id, qrDataUrl} ────────────────────────────────────── │
│ render QR + poll │ │
│ │ │
│ │ │
Phone scans QR ─▶ GET /scan/:token │ │
│ mark scanned, mint code │
│ 302 callbackUrl?id=&code= │
│ ─────────────────────────▶ callback handler
│ │ ◀── exchangeCode
│ │ ── mTLS ──▶
│ burn token, return metadata │
│ ─────────────────────────▶ createToken()
│ │ setSessionCookie()
│ │ 302 /
```
**Invariant**: the service never sets a cookie for any consuming domain.
It only confirms "this scan was real" and hands back opaque metadata.
## Packages
| Package | Purpose | Published |
|---------|---------|-----------|
| `server/` | Bun HTTP service (private) | No |
| `shared/``@lilith/qr-device-login-protocol` | Wire types shared by server + client | Yes |
| `client/``@lilith/qr-device-login-client` | Server-side SDK for consuming apps (mTLS via undici) | Yes |
| `react/``@lilith/qr-device-login-react` | `<DeviceLoginQR />` + `useDeviceLogin` hook | Yes |
## Consumer integration (3 thin proxy routes)
```ts
// backend: 3 thin proxy routes
import { createQrLoginClient } from '@lilith/qr-device-login-client';
const qr = createQrLoginClient({
baseUrl: process.env.QR_AUTH_BASE_URL!,
cert: readFileSync(process.env.QR_AUTH_CERT!),
key: readFileSync(process.env.QR_AUTH_KEY!),
ca: readFileSync(process.env.QR_AUTH_CA!),
});
// POST /api/device-link/start (authenticated)
const session = await qr.createSession({
callbackUrl: `${PUBLIC_BASE}/auth/device-link/callback`,
});
// GET /api/device-link/poll/:id (authenticated)
const status = await qr.pollSession(id);
// GET /auth/device-link/callback?id=&code= (public)
const { metadata } = await qr.exchangeCode(id, code);
// → createToken() + setSessionCookie() → 302 '/'
```
```tsx
// frontend
import { DeviceLoginQR } from '@lilith/qr-device-login-react';
<DeviceLoginQR
startUrl="/api/device-link/start"
pollUrl="/api/device-link/poll"
onSuccess={() => location.reload()}
/>
```
## Deploy
See `deploy/` — systemd unit + nginx site config for `qr-auth.lilith.live` on
vps-0. Consumer certs live under `deploy/pki/` (private keys gitignored).

View file

@ -0,0 +1,22 @@
name: qr-device-login
description: |
Reusable QR-based cross-device session-transfer service.
Consumers wrap via published SDK packages; session issuance stays with
each consuming app. mTLS-authenticated admin API + public scan landing.
category: auth
host: vps-0
services:
- name: qr-device-login
kind: systemd
unit: qr-device-login.service
workdir: /opt/qr-device-login
ports:
- { port: 8787, role: loopback, description: "Bun service, fronted by nginx" }
nginx:
sites:
- qr-auth.lilith.live
packages:
publish:
- shared # @lilith/qr-device-login-protocol
- client # @lilith/qr-device-login-client
- react # @lilith/qr-device-login-react

View file

@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"strict": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"exclude": ["node_modules", "dist", "build"]
}