nuxt-convex-module 0.12.2 → 0.13.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 (33) hide show
  1. package/dist/devtools-client/_nuxt/C-J9qZjr.js +3 -0
  2. package/dist/devtools-client/_nuxt/CB1p6iYX.js +1 -0
  3. package/dist/devtools-client/_nuxt/{COXexnyN.js → CtC_CmOY.js} +2 -2
  4. package/dist/devtools-client/_nuxt/{entry.DsZySRVW.css → entry.jOWrhpYd.css} +1 -1
  5. package/dist/devtools-client/index.html +1 -1
  6. package/dist/module.d.mts +1 -0
  7. package/dist/module.json +1 -1
  8. package/dist/module.mjs +10 -2
  9. package/dist/runtime/better-auth/nuxt/server.js +7 -3
  10. package/dist/runtime/better-auth/vue/client.d.ts +3 -3
  11. package/dist/runtime/better-auth/vue/cross-domain.d.ts +26 -0
  12. package/dist/runtime/better-auth/vue/index.d.ts +1 -1
  13. package/dist/runtime/better-auth/vue/plugin.server.js +6 -1
  14. package/dist/runtime/better-auth/vue/token-store.d.ts +1 -0
  15. package/dist/runtime/better-auth/vue/token-store.js +38 -0
  16. package/dist/runtime/better-auth/vue/use-better-auth.d.ts +4 -4
  17. package/dist/runtime/better-auth/vue/use-better-auth.js +89 -46
  18. package/dist/runtime/nuxt/composables/use-async-paginated-query.d.ts +1 -1
  19. package/dist/runtime/nuxt/composables/use-async-paginated-query.js +5 -3
  20. package/dist/runtime/nuxt/composables/use-async-query.d.ts +4 -1
  21. package/dist/runtime/nuxt/composables/use-async-query.js +6 -3
  22. package/dist/runtime/nuxt/config.d.ts +7 -1
  23. package/dist/runtime/nuxt/config.js +9 -3
  24. package/dist/runtime/nuxt/csp.d.ts +2 -1
  25. package/dist/runtime/nuxt/csp.js +9 -3
  26. package/dist/runtime/vue/auth/index.d.ts +2 -1
  27. package/dist/runtime/vue/auth/index.js +62 -13
  28. package/dist/runtime/vue/client.js +13 -0
  29. package/dist/runtime/vue/plugin.d.ts +2 -1
  30. package/dist/runtime/vue/plugin.js +2 -0
  31. package/package.json +12 -12
  32. package/dist/devtools-client/_nuxt/AqohFEu1.js +0 -1
  33. package/dist/devtools-client/_nuxt/BPtiIkWa.js +0 -3
@@ -1 +1 @@
1
- <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><script type="importmap">{"imports":{"#entry":"/__nuxt-convex-module/_nuxt/COXexnyN.js"}}</script><link rel="stylesheet" href="/__nuxt-convex-module/_nuxt/entry.DsZySRVW.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__nuxt-convex-module/_nuxt/COXexnyN.js"><script type="module" src="/__nuxt-convex-module/_nuxt/COXexnyN.js" crossorigin></script><link rel="prefetch" as="script" crossorigin href="/__nuxt-convex-module/_nuxt/BPtiIkWa.js"></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-convex-module",buildId:"d3335991-c747-41b3-a9e3-6d5c636b70f0",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1793365964988,false]</script></body></html>
1
+ <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><script type="importmap">{"imports":{"#entry":"/__nuxt-convex-module/_nuxt/CtC_CmOY.js"}}</script><link rel="stylesheet" href="/__nuxt-convex-module/_nuxt/entry.jOWrhpYd.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__nuxt-convex-module/_nuxt/CtC_CmOY.js"><script type="module" src="/__nuxt-convex-module/_nuxt/CtC_CmOY.js" crossorigin></script><link rel="prefetch" as="script" crossorigin href="/__nuxt-convex-module/_nuxt/C-J9qZjr.js"></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-convex-module",buildId:"ad777e0e-9095-4e0e-9b9b-8010fe4e34c6",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1794085598149,false]</script></body></html>
package/dist/module.d.mts CHANGED
@@ -133,6 +133,7 @@ declare module '@nuxt/schema' {
133
133
  siteUrl: string;
134
134
  crossDomainCallbackRoute: string;
135
135
  loginPath: string;
136
+ authRoute: string;
136
137
  };
