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.
- package/README.md +78 -24
- package/dist/devtools-client/200.html +1 -1
- package/dist/devtools-client/404.html +1 -1
- package/dist/devtools-client/_nuxt/{CZbAGcNC.js → BAaUCl_m.js} +1 -1
- package/dist/devtools-client/_nuxt/BmBQNDID.js +1 -0
- package/dist/devtools-client/_nuxt/C2KGYsR4.js +1 -0
- package/dist/devtools-client/_nuxt/{DPWXUmcI.js → CKakiU_h.js} +1 -1
- package/dist/devtools-client/_nuxt/CgIEvOQU.js +2 -0
- package/dist/devtools-client/_nuxt/Cm7YqZQ-.js +1 -0
- package/dist/devtools-client/_nuxt/CtxksWnu.js +1 -0
- package/dist/devtools-client/_nuxt/D2ksFYuU.js +2 -0
- package/dist/devtools-client/_nuxt/{dy_xXcv9.js → DQmVr-EY.js} +1 -1
- package/dist/devtools-client/_nuxt/D_Rij7MD.js +1 -0
- package/dist/devtools-client/_nuxt/EZ4-8Xzm.js +3 -0
- package/dist/devtools-client/_nuxt/Kba0XpjH.js +1 -0
- package/dist/devtools-client/_nuxt/OVE7s6ty.js +1 -0
- package/dist/devtools-client/_nuxt/builds/latest.json +1 -1
- package/dist/devtools-client/_nuxt/builds/meta/23303226-2230-4d14-87ce-87b0f562de6c.json +1 -0
- package/dist/devtools-client/_nuxt/entry.HF8C6P11.css +1 -0
- package/dist/devtools-client/_nuxt/error-404.BpRUKU69.css +1 -0
- package/dist/devtools-client/_nuxt/error-500.DuDYJ5TI.css +1 -0
- package/dist/devtools-client/_nuxt/queries.BmISPDaV.css +1 -0
- package/dist/devtools-client/_nuxt/vZZlEX5A.js +1 -0
- package/dist/devtools-client/auth/index.html +1 -1
- package/dist/devtools-client/index.html +1 -1
- package/dist/devtools-client/logs/index.html +1 -1
- package/dist/devtools-client/queries/index.html +1 -1
- package/dist/{module.d.ts → module.d.mts} +51 -35
- package/dist/module.json +1 -1
- package/dist/{module.js → module.mjs} +63 -84
- package/dist/runtime/better-auth/nuxt/middleware.d.ts +9 -1
- package/dist/runtime/better-auth/nuxt/middleware.js +11 -7
- package/dist/runtime/better-auth/nuxt/server.js +15 -0
- package/dist/runtime/better-auth/vue/auth-boundary.d.ts +2 -1
- package/dist/runtime/better-auth/vue/client.d.ts +1 -1
- package/dist/runtime/better-auth/vue/cross-domain.d.ts +4 -5
- package/dist/runtime/better-auth/vue/cross-domain.js +4 -2
- 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/redirect.d.ts +45 -0
- package/dist/runtime/better-auth/vue/redirect.js +14 -0
- package/dist/runtime/better-auth/vue/use-auth.js +2 -1
- package/dist/runtime/devtools/bridge.js +1 -1
- package/dist/runtime/nuxt/csp.d.ts +44 -0
- package/dist/runtime/nuxt/csp.js +46 -0
- package/dist/runtime/nuxt/index.d.ts +6 -3
- package/dist/runtime/nuxt/security.d.ts +33 -0
- package/dist/runtime/nuxt/security.js +14 -0
- package/dist/runtime/polar/vue/components.js +1 -1
- package/dist/runtime/vue/auth/index.js +6 -2
- package/dist/runtime/vue/client.d.ts +2 -0
- package/dist/runtime/vue/client.js +2 -0
- package/dist/types.d.mts +3 -0
- package/package.json +58 -47
- package/dist/devtools-client/_nuxt/B29GGJ5n.js +0 -1
- package/dist/devtools-client/_nuxt/BSDEpQet.js +0 -1
- package/dist/devtools-client/_nuxt/BY23OQOo.js +0 -1
- package/dist/devtools-client/_nuxt/BZYRaL-G.js +0 -1
- package/dist/devtools-client/_nuxt/Byw6FxnC.js +0 -1
- package/dist/devtools-client/_nuxt/CJJ_hcZN.js +0 -1
- package/dist/devtools-client/_nuxt/CS6WMFOx.js +0 -2
- package/dist/devtools-client/_nuxt/Cr6_aklh.js +0 -2
- package/dist/devtools-client/_nuxt/DNfpyBVU.js +0 -3
- package/dist/devtools-client/_nuxt/builds/meta/a7708498-bd09-4de2-b5ab-19b380293c77.json +0 -1
- package/dist/devtools-client/_nuxt/entry.DbTJhkHT.css +0 -1
- package/dist/devtools-client/_nuxt/error-404.CwGGO2Kz.css +0 -1
- package/dist/devtools-client/_nuxt/error-500.8ZJqCQm8.css +0 -1
- package/dist/devtools-client/_nuxt/ncgEdbEt.js +0 -1
- package/dist/devtools-client/_nuxt/queries.CE_dtt9-.css +0 -1
- 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
|
|
53
|
-
// module dependency
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
210
|
+
function readSecurityOption(nuxt) {
|
|
203
211
|
const opts = nuxt.options;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
215
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
250
|
+
url,
|
|
238
251
|
siteUrl,
|
|
239
|
-
crossDomainCallbackRoute
|
|
252
|
+
crossDomainCallbackRoute,
|
|
253
|
+
loginPath
|
|
240
254
|
};
|
|
241
255
|
runtimeConfig.convex = { ...runtimeConfig.convex, siteUrl };
|
|
242
|
-
return { url
|
|
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}/**`, {
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
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 !==
|
|
11
|
-
return nuxtApp.runWithContext(() => navigateTo(
|
|
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
|
|
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 !==
|
|
37
|
-
return navigateTo(
|
|
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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
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;
|