Polish the Journal Entry Config modal to match the desired section outline, hide alert messaging unless enabled, update the shared draft typing for entry flows, and replace deprecated privacy icons. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
11 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'
}
x-no-account-id — Narrow Transport Exception
x-no-account-id is a transport-level escape hatch that strips account context before the request leaves the frontend. It is not a permission grant and it is not a replacement for JWT or x-account-id.
Use it only when the request truly cannot be made account-scoped. Current legitimate cases should stay narrow:
- Bootstrap / site-domain discovery before the account is known.
- Explicit public or guest endpoints that do not have an account context.
- Helper paths that intentionally need a global-default fallback.
If a request already has a valid account context, prefer x-account-id and let the JWT carry session identity. Treat any new x-no-account-id use as temporary until it is reviewed and either replaced or justified.
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.
JWT usage guidance
- JWTs are the preferred proof of an established session. Keep them attached to authenticated flows instead of leaning on transport-level bypasses.
- If a route or helper can work with a JWT and an account ID, it should not need
x-no-account-id. - If a helper still needs the bypass today, document the reason and add a removal target.
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
Journals — Entry Config Admin Actions
- Entry configuration admin controls are gated to
trusted_accessand above. manager_accessandadministrator_accesssee the Delete action, which performs a hard delete.trusted_accessusers see Remove instead, which follows disable semantics rather than a hard delete.- The Admin section is the place for staff notes, enabled/default access state, and destructive entry actions; the template toggle belongs in Metadata, while visibility/audience flags remain separate.
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.