@ai-matrx/auth 0.2.4 → 0.3.0
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 +21 -2
- package/dist/{active-organization-Cxm6GDIy.d.cts → active-organization-B6s9ONIe.d.cts} +7 -1
- package/dist/{active-organization-Cxm6GDIy.d.ts → active-organization-B6s9ONIe.d.ts} +7 -1
- package/dist/{claims-DXdbe3cW.d.cts → claims-KrRxsfaG.d.cts} +14 -1
- package/dist/{claims-DXdbe3cW.d.ts → claims-KrRxsfaG.d.ts} +14 -1
- package/dist/guest.cjs +1 -12
- package/dist/guest.d.cts +28 -6
- package/dist/guest.d.ts +28 -6
- package/dist/guest.js +1 -12
- package/dist/index.cjs +32 -0
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +32 -0
- package/dist/next.d.cts +1 -1
- package/dist/next.d.ts +1 -1
- package/dist/organizations.cjs +58 -0
- package/dist/organizations.d.cts +69 -2
- package/dist/organizations.d.ts +69 -2
- package/dist/organizations.js +58 -0
- package/dist/react.cjs +45 -0
- package/dist/react.d.cts +32 -1
- package/dist/react.d.ts +32 -1
- package/dist/react.js +45 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog — @ai-matrx/auth
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
### auth consolidation tail (round 3)
|
|
5
|
+
|
|
6
|
+
- `/organizations`: `organization-state` — pure decisions the apps' stores made themselves:
|
|
7
|
+
`shouldPromptForOrganization`, `organizationReadFailureReason`, `organizationWasReplaced`,
|
|
8
|
+
`reconcileRehydratedOrganization` (+ type `OrganizationStoreFields`).
|
|
9
|
+
- `.`: `looksLikeJwt(token)`, `profileFromClaims(claims)` (+ type `ClaimsProfile`).
|
|
10
|
+
- `createActiveOrganization`: `clearStaleHeld` option (default false) clears a held choice that is no longer a membership.
|
|
11
|
+
- `/react`: `useActiveOrganizationView({ signedIn, resolve, list, choose, deps?, subscribe? })` — load / error vs none /
|
|
12
|
+
must-choose / choose / reload state machine over host-injected reads.
|
|
13
|
+
- Tests: `organization-state`, `react-view`, `completeSignIn` replay (already-used code and burned code refuse cleanly), claims, clearStaleHeld.
|
|
14
|
+
|
|
15
|
+
## 0.2.5
|
|
16
|
+
|
|
17
|
+
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
18
|
+
`git diff npm/auth/v0.2.4..npm/auth/v0.2.5 -- apps/shared/auth`).
|
|
19
|
+
No source changes intended and no consumer action required.
|
|
20
|
+
|
|
3
21
|
## 0.2.4
|
|
4
22
|
### active organization options (round 2)
|
|
5
23
|
|
|
@@ -48,7 +66,7 @@ No source changes intended and no consumer action required.
|
|
|
48
66
|
attaches (bounded wait, one retry after the session re-resolves, a foreign-authority token is signed out
|
|
49
67
|
and never retried, every firing reported). Host-neutral: the host injects `report`, `isAnonymousByDesign`,
|
|
50
68
|
`bypassesSessionWait`, `endUnverifiable`. From the website's `utils/supabase/sessionBarrier.ts`.
|
|
51
|
-
- `/guest` (new section) — `createGuestHandover(...)
|
|
69
|
+
- `/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
|
|
52
70
|
account the session ends in, cookie kept on failure so the next sign-in retries) and sign-up (the same
|
|
53
71
|
account is promoted in place); `guestClientCookieOptions` (the guest client's own cookie name, injected);
|
|
54
72
|
`createGuestSessionEnsurer` (mint a guest once per tab/browser). From the website's `lib/guest/*`.
|
|
@@ -67,7 +85,8 @@ Tests: `session-barrier.test.ts`, `guest.test.ts`.
|
|
|
67
85
|
(injectable `messages`), `resolveOrganizationWithLink` (the one ladder + the link, share admission injected).
|
|
68
86
|
- `organization-gate` — `createOrganizationBootstrapGate`, `isNotAMemberRefusal`, `createNotAMemberRefusalNotice`,
|
|
69
87
|
`resolveChildOrganizationId`, `resolveAdminSeat`, `adminSeatReplacesUserContext`.
|
|
70
|
-
|
|
88
|
+
- `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.
|
|
89
|
+
Tests: active-organization (+5), organization-gate (11).
|
|
71
90
|
|
|
72
91
|
## 0.2.0
|
|
73
92
|
|
|
@@ -638,6 +638,12 @@ interface CreateActiveOrganizationOptions {
|
|
|
638
638
|
* explicit `switchTo` writes the held choice (the extension's rule: a computed answer is never a choice).
|
|
639
639
|
*/
|
|
640
640
|
readonly holdResolved?: boolean | undefined;
|
|
641
|
+
/**
|
|
642
|
+
* After a read, a held choice that is no longer a membership is CLEARED from the held store (a
|
|
643
|
+
* selection must not outlive the membership). Default false. With `holdResolved` true the
|
|
644
|
+
* ladder's answer has already replaced it, so this matters for `holdResolved: false`.
|
|
645
|
+
*/
|
|
646
|
+
readonly clearStaleHeld?: boolean | undefined;
|
|
641
647
|
}
|
|
642
648
|
/** A held store over a `Storage` (browser `sessionStorage` = per tab), identity-keyed. */
|
|
643
649
|
declare function storageHeldOrganization(storage: Pick<Storage, "getItem" | "setItem" | "removeItem">, key?: string): Omit<HeldOrganizationStore, "read"> & {
|
|
@@ -647,4 +653,4 @@ declare function storageHeldOrganization(storage: Pick<Storage, "getItem" | "set
|
|
|
647
653
|
declare function memoryHeldOrganization(): HeldOrganizationStore;
|
|
648
654
|
declare function createActiveOrganization(options: CreateActiveOrganizationOptions): ActiveOrganization;
|
|
649
655
|
|
|
650
|
-
export { createNotAMemberRefusalNotice as $, type AuthClient as A, NoActiveOrganizationError as B, type CachedActiveOrganization as C, NotAMemberError as D, type NotAMemberRefusalNotice as E,
|
|
656
|
+
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 };
|
|
@@ -638,6 +638,12 @@ interface CreateActiveOrganizationOptions {
|
|
|
638
638
|
* explicit `switchTo` writes the held choice (the extension's rule: a computed answer is never a choice).
|
|
639
639
|
*/
|
|
640
640
|
readonly holdResolved?: boolean | undefined;
|
|
641
|
+
/**
|
|
642
|
+
* After a read, a held choice that is no longer a membership is CLEARED from the held store (a
|
|
643
|
+
* selection must not outlive the membership). Default false. With `holdResolved` true the
|
|
644
|
+
* ladder's answer has already replaced it, so this matters for `holdResolved: false`.
|
|
645
|
+
*/
|
|
646
|
+
readonly clearStaleHeld?: boolean | undefined;
|
|
641
647
|
}
|
|
642
648
|
/** A held store over a `Storage` (browser `sessionStorage` = per tab), identity-keyed. */
|
|
643
649
|
declare function storageHeldOrganization(storage: Pick<Storage, "getItem" | "setItem" | "removeItem">, key?: string): Omit<HeldOrganizationStore, "read"> & {
|
|
@@ -647,4 +653,4 @@ declare function storageHeldOrganization(storage: Pick<Storage, "getItem" | "set
|
|
|
647
653
|
declare function memoryHeldOrganization(): HeldOrganizationStore;
|
|
648
654
|
declare function createActiveOrganization(options: CreateActiveOrganizationOptions): ActiveOrganization;
|
|
649
655
|
|
|
650
|
-
export { createNotAMemberRefusalNotice as $, type AuthClient as A, NoActiveOrganizationError as B, type CachedActiveOrganization as C, NotAMemberError as D, type NotAMemberRefusalNotice as E,
|
|
656
|
+
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 };
|
|
@@ -74,6 +74,19 @@ declare function isSignedOutError(error: AuthError | {
|
|
|
74
74
|
}): boolean;
|
|
75
75
|
/** The verified claims as a `{ id, ... }` user, or `null` when there are none. */
|
|
76
76
|
declare function userFromClaims(claims: JwtPayload | undefined): ApiClaimsUser | null;
|
|
77
|
+
/** Three segments whose header and payload decode as JSON. A malformed token is INVALID, not "no verdict". */
|
|
78
|
+
declare function looksLikeJwt(token: string): boolean;
|
|
79
|
+
/** The profile fields a verified token states (names match the `users` profile row). */
|
|
80
|
+
interface ClaimsProfile {
|
|
81
|
+
id: string;
|
|
82
|
+
email: string | null;
|
|
83
|
+
full_name: string | null;
|
|
84
|
+
avatar_url: string | null;
|
|
85
|
+
/** Only when the token carries it. */
|
|
86
|
+
email_verified?: boolean;
|
|
87
|
+
}
|
|
88
|
+
/** Map verified claims to a profile, or `null` when the token carries no subject. */
|
|
89
|
+
declare function profileFromClaims(claims: Record<string, unknown> | null | undefined): ClaimsProfile | null;
|
|
77
90
|
/**
|
|
78
91
|
* GUEST vs ACCOUNT. A signed-in session is a person with an ACCOUNT or a GUEST (anonymous session,
|
|
79
92
|
* `is_anonymous: true`). Account UI (menu, billing, sign-out, org picker) keys on `hasAccount`.
|
|
@@ -135,4 +148,4 @@ declare function createClaimsCache(client: ClaimsCapableClient & {
|
|
|
135
148
|
};
|
|
136
149
|
}, options: ClaimsCacheOptions): () => Promise<ClaimsResult>;
|
|
137
150
|
|
|
138
|
-
export { type ApiClaimsUser as A, type ClaimsCacheOptions as C, PROJECT_SIGNING_KEYS as P, type ClaimsCapableClient as a, type
|
|
151
|
+
export { type ApiClaimsUser as A, type ClaimsCacheOptions as C, PROJECT_SIGNING_KEYS as P, type ClaimsCapableClient as a, type ClaimsProfile as b, type ClaimsResult as c, PROJECT_SIGNING_KEY_AUTHORITIES as d, accountFlagsFor as e, confirmUnverifiableSession as f, createClaimsCache as g, getClaimsUser as h, isAuthTransportFailure as i, isSignedOutError as j, isUnverifiableTokenError as k, isUnverifiableTokenRefusal as l, looksLikeJwt as m, profileFromClaims as n, pinnedSigningKeysFor as p, userFromClaims as u };
|
|
@@ -74,6 +74,19 @@ declare function isSignedOutError(error: AuthError | {
|
|
|
74
74
|
}): boolean;
|
|
75
75
|
/** The verified claims as a `{ id, ... }` user, or `null` when there are none. */
|
|
76
76
|
declare function userFromClaims(claims: JwtPayload | undefined): ApiClaimsUser | null;
|
|
77
|
+
/** Three segments whose header and payload decode as JSON. A malformed token is INVALID, not "no verdict". */
|
|
78
|
+
declare function looksLikeJwt(token: string): boolean;
|
|
79
|
+
/** The profile fields a verified token states (names match the `users` profile row). */
|
|
80
|
+
interface ClaimsProfile {
|
|
81
|
+
id: string;
|
|
82
|
+
email: string | null;
|
|
83
|
+
full_name: string | null;
|
|
84
|
+
avatar_url: string | null;
|
|
85
|
+
/** Only when the token carries it. */
|
|
86
|
+
email_verified?: boolean;
|
|
87
|
+
}
|
|
88
|
+
/** Map verified claims to a profile, or `null` when the token carries no subject. */
|
|
89
|
+
declare function profileFromClaims(claims: Record<string, unknown> | null | undefined): ClaimsProfile | null;
|
|
77
90
|
/**
|
|
78
91
|
* GUEST vs ACCOUNT. A signed-in session is a person with an ACCOUNT or a GUEST (anonymous session,
|
|
79
92
|
* `is_anonymous: true`). Account UI (menu, billing, sign-out, org picker) keys on `hasAccount`.
|
|
@@ -135,4 +148,4 @@ declare function createClaimsCache(client: ClaimsCapableClient & {
|
|
|
135
148
|
};
|
|
136
149
|
}, options: ClaimsCacheOptions): () => Promise<ClaimsResult>;
|
|
137
150
|
|
|
138
|
-
export { type ApiClaimsUser as A, type ClaimsCacheOptions as C, PROJECT_SIGNING_KEYS as P, type ClaimsCapableClient as a, type
|
|
151
|
+
export { type ApiClaimsUser as A, type ClaimsCacheOptions as C, PROJECT_SIGNING_KEYS as P, type ClaimsCapableClient as a, type ClaimsProfile as b, type ClaimsResult as c, PROJECT_SIGNING_KEY_AUTHORITIES as d, accountFlagsFor as e, confirmUnverifiableSession as f, createClaimsCache as g, getClaimsUser as h, isAuthTransportFailure as i, isSignedOutError as j, isUnverifiableTokenError as k, isUnverifiableTokenRefusal as l, looksLikeJwt as m, profileFromClaims as n, pinnedSigningKeysFor as p, userFromClaims as u };
|
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;
|
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;
|
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.cjs
CHANGED
|
@@ -46,10 +46,12 @@ __export(src_exports, {
|
|
|
46
46
|
isUnverifiableTokenError: () => isUnverifiableTokenError,
|
|
47
47
|
isUnverifiableTokenRefusal: () => isUnverifiableTokenRefusal,
|
|
48
48
|
keyValueStorage: () => keyValueStorage,
|
|
49
|
+
looksLikeJwt: () => looksLikeJwt,
|
|
49
50
|
memoryStorage: () => memoryStorage,
|
|
50
51
|
oauthAuthClient: () => oauthAuthClient,
|
|
51
52
|
onSessionChange: () => onSessionChange,
|
|
52
53
|
pinnedSigningKeysFor: () => pinnedSigningKeysFor,
|
|
54
|
+
profileFromClaims: () => profileFromClaims,
|
|
53
55
|
refreshSession: () => refreshSession,
|
|
54
56
|
sessionCredentials: () => sessionCredentials,
|
|
55
57
|
signInWithOAuth: () => signInWithOAuth,
|
|
@@ -102,6 +104,36 @@ function userFromClaims(claims) {
|
|
|
102
104
|
user_metadata: claims.user_metadata ?? {}
|
|
103
105
|
};
|
|
104
106
|
}
|
|
107
|
+
function looksLikeJwt(token) {
|
|
108
|
+
const parts = token.split(".");
|
|
109
|
+
if (parts.length !== 3) return false;
|
|
110
|
+
try {
|
|
111
|
+
for (const part of parts.slice(0, 2)) {
|
|
112
|
+
const normalized = part.replace(/-/g, "+").replace(/_/g, "/");
|
|
113
|
+
const binary = atob(normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="));
|
|
114
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
115
|
+
JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function profileFromClaims(claims) {
|
|
123
|
+
if (!claims) return null;
|
|
124
|
+
const text = (value) => typeof value === "string" && value.length > 0 ? value : null;
|
|
125
|
+
const id = text(claims["sub"]);
|
|
126
|
+
if (!id) return null;
|
|
127
|
+
const raw = claims["user_metadata"];
|
|
128
|
+
const meta = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
129
|
+
return {
|
|
130
|
+
id,
|
|
131
|
+
email: text(claims["email"]),
|
|
132
|
+
full_name: (typeof meta["full_name"] === "string" ? meta["full_name"] : null) ?? (typeof meta["name"] === "string" ? meta["name"] : null),
|
|
133
|
+
avatar_url: typeof meta["avatar_url"] === "string" ? meta["avatar_url"] : null,
|
|
134
|
+
...typeof meta["email_verified"] === "boolean" && { email_verified: meta["email_verified"] }
|
|
135
|
+
};
|
|
136
|
+
}
|
|
105
137
|
function accountFlagsFor(claims) {
|
|
106
138
|
if (!claims) return { isGuest: false, hasAccount: false };
|
|
107
139
|
const isGuest = claims.is_anonymous === true;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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-B6s9ONIe.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-B6s9ONIe.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';
|
|
6
6
|
export { AuthChangeEvent, Session, User, UserAttributes } from '@supabase/supabase-js';
|
|
7
7
|
export { CredentialsPort, MatrxCredential, MatrxCredentialKind } from './credentials.cjs';
|
|
8
|
-
export { A as ApiClaimsUser, C as ClaimsCacheOptions, a as ClaimsCapableClient, b as ClaimsResult, P as PROJECT_SIGNING_KEYS,
|
|
8
|
+
export { A as ApiClaimsUser, C as ClaimsCacheOptions, a as ClaimsCapableClient, b as ClaimsProfile, c as ClaimsResult, P as PROJECT_SIGNING_KEYS, d as PROJECT_SIGNING_KEY_AUTHORITIES, e as accountFlagsFor, f as confirmUnverifiableSession, g as createClaimsCache, h as getClaimsUser, i as isAuthTransportFailure, j as isSignedOutError, k as isUnverifiableTokenError, l as isUnverifiableTokenRefusal, m as looksLikeJwt, p as pinnedSigningKeysFor, n as profileFromClaims, u as userFromClaims } from './claims-KrRxsfaG.cjs';
|
|
9
9
|
|
|
10
10
|
/** How long a request may wait for the first auth report before going anyway. */
|
|
11
11
|
declare const SESSION_ATTACH_BUDGET_MS = 3000;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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-B6s9ONIe.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-B6s9ONIe.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';
|
|
6
6
|
export { AuthChangeEvent, Session, User, UserAttributes } from '@supabase/supabase-js';
|
|
7
7
|
export { CredentialsPort, MatrxCredential, MatrxCredentialKind } from './credentials.js';
|
|
8
|
-
export { A as ApiClaimsUser, C as ClaimsCacheOptions, a as ClaimsCapableClient, b as ClaimsResult, P as PROJECT_SIGNING_KEYS,
|
|
8
|
+
export { A as ApiClaimsUser, C as ClaimsCacheOptions, a as ClaimsCapableClient, b as ClaimsProfile, c as ClaimsResult, P as PROJECT_SIGNING_KEYS, d as PROJECT_SIGNING_KEY_AUTHORITIES, e as accountFlagsFor, f as confirmUnverifiableSession, g as createClaimsCache, h as getClaimsUser, i as isAuthTransportFailure, j as isSignedOutError, k as isUnverifiableTokenError, l as isUnverifiableTokenRefusal, m as looksLikeJwt, p as pinnedSigningKeysFor, n as profileFromClaims, u as userFromClaims } from './claims-KrRxsfaG.js';
|
|
9
9
|
|
|
10
10
|
/** How long a request may wait for the first auth report before going anyway. */
|
|
11
11
|
declare const SESSION_ATTACH_BUDGET_MS = 3000;
|
package/dist/index.js
CHANGED
|
@@ -41,6 +41,36 @@ function userFromClaims(claims) {
|
|
|
41
41
|
user_metadata: claims.user_metadata ?? {}
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
+
function looksLikeJwt(token) {
|
|
45
|
+
const parts = token.split(".");
|
|
46
|
+
if (parts.length !== 3) return false;
|
|
47
|
+
try {
|
|
48
|
+
for (const part of parts.slice(0, 2)) {
|
|
49
|
+
const normalized = part.replace(/-/g, "+").replace(/_/g, "/");
|
|
50
|
+
const binary = atob(normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="));
|
|
51
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
52
|
+
JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function profileFromClaims(claims) {
|
|
60
|
+
if (!claims) return null;
|
|
61
|
+
const text = (value) => typeof value === "string" && value.length > 0 ? value : null;
|
|
62
|
+
const id = text(claims["sub"]);
|
|
63
|
+
if (!id) return null;
|
|
64
|
+
const raw = claims["user_metadata"];
|
|
65
|
+
const meta = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
66
|
+
return {
|
|
67
|
+
id,
|
|
68
|
+
email: text(claims["email"]),
|
|
69
|
+
full_name: (typeof meta["full_name"] === "string" ? meta["full_name"] : null) ?? (typeof meta["name"] === "string" ? meta["name"] : null),
|
|
70
|
+
avatar_url: typeof meta["avatar_url"] === "string" ? meta["avatar_url"] : null,
|
|
71
|
+
...typeof meta["email_verified"] === "boolean" && { email_verified: meta["email_verified"] }
|
|
72
|
+
};
|
|
73
|
+
}
|
|
44
74
|
function accountFlagsFor(claims) {
|
|
45
75
|
if (!claims) return { isGuest: false, hasAccount: false };
|
|
46
76
|
const isGuest = claims.is_anonymous === true;
|
|
@@ -726,10 +756,12 @@ export {
|
|
|
726
756
|
isUnverifiableTokenError,
|
|
727
757
|
isUnverifiableTokenRefusal,
|
|
728
758
|
keyValueStorage,
|
|
759
|
+
looksLikeJwt,
|
|
729
760
|
memoryStorage,
|
|
730
761
|
oauthAuthClient,
|
|
731
762
|
onSessionChange,
|
|
732
763
|
pinnedSigningKeysFor,
|
|
764
|
+
profileFromClaims,
|
|
733
765
|
refreshSession,
|
|
734
766
|
sessionCredentials,
|
|
735
767
|
signInWithOAuth,
|
package/dist/next.d.cts
CHANGED
|
@@ -2,7 +2,7 @@ import { createServerClient, createBrowserClient } from '@supabase/ssr';
|
|
|
2
2
|
import { b as AuthCookieValue, A as AuthCookie, a as AuthCookieOptions } from './auth-cookie-DWx6Q3xo.cjs';
|
|
3
3
|
export { C as CreateAuthCookieArgs, D as DEFAULT_AUTH_COOKIE_NAME, c as createAuthCookie } from './auth-cookie-DWx6Q3xo.cjs';
|
|
4
4
|
import { SupabaseClient } from '@supabase/supabase-js';
|
|
5
|
-
import { a as ClaimsCapableClient, A as ApiClaimsUser } from './claims-
|
|
5
|
+
import { a as ClaimsCapableClient, A as ApiClaimsUser } from './claims-KrRxsfaG.cjs';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @ai-matrx/data/next — the Next.js Supabase binding.
|
package/dist/next.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { createServerClient, createBrowserClient } from '@supabase/ssr';
|
|
|
2
2
|
import { b as AuthCookieValue, A as AuthCookie, a as AuthCookieOptions } from './auth-cookie-DWx6Q3xo.js';
|
|
3
3
|
export { C as CreateAuthCookieArgs, D as DEFAULT_AUTH_COOKIE_NAME, c as createAuthCookie } from './auth-cookie-DWx6Q3xo.js';
|
|
4
4
|
import { SupabaseClient } from '@supabase/supabase-js';
|
|
5
|
-
import { a as ClaimsCapableClient, A as ApiClaimsUser } from './claims-
|
|
5
|
+
import { a as ClaimsCapableClient, A as ApiClaimsUser } from './claims-KrRxsfaG.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @ai-matrx/data/next — the Next.js Supabase binding.
|
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,
|
|
@@ -51,20 +52,25 @@ __export(organizations_exports, {
|
|
|
51
52
|
oldestMembership: () => oldestMembership,
|
|
52
53
|
orderOrganizations: () => orderOrganizations,
|
|
53
54
|
organizationMatches: () => organizationMatches,
|
|
55
|
+
organizationReadFailureReason: () => organizationReadFailureReason,
|
|
54
56
|
organizationRequired: () => organizationRequired,
|
|
55
57
|
organizationRequiredEnvelope: () => organizationRequiredEnvelope,
|
|
56
58
|
organizationRequiredResponse: () => organizationRequiredResponse,
|
|
59
|
+
organizationWasReplaced: () => organizationWasReplaced,
|
|
57
60
|
readAccountOrganizationChoices: () => readAccountOrganizationChoices,
|
|
58
61
|
readCallerMemberships: () => readCallerMemberships,
|
|
59
62
|
readLinkOrganizationParam: () => readLinkOrganizationParam,
|
|
63
|
+
readMembershipRows: () => readMembershipRows,
|
|
60
64
|
readMemberships: () => readMemberships,
|
|
61
65
|
readOrganizationFavourites: () => readOrganizationFavourites,
|
|
66
|
+
reconcileRehydratedOrganization: () => reconcileRehydratedOrganization,
|
|
62
67
|
resolveActiveOrganization: () => resolveActiveOrganization,
|
|
63
68
|
resolveActiveOrganizationLazily: () => resolveActiveOrganizationLazily,
|
|
64
69
|
resolveAdminSeat: () => resolveAdminSeat,
|
|
65
70
|
resolveChildOrganizationId: () => resolveChildOrganizationId,
|
|
66
71
|
resolveOrganizationWithLink: () => resolveOrganizationWithLink,
|
|
67
72
|
setOrganizationFavourite: () => setOrganizationFavourite,
|
|
73
|
+
shouldPromptForOrganization: () => shouldPromptForOrganization,
|
|
68
74
|
storageHeldOrganization: () => storageHeldOrganization,
|
|
69
75
|
toAccountOrganizationChoices: () => toAccountOrganizationChoices,
|
|
70
76
|
writeLastActiveOrganization: () => writeLastActiveOrganization,
|
|
@@ -608,6 +614,10 @@ function createActiveOrganization(options) {
|
|
|
608
614
|
return current;
|
|
609
615
|
}
|
|
610
616
|
if (options.holdResolved ?? true) await held.write(person.id, resolved.organization.id);
|
|
617
|
+
if (options.clearStaleHeld) {
|
|
618
|
+
const stale = await held.read(person.id);
|
|
619
|
+
if (stale && !memberships.some((m) => m.id === stale)) await held.clear();
|
|
620
|
+
}
|
|
611
621
|
const wasOrg = before.status === "ready" || before.status === "unchecked" ? before.organization : null;
|
|
612
622
|
const lapsed = wasOrg && wasOrg.id !== resolved.organization.id && !memberships.some((m) => m.id === wasOrg.id) ? wasOrg : null;
|
|
613
623
|
if (linkDecision && linkDecision.kind !== "no-link") options.link?.onDecision?.(linkDecision);
|
|
@@ -858,3 +868,51 @@ function organizationRequiredEnvelope(error, setOn = "request") {
|
|
|
858
868
|
function organizationRequiredResponse(error) {
|
|
859
869
|
return Response.json(organizationRequiredEnvelope(error), { status: 400 });
|
|
860
870
|
}
|
|
871
|
+
|
|
872
|
+
// src/organization-membership-rows.ts
|
|
873
|
+
var MEMBERSHIP_ROWS_PAGE = 500;
|
|
874
|
+
async function readMembershipRows(db, containerType, pageSize = MEMBERSHIP_ROWS_PAGE) {
|
|
875
|
+
const rows = [];
|
|
876
|
+
for (let from = 0; ; from += pageSize) {
|
|
877
|
+
const answer = await db.rpc("mbr_for_user", { p_container_type: containerType }).order("id", { ascending: true }).range(from, from + pageSize - 1);
|
|
878
|
+
if (answer.error) return { data: null, error: answer.error, status: answer.status };
|
|
879
|
+
const page = Array.isArray(answer.data) ? answer.data : [];
|
|
880
|
+
rows.push(...page);
|
|
881
|
+
if (page.length < pageSize) break;
|
|
882
|
+
}
|
|
883
|
+
return { data: rows, error: null };
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// src/organization-state.ts
|
|
887
|
+
function shouldPromptForOrganization(input) {
|
|
888
|
+
return input.resolved === true && (input.organizationId ?? null) == null && (input.failure ?? null) == null;
|
|
889
|
+
}
|
|
890
|
+
function organizationReadFailureReason(error) {
|
|
891
|
+
return error instanceof Error && error.message ? `the organization read failed: ${error.message}` : "the organization read failed";
|
|
892
|
+
}
|
|
893
|
+
function organizationWasReplaced(input) {
|
|
894
|
+
return Boolean(input.held && input.chosen && input.chosen !== input.held && input.linkKind !== "honoured");
|
|
895
|
+
}
|
|
896
|
+
function reconcileRehydratedOrganization(current, loaded) {
|
|
897
|
+
const fromLadder = loaded.orgBootstrapResolved === true;
|
|
898
|
+
if (!fromLadder) {
|
|
899
|
+
if (current.organization_id == null && loaded.organization_id) {
|
|
900
|
+
return { organization_id: loaded.organization_id, organization_name: loaded.organization_name ?? null };
|
|
901
|
+
}
|
|
902
|
+
return {};
|
|
903
|
+
}
|
|
904
|
+
const heldHere = current.orgBootstrapResolved && current.organization_id != null;
|
|
905
|
+
const patch = { orgBootstrapResolved: true };
|
|
906
|
+
if (loaded.orgBootstrapFailure !== void 0) {
|
|
907
|
+
patch.orgBootstrapFailure = loaded.organization_id != null ? null : loaded.orgBootstrapFailure;
|
|
908
|
+
}
|
|
909
|
+
if (loaded.organization_id === void 0) return patch;
|
|
910
|
+
if (heldHere && loaded.organization_id === current.organization_id) {
|
|
911
|
+
if (loaded.organization_name != null) patch.organization_name = loaded.organization_name;
|
|
912
|
+
return patch;
|
|
913
|
+
}
|
|
914
|
+
if (heldHere) return patch;
|
|
915
|
+
patch.organization_id = loaded.organization_id;
|
|
916
|
+
patch.organization_name = loaded.organization_name ?? null;
|
|
917
|
+
return patch;
|
|
918
|
+
}
|
package/dist/organizations.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { O as OrganizationsDb } from './active-organization-B6s9ONIe.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-B6s9ONIe.cjs';
|
|
2
3
|
import '@supabase/supabase-js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -107,4 +108,70 @@ 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
|
+
/**
|
|
134
|
+
* The DECISIONS an app's organization store used to make for itself (moved from the website's
|
|
135
|
+
* `shouldPromptForOrganization`, `appContextSlice` rehydrate reducer and `activeOrgBootstrap`
|
|
136
|
+
* thunk). Pure: an app's Redux/zustand slice only calls these and assigns what they return.
|
|
137
|
+
*/
|
|
138
|
+
/** The organization fields an app keeps in its store. */
|
|
139
|
+
interface OrganizationStoreFields {
|
|
140
|
+
readonly organization_id: string | null;
|
|
141
|
+
readonly organization_name: string | null;
|
|
142
|
+
readonly orgBootstrapResolved: boolean;
|
|
143
|
+
readonly orgBootstrapFailure: string | null;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* True when the UI should nudge the person to choose an organization: the bootstrap has resolved,
|
|
147
|
+
* none is selected, AND the read of their memberships succeeded. A failed read is never the nudge
|
|
148
|
+
* ("we could not check" speaks instead) — a member of thirteen organizations was once told to pick
|
|
149
|
+
* one after a single failed fetch.
|
|
150
|
+
*/
|
|
151
|
+
declare function shouldPromptForOrganization(input: {
|
|
152
|
+
readonly resolved: boolean;
|
|
153
|
+
readonly organizationId: string | null | undefined;
|
|
154
|
+
readonly failure: string | null | undefined;
|
|
155
|
+
}): boolean;
|
|
156
|
+
/** The words for a failed membership read — one sentence for every host. */
|
|
157
|
+
declare function organizationReadFailureReason(error: unknown): string;
|
|
158
|
+
/**
|
|
159
|
+
* A held organization was taken away (removed, archived) and the ladder chose another, and the
|
|
160
|
+
* choice is not the link's own: switch, and say so.
|
|
161
|
+
*/
|
|
162
|
+
declare function organizationWasReplaced(input: {
|
|
163
|
+
readonly held: string | null | undefined;
|
|
164
|
+
readonly chosen: string | null | undefined;
|
|
165
|
+
readonly linkKind?: string | undefined;
|
|
166
|
+
}): boolean;
|
|
167
|
+
/**
|
|
168
|
+
* What a REHYDRATE does to the organization fields. Two kinds:
|
|
169
|
+
* - the browser CACHE (no `orgBootstrapResolved`) only PAINTS an empty store and never marks the
|
|
170
|
+
* question answered, so no request leaves on it;
|
|
171
|
+
* - the LOAD LADDER's answer (`orgBootstrapResolved: true`) is the account's and wins over the
|
|
172
|
+
* painted cache — unless this store already holds an organization the person chose here.
|
|
173
|
+
* Returns the fields to assign (empty = change nothing).
|
|
174
|
+
*/
|
|
175
|
+
declare function reconcileRehydratedOrganization(current: Pick<OrganizationStoreFields, "organization_id" | "orgBootstrapResolved">, loaded: Partial<OrganizationStoreFields>): Partial<OrganizationStoreFields>;
|
|
176
|
+
|
|
177
|
+
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, type OrganizationStoreFields, OrganizationsDb, extractOrganizationHoldMemberships, isOrganizationRequiredEnvelope, isOrganizationRequiredError, isOrganizationRequiredServerError, organizationReadFailureReason, organizationRequired, organizationRequiredEnvelope, organizationRequiredResponse, organizationWasReplaced, readCallerMemberships, readMembershipRows, reconcileRehydratedOrganization, shouldPromptForOrganization };
|
package/dist/organizations.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { O as OrganizationsDb } from './active-organization-B6s9ONIe.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-B6s9ONIe.js';
|
|
2
3
|
import '@supabase/supabase-js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -107,4 +108,70 @@ 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
|
+
/**
|
|
134
|
+
* The DECISIONS an app's organization store used to make for itself (moved from the website's
|
|
135
|
+
* `shouldPromptForOrganization`, `appContextSlice` rehydrate reducer and `activeOrgBootstrap`
|
|
136
|
+
* thunk). Pure: an app's Redux/zustand slice only calls these and assigns what they return.
|
|
137
|
+
*/
|
|
138
|
+
/** The organization fields an app keeps in its store. */
|
|
139
|
+
interface OrganizationStoreFields {
|
|
140
|
+
readonly organization_id: string | null;
|
|
141
|
+
readonly organization_name: string | null;
|
|
142
|
+
readonly orgBootstrapResolved: boolean;
|
|
143
|
+
readonly orgBootstrapFailure: string | null;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* True when the UI should nudge the person to choose an organization: the bootstrap has resolved,
|
|
147
|
+
* none is selected, AND the read of their memberships succeeded. A failed read is never the nudge
|
|
148
|
+
* ("we could not check" speaks instead) — a member of thirteen organizations was once told to pick
|
|
149
|
+
* one after a single failed fetch.
|
|
150
|
+
*/
|
|
151
|
+
declare function shouldPromptForOrganization(input: {
|
|
152
|
+
readonly resolved: boolean;
|
|
153
|
+
readonly organizationId: string | null | undefined;
|
|
154
|
+
readonly failure: string | null | undefined;
|
|
155
|
+
}): boolean;
|
|
156
|
+
/** The words for a failed membership read — one sentence for every host. */
|
|
157
|
+
declare function organizationReadFailureReason(error: unknown): string;
|
|
158
|
+
/**
|
|
159
|
+
* A held organization was taken away (removed, archived) and the ladder chose another, and the
|
|
160
|
+
* choice is not the link's own: switch, and say so.
|
|
161
|
+
*/
|
|
162
|
+
declare function organizationWasReplaced(input: {
|
|
163
|
+
readonly held: string | null | undefined;
|
|
164
|
+
readonly chosen: string | null | undefined;
|
|
165
|
+
readonly linkKind?: string | undefined;
|
|
166
|
+
}): boolean;
|
|
167
|
+
/**
|
|
168
|
+
* What a REHYDRATE does to the organization fields. Two kinds:
|
|
169
|
+
* - the browser CACHE (no `orgBootstrapResolved`) only PAINTS an empty store and never marks the
|
|
170
|
+
* question answered, so no request leaves on it;
|
|
171
|
+
* - the LOAD LADDER's answer (`orgBootstrapResolved: true`) is the account's and wins over the
|
|
172
|
+
* painted cache — unless this store already holds an organization the person chose here.
|
|
173
|
+
* Returns the fields to assign (empty = change nothing).
|
|
174
|
+
*/
|
|
175
|
+
declare function reconcileRehydratedOrganization(current: Pick<OrganizationStoreFields, "organization_id" | "orgBootstrapResolved">, loaded: Partial<OrganizationStoreFields>): Partial<OrganizationStoreFields>;
|
|
176
|
+
|
|
177
|
+
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, type OrganizationStoreFields, OrganizationsDb, extractOrganizationHoldMemberships, isOrganizationRequiredEnvelope, isOrganizationRequiredError, isOrganizationRequiredServerError, organizationReadFailureReason, organizationRequired, organizationRequiredEnvelope, organizationRequiredResponse, organizationWasReplaced, readCallerMemberships, readMembershipRows, reconcileRehydratedOrganization, shouldPromptForOrganization };
|
package/dist/organizations.js
CHANGED
|
@@ -534,6 +534,10 @@ function createActiveOrganization(options) {
|
|
|
534
534
|
return current;
|
|
535
535
|
}
|
|
536
536
|
if (options.holdResolved ?? true) await held.write(person.id, resolved.organization.id);
|
|
537
|
+
if (options.clearStaleHeld) {
|
|
538
|
+
const stale = await held.read(person.id);
|
|
539
|
+
if (stale && !memberships.some((m) => m.id === stale)) await held.clear();
|
|
540
|
+
}
|
|
537
541
|
const wasOrg = before.status === "ready" || before.status === "unchecked" ? before.organization : null;
|
|
538
542
|
const lapsed = wasOrg && wasOrg.id !== resolved.organization.id && !memberships.some((m) => m.id === wasOrg.id) ? wasOrg : null;
|
|
539
543
|
if (linkDecision && linkDecision.kind !== "no-link") options.link?.onDecision?.(linkDecision);
|
|
@@ -784,9 +788,58 @@ function organizationRequiredEnvelope(error, setOn = "request") {
|
|
|
784
788
|
function organizationRequiredResponse(error) {
|
|
785
789
|
return Response.json(organizationRequiredEnvelope(error), { status: 400 });
|
|
786
790
|
}
|
|
791
|
+
|
|
792
|
+
// src/organization-membership-rows.ts
|
|
793
|
+
var MEMBERSHIP_ROWS_PAGE = 500;
|
|
794
|
+
async function readMembershipRows(db, containerType, pageSize = MEMBERSHIP_ROWS_PAGE) {
|
|
795
|
+
const rows = [];
|
|
796
|
+
for (let from = 0; ; from += pageSize) {
|
|
797
|
+
const answer = await db.rpc("mbr_for_user", { p_container_type: containerType }).order("id", { ascending: true }).range(from, from + pageSize - 1);
|
|
798
|
+
if (answer.error) return { data: null, error: answer.error, status: answer.status };
|
|
799
|
+
const page = Array.isArray(answer.data) ? answer.data : [];
|
|
800
|
+
rows.push(...page);
|
|
801
|
+
if (page.length < pageSize) break;
|
|
802
|
+
}
|
|
803
|
+
return { data: rows, error: null };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/organization-state.ts
|
|
807
|
+
function shouldPromptForOrganization(input) {
|
|
808
|
+
return input.resolved === true && (input.organizationId ?? null) == null && (input.failure ?? null) == null;
|
|
809
|
+
}
|
|
810
|
+
function organizationReadFailureReason(error) {
|
|
811
|
+
return error instanceof Error && error.message ? `the organization read failed: ${error.message}` : "the organization read failed";
|
|
812
|
+
}
|
|
813
|
+
function organizationWasReplaced(input) {
|
|
814
|
+
return Boolean(input.held && input.chosen && input.chosen !== input.held && input.linkKind !== "honoured");
|
|
815
|
+
}
|
|
816
|
+
function reconcileRehydratedOrganization(current, loaded) {
|
|
817
|
+
const fromLadder = loaded.orgBootstrapResolved === true;
|
|
818
|
+
if (!fromLadder) {
|
|
819
|
+
if (current.organization_id == null && loaded.organization_id) {
|
|
820
|
+
return { organization_id: loaded.organization_id, organization_name: loaded.organization_name ?? null };
|
|
821
|
+
}
|
|
822
|
+
return {};
|
|
823
|
+
}
|
|
824
|
+
const heldHere = current.orgBootstrapResolved && current.organization_id != null;
|
|
825
|
+
const patch = { orgBootstrapResolved: true };
|
|
826
|
+
if (loaded.orgBootstrapFailure !== void 0) {
|
|
827
|
+
patch.orgBootstrapFailure = loaded.organization_id != null ? null : loaded.orgBootstrapFailure;
|
|
828
|
+
}
|
|
829
|
+
if (loaded.organization_id === void 0) return patch;
|
|
830
|
+
if (heldHere && loaded.organization_id === current.organization_id) {
|
|
831
|
+
if (loaded.organization_name != null) patch.organization_name = loaded.organization_name;
|
|
832
|
+
return patch;
|
|
833
|
+
}
|
|
834
|
+
if (heldHere) return patch;
|
|
835
|
+
patch.organization_id = loaded.organization_id;
|
|
836
|
+
patch.organization_name = loaded.organization_name ?? null;
|
|
837
|
+
return patch;
|
|
838
|
+
}
|
|
787
839
|
export {
|
|
788
840
|
LINK_ORGANIZATION_QUERY_KEY,
|
|
789
841
|
MEMBERSHIPS_URL,
|
|
842
|
+
MEMBERSHIP_ROWS_PAGE,
|
|
790
843
|
NOT_A_MEMBER_RECHECK_GAP_MS,
|
|
791
844
|
NoActiveOrganizationError,
|
|
792
845
|
NotAMemberError,
|
|
@@ -816,20 +869,25 @@ export {
|
|
|
816
869
|
oldestMembership,
|
|
817
870
|
orderOrganizations,
|
|
818
871
|
organizationMatches,
|
|
872
|
+
organizationReadFailureReason,
|
|
819
873
|
organizationRequired,
|
|
820
874
|
organizationRequiredEnvelope,
|
|
821
875
|
organizationRequiredResponse,
|
|
876
|
+
organizationWasReplaced,
|
|
822
877
|
readAccountOrganizationChoices,
|
|
823
878
|
readCallerMemberships,
|
|
824
879
|
readLinkOrganizationParam,
|
|
880
|
+
readMembershipRows,
|
|
825
881
|
readMemberships,
|
|
826
882
|
readOrganizationFavourites,
|
|
883
|
+
reconcileRehydratedOrganization,
|
|
827
884
|
resolveActiveOrganization,
|
|
828
885
|
resolveActiveOrganizationLazily,
|
|
829
886
|
resolveAdminSeat,
|
|
830
887
|
resolveChildOrganizationId,
|
|
831
888
|
resolveOrganizationWithLink,
|
|
832
889
|
setOrganizationFavourite,
|
|
890
|
+
shouldPromptForOrganization,
|
|
833
891
|
storageHeldOrganization,
|
|
834
892
|
toAccountOrganizationChoices,
|
|
835
893
|
writeLastActiveOrganization,
|
package/dist/react.cjs
CHANGED
|
@@ -23,6 +23,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
23
23
|
var react_exports = {};
|
|
24
24
|
__export(react_exports, {
|
|
25
25
|
useActiveOrganization: () => useActiveOrganization,
|
|
26
|
+
useActiveOrganizationView: () => useActiveOrganizationView,
|
|
26
27
|
useSession: () => useSession,
|
|
27
28
|
useSessionIdentity: () => useSessionIdentity
|
|
28
29
|
});
|
|
@@ -206,3 +207,47 @@ function useSessionIdentity(options) {
|
|
|
206
207
|
const source = identity ?? NO_IDENTITY;
|
|
207
208
|
return (0, import_react.useSyncExternalStore)(source.subscribe, source.snapshot, () => SERVER_SESSION_IDENTITY);
|
|
208
209
|
}
|
|
210
|
+
function useActiveOrganizationView(options) {
|
|
211
|
+
const latest = (0, import_react.useRef)(options);
|
|
212
|
+
latest.current = options;
|
|
213
|
+
const [active, setActive] = (0, import_react.useState)(null);
|
|
214
|
+
const [organizations, setOrganizations] = (0, import_react.useState)([]);
|
|
215
|
+
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
216
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
217
|
+
const [nonce, setNonce] = (0, import_react.useState)(0);
|
|
218
|
+
const reload = (0, import_react.useRef)(() => setNonce((n) => n + 1)).current;
|
|
219
|
+
(0, import_react.useEffect)(() => {
|
|
220
|
+
let cancelled = false;
|
|
221
|
+
if (!options.signedIn) {
|
|
222
|
+
setActive(null);
|
|
223
|
+
setOrganizations([]);
|
|
224
|
+
setLoading(false);
|
|
225
|
+
setError(null);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
setLoading(true);
|
|
229
|
+
void (async () => {
|
|
230
|
+
try {
|
|
231
|
+
const [resolved, all] = await Promise.all([latest.current.resolve(), latest.current.list()]);
|
|
232
|
+
if (cancelled) return;
|
|
233
|
+
setActive(resolved);
|
|
234
|
+
setOrganizations(all);
|
|
235
|
+
setError(null);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
if (cancelled) return;
|
|
238
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
239
|
+
} finally {
|
|
240
|
+
if (!cancelled) setLoading(false);
|
|
241
|
+
}
|
|
242
|
+
})();
|
|
243
|
+
return () => {
|
|
244
|
+
cancelled = true;
|
|
245
|
+
};
|
|
246
|
+
}, [options.signedIn, nonce, ...options.deps ?? []]);
|
|
247
|
+
const subscribe = options.subscribe;
|
|
248
|
+
(0, import_react.useEffect)(() => subscribe ? subscribe(reload) : void 0, [subscribe, reload]);
|
|
249
|
+
const choose = (0, import_react.useRef)(async (organizationId) => {
|
|
250
|
+
setActive(await latest.current.choose(organizationId));
|
|
251
|
+
}).current;
|
|
252
|
+
return { active, organizations, loading, error, mustChoose: options.signedIn && !loading && !error && !active, choose, reload };
|
|
253
|
+
}
|
package/dist/react.d.cts
CHANGED
|
@@ -180,5 +180,36 @@ declare function useActiveOrganization(store: ActiveOrganization): ActiveOrganiz
|
|
|
180
180
|
* the first session read answers; StrictMode-safe (the store is made and disposed in an effect).
|
|
181
181
|
*/
|
|
182
182
|
declare function useSessionIdentity<Id extends string = string>(options: SessionIdentityOptions<Id>): SessionIdentitySnapshot<Id>;
|
|
183
|
+
interface ActiveOrganizationViewOptions<M> {
|
|
184
|
+
/** Nobody signed in: the view is empty and loads nothing. */
|
|
185
|
+
readonly signedIn: boolean;
|
|
186
|
+
/** The organization every request carries now, verified against live membership. Null = must choose. */
|
|
187
|
+
readonly resolve: () => Promise<M | null>;
|
|
188
|
+
/** The organizations to list (the host picks the archive filter). */
|
|
189
|
+
readonly list: () => Promise<readonly M[]>;
|
|
190
|
+
/** Make `organizationId` the active one; answers the organization now active. */
|
|
191
|
+
readonly choose: (organizationId: string) => Promise<M | null>;
|
|
192
|
+
/** Anything that changes what `resolve`/`list` answer (the archive filter, the person). Re-reads when it changes. */
|
|
193
|
+
readonly deps?: readonly unknown[];
|
|
194
|
+
/** Another context moved the active organization: call `reload` when it fires. Returns the unsubscribe. */
|
|
195
|
+
readonly subscribe?: (reload: () => void) => () => void;
|
|
196
|
+
}
|
|
197
|
+
interface ActiveOrganizationView<M> {
|
|
198
|
+
readonly active: M | null;
|
|
199
|
+
readonly organizations: readonly M[];
|
|
200
|
+
readonly loading: boolean;
|
|
201
|
+
/** Non-null when the organizations could not be READ — never presented as "no organization". */
|
|
202
|
+
readonly error: string | null;
|
|
203
|
+
/** True only when signed in, the read succeeded, and no organization is active. */
|
|
204
|
+
readonly mustChoose: boolean;
|
|
205
|
+
readonly choose: (organizationId: string) => Promise<void>;
|
|
206
|
+
readonly reload: () => void;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The state machine every picker/header needs around the active organization: load, error vs none,
|
|
210
|
+
* must-choose, choose, reload. The host injects its reads (so a UI and a request kernel that resolve
|
|
211
|
+
* through the same function can never disagree); this owns the cancellation and the honest states.
|
|
212
|
+
*/
|
|
213
|
+
declare function useActiveOrganizationView<M>(options: ActiveOrganizationViewOptions<M>): ActiveOrganizationView<M>;
|
|
183
214
|
|
|
184
|
-
export { type SessionState, useActiveOrganization, useSession, useSessionIdentity };
|
|
215
|
+
export { type ActiveOrganizationView, type ActiveOrganizationViewOptions, type SessionState, useActiveOrganization, useActiveOrganizationView, useSession, useSessionIdentity };
|
package/dist/react.d.ts
CHANGED
|
@@ -180,5 +180,36 @@ declare function useActiveOrganization(store: ActiveOrganization): ActiveOrganiz
|
|
|
180
180
|
* the first session read answers; StrictMode-safe (the store is made and disposed in an effect).
|
|
181
181
|
*/
|
|
182
182
|
declare function useSessionIdentity<Id extends string = string>(options: SessionIdentityOptions<Id>): SessionIdentitySnapshot<Id>;
|
|
183
|
+
interface ActiveOrganizationViewOptions<M> {
|
|
184
|
+
/** Nobody signed in: the view is empty and loads nothing. */
|
|
185
|
+
readonly signedIn: boolean;
|
|
186
|
+
/** The organization every request carries now, verified against live membership. Null = must choose. */
|
|
187
|
+
readonly resolve: () => Promise<M | null>;
|
|
188
|
+
/** The organizations to list (the host picks the archive filter). */
|
|
189
|
+
readonly list: () => Promise<readonly M[]>;
|
|
190
|
+
/** Make `organizationId` the active one; answers the organization now active. */
|
|
191
|
+
readonly choose: (organizationId: string) => Promise<M | null>;
|
|
192
|
+
/** Anything that changes what `resolve`/`list` answer (the archive filter, the person). Re-reads when it changes. */
|
|
193
|
+
readonly deps?: readonly unknown[];
|
|
194
|
+
/** Another context moved the active organization: call `reload` when it fires. Returns the unsubscribe. */
|
|
195
|
+
readonly subscribe?: (reload: () => void) => () => void;
|
|
196
|
+
}
|
|
197
|
+
interface ActiveOrganizationView<M> {
|
|
198
|
+
readonly active: M | null;
|
|
199
|
+
readonly organizations: readonly M[];
|
|
200
|
+
readonly loading: boolean;
|
|
201
|
+
/** Non-null when the organizations could not be READ — never presented as "no organization". */
|
|
202
|
+
readonly error: string | null;
|
|
203
|
+
/** True only when signed in, the read succeeded, and no organization is active. */
|
|
204
|
+
readonly mustChoose: boolean;
|
|
205
|
+
readonly choose: (organizationId: string) => Promise<void>;
|
|
206
|
+
readonly reload: () => void;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The state machine every picker/header needs around the active organization: load, error vs none,
|
|
210
|
+
* must-choose, choose, reload. The host injects its reads (so a UI and a request kernel that resolve
|
|
211
|
+
* through the same function can never disagree); this owns the cancellation and the honest states.
|
|
212
|
+
*/
|
|
213
|
+
declare function useActiveOrganizationView<M>(options: ActiveOrganizationViewOptions<M>): ActiveOrganizationView<M>;
|
|
183
214
|
|
|
184
|
-
export { type SessionState, useActiveOrganization, useSession, useSessionIdentity };
|
|
215
|
+
export { type ActiveOrganizationView, type ActiveOrganizationViewOptions, type SessionState, useActiveOrganization, useActiveOrganizationView, useSession, useSessionIdentity };
|
package/dist/react.js
CHANGED
|
@@ -181,8 +181,53 @@ function useSessionIdentity(options) {
|
|
|
181
181
|
const source = identity ?? NO_IDENTITY;
|
|
182
182
|
return useSyncExternalStore(source.subscribe, source.snapshot, () => SERVER_SESSION_IDENTITY);
|
|
183
183
|
}
|
|
184
|
+
function useActiveOrganizationView(options) {
|
|
185
|
+
const latest = useRef(options);
|
|
186
|
+
latest.current = options;
|
|
187
|
+
const [active, setActive] = useState(null);
|
|
188
|
+
const [organizations, setOrganizations] = useState([]);
|
|
189
|
+
const [loading, setLoading] = useState(true);
|
|
190
|
+
const [error, setError] = useState(null);
|
|
191
|
+
const [nonce, setNonce] = useState(0);
|
|
192
|
+
const reload = useRef(() => setNonce((n) => n + 1)).current;
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
let cancelled = false;
|
|
195
|
+
if (!options.signedIn) {
|
|
196
|
+
setActive(null);
|
|
197
|
+
setOrganizations([]);
|
|
198
|
+
setLoading(false);
|
|
199
|
+
setError(null);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
setLoading(true);
|
|
203
|
+
void (async () => {
|
|
204
|
+
try {
|
|
205
|
+
const [resolved, all] = await Promise.all([latest.current.resolve(), latest.current.list()]);
|
|
206
|
+
if (cancelled) return;
|
|
207
|
+
setActive(resolved);
|
|
208
|
+
setOrganizations(all);
|
|
209
|
+
setError(null);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
if (cancelled) return;
|
|
212
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
213
|
+
} finally {
|
|
214
|
+
if (!cancelled) setLoading(false);
|
|
215
|
+
}
|
|
216
|
+
})();
|
|
217
|
+
return () => {
|
|
218
|
+
cancelled = true;
|
|
219
|
+
};
|
|
220
|
+
}, [options.signedIn, nonce, ...options.deps ?? []]);
|
|
221
|
+
const subscribe = options.subscribe;
|
|
222
|
+
useEffect(() => subscribe ? subscribe(reload) : void 0, [subscribe, reload]);
|
|
223
|
+
const choose = useRef(async (organizationId) => {
|
|
224
|
+
setActive(await latest.current.choose(organizationId));
|
|
225
|
+
}).current;
|
|
226
|
+
return { active, organizations, loading, error, mustChoose: options.signedIn && !loading && !error && !active, choose, reload };
|
|
227
|
+
}
|
|
184
228
|
export {
|
|
185
229
|
useActiveOrganization,
|
|
230
|
+
useActiveOrganizationView,
|
|
186
231
|
useSession,
|
|
187
232
|
useSessionIdentity
|
|
188
233
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-matrx/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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": "032b64c11a538c439562215e8dc33fba11bc0050"
|
|
215
215
|
}
|