use-convex 1.2.0 → 2.0.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.
Files changed (55) hide show
  1. package/README.md +120 -41
  2. package/dist/module.d.mts +73 -31
  3. package/dist/module.mjs +73 -17
  4. package/dist/runtime/auth/oauth.d.ts +96 -0
  5. package/dist/runtime/auth/oauth.js +170 -0
  6. package/dist/runtime/auth/sessionRoutes.d.ts +37 -0
  7. package/dist/runtime/auth/sessionRoutes.js +41 -0
  8. package/dist/runtime/composables/useAuth.d.ts +81 -48
  9. package/dist/runtime/composables/useAuth.js +69 -398
  10. package/dist/runtime/composables/useAuthToken.d.ts +2 -2
  11. package/dist/runtime/composables/useAuthToken.js +4 -0
  12. package/dist/runtime/composables/useConvexAuth.js +1 -1
  13. package/dist/runtime/composables/useConvexPaginatedQuery.js +6 -1
  14. package/dist/runtime/composables/useConvexQueries.d.ts +36 -7
  15. package/dist/runtime/composables/useConvexQueries.js +37 -13
  16. package/dist/runtime/composables/useConvexQuery.js +6 -1
  17. package/dist/runtime/plugin.auth.client.d.ts +8 -3
  18. package/dist/runtime/plugin.auth.client.js +208 -23
  19. package/dist/runtime/plugin.server.js +8 -2
  20. package/dist/runtime/server/auth.d.ts +7 -2
  21. package/dist/runtime/server/auth.js +14 -3
  22. package/dist/runtime/server/convexAuth/refresh.d.ts +3 -0
  23. package/dist/runtime/server/convexAuth/refresh.js +5 -0
  24. package/dist/runtime/server/convexAuth/session.middleware.d.ts +10 -0
  25. package/dist/runtime/server/convexAuth/session.middleware.js +37 -0
  26. package/dist/runtime/server/convexAuth/setup.d.ts +21 -0
  27. package/dist/runtime/server/convexAuth/setup.js +90 -0
  28. package/dist/runtime/server/convexAuth/signin.d.ts +3 -0
  29. package/dist/runtime/server/convexAuth/signin.js +5 -0
  30. package/dist/runtime/server/convexAuth/signout.d.ts +3 -0
  31. package/dist/runtime/server/convexAuth/signout.js +5 -0
  32. package/dist/runtime/server/convexConfig.d.ts +2 -6
  33. package/dist/runtime/server/fetch.d.ts +9 -1
  34. package/dist/runtime/server/fetch.js +7 -10
  35. package/dist/runtime/utils/authClientOptions.d.ts +54 -0
  36. package/dist/runtime/utils/authClientOptions.js +10 -0
  37. package/dist/runtime/utils/authCookie.d.ts +8 -14
  38. package/dist/runtime/utils/authCookie.js +23 -27
  39. package/dist/runtime/utils/authStorage.d.ts +80 -42
  40. package/dist/runtime/utils/authStorage.js +56 -66
  41. package/dist/runtime/utils/context.d.ts +42 -6
  42. package/dist/runtime/utils/http.d.ts +7 -0
  43. package/dist/runtime/utils/http.js +5 -2
  44. package/dist/types.d.mts +6 -2
  45. package/package.json +10 -5
  46. package/dist/runtime/server/api/convex/auth/session.d.ts +0 -36
  47. package/dist/runtime/server/api/convex/auth/session.js +0 -90
  48. package/dist/runtime/server/authCookies.d.ts +0 -9
  49. package/dist/runtime/server/authCookies.js +0 -64
  50. package/dist/runtime/server/sameOrigin.d.ts +0 -14
  51. package/dist/runtime/server/sameOrigin.js +0 -29
  52. package/dist/runtime/utils/authMutex.d.ts +0 -15
  53. package/dist/runtime/utils/authMutex.js +0 -47
  54. package/dist/runtime/utils/oauthRedirect.d.ts +0 -5
  55. package/dist/runtime/utils/oauthRedirect.js +0 -13
