# Design ## Context `id-image-bot` is three things: a Telegram intake bot (handlers disabled in code), a picture-review pipeline (Azure queue `id-images`, Minio `files` bucket, nodered review/process endpoints), and the operator web tool ("Smart Photo Editor": Vue 2 + vue-croppa, phone/OTP login against a2loft). Only the web tool moves; the bot and its deployment stay. The a2loft save route (`POST /:domainCode/profile/picture`) accepts only `?phone=&otp=` — no key variant — so the operator OTP survives, but only at save time. See proposal.md for motivation. ## Goals / Non-Goals **Goals:** - One login story: YOUreka session everywhere; the cropper adds only a save-time OTP (a2loft's requirement). - Uniform `crop_image` authorization over every proxied cropper endpoint, cheap for the portal (read cache, live at save). - Faithful behavior: same queue/review/nodered flows and review-reason list. **Non-Goals:** - Moving the Telegram bot; decommissioning `spxa.yrka.app`. - Changing the review/reject workflow or the review-reason catalog. - Replacing toastr/swal UX patterns wholesale. ## Decisions 1. **Namespaced router, `tools/cropper.js` with DI.** `createCropperRouter({ minio, queue, fetch, requireApi, cropGate, a2loftBase, ... })` — the same injectable pattern as the app factory, so tests fake minio/queue/upstreams. Mounted at `/cropper` in server.js; the UI uses relative request paths. 2. **Unified login.** The bot app's phone+OTP middleware validated every request; here the YOUreka session replaces it, and the one a2loft call that needs an OTP (picture save) takes an operator-entered OTP forwarded with the session phone and the picture's domain. Phone prefill from `/auth/session`. 3. **`crop_image` gate with read cache.** `permit.js` grows `cropPermission( phone, {force})` beside the domain checker: same verifyOnce plumbing, `relation: 'crop_image'`, object `domain:idid`, no fallback; results cached per phone for 5 minutes, save forces live. Router-level middleware covers every cropper route uniformly. 4. **Credentials via env, URLs hardcoded.** `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`, `AZURE_STORAGE_CONNECTION_STRING`, `NODERED_HTTP_USERNAME`, `NODERED_HTTP_PASSWORD`, `CARVEKIT_API_KEY` — same names the bot app used, so an existing `.env` is drop-in. Clients are built only when the env is present; without it the router answers 503 and the rest of the app is unaffected. 5. **Restyle to the tools design language.** Same stylesheet and panel structure as `/fingerprint` (navbar, step chips, badges, buttons); toastr stays for toasts. No behavior change beyond namespacing and auth. 6. **Native `FormData`** (Node 22) replaces the `form-data` dependency for the cleanup proxy. ## Risks / Trade-offs - [a2loft `validateOtp` may not recognize the YOUreka session phone format] → the fingerprint walkthrough already saw a2loft accept the session phone on lookups; the field pass confirms on the save path before cutover. - [Cached crop gate lets a revoked operator act for ≤5 minutes] → reads only; the save verifies live, and a2loft's OTP check remains the hard gate. - [Review/process endpoints depend on nodered availability] → unchanged from the bot app; failures surface as route errors, not crashes. - [Two auth domains (mnet/ucth) disappear from the login] → the save validates against the picture's domain; the field pass exercises a real item. ## Migration Plan 1. Land the cropper in `tools/` while `id-image-bot` keeps running unchanged. 2. Field pass: login → gate → load a real queued item (or by ID) → crop → clean → save with OTP; confirm the picture lands in a2loft. 3. Repoint the landing card (done in the same change); leave `spxa.yrka.app` running until the team confirms the new home. 4. Rollback: the old deployment still serves the tool. ## Open Questions None — auth unification, secret handling and scope were confirmed with the operator.