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/docs/decisions/design-session.md

858 lines
33 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

---
title: arts/session — design consultation record
type: decision-log
audience: human + agent
authority: historical design record — the session lifecycle design space (adopt/revoke/refresh, profile loading, SSR via cookie reader); superseded by src/arts/session/README.md as the current reference
status: historical
source: moved from src/arts/session/DESIGN.md (2026-07-03, arts-docs-reconciliation B2; kept verbatim)
---
# `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`, `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
> (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 |
| `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 |
| `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>
// 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' },
logger: { level: LogLevel.INFO, transports: [...] },
storage: { adapter: localAdapter },
http: { baseUrl: 'https://api.example.com' }
});
App.langs.t(...)
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
- Http `logger` → injected automatically
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, preference projection, 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
- `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.