@ai-matrx/auth 0.2.2 → 0.2.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog — @ai-matrx/auth
2
2
 
3
+ ## 0.2.4
4
+ ### active organization options (round 2)
5
+
6
+ All options default to today's behaviour. `createActiveOrganization`:
7
+ - `accountChoices` — the snapshot carries `favourites` and `startupOrganizationId` (always present: `[]` / null when off); when on,
8
+ `memberships` is ordered favourites-first and a failed favourites read leaves them out. New `setFavourite(id, on)` (optimistic,
9
+ rolled back and rethrown on failure) and `setStartup(id | null)`.
10
+ - `cached` `{ userId, read, write? }` — a new `unchecked` status: the memberships could not be read, so the host's cached last answer
11
+ (or the list already on screen) is shown unverified. `current()` / `ensureOrgId()` answer in `unchecked`.
12
+ - `onLapse({ was, now })` — the organization the person was in is no longer a membership and the ladder chose another.
13
+ - `refreshGapMs` + `refresh({ force? })` — a throttled re-read.
14
+ - `holdResolved` (default true) — false: the ladder's own answer is never written as the held choice; only `switchTo` writes it
15
+ (the extension's rule).
16
+ - `ActiveOrganizationSnapshot` gains `favourites` and `startupOrganizationId`; new types `CachedActiveOrganization`, `OrganizationLapse`.
17
+ Tests: `active-organization.test.ts` (+9).
18
+
19
+ ## 0.2.3
20
+
21
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
22
+ `git diff npm/auth/v0.2.2..npm/auth/v0.2.3 -- apps/shared/auth`).
23
+ No source changes intended and no consumer action required.
24
+
3
25
  ## 0.2.2
4
26
  ### extension/desktop options (round 2)
5
27
 
@@ -515,7 +515,11 @@ interface HeldOrganizationStore {
515
515
  write(userId: string, organizationId: string): void | Promise<void>;
516
516
  clear(): void | Promise<void>;
517
517
  }
