@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/dist/node.d.cts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { JWK, JwtPayload, AuthError, UserAppMetadata, UserMetadata, SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Session storage per platform — where a Supabase session lives between runs.
|
|
3
5
|
*
|
|
@@ -21,6 +23,120 @@ interface SessionStorage {
|
|
|
21
23
|
removeItem(key: string): void | Promise<void>;
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
/**
|
|
27
|
+
* THE identity read every server and client path shares (moved from the website's
|
|
28
|
+
* `utils/supabase/{claimsUser,projectSigningKeys,unverifiableSession}.ts`, 2026-10-12).
|
|
29
|
+
*
|
|
30
|
+
* Verifies the access token LOCALLY (WebCrypto against the project's JWKS; the live project signs
|
|
31
|
+
* ES256) and never asks the auth server who the caller is. `getClaims()`, NEVER `getUser()`:
|
|
32
|
+
* `getUser()` is one auth-server round trip per call, with auth-js retrying a stalled refresh for
|
|
33
|
+
* up to 30s — on 2026-09-20 and 09-21 that put 504s in front of real people.
|
|
34
|
+
*
|
|
35
|
+
* Framework-free: no `next/*`, no `window` at import time. Server doors (`createServerAuth` in
|
|
36
|
+
* `/next`, `verifyRequest` in `/node`) and the browser's de-duplicated read (`createClaimsCache`)
|
|
37
|
+
* are thin wrappers over `getClaimsUser`.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The verified caller, built from the access token's claims. `id` is `sub`; `app_metadata` /
|
|
42
|
+
* `user_metadata` default to `{}` so a reader never null-checks what `getUser()` always handed it.
|
|
43
|
+
*/
|
|
44
|
+
interface ApiClaimsUser extends JwtPayload {
|
|
45
|
+
/** The `sub` claim — the user id. */
|
|
46
|
+
id: string;
|
|
47
|
+
app_metadata: UserAppMetadata;
|
|
48
|
+
user_metadata: UserMetadata;
|
|
49
|
+
}
|
|
50
|
+
/** Any Supabase client — the only thing this door needs is `auth.getClaims`. */
|
|
51
|
+
type ClaimsCapableClient = {
|
|
52
|
+
auth: {
|
|
53
|
+
getClaims(jwt?: string, options?: {
|
|
54
|
+
jwks?: {
|
|
55
|
+
keys: JWK[];
|
|
56
|
+
};
|
|
57
|
+
}): Promise<{
|
|
58
|
+
data: {
|
|
59
|
+
claims?: JwtPayload;
|
|
60
|
+
} | null;
|
|
61
|
+
error: AuthError | null;
|
|
62
|
+
}>;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Server identity doors (moved from the website's `utils/supabase/{getServerAuth,resolveUser,
|
|
68
|
+
* adminClient}.ts`, 2026-10-12). Framework-free: the app injects how it builds its cookie client,
|
|
69
|
+
* and (for Next) React's `cache()` so one request resolves once. Exported from `/next` and `/node`.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
interface ServerAuthState {
|
|
73
|
+
/** `true` when the request carries a locally verified access token. */
|
|
74
|
+
isAuthenticated: boolean;
|
|
75
|
+
/** The verified caller (claims, `id` = `sub`), or `null` for guests and unresolvable requests. */
|
|
76
|
+
user: ApiClaimsUser | null;
|
|
77
|
+
/**
|
|
78
|
+
* `true` when no verdict could be REACHED (authority/JWKS unreachable, identity budget spent).
|
|
79
|
+
* The person is NOT signed out: never bounce to sign-in on it. Never set for a token that was
|
|
80
|
+
* judged bad — that is a settled answer, the same line the proxy draws.
|
|
81
|
+
*/
|
|
82
|
+
authUnavailable: boolean;
|
|
83
|
+
/** `true` for a guest (anonymous) session. */
|
|
84
|
+
isGuest: boolean;
|
|
85
|
+
/** `isAuthenticated && !isGuest` — the flag account UI keys on. */
|
|
86
|
+
hasAccount: boolean;
|
|
87
|
+
}
|
|
88
|
+
interface ServerAuthOptions {
|
|
89
|
+
/** A FRESH cookie-bound client per attempt (the identity budget belongs to the client). */
|
|
90
|
+
createClient: () => Promise<ClaimsCapableClient> | ClaimsCapableClient;
|
|
91
|
+
/** The URL the app talks to — decides which pinned keys apply. */
|
|
92
|
+
supabaseUrl: string | null | undefined;
|
|
93
|
+
/** React's `cache` on a Next server, so the whole tree shares one resolve per request. */
|
|
94
|
+
cache?: <F extends (...args: never[]) => unknown>(fn: F) => F;
|
|
95
|
+
/** Wait before the one retry (default 250ms). */
|
|
96
|
+
retryAfterMs?: number;
|
|
97
|
+
/** Where the "could not verify" warnings go (default `console.warn`). */
|
|
98
|
+
warn?: (message: string) => void;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Build THE server identity door: claims verified locally, asked twice before it hands anyone
|
|
102
|
+
* "could not verify" (that is almost always one cold first load spending its budget).
|
|
103
|
+
*
|
|
104
|
+
* export const getServerAuth = createServerAuth({ createClient, supabaseUrl, cache });
|
|
105
|
+
*/
|
|
106
|
+
declare function createServerAuth(options: ServerAuthOptions): () => Promise<ServerAuthState>;
|
|
107
|
+
/** The bit of a request this needs: its headers (a `Request`, a `NextRequest`, a Node shim). */
|
|
108
|
+
interface RequestLike {
|
|
109
|
+
headers: {
|
|
110
|
+
get(name: string): string | null;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
interface VerifyRequestOptions {
|
|
114
|
+
url: string;
|
|
115
|
+
/** The publishable key (`sb_publishable_*`). */
|
|
116
|
+
publishableKey: string;
|
|
117
|
+
/** The app's cookie-bound client, used when the request carries no Bearer token. */
|
|
118
|
+
cookieClient?: () => Promise<ClaimsCapableClient> | ClaimsCapableClient;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Who is calling this API route: a Bearer token (public/mobile/extension/desktop clients) or the
|
|
122
|
+
* session cookie (browser). `user` is null when the caller is signed out OR could not be verified;
|
|
123
|
+
* `error` tells the two apart (null = settled signed out).
|
|
124
|
+
*/
|
|
125
|
+
declare function verifyRequest(request: RequestLike, options: VerifyRequestOptions): Promise<{
|
|
126
|
+
user: ApiClaimsUser | null;
|
|
127
|
+
error: Error | null;
|
|
128
|
+
}>;
|
|
129
|
+
/**
|
|
130
|
+
* THE service (secret-key) client — server-only, bypasses row security. The package's browser
|
|
131
|
+
* clients refuse a secret key by design (`SecretKeyRefusedError`); this is the one door for it.
|
|
132
|
+
* Never ship it to a browser bundle: it lives in `/node` and `/next` only.
|
|
133
|
+
*/
|
|
134
|
+
declare function createServiceClient<Database = unknown>(options: {
|
|
135
|
+
url: string | null | undefined;
|
|
136
|
+
/** `sb_secret_*` (the legacy service-role JWT is banned). */
|
|
137
|
+
secretKey: string | null | undefined;
|
|
138
|
+
}): SupabaseClient<Database>;
|
|
139
|
+
|
|
24
140
|
/**
|
|
25
141
|
* @ai-matrx/auth/node — session storage for Node scripts and CLIs: the session survives between
|
|
26
142
|
* runs in one JSON file (mode 0600), so a script signs in once and refreshes after.
|
|
@@ -30,4 +146,4 @@ interface SessionStorage {
|
|
|
30
146
|
|
|
31
147
|
declare function fileStorage(path: string): SessionStorage;
|
|
32
148
|
|
|
33
|
-
export { fileStorage };
|
|
149
|
+
export { type RequestLike, type ServerAuthOptions, type ServerAuthState, type VerifyRequestOptions, createServerAuth, createServiceClient, fileStorage, verifyRequest };
|
package/dist/node.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { JWK, JwtPayload, AuthError, UserAppMetadata, UserMetadata, SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Session storage per platform — where a Supabase session lives between runs.
|
|
3
5
|
*
|
|
@@ -21,6 +23,120 @@ interface SessionStorage {
|
|
|
21
23
|
removeItem(key: string): void | Promise<void>;
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
/**
|
|
27
|
+
* THE identity read every server and client path shares (moved from the website's
|
|
28
|
+
* `utils/supabase/{claimsUser,projectSigningKeys,unverifiableSession}.ts`, 2026-10-12).
|
|
29
|
+
*
|
|
30
|
+
* Verifies the access token LOCALLY (WebCrypto against the project's JWKS; the live project signs
|
|
31
|
+
* ES256) and never asks the auth server who the caller is. `getClaims()`, NEVER `getUser()`:
|
|
32
|
+
* `getUser()` is one auth-server round trip per call, with auth-js retrying a stalled refresh for
|
|
33
|
+
* up to 30s — on 2026-09-20 and 09-21 that put 504s in front of real people.
|
|
34
|
+
*
|
|
35
|
+
* Framework-free: no `next/*`, no `window` at import time. Server doors (`createServerAuth` in
|
|
36
|
+
* `/next`, `verifyRequest` in `/node`) and the browser's de-duplicated read (`createClaimsCache`)
|
|
37
|
+
* are thin wrappers over `getClaimsUser`.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The verified caller, built from the access token's claims. `id` is `sub`; `app_metadata` /
|
|
42
|
+
* `user_metadata` default to `{}` so a reader never null-checks what `getUser()` always handed it.
|
|
43
|
+
*/
|
|
44
|
+
interface ApiClaimsUser extends JwtPayload {
|
|
45
|
+
/** The `sub` claim — the user id. */
|
|
46
|
+
id: string;
|
|
47
|
+
app_metadata: UserAppMetadata;
|
|
48
|
+
user_metadata: UserMetadata;
|
|
49
|
+
}
|
|
50
|
+
/** Any Supabase client — the only thing this door needs is `auth.getClaims`. */
|
|
51
|
+
type ClaimsCapableClient = {
|
|
52
|
+
auth: {
|
|
53
|
+
getClaims(jwt?: string, options?: {
|
|
54
|
+
jwks?: {
|
|
55
|
+
keys: JWK[];
|
|
56
|
+
};
|
|
57
|
+
}): Promise<{
|
|
58
|
+
data: {
|
|
59
|
+
claims?: JwtPayload;
|
|
60
|
+
} | null;
|
|
61
|
+
error: AuthError | null;
|
|
62
|
+
}>;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Server identity doors (moved from the website's `utils/supabase/{getServerAuth,resolveUser,
|
|
68
|
+
* adminClient}.ts`, 2026-10-12). Framework-free: the app injects how it builds its cookie client,
|
|
69
|
+
* and (for Next) React's `cache()` so one request resolves once. Exported from `/next` and `/node`.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
interface ServerAuthState {
|
|
73
|
+
/** `true` when the request carries a locally verified access token. */
|
|
74
|
+
isAuthenticated: boolean;
|
|
75
|
+
/** The verified caller (claims, `id` = `sub`), or `null` for guests and unresolvable requests. */
|
|
76
|
+
user: ApiClaimsUser | null;
|
|
77
|
+
/**
|
|
78
|
+
* `true` when no verdict could be REACHED (authority/JWKS unreachable, identity budget spent).
|
|
79
|
+
* The person is NOT signed out: never bounce to sign-in on it. Never set for a token that was
|
|
80
|
+
* judged bad — that is a settled answer, the same line the proxy draws.
|
|
81
|
+
*/
|
|
82
|
+
authUnavailable: boolean;
|
|
83
|
+
/** `true` for a guest (anonymous) session. */
|
|
84
|
+
isGuest: boolean;
|
|
85
|
+
/** `isAuthenticated && !isGuest` — the flag account UI keys on. */
|
|
86
|
+
hasAccount: boolean;
|
|
87
|
+
}
|
|
88
|
+
interface ServerAuthOptions {
|
|
89
|
+
/** A FRESH cookie-bound client per attempt (the identity budget belongs to the client). */
|
|
90
|
+
createClient: () => Promise<ClaimsCapableClient> | ClaimsCapableClient;
|
|
91
|
+
/** The URL the app talks to — decides which pinned keys apply. */
|
|
92
|
+
supabaseUrl: string | null | undefined;
|
|
93
|
+
/** React's `cache` on a Next server, so the whole tree shares one resolve per request. */
|
|
94
|
+
cache?: <F extends (...args: never[]) => unknown>(fn: F) => F;
|
|
95
|
+
/** Wait before the one retry (default 250ms). */
|
|
96
|
+
retryAfterMs?: number;
|
|
97
|
+
/** Where the "could not verify" warnings go (default `console.warn`). */
|
|
98
|
+
warn?: (message: string) => void;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Build THE server identity door: claims verified locally, asked twice before it hands anyone
|
|
102
|
+
* "could not verify" (that is almost always one cold first load spending its budget).
|
|
103
|
+
*
|
|
104
|
+
* export const getServerAuth = createServerAuth({ createClient, supabaseUrl, cache });
|
|
105
|
+
*/
|
|
106
|
+
declare function createServerAuth(options: ServerAuthOptions): () => Promise<ServerAuthState>;
|
|
107
|
+
/** The bit of a request this needs: its headers (a `Request`, a `NextRequest`, a Node shim). */
|
|
108
|
+
interface RequestLike {
|
|
109
|
+
headers: {
|
|
110
|
+
get(name: string): string | null;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
interface VerifyRequestOptions {
|
|
114
|
+
url: string;
|
|
115
|
+
/** The publishable key (`sb_publishable_*`). */
|
|
116
|
+
publishableKey: string;
|
|
117
|
+
/** The app's cookie-bound client, used when the request carries no Bearer token. */
|
|
118
|
+
cookieClient?: () => Promise<ClaimsCapableClient> | ClaimsCapableClient;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Who is calling this API route: a Bearer token (public/mobile/extension/desktop clients) or the
|
|
122
|
+
* session cookie (browser). `user` is null when the caller is signed out OR could not be verified;
|
|
123
|
+
* `error` tells the two apart (null = settled signed out).
|
|
124
|
+
*/
|
|
125
|
+
declare function verifyRequest(request: RequestLike, options: VerifyRequestOptions): Promise<{
|
|
126
|
+
user: ApiClaimsUser | null;
|
|
127
|
+
error: Error | null;
|
|
128
|
+
}>;
|
|
129
|
+
/**
|
|
130
|
+
* THE service (secret-key) client — server-only, bypasses row security. The package's browser
|
|
131
|
+
* clients refuse a secret key by design (`SecretKeyRefusedError`); this is the one door for it.
|
|
132
|
+
* Never ship it to a browser bundle: it lives in `/node` and `/next` only.
|
|
133
|
+
*/
|
|
134
|
+
declare function createServiceClient<Database = unknown>(options: {
|
|
135
|
+
url: string | null | undefined;
|
|
136
|
+
/** `sb_secret_*` (the legacy service-role JWT is banned). */
|
|
137
|
+
secretKey: string | null | undefined;
|
|
138
|
+
}): SupabaseClient<Database>;
|
|
139
|
+
|
|
24
140
|
/**
|
|
25
141
|
* @ai-matrx/auth/node — session storage for Node scripts and CLIs: the session survives between
|
|
26
142
|
* runs in one JSON file (mode 0600), so a script signs in once and refreshes after.
|
|
@@ -30,4 +146,4 @@ interface SessionStorage {
|
|
|
30
146
|
|
|
31
147
|
declare function fileStorage(path: string): SessionStorage;
|
|
32
148
|
|
|
33
|
-
export { fileStorage };
|
|
149
|
+
export { type RequestLike, type ServerAuthOptions, type ServerAuthState, type VerifyRequestOptions, createServerAuth, createServiceClient, fileStorage, verifyRequest };
|
package/dist/node.js
CHANGED
|
@@ -1,3 +1,135 @@
|
|
|
1
|
+
// src/server-auth.ts
|
|
2
|
+
import { createClient } from "@supabase/supabase-js";
|
|
3
|
+
|
|
4
|
+
// src/claims.ts
|
|
5
|
+
import {
|
|
6
|
+
AuthError,
|
|
7
|
+
AuthSessionMissingError
|
|
8
|
+
} from "@supabase/supabase-js";
|
|
9
|
+
async function getClaimsUser(client, jwt, options) {
|
|
10
|
+
const { data, error } = options ? await client.auth.getClaims(jwt, options) : await client.auth.getClaims(jwt);
|
|
11
|
+
if (error) {
|
|
12
|
+
if (isSignedOutError(error)) return { data: { user: null }, error: null };
|
|
13
|
+
return { data: { user: null }, error };
|
|
14
|
+
}
|
|
15
|
+
if (!data?.claims) return { data: { user: null }, error: null };
|
|
16
|
+
const user = userFromClaims(data.claims);
|
|
17
|
+
if (!user) {
|
|
18
|
+
return {
|
|
19
|
+
data: { user: null },
|
|
20
|
+
error: new AuthError("Verified token carries no `sub` claim", 401, "bad_jwt")
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return { data: { user }, error: null };
|
|
24
|
+
}
|
|
25
|
+
var BUDGET_EXHAUSTED_STATUS = 408;
|
|
26
|
+
var BUDGET_EXHAUSTED_CODE = "auth_budget_exhausted";
|
|
27
|
+
function isAuthTransportFailure(error) {
|
|
28
|
+
if (!error || typeof error !== "object") return false;
|
|
29
|
+
const name = String(error.name ?? "");
|
|
30
|
+
if (name === "AuthRetryableFetchError" || name === "TimeoutError" || name === "AbortError") return true;
|
|
31
|
+
const status = error.status;
|
|
32
|
+
if (status === 0 || status === 504 || status === 503) return true;
|
|
33
|
+
return status === BUDGET_EXHAUSTED_STATUS || error.code === BUDGET_EXHAUSTED_CODE;
|
|
34
|
+
}
|
|
35
|
+
function isSignedOutError(error) {
|
|
36
|
+
return error instanceof AuthSessionMissingError || error.name === "AuthSessionMissingError" || error.code === "session_not_found";
|
|
37
|
+
}
|
|
38
|
+
function userFromClaims(claims) {
|
|
39
|
+
if (!claims || typeof claims.sub !== "string") return null;
|
|
40
|
+
return {
|
|
41
|
+
...claims,
|
|
42
|
+
id: claims.sub,
|
|
43
|
+
app_metadata: claims.app_metadata ?? {},
|
|
44
|
+
user_metadata: claims.user_metadata ?? {}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function accountFlagsFor(claims) {
|
|
48
|
+
if (!claims) return { isGuest: false, hasAccount: false };
|
|
49
|
+
const isGuest = claims.is_anonymous === true;
|
|
50
|
+
return { isGuest, hasAccount: !isGuest };
|
|
51
|
+
}
|
|
52
|
+
var PROJECT_SIGNING_KEY_AUTHORITIES = [
|
|
53
|
+
"db.matrxserver.com",
|
|
54
|
+
"brsgrqvjdzwihsvnfqkf.supabase.co"
|
|
55
|
+
];
|
|
56
|
+
var PROJECT_SIGNING_KEYS = [
|
|
57
|
+
{
|
|
58
|
+
alg: "ES256",
|
|
59
|
+
crv: "P-256",
|
|
60
|
+
ext: true,
|
|
61
|
+
key_ops: ["verify"],
|
|
62
|
+
kid: "a00aed0b-202c-483a-91ff-fc74719a3bb2",
|
|
63
|
+
kty: "EC",
|
|
64
|
+
use: "sig",
|
|
65
|
+
x: "guqOsgjJqGVU6g_K0iIXKrzixlkeXKOqkPUQCconp6U",
|
|
66
|
+
y: "39zQtkzlfQuPf_MtIhYmMdxNQpIR2Pkl11AToywaslY"
|
|
67
|
+
}
|
|
68
|
+
];
|
|
69
|
+
function pinnedSigningKeysFor(supabaseUrl) {
|
|
70
|
+
if (!supabaseUrl) return [];
|
|
71
|
+
let host;
|
|
72
|
+
try {
|
|
73
|
+
host = new URL(supabaseUrl).host.toLowerCase();
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
return PROJECT_SIGNING_KEY_AUTHORITIES.includes(host) ? PROJECT_SIGNING_KEYS : [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/server-auth.ts
|
|
81
|
+
function createServerAuth(options) {
|
|
82
|
+
const warn = options.warn ?? ((m) => console.warn(m));
|
|
83
|
+
const resolveOnce = async () => {
|
|
84
|
+
const client = await options.createClient();
|
|
85
|
+
const {
|
|
86
|
+
data: { user },
|
|
87
|
+
error
|
|
88
|
+
} = await getClaimsUser(client, void 0, { jwks: { keys: pinnedSigningKeysFor(options.supabaseUrl) } });
|
|
89
|
+
const authUnavailable = !user && error !== null && isAuthTransportFailure(error);
|
|
90
|
+
if (authUnavailable) {
|
|
91
|
+
warn(
|
|
92
|
+
`[getServerAuth] identity could not be verified on this attempt \u2014 rendering without a user, NOT as signed out. Cause: ${error?.name ?? "unknown"}: ${error?.message ?? ""}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return { isAuthenticated: !!user, user, authUnavailable, ...accountFlagsFor(user) };
|
|
96
|
+
};
|
|
97
|
+
const door = async () => {
|
|
98
|
+
const first = await resolveOnce();
|
|
99
|
+
if (!first.authUnavailable) return first;
|
|
100
|
+
await new Promise((resolve) => setTimeout(resolve, options.retryAfterMs ?? 250));
|
|
101
|
+
const second = await resolveOnce();
|
|
102
|
+
if (!second.authUnavailable) warn("[getServerAuth] identity verified on the retry \u2014 the first attempt's budget was spent.");
|
|
103
|
+
return second;
|
|
104
|
+
};
|
|
105
|
+
return options.cache ? options.cache(door) : door;
|
|
106
|
+
}
|
|
107
|
+
async function verifyRequest(request, options) {
|
|
108
|
+
const header = request.headers.get("Authorization") ?? request.headers.get("authorization");
|
|
109
|
+
if (header?.startsWith("Bearer ")) {
|
|
110
|
+
const token = header.slice(7);
|
|
111
|
+
const client2 = createClient(options.url, options.publishableKey, {
|
|
112
|
+
global: { headers: { Authorization: `Bearer ${token}` } },
|
|
113
|
+
auth: { persistSession: false, autoRefreshToken: false }
|
|
114
|
+
});
|
|
115
|
+
const { data: data2, error: error2 } = await getClaimsUser(client2, token, {
|
|
116
|
+
jwks: { keys: pinnedSigningKeysFor(options.url) }
|
|
117
|
+
});
|
|
118
|
+
return { user: data2.user, error: error2 };
|
|
119
|
+
}
|
|
120
|
+
if (!options.cookieClient) return { user: null, error: null };
|
|
121
|
+
const client = await options.cookieClient();
|
|
122
|
+
const { data, error } = await getClaimsUser(client, void 0, { jwks: { keys: pinnedSigningKeysFor(options.url) } });
|
|
123
|
+
return { user: data.user, error };
|
|
124
|
+
}
|
|
125
|
+
function createServiceClient(options) {
|
|
126
|
+
const url = options.url?.trim();
|
|
127
|
+
const key = options.secretKey?.trim();
|
|
128
|
+
if (!url) throw new Error("createServiceClient: the Supabase URL is not configured");
|
|
129
|
+
if (!key) throw new Error("createServiceClient: the secret key (SUPABASE_SECRET_KEY) is not configured");
|
|
130
|
+
return createClient(url, key, { auth: { persistSession: false, autoRefreshToken: false } });
|
|
131
|
+
}
|
|
132
|
+
|
|
1
133
|
// src/node.ts
|
|
2
134
|
function builtin(name) {
|
|
3
135
|
const get = globalThis.process?.getBuiltinModule;
|
|
@@ -32,5 +164,8 @@ function fileStorage(path) {
|
|
|
32
164
|
};
|
|
33
165
|
}
|
|
34
166
|
export {
|
|
35
|
-
|
|
167
|
+
createServerAuth,
|
|
168
|
+
createServiceClient,
|
|
169
|
+
fileStorage,
|
|
170
|
+
verifyRequest
|
|
36
171
|
};
|
package/dist/organizations.cjs
CHANGED
|
@@ -20,16 +20,29 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/organizations.ts
|
|
21
21
|
var organizations_exports = {};
|
|
22
22
|
__export(organizations_exports, {
|
|
23
|
+
MEMBERSHIPS_URL: () => MEMBERSHIPS_URL,
|
|
23
24
|
NoActiveOrganizationError: () => NoActiveOrganizationError,
|
|
24
25
|
NotAMemberError: () => NotAMemberError,
|
|
26
|
+
ORGANIZATION_CONTEXT_REQUIRED_CODE: () => ORGANIZATION_CONTEXT_REQUIRED_CODE,
|
|
25
27
|
ORGANIZATION_ENTITY_TYPE: () => ORGANIZATION_ENTITY_TYPE,
|
|
28
|
+
ORGANIZATION_REQUIRED_CODE: () => ORGANIZATION_REQUIRED_CODE,
|
|
29
|
+
ORGANIZATION_REQUIRED_WIRE_CODE: () => ORGANIZATION_REQUIRED_WIRE_CODE,
|
|
30
|
+
OrganizationRequiredServerError: () => OrganizationRequiredServerError,
|
|
26
31
|
classifyOrganizationRefusal: () => classifyOrganizationRefusal,
|
|
27
32
|
createActiveOrganization: () => createActiveOrganization,
|
|
33
|
+
extractOrganizationHoldMemberships: () => extractOrganizationHoldMemberships,
|
|
34
|
+
isOrganizationRequiredEnvelope: () => isOrganizationRequiredEnvelope,
|
|
35
|
+
isOrganizationRequiredError: () => isOrganizationRequiredError,
|
|
36
|
+
isOrganizationRequiredServerError: () => isOrganizationRequiredServerError,
|
|
28
37
|
memoryHeldOrganization: () => memoryHeldOrganization,
|
|
29
38
|
oldestMembership: () => oldestMembership,
|
|
30
39
|
orderOrganizations: () => orderOrganizations,
|
|
31
40
|
organizationMatches: () => organizationMatches,
|
|
41
|
+
organizationRequired: () => organizationRequired,
|
|
42
|
+
organizationRequiredEnvelope: () => organizationRequiredEnvelope,
|
|
43
|
+
organizationRequiredResponse: () => organizationRequiredResponse,
|
|
32
44
|
readAccountOrganizationChoices: () => readAccountOrganizationChoices,
|
|
45
|
+
readCallerMemberships: () => readCallerMemberships,
|
|
33
46
|
readMemberships: () => readMemberships,
|
|
34
47
|
readOrganizationFavourites: () => readOrganizationFavourites,
|
|
35
48
|
resolveActiveOrganization: () => resolveActiveOrganization,
|
|
@@ -182,10 +195,43 @@ function check(door, data, error) {
|
|
|
182
195
|
if (error) throw new SignInError(door, error.message, error.status, error.code);
|
|
183
196
|
return data;
|
|
184
197
|
}
|
|
198
|
+
var personCache = /* @__PURE__ */ new WeakMap();
|
|
199
|
+
function personEntry(client) {
|
|
200
|
+
let entry = personCache.get(client);
|
|
201
|
+
if (!entry) {
|
|
202
|
+
const fresh = { read: null, epoch: 0 };
|
|
203
|
+
entry = fresh;
|
|
204
|
+
personCache.set(client, fresh);
|
|
205
|
+
const subscribe = client.auth?.onAuthStateChange;
|
|
206
|
+
if (typeof subscribe === "function") {
|
|
207
|
+
subscribe.call(client.auth, async (event) => {
|
|
208
|
+
if (event === "SIGNED_IN" || event === "SIGNED_OUT" || event === "USER_UPDATED") invalidatePerson(client);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return entry;
|
|
213
|
+
}
|
|
214
|
+
function invalidatePerson(client) {
|
|
215
|
+
const entry = personCache.get(client);
|
|
216
|
+
if (!entry) return;
|
|
217
|
+
entry.epoch += 1;
|
|
218
|
+
entry.read = null;
|
|
219
|
+
}
|
|
185
220
|
async function getPerson(client) {
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
188
|
-
|
|
221
|
+
const entry = personEntry(client);
|
|
222
|
+
if (entry.read) return entry.read;
|
|
223
|
+
const epoch = entry.epoch;
|
|
224
|
+
const read = (async () => {
|
|
225
|
+
const { data, error } = await client.auth.getUser();
|
|
226
|
+
if (error && (error.name === "AuthSessionMissingError" || error.status === 401)) return null;
|
|
227
|
+
return check("getPerson", data, error).user;
|
|
228
|
+
})();
|
|
229
|
+
entry.read = read;
|
|
230
|
+
const settle = (keep) => {
|
|
231
|
+
if (entry.epoch === epoch && entry.read === read && !keep) entry.read = null;
|
|
232
|
+
};
|
|
233
|
+
read.then((user) => settle(user !== null), () => settle(false));
|
|
234
|
+
return read;
|
|
189
235
|
}
|
|
190
236
|
function onSessionChange(client, cb) {
|
|
191
237
|
const { data } = client.auth.onAuthStateChange(cb);
|
|
@@ -318,6 +364,7 @@ function createActiveOrganization(options) {
|
|
|
318
364
|
const load = () => {
|
|
319
365
|
if (disposed) return Promise.resolve(current);
|
|
320
366
|
generation += 1;
|
|
367
|
+
invalidatePerson(client);
|
|
321
368
|
inflight = run(generation).finally(() => {
|
|
322
369
|
inflight = null;
|
|
323
370
|
});
|
|
@@ -362,6 +409,7 @@ function createActiveOrganization(options) {
|
|
|
362
409
|
const organization = snap.memberships.find((m) => m.id === organizationId);
|
|
363
410
|
if (!organization || !snap.userId) throw new NotAMemberError(organizationId);
|
|
364
411
|
generation += 1;
|
|
412
|
+
invalidatePerson(client);
|
|
365
413
|
await held.write(snap.userId, organizationId);
|
|
366
414
|
publish({ ...snap, status: "ready", organizationId, organization, rung: "switched", error: null });
|
|
367
415
|
await writeLastActiveOrganization(client, organizationId);
|
|
@@ -387,3 +435,84 @@ function createActiveOrganization(options) {
|
|
|
387
435
|
if (options.autoLoad ?? true) void load();
|
|
388
436
|
return api;
|
|
389
437
|
}
|
|
438
|
+
|
|
439
|
+
// src/organization-required.ts
|
|
440
|
+
var ORGANIZATION_REQUIRED_CODE = "organization_required";
|
|
441
|
+
var ORGANIZATION_REQUIRED_WIRE_CODE = ORGANIZATION_REQUIRED_CODE;
|
|
442
|
+
var ORGANIZATION_CONTEXT_REQUIRED_CODE = "organization_context_required";
|
|
443
|
+
var MEMBERSHIPS_URL = "/auth/organizations";
|
|
444
|
+
function isOrganizationRequiredError(err) {
|
|
445
|
+
if (!(err instanceof Error)) return false;
|
|
446
|
+
const code = err.code;
|
|
447
|
+
return code === ORGANIZATION_CONTEXT_REQUIRED_CODE || code === ORGANIZATION_REQUIRED_CODE;
|
|
448
|
+
}
|
|
449
|
+
function isOrganizationRequiredEnvelope(body) {
|
|
450
|
+
return typeof body === "object" && body !== null && body.code === ORGANIZATION_REQUIRED_CODE;
|
|
451
|
+
}
|
|
452
|
+
function extractOrganizationHoldMemberships(err) {
|
|
453
|
+
if (!err || typeof err !== "object") return null;
|
|
454
|
+
if (err.code !== ORGANIZATION_REQUIRED_CODE) return null;
|
|
455
|
+
const container = err;
|
|
456
|
+
return readOrganizationsField(container.details) ?? readOrganizationsField(container.serverDetail);
|
|
457
|
+
}
|
|
458
|
+
function readOrganizationsField(details) {
|
|
459
|
+
if (!details || typeof details !== "object") return null;
|
|
460
|
+
const organizations = details.organizations;
|
|
461
|
+
if (!Array.isArray(organizations)) return null;
|
|
462
|
+
return organizations.filter(
|
|
463
|
+
(org) => !!org && typeof org === "object" && typeof org.id === "string" && typeof org.name === "string"
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
var OrganizationRequiredServerError = class extends Error {
|
|
467
|
+
name = "OrganizationRequiredServerError";
|
|
468
|
+
code = ORGANIZATION_REQUIRED_CODE;
|
|
469
|
+
/** `null` only when the list could not be read ("could not ask"), distinct from `[]`. */
|
|
470
|
+
memberships;
|
|
471
|
+
constructor(message, memberships = null) {
|
|
472
|
+
super(message);
|
|
473
|
+
this.memberships = memberships;
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
function isOrganizationRequiredServerError(error) {
|
|
477
|
+
return error instanceof OrganizationRequiredServerError || error instanceof Error && error.name === "OrganizationRequiredServerError";
|
|
478
|
+
}
|
|
479
|
+
async function readCallerMemberships(client) {
|
|
480
|
+
try {
|
|
481
|
+
const { data: claims } = await client.auth.getClaims();
|
|
482
|
+
const userId = claims?.claims?.sub;
|
|
483
|
+
if (typeof userId !== "string" || !userId) return null;
|
|
484
|
+
const query = client.schema("iam").from("memberships").select("organization_id, organizations:organization_id(id,name,abbreviation)");
|
|
485
|
+
const { data, error } = await query.eq("user_id", userId).eq("container_type", "organization").eq("status", "active").is("deleted_at", null);
|
|
486
|
+
if (error || !Array.isArray(data)) return null;
|
|
487
|
+
const rows = data;
|
|
488
|
+
return rows.map((row) => ({
|
|
489
|
+
id: row.organizations?.id ?? row.organization_id,
|
|
490
|
+
name: row.organizations?.name ?? "Untitled organization",
|
|
491
|
+
...row.organizations?.abbreviation ? { abbreviation: row.organizations.abbreviation } : {}
|
|
492
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
493
|
+
} catch {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
async function organizationRequired(client, message) {
|
|
498
|
+
throw new OrganizationRequiredServerError(message, await readCallerMemberships(client));
|
|
499
|
+
}
|
|
500
|
+
function organizationRequiredEnvelope(error, setOn = "request") {
|
|
501
|
+
return {
|
|
502
|
+
error: ORGANIZATION_REQUIRED_CODE,
|
|
503
|
+
code: ORGANIZATION_REQUIRED_CODE,
|
|
504
|
+
message: error.message,
|
|
505
|
+
user_message: "Choose the organization you're working in, then try again.",
|
|
506
|
+
details: {
|
|
507
|
+
hold: ORGANIZATION_REQUIRED_CODE,
|
|
508
|
+
can_choose: true,
|
|
509
|
+
set_on: setOn,
|
|
510
|
+
remedy: "Choose the organization you're working in and send it with the request (the X-Organization-Id header), then try again.",
|
|
511
|
+
organizations: error.memberships,
|
|
512
|
+
memberships_url: MEMBERSHIPS_URL
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function organizationRequiredResponse(error) {
|
|
517
|
+
return Response.json(organizationRequiredEnvelope(error), { status: 400 });
|
|
518
|
+
}
|