@ai-matrx/auth 0.2.3 → 0.2.5

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,6 +1,34 @@
1
1
  # Changelog — @ai-matrx/auth
2
2
 
3
+ ## 0.2.5
4
+
5
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
+ `git diff npm/auth/v0.2.4..npm/auth/v0.2.5 -- apps/shared/auth`).
7
+ No source changes intended and no consumer action required.
8
+
9
+ ## 0.2.4
10
+ ### active organization options (round 2)
11
+
12
+ All options default to today's behaviour. `createActiveOrganization`:
13
+ - `accountChoices` — the snapshot carries `favourites` and `startupOrganizationId` (always present: `[]` / null when off); when on,
14
+ `memberships` is ordered favourites-first and a failed favourites read leaves them out. New `setFavourite(id, on)` (optimistic,
15
+ rolled back and rethrown on failure) and `setStartup(id | null)`.
16
+ - `cached` `{ userId, read, write? }` — a new `unchecked` status: the memberships could not be read, so the host's cached last answer
17
+ (or the list already on screen) is shown unverified. `current()` / `ensureOrgId()` answer in `unchecked`.
18
+ - `onLapse({ was, now })` — the organization the person was in is no longer a membership and the ladder chose another.
19
+ - `refreshGapMs` + `refresh({ force? })` — a throttled re-read.
20
+ - `holdResolved` (default true) — false: the ladder's own answer is never written as the held choice; only `switchTo` writes it
21
+ (the extension's rule).
22
+ - `ActiveOrganizationSnapshot` gains `favourites` and `startupOrganizationId`; new types `CachedActiveOrganization`, `OrganizationLapse`.
23
+ Tests: `active-organization.test.ts` (+9).
24
+
3
25
  ## 0.2.3
26
+
27
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
28
+ `git diff npm/auth/v0.2.2..npm/auth/v0.2.3 -- apps/shared/auth`).
29
+ No source changes intended and no consumer action required.
30
+
31
+ ## 0.2.2
4
32
  ### extension/desktop options (round 2)
5
33
 
