@robelest/convex-auth 0.0.2-preview.1 → 0.0.2-preview.2
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/dist/client/index.d.ts +84 -30
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +259 -59
- package/dist/client/index.js.map +1 -1
- package/dist/component/index.d.ts +2 -2
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component/index.js +2 -2
- package/dist/component/index.js.map +1 -1
- package/dist/providers/{Anonymous.d.ts → anonymous.d.ts} +8 -8
- package/dist/providers/{Anonymous.d.ts.map → anonymous.d.ts.map} +1 -1
- package/dist/providers/{Anonymous.js → anonymous.js} +9 -10
- package/dist/providers/anonymous.js.map +1 -0
- package/dist/providers/{ConvexCredentials.d.ts → credentials.d.ts} +11 -11
- package/dist/providers/credentials.d.ts.map +1 -0
- package/dist/providers/{ConvexCredentials.js → credentials.js} +8 -8
- package/dist/providers/credentials.js.map +1 -0
- package/dist/providers/{Email.d.ts → email.d.ts} +6 -6
- package/dist/providers/email.d.ts.map +1 -0
- package/dist/providers/{Email.js → email.js} +6 -6
- package/dist/providers/email.js.map +1 -0
- package/dist/providers/{Password.d.ts → password.d.ts} +10 -10
- package/dist/providers/{Password.d.ts.map → password.d.ts.map} +1 -1
- package/dist/providers/{Password.js → password.js} +19 -20
- package/dist/providers/password.js.map +1 -0
- package/dist/providers/{Phone.d.ts → phone.d.ts} +3 -3
- package/dist/providers/{Phone.d.ts.map → phone.d.ts.map} +1 -1
- package/dist/providers/{Phone.js → phone.js} +3 -3
- package/dist/providers/{Phone.js.map → phone.js.map} +1 -1
- package/dist/server/implementation/index.d.ts +73 -159
- package/dist/server/implementation/index.d.ts.map +1 -1
- package/dist/server/implementation/index.js +74 -100
- package/dist/server/implementation/index.js.map +1 -1
- package/dist/server/implementation/sessions.d.ts +2 -20
- package/dist/server/implementation/sessions.d.ts.map +1 -1
- package/dist/server/implementation/sessions.js +2 -20
- package/dist/server/implementation/sessions.js.map +1 -1
- package/dist/server/index.d.ts +18 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +255 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/provider_utils.d.ts.map +1 -1
- package/dist/server/types.d.ts +70 -9
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +3 -6
- package/src/client/index.ts +347 -110
- package/src/component/index.ts +1 -8
- package/src/providers/{Anonymous.ts → anonymous.ts} +10 -11
- package/src/providers/{ConvexCredentials.ts → credentials.ts} +11 -11
- package/src/providers/{Email.ts → email.ts} +5 -5
- package/src/providers/{Password.ts → password.ts} +22 -27
- package/src/providers/{Phone.ts → phone.ts} +2 -2
- package/src/server/implementation/index.ts +119 -231
- package/src/server/implementation/sessions.ts +2 -20
- package/src/server/index.ts +373 -0
- package/src/server/types.ts +95 -8
- package/dist/providers/Anonymous.js.map +0 -1
- package/dist/providers/ConvexCredentials.d.ts.map +0 -1
- package/dist/providers/ConvexCredentials.js.map +0 -1
- package/dist/providers/Email.d.ts.map +0 -1
- package/dist/providers/Email.js.map +0 -1
- package/dist/providers/Password.js.map +0 -1
- package/providers/Anonymous/package.json +0 -6
- package/providers/ConvexCredentials/package.json +0 -6
- package/providers/Email/package.json +0 -6
- package/providers/Password/package.json +0 -6
- package/providers/Phone/package.json +0 -6
- package/server/package.json +0 -6
package/dist/client/index.d.ts
CHANGED
|
@@ -1,49 +1,103 @@
|
|
|
1
|
-
import { FunctionReference, OptionalRestArgs } from "convex/server";
|
|
2
1
|
import { Value } from "convex/values";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Structural interface for any Convex client.
|
|
4
|
+
* Satisfied by both `ConvexClient` (`convex/browser`) and
|
|
5
|
+
* `ConvexReactClient` (`convex/react`).
|
|
6
|
+
*/
|
|
7
|
+
interface ConvexTransport {
|
|
8
|
+
action(action: any, args: any): Promise<any>;
|
|
9
|
+
setAuth(fetchToken: (args: {
|
|
10
|
+
forceRefreshToken: boolean;
|
|
11
|
+
}) => Promise<string | null | undefined>, onChange?: (isAuthenticated: boolean) => void): void;
|
|
12
|
+
clearAuth(): void;
|
|
13
|
+
}
|
|
14
|
+
/** Pluggable key-value storage (defaults to `localStorage`). */
|
|
15
|
+
export interface Storage {
|
|
12
16
|
getItem(key: string): string | null | undefined | Promise<string | null | undefined>;
|
|
13
17
|
setItem(key: string, value: string): void | Promise<void>;
|
|
14
18
|
removeItem(key: string): void | Promise<void>;
|
|
15
19
|
}
|
|
16
|
-
|
|
17
|
-
token: string;
|
|
18
|
-
refreshToken: string;
|
|
19
|
-
};
|
|
20
|
-
export type SignInResult = {
|
|
20
|
+
type SignInResult = {
|
|
21
21
|
signingIn: boolean;
|
|
22
22
|
redirect?: URL;
|
|
23
23
|
};
|
|
24
|
-
|
|
24
|
+
/** Reactive auth state snapshot returned by `auth.state` and `auth.onChange`. */
|
|
25
|
+
export type AuthState = {
|
|
25
26
|
isLoading: boolean;
|
|
26
27
|
isAuthenticated: boolean;
|
|
27
28
|
token: string | null;
|
|
28
29
|
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
/** Options for {@link client}. */
|
|
31
|
+
export type ClientOptions = {
|
|
32
|
+
/** Any Convex client (`ConvexClient` or `ConvexReactClient`). */
|
|
33
|
+
convex: ConvexTransport;
|
|
34
|
+
/**
|
|
35
|
+
* Convex deployment URL. Derived automatically from the client internals
|
|
36
|
+
* when omitted — pass explicitly only if auto-detection fails.
|
|
37
|
+
*/
|
|
38
|
+
url?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Key-value storage for persisting tokens.
|
|
41
|
+
*
|
|
42
|
+
* - Defaults to `localStorage` in SPA mode.
|
|
43
|
+
* - Defaults to `null` (in-memory only) when `proxy` is set,
|
|
44
|
+
* since httpOnly cookies handle persistence.
|
|
45
|
+
*/
|
|
46
|
+
storage?: Storage | null;
|
|
47
|
+
/** Override how the URL bar is updated after OAuth code exchange. */
|
|
33
48
|
replaceURL?: (relativeUrl: string) => void | Promise<void>;
|
|
34
|
-
|
|
35
|
-
|
|
49
|
+
/**
|
|
50
|
+
* SSR proxy endpoint (e.g. `"/api/auth"`).
|
|
51
|
+
*
|
|
52
|
+
* When set, `signIn`/`signOut`/token refresh POST to this URL
|
|
53
|
+
* (with `credentials: "include"`) instead of calling Convex directly.
|
|
54
|
+
* The server handles httpOnly cookies for token persistence.
|
|
55
|
+
*
|
|
56
|
+
* Pair with {@link ClientOptions.token} for flash-free SSR hydration.
|
|
57
|
+
*/
|
|
58
|
+
proxy?: string;
|
|
59
|
+
/**
|
|
60
|
+
* JWT from server-side hydration.
|
|
61
|
+
*
|
|
62
|
+
* In proxy mode the server reads the JWT from an httpOnly cookie
|
|
63
|
+
* and passes it to the client during SSR. This avoids a loading
|
|
64
|
+
* flash on first render — the client is immediately authenticated.
|
|
65
|
+
*/
|
|
66
|
+
token?: string | null;
|
|
36
67
|
};
|
|
37
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Create a framework-agnostic auth client.
|
|
70
|
+
*
|
|
71
|
+
* ### SPA mode (default)
|
|
72
|
+
*
|
|
73
|
+
* ```ts
|
|
74
|
+
* import { ConvexClient } from 'convex/browser'
|
|
75
|
+
* import { client } from '\@robelest/convex-auth/client'
|
|
76
|
+
*
|
|
77
|
+
* const convex = new ConvexClient(CONVEX_URL)
|
|
78
|
+
* const auth = client({ convex })
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* ### SSR / proxy mode
|
|
82
|
+
*
|
|
83
|
+
* ```ts
|
|
84
|
+
* const auth = client({
|
|
85
|
+
* convex,
|
|
86
|
+
* proxy: '/api/auth',
|
|
87
|
+
* initialToken: tokenFromServer, // read from httpOnly cookie during SSR
|
|
88
|
+
* })
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* In proxy mode all auth operations go through the proxy URL.
|
|
92
|
+
* Tokens are stored in httpOnly cookies server-side — the client
|
|
93
|
+
* only holds the JWT in memory.
|
|
94
|
+
*/
|
|
95
|
+
export declare function client(options: ClientOptions): {
|
|
96
|
+
/** Current auth state snapshot. */
|
|
97
|
+
readonly state: AuthState;
|
|
38
98
|
signIn: (provider?: string, args?: FormData | Record<string, Value>) => Promise<SignInResult>;
|
|
39
99
|
signOut: () => Promise<void>;
|
|
40
|
-
|
|
41
|
-
forceRefreshToken: boolean;
|
|
42
|
-
}) => Promise<string | null>;
|
|
43
|
-
handleCodeFlow: () => Promise<void>;
|
|
44
|
-
hydrateFromStorage: () => Promise<void>;
|
|
45
|
-
getSnapshot: () => AuthSnapshot;
|
|
46
|
-
subscribe: (cb: () => void) => () => boolean;
|
|
100
|
+
onChange: (cb: (state: AuthState) => void) => (() => void);
|
|
47
101
|
};
|
|
48
102
|
export {};
|
|
49
103
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAEtC;;;;GAIG;AACH,UAAU,eAAe;IACvB,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7C,OAAO,CACL,UAAU,EAAE,CAAC,IAAI,EAAE;QACjB,iBAAiB,EAAE,OAAO,CAAC;KAC5B,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EACxC,QAAQ,CAAC,EAAE,CAAC,eAAe,EAAE,OAAO,KAAK,IAAI,GAC5C,IAAI,CAAC;IACR,SAAS,IAAI,IAAI,CAAC;CACnB;AAED,gEAAgE;AAChE,MAAM,WAAW,OAAO;IACtB,OAAO,CACL,GAAG,EAAE,MAAM,GACV,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAClE,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAOD,KAAK,YAAY,GAAG;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB,CAAC;AAEF,iFAAiF;AACjF,MAAM,MAAM,SAAS,GAAG;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAEF,kCAAkC;AAClC,MAAM,MAAM,aAAa,GAAG;IAC1B,iEAAiE;IACjE,MAAM,EAAE,eAAe,CAAC;IACxB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,qEAAqE;IACrE,UAAU,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAyBF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,aAAa;IAoazC,mCAAmC;oBACtB,SAAS;wBAlPX,MAAM,SACV,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,KACtC,OAAO,CAAC,YAAY,CAAC;;mBA4LF,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,KAAG,CAAC,MAAM,IAAI,CAAC;EA2DhE"}
|
package/dist/client/index.js
CHANGED
|
@@ -1,49 +1,111 @@
|
|
|
1
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
1
2
|
const VERIFIER_STORAGE_KEY = "__convexAuthOAuthVerifier";
|
|
2
3
|
const JWT_STORAGE_KEY = "__convexAuthJWT";
|
|
3
4
|
const REFRESH_TOKEN_STORAGE_KEY = "__convexAuthRefreshToken";
|
|
4
5
|
const RETRY_BACKOFF = [500, 2000];
|
|
5
6
|
const RETRY_JITTER = 100;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the Convex deployment URL from the client.
|
|
9
|
+
*
|
|
10
|
+
* `ConvexReactClient` exposes `.url` directly.
|
|
11
|
+
* `ConvexClient` exposes `.client.url` via `BaseConvexClient`.
|
|
12
|
+
*/
|
|
13
|
+
function resolveUrl(convex, explicit) {
|
|
14
|
+
if (explicit)
|
|
15
|
+
return explicit;
|
|
16
|
+
const c = convex;
|
|
17
|
+
const url = c.url ?? c.client?.url;
|
|
18
|
+
if (typeof url === "string")
|
|
19
|
+
return url;
|
|
20
|
+
throw new Error("Could not determine Convex deployment URL. Pass `url` explicitly.");
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Create a framework-agnostic auth client.
|
|
24
|
+
*
|
|
25
|
+
* ### SPA mode (default)
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { ConvexClient } from 'convex/browser'
|
|
29
|
+
* import { client } from '\@robelest/convex-auth/client'
|
|
30
|
+
*
|
|
31
|
+
* const convex = new ConvexClient(CONVEX_URL)
|
|
32
|
+
* const auth = client({ convex })
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* ### SSR / proxy mode
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* const auth = client({
|
|
39
|
+
* convex,
|
|
40
|
+
* proxy: '/api/auth',
|
|
41
|
+
* initialToken: tokenFromServer, // read from httpOnly cookie during SSR
|
|
42
|
+
* })
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* In proxy mode all auth operations go through the proxy URL.
|
|
46
|
+
* Tokens are stored in httpOnly cookies server-side — the client
|
|
47
|
+
* only holds the JWT in memory.
|
|
48
|
+
*/
|
|
49
|
+
export function client(options) {
|
|
50
|
+
const { convex, proxy } = options;
|
|
51
|
+
// In proxy mode, default storage to null (cookies handle persistence).
|
|
52
|
+
const storage = options.storage !== undefined
|
|
53
|
+
? options.storage
|
|
54
|
+
: proxy
|
|
55
|
+
? null
|
|
56
|
+
: typeof window === "undefined"
|
|
57
|
+
? null
|
|
58
|
+
: window.localStorage;
|
|
59
|
+
const replaceURL = options.replaceURL ??
|
|
60
|
+
((url) => {
|
|
61
|
+
if (typeof window !== "undefined") {
|
|
62
|
+
window.history.replaceState({}, "", url);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const url = proxy ? undefined : resolveUrl(convex, options.url);
|
|
66
|
+
const escapedNamespace = proxy
|
|
67
|
+
? proxy.replace(/[^a-zA-Z0-9]/g, "")
|
|
68
|
+
: url.replace(/[^a-zA-Z0-9]/g, "");
|
|
13
69
|
const key = (name) => `${name}_${escapedNamespace}`;
|
|
14
70
|
const subscribers = new Set();
|
|
15
|
-
|
|
16
|
-
|
|
71
|
+
// Unauthenticated HTTP client for code verification & OAuth exchange.
|
|
72
|
+
// Only needed in SPA mode — proxy mode routes everything through the proxy.
|
|
73
|
+
const httpClient = proxy ? null : new ConvexHttpClient(url);
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// State
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// If a server-provided token was supplied (SSR hydration), start authenticated.
|
|
78
|
+
const serverToken = options.token ?? null;
|
|
79
|
+
const hasServerToken = serverToken !== null;
|
|
80
|
+
let token = serverToken;
|
|
81
|
+
let isLoading = !hasServerToken;
|
|
17
82
|
let snapshot = {
|
|
18
83
|
isLoading,
|
|
19
|
-
isAuthenticated:
|
|
84
|
+
isAuthenticated: hasServerToken,
|
|
20
85
|
token,
|
|
21
86
|
};
|
|
22
87
|
let handlingCodeFlow = false;
|
|
23
|
-
const logVerbose = (message) => {
|
|
24
|
-
if (transport.verbose) {
|
|
25
|
-
transport.logger?.logVerbose?.(message);
|
|
26
|
-
console.debug(`${new Date().toISOString()} ${message}`);
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
88
|
const notify = () => {
|
|
30
89
|
for (const cb of subscribers)
|
|
31
90
|
cb();
|
|
32
91
|
};
|
|
33
92
|
const updateSnapshot = () => {
|
|
34
|
-
const
|
|
93
|
+
const next = {
|
|
35
94
|
isLoading,
|
|
36
95
|
isAuthenticated: token !== null,
|
|
37
96
|
token,
|
|
38
97
|
};
|
|
39
|
-
if (snapshot.isLoading ===
|
|
40
|
-
snapshot.isAuthenticated ===
|
|
41
|
-
snapshot.token ===
|
|
98
|
+
if (snapshot.isLoading === next.isLoading &&
|
|
99
|
+
snapshot.isAuthenticated === next.isAuthenticated &&
|
|
100
|
+
snapshot.token === next.token) {
|
|
42
101
|
return false;
|
|
43
102
|
}
|
|
44
|
-
snapshot =
|
|
103
|
+
snapshot = next;
|
|
45
104
|
return true;
|
|
46
105
|
};
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Storage helpers (SPA mode only)
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
47
109
|
const storageGet = async (name) => storage ? ((await storage.getItem(key(name))) ?? null) : null;
|
|
48
110
|
const storageSet = async (name, value) => {
|
|
49
111
|
if (storage)
|
|
@@ -53,8 +115,10 @@ export function createAuthClient(options) {
|
|
|
53
115
|
if (storage)
|
|
54
116
|
await storage.removeItem(key(name));
|
|
55
117
|
};
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Token management
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
56
121
|
const setToken = async (args) => {
|
|
57
|
-
const wasAuthenticated = token !== null;
|
|
58
122
|
if (args.tokens === null) {
|
|
59
123
|
token = null;
|
|
60
124
|
if (args.shouldStore) {
|
|
@@ -69,9 +133,6 @@ export function createAuthClient(options) {
|
|
|
69
133
|
await storageSet(REFRESH_TOKEN_STORAGE_KEY, args.tokens.refreshToken);
|
|
70
134
|
}
|
|
71
135
|
}
|
|
72
|
-
if (wasAuthenticated !== (token !== null)) {
|
|
73
|
-
await onChange?.();
|
|
74
|
-
}
|
|
75
136
|
const hadPendingLoad = isLoading;
|
|
76
137
|
isLoading = false;
|
|
77
138
|
const changed = updateSnapshot();
|
|
@@ -79,12 +140,31 @@ export function createAuthClient(options) {
|
|
|
79
140
|
notify();
|
|
80
141
|
}
|
|
81
142
|
};
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Proxy fetch helper
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
const proxyFetch = async (body) => {
|
|
147
|
+
const response = await fetch(proxy, {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: { "Content-Type": "application/json" },
|
|
150
|
+
credentials: "include",
|
|
151
|
+
body: JSON.stringify(body),
|
|
152
|
+
});
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
const error = await response.json().catch(() => ({}));
|
|
155
|
+
throw new Error(error.error ?? `Proxy request failed: ${response.status}`);
|
|
156
|
+
}
|
|
157
|
+
return response.json();
|
|
158
|
+
};
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Code verification with retries (SPA mode only)
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
82
162
|
const verifyCode = async (args) => {
|
|
83
163
|
let lastError;
|
|
84
164
|
let retry = 0;
|
|
85
165
|
while (retry < RETRY_BACKOFF.length) {
|
|
86
166
|
try {
|
|
87
|
-
return await
|
|
167
|
+
return await httpClient.action("auth:signIn", "code" in args
|
|
88
168
|
? { params: { code: args.code }, verifier: args.verifier }
|
|
89
169
|
: args);
|
|
90
170
|
}
|
|
@@ -95,7 +175,6 @@ export function createAuthClient(options) {
|
|
|
95
175
|
break;
|
|
96
176
|
const wait = RETRY_BACKOFF[retry] + RETRY_JITTER * Math.random();
|
|
97
177
|
retry++;
|
|
98
|
-
logVerbose(`verifyCode network retry ${retry}/${RETRY_BACKOFF.length} in ${wait}ms`);
|
|
99
178
|
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
100
179
|
}
|
|
101
180
|
}
|
|
@@ -103,9 +182,15 @@ export function createAuthClient(options) {
|
|
|
103
182
|
};
|
|
104
183
|
const verifyCodeAndSetToken = async (args) => {
|
|
105
184
|
const { tokens } = await verifyCode(args);
|
|
106
|
-
await setToken({
|
|
185
|
+
await setToken({
|
|
186
|
+
shouldStore: true,
|
|
187
|
+
tokens: tokens ?? null,
|
|
188
|
+
});
|
|
107
189
|
return tokens !== null;
|
|
108
190
|
};
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// signIn
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
109
194
|
const signIn = async (provider, args) => {
|
|
110
195
|
const params = args instanceof FormData
|
|
111
196
|
? Array.from(args.entries()).reduce((acc, [k, v]) => {
|
|
@@ -113,16 +198,46 @@ export function createAuthClient(options) {
|
|
|
113
198
|
return acc;
|
|
114
199
|
}, {})
|
|
115
200
|
: args ?? {};
|
|
201
|
+
if (proxy) {
|
|
202
|
+
// Proxy mode: POST to the proxy endpoint.
|
|
203
|
+
const result = await proxyFetch({
|
|
204
|
+
action: "auth:signIn",
|
|
205
|
+
args: { provider, params },
|
|
206
|
+
});
|
|
207
|
+
if (result.redirect !== undefined) {
|
|
208
|
+
const redirectUrl = new URL(result.redirect);
|
|
209
|
+
// Verifier is stored server-side in an httpOnly cookie.
|
|
210
|
+
if (typeof window !== "undefined") {
|
|
211
|
+
window.location.href = redirectUrl.toString();
|
|
212
|
+
}
|
|
213
|
+
return { signingIn: false, redirect: redirectUrl };
|
|
214
|
+
}
|
|
215
|
+
if (result.tokens !== undefined) {
|
|
216
|
+
// Proxy returns { token, refreshToken: "dummy" }.
|
|
217
|
+
// Store JWT in memory only — real refresh token is in httpOnly cookie.
|
|
218
|
+
await setToken({
|
|
219
|
+
shouldStore: false,
|
|
220
|
+
tokens: result.tokens === null ? null : { token: result.tokens.token },
|
|
221
|
+
});
|
|
222
|
+
return { signingIn: result.tokens !== null };
|
|
223
|
+
}
|
|
224
|
+
return { signingIn: false };
|
|
225
|
+
}
|
|
226
|
+
// SPA mode: call Convex directly.
|
|
116
227
|
const verifier = (await storageGet(VERIFIER_STORAGE_KEY)) ?? undefined;
|
|
117
228
|
await storageRemove(VERIFIER_STORAGE_KEY);
|
|
118
|
-
const result = await
|
|
229
|
+
const result = await convex.action("auth:signIn", {
|
|
230
|
+
provider,
|
|
231
|
+
params,
|
|
232
|
+
verifier,
|
|
233
|
+
});
|
|
119
234
|
if (result.redirect !== undefined) {
|
|
120
|
-
const
|
|
235
|
+
const redirectUrl = new URL(result.redirect);
|
|
121
236
|
await storageSet(VERIFIER_STORAGE_KEY, result.verifier);
|
|
122
237
|
if (typeof window !== "undefined") {
|
|
123
|
-
window.location.href =
|
|
238
|
+
window.location.href = redirectUrl.toString();
|
|
124
239
|
}
|
|
125
|
-
return { signingIn: false, redirect:
|
|
240
|
+
return { signingIn: false, redirect: redirectUrl };
|
|
126
241
|
}
|
|
127
242
|
if (result.tokens !== undefined) {
|
|
128
243
|
await setToken({
|
|
@@ -133,34 +248,82 @@ export function createAuthClient(options) {
|
|
|
133
248
|
}
|
|
134
249
|
return { signingIn: false };
|
|
135
250
|
};
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// signOut
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
136
254
|
const signOut = async () => {
|
|
255
|
+
if (proxy) {
|
|
256
|
+
try {
|
|
257
|
+
await proxyFetch({ action: "auth:signOut", args: {} });
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// Already signed out is fine.
|
|
261
|
+
}
|
|
262
|
+
await setToken({ shouldStore: false, tokens: null });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
// SPA mode.
|
|
137
266
|
try {
|
|
138
|
-
await
|
|
267
|
+
await convex.action("auth:signOut", {});
|
|
139
268
|
}
|
|
140
269
|
catch {
|
|
141
270
|
// Already signed out is fine.
|
|
142
271
|
}
|
|
143
272
|
await setToken({ shouldStore: true, tokens: null });
|
|
144
273
|
};
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// fetchAccessToken — called by convex.setAuth()
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
145
277
|
const fetchAccessToken = async ({ forceRefreshToken, }) => {
|
|
146
278
|
if (!forceRefreshToken)
|
|
147
279
|
return token;
|
|
280
|
+
if (proxy) {
|
|
281
|
+
// Proxy mode: POST to the proxy to refresh.
|
|
282
|
+
// The proxy reads the real refresh token from the httpOnly cookie.
|
|
283
|
+
const tokenBeforeRefresh = token;
|
|
284
|
+
return await browserMutex("__convexAuthProxyRefresh", async () => {
|
|
285
|
+
// Another tab/call may have already refreshed.
|
|
286
|
+
if (token !== tokenBeforeRefresh)
|
|
287
|
+
return token;
|
|
288
|
+
try {
|
|
289
|
+
const result = await proxyFetch({
|
|
290
|
+
action: "auth:signIn",
|
|
291
|
+
args: { refreshToken: true },
|
|
292
|
+
});
|
|
293
|
+
if (result.tokens) {
|
|
294
|
+
await setToken({
|
|
295
|
+
shouldStore: false,
|
|
296
|
+
tokens: { token: result.tokens.token },
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
await setToken({ shouldStore: false, tokens: null });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
await setToken({ shouldStore: false, tokens: null });
|
|
305
|
+
}
|
|
306
|
+
return token;
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
// SPA mode: refresh via localStorage + httpClient.
|
|
148
310
|
const tokenBeforeLockAcquisition = token;
|
|
149
311
|
return await browserMutex(REFRESH_TOKEN_STORAGE_KEY, async () => {
|
|
150
312
|
const tokenAfterLockAcquisition = token;
|
|
151
313
|
if (tokenAfterLockAcquisition !== tokenBeforeLockAcquisition) {
|
|
152
|
-
logVerbose(`fetchAccessToken using synchronized token, is null: ${tokenAfterLockAcquisition === null}`);
|
|
153
314
|
return tokenAfterLockAcquisition;
|
|
154
315
|
}
|
|
155
316
|
const refreshToken = (await storageGet(REFRESH_TOKEN_STORAGE_KEY)) ?? null;
|
|
156
317
|
if (!refreshToken) {
|
|
157
|
-
logVerbose("fetchAccessToken found no refresh token");
|
|
158
318
|
return null;
|
|
159
319
|
}
|
|
160
320
|
await verifyCodeAndSetToken({ refreshToken });
|
|
161
321
|
return token;
|
|
162
322
|
});
|
|
163
323
|
};
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// OAuth code flow (SPA mode only — server handles this in proxy mode)
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
164
327
|
const handleCodeFlow = async () => {
|
|
165
328
|
if (typeof window === "undefined")
|
|
166
329
|
return;
|
|
@@ -169,24 +332,20 @@ export function createAuthClient(options) {
|
|
|
169
332
|
const code = new URLSearchParams(window.location.search).get("code");
|
|
170
333
|
if (!code)
|
|
171
334
|
return;
|
|
172
|
-
const shouldRun = shouldHandleCode === undefined
|
|
173
|
-
? true
|
|
174
|
-
: typeof shouldHandleCode === "function"
|
|
175
|
-
? shouldHandleCode()
|
|
176
|
-
: shouldHandleCode;
|
|
177
|
-
if (!shouldRun)
|
|
178
|
-
return;
|
|
179
335
|
handlingCodeFlow = true;
|
|
180
|
-
const
|
|
181
|
-
|
|
336
|
+
const codeUrl = new URL(window.location.href);
|
|
337
|
+
codeUrl.searchParams.delete("code");
|
|
182
338
|
try {
|
|
183
|
-
await replaceURL(
|
|
339
|
+
await replaceURL(codeUrl.pathname + codeUrl.search + codeUrl.hash);
|
|
184
340
|
await signIn(undefined, { code });
|
|
185
341
|
}
|
|
186
342
|
finally {
|
|
187
343
|
handlingCodeFlow = false;
|
|
188
344
|
}
|
|
189
345
|
};
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
// Hydrate from storage (SPA mode only)
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
190
349
|
const hydrateFromStorage = async () => {
|
|
191
350
|
const storedToken = (await storageGet(JWT_STORAGE_KEY)) ?? null;
|
|
192
351
|
await setToken({
|
|
@@ -194,36 +353,77 @@ export function createAuthClient(options) {
|
|
|
194
353
|
tokens: storedToken === null ? null : { token: storedToken },
|
|
195
354
|
});
|
|
196
355
|
};
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// Subscribe
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
/**
|
|
360
|
+
* Subscribe to auth state changes. Immediately invokes the callback
|
|
361
|
+
* with the current state and returns an unsubscribe function.
|
|
362
|
+
*
|
|
363
|
+
* ```ts
|
|
364
|
+
* const unsub = auth.onChange(setState)
|
|
365
|
+
* ```
|
|
366
|
+
*/
|
|
367
|
+
const onChange = (cb) => {
|
|
368
|
+
cb(snapshot);
|
|
369
|
+
const wrapped = () => cb(snapshot);
|
|
370
|
+
subscribers.add(wrapped);
|
|
371
|
+
return () => {
|
|
372
|
+
subscribers.delete(wrapped);
|
|
373
|
+
};
|
|
201
374
|
};
|
|
202
|
-
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
// Initialization
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
// Cross-tab sync via storage events (SPA mode only).
|
|
379
|
+
if (!proxy && typeof window !== "undefined") {
|
|
203
380
|
const onStorage = (event) => {
|
|
204
381
|
void (async () => {
|
|
205
|
-
if (event.key !== key(JWT_STORAGE_KEY))
|
|
382
|
+
if (event.key !== key(JWT_STORAGE_KEY))
|
|
206
383
|
return;
|
|
207
|
-
}
|
|
208
|
-
const value = event.newValue;
|
|
209
384
|
await setToken({
|
|
210
385
|
shouldStore: false,
|
|
211
|
-
tokens:
|
|
386
|
+
tokens: event.newValue === null ? null : { token: event.newValue },
|
|
212
387
|
});
|
|
213
388
|
})();
|
|
214
389
|
};
|
|
215
390
|
window.addEventListener("storage", onStorage);
|
|
216
391
|
}
|
|
392
|
+
// Auto-wire: feed our tokens into the Convex client so
|
|
393
|
+
// queries and mutations are automatically authenticated.
|
|
394
|
+
convex.setAuth(fetchAccessToken);
|
|
395
|
+
// Auto-hydrate and handle code flow.
|
|
396
|
+
if (typeof window !== "undefined") {
|
|
397
|
+
if (proxy) {
|
|
398
|
+
// Proxy mode: if no initialToken was provided, try a refresh
|
|
399
|
+
// to pick up any existing session from httpOnly cookies.
|
|
400
|
+
if (!hasServerToken) {
|
|
401
|
+
void fetchAccessToken({ forceRefreshToken: true });
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
// initialToken already set — mark loading as done.
|
|
405
|
+
isLoading = false;
|
|
406
|
+
updateSnapshot();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
// SPA mode: hydrate from localStorage, then handle OAuth code flow.
|
|
411
|
+
void hydrateFromStorage().then(() => handleCodeFlow());
|
|
412
|
+
}
|
|
413
|
+
}
|
|
217
414
|
return {
|
|
415
|
+
/** Current auth state snapshot. */
|
|
416
|
+
get state() {
|
|
417
|
+
return snapshot;
|
|
418
|
+
},
|
|
218
419
|
signIn,
|
|
219
420
|
signOut,
|
|
220
|
-
|
|
221
|
-
handleCodeFlow,
|
|
222
|
-
hydrateFromStorage,
|
|
223
|
-
getSnapshot,
|
|
224
|
-
subscribe,
|
|
421
|
+
onChange,
|
|
225
422
|
};
|
|
226
423
|
}
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
// Browser mutex — ensures only one tab refreshes a token at a time.
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
227
427
|
async function browserMutex(key, callback) {
|
|
228
428
|
const lockManager = globalThis?.navigator?.locks;
|
|
229
429
|
return lockManager !== undefined
|