use-convex 0.0.2 → 1.1.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 (52) hide show
  1. package/README.md +115 -35
  2. package/dist/module.d.mts +5 -3
  3. package/dist/module.mjs +35 -1
  4. package/dist/runtime/composables/useAuth.d.ts +5 -5
  5. package/dist/runtime/composables/useAuth.js +11 -24
  6. package/dist/runtime/composables/useAuthToken.d.ts +1 -1
  7. package/dist/runtime/composables/useAuthToken.js +2 -10
  8. package/dist/runtime/composables/useConvex.js +1 -1
  9. package/dist/runtime/composables/useConvexAction.js +6 -18
  10. package/dist/runtime/composables/useConvexAuth.js +4 -13
  11. package/dist/runtime/composables/useConvexFileUpload.d.ts +43 -0
  12. package/dist/runtime/composables/useConvexFileUpload.js +102 -0
  13. package/dist/runtime/composables/useConvexGate.js +3 -9
  14. package/dist/runtime/composables/useConvexMutation.js +7 -17
  15. package/dist/runtime/composables/useConvexPaginatedQuery.d.ts +1 -1
  16. package/dist/runtime/composables/useConvexPaginatedQuery.js +7 -29
  17. package/dist/runtime/composables/useConvexQueries.d.ts +12 -7
  18. package/dist/runtime/composables/useConvexQueries.js +1 -3
  19. package/dist/runtime/composables/useConvexQuery.js +6 -12
  20. package/dist/runtime/composables/useConvexR2Upload.d.ts +40 -0
  21. package/dist/runtime/composables/useConvexR2Upload.js +86 -0
  22. package/dist/runtime/plugin.client.js +3 -7
  23. package/dist/runtime/plugin.devtools.client.d.ts +15 -2
  24. package/dist/runtime/plugin.devtools.client.js +18 -4
  25. package/dist/runtime/plugin.server.js +3 -7
  26. package/dist/runtime/server/api/convex/auth/session.js +7 -12
  27. package/dist/runtime/server/auth.js +1 -9
  28. package/dist/runtime/server/authCookies.js +8 -30
  29. package/dist/runtime/server/convexConfig.d.ts +12 -0
  30. package/dist/runtime/server/convexConfig.js +9 -0
  31. package/dist/runtime/server/fetch.d.ts +2 -4
  32. package/dist/runtime/server/fetch.js +3 -13
  33. package/dist/runtime/server/routes/__convex_devtools.get.d.ts +6 -2
  34. package/dist/runtime/server/routes/__convex_devtools.get.js +184 -16
  35. package/dist/runtime/server/sameOrigin.js +1 -5
  36. package/dist/runtime/utils/authCookie.d.ts +0 -2
  37. package/dist/runtime/utils/authCookie.js +0 -1
  38. package/dist/runtime/utils/authState.js +1 -6
  39. package/dist/runtime/utils/authStorage.js +1 -1
  40. package/dist/runtime/utils/context.js +3 -2
  41. package/dist/runtime/utils/convexDashboard.d.ts +44 -0
  42. package/dist/runtime/utils/convexDashboard.js +77 -0
  43. package/dist/runtime/utils/errors.d.ts +14 -0
  44. package/dist/runtime/utils/errors.js +25 -0
  45. package/dist/runtime/utils/oauthRedirect.js +3 -2
  46. package/dist/runtime/utils/paginatedOptimistic.js +4 -22
  47. package/dist/runtime/utils/payloadCache.d.ts +6 -0
  48. package/dist/runtime/utils/payloadCache.js +7 -0
  49. package/dist/runtime/utils/pendingError.d.ts +9 -0
  50. package/dist/runtime/utils/pendingError.js +23 -0
  51. package/dist/types.d.mts +7 -3
  52. package/package.json +36 -20
