Files
tools-legacy/openspec/changes/cropper-token-auth/design.md
T
adebisi-fa 796094f61b feat(cropper): save with the session token — no OTP, domain from a2loft
Consumes the a2loft cropper-token-auth change: the save now sends the
session's PocketBase token as a bearer credential and no phone/otp. The
backend authorizes with OpenFGA crop_image (domain:<code> -> domain:idid),
so an operator no longer needs super-user rights in the profile's domain —
which is what made saves fail with a blanket 'Invalid OTP specified.'.

- the save's domain comes from a2loft's own record of the picture
  (dims/picture/{uploadId} -> domainCode), falling back to the queue label,
  guarded by a path-safety pattern instead of the catalog check
- the 'core' -> 'uc' alias is deleted: 'core' is a2loft's own pseudo-domain
  for a profile picture (special-cased there), so rewriting it changed the
  attribution and the ID-card notification
- queue loads are enriched with the profile's domainCode; the panel shows
  the profile's domain and, when it differs, the picture target
- the cropper's OTP field, state and parameters are gone; the fingerprint
  wizard is untouched (its own OTP flow on its own endpoints)
- tests updated: bearer + no query auth, resolved domain wins, unsafe values
  refused, no OTP field served (46 pass)

Requires the a2loft change deployed first.
2026-10-01 12:37:25 +01:00

2.1 KiB

Design

Context

See proposal.md. The a2loft cropper-token-auth change adds token mode to the profile-picture endpoints: Authorization: Bearer <pb token>, identity from the token's phone, authorized with OpenFGA crop_image on domain:<code> then domain:idid. This change consumes it.

Goals / Non-Goals

Goals: the cropper saves with the session token and no OTP; the save's domain is a2loft's own record of the picture; the panel is truthful about the profile's domain.

Non-Goals: the fingerprint wizard's OTP flow; the cropper's lookup (there is none — pictures come from the queue); removing OTP support in a2loft.

Decisions

  1. Token sent from the server, never exposed to the browser beyond the page's existing injected session: the proxy attaches Authorization: Bearer ${req.session.token}.
  2. Domain from a2loft, not the client: dims/picture/{uploadId} is the record of the picture; its domainCode is the profile's identity domain and its domain is the picture target (core). Sent as domainCode, displayed as both. Falls back to the queue item's label when a2loft has no record (custom ids), guarded by a path-safety pattern.
  3. The core → uc alias is deleted: core is a2loft's own pseudo-domain (special-cased in getUserByPhoneAsync/updatePicture), so rewriting it — as the old bot did for its own calls — changed attribution and the notification. Authorization no longer depends on the domain, so no alias is needed.
  4. Queue loads are enriched with domainCode for display (one extra read-only a2loft call per picture).

Risks / Trade-offs

  • [a2loft record unavailable for an upload] → falls back to the queue label under the path-safety check; the save still carries the token for authz.
  • [Operators outside the domain's super-users] → irrelevant now: the OpenFGA check with the domain:idid fallback authorizes them.

Migration Plan

Deploy a2loft first (dual-mode, no client change), then this change; the cropper saves without an OTP. Rollback = revert the proxy and re-add the field.

Open Questions

None.