SvelteKit load functions fire during link prefetch before Novi auth completes; `if (browser)` guards do not prevent this. Moving all IDAA data fetching into $effect hooks gated on `novi_verified || trusted_access` closes the IDB pre-population race across archives, bb/[post_id], and recovery_meetings/[event_id]. Also documents the Auth-Before-Cache rule and per-route status in AE__Permissions_and_Security.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9.1 KiB
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:
publicis a site-wide unlock (anyone with the passcode).authenticatedverifies 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:
$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:
- Components must never write to
$ae_loc.edit_mode— only the system menu toggle and sign-out/permission-drop handlers may change it. - Edit mode is only available to
trustedand above in 95% of modules (the toggle is hidden from lower-access users). - Edit mode persists across navigation — it is NOT reset by page loads or component mounts.
- Sign-out and permission drops to below
authenticatedshould resetedit_modetofalse.
Background: A bug was fixed (2026-02-27) where
ae_comp__badge_obj_view.sveltewas writing$ae_loc.edit_mode = falsein 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:
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.
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
authenticatedlevel 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:
- Load/layout
.tsfiles = thin shells. Pass URL params only. No API calls. Noif (browser)data fetching. - Data loading =
$effectin.sveltefiles, gated on:if (!$idaa_loc.novi_verified && !$ae_loc.trusted_access) return; - Three IDB purge paths in
(idaa)/+layout.svelte(auth failure, anonymous no-UUID, Reset & Retry button) cleardb_posts,db_archives, anddb_eventstables.
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—$effectgate added;bb/+page.tsstrippedidaa/bb/[post_id]/+page.ts— stripped; loading handled by trigger inbb/+layout.svelteidaa/archives/+page.svelte—$effectgate added;archives/+layout.tsstrippedidaa/archives/[archive_id]/+page.svelte—$effectgate added;[archive_id]/+page.tsstrippedidaa/recovery_meetings/+page.svelte—$effectgate already present;+layout.tsstrippedidaa/recovery_meetings/[event_id]/+page.svelte—$effectgate added;+page.tsstripped
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_KEYinstead. - Security model: API key is one layer; JWT +
x-account-idscoping provides the primary auth. - Do not introduce new usages. Prefer
PUBLIC_AE_BOOTSTRAP_KEYfor unauthenticated lookups.
Email Display
Non-trusted users must never see a full email address. Obscure using:
// 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 whenprint_count >= 1 - Edit mode for badges: limited to
trusted_accessusers (toggle hidden from lower levels) person_passcodefield (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_accessor higher for module access.
Common Template Patterns
<!-- Gate on trusted access -->
{#if $ae_loc.trusted_access}
<!-- Gate on edit mode (always check trusted too — edit mode alone is insufficient) -->
{#if $ae_loc.trusted_access && $ae_loc.edit_mode}
<!-- Gate on administrator -->
{#if $ae_loc.administrator_access}
<!-- Show full vs obscured email -->
{$ae_loc.trusted_access ? email : obscure_email(email)}
Never gate purely on
$ae_loc.edit_modewithout also checking a permission level. Edit mode is a UI preference, not a permission grant.