# Aether — Permissions and Security **Last updated:** 2026-02-27 **Source of truth:** `src/lib/ae_utils/ae_utils__perm_checks.ts`, `src/lib/stores/ae_stores.ts` --- ## Access Level Hierarchy Highest to lowest. Each level **inherits all access from every level below it**. | Level | `access_type` string | Typical Use | | --- | --- | --- | | Super | `super` | OSIT internal — full system access | | Manager | `manager` | Account managers | | Administrator | `administrator` | Event/account admins | | Trusted | `trusted` | **Onsite staff** — site passcode or AE login | | Public | `public` | Site-wide passcode granted | | Authenticated | `authenticated` | Identity verified (e.g. IDAA Novi UUID) | | Anonymous | `anonymous` | Default — not signed in | > **Note on Public vs Authenticated:** `public` is a *site-wide* unlock (anyone with the passcode). `authenticated` verifies a *specific identity*. In the hierarchy, public outranks authenticated because it implies broader site access. --- ## `$ae_loc` Store — Permission Flags `$ae_loc` is a `persisted()` store (backed by localStorage). Key fields: ```typescript $ae_loc.access_type // string: current access type ('anonymous', 'trusted', etc.) // Cumulative boolean flags (true = "you have AT LEAST this level") $ae_loc.anonymous_access // always true $ae_loc.authenticated_access // true from authenticated and above $ae_loc.public_access // true from public and above $ae_loc.trusted_access // true from trusted and above ← most-used gate $ae_loc.administrator_access // true from administrator and above $ae_loc.manager_access // true from manager and above $ae_loc.super_access // true only at super // Exclusive check flags (true = "you are EXACTLY this level") $ae_loc.trusted_check // true only if access_type === 'trusted' $ae_loc.administrator_check // etc. // (rarely needed — prefer the _access flags) // Behavior flags $ae_loc.edit_mode // boolean — user preference, see below $ae_loc.adv_mode // boolean — advanced mode toggle ``` ### Additional intermediate levels (in permission checks, not in hierarchy order) `support`, `assistant`, `verified`, `provisional` — appear in `_access` flags but are not part of the canonical `access_level_order`. Treat as internal/intermediate. --- ## Edit Mode — Critical Rules `$ae_loc.edit_mode` is a **user preference**, not a permission level. **Rules that must never be broken:** 1. **Components must never write to `$ae_loc.edit_mode`** — only the system menu toggle and sign-out/permission-drop handlers may change it. 2. Edit mode is only available to `trusted` and above in 95% of modules (the toggle is hidden from lower-access users). 3. Edit mode persists across navigation — it is NOT reset by page loads or component mounts. 4. Sign-out and permission drops to below `authenticated` should reset `edit_mode` to `false`. > **Background:** A bug was fixed (2026-02-27) where `ae_comp__badge_obj_view.svelte` was writing `$ae_loc.edit_mode = false` in a data-loading `$effect`, silently overriding the user's preference on every navigation to the badge print page. --- ## Authentication Methods | Method | Grants | Used For | | --- | --- | --- | | Site passcode (`site_access_code_kv`) | `trusted`, `public`, or `authenticated` | Onsite staff and event attendees | | AE Username + Password | `trusted` and above | Staff with AE accounts | | Novi UUID | `authenticated` | IDAA members (Novi membership system) | Passcodes are stored per-level in `$ae_loc.site_access_code_kv`: ```typescript site_access_code_kv: { administrator: null, // highest passcode tier trusted: null, // onsite staff passcode public: 'public1980', // example authenticated: 'auth1980' } ``` --- ## Utility Functions ### `process_permission_checks(access_type: string)` Returns a full permission object (`_check` and `_access` flags) for a given access type string. Used when access type changes to update `$ae_loc`. ```typescript import { process_permission_checks } from '$lib/ae_utils/ae_utils__perm_checks'; const checks = process_permission_checks('trusted'); // checks.trusted_access === true // checks.administrator_access === false ``` ### `compare_access_levels(level_a, level_b)` Returns `1` if `level_a` is higher, `-1` if lower, `0` if equal. Useful for threshold comparisons. --- ## Privacy and Security Rules ### IDAA — International Doctors in Alcoholics Anonymous - **ALL IDAA content is private. Always. No exceptions.** - BB (Bulletin Board / Posts), Archives, Recovery Meetings — all require authentication. - IDAA users authenticate via Novi UUID at `authenticated` level or higher. - A prior agent accidentally exposed IDAA BB data publicly — treat any IDAA exposure as Sev-1. #### IDAA IndexedDB (IDB) Caching — Auth-Before-Cache Rule **Root cause discovered 2026-04:** SvelteKit `+page.ts`/`+layout.ts` load functions run *before* layout `$effect` hooks and fire during link prefetch (hover). `if (browser)` guards do NOT prevent this — they only prevent SSR. This means API calls inside these files execute before Novi auth completes, writing private IDAA data to the user's IndexedDB even for unauthenticated sessions. **The fix — established pattern for all IDAA routes:** 1. **Load/layout `.ts` files = thin shells.** Pass URL params only. No API calls. No `if (browser)` data fetching. 2. **Data loading = `$effect` in `.svelte` files**, gated on: ```svelte if (!$idaa_loc.novi_verified && !$ae_loc.trusted_access) return; ``` 3. **Three IDB purge paths** in `(idaa)/+layout.svelte` (auth failure, anonymous no-UUID, Reset & Retry button) clear `db_posts`, `db_archives`, and `db_events` tables. **Auth path matrix:** | User type | `novi_verified` | `trusted_access` | Can load data? | Purge fires? | | --- | --- | --- | --- | --- | | Anonymous / unauthenticated | false | false | No | Yes (Case 1) | | Novi-verified IDAA member | true | false | Yes | No | | Manager / trusted access | false | true | Yes | No (Case 3 exemption) | **Applied to routes (as of 2026-04-19):** - `idaa/bb/+page.svelte` — `$effect` gate added; `bb/+page.ts` stripped - `idaa/bb/[post_id]/+page.ts` — stripped; loading handled by trigger in `bb/+layout.svelte` - `idaa/archives/+page.svelte` — `$effect` gate added; `archives/+layout.ts` stripped - `idaa/archives/[archive_id]/+page.svelte` — `$effect` gate added; `[archive_id]/+page.ts` stripped - `idaa/recovery_meetings/+page.svelte` — `$effect` gate already present; `+layout.ts` stripped - `idaa/recovery_meetings/[event_id]/+page.svelte` — `$effect` gate added; `+page.ts` stripped **When adding a new IDAA route:** never put API calls in `+page.ts`/`+layout.ts`. Always gate data fetching with the `$effect` pattern above. ### Journals - Private personal data. Always authenticated. Passcode/encryption features exist. - Never expose journal content publicly. ### `PUBLIC_AE_API_SECRET_KEY` - Audit closed 2026-03-11. `PUBLIC_*` prefix is by design — key is always in the client bundle. - Anonymous site-domain lookup uses the limited-permission `PUBLIC_AE_BOOTSTRAP_KEY` instead. - Security model: API key is one layer; JWT + `x-account-id` scoping provides the primary auth. - Do not introduce new usages. Prefer `PUBLIC_AE_BOOTSTRAP_KEY` for unauthenticated lookups. ### Email Display Non-trusted users must never see a full email address. Obscure using: ```typescript // joh***@example.com function obscure_email(email: string): string { const at = email.indexOf('@'); if (at < 0) return email; return `${email.slice(0, Math.min(3, at))}***${email.slice(at)}`; } ``` This pattern lives in `ae_comp__badge_obj_li.svelte` — move to `ae_utils` if needed elsewhere. --- ## Module-Specific Permission Patterns ### Events — Badges | Scenario | Visibility | Print Action | Review Actions | | --- | --- | --- | --- | | Anonymous / below trusted | Unprinted only | None (name display only) | Email Review Link button (→ email API) | | Trusted, not Edit Mode | Unprinted only | Clickable (first print) | Email Review Link button | | Trusted, Edit Mode | All non-hidden | Clickable incl. reprint; shows `Nx` count | Email Review Link + direct Review Link (clipboard) | - Print count badge: shown as `Nx` (e.g. `2×`) next to the printer icon when `print_count >= 1` - Edit mode for badges: limited to `trusted_access` users (toggle hidden from lower levels) - `person_passcode` field (for attendee-gated review URL): **not yet in DB** as of 2026-02-27 ### IDAA - Auth gate test must be the **first test** in any test file — privacy enforcement is a hard requirement. - Default required permission: `trusted_access` or higher for module access. --- ## Common Template Patterns ```svelte {#if $ae_loc.trusted_access} {#if $ae_loc.trusted_access && $ae_loc.edit_mode} {#if $ae_loc.administrator_access} {$ae_loc.trusted_access ? email : obscure_email(email)} ``` > Never gate purely on `$ae_loc.edit_mode` without also checking a permission level. Edit mode is a UI preference, not a permission grant.