@marianmeres/ownsuite 2.1.0 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -6,7 +6,7 @@ Machine-readable documentation for AI coding assistants.
6
6
 
7
7
  ```yaml
8
8
  name: "@marianmeres/ownsuite"
9
- version: "2.0.0"
9
+ version: "2.1.0"
10
10
  type: "library"
11
11
  language: "typescript"
12
12
  runtime: "deno"
@@ -153,16 +153,23 @@ export {
153
153
  createMockAuthAdapter, createMockProfileAdapter,
154
154
  createMockAuthStore, verifyMockAccount,
155
155
  } from "./adapters/mod.ts";
156
- export type { MockAuthStore } from "./adapters/mod.ts";
156
+ export type { MockAccount, MockAuthStore } from "./adapters/mod.ts";
157
+
158
+ // Upstream types re-exported so they appear in the public doc graph
159
+ // (they already surface as return types of subscribe / on). Not intended
160
+ // as primary consumer API — use them only when typing wrappers.
161
+ export type { PubSub, Subscriber, Unsubscriber } from "@marianmeres/pubsub";
162
+ export type { StoreLike } from "@marianmeres/store";
163
+ export type { Clog } from "@marianmeres/clog";
157
164
  ```
158
165
 
159
166
  ## Account lifecycle (optional)
160
167
 
161
168
  When `adapters.auth` is supplied, `createOwnsuite` instantiates three extra managers and attaches them as readonly suite properties:
162
169
 
163
- - **`suite.session: SessionManager`** — reactive `{ status, subject, jwt, expiresAt }` persisted via pluggable `SessionStorage` (`"local"` / `"session"` / `"memory"` / custom object). Hydrates on construction; discards expired sessions. Exposes `subscribe` (Svelte-compatible), `get()`, and state-mutation methods (`setAuthenticated`, `setUnverified`, `clear`, `patchSubject`). Writes are driven by `AuthManager`, not consumers directly.
170
+ - **`suite.session: SessionManager`** — reactive `{ status, subject, jwt, expiresAt }` persisted via pluggable `SessionStorage` (`"local"` / `"session"` / `"memory"` / custom object). With a built-in string backend, hydrates by probing in order `local → session → memory` and adopts the first non-expired payload as the active backend for the instance's lifetime (losing backends are wiped). `setAuthenticated({ storage })` pins the session to a specific built-in backend per login ("Remember me"); subsequent `patchSubject` / `setUnverified` writes land on the same backend. `clear()` wipes every built-in backend so stale blobs from a previous toggle don't leak back in. Exposes `subscribe` (Svelte-compatible), `get()`, and state-mutation methods (`setAuthenticated`, `setUnverified`, `clear`, `patchSubject`). Writes are driven by `AuthManager`, not consumers directly.
164
171
 
165
- - **`suite.auth: AuthManager`** — verbs only, no state. `register` / `login` / `logout` / `resendVerification` / `requestPasswordReset` / `changePassword` / `deleteAccount` / `initiateOAuth` (`mode: "popup" | "redirect"`) / `handleOAuthCallback` (redirect mode). Each call pipes the result into the session and fires an `onIdentityChanged` hook that resets + re-initializes every owner-scoped domain with the fresh context.
172
+ - **`suite.auth: AuthManager`** — verbs only, no state. `register(input, options?)` / `login(input, options?)` / `logout` / `resendVerification` / `requestPasswordReset` / `changePassword` / `deleteAccount` / `initiateOAuth(provider, opts)` (`mode: "popup" | "redirect"`) / `handleOAuthCallback(options?)` (redirect mode). The `options.remember` boolean (`true` → localStorage, `false` → sessionStorage, `undefined` → session manager default; same field on `OAuthInitOptions`) pins the resulting session's storage backend. Each call pipes the result into the session and fires an `onIdentityChanged` hook that resets + re-initializes every owner-scoped domain with the fresh context.
166
173
 
167
174
  - **`suite.profile: ProfileManager`** — singleton (one-row) `/me` CRUD. `fetch` / `update` / `listOAuth` / `unlinkOAuth`. Every successful fetch/update patches the session subject in place so consumers reading `suite.session` see email / roles / verification / connections without a second fetch. Update emits `profile:updated`.
168
175
 
package/API.md CHANGED
@@ -593,10 +593,12 @@ Current JWT, or `null` when anonymous.
593
593
 
594
594
  Shorthand reads.
595
595
 
596
- #### `session.setAuthenticated({ jwt, subject, expiresAt? })`
596
+ #### `session.setAuthenticated({ jwt, subject, expiresAt?, storage? })`
597
597
 
598
598
  Enter the `authenticated` state. Normally called by `AuthManager`, not consumers.
599
599
 
600
+ - `storage` (`SessionStorageType`, optional) — per-login storage pin. When `"local"` / `"session"` / `"memory"`, switches the active built-in backend for this session (and for subsequent `patchSubject` / `setUnverified` writes). The previously-active built-in backend's blob is wiped as part of the switch so "Remember me" toggles don't leave stale data. Silently ignored when the manager was constructed with a custom `SessionStorage` object, or when the override is itself an object.
601
+
600
602
  #### `session.setUnverified(email)` / `session.clear()` / `session.patchSubject(patch)`
