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:
parent
d6eb8cda9c
commit
41d6283f20
3 changed files with 142 additions and 0 deletions
97
qr-device-login/README.md
Normal file
97
qr-device-login/README.md
Normal 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).
|
||||
22
qr-device-login/app.manifest.yaml
Normal file
22
qr-device-login/app.manifest.yaml
Normal 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
|
||||
23
qr-device-login/tsconfig.base.json
Normal file
23
qr-device-login/tsconfig.base.json
Normal 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"]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue