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.
- package/README.md +120 -41
- package/dist/module.d.mts +73 -31
- package/dist/module.mjs +73 -17
- package/dist/runtime/auth/oauth.d.ts +96 -0
- package/dist/runtime/auth/oauth.js +170 -0
- package/dist/runtime/auth/sessionRoutes.d.ts +37 -0
- package/dist/runtime/auth/sessionRoutes.js +41 -0
- package/dist/runtime/composables/useAuth.d.ts +81 -48
- package/dist/runtime/composables/useAuth.js +69 -398
- package/dist/runtime/composables/useAuthToken.d.ts +2 -2
- package/dist/runtime/composables/useAuthToken.js +4 -0
- package/dist/runtime/composables/useConvexAuth.js +1 -1
- package/dist/runtime/composables/useConvexPaginatedQuery.js +6 -1
- package/dist/runtime/composables/useConvexQueries.d.ts +36 -7
- package/dist/runtime/composables/useConvexQueries.js +37 -13
- package/dist/runtime/composables/useConvexQuery.js +6 -1
- package/dist/runtime/plugin.auth.client.d.ts +8 -3
- package/dist/runtime/plugin.auth.client.js +208 -23
- package/dist/runtime/plugin.server.js +8 -2
- package/dist/runtime/server/auth.d.ts +7 -2
- package/dist/runtime/server/auth.js +14 -3
- package/dist/runtime/server/convexAuth/refresh.d.ts +3 -0
- package/dist/runtime/server/convexAuth/refresh.js +5 -0
- package/dist/runtime/server/convexAuth/session.middleware.d.ts +10 -0
- package/dist/runtime/server/convexAuth/session.middleware.js +37 -0
- package/dist/runtime/server/convexAuth/setup.d.ts +21 -0
- package/dist/runtime/server/convexAuth/setup.js +90 -0
- package/dist/runtime/server/convexAuth/signin.d.ts +3 -0
- package/dist/runtime/server/convexAuth/signin.js +5 -0
- package/dist/runtime/server/convexAuth/signout.d.ts +3 -0
- package/dist/runtime/server/convexAuth/signout.js +5 -0
- package/dist/runtime/server/convexConfig.d.ts +2 -6
- package/dist/runtime/server/fetch.d.ts +9 -1
- package/dist/runtime/server/fetch.js +7 -10
- package/dist/runtime/utils/authClientOptions.d.ts +54 -0
- package/dist/runtime/utils/authClientOptions.js +10 -0
- package/dist/runtime/utils/authCookie.d.ts +8 -14
- package/dist/runtime/utils/authCookie.js +23 -27
- package/dist/runtime/utils/authStorage.d.ts +80 -42
- package/dist/runtime/utils/authStorage.js +56 -66
- package/dist/runtime/utils/context.d.ts +42 -6
- package/dist/runtime/utils/http.d.ts +7 -0
- package/dist/runtime/utils/http.js +5 -2
- package/dist/types.d.mts +6 -2
- package/package.json +10 -5
- package/dist/runtime/server/api/convex/auth/session.d.ts +0 -36
- package/dist/runtime/server/api/convex/auth/session.js +0 -90
- package/dist/runtime/server/authCookies.d.ts +0 -9
- package/dist/runtime/server/authCookies.js +0 -64
- package/dist/runtime/server/sameOrigin.d.ts +0 -14
- package/dist/runtime/server/sameOrigin.js +0 -29
- package/dist/runtime/utils/authMutex.d.ts +0 -15
- package/dist/runtime/utils/authMutex.js +0 -47
- package/dist/runtime/utils/oauthRedirect.d.ts +0 -5
- package/dist/runtime/utils/oauthRedirect.js +0 -13
|
@@ -1,413 +1,84 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { computed, nextTick } from "vue";
|
|
4
|
-
import { useRuntimeConfig, useState } from "nuxt/app";
|
|
1
|
+
import { computed, ref, shallowRef } from "vue";
|
|
2
|
+
import { useConvexContext } from "../utils/context.js";
|
|
5
3
|
import { useConvexAuth } from "./useConvexAuth.js";
|
|
6
|
-
import { missingConvexUrlError, unreachableConvexUrlError } from "../utils/errors.js";
|
|
7
|
-
import { withRefreshMutex } from "../utils/authMutex.js";
|
|
8
|
-
import { useAuthJwtCookie, useAuthPresentCookie } from "../utils/authCookie.js";
|
|
9
4
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
5
|
+
OAUTH_ACTIONS_KEY,
|
|
6
|
+
OAUTH_FLOW_ERROR_KEY,
|
|
7
|
+
OAUTH_SETUP_ID
|
|
8
|
+
} from "../auth/oauth.js";
|
|
9
|
+
function useBinding(name) {
|
|
10
|
+
const ctx = useConvexContext();
|
|
11
|
+
if (!import.meta.server && !ctx.convexAuth) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
`[use-convex] ${name} requires \`convex.auth.provider: 'convex-auth'\` in nuxt.config.`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return ctx.convexAuth ?? null;
|
|
17
|
+
}
|
|
18
|
+
function requireBinding(name) {
|
|
19
|
+
const binding = useConvexContext().convexAuth;
|
|
20
|
+
if (!binding) {
|
|
21
|
+
throw new Error(`[use-convex] ${name} is browser-only and requires Convex Auth.`);
|
|
22
|
+
}
|
|
23
|
+
return binding;
|
|
24
|
+
}
|
|
26
25
|
export function useAuth() {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
isLoading: computed(() => session.isLoading.value),
|
|
31
|
-
isAuthenticated: computed(() => false),
|
|
32
|
-
isRefreshing: computed(() => false),
|
|
33
|
-
hasSsrSession: computed(() => false),
|
|
34
|
-
showAuthedUi: computed(() => false),
|
|
35
|
-
error: session.error,
|
|
36
|
-
pending: session.pending,
|
|
37
|
-
signIn,
|
|
38
|
-
signOut
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
const convexAuth = useConvexAuth();
|
|
26
|
+
const binding = useBinding("useAuth");
|
|
27
|
+
const auth = useConvexAuth();
|
|
42
28
|
return {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
showAuthedUi: convexAuth.showAuthedUi,
|
|
48
|
-
error: session.error,
|
|
49
|
-
pending: session.pending,
|
|
50
|
-
signIn,
|
|
51
|
-
signOut
|
|
29
|
+
...auth,
|
|
30
|
+
token: computed(() => binding?.state.value.token ?? null),
|
|
31
|
+
setSession: (session) => requireBinding("setSession").setSession(session),
|
|
32
|
+
signOut: () => signOut()
|
|
52
33
|
};
|
|
53
34
|
}
|
|
54
|
-
export async function signIn(provider, params) {
|
|
55
|
-
const session = useAuthSession();
|
|
56
|
-
const convexUrl = session.convexUrl;
|
|
57
|
-
if (!convexUrl) {
|
|
58
|
-
throw missingConvexUrlError("signIn");
|
|
59
|
-
}
|
|
60
|
-
session.pending.value = true;
|
|
61
|
-
session.error.value = null;
|
|
62
|
-
try {
|
|
63
|
-
const flatParams = flattenSignInParams(params);
|
|
64
|
-
const existingVerifier = readLocal(session.verifierKey.value) ?? void 0;
|
|
65
|
-
writeLocal(session.verifierKey.value, null);
|
|
66
|
-
const result = await callSignIn(convexUrl, {
|
|
67
|
-
provider,
|
|
68
|
-
params: flatParams,
|
|
69
|
-
verifier: existingVerifier
|
|
70
|
-
});
|
|
71
|
-
if (result.redirect !== void 0) {
|
|
72
|
-
const url = parseOAuthRedirect(result.redirect);
|
|
73
|
-
if (result.verifier) {
|
|
74
|
-
writeLocal(session.verifierKey.value, result.verifier);
|
|
75
|
-
}
|
|
76
|
-
if (import.meta.client) {
|
|
77
|
-
window.location.href = url.toString();
|
|
78
|
-
}
|
|
79
|
-
return { signingIn: false, redirect: url };
|
|
80
|
-
}
|
|
81
|
-
if (result.tokens !== void 0) {
|
|
82
|
-
await persistTokens(session, result.tokens);
|
|
83
|
-
return { signingIn: result.tokens !== null };
|
|
84
|
-
}
|
|
85
|
-
return { signingIn: false };
|
|
86
|
-
} catch (cause) {
|
|
87
|
-
const message = isNetworkError(cause) && convexUrl ? unreachableConvexUrlError(convexUrl).message : cause instanceof Error ? cause.message : "Authentication failed";
|
|
88
|
-
session.error.value = message;
|
|
89
|
-
throw cause;
|
|
90
|
-
} finally {
|
|
91
|
-
session.pending.value = false;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
35
|
export async function signOut() {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
try {
|
|
107
|
-
await http.action(authSignOut, {});
|
|
108
|
-
} catch {
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
await persistTokens(session, null);
|
|
112
|
-
} finally {
|
|
113
|
-
session.pending.value = false;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
export function hydrateAuthFromStorage() {
|
|
117
|
-
const session = useAuthSession();
|
|
118
|
-
if (!session.convexUrl) {
|
|
119
|
-
session.isLoading.value = false;
|
|
120
|
-
session.hasSession.value = false;
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
if (session.httpOnly) {
|
|
124
|
-
const present = useAuthPresentCookie();
|
|
125
|
-
if (present?.value === "1") {
|
|
126
|
-
session.hasSession.value = true;
|
|
127
|
-
session.isLoading.value = false;
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
session.isLoading.value = true;
|
|
131
|
-
void fetchAuthSession().then((result) => {
|
|
132
|
-
session.hasSession.value = result.hasSession;
|
|
133
|
-
session.isLoading.value = false;
|
|
134
|
-
if (result.hasSession) {
|
|
135
|
-
const marker = useAuthPresentCookie();
|
|
136
|
-
if (marker) {
|
|
137
|
-
marker.value = "1";
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
const token = readLocal(session.jwtKey.value);
|
|
144
|
-
session.hasSession.value = token !== null;
|
|
145
|
-
session.isLoading.value = false;
|
|
146
|
-
if (token) {
|
|
147
|
-
setJwtCookie(token);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
export async function getAuthToken({
|
|
151
|
-
forceRefreshToken
|
|
152
|
-
}) {
|
|
153
|
-
const session = useAuthSession();
|
|
154
|
-
const convexUrl = session.convexUrl;
|
|
155
|
-
if (!convexUrl) {
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
if (session.httpOnly) {
|
|
159
|
-
return withRefreshMutex(session.refreshKey.value, async () => {
|
|
160
|
-
if (!forceRefreshToken) {
|
|
161
|
-
const current = await fetchAuthSessionToken();
|
|
162
|
-
if (current.token) {
|
|
163
|
-
session.hasSession.value = true;
|
|
164
|
-
return current.token;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
const refreshed = await refreshAuthSession();
|
|
168
|
-
if (!refreshed) {
|
|
169
|
-
await clearAuthSession();
|
|
170
|
-
session.hasSession.value = false;
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
session.hasSession.value = true;
|
|
174
|
-
return refreshed;
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
if (!forceRefreshToken) {
|
|
178
|
-
return readLocal(session.jwtKey.value);
|
|
179
|
-
}
|
|
180
|
-
return withRefreshMutex(session.refreshKey.value, async () => {
|
|
181
|
-
const refreshToken = readLocal(session.refreshKey.value);
|
|
182
|
-
if (!refreshToken) {
|
|
183
|
-
persistTokens(session, null);
|
|
184
|
-
return null;
|
|
36
|
+
await requireBinding("signOut").signOut();
|
|
37
|
+
}
|
|
38
|
+
export function useConvexSignIn(fn, options = {}) {
|
|
39
|
+
useBinding("useConvexSignIn");
|
|
40
|
+
const ctx = useConvexContext();
|
|
41
|
+
const pending = ref(false);
|
|
42
|
+
const run = async (args) => {
|
|
43
|
+
const binding = ctx.convexAuth;
|
|
44
|
+
if (!binding) {
|
|
45
|
+
throw new Error("[use-convex] useConvexSignIn().run is browser-only.");
|
|
185
46
|
}
|
|
47
|
+
pending.value = true;
|
|
186
48
|
try {
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
return null;
|
|
49
|
+
const call = options.kind === "action" ? binding.signInApi.action : binding.signInApi.mutation;
|
|
50
|
+
const result = await call(fn, args);
|
|
51
|
+
if (result?.status === "complete" && result.tokens) {
|
|
52
|
+
await binding.setSession(result.tokens);
|
|
192
53
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
54
|
+
return result;
|
|
55
|
+
} catch (cause) {
|
|
56
|
+
return { status: "error", userError: { error: "OTHER_ERROR", cause } };
|
|
57
|
+
} finally {
|
|
58
|
+
pending.value = false;
|
|
198
59
|
}
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
export function hasPendingOAuthCallback() {
|
|
202
|
-
if (!import.meta.client) {
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
|
-
const session = useAuthSession();
|
|
206
|
-
if (!session.convexUrl) {
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
const code = new URLSearchParams(window.location.search).get("code");
|
|
210
|
-
const verifier = readLocal(session.verifierKey.value);
|
|
211
|
-
return shouldConsumeOAuthCode({ code, verifier });
|
|
212
|
-
}
|
|
213
|
-
export async function consumeOAuthCodeFromUrl() {
|
|
214
|
-
if (!import.meta.client) {
|
|
215
|
-
return false;
|
|
216
|
-
}
|
|
217
|
-
const session = useAuthSession();
|
|
218
|
-
if (!session.convexUrl) {
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
const code = new URLSearchParams(window.location.search).get("code");
|
|
222
|
-
const verifier = readLocal(session.verifierKey.value);
|
|
223
|
-
if (!shouldConsumeOAuthCode({ code, verifier })) {
|
|
224
|
-
return false;
|
|
225
|
-
}
|
|
226
|
-
const url = new URL(window.location.href);
|
|
227
|
-
url.searchParams.delete("code");
|
|
228
|
-
window.history.replaceState({}, "", url.pathname + url.search + url.hash);
|
|
229
|
-
session.pending.value = true;
|
|
230
|
-
session.error.value = null;
|
|
231
|
-
session.isLoading.value = true;
|
|
232
|
-
try {
|
|
233
|
-
writeLocal(session.verifierKey.value, null);
|
|
234
|
-
const result = await callSignInWithRetry(session.convexUrl, {
|
|
235
|
-
params: { code },
|
|
236
|
-
verifier
|
|
237
|
-
});
|
|
238
|
-
await persistTokens(session, result.tokens ?? null);
|
|
239
|
-
return result.tokens != null;
|
|
240
|
-
} catch (cause) {
|
|
241
|
-
const message = cause instanceof Error ? cause.message : "OAuth callback failed";
|
|
242
|
-
session.error.value = message;
|
|
243
|
-
await persistTokens(session, null);
|
|
244
|
-
return false;
|
|
245
|
-
} finally {
|
|
246
|
-
session.pending.value = false;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
export function useAuthProviderState() {
|
|
250
|
-
const session = useAuthSession();
|
|
251
|
-
return {
|
|
252
|
-
isLoading: computed(() => session.isLoading.value),
|
|
253
|
-
hasSession: computed(() => session.hasSession.value)
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
function useAuthSession() {
|
|
257
|
-
const config = useRuntimeConfig();
|
|
258
|
-
const convexConfig = config.public.convex;
|
|
259
|
-
const convexUrl = convexConfig?.url;
|
|
260
|
-
const httpOnly = isHttpOnlyAuth(convexConfig?.auth);
|
|
261
|
-
const isLoading = useState("convex-auth-loading", () => true);
|
|
262
|
-
const hasSession = useState("convex-auth-has-session", () => false);
|
|
263
|
-
const error = useState("convex-auth-error", () => null);
|
|
264
|
-
const pending = useState("convex-auth-pending", () => false);
|
|
265
|
-
const jwtKey = computed(
|
|
266
|
-
() => convexUrl ? storageKey(JWT_STORAGE_KEY, convexUrl) : JWT_STORAGE_KEY
|
|
267
|
-
);
|
|
268
|
-
const refreshKey = computed(
|
|
269
|
-
() => convexUrl ? storageKey(REFRESH_TOKEN_STORAGE_KEY, convexUrl) : REFRESH_TOKEN_STORAGE_KEY
|
|
270
|
-
);
|
|
271
|
-
const verifierKey = computed(
|
|
272
|
-
() => convexUrl ? storageKey(VERIFIER_STORAGE_KEY, convexUrl) : VERIFIER_STORAGE_KEY
|
|
273
|
-
);
|
|
274
|
-
return {
|
|
275
|
-
convexUrl,
|
|
276
|
-
httpOnly,
|
|
277
|
-
jwtKey,
|
|
278
|
-
refreshKey,
|
|
279
|
-
verifierKey,
|
|
280
|
-
isLoading,
|
|
281
|
-
hasSession,
|
|
282
|
-
error,
|
|
283
|
-
pending
|
|
284
60
|
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
method: "GET"
|
|
305
|
-
});
|
|
306
|
-
} catch {
|
|
307
|
-
return { hasSession: false };
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
async function fetchAuthSessionToken() {
|
|
311
|
-
try {
|
|
312
|
-
return await $fetch(AUTH_SESSION_PATH, {
|
|
313
|
-
method: "POST",
|
|
314
|
-
body: { getToken: true }
|
|
315
|
-
});
|
|
316
|
-
} catch {
|
|
317
|
-
return { hasSession: false, token: null };
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
async function refreshAuthSession() {
|
|
321
|
-
try {
|
|
322
|
-
const result = await $fetch(AUTH_SESSION_PATH, {
|
|
323
|
-
method: "POST",
|
|
324
|
-
body: { refresh: true }
|
|
325
|
-
});
|
|
326
|
-
return result.token;
|
|
327
|
-
} catch {
|
|
328
|
-
return null;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
async function clearAuthSession() {
|
|
332
|
-
try {
|
|
333
|
-
await $fetch(AUTH_SESSION_PATH, { method: "DELETE" });
|
|
334
|
-
} catch {
|
|
335
|
-
}
|
|
336
|
-
setPresentCookie(null);
|
|
337
|
-
}
|
|
338
|
-
async function writeHttpOnlySession(tokens) {
|
|
339
|
-
if (tokens === null) {
|
|
340
|
-
await clearAuthSession();
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
await $fetch(AUTH_SESSION_PATH, {
|
|
344
|
-
method: "POST",
|
|
345
|
-
body: {
|
|
346
|
-
token: tokens.token,
|
|
347
|
-
refreshToken: tokens.refreshToken
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
setPresentCookie("1");
|
|
351
|
-
}
|
|
352
|
-
async function persistTokens(session, tokens) {
|
|
353
|
-
if (session.httpOnly) {
|
|
354
|
-
if (tokens === null) {
|
|
355
|
-
await writeHttpOnlySession(null);
|
|
356
|
-
writeLocal(session.jwtKey.value, null);
|
|
357
|
-
writeLocal(session.refreshKey.value, null);
|
|
358
|
-
session.hasSession.value = false;
|
|
359
|
-
session.isLoading.value = false;
|
|
360
|
-
return;
|
|
61
|
+
return { run, pending };
|
|
62
|
+
}
|
|
63
|
+
export function useOAuth() {
|
|
64
|
+
const binding = useBinding("useOAuth");
|
|
65
|
+
if (!binding) {
|
|
66
|
+
return { flowError: shallowRef(null) };
|
|
67
|
+
}
|
|
68
|
+
const value = binding.ambientValue(OAUTH_SETUP_ID, OAUTH_FLOW_ERROR_KEY);
|
|
69
|
+
return { flowError: computed(() => value.value ?? null) };
|
|
70
|
+
}
|
|
71
|
+
export function useOAuthSignIn(refs) {
|
|
72
|
+
const binding = useBinding("useOAuthSignIn");
|
|
73
|
+
const actions = binding?.ambientValue(OAUTH_SETUP_ID, OAUTH_ACTIONS_KEY);
|
|
74
|
+
const signIn = (async (options) => {
|
|
75
|
+
const current = actions?.value;
|
|
76
|
+
if (!current) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"[use-convex] OAuth is not registered. Include convexOAuth() in configureConvexAuth({ ambientSignIns })."
|
|
79
|
+
);
|
|
361
80
|
}
|
|
362
|
-
|
|
363
|
-
session.isLoading.value = false;
|
|
364
|
-
await writeHttpOnlySession(tokens);
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
if (tokens === null) {
|
|
368
|
-
writeLocal(session.jwtKey.value, null);
|
|
369
|
-
writeLocal(session.refreshKey.value, null);
|
|
370
|
-
setJwtCookie(null);
|
|
371
|
-
session.hasSession.value = false;
|
|
372
|
-
session.isLoading.value = false;
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
writeLocal(session.jwtKey.value, tokens.token);
|
|
376
|
-
writeLocal(session.refreshKey.value, tokens.refreshToken);
|
|
377
|
-
session.hasSession.value = true;
|
|
378
|
-
session.isLoading.value = false;
|
|
379
|
-
nextTick(() => {
|
|
380
|
-
setJwtCookie(tokens.token);
|
|
81
|
+
return await current.signIn(refs, options);
|
|
381
82
|
});
|
|
382
|
-
}
|
|
383
|
-
async function callSignIn(convexUrl, args) {
|
|
384
|
-
const http = new ConvexHttpClient(convexUrl);
|
|
385
|
-
return await http.action(authSignIn, args);
|
|
386
|
-
}
|
|
387
|
-
async function callSignInWithRetry(convexUrl, args) {
|
|
388
|
-
let lastError;
|
|
389
|
-
let retry = 0;
|
|
390
|
-
while (retry < RETRY_BACKOFF.length) {
|
|
391
|
-
try {
|
|
392
|
-
return await callSignIn(convexUrl, args);
|
|
393
|
-
} catch (e) {
|
|
394
|
-
lastError = e;
|
|
395
|
-
if (!isNetworkError(e)) {
|
|
396
|
-
break;
|
|
397
|
-
}
|
|
398
|
-
const wait = RETRY_BACKOFF[retry] + RETRY_JITTER * Math.random();
|
|
399
|
-
retry++;
|
|
400
|
-
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
throw lastError;
|
|
404
|
-
}
|
|
405
|
-
function isNetworkError(error) {
|
|
406
|
-
if (error instanceof TypeError) {
|
|
407
|
-
return true;
|
|
408
|
-
}
|
|
409
|
-
if (error instanceof Error && /network|fetch|Failed to fetch/i.test(error.message)) {
|
|
410
|
-
return true;
|
|
411
|
-
}
|
|
412
|
-
return false;
|
|
83
|
+
return { signIn };
|
|
413
84
|
}
|
|
@@ -4,8 +4,8 @@ import { type ComputedRef } from 'vue';
|
|
|
4
4
|
* Current JWT for authenticated HTTP calls (React `@convex-dev/auth`
|
|
5
5
|
* `useAuthToken` parity).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* (
|
|
7
|
+
* Uses the Convex Auth client token when Convex Auth is enabled; otherwise
|
|
8
|
+
* `ConvexClient.getAuth()`, then the configured BYO `fetchToken`. Returns `null` when signed out or unavailable.
|
|
9
9
|
*/
|
|
10
10
|
export declare function useAuthToken(): ComputedRef<string | null>;
|
|
11
11
|
/**
|
|
@@ -9,6 +9,10 @@ export function useAuthToken() {
|
|
|
9
9
|
return computed(() => null);
|
|
10
10
|
}
|
|
11
11
|
const auth = useConvexAuth();
|
|
12
|
+
const binding = ctx.convexAuth;
|
|
13
|
+
if (binding) {
|
|
14
|
+
return computed(() => binding.state.value.token);
|
|
15
|
+
}
|
|
12
16
|
const refresh = async () => {
|
|
13
17
|
if (!ctx.client) {
|
|
14
18
|
token.value = null;
|
|
@@ -61,7 +61,7 @@ export function useConvexAuth(setup) {
|
|
|
61
61
|
);
|
|
62
62
|
};
|
|
63
63
|
watch(
|
|
64
|
-
() =>
|
|
64
|
+
[() => toValue(setup.isLoading) ?? false, () => toValue(setup.isAuthenticated) ?? true],
|
|
65
65
|
syncFromProvider,
|
|
66
66
|
{ immediate: true }
|
|
67
67
|
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sessionKeyFromToken } from "../utils/authStorage.js";
|
|
1
2
|
import { convexToJson, jsonToConvex } from "convex/values";
|
|
2
3
|
import { useAsyncData, useRuntimeConfig } from "nuxt/app";
|
|
3
4
|
import {
|
|
@@ -60,7 +61,11 @@ export async function useConvexPaginatedQuery(query, args = {}, options) {
|
|
|
60
61
|
server,
|
|
61
62
|
watch: [
|
|
62
63
|
() => toValue(args),
|
|
63
|
-
()
|
|
64
|
+
// Session identity (JWT iss + sub), not the raw token: Convex Auth
|
|
65
|
+
// rotates the access token every minute without changing session,
|
|
66
|
+
// but switching accounts must re-run the snapshot (`live: false`
|
|
67
|
+
// and BYO cookies included).
|
|
68
|
+
() => sessionKeyFromToken(options.token ?? ctx.ssrToken.value),
|
|
64
69
|
() => requireAuth && ctx.auth.isAuthenticated.value && !!(options.token ?? ctx.ssrToken.value)
|
|
65
70
|
]
|
|
66
71
|
}
|
|
@@ -5,10 +5,16 @@ import type { ConvexRef } from '../utils/functionReference.js';
|
|
|
5
5
|
export type ConvexQueryRequestEntry<Query extends ConvexRef<'query'> = ConvexRef<'query'>> = {
|
|
6
6
|
query: Query;
|
|
7
7
|
args: FunctionArgs<Query>;
|
|
8
|
+
/**
|
|
9
|
+
* Skip this entry until Convex confirms auth (`isAuthenticated`).
|
|
10
|
+
* Overrides the top-level {@link UseConvexQueriesOptions.authenticated}
|
|
11
|
+
* flag when set.
|
|
12
|
+
*/
|
|
13
|
+
authenticated?: boolean;
|
|
8
14
|
};
|
|
9
15
|
/**
|
|
10
16
|
* Request map for {@link useConvexQueries}.
|
|
11
|
-
* Values are `{ query, args }` or `'skip'`.
|
|
17
|
+
* Values are `{ query, args, authenticated? }` or `'skip'`.
|
|
12
18
|
*/
|
|
13
19
|
export type ConvexQueriesRequest = Record<string, ConvexQueryRequestEntry | 'skip'>;
|
|
14
20
|
export type ConvexQueriesResult<Request extends ConvexQueriesRequest> = {
|
|
@@ -17,17 +23,40 @@ export type ConvexQueriesResult<Request extends ConvexQueriesRequest> = {
|
|
|
17
23
|
args: infer _Args;
|
|
18
24
|
} ? Query extends ConvexRef<'query'> ? FunctionReturnType<Query> | undefined | Error : undefined : undefined;
|
|
19
25
|
};
|
|
26
|
+
export interface UseConvexQueriesOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Skip every entry until Convex confirms auth (`isAuthenticated`).
|
|
29
|
+
* Prefer this over wrapping each value in `'skip'`. Per-entry
|
|
30
|
+
* `authenticated` overrides this when set.
|
|
31
|
+
*
|
|
32
|
+
* @default false
|
|
33
|
+
*/
|
|
34
|
+
authenticated?: boolean;
|
|
35
|
+
}
|
|
20
36
|
/**
|
|
21
37
|
* Subscribe to a dynamic map of Convex queries (React `useQueries` parity).
|
|
22
38
|
*
|
|
23
|
-
* Pass a reactive object whose values are `{ query, args }`
|
|
24
|
-
* Returns a computed map of the same keys → result |
|
|
25
|
-
* `Error`.
|
|
39
|
+
* Pass a reactive object whose values are `{ query, args, authenticated? }`
|
|
40
|
+
* or `'skip'`. Returns a computed map of the same keys → result |
|
|
41
|
+
* `undefined` (loading) | `Error`.
|
|
26
42
|
*
|
|
27
43
|
* Browser-only live subscriptions. During SSR every entry is `undefined`
|
|
28
44
|
* (combine with `useConvexQuery` when you need SSR for known queries).
|
|
29
45
|
*
|
|
30
|
-
* Unchanged keys (same function name + args) keep their
|
|
31
|
-
* so deep reactive churn does not tear down
|
|
46
|
+
* Unchanged keys (same function name + args + auth-skip) keep their
|
|
47
|
+
* existing subscription so deep reactive churn does not tear down
|
|
48
|
+
* WebSocket watches.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* // Gate the whole map until Convex confirms auth
|
|
53
|
+
* const results = useConvexQueries(
|
|
54
|
+
* () => ({
|
|
55
|
+
* tasks: { query: api.tasks.list, args: {} },
|
|
56
|
+
* files: showFiles.value ? { query: api.files.list, args: {} } : 'skip',
|
|
57
|
+
* }),
|
|
58
|
+
* { authenticated: true },
|
|
59
|
+
* )
|
|
60
|
+
* ```
|
|
32
61
|
*/
|
|
33
|
-
export declare function useConvexQueries<Request extends ConvexQueriesRequest>(queries: MaybeRefOrGetter<Request
|
|
62
|
+
export declare function useConvexQueries<Request extends ConvexQueriesRequest>(queries: MaybeRefOrGetter<Request>, options?: UseConvexQueriesOptions): ComputedRef<ConvexQueriesResult<Request>>;
|
|
@@ -7,13 +7,18 @@ import {
|
|
|
7
7
|
toValue,
|
|
8
8
|
watch
|
|
9
9
|
} from "vue";
|
|
10
|
+
import { resolveAuthGatedArgs } from "../utils/authGate.js";
|
|
10
11
|
import { useConvexContext } from "../utils/context.js";
|
|
11
|
-
function subscriptionSignature(query, args) {
|
|
12
|
-
return `${getFunctionName(query)}:${JSON.stringify(convexToJson(args))}`;
|
|
12
|
+
function subscriptionSignature(query, args, authSkipped) {
|
|
13
|
+
return `${getFunctionName(query)}:${JSON.stringify(convexToJson(args))}:authSkip=${authSkipped}`;
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
+
function entryRequiresAuth(entry, defaultAuthenticated) {
|
|
16
|
+
return entry.authenticated ?? defaultAuthenticated;
|
|
17
|
+
}
|
|
18
|
+
export function useConvexQueries(queries, options = {}) {
|
|
15
19
|
const ctx = useConvexContext();
|
|
16
20
|
const results = shallowRef({});
|
|
21
|
+
const defaultAuthenticated = options.authenticated ?? false;
|
|
17
22
|
if (import.meta.server || !ctx.client) {
|
|
18
23
|
return computed(() => results.value);
|
|
19
24
|
}
|
|
@@ -29,9 +34,10 @@ export function useConvexQueries(queries) {
|
|
|
29
34
|
results.value = rest;
|
|
30
35
|
}
|
|
31
36
|
};
|
|
32
|
-
const sync = (request) => {
|
|
37
|
+
const sync = (request, isAuthenticated) => {
|
|
33
38
|
const nextKeys = new Set(Object.keys(request));
|
|
34
|
-
|
|
39
|
+
const known = /* @__PURE__ */ new Set([...unsubscribers.keys(), ...signatures.keys()]);
|
|
40
|
+
for (const key of known) {
|
|
35
41
|
if (!nextKeys.has(key)) {
|
|
36
42
|
dropKey(key);
|
|
37
43
|
}
|
|
@@ -41,15 +47,33 @@ export function useConvexQueries(queries) {
|
|
|
41
47
|
dropKey(key);
|
|
42
48
|
continue;
|
|
43
49
|
}
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
50
|
+
const requireAuth = entryRequiresAuth(entry, defaultAuthenticated);
|
|
51
|
+
const effectiveArgs = resolveAuthGatedArgs(entry.args, {
|
|
52
|
+
authenticated: requireAuth,
|
|
53
|
+
isAuthenticated
|
|
54
|
+
});
|
|
55
|
+
const authSkipped = effectiveArgs === "skip";
|
|
56
|
+
const signature = subscriptionSignature(
|
|
57
|
+
entry.query,
|
|
58
|
+
entry.args,
|
|
59
|
+
authSkipped
|
|
60
|
+
);
|
|
61
|
+
if (signatures.get(key) === signature && (unsubscribers.has(key) || authSkipped)) {
|
|
47
62
|
continue;
|
|
48
63
|
}
|
|
49
64
|
unsubscribers.get(key)?.();
|
|
65
|
+
unsubscribers.delete(key);
|
|
66
|
+
if (authSkipped) {
|
|
67
|
+
signatures.set(key, signature);
|
|
68
|
+
if (key in results.value) {
|
|
69
|
+
const { [key]: _, ...rest } = results.value;
|
|
70
|
+
results.value = rest;
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
50
74
|
const unsubscribe = client.onUpdate(
|
|
51
|
-
query,
|
|
52
|
-
|
|
75
|
+
entry.query,
|
|
76
|
+
effectiveArgs,
|
|
53
77
|
(value) => {
|
|
54
78
|
results.value = { ...results.value, [key]: value };
|
|
55
79
|
},
|
|
@@ -65,9 +89,9 @@ export function useConvexQueries(queries) {
|
|
|
65
89
|
}
|
|
66
90
|
};
|
|
67
91
|
watch(
|
|
68
|
-
() => toValue(queries),
|
|
69
|
-
(request) => {
|
|
70
|
-
sync(request);
|
|
92
|
+
() => [toValue(queries), ctx.auth.isAuthenticated.value],
|
|
93
|
+
([request, isAuthenticated]) => {
|
|
94
|
+
sync(request, isAuthenticated);
|
|
71
95
|
},
|
|
72
96
|
{ immediate: true, deep: true }
|
|
73
97
|
);
|