601
603
 
602
604
  Mutation helpers — typically driven by `AuthManager` / `ProfileManager`.
@@ -613,15 +615,15 @@ Verbs only. Attached as `suite.auth`. No state of its own — results flow into
613
615
 
614
616
  | Method | Purpose |
615
617
  |---|---|
616
- | `register({ email, password, password_confirm, roles?, extras? })` | Create account. Returns an `AuthTokenResult`. When the server's verification gate is on, the result carries `requiresVerification: true` and the session flips to `"unverified"` — no JWT yet. |
617
- | `login({ email, password })` | Exchange credentials for a JWT. Session flips to `"authenticated"` on success, `"unverified"` if the server reports the gate. |
618
+ | `register({ email, password, password_confirm, roles?, extras? }, options?)` | Create account. Returns an `AuthTokenResult`. When the server's verification gate is on, the result carries `requiresVerification: true` and the session flips to `"unverified"` — no JWT yet. `options.remember` pins the resulting session to `localStorage` (`true`) or `sessionStorage` (`false`); omit to use the `SessionManager` default. |
619
+ | `login({ email, password }, options?)` | Exchange credentials for a JWT. Session flips to `"authenticated"` on success, `"unverified"` if the server reports the gate. `options.remember` selects per-login storage (see `register`). |
618
620
  | `logout()` | Best-effort server revoke + local clear. Idempotent. |
619
621
  | `resendVerification({ email, lang? })` | Trigger a fresh verification email. Anti-enumeration: always resolves. |
620
622
  | `requestPasswordReset({ email, lang? })` | Trigger a password-reset email. Anti-enumeration. |
621
623
  | `changePassword({ current_password?, new_password, confirm_password, token? })` | Authenticated self-change (with `current_password`) or token-based reset. |
622
624
  | `deleteAccount({ password?, confirm? })` | Irreversible server delete + local session clear + identity-changed hook. |
623
- | `initiateOAuth(provider, opts)` | Start an OAuth flow. `mode: "popup"` (default) resolves with the auth result from the popup's `postMessage`; `mode: "redirect"` navigates the top window. |
624
- | `handleOAuthCallback()` | For `mode: "redirect"` apps, call from your callback route to extract the result from the URL (delegated to `adapter.handleOAuthCallback`). |
625
+ | `initiateOAuth(provider, opts)` | Start an OAuth flow. `mode: "popup"` (default) resolves with the auth result from the popup's `postMessage`; `mode: "redirect"` navigates the top window. `opts.remember` pins the resulting session's storage backend (only meaningful for `action: "login"`). |
626
+ | `handleOAuthCallback(options?)` | For `mode: "redirect"` apps, call from your callback route to extract the result from the URL (delegated to `adapter.handleOAuthCallback`). `options.remember` pins the resulting session's storage — pass the same value the user picked before the redirect. |
625
627
 
626
628
  Successful identity changes (register-with-autologin, login, OAuth login, logout, deleteAccount) fire the orchestrator's `onIdentityChanged` hook, which resets every owner-scoped domain and re-initializes them with the new context.
627
629
 
@@ -735,6 +737,14 @@ interface OAuthInitOptions {
735
737
  redirect?: string;
736
738
  lang?: string;
737
739
  mode?: "popup" | "redirect"; // default "popup"
740
+ remember?: boolean; // same semantics as AuthActionOptions.remember
741
+ }
742
+
743
+ interface AuthActionOptions {
744
+ /** true → localStorage; false → sessionStorage; undefined → default.
745
+ * Ignored when SessionManager was constructed with a custom
746
+ * SessionStorage object. */
747
+ remember?: boolean;
738
748
  }
739
749
 
