@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.
Files changed (67) hide show
  1. package/dist/client/index.d.ts +84 -30
  2. package/dist/client/index.d.ts.map +1 -1
  3. package/dist/client/index.js +259 -59
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/component/index.d.ts +2 -2
  6. package/dist/component/index.d.ts.map +1 -1
  7. package/dist/component/index.js +2 -2
  8. package/dist/component/index.js.map +1 -1
  9. package/dist/providers/{Anonymous.d.ts → anonymous.d.ts} +8 -8
  10. package/dist/providers/{Anonymous.d.ts.map → anonymous.d.ts.map} +1 -1
  11. package/dist/providers/{Anonymous.js → anonymous.js} +9 -10
  12. package/dist/providers/anonymous.js.map +1 -0
  13. package/dist/providers/{ConvexCredentials.d.ts → credentials.d.ts} +11 -11
  14. package/dist/providers/credentials.d.ts.map +1 -0
  15. package/dist/providers/{ConvexCredentials.js → credentials.js} +8 -8
  16. package/dist/providers/credentials.js.map +1 -0
  17. package/dist/providers/{Email.d.ts → email.d.ts} +6 -6
  18. package/dist/providers/email.d.ts.map +1 -0
  19. package/dist/providers/{Email.js → email.js} +6 -6
  20. package/dist/providers/email.js.map +1 -0
  21. package/dist/providers/{Password.d.ts → password.d.ts} +10 -10
  22. package/dist/providers/{Password.d.ts.map → password.d.ts.map} +1 -1
  23. package/dist/providers/{Password.js → password.js} +19 -20
  24. package/dist/providers/password.js.map +1 -0
  25. package/dist/providers/{Phone.d.ts → phone.d.ts} +3 -3
  26. package/dist/providers/{Phone.d.ts.map → phone.d.ts.map} +1 -1
  27. package/dist/providers/{Phone.js → phone.js} +3 -3
  28. package/dist/providers/{Phone.js.map → phone.js.map} +1 -1
  29. package/dist/server/implementation/index.d.ts +73 -159
  30. package/dist/server/implementation/index.d.ts.map +1 -1
  31. package/dist/server/implementation/index.js +74 -100
  32. package/dist/server/implementation/index.js.map +1 -1
  33. package/dist/server/implementation/sessions.d.ts +2 -20
  34. package/dist/server/implementation/sessions.d.ts.map +1 -1
  35. package/dist/server/implementation/sessions.js +2 -20
  36. package/dist/server/implementation/sessions.js.map +1 -1
  37. package/dist/server/index.d.ts +18 -0
  38. package/dist/server/index.d.ts.map +1 -1
  39. package/dist/server/index.js +255 -0
  40. package/dist/server/index.js.map +1 -1
  41. package/dist/server/provider_utils.d.ts.map +1 -1
  42. package/dist/server/types.d.ts +70 -9
  43. package/dist/server/types.d.ts.map +1 -1
  44. package/package.json +3 -6
  45. package/src/client/index.ts +347 -110
  46. package/src/component/index.ts +1 -8
  47. package/src/providers/{Anonymous.ts → anonymous.ts} +10 -11
  48. package/src/providers/{ConvexCredentials.ts → credentials.ts} +11 -11
  49. package/src/providers/{Email.ts → email.ts} +5 -5
  50. package/src/providers/{Password.ts → password.ts} +22 -27
  51. package/src/providers/{Phone.ts → phone.ts} +2 -2
  52. package/src/server/implementation/index.ts +119 -231
  53. package/src/server/implementation/sessions.ts +2 -20
  54. package/src/server/index.ts +373 -0
  55. package/src/server/types.ts +95 -8
  56. package/dist/providers/Anonymous.js.map +0 -1
  57. package/dist/providers/ConvexCredentials.d.ts.map +0 -1
  58. package/dist/providers/ConvexCredentials.js.map +0 -1
  59. package/dist/providers/Email.d.ts.map +0 -1
  60. package/dist/providers/Email.js.map +0 -1
  61. package/dist/providers/Password.js.map +0 -1
  62. package/providers/Anonymous/package.json +0 -6
  63. package/providers/ConvexCredentials/package.json +0 -6
  64. package/providers/Email/package.json +0 -6
  65. package/providers/Password/package.json +0 -6
  66. package/providers/Phone/package.json +0 -6
  67. package/server/package.json +0 -6
