@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.
@@ -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;
@@ -34,40 +35,67 @@ export interface SessionManagerOptions {
34
35
  /**
35
36
  * Session manager — pure reactive state + persistence, no HTTP.
36
37
  *
37
- * Writes are driven by the AuthManager. On construction it hydrates from
38
- * storage; if the stored JWT has expired, it transitions to anonymous and
39
- * clears the storage.
38
+ * Writes are driven by the AuthManager. On construction it hydrates by
39
+ * probing the built-in backends in order `local session memory` and
40
+ * adopting whichever holds a non-expired payload as the active backend for
41
+ * the rest of the instance's lifetime (until `clear()`). Stale blobs on the
42
+ * other built-in backends are wiped on adoption.
43
+ *
44
+ * When constructed with a custom `SessionStorage` object, there is a single
45
+ * backend and per-login `remember` choices are silently ignored.
40
46
  */
41
47
  export declare class SessionManager {
42
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. */
43
52
  constructor(options?: SessionManagerOptions);
44
53
  /** Svelte-compatible subscribe. */
45
54
  get subscribe(): StoreLike<SessionState>["subscribe"];
46
55
  /** Current session state snapshot. */
47
56
  get(): SessionState;
57
+ /** True when `status === "authenticated"`. Shorthand for `get().status`
58
+ * checks in consumer code. */
48
59
  get isAuthenticated(): boolean;
60
+ /** True when `status === "unverified"` — account exists but the server
61
+ * gates login behind email verification. */
49
62
  get isUnverified(): boolean;
63
+ /** True when `status === "anonymous"` — no session. */
50
64
  get isAnonymous(): boolean;
51
65
  /** JWT for adapter `Authorization` headers, or null when anonymous. */
52
66
  getJwt(): string | null;
53
67
  /** Transition to authenticated. Called after login/register/OAuth succeed
54
- * and the subject has been loaded. */
68
+ * and the subject has been loaded.
69
+ *
70
+ * When `opts.storage` is one of `"local"` / `"session"` / `"memory"`,
71
+ * pins this session to that built-in backend; subsequent
72
+ * `patchSubject` / `setUnverified` writes land on the same backend.
73
+ * The previously-active backend's blob is wiped as part of the switch
74
+ * so "Remember me" toggles don't leave stale data behind.
75
+ *
76
+ * Ignored when the manager was constructed with a custom `SessionStorage`
77
+ * object (single backend) or when `opts.storage` is itself an object
78
+ * (no multi-backend custom storage by design). */
55
79
  setAuthenticated(opts: {
56
80
  jwt: string;
57
81
  subject: SessionSubject;
58
82
  expiresAt?: number | null;
83
+ storage?: SessionStorageType;
59
84
  }): void;
60
85
  /** Server confirmed credentials but refuses login until email is
61
86
  * verified. Expose the email so the UI can prompt "check your inbox"
62
87
  * without a second server call. No JWT in this state. */
63
88
  setUnverified(email: string): void;
64
- /** Drop the session. Storage is cleared; downstream domains should be
65
- * reset by the suite orchestrator. */
89
+ /** Drop the session. Every built-in backend (local + session + memory)
90
+ * is wiped — not just the active one — so stale blobs from a previous
91
+ * "Remember me" toggle can't leak back in on the next construction.
92
+ * Resets the active backend to the manager's configured default.
93
+ * Downstream domains should be reset by the suite orchestrator. */
66
94
  clear(): void;
67
95
  /** Patch the subject in place without touching the JWT. Used when the
68
96
  * profile manager changes email / linked providers / etc. */
69
97
  patchSubject(patch: Partial<SessionSubject>): void;
70
- /** Test / reset hook — clears storage AND in-memory state without
98
+ /** Test / reset hook — clears every backend AND in-memory state without
71
99
  * emitting. Used by teardown. */
72
100
  destroy(): void;
73
101
  }
@@ -16,6 +16,11 @@
16
16
  import { createStore } from "@marianmeres/store";
17
17
  import { createPubSub } from "@marianmeres/pubsub";
18
18
  const DEFAULT_STORAGE_KEY = "ownsuite:session";
19
+ const BUILT_IN_ORDER = [
20
+ "local",
21
+ "session",
22
+ "memory",
23
+ ];
19
24
  const EMPTY = {
20
25
  status: "anonymous",
21
26
  subject: null,
@@ -82,56 +87,117 @@ export function resolveSessionStorage(type = "local") {
82
87
  /**
83
88
  * Session manager — pure reactive state + persistence, no HTTP.
84
89
  *
85
- * Writes are driven by the AuthManager. On construction it hydrates from
86
- * storage; if the stored JWT has expired, it transitions to anonymous and
87
- * clears the storage.
90
+ * Writes are driven by the AuthManager. On construction it hydrates by
91
+ * probing the built-in backends in order `local session memory` and
92
+ * adopting whichever holds a non-expired payload as the active backend for
93
+ * the rest of the instance's lifetime (until `clear()`). Stale blobs on the
94
+ * other built-in backends are wiped on adoption.
95
+ *
96
+ * When constructed with a custom `SessionStorage` object, there is a single
97
+ * backend and per-login `remember` choices are silently ignored.
88
98
  */
89
99
  export class SessionManager {
90
100
  #store;
91
101
  #pubsub;
92
- #storage;
93
102
  #storageKey;
103
+ /** Set only when the consumer passed a `SessionStorage` object at
104
+ * construction — then there is one backend and no toggling. */
105
+ #customStorage;
106
+ /** Resolved eagerly in the string-storage case so `clear()` can wipe
107
+ * every backend and per-login overrides can switch between them. */
108
+ #builtIn;
109
+ #defaultStorageType;
110
+ #activeStorage;
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. */
94
115
  constructor(options = {}) {
95
116
  this.#pubsub = options.pubsub ?? createPubSub();
96
- this.#storage = resolveSessionStorage(options.storage ?? "local");
97
117
  this.#storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;
98
118
  this.#store = createStore({ ...EMPTY });
119
+ const configured = options.storage ?? "local";
120
+ if (typeof configured === "string") {
121
+ this.#customStorage = null;
122
+ this.#builtIn = {
123
+ local: resolveSessionStorage("local"),
124
+ session: resolveSessionStorage("session"),
125
+ memory: createMemorySessionStorage(),
126
+ };
127
+ this.#defaultStorageType = configured;
128
+ this.#activeStorage = this.#builtIn[configured];
129
+ this.#activeStorageType = configured;
130
+ }
131
+ else {
132
+ this.#customStorage = configured;
133
+ this.#builtIn = null;
134
+ this.#defaultStorageType = "local";
135
+ this.#activeStorage = configured;
136
+ this.#activeStorageType = "custom";
137
+ }
99
138
  this.#hydrate();
100
139
  }
101
- /** Read from storage and populate the store. Expired sessions are wiped. */
102
- #hydrate() {
103
- const raw = this.#storage.get(this.#storageKey);
140
+ /** Try to parse a payload and validate shape + expiry. Returns the state
141
+ * on success, or `null` (and deletes the stored blob) on any failure. */
142
+ #readCandidate(storage) {
143
+ const raw = storage.get(this.#storageKey);
104
144
  if (!raw)
105
- return;
145
+ return null;
106
146
  try {
107
147
  const parsed = JSON.parse(raw);
108
- // Basic shape check.
109
148
  if (typeof parsed !== "object" ||
110
149
  parsed === null ||
111
150
  typeof parsed.status !== "string") {
112
- this.#storage.del(this.#storageKey);
113
- return;
151
+ storage.del(this.#storageKey);
152
+ return null;
114
153
  }
115
- // Expiry check (only meaningful when expiresAt is set).
116
154
  if (parsed.expiresAt !== null &&
117
155
  parsed.expiresAt !== undefined &&
118
156
  parsed.expiresAt * 1000 <= Date.now()) {
119
- this.#storage.del(this.#storageKey);
120
- return;
157
+ storage.del(this.#storageKey);
158
+ return null;
121
159
  }
122
- this.#store.set(parsed);
160
+ return parsed;
123
161
  }
124
162
  catch {
125
- this.#storage.del(this.#storageKey);
163
+ storage.del(this.#storageKey);
164
+ return null;
165
+ }
166
+ }
167
+ /** Read from storage and populate the store. Expired sessions are wiped.
168
+ * In the built-in case, probes `local → session → memory` and wipes
169
+ * the losing backends so stale blobs can't leak back in. */
170
+ #hydrate() {
171
+ if (this.#customStorage) {
172
+ const parsed = this.#readCandidate(this.#customStorage);
173
+ if (parsed)
174
+ this.#store.set(parsed);
175
+ return;
176
+ }
177
+ const builtIn = this.#builtIn;
178
+ for (const type of BUILT_IN_ORDER) {
179
+ const parsed = this.#readCandidate(builtIn[type]);
180
+ if (!parsed)
181
+ continue;
182
+ // Adopt this backend; wipe the others so a later login-with-toggle
183
+ // can't accidentally re-hydrate a stale blob.
184
+ for (const other of BUILT_IN_ORDER) {
185
+ if (other !== type)
186
+ builtIn[other].del(this.#storageKey);
187
+ }
188
+ this.#activeStorage = builtIn[type];
189
+ this.#activeStorageType = type;
190
+ this.#store.set(parsed);
191
+ return;
126
192
  }
127
193
  }
128
194
  #persist() {
129
195
  const s = this.#store.get();
130
196
  if (s.status === "anonymous") {
131
- this.#storage.del(this.#storageKey);
197
+ this.#activeStorage.del(this.#storageKey);
132
198
  }
133
199
  else {
134
- this.#storage.set(this.#storageKey, JSON.stringify(s));
200
+ this.#activeStorage.set(this.#storageKey, JSON.stringify(s));
135
201
  }
136
202
  }
137
203
  #emitChange() {
@@ -149,12 +215,17 @@ export class SessionManager {
149
215
  get() {
150
216
  return this.#store.get();
151
217
  }
218
+ /** True when `status === "authenticated"`. Shorthand for `get().status`
219
+ * checks in consumer code. */
152
220
  get isAuthenticated() {
153
221
  return this.#store.get().status === "authenticated";
154
222
  }
223
+ /** True when `status === "unverified"` — account exists but the server
224
+ * gates login behind email verification. */
155
225
  get isUnverified() {
156
226
  return this.#store.get().status === "unverified";
157
227
  }
228
+ /** True when `status === "anonymous"` — no session. */
158
229
  get isAnonymous() {
159
230
  return this.#store.get().status === "anonymous";
160
231
  }
@@ -163,9 +234,26 @@ export class SessionManager {
163
234
  return this.#store.get().jwt;
164
235
  }
165
236
  /** Transition to authenticated. Called after login/register/OAuth succeed
166
- * and the subject has been loaded. */
237
+ * and the subject has been loaded.
238
+ *
239
+ * When `opts.storage` is one of `"local"` / `"session"` / `"memory"`,
240
+ * pins this session to that built-in backend; subsequent
241
+ * `patchSubject` / `setUnverified` writes land on the same backend.
242
+ * The previously-active backend's blob is wiped as part of the switch
243
+ * so "Remember me" toggles don't leave stale data behind.
244
+ *
245
+ * Ignored when the manager was constructed with a custom `SessionStorage`
246
+ * object (single backend) or when `opts.storage` is itself an object
247
+ * (no multi-backend custom storage by design). */
167
248
  setAuthenticated(opts) {
168
- const { jwt, subject, expiresAt = null } = opts;
249
+ const { jwt, subject, expiresAt = null, storage } = opts;
250
+ if (typeof storage === "string" && this.#builtIn) {
251
+ if (this.#activeStorage !== this.#builtIn[storage]) {
252
+ this.#activeStorage.del(this.#storageKey);
253
+ this.#activeStorage = this.#builtIn[storage];
254
+ this.#activeStorageType = storage;
255
+ }
256
+ }
169
257
  this.#store.set({
170
258
  status: "authenticated",
171
259
  subject,
@@ -194,15 +282,32 @@ export class SessionManager {
194
282
  this.#persist();
195
283
  this.#emitChange();
196
284
  }
197
- /** Drop the session. Storage is cleared; downstream domains should be
198
- * reset by the suite orchestrator. */
285
+ /** Drop the session. Every built-in backend (local + session + memory)
286
+ * is wiped — not just the active one — so stale blobs from a previous
287
+ * "Remember me" toggle can't leak back in on the next construction.
288
+ * Resets the active backend to the manager's configured default.
289
+ * Downstream domains should be reset by the suite orchestrator. */
199
290
  clear() {
291
+ this.#wipeAllBackends();
292
+ if (this.#builtIn) {
293
+ this.#activeStorage = this.#builtIn[this.#defaultStorageType];
294
+ this.#activeStorageType = this.#defaultStorageType;
295
+ }
200
296
  if (this.#store.get().status === "anonymous")
201
297
  return;
202
298
  this.#store.set({ ...EMPTY });
203
- this.#persist();
204
299
  this.#emitChange();
205
300
  }
301
+ #wipeAllBackends() {
302
+ if (this.#customStorage) {
303
+ this.#customStorage.del(this.#storageKey);
304
+ return;
305
+ }
306
+ const builtIn = this.#builtIn;
307
+ for (const type of BUILT_IN_ORDER) {
308
+ builtIn[type].del(this.#storageKey);
309
+ }
310
+ }
206
311
  /** Patch the subject in place without touching the JWT. Used when the
207
312
  * profile manager changes email / linked providers / etc. */
208
313
  patchSubject(patch) {
@@ -217,10 +322,10 @@ export class SessionManager {
217
322
  this.#persist();
218
323
  this.#emitChange();
219
324
  }
220
- /** Test / reset hook — clears storage AND in-memory state without
325
+ /** Test / reset hook — clears every backend AND in-memory state without
221
326
  * emitting. Used by teardown. */
222
327
  destroy() {
223
- this.#storage.del(this.#storageKey);
328
+ this.#wipeAllBackends();
224
329
  this.#store.set({ ...EMPTY });
225
330
  }
226
331
  }
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
  /**