nuxt-convex-module 0.12.3 → 0.14.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/dist/devtools-client/_nuxt/C-J9qZjr.js +3 -0
- package/dist/devtools-client/_nuxt/CB1p6iYX.js +1 -0
- package/dist/devtools-client/_nuxt/{COXexnyN.js → CtC_CmOY.js} +2 -2
- package/dist/devtools-client/_nuxt/{entry.DsZySRVW.css → entry.jOWrhpYd.css} +1 -1
- 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 +129 -38
- 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 +4 -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/client.d.ts +3 -3
- package/dist/runtime/better-auth/vue/cross-domain.d.ts +26 -0
- package/dist/runtime/better-auth/vue/cross-domain.js +52 -35
- package/dist/runtime/better-auth/vue/index.d.ts +2 -1
- 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 +25 -1
- package/dist/runtime/better-auth/vue/use-better-auth.d.ts +11 -5
- package/dist/runtime/better-auth/vue/use-better-auth.js +135 -32
- package/dist/runtime/nuxt/composables/use-async-paginated-query.d.ts +1 -1
- package/dist/runtime/nuxt/composables/use-async-paginated-query.js +5 -3
- package/dist/runtime/nuxt/composables/use-async-query.d.ts +4 -1
- package/dist/runtime/nuxt/composables/use-async-query.js +6 -3
- package/dist/runtime/nuxt/csp.d.ts +2 -1
- package/dist/runtime/nuxt/csp.js +9 -3
- package/dist/runtime/nuxt/function-name.d.ts +9 -0
- package/dist/runtime/nuxt/function-name.js +38 -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/auth/index.d.ts +2 -1
- package/dist/runtime/vue/auth/index.js +62 -13
- package/dist/runtime/vue/composables/use-upload-queue.d.ts +70 -3
- package/dist/runtime/vue/composables/use-upload-queue.js +74 -19
- package/dist/runtime/vue/index.d.ts +1 -1
- package/package.json +19 -12
- package/dist/devtools-client/_nuxt/AqohFEu1.js +0 -1
- package/dist/devtools-client/_nuxt/BPtiIkWa.js +0 -3
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { getCurrentScope, onScopeDispose, shallowRef, watch } from "vue";
|
|
2
|
+
let loaded;
|
|
3
|
+
let loading;
|
|
4
|
+
let deferred = false;
|
|
5
|
+
const waiting = /* @__PURE__ */ new Set();
|
|
6
|
+
let evaluated;
|
|
7
|
+
let stopSignal;
|
|
8
|
+
export function loadAuthClient() {
|
|
9
|
+
if (evaluated) registerAuthClient(evaluated);
|
|
10
|
+
loading ??= import("#convex/auth-client").then(
|
|
11
|
+
({ authClient }) => {
|
|
12
|
+
registerAuthClient(authClient);
|
|
13
|
+
return authClient;
|
|
14
|
+
},
|
|
15
|
+
(error) => {
|
|
16
|
+
loading = void 0;
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
return loading;
|
|
21
|
+
}
|
|
22
|
+
export function registerEvaluatedAuthClient(client) {
|
|
23
|
+
if (loaded || evaluated === client) return;
|
|
24
|
+
if (!deferred) return registerAuthClient(client);
|
|
25
|
+
evaluated = client;
|
|
26
|
+
const signal = client.$store?.atoms?.$sessionSignal;
|
|
27
|
+
stopSignal = signal?.listen?.(() => registerAuthClient(client));
|
|
28
|
+
}
|
|
29
|
+
export function registerAuthClient(client) {
|
|
30
|
+
if (loaded) return;
|
|
31
|
+
stopSignal?.();
|
|
32
|
+
stopSignal = void 0;
|
|
33
|
+
evaluated = void 0;
|
|
34
|
+
loaded = client;
|
|
35
|
+
loading ??= Promise.resolve(client);
|
|
36
|
+
const listeners = [...waiting];
|
|
37
|
+
waiting.clear();
|
|
38
|
+
for (const listener of listeners) {
|
|
39
|
+
try {
|
|
40
|
+
listener(client);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
report(error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function report(error) {
|
|
47
|
+
if (typeof globalThis.reportError === "function") globalThis.reportError(error);
|
|
48
|
+
else queueMicrotask(() => {
|
|
49
|
+
throw error;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export function loadedAuthClient() {
|
|
53
|
+
return loaded;
|
|
54
|
+
}
|
|
55
|
+
export function onAuthClient(listener) {
|
|
56
|
+
if (loaded) {
|
|
57
|
+
listener(loaded);
|
|
58
|
+
return () => {
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
waiting.add(listener);
|
|
62
|
+
return () => waiting.delete(listener);
|
|
63
|
+
}
|
|
64
|
+
export function deferAuthClient() {
|
|
65
|
+
if (deferred || loaded) return;
|
|
66
|
+
deferred = true;
|
|
67
|
+
const onVisible = () => {
|
|
68
|
+
if (document.visibilityState !== "visible") return;
|
|
69
|
+
void loadAuthClient().then(() => document.removeEventListener("visibilitychange", onVisible), () => {
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
73
|
+
}
|
|
74
|
+
export function retryAuthClientLoad() {
|
|
75
|
+
if (loaded || typeof window === "undefined") return;
|
|
76
|
+
const retry = () => {
|
|
77
|
+
if (document.visibilityState !== "visible") return;
|
|
78
|
+
void loadAuthClient().then(() => {
|
|
79
|
+
window.removeEventListener("online", retry);
|
|
80
|
+
document.removeEventListener("visibilitychange", retry);
|
|
81
|
+
}, () => {
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
window.addEventListener("online", retry);
|
|
85
|
+
document.addEventListener("visibilitychange", retry);
|
|
86
|
+
}
|
|
87
|
+
export function isAuthClientDeferred() {
|
|
88
|
+
return deferred && !loaded;
|
|
89
|
+
}
|
|
90
|
+
function reach(client, path) {
|
|
91
|
+
let owner;
|
|
92
|
+
let value = client;
|
|
93
|
+
for (const key of path) {
|
|
94
|
+
owner = value;
|
|
95
|
+
value = value[key];
|
|
96
|
+
}
|
|
97
|
+
return { owner, value };
|
|
98
|
+
}
|
|
99
|
+
function call(client, path, args) {
|
|
100
|
+
const { owner, value } = reach(client, path);
|
|
101
|
+
return Reflect.apply(value, owner, args);
|
|
102
|
+
}
|
|
103
|
+
const HOOK = /^use[A-Z]/;
|
|
104
|
+
function pendingSubscription(path, args) {
|
|
105
|
+
let unsubscribe;
|
|
106
|
+
const stopWaiting = onAuthClient((client) => {
|
|
107
|
+
unsubscribe = call(client, path, args);
|
|
108
|
+
});
|
|
109
|
+
void loadAuthClient().catch(() => {
|
|
110
|
+
});
|
|
111
|
+
return () => {
|
|
112
|
+
stopWaiting();
|
|
113
|
+
unsubscribe?.();
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function pendingHook(path, args) {
|
|
117
|
+
const scope = getCurrentScope();
|
|
118
|
+
const refetch = async (...refetchArgs) => {
|
|
119
|
+
await loadAuthClient();
|
|
120
|
+
const next = state.value.refetch;
|
|
121
|
+
if (next === refetch) return;
|
|
122
|
+
return next(...refetchArgs);
|
|
123
|
+
};
|
|
124
|
+
const state = shallowRef({
|
|
125
|
+
data: null,
|
|
126
|
+
error: null,
|
|
127
|
+
isPending: true,
|
|
128
|
+
isRefetching: false,
|
|
129
|
+
refetch
|
|
130
|
+
});
|
|
131
|
+
if (import.meta.server) return state;
|
|
132
|
+
const stopWaiting = onAuthClient((client) => {
|
|
133
|
+
const follow = () => watch(call(client, path, args), (value) => {
|
|
134
|
+
state.value = value;
|
|
135
|
+
}, { immediate: true, flush: "sync" });
|
|
136
|
+
if (!scope) follow();
|
|
137
|
+
else if (scope.active) scope.run(follow);
|
|
138
|
+
});
|
|
139
|
+
if (scope) onScopeDispose(stopWaiting);
|
|
140
|
+
void loadAuthClient().catch(() => loadAuthClient()).catch((error) => {
|
|
141
|
+
if (!loaded) state.value = { ...state.value, isPending: false, error };
|
|
142
|
+
});
|
|
143
|
+
return state;
|
|
144
|
+
}
|
|
145
|
+
const withReceivers = /* @__PURE__ */ new WeakMap();
|
|
146
|
+
function withReceiver(member, owner) {
|
|
147
|
+
if (typeof member !== "function" || typeof owner !== "object" || owner === null) return member;
|
|
148
|
+
let byMethod = withReceivers.get(owner);
|
|
149
|
+
if (!byMethod) withReceivers.set(owner, byMethod = /* @__PURE__ */ new WeakMap());
|
|
150
|
+
let wrapped = byMethod.get(member);
|
|
151
|
+
if (!wrapped) {
|
|
152
|
+
wrapped = new Proxy(member, {
|
|
153
|
+
apply: (fn, _this, args) => Reflect.apply(fn, owner, args)
|
|
154
|
+
});
|
|
155
|
+
byMethod.set(member, wrapped);
|
|
156
|
+
}
|
|
157
|
+
return wrapped;
|
|
158
|
+
}
|
|
159
|
+
function lazyPath(path) {
|
|
160
|
+
return new Proxy(() => {
|
|
161
|
+
}, {
|
|
162
|
+
get(_target, key) {
|
|
163
|
+
if (loaded) {
|
|
164
|
+
const owner = reach(loaded, path).value;
|
|
165
|
+
return withReceiver(owner[key], owner);
|
|
166
|
+
}
|
|
167
|
+
if (key === "then") return void 0;
|
|
168
|
+
return lazyPath([...path, key]);
|
|
169
|
+
},
|
|
170
|
+
apply(_target, _this, args) {
|
|
171
|
+
if (loaded) return call(loaded, path, args);
|
|
172
|
+
const name = path.at(-1);
|
|
173
|
+
if (typeof name === "string" && HOOK.test(name) && typeof args[0] !== "function") return pendingHook(path, args);
|
|
174
|
+
if (name === "subscribe" || name === "listen") return pendingSubscription(path, args);
|
|
175
|
+
return loadAuthClient().then((client) => call(client, path, args));
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
export const lazyAuthClient = lazyPath([]);
|
|
180
|
+
export function __resetAuthClientLoaderForTests() {
|
|
181
|
+
deferred = false;
|
|
182
|
+
stopSignal?.();
|
|
183
|
+
stopSignal = void 0;
|
|
184
|
+
evaluated = void 0;
|
|
185
|
+
waiting.clear();
|
|
186
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { convexClient } from '@convex-dev/better-auth/client/plugins';
|
|
2
|
-
import {
|
|
3
|
-
export declare const authClient:
|
|
2
|
+
import { type VueAuthClient } from 'better-auth/vue';
|
|
3
|
+
export declare const authClient: VueAuthClient<{
|
|
4
4
|
plugins: ReturnType<typeof convexClient>[];
|
|
5
|
-
}
|
|
5
|
+
}>;
|
|
6
6
|
export type AuthClient = typeof authClient;
|
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
import type { AuthClient } from '#convex/auth-client';
|
|
2
|
+
/**
|
|
3
|
+
* An {@link AuthClient} that also includes the `crossDomainClient()` plugin.
|
|
4
|
+
*
|
|
5
|
+
* The aliased client's type only reflects the plugins the app installed (the
|
|
6
|
+
* bundled default omits `crossDomainClient()`), so this describes the
|
|
7
|
+
* cross-domain additions itself.
|
|
8
|
+
*
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
export type AuthClientWithCrossDomain = AuthClient & {
|
|
12
|
+
updateSession: () => void;
|
|
13
|
+
crossDomain?: {
|
|
14
|
+
oneTimeToken: {
|
|
15
|
+
verify: (args: {
|
|
16
|
+
token: string;
|
|
17
|
+
}) => Promise<{
|
|
18
|
+
data?: {
|
|
19
|
+
session?: {
|
|
20
|
+
token: string;
|
|
21
|
+
} | null;
|
|
22
|
+
} | null;
|
|
23
|
+
}>;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
};
|
|
1
27
|
/**
|
|
2
28
|
* Options for {@link consumeCrossDomainOneTimeToken}.
|
|
3
29
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadAuthClient, onAuthClient } 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,58 @@ 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) {
|
|
22
|
+
onAuthClient((client) => void exchangeKeptToken(client, token, url, options));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
await exchangeOneTimeToken(authClient, token, url, options);
|
|
26
|
+
}
|
|
27
|
+
async function exchangeKeptToken(authClient, token, url, options) {
|
|
28
|
+
const current = await Promise.resolve().then(() => authClient.getSession()).catch(() => void 0);
|
|
29
|
+
if (!current || current.error || current.data?.session) {
|
|
30
|
+
if (!current?.data?.session) console.warn("[nuxt-convex-module] Failed to consume cross-domain one-time token:", current?.error ?? "the session lookup failed");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
await exchangeOneTimeToken(authClient, token, url, options);
|
|
34
|
+
}
|
|
35
|
+
async function exchangeOneTimeToken(authClient, token, url, options) {
|
|
36
|
+
const authClientWithCrossDomain = authClient;
|
|
37
|
+
if (!authClientWithCrossDomain?.crossDomain) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (options.callbackRoute && normalizePathname(url.pathname) !== normalizePathname(options.callbackRoute)) {
|
|
41
|
+
console.warn(
|
|
42
|
+
`[nuxt-convex-module] Ignoring cross-domain one-time token on "${url.pathname}" \u2014 consumption is restricted to "${options.callbackRoute}" (convex.betterAuth.crossDomainCallbackRoute).`
|
|
43
|
+
);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (import.meta.dev && !options.callbackRoute) {
|
|
47
|
+
console.warn(
|
|
48
|
+
`[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.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const result = await authClientWithCrossDomain.crossDomain.oneTimeToken.verify({
|
|
53
|
+
token
|
|
54
|
+
});
|
|
55
|
+
const session = result.data?.session;
|
|
56
|
+
if (session) {
|
|
57
|
+
await authClientWithCrossDomain.getSession({
|
|
58
|
+
fetchOptions: {
|
|
59
|
+
headers: {
|
|
60
|
+
Authorization: `Bearer ${session.token}`
|
|
44
61
|
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
} catch (error) {
|
|
49
|
-
console.warn("[nuxt-convex-module] Failed to consume cross-domain one-time token:", error);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
await authClientWithCrossDomain.updateSession();
|
|
50
65
|
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.warn("[nuxt-convex-module] Failed to consume cross-domain one-time token:", error);
|
|
51
68
|
}
|
|
52
69
|
}
|
|
@@ -6,8 +6,9 @@
|
|
|
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
|
-
export { consumeCrossDomainOneTimeToken, type ConsumeCrossDomainOneTimeTokenOptions } from './cross-domain.js';
|
|
11
|
+
export { consumeCrossDomainOneTimeToken, type AuthClientWithCrossDomain, type ConsumeCrossDomainOneTimeTokenOptions } from './cross-domain.js';
|
|
11
12
|
export { resolveAuthRedirect } from './redirect.js';
|
|
12
13
|
export { AuthBoundary } from './auth-boundary.js';
|
|
13
14
|
export { convexClient, crossDomainClient } from '@convex-dev/better-auth/client/plugins';
|
|
@@ -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
|
});
|
|
@@ -5,14 +5,38 @@ export function createBetterAuthTokenStore() {
|
|
|
5
5
|
cachedTokenVersion: null,
|
|
6
6
|
pendingToken: null,
|
|
7
7
|
pendingTokenVersion: null,
|
|
8
|
-
initialTokenUsed: false
|
|
8
|
+
initialTokenUsed: false,
|
|
9
|
+
initialTokenSessionId: void 0
|
|
9
10
|
};
|
|
10
11
|
}
|
|
11
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
|
+
}
|
|
12
17
|
export const PENDING_AUTH_VERSION = "__pending__";
|
|
18
|
+
export function readTokenClaims(token) {
|
|
19
|
+
try {
|
|
20
|
+
const payload = token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
21
|
+
return JSON.parse(atob(payload)) ?? void 0;
|
|
22
|
+
} catch {
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const getTokenSessionId = (token) => {
|
|
27
|
+
const sessionId = readTokenClaims(token)?.sessionId;
|
|
28
|
+
return typeof sessionId === "string" ? sessionId : void 0;
|
|
29
|
+
};
|
|
13
30
|
export function seedInitialToken(store, initialToken) {
|
|
14
31
|
if (store.initialTokenUsed || !initialToken) return;
|
|
15
32
|
store.cachedToken.value = initialToken;
|
|
16
33
|
store.cachedTokenVersion = PENDING_AUTH_VERSION;
|
|
34
|
+
store.initialTokenSessionId = getTokenSessionId(initialToken);
|
|
17
35
|
store.initialTokenUsed = true;
|
|
18
36
|
}
|
|
37
|
+
export function clearToken(store) {
|
|
38
|
+
store.cachedToken.value = null;
|
|
39
|
+
store.cachedTokenVersion = null;
|
|
40
|
+
store.pendingToken = null;
|
|
41
|
+
store.pendingTokenVersion = null;
|
|
42
|
+
}
|
|
@@ -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;
|
|
@@ -13,9 +13,9 @@ export type BetterAuthUser = {
|
|
|
13
13
|
} & Record<string, unknown>;
|
|
14
14
|
/** What `useBetterAuth()` returns. */
|
|
15
15
|
export interface UseBetterAuthReturn {
|
|
16
|
-
/** `true` until the session is known and no prefetched token covers the gap. */
|
|
16
|
+
/** `true` until the session is known and no prefetched token covers the gap, or while a token from another user is replaced. */
|
|
17
17
|
isLoading: ComputedRef<boolean>;
|
|
18
|
-
/** `true` when Better Auth reports a session, or
|
|
18
|
+
/** `true` when Better Auth reports a session, or the prefetched token covers the session loading. */
|
|
19
19
|
isAuthenticated: ComputedRef<boolean>;
|
|
20
20
|
/** Convex's token fetcher: the Better Auth JWT for the current session, or `null`. */
|
|
21
21
|
fetchAccessToken: AuthTokenFetcher;
|
|
@@ -25,8 +25,8 @@ export interface UseBetterAuthReturn {
|
|
|
25
25
|
session: BetterAuthSession;
|
|
26
26
|
/** The current user, or `null` when signed out / still loading. */
|
|
27
27
|
user: ComputedRef<BetterAuthUser | null>;
|
|
28
|
-
/** The session id (or user id)
|
|
29
|
-
authVersion: ComputedRef<string | null>;
|
|
28
|
+
/** The session id (or user id), `null` when signed out, `undefined` until the session settles. */
|
|
29
|
+
authVersion: ComputedRef<string | null | undefined>;
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* The Better Auth service for the Vue/Nuxt runtime.
|
|
@@ -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;
|