nuxt-convex-module 0.0.0 → 0.0.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.
Files changed (70) hide show
  1. package/README.md +78 -24
  2. package/dist/devtools-client/200.html +1 -1
  3. package/dist/devtools-client/404.html +1 -1
  4. package/dist/devtools-client/_nuxt/{CZbAGcNC.js → BAaUCl_m.js} +1 -1
  5. package/dist/devtools-client/_nuxt/BmBQNDID.js +1 -0
  6. package/dist/devtools-client/_nuxt/C2KGYsR4.js +1 -0
  7. package/dist/devtools-client/_nuxt/{DPWXUmcI.js → CKakiU_h.js} +1 -1
  8. package/dist/devtools-client/_nuxt/CgIEvOQU.js +2 -0
  9. package/dist/devtools-client/_nuxt/Cm7YqZQ-.js +1 -0
  10. package/dist/devtools-client/_nuxt/CtxksWnu.js +1 -0
  11. package/dist/devtools-client/_nuxt/D2ksFYuU.js +2 -0
  12. package/dist/devtools-client/_nuxt/{dy_xXcv9.js → DQmVr-EY.js} +1 -1
  13. package/dist/devtools-client/_nuxt/D_Rij7MD.js +1 -0
  14. package/dist/devtools-client/_nuxt/EZ4-8Xzm.js +3 -0
  15. package/dist/devtools-client/_nuxt/Kba0XpjH.js +1 -0
  16. package/dist/devtools-client/_nuxt/OVE7s6ty.js +1 -0
  17. package/dist/devtools-client/_nuxt/builds/latest.json +1 -1
  18. package/dist/devtools-client/_nuxt/builds/meta/23303226-2230-4d14-87ce-87b0f562de6c.json +1 -0
  19. package/dist/devtools-client/_nuxt/entry.HF8C6P11.css +1 -0
  20. package/dist/devtools-client/_nuxt/error-404.BpRUKU69.css +1 -0
  21. package/dist/devtools-client/_nuxt/error-500.DuDYJ5TI.css +1 -0
  22. package/dist/devtools-client/_nuxt/queries.BmISPDaV.css +1 -0
  23. package/dist/devtools-client/_nuxt/vZZlEX5A.js +1 -0
  24. package/dist/devtools-client/auth/index.html +1 -1
  25. package/dist/devtools-client/index.html +1 -1
  26. package/dist/devtools-client/logs/index.html +1 -1
  27. package/dist/devtools-client/queries/index.html +1 -1
  28. package/dist/{module.d.ts → module.d.mts} +51 -35
  29. package/dist/module.json +1 -1
  30. package/dist/{module.js → module.mjs} +63 -84
  31. package/dist/runtime/better-auth/nuxt/middleware.d.ts +9 -1
  32. package/dist/runtime/better-auth/nuxt/middleware.js +11 -7
  33. package/dist/runtime/better-auth/nuxt/server.js +15 -0
  34. package/dist/runtime/better-auth/vue/auth-boundary.d.ts +2 -1
  35. package/dist/runtime/better-auth/vue/client.d.ts +1 -1
  36. package/dist/runtime/better-auth/vue/cross-domain.d.ts +4 -5
  37. package/dist/runtime/better-auth/vue/cross-domain.js +4 -2
  38. package/dist/runtime/better-auth/vue/index.d.ts +1 -0
  39. package/dist/runtime/better-auth/vue/index.js +1 -0
  40. package/dist/runtime/better-auth/vue/redirect.d.ts +45 -0
  41. package/dist/runtime/better-auth/vue/redirect.js +14 -0
  42. package/dist/runtime/better-auth/vue/use-auth.js +2 -1
  43. package/dist/runtime/devtools/bridge.js +1 -1
  44. package/dist/runtime/nuxt/csp.d.ts +44 -0
  45. package/dist/runtime/nuxt/csp.js +46 -0
  46. package/dist/runtime/nuxt/index.d.ts +6 -3
  47. package/dist/runtime/nuxt/security.d.ts +33 -0
  48. package/dist/runtime/nuxt/security.js +14 -0
  49. package/dist/runtime/polar/vue/components.js +1 -1
  50. package/dist/runtime/vue/auth/index.js +6 -2
  51. package/dist/runtime/vue/client.d.ts +2 -0
  52. package/dist/runtime/vue/client.js +2 -0
  53. package/dist/types.d.mts +3 -0
  54. package/package.json +58 -47
  55. package/dist/devtools-client/_nuxt/B29GGJ5n.js +0 -1
  56. package/dist/devtools-client/_nuxt/BSDEpQet.js +0 -1
  57. package/dist/devtools-client/_nuxt/BY23OQOo.js +0 -1
  58. package/dist/devtools-client/_nuxt/BZYRaL-G.js +0 -1
  59. package/dist/devtools-client/_nuxt/Byw6FxnC.js +0 -1
  60. package/dist/devtools-client/_nuxt/CJJ_hcZN.js +0 -1
  61. package/dist/devtools-client/_nuxt/CS6WMFOx.js +0 -2
  62. package/dist/devtools-client/_nuxt/Cr6_aklh.js +0 -2
  63. package/dist/devtools-client/_nuxt/DNfpyBVU.js +0 -3
  64. package/dist/devtools-client/_nuxt/builds/meta/a7708498-bd09-4de2-b5ab-19b380293c77.json +0 -1
  65. package/dist/devtools-client/_nuxt/entry.DbTJhkHT.css +0 -1
  66. package/dist/devtools-client/_nuxt/error-404.CwGGO2Kz.css +0 -1
  67. package/dist/devtools-client/_nuxt/error-500.8ZJqCQm8.css +0 -1
  68. package/dist/devtools-client/_nuxt/ncgEdbEt.js +0 -1
  69. package/dist/devtools-client/_nuxt/queries.CE_dtt9-.css +0 -1
  70. package/dist/types.d.ts +0 -3