@@ -1,10 +1,4 @@
1
- import {
2
- deleteCookie,
3
- getCookie,
4
- getRequestHeader,
5
- setCookie
6
- } from "h3";
7
- import { useRuntimeConfig } from "nitropack/runtime";
1
+ import { deleteCookie, getCookie, getRequestHeader, setCookie } from "h3";
8
2
  import {
9
3
  AUTH_JWT_COOKIE_MAX_AGE,
10
4
  isHttpOnlyAuth,
@@ -12,32 +6,18 @@ import {
12
6
  resolveAuthPresentCookieName,
13
7
  resolveAuthRefreshCookieName
14
8
  } from "../utils/authStorage.js";
9
+ import { readConvexConfig } from "./convexConfig.js";
15
10
  function readAuthConfig(event) {
16
- try {
17
- const config = useRuntimeConfig(event);
18
- return config.public?.convex?.auth;
19
- } catch {
20
- return void 0;
21
- }
11
+ return readConvexConfig(event)?.auth;
22
12
  }
23
13
  function isLocalHost(event) {
24
14
  const host = getRequestHeader(event, "host") ?? "";
25
15
  return host.startsWith("localhost") || host.startsWith("127.0.0.1") || host.startsWith("[::1]");
26
16
  }
27
- function httpOnlyCookieOptions(event) {
28
- const local = isLocalHost(event);
29
- return {
30
- httpOnly: true,
31
- secure: !local,
32
- sameSite: "lax",
33
- path: "/",
34
- maxAge: AUTH_JWT_COOKIE_MAX_AGE
35
- };
36
- }
37
- function readableCookieOptions(event) {
17
+ function cookieOptions(event, httpOnly) {
38
18
  const local = isLocalHost(event);
39
19
  return {
40
- httpOnly: false,
20
+ httpOnly,
41
21
  secure: !local,
42
22
  sameSite: "lax",
43
23
  path: "/",
@@ -47,15 +27,13 @@ function readableCookieOptions(event) {
47
27
  export function setAuthCookies(event, tokens) {
48
28
  const auth = readAuthConfig(event);
49
29
  if (!isHttpOnlyAuth(auth)) {
50
- throw new Error(
51
- "[convex-nuxt] setAuthCookies requires convex.auth.httpOnly: true"
52
- );
30
+ throw new Error("[use-convex] setAuthCookies requires convex.auth.httpOnly: true");
53
31
  }
54
32
  const jwtName = resolveAuthCookieName(auth);
55
33
  const refreshName = resolveAuthRefreshCookieName(auth);
56
34
  const presentName = resolveAuthPresentCookieName(auth);
57
- const httpOpts = httpOnlyCookieOptions(event);
58
- const presentOpts = readableCookieOptions(event);
35
+ const httpOpts = cookieOptions(event, true);
36
+ const presentOpts = cookieOptions(event, false);
59
37
  if (!tokens.token) {
60
38
  deleteCookie(event, jwtName, httpOpts);
61
39
  deleteCookie(event, refreshName, httpOpts);
@@ -0,0 +1,12 @@
1
+ import type { H3Event } from 'h3';
2
+ export interface ConvexPublicConfig {
3
+ url?: string;
4
+ auth?: {
5
+ provider?: string;
6
+ cookie?: string;
7
+ httpOnly?: boolean;
8
+ presentCookie?: string;
9
+ };
10
+ }
11
+ /** Read `runtimeConfig.public.convex`, or `undefined` outside a Nitro request. */
12
+ export declare function readConvexConfig(event?: H3Event): ConvexPublicConfig | undefined;
@@ -0,0 +1,9 @@
1
+ import { useRuntimeConfig } from "nitropack/runtime";
2
+ export function readConvexConfig(event) {
3
+ try {
4
+ const config = useRuntimeConfig(event);
5
+ return config.public?.convex;
6
+ } catch {
7
+ return void 0;
8
+ }
9
+ }
@@ -1,7 +1,5 @@
1
1
  import type { FunctionArgs, FunctionReference, FunctionReturnType } from 'convex/server';
2
2
  import type { H3Event } from 'h3';
3
- import { resolveFetchToken } from '../utils/fetchToken.js';
4
- export { resolveFetchToken };
5
3
  export interface ConvexFetchOptions {
6
4
  /**
7
5
  * Deployment URL. Defaults to `NUXT_PUBLIC_CONVEX_URL` / runtimeConfig.
@@ -18,9 +16,9 @@ export interface ConvexFetchOptions {
18
16
  event?: H3Event;
19
17
  skipConvexDeploymentUrlCheck?: boolean;
20
18
  }
21
- /** @internal Exported for unit tests. */
19
+ /** @internal */
22
20
  export declare function resolveUrl(options: ConvexFetchOptions): string;
23
- /** @internal Exported for unit tests. */
21
+ /** @internal */
24
22
  export declare function resolveToken(options: ConvexFetchOptions): string | undefined;
25
23
  /**
26
24
  * One-shot query via ConvexHttpClient. Use in Nitro routes / server middleware.
@@ -1,17 +1,9 @@
1
1
  import { getCookie } from "h3";
2
- import { useRuntimeConfig } from "nitropack/runtime";
3
2
  import { createHttpClient } from "../utils/http.js";
4
3
  import { resolveAuthCookieName } from "../utils/authStorage.js";
5
4
  import { resolveFetchToken } from "../utils/fetchToken.js";
6
- export { resolveFetchToken };
7
- function readConvexConfig(event) {
8
- try {
9
- const config = useRuntimeConfig(event);
10
- return config.public?.convex;
11
- } catch {
12
- return void 0;
13
- }
14
- }
5
+ import { missingConvexUrlError } from "../utils/errors.js";
6
+ import { readConvexConfig } from "./convexConfig.js";
15
7
  export function resolveUrl(options) {
16
8
  if (options.url) {
17
9
  return options.url;
@@ -24,9 +16,7 @@ export function resolveUrl(options) {
24
16
  if (fromEnv) {
25
17
  return fromEnv;
26
18
  }
27
- throw new Error(
28
- "[convex-nuxt] No Convex URL. Pass { url } or set NUXT_PUBLIC_CONVEX_URL."
29
- );
19
+ throw missingConvexUrlError("fetchQuery/Mutation/Action");
30
20
  }
31
21
  export function resolveToken(options) {
32
22
  if (options.token !== void 0) {
@@ -1,6 +1,10 @@
1
1
  /**
2
- * Minimal DevTools iframe — shows public Convex runtime config.
3
- * Connection state is available in-app via `useConvexConnectionState()`.
2
+ * DevTools iframe — embeds Convex dashboard (cloud) + public config tips.
3
+ * Live auth/connection: `window.__CONVEX_NUXT__` (devtools client plugin).
4
+ *
5
+ * Auto-login uses CONVEX_DEPLOY_KEY via postMessage to dashboard-embedded.
6
+ * Without a key, the embed shows Convex's credential form; Open dashboard
7
+ * still deep-links to dashboard.convex.dev (existing session).
4
8
  */
5
9
  declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, string>;
6
10
  export default _default;
@@ -1,32 +1,200 @@
1
1
  import { defineEventHandler } from "h3";
2
2
  import { useRuntimeConfig } from "nitropack/runtime";
3
+ import { buildDevtoolsDashboardPayload } from "../../utils/convexDashboard.js";
3
4
  export default defineEventHandler((event) => {
4
5
  const config = useRuntimeConfig(event);
5
6
  const convex = config.public?.convex ?? {};
6
- const body = JSON.stringify(
7
- {
8
- url: convex.url ?? null,
9
- server: convex.server ?? true,
10
- auth: convex.auth ?? null,
11
- tip: "useConvexConnectionState() for live WebSocket status in the app"
12
- },
13
- null,
14
- 2
15
- );
7
+ const devtools = config.convexDevtools;
8
+ const url = typeof convex.url === "string" && convex.url.trim() || typeof devtools?.url === "string" && devtools.url.trim() || process.env.NUXT_PUBLIC_CONVEX_URL?.trim() || process.env.CONVEX_URL?.trim() || null;
9
+ const hasUrl = Boolean(url);
10
+ const summary = {
11
+ url,
12
+ server: convex.server ?? true,
13
+ auth: convex.auth ? {
14
+ provider: convex.auth.provider ?? null,
15
+ cookie: convex.auth.cookie ?? null,
16
+ httpOnly: convex.auth.httpOnly ?? false,
17
+ presentCookie: convex.auth.presentCookie ?? null
18
+ } : null
19
+ };
20
+ const tips = [
21
+ hasUrl ? "Deployment URL is set." : "No URL \u2014 set convex.url or NUXT_PUBLIC_CONVEX_URL.",
22
+ "Set CONVEX_DEPLOY_KEY for dashboard auto-login in this tab (dev only).",
23
+ "In the app console: window.__CONVEX_NUXT__.connection (WebSocket).",
24
+ "In the app console: window.__CONVEX_NUXT__.auth (gate flags).",
25
+ "Gate private live queries with { authenticated: true }.",
26
+ "SSR off globally: convex.server: false \u2014 or per call { server: false }."
27
+ ];
28
+ const dashboard = buildDevtoolsDashboardPayload({
29
+ url,
30
+ convexDeployment: devtools?.deployment || process.env.CONVEX_DEPLOYMENT,
31
+ deployKey: devtools?.deployKey || process.env.CONVEX_DEPLOY_KEY
32
+ });
33
+ const configJson = JSON.stringify({ config: summary, tips }).replace(/</g, "\\u003c");
34
+ const dashboardJson = JSON.stringify({
35
+ deploymentUrl: dashboard.deploymentUrl,
36
+ deploymentName: dashboard.deploymentName,
37
+ adminKey: dashboard.adminKey,
38
+ embed: dashboard.embed,
39
+ embedSrc: dashboard.embedSrc,
40
+ embedOrigin: dashboard.embedOrigin
41
+ }).replace(/</g, "\\u003c");
42
+ const openHref = dashboard.openDashboardUrl ? escapeHtml(dashboard.openDashboardUrl) : null;
43
+ const urlLabel = dashboard.deploymentUrl ? escapeHtml(dashboard.deploymentUrl) : "No deployment URL";
44
+ const statusLabel = !dashboard.embed ? "Hosted embed needs a *.convex.cloud URL" : dashboard.adminKey ? "Auto-login via CONVEX_DEPLOY_KEY" : "Paste credentials in the embed, or Open dashboard";
45
+ const embedBlock = dashboard.embed ? `<div id="embed-wrap" class="embed${dashboard.adminKey ? " pending" : ""}">
46
+ <iframe
47
+ id="convex-dashboard"
48
+ title="Convex dashboard"
49
+ src="${escapeHtml(dashboard.embedSrc)}"
50
+ allow="clipboard-write"
51
+ ></iframe>
52
+ </div>` : `<div class="notice">
53
+ <p>The hosted Convex dashboard embed only works with Convex Cloud URLs (<code>*.convex.cloud</code>).</p>
54
+ <p>Local / self-hosted deployments: use the CLI dashboard or Open dashboard when a name is known.</p>
55
+ </div>`;
16
56
  return `<!doctype html>
17
57
  <html lang="en">
18
58
  <head>
19
59
  <meta charset="utf-8" />
20
- <title>Convex \xB7 Nuxt DevTools</title>
60
+ <title>use-convex \xB7 DevTools</title>
21
61
  <style>
22
- body { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; margin: 1rem; color: #e5e7eb; background: #0b1220; }
23
- h1 { font-size: 0.95rem; font-weight: 600; margin: 0 0 0.75rem; color: #93c5fd; }
24
- pre { margin: 0; white-space: pre-wrap; word-break: break-word; font-size: 0.8rem; line-height: 1.45; }
62
+ * { box-sizing: border-box; }
63
+ html, body {
64
+ height: 100%;
65
+ margin: 0;
66
+ }
67
+ body {
68
+ display: flex;
69
+ flex-direction: column;
70
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
71
+ color: #e5e7eb;
72
+ background: #0b1220;
73
+ }
74
+ header {
75
+ flex: 0 0 auto;
76
+ padding: 0.65rem 0.85rem;
77
+ border-bottom: 1px solid #1f2937;
78
+ background: #0f172a;
79
+ }
80
+ .row {
81
+ display: flex;
82
+ flex-wrap: wrap;
83
+ align-items: baseline;
84
+ gap: 0.5rem 0.85rem;
85
+ }
86
+ h1 {
87
+ font-size: 0.9rem;
88
+ font-weight: 600;
89
+ margin: 0;
90
+ color: #93c5fd;
91
+ }
92
+ .url {
93
+ font-size: 0.7rem;
94
+ color: #9ca3af;
95
+ word-break: break-all;
96
+ }
97
+ .status {
98
+ font-size: 0.7rem;
99
+ color: #a5b4fc;
100
+ }
101
+ a.open {
102
+ font-size: 0.75rem;
103
+ color: #67e8f9;
104
+ text-decoration: none;
105
+ }
106
+ a.open:hover { text-decoration: underline; }
107
+ details {
108
+ margin-top: 0.45rem;
109
+ }
110
+ summary {
111
+ cursor: pointer;
112
+ font-size: 0.7rem;
113
+ color: #9ca3af;
114
+ user-select: none;
115
+ }
116
+ pre {
117
+ margin: 0.4rem 0 0;
118
+ white-space: pre-wrap;
119
+ word-break: break-word;
120
+ font-size: 0.72rem;
121
+ line-height: 1.4;
122
+ max-height: 12rem;
123
+ overflow: auto;
124
+ }
125
+ .embed {
126
+ flex: 1 1 auto;
127
+ min-height: 0;
128
+ position: relative;
129
+ }
130
+ .embed.pending iframe { visibility: hidden; }
131
+ iframe {
132
+ display: block;
133
+ width: 100%;
134
+ height: 100%;
135
+ border: 0;
136
+ background: #111827;
137
+ }
138
+ .notice {
139
+ flex: 1 1 auto;
140
+ padding: 1rem 0.85rem;
141
+ font-size: 0.8rem;
142
+ color: #9ca3af;
143
+ line-height: 1.5;
144
+ }
145
+ .notice code { color: #93c5fd; }
25
146
  </style>
26
147
  </head>
27
148
  <body>
28
- <h1>use-convex</h1>
29
- <pre>${body.replace(/</g, "&lt;")}</pre>
149
+ <header>
150
+ <div class="row">
151
+ <h1>use-convex</h1>
152
+ <span class="url">${urlLabel}</span>
153
+ ${openHref ? `<a class="open" href="${openHref}" target="_blank" rel="noopener noreferrer">Open dashboard \u2197</a>` : ""}
154
+ </div>
155
+ <p class="status">${escapeHtml(statusLabel)}</p>
156
+ <details>
157
+ <summary>Module config &amp; tips</summary>
158
+ <pre id="config-pre"></pre>
159
+ </details>
160
+ </header>
161
+ ${embedBlock}
162
+ <script>
163
+ (function () {
164
+ var config = ${configJson};
165
+ var dash = ${dashboardJson};
166
+ var pre = document.getElementById('config-pre');
167
+ if (pre) pre.textContent = JSON.stringify(config, null, 2);
168
+
169
+ if (!dash.embed || !dash.adminKey) return;
170
+
171
+ var iframe = document.getElementById('convex-dashboard');
172
+ var wrap = document.getElementById('embed-wrap');
173
+ if (!iframe || !iframe.contentWindow) return;
174
+
175
+ function reveal() {
176
+ if (wrap) wrap.classList.remove('pending');
177
+ }
178
+
179
+ window.addEventListener('message', function (event) {
180
+ if (event.origin !== dash.embedOrigin) return;
181
+ if (!event.data || event.data.type !== 'dashboard-credentials-request') return;
182
+ iframe.contentWindow.postMessage(
183
+ {
184
+ type: 'dashboard-credentials',
185
+ adminKey: dash.adminKey,
186
+ deploymentUrl: dash.deploymentUrl,
187
+ deploymentName: dash.deploymentName,
188
+ },
189
+ dash.embedOrigin,
190
+ );
191
+ reveal();
192
+ });
193
+ })();
194
+ <\/script>
30
195
  </body>
31
196
  </html>`;
32
197
  });
198
+ function escapeHtml(value) {
199
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
200
+ }
@@ -1,8 +1,4 @@
1
- import {
2
- createError,
3
- getRequestHeader,
4
- getRequestProtocol
5
- } from "h3";
1
+ import { createError, getRequestHeader, getRequestProtocol } from "h3";
6
2
  export function assertSameOrigin(event) {
7
3
  const secFetchSite = getRequestHeader(event, "sec-fetch-site");
8
4
  if (secFetchSite === "same-origin" || secFetchSite === "none") {
@@ -1,6 +1,4 @@
1
1
  import { type ComputedRef } from 'vue';
2
- import { AUTH_JWT_COOKIE_MAX_AGE } from './authStorage.js';
3
- export { AUTH_JWT_COOKIE_MAX_AGE };
4
2
  /**
5
3
  * Shared JWT cookie for SSR snapshots. Same name + options as persistTokens
6
4
  * so plugin reads and sign-in/out writes share one Nuxt cookie ref.
@@ -7,7 +7,6 @@ import {
7
7
  resolveAuthCookieName,
8
8
  resolveAuthPresentCookieName
9
9
  } from "./authStorage.js";
10
- export { AUTH_JWT_COOKIE_MAX_AGE };
11
10
  function cookieOptions(httpOnly) {
12
11
  return {
13
12
  sameSite: "lax",
@@ -1,10 +1,5 @@
1
1
  export function resolveConvexAuthState(flags) {
2
- const {
3
- authProviderLoading,
4
- authProviderAuthenticated,
5
- isConvexAuthenticated,
6
- isRefreshing
7
- } = flags;
2
+ const { authProviderLoading, authProviderAuthenticated, isConvexAuthenticated, isRefreshing } = flags;
8
3
  if (authProviderLoading) {
9
4
  return {
10
5
  isLoading: true,
@@ -7,7 +7,7 @@ export const DEFAULT_AUTH_PRESENT_COOKIE = "convex_auth_present";
7
7
  export const HTTPONLY_JWT_COOKIE = "__convexAuthJWT";
8
8
  export const HTTPONLY_REFRESH_COOKIE = "__convexAuthRefreshToken";
9
9
  export function storageNamespace(url) {
10
- return url.replace(/[^a-zA-Z0-9]/g, "");
10
+ return url.replace(/[^a-z0-9]/gi, "");
11
11
  }
12
12
  export function storageKey(base, url) {
13
13
  return `${base}_${storageNamespace(url)}`;
@@ -1,5 +1,6 @@
1
1
  import { inject, ref } from "vue";
2
2
  import { useNuxtApp } from "nuxt/app";
3
+ import { missingConvexUrlError } from "./errors.js";
3
4
  export const convexNuxtKey = Symbol("convex-nuxt");
4
5
  export function createAuthContext() {
5
6
  return {
@@ -33,8 +34,8 @@ export function tryUseConvexContext() {
33
34
  export function useConvexContext() {
34
35
  const ctx = tryUseConvexContext();
35
36
  if (!ctx) {
36
- throw new Error(
37
- "[convex-nuxt] Convex plugin did not start \u2014 set convex.url or NUXT_PUBLIC_CONVEX_URL (the module no-ops when the URL is empty)."
37
+ throw missingConvexUrlError(
38
+ "Convex plugin did not start (the module no-ops when the URL is empty)"
38
39
  );
39
40
  }
40
41
  return ctx;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Helpers for linking / embedding the Convex dashboard from Nuxt DevTools.
3
+ * Pure — no Nitro / Nuxt imports so unit tests stay lightweight.
4
+ */
5
+ export declare const DASHBOARD_EMBED_ORIGIN = "https://dashboard-embedded.convex.dev";
6
+ export declare const DASHBOARD_EMBED_DATA_URL = "https://dashboard-embedded.convex.dev/data";
7
+ export declare const DASHBOARD_HOST = "https://dashboard.convex.dev";
8
+ /**
9
+ * Strip CLI prefixes like `dev:happy-animal-123` → `happy-animal-123`.
10
+ */
11
+ export declare function deploymentNameFromEnv(value: string | undefined | null): string | null;
12
+ /**
13
+ * Derive deployment name from a cloud URL host: `happy-animal-123.convex.cloud`.
14
+ */
15
+ export declare function deploymentNameFromUrl(url: string | undefined | null): string | null;
16
+ export declare function resolveDeploymentName(options: {
17
+ url?: string | null;
18
+ convexDeployment?: string | null;
19
+ }): string | null;
20
+ /** True when the URL looks like Convex Cloud (embeddable hosted dashboard). */
21
+ export declare function isConvexCloudUrl(url: string | undefined | null): boolean;
22
+ /** True for a loopback Convex backend (`npx convex dev --local`). */
23
+ export declare function isLocalConvexUrl(url: string | undefined | null): boolean;
24
+ /**
25
+ * When CONVEX_DEPLOYMENT is a cloud `dev:`/`prod:` name but the app URL is
26
+ * still loopback (CLI: "Can't safely modify .env.local"), return the cloud URL.
27
+ */
28
+ export declare function suggestedCloudUrlFromMismatch(url: string | undefined | null, convexDeployment: string | undefined | null): string | null;
29
+ export declare function dashboardDeepLink(deploymentName: string | null): string | null;
30
+ export interface DevtoolsDashboardPayload {
31
+ deploymentUrl: string | null;
32
+ deploymentName: string | null;
33
+ /** Present only when CONVEX_DEPLOY_KEY is set (dev server only). */
34
+ adminKey: string | null;
35
+ embed: boolean;
36
+ openDashboardUrl: string | null;
37
+ embedSrc: string;
38
+ embedOrigin: string;
39
+ }
40
+ export declare function buildDevtoolsDashboardPayload(options: {
41
+ url?: string | null;
42
+ convexDeployment?: string | null;
43
+ deployKey?: string | null;
44
+ }): DevtoolsDashboardPayload;
@@ -0,0 +1,77 @@
1
+ export const DASHBOARD_EMBED_ORIGIN = "https://dashboard-embedded.convex.dev";
2
+ export const DASHBOARD_EMBED_DATA_URL = `${DASHBOARD_EMBED_ORIGIN}/data`;
3
+ export const DASHBOARD_HOST = "https://dashboard.convex.dev";
4
+ export function deploymentNameFromEnv(value) {
5
+ if (!value) return null;
6
+ const trimmed = value.split("#")[0]?.trim() ?? "";
7
+ if (!trimmed) return null;
8
+ const colon = trimmed.indexOf(":");
9
+ if (colon >= 0 && colon < trimmed.length - 1) {
10
+ return trimmed.slice(colon + 1);
11
+ }
12
+ return trimmed;
13
+ }
14
+ export function deploymentNameFromUrl(url) {
15
+ if (!url) return null;
16
+ try {
17
+ const { hostname } = new URL(url);
18
+ if (hostname.endsWith(".convex.cloud")) {
19
+ const name = hostname.slice(0, -".convex.cloud".length);
20
+ return name || null;
21
+ }
22
+ return null;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+ export function resolveDeploymentName(options) {
28
+ return deploymentNameFromUrl(options.url) ?? deploymentNameFromEnv(options.convexDeployment) ?? null;
29
+ }
30
+ export function isConvexCloudUrl(url) {
31
+ if (!url) return false;
32
+ try {
33
+ const { hostname } = new URL(url);
34
+ return hostname.endsWith(".convex.cloud");
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+ export function isLocalConvexUrl(url) {
40
+ if (!url) return false;
41
+ try {
42
+ const { hostname } = new URL(url);
43
+ return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "[::1]";
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+ export function suggestedCloudUrlFromMismatch(url, convexDeployment) {
49
+ if (!isLocalConvexUrl(url)) return null;
50
+ const trimmed = convexDeployment?.split("#")[0]?.trim() ?? "";
51
+ if (!/^(dev|prod):/.test(trimmed)) return null;
52
+ const name = deploymentNameFromEnv(trimmed);
53
+ if (!name) return null;
54
+ return `https://${name}.convex.cloud`;
55
+ }
56
+ export function dashboardDeepLink(deploymentName) {
57
+ if (!deploymentName) return null;
58
+ return `${DASHBOARD_HOST}/d/${encodeURIComponent(deploymentName)}`;
59
+ }
60
+ export function buildDevtoolsDashboardPayload(options) {
61
+ const deploymentUrl = options.url?.trim() || null;
62
+ const deploymentName = resolveDeploymentName({
63
+ url: deploymentUrl,
64
+ convexDeployment: options.convexDeployment
65
+ });
66
+ const adminKey = options.deployKey?.trim() || null;
67
+ const embed = isConvexCloudUrl(deploymentUrl);
68
+ return {
69
+ deploymentUrl,
70
+ deploymentName,
71
+ adminKey: embed && adminKey ? adminKey : null,
72
+ embed,
73
+ openDashboardUrl: dashboardDeepLink(deploymentName),
74
+ embedSrc: DASHBOARD_EMBED_DATA_URL,
75
+ embedOrigin: DASHBOARD_EMBED_ORIGIN
76
+ };
77
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Consistent, actionable errors for consumers of `use-convex`.
3
+ *
4
+ * Branding uses `[use-convex]` (package name). Older `[use-convex]` strings
5
+ * are migrated here so setup failures point to the same fix.
6
+ */
7
+ export declare const USE_CONVEX_PREFIX = "[use-convex]";
8
+ /** Missing deployment URL — the most common first-run failure. */
9
+ export declare const MISSING_URL_HINT = "Set convex.url in nuxt.config or NUXT_PUBLIC_CONVEX_URL (runtimeConfig.public.convex.url).";
10
+ export declare function useConvexError(message: string): Error;
11
+ export declare function missingConvexUrlError(context?: string): Error;
12
+ export declare function warnMissingConvexUrl(scope: 'client' | 'server'): void;
13
+ export declare function unreachableConvexUrlError(convexUrl: string): Error;
14
+ export declare function warnStaleLocalConvexUrl(localUrl: string, cloudUrl: string): void;
@@ -0,0 +1,25 @@
1
+ export const USE_CONVEX_PREFIX = "[use-convex]";
2
+ export const MISSING_URL_HINT = "Set convex.url in nuxt.config or NUXT_PUBLIC_CONVEX_URL (runtimeConfig.public.convex.url).";
3
+ export function useConvexError(message) {
4
+ const text = message.startsWith(USE_CONVEX_PREFIX) ? message : `${USE_CONVEX_PREFIX} ${message}`;
5
+ return new Error(text);
6
+ }
7
+ export function missingConvexUrlError(context) {
8
+ const where = context ? `${context}: ` : "";
9
+ return useConvexError(`${where}No Convex URL. ${MISSING_URL_HINT}`);
10
+ }
11
+ export function warnMissingConvexUrl(scope) {
12
+ console.warn(
13
+ `${USE_CONVEX_PREFIX} No Convex URL on ${scope}. ${MISSING_URL_HINT} The module no-ops until a URL is set.`
14
+ );
15
+ }
16
+ export function unreachableConvexUrlError(convexUrl) {
17
+ return useConvexError(
18
+ `Could not reach Convex at ${convexUrl}. If you switched from a local to a cloud deployment, set CONVEX_URL and NUXT_PUBLIC_CONVEX_URL in .env.local to the URL printed by \`npx convex dev\` and restart Nuxt. If you use a local backend, keep \`npx convex dev\` running.`
19
+ );
20
+ }
21
+ export function warnStaleLocalConvexUrl(localUrl, cloudUrl) {
22
+ console.warn(
23
+ `${USE_CONVEX_PREFIX} Convex URL is ${localUrl} but CONVEX_DEPLOYMENT is a cloud deployment. The Convex CLI could not update .env.local \u2014 set CONVEX_URL and NUXT_PUBLIC_CONVEX_URL to ${cloudUrl} and restart Nuxt.`
24
+ );
25
+ }
@@ -1,12 +1,13 @@
1
+ import { useConvexError } from "./errors.js";
1
2
  export function parseOAuthRedirect(redirect) {
2
3
  let url;
3
4
  try {
4
5
  url = new URL(redirect);
5
6
  } catch {
6
- throw new Error("Invalid OAuth redirect URL");
7
+ throw useConvexError("Invalid OAuth redirect URL");
7
8
  }
8
9
  if (url.protocol !== "http:" && url.protocol !== "https:") {
9
- throw new Error("OAuth redirect must be http(s)");
10
+ throw useConvexError("OAuth redirect must be http(s)");
10
11
  }
11
12
  return url;
12
13
  }