518
- type ActiveOrganizationStatus = "idle" | "resolving" | "ready" | "none" | "error";
518
+ /**
519
+ * `unchecked`: the memberships could not be read, and the answer shown is the host's cached last answer
520
+ * (`options.cached`), not verified against the database. Only ever produced when `cached` is given.
521
+ */
522
+ type ActiveOrganizationStatus = "idle" | "resolving" | "ready" | "none" | "error" | "unchecked";
519
523
  interface ActiveOrganizationSnapshot {
520
524
  readonly status: ActiveOrganizationStatus;
521
525
  readonly userId: string | null;
@@ -525,6 +529,25 @@ interface ActiveOrganizationSnapshot {
525
529
  /** Which ladder rung answered (or "switched" after a person's click). */
526
530
  readonly rung: OrganizationRung | "switched" | "link" | null;
527
531
  readonly error: string | null;
532
+ /**
533
+ * The account's favourite organization ids. Read only when `options.accountChoices` is on; `[]`
534
+ * otherwise. When on, `memberships` is ordered favourites, then own, then the rest.
535
+ */
536
+ readonly favourites: readonly string[];
537
+ /** The account's start-up organization setting. Read only when `options.accountChoices` is on; null otherwise. */
538
+ readonly startupOrganizationId: string | null;
539
+ }
540
+ /** What a host keeps so a launch with no network can still show the last answer (`options.cached`). */
541
+ interface CachedActiveOrganization {
542
+ readonly memberships: readonly OrganizationMembership[];
543
+ readonly organizationId: string | null;
544
+ readonly favourites: readonly string[];
545
+ readonly startupOrganizationId: string | null;
546
+ }
547
+ /** A held organization that is no longer a membership, and the one the ladder chose instead. */
548
+ interface OrganizationLapse {
549
+ readonly was: OrganizationMembership;
550
+ readonly now: OrganizationMembership;
528
551
  }
529
552
  declare class NoActiveOrganizationError extends Error {
530
553
  readonly reason: "signed-out" | "no-memberships" | "unreadable";
@@ -542,6 +565,14 @@ interface ActiveOrganization {
542
565
  /** The active organization id now, or null while resolving / with none. Sync (api's getter). */
543
566
  current(): string | null;
544
567
  /** THE HELD REQUEST: the record's own organization, else the active one once resolved. */
568
+ /** Re-read, unless `options.refreshGapMs` has not elapsed since the last read (`force` always reads). */
569
+ refresh(options?: {
570
+ readonly force?: boolean;
571
+ }): Promise<ActiveOrganizationSnapshot>;
572
+ /** Save the account's start-up organization (null: the first). Needs `accountChoices`; throws on refusal. */
573
+ setStartup(organizationId: string | null): Promise<void>;
574
+ /** Add or remove a favourite and re-order. Optimistic; rolled back and rethrown if the save fails. */
575
+ setFavourite(organizationId: string, favourite: boolean): Promise<void>;
545
576
  ensureOrgId(recordOrganizationId?: string | null): Promise<string>;
546
577
  /** A person's click: switch, remember for this tab, save as last active for the next load. */
547
578
  switchTo(organizationId: string): Promise<void>;
@@ -582,13 +613,38 @@ interface CreateActiveOrganizationOptions {
582
613
  readonly refusalRecheckGapMs?: number | undefined;
583
614
  /** Load as soon as it is created (default true). */
584
615
  readonly autoLoad?: boolean | undefined;
616
+ /**
617
+ * Read the account's favourites and start-up organization with the memberships, expose both in
618
+ * the snapshot and order `memberships` favourites-first. A failed favourites read leaves them out
619
+ * (the list still shows). Default false.
620
+ */
621
+ readonly accountChoices?: boolean | undefined;
622
+ /**
623
+ * Offline answer. When the memberships cannot be read and nothing is shown yet, the cached answer
624
+ * is published as `unchecked` if its organization is the held one (or the cached one). `userId`
625
+ * says whose cache to read when the person cannot be verified; `write` is called after each good read.
626
+ */
627
+ readonly cached?: {
628
+ readonly userId: () => string | null | undefined;
629
+ readonly read: (userId: string) => CachedActiveOrganization | null | Promise<CachedActiveOrganization | null>;
630
+ readonly write?: ((userId: string, answer: CachedActiveOrganization) => void | Promise<void>) | undefined;
631
+ } | undefined;
632
+ /** Called once when the organization the person was in is no longer a membership and the ladder chose another. */
633
+ readonly onLapse?: ((lapse: OrganizationLapse) => void) | undefined;
634
+ /** `refresh()` without `force` does nothing within this many ms of the last read. Default 0 (always reads). */
635
+ readonly refreshGapMs?: number | undefined;
636
+ /**
637
+ * Write the ladder's own answer back as the held organization. Default true. False: only an
638
+ * explicit `switchTo` writes the held choice (the extension's rule: a computed answer is never a choice).
639
+ */
640
+ readonly holdResolved?: boolean | undefined;
585
641
  }
586
642
  /** A held store over a `Storage` (browser `sessionStorage` = per tab), identity-keyed. */
587
- declare function storageHeldOrganization(storage: Pick<Storage, "getItem" | "setItem" | "removeItem">, key?: string): HeldOrganizationStore & {
643
+ declare function storageHeldOrganization(storage: Pick<Storage, "getItem" | "setItem" | "removeItem">, key?: string): Omit<HeldOrganizationStore, "read"> & {
588
644
  read(userId: string): string | null;
589
645
  };
590
646
  /** Held in memory: the running app's own choice (desktop, scripts, tests). */
591
647
  declare function memoryHeldOrganization(): HeldOrganizationStore;
592
648
  declare function createActiveOrganization(options: CreateActiveOrganizationOptions): ActiveOrganization;
593
649
 
594
- export { decideLinkOrganization as $, type AuthClient as A, NotAMemberError as B, type CreateActiveOrganizationOptions as C, type NotAMemberRefusalNotice as D, ORGANIZATION_RESOLUTION_TIMEOUT_MS as E, type OrganizationBootstrapGate as F, type OrganizationMembership as G, type HeldOrganizationStore as H, type OrganizationRefusal as I, type OrganizationRung as J, type OrganizationsDb as K, LINK_ORGANIZATION_QUERY_KEY as L, type ResolveWithLinkInput as M, NOT_A_MEMBER_RECHECK_GAP_MS as N, ORGANIZATION_ENTITY_TYPE as O, type ParentOrganization as P, type ResolvedOrganization as Q, type ReadMembershipsOptions as R, type SessionSourceLike as S, type ResolvedWithLink as T, type UserCredentialsPort as U, adminSeatReplacesUserContext as V, classifyLinkOrganizationValue as W, classifyOrganizationRefusal as X, createActiveOrganization as Y, createNotAMemberRefusalNotice as Z, createOrganizationBootstrapGate as _, SignInError as a, isNotAMemberRefusal as a0, linkOrganizationMalformedMessage as a1, linkOrganizationNotAMemberMessage as a2, linkOrganizationOfferMessage as a3, linkOrganizationSwitchAnnouncement as a4, memoryHeldOrganization as a5, oldestMembership as a6, orderOrganizations as a7, organizationMatches as a8, readAccountOrganizationChoices as a9, readLinkOrganizationParam as aa, readMemberships as ab, readOrganizationFavourites as ac, resolveActiveOrganization as ad, resolveActiveOrganizationLazily as ae, resolveAdminSeat as af, resolveChildOrganizationId as ag, resolveOrganizationWithLink as ah, setOrganizationFavourite as ai, storageHeldOrganization as aj, toAccountOrganizationChoices as ak, writeLastActiveOrganization as al, writeStartupOrganization as am, getSession as b, signInWithOAuth as c, signInWithPassword as d, signOut as e, userCredentials as f, getPerson as g, type AccountOrganizationChoices as h, invalidatePerson as i, type ActiveOrganization as j, type ActiveOrganizationSnapshot as k, type ActiveOrganizationStatus as l, type AdminSeat as m, type AdminSeatInput as n, onSessionChange as o, type ArchiveFilter as p, type LadderInput as q, refreshSession as r, sessionCredentials as s, type LinkOrganizationDecision as t, updatePerson as u, type LinkOrganizationDecisionInput as v, type LinkOrganizationMembership as w, type LinkOrganizationMessages as x, type LinkOrganizationParam as y, NoActiveOrganizationError as z };
650
+ export { createNotAMemberRefusalNotice as $, type AuthClient as A, NoActiveOrganizationError as B, type CachedActiveOrganization as C, NotAMemberError as D, type NotAMemberRefusalNotice as E, ORGANIZATION_RESOLUTION_TIMEOUT_MS as F, type OrganizationBootstrapGate as G, type HeldOrganizationStore as H, type OrganizationLapse as I, type OrganizationMembership as J, type OrganizationRefusal as K, LINK_ORGANIZATION_QUERY_KEY as L, type OrganizationRung as M, NOT_A_MEMBER_RECHECK_GAP_MS as N, ORGANIZATION_ENTITY_TYPE as O, type OrganizationsDb as P, type ParentOrganization as Q, type ReadMembershipsOptions as R, type SessionSourceLike as S, type ResolveWithLinkInput as T, type UserCredentialsPort as U, type ResolvedOrganization as V, type ResolvedWithLink as W, adminSeatReplacesUserContext as X, classifyLinkOrganizationValue as Y, classifyOrganizationRefusal as Z, createActiveOrganization as _, SignInError as a, createOrganizationBootstrapGate as a0, decideLinkOrganization as a1, isNotAMemberRefusal as a2, linkOrganizationMalformedMessage as a3, linkOrganizationNotAMemberMessage as a4, linkOrganizationOfferMessage as a5, linkOrganizationSwitchAnnouncement as a6, memoryHeldOrganization as a7, oldestMembership as a8, orderOrganizations as a9, organizationMatches as aa, readAccountOrganizationChoices as ab, readLinkOrganizationParam as ac, readMemberships as ad, readOrganizationFavourites as ae, resolveActiveOrganization as af, resolveActiveOrganizationLazily as ag, resolveAdminSeat as ah, resolveChildOrganizationId as ai, resolveOrganizationWithLink as aj, setOrganizationFavourite as ak, storageHeldOrganization as al, toAccountOrganizationChoices as am, writeLastActiveOrganization as an, writeStartupOrganization as ao, getSession as b, signInWithOAuth as c, signInWithPassword as d, signOut as e, userCredentials as f, getPerson as g, type AccountOrganizationChoices as h, invalidatePerson as i, type ActiveOrganization as j, type ActiveOrganizationSnapshot as k, type ActiveOrganizationStatus as l, type AdminSeat as m, type AdminSeatInput as n, onSessionChange as o, type ArchiveFilter as p, type CreateActiveOrganizationOptions as q, refreshSession as r, sessionCredentials as s, type LadderInput as t, updatePerson as u, type LinkOrganizationDecision as v, type LinkOrganizationDecisionInput as w, type LinkOrganizationMembership as x, type LinkOrganizationMessages as y, type LinkOrganizationParam as z };
@@ -515,7 +515,11 @@ interface HeldOrganizationStore {
515
515
  write(userId: string, organizationId: string): void | Promise<void>;
516
516
  clear(): void | Promise<void>;
517
517
  }
518
- type ActiveOrganizationStatus = "idle" | "resolving" | "ready" | "none" | "error";
518
+ /**
519
+ * `unchecked`: the memberships could not be read, and the answer shown is the host's cached last answer
520
+ * (`options.cached`), not verified against the database. Only ever produced when `cached` is given.
521
+ */
522
+ type ActiveOrganizationStatus = "idle" | "resolving" | "ready" | "none" | "error" | "unchecked";
519
523
  interface ActiveOrganizationSnapshot {
520
524
  readonly status: ActiveOrganizationStatus;
521
525
  readonly userId: string | null;
@@ -525,6 +529,25 @@ interface ActiveOrganizationSnapshot {
525
529
  /** Which ladder rung answered (or "switched" after a person's click). */
526
530
  readonly rung: OrganizationRung | "switched" | "link" | null;
527
531
  readonly error: string | null;
532
+ /**
533
+ * The account's favourite organization ids. Read only when `options.accountChoices` is on; `[]`
534
+ * otherwise. When on, `memberships` is ordered favourites, then own, then the rest.
535
+ */
536
+ readonly favourites: readonly string[];
537
+ /** The account's start-up organization setting. Read only when `options.accountChoices` is on; null otherwise. */
538
+ readonly startupOrganizationId: string | null;
539
+ }
540
+ /** What a host keeps so a launch with no network can still show the last answer (`options.cached`). */
541
+ interface CachedActiveOrganization {
542
+ readonly memberships: readonly OrganizationMembership[];
543
+ readonly organizationId: string | null;
544
+ readonly favourites: readonly string[];
545
+ readonly startupOrganizationId: string | null;
546
+ }
547
+ /** A held organization that is no longer a membership, and the one the ladder chose instead. */
548
+ interface OrganizationLapse {
549
+ readonly was: OrganizationMembership;
550
+ readonly now: OrganizationMembership;
528
551
  }
529
552
  declare class NoActiveOrganizationError extends Error {
530
553
  readonly reason: "signed-out" | "no-memberships" | "unreadable";
@@ -542,6 +565,14 @@ interface ActiveOrganization {
542
565
  /** The active organization id now, or null while resolving / with none. Sync (api's getter). */
543
566
  current(): string | null;
544
567
  /** THE HELD REQUEST: the record's own organization, else the active one once resolved. */
568
+ /** Re-read, unless `options.refreshGapMs` has not elapsed since the last read (`force` always reads). */
569
+ refresh(options?: {
570
+ readonly force?: boolean;
571
+ }): Promise<ActiveOrganizationSnapshot>;
572
+ /** Save the account's start-up organization (null: the first). Needs `accountChoices`; throws on refusal. */
573
+ setStartup(organizationId: string | null): Promise<void>;
574
+ /** Add or remove a favourite and re-order. Optimistic; rolled back and rethrown if the save fails. */
575
+ setFavourite(organizationId: string, favourite: boolean): Promise<void>;
545
576
  ensureOrgId(recordOrganizationId?: string | null): Promise<string>;
546
577
  /** A person's click: switch, remember for this tab, save as last active for the next load. */
547
578
  switchTo(organizationId: string): Promise<void>;
@@ -582,13 +613,38 @@ interface CreateActiveOrganizationOptions {
582
613
  readonly refusalRecheckGapMs?: number | undefined;
583
614
  /** Load as soon as it is created (default true). */
584
615
  readonly autoLoad?: boolean | undefined;
616
+ /**
617
+ * Read the account's favourites and start-up organization with the memberships, expose both in
618
+ * the snapshot and order `memberships` favourites-first. A failed favourites read leaves them out
619
+ * (the list still shows). Default false.
620
+ */
621
+ readonly accountChoices?: boolean | undefined;
622
+ /**
623
+ * Offline answer. When the memberships cannot be read and nothing is shown yet, the cached answer
624
+ * is published as `unchecked` if its organization is the held one (or the cached one). `userId`
625
+ * says whose cache to read when the person cannot be verified; `write` is called after each good read.
626
+ */
627
+ readonly cached?: {
628
+ readonly userId: () => string | null | undefined;
629
+ readonly read: (userId: string) => CachedActiveOrganization | null | Promise<CachedActiveOrganization | null>;
630
+ readonly write?: ((userId: string, answer: CachedActiveOrganization) => void | Promise<void>) | undefined;
631
+ } | undefined;
632
+ /** Called once when the organization the person was in is no longer a membership and the ladder chose another. */
633
+ readonly onLapse?: ((lapse: OrganizationLapse) => void) | undefined;
634
+ /** `refresh()` without `force` does nothing within this many ms of the last read. Default 0 (always reads). */
635
+ readonly refreshGapMs?: number | undefined;
636
+ /**
637
+ * Write the ladder's own answer back as the held organization. Default true. False: only an
638
+ * explicit `switchTo` writes the held choice (the extension's rule: a computed answer is never a choice).
639
+ */
640
+ readonly holdResolved?: boolean | undefined;
585
641
  }
586
642
  /** A held store over a `Storage` (browser `sessionStorage` = per tab), identity-keyed. */
587
- declare function storageHeldOrganization(storage: Pick<Storage, "getItem" | "setItem" | "removeItem">, key?: string): HeldOrganizationStore & {
643
+ declare function storageHeldOrganization(storage: Pick<Storage, "getItem" | "setItem" | "removeItem">, key?: string): Omit<HeldOrganizationStore, "read"> & {
588
644
  read(userId: string): string | null;
589
645
  };
590
646
  /** Held in memory: the running app's own choice (desktop, scripts, tests). */
591
647
  declare function memoryHeldOrganization(): HeldOrganizationStore;
592
648
  declare function createActiveOrganization(options: CreateActiveOrganizationOptions): ActiveOrganization;
593
649
 
594
- export { decideLinkOrganization as $, type AuthClient as A, NotAMemberError as B, type CreateActiveOrganizationOptions as C, type NotAMemberRefusalNotice as D, ORGANIZATION_RESOLUTION_TIMEOUT_MS as E, type OrganizationBootstrapGate as F, type OrganizationMembership as G, type HeldOrganizationStore as H, type OrganizationRefusal as I, type OrganizationRung as J, type OrganizationsDb as K, LINK_ORGANIZATION_QUERY_KEY as L, type ResolveWithLinkInput as M, NOT_A_MEMBER_RECHECK_GAP_MS as N, ORGANIZATION_ENTITY_TYPE as O, type ParentOrganization as P, type ResolvedOrganization as Q, type ReadMembershipsOptions as R, type SessionSourceLike as S, type ResolvedWithLink as T, type UserCredentialsPort as U, adminSeatReplacesUserContext as V, classifyLinkOrganizationValue as W, classifyOrganizationRefusal as X, createActiveOrganization as Y, createNotAMemberRefusalNotice as Z, createOrganizationBootstrapGate as _, SignInError as a, isNotAMemberRefusal as a0, linkOrganizationMalformedMessage as a1, linkOrganizationNotAMemberMessage as a2, linkOrganizationOfferMessage as a3, linkOrganizationSwitchAnnouncement as a4, memoryHeldOrganization as a5, oldestMembership as a6, orderOrganizations as a7, organizationMatches as a8, readAccountOrganizationChoices as a9, readLinkOrganizationParam as aa, readMemberships as ab, readOrganizationFavourites as ac, resolveActiveOrganization as ad, resolveActiveOrganizationLazily as ae, resolveAdminSeat as af, resolveChildOrganizationId as ag, resolveOrganizationWithLink as ah, setOrganizationFavourite as ai, storageHeldOrganization as aj, toAccountOrganizationChoices as ak, writeLastActiveOrganization as al, writeStartupOrganization as am, getSession as b, signInWithOAuth as c, signInWithPassword as d, signOut as e, userCredentials as f, getPerson as g, type AccountOrganizationChoices as h, invalidatePerson as i, type ActiveOrganization as j, type ActiveOrganizationSnapshot as k, type ActiveOrganizationStatus as l, type AdminSeat as m, type AdminSeatInput as n, onSessionChange as o, type ArchiveFilter as p, type LadderInput as q, refreshSession as r, sessionCredentials as s, type LinkOrganizationDecision as t, updatePerson as u, type LinkOrganizationDecisionInput as v, type LinkOrganizationMembership as w, type LinkOrganizationMessages as x, type LinkOrganizationParam as y, NoActiveOrganizationError as z };
650
+ export { createNotAMemberRefusalNotice as $, type AuthClient as A, NoActiveOrganizationError as B, type CachedActiveOrganization as C, NotAMemberError as D, type NotAMemberRefusalNotice as E, ORGANIZATION_RESOLUTION_TIMEOUT_MS as F, type OrganizationBootstrapGate as G, type HeldOrganizationStore as H, type OrganizationLapse as I, type OrganizationMembership as J, type OrganizationRefusal as K, LINK_ORGANIZATION_QUERY_KEY as L, type OrganizationRung as M, NOT_A_MEMBER_RECHECK_GAP_MS as N, ORGANIZATION_ENTITY_TYPE as O, type OrganizationsDb as P, type ParentOrganization as Q, type ReadMembershipsOptions as R, type SessionSourceLike as S, type ResolveWithLinkInput as T, type UserCredentialsPort as U, type ResolvedOrganization as V, type ResolvedWithLink as W, adminSeatReplacesUserContext as X, classifyLinkOrganizationValue as Y, classifyOrganizationRefusal as Z, createActiveOrganization as _, SignInError as a, createOrganizationBootstrapGate as a0, decideLinkOrganization as a1, isNotAMemberRefusal as a2, linkOrganizationMalformedMessage as a3, linkOrganizationNotAMemberMessage as a4, linkOrganizationOfferMessage as a5, linkOrganizationSwitchAnnouncement as a6, memoryHeldOrganization as a7, oldestMembership as a8, orderOrganizations as a9, organizationMatches as aa, readAccountOrganizationChoices as ab, readLinkOrganizationParam as ac, readMemberships as ad, readOrganizationFavourites as ae, resolveActiveOrganization as af, resolveActiveOrganizationLazily as ag, resolveAdminSeat as ah, resolveChildOrganizationId as ai, resolveOrganizationWithLink as aj, setOrganizationFavourite as ak, storageHeldOrganization as al, toAccountOrganizationChoices as am, writeLastActiveOrganization as an, writeStartupOrganization as ao, getSession as b, signInWithOAuth as c, signInWithPassword as d, signOut as e, userCredentials as f, getPerson as g, type AccountOrganizationChoices as h, invalidatePerson as i, type ActiveOrganization as j, type ActiveOrganizationSnapshot as k, type ActiveOrganizationStatus as l, type AdminSeat as m, type AdminSeatInput as n, onSessionChange as o, type ArchiveFilter as p, type CreateActiveOrganizationOptions as q, refreshSession as r, sessionCredentials as s, type LadderInput as t, updatePerson as u, type LinkOrganizationDecision as v, type LinkOrganizationDecisionInput as w, type LinkOrganizationMembership as x, type LinkOrganizationMessages as y, type LinkOrganizationParam as z };
package/dist/guest.d.cts CHANGED
@@ -137,7 +137,7 @@ declare const DEFAULT_GUEST_CARRIER_COOKIE = "matrx_guest_visitor";
137
137
  declare function createGuestHandover(options: GuestHandoverOptions): GuestHandover;
138
138
  /**
139
139
  * Cookie options for the guest's browser client: its OWN cookie name, never the app singleton's. Pass as
140
- * `createBrowserClient(url, key, { isSingleton: false, cookieOptions: guestClientCookieOptions(name) })`.
140
+ * the browser client constructor with `isSingleton: false` and `cookieOptions: guestClientCookieOptions(name)`.
141
141
  */
142
142
  declare function guestClientCookieOptions(cookieName: string, secure?: boolean): {
143
143
  name: string;
package/dist/guest.d.ts CHANGED
@@ -137,7 +137,7 @@ declare const DEFAULT_GUEST_CARRIER_COOKIE = "matrx_guest_visitor";
137
137
  declare function createGuestHandover(options: GuestHandoverOptions): GuestHandover;
138
138
  /**
139
139
  * Cookie options for the guest's browser client: its OWN cookie name, never the app singleton's. Pass as
140
- * `createBrowserClient(url, key, { isSingleton: false, cookieOptions: guestClientCookieOptions(name) })`.
140
+ * the browser client constructor with `isSingleton: false` and `cookieOptions: guestClientCookieOptions(name)`.
141
141
  */
142
142
  declare function guestClientCookieOptions(cookieName: string, secure?: boolean): {
143
143
  name: string;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { H as HeldOrganizationStore, A as AuthClient } from './active-organization-Dy4UtiyR.cjs';
2
- export { S as SessionSourceLike, a as SignInError, U as UserCredentialsPort, g as getPerson, b as getSession, i as invalidatePerson, o as onSessionChange, r as refreshSession, s as sessionCredentials, c as signInWithOAuth, d as signInWithPassword, e as signOut, u as updatePerson, f as userCredentials } from './active-organization-Dy4UtiyR.cjs';
1
+ import { H as HeldOrganizationStore, A as AuthClient } from './active-organization-Cxm6GDIy.cjs';
2
+ export { S as SessionSourceLike, a as SignInError, U as UserCredentialsPort, g as getPerson, b as getSession, i as invalidatePerson, o as onSessionChange, r as refreshSession, s as sessionCredentials, c as signInWithOAuth, d as signInWithPassword, e as signOut, u as updatePerson, f as userCredentials } from './active-organization-Cxm6GDIy.cjs';
3
3
  import { S as SessionStorage, M as MatrxOAuthClient } from './matrx-oauth-_Ji8X5Wm.cjs';
4
4
  export { C as ChromeStorageAreaLike, a as StorageUnavailableError, b as browserStorage, e as extensionStorage, k as keyValueStorage, m as memoryStorage } from './matrx-oauth-_Ji8X5Wm.cjs';
5
5
  import { SupabaseClient, User } from '@supabase/supabase-js';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { H as HeldOrganizationStore, A as AuthClient } from './active-organization-Dy4UtiyR.js';
2
- export { S as SessionSourceLike, a as SignInError, U as UserCredentialsPort, g as getPerson, b as getSession, i as invalidatePerson, o as onSessionChange, r as refreshSession, s as sessionCredentials, c as signInWithOAuth, d as signInWithPassword, e as signOut, u as updatePerson, f as userCredentials } from './active-organization-Dy4UtiyR.js';
1
+ import { H as HeldOrganizationStore, A as AuthClient } from './active-organization-Cxm6GDIy.js';
2
+ export { S as SessionSourceLike, a as SignInError, U as UserCredentialsPort, g as getPerson, b as getSession, i as invalidatePerson, o as onSessionChange, r as refreshSession, s as sessionCredentials, c as signInWithOAuth, d as signInWithPassword, e as signOut, u as updatePerson, f as userCredentials } from './active-organization-Cxm6GDIy.js';
3
3
  import { S as SessionStorage, M as MatrxOAuthClient } from './matrx-oauth-_Ji8X5Wm.js';
4
4
  export { C as ChromeStorageAreaLike, a as StorageUnavailableError, b as browserStorage, e as extensionStorage, k as keyValueStorage, m as memoryStorage } from './matrx-oauth-_Ji8X5Wm.js';
5
5
  import { SupabaseClient, User } from '@supabase/supabase-js';
@@ -523,7 +523,9 @@ var IDLE = Object.freeze({
523
523
  organization: null,
524
524
  memberships: Object.freeze([]),
525
525
  rung: null,
526
- error: null
526
+ error: null,
527
+ favourites: Object.freeze([]),
528
+ startupOrganizationId: null
527
529
  });
528
530
  function createActiveOrganization(options) {
529
531
  const { client } = options;
@@ -534,8 +536,11 @@ function createActiveOrganization(options) {
534
536
  let generation = 0;
535
537
  let waiters = [];
536
538
  let disposed = false;
539
+ let lastSettled = IDLE;
540
+ let lastReadAt = Number.NEGATIVE_INFINITY;
537
541
  const publish = (next) => {
538
542
  current = Object.freeze(next);
543
+ if (current.status !== "resolving") lastSettled = current;
539
544
  if (current.status !== "resolving" && current.status !== "idle") {
540
545
  options.bootstrapGate?.markResolved();
541
546
  const ready = waiters;
@@ -545,6 +550,7 @@ function createActiveOrganization(options) {
545
550
  for (const l of [...listeners]) l();
546
551
  };
547
552
  const run = async (gen) => {
553
+ const prior = current.status === "resolving" ? lastSettled : current;
548
554
  publish({ ...current, status: "resolving", error: null });
549
555
  try {
550
556
  const person = await getPerson(client);
@@ -553,8 +559,24 @@ function createActiveOrganization(options) {
553
559
  publish({ ...IDLE, status: "none", error: "signed-out" });
554
560
  return current;
555
561
  }
556
- const memberships = await readMemberships(client, person.id);
562
+ const rawMemberships = await readMemberships(client, person.id);
557
563
  if (gen !== generation) return current;
564
+ let favourites = current.favourites;
565
+ let startupOrganizationId = current.startupOrganizationId;
566
+ let account = null;
567
+ if (options.accountChoices) {
568
+ const [fav, acct] = await Promise.all([
569
+ readOrganizationFavourites(client).catch(() => favourites),
570
+ readAccountOrganizationChoices(client, person.id)
571
+ ]);
572
+ favourites = fav;
573
+ account = acct;
574
+ startupOrganizationId = acct.startupOrganizationId;
575
+ if (gen !== generation) return current;
576
+ }
577
+ const memberships = options.accountChoices ? orderOrganizations(rawMemberships, favourites) : rawMemberships;
578
+ const readAccount = () => account ? Promise.resolve(account) : readAccountOrganizationChoices(client, person.id);
579
+ const before = prior;
558
580
  const linkOptions = options.link;
559
581
  let resolved;
560
582
  let linkDecision = null;
@@ -562,7 +584,7 @@ function createActiveOrganization(options) {
562
584
  const r = await resolveOrganizationWithLink({
563
585
  memberships,
564
586
  held: await held.read(person.id),
565
- readAccount: () => readAccountOrganizationChoices(client, person.id),
587
+ readAccount,
566
588
  link: linkOptions.source() ?? null,
567
589
  switchWhenALinkAsks: linkOptions.switchWhenALinkAsks?.(),
568
590
  signedInAs: linkOptions.signedInAs?.(),
@@ -577,7 +599,7 @@ function createActiveOrganization(options) {
577
599
  resolved = await resolveActiveOrganizationLazily({
578
600
  memberships,
579
601
  held: heldId,
580
- readAccount: () => readAccountOrganizationChoices(client, person.id)
602
+ readAccount
581
603
  });
582
604
  }
583
605
  if (gen !== generation) return current;
@@ -585,7 +607,9 @@ function createActiveOrganization(options) {
585
607
  publish({ ...IDLE, status: "none", userId: person.id, error: "no-memberships" });
586
608
  return current;
587
609
  }
588
- await held.write(person.id, resolved.organization.id);
610
+ if (options.holdResolved ?? true) await held.write(person.id, resolved.organization.id);
611
+ const wasOrg = before.status === "ready" || before.status === "unchecked" ? before.organization : null;
612
+ const lapsed = wasOrg && wasOrg.id !== resolved.organization.id && !memberships.some((m) => m.id === wasOrg.id) ? wasOrg : null;
589
613
  if (linkDecision && linkDecision.kind !== "no-link") options.link?.onDecision?.(linkDecision);
590
614
  publish({
591
615
  status: "ready",
@@ -594,12 +618,51 @@ function createActiveOrganization(options) {
594
618
  organization: resolved.organization,
595
619
  memberships,
596
620
  rung: resolved.rung,
597
- error: null
621
+ error: null,
622
+ favourites,
623
+ startupOrganizationId
598
624
  });
625
+ if (lapsed) options.onLapse?.({ was: lapsed, now: resolved.organization });
626
+ lastReadAt = Date.now();
627
+ if (options.cached?.write) {
628
+ try {
629
+ await options.cached.write(person.id, { memberships, organizationId: resolved.organization.id, favourites, startupOrganizationId });
630
+ } catch {
631
+ }
632
+ }
599
633
  return current;
600
634
  } catch (e) {
601
635
  if (gen !== generation) return current;
602
- publish({ ...current, status: "error", error: e?.message ?? String(e) });
636
+ const message = e?.message ?? String(e);
637
+ if (options.cached && (prior.status === "ready" || prior.status === "unchecked") && prior.organization) {
638
+ publish({ ...prior, status: "unchecked", error: message });
639
+ return current;
640
+ }
641
+ const cachedUser = options.cached?.userId();
642
+ if (options.cached && cachedUser) {
643
+ try {
644
+ const answer = await options.cached.read(cachedUser);
645
+ if (gen !== generation) return current;
646
+ const wanted = await held.read(cachedUser) ?? answer?.organizationId ?? null;
647
+ const organization = answer?.memberships.find((m) => m.id === wanted) ?? null;
648
+ if (answer && organization) {
649
+ publish({
650
+ status: "unchecked",
651
+ userId: cachedUser,
652
+ organizationId: organization.id,
653
+ organization,
654
+ memberships: orderOrganizations(answer.memberships, answer.favourites),
655
+ rung: "held",
656
+ error: message,
657
+ favourites: answer.favourites,
658
+ startupOrganizationId: answer.startupOrganizationId
659
+ });
660
+ return current;
661
+ }
662
+ } catch {
663
+ }
664
+ }
665
+ publish({ ...current, status: "error", error: message });
603
666
  return current;
604
667
  }
605
668
  };
@@ -643,11 +706,34 @@ function createActiveOrganization(options) {
643
706
  };
644
707
  },
645
708
  load,
646
- current: () => current.status === "ready" ? seatFor(current) ?? current.organizationId : null,
709
+ current: () => current.status === "ready" || current.status === "unchecked" ? seatFor(current) ?? current.organizationId : null,
710
+ refresh(refreshOptions) {
711
+ const gap = options.refreshGapMs ?? 0;
712
+ if (!refreshOptions?.force && gap > 0 && current.status !== "idle" && Date.now() - lastReadAt < gap) return Promise.resolve(current);
713
+ if (inflight) return inflight;
714
+ return load();
715
+ },
716
+ async setStartup(organizationId) {
717
+ const snap = await settled();
718
+ await writeStartupOrganization(client, organizationId);
719
+ publish({ ...snap, startupOrganizationId: organizationId });
720
+ },
721
+ async setFavourite(organizationId, favourite) {
722
+ const snap = await settled();
723
+ const before = snap.favourites;
724
+ const next = favourite ? [...before.filter((f) => f !== organizationId), organizationId] : before.filter((f) => f !== organizationId);
725
+ publish({ ...snap, favourites: next, memberships: orderOrganizations(snap.memberships, next) });
726
+ try {
727
+ await setOrganizationFavourite(client, organizationId, favourite);
728
+ } catch (e) {
729
+ publish({ ...current, favourites: before, memberships: orderOrganizations(current.memberships, before) });
730
+ throw e;
731
+ }
732
+ },
647
733
  async ensureOrgId(recordOrganizationId) {
648
734
  if (recordOrganizationId) return recordOrganizationId;
649
735
  const snap = await settled();
650
- if (snap.status === "ready" && snap.organizationId) return seatFor(snap) ?? snap.organizationId;
736
+ if ((snap.status === "ready" || snap.status === "unchecked") && snap.organizationId) return seatFor(snap) ?? snap.organizationId;
651
737
  if (snap.status === "error") throw new NoActiveOrganizationError("unreadable", snap.error ?? void 0);
652
738
  throw new NoActiveOrganizationError(snap.error === "signed-out" ? "signed-out" : "no-memberships");
653
739
  },
@@ -1,4 +1,4 @@
1
- export { h as AccountOrganizationChoices, j as ActiveOrganization, k as ActiveOrganizationSnapshot, l as ActiveOrganizationStatus, m as AdminSeat, n as AdminSeatInput, p as ArchiveFilter, C as CreateActiveOrganizationOptions, H as HeldOrganizationStore, L as LINK_ORGANIZATION_QUERY_KEY, q as LadderInput, t as LinkOrganizationDecision, v as LinkOrganizationDecisionInput, w as LinkOrganizationMembership, x as LinkOrganizationMessages, y as LinkOrganizationParam, N as NOT_A_MEMBER_RECHECK_GAP_MS, z as NoActiveOrganizationError, B as NotAMemberError, D as NotAMemberRefusalNotice, O as ORGANIZATION_ENTITY_TYPE, E as ORGANIZATION_RESOLUTION_TIMEOUT_MS, F as OrganizationBootstrapGate, G as OrganizationMembership, I as OrganizationRefusal, J as OrganizationRung, K as OrganizationsDb, P as ParentOrganization, R as ReadMembershipsOptions, M as ResolveWithLinkInput, Q as ResolvedOrganization, T as ResolvedWithLink, V as adminSeatReplacesUserContext, W as classifyLinkOrganizationValue, X as classifyOrganizationRefusal, Y as createActiveOrganization, Z as createNotAMemberRefusalNotice, _ as createOrganizationBootstrapGate, $ as decideLinkOrganization, a0 as isNotAMemberRefusal, a1 as linkOrganizationMalformedMessage, a2 as linkOrganizationNotAMemberMessage, a3 as linkOrganizationOfferMessage, a4 as linkOrganizationSwitchAnnouncement, a5 as memoryHeldOrganization, a6 as oldestMembership, a7 as orderOrganizations, a8 as organizationMatches, a9 as readAccountOrganizationChoices, aa as readLinkOrganizationParam, ab as readMemberships, ac as readOrganizationFavourites, ad as resolveActiveOrganization, ae as resolveActiveOrganizationLazily, af as resolveAdminSeat, ag as resolveChildOrganizationId, ah as resolveOrganizationWithLink, ai as setOrganizationFavourite, aj as storageHeldOrganization, ak as toAccountOrganizationChoices, al as writeLastActiveOrganization, am as writeStartupOrganization } from './active-organization-Dy4UtiyR.cjs';
1
+ export { h as AccountOrganizationChoices, j as ActiveOrganization, k as ActiveOrganizationSnapshot, l as ActiveOrganizationStatus, m as AdminSeat, n as AdminSeatInput, p as ArchiveFilter, C as CachedActiveOrganization, q as CreateActiveOrganizationOptions, H as HeldOrganizationStore, L as LINK_ORGANIZATION_QUERY_KEY, t as LadderInput, v as LinkOrganizationDecision, w as LinkOrganizationDecisionInput, x as LinkOrganizationMembership, y as LinkOrganizationMessages, z as LinkOrganizationParam, N as NOT_A_MEMBER_RECHECK_GAP_MS, B as NoActiveOrganizationError, D as NotAMemberError, E as NotAMemberRefusalNotice, O as ORGANIZATION_ENTITY_TYPE, F as ORGANIZATION_RESOLUTION_TIMEOUT_MS, G as OrganizationBootstrapGate, I as OrganizationLapse, J as OrganizationMembership, K as OrganizationRefusal, M as OrganizationRung, P as OrganizationsDb, Q as ParentOrganization, R as ReadMembershipsOptions, T as ResolveWithLinkInput, V as ResolvedOrganization, W as ResolvedWithLink, X as adminSeatReplacesUserContext, Y as classifyLinkOrganizationValue, Z as classifyOrganizationRefusal, _ as createActiveOrganization, $ as createNotAMemberRefusalNotice, a0 as createOrganizationBootstrapGate, a1 as decideLinkOrganization, a2 as isNotAMemberRefusal, a3 as linkOrganizationMalformedMessage, a4 as linkOrganizationNotAMemberMessage, a5 as linkOrganizationOfferMessage, a6 as linkOrganizationSwitchAnnouncement, a7 as memoryHeldOrganization, a8 as oldestMembership, a9 as orderOrganizations, aa as organizationMatches, ab as readAccountOrganizationChoices, ac as readLinkOrganizationParam, ad as readMemberships, ae as readOrganizationFavourites, af as resolveActiveOrganization, ag as resolveActiveOrganizationLazily, ah as resolveAdminSeat, ai as resolveChildOrganizationId, aj as resolveOrganizationWithLink, ak as setOrganizationFavourite, al as storageHeldOrganization, am as toAccountOrganizationChoices, an as writeLastActiveOrganization, ao as writeStartupOrganization } from './active-organization-Cxm6GDIy.cjs';
2
2
  import '@supabase/supabase-js';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- export { h as AccountOrganizationChoices, j as ActiveOrganization, k as ActiveOrganizationSnapshot, l as ActiveOrganizationStatus, m as AdminSeat, n as AdminSeatInput, p as ArchiveFilter, C as CreateActiveOrganizationOptions, H as HeldOrganizationStore, L as LINK_ORGANIZATION_QUERY_KEY, q as LadderInput, t as LinkOrganizationDecision, v as LinkOrganizationDecisionInput, w as LinkOrganizationMembership, x as LinkOrganizationMessages, y as LinkOrganizationParam, N as NOT_A_MEMBER_RECHECK_GAP_MS, z as NoActiveOrganizationError, B as NotAMemberError, D as NotAMemberRefusalNotice, O as ORGANIZATION_ENTITY_TYPE, E as ORGANIZATION_RESOLUTION_TIMEOUT_MS, F as OrganizationBootstrapGate, G as OrganizationMembership, I as OrganizationRefusal, J as OrganizationRung, K as OrganizationsDb, P as ParentOrganization, R as ReadMembershipsOptions, M as ResolveWithLinkInput, Q as ResolvedOrganization, T as ResolvedWithLink, V as adminSeatReplacesUserContext, W as classifyLinkOrganizationValue, X as classifyOrganizationRefusal, Y as createActiveOrganization, Z as createNotAMemberRefusalNotice, _ as createOrganizationBootstrapGate, $ as decideLinkOrganization, a0 as isNotAMemberRefusal, a1 as linkOrganizationMalformedMessage, a2 as linkOrganizationNotAMemberMessage, a3 as linkOrganizationOfferMessage, a4 as linkOrganizationSwitchAnnouncement, a5 as memoryHeldOrganization, a6 as oldestMembership, a7 as orderOrganizations, a8 as organizationMatches, a9 as readAccountOrganizationChoices, aa as readLinkOrganizationParam, ab as readMemberships, ac as readOrganizationFavourites, ad as resolveActiveOrganization, ae as resolveActiveOrganizationLazily, af as resolveAdminSeat, ag as resolveChildOrganizationId, ah as resolveOrganizationWithLink, ai as setOrganizationFavourite, aj as storageHeldOrganization, ak as toAccountOrganizationChoices, al as writeLastActiveOrganization, am as writeStartupOrganization } from './active-organization-Dy4UtiyR.js';
1
+ export { h as AccountOrganizationChoices, j as ActiveOrganization, k as ActiveOrganizationSnapshot, l as ActiveOrganizationStatus, m as AdminSeat, n as AdminSeatInput, p as ArchiveFilter, C as CachedActiveOrganization, q as CreateActiveOrganizationOptions, H as HeldOrganizationStore, L as LINK_ORGANIZATION_QUERY_KEY, t as LadderInput, v as LinkOrganizationDecision, w as LinkOrganizationDecisionInput, x as LinkOrganizationMembership, y as LinkOrganizationMessages, z as LinkOrganizationParam, N as NOT_A_MEMBER_RECHECK_GAP_MS, B as NoActiveOrganizationError, D as NotAMemberError, E as NotAMemberRefusalNotice, O as ORGANIZATION_ENTITY_TYPE, F as ORGANIZATION_RESOLUTION_TIMEOUT_MS, G as OrganizationBootstrapGate, I as OrganizationLapse, J as OrganizationMembership, K as OrganizationRefusal, M as OrganizationRung, P as OrganizationsDb, Q as ParentOrganization, R as ReadMembershipsOptions, T as ResolveWithLinkInput, V as ResolvedOrganization, W as ResolvedWithLink, X as adminSeatReplacesUserContext, Y as classifyLinkOrganizationValue, Z as classifyOrganizationRefusal, _ as createActiveOrganization, $ as createNotAMemberRefusalNotice, a0 as createOrganizationBootstrapGate, a1 as decideLinkOrganization, a2 as isNotAMemberRefusal, a3 as linkOrganizationMalformedMessage, a4 as linkOrganizationNotAMemberMessage, a5 as linkOrganizationOfferMessage, a6 as linkOrganizationSwitchAnnouncement, a7 as memoryHeldOrganization, a8 as oldestMembership, a9 as orderOrganizations, aa as organizationMatches, ab as readAccountOrganizationChoices, ac as readLinkOrganizationParam, ad as readMemberships, ae as readOrganizationFavourites, af as resolveActiveOrganization, ag as resolveActiveOrganizationLazily, ah as resolveAdminSeat, ai as resolveChildOrganizationId, aj as resolveOrganizationWithLink, ak as setOrganizationFavourite, al as storageHeldOrganization, am as toAccountOrganizationChoices, an as writeLastActiveOrganization, ao as writeStartupOrganization } from './active-organization-Cxm6GDIy.js';
2
2
  import '@supabase/supabase-js';
3
3
 
4
4
  /**
@@ -449,7 +449,9 @@ var IDLE = Object.freeze({
449
449
  organization: null,
450
450
  memberships: Object.freeze([]),
451
451
  rung: null,
452
- error: null
452
+ error: null,
453
+ favourites: Object.freeze([]),
454
+ startupOrganizationId: null
453
455
  });
454
456
  function createActiveOrganization(options) {
455
457
  const { client } = options;
@@ -460,8 +462,11 @@ function createActiveOrganization(options) {
460
462
  let generation = 0;
461
463
  let waiters = [];
462
464
  let disposed = false;
465
+ let lastSettled = IDLE;
466
+ let lastReadAt = Number.NEGATIVE_INFINITY;
463
467
  const publish = (next) => {
464
468
  current = Object.freeze(next);
469
+ if (current.status !== "resolving") lastSettled = current;
465
470
  if (current.status !== "resolving" && current.status !== "idle") {
466
471
  options.bootstrapGate?.markResolved();
467
472
  const ready = waiters;
@@ -471,6 +476,7 @@ function createActiveOrganization(options) {
471
476
  for (const l of [...listeners]) l();
472
477
  };
473
478
  const run = async (gen) => {
479
+ const prior = current.status === "resolving" ? lastSettled : current;
474
480
  publish({ ...current, status: "resolving", error: null });
475
481
  try {
476
482
  const person = await getPerson(client);
@@ -479,8 +485,24 @@ function createActiveOrganization(options) {
479
485
  publish({ ...IDLE, status: "none", error: "signed-out" });
480
486
  return current;
481
487
  }
482
- const memberships = await readMemberships(client, person.id);
488
+ const rawMemberships = await readMemberships(client, person.id);
483
489
  if (gen !== generation) return current;
490
+ let favourites = current.favourites;
491
+ let startupOrganizationId = current.startupOrganizationId;
492
+ let account = null;
493
+ if (options.accountChoices) {
494
+ const [fav, acct] = await Promise.all([
495
+ readOrganizationFavourites(client).catch(() => favourites),
496
+ readAccountOrganizationChoices(client, person.id)
497
+ ]);
498
+ favourites = fav;
499
+ account = acct;
500
+ startupOrganizationId = acct.startupOrganizationId;
501
+ if (gen !== generation) return current;
502
+ }
503
+ const memberships = options.accountChoices ? orderOrganizations(rawMemberships, favourites) : rawMemberships;
504
+ const readAccount = () => account ? Promise.resolve(account) : readAccountOrganizationChoices(client, person.id);
505
+ const before = prior;
484
506
  const linkOptions = options.link;
485
507
  let resolved;
486
508
  let linkDecision = null;
@@ -488,7 +510,7 @@ function createActiveOrganization(options) {
488
510
  const r = await resolveOrganizationWithLink({
489
511
  memberships,
490
512
  held: await held.read(person.id),
491
- readAccount: () => readAccountOrganizationChoices(client, person.id),
513
+ readAccount,
492
514
  link: linkOptions.source() ?? null,
493
515
  switchWhenALinkAsks: linkOptions.switchWhenALinkAsks?.(),
494
516
  signedInAs: linkOptions.signedInAs?.(),
@@ -503,7 +525,7 @@ function createActiveOrganization(options) {
503
525
  resolved = await resolveActiveOrganizationLazily({
504
526
  memberships,
505
527
  held: heldId,
506
- readAccount: () => readAccountOrganizationChoices(client, person.id)
528
+ readAccount
507
529
  });
508
530
  }
509
531
  if (gen !== generation) return current;
@@ -511,7 +533,9 @@ function createActiveOrganization(options) {
511
533
  publish({ ...IDLE, status: "none", userId: person.id, error: "no-memberships" });
512
534
  return current;
513
535
  }
514
- await held.write(person.id, resolved.organization.id);
536
+ if (options.holdResolved ?? true) await held.write(person.id, resolved.organization.id);
537
+ const wasOrg = before.status === "ready" || before.status === "unchecked" ? before.organization : null;
538
+ const lapsed = wasOrg && wasOrg.id !== resolved.organization.id && !memberships.some((m) => m.id === wasOrg.id) ? wasOrg : null;
515
539
  if (linkDecision && linkDecision.kind !== "no-link") options.link?.onDecision?.(linkDecision);
516
540
  publish({
517
541
  status: "ready",
@@ -520,12 +544,51 @@ function createActiveOrganization(options) {
520
544
  organization: resolved.organization,
521
545
  memberships,
522
546
  rung: resolved.rung,
523
- error: null
547
+ error: null,
548
+ favourites,
549
+ startupOrganizationId
524
550
  });
551
+ if (lapsed) options.onLapse?.({ was: lapsed, now: resolved.organization });
552
+ lastReadAt = Date.now();
553
+ if (options.cached?.write) {
554
+ try {
555
+ await options.cached.write(person.id, { memberships, organizationId: resolved.organization.id, favourites, startupOrganizationId });
556
+ } catch {
557
+ }
558
+ }
525
559
  return current;
526
560
  } catch (e) {
527
561
  if (gen !== generation) return current;
528
- publish({ ...current, status: "error", error: e?.message ?? String(e) });
562
+ const message = e?.message ?? String(e);
563
+ if (options.cached && (prior.status === "ready" || prior.status === "unchecked") && prior.organization) {
564
+ publish({ ...prior, status: "unchecked", error: message });
565
+ return current;
566
+ }
567
+ const cachedUser = options.cached?.userId();
568
+ if (options.cached && cachedUser) {
569
+ try {
570
+ const answer = await options.cached.read(cachedUser);
571
+ if (gen !== generation) return current;
572
+ const wanted = await held.read(cachedUser) ?? answer?.organizationId ?? null;
573
+ const organization = answer?.memberships.find((m) => m.id === wanted) ?? null;
574
+ if (answer && organization) {
575
+ publish({
576
+ status: "unchecked",
577
+ userId: cachedUser,
578
+ organizationId: organization.id,
579
+ organization,
580
+ memberships: orderOrganizations(answer.memberships, answer.favourites),
581
+ rung: "held",
582
+ error: message,
583
+ favourites: answer.favourites,
584
+ startupOrganizationId: answer.startupOrganizationId
585
+ });
586
+ return current;
587
+ }
588
+ } catch {
589
+ }
590
+ }
591
+ publish({ ...current, status: "error", error: message });
529
592
  return current;
530
593
  }
531
594
  };
@@ -569,11 +632,34 @@ function createActiveOrganization(options) {
569
632
  };
570
633
  },
571
634
  load,
572
- current: () => current.status === "ready" ? seatFor(current) ?? current.organizationId : null,
635
+ current: () => current.status === "ready" || current.status === "unchecked" ? seatFor(current) ?? current.organizationId : null,
636
+ refresh(refreshOptions) {
637
+ const gap = options.refreshGapMs ?? 0;
638
+ if (!refreshOptions?.force && gap > 0 && current.status !== "idle" && Date.now() - lastReadAt < gap) return Promise.resolve(current);
639
+ if (inflight) return inflight;
640
+ return load();
641
+ },
642
+ async setStartup(organizationId) {
643
+ const snap = await settled();
644
+ await writeStartupOrganization(client, organizationId);
645
+ publish({ ...snap, startupOrganizationId: organizationId });
646
+ },
647
+ async setFavourite(organizationId, favourite) {
648
+ const snap = await settled();
649
+ const before = snap.favourites;
650
+ const next = favourite ? [...before.filter((f) => f !== organizationId), organizationId] : before.filter((f) => f !== organizationId);
651
+ publish({ ...snap, favourites: next, memberships: orderOrganizations(snap.memberships, next) });
652
+ try {
653
+ await setOrganizationFavourite(client, organizationId, favourite);
654
+ } catch (e) {
655
+ publish({ ...current, favourites: before, memberships: orderOrganizations(current.memberships, before) });
656
+ throw e;
657
+ }
658
+ },
573
659
  async ensureOrgId(recordOrganizationId) {
574
660
  if (recordOrganizationId) return recordOrganizationId;
575
661
  const snap = await settled();
576
- if (snap.status === "ready" && snap.organizationId) return seatFor(snap) ?? snap.organizationId;
662
+ if ((snap.status === "ready" || snap.status === "unchecked") && snap.organizationId) return seatFor(snap) ?? snap.organizationId;
577
663
  if (snap.status === "error") throw new NoActiveOrganizationError("unreadable", snap.error ?? void 0);
578
664
  throw new NoActiveOrganizationError(snap.error === "signed-out" ? "signed-out" : "no-memberships");
579
665
  },
package/dist/react.cjs CHANGED
@@ -182,7 +182,9 @@ var SERVER_ACTIVE = Object.freeze({
182
182
  organization: null,
183
183
  memberships: Object.freeze([]),
184
184
  rung: null,
185
- error: null
185
+ error: null,
186
+ favourites: Object.freeze([]),
187
+ startupOrganizationId: null
186
188
  });
187
189
  function useActiveOrganization(store) {
188
190
  return (0, import_react.useSyncExternalStore)(store.subscribe, store.snapshot, () => SERVER_ACTIVE);
package/dist/react.d.cts CHANGED
@@ -90,7 +90,11 @@ type OrganizationRung = "held" | "last-active" | "startup" | "first";
90
90
  * the last person's organization, and a sign-out resets the store.
91
91
  */
92
92
 
93
- type ActiveOrganizationStatus = "idle" | "resolving" | "ready" | "none" | "error";
93
+ /**
94
+ * `unchecked`: the memberships could not be read, and the answer shown is the host's cached last answer
95
+ * (`options.cached`), not verified against the database. Only ever produced when `cached` is given.
96
+ */
97
+ type ActiveOrganizationStatus = "idle" | "resolving" | "ready" | "none" | "error" | "unchecked";
94
98
  interface ActiveOrganizationSnapshot {
95
99
  readonly status: ActiveOrganizationStatus;
96
100
  readonly userId: string | null;
@@ -100,6 +104,13 @@ interface ActiveOrganizationSnapshot {
100
104
  /** Which ladder rung answered (or "switched" after a person's click). */
101
105
  readonly rung: OrganizationRung | "switched" | "link" | null;
102
106
  readonly error: string | null;
107
+ /**
108
+ * The account's favourite organization ids. Read only when `options.accountChoices` is on; `[]`
109
+ * otherwise. When on, `memberships` is ordered favourites, then own, then the rest.
110
+ */
111
+ readonly favourites: readonly string[];
112
+ /** The account's start-up organization setting. Read only when `options.accountChoices` is on; null otherwise. */
113
+ readonly startupOrganizationId: string | null;
103
114
  }
104
115
  interface ActiveOrganization {
105
116
  snapshot(): ActiveOrganizationSnapshot;
@@ -109,6 +120,14 @@ interface ActiveOrganization {
109
120
  /** The active organization id now, or null while resolving / with none. Sync (api's getter). */
110
121
  current(): string | null;
111
122
  /** THE HELD REQUEST: the record's own organization, else the active one once resolved. */
123
+ /** Re-read, unless `options.refreshGapMs` has not elapsed since the last read (`force` always reads). */
124
+ refresh(options?: {
125
+ readonly force?: boolean;
126
+ }): Promise<ActiveOrganizationSnapshot>;
127
+ /** Save the account's start-up organization (null: the first). Needs `accountChoices`; throws on refusal. */
128
+ setStartup(organizationId: string | null): Promise<void>;
129
+ /** Add or remove a favourite and re-order. Optimistic; rolled back and rethrown if the save fails. */
130
+ setFavourite(organizationId: string, favourite: boolean): Promise<void>;
112
131
  ensureOrgId(recordOrganizationId?: string | null): Promise<string>;
113
132
  /** A person's click: switch, remember for this tab, save as last active for the next load. */
114
133
  switchTo(organizationId: string): Promise<void>;
package/dist/react.d.ts CHANGED
@@ -90,7 +90,11 @@ type OrganizationRung = "held" | "last-active" | "startup" | "first";
90
90
  * the last person's organization, and a sign-out resets the store.
91
91
  */
92
92
 
93
- type ActiveOrganizationStatus = "idle" | "resolving" | "ready" | "none" | "error";
93
+ /**
94
+ * `unchecked`: the memberships could not be read, and the answer shown is the host's cached last answer
95
+ * (`options.cached`), not verified against the database. Only ever produced when `cached` is given.
96
+ */
97
+ type ActiveOrganizationStatus = "idle" | "resolving" | "ready" | "none" | "error" | "unchecked";
94
98
  interface ActiveOrganizationSnapshot {
95
99
  readonly status: ActiveOrganizationStatus;
96
100
  readonly userId: string | null;
@@ -100,6 +104,13 @@ interface ActiveOrganizationSnapshot {
100
104
  /** Which ladder rung answered (or "switched" after a person's click). */
101
105
  readonly rung: OrganizationRung | "switched" | "link" | null;
102
106
  readonly error: string | null;
107
+ /**
108
+ * The account's favourite organization ids. Read only when `options.accountChoices` is on; `[]`
109
+ * otherwise. When on, `memberships` is ordered favourites, then own, then the rest.
110
+ */
111
+ readonly favourites: readonly string[];
112
+ /** The account's start-up organization setting. Read only when `options.accountChoices` is on; null otherwise. */
113
+ readonly startupOrganizationId: string | null;
103
114
  }
104
115
  interface ActiveOrganization {
105
116
  snapshot(): ActiveOrganizationSnapshot;
@@ -109,6 +120,14 @@ interface ActiveOrganization {
109
120
  /** The active organization id now, or null while resolving / with none. Sync (api's getter). */
110
121
  current(): string | null;
111
122
  /** THE HELD REQUEST: the record's own organization, else the active one once resolved. */
123
+ /** Re-read, unless `options.refreshGapMs` has not elapsed since the last read (`force` always reads). */
124
+ refresh(options?: {
125
+ readonly force?: boolean;
126
+ }): Promise<ActiveOrganizationSnapshot>;
127
+ /** Save the account's start-up organization (null: the first). Needs `accountChoices`; throws on refusal. */
128
+ setStartup(organizationId: string | null): Promise<void>;
129
+ /** Add or remove a favourite and re-order. Optimistic; rolled back and rethrown if the save fails. */
130
+ setFavourite(organizationId: string, favourite: boolean): Promise<void>;
112
131
  ensureOrgId(recordOrganizationId?: string | null): Promise<string>;
113
132
  /** A person's click: switch, remember for this tab, save as last active for the next load. */
114
133
  switchTo(organizationId: string): Promise<void>;
package/dist/react.js CHANGED
@@ -157,7 +157,9 @@ var SERVER_ACTIVE = Object.freeze({
157
157
  organization: null,
158
158
  memberships: Object.freeze([]),
159
159
  rung: null,
160
- error: null
160
+ error: null,
161
+ favourites: Object.freeze([]),
162
+ startupOrganizationId: null
161
163
  });
162
164
  function useActiveOrganization(store) {
163
165
  return useSyncExternalStore(store.subscribe, store.snapshot, () => SERVER_ACTIVE);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/auth",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "AI Matrx sign-in, session and organizations for every app: sign in/out, session storage and refresh per platform (browser, Next.js, extension, desktop, Node), the current person, their organizations and favourites, the one active organization (held requests, never a list filter), and the credentials @ai-matrx/data and @ai-matrx/api read.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -211,5 +211,5 @@
211
211
  "test": "vitest run",
212
212
  "verify:tarball": "node ./scripts/verify-tarball.mjs"
213
213
  },
214
- "gitHead": "09da8756d7c80a6f2e611b1dd2d713efee4b0858"
214
+ "gitHead": "f9020cf33c7ac2b8b854856131eb0b1268eb220d"
215
215
  }