v0.4.6 — audit remediation, privacy retention, release polish
Pre-release
Pre-release
[0.4.6] - 2026-06-27
Changed
- Admin and operator check-in: desktop camera renders inline in the main panel (stats sidebar stays visible); mobile keeps fullscreen camera overlay; operator mobile autostart with "Use camera" button to reopen after close; desktop inline shows lookup errors in the scan bar area and renders full AttendeeCard (items, notes, undo) below the camera preview
- Attendee note modal includes a one-line reminder not to record medical, dietary, or other sensitive personal data
- Staff SPA Content-Security-Policy allows HTTPS branding logo URLs in setup wizard and settings preview (aligned with existing
font-src https:for theme fonts) - Privacy docs: retention tables distinguish product-automated cleanup (sessions, email snapshots) from operator-controlled data (IP logs, attendee lists); OIDC IdP group membership documented; attendee erasure documented as API-only (no SPA delete button)
SECURITY.md: Trivy HIGH remediation SLA (30 days when fix available); blocking HIGH gate deferred to v1.0deploy/README.md: container startup documents auth-state and email snapshot retention purge steps- README: MFA first-login flow notes backup-code acknowledgment is persisted in the database
Added
- Account self-service at
/accountfor all signed-in staff: profile, password change (re-auth), TOTP enrollment/reset, and session management (/api/account/*) - Nullable
User.password_hashfor OIDC-only accounts (additive migration; existing rows unchanged) - IAM Users & roles page at
/admin/users: staff user table (search, filters, pagination), invite-user modal, edit-user modal with role management, reset MFA/password/sessions, and role-assignments tab GET/POST/PATCH /api/admin/usersand role grant/revoke, reset-2fa, reset-password, revoke-sessions endpoints with anti-lockout guards and audit logging (superadmin; org admin may grant/revoke operator@event only)GET /api/admin/role-assignmentsfor non-instance role grantsmust_change_passwordonUserwith server-rendered/change-passwordflow after login or admin password reset- First-run setup: server-rendered
/setupfor empty-database superadmin bootstrap; 5-step React onboarding wizard (system checks, mail, org branding, first event, completion) gated bysetup_completeinSystemSettings - Settings audit log viewer: superadmin-only paginated table of
AdminAuditLogentries with action-type and date filters (GET /api/admin/audit-log) - Event reports page at
/admin/events/:id/reports: admission stats, hourly CSS chart, ticket-type breakdown, paginated admission log, CSV export, and printable HTML/PDF export viaGET /api/admin/events/:eventId/reportsand/reports/export; exports writereports_exportedto the event audit log - Admin attendee erasure now has a GDPR-ready
DELETE /api/admin/events/:eventId/attendees/:idpath that removes dependent delivery, wallet, and check-in rows in one transaction while preserving an event-scoped audit entry (PRIV-001, PRIV-004)
Fixed
- Check-in camera result panel shows the actual admission timestamp on repeat scans instead of a hardcoded "Entered earlier today" subtitle
- Attendee note modal privacy hint is linked to the textarea via
aria-describedbyfor screen readers - Ops health/readiness rate limiters import audit logging via
@admitto/auth/auditso unit tests do not load the full auth barrel (flakyPrismaClientinit in CI) - Account page (
/account): profile Save disabled when unchanged; live password-confirm mismatch feedback; Spinner loading states; SPALinknavigation to account from staff/operator shells @admitto/authrunInTransactionno longer value-importsPrismaClient, avoiding flakyhealthz-rate-limitunit tests when the auth barrel loads beforeprisma generate- First-run
POST /setupbootstrap race: Serializable transaction re-checks empty user table so only one superadmin can be created - IAM anti-lockout guards (
last_superadminon role revoke and superadmin deactivation) run atomically in Serializable transactions; idempotent role DELETE returns 204 instead of 500 when the assignment is already gone; 404 when the assignment exists under a different user id in the URL - Setup wizard no longer bypassed on
/operator—setup_completeincluded on/api/auth/mefor instance superadmins - Event slug helper truncates before trimming trailing dashes; wizard step 4 disables Continue when slug is empty and uses max slug length 80 (aligned with API and CreateEventModal)
- DB partial unique index enforces at most one instance-scoped superadmin
RoleAssignment - Admin mutations that write
AdminAuditLognow persist audit rows in the same database transaction as the primary change (users IAM routes, mail settings PUT); audit failure rolls back the mutation instead of leaving inconsistent state (BE-001, BE-002) - Concurrent attendee import commits for the same event are serialized with a PostgreSQL advisory lock so duplicate bulk audit rows cannot be written (BE-004)
- Agency
public_refbackfill processes attendees in bounded batches instead of loading all rows at once (BE-005) - User deactivation revokes sessions after the Serializable user-update transaction commits, avoiding serialization conflicts with concurrent
last_seen_atupdates (Bugbot) - Import commit transaction timeout raised to 120s so a second commit can wait on the per-event advisory lock without aborting mid-queue (Bugbot)
Attendee.rsvp_statusnow has a database CHECK constraint matching the application enum, preventing invalid RSVP states from raw SQL or future scripts (DATA-002)OidcRoleGrantnow uses partial unique indexes for scoped and instance grants, correctly enforcing uniqueness whenscope_idisNULL(DATA-004)- Attendee, check-in, and event item state status columns now have database CHECK constraints matching persisted application values (DATA-003)
- Destructive migration scanning now flags
DELETE FROMDML in new Prisma migrations unless explicitly approved (DATA-006) - Staff account creation audit metadata and import CLI skipped-row output now redact email addresses, and privacy/DSAR docs now document attendee-note special-category risk plus manual erasure FK ordering (PRIV-001, PRIV-002, PRIV-003, PRIV-004, PRIV-006)
- Container startup now wraps the agency
public_refbackfill in a 120-second timeout, and PR-Agent comments are limited to collaborators/members/owners to prevent public API-credit drain (INFRA-002, INFRA-003) - Container startup now attempts a best-effort purge of expired/revoked
SessionandTrustedDevicerows after migrations/backfills with a 120-second timeout; operators can also runnpm run cli -w @admitto/auth -- purge-auth-retention --dry-runto preview counts, reducing stale auth-state retention (DATA-001, PRIV-005) - Container startup now nullifies stale
EmailDelivery.rendered_html/rendered_subjectsnapshots on terminal deliveries older than 60 days (configurable viaEMAIL_DELIVERY_SNAPSHOT_RETENTION_DAYS); operators can preview counts withnpm run cli -w @admitto/mail-delivery -- nullify-delivery-snapshots --dry-run(DATA-005, PRIV-001)
Security
- Forced password change is now enforced as a dedicated
change_password_requiredsession stage: a user whose password was reset by an admin cannot reach any protected route (API or UI) until they set a new password — the previousnext: change_passwordhint was a UI directive only and could be ignored by any HTTP client (IAM-001) - Backup recovery codes must be acknowledged before a full session is granted, even after a fresh login or when the completion request lands on a different process: acknowledgment is now persisted on
UserMfaMethod(backup_codes_acknowledged_at) instead of an in-memory stash, closing a bypass where a returning user could skip saving recovery codes (IAM-002) - Forced password-change form now enforces the same 12-character minimum as all other self-set passwords (was 8) (IAM-003)
- Granting
superadmin@instanceto a second user now returns HTTP 409single_superadmin_limitinstead of an unhandled 500 (IAM-004) - Known limitation: OIDC group→role mappings are fully reconciled at OIDC login only; deployments using OIDC-managed admin/superadmin roles should set admin session TTL to 8h or less and follow the documented offboarding runbook (accepted risk, IAM-005)
- Attendee CSV export now uses the shared CSV formula-injection sanitizer (covers newline-prefixed formulas and whitespace-padded
=) across attendee, event-settings PII, and reports exports (SEC-001) - Printable HTML/PDF event report export now sends a restrictive Content-Security-Policy header (SEC-003)
EmailDelivery.errorsanitization now redacts URLs (e.g. Power Automate webhooks) before persistence (BE-006)- REVOKED check-in audit rows are written inside a transaction for consistency with other check-in paths (BE-003)
- Known limitation: automated post-event attendee PII purge deferred to v1.0 — use Attendees export + per-attendee
DELETEAPI (no SPA delete button yet) - Known limitation: OIDC group→role reconciliation runs at OIDC login only — shorten admin session TTL and follow offboarding guidance in
SECURITY-CONTROLS.md - Known limitation: Account page TOTP enrollment shows an
otpauth://URI string; HTML/mfa/enrollshows a QR code — SPA QR deferred to v0.4.7 - Known limitation: ticket token may appear in server access logs when operators open ticket URLs — accepted risk; control via reverse-proxy log retention
Deploy
- Container image:
ghcr.io/solarssk/admitto:0.4.6(rolling:0.4) - Migrations apply automatically on container start (with pre-migration backup when pending). No manual
migrate deploy.