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

34 KiB

title type audience authority status source
arts/session — design consultation record decision-log human + agent 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 historical 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:

// 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.):

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:

// 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":

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

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

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

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

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.

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.

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.

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.

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.

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.

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.

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.

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.

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(...), 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.