@marianmeres/ownsuite 2.2.1 → 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,7 +153,14 @@ 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)
package/API.md CHANGED
@@ -797,9 +797,18 @@ interface StackAccountAdapterOptions {
797
797
 
798
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.
799
799
 
800
- ### `MockAuthStore`
800
+ ### `MockAccount` / `MockAuthStore`
801
801
 
802
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
+
803
812
  interface MockAuthStore {
804
813
  accounts: Map<string, MockAccount>;
805
814
  requireVerifiedEmail: boolean;
@@ -807,6 +816,8 @@ interface MockAuthStore {
807
816
  }
808
817
  ```
809
818
 
819
+ Fields are public by design so test code can peek at / mutate them directly.
820
+
810
821
  ---
811
822
 
812
823
  ## Account-lifecycle events
@@ -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);
@@ -15,13 +15,19 @@ import { type PubSub } from "@marianmeres/pubsub";
15
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,11 +41,30 @@ 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;
@@ -47,25 +72,42 @@ export declare class AuthManager {
47
72
  roles?: string[];
48
73
  extras?: Record<string, unknown>;
49
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
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;
@@ -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
  }
@@ -101,6 +114,12 @@ export class AuthManager {
101
114
  return result;
102
115
  }
103
116
  // ─────────────────────── verbs ──────────────────────────────────────────
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}. */
104
123
  async register(input, options) {
105
124
  const result = await this.#adapter.register(input, this.#ctx());
106
125
  this.#pubsub.publish("auth:register", {
@@ -111,6 +130,11 @@ export class AuthManager {
111
130
  });
112
131
  return await this.#applyAuthResult(result, options?.remember);
113
132
  }
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. */
114
138
  async login(input, options) {
115
139
  const result = await this.#adapter.login(input, this.#ctx());
116
140
  this.#pubsub.publish("auth:login", {
@@ -120,6 +144,10 @@ export class AuthManager {
120
144
  });
121
145
  return await this.#applyAuthResult(result, options?.remember);
122
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. */
123
151
  async logout() {
124
152
  const subjectId = this.#session.get().subject?.id;
125
153
  try {
@@ -143,15 +171,23 @@ export class AuthManager {
143
171
  }
144
172
  }
145
173
  }
174
+ /** Trigger a fresh verification email. Anti-enumeration: always resolves
175
+ * regardless of whether the address corresponds to a real account. */
146
176
  async resendVerification(input) {
147
177
  await this.#adapter.resendVerification(input, this.#ctx());
148
178
  }
179
+ /** Trigger a password-reset email. Anti-enumeration: always resolves. */
149
180
  async requestPasswordReset(input) {
150
181
  await this.#adapter.requestPasswordReset(input, this.#ctx());
151
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. */
152
186
  async changePassword(input) {
153
187
  await this.#adapter.changePassword(input, this.#ctx());
154
188
  }
189
+ /** Irreversibly delete the authenticated account. On success clears the
190
+ * local session and fires `auth:logout` + `onIdentityChanged`. */
155
191
  async deleteAccount(input) {
156
192
  await this.#adapter.deleteAccount(input, this.#ctx());
157
193
  // Clear session locally and fire logout/identity-change.
@@ -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
  }
@@ -16,7 +16,10 @@
16
16
  import type { OwnedCollectionAdapter } from "../types/adapter.js";
17
17
  import type { OwnedCollectionState } from "../types/state.js";
18
18
  import { BaseDomainManager, type BaseDomainOptions } from "./base.js";
19
+ /** Construction-time options for {@link OwnedCollectionManager}. */
19
20
  export interface OwnedCollectionManagerOptions<TRow, TCreate, TUpdate> extends BaseDomainOptions {
21
+ /** Server adapter for this domain. Optional at construction — can be
22
+ * installed later with `setAdapter()`. */
20
23
  adapter?: OwnedCollectionAdapter<TRow, TCreate, TUpdate>;
21
24
  /** Function that extracts the row id from a row. Defaults to `row.model_id`. */
22
25
  getRowId?: (row: TRow) => string;
@@ -30,6 +33,8 @@ export interface OwnedCollectionManagerOptions<TRow, TCreate, TUpdate> extends B
30
33
  */
31
34
  export declare class OwnedCollectionManager<TRow = Record<string, unknown>, TCreate = Partial<TRow>, TUpdate = Partial<TRow>> extends BaseDomainManager<OwnedCollectionState<TRow>, OwnedCollectionAdapter<TRow, TCreate, TUpdate>> {
32
35
  #private;
36
+ /** Build a new manager. Normally called by `Ownsuite.registerDomain`,
37
+ * not consumers. */
33
38
  constructor(domainName: string, options?: OwnedCollectionManagerOptions<TRow, TCreate, TUpdate>);
34
39
  /** Initialize by fetching the list from the server. */
35
40
  initialize(): Promise<void>;
@@ -32,6 +32,8 @@ const defaultGetRowId = (row) => {
32
32
  */
33
33
  export class OwnedCollectionManager extends BaseDomainManager {
34
34
  #getRowId;
35
+ /** Build a new manager. Normally called by `Ownsuite.registerDomain`,
36
+ * not consumers. */
35
37
  constructor(domainName, options = {}) {
36
38
  super(domainName, options);
37
39
  this.#getRowId = options.getRowId ?? defaultGetRowId;
@@ -20,8 +20,14 @@ import { type StoreLike } from "@marianmeres/store";
20
20
  import { type PubSub } from "@marianmeres/pubsub";
21
21
  import type { OAuthConnection, OAuthProvider, OwnsuiteContext, ProfileAdapter, ProfileResult } from "../types/mod.js";
22
22
  import type { SessionManager } from "./session.js";
23
+ /** Construction-time options for {@link ProfileManager}. Assembled by
24
+ * `createOwnsuite` when a profile adapter is supplied. */
23
25
  export interface ProfileManagerOptions {
26
+ /** Profile adapter — talks to `/me`. */
24
27
  adapter: ProfileAdapter;
28
+ /** Session manager — used to patch the subject in place on fetch /
29
+ * update so consumers reading the session see updates without a second
30
+ * subscription. */
25
31
  session: SessionManager;
26
32
  /** Shared pubsub for event emission. Private if omitted. */
27
33
  pubsub?: PubSub;
@@ -29,6 +35,7 @@ export interface ProfileManagerOptions {
29
35
  * `jwt` and `signal` by the manager. */
30
36
  context?: OwnsuiteContext;
31
37
  }
38
+ /** Reactive state exposed by {@link ProfileManager.get} / `subscribe`. */
32
39
  export interface ProfileState {
33
40
  /** Null until the first successful fetch. */
34
41
  profile: ProfileResult | null;
@@ -42,10 +49,15 @@ export interface ProfileState {
42
49
  */
43
50
  export declare class ProfileManager {
44
51
  #private;
52
+ /** Build a new profile manager. Normally called by `createOwnsuite`. */
45
53
  constructor(options: ProfileManagerOptions);
54
+ /** Svelte-compatible subscribe method over {@link ProfileState}. */
46
55
  get subscribe(): StoreLike<ProfileState>["subscribe"];
56
+ /** Current profile state snapshot. */
47
57
  get(): ProfileState;
58
+ /** Merge `ctx` into the adapter context (keys not present are kept). */
48
59
  setContext(ctx: OwnsuiteContext): void;
60
+ /** Replace the adapter context wholesale. */
49
61
  replaceContext(ctx: OwnsuiteContext): void;
50
62
  /** Fetch `/me`. Supersedes any in-flight fetch. */
51
63
  fetch(): Promise<ProfileResult>;
@@ -55,8 +67,14 @@ export declare class ProfileManager {
55
67
  email?: string;
56
68
  current_password?: string;
57
69
  }): Promise<ProfileResult>;
70
+ /** List OAuth provider connections linked to the authenticated account. */
58
71
  listOAuth(): Promise<OAuthConnection[]>;
72
+ /** Unlink an OAuth provider. Emits `oauth:unlinked` and best-effort
73
+ * re-fetches the profile so the connection list reflects the change. */
59
74
  unlinkOAuth(provider: OAuthProvider): Promise<void>;
75
+ /** Abort any in-flight fetch and clear cached profile state. */
60
76
  reset(): void;
77
+ /** Tear down the manager — aborts in-flight fetches and clears state.
78
+ * Called by `Ownsuite.destroy()`. */
61
79
  destroy(): void;
62
80
  }
@@ -34,6 +34,7 @@ export class ProfileManager {
34
34
  #context;
35
35
  /** Currently-active read controller, for abort-supersede semantics. */
36
36
  #readController = null;
37
+ /** Build a new profile manager. Normally called by `createOwnsuite`. */
37
38
  constructor(options) {
38
39
  this.#adapter = options.adapter;
39
40
  this.#session = options.session;
@@ -41,15 +42,19 @@ export class ProfileManager {
41
42
  this.#context = options.context ?? {};
42
43
  this.#store = createStore({ ...EMPTY });
43
44
  }
45
+ /** Svelte-compatible subscribe method over {@link ProfileState}. */
44
46
  get subscribe() {
45
47
  return this.#store.subscribe;
46
48
  }
49
+ /** Current profile state snapshot. */
47
50
  get() {
48
51
  return this.#store.get();
49
52
  }
53
+ /** Merge `ctx` into the adapter context (keys not present are kept). */
50
54
  setContext(ctx) {
51
55
  this.#context = { ...this.#context, ...ctx };
52
56
  }
57
+ /** Replace the adapter context wholesale. */
53
58
  replaceContext(ctx) {
54
59
  this.#context = { ...ctx };
55
60
  }
@@ -139,10 +144,13 @@ export class ProfileManager {
139
144
  throw e;
140
145
  }
141
146
  }
147
+ /** List OAuth provider connections linked to the authenticated account. */
142
148
  async listOAuth() {
143
149
  const ctrl = new AbortController();
144
150
  return await this.#adapter.listOAuth(this.#ctxFor(ctrl.signal));
145
151
  }
152
+ /** Unlink an OAuth provider. Emits `oauth:unlinked` and best-effort
153
+ * re-fetches the profile so the connection list reflects the change. */
146
154
  async unlinkOAuth(provider) {
147
155
  const ctrl = new AbortController();
148
156
  await this.#adapter.unlinkOAuth(provider, this.#ctxFor(ctrl.signal));
@@ -159,10 +167,13 @@ export class ProfileManager {
159
167
  // swallow — caller already got a successful unlink
160
168
  }
161
169
  }
170
+ /** Abort any in-flight fetch and clear cached profile state. */
162
171
  reset() {
163
172
  this.#abortActiveRead("reset");
164
173
  this.#store.set({ ...EMPTY });
165
174
  }
175
+ /** Tear down the manager — aborts in-flight fetches and clears state.
176
+ * Called by `Ownsuite.destroy()`. */
166
177
  destroy() {
167
178
  this.#abortActiveRead("destroyed");
168
179
  this.#store.set({ ...EMPTY });
@@ -22,6 +22,7 @@ export declare function createMemorySessionStorage(): SessionStorage;
22
22
  * Gracefully falls back to memory storage when running in an environment
23
23
  * without Web Storage (SSR, worker without storage, etc). */
24
24
  export declare function resolveSessionStorage(type?: SessionStorageType): SessionStorage;
25
+ /** Construction-time options for {@link SessionManager}. */
25
26
  export interface SessionManagerOptions {
26
27
  /** Storage backend for session persistence. Default: "local". */
27
28
  storage?: SessionStorageType;
@@ -45,13 +46,21 @@ export interface SessionManagerOptions {
45
46
  */
46
47
  export declare class SessionManager {
47
48
  #private;
49
+ /** Build a new session manager. Hydrates synchronously from storage
50
+ * (probing `local → session → memory` in the built-in case) before the
51
+ * constructor returns, so consumers can read `get()` immediately. */
48
52
  constructor(options?: SessionManagerOptions);
49
53
  /** Svelte-compatible subscribe. */
50
54
  get subscribe(): StoreLike<SessionState>["subscribe"];
51
55
  /** Current session state snapshot. */
52
56
  get(): SessionState;
57
+ /** True when `status === "authenticated"`. Shorthand for `get().status`
58
+ * checks in consumer code. */
53
59
  get isAuthenticated(): boolean;
60
+ /** True when `status === "unverified"` — account exists but the server
61
+ * gates login behind email verification. */
54
62
  get isUnverified(): boolean;
63
+ /** True when `status === "anonymous"` — no session. */
55
64
  get isAnonymous(): boolean;
56
65
  /** JWT for adapter `Authorization` headers, or null when anonymous. */
57
66
  getJwt(): string | null;
@@ -109,6 +109,9 @@ export class SessionManager {
109
109
  #defaultStorageType;
110
110
  #activeStorage;
111
111
  #activeStorageType;
112
+ /** Build a new session manager. Hydrates synchronously from storage
113
+ * (probing `local → session → memory` in the built-in case) before the
114
+ * constructor returns, so consumers can read `get()` immediately. */
112
115
  constructor(options = {}) {
113
116
  this.#pubsub = options.pubsub ?? createPubSub();
114
117
  this.#storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;
@@ -212,12 +215,17 @@ export class SessionManager {
212
215
  get() {
213
216
  return this.#store.get();
214
217
  }
218
+ /** True when `status === "authenticated"`. Shorthand for `get().status`
219
+ * checks in consumer code. */
215
220
  get isAuthenticated() {
216
221
  return this.#store.get().status === "authenticated";
217
222
  }
223
+ /** True when `status === "unverified"` — account exists but the server
224
+ * gates login behind email verification. */
218
225
  get isUnverified() {
219
226
  return this.#store.get().status === "unverified";
220
227
  }
228
+ /** True when `status === "anonymous"` — no session. */
221
229
  get isAnonymous() {
222
230
  return this.#store.get().status === "anonymous";
223
231
  }
package/dist/mod.d.ts CHANGED
@@ -30,3 +30,6 @@ export * from "./domains/mod.js";
30
30
  export * from "./types/mod.js";
31
31
  export * from "./adapters/mod.js";
32
32
  export * from "./oauth/popup.js";
33
+ export type { PubSub, Subscriber, Unsubscriber } from "@marianmeres/pubsub";
34
+ export type { StoreLike } from "@marianmeres/store";
35
+ export type { Clog } from "@marianmeres/clog";
@@ -11,37 +11,62 @@
11
11
  * For tests, an injectable window-like interface lets us shim `postMessage`
12
12
  * via a MessageChannel without a real browser.
13
13
  */
14
+ /** `postMessage` shape the server's OAuth callback posts on successful
15
+ * login (`action: "login"`). */
14
16
  export interface OAuthPopupLoginMessage {
17
+ /** Discriminator. */
15
18
  type: "oauth_login_success";
19
+ /** Freshly minted JWT. */
16
20
  jwt: string;
21
+ /** Email on the account (after provider-email resolution). */
17
22
  email: string;
23
+ /** Roles assigned to the account. */
18
24
  roles?: string[];
25
+ /** True when the account was created as part of this flow. */
19
26
  isNewAccount?: boolean;
27
+ /** Optional post-login redirect hint the server proposes. */
20
28
  redirectUrl?: string;
21
29
  }
30
+ /** `postMessage` shape the server posts on successful link
31
+ * (`action: "link"`). */
22
32
  export interface OAuthPopupLinkMessage {
33
+ /** Discriminator. */
23
34
  type: "oauth_link_success";
35
+ /** Which provider was just linked. */
24
36
  provider: string;
25
37
  }
38
+ /** `postMessage` shape the server posts on error. Rejects the promise. */
26
39
  export interface OAuthPopupErrorMessage {
40
+ /** Discriminator. */
27
41
  type: "oauth_error";
42
+ /** Error string suitable for surfacing to the user. */
28
43
  error: string;
29
44
  }
45
+ /** Union returned by {@link openOAuthPopup}. */
30
46
  export type OAuthPopupMessage = OAuthPopupLoginMessage | OAuthPopupLinkMessage;
31
47
  /**
32
48
  * Minimal window-like surface we need. Real `Window` satisfies this; tests
33
49
  * supply a shim.
34
50
  */
35
51
  export interface PopupWindowHost {
52
+ /** Open a popup and return a handle, or `null` when blocked. */
36
53
  open(url: string, target: string, features?: string): PopupWindowHandle | null;
54
+ /** Listen for `"message"` events emitted by the popup. */
37
55
  addEventListener(type: "message", listener: (event: MessageEvent) => void): void;
56
+ /** Stop listening for `"message"` events. */
38
57
  removeEventListener(type: "message", listener: (event: MessageEvent) => void): void;
39
58
  }
59
+ /** Minimal surface of a popup window handle that {@link openOAuthPopup}
60
+ * interacts with after `host.open(...)`. */
40
61
  export interface PopupWindowHandle {
62
+ /** Reflects whether the popup has been closed (by the user or server). */
41
63
  closed: boolean;
64
+ /** Bring the popup to the front, when the host supports it. */
42
65
  focus?(): void;
66
+ /** Close the popup programmatically. */
43
67
  close?(): void;
44
68
  }
69
+ /** Options for {@link openOAuthPopup}. */
45
70
  export interface OpenOAuthPopupOptions {
46
71
  /** Host window — defaults to `globalThis` when running in the browser. */
47
72
  host?: PopupWindowHost;
@@ -25,7 +25,10 @@ import { SessionManager } from "./domains/session.js";
25
25
  * (defaults to reading `row.model_id` or `row.id`).
26
26
  */
27
27
  export interface OwnsuiteDomainConfig<TRow = any, TCreate = any, TUpdate = any> {
28
+ /** Adapter that talks to the server for this domain. */
28
29
  adapter: OwnedCollectionAdapter<TRow, TCreate, TUpdate>;
30
+ /** Row-id extractor — used for optimistic update / delete matching.
31
+ * Defaults to reading `row.model_id` or `row.id`. */
29
32
  getRowId?: (row: TRow) => string;
30
33
  }
31
34
  /** Top-level ownsuite configuration. */
@@ -76,11 +79,18 @@ export interface SetContextOptions {
76
79
  */
77
80
  export declare class Ownsuite {
78
81
  #private;
79
- /** Optional auth/session/profile managers. Present iff `adapters.auth`
80
- * was supplied at construction. */
82
+ /** Session manager. Present iff `adapters.auth` was supplied at
83
+ * construction; otherwise `null`. */
81
84
  readonly session: SessionManager | null;
85
+ /** Auth manager (register/login/OAuth verbs). Present iff
86
+ * `adapters.auth` was supplied; otherwise `null`. */
82
87
  readonly auth: AuthManager | null;
88
+ /** Profile manager for `/me`. Present iff BOTH `adapters.auth` and
89
+ * `adapters.profile` were supplied; otherwise `null`. */
83
90
  readonly profile: ProfileManager | null;
91
+ /** Build a new suite. See {@link OwnsuiteConfig} for every knob.
92
+ * Account-lifecycle managers are attached only when `adapters.auth`
93
+ * is supplied. */
84
94
  constructor(config?: OwnsuiteConfig);
85
95
  /** True after `destroy()` has been called. */
86
96
  get isDestroyed(): boolean;
@@ -109,6 +119,7 @@ export declare class Ownsuite {
109
119
  * linger when, e.g., `subjectId` changes).
110
120
  */
111
121
  setContext(ctx: OwnsuiteContext, options?: SetContextOptions): void;
122
+ /** Snapshot of the shared context propagated to every domain adapter. */
112
123
  getContext(): OwnsuiteContext;
113
124
  /** Subscribe to a specific event type. */
114
125
  on(type: OwnsuiteEventType, subscriber: Subscriber): Unsubscriber;
package/dist/ownsuite.js CHANGED
@@ -41,11 +41,18 @@ export class Ownsuite {
41
41
  // deno-lint-ignore no-explicit-any
42
42
  #domains = new Map();
43
43
  #destroyed = false;
44
- /** Optional auth/session/profile managers. Present iff `adapters.auth`
45
- * was supplied at construction. */
44
+ /** Session manager. Present iff `adapters.auth` was supplied at
45
+ * construction; otherwise `null`. */
46
46
  session = null;
47
+ /** Auth manager (register/login/OAuth verbs). Present iff
48
+ * `adapters.auth` was supplied; otherwise `null`. */
47
49
  auth = null;
50
+ /** Profile manager for `/me`. Present iff BOTH `adapters.auth` and
51
+ * `adapters.profile` were supplied; otherwise `null`. */
48
52
  profile = null;
53
+ /** Build a new suite. See {@link OwnsuiteConfig} for every knob.
54
+ * Account-lifecycle managers are attached only when `adapters.auth`
55
+ * is supplied. */
49
56
  constructor(config = {}) {
50
57
  this.#pubsub = createPubSub();
51
58
  this.#context = { ...(config.context ?? {}) };
@@ -221,6 +228,7 @@ export class Ownsuite {
221
228
  }
222
229
  }
223
230
  }
231
+ /** Snapshot of the shared context propagated to every domain adapter. */
224
232
  getContext() {
225
233
  return { ...this.#context };
226
234
  }
@@ -15,7 +15,9 @@ import type { OwnsuiteContext } from "./state.js";
15
15
  * whatever shape their server uses and map it here.
16
16
  */
17
17
  export interface OwnedListResult<TRow> {
18
+ /** Rows returned by the list call. */
18
19
  data: TRow[];
20
+ /** Pagination / total-count / any arbitrary server-supplied metadata. */
19
21
  meta: Record<string, unknown>;
20
22
  }
21
23
  /**
@@ -23,7 +25,9 @@ export interface OwnedListResult<TRow> {
23
25
  * collection package's REST envelope.
24
26
  */
25
27
  export interface OwnedRowResult<TRow> {
28
+ /** The row. */
26
29
  data: TRow;
30
+ /** Optional per-row metadata from the server. */
27
31
  meta?: Record<string, unknown>;
28
32
  }
29
33
  /**
@@ -33,17 +33,24 @@ export type SessionStatus =
33
33
  * `/me` returns plus the fields needed for role-gating.
34
34
  */
35
35
  export interface SessionSubject {
36
+ /** Server-assigned subject identifier. Empty string before `/me` lands. */
36
37
  id: string;
38
+ /** Verified or unverified email address of the subject. */
37
39
  email: string;
40
+ /** Authorization roles — drive UI gating, not security. */
38
41
  roles: string[];
42
+ /** True when the server has confirmed the email address. */
39
43
  isVerified: boolean;
40
44
  /** Whether the account has a password (OAuth-only accounts do not). */
41
45
  hasPassword: boolean;
42
46
  }
43
47
  /** Observable session state — stored by the SessionManager. */
44
48
  export interface SessionState {
49
+ /** Current lifecycle bucket; see {@link SessionStatus}. */
45
50
  status: SessionStatus;
51
+ /** Loaded subject, or `null` when anonymous. */
46
52
  subject: SessionSubject | null;
53
+ /** JWT for outgoing `Authorization` headers, `null` when anonymous. */
47
54
  jwt: string | null;
48
55
  /** Unix-seconds expiry (optional — not every server shape returns one). */
49
56
  expiresAt: number | null;
@@ -54,22 +61,39 @@ export interface SessionState {
54
61
  * matching this interface (useful for Tauri, WebExtensions, SSR guards).
55
62
  */
56
63
  export interface SessionStorage {
64
+ /** Read a value by key; return `null` when missing or unreadable. */
57
65
  get(key: string): string | null;
66
+ /** Write a value; silently no-op on quota errors. */
58
67
  set(key: string, value: string): void;
68
+ /** Delete a value; silently no-op when absent. */
59
69
  del(key: string): void;
60
70
  }
71
+ /** Accepted `storage` option for `SessionManager`. String values resolve to
72
+ * the three built-in backends; a {@link SessionStorage} object overrides
73
+ * the defaults entirely (single custom backend — per-login `remember` is
74
+ * silently ignored in that mode). */
61
75
  export type SessionStorageType = "local" | "session" | "memory" | SessionStorage;
76
+ /** Supported OAuth provider identifiers — the server side (`@marianmeres/
77
+ * stack-account`) decides which are actually enabled. */
62
78
  export type OAuthProvider = "google" | "facebook" | "apple" | "twitter";
79
+ /** A single linked OAuth provider connection on the subject's account. */
63
80
  export interface OAuthConnection {
81
+ /** Which provider this connection represents. */
64
82
  provider: OAuthProvider;
83
+ /** Human-readable display name as reported by the provider. */
65
84
  display_name?: string;
85
+ /** Avatar URL reported by the provider (not proxied — may be public). */
66
86
  avatar_url?: string;
87
+ /** Email reported by the provider (may differ from the account email). */
67
88
  email?: string;
68
89
  }
69
90
  /** Action verb for the OAuth init URL — `login` creates/looks-up an account,
70
91
  * `link` attaches the provider to the currently authenticated subject. */
71
92
  export type OAuthAction = "login" | "link";
93
+ /** Options for starting an OAuth flow via `AuthManager.initiateOAuth`. */
72
94
  export interface OAuthInitOptions {
95
+ /** Whether this flow creates/looks-up an account (`login`) or attaches
96
+ * the provider to the currently authenticated subject (`link`). */
73
97
  action: OAuthAction;
74
98
  /** Where to redirect after the provider callback (server honours this). */
75
99
  redirect?: string;
@@ -107,33 +131,60 @@ export interface AuthActionOptions {
107
131
  * manager flips session.status to "unverified".
108
132
  */
109
133
  export interface AuthTokenResult {
134
+ /** JWT to present on subsequent authenticated requests. Absent when
135
+ * `requiresVerification` is `true`. */
110
136
  jwt?: string;
137
+ /** Email as recognised by the server (may be normalised). */
111
138
  email: string;
139
+ /** Roles granted by the server. */
112
140
  roles: string[];
141
+ /** True when the server confirms the email is already verified. */
113
142
  isVerified?: boolean;
143
+ /** JWT `not-before` claim in unix seconds (when server returns one). */
114
144
  validFrom?: number;
145
+ /** JWT `expires-at` claim in unix seconds (when server returns one). */
115
146
  validUntil?: number;
116
147
  /** Set by the server when login auto-login is declined for a not-yet-
117
148
  * verified account. Mutually exclusive with jwt in practice. */
118
149
  requiresVerification?: boolean;
119
150
  }
151
+ /** `/me` profile payload returned by `ProfileAdapter.get` / `update`. */
120
152
  export interface ProfileResult {
153
+ /** Current email on the account. */
121
154
  email: string;
155
+ /** Current authorization roles. */
122
156
  roles: string[];
157
+ /** Whether the email is verified. */
123
158
  isVerified: boolean;
159
+ /** Whether the account has a password. */
124
160
  hasPassword: boolean;
161
+ /** Linked OAuth provider connections. */
125
162
  oauthConnections: OAuthConnection[];
126
163
  }
164
+ /**
165
+ * Contract the `AuthManager` uses to talk to the server. Implement this
166
+ * against whatever transport the backend exposes — `createStackAccountAuth
167
+ * Adapter` provides the default for `@marianmeres/stack-account`. Adapters
168
+ * hold no state; they forward `ctx.jwt` / `ctx.signal` per call.
169
+ */
127
170
  export interface AuthAdapter {
171
+ /** Create an account. When the verification gate is on, the returned
172
+ * result sets `requiresVerification: true` and omits the JWT. */
128
173
  register(input: {
174
+ /** Email as entered; server normalises + stores. */
129
175
  email: string;
176
+ /** Plaintext password — server hashes. */
130
177
  password: string;
178
+ /** Plaintext confirmation, server cross-checks. */
131
179
  password_confirm: string;
180
+ /** Roles to request on the account (server may reject). */
132
181
  roles?: string[];
133
182
  /** Optional extras — consumer forwards any configured
134
183
  * registrationFields the server expects. */
135
184
  extras?: Record<string, unknown>;
136
185
  }, ctx: OwnsuiteContext): Promise<AuthTokenResult>;
186
+ /** Exchange credentials for a JWT. Throws on bad credentials; sets
187
+ * `requiresVerification: true` when the verification gate is on. */
137
188
  login(input: {
138
189
  email: string;
139
190
  password: string;
@@ -148,22 +199,32 @@ export interface AuthAdapter {
148
199
  * URL (or a server-rendered payload) and return it. Popup-mode uses the
149
200
  * `openOAuthPopup` helper directly and does not call this. */
150
201
  handleOAuthCallback?(ctx: OwnsuiteContext): Promise<AuthTokenResult>;
202
+ /** Request a fresh verification email. Anti-enumeration: must resolve
203
+ * regardless of whether the address exists. */
151
204
  resendVerification(input: {
152
205
  email: string;
153
206
  lang?: string;
154
207
  }, ctx: OwnsuiteContext): Promise<void>;
208
+ /** Request a password-reset email. Anti-enumeration: must resolve
209
+ * regardless of whether the address exists. */
155
210
  requestPasswordReset(input: {
156
211
  email: string;
157
212
  lang?: string;
158
213
  }, ctx: OwnsuiteContext): Promise<void>;
214
+ /** Change or reset the password. Provide `current_password` for an
215
+ * authenticated self-change, or `token` for a reset-link flow. */
159
216
  changePassword(input: {
160
217
  /** Required for authenticated self-change. */
161
218
  current_password?: string;
219
+ /** New password (plaintext — hashed server-side). */
162
220
  new_password: string;
221
+ /** Confirmation — must match new_password. */
163
222
  confirm_password: string;
164
223
  /** Required for token-based reset. */
165
224
  token?: string;
166
225
  }, ctx: OwnsuiteContext): Promise<void>;
226
+ /** Irreversibly delete the authenticated account. Resolves with
227
+ * `{ deleted: true }` on success; any failure rejects. */
167
228
  deleteAccount(input: {
168
229
  password?: string;
169
230
  confirm?: boolean;
@@ -171,12 +232,19 @@ export interface AuthAdapter {
171
232
  deleted: true;
172
233
  }>;
173
234
  }
235
+ /** Contract the `ProfileManager` uses to read + mutate `/me`. Like
236
+ * `AuthAdapter`, stateless — reads `ctx.jwt` per call. */
174
237
  export interface ProfileAdapter {
238
+ /** GET `/me` → current profile snapshot. */
175
239
  get(ctx: OwnsuiteContext): Promise<ProfileResult>;
240
+ /** PUT `/me` — update email and/or confirm current password. Returns
241
+ * the refreshed profile. */
176
242
  update(input: {
177
243
  email?: string;
178
244
  current_password?: string;
179
245
  }, ctx: OwnsuiteContext): Promise<ProfileResult>;
246
+ /** GET linked OAuth connections for the authenticated subject. */
180
247
  listOAuth(ctx: OwnsuiteContext): Promise<OAuthConnection[]>;
248
+ /** DELETE a linked OAuth connection. */
181
249
  unlinkOAuth(provider: OAuthProvider, ctx: OwnsuiteContext): Promise<void>;
182
250
  }
@@ -20,82 +20,133 @@ export interface OwnsuiteEventBase {
20
20
  /** Domain that emitted the event */
21
21
  domain: DomainName;
22
22
  }
23
- /** State change event. */
23
+ /** Emitted when a domain transitions between lifecycle states
24
+ * (`initializing` → `ready` ↔ `syncing` → `error`). */
24
25
  export interface StateChangedEvent extends OwnsuiteEventBase {
26
+ /** Discriminator. */
25
27
  type: "domain:state:changed";
28
+ /** State before the transition. */
26
29
  previousState: DomainState;
30
+ /** State after the transition. */
27
31
  newState: DomainState;
28
32
  }
29
- /** Error event. */
33
+ /** Emitted when a domain's mutation or read fails. */
30
34
  export interface ErrorEvent extends OwnsuiteEventBase {
35
+ /** Discriminator. */
31
36
  type: "domain:error";
37
+ /** The failure — normalized across list / get / mutation paths. */
32
38
  error: DomainError;
33
39
  }
34
- /** Sync completed event. */
40
+ /** Emitted when a sync (initialize / refresh / mutation) completes. */
35
41
  export interface SyncedEvent extends OwnsuiteEventBase {
42
+ /** Discriminator. */
36
43
  type: "domain:synced";
37
44
  }
38
- /** List fetched event. */
45
+ /** Emitted after a successful list read. */
39
46
  export interface ListFetchedEvent extends OwnsuiteEventBase {
47
+ /** Discriminator. */
40
48
  type: "own:list:fetched";
49
+ /** Number of rows returned. */
41
50
  count: number;
42
51
  }
43
- /** Single row fetched event. */
52
+ /** Emitted after a successful single-row read (`getOne`). */
44
53
  export interface RowFetchedEvent extends OwnsuiteEventBase {
54
+ /** Discriminator. */
45
55
  type: "own:row:fetched";
56
+ /** Id of the row that was fetched. */
46
57
  rowId: string;
47
58
  }
48
- /** Row created event. */
59
+ /** Emitted after a row is created on the server. */
49
60
  export interface RowCreatedEvent extends OwnsuiteEventBase {
61
+ /** Discriminator. */
50
62
  type: "own:row:created";
63
+ /** Server-assigned id of the new row. */
51
64
  rowId: string;
52
65
  }
53
- /** Row updated event. */
66
+ /** Emitted after a row is updated on the server. */
54
67
  export interface RowUpdatedEvent extends OwnsuiteEventBase {
68
+ /** Discriminator. */
55
69
  type: "own:row:updated";
70
+ /** Id of the updated row. */
56
71
  rowId: string;
57
72
  }
58
- /** Row deleted event. */
73
+ /** Emitted after a row is deleted on the server. */
59
74
  export interface RowDeletedEvent extends OwnsuiteEventBase {
75
+ /** Discriminator. */
60
76
  type: "own:row:deleted";
77
+ /** Id of the deleted row. */
61
78
  rowId: string;
62
79
  }
80
+ /** Base fields shared by every auth / profile / session event. Unlike
81
+ * {@link OwnsuiteEventBase} there is no `domain` — these events live at the
82
+ * suite level, not a specific domain. */
63
83
  export interface AuthEventBase {
84
+ /** Unix millis at emission time. */
64
85
  timestamp: number;
65
86
  }
87
+ /** Emitted after `AuthManager.register` returns a server response. Fires
88
+ * whether or not the verification gate was hit. */
66
89
  export interface AuthRegisterEvent extends AuthEventBase {
90
+ /** Discriminator. */
67
91
  type: "auth:register";
92
+ /** Email the account was created with. */
68
93
  email: string;
69
94
  /** True when the server requires email verification (no auto-login). */
70
95
  requiresVerification: boolean;
71
96
  }
97
+ /** Emitted after `AuthManager.login` completes a credential exchange. Does
98
+ * not imply `status === "authenticated"` — the verification gate may still
99
+ * flip it to `"unverified"` afterwards. */
72
100
  export interface AuthLoginEvent extends AuthEventBase {
101
+ /** Discriminator. */
73
102
  type: "auth:login";
103
+ /** Email used to log in. */
74
104
  email: string;
75
105
  }
106
+ /** Emitted on `AuthManager.logout` and `deleteAccount` once the session has
107
+ * been cleared locally. */
76
108
  export interface AuthLogoutEvent extends AuthEventBase {
109
+ /** Discriminator. */
77
110
  type: "auth:logout";
78
111
  /** Id of the subject that just logged out, if known. */
79
112
  subjectId?: string;
80
113
  }
114
+ /** Emitted whenever `SessionManager` persists a new state — including
115
+ * hydration-driven changes and `patchSubject` writes. */
81
116
  export interface AuthSessionChangedEvent extends AuthEventBase {
117
+ /** Discriminator. */
82
118
  type: "auth:session:changed";
119
+ /** Snapshot of the session *after* the change. */
83
120
  session: SessionState;
84
121
  }
122
+ /** Emitted when the server rejects login/register because the account is
123
+ * not yet verified. UI should surface a "check your inbox" prompt. */
85
124
  export interface AuthVerificationRequiredEvent extends AuthEventBase {
125
+ /** Discriminator. */
86
126
  type: "auth:verification:required";
127
+ /** Email that needs to be verified. */
87
128
  email: string;
88
129
  }
130
+ /** Emitted after `ProfileManager.update` succeeds. The session subject has
131
+ * already been patched in place by the time this fires. */
89
132
  export interface ProfileUpdatedEvent extends AuthEventBase {
133
+ /** Discriminator. */
90
134
  type: "profile:updated";
135
+ /** Email on the profile *after* the update. */
91
136
  email: string;
92
137
  }
138
+ /** Emitted after a successful OAuth *link* flow (popup / redirect). */
93
139
  export interface OAuthLinkedEvent extends AuthEventBase {
140
+ /** Discriminator. */
94
141
  type: "oauth:linked";
142
+ /** Connection that was just added to the subject's account. */
95
143
  connection: OAuthConnection;
96
144
  }
145
+ /** Emitted after `ProfileManager.unlinkOAuth` succeeds. */
97
146
  export interface OAuthUnlinkedEvent extends AuthEventBase {
147
+ /** Discriminator. */
98
148
  type: "oauth:unlinked";
149
+ /** Provider that was just unlinked. */
99
150
  provider: OAuthProvider;
100
151
  }
101
152
  /** All event types union. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/ownsuite",
3
- "version": "2.2.1",
3
+ "version": "2.2.2",
4
4
  "type": "module",
5
5
  "main": "dist/mod.js",
6
6
  "types": "dist/mod.d.ts",