@ai-matrx/auth 0.1.3 → 0.2.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 +32 -0
- package/README.md +11 -0
- package/dist/claims-DXdbe3cW.d.cts +138 -0
- package/dist/claims-DXdbe3cW.d.ts +138 -0
- package/dist/index.cjs +175 -4
- package/dist/index.d.cts +5 -52
- package/dist/index.d.ts +5 -52
- package/dist/index.js +178 -4
- package/dist/matrx-oauth.cjs +458 -0
- package/dist/matrx-oauth.d.cts +181 -0
- package/dist/matrx-oauth.d.ts +181 -0
- package/dist/matrx-oauth.js +437 -0
- package/dist/next.cjs +133 -1
- package/dist/next.d.cts +77 -1
- package/dist/next.d.ts +77 -1
- package/dist/next.js +136 -1
- package/dist/node.cjs +139 -2
- package/dist/node.d.cts +117 -1
- package/dist/node.d.ts +117 -1
- package/dist/node.js +136 -1
- package/dist/organizations.cjs +132 -3
- package/dist/organizations.d.cts +108 -2
- package/dist/organizations.d.ts +108 -2
- package/dist/organizations.js +132 -3
- package/dist/{session-DhRO4euL.d.cts → session-DdQiStp6.d.cts} +12 -2
- package/dist/{session-DhRO4euL.d.ts → session-DdQiStp6.d.ts} +12 -2
- package/dist/storage-Dqnye4KF.d.cts +51 -0
- package/dist/storage-Dqnye4KF.d.ts +51 -0
- package/package.json +12 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog — @ai-matrx/auth
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
**The apps' own identity, server-door and sign-in copies move in (consolidation U1).**
|
|
6
|
+
- `.` — `getClaimsUser` (local JWKS claims read, never `getUser()`), `pinnedSigningKeysFor` + the live
|
|
7
|
+
project's public signing key, `isAuthTransportFailure`, `isSignedOutError`, `userFromClaims`,
|
|
8
|
+
`accountFlagsFor`, the unverifiable-session detectors (`isUnverifiableTokenRefusal`,
|
|
9
|
+
`isUnverifiableTokenError`, `confirmUnverifiableSession`) and `createClaimsCache` (the browser's
|
|
10
|
+
de-duplicated claims read). From the website's `utils/supabase/{claimsUser,projectSigningKeys,
|
|
11
|
+
clientClaimsCache,unverifiableSession}.ts`; media carried an identical copy.
|
|
12
|
+
- `/next` and `/node` — `createServerAuth` (THE server identity door: claims, one retry on a fresh
|
|
13
|
+
client, React `cache` injected), `verifyRequest` (Bearer-or-cookie for API routes),
|
|
14
|
+
`createServiceClient` (the one server-only secret-key client).
|
|
15
|
+
- `/organizations` — the organization-required refusal: `isOrganizationRequiredError`,
|
|
16
|
+
`OrganizationRequiredServerError`, `organizationRequired`, `readCallerMemberships`,
|
|
17
|
+
`organizationRequiredEnvelope` / `organizationRequiredResponse` (aidream's envelope field for field),
|
|
18
|
+
`extractOrganizationHoldMemberships`.
|
|
19
|
+
- `/matrx-oauth` (new section) — "Sign in with AI Matrx" OAuth 2.1 + PKCE for non-web hosts:
|
|
20
|
+
`createMatrxOAuth(config, ports)` with single-flight refresh, host-injected storage, browser opener,
|
|
21
|
+
lock and refresh-token sealing; PKCE primitives. From the extension's `lib/auth/flow.ts` and the
|
|
22
|
+
desktop's `app/main/auth/{pkce,oauth-client}.ts`.
|
|
23
|
+
Tests: `claims.test.ts` (17), `matrx-oauth.test.ts` (18).
|
|
24
|
+
|
|
25
|
+
## 0.1.4
|
|
26
|
+
|
|
27
|
+
**Fix: `getPerson` no longer calls the auth server on every access check (Wave 0).** The verified
|
|
28
|
+
person is now kept per client until the session changes: concurrent callers share one read, and
|
|
29
|
+
sign-in, sign-out, `updatePerson`, an organization switch (`switchTo`), an active-organization
|
|
30
|
+
reload, and the `SIGNED_IN` / `SIGNED_OUT` / `USER_UPDATED` events drop it. A token refresh keeps
|
|
31
|
+
it; signed-out answers and failures are never cached. New export `invalidatePerson(client)` for a
|
|
32
|
+
change made outside this package. Tests: `session.test.ts` "getPerson asks the auth server once per
|
|
33
|
+
session", "asks again after sign-in, sign-out ...", "a signed-out answer or a failure is never cached".
|
|
34
|
+
|
|
3
35
|
## 0.1.3
|
|
4
36
|
|
|
5
37
|
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
package/README.md
CHANGED
|
@@ -31,3 +31,14 @@ Session storage: browser `browserStorage()`, extension `extensionStorage()`, des
|
|
|
31
31
|
|
|
32
32
|
The active organization is never a list filter: reads show everything the person can see; the
|
|
33
33
|
active organization is only where new things are saved and which organization a server call runs in.
|
|
34
|
+
|
|
35
|
+
## Sections added in 0.2.0 — one entry point each
|
|
36
|
+
|
|
37
|
+
| Need | Import |
|
|
38
|
+
|---|---|
|
|
39
|
+
| Who is calling (local claims; browser de-dup) | `getClaimsUser`, `createClaimsCache` from `@ai-matrx/auth` |
|
|
40
|
+
| Server identity in a Next app | `createServerAuth({ createClient, supabaseUrl, cache })` from `@ai-matrx/auth/next` |
|
|
41
|
+
| API route caller (Bearer or cookie) | `verifyRequest(request, { url, publishableKey, cookieClient })` from `@ai-matrx/auth/node` (or `/next`) |
|
|
42
|
+
| Server-only secret-key client | `createServiceClient({ url, secretKey })` from `@ai-matrx/auth/node` (or `/next`) |
|
|
43
|
+
| "Needs an organization" refusal | `isOrganizationRequiredError`, `organizationRequired`, `organizationRequiredResponse` from `@ai-matrx/auth/organizations` |
|
|
44
|
+
| Sign in with AI Matrx (extension, desktop, CLI) | `createMatrxOAuth(config, { storage, openAuthorization })` from `@ai-matrx/auth/matrx-oauth` |
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { JwtPayload, UserAppMetadata, UserMetadata, JWK, AuthError } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* THE identity read every server and client path shares (moved from the website's
|
|
5
|
+
* `utils/supabase/{claimsUser,projectSigningKeys,unverifiableSession}.ts`, 2026-10-12).
|
|
6
|
+
*
|
|
7
|
+
* Verifies the access token LOCALLY (WebCrypto against the project's JWKS; the live project signs
|
|
8
|
+
* ES256) and never asks the auth server who the caller is. `getClaims()`, NEVER `getUser()`:
|
|
9
|
+
* `getUser()` is one auth-server round trip per call, with auth-js retrying a stalled refresh for
|
|
10
|
+
* up to 30s — on 2026-09-20 and 09-21 that put 504s in front of real people.
|
|
11
|
+
*
|
|
12
|
+
* Framework-free: no `next/*`, no `window` at import time. Server doors (`createServerAuth` in
|
|
13
|
+
* `/next`, `verifyRequest` in `/node`) and the browser's de-duplicated read (`createClaimsCache`)
|
|
14
|
+
* are thin wrappers over `getClaimsUser`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The verified caller, built from the access token's claims. `id` is `sub`; `app_metadata` /
|
|
19
|
+
* `user_metadata` default to `{}` so a reader never null-checks what `getUser()` always handed it.
|
|
20
|
+
*/
|
|
21
|
+
interface ApiClaimsUser extends JwtPayload {
|
|
22
|
+
/** The `sub` claim — the user id. */
|
|
23
|
+
id: string;
|
|
24
|
+
app_metadata: UserAppMetadata;
|
|
25
|
+
user_metadata: UserMetadata;
|
|
26
|
+
}
|
|
27
|
+
/** Any Supabase client — the only thing this door needs is `auth.getClaims`. */
|
|
28
|
+
type ClaimsCapableClient = {
|
|
29
|
+
auth: {
|
|
30
|
+
getClaims(jwt?: string, options?: {
|
|
31
|
+
jwks?: {
|
|
32
|
+
keys: JWK[];
|
|
33
|
+
};
|
|
34
|
+
}): Promise<{
|
|
35
|
+
data: {
|
|
36
|
+
claims?: JwtPayload;
|
|
37
|
+
} | null;
|
|
38
|
+
error: AuthError | null;
|
|
39
|
+
}>;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
interface ClaimsResult {
|
|
43
|
+
data: {
|
|
44
|
+
user: ApiClaimsUser | null;
|
|
45
|
+
};
|
|
46
|
+
error: AuthError | null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* `client.auth.getUser()` without the auth-server round trip, in the SAME envelope, with one
|
|
50
|
+
* deliberate distinction:
|
|
51
|
+
* - CONFIRMED SIGNED OUT (no session, no token) → `{ user: null, error: null }` — a settled fact.
|
|
52
|
+
* - COULD NOT VERIFY (authority/JWKS unreachable, malformed or badly-signed token, a verified
|
|
53
|
+
* token without `sub`) → `error` kept, so a retry branch fires and a screen never paints
|
|
54
|
+
* "signed out" over an outage.
|
|
55
|
+
* Pass `jwt` when the token is in hand (a Bearer header). `options.jwks` are known public keys
|
|
56
|
+
* tried BEFORE any JWKS fetch (`pinnedSigningKeysFor`) — can only remove a network call.
|
|
57
|
+
*/
|
|
58
|
+
declare function getClaimsUser(client: ClaimsCapableClient, jwt?: string, options?: {
|
|
59
|
+
jwks: {
|
|
60
|
+
keys: JWK[];
|
|
61
|
+
};
|
|
62
|
+
}): Promise<ClaimsResult>;
|
|
63
|
+
/**
|
|
64
|
+
* Could we not REACH a verdict (transport failure, spent budget), as opposed to a token we reached
|
|
65
|
+
* a verdict on? Too narrow reads an outage as a logout; too wide holds a person with a forged or
|
|
66
|
+
* rotated-key token on "reload in a moment" forever while the proxy sends them to /login. This is
|
|
67
|
+
* the same line the `/next` middleware pass draws.
|
|
68
|
+
*/
|
|
69
|
+
declare function isAuthTransportFailure(error: unknown): boolean;
|
|
70
|
+
/** Is this `getClaims()` error the settled fact "nobody is signed in"? */
|
|
71
|
+
declare function isSignedOutError(error: AuthError | {
|
|
72
|
+
name?: string;
|
|
73
|
+
code?: string;
|
|
74
|
+
}): boolean;
|
|
75
|
+
/** The verified claims as a `{ id, ... }` user, or `null` when there are none. */
|
|
76
|
+
declare function userFromClaims(claims: JwtPayload | undefined): ApiClaimsUser | null;
|
|
77
|
+
/**
|
|
78
|
+
* GUEST vs ACCOUNT. A signed-in session is a person with an ACCOUNT or a GUEST (anonymous session,
|
|
79
|
+
* `is_anonymous: true`). Account UI (menu, billing, sign-out, org picker) keys on `hasAccount`.
|
|
80
|
+
*/
|
|
81
|
+
declare function accountFlagsFor(claims: {
|
|
82
|
+
is_anonymous?: boolean;
|
|
83
|
+
} | null | undefined): {
|
|
84
|
+
isGuest: boolean;
|
|
85
|
+
hasAccount: boolean;
|
|
86
|
+
};
|
|
87
|
+
/** The auth authorities the keys below were issued by (live, by every name it answers to). */
|
|
88
|
+
declare const PROJECT_SIGNING_KEY_AUTHORITIES: readonly string[];
|
|
89
|
+
declare const PROJECT_SIGNING_KEYS: JWK[];
|
|
90
|
+
/** The pinned keys for the authority at `supabaseUrl`; none for any other authority. */
|
|
91
|
+
declare function pinnedSigningKeysFor(supabaseUrl: string | null | undefined): JWK[];
|
|
92
|
+
interface AuthErrorLike {
|
|
93
|
+
name?: string | undefined;
|
|
94
|
+
code?: string | undefined;
|
|
95
|
+
status?: number | undefined;
|
|
96
|
+
message?: string | undefined;
|
|
97
|
+
}
|
|
98
|
+
/** Is this PostgREST result the database saying "I cannot verify this token"? (Not expiry.) */
|
|
99
|
+
declare function isUnverifiableTokenRefusal(result: {
|
|
100
|
+
error?: {
|
|
101
|
+
code?: string;
|
|
102
|
+
message?: string;
|
|
103
|
+
} | null;
|
|
104
|
+
status?: number;
|
|
105
|
+
}): boolean;
|
|
106
|
+
/** Is this `getClaims()` error the authority's SETTLED verdict that the token is not its own? */
|
|
107
|
+
declare function isUnverifiableTokenError(error: AuthErrorLike | null | undefined): boolean;
|
|
108
|
+
/** Ask the authority once: is the stored session's token one it can verify? */
|
|
109
|
+
declare function confirmUnverifiableSession(auth: {
|
|
110
|
+
getClaims(jwt?: string, options?: {
|
|
111
|
+
jwks?: {
|
|
112
|
+
keys: JWK[];
|
|
113
|
+
};
|
|
114
|
+
}): Promise<{
|
|
115
|
+
data: unknown;
|
|
116
|
+
error: AuthErrorLike | null;
|
|
117
|
+
}>;
|
|
118
|
+
}, supabaseUrl: string | null | undefined): Promise<boolean>;
|
|
119
|
+
interface ClaimsCacheOptions {
|
|
120
|
+
/** The URL the client talks to — decides which pinned keys apply. */
|
|
121
|
+
supabaseUrl: string | null | undefined;
|
|
122
|
+
/** How long one verdict is shared (default 2s; any identity-changing event clears it at once). */
|
|
123
|
+
ttlMs?: number;
|
|
124
|
+
/** Called when the authority settled that the browser's token is not one of its own. */
|
|
125
|
+
onUnverifiable?: () => void;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The current browser session's verified claims, de-duplicated: callers within `ttlMs` share one
|
|
129
|
+
* verification (and so one JWKS fetch on a cold cache). Browser-only: a server request must never
|
|
130
|
+
* share another request's verdict — servers call `getClaimsUser` / `createServerAuth`.
|
|
131
|
+
*/
|
|
132
|
+
declare function createClaimsCache(client: ClaimsCapableClient & {
|
|
133
|
+
auth: {
|
|
134
|
+
onAuthStateChange(cb: (event: string) => void): unknown;
|
|
135
|
+
};
|
|
136
|
+
}, options: ClaimsCacheOptions): () => Promise<ClaimsResult>;
|
|
137
|
+
|
|
138
|
+
export { type ApiClaimsUser as A, type ClaimsCacheOptions as C, PROJECT_SIGNING_KEYS as P, type ClaimsCapableClient as a, type ClaimsResult as b, PROJECT_SIGNING_KEY_AUTHORITIES as c, accountFlagsFor as d, confirmUnverifiableSession as e, createClaimsCache as f, getClaimsUser as g, isSignedOutError as h, isAuthTransportFailure as i, isUnverifiableTokenError as j, isUnverifiableTokenRefusal as k, pinnedSigningKeysFor as p, userFromClaims as u };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { JwtPayload, UserAppMetadata, UserMetadata, JWK, AuthError } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* THE identity read every server and client path shares (moved from the website's
|
|
5
|
+
* `utils/supabase/{claimsUser,projectSigningKeys,unverifiableSession}.ts`, 2026-10-12).
|
|
6
|
+
*
|
|
7
|
+
* Verifies the access token LOCALLY (WebCrypto against the project's JWKS; the live project signs
|
|
8
|
+
* ES256) and never asks the auth server who the caller is. `getClaims()`, NEVER `getUser()`:
|
|
9
|
+
* `getUser()` is one auth-server round trip per call, with auth-js retrying a stalled refresh for
|
|
10
|
+
* up to 30s — on 2026-09-20 and 09-21 that put 504s in front of real people.
|
|
11
|
+
*
|
|
12
|
+
* Framework-free: no `next/*`, no `window` at import time. Server doors (`createServerAuth` in
|
|
13
|
+
* `/next`, `verifyRequest` in `/node`) and the browser's de-duplicated read (`createClaimsCache`)
|
|
14
|
+
* are thin wrappers over `getClaimsUser`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The verified caller, built from the access token's claims. `id` is `sub`; `app_metadata` /
|
|
19
|
+
* `user_metadata` default to `{}` so a reader never null-checks what `getUser()` always handed it.
|
|
20
|
+
*/
|
|
21
|
+
interface ApiClaimsUser extends JwtPayload {
|
|
22
|
+
/** The `sub` claim — the user id. */
|
|
23
|
+
id: string;
|
|
24
|
+
app_metadata: UserAppMetadata;
|
|
25
|
+
user_metadata: UserMetadata;
|
|
26
|
+
}
|
|
27
|
+
/** Any Supabase client — the only thing this door needs is `auth.getClaims`. */
|
|
28
|
+
type ClaimsCapableClient = {
|
|
29
|
+
auth: {
|
|
30
|
+
getClaims(jwt?: string, options?: {
|
|
31
|
+
jwks?: {
|
|
32
|
+
keys: JWK[];
|
|
33
|
+
};
|
|
34
|
+
}): Promise<{
|
|
35
|
+
data: {
|
|
36
|
+
claims?: JwtPayload;
|
|
37
|
+
} | null;
|
|
38
|
+
error: AuthError | null;
|
|
39
|
+
}>;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
interface ClaimsResult {
|
|
43
|
+
data: {
|
|
44
|
+
user: ApiClaimsUser | null;
|
|
45
|
+
};
|
|
46
|
+
error: AuthError | null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* `client.auth.getUser()` without the auth-server round trip, in the SAME envelope, with one
|
|
50
|
+
* deliberate distinction:
|
|
51
|
+
* - CONFIRMED SIGNED OUT (no session, no token) → `{ user: null, error: null }` — a settled fact.
|
|
52
|
+
* - COULD NOT VERIFY (authority/JWKS unreachable, malformed or badly-signed token, a verified
|
|
53
|
+
* token without `sub`) → `error` kept, so a retry branch fires and a screen never paints
|
|
54
|
+
* "signed out" over an outage.
|
|
55
|
+
* Pass `jwt` when the token is in hand (a Bearer header). `options.jwks` are known public keys
|
|
56
|
+
* tried BEFORE any JWKS fetch (`pinnedSigningKeysFor`) — can only remove a network call.
|
|
57
|
+
*/
|
|
58
|
+
declare function getClaimsUser(client: ClaimsCapableClient, jwt?: string, options?: {
|
|
59
|
+
jwks: {
|
|
60
|
+
keys: JWK[];
|
|
61
|
+
};
|
|
62
|
+
}): Promise<ClaimsResult>;
|
|
63
|
+
/**
|
|
64
|
+
* Could we not REACH a verdict (transport failure, spent budget), as opposed to a token we reached
|
|
65
|
+
* a verdict on? Too narrow reads an outage as a logout; too wide holds a person with a forged or
|
|
66
|
+
* rotated-key token on "reload in a moment" forever while the proxy sends them to /login. This is
|
|
67
|
+
* the same line the `/next` middleware pass draws.
|
|
68
|
+
*/
|
|
69
|
+
declare function isAuthTransportFailure(error: unknown): boolean;
|
|
70
|
+
/** Is this `getClaims()` error the settled fact "nobody is signed in"? */
|
|
71
|
+
declare function isSignedOutError(error: AuthError | {
|
|
72
|
+
name?: string;
|
|
73
|
+
code?: string;
|
|
74
|
+
}): boolean;
|
|
75
|
+
/** The verified claims as a `{ id, ... }` user, or `null` when there are none. */
|
|
76
|
+
declare function userFromClaims(claims: JwtPayload | undefined): ApiClaimsUser | null;
|
|
77
|
+
/**
|
|
78
|
+
* GUEST vs ACCOUNT. A signed-in session is a person with an ACCOUNT or a GUEST (anonymous session,
|
|
79
|
+
* `is_anonymous: true`). Account UI (menu, billing, sign-out, org picker) keys on `hasAccount`.
|
|
80
|
+
*/
|
|
81
|
+
declare function accountFlagsFor(claims: {
|
|
82
|
+
is_anonymous?: boolean;
|
|
83
|
+
} | null | undefined): {
|
|
84
|
+
isGuest: boolean;
|
|
85
|
+
hasAccount: boolean;
|
|
86
|
+
};
|
|
87
|
+
/** The auth authorities the keys below were issued by (live, by every name it answers to). */
|
|
88
|
+
declare const PROJECT_SIGNING_KEY_AUTHORITIES: readonly string[];
|
|
89
|
+
declare const PROJECT_SIGNING_KEYS: JWK[];
|
|
90
|
+
/** The pinned keys for the authority at `supabaseUrl`; none for any other authority. */
|
|
91
|
+
declare function pinnedSigningKeysFor(supabaseUrl: string | null | undefined): JWK[];
|
|
92
|
+
interface AuthErrorLike {
|
|
93
|
+
name?: string | undefined;
|
|
94
|
+
code?: string | undefined;
|
|
95
|
+
status?: number | undefined;
|
|
96
|
+
message?: string | undefined;
|
|
97
|
+
}
|
|
98
|
+
/** Is this PostgREST result the database saying "I cannot verify this token"? (Not expiry.) */
|
|
99
|
+
declare function isUnverifiableTokenRefusal(result: {
|
|
100
|
+
error?: {
|
|
101
|
+
code?: string;
|
|
102
|
+
message?: string;
|
|
103
|
+
} | null;
|
|
104
|
+
status?: number;
|
|
105
|
+
}): boolean;
|
|
106
|
+
/** Is this `getClaims()` error the authority's SETTLED verdict that the token is not its own? */
|
|
107
|
+
declare function isUnverifiableTokenError(error: AuthErrorLike | null | undefined): boolean;
|
|
108
|
+
/** Ask the authority once: is the stored session's token one it can verify? */
|
|
109
|
+
declare function confirmUnverifiableSession(auth: {
|
|
110
|
+
getClaims(jwt?: string, options?: {
|
|
111
|
+
jwks?: {
|
|
112
|
+
keys: JWK[];
|
|
113
|
+
};
|
|
114
|
+
}): Promise<{
|
|
115
|
+
data: unknown;
|
|
116
|
+
error: AuthErrorLike | null;
|
|
117
|
+
}>;
|
|
118
|
+
}, supabaseUrl: string | null | undefined): Promise<boolean>;
|
|
119
|
+
interface ClaimsCacheOptions {
|
|
120
|
+
/** The URL the client talks to — decides which pinned keys apply. */
|
|
121
|
+
supabaseUrl: string | null | undefined;
|
|
122
|
+
/** How long one verdict is shared (default 2s; any identity-changing event clears it at once). */
|
|
123
|
+
ttlMs?: number;
|
|
124
|
+
/** Called when the authority settled that the browser's token is not one of its own. */
|
|
125
|
+
onUnverifiable?: () => void;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The current browser session's verified claims, de-duplicated: callers within `ttlMs` share one
|
|
129
|
+
* verification (and so one JWKS fetch on a cold cache). Browser-only: a server request must never
|
|
130
|
+
* share another request's verdict — servers call `getClaimsUser` / `createServerAuth`.
|
|
131
|
+
*/
|
|
132
|
+
declare function createClaimsCache(client: ClaimsCapableClient & {
|
|
133
|
+
auth: {
|
|
134
|
+
onAuthStateChange(cb: (event: string) => void): unknown;
|
|
135
|
+
};
|
|
136
|
+
}, options: ClaimsCacheOptions): () => Promise<ClaimsResult>;
|
|
137
|
+
|
|
138
|
+
export { type ApiClaimsUser as A, type ClaimsCacheOptions as C, PROJECT_SIGNING_KEYS as P, type ClaimsCapableClient as a, type ClaimsResult as b, PROJECT_SIGNING_KEY_AUTHORITIES as c, accountFlagsFor as d, confirmUnverifiableSession as e, createClaimsCache as f, getClaimsUser as g, isSignedOutError as h, isAuthTransportFailure as i, isUnverifiableTokenError as j, isUnverifiableTokenRefusal as k, pinnedSigningKeysFor as p, userFromClaims as u };
|
package/dist/index.cjs
CHANGED
|
@@ -20,24 +20,37 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
|
+
PROJECT_SIGNING_KEYS: () => PROJECT_SIGNING_KEYS,
|
|
24
|
+
PROJECT_SIGNING_KEY_AUTHORITIES: () => PROJECT_SIGNING_KEY_AUTHORITIES,
|
|
23
25
|
SecretKeyRefusedError: () => SecretKeyRefusedError,
|
|
24
26
|
SignInError: () => SignInError,
|
|
25
27
|
StorageUnavailableError: () => StorageUnavailableError,
|
|
28
|
+
accountFlagsFor: () => accountFlagsFor,
|
|
26
29
|
browserStorage: () => browserStorage,
|
|
30
|
+
confirmUnverifiableSession: () => confirmUnverifiableSession,
|
|
27
31
|
createAuthClient: () => createAuthClient,
|
|
32
|
+
createClaimsCache: () => createClaimsCache,
|
|
28
33
|
extensionStorage: () => extensionStorage,
|
|
34
|
+
getClaimsUser: () => getClaimsUser,
|
|
29
35
|
getPerson: () => getPerson,
|
|
30
36
|
getSession: () => getSession,
|
|
37
|
+
invalidatePerson: () => invalidatePerson,
|
|
38
|
+
isAuthTransportFailure: () => isAuthTransportFailure,
|
|
39
|
+
isSignedOutError: () => isSignedOutError,
|
|
40
|
+
isUnverifiableTokenError: () => isUnverifiableTokenError,
|
|
41
|
+
isUnverifiableTokenRefusal: () => isUnverifiableTokenRefusal,
|
|
31
42
|
keyValueStorage: () => keyValueStorage,
|
|
32
43
|
memoryStorage: () => memoryStorage,
|
|
33
44
|
onSessionChange: () => onSessionChange,
|
|
45
|
+
pinnedSigningKeysFor: () => pinnedSigningKeysFor,
|
|
34
46
|
refreshSession: () => refreshSession,
|
|
35
47
|
sessionCredentials: () => sessionCredentials,
|
|
36
48
|
signInWithOAuth: () => signInWithOAuth,
|
|
37
49
|
signInWithPassword: () => signInWithPassword,
|
|
38
50
|
signOut: () => signOut,
|
|
39
51
|
updatePerson: () => updatePerson,
|
|
40
|
-
userCredentials: () => userCredentials
|
|
52
|
+
userCredentials: () => userCredentials,
|
|
53
|
+
userFromClaims: () => userFromClaims
|
|
41
54
|
});
|
|
42
55
|
module.exports = __toCommonJS(src_exports);
|
|
43
56
|
|
|
@@ -62,10 +75,43 @@ async function getSession(client) {
|
|
|
62
75
|
const { data, error } = await client.auth.getSession();
|
|
63
76
|
return check("getSession", data, error).session;
|
|
64
77
|
}
|
|
78
|
+
var personCache = /* @__PURE__ */ new WeakMap();
|
|
79
|
+
function personEntry(client) {
|
|
80
|
+
let entry = personCache.get(client);
|
|
81
|
+
if (!entry) {
|
|
82
|
+
const fresh = { read: null, epoch: 0 };
|
|
83
|
+
entry = fresh;
|
|
84
|
+
personCache.set(client, fresh);
|
|
85
|
+
const subscribe = client.auth?.onAuthStateChange;
|
|
86
|
+
if (typeof subscribe === "function") {
|
|
87
|
+
subscribe.call(client.auth, async (event) => {
|
|
88
|
+
if (event === "SIGNED_IN" || event === "SIGNED_OUT" || event === "USER_UPDATED") invalidatePerson(client);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return entry;
|
|
93
|
+
}
|
|
94
|
+
function invalidatePerson(client) {
|
|
95
|
+
const entry = personCache.get(client);
|
|
96
|
+
if (!entry) return;
|
|
97
|
+
entry.epoch += 1;
|
|
98
|
+
entry.read = null;
|
|
99
|
+
}
|
|
65
100
|
async function getPerson(client) {
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
68
|
-
|
|
101
|
+
const entry = personEntry(client);
|
|
102
|
+
if (entry.read) return entry.read;
|
|
103
|
+
const epoch = entry.epoch;
|
|
104
|
+
const read = (async () => {
|
|
105
|
+
const { data, error } = await client.auth.getUser();
|
|
106
|
+
if (error && (error.name === "AuthSessionMissingError" || error.status === 401)) return null;
|
|
107
|
+
return check("getPerson", data, error).user;
|
|
108
|
+
})();
|
|
109
|
+
entry.read = read;
|
|
110
|
+
const settle = (keep) => {
|
|
111
|
+
if (entry.epoch === epoch && entry.read === read && !keep) entry.read = null;
|
|
112
|
+
};
|
|
113
|
+
read.then((user) => settle(user !== null), () => settle(false));
|
|
114
|
+
return read;
|
|
69
115
|
}
|
|
70
116
|
function onSessionChange(client, cb) {
|
|
71
117
|
const { data } = client.auth.onAuthStateChange(cb);
|
|
@@ -73,6 +119,7 @@ function onSessionChange(client, cb) {
|
|
|
73
119
|
}
|
|
74
120
|
async function signInWithPassword(client, credentials) {
|
|
75
121
|
const { data, error } = await client.auth.signInWithPassword(credentials);
|
|
122
|
+
invalidatePerson(client);
|
|
76
123
|
const ok = check("signInWithPassword", data, error);
|
|
77
124
|
return { session: ok.session, person: ok.user };
|
|
78
125
|
}
|
|
@@ -84,10 +131,12 @@ async function signInWithOAuth(client, options) {
|
|
|
84
131
|
}
|
|
85
132
|
async function signOut(client, scope = "global") {
|
|
86
133
|
const { error } = await client.auth.signOut({ scope });
|
|
134
|
+
invalidatePerson(client);
|
|
87
135
|
check("signOut", null, error);
|
|
88
136
|
}
|
|
89
137
|
async function updatePerson(client, attributes) {
|
|
90
138
|
const { data, error } = await client.auth.updateUser(attributes);
|
|
139
|
+
invalidatePerson(client);
|
|
91
140
|
return check("updatePerson", data, error).user;
|
|
92
141
|
}
|
|
93
142
|
function sessionCredentials(client) {
|
|
@@ -187,3 +236,125 @@ function createAuthClient(options) {
|
|
|
187
236
|
if (options.detectSessionInUrl !== void 0) auth["detectSessionInUrl"] = options.detectSessionInUrl;
|
|
188
237
|
return (0, import_supabase_js.createClient)(options.url, options.key, { auth });
|
|
189
238
|
}
|
|
239
|
+
|
|
240
|
+
// src/claims.ts
|
|
241
|
+
var import_supabase_js2 = require("@supabase/supabase-js");
|
|
242
|
+
async function getClaimsUser(client, jwt, options) {
|
|
243
|
+
const { data, error } = options ? await client.auth.getClaims(jwt, options) : await client.auth.getClaims(jwt);
|
|
244
|
+
if (error) {
|
|
245
|
+
if (isSignedOutError(error)) return { data: { user: null }, error: null };
|
|
246
|
+
return { data: { user: null }, error };
|
|
247
|
+
}
|
|
248
|
+
if (!data?.claims) return { data: { user: null }, error: null };
|
|
249
|
+
const user = userFromClaims(data.claims);
|
|
250
|
+
if (!user) {
|
|
251
|
+
return {
|
|
252
|
+
data: { user: null },
|
|
253
|
+
error: new import_supabase_js2.AuthError("Verified token carries no `sub` claim", 401, "bad_jwt")
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return { data: { user }, error: null };
|
|
257
|
+
}
|
|
258
|
+
var BUDGET_EXHAUSTED_STATUS = 408;
|
|
259
|
+
var BUDGET_EXHAUSTED_CODE = "auth_budget_exhausted";
|
|
260
|
+
function isAuthTransportFailure(error) {
|
|
261
|
+
if (!error || typeof error !== "object") return false;
|
|
262
|
+
const name = String(error.name ?? "");
|
|
263
|
+
if (name === "AuthRetryableFetchError" || name === "TimeoutError" || name === "AbortError") return true;
|
|
264
|
+
const status = error.status;
|
|
265
|
+
if (status === 0 || status === 504 || status === 503) return true;
|
|
266
|
+
return status === BUDGET_EXHAUSTED_STATUS || error.code === BUDGET_EXHAUSTED_CODE;
|
|
267
|
+
}
|
|
268
|
+
function isSignedOutError(error) {
|
|
269
|
+
return error instanceof import_supabase_js2.AuthSessionMissingError || error.name === "AuthSessionMissingError" || error.code === "session_not_found";
|
|
270
|
+
}
|
|
271
|
+
function userFromClaims(claims) {
|
|
272
|
+
if (!claims || typeof claims.sub !== "string") return null;
|
|
273
|
+
return {
|
|
274
|
+
...claims,
|
|
275
|
+
id: claims.sub,
|
|
276
|
+
app_metadata: claims.app_metadata ?? {},
|
|
277
|
+
user_metadata: claims.user_metadata ?? {}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function accountFlagsFor(claims) {
|
|
281
|
+
if (!claims) return { isGuest: false, hasAccount: false };
|
|
282
|
+
const isGuest = claims.is_anonymous === true;
|
|
283
|
+
return { isGuest, hasAccount: !isGuest };
|
|
284
|
+
}
|
|
285
|
+
var PROJECT_SIGNING_KEY_AUTHORITIES = [
|
|
286
|
+
"db.matrxserver.com",
|
|
287
|
+
"brsgrqvjdzwihsvnfqkf.supabase.co"
|
|
288
|
+
];
|
|
289
|
+
var PROJECT_SIGNING_KEYS = [
|
|
290
|
+
{
|
|
291
|
+
alg: "ES256",
|
|
292
|
+
crv: "P-256",
|
|
293
|
+
ext: true,
|
|
294
|
+
key_ops: ["verify"],
|
|
295
|
+
kid: "a00aed0b-202c-483a-91ff-fc74719a3bb2",
|
|
296
|
+
kty: "EC",
|
|
297
|
+
use: "sig",
|
|
298
|
+
x: "guqOsgjJqGVU6g_K0iIXKrzixlkeXKOqkPUQCconp6U",
|
|
299
|
+
y: "39zQtkzlfQuPf_MtIhYmMdxNQpIR2Pkl11AToywaslY"
|
|
300
|
+
}
|
|
301
|
+
];
|
|
302
|
+
function pinnedSigningKeysFor(supabaseUrl) {
|
|
303
|
+
if (!supabaseUrl) return [];
|
|
304
|
+
let host;
|
|
305
|
+
try {
|
|
306
|
+
host = new URL(supabaseUrl).host.toLowerCase();
|
|
307
|
+
} catch {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
return PROJECT_SIGNING_KEY_AUTHORITIES.includes(host) ? PROJECT_SIGNING_KEYS : [];
|
|
311
|
+
}
|
|
312
|
+
function isUnverifiableTokenRefusal(result) {
|
|
313
|
+
if (result.status !== 401) return false;
|
|
314
|
+
if (result.error?.code !== "PGRST301") return false;
|
|
315
|
+
return !/expired/i.test(result.error?.message ?? "");
|
|
316
|
+
}
|
|
317
|
+
function isUnverifiableTokenError(error) {
|
|
318
|
+
if (!error) return false;
|
|
319
|
+
const name = String(error.name ?? "");
|
|
320
|
+
if (name === "AuthRetryableFetchError" || name === "TimeoutError" || name === "AbortError") return false;
|
|
321
|
+
if (name === "AuthSessionMissingError" || error.code === "session_not_found") return false;
|
|
322
|
+
const status = error.status;
|
|
323
|
+
if (status === 0 || status === 408 || status === 503 || status === 504) return false;
|
|
324
|
+
if (/expired/i.test(error.message ?? "")) return false;
|
|
325
|
+
return error.code === "bad_jwt" || name === "AuthInvalidJwtError";
|
|
326
|
+
}
|
|
327
|
+
async function confirmUnverifiableSession(auth, supabaseUrl) {
|
|
328
|
+
try {
|
|
329
|
+
const { error } = await auth.getClaims(void 0, { jwks: { keys: pinnedSigningKeysFor(supabaseUrl) } });
|
|
330
|
+
return isUnverifiableTokenError(error);
|
|
331
|
+
} catch {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function createClaimsCache(client, options) {
|
|
336
|
+
const ttl = options.ttlMs ?? 2e3;
|
|
337
|
+
let cached = null;
|
|
338
|
+
let inflight = null;
|
|
339
|
+
let listening = false;
|
|
340
|
+
return () => {
|
|
341
|
+
if (!listening) {
|
|
342
|
+
listening = true;
|
|
343
|
+
client.auth.onAuthStateChange((event) => {
|
|
344
|
+
if (event === "SIGNED_OUT" || event === "SIGNED_IN" || event === "TOKEN_REFRESHED" || event === "USER_UPDATED") {
|
|
345
|
+
cached = null;
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (cached && cached.expiresAt > Date.now()) return Promise.resolve(cached.value);
|
|
350
|
+
if (inflight) return inflight;
|
|
351
|
+
inflight = getClaimsUser(client, void 0, { jwks: { keys: pinnedSigningKeysFor(options.supabaseUrl) } }).then((result) => {
|
|
352
|
+
if (isUnverifiableTokenError(result.error)) options.onUnverifiable?.();
|
|
353
|
+
cached = { value: result, expiresAt: Date.now() + ttl };
|
|
354
|
+
return result;
|
|
355
|
+
}).finally(() => {
|
|
356
|
+
inflight = null;
|
|
357
|
+
});
|
|
358
|
+
return inflight;
|
|
359
|
+
};
|
|
360
|
+
}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,57 +1,10 @@
|
|
|
1
|
-
export { A as AuthClient, S as SessionSourceLike, a as SignInError, U as UserCredentialsPort, g as getPerson, b as getSession, 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 './session-
|
|
1
|
+
export { A as AuthClient, 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 './session-DdQiStp6.cjs';
|
|
2
|
+
import { S as SessionStorage } from './storage-Dqnye4KF.cjs';
|
|
3
|
+
export { C as ChromeStorageAreaLike, a as StorageUnavailableError, b as browserStorage, e as extensionStorage, k as keyValueStorage, m as memoryStorage } from './storage-Dqnye4KF.cjs';
|
|
2
4
|
import { SupabaseClient } from '@supabase/supabase-js';
|
|
3
5
|
export { AuthChangeEvent, Session, User, UserAttributes } from '@supabase/supabase-js';
|
|
4
6
|
export { CredentialsPort, MatrxCredential, MatrxCredentialKind } from './credentials.cjs';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Session storage per platform — where a Supabase session lives between runs.
|
|
8
|
-
*
|
|
9
|
-
* supabase-js takes any `{ getItem, setItem, removeItem }` (sync or async). These are the ones
|
|
10
|
-
* every Matrx client needs, written once:
|
|
11
|
-
*
|
|
12
|
-
* browser — `browserStorage("local")` (supabase-js's own default; `"session"` = this tab only)
|
|
13
|
-
* Next.js — the cookie-backed clients in `@ai-matrx/auth/next` (the domain-wide auth cookie)
|
|
14
|
-
* extension — `extensionStorage()` over `chrome.storage.local` (shared by popup, side panel and
|
|
15
|
-
* service worker; `localStorage` does not exist in a service worker)
|
|
16
|
-
* desktop — `keyValueStorage({ get, set, remove })` over the host's store (Tauri store, keychain)
|
|
17
|
-
* Node — `fileStorage(path)` in `@ai-matrx/auth/node`, or `memoryStorage()` for a one-shot run
|
|
18
|
-
*
|
|
19
|
-
* None of these is ever silently swapped for another: a storage the platform does not have throws
|
|
20
|
-
* a named error at creation, never a quiet in-memory fallback that signs the person out on reload.
|
|
21
|
-
*/
|
|
22
|
-
/** The storage contract supabase-js reads (sync or async methods). */
|
|
23
|
-
interface SessionStorage {
|
|
24
|
-
getItem(key: string): string | null | Promise<string | null>;
|
|
25
|
-
setItem(key: string, value: string): void | Promise<void>;
|
|
26
|
-
removeItem(key: string): void | Promise<void>;
|
|
27
|
-
}
|
|
28
|
-
declare class StorageUnavailableError extends Error {
|
|
29
|
-
readonly storage: string;
|
|
30
|
-
constructor(storage: string, reason: string);
|
|
31
|
-
}
|
|
32
|
-
/** In memory only: the session ends with the process (one-shot scripts, tests, server renders). */
|
|
33
|
-
declare function memoryStorage(): SessionStorage & {
|
|
34
|
-
readonly size: () => number;
|
|
35
|
-
};
|
|
36
|
-
/** The browser's `localStorage` (shared by every tab of the origin) or `sessionStorage` (this tab). */
|
|
37
|
-
declare function browserStorage(kind?: "local" | "session"): SessionStorage;
|
|
38
|
-
/** The structural slice of `chrome.storage.<area>` read here (MV3 promise API). */
|
|
39
|
-
interface ChromeStorageAreaLike {
|
|
40
|
-
get(keys: string): Promise<Record<string, unknown>>;
|
|
41
|
-
set(items: Record<string, unknown>): Promise<void>;
|
|
42
|
-
remove(keys: string): Promise<void>;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* A browser extension's `chrome.storage` area (default `local`): one session for the popup, side
|
|
46
|
-
* panel, content scripts' relay and the service worker. Pass `area` to inject one (tests, Firefox).
|
|
47
|
-
*/
|
|
48
|
-
declare function extensionStorage(area?: ChromeStorageAreaLike): SessionStorage;
|
|
49
|
-
/** Any async key-value store the host owns (a desktop app's Tauri store or keychain). */
|
|
50
|
-
declare function keyValueStorage(store: {
|
|
51
|
-
get(key: string): Promise<string | null | undefined>;
|
|
52
|
-
set(key: string, value: string): Promise<void>;
|
|
53
|
-
remove(key: string): Promise<void>;
|
|
54
|
-
}): SessionStorage;
|
|
7
|
+
export { A as ApiClaimsUser, C as ClaimsCacheOptions, a as ClaimsCapableClient, b as ClaimsResult, P as PROJECT_SIGNING_KEYS, c as PROJECT_SIGNING_KEY_AUTHORITIES, d as accountFlagsFor, e as confirmUnverifiableSession, f as createClaimsCache, g as getClaimsUser, i as isAuthTransportFailure, h as isSignedOutError, j as isUnverifiableTokenError, k as isUnverifiableTokenRefusal, p as pinnedSigningKeysFor, u as userFromClaims } from './claims-DXdbe3cW.cjs';
|
|
55
8
|
|
|
56
9
|
/**
|
|
57
10
|
* `createAuthClient` — the one Supabase client factory OUTSIDE Next.js (Next apps use
|
|
@@ -83,4 +36,4 @@ declare class SecretKeyRefusedError extends Error {
|
|
|
83
36
|
}
|
|
84
37
|
declare function createAuthClient<Database = any>(options: CreateAuthClientOptions): SupabaseClient<Database>;
|
|
85
38
|
|
|
86
|
-
export { type
|
|
39
|
+
export { type CreateAuthClientOptions, SecretKeyRefusedError, SessionStorage, createAuthClient };
|