You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/arts/session/DESIGN.md

852 lines
33 KiB

# `arts/session` — Design Consultation Document
> **Historical note 2026-05-14.** This consultation predates the directory
> rename sweep. Current canonical artifact names are `langs`, `logger`,
> `format`, `frontend` (legacy), `storage`, `session`, `connection`, `cache`,
> `timer` and `active-app`. Mentions of `lang`, `logr`, `fmts`, `fend`,
> `stor`, `sess`, `conn`, `cach`, `timr` and `aapp` below are historical
> context, not current naming doctrine.
>
> **Purpose.** Lay out the design space for `arts/session` so external reviewers
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
> (humans or AI) can evaluate proposed designs independently. The author has
> already iterated several times and proposed multiple incompatible designs;
> this document is intentionally neutral about which answer is correct. It
> should give you everything needed to form an opinion from first principles.
>
> **Audience.** Software architects familiar with TypeScript, SvelteKit and
> auth/session patterns. Read sections 1–6 for context; sections 7–9 are the
> actual decisions to evaluate.
---
## 1. Project context
### What is being built
A SvelteKit application at `G:\dev\svelte\active` whose runtime
infrastructure lives under `src/arts/`. Each subdirectory is an independent
"artifact" (zero-deps, runtime-focused, headless-of-UI). Eight artifacts
already exist:
| Artifact | Layer(s) | Purpose |
| ---------- | ---------------------------------------- | ------------------------------------------------- |
| `lang` | `EngineLangs`, `ActiveLangs`, `MonoLangs` | i18n: type-safe translations, BCP 47 resolution |
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
| `logr` | `EngineLogger` | Structured logger: levels, transports, filters |
| `fmts` | `EngineFormat`, `ActiveFormat` | Localized formatting: numbers, currency, units, dates |
| `sium` | `EngineSium` | Validation contracts (Standard Schema interop) |
| `adom` | `ActiveDom` | Reactive DOM service |
| `fend` | `ActiveFrontend` | Frontend preferences (theme, mode, dir, density) |
| `stor` | `EngineStorage`, `ActiveStorage` | Reactive sync key/value with pluggable adapters |
| `http` | `EngineHttp` | HTTP client with retry, schema validation, hooks |
| `aapp` | `ActiveApp` | Composition root that wires the rest |
A ninth artifact, `arts/sess`, is being designed: **session lifecycle
infrastructure on the client**.
### Quality bar
The author has stated repeatedly: "professional/impeccable" library quality.
Zero deps. Strict types. Comprehensive tests. Production-grade docs. The
artifacts will be consumed by many developers.
---
## 2. Established patterns from existing artifacts
### 2.1 `Engine*` vs `Active*` convention
- **`Engine*`** — public methods over private state (or no state at all).
Pure factory; locale, logger or any volatile input is passed as argument
on every call. Lives in `.ts` files, runes-free, safe to import from
`+page.server.ts` / `hooks.server.ts`.
- **`Active*`** — an `Engine*` that exposes public reactive state. Lives in
a `.svelte.ts` file because it owns `$state`. Imports must target the file
directly, not the barrel, to keep the rest of the artifact runes-free.
### 2.2 Generic-on-construction patterns
Each artifact handles consumer-defined types differently:
```ts
// langs: generic on the schema parameter
createEngineLangs<S extends LangNode>(schema: S): EngineLangs<S>
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
// Consumer's S extends LangNode flows through.
// stor: generic per entry, NOT per engine
const Storage = createActiveStorage(opts); // engine-level, not generic
const theme = Storage.entry<string>('theme', 'base'); // generic at entry call
// sium: generic at primitive call sites
sium.object({ x: sium.string() }) // schema construction is typed; engine is monomorphic
// http: generic per request via opt-in schema parameter
http.get<S>(url, { schema?: S })
// Returns Out<S> = StandardSchemaV1.InferOutput<S> | unknown
```
### 2.3 Composition via `createActiveApp`
`createActiveApp(options)` builds the runtime members. Every member is
**always present** — when not configured, App provides a structurally
identical adapter (mono lang, in-memory storage, etc.):
```ts
const App = createActiveApp({
langs: { schema, defaultLocale: 'es' },
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
logger: { level: LogLevel.INFO, transports: [...] },
storage: { adapter: localAdapter },
http: { baseUrl: 'https://api.example.com' }
});
App.langs.t(...)
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
App.storage.entry(...)
App.http.get(...)
```
### 2.4 Sium-on-App exception
Sium is intentionally **not** a member of App. Validation is page-scoped —
pages with forms construct their own engine via a one-line helper:
```ts
// In service schema:
const App = createActiveApp({
services: { sium: defineEngineSium() }
});
// At call site:
const sium = App.sium;
const result = await sium.validate(LoginSchema, input);
```
The helper takes **zero arguments** because Lang, Logger and the active
locale all flow from App. From `feedback_minimal_apis.md`:
> When designing helpers on `ActiveApp` that build dependent objects, do NOT
> accept an `options` parameter for fields that App already owns.
### 2.5 Auto-wiring between artifacts
`createActiveApp` already auto-wires obvious cross-artifact integrations:
- Storage diagnostics → shared `Logger`
- Lang `setLogger(Logger)` (so DEV warnings flow through Logger)
- Format `localeSource` derived from Lang
- Frontend `localeSource` shared with Format
- Http `logger` → injected automatically
- Frontend `persist` → wires through Storage
The pattern: pairs of artifacts that always go together get auto-wired.
### 2.6 `arts/stor.entry<T>` precedent (relevant for sess)
`arts/stor` is the closest existing precedent for "reactive value container
with lifecycle":
```ts
interface ActiveStorageEntry<T> {
readonly key: string;
readonly fullKey: string;
/** Reactive — the only public reactive property in the artifact. */
current: T;
set(value: T): void;
update(fn: (prev: T) => T): void;
remove(): void;
onChange(fn): () => void;
dispose(): void;
}
```
Pattern: opaque `T`, single reactive property `current`, plus methods.
`Engine*` returns `entry()` instances; `Active*` adds the reactive `current`
getter. The engine doesn't know what `T` is.
---
## 3. Memory-encoded constraints
These are explicit rules from `.claude/projects/.../memory/*.md`. The author
has reaffirmed each multiple times in conversation:
### 3.1 No domain in arts/
From `project_layer_boundaries.md`:
> `arts/*` are **runtime infrastructure** — logging, i18n, formatting,
> validation contracts, DOM service, frontend preferences, storage, app
> composition. Each is independent, runtime-focused, and headless of UI
> choices.
> Things that **do not** belong in `arts/`:
> - Forms (state binding to inputs, generated UI)
> - Generated components (buttons, inputs, dialogs)
> - **Stores for domain state** (cart, user, session) — Svelte 5 runes in
> `.svelte.ts` modules where they belong, not under `App.Stores`.
> Rule: when evaluating a new artifact proposal, ask: "could this run
> server-side or in a Web Worker?" If no, it belongs above `arts/`.
### 3.2 Minimal API surface
From `feedback_minimal_apis.md`:
> When designing helpers on `ActiveApp` that build dependent objects, do not
> accept an `options` parameter for fields that App already owns.
> Reasoning: "si están todos en la App, no tiene porque pasar ningún
> parámetro". Adding overrides duplicates what the App already provides as
> the single source of truth, invites inconsistency between layers, and
> bloats the API surface for cases that don't exist in practice.
### 3.3 Professional design preferences
From `feedback_professional_design.md`:
> Patterns explicitly rejected during prior artifact builds:
> - i18n coupled into the logger
> - `minLevel` single cutoff on transports (replaced by `levels` per-severity)
> - Runtime arg-shape discrimination (explicit > magical)
> - `any` in public types (use `unknown` + type guards or proper unions)
> - Directory-name prefixes in external identifiers
> - 4-level log hierarchy (upgraded to 6 to match industry standard)
### 3.4 Standard Schema as the validation contract
All artifacts that accept user-defined schemas use the Standard Schema v1
interface (`$libs/standard-schema`) so consumers can pass Sium / Zod /
Valibot / ArkType interchangeably. **No vendor lock-in.**
### 3.5 `arts/sess` was the topic of prior parking-lot entries
From `project_deferred_decisions.md` (current draft):
> `arts/sock` (real-time WebSocket / SSE) — parked. Real-time push is a
> real gap but deliberately deferred until a concrete consumer surfaces.
`arts/cache` (TanStack-Query-style reactive cache) is also parked — a
reactive cache with dedup/staleTime/gcTime sits above stor and sess.
---
## 4. Prior art summary
What other libraries do, briefly. **Conclusions are intentionally not
drawn — they are inputs for the reviewer.**
| Library | Storage | Refresh | User | Perms | Cross-tab | Notable |
| ---------------- | -------------------- | ------------------------ | ----------------- | ------------------- | ---------------- | -------------------------------------------------------------------- |
| **Lucia v3** | Server DB + cookie | Rolling on validate | `userId` (FK) | n/a | implicit | Server-only. `Register` augmentation for session/user attrs. |
| **iron-session** | Encrypted cookie | Manual `save()` | Opaque `<T>` | n/a | implicit | 4 KB hard limit. Password rotation supported. |
| **Supabase** | Pluggable, default localStorage | Pre-emptive ticker (30s tick, 90s margin) + visibility + 401 | Strongly typed `Session.user` | n/a (server RLS) | `BroadcastChannel` | `_refreshingDeferred` dedup. ~3000 LOC monolith. |
| **better-auth** | DB + signed cookie cache | Rolling on getSession | Inferred from server | First-class plugins | None at lib | Cookie chunking (3896 B). Plugin-driven. |
| **Auth.js** | DB + JWT | Provider-driven | Module augmentation | None | Cookie-implicit | Provider-coupled (OAuth in scope). |
| **Clerk** | Opaque session ID | Pre-emptive | First-class | `session.has(...)` first-class | `BroadcastChannel` | ~75 KB. `addListener` event bus. |
Common patterns observable across all:
- Refresh dedup via shared promise
- Pre-emptive timer + reactive 401
- `INITIAL_SESSION` synchronous on subscribe (Supabase pattern)
- Some form of cross-tab broadcast
- Visibility-change handling
Common divergences:
- Whether user identity is in the engine or external
- Whether credential shape is named or opaque
- Whether permissions are first-class or external
- Whether the engine assumes JWT or is auth-scheme-agnostic
---
## 5. Use cases the final design MUST support
Any proposed design must handle ALL of the following without mental
gymnastics. If a use case requires unusual ceremony, the design has a
problem.
### UC-1: JWT/OAuth SPA
- Login form returns `{user, accessToken, refreshToken, expiresAt}`.
- Auto-refresh fires before expiry.
- All HTTP requests carry `Authorization: Bearer <accessToken>`.
- 401 from server → refresh → retry the request.
### UC-2: Cookie-based SSR
- Server sets `HttpOnly` cookie on login.
- Client never sees the credential.
- Browser sends cookie automatically with `credentials: 'include'`.
- "Logout" hits a server endpoint that clears the cookie.
### UC-3: API-key SaaS dashboard
- Long-lived API key stored in localStorage (or generated per-tenant).
- No refresh — keys are revoked, not rotated.
- All requests carry `X-Api-Key: <key>`.
### UC-4: Anonymous session + later login (e-commerce)
- Visitor lands, server starts an anonymous session with a `cartId`.
- Visitor adds items to cart without identifying.
- Visitor logs in: same session, now with `user` attached, cart preserved.
### UC-5: Multi-tab logout
- User has 3 tabs open.
- Tab A clicks "Logout".
- Tabs B and C reflect the logout (UI shows login prompt) without polling.
### UC-6: SSR hydration without re-validation
- `+layout.server.ts` reads cookie, validates session against DB, returns
`{user, expiresAt, ...}` to the client via `data`.
- Client adopts the snapshot. Schema validation runs ON THE SERVER ONLY —
client trusts the SSR boundary.
### UC-7: Concurrent 401s during refresh
- 5 in-flight requests all return 401 simultaneously.
- ONE refresh fires, the other 4 share its promise.
- After refresh, all 5 retry with the new credential.
### UC-8: Refresh failure semantics
- Network glitch (transient) → preserve session, retry later.
- Server says "refresh token invalid" (fatal) → revoke locally, emit
`EXPIRED` event, navigate to `/login`.
- Caller must distinguish these without imposing logout-on-flaky-network.
### UC-9: Visibility-change refresh
- User closes laptop lid for 90 minutes.
- Opens it. Tab becomes visible.
- Engine checks expiry, refreshes if needed, before any new request goes
out.
### UC-10: Cross-tab WITHOUT exposing tokens via broadcast
- Tab A refreshes → broadcast fires → tab B re-reads storage and updates
local state.
- Broadcast payload contains event + version, NEVER tokens.
- An XSS in tab A cannot exfiltrate tokens through the broadcast channel.
### UC-11: Three-state user model (newly raised)
The author has stated the design must distinguish:
- **No-identificado** (no session OR anonymous session)
- **Identificado** (session has a user)
- **Autorizado** (session+user+permission for a specific action)
These are three conceptually distinct states the design must be able to
express clearly.
---
## 6. Decisions LOCKED IN (not up for debate)
These came out of audit + iteration cycles and are settled. A proposed
design is invalid if it violates any of them.
### 6.1 Refresh contract
```ts
type RefreshFn = (current) => Promise<NewSession | null>;
// Resolves to NewSession → engine adopts, fires REFRESHED
// Resolves to null → fatal: engine auto-revokes, fires EXPIRED
// Throws → transient: engine preserves session, fires REFRESH_FAILED
```
Rationale: distinguishes "credential is dead" from "network is flaky".
Mismatch = user logged out by a flaky wifi.
### 6.2 Generation guard
The engine maintains a `generation` counter incremented on every
adopt/revoke. `refresh()` captures the counter at start and discards the
result if the counter changed during the await.
Rationale: prevents stale refresh from resurrecting a revoked session
when ticker + visibility + 401 hook + manual revoke + cross-tab all coexist.
### 6.3 Refresh dedup
Concurrent `refresh()` calls share a single in-flight promise
(`refreshingDeferred` slot, Supabase pattern).
Rationale: prevents stampede when N requests fail with 401 simultaneously.
### 6.4 `RevokeResult` is tagged
```ts
type RevokeResult =
| { localRevoked: true; globalRevoked: true; scope: 'global' }
| { localRevoked: true; globalRevoked: false; scope: 'local';
reason?: 'missing_revoke_url' | 'network_error' | 'server_rejected' };
```
`revoke()` is **never** `Promise<void>`. Caller must be able to distinguish
"signed out everywhere" from "signed out locally only" — UI copy and
security decisions depend on it.
### 6.5 `INITIAL_SESSION` synchronous on subscribe
`onChange(listener)` invokes the listener **synchronously once** with
`('INITIAL_SESSION', current)` so subscribers are the single source of
truth — no race between "read snapshot" and "subscribe".
### 6.6 Broadcast carries event + generation only
The `BroadcastChannel` payload is `{type, event, generation}`. NEVER
includes tokens, user data, or any sensitive fields. Receiving tabs
re-read storage as the source of truth.
### 6.7 `expiresAt` as epoch ms internally
Internal storage and broadcast use `number` (epoch ms). Public APIs
accept `Date | number | string` and normalize at the boundary.
### 6.8 `adoptServer` is a separate method, not a flag
```ts
adopt(input): Promise<...> // validates against schema
adoptServer(input): SessionSnapshot // skips validation, normalizes only
```
Rationale: `adopt(snapshot, { trusted: true })` is a footgun (caller might
forget the flag); separate method names enforce the distinction.
### 6.9 Auto-refresh wrapper opt-in, not default
```ts
withAutoRefresh(engine, { tickMs, marginMs, jitterMs, refreshOnVisible });
```
- Includes `visibilitychange` handling (battery saver + recovery)
- Includes jitter to avoid N-tab stampede
- Cleanup function returned
### 6.10 Standard Schema validation (no vendor lock)
Any user-supplied schema is `StandardSchemaV1<unknown, T>`. Works with
Sium, Zod, Valibot, ArkType identically. The artifact only depends on the
type interface (~30 LOC in `$libs/standard-schema`).
### 6.11 Out of scope
The following are NOT `arts/sess` responsibilities:
- OAuth flows (Google, GitHub, etc.)
- Password handling / hashing
- MFA / TOTP / WebAuthn
- JWT signature verification
- CSRF protection (delegate to SvelteKit `csrf`)
- Server-side session storage (DB, Redis) — separate `arts/sess-server` if needed
- Acquisition flow (login form, credential exchange) — consumer territory
---
## 7. OPEN design questions
Each question lists 2–4 options with concrete pros/cons. **The reviewer is
expected to form an opinion based on the philosophy in §2–3 and the use
cases in §5, not on the author's framing.**
### Q1: Session payload generic shape
What shape does `SessionSnapshot` take?
**Option A — Single opaque payload generic.**
```ts
SessionSnapshot<TPayload> = {
payload: TPayload;
expiresAt: number;
issuedAt: number;
}
```
- (+) Maximum agnosticism; engine knows nothing about user/credential structure
- (+) Mirrors `arts/stor.entry<T>` precedent exactly
- (–) Loses semantic structure (can't distinguish identity from credential)
- (–) `Sess.current.payload.user.name` is verbose
**Option B — User generic + opaque credential.**
```ts
SessionSnapshot<TUser, TCredential = undefined> = {
user: TUser | null; // null = anonymous
credential?: TCredential; // opaque
expiresAt: number;
issuedAt: number;
}
```
- (+) `user: TUser | null` codifies the unidentified/anonymous/identified state machine
- (+) Credential stays opaque (auth-scheme agnostic)
- (–) Two generics; potentially over-modeled
- (–) Forces consumer to think about user shape upfront
**Option C — Three generics: user + credential + claims.**
```ts
SessionSnapshot<TUser, TCredential, TClaims> = {
user: TUser | null;
credential?: TCredential;
claims: TClaims;
expiresAt: number;
issuedAt: number;
}
```
- (+) Mirrors Clerk's session.has({permission}) pattern
- (–) `TClaims` is authorization concern — does it belong in sess?
- (–) Three generics is a lot
**Option D — Lucia-style fixed shape with module augmentation.**
```ts
SessionSnapshot = {
id: string;
user: User; // declared via module augmentation
expiresAt: number;
fresh: boolean;
} & SessionAttributes // also from augmentation
```
- (+) Single shape, consumer extends globally
- (–) Module augmentation is project-global (one shape per app — fine?)
- (–) Doesn't match generic-on-construction patterns elsewhere in `arts/*`
### Q2: Where does "user" live?
Strongly correlated with Q1.
**Option A — In sess as typed field.**
- `Sess.current.user` is the consumer's user object.
- Lifecycle of "user" tied to lifecycle of session.
**Option B — NOT in sess.**
- Sess only knows credential + lifecycle.
- Consumer's separate `.svelte.ts` module loads/caches user from `/api/me`
on `ADOPTED` event.
- Cleanest separation but no engine-level support for "anonymous session
with cart" (UC-4) — consumer fully owns the distinction.
**Option C — Both (minimal subject + full profile separately).**
- Sess has `subject: string | null` (login/email/userId).
- Full `User` profile lives in consumer's user store.
- "Identified" means `subject !== null`.
### Q3: Where does "credential" live?
**Option A — Named `accessToken`/`refreshToken` fields.**
- `Sess.current.accessToken`, `Sess.current.refreshToken`.
- (+) Trivial bearer header construction; first-class refresh contract
- (–) Couples to JWT/OAuth; cookie-only and API-key apps don't fit
**Option B — Opaque `credential: TCredential` generic.**
- Consumer types it as `{accessToken, refreshToken}` for JWT or `{apiKey}`
for API-key auth or `undefined` for cookie auth.
- (+) Auth-scheme agnostic
- (–) Engine can't auto-construct headers; consumer composes
**Option C — Not in sess at all.**
- Cookie-only model: server handles credential, client never sees it.
- For JWT/API-key, consumer stores credential in their own module.
- (–) Loses centralized lifecycle (refresh rotates the credential — needs
a writable target)
### Q4: Perms/roles in sess?
**Option A — `Sess.has({permission, role})` first-class.**
- Typed via `claimsSchema` if provided.
- (+) Convenient call site
- (–) Authorization is per-action runtime decision, not session state
- (–) Forces a permission model on consumers (RBAC vs ABAC vs claims-based)
**Option B — Not in sess.**
- Consumer module exposes `has(permission)`.
- (+) Consumer picks RBAC, ABAC, capability-based, anything
- (–) Every consumer reimplements the wheel
**Option C — Future `arts/auth` or `$libs/auth` artifact.**
- Sess provides primitives; auth artifact provides permission models.
- (+) Composable
- (–) Doesn't exist yet; speculative
### Q5: How is the session engine constructed?
**Option A — Standalone factory, consumer composes with App.**
```ts
const Sess = createActiveSession<MyUser>({...});
const App = createActiveApp({
http: { headers: () => bearerHeader(Sess) ?? {} }
});
```
- (+) Sess fully independent of App; testable in isolation
- (+) Consumer controls construction order
- (–) App can't auto-wire Sess→Http integrations
- (–) Awkward closure pattern when `onRefresh` needs `App.http`
**Option B — App accepts a pre-built Sess instance.**
```ts
const Sess = createActiveSession<MyUser>({...});
const App = createActiveApp({ sess: Sess, http: {...} });
// App.session === Sess; App auto-wires 401 hook
```
- (+) Type flows from Sess to App.session
- (+) App can auto-wire what's auto-wireable
- (–) Two-step setup
- (–) Sess construction can't reference App (circular)
**Option C — App constructs Sess from config.**
```ts
const App = createActiveApp({
sess: { userSchema: MyUserSchema, ... }
});
// App.session = ActiveSession<unknown> (untyped) OR
// ActiveApp<S, TUser> (App is now generic on TUser)
```
- (+) Single setup step; matches Lang/Storage/Http pattern
- (–) App gains generics for TUser/TCredential — surface bloat
- (–) Or App.session is untyped — caller casts everywhere
**Option D — App provides factory; consumer calls it.**
```ts
const App = createActiveApp({
auth: { // infrastructure config
storage: ..., onRefresh: ..., revokeUrl: ...
}
});
const Sess = App.createActiveSession<MyUser>({ // typed factory
userSchema: MyUserSchema
});
// App.session === Sess (cached); typed at call site
```
- (+) Mirrors `App.sium` precedent
- (+) Infrastructure at App, types at call-site
- (+) Type flows correctly via factory generics
- (–) Single-call required (second call must throw or replace)
- (–) Asymmetry: App constructs Sess but not via App config; uses factory instead
### Q6: Where does configuration live?
**Option A — All on call-site of `createActiveSession`.**
- userSchema, storage, onRefresh, revokeUrl, autoRefresh — all together
- (+) One place to look
- (–) Mixes infrastructure (storage) with types (userSchema)
**Option B — Split: infra on App, types on call-site.**
- App config: storage, onRefresh, revokeUrl, autoRefresh
- Call site: userSchema, credentialSchema
- (+) Clean separation
- (–) Two sources of truth to reconcile
**Option C — All on App config; factory takes only generics.**
- App config: everything including userSchema
- Call site: just `App.createActiveSession<TUser>()`
- (–) Tight coupling between App and consumer types
### Q7: Section name in App config
**Option A — `auth: { ... }`**
- Consumer-facing semantic name ("I'm configuring authentication").
**Option B — `sess: { ... }`**
- Matches the artifact name.
**Option C — Both/different name (e.g., `session`, `identity`).**
### Q8: HTTP coupling
How tightly does the design couple `arts/sess` to `arts/http`?
**Option A — App auto-wires both `headers` and `beforeError` hook.**
- Requires sess to expose `bearerHeader()` (assumes JWT/Bearer).
- (+) Zero-config for the common case
- (–) Wrong for cookie auth, API-key auth, custom schemes
**Option B — App auto-wires only `beforeError` hook (refresh is agnostic).**
- Consumer composes headers themselves (3 lines).
- (+) Auth-scheme agnostic
- (–) Consumer must remember to wire bearer if they use it
**Option C — No auto-wiring, consumer composes everything.**
- (+) Maximum control
- (–) Boilerplate at every consumer
**Option D — App auto-wires via consumer-provided header builder.**
```ts
auth: {
bearerHeader: () => Sess.current?.token // consumer provides
}
// App applies it to App.http.headers
```
### Q9: User profile loading — where?
**Option A — In sess (user is part of session snapshot).**
- See Q2 Option A.
**Option B — Consumer `.svelte.ts` store (no library helper).**
- Consumer subscribes to Sess events, loads user from `/api/me`,
caches in `$state`.
- ~30 LOC per app.
**Option C — `$libs/auth` helper module.**
- Provides `createUserStore({ fetcher, cache })`.
- (+) Reusable
- (–) Duplicates what `arts/cache` (deferred) will provide.
**Option D — Defer until `arts/cache` lands.**
- User profile is just a query; let consumers wire it manually until cache
exists; then promote the pattern.
### Q10: Multi-instance support
**Option A — Single `Sess` per app.**
- Second `createActiveSession` throws `SessionAlreadyCreatedError`.
- (+) Simple
- (–) No multi-account UX
**Option B — Multiple `Sess` instances allowed.**
- Each has its own storage key, refresh, etc.
- App.session is the "active" one; can be switched.
- (–) Complicates auto-wiring (which Sess does http use?)
**Option C — Single Sess, but with internal "scopes".**
- (–) Speculative; defer.
---
## 8. Decision dependencies
How the questions interact:
- **Q1 ↔ Q2 ↔ Q3** — payload shape decides where user/credential live.
- **Q5 ↔ Q6** — factory location determines where config lives.
- **Q4 ↔ Q9** — permissions and user are both auth-layer concerns; if one
is in sess, the other probably should be too (or strictly neither).
- **Q3 ↔ Q8** — auto-wiring headers is only possible if credential shape
is known to the engine.
- **Q5 (Option D) ↔ Q6 (Option B)** — the factory-on-App pattern naturally
splits config: App config for infra, factory call for types.
- **Q7 ↔ Q5** — section name should match where the config lives.
---
## 9. Cross-AI evaluation framework
When evaluating a proposed design, ask:
1. **Does it work for ALL 11 use cases (UC-1 to UC-11)?** If any case
requires unusual ceremony, the design has a problem.
2. **Does it respect §3 constraints?** Specifically: no domain in arts/,
minimal surface, professional preferences (no `any`, no magical
discrimination, opaque generics).
3. **Does it match existing patterns in §2?** Engine/Active split, Standard
Schema, opaque generics, auto-wiring conventions, Sium-on-App helper.
4. **Does it survive `arts/cache` landing later?** A design that builds a
custom user-store mechanism becomes redundant when cache exists.
5. **Does it work for cookie-only auth (UC-2)** where the client has NO
credential? Many auth-tutorial designs assume JWT and break here.
6. **Is the auth-scheme agnosticism real or aspirational?** Test by mentally
instantiating UC-1, UC-2, UC-3 and seeing whether the API forces
different code paths or scales naturally.
7. **Is the three-state machine (UC-11) expressible without consumer code
tracking previous values?** The engine should emit the events that
distinguish the transitions.
8. **Can each piece be tested in isolation?** Engine without runes,
wrappers without engine, etc.
9. **Is the API surface small?** Count public exports. The author has
stated a strong preference for minimal API.
---
## 10. Prior implementation that was rejected
A first implementation of `arts/sess` was built and rejected by the author.
What it did:
- `SessionSnapshot<TUser, TClaims>` with `user`, `claims`, `accessToken`,
`refreshToken` all as named fields.
- `userSchema` and `claimsSchema` required at engine construction.
- `bearerHeader()` method on the engine.
- `has({permission, role})` method on the engine.
- Generics flowed through `App<S, TUser, TClaims>`.
- App auto-wired bearer header into `App.http`.
Reasons for rejection (in author's own words):
> "no entiendo porque session y autentication estan tan acoplados, eso es
> una mierda y donde caen todos los sistemas, el diseño es una puta mierda.
> deberian de estar en dos modulos separados session a nivel de arts a con
> un la informacion de session que no la veo, el usuario deberia de
> definirse con un interface minimo, login, name, date start, pero no
> acoplado a session y el modulo auth a nivel de libreria"
> "se debe de exponer un usuario como generico en session pero la auth no
> le corresponde a session y los permisos tampoco, eso corresponde a auth y
> el modelo de permisos que se van a propoercionar, al igual que no tiene
> sentido el acoplamiento al token porque ya has definido que todo va a ser
> por jwt y si quiero otro sistema de autenticacion y autorizacion ?"
> "tampoco has tenido en cuenta el estado de un usuario que puede ser
> no-identificado | identificado | autorizado"
> "la app tiene que propoercionar de crear la session
> app.createActiveSession<MiUser>(...), la configuracion de auth debera de
> estar definida explicitamente a nivel de la app"
Distilled feedback (interpreted, may contain bias):
1. Session and authentication should be separated.
2. User SHOULD be a generic on session, but permissions and authentication
should NOT be in session.
3. Don't couple to JWT/Bearer (e.g., named `accessToken` field assumes one
auth scheme).
4. The state machine (unidentified | identified | authorized) must be
first-class.
5. App should provide the session factory: `App.createActiveSession<TUser>(...)`.
6. Auth configuration should be explicit at App level.
These are interpretive — the reviewer should not assume any one
implementation matches the author's intent; instead, evaluate which design
options in §7 best satisfy these statements simultaneously.
---
## 11. What the author is asking for
A design that:
- Resolves the 10 open questions in §7 with explicit reasoning.
- Provides TypeScript signatures for the public API (`SessionSnapshot`,
`EngineSession`, `ActiveSession`, factories, hooks, events).
- Shows the App composition shape (config section + how Sess is built).
- Demonstrates how each of the 11 use cases is implemented in 5–10 lines of
consumer code.
- Identifies any locked-in decisions in §6 that the design forces a change
to (and justifies it).
- Calls out trade-offs honestly — "I picked X over Y because Z, but
acknowledge that Y is better for use case Q".
The reviewer should NOT assume any design proposed earlier in the
conversation is correct. The author has explicitly stated distrust of the
prior design iterations.
---
## Appendix A — Existing artifact code references
Key files for context:
- `src/arts/active-app/active-app.svelte.ts` — composition pattern
- `src/arts/active-app/types.ts` — `ActiveAppOptions` shape
- `src/arts/storage/active-storage.svelte.ts` — `entry().current` reactive pattern
- `src/arts/storage/types.ts` — `SyncStorageAdapter`, `withBroadcast` interface
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
- `src/arts/http/types.ts` — `HttpResult` tagged union, hook lifecycle
- `src/arts/http/engine-http.ts` — `headers: () => ...` lazy hook pattern
- `src/arts/sium/engine-sium.ts` — `App.sium` precedent
- `src/libs/standard-schema.ts` — Standard Schema v1 type port
---
## Appendix B — Locked-in pieces of the engine implementation
These are implementation details from the prior iteration that the author
considered correct and would carry over regardless of the high-level design:
- Generation guard (counter + capture-then-validate-on-resolve)
- `refreshingDeferred` slot for in-flight dedup
- `applyExternalChange` helper for cross-tab broadcast receivers (re-reads
storage rather than trusting payload)
- `validateAndNormalize` helper that runs schemas + normalizes time fields
- `commitAdoption` / `commitRevocation` helpers that bump generation and
dispatch events atomically
- `withAutoRefresh(engine, opts)` as a separate composable wrapper (not
baked into the engine)
- `extractJwtExp(token)` as opt-in helper in a separate file
- `readSessionFromCookies(cookies, opts)` as SSR helper
These can be reused regardless of the public API decisions in §7.

Powered by TurnKey Linux.