@@ -1,49 +1,103 @@
1
- import { FunctionReference, OptionalRestArgs } from "convex/server";
2
1
  import { Value } from "convex/values";
3
- type AuthActionCaller = {
4
- authenticatedCall<Action extends FunctionReference<"action", "public">>(action: Action, ...args: OptionalRestArgs<Action>): Promise<Action["_returnType"]>;
5
- unauthenticatedCall<Action extends FunctionReference<"action", "public">>(action: Action, ...args: OptionalRestArgs<Action>): Promise<Action["_returnType"]>;
6
- verbose?: boolean;
7
- logger?: {
8
- logVerbose?: (message: string) => void;
9
- };
10
- };
11
- export interface TokenStorage {
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
- export type AuthSession = {
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
- export type AuthSnapshot = {
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
- export type AuthClientOptions = {
30
- transport: AuthActionCaller;
31
- storage?: TokenStorage | null;
32
- storageNamespace: string;
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
- shouldHandleCode?: (() => boolean) | boolean;
35
- onChange?: () => Promise<unknown>;
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
- export declare function createAuthClient(options: AuthClientOptions): {
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
- fetchAccessToken: ({ forceRefreshToken, }: {
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":"AAAA,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAMtC,KAAK,gBAAgB,GAAG;IACtB,iBAAiB,CAAC,MAAM,SAAS,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,CAAC,EACpE,MAAM,EAAE,MAAM,EACd,GAAG,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC,GAChC,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC;IAClC,mBAAmB,CAAC,MAAM,SAAS,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,CAAC,EACtE,MAAM,EAAE,MAAM,EACd,GAAG,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC,GAChC,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE;QACP,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;KACxC,CAAC;CACH,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACrF,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;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,SAAS,EAAE,gBAAgB,CAAC;IAC5B,OAAO,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAC9B,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,gBAAgB,CAAC,EAAE,CAAC,MAAM,OAAO,CAAC,GAAG,OAAO,CAAC;IAC7C,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACnC,CAAC;AASF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB;wBAoI5C,MAAM,SACV,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,KACtC,OAAO,CAAC,YAAY,CAAC;;+CAiDrB;QACD,iBAAiB,EAAE,OAAO,CAAC;KAC5B,KAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;;;uBAoDF,YAAY;oBAEb,MAAM,IAAI;EA8BlC"}
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"}
@@ -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
- export function createAuthClient(options) {
7
- const { transport, storage = typeof window === "undefined" ? null : window.localStorage, storageNamespace, replaceURL = (url) => {
8
- if (typeof window !== "undefined") {
9
- window.history.replaceState({}, "", url);
10
- }
11
- }, shouldHandleCode, onChange, } = options;
12
- const escapedNamespace = storageNamespace.replace(/[^a-zA-Z0-9]/g, "");
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
- let token = null;
16
- let isLoading = true;
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: false,
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 nextSnapshot = {
93
+ const next = {
35
94
  isLoading,
36
95
  isAuthenticated: token !== null,
37
96
  token,
38
97
  };
39
- if (snapshot.isLoading === nextSnapshot.isLoading &&
40
- snapshot.isAuthenticated === nextSnapshot.isAuthenticated &&
41
- snapshot.token === nextSnapshot.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 = nextSnapshot;
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 transport.unauthenticatedCall("auth:signIn", "code" in args
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({ shouldStore: true, tokens: tokens ?? null });
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 transport.authenticatedCall("auth:signIn", { provider, params, verifier });
229
+ const result = await convex.action("auth:signIn", {
230
+ provider,
231
+ params,
232
+ verifier,
233
+ });
119
234
  if (result.redirect !== undefined) {
120
- const url = new URL(result.redirect);
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 = url.toString();
238
+ window.location.href = redirectUrl.toString();
124
239
  }
125
- return { signingIn: false, redirect: url };
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 transport.authenticatedCall("auth:signOut");
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 url = new URL(window.location.href);
181
- url.searchParams.delete("code");
336
+ const codeUrl = new URL(window.location.href);
337
+ codeUrl.searchParams.delete("code");
182
338
  try {
183
- await replaceURL(url.pathname + url.search + url.hash);
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
- const getSnapshot = () => snapshot;
198
- const subscribe = (cb) => {
199
- subscribers.add(cb);
200
- return () => subscribers.delete(cb);
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
- if (typeof window !== "undefined") {
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: value === null ? null : { token: value },
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
- fetchAccessToken,
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