docs(openspec): archive cropper-token-auth and sync specs
The cropper upload POSTs with the session token and no OTP query auth, and the panel shows the profile's domain and picture target. Field pass was verified earlier (upload 0b90075b, saved with no OTP). Merged the image-cropper delta into the main spec and archived the change.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-01
|
||||
@@ -0,0 +1,51 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Proposal: cropper-token-auth
|
||||
|
||||
## Why
|
||||
|
||||
The cropper's save proves the operator with a phone number and an OTP. a2loft
|
||||
resolves that phone against the domain being saved to, so an operator without
|
||||
super-user rights in that domain is rejected with a blanket `Invalid OTP
|
||||
specified.` — even though they are authorized to crop images for the domain.
|
||||
The a2loft endpoints now accept a PocketBase token (see the a2loft change
|
||||
`cropper-token-auth`), which authorizes with OpenFGA `crop_image` and takes the
|
||||
actor's identity from the token.
|
||||
|
||||
## What Changes
|
||||
|
||||
- The cropper's save posts the **session token** to a2loft instead of `phone` +
|
||||
`otp`; the OTP input is removed from the UI.
|
||||
- The save's domain comes from **a2loft's own picture record**
|
||||
(`dims/picture/{uploadId}` → `domainCode`), falling back to the queue item's
|
||||
label, and the panel shows both (the profile's domain and the picture
|
||||
target).
|
||||
- The `core → uc` alias is deleted: `core` is a2loft's own pseudo-domain for a
|
||||
profile picture, not an alias for `uc`.
|
||||
- The fingerprint wizard's staff lookup and fingerprint save keep their OTP;
|
||||
only the cropper's calls change.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
(none)
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `image-cropper`: the save is token-authenticated (no OTP), the domain comes
|
||||
from a2loft's record, and the OTP requirement is removed.
|
||||
|
||||
## Impact
|
||||
|
||||
- `tools/cropper.js`: save handler (Bearer token, no `?otp=`), domain
|
||||
resolution via dims with the label as fallback, path-safety check replacing
|
||||
the catalog check, queue loads enriched with the profile's domain.
|
||||
- `tools/public/assets/cropper/`: the OTP field and all `otp` state removed;
|
||||
the panel shows the profile's domain and the picture target.
|
||||
- Requires the a2loft `cropper-token-auth` change to be deployed first.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Spec Delta: image-cropper
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Saving requires a live gate and an OTP
|
||||
Saving a cropped picture (`/cropper/profile/picture`) SHALL verify the crop
|
||||
permission live (bypassing the cache) and SHALL authenticate the operator with
|
||||
the session's PocketBase token, sent to a2loft as a bearer credential; no OTP
|
||||
SHALL be collected or forwarded. The picture's domain SHALL come from a2loft's
|
||||
own record for the upload (`dims/picture/{uploadId}` → `domainCode`), falling
|
||||
back to the queue item's label, and SHALL match a path-safe pattern before it
|
||||
is used in the request. A denied save SHALL not reach a2loft.
|
||||
|
||||
#### Scenario: Successful save
|
||||
- **WHEN** a permitted operator saves
|
||||
- **THEN** the route verifies the live gate and forwards the picture to
|
||||
a2loft with the session token as a bearer credential and the profile's
|
||||
resolved domain
|
||||
|
||||
#### Scenario: Denied save does not reach a2loft
|
||||
- **WHEN** the live crop check denies, or the request carries no message body
|
||||
- **THEN** the route answers 403/400 and makes no a2loft call
|
||||
|
||||
#### Scenario: Unsafe domain values are refused
|
||||
- **WHEN** neither a2loft's record nor the queue label yields a path-safe
|
||||
domain
|
||||
- **THEN** the save is refused with 400 and a2loft is not contacted
|
||||
|
||||
### Requirement: Unified login — no separate artist login
|
||||
The cropper SHALL NOT implement its own login. The operator's identity comes
|
||||
from the YOUreka session and is presented to a2loft as the session's PocketBase
|
||||
token; the picture's own metadata carries the domain. The cropper collects no
|
||||
other credential — there is no save-time OTP.
|
||||
|
||||
#### Scenario: Phone prefilled from the session
|
||||
- **WHEN** a signed-in operator opens the cropper
|
||||
- **THEN** their phone is already known to the page without any login form
|
||||
@@ -0,0 +1,18 @@
|
||||
# Tasks
|
||||
|
||||
- [x] 1. Save handler: `Authorization: Bearer <session token>`, no `?phone/otp`;
|
||||
domain resolved from `dims/picture/{uploadId}` (`domainCode`) with the queue
|
||||
label as fallback, guarded by a path-safety pattern; `requireOtp` and the
|
||||
`core → uc` alias deleted.
|
||||
- [x] 2. Queue loads enriched with the profile's `domainCode` for display;
|
||||
load-by-id passes it through.
|
||||
- [x] 3. UI: OTP field, state and call parameters removed; panel shows the
|
||||
profile's domain and the picture target.
|
||||
- [x] 4. Tests: save carries the bearer token and no query auth; the resolved
|
||||
domain wins over the body; unsafe values are refused; no OTP field in the
|
||||
served page; queue load exposes `domainCode` (46 tests pass).
|
||||
- [x] 5. Deploy a2loft and this app, then a field pass: load a queued picture →
|
||||
save with no OTP → confirm the picture lands attributed to the token's phone.
|
||||
Verified: loaded upload 0b90075b-… (panel showed "Domain ucth · picture target
|
||||
core"), no OTP field, save POSTed with the session token and no query auth,
|
||||
answered "Saved — Picture saved for +2348068287222" and reset the panel.
|
||||
Reference in New Issue
Block a user