@@ -0,0 +1,170 @@
1
+ import { getFunctionName, makeFunctionReference } from "convex/server";
2
+ export const OAUTH_CODE_PARAM = "convexAuthCode";
3
+ export const OAUTH_ERROR_PARAM = "convexAuthError";
4
+ export const OAUTH_SETUP_ID = "oauth";
5
+ export const OAUTH_ACTIONS_KEY = "actions";
6
+ export const OAUTH_FLOW_ERROR_KEY = "flowError";
7
+ const OAUTH_FLOW_STORAGE_KEY = "flow";
8
+ const RETRY_BACKOFF = [500, 2e3];
9
+ function isNetworkError(error) {
10
+ return error instanceof TypeError && /network|failed to fetch|load failed/i.test(error.message);
11
+ }
12
+ async function retryOnNetworkError(fn) {
13
+ for (const backoff of RETRY_BACKOFF) {
14
+ try {
15
+ return await fn();
16
+ } catch (error) {
17
+ if (!isNetworkError(error)) {
18
+ throw error;
19
+ }
20
+ await new Promise((resolve) => setTimeout(resolve, backoff + 100 * Math.random()));
21
+ }
22
+ }
23
+ return await fn();
24
+ }
25
+ function callbackFlowError(param) {
26
+ switch (param) {
27
+ case "access_denied":
28
+ return { error: "ACCESS_DENIED" };
29
+ case "expired":
30
+ return { error: "EXPIRED" };
31
+ default:
32
+ return { error: "OTHER_ERROR", cause: param };
33
+ }
34
+ }
35
+ function currentHref() {
36
+ if (typeof window === "undefined" || window.location === void 0) {
37
+ return null;
38
+ }
39
+ return window.location.href;
40
+ }
41
+ async function takePendingFlow(storage) {
42
+ const raw = await storage.get(OAUTH_FLOW_STORAGE_KEY);
43
+ await storage.remove(OAUTH_FLOW_STORAGE_KEY);
44
+ if (raw === null || raw === void 0) {
45
+ return null;
46
+ }
47
+ try {
48
+ const parsed = JSON.parse(raw);
49
+ if (typeof parsed.providerName !== "string" || typeof parsed.state !== "string" || typeof parsed.completeSignIn !== "string") {
50
+ return null;
51
+ }
52
+ return parsed;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+ async function dropPendingFlow(storage) {
58
+ try {
59
+ await storage.remove(OAUTH_FLOW_STORAGE_KEY);
60
+ } catch {
61
+ }
62
+ }
63
+ export function convexOAuth(options = {}) {
64
+ const setup = (ctx) => {
65
+ const { values, signInApi, convex, replaceUrl } = ctx;
66
+ const flowStorage = options.flowStorage;
67
+ const suffix = (options.namespace ?? "").replace(/[^a-z0-9]/gi, "");
68
+ const flowKey = (key) => `__convexAuthOAuth_${key}_${suffix}`;
69
+ const storage = flowStorage ? {
70
+ get: (key) => flowStorage.getItem(flowKey(key)),
71
+ set: (key, value) => flowStorage.setItem(flowKey(key), value),
72
+ remove: (key) => flowStorage.removeItem(flowKey(key))
73
+ } : ctx.storage;
74
+ const client = ctx.client;
75
+ const setFlowError = (flowError) => {
76
+ values.set(OAUTH_FLOW_ERROR_KEY, flowError);
77
+ };
78
+ const redeem = async (code) => {
79
+ try {
80
+ const pending = await takePendingFlow(storage);
81
+ if (pending === null) {
82
+ return { status: "error", userError: { error: "INVALID_FLOW" } };
83
+ }
84
+ const completeSignIn = makeFunctionReference(pending.completeSignIn);
85
+ const result = await retryOnNetworkError(
86
+ () => signInApi.mutation(completeSignIn, { code, state: pending.state })
87
+ );
88
+ if (result.status === "error") {
89
+ return { status: "error", userError: { error: "EXPIRED" } };
90
+ }
91
+ await client.setSession(result.tokens);
92
+ return { status: "complete" };
93
+ } catch (cause) {
94
+ return { status: "error", userError: { error: "OTHER_ERROR", cause } };
95
+ }
96
+ };
97
+ const handleCallback = () => {
98
+ const href = currentHref();
99
+ if (href === null) {
100
+ return;
101
+ }
102
+ const url = new URL(href);
103
+ const code = url.searchParams.get(OAUTH_CODE_PARAM);
104
+ const errorParam = url.searchParams.get(OAUTH_ERROR_PARAM);
105
+ if (code === null && errorParam === null) {
106
+ return;
107
+ }
108
+ url.searchParams.delete(OAUTH_CODE_PARAM);
109
+ url.searchParams.delete(OAUTH_ERROR_PARAM);
110
+ replaceUrl(url.toString());
111
+ if (errorParam !== null) {
112
+ void dropPendingFlow(storage);
113
+ setFlowError(callbackFlowError(errorParam));
114
+ return;
115
+ }
116
+ if (code === null) {
117
+ return;
118
+ }
119
+ void client.withSignInPending(async () => {
120
+ const result = await redeem(code);
121
+ if (result.status === "error") {
122
+ setFlowError(result.userError);
123
+ }
124
+ });
125
+ };
126
+ const signIn = async (refs, options2) => {
127
+ const code = options2?.code;
128
+ if (code !== void 0) {
129
+ setFlowError(null);
130
+ return await client.withSignInPending(() => redeem(code));
131
+ }
132
+ const href = currentHref();
133
+ const redirectTo = options2?.redirectTo ?? href;
134
+ if (redirectTo === null) {
135
+ throw new Error("`redirectTo` is required where there is no current page URL.");
136
+ }
137
+ setFlowError(null);
138
+ try {
139
+ const { redirect, state } = await convex.mutation(
140
+ refs.startSignIn,
141
+ {
142
+ redirectTo
143
+ }
144
+ );
145
+ await storage.set(
146
+ OAUTH_FLOW_STORAGE_KEY,
147
+ JSON.stringify({
148
+ providerName: refs.providerName,
149
+ state,
150
+ completeSignIn: getFunctionName(refs.completeSignIn)
151
+ })
152
+ );
153
+ const url = new URL(redirect);
154
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
155
+ throw new Error("OAuth redirect must be http(s)");
156
+ }
157
+ if (href !== null) {
158
+ window.location.href = url.toString();
159
+ }
160
+ return { status: "redirect", redirect: url };
161
+ } catch (cause) {
162
+ return { status: "error", userError: { error: "OTHER_ERROR", cause } };
163
+ }
164
+ };
165
+ values.set(OAUTH_ACTIONS_KEY, { signIn });
166
+ setFlowError(null);
167
+ return { onInit: handleCallback };
168
+ };
169
+ return { id: OAUTH_SETUP_ID, setup };
170
+ }
@@ -0,0 +1,37 @@
1
+ /** Client side of the Nitro Convex Auth routes (SSR mode). */
2
+ export type AuthSessionResponse = {
3
+ tokens: {
4
+ accessToken: string;
5
+ userId: string;
6
+ } | null;
7
+ };
8
+ /**
9
+ * POST to the SSR refresh / sign-out route.
10
+ *
11
+ * Accepts a 2xx `{ tokens }` body, and the documented 401 `{ tokens: null }`
12
+ * for a dead session. Anything else (a proxy's HTML 502, a 403 origin
13
+ * refusal, a malformed body) throws, so a temporary outage is not mistaken
14
+ * for "signed out" and the stored session is kept for the next attempt.
15
+ */
16
+ export declare function postAuth(route: string): Promise<AuthSessionResponse>;
17
+ type TokenFetcher = (args: {
18
+ forceRefreshToken: boolean;
19
+ }) => Promise<string | null | undefined>;
20
+ /**
21
+ * Wrap a token fetcher so it never rejects.
22
+ *
23
+ * `ConvexClient` does not catch a throwing fetcher: a refresh that fails
24
+ * during an outage would leave the socket paused with no further refresh
25
+ * scheduled. Instead, retry with capped exponential backoff until the
26
+ * refresh gives a real answer (a token, or `null` for "no session"), and
27
+ * stop early once the session is gone locally (e.g. the user signed out).
28
+ */
29
+ export declare function retryingTokenFetcher(fetchToken: TokenFetcher, options: {
30
+ hasSession: () => boolean;
31
+ onRetry?: (error: unknown, delayMs: number) => void;
32
+ sleep?: (ms: number) => Promise<void>;
33
+ maxDelayMs?: number;
34
+ }): (args: {
35
+ forceRefreshToken: boolean;
36
+ }) => Promise<string | null>;
37
+ export {};
@@ -0,0 +1,41 @@
1
+ function isSessionResponse(body) {
2
+ if (typeof body !== "object" || body === null || !("tokens" in body)) {
3
+ return false;
4
+ }
5
+ const tokens = body.tokens;
6
+ return tokens === null || typeof tokens === "object" && typeof tokens.accessToken === "string" && typeof tokens.userId === "string";
7
+ }
8
+ export async function postAuth(route) {
9
+ const res = await fetch(route, {
10
+ method: "POST",
11
+ headers: { "content-type": "application/json" },
12
+ body: "{}",
13
+ credentials: "same-origin"
14
+ });
15
+ const body = await res.json().catch(() => void 0);
16
+ if (isSessionResponse(body) && (res.ok || res.status === 401 && body.tokens === null)) {
17
+ return body;
18
+ }
19
+ throw new Error(`[use-convex] ${route} failed with ${res.status}`);
20
+ }
21
+ export function retryingTokenFetcher(fetchToken, options) {
22
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
23
+ const maxDelayMs = options.maxDelayMs ?? 3e4;
24
+ return async (args) => {
25
+ for (let attempt = 0; ; attempt++) {
26
+ try {
27
+ return await fetchToken(args) ?? null;
28
+ } catch (error) {
29
+ if (!options.hasSession()) {
30
+ return null;
31
+ }
32
+ const delayMs = Math.min(1e3 * 2 ** attempt, maxDelayMs);
33
+ options.onRetry?.(error, delayMs);
34
+ await sleep(delayMs);
35
+ if (!options.hasSession()) {
36
+ return null;
37
+ }
38
+ }
39
+ }
40
+ };
41
+ }
@@ -1,60 +1,93 @@
1
+ import type { FunctionArgs, FunctionReference, FunctionReturnType } from 'convex/server';
1
2
  import { type ComputedRef, type Ref } from 'vue';
2
- export type AuthTokens = {
3
- token: string;
4
- refreshToken: string;
3
+ import { type UseConvexAuthReturn } from './useConvexAuth.js';
4
+ import { type OAuthCompleteResult, type OAuthFlowError, type OAuthProviderRefs, type OAuthSignInOptions, type OAuthStartResult } from '../auth/oauth.js';
5
+ export type { OAuthCompleteResult, OAuthFlowError, OAuthProviderRefs, OAuthSignInOptions, OAuthStartResult, } from '../auth/oauth.js';
6
+ /** The session a method's sign-in returns (`TokenBundle` / `SlimTokenBundle`). */
7
+ export type ConvexAuthSession = {
8
+ accessToken: string;
9
+ userId: string;
10
+ refreshToken?: string;
11
+ refreshTokenExpiresAt?: number;
5
12
  };
6
- export type SignInResult = {
7
- signingIn: boolean;
8
- redirect?: URL;
13
+ /**
14
+ * The shared sign-in envelope every Convex Auth v2 sign-in function returns,
15
+ * widened with the `OTHER_ERROR` arm for thrown errors.
16
+ */
17
+ export type ConvexSignInResult<Result> = Result | {
18
+ status: 'error';
19
+ userError: {
20
+ error: 'OTHER_ERROR';
21
+ cause: unknown;
22
+ };
9
23
  };
10
- export interface UseAuthReturn {
11
- isLoading: ComputedRef<boolean>;
12
- isAuthenticated: ComputedRef<boolean>;
13
- isRefreshing: ComputedRef<boolean>;
14
- /**
15
- * JWT cookie is present. Use with `isAuthenticated` to keep an SSR-gated
16
- * shell mounted while Convex confirms — do not live-subscribe on this alone.
17
- */
18
- hasSsrSession: ComputedRef<boolean>;
19
- /**
20
- * Mount the signed-in shell when the SSR cookie is present **or** Convex
21
- * has confirmed auth. Prefer this over `isAuthenticated` alone.
22
- * Live subscriptions must still gate on `isAuthenticated`.
23
- */
24
- showAuthedUi: ComputedRef<boolean>;
25
- error: Ref<string | null>;
26
- pending: Ref<boolean>;
27
- signIn: typeof signIn;
28
- signOut: typeof signOut;
24
+ export interface UseAuthReturn extends UseConvexAuthReturn {
25
+ /** Current access token (JWT), or null. Short-lived — don't persist it. */
26
+ token: ComputedRef<string | null>;
27
+ /** Adopt a session returned by a method's sign-in function. */
28
+ setSession: (session: ConvexAuthSession) => Promise<void>;
29
+ /** Revoke the session on the server and clear it locally. */
30
+ signOut: () => Promise<void>;
29
31
  }
30
32
  /**
31
- * First-party Convex Auth session state + actions.
32
- *
33
- * Only auto-imported when `convex.auth.provider === 'convex-auth'`.
34
- * Wire-up happens in `plugin.auth.client` — call `signIn` / `signOut` from forms.
33
+ * Convex Auth v2 session state and actions (React `useAuthActions` +
34
+ * `useConvexAuth` parity): `isLoading`, `isAuthenticated`, `isRefreshing`,
35
+ * `hasSsrSession`, `showAuthedUi`, `token`, `setSession`, `signOut`. Sign in with {@link useConvexSignIn} or
36
+ * {@link useOAuthSignIn}.
35
37
  */
36
38
  export declare function useAuth(): UseAuthReturn;
39
+ /** Revoke the current Convex Auth session and clear it locally. */
40
+ export declare function signOut(): Promise<void>;
41
+ /**
42
+ * Run any Convex Auth v2 sign-in function (username/email + password
43
+ * sign-in or sign-up, anonymous, …) and adopt the session it returns.
44
+ *
45
+ * In SSR mode the call goes through the Nitro auth proxy, so the function
46
+ * must be listed in `convex.auth.signIn`.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * const { run: signIn, pending } = useConvexSignIn(api.auth.signInWithUsernamePassword)
51
+ * const result = await signIn({ username, password })
52
+ * if (result.status === 'error') showError(result.userError)
53
+ * ```
54
+ */
55
+ export declare function useConvexSignIn<Fn extends FunctionReference<'mutation' | 'action', 'public'>>(fn: Fn, options?: {
56
+ /**
57
+ * `FunctionReference` kinds are type-only, so say when the sign-in
58
+ * function is an action. Convex Auth's built-in methods use mutations.
59
+ * @default 'mutation'
60
+ */
61
+ kind?: 'mutation' | 'action';
62
+ }): {
63
+ run: (args: FunctionArgs<Fn>) => Promise<ConvexSignInResult<FunctionReturnType<Fn>>>;
64
+ pending: Readonly<Ref<boolean>>;
65
+ };
37
66
  /**
38
- * Sign in with a Convex Auth provider (password, OAuth, OTP, …).
39
- * Mirrors `@convex-dev/auth/react` `useAuthActions().signIn`.
67
+ * OAuth state not tied to one provider: `flowError` explains why the last
68
+ * redirect-back failed, on whichever page the provider returned to.
40
69
  */
41
- export declare function signIn(provider?: string, params?: FormData | Record<string, string>): Promise<SignInResult>;
70
+ export declare function useOAuth(): {
71
+ flowError: Readonly<Ref<OAuthFlowError | null>>;
72
+ };
42
73
  /**
43
- * Sign out and clear local tokens + SSR JWT cookie.
74
+ * Start a provider's OAuth flow (navigates to the provider), or finish one
75
+ * started elsewhere with `{ code }`.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * const { signIn } = useOAuthSignIn({
80
+ * providerName: 'github',
81
+ * startSignIn: api.auth.startSignInWithGithub,
82
+ * completeSignIn: api.auth.completeSignInWithGithub,
83
+ * })
84
+ * ```
44
85
  */
45
- export declare function signOut(): Promise<void>;
46
- /** @internal */
47
- export declare function hydrateAuthFromStorage(): void;
48
- /** @internal */
49
- export declare function getAuthToken({ forceRefreshToken, }: {
50
- forceRefreshToken: boolean;
51
- }): Promise<string | null>;
52
- /** @internal */
53
- export declare function hasPendingOAuthCallback(): boolean;
54
- /** @internal */
55
- export declare function consumeOAuthCodeFromUrl(): Promise<boolean>;
56
- /** @internal */
57
- export declare function useAuthProviderState(): {
58
- isLoading: ComputedRef<boolean>;
59
- hasSession: ComputedRef<boolean>;
86
+ export declare function useOAuthSignIn(refs: OAuthProviderRefs): {
87
+ signIn: {
88
+ (options: OAuthSignInOptions & {
89
+ code: string;
90
+ }): Promise<OAuthCompleteResult>;
91
+ (options?: Omit<OAuthSignInOptions, 'code'>): Promise<OAuthStartResult>;
92
+ };
60
93
  };