From 9c438fa60418ed890b9455527ce801ac2a637737 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Fri, 26 Jun 2026 18:48:33 -0700 Subject: [PATCH 1/5] Update RDE project keys --- .../[projectId]/(overview)/setup-page.tsx | 104 ++++++++++++++++-- .../[projectId]/project-keys/page-client.tsx | 74 ++++++++++++- 2 files changed, 165 insertions(+), 13 deletions(-) diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx index b618fc3e9..eb7676ade 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx @@ -73,12 +73,50 @@ function buildCloudSetupPrompt(options: { `; } +function buildCliDevSetupPrompt(options: { + docsBaseUrl: string, +}) { + const { docsBaseUrl } = options; + const normalizedDocsBaseUrl = docsBaseUrl.replace(/\/$/, ''); + const reminders = remindersPrompt.replaceAll(PROD_DOCS_BASE_URL, normalizedDocsBaseUrl); + + return deindent` + Install and set up Hexclave in this project by following these instructions: + + Read https://skill.hexclave.com and follow the setup instructions it gives for this project's specific framework and language. + + Follow skill.hexclave.com as written, but use the local dashboard / Hexclave CLI development setup. Do not use the cloud environment-variable setup for local development. + + Set up the app's dev command so Hexclave starts through the CLI: + + \`\`\`json + { + "scripts": { + "dev": "hexclave dev --config-file ./hexclave.config.ts -- npm run dev:inner", + "dev:inner": "" + } + } + \`\`\` + + If the Hexclave CLI is not installed globally, use \`npx @hexclave/cli dev --config-file ./hexclave.config.ts -- npm run dev:inner\` instead. + + Do not create Hexclave project keys or ask for Hexclave environment variables for local development. The \`hexclave dev\` command automatically creates or links the local config project and injects the project ID and secret server key into the child app process. + + Keep project configuration in \`hexclave.config.ts\`. Once setup is done, run \`npm run dev\` and create the first user in the app. + + After setup finishes, verify that the Hexclave MCP server is registered in your AI client config — name: \`hexclave\`, transport: \`http\`, URL: \`https://mcp.hexclave.com/mcp\`. If it is not registered, add it manually so future agents have live access to Hexclave docs and APIs. + + ${reminders} + `; +} + export default function SetupPage(props: { toMetrics: () => void }) { const adminApp = useAdminApp(); const [setupMode, setSetupMode] = useState("recommended"); const [keys, setKeys] = useState<{ projectId: string, publishableClientKey?: string, secretServerKey: string } | null>(null); const projectConfig = adminApp.useProject().useConfig(); const requirePublishableClientKey = projectConfig.project.requirePublishableClientKey; + const isRemoteDevelopmentEnvironment = getPublicEnvVar("NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT") === "true"; const onGenerateKeys = async () => { const newKey = await adminApp.createInternalApiKey({ @@ -96,11 +134,16 @@ export default function SetupPage(props: { toMetrics: () => void }) { }); }; - const selectedInstallPrompt = buildCloudSetupPrompt({ - docsBaseUrl: getSetupDocsBaseUrl(), - projectId: adminApp.projectId, - apiBaseUrl: getSetupApiBaseUrl(), - }); + const setupDocsBaseUrl = getSetupDocsBaseUrl(); + const selectedInstallPrompt = isRemoteDevelopmentEnvironment + ? buildCliDevSetupPrompt({ + docsBaseUrl: setupDocsBaseUrl, + }) + : buildCloudSetupPrompt({ + docsBaseUrl: setupDocsBaseUrl, + projectId: adminApp.projectId, + apiBaseUrl: getSetupApiBaseUrl(), + }); const manualSetupDocsUrl = getManualSetupDocsUrl(); return ( @@ -130,13 +173,18 @@ export default function SetupPage(props: { toMetrics: () => void }) { variant='outline' size='sm' onClick={() => { - window.open(getSetupDocsBaseUrl(), '_blank'); + window.open(setupDocsBaseUrl, '_blank'); }} > Full Documentation + {isRemoteDevelopmentEnvironment && ( + + For local config projects, run your app with hexclave dev. It injects the project ID and secret key automatically, so you do not need to create project keys or write Hexclave environment variables. + + )} @@ -152,7 +200,31 @@ export default function SetupPage(props: { toMetrics: () => void }) { {setupMode === "recommended" ? (
    - {[ + {(isRemoteDevelopmentEnvironment ? [ + { + step: 1, + title: "Copy Setup Prompt", + content:
    + + {selectedInstallPrompt} + + } + title="Prompt for your AI agent" + icon="code" + maxHeight={480} + /> +
    , + }, + { + step: 2, + title: "Done", + content: , + }, + ] : [ { step: 1, title: "Copy Setup Prompt", @@ -186,7 +258,7 @@ export default function SetupPage(props: { toMetrics: () => void }) { title: "Done", content: , }, - ].map((item) => ( + ]).map((item) => (
  1. @@ -310,6 +382,22 @@ function SetupRecommendedDoneStep(props: { onExploreDashboard: () => void }) { ); } +function CliDevSetupStep() { + return ( +
    + + Start the app through the Hexclave CLI instead of copying project keys into an env file. + +
    + hexclave dev --config-file ./hexclave.config.ts -- <your-dev-command> +
    + + The CLI creates or links the local config project and injects the project ID and secret server key into the child process automatically. Use npx @hexclave/cli dev if the CLI is not installed globally. + +
    + ); +} + function HexclaveKeys(props: { keys: { projectId: string, publishableClientKey?: string, secretServerKey: string } | null, onGenerateKeys: () => Promise, diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-keys/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-keys/page-client.tsx index bccc0d1b6..79df4d7ce 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-keys/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-keys/page-client.tsx @@ -1,11 +1,13 @@ "use client"; import { InternalApiKeyTable } from "@/components/data-table/api-key-table"; -import { DesignAlert, DesignButton } from "@/components/design-components"; +import { DesignAlert, DesignButton, DesignCard } from "@/components/design-components"; import { EnvKeys } from "@/components/env-keys"; import { SmartFormDialog } from "@/components/form-dialog"; import { SelectField } from "@/components/form-fields"; +import { InlineCode } from "@/components/inline-code"; import { SettingSwitch } from "@/components/settings"; -import { ActionDialog, Button, Typography } from "@/components/ui"; +import { ActionDialog, Typography } from "@/components/ui"; +import { getPublicEnvVar } from "@/lib/env"; import { InternalApiKeyFirstView } from "@hexclave/next"; import { useSearchParams } from "next/navigation"; import { useState } from "react"; @@ -15,15 +17,32 @@ import { useAdminApp } from "../use-admin-app"; export default function PageClient() { + const hexclaveAdminApp = useAdminApp(); + const project = hexclaveAdminApp.useProject(); + const params = useSearchParams(); + const create = params.get("create") === "true"; + const isRemoteDevelopmentEnvironment = getPublicEnvVar("NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT") === "true"; + const showLocalConfigInstructions = isRemoteDevelopmentEnvironment && project.isDevelopmentEnvironment; + + if (showLocalConfigInstructions) { + return ( + + + + ); + } + + return ; +} + +function ProjectKeysManagement(props: { create: boolean }) { const hexclaveAdminApp = useAdminApp(); const project = hexclaveAdminApp.useProject(); const config = project.useConfig(); const requirePublishableClientKey = config.project.requirePublishableClientKey; const apiKeySets = hexclaveAdminApp.useInternalApiKeys(); - const params = useSearchParams(); - const create = params.get("create") === "true"; - const [isNewApiKeyDialogOpen, setIsNewApiKeyDialogOpen] = useState(create); + const [isNewApiKeyDialogOpen, setIsNewApiKeyDialogOpen] = useState(props.create); const [returnedApiKey, setReturnedApiKey] = useState(null); return ( @@ -66,6 +85,51 @@ export default function PageClient() { ); } +function LocalConfigProjectKeysInstructions() { + const hexclaveAdminApp = useAdminApp(); + const project = hexclaveAdminApp.useProject(); + const config = project.useConfig(); + const requirePublishableClientKey = config.project.requirePublishableClientKey; + + return ( + <> + + + +
    + + Run your app through the CLI so Hexclave can keep hexclave.config.ts and your app environment in sync: + +
    + npx @hexclave/cli dev --config-file <path-to-hexclave.config.ts> -- <your-dev-command> +
    + + This will automatically provide the correct environment variables to the specified command. + + + If you have the CLI installed globally, the same command starts with hexclave dev. Keep project settings in the config file; the CLI provides the runtime keys automatically. + +
    +
    + + { + await project.update({ + requirePublishableClientKey: checked, + }); + }} + /> + + ); +} + const neverInMs = 1000 * 60 * 60 * 24 * 365 * 200; const expiresInOptions = { [1000 * 60 * 60 * 24 * 1]: "1 day", From c6d162e5c8159fb422fd4a07d04745548a87447b Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Sat, 27 Jun 2026 14:54:24 -0700 Subject: [PATCH 2/5] Fix negative email queue step deltas --- .../backend/src/lib/email-queue-step.test.tsx | 42 +++++++++++++++++- apps/backend/src/lib/email-queue-step.tsx | 44 +++++++++++-------- 2 files changed, 66 insertions(+), 20 deletions(-) diff --git a/apps/backend/src/lib/email-queue-step.test.tsx b/apps/backend/src/lib/email-queue-step.test.tsx index bb255e77e..be7172666 100644 --- a/apps/backend/src/lib/email-queue-step.test.tsx +++ b/apps/backend/src/lib/email-queue-step.test.tsx @@ -1,10 +1,48 @@ import { EmailOutboxCreatedWith } from "@/generated/prisma/client"; import { globalPrismaClient } from "@/prisma-client"; -import { afterAll, describe, expect, it } from "vitest"; +import { afterAll, describe, expect, it, vi } from "vitest"; import { _forTesting } from "./email-queue-step"; import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch } from "./tenancies"; -const { failEmailsStuckInSending, STUCK_EMAIL_TIMEOUT_MS } = _forTesting; +const { failEmailsStuckInSending, STUCK_EMAIL_TIMEOUT_MS, updateLastExecutionTime } = _forTesting; + +describe.sequential("updateLastExecutionTime", () => { + const metadataKeys: string[] = []; + + afterAll(async () => { + await globalPrismaClient.emailOutboxProcessingMetadata.deleteMany({ + where: { key: { in: metadataKeys } }, + }); + }); + + it("does not move lastExecutedAt backwards when the stored timestamp is ahead", async () => { + const key = `email-queue-step-delta-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + metadataKeys.push(key); + + const futureTimestamp = new Date(Date.now() + 60_000); + await globalPrismaClient.emailOutboxProcessingMetadata.create({ + data: { + key, + lastExecutedAt: futureTimestamp, + }, + }); + + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + try { + const delta = await updateLastExecutionTime(key); + + expect(delta).toBe(0); + expect(warnSpy).not.toHaveBeenCalled(); + + const after = await globalPrismaClient.emailOutboxProcessingMetadata.findUniqueOrThrow({ + where: { key }, + }); + expect(after.lastExecutedAt?.toISOString()).toBe(futureTimestamp.toISOString()); + } finally { + warnSpy.mockRestore(); + } + }); +}); // These tests connect to the real dev DB (like payments.test.tsx) and create real EmailOutbox // rows against the seeded `internal` tenancy. Each row is tagged with a unique tsxSource so we diff --git a/apps/backend/src/lib/email-queue-step.tsx b/apps/backend/src/lib/email-queue-step.tsx index f9584c1cc..3b73e1478 100644 --- a/apps/backend/src/lib/email-queue-step.tsx +++ b/apps/backend/src/lib/email-queue-step.tsx @@ -203,32 +203,40 @@ async function failEmailsStuckInSending(additionalWhere?: Prisma.EmailOutboxWher export const _forTesting = { failEmailsStuckInSending, STUCK_EMAIL_TIMEOUT_MS, + updateLastExecutionTime, }; -async function updateLastExecutionTime(): Promise { - const key = "EMAIL_QUEUE_METADATA_KEY"; - +async function updateLastExecutionTime(key = "EMAIL_QUEUE_METADATA_KEY"): Promise { // This query atomically claims the next execution slot and returns the delta. - // It uses FOR UPDATE to lock the row, preventing concurrent workers from reading - // the same previous timestamp. The pattern is: + // It uses FOR UPDATE to lock the row, preventing concurrent workers from reading the + // same previous timestamp. Use clock_timestamp(), not NOW(): NOW() is fixed at the + // transaction start, so a transaction that started earlier but acquired the row lock + // later could otherwise move lastExecutedAt backwards by a few milliseconds. + // The pattern is: // 1. Try UPDATE first (locks row with FOR UPDATE, returns old and new timestamps) // 2. If no row exists, INSERT (with ON CONFLICT DO NOTHING for race handling) // 3. Compute delta based on the result const [{ delta }] = await globalPrismaClient.$queryRaw<{ delta: number }[]>` - WITH now_ts AS ( - SELECT NOW() AS now - ), - do_update AS ( + WITH do_update AS ( -- Update existing row, locking it first and capturing the old timestamp UPDATE "EmailOutboxProcessingMetadata" AS m SET - "updatedAt" = (SELECT now FROM now_ts), - "lastExecutedAt" = (SELECT now FROM now_ts) + "updatedAt" = old.next_timestamp, + "lastExecutedAt" = old.next_timestamp FROM ( - SELECT "key", "lastExecutedAt" AS previous_timestamp - FROM "EmailOutboxProcessingMetadata" - WHERE "key" = ${key} - FOR UPDATE + SELECT + locked."key", + locked."lastExecutedAt" AS previous_timestamp, + GREATEST(locked.observed_timestamp, COALESCE(locked."lastExecutedAt", locked.observed_timestamp)) AS next_timestamp + FROM ( + SELECT + "key", + "lastExecutedAt", + clock_timestamp()::timestamp(3) AS observed_timestamp + FROM "EmailOutboxProcessingMetadata" + WHERE "key" = ${key} + FOR UPDATE + ) AS locked ) AS old WHERE m."key" = old."key" RETURNING old.previous_timestamp, m."lastExecutedAt" AS new_timestamp @@ -236,7 +244,8 @@ async function updateLastExecutionTime(): Promise { do_insert AS ( -- Insert new row if no existing row was updated INSERT INTO "EmailOutboxProcessingMetadata" ("key", "lastExecutedAt", "updatedAt") - SELECT ${key}, (SELECT now FROM now_ts), (SELECT now FROM now_ts) + SELECT ${key}, observed_timestamp, observed_timestamp + FROM (SELECT clock_timestamp()::timestamp(3) AS observed_timestamp) AS now_ts WHERE NOT EXISTS (SELECT 1 FROM do_update) ON CONFLICT ("key") DO NOTHING RETURNING NULL::timestamp AS previous_timestamp, "lastExecutedAt" AS new_timestamp @@ -261,8 +270,7 @@ async function updateLastExecutionTime(): Promise { `; if (delta < 0) { - // TODO: why does this happen, actually? investigate. - console.warn("Email queue step delta is negative. Not sure why it happened. Ignoring the delta. TODO investigate", { delta }); + console.warn("Email queue step delta is negative after monotonic timestamp update; ignoring the delta so the send quota cannot go negative", { delta }); return 0; } From 092c27dd0e16db0409d865e666880a87f120f760 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 Jun 2026 22:00:12 +0000 Subject: [PATCH 3/5] chore: update package versions --- apps/backend/package.json | 2 +- apps/dashboard/package.json | 2 +- apps/dev-launchpad/package.json | 2 +- apps/e2e/package.json | 2 +- apps/hosted-components/package.json | 2 +- apps/internal-tool/package.json | 2 +- apps/mcp/package.json | 2 +- apps/mock-oauth-server/package.json | 2 +- apps/skills/package.json | 2 +- docs-mintlify/package.json | 2 +- docs/package.json | 2 +- examples/cjs-test/package.json | 2 +- examples/convex/package.json | 2 +- examples/demo/package.json | 2 +- examples/docs-examples/package.json | 2 +- examples/e-commerce/package.json | 2 +- examples/js-example/package.json | 2 +- examples/lovable-react-18-example/package.json | 2 +- examples/middleware/package.json | 2 +- examples/react-example/package.json | 2 +- examples/supabase/package.json | 2 +- examples/tanstack-start-demo/package.json | 2 +- packages/cli/package.json | 2 +- packages/dashboard-ui-components/package.json | 2 +- packages/js/package.json | 2 +- packages/next/package.json | 2 +- packages/react/package.json | 2 +- packages/sc/package.json | 2 +- packages/shared-backend/package.json | 2 +- packages/shared/package.json | 2 +- packages/tanstack-start/package.json | 2 +- packages/template/package-template.json | 2 +- packages/template/package.json | 2 +- packages/ui/package.json | 2 +- sdks/implementations/swift/package.json | 2 +- sdks/spec/package.json | 2 +- 36 files changed, 36 insertions(+), 36 deletions(-) diff --git a/apps/backend/package.json b/apps/backend/package.json index 103d43839..cbe30dc6a 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/backend", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "private": true, "type": "module", diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 55ddf0db4..0db91e513 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/dashboard", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "private": true, "scripts": { diff --git a/apps/dev-launchpad/package.json b/apps/dev-launchpad/package.json index 7e79ce4e4..6386fa2bf 100644 --- a/apps/dev-launchpad/package.json +++ b/apps/dev-launchpad/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/dev-launchpad", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "private": true, "scripts": { diff --git a/apps/e2e/package.json b/apps/e2e/package.json index 8990716da..3fb2f961a 100644 --- a/apps/e2e/package.json +++ b/apps/e2e/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/e2e-tests", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "private": true, "type": "module", diff --git a/apps/hosted-components/package.json b/apps/hosted-components/package.json index c83caecfc..9f53c1e72 100644 --- a/apps/hosted-components/package.json +++ b/apps/hosted-components/package.json @@ -1,7 +1,7 @@ { "name": "@hexclave/hosted-components", "private": true, - "version": "1.0.36", + "version": "1.0.37", "type": "module", "scripts": { "dev": "vite dev --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09", diff --git a/apps/internal-tool/package.json b/apps/internal-tool/package.json index dd591bb43..0f765190f 100644 --- a/apps/internal-tool/package.json +++ b/apps/internal-tool/package.json @@ -1,7 +1,7 @@ { "name": "@hexclave/internal-tool", "private": true, - "version": "1.0.36", + "version": "1.0.37", "type": "module", "scripts": { "dev": "node scripts/pre-dev.mjs && next dev --turbopack --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}41", diff --git a/apps/mcp/package.json b/apps/mcp/package.json index d68c556ce..10f1d48df 100644 --- a/apps/mcp/package.json +++ b/apps/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/mcp", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "private": true, "type": "module", diff --git a/apps/mock-oauth-server/package.json b/apps/mock-oauth-server/package.json index a945d2126..b9bc42d31 100644 --- a/apps/mock-oauth-server/package.json +++ b/apps/mock-oauth-server/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/mock-oauth-server", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "private": true, "main": "index.js", diff --git a/apps/skills/package.json b/apps/skills/package.json index 4de8d4616..cecd04030 100644 --- a/apps/skills/package.json +++ b/apps/skills/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/skills", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "private": true, "type": "module", diff --git a/docs-mintlify/package.json b/docs-mintlify/package.json index 8eb5be202..57dbb1973 100644 --- a/docs-mintlify/package.json +++ b/docs-mintlify/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/docs-mintlify", - "version": "1.0.36", + "version": "1.0.37", "private": true, "scripts": { "dev": "mint dev --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}04 --no-open", diff --git a/docs/package.json b/docs/package.json index 323bee897..7bf9bb5bc 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/docs", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "description": "", "main": "index.js", diff --git a/examples/cjs-test/package.json b/examples/cjs-test/package.json index 64cf0a081..de7edfc66 100644 --- a/examples/cjs-test/package.json +++ b/examples/cjs-test/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/example-cjs-test", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "private": true, "scripts": { diff --git a/examples/convex/package.json b/examples/convex/package.json index cd2808c6a..2def235c6 100644 --- a/examples/convex/package.json +++ b/examples/convex/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/convex-example", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "private": true, "scripts": { diff --git a/examples/demo/package.json b/examples/demo/package.json index fefe72be7..5bbf52958 100644 --- a/examples/demo/package.json +++ b/examples/demo/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/example-demo-app", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "description": "", "private": true, diff --git a/examples/docs-examples/package.json b/examples/docs-examples/package.json index 32377198f..f58eea314 100644 --- a/examples/docs-examples/package.json +++ b/examples/docs-examples/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/docs-examples", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "description": "", "private": true, diff --git a/examples/e-commerce/package.json b/examples/e-commerce/package.json index be4c09425..984c08933 100644 --- a/examples/e-commerce/package.json +++ b/examples/e-commerce/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/e-commerce-demo", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "private": true, "scripts": { diff --git a/examples/js-example/package.json b/examples/js-example/package.json index 523cf3e1e..ca038f432 100644 --- a/examples/js-example/package.json +++ b/examples/js-example/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/js-example", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "private": true, "description": "", diff --git a/examples/lovable-react-18-example/package.json b/examples/lovable-react-18-example/package.json index 9a3df8697..87c76baf0 100644 --- a/examples/lovable-react-18-example/package.json +++ b/examples/lovable-react-18-example/package.json @@ -1,7 +1,7 @@ { "name": "@hexclave/lovable-react-18-example", "private": true, - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "type": "module", "scripts": { diff --git a/examples/middleware/package.json b/examples/middleware/package.json index acba41221..5c2e2c0ea 100644 --- a/examples/middleware/package.json +++ b/examples/middleware/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/example-middleware-demo", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "private": true, "scripts": { diff --git a/examples/react-example/package.json b/examples/react-example/package.json index a69bc0176..8ece1877a 100644 --- a/examples/react-example/package.json +++ b/examples/react-example/package.json @@ -1,7 +1,7 @@ { "name": "react-example", "private": true, - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "type": "module", "scripts": { diff --git a/examples/supabase/package.json b/examples/supabase/package.json index 137aa413b..acce874f0 100644 --- a/examples/supabase/package.json +++ b/examples/supabase/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/example-supabase", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "private": true, "scripts": { diff --git a/examples/tanstack-start-demo/package.json b/examples/tanstack-start-demo/package.json index 0d3139d50..282d723bb 100644 --- a/examples/tanstack-start-demo/package.json +++ b/examples/tanstack-start-demo/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/example-tanstack-start-demo", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "description": "TanStack Start demo app for Hexclave", "private": true, diff --git a/packages/cli/package.json b/packages/cli/package.json index a8efc88a3..66c6703c5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/cli", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "description": "The CLI for Hexclave. https://hexclave.com", "main": "dist/index.js", diff --git a/packages/dashboard-ui-components/package.json b/packages/dashboard-ui-components/package.json index e7c1c8af9..c26854855 100644 --- a/packages/dashboard-ui-components/package.json +++ b/packages/dashboard-ui-components/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/dashboard-ui-components", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/js/package.json b/packages/js/package.json index 0a185906d..6769e458d 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -1,7 +1,7 @@ { "//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)", "name": "@hexclave/js", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "sideEffects": false, "main": "./dist/index.js", diff --git a/packages/next/package.json b/packages/next/package.json index e973c42ef..611454544 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,7 +1,7 @@ { "//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)", "name": "@hexclave/next", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "sideEffects": false, "main": "./dist/index.js", diff --git a/packages/react/package.json b/packages/react/package.json index 5a7b1a22d..e3c3d96c0 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,7 +1,7 @@ { "//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)", "name": "@hexclave/react", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "sideEffects": false, "main": "./dist/index.js", diff --git a/packages/sc/package.json b/packages/sc/package.json index 302d02be8..c0eae2df9 100644 --- a/packages/sc/package.json +++ b/packages/sc/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/sc", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "exports": { "./force-react-server": { diff --git a/packages/shared-backend/package.json b/packages/shared-backend/package.json index 5f32d00c0..497153785 100644 --- a/packages/shared-backend/package.json +++ b/packages/shared-backend/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/shared-backend", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/shared/package.json b/packages/shared/package.json index 10bf7a272..ec911b348 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/shared", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "scripts": { "build": "rimraf dist && tsdown", diff --git a/packages/tanstack-start/package.json b/packages/tanstack-start/package.json index c396708c0..c91d38e94 100644 --- a/packages/tanstack-start/package.json +++ b/packages/tanstack-start/package.json @@ -1,7 +1,7 @@ { "//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)", "name": "@hexclave/tanstack-start", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "sideEffects": false, "main": "./dist/index.js", diff --git a/packages/template/package-template.json b/packages/template/package-template.json index cc618c8df..bd4242a3e 100644 --- a/packages/template/package-template.json +++ b/packages/template/package-template.json @@ -13,7 +13,7 @@ "//": "NEXT_LINE_PLATFORM template", "private": true, - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "sideEffects": false, "main": "./dist/index.js", diff --git a/packages/template/package.json b/packages/template/package.json index 274aac3ba..a22603532 100644 --- a/packages/template/package.json +++ b/packages/template/package.json @@ -2,7 +2,7 @@ "//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)", "name": "@hexclave/template", "private": true, - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "sideEffects": false, "main": "./dist/index.js", diff --git a/packages/ui/package.json b/packages/ui/package.json index 02cedd167..75766986a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/ui", - "version": "1.0.36", + "version": "1.0.37", "repository": "https://github.com/hexclave/hexclave", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/sdks/implementations/swift/package.json b/sdks/implementations/swift/package.json index 37dc745ab..3c8151553 100644 --- a/sdks/implementations/swift/package.json +++ b/sdks/implementations/swift/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/swift-sdk", - "version": "1.0.36", + "version": "1.0.37", "private": true, "description": "Hexclave Swift SDK", "scripts": { diff --git a/sdks/spec/package.json b/sdks/spec/package.json index 755fcbd59..ea531cec1 100644 --- a/sdks/spec/package.json +++ b/sdks/spec/package.json @@ -1,6 +1,6 @@ { "name": "@hexclave/sdk-spec", - "version": "1.0.36", + "version": "1.0.37", "private": true, "description": "Hexclave SDK specification files", "scripts": {} From 4a0f2b177865fb3138c71a3be8c060f2bb1292ac Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 09:31:06 -0700 Subject: [PATCH 4/5] Default user export to filtered scope; note Anonymous in all-users label (#1679) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Two changes to the user data export dialog on the project Users page: 1. The export scope now defaults to **"Export only filtered/searched users"** instead of "all users". 2. The all-users option label is now **"Export all users in the project (includes Anonymous)"**. To keep this scoped to the Users table (the shared export dialog is reused by other tables), the dialog's default scope is made configurable rather than changed globally: - `DataGridExportOptions` gains `defaultScope?: DataGridExportScope` (defaults to `"all"`). - The dialog initializes `useState(exportOptions?.defaultScope ?? "all")`. - `user-table.tsx` passes `defaultScope: "filtered"` and the updated `allScopeLabel`. Other tables (teams, transactions, emails) are unaffected — they keep the `"all"` default. Link to Devin session: https://app.devin.ai/sessions/4996678b2b944090b6eef2f64f0a62a1 --- ## Summary by cubic Default the Users export dialog to filtered scope and clarify that the "all users" option includes Anonymous; scope resets to the per-table default only when the dialog reopens, and other tables keep "all". - **New Features** - Added `defaultScope` to export options and initialized scope from it; Users table sets `defaultScope: "filtered"` and updates the all-users label. - Reset scope to `defaultScope` only on a closed→open transition to avoid changing it while the dialog is open. - **Bug Fixes** - Stubbed `NODE_ENV` via `vi.stubEnv` in `apps/backend/src/oauth/ssrf-protection.test.ts` to fix lint and prevent env mutation. Written for commit 3aa670b6d2249af737d4d05506b2f3a3737b075c. Summary will update on new commits. Review in cubic --------- Co-authored-by: vedanta.gawande Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../backend/src/oauth/ssrf-protection.test.ts | 11 +++------- .../src/components/data-table/user-table.tsx | 3 ++- .../data-grid/data-grid-export-dialog.tsx | 20 +++++++++++++++++-- .../src/components/data-grid/types.ts | 2 ++ 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/apps/backend/src/oauth/ssrf-protection.test.ts b/apps/backend/src/oauth/ssrf-protection.test.ts index 330fb7f25..68b001db3 100644 --- a/apps/backend/src/oauth/ssrf-protection.test.ts +++ b/apps/backend/src/oauth/ssrf-protection.test.ts @@ -1,19 +1,14 @@ import { StatusError } from "@hexclave/shared/dist/utils/errors"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import dns from "node:dns"; import { assertSafeOAuthResolvedAddress, assertSafeOAuthUrlWithoutDns, isBlockedOAuthIpAddress, safeOAuthDnsLookup } from "./ssrf-protection"; async function withProductionNodeEnv(callback: () => Promise): Promise { - const previousNodeEnv = process.env.NODE_ENV; - process.env.NODE_ENV = "production"; + vi.stubEnv("NODE_ENV", "production"); try { return await callback(); } finally { - if (previousNodeEnv === undefined) { - delete process.env.NODE_ENV; - } else { - process.env.NODE_ENV = previousNodeEnv; - } + vi.unstubAllEnvs(); } } diff --git a/apps/dashboard/src/components/data-table/user-table.tsx b/apps/dashboard/src/components/data-table/user-table.tsx index 9d726663e..f0f44411c 100644 --- a/apps/dashboard/src/components/data-table/user-table.tsx +++ b/apps/dashboard/src/components/data-table/user-table.tsx @@ -426,7 +426,8 @@ function UserTableBody(props: { fetchRows: fetchExportRows, emptyExportTitle: "No users to export", emptyExportDescription: "There are no users matching the current filters", - allScopeLabel: "Export all users in the project", + defaultScope: "filtered", + allScopeLabel: "Export all users in the project (includes Anonymous)", filteredScopeLabel: ( <> Export only filtered/searched users diff --git a/packages/dashboard-ui-components/src/components/data-grid/data-grid-export-dialog.tsx b/packages/dashboard-ui-components/src/components/data-grid/data-grid-export-dialog.tsx index 4c4e3d722..249b158a7 100644 --- a/packages/dashboard-ui-components/src/components/data-grid/data-grid-export-dialog.tsx +++ b/packages/dashboard-ui-components/src/components/data-grid/data-grid-export-dialog.tsx @@ -1,7 +1,7 @@ "use client"; import { DownloadSimpleIcon } from "@phosphor-icons/react"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { DesignButton } from "../button"; import { DesignDialog } from "../dialog"; @@ -55,7 +55,7 @@ export function DataGridExportDialog({ [exportOptions?.fields, columns], ); const [format, setFormat] = useState("csv"); - const [scope, setScope] = useState("all"); + const [scope, setScope] = useState(exportOptions?.defaultScope ?? "all"); const [fields, setFields] = useState[]>(resolvedFields); const [isExporting, setIsExporting] = useState(false); const [progress, setProgress] = useState(idleExportProgress); @@ -67,6 +67,22 @@ export function DataGridExportDialog({ } }, [isExporting, resolvedFields]); + // Reset the scope to its default each time the dialog opens. The dialog stays + // mounted between opens, so without this the scope would retain whatever the + // user last picked instead of honoring `defaultScope` on every open. We track + // the previous `open` value with a ref so the reset only fires on a genuine + // closed->open transition -- not on every render that flips other state (e.g. + // `isExporting` going false after a failed/empty export would otherwise wipe + // the user's current selection while the dialog is still open). + const defaultScope = exportOptions?.defaultScope ?? "all"; + const wasOpenRef = useRef(false); + useEffect(() => { + if (open && !wasOpenRef.current) { + setScope(defaultScope); + } + wasOpenRef.current = open; + }, [open, defaultScope]); + const entityName = exportOptions?.entityName ?? "row"; const entityNamePlural = exportOptions?.entityNamePlural ?? "rows"; const filenamePrefix = exportOptions?.filenamePrefix ?? exportFilename; diff --git a/packages/dashboard-ui-components/src/components/data-grid/types.ts b/packages/dashboard-ui-components/src/components/data-grid/types.ts index 965e90894..f4b6ea286 100644 --- a/packages/dashboard-ui-components/src/components/data-grid/types.ts +++ b/packages/dashboard-ui-components/src/components/data-grid/types.ts @@ -251,6 +251,8 @@ export type DataGridExportOptions = { allScopeLabel?: ReactNode; filteredScopeLabel?: ReactNode; progressSubjectLabel?: string; + /** Which export scope is selected by default when the dialog opens. Defaults to `"all"`. */ + defaultScope?: DataGridExportScope; }; // ─── Callbacks ─────────────────────────────────────────────────────── From c54ab2874d9d0ca05fcfb6bb270f8c009ef1a48e Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 10:18:16 -0700 Subject: [PATCH 5/5] Replace browser alert() with DesignDialog in user create/edit flow (#1680) ## Summary In `UserDialog`, duplicate-email errors (`UserWithEmailAlreadyExists`, `ContactChannelAlreadyUsedForAuthBySomeoneElse`) were shown via `window.alert()`. Replaced with a controlled `DesignDialog` using `WarningCircleIcon`, so the error appears as a styled modal instead of a native browser popup. Link to Devin session: https://app.devin.ai/sessions/99d73091c47a4a58b33d8724df5a7ce8 --- ## Summary by cubic Replaced `window.alert()` in the user create/edit flow with a controlled `DesignDialog` for duplicate-email errors (`UserWithEmailAlreadyExists`, `ContactChannelAlreadyUsedForAuthBySomeoneElse`). Uses `WarningCircleIcon` with an OK action, preserves form data via `prevent-close-and-prevent-reset`, clears dialog state when the form closes, and fixes a max-statements-per-line lint warning. Written for commit 274fcb8e9bb99ab1fa3b2976ca1137f378a6c167. Summary will update on new commits. Review in cubic --------- Co-authored-by: vedanta.gawande Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Armaan Jain <84474476+Developing-Gamer@users.noreply.github.com> --- apps/dashboard/src/components/user-dialog.tsx | 254 ++++++++++-------- 1 file changed, 141 insertions(+), 113 deletions(-) diff --git a/apps/dashboard/src/components/user-dialog.tsx b/apps/dashboard/src/components/user-dialog.tsx index 8a206bde8..688562744 100644 --- a/apps/dashboard/src/components/user-dialog.tsx +++ b/apps/dashboard/src/components/user-dialog.tsx @@ -3,6 +3,9 @@ import { ServerUser } from "@hexclave/next"; import { KnownErrors } from "@hexclave/shared"; import { countryCodeSchema, emailSchema, jsonStringOrEmptySchema, passwordSchema } from "@hexclave/shared/dist/schema-fields"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Button, Typography } from "@/components/ui"; +import { DesignButton, DesignDialog, DesignDialogClose } from "@/components/design-components"; +import { WarningCircleIcon } from "@phosphor-icons/react"; +import { useState } from "react"; import * as yup from "yup"; import { FormDialog } from "./form-dialog"; import { CountryCodeField } from "./country-code-select"; @@ -24,6 +27,7 @@ export function UserDialog(props: { })) { const adminApp = useAdminApp(); const project = adminApp.useProject(); + const [errorDialog, setErrorDialog] = useState<{ title: string; description: string } | null>(null); let defaultValues; if (props.type === 'edit') { @@ -127,135 +131,159 @@ export function UserDialog(props: { } } catch (error) { if (KnownErrors.UserWithEmailAlreadyExists.isInstance(error)) { - alert("Email already exists. Please choose a different email address."); - return 'prevent-close'; + setErrorDialog({ + title: "Email already exists", + description: "Please choose a different email address.", + }); + return 'prevent-close-and-prevent-reset'; } if (KnownErrors.ContactChannelAlreadyUsedForAuthBySomeoneElse.isInstance(error)) { - alert("Email already used for authentication. This email is already used for sign-in by another account. Please choose a different email address."); - return 'prevent-close'; + setErrorDialog({ + title: "Email already used for authentication", + description: "This email is already used for sign-in by another account. Please choose a different email address.", + }); + return 'prevent-close-and-prevent-reset'; } throw error; } } - return ( - <> - {props.type === 'edit' ? ID: {props.user.id} : null} + return <> + { if (!open) setErrorDialog(null); }} + size="sm" + icon={WarningCircleIcon} + title={errorDialog?.title ?? ""} + description={errorDialog?.description ?? ""} + footer={ + + OK + + } + /> + { + if (!open) setErrorDialog(null); + props.onOpenChange?.(open); + }} + trigger={props.trigger} + title={props.type === 'edit' ? "Edit User" : "Create User"} + formSchema={formSchema} + defaultValues={defaultValues} + okButton={{ label: props.type === 'edit' ? "Save" : "Create" }} + render={(form) => ( + <> + {props.type === 'edit' ? ID: {props.user.id} : null} -
    -
    - +
    +
    + +
    +
    + +
    -
    - -
    -
    - + - + - {project.config.magicLinkEnabled && } - {project.config.credentialEnabled && } - {form.watch("passwordEnabled") && ( - props.type === 'edit' && !form.watch("password") && !form.watch("updatePassword") ? ( - - ) : ( - - ) - )} - {!form.watch("primaryEmailVerified") && form.watch("otpAuthEnabled") && Primary email must be verified if OTP/magic link sign-in is enabled} + {project.config.magicLinkEnabled && } + {project.config.credentialEnabled && } + {form.watch("passwordEnabled") && ( + props.type === 'edit' && !form.watch("password") && !form.watch("updatePassword") ? ( + + ) : ( + + ) + )} + {!form.watch("primaryEmailVerified") && form.watch("otpAuthEnabled") && Primary email must be verified if OTP/magic link sign-in is enabled} + + {props.type === "create" && ( + + + Risk and Geo + + +
    + + +
    + + Optional admin-only values for imports or custom anti-abuse systems. Leave blank to use the defaults. + +
    +
    +
    + )} - {props.type === "create" && ( - - Risk and Geo + + Metadata - -
    - - -
    - - Optional admin-only values for imports or custom anti-abuse systems. Leave blank to use the defaults. - + + Custom JSON clients can read and update; avoid sensitive data.{" "} + Learn more in the docs. + + } + /> + + Custom JSON clients can read but only your backend can change.{" "} + Learn more in the docs. + + } + /> + + Custom JSON reserved for server-side logic and never exposed to clients.{" "} + Learn more in the docs. + + } + />
    - )} - - - - Metadata - - - Custom JSON clients can read and update; avoid sensitive data.{" "} - Learn more in the docs. - - } - /> - - Custom JSON clients can read but only your backend can change.{" "} - Learn more in the docs. - - } - /> - - Custom JSON reserved for server-side logic and never exposed to clients.{" "} - Learn more in the docs. - - } - /> - - - - - )} - onSubmit={handleSubmit} - cancelButton - />; + + )} + onSubmit={handleSubmit} + cancelButton + /> + ; }