740
750
  interface OAuthConnection {
@@ -787,9 +797,18 @@ interface StackAccountAdapterOptions {
787
797
 
788
798
  See [src/oauth/popup.ts](src/oauth/popup.ts) for full definitions. `OAuthPopupMessage` is a union of `OAuthPopupLoginMessage` (`{ type: "oauth_login_success", jwt, email, roles?, ... }`) and `OAuthPopupLinkMessage` (`{ type: "oauth_link_success", provider }`). Errors posted by the server (`{ type: "oauth_error", error }`) cause the promise to reject.
789
799
 
790
- ### `MockAuthStore`
800
+ ### `MockAccount` / `MockAuthStore`
791
801
 
792
802
  ```typescript
803
+ interface MockAccount {
804
+ email: string;
805
+ password: string; // plaintext — mock does no hashing
806
+ roles: string[];
807
+ isVerified: boolean;
808
+ hasPassword: boolean;
809
+ oauthConnections: OAuthConnection[];
810
+ }
811
+
793
812
  interface MockAuthStore {
794
813
  accounts: Map<string, MockAccount>;
795
814
  requireVerifiedEmail: boolean;
@@ -797,6 +816,8 @@ interface MockAuthStore {
797
816
  }
798
817
  ```
799
818
 
819
+ Fields are public by design so test code can peek at / mutate them directly.
820
+
800
821
  ---
801
822
 
802
823
  ## Account-lifecycle events
package/README.md CHANGED
@@ -67,7 +67,10 @@ await suite.auth!.register({
67
67
  // suite.session!.get().status === "unverified"
68
68
 
69
69
  // After the user clicks the email link and the server flips isVerified:
70
- await suite.auth!.login({ email: "alice@example.com", password: "mysecretpassword" });
70
+ await suite.auth!.login(
71
+ { email: "alice@example.com", password: "mysecretpassword" },
72
+ { remember: true }, // true → localStorage; false → sessionStorage (per-login override)
73
+ );
71
74
  // suite.session!.get().status === "authenticated"
72
75
  // Every registered owner-scoped domain is re-initialized with the new JWT.
73
76
 
@@ -86,7 +89,7 @@ await suite.profile!.update({
86
89
  await suite.auth!.logout();
87
90
  ```
88
91
 
89
- Session state is persisted through a pluggable `SessionStorage` backend (`"local"` / `"session"` / `"memory"` / custom object with `get`/`set`/`del`). Expired stored sessions are discarded on construction so a reload after the JWT lapses starts anonymous.
92
+ Session state is persisted through a pluggable `SessionStorage` backend (`"local"` / `"session"` / `"memory"` / custom object with `get`/`set`/`del`). Expired stored sessions are discarded on construction so a reload after the JWT lapses starts anonymous. With a built-in string backend, hydration probes `local → session → memory` and adopts whichever holds a non-expired payload — the per-login `remember` flag on `auth.login` / `auth.register` / `auth.initiateOAuth` (`true` → `localStorage`, `false` → `sessionStorage`) switches the active backend for that session, and `session.clear()` wipes all built-in backends so toggles can't leak stale data.
90
93
 
91
94
  Tests can use the in-memory mock adapters (`createMockAuthAdapter`, `createMockProfileAdapter`, `createMockAuthStore`, `verifyMockAccount`) and the injectable popup host for deterministic OAuth dances.
92
95
 
@@ -10,15 +10,27 @@
10
10
  * adapters close over, so tests can peek at or mutate state directly.
11
11
  */
12
12
  import type { AuthAdapter, OAuthConnection, ProfileAdapter } from "../types/mod.js";
13
- interface MockAccount {
13
+ /** In-memory account record held by {@link MockAuthStore}. Test code may
14
+ * construct these via the `seed` option of {@link createMockAuthStore}. */
15
+ export interface MockAccount {
16
+ /** Email (and implicit primary key in the mock store). */
14
17
  email: string;
18
+ /** Plaintext password — the mock runs no hashing. */
15
19
  password: string;
20
+ /** Authorization roles returned on the next login / `/me` read. */
16
21
  roles: string[];
22
+ /** Verified flag — flipped by {@link verifyMockAccount}. */
17
23
  isVerified: boolean;
24
+ /** Whether the account has a local password (OAuth-only accounts: `false`). */
18
25
  hasPassword: boolean;
26
+ /** Linked OAuth provider connections. */
19
27
  oauthConnections: OAuthConnection[];
20
28
  }
29
+ /** In-memory store backing the mock auth + profile adapters. Pass the same
30
+ * store to `createMockAuthAdapter` and `createMockProfileAdapter` so they
31
+ * share state. Fields are public so test code can peek or mutate them. */
21
32
  export interface MockAuthStore {
33
+ /** Account records, keyed by email. */
22
34
  accounts: Map<string, MockAccount>;
23
35
  /** If true, register/login return requiresVerification=true until the
24
36
  * email is explicitly verified via `verifyMockAccount()`. */
@@ -26,13 +38,23 @@ export interface MockAuthStore {
26
38
  /** Last-issued JWT per email (just a synthetic string). */
27
39
  jwtsByEmail: Map<string, string>;
28
40
  }
41
+ /** Create a fresh in-memory {@link MockAuthStore}. Pass `seed` to preload
42
+ * accounts, `requireVerifiedEmail: true` to make login gate on verification. */
29
43
  export declare function createMockAuthStore(init?: {
44
+ /** When true, register/login return `requiresVerification: true` until
45
+ * the account is verified via {@link verifyMockAccount}. Default: `false`. */
30
46
  requireVerifiedEmail?: boolean;
47
+ /** Accounts to preload into the store. */
31
48
  seed?: MockAccount[];
32
49
  }): MockAuthStore;
50
+ /** Build an {@link AuthAdapter} backed by the given in-memory
51
+ * {@link MockAuthStore}. Simulates register / login / password change /
52
+ * delete without a server. JWTs are synthetic strings — do not ship. */
33
53
  export declare function createMockAuthAdapter(store: MockAuthStore): AuthAdapter;
54
+ /** Build a {@link ProfileAdapter} backed by the given {@link MockAuthStore}.
55
+ * Shares state with an auth adapter created from the same store so login-
56
+ * then-`/me` round-trips reflect each other. */
34
57
  export declare function createMockProfileAdapter(store: MockAuthStore): ProfileAdapter;
35
58
  /** Test helper — mark a mock account as verified as if the user had clicked
36
59
  * the link in the verification email. */
37
60
  export declare function verifyMockAccount(store: MockAuthStore, email: string): void;
38
- export {};
@@ -9,6 +9,8 @@
9
9
  * Deliberately small: everything is held in a shared `Store` object the
10
10
  * adapters close over, so tests can peek at or mutate state directly.
11
11
  */
12
+ /** Create a fresh in-memory {@link MockAuthStore}. Pass `seed` to preload
13
+ * accounts, `requireVerifiedEmail: true` to make login gate on verification. */
12
14
  export function createMockAuthStore(init = {}) {
13
15
  const store = {
14
16
  accounts: new Map(),
@@ -23,6 +25,9 @@ export function createMockAuthStore(init = {}) {
23
25
  function mintJwt(email) {
24
26
  return `mock.${btoa(email)}.${Date.now().toString(36)}`;
25
27
  }
28
+ /** Build an {@link AuthAdapter} backed by the given in-memory
29
+ * {@link MockAuthStore}. Simulates register / login / password change /
30
+ * delete without a server. JWTs are synthetic strings — do not ship. */
26
31
  export function createMockAuthAdapter(store) {
27
32
  return {
28
33
  register(input, _ctx) {
@@ -148,6 +153,9 @@ export function createMockAuthAdapter(store) {
148
153
  },
149
154
  };
150
155
  }
156
+ /** Build a {@link ProfileAdapter} backed by the given {@link MockAuthStore}.
157
+ * Shares state with an auth adapter created from the same store so login-
158
+ * then-`/me` round-trips reflect each other. */
151
159
  export function createMockProfileAdapter(store) {
152
160
  function emailFromCtx(ctx) {
153
161
  const jwt = ctx.jwt;
@@ -8,6 +8,8 @@
8
8
  * path deterministically.
9
9
  */
10
10
  import type { OwnedCollectionAdapter } from "../types/adapter.js";
11
+ /** Options for {@link createMockOwnedCollectionAdapter}. Only present in
12
+ * tests / storybooks — production code never calls this. */
11
13
  export interface MockAdapterOptions<TRow> {
12
14
  /** Initial seed rows. */
13
15
  seed?: TRow[];
@@ -28,11 +28,18 @@
28
28
  * AuthAdapter / ProfileAdapter interfaces.
29
29
  */
30
30
  import type { AuthAdapter, ProfileAdapter } from "../types/mod.js";
31
+ /** Options shared by the two stack-account adapter factories. */
31
32
  export interface StackAccountAdapterOptions {
32
33
  /** Base URL of the mounted stack-account app. Default: "/api/account". */
33
34
  baseUrl?: string;
34
35
  /** Override the `fetch` implementation (useful for tests / SSR). */
35
36
  fetch?: typeof fetch;
36
37
  }
38
+ /** Build the default {@link AuthAdapter} for `@marianmeres/stack-account`.
39
+ * Points at `{baseUrl}/auth/*` (register/login/logout/verify/password/
40
+ * delete) and `{baseUrl}/oauth/*` (init + callback). */
37
41
  export declare function createStackAccountAuthAdapter(opts?: StackAccountAdapterOptions): AuthAdapter;
42
+ /** Build the default {@link ProfileAdapter} for `@marianmeres/stack-account`.
43
+ * Points at `{baseUrl}/me` (GET + PUT) and `{baseUrl}/me/oauth/*` for
44
+ * connection listing / unlinking. */
38
45
  export declare function createStackAccountProfileAdapter(opts?: StackAccountAdapterOptions): ProfileAdapter;
@@ -82,6 +82,9 @@ async function requestJson(doFetch, url, init, ctx) {
82
82
  return undefined;
83
83
  return (await res.json());
84
84
  }
85
+ /** Build the default {@link AuthAdapter} for `@marianmeres/stack-account`.
86
+ * Points at `{baseUrl}/auth/*` (register/login/logout/verify/password/
87
+ * delete) and `{baseUrl}/oauth/*` (init + callback). */
85
88
  export function createStackAccountAuthAdapter(opts = {}) {
86
89
  const base = opts.baseUrl ?? "/api/account";
87
90
  const doFetch = resolveFetch(opts);
@@ -123,6 +126,9 @@ export function createStackAccountAuthAdapter(opts = {}) {
123
126
  },
124
127
  };
125
128
  }
129
+ /** Build the default {@link ProfileAdapter} for `@marianmeres/stack-account`.
130
+ * Points at `{baseUrl}/me` (GET + PUT) and `{baseUrl}/me/oauth/*` for
131
+ * connection listing / unlinking. */
126
132
  export function createStackAccountProfileAdapter(opts = {}) {
127
133
  const base = opts.baseUrl ?? "/api/account";
128
134
  const doFetch = resolveFetch(opts);
@@ -12,16 +12,22 @@
12
12
  * management.
13
13
  */
14
14
  import { type PubSub } from "@marianmeres/pubsub";
15
- import type { AuthAdapter, AuthTokenResult, OAuthInitOptions, OAuthProvider, OwnsuiteContext, ProfileAdapter } from "../types/mod.js";
15
+ import type { AuthActionOptions, AuthAdapter, AuthTokenResult, OAuthInitOptions, OAuthProvider, OwnsuiteContext, ProfileAdapter } from "../types/mod.js";
16
16
  import type { SessionManager } from "./session.js";
17
17
  import type { ProfileManager } from "./profile.js";
18
+ /** Construction-time options for {@link AuthManager}. Normally assembled
19
+ * by `createOwnsuite` — call sites rarely instantiate this directly. */
18
20
  export interface AuthManagerOptions {
21
+ /** Server adapter. The manager is stateless beyond what it pushes into
22
+ * {@link SessionManager}. */
19
23
  adapter: AuthAdapter;
24
+ /** Session manager that receives the JWT / subject / status writes. */
20
25
  session: SessionManager;
21
26
  /** Profile manager — auth hydrates the session subject from `/me`
22
27
  * immediately after login so subscribers see roles/isVerified/etc.
23
28
  * without a second await. */
24
29
  profile: ProfileManager;
30
+ /** Shared pubsub for event emission. Created privately when omitted. */
25
31
  pubsub?: PubSub;
26
32
  /** Called after a successful identity change (register/login/OAuth
27
33
  * login/logout). The orchestrator wires this to reset + re-init every
@@ -35,37 +41,73 @@ export interface AuthManagerOptions {
35
41
  * tests that don't need a separate profile fetch. */
36
42
  profileAdapter?: ProfileAdapter;
37
43
  }
44
+ /**
45
+ * Verbs for the account lifecycle: register / login / logout / OAuth /
46
+ * password-reset / delete account. Holds no state of its own — every
47
+ * outcome is piped into the linked {@link SessionManager}, which is the
48
+ * single source of truth for JWT + subject + status. Usually accessed via
49
+ * `suite.auth`, not constructed directly.
50
+ */
38
51
  export declare class AuthManager {
39
52
  #private;
53
+ /** Construct a new `AuthManager`. Normally called by `createOwnsuite`,
54
+ * not by consumers. */
40
55
  constructor(options: AuthManagerOptions);
56
+ /** Merge `ctx` into the current adapter context. Keys not in `ctx`
57
+ * are preserved. */
41
58
  setContext(ctx: OwnsuiteContext): void;
59
+ /** Replace the adapter context wholesale. Callers use this when
60
+ * switching subjects or clearing host-app context. */
42
61
  replaceContext(ctx: OwnsuiteContext): void;
62
+ /** Create a new account. Returns the server's {@link AuthTokenResult}.
63
+ * When the verification gate is on, the result carries
64
+ * `requiresVerification: true` and the session flips to `"unverified"`
65
+ * (no JWT). Otherwise the session flips to `"authenticated"` and the
66
+ * profile is hydrated from `/me`. `options.remember` pins the session's
67
+ * storage backend — see {@link AuthActionOptions.remember}. */
43
68
  register(input: {
44
69
  email: string;
45
70
  password: string;
46
71
  password_confirm: string;
47
72
  roles?: string[];
48
73
  extras?: Record<string, unknown>;
49
- }): Promise<AuthTokenResult>;
74
+ }, options?: AuthActionOptions): Promise<AuthTokenResult>;
75
+ /** Exchange credentials for a JWT. On success the session flips to
76
+ * `"authenticated"` and the profile is hydrated from `/me`. Rejects
77
+ * on bad credentials (session untouched) or — when the server's
78
+ * verification gate is on for an unverified account — throws through
79
+ * the adapter. `options.remember` controls storage persistence. */
50
80
  login(input: {
51
81
  email: string;
52
82
  password: string;
53
- }): Promise<AuthTokenResult>;
83
+ }, options?: AuthActionOptions): Promise<AuthTokenResult>;
84
+ /** Log out. Best-effort server revoke + unconditional local clear;
85
+ * the session is always wiped even if the server call fails. Fires
86
+ * `auth:logout` and the `onIdentityChanged` hook. Safe to call when
87
+ * already anonymous. */
54
88
  logout(): Promise<void>;
89
+ /** Trigger a fresh verification email. Anti-enumeration: always resolves
90
+ * regardless of whether the address corresponds to a real account. */
55
91
  resendVerification(input: {
56
92
  email: string;
57
93
  lang?: string;
58
94
  }): Promise<void>;
95
+ /** Trigger a password-reset email. Anti-enumeration: always resolves. */
59
96
  requestPasswordReset(input: {
60
97
  email: string;
61
98
  lang?: string;
62
99
  }): Promise<void>;
100
+ /** Change the password. Pass `current_password` for an authenticated
101
+ * self-change, or `token` for a reset-link flow. `new_password` and
102
+ * `confirm_password` are always required. */
63
103
  changePassword(input: {
64
104
  current_password?: string;
65
105
  new_password: string;
66
106
  confirm_password: string;
67
107
  token?: string;
68
108
  }): Promise<void>;
109
+ /** Irreversibly delete the authenticated account. On success clears the
110
+ * local session and fires `auth:logout` + `onIdentityChanged`. */
69
111
  deleteAccount(input: {
70
112
  password?: string;
71
113
  confirm?: boolean;
@@ -78,6 +120,8 @@ export declare class AuthManager {
78
120
  */
79
121
  initiateOAuth(provider: OAuthProvider, opts: OAuthInitOptions): Promise<AuthTokenResult | void>;
80
122
  /** For the redirect-mode callback page: delegate to the adapter if
81
- * available to extract the result from the current URL/page state. */
82
- handleOAuthCallback(): Promise<AuthTokenResult | void>;
123
+ * available to extract the result from the current URL/page state.
124
+ * `options.remember` pins the resulting session to local/session
125
+ * storage — pass the same value the user picked before the redirect. */
126
+ handleOAuthCallback(options?: AuthActionOptions): Promise<AuthTokenResult | void>;
83
127
  }
@@ -13,6 +13,13 @@
13
13
  */
14
14
  import { createPubSub } from "@marianmeres/pubsub";
15
15
  import { openOAuthPopup } from "../oauth/popup.js";
16
+ /**
17
+ * Verbs for the account lifecycle: register / login / logout / OAuth /
18
+ * password-reset / delete account. Holds no state of its own — every
19
+ * outcome is piped into the linked {@link SessionManager}, which is the
20
+ * single source of truth for JWT + subject + status. Usually accessed via
21
+ * `suite.auth`, not constructed directly.
22
+ */
16
23
  export class AuthManager {
17
24
  #pubsub;
18
25
  #adapter;
@@ -20,6 +27,8 @@ export class AuthManager {
20
27
  #profile;
21
28
  #onIdentityChanged;
22
29
  #context;
30
+ /** Construct a new `AuthManager`. Normally called by `createOwnsuite`,
31
+ * not by consumers. */
23
32
  constructor(options) {
24
33
  this.#adapter = options.adapter;
25
34
  this.#session = options.session;
@@ -28,9 +37,13 @@ export class AuthManager {
28
37
  this.#onIdentityChanged = options.onIdentityChanged;
29
38
  this.#context = options.context ?? {};
30
39
  }
40
+ /** Merge `ctx` into the current adapter context. Keys not in `ctx`
41
+ * are preserved. */
31
42
  setContext(ctx) {
32
43
  this.#context = { ...this.#context, ...ctx };
33
44
  }
45
+ /** Replace the adapter context wholesale. Callers use this when
46
+ * switching subjects or clearing host-app context. */
34
47
  replaceContext(ctx) {
35
48
  this.#context = { ...ctx };
36
49
  }
@@ -46,8 +59,13 @@ export class AuthManager {
46
59
  * present, we pull the profile to build a complete subject. When the
47
60
  * server returned requiresVerification, we surface the "unverified"
48
61
  * status without any profile fetch. Triggers identity-change hook on
49
- * actual login. */
50
- async #applyAuthResult(result) {
62
+ * actual login.
63
+ *
64
+ * `remember` translates to a per-login storage pin on the session:
65
+ * `true` → `"local"`, `false` → `"session"`, `undefined` → session
66
+ * manager's configured default. Silently no-op in the verification-gate
67
+ * branch (no JWT yet → nothing to persist). */
68
+ async #applyAuthResult(result, remember) {
51
69
  if (result.requiresVerification || !result.jwt) {
52
70
  this.#session.setUnverified(result.email);
53
71
  this.#pubsub.publish("auth:verification:required", {
@@ -66,10 +84,16 @@ export class AuthManager {
66
84
  isVerified: result.isVerified ?? false,
67
85
  hasPassword: true, // corrected by profile fetch below
68
86
  };
87
+ const storage = remember === true
88
+ ? "local"
89
+ : remember === false
90
+ ? "session"
91
+ : undefined;
69
92
  this.#session.setAuthenticated({
70
93
  jwt: result.jwt,
71
94
  subject: provisional,
72
95
  expiresAt: result.validUntil ?? null,
96
+ ...(storage ? { storage } : {}),
73
97
  });
74
98
  // Best-effort hydrate the subject from /me. If this fails we keep the
75
99
  // provisional subject; consumers can retry via profile.fetch().
@@ -90,7 +114,13 @@ export class AuthManager {
90
114
  return result;
91
115
  }
92
116
  // ─────────────────────── verbs ──────────────────────────────────────────
93
- async register(input) {
117
+ /** Create a new account. Returns the server's {@link AuthTokenResult}.
118
+ * When the verification gate is on, the result carries
119
+ * `requiresVerification: true` and the session flips to `"unverified"`
120
+ * (no JWT). Otherwise the session flips to `"authenticated"` and the
121
+ * profile is hydrated from `/me`. `options.remember` pins the session's
122
+ * storage backend — see {@link AuthActionOptions.remember}. */
123
+ async register(input, options) {
94
124
  const result = await this.#adapter.register(input, this.#ctx());
95
125
  this.#pubsub.publish("auth:register", {
96
126
  type: "auth:register",
@@ -98,17 +128,26 @@ export class AuthManager {
98
128
  email: input.email,
99
129
  requiresVerification: Boolean(result.requiresVerification),
100
130
  });
101
- return await this.#applyAuthResult(result);
131
+ return await this.#applyAuthResult(result, options?.remember);
102
132
  }
103
- async login(input) {
133
+ /** Exchange credentials for a JWT. On success the session flips to
134
+ * `"authenticated"` and the profile is hydrated from `/me`. Rejects
135
+ * on bad credentials (session untouched) or — when the server's
136
+ * verification gate is on for an unverified account — throws through
137
+ * the adapter. `options.remember` controls storage persistence. */
138
+ async login(input, options) {
104
139
  const result = await this.#adapter.login(input, this.#ctx());
105
140
  this.#pubsub.publish("auth:login", {
106
141
  type: "auth:login",
107
142
  timestamp: Date.now(),
108
143
  email: input.email,
109
144
  });
110
- return await this.#applyAuthResult(result);
145
+ return await this.#applyAuthResult(result, options?.remember);
111
146
  }
147
+ /** Log out. Best-effort server revoke + unconditional local clear;
148
+ * the session is always wiped even if the server call fails. Fires
149
+ * `auth:logout` and the `onIdentityChanged` hook. Safe to call when
150
+ * already anonymous. */
112
151
  async logout() {
113
152
  const subjectId = this.#session.get().subject?.id;
114
153
  try {
@@ -132,15 +171,23 @@ export class AuthManager {
132
171
  }
133
172
  }
134
173
  }
174
+ /** Trigger a fresh verification email. Anti-enumeration: always resolves
175
+ * regardless of whether the address corresponds to a real account. */
135
176
  async resendVerification(input) {
136
177
  await this.#adapter.resendVerification(input, this.#ctx());
137
178
  }
179
+ /** Trigger a password-reset email. Anti-enumeration: always resolves. */
138
180
  async requestPasswordReset(input) {
139
181
  await this.#adapter.requestPasswordReset(input, this.#ctx());
140
182
  }
183
+ /** Change the password. Pass `current_password` for an authenticated
184
+ * self-change, or `token` for a reset-link flow. `new_password` and
185
+ * `confirm_password` are always required. */
141
186
  async changePassword(input) {
142
187
  await this.#adapter.changePassword(input, this.#ctx());
143
188
  }
189
+ /** Irreversibly delete the authenticated account. On success clears the
190
+ * local session and fires `auth:logout` + `onIdentityChanged`. */
144
191
  async deleteAccount(input) {
145
192
  await this.#adapter.deleteAccount(input, this.#ctx());
146
193
  // Clear session locally and fire logout/identity-change.
@@ -197,15 +244,17 @@ export class AuthManager {
197
244
  roles: message.roles ?? [],
198
245
  isVerified: true,
199
246
  };
200
- return await this.#applyAuthResult(result);
247
+ return await this.#applyAuthResult(result, opts.remember);
201
248
  }
202
249
  /** For the redirect-mode callback page: delegate to the adapter if
203
- * available to extract the result from the current URL/page state. */
204
- async handleOAuthCallback() {
250
+ * available to extract the result from the current URL/page state.
251
+ * `options.remember` pins the resulting session to local/session
252
+ * storage — pass the same value the user picked before the redirect. */
253
+ async handleOAuthCallback(options) {
205
254
  if (!this.#adapter.handleOAuthCallback) {
206
255
  throw new Error("AuthManager: handleOAuthCallback is not implemented by the adapter");
207
256
  }
208
257
  const result = await this.#adapter.handleOAuthCallback(this.#ctx());
209
- return await this.#applyAuthResult(result);
258
+ return await this.#applyAuthResult(result, options?.remember);
210
259
  }
211
260
  }
@@ -27,12 +27,24 @@ export interface BaseDomainOptions {
27
27
  */
28
28
  export declare abstract class BaseDomainManager<TData, TAdapter> {
29
29
  #private;
30
+ /** Reactive store holding the full {@link DomainStateWrapper} for this
31
+ * domain. Subclasses read + publish through it. */
30
32
  protected readonly store: StoreLike<DomainStateWrapper<TData>>;
33
+ /** Shared pubsub used to emit domain lifecycle / CRUD events. */
31
34
  protected readonly pubsub: PubSub;
35
+ /** Stable name of this domain (used as event `domain` and log prefix). */
32
36
  protected readonly domainName: DomainName;
37
+ /** Scoped logger, prefixed with `ownsuite:<domainName>`. */
33
38
  protected readonly clog: Clog;
39
+ /** Server adapter instance; `null` until `setAdapter()` / constructor
40
+ * installs one. */
34
41
  protected adapter: TAdapter | null;
42
+ /** Current context forwarded to every adapter call (jwt, subjectId,
43
+ * signal, plus any consumer-supplied extras). */
35
44
  protected context: OwnsuiteContext;
45
+ /** Construct a new domain manager. Not called directly — subclasses like
46
+ * {@link OwnedCollectionManager} extend this and are instantiated by
47
+ * the suite. */
36
48
  constructor(domainName: DomainName, options?: BaseDomainOptions);
37
49
  /** Svelte-compatible subscribe method. */
38
50
  get subscribe(): StoreLike<DomainStateWrapper<TData>>["subscribe"];
@@ -40,7 +52,11 @@ export declare abstract class BaseDomainManager<TData, TAdapter> {
40
52
  get(): DomainStateWrapper<TData>;
41
53
  /** True after `destroy()` has been called. */
42
54
  get isDestroyed(): boolean;
55
+ /** Install or replace the server adapter for this domain. Call
56
+ * {@link refresh} afterwards to pick up data from the new source. */
43
57
  setAdapter(adapter: TAdapter): void;
58
+ /** Current adapter, or `null` when none installed (typically only in
59
+ * tests or between `destroy()` and re-attachment). */
44
60
  getAdapter(): TAdapter | null;
45
61
  /**
46
62
  * Merge `ctx` into the current context. Keys not present in `ctx` are
@@ -49,6 +65,8 @@ export declare abstract class BaseDomainManager<TData, TAdapter> {
49
65
  setContext(context: OwnsuiteContext): void;
50
66
  /** Replace the context object entirely (no merge with existing). */
51
67
  replaceContext(context: OwnsuiteContext): void;
68
+ /** Snapshot of the current context. Mutating the returned object does
69
+ * not affect the manager. */
52
70
  getContext(): OwnsuiteContext;
53
71
  /** Transition to a new state. */
54
72
  protected setState(state: DomainState): void;
@@ -101,6 +119,8 @@ export declare abstract class BaseDomainManager<TData, TAdapter> {
101
119
  * callers that prefer a whole-data restore over per-change inversion.
102
120
  */
103
121
  protected withOptimisticUpdate<T>(operation: string, optimisticUpdate: () => void, serverSync: () => Promise<T>, onSuccess?: (result: T) => void, onError?: (error: DomainError, snapshot: TData | null) => void): Promise<void>;
122
+ /** Boot the domain — typically fetch its initial list. Subclasses
123
+ * implement this; see {@link OwnedCollectionManager.initialize}. */
104
124
  abstract initialize(): Promise<void>;
105
125
  /**
106
126
  * Reset to `initializing` state. Aborts any in-flight reads/mutations
@@ -38,11 +38,20 @@ function safeClone(value) {
38
38
  * @typeParam TAdapter - The adapter interface type for server communication.
39
39
  */
40
40
  export class BaseDomainManager {
41
+ /** Reactive store holding the full {@link DomainStateWrapper} for this
42
+ * domain. Subclasses read + publish through it. */
41
43
  store;
44
+ /** Shared pubsub used to emit domain lifecycle / CRUD events. */
42
45
  pubsub;
46
+ /** Stable name of this domain (used as event `domain` and log prefix). */
43
47
  domainName;
48
+ /** Scoped logger, prefixed with `ownsuite:<domainName>`. */
44
49
  clog;
50
+ /** Server adapter instance; `null` until `setAdapter()` / constructor
51
+ * installs one. */
45
52
  adapter = null;
53
+ /** Current context forwarded to every adapter call (jwt, subjectId,
54
+ * signal, plus any consumer-supplied extras). */
46
55
  context = {};
47
56
  /** Mutation chain head. Each create/update/delete appends itself here. */
48
57
  #mutationChain = Promise.resolve();
@@ -51,6 +60,9 @@ export class BaseDomainManager {
51
60
  /** All active controllers created via `newController()`, for bulk abort. */
52
61
  #activeControllers = new Set();
53
62
  #destroyed = false;
63
+ /** Construct a new domain manager. Not called directly — subclasses like
64
+ * {@link OwnedCollectionManager} extend this and are instantiated by
65
+ * the suite. */
54
66
  constructor(domainName, options = {}) {
55
67
  this.domainName = domainName;
56
68
  this.clog = createClog(`ownsuite:${domainName}`, { color: "auto" });
@@ -76,9 +88,13 @@ export class BaseDomainManager {
76
88
  get isDestroyed() {
77
89
  return this.#destroyed;
78
90
  }
91
+ /** Install or replace the server adapter for this domain. Call
92
+ * {@link refresh} afterwards to pick up data from the new source. */
79
93
  setAdapter(adapter) {
80
94
  this.adapter = adapter;
81
95
  }
96
+ /** Current adapter, or `null` when none installed (typically only in
97
+ * tests or between `destroy()` and re-attachment). */
82
98
  getAdapter() {
83
99
  return this.adapter;
84
100
  }
@@ -93,6 +109,8 @@ export class BaseDomainManager {
93
109
  replaceContext(context) {
94
110
  this.context = { ...context };
95
111
  }
112
+ /** Snapshot of the current context. Mutating the returned object does
113
+ * not affect the manager. */
96
114
  getContext() {
97
115
  return { ...this.context };
98
116
  }