nuxt-convex-module 0.13.0 → 0.14.1
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/devtools-client/index.html +1 -1
- package/dist/module.d.mts +18 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +120 -36
- package/dist/runtime/better-auth/nuxt/middleware.js +8 -2
- package/dist/runtime/better-auth/nuxt/no-session-vary.d.ts +38 -0
- package/dist/runtime/better-auth/nuxt/no-session-vary.js +21 -0
- package/dist/runtime/better-auth/nuxt/server.js +2 -1
- package/dist/runtime/better-auth/nuxt/shared-render.d.ts +9 -0
- package/dist/runtime/better-auth/nuxt/shared-render.js +17 -0
- package/dist/runtime/better-auth/vue/auth-client-loader.d.ts +17 -0
- package/dist/runtime/better-auth/vue/auth-client-loader.js +186 -0
- package/dist/runtime/better-auth/vue/cross-domain.d.ts +2 -2
- package/dist/runtime/better-auth/vue/cross-domain.js +41 -35
- package/dist/runtime/better-auth/vue/index.d.ts +1 -0
- package/dist/runtime/better-auth/vue/index.js +1 -0
- package/dist/runtime/better-auth/vue/plugin.client.js +21 -2
- package/dist/runtime/better-auth/vue/plugin.server.js +23 -3
- package/dist/runtime/better-auth/vue/token-store.js +4 -0
- package/dist/runtime/better-auth/vue/use-better-auth.d.ts +7 -1
- package/dist/runtime/better-auth/vue/use-better-auth.js +78 -6
- package/dist/runtime/nuxt/function-name.d.ts +9 -0
- package/dist/runtime/nuxt/function-name.js +44 -0
- package/dist/runtime/nuxt/index.js +4 -3
- package/dist/runtime/nuxt/testing.d.ts +21 -0
- package/dist/runtime/nuxt/testing.js +1 -0
- package/dist/runtime/vue/composables/use-upload-queue.d.ts +82 -6
- package/dist/runtime/vue/composables/use-upload-queue.js +96 -25
- package/dist/runtime/vue/index.d.ts +1 -1
- package/package.json +8 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadAuthClient } from "./auth-client-loader.js";
|
|
2
2
|
const normalizePathname = (path) => {
|
|
3
3
|
const slashed = path.startsWith("/") ? path : `/${path}`;
|
|
4
4
|
let end = slashed.length;
|
|
@@ -12,41 +12,47 @@ export async function consumeCrossDomainOneTimeToken(options = {}) {
|
|
|
12
12
|
}
|
|
13
13
|
const url = new URL(window.location.href);
|
|
14
14
|
const token = url.searchParams.get("ott");
|
|
15
|
-
if (token)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
15
|
+
if (!token) return;
|
|
16
|
+
url.searchParams.delete("ott");
|
|
17
|
+
window.history.replaceState({}, "", url);
|
|
18
|
+
const authClient = await loadAuthClient().catch(() => loadAuthClient()).catch((error) => {
|
|
19
|
+
console.warn("[nuxt-convex-module] Failed to load the Better Auth client:", error);
|
|
20
|
+
});
|
|
21
|
+
if (!authClient) return;
|
|
22
|
+
await exchangeOneTimeToken(authClient, token, url, options);
|
|
23
|
+
}
|
|
24
|
+
async function exchangeOneTimeToken(authClient, token, url, options) {
|
|
25
|
+
const authClientWithCrossDomain = authClient;
|
|
26
|
+
if (!authClientWithCrossDomain?.crossDomain) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (options.callbackRoute && normalizePathname(url.pathname) !== normalizePathname(options.callbackRoute)) {
|
|
30
|
+
console.warn(
|
|
31
|
+
`[nuxt-convex-module] Ignoring cross-domain one-time token on "${url.pathname}" \u2014 consumption is restricted to "${options.callbackRoute}" (convex.betterAuth.crossDomainCallbackRoute).`
|
|
32
|
+
);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (import.meta.dev && !options.callbackRoute) {
|
|
36
|
+
console.warn(
|
|
37
|
+
`[nuxt-convex-module] Exchanged a cross-domain one-time token on "${url.pathname}" with no \`convex.betterAuth.crossDomainCallbackRoute\` set \u2014 any page can complete sign-in. Restrict it to your callback route.`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const result = await authClientWithCrossDomain.crossDomain.oneTimeToken.verify({
|
|
42
|
+
token
|
|
43
|
+
});
|
|
44
|
+
const session = result.data?.session;
|
|
45
|
+
if (session) {
|
|
46
|
+
await authClientWithCrossDomain.getSession({
|
|
47
|
+
fetchOptions: {
|
|
48
|
+
headers: {
|
|
49
|
+
Authorization: `Bearer ${session.token}`
|
|
44
50
|
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
} catch (error) {
|
|
49
|
-
console.warn("[nuxt-convex-module] Failed to consume cross-domain one-time token:", error);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
await authClientWithCrossDomain.updateSession();
|
|
50
54
|
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.warn("[nuxt-convex-module] Failed to consume cross-domain one-time token:", error);
|
|
51
57
|
}
|
|
52
58
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { useBetterAuth, type UseBetterAuthReturn, type BetterAuthSession, type BetterAuthUser } from './use-better-auth.js';
|
|
8
8
|
export { authClient, type AuthClient } from '#convex/auth-client';
|
|
9
|
+
export { loadAuthClient } from './auth-client-loader.js';
|
|
9
10
|
export { usePreloadedAuthQuery } from './hydration.js';
|
|
10
11
|
export { consumeCrossDomainOneTimeToken, type AuthClientWithCrossDomain, type ConsumeCrossDomainOneTimeTokenOptions } from './cross-domain.js';
|
|
11
12
|
export { resolveAuthRedirect } from './redirect.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { useBetterAuth } from "./use-better-auth.js";
|
|
2
2
|
export { authClient } from "#convex/auth-client";
|
|
3
|
+
export { loadAuthClient } from "./auth-client-loader.js";
|
|
3
4
|
export { usePreloadedAuthQuery } from "./hydration.js";
|
|
4
5
|
export { consumeCrossDomainOneTimeToken } from "./cross-domain.js";
|
|
5
6
|
export { resolveAuthRedirect } from "./redirect.js";
|
|
@@ -2,18 +2,37 @@ import { defineNuxtPlugin, useRuntimeConfig, useState } from "#app";
|
|
|
2
2
|
import { ConvexVueClient, ConvexClientKey } from "../../vue/client.js";
|
|
3
3
|
import { ConvexAuthStateKey, createScopedConvexAuthState } from "../../vue/auth/index.js";
|
|
4
4
|
import { CONVEX_INITIAL_TOKEN_KEY } from "../../nuxt/config.js";
|
|
5
|
-
import { useBetterAuth } from "./use-better-auth.js";
|
|
5
|
+
import { settleSessionWithoutClient, useBetterAuth } from "./use-better-auth.js";
|
|
6
|
+
import { deferAuthClient } from "./auth-client-loader.js";
|
|
7
|
+
import { NO_SESSION_KEY } from "./token-store.js";
|
|
6
8
|
import { consumeCrossDomainOneTimeToken } from "./cross-domain.js";
|
|
9
|
+
function holdsCrossDomainSession() {
|
|
10
|
+
return [() => localStorage, () => sessionStorage].some((open) => {
|
|
11
|
+
try {
|
|
12
|
+
const storage = open();
|
|
13
|
+
for (let index = 0; index < storage.length; index++) {
|
|
14
|
+
const key = storage.key(index);
|
|
15
|
+
if (key?.endsWith("_cookie") && storage.getItem(key)?.includes("session_token")) return true;
|
|
16
|
+
}
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
7
22
|
export default defineNuxtPlugin({
|
|
8
23
|
name: "nuxt-convex-module:better-auth:client",
|
|
9
24
|
async setup(nuxtApp) {
|
|
10
|
-
const { url, crossDomainCallbackRoute } = useRuntimeConfig().public.convex;
|
|
25
|
+
const { url, crossDomainCallbackRoute, deferAuthClient: mayDefer } = useRuntimeConfig().public.convex;
|
|
11
26
|
if (!url) {
|
|
12
27
|
console.warn("[nuxt-convex-module] No Convex deployment URL configured \u2014 client and auth not created. Set NUXT_PUBLIC_CONVEX_URL or `convex.url` in nuxt.config.");
|
|
13
28
|
return;
|
|
14
29
|
}
|
|
15
30
|
const client = new ConvexVueClient(url);
|
|
16
31
|
nuxtApp.vueApp.provide(ConvexClientKey, client);
|
|
32
|
+
if (mayDefer !== false && useState(NO_SESSION_KEY, () => false).value && !new URL(window.location.href).searchParams.has("ott") && !holdsCrossDomainSession()) {
|
|
33
|
+
deferAuthClient();
|
|
34
|
+
nuxtApp.hook("app:suspense:resolve", settleSessionWithoutClient);
|
|
35
|
+
}
|
|
17
36
|
await consumeCrossDomainOneTimeToken({
|
|
18
37
|
callbackRoute: crossDomainCallbackRoute || void 0
|
|
19
38
|
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
|
-
import { defineNuxtPlugin, useRuntimeConfig, useState, useRequestEvent, useResponseHeader } from "#app";
|
|
2
|
+
import { defineNuxtPlugin, useRuntimeConfig, useState, useRequestEvent, useRequestHeaders, useResponseHeader } from "#app";
|
|
3
3
|
import { ConvexVueClient, ConvexClientKey } from "../../vue/client.js";
|
|
4
4
|
import { CONVEX_INITIAL_TOKEN_KEY, closeAfterRender } from "../../nuxt/config.js";
|
|
5
5
|
import { convexAuth } from "../nuxt/server.js";
|
|
6
6
|
import { ConvexAuthStateKey } from "../../vue/auth/index.js";
|
|
7
|
-
import {
|
|
7
|
+
import { CACHING_HEADERS, isSharedRender, NO_SESSION_CONTEXT_KEY } from "../nuxt/shared-render.js";
|
|
8
|
+
import { BetterAuthTokenStoreKey, createBetterAuthTokenStore, hasSessionCookie, NO_SESSION_KEY, seedInitialToken } from "./token-store.js";
|
|
8
9
|
export async function prefetchAuthToken(event, initialToken, cacheControl) {
|
|
9
10
|
try {
|
|
10
11
|
const token = await convexAuth(event).getToken();
|
|
@@ -12,10 +13,20 @@ export async function prefetchAuthToken(event, initialToken, cacheControl) {
|
|
|
12
13
|
if (token && event) {
|
|
13
14
|
cacheControl.value = "private, no-store";
|
|
14
15
|
}
|
|
16
|
+
return true;
|
|
15
17
|
} catch (error) {
|
|
16
18
|
console.warn("[nuxt-convex-module] Failed to prefetch auth token for SSR:", error);
|
|
19
|
+
return false;
|
|
17
20
|
}
|
|
18
21
|
}
|
|
22
|
+
function renderContext(event) {
|
|
23
|
+
const context = event.context;
|
|
24
|
+
return {
|
|
25
|
+
routeRules: context._nitro?.routeRules,
|
|
26
|
+
cache: context.cache,
|
|
27
|
+
headers: Object.fromEntries(CACHING_HEADERS.map((name) => [name, event.node?.res?.getHeader?.(name)]))
|
|
28
|
+
};
|
|
29
|
+
}
|
|
19
30
|
export default defineNuxtPlugin({
|
|
20
31
|
name: "nuxt-convex-module:better-auth:server",
|
|
21
32
|
async setup(nuxtApp) {
|
|
@@ -36,8 +47,17 @@ export default defineNuxtPlugin({
|
|
|
36
47
|
nuxtApp.vueApp.provide(ConvexAuthStateKey, ssrAuthState);
|
|
37
48
|
const event = useRequestEvent();
|
|
38
49
|
if (event && !import.meta.prerender) {
|
|
39
|
-
|
|
50
|
+
const cacheControl = useResponseHeader("Cache-Control");
|
|
51
|
+
const noSession = useState(NO_SESSION_KEY, () => false);
|
|
52
|
+
const { cookie } = useRequestHeaders(["cookie"]);
|
|
53
|
+
const answered = await prefetchAuthToken(event, initialToken, cacheControl);
|
|
40
54
|
seedInitialToken(tokenStore, initialToken.value);
|
|
55
|
+
noSession.value = answered && !initialToken.value && !hasSessionCookie(cookie) && !isSharedRender(renderContext(event));
|
|
56
|
+
nuxtApp.hooks.hook("app:rendered", () => {
|
|
57
|
+
if (noSession.value && isSharedRender(renderContext(event))) noSession.value = false;
|
|
58
|
+
const context = event.context;
|
|
59
|
+
context[NO_SESSION_CONTEXT_KEY] = noSession.value;
|
|
60
|
+
});
|
|
41
61
|
}
|
|
42
62
|
}
|
|
43
63
|
});
|
|
@@ -10,6 +10,10 @@ export function createBetterAuthTokenStore() {
|
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
export const BetterAuthTokenStoreKey = Symbol("BetterAuthTokenStore");
|
|
13
|
+
export const NO_SESSION_KEY = "convex:betterAuth:noSession";
|
|
14
|
+
export function hasSessionCookie(cookie) {
|
|
15
|
+
return !!cookie && cookie.split(";").some((pair) => pair.split("=", 1)[0].trim().endsWith("session_token"));
|
|
16
|
+
}
|
|
13
17
|
export const PENDING_AUTH_VERSION = "__pending__";
|
|
14
18
|
export function readTokenClaims(token) {
|
|
15
19
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AuthTokenFetcher } from 'convex/browser';
|
|
2
2
|
import { type ComputedRef } from 'vue';
|
|
3
|
-
import {
|
|
3
|
+
import type { AuthClient } from '#convex/auth-client';
|
|
4
4
|
export type BetterAuthSession = AuthClient['useSession'] extends {
|
|
5
5
|
(): infer R;
|
|
6
6
|
(useFetch: any): any;
|
|
@@ -49,3 +49,9 @@ export interface UseBetterAuthReturn {
|
|
|
49
49
|
* @public
|
|
50
50
|
*/
|
|
51
51
|
export declare function useBetterAuth(initialToken?: string | null): UseBetterAuthReturn;
|
|
52
|
+
/**
|
|
53
|
+
* Forget what the Better Auth integration keeps for the app's lifetime: the
|
|
54
|
+
* browser's token cache and its session subscription. Published from
|
|
55
|
+
* `nuxt-convex-module/testing`; call it between tests that sign in.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resetBetterAuthState(): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { computed, effectScope, hasInjectionContext, inject, shallowRef, triggerRef, watch } from "vue";
|
|
2
|
-
import {
|
|
2
|
+
import { __resetAuthClientLoaderForTests, isAuthClientDeferred, lazyAuthClient, loadAuthClient, loadedAuthClient, onAuthClient, retryAuthClientLoad } from "./auth-client-loader.js";
|
|
3
3
|
import { BetterAuthTokenStoreKey, clearToken, createBetterAuthTokenStore, PENDING_AUTH_VERSION, readTokenClaims, seedInitialToken } from "./token-store.js";
|
|
4
4
|
const browserStore = createBetterAuthTokenStore();
|
|
5
5
|
function useTokenStore() {
|
|
@@ -16,23 +16,85 @@ const serverSession = () => shallowRef({
|
|
|
16
16
|
});
|
|
17
17
|
let browserSession;
|
|
18
18
|
let browserSessionScope;
|
|
19
|
+
let settledWithoutClient = false;
|
|
20
|
+
const clientAnswered = shallowRef(false);
|
|
21
|
+
const pendingSessionState = () => {
|
|
22
|
+
const refetch = async (...args) => {
|
|
23
|
+
await loadAuthClient();
|
|
24
|
+
const next = browserSession.value.refetch;
|
|
25
|
+
if (next === refetch) return;
|
|
26
|
+
return next(...args);
|
|
27
|
+
};
|
|
28
|
+
return { data: null, error: null, isPending: true, isRefetching: false, refetch };
|
|
29
|
+
};
|
|
19
30
|
const useClientSession = () => {
|
|
20
31
|
if (import.meta.server) return serverSession();
|
|
21
32
|
browserSessionScope ??= effectScope(true);
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
const scope = browserSessionScope;
|
|
34
|
+
return browserSession ??= scope.run(() => {
|
|
35
|
+
const session = shallowRef(pendingSessionState());
|
|
36
|
+
onAuthClient((client) => scope.active && scope.run(() => {
|
|
37
|
+
watch(client.useSession(), (state) => {
|
|
38
|
+
if (settledWithoutClient && state.isPending) {
|
|
39
|
+
if (session.value.refetch !== state.refetch) session.value = { ...session.value, refetch: state.refetch };
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
settledWithoutClient = false;
|
|
43
|
+
session.value = state;
|
|
44
|
+
if (!state.isPending) clientAnswered.value = true;
|
|
45
|
+
}, { immediate: true, flush: "sync" });
|
|
46
|
+
}));
|
|
47
|
+
if (!isAuthClientDeferred()) void loadAtStartup();
|
|
24
48
|
watch(session, ({ data, isPending }) => {
|
|
25
49
|
if (!data && !isPending) clearToken(browserStore);
|
|
26
50
|
}, { flush: "sync" });
|
|
27
51
|
return session;
|
|
28
52
|
});
|
|
29
53
|
};
|
|
54
|
+
async function loadAtStartup() {
|
|
55
|
+
try {
|
|
56
|
+
await loadAuthClient().catch(() => loadAuthClient());
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.warn("[nuxt-convex-module] Failed to load the Better Auth client:", error);
|
|
59
|
+
if (!coveredByServerToken()) settle(error);
|
|
60
|
+
retryAuthClientLoad();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function coveredByServerToken() {
|
|
64
|
+
return Boolean(browserStore.cachedToken.value) && browserStore.cachedTokenVersion === PENDING_AUTH_VERSION;
|
|
65
|
+
}
|
|
66
|
+
export async function clientSessionSettled() {
|
|
67
|
+
useClientSession();
|
|
68
|
+
try {
|
|
69
|
+
await loadAuthClient().catch(() => loadAuthClient());
|
|
70
|
+
} catch {
|
|
71
|
+
if (!coveredByServerToken()) settle();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (clientAnswered.value) return;
|
|
75
|
+
await new Promise((resolve) => {
|
|
76
|
+
const stop = watch(clientAnswered, (answered) => {
|
|
77
|
+
if (!answered) return;
|
|
78
|
+
stop();
|
|
79
|
+
resolve();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function settle(error = null) {
|
|
84
|
+
const session = useClientSession();
|
|
85
|
+
if (loadedAuthClient() || !session.value.isPending) return;
|
|
86
|
+
settledWithoutClient = true;
|
|
87
|
+
session.value = { ...session.value, isPending: false, error };
|
|
88
|
+
}
|
|
89
|
+
export function settleSessionWithoutClient() {
|
|
90
|
+
settle();
|
|
91
|
+
}
|
|
30
92
|
export function useBetterAuth(initialToken) {
|
|
31
93
|
const store = useTokenStore();
|
|
32
94
|
seedInitialToken(store, initialToken);
|
|
33
95
|
const { cachedToken } = store;
|
|
34
96
|
const session = useClientSession();
|
|
35
|
-
const client =
|
|
97
|
+
const client = loadedAuthClient() ?? lazyAuthClient;
|
|
36
98
|
const authVersion = computed(() => {
|
|
37
99
|
const data = session.value.data;
|
|
38
100
|
const id = data?.session?.id ?? data?.user?.id ?? null;
|
|
@@ -47,11 +109,17 @@ export function useBetterAuth(initialToken) {
|
|
|
47
109
|
return !!data.user?.id && subject !== void 0 && subject !== data.user.id;
|
|
48
110
|
});
|
|
49
111
|
const isServerTokenActive = () => session.value.isPending && cachedToken.value !== null && store.cachedTokenVersion === PENDING_AUTH_VERSION;
|
|
112
|
+
const signedInSince = async (forceRefreshToken) => {
|
|
113
|
+
if (!forceRefreshToken || import.meta.server || clientAnswered.value) return false;
|
|
114
|
+
await clientSessionSettled();
|
|
115
|
+
return currentAuthVersion() !== null;
|
|
116
|
+
};
|
|
50
117
|
const fetchAccessToken = async ({
|
|
51
118
|
forceRefreshToken = false
|
|
52
119
|
} = {}) => {
|
|
53
120
|
const version = currentAuthVersion();
|
|
54
121
|
if (version === null) {
|
|
122
|
+
if (await signedInSince(forceRefreshToken)) return fetchAccessToken({ forceRefreshToken });
|
|
55
123
|
clearToken(store);
|
|
56
124
|
return null;
|
|
57
125
|
}
|
|
@@ -78,7 +146,7 @@ export function useBetterAuth(initialToken) {
|
|
|
78
146
|
};
|
|
79
147
|
store.pendingTokenVersion = version;
|
|
80
148
|
const isLatest = () => store.pendingToken === pending;
|
|
81
|
-
const pending = store.pendingToken =
|
|
149
|
+
const pending = store.pendingToken = loadAuthClient().then((client2) => client2.convex.token({ fetchOptions: { throw: false } })).then(({ data, error }) => {
|
|
82
150
|
if (error && error.status !== 401) {
|
|
83
151
|
return afterFailure();
|
|
84
152
|
}
|
|
@@ -117,10 +185,14 @@ export function useBetterAuth(initialToken) {
|
|
|
117
185
|
authVersion
|
|
118
186
|
};
|
|
119
187
|
}
|
|
120
|
-
export function
|
|
188
|
+
export function resetBetterAuthState() {
|
|
121
189
|
Object.assign(browserStore, createBetterAuthTokenStore(), { cachedToken: browserStore.cachedToken });
|
|
122
190
|
browserStore.cachedToken.value = null;
|
|
123
191
|
browserSessionScope?.stop();
|
|
124
192
|
browserSessionScope = void 0;
|
|
125
193
|
browserSession = void 0;
|
|
194
|
+
settledWithoutClient = false;
|
|
195
|
+
clientAnswered.value = false;
|
|
196
|
+
__resetAuthClientLoaderForTests();
|
|
126
197
|
}
|
|
198
|
+
export const __resetUseBetterAuthForTests = resetBetterAuthState;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getFunctionName } from 'convex/server';
|
|
2
|
+
declare const SOURCE_PREFIX: {
|
|
3
|
+
readonly query: "Q";
|
|
4
|
+
readonly mutation: "M";
|
|
5
|
+
readonly action: "A";
|
|
6
|
+
};
|
|
7
|
+
/** `call`, with the function's name put in front of the message of the error it rejects with. */
|
|
8
|
+
export declare function withFunctionName<T>(source: keyof typeof SOURCE_PREFIX, reference: Parameters<typeof getFunctionName>[0], call: Promise<T>): Promise<T>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { getFunctionName } from "convex/server";
|
|
2
|
+
const SOURCE_PREFIX = { query: "Q", mutation: "M", action: "A" };
|
|
3
|
+
const unnamed = /* @__PURE__ */ new WeakMap();
|
|
4
|
+
export async function withFunctionName(source, reference, call) {
|
|
5
|
+
try {
|
|
6
|
+
return await call;
|
|
7
|
+
} catch (error) {
|
|
8
|
+
if (error instanceof Error && !error.message.startsWith("[CONVEX ")) name(error, `[CONVEX ${SOURCE_PREFIX[source]}(${getFunctionName(reference)})] `);
|
|
9
|
+
throw error;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function name(error, prefix) {
|
|
13
|
+
const original = error.message;
|
|
14
|
+
const stack = error.stack;
|
|
15
|
+
try {
|
|
16
|
+
error.message = `${prefix}${original}`;
|
|
17
|
+
} catch {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (error.message !== `${prefix}${original}`) return;
|
|
21
|
+
unnamed.set(error, { message: original, stack });
|
|
22
|
+
const header = `${error.name}: ${original}`;
|
|
23
|
+
if (original && stack?.startsWith(header)) setStack(error, `${error.name}: ${error.message}${stack.slice(header.length)}`);
|
|
24
|
+
}
|
|
25
|
+
function setStack(error, stack) {
|
|
26
|
+
try {
|
|
27
|
+
error.stack = stack;
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function withoutFunctionName(error, read) {
|
|
32
|
+
const before = error instanceof Error ? unnamed.get(error) : void 0;
|
|
33
|
+
if (!before) return read(error);
|
|
34
|
+
const named = error;
|
|
35
|
+
const { message, stack } = named;
|
|
36
|
+
named.message = before.message;
|
|
37
|
+
setStack(named, before.stack);
|
|
38
|
+
try {
|
|
39
|
+
return read(error);
|
|
40
|
+
} finally {
|
|
41
|
+
named.message = message;
|
|
42
|
+
setStack(named, stack);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -2,6 +2,7 @@ import { ConvexHttpClient } from "convex/browser";
|
|
|
2
2
|
import { getFunctionName } from "convex/server";
|
|
3
3
|
import { convexToJson, jsonToConvex } from "convex/values";
|
|
4
4
|
import { getConvexRuntimeConfig } from "./config.js";
|
|
5
|
+
import { withFunctionName } from "./function-name.js";
|
|
5
6
|
export async function preloadQuery(query, ...args) {
|
|
6
7
|
const value = await fetchQuery(query, ...args);
|
|
7
8
|
const preloaded = {
|
|
@@ -17,17 +18,17 @@ export function preloadedQueryResult(preloaded) {
|
|
|
17
18
|
export async function fetchQuery(query, ...args) {
|
|
18
19
|
const [fnArgs, options] = args;
|
|
19
20
|
const client = setupClient(options ?? {});
|
|
20
|
-
return client.query(query, fnArgs || {});
|
|
21
|
+
return withFunctionName("query", query, client.query(query, fnArgs || {}));
|
|
21
22
|
}
|
|
22
23
|
export async function fetchMutation(mutation, ...args) {
|
|
23
24
|
const [fnArgs, options] = args;
|
|
24
25
|
const client = setupClient(options ?? {});
|
|
25
|
-
return client.mutation(mutation, fnArgs || {});
|
|
26
|
+
return withFunctionName("mutation", mutation, client.mutation(mutation, fnArgs || {}));
|
|
26
27
|
}
|
|
27
28
|
export async function fetchAction(action, ...args) {
|
|
28
29
|
const [fnArgs, options] = args;
|
|
29
30
|
const client = setupClient(options ?? {});
|
|
30
|
-
return client.action(action, fnArgs || {});
|
|
31
|
+
return withFunctionName("action", action, client.action(action, fnArgs || {}));
|
|
31
32
|
}
|
|
32
33
|
function setupClient(options) {
|
|
33
34
|
if ("url" in options && options.url === void 0) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for an app's own tests, kept off the runtime entries.
|
|
3
|
+
*
|
|
4
|
+
* The module keeps some state for the app's lifetime, as upstream's provider
|
|
5
|
+
* keeps it for the provider's: a test that signs in leaves it behind for the
|
|
6
|
+
* next. Reset it here instead of reaching into the module's files by path.
|
|
7
|
+
*
|
|
8
|
+
* Load it where the app's runtime loads, in `@nuxt/test-utils`' `nuxt`
|
|
9
|
+
* environment: it imports `#convex/auth-client`, the alias the module (or
|
|
10
|
+
* your test config) registers.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { afterEach } from 'vitest'
|
|
14
|
+
* import { resetBetterAuthState } from 'nuxt-convex-module/testing'
|
|
15
|
+
*
|
|
16
|
+
* afterEach(() => resetBetterAuthState())
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @module testing
|
|
20
|
+
*/
|
|
21
|
+
export { resetBetterAuthState } from '../better-auth/vue/use-better-auth.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { resetBetterAuthState } from "../better-auth/vue/use-better-auth.js";
|
|
@@ -6,6 +6,13 @@ import { type GenerateUploadUrl, type StorageId } from './use-upload.js';
|
|
|
6
6
|
* @public
|
|
7
7
|
*/
|
|
8
8
|
export type UploadItemStatus = 'pending' | 'uploading' | 'success' | 'error';
|
|
9
|
+
/**
|
|
10
|
+
* Which step an `uploading` item is on: its `prepare` hook, the transfer, or
|
|
11
|
+
* its `after` hook. `null` outside `uploading`.
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
export type UploadItemStage = 'prepare' | 'upload' | 'after';
|
|
9
16
|
/**
|
|
10
17
|
* A single entry in the queue managed by {@link useUploadQueue}.
|
|
11
18
|
*
|
|
@@ -20,10 +27,27 @@ export interface UploadQueueItem {
|
|
|
20
27
|
status: UploadItemStatus;
|
|
21
28
|
/** Upload progress for this item, from `0` to `1`. */
|
|
22
29
|
progress: number;
|
|
23
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* The storage id once the file is stored, or `null`. An item that failed
|
|
32
|
+
* after that keeps it (see `after` and `onItemSuccess`).
|
|
33
|
+
*/
|
|
24
34
|
storageId: StorageId | null;
|
|
25
35
|
/** The error if this item failed, or `null`. */
|
|
26
36
|
error: Error | null;
|
|
37
|
+
/**
|
|
38
|
+
* Whether the error is a refusal rather than a failure: the app said no
|
|
39
|
+
* (`isRefusal`; by default a `ConvexError`), so the same file would be
|
|
40
|
+
* turned away again. `false` while the item has no error.
|
|
41
|
+
*/
|
|
42
|
+
refused: boolean;
|
|
43
|
+
/** The step an `uploading` item is on, or `null`. */
|
|
44
|
+
stage: UploadItemStage | null;
|
|
45
|
+
/**
|
|
46
|
+
* What `prepare` returned, which is what gets sent; `null` until it runs,
|
|
47
|
+
* or without a `prepare` hook (the file itself is sent). Kept for a retry
|
|
48
|
+
* and for a preview.
|
|
49
|
+
*/
|
|
50
|
+
prepared: Blob | null;
|
|
27
51
|
}
|
|
28
52
|
/**
|
|
29
53
|
* Options for {@link useUploadQueue}.
|
|
@@ -33,11 +57,44 @@ export interface UploadQueueItem {
|
|
|
33
57
|
export interface UseUploadQueueOptions {
|
|
34
58
|
/** Maximum number of uploads to run at once. Defaults to `3`. */
|
|
35
59
|
concurrency?: number;
|
|
36
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Turn a queued file into what is sent, before an upload URL is asked
|
|
62
|
+
* for: resize, re-encode, strip metadata. Throw to turn the file away; the
|
|
63
|
+
* item ends `error`, refused when `isRefusal` says so. Runs once per
|
|
64
|
+
* item: a retry reuses what it returned.
|
|
65
|
+
*/
|
|
66
|
+
prepare?: (file: Blob, item: UploadQueueItem, signal: AbortSignal) => Blob | Promise<Blob>;
|
|
67
|
+
/**
|
|
68
|
+
* Runs once the file is stored, before the item counts as a success:
|
|
69
|
+
* attach the storage id to a document, run a check on it. The item stays
|
|
70
|
+
* `uploading` (stage `'after'`) until it settles. A throw ends the item
|
|
71
|
+
* `error`; a retry then runs only this hook again, with the same storage
|
|
72
|
+
* id, unless the throw was a refusal, which sends the file again. After a
|
|
73
|
+
* refusal `item.storageId` still holds the stored file while the item is
|
|
74
|
+
* in error, so `onItemError` can delete it; the queue never deletes files,
|
|
75
|
+
* and a retry forgets it before uploading again.
|
|
76
|
+
*/
|
|
77
|
+
after?: (storageId: StorageId, item: UploadQueueItem, signal: AbortSignal) => void | Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Whether an error is a refusal (the app turned the file away) rather than
|
|
80
|
+
* a failure (the network, the deployment, an abort). Defaults to: the
|
|
81
|
+
* error is a `ConvexError`, which a Convex function throws on purpose.
|
|
82
|
+
* An aborted item is never refused, and neither is an error this check
|
|
83
|
+
* throws on.
|
|
84
|
+
*/
|
|
85
|
+
isRefusal?: (error: Error, item: UploadQueueItem) => boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Called when an individual item finishes uploading (after `after`). A
|
|
88
|
+
* throw ends the item `error` with its storage id kept; a retry then runs
|
|
89
|
+
* only this callback again, never `after` a second time.
|
|
90
|
+
*/
|
|
37
91
|
onItemSuccess?: (storageId: StorageId, item: UploadQueueItem) => void | Promise<void>;
|
|
38
|
-
/** Called when an individual item fails. */
|
|
92
|
+
/** Called when an individual item fails or is refused (`item.refused` says which). */
|
|
39
93
|
onItemError?: (error: Error, item: UploadQueueItem) => void;
|
|
40
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Called once every queued item has settled (succeeded or failed), after
|
|
96
|
+
* every `onItemSuccess` has returned.
|
|
97
|
+
*/
|
|
41
98
|
onComplete?: (items: UploadQueueItem[]) => void;
|
|
42
99
|
}
|
|
43
100
|
/** Accepted inputs to {@link VueUploadQueue.enqueue}. @public */
|
|
@@ -60,12 +117,26 @@ export interface VueUploadQueue {
|
|
|
60
117
|
pendingCount: Readonly<Ref<number>>;
|
|
61
118
|
/** Number of items currently uploading. */
|
|
62
119
|
activeCount: Readonly<Ref<number>>;
|
|
63
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* Abort all in-flight uploads. Queued items stay `pending` until the next
|
|
122
|
+
* `enqueue` or `retry()`, which start them again.
|
|
123
|
+
*/
|
|
64
124
|
cancel: () => void;
|
|
65
125
|
/** Remove an item by id, aborting it first if it is uploading. */
|
|
66
126
|
remove: (id: string) => void;
|
|
67
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* Abort everything and empty the queue. Once the aborted uploads end,
|
|
129
|
+
* `onComplete` gets `[]`, unless files were queued since.
|
|
130
|
+
*/
|
|
68
131
|
clear: () => void;
|
|
132
|
+
/**
|
|
133
|
+
* Queue an errored item again, or every errored item without an id. It
|
|
134
|
+
* picks up where it stopped: `onItemSuccess` alone when `after` already
|
|
135
|
+
* finished, `after` alone when the file is stored, the transfer when
|
|
136
|
+
* `prepare` already ran. Whether a refused item is worth
|
|
137
|
+
* retrying is the app's call; nothing here stops it.
|
|
138
|
+
*/
|
|
139
|
+
retry: (id?: string) => void;
|
|
69
140
|
}
|
|
70
141
|
/**
|
|
71
142
|
* Upload many files to Convex file storage with bounded concurrency and
|
|
@@ -76,6 +147,11 @@ export interface VueUploadQueue {
|
|
|
76
147
|
* a multi-file `<input>`) and the queue uploads up to `concurrency` at a time,
|
|
77
148
|
* exposing per-item and aggregate progress.
|
|
78
149
|
*
|
|
150
|
+
* Options `prepare` and `after` add the app's own steps on either side of
|
|
151
|
+
* the transfer (shrink a photo first; attach it once stored), and a failed
|
|
152
|
+
* item says whether the app `refused` it or it failed, so the UI can show a
|
|
153
|
+
* reason for one and {@link VueUploadQueue.retry} the other.
|
|
154
|
+
*
|
|
79
155
|
* @example
|
|
80
156
|
* ```vue
|
|
81
157
|
* <script setup lang="ts">
|