@@ -1,4 +1,4 @@
1
- import { useLogger, defineNuxtModule, createResolver, addPlugin, addPluginTemplate, addTypeTemplate, updateTemplates, addImports, addComponent, addServerImports, addServerHandler, extendRouteRules, addRouteMiddleware } from '@nuxt/kit';
1
+ import { useLogger, defineNuxtModule, createResolver, addPlugin, hasNuxtModule, addServerPlugin, addPluginTemplate, addTypeTemplate, updateTemplates, addImports, addComponent, addServerImports, addServerHandler, extendRouteRules, addRouteMiddleware } from '@nuxt/kit';
2
2
  import { join, isAbsolute } from 'node:path';
3
3
  import { existsSync, readFileSync } from 'node:fs';
4
4
  import { createRequire } from 'node:module';
@@ -49,14 +49,13 @@ const module$1 = defineNuxtModule({
49
49
  authRoute: "/api/auth",
50
50
  devtools: true
51
51
  },
52
- // nuxt-security is an integral part of the integration: declaring it as a
53
- // module dependency makes Nuxt's core loader install it (hoisting its types
54
- // and deduping if you also register it yourself), and guarantees our setup
55
- // runs *before* it — so the Convex-aware CSP defaults below land before
56
- // nuxt-security reads its config. Configure it via the `security` key.
57
- moduleDependencies: {
58
- "nuxt-security": {}
59
- },
52
+ // nuxt-security is optional. When the app has it installed (and hasn't
53
+ // switched it off), declare it as a module dependency so Nuxt's core loader
54
+ // registers it — deduping if it's already in `modules`, hoisting its types —
55
+ // with no extra entry on the consumer's side. The Convex-aware CSP is applied
56
+ // at runtime (see `registerSecurity`), so module order is irrelevant. Nothing
57
+ // is declared otherwise: kit fails the build resolving an absent package.
58
+ moduleDependencies: (nuxt) => resolveSecurityState(nuxt, readSecurityOption(nuxt)).enabled ? { "nuxt-security": {} } : {},
60
59
  async setup(options, nuxt) {
61
60
  const resolver = createResolver(import.meta.url);
62
61
  nuxt.options.build.transpile.push(resolver.resolve("./runtime"));
@@ -79,7 +78,6 @@ const module$1 = defineNuxtModule({
79
78
  registerAuthComponents(resolver);
80
79
  registerServerImports(resolver);
81
80
  const integrations = registerIntegrations(resolver, nuxt, options);
82
- applyConvexCsp(nuxt, url, siteUrl);
83
81
  watchConvexCodegen(nuxt);
84
82
  if (nuxt.options.dev && options.devtools !== false && isDevtoolsUiEnabled(nuxt)) {
85
83
  const { setupDevtools } = await import('./chunks/index.mjs');
@@ -106,6 +104,11 @@ function resolveIntegrationState(explicit, installed) {
106
104
  if (explicit === void 0) return { enabled: installed, missingPackage: false };
107
105
  return installed ? { enabled: true, missingPackage: false } : { enabled: false, missingPackage: true };
108
106
  }
107
+ function stripTrailingSlashes(value) {
108
+ let end = value.length;
109
+ while (end > 0 && value[end - 1] === "/") end--;
110
+ return value.slice(0, end);
111
+ }
109
112
  function validateModuleOptions(input) {
110
113
  const errors = [];
111
114
  const warnings = [];
@@ -135,7 +138,7 @@ function validateModuleOptions(input) {
135
138
  );
136
139
  }
137
140
  if (authRoute.length > 1 && authRoute.endsWith("/")) {
138
- authRoute = authRoute.replace(/\/+$/, "");
141
+ authRoute = stripTrailingSlashes(authRoute);
139
142
  }
140
143
  if (input.authClient && !authClientModuleExists(input.authClient, input.rootDir)) {
141
144
  errors.push(
@@ -166,19 +169,20 @@ function formatStartupSummary(url, functionsDir, integrations) {
166
169
  betterAuth: "better-auth",
167
170
  clerk: "clerk",
168
171
  auth0: "auth0",
169
- polar: "polar"
172
+ polar: "polar",
173
+ security: "nuxt-security"
170
174
  };
171
175
  const enabled = Object.keys(names).filter((key) => integrations[key]).map((key) => names[key]);
172
176
  return `Convex ${url || "(no URL)"} \xB7 functions: ${functionsDir}/ \xB7 integrations: ${enabled.join(", ") || "none"}`;
173
177
  }
174
178
  function registerIntegrations(resolver, nuxt, options) {
175
- const resolve = (key, pkg) => {
176
- const state = resolveIntegrationState(options[key], isPackageInstalled(pkg, nuxt.options.rootDir));
179
+ const report = (key, pkg, state) => {
177
180
  if (state.missingPackage) {
178
181
  logger.error(`\`convex.${key}\` is enabled but \`${pkg}\` is not installed. Run \`npm install ${pkg}\` or remove the option.`);
179
182
  }
180
183
  return state.enabled;
181
184
  };
185
+ const resolve = (key, pkg) => report(key, pkg, resolveIntegrationState(options[key], isPackageInstalled(pkg, nuxt.options.rootDir)));
182
186
  const betterAuth = resolve("betterAuth", "@convex-dev/better-auth");
183
187
  if (betterAuth) {
184
188
  registerBetterAuth(resolver, options.authRoute || "/api/auth");
@@ -197,49 +201,59 @@ function registerIntegrations(resolver, nuxt, options) {
197
201
  if (polar) {
198
202
  registerPolarComponents(resolver);
199
203
  }
200
- return { betterAuth, clerk, auth0, polar };
204
+ const security = report("security", "nuxt-security", resolveSecurityState(nuxt, options.security));
205
+ if (security) {
206
+ registerSecurity(resolver);
207
+ }
208
+ return { betterAuth, clerk, auth0, polar, security };
201
209
  }
202
- function applyConvexCsp(nuxt, url, siteUrl) {
210
+ function readSecurityOption(nuxt) {
203
211
  const opts = nuxt.options;
204
- if (!url || opts.security === false) return;
205
- const security = opts.security ??= {};
206
- applyConvexSecurityDefaults(
207
- security,
208
- nuxt.options.dev ? [] : convexConnectSrc(url, siteUrl),
209
- convexResourceSrc(url)
210
- );
212
+ return opts.convex?.security;
213
+ }
214
+ function resolveSecurityState(nuxt, explicit) {
215
+ const opts = nuxt.options;
216
+ if (opts.security === false) return { enabled: false, missingPackage: false };
217
+ const installed = hasNuxtModule("nuxt-security", nuxt) || isPackageInstalled("nuxt-security", nuxt.options.rootDir);
218
+ return resolveIntegrationState(explicit, installed);
219
+ }
220
+ function registerSecurity(resolver) {
221
+ addServerPlugin(resolver.resolve("./runtime/nuxt/security"));
211
222
  }
212
223
  function isPackageInstalled(id, rootDir) {
213
224
  for (const base of [join(rootDir, "package.json"), import.meta.url]) {
214
- const require = createRequire(base);
215
- for (const target of [id, `${id}/package.json`]) {
216
- try {
217
- require.resolve(target);
218
- return true;
219
- } catch {
220
- }
221
- }
225
+ const lookupPaths = createRequire(base).resolve.paths(id) ?? [];
226
+ if (lookupPaths.some((dir) => existsSync(join(dir, id, "package.json")))) return true;
222
227
  }
223
228
  return false;
224
229
  }
230
+ function resolveDeploymentUrls(options, env) {
231
+ return {
232
+ url: options.url || env.NUXT_PUBLIC_CONVEX_URL || env.CONVEX_URL || "",
233
+ siteUrl: options.siteUrl || env.NUXT_PUBLIC_CONVEX_SITE_URL || env.CONVEX_SITE_URL || ""
234
+ };
235
+ }
225
236
  function applyRuntimeConfig(nuxt, options) {
226
- const url = nuxt.options._prepare ? void 0 : options.url || process.env.NUXT_PUBLIC_CONVEX_URL;
237
+ const resolved = resolveDeploymentUrls(options, process.env);
238
+ const url = nuxt.options._prepare ? "" : resolved.url;
227
239
  if (!url && !nuxt.options._prepare) {
228
240
  logger.warn(
229
- "No Convex deployment URL configured. Set NUXT_PUBLIC_CONVEX_URL or `convex.url` in nuxt.config. Note: `npx convex dev` writes CONVEX_URL to .env.local, which Nuxt does not load."
241
+ "No Convex deployment URL configured. Set NUXT_PUBLIC_CONVEX_URL or `convex.url` in nuxt.config. Note: `npx convex dev` writes CONVEX_URL to .env.local, which Nuxt does not read without `nuxt dev --dotenv .env.local`."
230
242
  );
231
243
  }
232
- const siteUrl = options.siteUrl || process.env.NUXT_PUBLIC_CONVEX_SITE_URL || "";
244
+ const siteUrl = resolved.siteUrl;
233
245
  const crossDomainCallbackRoute = typeof options.betterAuth === "object" && options.betterAuth.crossDomainCallbackRoute || "";
246
+ const loginPath = typeof options.betterAuth === "object" && options.betterAuth.loginPath || "/login";
234
247
  const runtimeConfig = nuxt.options.runtimeConfig;
235
248
  runtimeConfig.public.convex = {
236
249
  ...runtimeConfig.public.convex,
237
- url: url || "",
250
+ url,
238
251
  siteUrl,
239
- crossDomainCallbackRoute
252
+ crossDomainCallbackRoute,
253
+ loginPath
240
254
  };
241
255
  runtimeConfig.convex = { ...runtimeConfig.convex, siteUrl };
242
- return { url: url || "", siteUrl };
256
+ return { url, siteUrl };
243
257
  }
244
258
  function getConvexAliases(rootDir) {
245
259
  const functionsDir = resolveFunctionsDir(rootDir);
@@ -395,12 +409,17 @@ function registerServerImports(resolver) {
395
409
  const serverUtils = ["fetchQuery", "fetchMutation", "fetchAction", "preloadQuery", "preloadedQueryResult"];
396
410
  addServerImports(serverUtils.map((name) => ({ name, from: fromNuxt })));
397
411
  }
412
+ const AUTH_PROXY_SECURITY_RULES = {
413
+ xssValidator: false,
414
+ allowedMethodsRestricter: { methods: ["GET", "HEAD", "POST", "OPTIONS"] }
415
+ };
398
416
  function registerBetterAuth(resolver, authRoute) {
399
417
  addPlugin(resolver.resolve("./runtime/better-auth/vue/plugin.client"));
400
418
  addPlugin(resolver.resolve("./runtime/better-auth/vue/plugin.server"));
401
419
  addImports([
402
420
  { name: "useAuth", from: resolver.resolve("./runtime/better-auth/vue/use-auth") },
403
- { name: "usePreloadedAuthQuery", from: resolver.resolve("./runtime/better-auth/vue/hydration") }
421
+ { name: "usePreloadedAuthQuery", from: resolver.resolve("./runtime/better-auth/vue/hydration") },
422
+ { name: "resolveAuthRedirect", from: resolver.resolve("./runtime/better-auth/vue/redirect") }
404
423
  ]);
405
424
  addComponent({
406
425
  name: "AuthBoundary",
@@ -411,7 +430,11 @@ function registerBetterAuth(resolver, authRoute) {
411
430
  route: `${authRoute}/**`,
412
431
  handler: resolver.resolve("./runtime/better-auth/nuxt/proxy")
413
432
  });
414
- extendRouteRules(`${authRoute}/**`, { cache: false, prerender: false });
433
+ extendRouteRules(`${authRoute}/**`, {
434
+ cache: false,
435
+ prerender: false,
436
+ security: AUTH_PROXY_SECURITY_RULES
437
+ });
415
438
  addRouteMiddleware({
416
439
  name: "auth",
417
440
  path: resolver.resolve("./runtime/better-auth/nuxt/middleware"),
@@ -441,49 +464,5 @@ function registerPolarComponents(resolver) {
441
464
  addComponent({ name, filePath: componentsFile, export: name });
442
465
  }
443
466
  }
444
- function toHttpOrigin(raw) {
445
- if (!raw) return void 0;
446
- try {
447
- return new URL(raw).origin;
448
- } catch {
449
- return void 0;
450
- }
451
- }
452
- function toWsOrigin(raw) {
453
- if (!raw) return void 0;
454
- try {
455
- const u = new URL(raw);
456
- return `${u.protocol === "https:" ? "wss:" : "ws:"}//${u.host}`;
457
- } catch {
458
- return void 0;
459
- }
460
- }
461
- function uniq(values) {
462
- return Array.from(new Set(values.filter((v) => Boolean(v))));
463
- }
464
- function convexConnectSrc(url, siteUrl) {
465
- return uniq([toHttpOrigin(url), toWsOrigin(url), toHttpOrigin(siteUrl), toWsOrigin(siteUrl)]);
466
- }
467
- function convexResourceSrc(url) {
468
- return uniq([toHttpOrigin(url)]);
469
- }
470
- function applyConvexSecurityDefaults(security, connectSrc, resourceSrc) {
471
- if (security.headers === false) return;
472
- if (typeof security.headers !== "object" || security.headers === null) security.headers = {};
473
- const headers = security.headers;
474
- if (headers.contentSecurityPolicy === false) return;
475
- if (typeof headers.contentSecurityPolicy !== "object" || headers.contentSecurityPolicy === null) {
476
- headers.contentSecurityPolicy = {};
477
- }
478
- const csp = headers.contentSecurityPolicy;
479
- tightenDirective(csp, "connect-src", ["'self'"], connectSrc);
480
- tightenDirective(csp, "img-src", ["'self'", "data:"], resourceSrc);
481
- tightenDirective(csp, "media-src", ["'self'"], resourceSrc);
482
- }
483
- function tightenDirective(csp, name, baseline, additions) {
484
- if (additions.length === 0) return;
485
- const existing = Array.isArray(csp[name]) ? csp[name] : baseline;
486
- csp[name] = uniq([...existing, ...additions]);
487
- }
488
467
 
489
- export { applyConvexSecurityDefaults, convexConnectSrc, convexResourceSrc, convexTypeFallbackContents, module$1 as default, formatStartupSummary, getConvexAliases, resolveIntegrationState, validateModuleOptions };
468
+ export { convexTypeFallbackContents, module$1 as default, formatStartupSummary, getConvexAliases, resolveDeploymentUrls, resolveIntegrationState, validateModuleOptions };
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * Auth route middleware — protects pages from unauthenticated access.
3
3
  *
4
+ * Unauthenticated visitors are sent to the configured login route
5
+ * (`convex.betterAuth.loginPath`, default `/login`) with the original
6
+ * destination in a `?redirect=` query.
7
+ *
4
8
  * Usage in page:
5
9
  * ```vue
6
10
  * <script setup>
@@ -8,7 +12,11 @@
8
12
  * </script>
9
13
  * ```
10
14
  */
15
+ interface GuardedRoute {
16
+ path: string;
17
+ fullPath: string;
18
+ }
11
19
  /** Exported for unit tests — `import.meta.server` is compile-time. @internal */
12
- export declare function serverGuard(path: string): Promise<string | false | void | import("vue-router").RouteLocationAsRelativeGeneric | import("vue-router").RouteLocationAsPathGeneric | import("vue-router").NavigationFailure>;
20
+ export declare function serverGuard(to: GuardedRoute, loginPath: string): Promise<string | false | void | import("vue-router").RouteLocationAsRelativeGeneric | import("vue-router").RouteLocationAsPathGeneric | import("vue-router").NavigationFailure>;
13
21
  declare const _default: import("nuxt/app").RouteMiddleware;
14
22
  export default _default;
@@ -1,14 +1,17 @@
1
- import { defineNuxtRouteMiddleware, navigateTo, useNuxtApp, useRequestEvent } from "#app";
1
+ import { defineNuxtRouteMiddleware, navigateTo, useNuxtApp, useRequestEvent, useRuntimeConfig } from "#app";
2
2
  import { watch } from "vue";
3
3
  import { useAuth } from "../vue/use-auth.js";
4
4
  import { convexAuth } from "./server.js";
5
- export async function serverGuard(path) {
5
+ function loginTarget(to, loginPath) {
6
+ return { path: loginPath, query: { redirect: to.fullPath } };
7
+ }
8
+ export async function serverGuard(to, loginPath) {
6
9
  const event = useRequestEvent();
7
10
  if (!event) return;
8
11
  const nuxtApp = useNuxtApp();
9
12
  const authed = await convexAuth(event).isAuthenticated();
10
- if (!authed && path !== "/login") {
11
- return nuxtApp.runWithContext(() => navigateTo("/login"));
13
+ if (!authed && to.path !== loginPath) {
14
+ return nuxtApp.runWithContext(() => navigateTo(loginTarget(to, loginPath)));
12
15
  }
13
16
  }
14
17
  function waitForSession(isPending) {
@@ -26,14 +29,15 @@ function waitForSession(isPending) {
26
29
  });
27
30
  }
28
31
  export default defineNuxtRouteMiddleware(async (to) => {
32
+ const loginPath = useRuntimeConfig().public.convex.loginPath || "/login";
29
33
  if (import.meta.server) {
30
- return serverGuard(to.path);
34
+ return serverGuard(to, loginPath);
31
35
  }
32
36
  const { session } = useAuth();
33
37
  if (session.value.isPending) {
34
38
  await waitForSession(() => session.value.isPending);
35
39
  }
36
- if (!session.value.data && to.path !== "/login") {
37
- return navigateTo("/login");
40
+ if (!session.value.data && to.path !== loginPath) {
41
+ return navigateTo(loginTarget(to, loginPath));
38
42
  }
39
43
  });
@@ -11,6 +11,21 @@ const parseConvexSiteUrl = (url) => {
11
11
  if (url.endsWith(".convex.cloud")) {
12
12
  throw new Error(
13
13
  `NUXT_PUBLIC_CONVEX_SITE_URL should be set to your Convex Site URL, which ends in .convex.site.
14
+ Currently set to ${url}.`
15
+ );
16
+ }
17
+ let protocol;
18
+ try {
19
+ protocol = new URL(url).protocol;
20
+ } catch {
21
+ throw new Error(
22
+ `NUXT_PUBLIC_CONVEX_SITE_URL must be an absolute URL, including the scheme.
23
+ Currently set to ${url}.`
24
+ );
25
+ }
26
+ if (protocol !== "https:" && protocol !== "http:") {
27
+ throw new Error(
28
+ `NUXT_PUBLIC_CONVEX_SITE_URL must use http: or https:.
14
29
  Currently set to ${url}.`
15
30
  );
16
31
  }
@@ -1,7 +1,7 @@
1
1
  import { type PropType, type VNode } from 'vue';
2
2
  import type { FunctionReference } from 'convex/server';
3
- import type { EmptyObject } from 'convex-helpers';
4
3
  import type { AuthClient } from '#convex/auth-client';
4
+ type EmptyObject = Record<string, never>;
5
5
  /**
6
6
  * _Experimental_
7
7
  *
@@ -129,3 +129,4 @@ export declare const AuthBoundary: import("vue").DefineComponent<import("vue").E
129
129
  }>> & Readonly<{}>, {
130
130
  renderFallback: () => VNode | null;
131
131
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
132
+ export {};
@@ -172,7 +172,7 @@ export declare const authClient: import("better-auth/vue").VueAuthClient<{
172
172
  metadata: {
173
173
  isAction: false;
174
174
  };
175
- }, import("better-auth/plugins").OIDCMetadata>;
175
+ }, import("better-auth/plugins/oidc-provider").OIDCMetadata>;
176
176
  getJwks: import("better-auth").StrictEndpoint<"/convex/jwks", {
177
177
  method: "GET";
178
178
  metadata: {
@@ -11,11 +11,10 @@ export interface ConsumeCrossDomainOneTimeTokenOptions {
11
11
  *
12
12
  * The cross-domain protocol cannot bind the token to the browser that
13
13
  * started the sign-in flow (magic-link flows legitimately finish in another
14
- * browsing context), so without this restriction a crafted
15
- * `/any-page?ott=…` link silently signs the visitor into the link author's
16
- * session — login CSRF. Restricting consumption to the one route your
17
- * sign-in `callbackURL`s point at shrinks that surface to a single
18
- * predictable URL.
14
+ * browsing context), so without this restriction a token completes sign-in
15
+ * on whatever page receives it. Restricting consumption to the one route
16
+ * your sign-in `callbackURL`s point at keeps that to a single predictable
17
+ * URL.
19
18
  *
20
19
  * In Nuxt, set `convex.betterAuth.crossDomainCallbackRoute` instead — the
21
20
  * client plugin forwards it here.
@@ -1,7 +1,9 @@
1
1
  import { authClient } from "#convex/auth-client";
2
2
  const normalizePathname = (path) => {
3
3
  const slashed = path.startsWith("/") ? path : `/${path}`;
4
- const trimmed = slashed.replace(/\/+$/, "");
4
+ let end = slashed.length;
5
+ while (end > 0 && slashed[end - 1] === "/") end--;
6
+ const trimmed = slashed.slice(0, end);
5
7
  return trimmed === "" ? "/" : trimmed;
6
8
  };
7
9
  export async function consumeCrossDomainOneTimeToken(options = {}) {
@@ -36,7 +38,7 @@ export async function consumeCrossDomainOneTimeToken(options = {}) {
36
38
  }
37
39
  }
38
40
  });
39
- authClientWithCrossDomain.updateSession();
41
+ await authClientWithCrossDomain.updateSession();
40
42
  }
41
43
  } catch (error) {
42
44
  console.warn("[nuxt-convex-module] failed to consume cross-domain one-time token", error);
@@ -8,5 +8,6 @@ export { useAuth, type UseAuthService, type AuthSession, type AuthUser } from '.
8
8
  export { authClient, type AuthClient } from '#convex/auth-client';
9
9
  export { usePreloadedAuthQuery } from './hydration.js';
10
10
  export { consumeCrossDomainOneTimeToken, type ConsumeCrossDomainOneTimeTokenOptions } from './cross-domain.js';
11
+ export { resolveAuthRedirect } from './redirect.js';
11
12
  export { AuthBoundary } from './auth-boundary.js';
12
13
  export { convexClient, crossDomainClient } from '@convex-dev/better-auth/client/plugins';
@@ -2,5 +2,6 @@ export { useAuth } from "./use-auth.js";
2
2
  export { authClient } from "#convex/auth-client";
3
3
  export { usePreloadedAuthQuery } from "./hydration.js";
4
4
  export { consumeCrossDomainOneTimeToken } from "./cross-domain.js";
5
+ export { resolveAuthRedirect } from "./redirect.js";
5
6
  export { AuthBoundary } from "./auth-boundary.js";
6
7
  export { convexClient, crossDomainClient } from "@convex-dev/better-auth/client/plugins";
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Safe handling of the `?redirect=` destination the `auth` route middleware
3
+ * attaches when it sends an unauthenticated visitor to the login page.
4
+ *
5
+ * The middleware only ever writes a router path there, but the query is part
6
+ * of a URL anyone can craft: a link to `/login?redirect=https://evil.example`
7
+ * turns a login page that navigates to `route.query.redirect` verbatim into an
8
+ * open redirect — a phishing primitive that borrows the app's own domain, and
9
+ * one that carries whatever the app appends to the destination.
10
+ *
11
+ * @module better-auth/redirect
12
+ */
13
+ /**
14
+ * Validate a post-sign-in redirect destination, returning `fallback` unless it
15
+ * is a same-origin path.
16
+ *
17
+ * Use it wherever a login page consumes the `?redirect=` query the `auth`
18
+ * middleware sets — never navigate to that value directly.
19
+ *
20
+ * @example
21
+ * ```vue
22
+ * <script setup lang="ts">
23
+ * const route = useRoute()
24
+ * const { signIn } = useAuth()
25
+ *
26
+ * async function onSubmit() {
27
+ * await signIn.email({ email, password })
28
+ * // '/dashboard' survives; 'https://evil.example' becomes '/'.
29
+ * await navigateTo(resolveAuthRedirect(route.query.redirect))
30
+ * }
31
+ * </script>
32
+ * ```
33
+ *
34
+ * @param value - Candidate destination, typically `route.query.redirect`. A
35
+ * repeated query parameter arrives as an array and is rejected: which entry
36
+ * the app meant is ambiguous, and an attacker chooses the other one.
37
+ * @param fallback - Where to send the visitor when `value` is not a safe
38
+ * same-origin path. Defaults to `/`, and is returned as given — pass a path
39
+ * you control, not user input.
40
+ *
41
+ * @returns The normalized `pathname + search + hash`, or `fallback`.
42
+ *
43
+ * @public
44
+ */
45
+ export declare function resolveAuthRedirect(value: unknown, fallback?: string): string;
@@ -0,0 +1,14 @@
1
+ const SENTINEL_ORIGIN = "http://redirect.invalid";
2
+ export function resolveAuthRedirect(value, fallback = "/") {
3
+ if (typeof value !== "string" || value === "") return fallback;
4
+ if (!value.startsWith("/")) return fallback;
5
+ try {
6
+ const url = new URL(value, SENTINEL_ORIGIN);
7
+ if (url.origin !== SENTINEL_ORIGIN) return fallback;
8
+ const path = `${url.pathname}${url.search}${url.hash}`;
9
+ if (new URL(path, SENTINEL_ORIGIN).origin !== SENTINEL_ORIGIN) return fallback;
10
+ return path;
11
+ } catch {
12
+ return fallback;
13
+ }
14
+ }
@@ -55,6 +55,7 @@ export function useAuth(initialToken) {
55
55
  };
56
56
  return {
57
57
  isLoading: computed(() => session.value.isPending && !cachedToken.value),
58
+ // PARITY: D-03
58
59
  // Diverges from upstream's `Boolean(session?.session) || cachedToken !== null`:
59
60
  // a settled signed-out session must read unauthenticated immediately (Vue has
60
61
  // no re-render to run upstream's cache-clearing effect first), and Better Auth
@@ -63,7 +64,7 @@ export function useAuth(initialToken) {
63
64
  () => !!session.value.data || session.value.isPending && cachedToken.value !== null
64
65
  ),
65
66
  fetchAccessToken,
66
- // Vue-only service extensions (documented in PARITY.md).
67
+ // PARITY: A-06 — Vue-only service extensions.
67
68
  client,
68
69
  session,
69
70
  user: computed(() => {
@@ -17,7 +17,7 @@ function toPlainJson(value) {
17
17
  try {
18
18
  return convexToJson(value);
19
19
  } catch {
20
- return String(value);
20
+ return typeof value === "object" && value !== null ? `[unserializable ${Array.isArray(value) ? "array" : "object"}]` : String(value);
21
21
  }
22
22
  }
23
23
  function renderLogArg(arg) {
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Pure helpers behind the Convex-aware Content Security Policy the module
3
+ * applies when `nuxt-security` is present — shared by the Nitro plugin
4
+ * (`./security`) and the unit tests. Import-free so it runs anywhere.
5
+ */
6
+ /**
7
+ * The slice of a nuxt-security rule set this module touches: the resolved
8
+ * options for one route (`rules['/**']` at runtime), where `headers` and
9
+ * `contentSecurityPolicy` may each be `false` when the app switched them off.
10
+ */
11
+ export interface SecurityRules {
12
+ headers?: false | {
13
+ contentSecurityPolicy?: false | Record<string, unknown>;
14
+ [header: string]: unknown;
15
+ };
16
+ [option: string]: unknown;
17
+ }
18
+ /**
19
+ * CSP `connect-src` entries the browser needs to reach a Convex deployment: the
20
+ * deployment URL over both HTTPS and WebSocket (the realtime sync channel) and,
21
+ * when configured, the `.site` URL that serves Convex HTTP actions. Returns
22
+ * `[]` for empty or unparseable input.
23
+ */
24
+ export declare function convexConnectSrc(url?: string, siteUrl?: string): string[];
25
+ /**
26
+ * CSP sources for Convex-served resources (`img-src` / `media-src`): files
27
+ * uploaded through `useStorageUrl()` are served from the deployment origin,
28
+ * and files streamed by HTTP actions from the `.site` origin.
29
+ */
30
+ export declare function convexResourceSrc(url?: string, siteUrl?: string): string[];
31
+ /**
32
+ * Apply nuxt-convex-module's secure-by-default, Convex-aware CSP onto a
33
+ * nuxt-security rule set (mutated in place). This tightens the directives we
34
+ * can safely pre-fill — locking network egress and Convex-served media to
35
+ * same-origin plus the Convex deployment — while leaving every other directive
36
+ * (script/style/font, etc.) to nuxt-security's own defaults.
37
+ *
38
+ * For each directive we keep any value the user already set and *append* the
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
41
+ * `contentSecurityPolicy: false` (CSP disabled) is left untouched, as is a rule
42
+ * set with nothing to add (no Convex URL).
43
+ */
44
+ export declare function applyConvexSecurityDefaults(rules: SecurityRules, connectSrc: string[], resourceSrc: string[]): void;
@@ -0,0 +1,46 @@
1
+ function toHttpOrigin(raw) {
2
+ if (!raw) return void 0;
3
+ try {
4
+ return new URL(raw).origin;
5
+ } catch {
6
+ return void 0;
7
+ }
8
+ }
9
+ function toWsOrigin(raw) {
10
+ if (!raw) return void 0;
11
+ try {
12
+ const u = new URL(raw);
13
+ return `${u.protocol === "https:" ? "wss:" : "ws:"}//${u.host}`;
14
+ } catch {
15
+ return void 0;
16
+ }
17
+ }
18
+ function uniq(values) {
19
+ return Array.from(new Set(values.filter((v) => Boolean(v))));
20
+ }
21
+ export function convexConnectSrc(url, siteUrl) {
22
+ return uniq([toHttpOrigin(url), toWsOrigin(url), toHttpOrigin(siteUrl), toWsOrigin(siteUrl)]);
23
+ }
24
+ export function convexResourceSrc(url, siteUrl) {
25
+ return uniq([toHttpOrigin(url), toHttpOrigin(siteUrl)]);
26
+ }
27
+ export function applyConvexSecurityDefaults(rules, connectSrc, resourceSrc) {
28
+ if (connectSrc.length === 0 && resourceSrc.length === 0) return;
29
+ if (rules.headers === false) return;
30
+ if (typeof rules.headers !== "object" || rules.headers === null) rules.headers = {};
31
+ const headers = rules.headers;
32
+ if (headers.contentSecurityPolicy === false) return;
33
+ if (typeof headers.contentSecurityPolicy !== "object" || headers.contentSecurityPolicy === null) {
34
+ headers.contentSecurityPolicy = {};
35
+ }
36
+ const csp = headers.contentSecurityPolicy;
37
+ tightenDirective(csp, "connect-src", ["'self'"], connectSrc);
38
+ tightenDirective(csp, "img-src", ["'self'", "data:"], resourceSrc);
39
+ tightenDirective(csp, "media-src", ["'self'"], resourceSrc);
40
+ }
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;
45
+ csp[name] = uniq([...existing, ...additions]);
46
+ }
@@ -8,9 +8,12 @@
8
8
  *
9
9
  * ## Usage
10
10
  *
11
- * All exported functions assume that a Convex deployment URL is set in the
12
- * `NUXT_PUBLIC_CONVEX_URL` environment variable. `npx convex dev` will
13
- * automatically set it during local development.
11
+ * All exported functions assume that a Convex deployment URL is configured —
12
+ * `convex.url` in `nuxt.config`, `NUXT_PUBLIC_CONVEX_URL`, or the `CONVEX_URL`
13
+ * that `npx convex dev` writes to `.env.local`. Unlike its Next counterpart,
14
+ * `npx convex dev` does *not* set the framework-prefixed name for Nuxt: the
15
+ * Convex CLI has no Nuxt case in its framework detection, so it writes the
16
+ * unprefixed `CONVEX_URL`, which the module reads too.
14
17
  *
15
18
  * ### Preloading data
16
19
  *
@@ -0,0 +1,33 @@
1
+ import { type SecurityRules } from './csp.js';
2
+ /**
3
+ * The slice of the Nitro app this plugin touches, typed locally: the hook's
4
+ * real declaration lives in the types nuxt-security generates into the app,
5
+ * which only exist when the app has it installed — and this plugin is only
6
+ * registered then. Nitro itself is not imported, so the published types don't
7
+ * depend on how the app resolves `nitropack`.
8
+ */
9
+ export interface SecurityNitroApp {
10
+ hooks: {
11
+ hook: (name: 'nuxt-security:routeRules', fn: (rules: Record<string, SecurityRules>) => void) => unknown;
12
+ };
13
+ }
14
+ /**
15
+ * Nitro plugin: extend nuxt-security's Content Security Policy with the Convex
16
+ * origins. Registered by the module only when the nuxt-security integration is
17
+ * on (see `registerSecurity` in `module.ts`).
18
+ *
19
+ * Listens for `nuxt-security:routeRules`, which nuxt-security fires once its
20
+ * global config and every route rule are merged into one table — so the
21
+ * origins are appended to whatever the app configured, whatever the order of
22
+ * `modules`. nuxt-security sorts its own Nitro plugins last, so this listener
23
+ * is in place before the hook fires. The deployment URL comes from runtime
24
+ * config, so it need not be known at build time.
25
+ *
26
+ * `connect-src` is tightened only in production: creating it in dev would
27
+ * block the Vite HMR and devtools WebSockets, which run on ports `'self'`
28
+ * doesn't cover, while nuxt-security's defaults leave the directive open.
29
+ * `img-src` / `media-src` are widened in dev too — nuxt-security enforces its
30
+ * default `img-src 'self' data:` during `nuxt dev` as well, which would block
31
+ * files served from Convex storage.
32
+ */
33
+ export default function convexSecurityPlugin(nitroApp: SecurityNitroApp): void;