137
138
  }
138
139
  }
package/dist/module.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "compatibility": {
6
6
  "nuxt": ">=4.6.0"
7
7
  },
8
- "version": "0.12.2",
8
+ "version": "0.13.0",
9
9
  "builder": {
10
10
  "@nuxt/module-builder": "1.0.3",
11
11
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -95,7 +95,7 @@ function integrationWarnings(flags, isInstalled) {
95
95
  }
96
96
  if (flags.polar && !isInstalled("@polar-sh/checkout")) {
97
97
  warnings.push(
98
- "`@convex-dev/polar` is installed but its peer `@polar-sh/checkout` is not \u2014 `<CheckoutLink>` imports it and the build fails the moment the component is used. Run `npm install @polar-sh/checkout@~0.4.0`."
98
+ "`@convex-dev/polar` is installed but its peer `@polar-sh/checkout` is not \u2014 `<CheckoutLink>` imports it and the build fails the moment the component is used. Run `npm install @polar-sh/checkout@~0.6.0`."
99
99
  );
100
100
  }
101
101
  return warnings;
@@ -383,7 +383,7 @@ const module$1 = defineNuxtModule({
383
383
  // with no extra entry on the consumer's side. The Convex-aware CSP is applied
384
384
  // at runtime (see `registerSecurity`), so module order is irrelevant. Nothing
385
385
  // is declared otherwise: kit fails the build resolving an absent package.
386
- moduleDependencies: (nuxt) => resolveSecurityState(nuxt, readSecurityOption(nuxt)).enabled ? { "nuxt-security": {} } : {},
386
+ moduleDependencies: (nuxt) => resolveSecurityState(nuxt, readSecurityOption(nuxt)).enabled ? { "nuxt-security": nuxt.options.dev ? { defaults: DEV_SECURITY_DEFAULTS } : {} } : {},
387
387
  async setup(options, nuxt) {
388
388
  const resolver = createResolver(import.meta.url);
389
389
  nuxt.options.build.transpile.push(resolver.resolve("./runtime"));
@@ -402,6 +402,7 @@ const module$1 = defineNuxtModule({
402
402
  for (const message of diagnostics.errors) logger.error(message);
403
403
  for (const message of diagnostics.warnings) logger.warn(message);
404
404
  nuxt.options.runtimeConfig.public.convex.loginPath = diagnostics.loginPath;
405
+ nuxt.options.runtimeConfig.public.convex.authRoute = diagnostics.authRoute;
405
406
  registerAuthClientAlias(resolver, nuxt, options);
406
407
  registerConvexAliases(nuxt);
407
408
  registerConvexTypeFallback(nuxt);
@@ -485,6 +486,13 @@ function registerIntegrations(resolver, nuxt, options, autoImports, authRoute) {
485
486
  }
486
487
  return flags;
487
488
  }
489
+ const DEV_SECURITY_DEFAULTS = {
490
+ rateLimiter: false,
491
+ headers: {
492
+ contentSecurityPolicy: { "upgrade-insecure-requests": false },
493
+ strictTransportSecurity: false
494
+ }
495
+ };
488
496
  function readSecurityOption(nuxt) {
489
497
  const opts = nuxt.options;
490
498
  return opts.convex?.security;
@@ -44,6 +44,8 @@ const handler = async (request, siteUrl) => {
44
44
  headers.delete("connection");
45
45
  headers.set("accept-encoding", "application/json");
46
46
  headers.set("host", new URL(siteUrl).host);
47
+ headers.delete("x-forwarded-host");
48
+ headers.delete("x-forwarded-proto");
47
49
  headers.set("x-better-auth-forwarded-host", requestUrl.host);
48
50
  headers.set("x-better-auth-forwarded-proto", requestUrl.protocol.replace(/:$/, ""));
49
51
  const init = {
@@ -82,10 +84,12 @@ const getArgsAndOptions = (args, token) => {
82
84
  return [args[0], { token }];
83
85
  };
84
86
  export function convexAuth(event, opts) {
87
+ const config = getConvexRuntimeConfig(event);
85
88
  const siteUrl = parseConvexSiteUrl(
86
- opts?.convexSiteUrl ?? getConvexRuntimeConfig().siteUrl ?? process.env.NUXT_PUBLIC_CONVEX_SITE_URL
89
+ opts?.convexSiteUrl || config.siteUrl || process.env.NUXT_PUBLIC_CONVEX_SITE_URL
87
90
  );
88
- const cacheKey = `__nuxtConvexToken:${siteUrl}|${opts?.basePath ?? ""}`;
91
+ const basePath = opts?.basePath ?? config.authRoute;
92
+ const cacheKey = `__nuxtConvexToken:${siteUrl}|${basePath ?? ""}`;
89
93
  const tokenCache = event.context;
90
94
  const cachedGetToken = ({ forceRefresh } = {}) => {
91
95
  const cached = tokenCache[cacheKey];
@@ -94,7 +98,7 @@ export function convexAuth(event, opts) {
94
98
  mutableHeaders.delete("content-length");
95
99
  mutableHeaders.delete("transfer-encoding");
96
100
  mutableHeaders.set("accept-encoding", "identity");
97
- const promise = getToken(siteUrl, mutableHeaders, { ...opts, forceRefresh });
101
+ const promise = getToken(siteUrl, mutableHeaders, { ...opts, basePath, forceRefresh });
98
102
  tokenCache[cacheKey] = promise;
99
103
  return promise;
100
104
  };
@@ -1,6 +1,6 @@
1
1
  import { convexClient } from '@convex-dev/better-auth/client/plugins';
2
- import { createAuthClient } from 'better-auth/vue';
3
- export declare const authClient: ReturnType<typeof createAuthClient<{
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 { 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 = typeof 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
  *
@@ -7,7 +7,7 @@
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
9
  export { usePreloadedAuthQuery } from './hydration.js';
10
- export { consumeCrossDomainOneTimeToken, type ConsumeCrossDomainOneTimeTokenOptions } from './cross-domain.js';
10
+ export { consumeCrossDomainOneTimeToken, type AuthClientWithCrossDomain, type ConsumeCrossDomainOneTimeTokenOptions } from './cross-domain.js';
11
11
  export { resolveAuthRedirect } from './redirect.js';
12
12
  export { AuthBoundary } from './auth-boundary.js';
13
13
  export { convexClient, crossDomainClient } from '@convex-dev/better-auth/client/plugins';
@@ -1,9 +1,10 @@
1
1
  import { computed } from "vue";
2
2
  import { defineNuxtPlugin, useRuntimeConfig, useState, useRequestEvent, useResponseHeader } from "#app";
3
3
  import { ConvexVueClient, ConvexClientKey } from "../../vue/client.js";
4
- import { CONVEX_INITIAL_TOKEN_KEY } from "../../nuxt/config.js";
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 { BetterAuthTokenStoreKey, createBetterAuthTokenStore, seedInitialToken } from "./token-store.js";
7
8
  export async function prefetchAuthToken(event, initialToken, cacheControl) {
8
9
  try {
9
10
  const token = await convexAuth(event).getToken();
@@ -19,10 +20,13 @@ export default defineNuxtPlugin({
19
20
  name: "nuxt-convex-module:better-auth:server",
20
21
  async setup(nuxtApp) {
21
22
  const initialToken = useState(CONVEX_INITIAL_TOKEN_KEY, () => null);
23
+ const tokenStore = createBetterAuthTokenStore();
24
+ nuxtApp.vueApp.provide(BetterAuthTokenStoreKey, tokenStore);
22
25
  const url = useRuntimeConfig().public.convex.url;
23
26
  const ssrClient = url ? new ConvexVueClient(url) : void 0;
24
27
  if (ssrClient) {
25
28
  nuxtApp.vueApp.provide(ConvexClientKey, ssrClient);
29
+ closeAfterRender(nuxtApp, ssrClient);
26
30
  }
27
31
  const ssrAuthState = {
28
32
  isLoading: computed(() => true),
@@ -33,6 +37,7 @@ export default defineNuxtPlugin({
33
37
  const event = useRequestEvent();
34
38
  if (event && !import.meta.prerender) {
35
39
  await prefetchAuthToken(event, initialToken, useResponseHeader("Cache-Control"));
40
+ seedInitialToken(tokenStore, initialToken.value);
36
41
  }
37
42
  }
38
43
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ import { ref } from "vue";
2
+ export function createBetterAuthTokenStore() {
3
+ return {
4
+ cachedToken: ref(null),
5
+ cachedTokenVersion: null,
6
+ pendingToken: null,
7
+ pendingTokenVersion: null,
8
+ initialTokenUsed: false,
9
+ initialTokenSessionId: void 0
10
+ };
11
+ }
12
+ export const BetterAuthTokenStoreKey = Symbol("BetterAuthTokenStore");
13
+ export const PENDING_AUTH_VERSION = "__pending__";
14
+ export function readTokenClaims(token) {
15
+ try {
16
+ const payload = token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/");
17
+ return JSON.parse(atob(payload)) ?? void 0;
18
+ } catch {
19
+ return void 0;
20
+ }
21
+ }
22
+ const getTokenSessionId = (token) => {
23
+ const sessionId = readTokenClaims(token)?.sessionId;
24
+ return typeof sessionId === "string" ? sessionId : void 0;
25
+ };
26
+ export function seedInitialToken(store, initialToken) {
27
+ if (store.initialTokenUsed || !initialToken) return;
28
+ store.cachedToken.value = initialToken;
29
+ store.cachedTokenVersion = PENDING_AUTH_VERSION;
30
+ store.initialTokenSessionId = getTokenSessionId(initialToken);
31
+ store.initialTokenUsed = true;
32
+ }
33
+ export function clearToken(store) {
34
+ store.cachedToken.value = null;
35
+ store.cachedTokenVersion = null;
36
+ store.pendingToken = null;
37
+ store.pendingTokenVersion = null;
38
+ }
@@ -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 a token is cached while it reloads. */
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); changes when the signed-in identity changes. */
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.
@@ -1,67 +1,110 @@
1
- import { computed, ref } from "vue";
1
+ import { computed, effectScope, hasInjectionContext, inject, shallowRef, triggerRef, watch } from "vue";
2
2
  import { authClient } from "#convex/auth-client";
3
- const cachedToken = ref(null);
4
- let cachedTokenVersion = null;
5
- let pendingToken = null;
6
- let pendingTokenVersion = null;
7
- let initialTokenUsed = false;
8
- const PENDING_AUTH_VERSION = "__pending__";
9
- const useClientSession = () => authClient.useSession();
10
- export function useBetterAuth(initialToken) {
11
- if (!initialTokenUsed && initialToken) {
12
- cachedToken.value = initialToken;
13
- cachedTokenVersion = PENDING_AUTH_VERSION;
14
- initialTokenUsed = true;
3
+ import { BetterAuthTokenStoreKey, clearToken, createBetterAuthTokenStore, PENDING_AUTH_VERSION, readTokenClaims, seedInitialToken } from "./token-store.js";
4
+ const browserStore = createBetterAuthTokenStore();
5
+ function useTokenStore() {
6
+ if (!import.meta.server) return browserStore;
7
+ return (hasInjectionContext() ? inject(BetterAuthTokenStoreKey, null) : null) ?? createBetterAuthTokenStore();
8
+ }
9
+ const serverSession = () => shallowRef({
10
+ data: null,
11
+ error: null,
12
+ isPending: true,
13
+ isRefetching: false,
14
+ refetch: async () => {
15
15
  }
16
+ });
17
+ let browserSession;
18
+ let browserSessionScope;
19
+ const useClientSession = () => {
20
+ if (import.meta.server) return serverSession();
21
+ browserSessionScope ??= effectScope(true);
22
+ return browserSession ??= browserSessionScope.run(() => {
23
+ const session = authClient.useSession();
24
+ watch(session, ({ data, isPending }) => {
25
+ if (!data && !isPending) clearToken(browserStore);
26
+ }, { flush: "sync" });
27
+ return session;
28
+ });
29
+ };
30
+ export function useBetterAuth(initialToken) {
31
+ const store = useTokenStore();
32
+ seedInitialToken(store, initialToken);
33
+ const { cachedToken } = store;
16
34
  const session = useClientSession();
17
35
  const client = authClient;
18
36
  const authVersion = computed(() => {
19
37
  const data = session.value.data;
20
- return data?.session?.id ?? data?.user?.id ?? null;
38
+ const id = data?.session?.id ?? data?.user?.id ?? null;
39
+ return id === null && session.value.isPending ? void 0 : id;
21
40
  });
22
- const currentAuthVersion = () => authVersion.value ?? (session.value.isPending ? PENDING_AUTH_VERSION : null);
41
+ const currentAuthVersion = () => authVersion.value === void 0 ? PENDING_AUTH_VERSION : authVersion.value;
42
+ const holdsAnotherSessionsToken = computed(() => {
43
+ const data = session.value.data;
44
+ if (!data || !cachedToken.value || store.cachedTokenVersion !== PENDING_AUTH_VERSION) return false;
45
+ if (store.initialTokenSessionId && data.session?.id) return store.initialTokenSessionId !== data.session.id;
46
+ const subject = readTokenClaims(cachedToken.value)?.sub;
47
+ return !!data.user?.id && subject !== void 0 && subject !== data.user.id;
48
+ });
49
+ const isServerTokenActive = () => session.value.isPending && cachedToken.value !== null && store.cachedTokenVersion === PENDING_AUTH_VERSION;
23
50
  const fetchAccessToken = async ({
24
51
  forceRefreshToken = false
25
52
  } = {}) => {
26
53
  const version = currentAuthVersion();
27
54
  if (version === null) {
28
- cachedToken.value = null;
29
- cachedTokenVersion = null;
30
- pendingToken = null;
31
- pendingTokenVersion = null;
55
+ clearToken(store);
32
56
  return null;
33
57
  }
34
- if (cachedToken.value && !forceRefreshToken && cachedTokenVersion === version) {
35
- return cachedToken.value;
58
+ if (store.cachedTokenVersion === PENDING_AUTH_VERSION && store.initialTokenSessionId === version) {
59
+ store.cachedTokenVersion = version;
36
60
  }
37
- if (!forceRefreshToken && pendingToken && pendingTokenVersion === version) {
38
- return pendingToken;
61
+ if (!forceRefreshToken) {
62
+ if (store.pendingToken && store.pendingTokenVersion === version) {
63
+ return store.pendingToken;
64
+ }
65
+ if (cachedToken.value && store.cachedTokenVersion === version) {
66
+ return cachedToken.value;
67
+ }
39
68
  }
40
- pendingTokenVersion = version;
41
- pendingToken = client.convex.token({ fetchOptions: { throw: false } }).then(({ data }) => {
69
+ const afterFailure = () => {
70
+ if (cachedToken.value && !holdsAnotherSessionsToken.value && (store.cachedTokenVersion === version || store.cachedTokenVersion === PENDING_AUTH_VERSION)) {
71
+ return cachedToken.value;
72
+ }
73
+ if (isLatest()) {
74
+ cachedToken.value = null;
75
+ store.cachedTokenVersion = version;
76
+ }
77
+ return null;
78
+ };
79
+ store.pendingTokenVersion = version;
80
+ const isLatest = () => store.pendingToken === pending;
81
+ const pending = store.pendingToken = client.convex.token({ fetchOptions: { throw: false } }).then(({ data, error }) => {
82
+ if (error && error.status !== 401) {
83
+ return afterFailure();
84
+ }
42
85
  const token = data?.token || null;
43
- cachedToken.value = token;
44
- cachedTokenVersion = version;
86
+ if (isLatest()) {
87
+ store.cachedTokenVersion = version;
88
+ cachedToken.value = token;
89
+ triggerRef(cachedToken);
90
+ }
45
91
  return token;
46
- }).catch(() => {
47
- cachedToken.value = null;
48
- cachedTokenVersion = version;
49
- return null;
50
- }).finally(() => {
51
- pendingToken = null;
52
- pendingTokenVersion = null;
92
+ }).catch(afterFailure).finally(() => {
93
+ if (isLatest()) {
94
+ store.pendingToken = null;
95
+ store.pendingTokenVersion = null;
96
+ }
53
97
  });
54
- return pendingToken;
98
+ return pending;
55
99
  };
56
100
  return {
57
- isLoading: computed(() => session.value.isPending && !cachedToken.value),
101
+ isLoading: computed(() => session.value.isPending && !isServerTokenActive() || holdsAnotherSessionsToken.value),
58
102
  // PARITY: D-03
59
- // Diverges from upstream's `Boolean(session?.session) || cachedToken !== null`:
60
- // a settled signed-out session must read unauthenticated immediately (Vue has
61
- // no re-render to run upstream's cache-clearing effect first), and Better Auth
62
- // session data without a nested `session` still counts as signed in.
103
+ // Diverges from upstream's `Boolean(session?.session) || isSsrTokenActive`:
104
+ // Better Auth session data without a nested `session` still counts as
105
+ // signed in.
63
106
  isAuthenticated: computed(
64
- () => !!session.value.data || session.value.isPending && cachedToken.value !== null
107
+ () => !!session.value.data || isServerTokenActive()
65
108
  ),
66
109
  fetchAccessToken,
67
110
  // PARITY: A-06 — Vue-only service extensions.
@@ -75,9 +118,9 @@ export function useBetterAuth(initialToken) {
75
118
  };
76
119
  }
77
120
  export function __resetUseBetterAuthForTests() {
78
- cachedToken.value = null;
79
- cachedTokenVersion = null;
80
- pendingToken = null;
81
- pendingTokenVersion = null;
82
- initialTokenUsed = false;
121
+ Object.assign(browserStore, createBetterAuthTokenStore(), { cachedToken: browserStore.cachedToken });
122
+ browserStore.cachedToken.value = null;
123
+ browserSessionScope?.stop();
124
+ browserSessionScope = void 0;
125
+ browserSession = void 0;
83
126
  }
@@ -32,7 +32,7 @@ export interface AsyncPaginatedQueryData<Item> {
32
32
  status: ComputedRef<PaginationStatus>;
33
33
  /** `true` while no page — server or live — is available yet, or more rows are loading. */
34
34
  isLoading: ComputedRef<boolean>;
35
- /** The SSR fetch error or the live subscription's, `null` when none. Never thrown. */
35
+ /** The live subscription's error, or the SSR fetch error until the live query has a page; `null` when none. Never thrown. */
36
36
  error: ComputedRef<Error | null>;
37
37
  /**
38
38
  * Fetch `numItems` more rows. Takes effect once the live subscription has
@@ -70,9 +70,11 @@ export function useAsyncPaginatedQuery(query, args, options) {
70
70
  return "LoadingFirstPage";
71
71
  });
72
72
  const isLoading = computed(() => status.value === "LoadingFirstPage" || status.value === "LoadingMore");
73
- const error = computed(
74
- () => liveState.value?.error ?? clientError.value ?? asyncData.error.value ?? null
75
- );
73
+ const error = computed(() => {
74
+ const liveError = liveState.value?.error ?? clientError.value;
75
+ if (liveError) return liveError;
76
+ return liveHasPage.value ? null : asyncData.error.value ?? null;
77
+ });
76
78
  const loadMore = (numItems) => {
77
79
  live?.loadMore(numItems);
78
80
  };
@@ -66,7 +66,10 @@ export interface AsyncQueryReturn<T> extends PromiseLike<AsyncQueryData<T>> {
66
66
  * nothing has loaded (or while skipped).
67
67
  */
68
68
  data: ComputedRef<T | undefined>;
69
- /** The initial-fetch or live-subscription error, `null` when none. */
69
+ /**
70
+ * The initial-fetch or live-subscription error, `null` when none. A failed
71
+ * initial fetch stops counting once the live subscription has a value.
72
+ */
70
73
  error: ComputedRef<Error | null>;
71
74
  /**
72
75
  * Initial-fetch status. Live pushes don't churn it: once data exists it
@@ -75,9 +75,12 @@ export function useAsyncQuery(query, args, options = {}) {
75
75
  const data = computed(
76
76
  () => liveValue.value !== void 0 ? liveValue.value : payloadValue.value
77
77
  );
78
- const error = computed(
79
- () => liveError.value ?? asyncData.error.value ?? null
80
- );
78
+ const error = computed(() => {
79
+ if (liveError.value) {
80
+ return liveError.value;
81
+ }
82
+ return liveValue.value !== void 0 ? null : asyncData.error.value ?? null;
83
+ });
81
84
  const status = computed(() => {
82
85
  if (liveError.value) {
83
86
  return "error";
@@ -8,11 +8,17 @@ export declare const CONVEX_INITIAL_TOKEN_KEY = "convex:initialToken";
8
8
  /**
9
9
  * The module's runtime config, tolerating a missing Nuxt context (a plain
10
10
  * server import outside a request) by returning an empty object.
11
+ *
12
+ * Given a server route's event, it reads that request's config: an
13
+ * environment variable set after the server started (by a Nitro plugin, say)
14
+ * reaches it, where the config read without one was fixed at startup.
11
15
  */
12
- export declare function getConvexRuntimeConfig(): {
16
+ export declare function getConvexRuntimeConfig(event?: unknown): {
13
17
  url: string | undefined;
14
18
  siteUrl: string | undefined;
19
+ authRoute: string | undefined;
15
20
  } | {
16
21
  url?: undefined;
17
22
  siteUrl?: undefined;
23
+ authRoute?: undefined;
18
24
  };
@@ -1,11 +1,17 @@
1
1
  import { useRuntimeConfig } from "#imports";
2
2
  export const CONVEX_INITIAL_TOKEN_KEY = "convex:initialToken";
3
- export function getConvexRuntimeConfig() {
3
+ export function closeAfterRender(nuxtApp, client) {
4
+ const close = () => void client.close();
5
+ nuxtApp.hook("app:rendered", close);
6
+ nuxtApp.hook("app:error", close);
7
+ }
8
+ export function getConvexRuntimeConfig(event) {
4
9
  try {
5
- const config = useRuntimeConfig();
10
+ const config = useRuntimeConfig(event);
6
11
  return {
7
12
  url: config.public?.convex?.url,
8
- siteUrl: config.public?.convex?.siteUrl
13
+ siteUrl: config.public?.convex?.siteUrl,
14
+ authRoute: config.public?.convex?.authRoute
9
15
  };
10
16
  } catch {
11
17
  return {};
@@ -37,7 +37,8 @@ export declare function convexResourceSrc(url?: string, siteUrl?: string): strin
37
37
  *
38
38
  * For each directive we keep any value the user already set and *append* the
39
39
  * Convex origins, so consumers extend (add their own third parties) rather than
40
- * fight the defaults, and the Convex origins are always present. A user who sets
40
+ * fight the defaults, and the Convex origins are always present. That covers a
41
+ * directive the user left to `default-src`, whose places carry over. A user who sets
41
42
  * `contentSecurityPolicy: false` (CSP disabled) is left untouched, as is a rule
42
43
  * set with nothing to add (no Convex URL).
43
44
  */
@@ -39,8 +39,14 @@ export function applyConvexSecurityDefaults(rules, connectSrc, resourceSrc) {
39
39
  tightenDirective(csp, "media-src", ["'self'"], resourceSrc);
40
40
  }
41
41
  function tightenDirective(csp, name, baseline, additions) {
42
- if (additions.length === 0) return;
43
- const current = csp[name];
44
- const existing = Array.isArray(current) ? current : typeof current === "string" ? current.split(/\s+/).filter(Boolean) : baseline;
42
+ if (additions.length === 0 || csp[name] === false) return;
43
+ const existing = sourcesOf(csp[name]) ?? [...baseline, ...inheritedSources(csp)];
45
44
  csp[name] = uniq([...existing, ...additions]);
46
45
  }
46
+ function sourcesOf(value) {
47
+ if (Array.isArray(value)) return value;
48
+ return typeof value === "string" ? value.split(/\s+/).filter(Boolean) : void 0;
49
+ }
50
+ function inheritedSources(csp) {
51
+ return (sourcesOf(csp["default-src"]) ?? []).filter((source) => source === "'self'" || !source.startsWith("'"));
52
+ }
@@ -66,7 +66,8 @@ export interface ConvexAuthProviderOptions {
66
66
  * Alternative re-authentication trigger for providers that hand out a
67
67
  * stable fetcher function: when this value changes, auth state transitions
68
68
  * back to loading, `clearAuth` runs, and `fetchAccessToken` is called
69
- * again against the new auth context.
69
+ * again against the new auth context. `undefined` means the provider does
70
+ * not know the context yet, so the first value it learns is no change.
70
71
  */
71
72
  authVersion?: MaybeRefOrGetter<unknown>;
72
73
  };