6
34
  - `readMemberships(db, userId, { archived: "active" | "archived" | "all" })` — archived filter (default
@@ -26,7 +54,7 @@
26
54
  attaches (bounded wait, one retry after the session re-resolves, a foreign-authority token is signed out
27
55
  and never retried, every firing reported). Host-neutral: the host injects `report`, `isAnonymousByDesign`,
28
56
  `bypassesSessionWait`, `endUnverifiable`. From the website's `utils/supabase/sessionBarrier.ts`.
29
- - `/guest` (new section) — `createGuestHandover(...)`: a visitor's guest data survives sign-in (claim into the
57
+ - `/guest` (new section) — `createGuestHandover(...)` (the host injects `readGuestClaims`; the section builds no Supabase client): a visitor's guest data survives sign-in (claim into the
30
58
  account the session ends in, cookie kept on failure so the next sign-in retries) and sign-up (the same
31
59
  account is promoted in place); `guestClientCookieOptions` (the guest client's own cookie name, injected);
32
60
  `createGuestSessionEnsurer` (mint a guest once per tab/browser). From the website's `lib/guest/*`.
@@ -45,7 +73,8 @@ Tests: `session-barrier.test.ts`, `guest.test.ts`.
45
73
  (injectable `messages`), `resolveOrganizationWithLink` (the one ladder + the link, share admission injected).
46
74
  - `organization-gate` — `createOrganizationBootstrapGate`, `isNotAMemberRefusal`, `createNotAMemberRefusalNotice`,
47
75
  `resolveChildOrganizationId`, `resolveAdminSeat`, `adminSeatReplacesUserContext`.
48
- Tests: active-organization (+5), organization-gate (9).
76
+ - `readMembershipRows(db, containerType)` — the paged `mbr_for_user` raw-row read (any container type), returning the failing page's `{error,status}` so a host can wrap it in its own session retry.
77
+ Tests: active-organization (+5), organization-gate (11).
49
78
 
50
79
  ## 0.2.0
51
80
 
@@ -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,6 +613,31 @@ 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
643
  declare function storageHeldOrganization(storage: Pick<Storage, "getItem" | "setItem" | "removeItem">, key?: string): Omit<HeldOrganizationStore, "read"> & {
@@ -591,4 +647,4 @@ declare function storageHeldOrganization(storage: Pick<Storage, "getItem" | "set
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_ENTITY_TYPE as F, ORGANIZATION_RESOLUTION_TIMEOUT_MS as G, type HeldOrganizationStore as H, type OrganizationBootstrapGate as I, type OrganizationLapse as J, type OrganizationMembership as K, LINK_ORGANIZATION_QUERY_KEY as L, type OrganizationRefusal as M, NOT_A_MEMBER_RECHECK_GAP_MS as N, type OrganizationsDb as O, type OrganizationRung 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,6 +613,31 @@ 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
643
  declare function storageHeldOrganization(storage: Pick<Storage, "getItem" | "setItem" | "removeItem">, key?: string): Omit<HeldOrganizationStore, "read"> & {
@@ -591,4 +647,4 @@ declare function storageHeldOrganization(storage: Pick<Storage, "getItem" | "set
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_ENTITY_TYPE as F, ORGANIZATION_RESOLUTION_TIMEOUT_MS as G, type HeldOrganizationStore as H, type OrganizationBootstrapGate as I, type OrganizationLapse as J, type OrganizationMembership as K, LINK_ORGANIZATION_QUERY_KEY as L, type OrganizationRefusal as M, NOT_A_MEMBER_RECHECK_GAP_MS as N, type OrganizationsDb as O, type OrganizationRung 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.cjs CHANGED
@@ -28,7 +28,6 @@ __export(guest_exports, {
28
28
  looksLikeVisitorId: () => looksLikeVisitorId
29
29
  });
30
30
  module.exports = __toCommonJS(guest_exports);
31
- var import_ssr = require("@supabase/ssr");
32
31
  var VISITOR_ID = /^[A-Za-z0-9]{16,200}$/;
33
32
  function looksLikeVisitorId(id) {
34
33
  return typeof id === "string" && !id.startsWith("temp_") && VISITOR_ID.test(id);
@@ -42,21 +41,11 @@ function createGuestHandover(options) {
42
41
  const loud = options.log ?? ((m) => console.error(`[session-handover] LOUD: ${m}`));
43
42
  const isGuestCookie = (name) => isGuestCookieName(options.guestCookieName, name);
44
43
  const msg = (err) => err instanceof Error ? err.message : String(err);
45
- async function readClaims(jar) {
46
- if (options.readGuestClaims) return options.readGuestClaims(jar);
47
- if (!options.supabase) return null;
48
- const guest = (0, import_ssr.createServerClient)(options.supabase.url, options.supabase.publishableKey, {
49
- cookieOptions: { name: options.guestCookieName },
50
- cookies: { getAll: () => jar.getAll(), setAll: () => void 0 }
51
- });
52
- const { data } = await guest.auth.getClaims();
53
- return data?.claims ?? null;
54
- }
55
44
  async function readGuestPossession(visitorId) {
56
45
  const jar = await options.cookies();
57
46
  if (jar.getAll().some((c) => isGuestCookie(c.name))) {
58
47
  try {
59
- const claims = await readClaims(jar);
48
+ const claims = await options.readGuestClaims(jar);
60
49
  if (claims?.sub && claims.is_anonymous === true) return { guestId: claims.sub, proof: "guest_session" };
61
50
  } catch (err) {
62
51
  loud(`the guest cookie could not be verified: ${msg(err)}`);
package/dist/guest.d.cts CHANGED
@@ -1,3 +1,25 @@
1
+ /**
2
+ * @ai-matrx/auth/guest — a visitor's guest data survives sign-up and sign-in (moved from the
3
+ * website's `lib/guest/{session-handover,guest-supabase-client,ensure-guest-session}.ts`, 2026-10-12).
4
+ *
5
+ * A signed-out visitor who saves holds a GUEST session: a real, not-yet-permanent account that lives in
6
+ * its OWN cookie (never the main auth cookie, so a guest never looks signed in). Every place a session is
7
+ * REPLACED (password login, OAuth callback, email confirm, dev-login) runs through `handOverSession`:
8
+ *
9
+ * 1. read who the browser's guest is BEFORE the replacement (the verified claims of the guest cookie,
10
+ * else the visitor id mapped through the registry — the lost-session case);
11
+ * 2. replace the session;
12
+ * 3. when the result is a permanent account other than the guest, CLAIM the guest's workspace into it
13
+ * (all or nothing) and drop the guest cookie. A failed claim keeps the cookie, so the next replacement
14
+ * (or `settlePendingGuestClaim` on a signed-in load) tries again — it screams, never silently.
15
+ *
16
+ * Sign-up with email + password keeps the SAME account (`promoteGuest`): revive, promote in place, revive
17
+ * again, stamp the registry. Never falls back to a fresh account (that would orphan the guest).
18
+ *
19
+ * Host-neutral: the host injects the cookie jar, the guest store (admin reads and the claim/revive doors)
20
+ * and the cookie names. The browser half is `createGuestClientOptions` (the guest client's own cookie name,
21
+ * injected) and `createGuestSessionEnsurer` (mint a guest once per tab / per browser).
22
+ */
1
23
  type HandoverVia = "password_login" | "oauth_callback" | "email_otp" | "dev_login" | "sign_up";
2
24
  interface GuestPossession {
3
25
  guestId: string;
@@ -93,15 +115,15 @@ interface GuestHandoverOptions {
93
115
  /** Server cookie jar for this request. */
94
116
  cookies(): Promise<GuestCookieJar>;
95
117
  store: GuestStore;
96
- /** Verified claims of the guest cookie. Default: a server client over `supabase` url + key. */
97
- readGuestClaims?(jar: GuestCookieJar): Promise<{
118
+ /**
119
+ * Verified claims of the guest cookie (the anonymous JWT in this very request is the proof of possession).
120
+ * The host builds a server client over the guest cookie name and reads `getClaimsUser`; this section builds no
121
+ * Supabase client of its own.
122
+ */
123
+ readGuestClaims(jar: GuestCookieJar): Promise<{
98
124
  sub?: string;
99
125
  is_anonymous?: boolean;
100
126
  } | null>;
101
- supabase?: {
102
- url: string;
103
- publishableKey: string;
104
- };
105
127
  /** `Secure` on the carrier cookie. */
106
128
  secureCookies?: boolean;
107
129
  log?(message: string): void;
@@ -137,7 +159,7 @@ declare const DEFAULT_GUEST_CARRIER_COOKIE = "matrx_guest_visitor";
137
159
  declare function createGuestHandover(options: GuestHandoverOptions): GuestHandover;
138
160
  /**
139
161
  * 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) })`.
162
+ * the browser client constructor with `isSingleton: false` and `cookieOptions: guestClientCookieOptions(name)`.
141
163
  */
142
164
  declare function guestClientCookieOptions(cookieName: string, secure?: boolean): {
143
165
  name: string;
package/dist/guest.d.ts CHANGED
@@ -1,3 +1,25 @@
1
+ /**
2
+ * @ai-matrx/auth/guest — a visitor's guest data survives sign-up and sign-in (moved from the
3
+ * website's `lib/guest/{session-handover,guest-supabase-client,ensure-guest-session}.ts`, 2026-10-12).
4
+ *
5
+ * A signed-out visitor who saves holds a GUEST session: a real, not-yet-permanent account that lives in
6
+ * its OWN cookie (never the main auth cookie, so a guest never looks signed in). Every place a session is
7
+ * REPLACED (password login, OAuth callback, email confirm, dev-login) runs through `handOverSession`:
8
+ *
9
+ * 1. read who the browser's guest is BEFORE the replacement (the verified claims of the guest cookie,
10
+ * else the visitor id mapped through the registry — the lost-session case);
11
+ * 2. replace the session;
12
+ * 3. when the result is a permanent account other than the guest, CLAIM the guest's workspace into it
13
+ * (all or nothing) and drop the guest cookie. A failed claim keeps the cookie, so the next replacement
14
+ * (or `settlePendingGuestClaim` on a signed-in load) tries again — it screams, never silently.
15
+ *
16
+ * Sign-up with email + password keeps the SAME account (`promoteGuest`): revive, promote in place, revive
17
+ * again, stamp the registry. Never falls back to a fresh account (that would orphan the guest).
18
+ *
19
+ * Host-neutral: the host injects the cookie jar, the guest store (admin reads and the claim/revive doors)
20
+ * and the cookie names. The browser half is `createGuestClientOptions` (the guest client's own cookie name,
21
+ * injected) and `createGuestSessionEnsurer` (mint a guest once per tab / per browser).
22
+ */
1
23
  type HandoverVia = "password_login" | "oauth_callback" | "email_otp" | "dev_login" | "sign_up";
2
24
  interface GuestPossession {
3
25
  guestId: string;
@@ -93,15 +115,15 @@ interface GuestHandoverOptions {
93
115
  /** Server cookie jar for this request. */
94
116
  cookies(): Promise<GuestCookieJar>;
95
117
  store: GuestStore;
96
- /** Verified claims of the guest cookie. Default: a server client over `supabase` url + key. */
97
- readGuestClaims?(jar: GuestCookieJar): Promise<{
118
+ /**
119
+ * Verified claims of the guest cookie (the anonymous JWT in this very request is the proof of possession).
120
+ * The host builds a server client over the guest cookie name and reads `getClaimsUser`; this section builds no
121
+ * Supabase client of its own.
122
+ */
123
+ readGuestClaims(jar: GuestCookieJar): Promise<{
98
124
  sub?: string;
99
125
  is_anonymous?: boolean;
100
126
  } | null>;
101
- supabase?: {
102
- url: string;
103
- publishableKey: string;
104
- };
105
127
  /** `Secure` on the carrier cookie. */
106
128
  secureCookies?: boolean;
107
129
  log?(message: string): void;
@@ -137,7 +159,7 @@ declare const DEFAULT_GUEST_CARRIER_COOKIE = "matrx_guest_visitor";
137
159
  declare function createGuestHandover(options: GuestHandoverOptions): GuestHandover;
138
160
  /**
139
161
  * 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) })`.
162
+ * the browser client constructor with `isSingleton: false` and `cookieOptions: guestClientCookieOptions(name)`.
141
163
  */
142
164
  declare function guestClientCookieOptions(cookieName: string, secure?: boolean): {
143
165
  name: string;
package/dist/guest.js CHANGED
@@ -1,5 +1,4 @@
1
1
  // src/guest.ts
2
- import { createServerClient } from "@supabase/ssr";
3
2
  var VISITOR_ID = /^[A-Za-z0-9]{16,200}$/;
4
3
  function looksLikeVisitorId(id) {
5
4
  return typeof id === "string" && !id.startsWith("temp_") && VISITOR_ID.test(id);
@@ -13,21 +12,11 @@ function createGuestHandover(options) {
13
12
  const loud = options.log ?? ((m) => console.error(`[session-handover] LOUD: ${m}`));
14
13
  const isGuestCookie = (name) => isGuestCookieName(options.guestCookieName, name);
15
14
  const msg = (err) => err instanceof Error ? err.message : String(err);
16
- async function readClaims(jar) {
17
- if (options.readGuestClaims) return options.readGuestClaims(jar);
18
- if (!options.supabase) return null;
19
- const guest = createServerClient(options.supabase.url, options.supabase.publishableKey, {
20
- cookieOptions: { name: options.guestCookieName },
21
- cookies: { getAll: () => jar.getAll(), setAll: () => void 0 }
22
- });
23
- const { data } = await guest.auth.getClaims();
24
- return data?.claims ?? null;
25
- }
26
15
  async function readGuestPossession(visitorId) {
27
16
  const jar = await options.cookies();
28
17
  if (jar.getAll().some((c) => isGuestCookie(c.name))) {
29
18
  try {
30
- const claims = await readClaims(jar);
19
+ const claims = await options.readGuestClaims(jar);
31
20
  if (claims?.sub && claims.is_anonymous === true) return { guestId: claims.sub, proof: "guest_session" };
32
21
  } catch (err) {
33
22
  loud(`the guest cookie could not be verified: ${msg(err)}`);
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { H as HeldOrganizationStore, A as AuthClient } from './active-organization-CIvSdboO.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-CIvSdboO.cjs';
1
+ import { H as HeldOrganizationStore, A as AuthClient } from './active-organization-DHitxVeN.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-DHitxVeN.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-CIvSdboO.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-CIvSdboO.js';
1
+ import { H as HeldOrganizationStore, A as AuthClient } from './active-organization-DHitxVeN.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-DHitxVeN.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';
@@ -22,6 +22,7 @@ var organizations_exports = {};
22
22
  __export(organizations_exports, {
23
23
  LINK_ORGANIZATION_QUERY_KEY: () => LINK_ORGANIZATION_QUERY_KEY,
24
24
  MEMBERSHIPS_URL: () => MEMBERSHIPS_URL,
25
+ MEMBERSHIP_ROWS_PAGE: () => MEMBERSHIP_ROWS_PAGE,
25
26
  NOT_A_MEMBER_RECHECK_GAP_MS: () => NOT_A_MEMBER_RECHECK_GAP_MS,
26
27
  NoActiveOrganizationError: () => NoActiveOrganizationError,
27
28
  NotAMemberError: () => NotAMemberError,
@@ -57,6 +58,7 @@ __export(organizations_exports, {
57
58
  readAccountOrganizationChoices: () => readAccountOrganizationChoices,
58
59
  readCallerMemberships: () => readCallerMemberships,
59
60
  readLinkOrganizationParam: () => readLinkOrganizationParam,
61
+ readMembershipRows: () => readMembershipRows,
60
62
  readMemberships: () => readMemberships,
61
63
  readOrganizationFavourites: () => readOrganizationFavourites,
62
64
  resolveActiveOrganization: () => resolveActiveOrganization,
@@ -523,7 +525,9 @@ var IDLE = Object.freeze({
523
525
  organization: null,
524
526
  memberships: Object.freeze([]),
525
527
  rung: null,
526
- error: null
528
+ error: null,
529
+ favourites: Object.freeze([]),
530
+ startupOrganizationId: null
527
531
  });
528
532
  function createActiveOrganization(options) {
529
533
  const { client } = options;
@@ -534,8 +538,11 @@ function createActiveOrganization(options) {
534
538
  let generation = 0;
535
539
  let waiters = [];
536
540
  let disposed = false;
541
+ let lastSettled = IDLE;
542
+ let lastReadAt = Number.NEGATIVE_INFINITY;
537
543
  const publish = (next) => {
538
544
  current = Object.freeze(next);
545
+ if (current.status !== "resolving") lastSettled = current;
539
546
  if (current.status !== "resolving" && current.status !== "idle") {
540
547
  options.bootstrapGate?.markResolved();
541
548
  const ready = waiters;
@@ -545,6 +552,7 @@ function createActiveOrganization(options) {
545
552
  for (const l of [...listeners]) l();
546
553
  };
547
554
  const run = async (gen) => {
555
+ const prior = current.status === "resolving" ? lastSettled : current;
548
556
  publish({ ...current, status: "resolving", error: null });
549
557
  try {
550
558
  const person = await getPerson(client);
@@ -553,8 +561,24 @@ function createActiveOrganization(options) {
553
561
  publish({ ...IDLE, status: "none", error: "signed-out" });
554
562
  return current;
555
563
  }
556
- const memberships = await readMemberships(client, person.id);
564
+ const rawMemberships = await readMemberships(client, person.id);
557
565
  if (gen !== generation) return current;
566
+ let favourites = current.favourites;
567
+ let startupOrganizationId = current.startupOrganizationId;
568
+ let account = null;
569
+ if (options.accountChoices) {
570
+ const [fav, acct] = await Promise.all([
571
+ readOrganizationFavourites(client).catch(() => favourites),
572
+ readAccountOrganizationChoices(client, person.id)
573
+ ]);
574
+ favourites = fav;
575
+ account = acct;
576
+ startupOrganizationId = acct.startupOrganizationId;
577
+ if (gen !== generation) return current;
578
+ }
579
+ const memberships = options.accountChoices ? orderOrganizations(rawMemberships, favourites) : rawMemberships;
580
+ const readAccount = () => account ? Promise.resolve(account) : readAccountOrganizationChoices(client, person.id);
581
+ const before = prior;
558
582
  const linkOptions = options.link;
559
583
  let resolved;
560
584
  let linkDecision = null;
@@ -562,7 +586,7 @@ function createActiveOrganization(options) {
562
586
  const r = await resolveOrganizationWithLink({
563
587
  memberships,
564
588
  held: await held.read(person.id),
565
- readAccount: () => readAccountOrganizationChoices(client, person.id),
589
+ readAccount,
566
590
  link: linkOptions.source() ?? null,
567
591
  switchWhenALinkAsks: linkOptions.switchWhenALinkAsks?.(),
568
592
  signedInAs: linkOptions.signedInAs?.(),
@@ -577,7 +601,7 @@ function createActiveOrganization(options) {
577
601
  resolved = await resolveActiveOrganizationLazily({
578
602
  memberships,
579
603
  held: heldId,
580
- readAccount: () => readAccountOrganizationChoices(client, person.id)
604
+ readAccount
581
605
  });
582
606
  }
583
607
  if (gen !== generation) return current;
@@ -585,7 +609,9 @@ function createActiveOrganization(options) {
585
609
  publish({ ...IDLE, status: "none", userId: person.id, error: "no-memberships" });
586
610
  return current;
587
611
  }
588
- await held.write(person.id, resolved.organization.id);
612
+ if (options.holdResolved ?? true) await held.write(person.id, resolved.organization.id);
613
+ const wasOrg = before.status === "ready" || before.status === "unchecked" ? before.organization : null;
614
+ const lapsed = wasOrg && wasOrg.id !== resolved.organization.id && !memberships.some((m) => m.id === wasOrg.id) ? wasOrg : null;
589
615
  if (linkDecision && linkDecision.kind !== "no-link") options.link?.onDecision?.(linkDecision);
590
616
  publish({
591
617
  status: "ready",
@@ -594,12 +620,51 @@ function createActiveOrganization(options) {
594
620
  organization: resolved.organization,
595
621
  memberships,
596
622
  rung: resolved.rung,
597
- error: null
623
+ error: null,
624
+ favourites,
625
+ startupOrganizationId
598
626
  });
627
+ if (lapsed) options.onLapse?.({ was: lapsed, now: resolved.organization });
628
+ lastReadAt = Date.now();
629
+ if (options.cached?.write) {
630
+ try {
631
+ await options.cached.write(person.id, { memberships, organizationId: resolved.organization.id, favourites, startupOrganizationId });
632
+ } catch {
633
+ }
634
+ }
599
635
  return current;
600
636
  } catch (e) {
601
637
  if (gen !== generation) return current;
602
- publish({ ...current, status: "error", error: e?.message ?? String(e) });
638
+ const message = e?.message ?? String(e);
639
+ if (options.cached && (prior.status === "ready" || prior.status === "unchecked") && prior.organization) {
640
+ publish({ ...prior, status: "unchecked", error: message });
641
+ return current;
642
+ }
643
+ const cachedUser = options.cached?.userId();
644
+ if (options.cached && cachedUser) {
645
+ try {
646
+ const answer = await options.cached.read(cachedUser);
647
+ if (gen !== generation) return current;
648
+ const wanted = await held.read(cachedUser) ?? answer?.organizationId ?? null;
649
+ const organization = answer?.memberships.find((m) => m.id === wanted) ?? null;
650
+ if (answer && organization) {
651
+ publish({
652
+ status: "unchecked",
653
+ userId: cachedUser,
654
+ organizationId: organization.id,
655
+ organization,
656
+ memberships: orderOrganizations(answer.memberships, answer.favourites),
657
+ rung: "held",
658
+ error: message,
659
+ favourites: answer.favourites,
660
+ startupOrganizationId: answer.startupOrganizationId
661
+ });
662
+ return current;
663
+ }
664
+ } catch {
665
+ }
666
+ }
667
+ publish({ ...current, status: "error", error: message });
603
668
  return current;
604
669
  }
605
670
  };
@@ -643,11 +708,34 @@ function createActiveOrganization(options) {
643
708
  };
644
709
  },
645
710
  load,
646
- current: () => current.status === "ready" ? seatFor(current) ?? current.organizationId : null,
711
+ current: () => current.status === "ready" || current.status === "unchecked" ? seatFor(current) ?? current.organizationId : null,
712
+ refresh(refreshOptions) {
713
+ const gap = options.refreshGapMs ?? 0;
714
+ if (!refreshOptions?.force && gap > 0 && current.status !== "idle" && Date.now() - lastReadAt < gap) return Promise.resolve(current);
715
+ if (inflight) return inflight;
716
+ return load();
717
+ },
718
+ async setStartup(organizationId) {
719
+ const snap = await settled();
720
+ await writeStartupOrganization(client, organizationId);
721
+ publish({ ...snap, startupOrganizationId: organizationId });
722
+ },
723
+ async setFavourite(organizationId, favourite) {
724
+ const snap = await settled();
725
+ const before = snap.favourites;
726
+ const next = favourite ? [...before.filter((f) => f !== organizationId), organizationId] : before.filter((f) => f !== organizationId);
727
+ publish({ ...snap, favourites: next, memberships: orderOrganizations(snap.memberships, next) });
728
+ try {
729
+ await setOrganizationFavourite(client, organizationId, favourite);
730
+ } catch (e) {
731
+ publish({ ...current, favourites: before, memberships: orderOrganizations(current.memberships, before) });
732
+ throw e;
733
+ }
734
+ },
647
735
  async ensureOrgId(recordOrganizationId) {
648
736
  if (recordOrganizationId) return recordOrganizationId;
649
737
  const snap = await settled();
650
- if (snap.status === "ready" && snap.organizationId) return seatFor(snap) ?? snap.organizationId;
738
+ if ((snap.status === "ready" || snap.status === "unchecked") && snap.organizationId) return seatFor(snap) ?? snap.organizationId;
651
739
  if (snap.status === "error") throw new NoActiveOrganizationError("unreadable", snap.error ?? void 0);
652
740
  throw new NoActiveOrganizationError(snap.error === "signed-out" ? "signed-out" : "no-memberships");
653
741
  },
@@ -772,3 +860,17 @@ function organizationRequiredEnvelope(error, setOn = "request") {
772
860
  function organizationRequiredResponse(error) {
773
861
  return Response.json(organizationRequiredEnvelope(error), { status: 400 });
774
862
  }
863
+
864
+ // src/organization-membership-rows.ts
865
+ var MEMBERSHIP_ROWS_PAGE = 500;
866
+ async function readMembershipRows(db, containerType, pageSize = MEMBERSHIP_ROWS_PAGE) {
867
+ const rows = [];
868
+ for (let from = 0; ; from += pageSize) {
869
+ const answer = await db.rpc("mbr_for_user", { p_container_type: containerType }).order("id", { ascending: true }).range(from, from + pageSize - 1);
870
+ if (answer.error) return { data: null, error: answer.error, status: answer.status };
871
+ const page = Array.isArray(answer.data) ? answer.data : [];
872
+ rows.push(...page);
873
+ if (page.length < pageSize) break;
874
+ }
875
+ return { data: rows, error: null };
876
+ }
@@ -1,4 +1,5 @@
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-CIvSdboO.cjs';
1
+ import { O as OrganizationsDb } from './active-organization-DHitxVeN.cjs';
2
+ 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, F as ORGANIZATION_ENTITY_TYPE, G as ORGANIZATION_RESOLUTION_TIMEOUT_MS, I as OrganizationBootstrapGate, J as OrganizationLapse, K as OrganizationMembership, M as OrganizationRefusal, P as OrganizationRung, 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-DHitxVeN.cjs';
2
3
  import '@supabase/supabase-js';
3
4
 
4
5
  /**
@@ -107,4 +108,26 @@ declare function organizationRequiredEnvelope(error: OrganizationRequiredServerE
107
108
  /** The standard `Response` (status 400) a route answers the refusal with — no framework needed. */
108
109
  declare function organizationRequiredResponse(error: OrganizationRequiredServerError): Response;
109
110
 
110
- export { MEMBERSHIPS_URL, type MembershipReadClient, ORGANIZATION_CONTEXT_REQUIRED_CODE, ORGANIZATION_REQUIRED_CODE, ORGANIZATION_REQUIRED_WIRE_CODE, type OrganizationHoldDetails, type OrganizationHoldSetOn, type OrganizationMembershipSummary, type OrganizationRequiredEnvelope, OrganizationRequiredServerError, type OrganizationRequiredWireMembership, extractOrganizationHoldMemberships, isOrganizationRequiredEnvelope, isOrganizationRequiredError, isOrganizationRequiredServerError, organizationRequired, organizationRequiredEnvelope, organizationRequiredResponse, readCallerMemberships };
111
+ /**
112
+ * The person's raw membership rows for ONE container type, PAGED — the canonical `mbr_for_user`
113
+ * door (moved from the website's membershipsService). PostgREST caps one answer at its max-rows
114
+ * (1000): a person in 1,043 organizations lost the last 43, so the read pages in stable id order
115
+ * until a short page.
116
+ *
117
+ * It does NOT throw: it returns the first failing page's `{ error, status }` so a host can wrap
118
+ * the whole read in its own session retry (a 401 on a recovering session) and map the error its
119
+ * own way.
120
+ */
121
+
122
+ declare const MEMBERSHIP_ROWS_PAGE = 500;
123
+ interface MembershipRowsAnswer<Row> {
124
+ readonly data: Row[] | null;
125
+ readonly error: {
126
+ message: string;
127
+ code?: string;
128
+ } | null;
129
+ readonly status?: number | undefined;
130
+ }
131
+ declare function readMembershipRows<Row = Record<string, unknown>>(db: OrganizationsDb, containerType: string, pageSize?: number): Promise<MembershipRowsAnswer<Row>>;
132
+
133
+ export { MEMBERSHIPS_URL, MEMBERSHIP_ROWS_PAGE, type MembershipReadClient, type MembershipRowsAnswer, ORGANIZATION_CONTEXT_REQUIRED_CODE, ORGANIZATION_REQUIRED_CODE, ORGANIZATION_REQUIRED_WIRE_CODE, type OrganizationHoldDetails, type OrganizationHoldSetOn, type OrganizationMembershipSummary, type OrganizationRequiredEnvelope, OrganizationRequiredServerError, type OrganizationRequiredWireMembership, OrganizationsDb, extractOrganizationHoldMemberships, isOrganizationRequiredEnvelope, isOrganizationRequiredError, isOrganizationRequiredServerError, organizationRequired, organizationRequiredEnvelope, organizationRequiredResponse, readCallerMemberships, readMembershipRows };
@@ -1,4 +1,5 @@
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-CIvSdboO.js';
1
+ import { O as OrganizationsDb } from './active-organization-DHitxVeN.js';
2
+ 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, F as ORGANIZATION_ENTITY_TYPE, G as ORGANIZATION_RESOLUTION_TIMEOUT_MS, I as OrganizationBootstrapGate, J as OrganizationLapse, K as OrganizationMembership, M as OrganizationRefusal, P as OrganizationRung, 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-DHitxVeN.js';
2
3
  import '@supabase/supabase-js';
3
4
 
4
5
  /**
@@ -107,4 +108,26 @@ declare function organizationRequiredEnvelope(error: OrganizationRequiredServerE
107
108
  /** The standard `Response` (status 400) a route answers the refusal with — no framework needed. */
108
109
  declare function organizationRequiredResponse(error: OrganizationRequiredServerError): Response;
109
110
 
110
- export { MEMBERSHIPS_URL, type MembershipReadClient, ORGANIZATION_CONTEXT_REQUIRED_CODE, ORGANIZATION_REQUIRED_CODE, ORGANIZATION_REQUIRED_WIRE_CODE, type OrganizationHoldDetails, type OrganizationHoldSetOn, type OrganizationMembershipSummary, type OrganizationRequiredEnvelope, OrganizationRequiredServerError, type OrganizationRequiredWireMembership, extractOrganizationHoldMemberships, isOrganizationRequiredEnvelope, isOrganizationRequiredError, isOrganizationRequiredServerError, organizationRequired, organizationRequiredEnvelope, organizationRequiredResponse, readCallerMemberships };
111
+ /**
112
+ * The person's raw membership rows for ONE container type, PAGED — the canonical `mbr_for_user`
113
+ * door (moved from the website's membershipsService). PostgREST caps one answer at its max-rows
114
+ * (1000): a person in 1,043 organizations lost the last 43, so the read pages in stable id order
115
+ * until a short page.
116
+ *
117
+ * It does NOT throw: it returns the first failing page's `{ error, status }` so a host can wrap
118
+ * the whole read in its own session retry (a 401 on a recovering session) and map the error its
119
+ * own way.
120
+ */
121
+
122
+ declare const MEMBERSHIP_ROWS_PAGE = 500;
123
+ interface MembershipRowsAnswer<Row> {
124
+ readonly data: Row[] | null;
125
+ readonly error: {
126
+ message: string;
127
+ code?: string;
128
+ } | null;
129
+ readonly status?: number | undefined;
130
+ }
131
+ declare function readMembershipRows<Row = Record<string, unknown>>(db: OrganizationsDb, containerType: string, pageSize?: number): Promise<MembershipRowsAnswer<Row>>;
132
+
133
+ export { MEMBERSHIPS_URL, MEMBERSHIP_ROWS_PAGE, type MembershipReadClient, type MembershipRowsAnswer, ORGANIZATION_CONTEXT_REQUIRED_CODE, ORGANIZATION_REQUIRED_CODE, ORGANIZATION_REQUIRED_WIRE_CODE, type OrganizationHoldDetails, type OrganizationHoldSetOn, type OrganizationMembershipSummary, type OrganizationRequiredEnvelope, OrganizationRequiredServerError, type OrganizationRequiredWireMembership, OrganizationsDb, extractOrganizationHoldMemberships, isOrganizationRequiredEnvelope, isOrganizationRequiredError, isOrganizationRequiredServerError, organizationRequired, organizationRequiredEnvelope, organizationRequiredResponse, readCallerMemberships, readMembershipRows };
@@ -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
  },
@@ -698,9 +784,24 @@ function organizationRequiredEnvelope(error, setOn = "request") {
698
784
  function organizationRequiredResponse(error) {
699
785
  return Response.json(organizationRequiredEnvelope(error), { status: 400 });
700
786
  }
787
+
788
+ // src/organization-membership-rows.ts
789
+ var MEMBERSHIP_ROWS_PAGE = 500;
790
+ async function readMembershipRows(db, containerType, pageSize = MEMBERSHIP_ROWS_PAGE) {
791
+ const rows = [];
792
+ for (let from = 0; ; from += pageSize) {
793
+ const answer = await db.rpc("mbr_for_user", { p_container_type: containerType }).order("id", { ascending: true }).range(from, from + pageSize - 1);
794
+ if (answer.error) return { data: null, error: answer.error, status: answer.status };
795
+ const page = Array.isArray(answer.data) ? answer.data : [];
796
+ rows.push(...page);
797
+ if (page.length < pageSize) break;
798
+ }
799
+ return { data: rows, error: null };
800
+ }
701
801
  export {
702
802
  LINK_ORGANIZATION_QUERY_KEY,
703
803
  MEMBERSHIPS_URL,
804
+ MEMBERSHIP_ROWS_PAGE,
704
805
  NOT_A_MEMBER_RECHECK_GAP_MS,
705
806
  NoActiveOrganizationError,
706
807
  NotAMemberError,
@@ -736,6 +837,7 @@ export {
736
837
  readAccountOrganizationChoices,
737
838
  readCallerMemberships,
738
839
  readLinkOrganizationParam,
840
+ readMembershipRows,
739
841
  readMemberships,
740
842
  readOrganizationFavourites,
741
843
  resolveActiveOrganization,
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.3",
3
+ "version": "0.2.5",
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": "2d910d0280828c62bf8f52534665eba32abe02ad"
214
+ "gitHead": "9517977774bcd6e3ed1018bd331ca397a8e7ddfd"
215
215
  }