@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 +31 -2
- package/dist/{active-organization-CIvSdboO.d.cts → active-organization-DHitxVeN.d.cts} +58 -2
- package/dist/{active-organization-CIvSdboO.d.ts → active-organization-DHitxVeN.d.ts} +58 -2
- package/dist/guest.cjs +1 -12
- package/dist/guest.d.cts +29 -7
- package/dist/guest.d.ts +29 -7
- package/dist/guest.js +1 -12
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/organizations.cjs +111 -9
- package/dist/organizations.d.cts +25 -2
- package/dist/organizations.d.ts +25 -2
- package/dist/organizations.js +111 -9
- package/dist/react.cjs +3 -1
- package/dist/react.d.cts +20 -1
- package/dist/react.d.ts +20 -1
- package/dist/react.js +3 -1
- package/package.json +2 -2
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(...)
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
/**
|
|
97
|
-
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
97
|
-
|
|
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
|
-
*
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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';
|
package/dist/organizations.cjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/organizations.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/organizations.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/organizations.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"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": "
|
|
214
|
+
"gitHead": "9517977774bcd6e3ed1018bd331ca397a8e7ddfd"
|
|
215
215
|
}
|