Skip to main content

Slice 1 — Identity & Audit

The foundational slice. Builds authentication, authorization, session management, and the tamper-evident audit log. Every subsequent slice's mutations write through this slice's audit helper.

Status: Built. Auth + roles + per-staff PINs + audit-event/mutation writes are live across every mutating endpoint via recordMutation(). The remaining slice-1 follow-ons are API-level gating (today the cookie identifies the user; most endpoints don't refuse anonymous callers) and the daily chain-hash verification cron.

Drafted from planning · v0.1

Scope

  • Staff table + roles + permissions (role defaults + per-staff overrides)
  • Sign-in flow (PIN entry, lockout, session cookie)
  • Sign-out + idle timeout
  • Audit-chain schema (audit_events + audit_mutations)
  • Audit helper (withAudit) used by every mutating endpoint
  • Daily chain-verification cron
  • Admin reset code path (466687)

Schema

  • staff — see staff entity
  • roles — Sys Admin, Owner, Sales, Mechanic, Junior, Service Lead
  • role_permissions — defaults per role × screen
  • staff_screen_permissions — per-staff overrides; wins over role
  • screens — Today, Sales, Customers, Service, Inventory, Trades, Rentals, Orders, Reports, Settings
  • staff_sessions — active sessions
  • auth_attempts — failed sign-in attempts (for rate limit)
  • audit_events — chain-linked; build_version column added in migration 013 (build versioning)
  • audit_mutations — before/after snapshots
  • device_sessions — Layer-1 (OAuth) device cookies; added migration 015. One row per authenticated browser, 30-day sliding expiry. See ADR-0026.
  • shop_config.authorized_google_emails — JSON allowlist column added migration 015. Seeded with Swicked's day-one emails.
  • idempotency_records — added migration 014 for offline-arch Slice 2
  • staff_messages — added migration 032. Lightweight 1-on-1 DMs between signed-in staff. Text only (1-2000 chars), no attachments, no groups. CHECK (from_staff_id != to_staff_id) and a soft-delete flag (preserved for audit; never hard-deleted).
  • shop_config.idle_lockout_seconds, roles.idle_lockout_seconds, staff.idle_lockout_seconds — added migration 018. Three-tier resolution COALESCE(staff, role, shop_config). NULL = inherit; 0 = never lock; 15..3600 = seconds. CHECK constraints enforce the range so an Owner can't accidentally pick a 5-second lockout that would interrupt every transaction.
  • shop_settings — added migration 019. Manifest-driven settings registry: the JS side is helmSettings.registerSection() + helmSettings.registerSetting() in public/index.html; settings render in both the search-driven Settings tab and the in-situ section-header popover. Key-value table; mutations via PUT /api/settings/:key audit through recordMutation with audit_mutations.trigger_source ∈ {settings-tab, in-situ} (two new values widening the existing column's vocabulary — no schema change to audit_mutations itself).
  • shop_config.fiscal_year_end_mmdd — added migration 035. MM-DD string, default '12-31'. Drives YTD anchoring for the Sales report endpoint and the YTD pill on the Sales screen. Editable from Settings → Store Info and surfaced in the new-shop orientation questionnaire. Validated server-side as a real calendar date.
  • integration_gbp — added migration 036. Single-row Google Business Profile connection state. See integrations.
  • custom_integrations — added migration 037. Owner-defined third-party integrations with five auth-type templates. See integrations.

Endpoints

Layer 1 (Google OAuth + device sessions) — added 2026-05-13, ADR-0026:

  • GET /api/auth/google/start — generate state + PKCE code-verifier, redirect to Google's authorization endpoint
  • GET /api/auth/google/callback — exchange code → tokens, validate email against shop_config.authorized_google_emails, mint device_sessions row + helm_device cookie, redirect to /
  • POST /api/auth/google/signout — mark current device_sessions row revoked, clear helm_device cookie
  • GET /api/auth/allowlist — read the authorized-emails list (Sys Admin only)
  • POST /api/auth/allowlist — add an email to the allowlist
  • DELETE /api/auth/allowlist — remove an email

Layer 2 (PIN) — unchanged from earlier:

  • POST /api/auth/login — PIN entry, returns helm_session cookie (requires valid helm_device cookie)
  • POST /api/auth/logout — clears session
  • GET /api/auth/me — returns the currently-signed-in staff plus the resolved idle_lockout_seconds (migration 018 — COALESCE(staff, role, shop_config)). Frontend uses this instead of a hard-coded IDLE_MS constant.
  • POST /api/auth/reset-pin — admin code path
  • GET /api/staff — list (Settings → Staff visible only)
  • PUT /api/roles/:id — update role-level properties (today: idle_lockout_seconds only) ✓
  • GET /api/staff/:id — detail
  • PUT /api/staff/:id — edit
  • PUT /api/staff/:id/pin — set PIN
  • DELETE /api/staff/:id/pin — clear PIN
  • PUT /api/staff/:id/screens — set per-staff permission overrides
  • DELETE /api/staff/:id/screens — clear all overrides (reset to role defaults)
  • GET /api/roles — list roles
  • GET /api/screens — list screens
  • PUT /api/roles/:id/permissions — change role defaults (not built; not needed in v0.1)

UI

Settings → Staff & Permissions:

  • List of staff cards
  • Per staff: role dropdown, per-screen visibility checkboxes, Select/Deselect All, "Reset to role defaults", PIN management
    • Add staff button

Sign-in overlay:

  • Always present on app load (no Sign In button anywhere)
  • Triggered by idle timeout
  • 5-digit PIN entry; lockout after 5 wrong attempts
  • "I forgot my PIN" → admin reset code modal

Migration from AIM

9 staff records carried forward; AIM permission model not migrated. Initial PIN set by Sys Admin on first sign-in. See migration from AIM.

What's built

  • All endpoints listed above ✓
  • Sign-in overlay UI ✓
  • Idle timeout (60s) ✓
  • PIN management (set/clear/reset) ✓
  • Per-staff overrides for screen visibility ✓
  • Admin reset code (466687) direct sign-in ✓
  • Audit helper (recordMutation) called on every mutating endpoint ✓ — writes both audit_events + audit_mutations (before/after JSON, diff) per mutation
  • Audit recent-feed endpoints (GET /api/audit/recent, GET /api/audit/events/:id) ✓
  • POST /api/audit/manual — bridge endpoint for client-only flows (e.g., the Beta Feedback system) to land in the audit chain. Wrapped in withIdempotency (ADR-0015 implementation, migration 014) so client retries on the same key replay the original response instead of double-writing. ✓
  • GET /api/build-info — current build identity (version, git_sha, built_at, environment); used by the user-menu version pill and by Beta Comments to snapshot the build at click time. See build versioning. ✓
  • Build version stamped on every audit row — migration 013 added audit_events.build_version + filtered index; recordAuditEvent passes the module-level BUILD_VERSION constant on every INSERT (mutations, auth, manual). ✓
  • Permissions module v0.5.0 (live 2026-05-23, migration 053 + public/js/helm-perms.js) ✓ — the substrate has existed since migration 001 (staff → role_id → roles ← role_permissions → permissions) but only screen.* keys were ever populated. This migration ships the full catalog (~95 dot-notation permission keys covering sales.*, service.*, inventory.*, customers.*, reports.*, sys_admin.*, screen.*), the 6 canonical role grants (sys_admin, owner, manager, mechanic, sales, junior — sys_admin is finally seeded by this migration; everything that referenced ADMIN_ROLE_CODES already included it), and a new staff_permission_overrides table with override semantics distinct from migration 016's staff_screen_permissions (016 = REPLACE: rows-present-means-those-are-the-set; 053 = OVERRIDE: allow/revoke individual perms on top of role defaults, with expires_at for time-bound grants and granted_by_staff_id + reason for audit). Idempotent (INSERT OR IGNORE + CREATE TABLE IF NOT EXISTS). Client-side enforcement: helm-perms.js (~190 LOC) fetches /api/me/permissions on boot, exposes window.helmCan(perm) + window.helmPermissions + window.helmCurrentRole. DOM walker hides (or disables, per data-perm-mode="disable") any element with data-perm="some.perm.key" the user doesn't hold; walker re-runs on helmPermsRefresh() for dynamic modals.
  • Staff messages (migration 032, live 2026-05-16) — lightweight 1-on-1 DMs between signed-in staff. Polling-based to keep the Worker side cheap:
    • badge poll every 30s (GET /api/messages/unread-count)
    • inbox poll every 10s when open (GET /api/messages/inbox)
    • thread poll every 5s when open (GET /api/messages/thread/:other_id)
    • send: POST /api/messages { to_staff_id, body }; mark-read: POST /api/messages/mark-read; search: GET /api/messages/search?q=...
    • sender can soft-delete their own messages (is_deleted flag preserved for audit; never hard-deleted). ✓

What's not yet built

  • API-level gating — cookie identifies the user; most endpoints don't refuse anonymous callers. Nav-tab gating in the frontend (per the resolved screen permissions) is the next concrete piece
  • Daily chain-hash verification cron — schema captures prev_chain_hash + chain_hash per event; the cron that walks the chain end-to-end and reports breaks is not wired (no scheduled handlers in the Worker yet)
  • Action-level permissions — only screen-level today; "can refund within sales" needs the screen_actions table
  • CSRF tokens — session cookie SameSite=Lax handles most, but explicit tokens for state-changing endpoints are TBD
  • Rate-limit storage in KVauth_attempts table works but a KV-backed counter would be faster
  • External anchoring — chain hash is local; posting to an external timestamping service is roadmapped

Acceptance criteria for "slice 1 done"

  • Every mutating endpoint writes an audit row ✓
  • Daily cron verifies the chain end-to-end and reports breaks
  • API-level gating refuses anonymous callers on every endpoint per the resolved screen permissions
  • Action-level permissions cover the obvious privileged operations (refunds, deletes of audit-protected entities, settings changes)
  • A KV-backed login rate limit is wired
  • The Staff & Permissions UI shows audit history per staff (last 30 days)

See also