|
|
---
|
|
|
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.
|