@ai-matrx/auth 0.0.0 → 0.1.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 +25 -0
- package/LICENSE +21 -0
- package/README.md +31 -1
- package/dist/auth-cookie-DWx6Q3xo.d.cts +168 -0
- package/dist/auth-cookie-DWx6Q3xo.d.ts +168 -0
- package/dist/cookies.cjs +233 -0
- package/dist/cookies.d.cts +109 -0
- package/dist/cookies.d.ts +109 -0
- package/dist/cookies.js +210 -0
- package/dist/credentials.cjs +68 -0
- package/dist/credentials.d.cts +82 -0
- package/dist/credentials.d.ts +82 -0
- package/dist/credentials.js +47 -0
- package/dist/index.cjs +189 -0
- package/dist/index.d.cts +86 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +166 -0
- package/dist/next.cjs +655 -0
- package/dist/next.d.cts +389 -0
- package/dist/next.d.ts +389 -0
- package/dist/next.js +637 -0
- package/dist/node.cjs +61 -0
- package/dist/node.d.cts +33 -0
- package/dist/node.d.ts +33 -0
- package/dist/node.js +36 -0
- package/dist/organizations.cjs +389 -0
- package/dist/organizations.d.cts +226 -0
- package/dist/organizations.d.ts +226 -0
- package/dist/organizations.js +366 -0
- package/dist/react.cjs +206 -0
- package/dist/react.d.cts +163 -0
- package/dist/react.d.ts +163 -0
- package/dist/react.js +186 -0
- package/dist/session-4rdIyV28.d.cts +102 -0
- package/dist/session-4rdIyV28.d.ts +102 -0
- package/dist/session-identity.cjs +125 -0
- package/dist/session-identity.d.cts +100 -0
- package/dist/session-identity.d.ts +100 -0
- package/dist/session-identity.js +104 -0
- package/dist/testing.cjs +67 -0
- package/dist/testing.d.cts +38 -0
- package/dist/testing.d.ts +38 -0
- package/dist/testing.js +46 -0
- package/package.json +186 -6
package/dist/react.d.cts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { SupabaseClient, Session, User } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @ai-matrx/auth — sign-in and session doors (moved from @ai-matrx/data/auth, 2026-10-09).
|
|
5
|
+
*
|
|
6
|
+
* auth owns Supabase sign-in; @ai-matrx/data reads the same client and session for its doors. A package or app asks these doors instead of reaching into
|
|
7
|
+
* `client.auth.*`; the session leaves for `@ai-matrx/api` only as the `user` credential
|
|
8
|
+
* (`sessionCredentials(client)`, the shape api's `CredentialsPort` takes, and the same port
|
|
9
|
+
* `@ai-matrx/data`'s `createDb(...).credentials` returns), so api reads the token and never
|
|
10
|
+
* signs anyone in.
|
|
11
|
+
*
|
|
12
|
+
* const session = await getSession(supabase); // Session | null
|
|
13
|
+
* const person = await getPerson(supabase); // User | null (verified by the server)
|
|
14
|
+
* const stop = onSessionChange(supabase, (event, session) => …);
|
|
15
|
+
* await signInWithPassword(supabase, { email, password }); // throws SignInError
|
|
16
|
+
* const { url } = await signInWithOAuth(supabase, { provider: "google", redirectTo });
|
|
17
|
+
* await signOut(supabase);
|
|
18
|
+
* await updatePerson(supabase, { data: { full_name } });
|
|
19
|
+
*
|
|
20
|
+
* Every refusal throws `SignInError` carrying the auth server's own message, status and code.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Any supabase-js client (browser, server, route, extension) — only its `auth` is used here. */
|
|
24
|
+
type AuthClient = Pick<SupabaseClient<any, any, any>, "auth">;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @ai-matrx/auth/organizations (moved from @ai-matrx/data/organizations, 2026-10-09) — THE active-organization ladder and the account's organization
|
|
28
|
+
* facts, for every Matrx client (the web, Matrx 2, the extension, Studio).
|
|
29
|
+
*
|
|
30
|
+
* 🚨 ONE ACTIVE ORGANIZATION, SET ONCE WHEN THE APP LOADS, NEVER NONE (Arman, 2026-10-07;
|
|
31
|
+
* organizations STATE rules 10–15). The ladder picks what the UI OPENS to; from then on every
|
|
32
|
+
* request carries the one active organization the person can see. Reads never filter by it.
|
|
33
|
+
*
|
|
34
|
+
* THE RUNGS, each kept only if it is a CURRENT membership:
|
|
35
|
+
* 1. HELD — what this tab (web) or this running app (desktop, in memory) already works in;
|
|
36
|
+
* 2. LAST ACTIVE — `users.user_preferences.last_active_organization_id` (follows the person
|
|
37
|
+
* across devices: STATE rule 12);
|
|
38
|
+
* 3. START-UP — `users.user_preferences.startup_organization_id` (the "Start-up organization"
|
|
39
|
+
* setting);
|
|
40
|
+
* 4. FIRST — the oldest active membership.
|
|
41
|
+
* Zero memberships → null. A failed read THROWS (the host shows "could not check" and a retry,
|
|
42
|
+
* never a guess). The ladder's own answer never writes anything (rule 14): only a person's click
|
|
43
|
+
* writes last active.
|
|
44
|
+
*
|
|
45
|
+
* Hosts with extra rungs keep them (the web's `?org=` link rung) and call the pure core here.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/** One organization the person is an active member of, as every picker draws it. */
|
|
49
|
+
interface OrganizationMembership {
|
|
50
|
+
readonly id: string;
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly slug: string | null;
|
|
53
|
+
/** The database-validated 2–3 letters, when the organization has them. */
|
|
54
|
+
readonly abbreviation: string | null;
|
|
55
|
+
readonly logoUrl: string | null;
|
|
56
|
+
/** owner | admin | member (the membership's role). */
|
|
57
|
+
readonly role: string;
|
|
58
|
+
/** The person created it (drawn under the favourites, before the rest). */
|
|
59
|
+
readonly isOwn: boolean;
|
|
60
|
+
/** Classified in `iam.organizations.settings.test_fixture` — never guessed from a name. */
|
|
61
|
+
readonly isTestFixture: boolean;
|
|
62
|
+
/** When the membership began (ISO): the oldest is rung 4. */
|
|
63
|
+
readonly joinedAt: string;
|
|
64
|
+
}
|
|
65
|
+
type OrganizationRung = "held" | "last-active" | "startup" | "first";
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* THE ACTIVE ORGANIZATION — one per tab / running app, set once when the app loads, never none
|
|
69
|
+
* while the person has a membership (Arman, 2026-10-07; organizations STATE rules 10–15).
|
|
70
|
+
*
|
|
71
|
+
* const active = createActiveOrganization({ client: supabase });
|
|
72
|
+
* await active.load(); // the ladder: held → last active → start-up → first
|
|
73
|
+
* const orgId = await active.ensureOrgId(); // HELD until the ladder answers; never a guess
|
|
74
|
+
* await active.switchTo(otherId); // a person's click: saves last active for next load
|
|
75
|
+
*
|
|
76
|
+
* HELD-REQUEST SEMANTICS. A write or server call that needs the organization asks
|
|
77
|
+
* `ensureOrgId()`: while the ladder is still resolving the call WAITS (it is held, never sent
|
|
78
|
+
* without an organization and never sent with a guessed one); once resolved it gets the one
|
|
79
|
+
* active organization; with zero memberships or a failed read it is REFUSED with a named error.
|
|
80
|
+
* Acting on a record passes that record's own organization (`ensureOrgId(record.organization_id)`),
|
|
81
|
+
* which wins and never switches the active one.
|
|
82
|
+
*
|
|
83
|
+
* 🚨 NEVER A LIST FILTER. Reads show everything the person can see across all their
|
|
84
|
+
* organizations; nothing here narrows a read. The active organization is only where new things
|
|
85
|
+
* are saved and which organization a server call runs in.
|
|
86
|
+
*
|
|
87
|
+
* The held id is identity-keyed: a different person signing in on the same device never inherits
|
|
88
|
+
* the last person's organization, and a sign-out resets the store.
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
type ActiveOrganizationStatus = "idle" | "resolving" | "ready" | "none" | "error";
|
|
92
|
+
interface ActiveOrganizationSnapshot {
|
|
93
|
+
readonly status: ActiveOrganizationStatus;
|
|
94
|
+
readonly userId: string | null;
|
|
95
|
+
readonly organizationId: string | null;
|
|
96
|
+
readonly organization: OrganizationMembership | null;
|
|
97
|
+
readonly memberships: readonly OrganizationMembership[];
|
|
98
|
+
/** Which ladder rung answered (or "switched" after a person's click). */
|
|
99
|
+
readonly rung: OrganizationRung | "switched" | null;
|
|
100
|
+
readonly error: string | null;
|
|
101
|
+
}
|
|
102
|
+
interface ActiveOrganization {
|
|
103
|
+
snapshot(): ActiveOrganizationSnapshot;
|
|
104
|
+
subscribe(listener: () => void): () => void;
|
|
105
|
+
/** Run the ladder (again). Resolves with the settled snapshot; never throws. */
|
|
106
|
+
load(): Promise<ActiveOrganizationSnapshot>;
|
|
107
|
+
/** The active organization id now, or null while resolving / with none. Sync (api's getter). */
|
|
108
|
+
current(): string | null;
|
|
109
|
+
/** THE HELD REQUEST: the record's own organization, else the active one once resolved. */
|
|
110
|
+
ensureOrgId(recordOrganizationId?: string | null): Promise<string>;
|
|
111
|
+
/** A person's click: switch, remember for this tab, save as last active for the next load. */
|
|
112
|
+
switchTo(organizationId: string): Promise<void>;
|
|
113
|
+
/** Feed a server refusal in: a lapsed organization re-runs the ladder. Returns the class. */
|
|
114
|
+
handleRefusal(status: number, body: unknown): Promise<"lapsed" | "retry" | null>;
|
|
115
|
+
dispose(): void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface SessionIdentitySnapshot<Id extends string = string> {
|
|
119
|
+
/** `resolving` until the first `getSession()` answers. */
|
|
120
|
+
readonly state: "resolving" | "resolved";
|
|
121
|
+
/** Who the session says is acting, or `null` for nobody. */
|
|
122
|
+
readonly userId: Id | null;
|
|
123
|
+
}
|
|
124
|
+
interface SessionIdentityDiagnostic {
|
|
125
|
+
level: "info" | "warn" | "error";
|
|
126
|
+
message: string;
|
|
127
|
+
remedy?: string | undefined;
|
|
128
|
+
}
|
|
129
|
+
/** The sentences a product says about its own session. */
|
|
130
|
+
interface SessionIdentityWords {
|
|
131
|
+
/** The product's name as the subject of a sentence, e.g. "Messaging". */
|
|
132
|
+
readonly product: string;
|
|
133
|
+
/** Remedy when the client has no auth source. */
|
|
134
|
+
readonly noAuthSourceRemedy: string;
|
|
135
|
+
/** Remedy when the session could not be read. */
|
|
136
|
+
readonly unreadableRemedy: string;
|
|
137
|
+
}
|
|
138
|
+
interface SessionIdentityOptions<Id extends string = string> {
|
|
139
|
+
/** The host's browser Supabase client (anything with `auth.getSession` / `onAuthStateChange`). */
|
|
140
|
+
readonly client: unknown;
|
|
141
|
+
readonly words: SessionIdentityWords;
|
|
142
|
+
/** Brand the raw id (default: the string itself). */
|
|
143
|
+
readonly toUserId?: ((id: string) => Id) | undefined;
|
|
144
|
+
readonly onDiagnostic?: ((event: SessionIdentityDiagnostic) => void) | undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface SessionState {
|
|
148
|
+
readonly status: "resolving" | "signed-in" | "signed-out" | "error";
|
|
149
|
+
readonly session: Session | null;
|
|
150
|
+
readonly person: User | null;
|
|
151
|
+
readonly error: string | null;
|
|
152
|
+
}
|
|
153
|
+
/** The current session and person, live across sign-in, refresh, account switch and sign-out. */
|
|
154
|
+
declare function useSession(client: AuthClient): SessionState;
|
|
155
|
+
/** The active organization store's snapshot (stable reference until it moves). */
|
|
156
|
+
declare function useActiveOrganization(store: ActiveOrganization): ActiveOrganizationSnapshot;
|
|
157
|
+
/**
|
|
158
|
+
* Who is acting, read from the client's session (never handed in as a prop). `resolving` until
|
|
159
|
+
* the first session read answers; StrictMode-safe (the store is made and disposed in an effect).
|
|
160
|
+
*/
|
|
161
|
+
declare function useSessionIdentity<Id extends string = string>(options: SessionIdentityOptions<Id>): SessionIdentitySnapshot<Id>;
|
|
162
|
+
|
|
163
|
+
export { type SessionState, useActiveOrganization, useSession, useSessionIdentity };
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { SupabaseClient, Session, User } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @ai-matrx/auth — sign-in and session doors (moved from @ai-matrx/data/auth, 2026-10-09).
|
|
5
|
+
*
|
|
6
|
+
* auth owns Supabase sign-in; @ai-matrx/data reads the same client and session for its doors. A package or app asks these doors instead of reaching into
|
|
7
|
+
* `client.auth.*`; the session leaves for `@ai-matrx/api` only as the `user` credential
|
|
8
|
+
* (`sessionCredentials(client)`, the shape api's `CredentialsPort` takes, and the same port
|
|
9
|
+
* `@ai-matrx/data`'s `createDb(...).credentials` returns), so api reads the token and never
|
|
10
|
+
* signs anyone in.
|
|
11
|
+
*
|
|
12
|
+
* const session = await getSession(supabase); // Session | null
|
|
13
|
+
* const person = await getPerson(supabase); // User | null (verified by the server)
|
|
14
|
+
* const stop = onSessionChange(supabase, (event, session) => …);
|
|
15
|
+
* await signInWithPassword(supabase, { email, password }); // throws SignInError
|
|
16
|
+
* const { url } = await signInWithOAuth(supabase, { provider: "google", redirectTo });
|
|
17
|
+
* await signOut(supabase);
|
|
18
|
+
* await updatePerson(supabase, { data: { full_name } });
|
|
19
|
+
*
|
|
20
|
+
* Every refusal throws `SignInError` carrying the auth server's own message, status and code.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Any supabase-js client (browser, server, route, extension) — only its `auth` is used here. */
|
|
24
|
+
type AuthClient = Pick<SupabaseClient<any, any, any>, "auth">;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @ai-matrx/auth/organizations (moved from @ai-matrx/data/organizations, 2026-10-09) — THE active-organization ladder and the account's organization
|
|
28
|
+
* facts, for every Matrx client (the web, Matrx 2, the extension, Studio).
|
|
29
|
+
*
|
|
30
|
+
* 🚨 ONE ACTIVE ORGANIZATION, SET ONCE WHEN THE APP LOADS, NEVER NONE (Arman, 2026-10-07;
|
|
31
|
+
* organizations STATE rules 10–15). The ladder picks what the UI OPENS to; from then on every
|
|
32
|
+
* request carries the one active organization the person can see. Reads never filter by it.
|
|
33
|
+
*
|
|
34
|
+
* THE RUNGS, each kept only if it is a CURRENT membership:
|
|
35
|
+
* 1. HELD — what this tab (web) or this running app (desktop, in memory) already works in;
|
|
36
|
+
* 2. LAST ACTIVE — `users.user_preferences.last_active_organization_id` (follows the person
|
|
37
|
+
* across devices: STATE rule 12);
|
|
38
|
+
* 3. START-UP — `users.user_preferences.startup_organization_id` (the "Start-up organization"
|
|
39
|
+
* setting);
|
|
40
|
+
* 4. FIRST — the oldest active membership.
|
|
41
|
+
* Zero memberships → null. A failed read THROWS (the host shows "could not check" and a retry,
|
|
42
|
+
* never a guess). The ladder's own answer never writes anything (rule 14): only a person's click
|
|
43
|
+
* writes last active.
|
|
44
|
+
*
|
|
45
|
+
* Hosts with extra rungs keep them (the web's `?org=` link rung) and call the pure core here.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/** One organization the person is an active member of, as every picker draws it. */
|
|
49
|
+
interface OrganizationMembership {
|
|
50
|
+
readonly id: string;
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly slug: string | null;
|
|
53
|
+
/** The database-validated 2–3 letters, when the organization has them. */
|
|
54
|
+
readonly abbreviation: string | null;
|
|
55
|
+
readonly logoUrl: string | null;
|
|
56
|
+
/** owner | admin | member (the membership's role). */
|
|
57
|
+
readonly role: string;
|
|
58
|
+
/** The person created it (drawn under the favourites, before the rest). */
|
|
59
|
+
readonly isOwn: boolean;
|
|
60
|
+
/** Classified in `iam.organizations.settings.test_fixture` — never guessed from a name. */
|
|
61
|
+
readonly isTestFixture: boolean;
|
|
62
|
+
/** When the membership began (ISO): the oldest is rung 4. */
|
|
63
|
+
readonly joinedAt: string;
|
|
64
|
+
}
|
|
65
|
+
type OrganizationRung = "held" | "last-active" | "startup" | "first";
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* THE ACTIVE ORGANIZATION — one per tab / running app, set once when the app loads, never none
|
|
69
|
+
* while the person has a membership (Arman, 2026-10-07; organizations STATE rules 10–15).
|
|
70
|
+
*
|
|
71
|
+
* const active = createActiveOrganization({ client: supabase });
|
|
72
|
+
* await active.load(); // the ladder: held → last active → start-up → first
|
|
73
|
+
* const orgId = await active.ensureOrgId(); // HELD until the ladder answers; never a guess
|
|
74
|
+
* await active.switchTo(otherId); // a person's click: saves last active for next load
|
|
75
|
+
*
|
|
76
|
+
* HELD-REQUEST SEMANTICS. A write or server call that needs the organization asks
|
|
77
|
+
* `ensureOrgId()`: while the ladder is still resolving the call WAITS (it is held, never sent
|
|
78
|
+
* without an organization and never sent with a guessed one); once resolved it gets the one
|
|
79
|
+
* active organization; with zero memberships or a failed read it is REFUSED with a named error.
|
|
80
|
+
* Acting on a record passes that record's own organization (`ensureOrgId(record.organization_id)`),
|
|
81
|
+
* which wins and never switches the active one.
|
|
82
|
+
*
|
|
83
|
+
* 🚨 NEVER A LIST FILTER. Reads show everything the person can see across all their
|
|
84
|
+
* organizations; nothing here narrows a read. The active organization is only where new things
|
|
85
|
+
* are saved and which organization a server call runs in.
|
|
86
|
+
*
|
|
87
|
+
* The held id is identity-keyed: a different person signing in on the same device never inherits
|
|
88
|
+
* the last person's organization, and a sign-out resets the store.
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
type ActiveOrganizationStatus = "idle" | "resolving" | "ready" | "none" | "error";
|
|
92
|
+
interface ActiveOrganizationSnapshot {
|
|
93
|
+
readonly status: ActiveOrganizationStatus;
|
|
94
|
+
readonly userId: string | null;
|
|
95
|
+
readonly organizationId: string | null;
|
|
96
|
+
readonly organization: OrganizationMembership | null;
|
|
97
|
+
readonly memberships: readonly OrganizationMembership[];
|
|
98
|
+
/** Which ladder rung answered (or "switched" after a person's click). */
|
|
99
|
+
readonly rung: OrganizationRung | "switched" | null;
|
|
100
|
+
readonly error: string | null;
|
|
101
|
+
}
|
|
102
|
+
interface ActiveOrganization {
|
|
103
|
+
snapshot(): ActiveOrganizationSnapshot;
|
|
104
|
+
subscribe(listener: () => void): () => void;
|
|
105
|
+
/** Run the ladder (again). Resolves with the settled snapshot; never throws. */
|
|
106
|
+
load(): Promise<ActiveOrganizationSnapshot>;
|
|
107
|
+
/** The active organization id now, or null while resolving / with none. Sync (api's getter). */
|
|
108
|
+
current(): string | null;
|
|
109
|
+
/** THE HELD REQUEST: the record's own organization, else the active one once resolved. */
|
|
110
|
+
ensureOrgId(recordOrganizationId?: string | null): Promise<string>;
|
|
111
|
+
/** A person's click: switch, remember for this tab, save as last active for the next load. */
|
|
112
|
+
switchTo(organizationId: string): Promise<void>;
|
|
113
|
+
/** Feed a server refusal in: a lapsed organization re-runs the ladder. Returns the class. */
|
|
114
|
+
handleRefusal(status: number, body: unknown): Promise<"lapsed" | "retry" | null>;
|
|
115
|
+
dispose(): void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface SessionIdentitySnapshot<Id extends string = string> {
|
|
119
|
+
/** `resolving` until the first `getSession()` answers. */
|
|
120
|
+
readonly state: "resolving" | "resolved";
|
|
121
|
+
/** Who the session says is acting, or `null` for nobody. */
|
|
122
|
+
readonly userId: Id | null;
|
|
123
|
+
}
|
|
124
|
+
interface SessionIdentityDiagnostic {
|
|
125
|
+
level: "info" | "warn" | "error";
|
|
126
|
+
message: string;
|
|
127
|
+
remedy?: string | undefined;
|
|
128
|
+
}
|
|
129
|
+
/** The sentences a product says about its own session. */
|
|
130
|
+
interface SessionIdentityWords {
|
|
131
|
+
/** The product's name as the subject of a sentence, e.g. "Messaging". */
|
|
132
|
+
readonly product: string;
|
|
133
|
+
/** Remedy when the client has no auth source. */
|
|
134
|
+
readonly noAuthSourceRemedy: string;
|
|
135
|
+
/** Remedy when the session could not be read. */
|
|
136
|
+
readonly unreadableRemedy: string;
|
|
137
|
+
}
|
|
138
|
+
interface SessionIdentityOptions<Id extends string = string> {
|
|
139
|
+
/** The host's browser Supabase client (anything with `auth.getSession` / `onAuthStateChange`). */
|
|
140
|
+
readonly client: unknown;
|
|
141
|
+
readonly words: SessionIdentityWords;
|
|
142
|
+
/** Brand the raw id (default: the string itself). */
|
|
143
|
+
readonly toUserId?: ((id: string) => Id) | undefined;
|
|
144
|
+
readonly onDiagnostic?: ((event: SessionIdentityDiagnostic) => void) | undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface SessionState {
|
|
148
|
+
readonly status: "resolving" | "signed-in" | "signed-out" | "error";
|
|
149
|
+
readonly session: Session | null;
|
|
150
|
+
readonly person: User | null;
|
|
151
|
+
readonly error: string | null;
|
|
152
|
+
}
|
|
153
|
+
/** The current session and person, live across sign-in, refresh, account switch and sign-out. */
|
|
154
|
+
declare function useSession(client: AuthClient): SessionState;
|
|
155
|
+
/** The active organization store's snapshot (stable reference until it moves). */
|
|
156
|
+
declare function useActiveOrganization(store: ActiveOrganization): ActiveOrganizationSnapshot;
|
|
157
|
+
/**
|
|
158
|
+
* Who is acting, read from the client's session (never handed in as a prop). `resolving` until
|
|
159
|
+
* the first session read answers; StrictMode-safe (the store is made and disposed in an effect).
|
|
160
|
+
*/
|
|
161
|
+
declare function useSessionIdentity<Id extends string = string>(options: SessionIdentityOptions<Id>): SessionIdentitySnapshot<Id>;
|
|
162
|
+
|
|
163
|
+
export { type SessionState, useActiveOrganization, useSession, useSessionIdentity };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
// src/react.ts
|
|
5
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
6
|
+
|
|
7
|
+
// src/session.ts
|
|
8
|
+
var SignInError = class extends Error {
|
|
9
|
+
constructor(door, message, status, code) {
|
|
10
|
+
super(`${door}: ${message}`);
|
|
11
|
+
this.door = door;
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.name = "SignInError";
|
|
15
|
+
}
|
|
16
|
+
door;
|
|
17
|
+
status;
|
|
18
|
+
code;
|
|
19
|
+
};
|
|
20
|
+
function check(door, data, error) {
|
|
21
|
+
if (error) throw new SignInError(door, error.message, error.status, error.code);
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
24
|
+
async function getSession(client) {
|
|
25
|
+
const { data, error } = await client.auth.getSession();
|
|
26
|
+
return check("getSession", data, error).session;
|
|
27
|
+
}
|
|
28
|
+
function onSessionChange(client, cb) {
|
|
29
|
+
const { data } = client.auth.onAuthStateChange(cb);
|
|
30
|
+
return () => data.subscription.unsubscribe();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/session-identity.ts
|
|
34
|
+
var RESOLVING = Object.freeze({
|
|
35
|
+
state: "resolving",
|
|
36
|
+
userId: null
|
|
37
|
+
});
|
|
38
|
+
var SERVER_SESSION_IDENTITY = RESOLVING;
|
|
39
|
+
function authOf(client) {
|
|
40
|
+
if (client === null || typeof client !== "object" && typeof client !== "function") return null;
|
|
41
|
+
const candidate = client.auth;
|
|
42
|
+
if (candidate === null || typeof candidate !== "object") return null;
|
|
43
|
+
const auth = candidate;
|
|
44
|
+
if (typeof auth.getSession !== "function") return null;
|
|
45
|
+
if (typeof auth.onAuthStateChange !== "function") return null;
|
|
46
|
+
return auth;
|
|
47
|
+
}
|
|
48
|
+
function sessionUserId(session) {
|
|
49
|
+
const id = session?.user?.id;
|
|
50
|
+
return typeof id === "string" && id.length > 0 ? id : null;
|
|
51
|
+
}
|
|
52
|
+
function noAuthSourceMessage(words) {
|
|
53
|
+
return {
|
|
54
|
+
message: `${words.product} cannot tell who is signed in: the Supabase client it was given has no auth session source.`,
|
|
55
|
+
remedy: words.noAuthSourceRemedy
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function unreadableSessionMessage(words, error) {
|
|
59
|
+
return `${words.product} could not read the signed-in session: ${String(
|
|
60
|
+
error?.message ?? error
|
|
61
|
+
)}`;
|
|
62
|
+
}
|
|
63
|
+
function createSessionIdentity(options) {
|
|
64
|
+
const { client, onDiagnostic, words } = options;
|
|
65
|
+
const brand = options.toUserId ?? ((id) => id);
|
|
66
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
67
|
+
let current = RESOLVING;
|
|
68
|
+
let disposed = false;
|
|
69
|
+
const userIdOf = (session) => {
|
|
70
|
+
const id = sessionUserId(session);
|
|
71
|
+
return id === null ? null : brand(id);
|
|
72
|
+
};
|
|
73
|
+
const publish = (state, userId) => {
|
|
74
|
+
if (disposed) return;
|
|
75
|
+
if (current.state === state && current.userId === userId) return;
|
|
76
|
+
current = Object.freeze({ state, userId });
|
|
77
|
+
for (const listener of [...listeners]) listener();
|
|
78
|
+
};
|
|
79
|
+
const subscribeOnly = (hasAuthSource) => ({
|
|
80
|
+
hasAuthSource,
|
|
81
|
+
snapshot: () => current,
|
|
82
|
+
subscribe: (listener) => {
|
|
83
|
+
listeners.add(listener);
|
|
84
|
+
return () => {
|
|
85
|
+
listeners.delete(listener);
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
dispose: () => {
|
|
89
|
+
disposed = true;
|
|
90
|
+
listeners.clear();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
const auth = authOf(client);
|
|
94
|
+
if (auth === null) {
|
|
95
|
+
onDiagnostic?.({ level: "error", ...noAuthSourceMessage(words) });
|
|
96
|
+
current = Object.freeze({ state: "resolved", userId: null });
|
|
97
|
+
return subscribeOnly(false);
|
|
98
|
+
}
|
|
99
|
+
void (async () => {
|
|
100
|
+
try {
|
|
101
|
+
const result = await auth.getSession();
|
|
102
|
+
publish("resolved", userIdOf(result?.data?.session));
|
|
103
|
+
} catch (error) {
|
|
104
|
+
onDiagnostic?.({
|
|
105
|
+
level: "warn",
|
|
106
|
+
message: unreadableSessionMessage(words, error),
|
|
107
|
+
remedy: words.unreadableRemedy
|
|
108
|
+
});
|
|
109
|
+
publish("resolved", null);
|
|
110
|
+
}
|
|
111
|
+
})();
|
|
112
|
+
const subscription = auth.onAuthStateChange((_event, session) => {
|
|
113
|
+
publish("resolved", userIdOf(session));
|
|
114
|
+
});
|
|
115
|
+
const base = subscribeOnly(true);
|
|
116
|
+
return {
|
|
117
|
+
hasAuthSource: true,
|
|
118
|
+
snapshot: base.snapshot,
|
|
119
|
+
subscribe: base.subscribe,
|
|
120
|
+
dispose: () => {
|
|
121
|
+
base.dispose();
|
|
122
|
+
try {
|
|
123
|
+
subscription.data.subscription.unsubscribe();
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/react.ts
|
|
131
|
+
var RESOLVING2 = Object.freeze({ status: "resolving", session: null, person: null, error: null });
|
|
132
|
+
function useSession(client) {
|
|
133
|
+
const [state, setState] = useState(RESOLVING2);
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
let alive = true;
|
|
136
|
+
const apply = (session) => {
|
|
137
|
+
if (!alive) return;
|
|
138
|
+
setState(
|
|
139
|
+
session ? { status: "signed-in", session, person: session.user, error: null } : { status: "signed-out", session: null, person: null, error: null }
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
getSession(client).then(apply, (e) => {
|
|
143
|
+
if (alive) setState({ status: "error", session: null, person: null, error: e?.message ?? String(e) });
|
|
144
|
+
});
|
|
145
|
+
const stop = onSessionChange(client, (_event, session) => apply(session));
|
|
146
|
+
return () => {
|
|
147
|
+
alive = false;
|
|
148
|
+
stop();
|
|
149
|
+
};
|
|
150
|
+
}, [client]);
|
|
151
|
+
return state;
|
|
152
|
+
}
|
|
153
|
+
var SERVER_ACTIVE = Object.freeze({
|
|
154
|
+
status: "idle",
|
|
155
|
+
userId: null,
|
|
156
|
+
organizationId: null,
|
|
157
|
+
organization: null,
|
|
158
|
+
memberships: Object.freeze([]),
|
|
159
|
+
rung: null,
|
|
160
|
+
error: null
|
|
161
|
+
});
|
|
162
|
+
function useActiveOrganization(store) {
|
|
163
|
+
return useSyncExternalStore(store.subscribe, store.snapshot, () => SERVER_ACTIVE);
|
|
164
|
+
}
|
|
165
|
+
var NO_IDENTITY = {
|
|
166
|
+
subscribe: () => () => {
|
|
167
|
+
},
|
|
168
|
+
snapshot: () => SERVER_SESSION_IDENTITY
|
|
169
|
+
};
|
|
170
|
+
function useSessionIdentity(options) {
|
|
171
|
+
const latest = useRef(options);
|
|
172
|
+
latest.current = options;
|
|
173
|
+
const [identity, setIdentity] = useState(null);
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
const made = createSessionIdentity(latest.current);
|
|
176
|
+
setIdentity(made);
|
|
177
|
+
return () => made.dispose();
|
|
178
|
+
}, [options.client]);
|
|
179
|
+
const source = identity ?? NO_IDENTITY;
|
|
180
|
+
return useSyncExternalStore(source.subscribe, source.snapshot, () => SERVER_SESSION_IDENTITY);
|
|
181
|
+
}
|
|
182
|
+
export {
|
|
183
|
+
useActiveOrganization,
|
|
184
|
+
useSession,
|
|
185
|
+
useSessionIdentity
|
|
186
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { SupabaseClient, User, Session, AuthChangeEvent, Provider, UserAttributes } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @ai-matrx/auth — sign-in and session doors (moved from @ai-matrx/data/auth, 2026-10-09).
|
|
5
|
+
*
|
|
6
|
+
* auth owns Supabase sign-in; @ai-matrx/data reads the same client and session for its doors. A package or app asks these doors instead of reaching into
|
|
7
|
+
* `client.auth.*`; the session leaves for `@ai-matrx/api` only as the `user` credential
|
|
8
|
+
* (`sessionCredentials(client)`, the shape api's `CredentialsPort` takes, and the same port
|
|
9
|
+
* `@ai-matrx/data`'s `createDb(...).credentials` returns), so api reads the token and never
|
|
10
|
+
* signs anyone in.
|
|
11
|
+
*
|
|
12
|
+
* const session = await getSession(supabase); // Session | null
|
|
13
|
+
* const person = await getPerson(supabase); // User | null (verified by the server)
|
|
14
|
+
* const stop = onSessionChange(supabase, (event, session) => …);
|
|
15
|
+
* await signInWithPassword(supabase, { email, password }); // throws SignInError
|
|
16
|
+
* const { url } = await signInWithOAuth(supabase, { provider: "google", redirectTo });
|
|
17
|
+
* await signOut(supabase);
|
|
18
|
+
* await updatePerson(supabase, { data: { full_name } });
|
|
19
|
+
*
|
|
20
|
+
* Every refusal throws `SignInError` carrying the auth server's own message, status and code.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Any supabase-js client (browser, server, route, extension) — only its `auth` is used here. */
|
|
24
|
+
type AuthClient = Pick<SupabaseClient<any, any, any>, "auth">;
|
|
25
|
+
/** A refusal from the auth server — its own message, plus status/code when it sent them. */
|
|
26
|
+
declare class SignInError extends Error {
|
|
27
|
+
readonly door: string;
|
|
28
|
+
readonly status?: number | undefined;
|
|
29
|
+
readonly code?: string | undefined;
|
|
30
|
+
constructor(door: string, message: string, status?: number | undefined, code?: string | undefined);
|
|
31
|
+
}
|
|
32
|
+
/** The stored session (no network unless a refresh is due), or null when signed out. */
|
|
33
|
+
declare function getSession(client: AuthClient): Promise<Session | null>;
|
|
34
|
+
/** The signed-in person, verified by the auth server, or null when signed out. */
|
|
35
|
+
declare function getPerson(client: AuthClient): Promise<User | null>;
|
|
36
|
+
/** Hear every session change (sign-in, refresh, sign-out). Returns the unsubscribe. */
|
|
37
|
+
declare function onSessionChange(client: AuthClient, cb: (event: AuthChangeEvent, session: Session | null) => void): () => void;
|
|
38
|
+
declare function signInWithPassword(client: AuthClient, credentials: {
|
|
39
|
+
email: string;
|
|
40
|
+
password: string;
|
|
41
|
+
} | {
|
|
42
|
+
phone: string;
|
|
43
|
+
password: string;
|
|
44
|
+
}): Promise<{
|
|
45
|
+
session: Session;
|
|
46
|
+
person: User;
|
|
47
|
+
}>;
|
|
48
|
+
declare function signInWithOAuth(client: AuthClient, options: {
|
|
49
|
+
provider: Provider;
|
|
50
|
+
redirectTo?: string;
|
|
51
|
+
scopes?: string;
|
|
52
|
+
queryParams?: Record<string, string>;
|
|
53
|
+
skipBrowserRedirect?: boolean;
|
|
54
|
+
}): Promise<{
|
|
55
|
+
provider: Provider;
|
|
56
|
+
url: string | null;
|
|
57
|
+
}>;
|
|
58
|
+
declare function signOut(client: AuthClient, scope?: "global" | "local" | "others"): Promise<void>;
|
|
59
|
+
/** Change the signed-in person's email, phone, password or metadata. */
|
|
60
|
+
declare function updatePerson(client: AuthClient, attributes: UserAttributes): Promise<User>;
|
|
61
|
+
/** The slice of a client the session credential reads: its `auth`, when it has one. */
|
|
62
|
+
interface SessionSourceLike {
|
|
63
|
+
auth?: {
|
|
64
|
+
getSession(): PromiseLike<{
|
|
65
|
+
data: {
|
|
66
|
+
session: {
|
|
67
|
+
access_token: string;
|
|
68
|
+
} | null;
|
|
69
|
+
};
|
|
70
|
+
}>;
|
|
71
|
+
onAuthStateChange(cb: (event: string, session: unknown) => void): {
|
|
72
|
+
data: {
|
|
73
|
+
subscription: {
|
|
74
|
+
unsubscribe(): void;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
} | undefined;
|
|
79
|
+
}
|
|
80
|
+
/** The `user` credential port (structural; the shape `@ai-matrx/api`'s `CredentialsPort` takes). */
|
|
81
|
+
interface UserCredentialsPort {
|
|
82
|
+
get(): Promise<{
|
|
83
|
+
kind: "user";
|
|
84
|
+
accessToken: string;
|
|
85
|
+
} | null>;
|
|
86
|
+
onChange?(cb: () => void): () => void;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* THE ONE session → credential mapping: the current access token as api's `user` credential,
|
|
90
|
+
* re-read on every call (a refresh rotates it), with `onChange` on every session change.
|
|
91
|
+
* A client with no `auth` is anonymous (null), never an error.
|
|
92
|
+
*/
|
|
93
|
+
declare function sessionCredentials(client: SessionSourceLike): UserCredentialsPort;
|
|
94
|
+
/** The session as `@ai-matrx/api`'s `user` credential — the name `@ai-matrx/data/auth` shipped. */
|
|
95
|
+
declare const userCredentials: typeof sessionCredentials;
|
|
96
|
+
/**
|
|
97
|
+
* Refresh the session now (a long-lived script, a desktop app waking from sleep, a tab that was
|
|
98
|
+
* frozen). Returns the new session; throws `SignInError` when the refresh token is refused.
|
|
99
|
+
*/
|
|
100
|
+
declare function refreshSession(client: AuthClient): Promise<Session>;
|
|
101
|
+
|
|
102
|
+
export { type AuthClient as A, type SessionSourceLike as S, type UserCredentialsPort as U, SignInError as a, getSession as b, signInWithOAuth as c, signInWithPassword as d, signOut as e, userCredentials as f, getPerson as g, onSessionChange as o, refreshSession as r, sessionCredentials as s, updatePerson as u };
|