The Invariant
One credential, one boundary. A credential minted for one surface never silently works on another: edge keys drive the sync channel, PIN sessions drive kernel actions, portal sessions drive the tenant API, admin sessions drive operations. Least privilege is structural — a bridge token physically cannot open a lot.
The Contract
| Credential | Minted by | Valid on | Scope |
|---|---|---|---|
| Operator PIN session | POST /api/v1/auth/login (edge) |
/api/v1/actions/*, /api/v1/auth/*, /api/v1/audit/verify |
role-scoped (manifest permissions) |
Edge API key (bootstrap claim) |
POST /api/v1/bootstrap/claim |
/api/v1/sync/* |
sync (default) / full |
| Portal session | POST /api/v1/portal/login or pi_* invite claim |
/api/v1/tenant/*, /api/v1/portal/* |
owner / member |
| Admin session | POST /api/v1/admin/login |
/api/v1/admin/* |
ops only, every mutation audit-logged |
Role registry (production-line manifest): MAGAZZINIERE (receiving triggers + reads), OPERATORE (work triggers), QUALITA (inspection triggers), ADMIN (wildcard). Roles live in the manifest’s rbac_engine — each vertical declares its own.
The Flow
edge: PIN → /auth/login → session (12h TTL, ax_sessions, revocable)
PIN shared? → 409 PIN_AMBIGUOUS {candidates:[{operatorId,name}]}
→ re-send with operatorId
cloud: email+password → /portal/login → session (12h)
email on >1 tenant? → 401 TENANT_AMBIGUOUS → pass tenant_id
sync: bt_* single-use token → /bootstrap/claim → {tenant_id, api_key}
re-claim ROTATES the key — old key dies instantly
Payload Example
# Edge operator session
curl -s http://edge:8291/api/v1/auth/login -H 'content-type: application/json' \
-d '{"tenantId":"<uuid>","deviceCode":"IPC-01","pin":"1234"}'
# → {"token":"…","expiresInHours":12,"context":{"tenantId":"…","operatorId":"…","role":"MAGAZZINIERE","deviceId":"IPC-01"}}
# Cloud portal session
curl -s https://cp.axiom-trace.eu/api/v1/portal/login -H 'content-type: application/json' \
-d '{"email":"qa@customer.com","password":"…"}'
# → {"token":"…","tenant_id":"…","role":"owner","display_name":"…"}
Known Failures
| Symptom | Cause → fix |
|---|---|
409 PIN_AMBIGUOUS |
PIN shared → re-send with operatorId from candidates |
401 TENANT_AMBIGUOUS |
Email on multiple tenants → pass tenant_id explicitly |
401 on /tenant/* with ak_* |
Key scope is sync → portal surface needs a portal session or full-scope key |
403 with valid login |
Origin binding (portal_origin) or role ceiling (member vs owner-only) |
Login works, actions 401 |
Session TTL 12h — refresh by re-login; never cache tokens beyond TTL |