Merge branch 'dev' into ask-mcp-endpoint-on-skill

This commit is contained in:
Armaan Jain 2026-06-10 15:50:05 -07:00 committed by GitHub
commit 4ad3c135fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 520 additions and 101 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/backend",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"type": "module",

View File

@ -96,7 +96,7 @@ describe("local emulator config", () => {
await writeConfigToFile(absoluteFilePath, { auth: { allowLocalhost: true } });
await expect(fs.readFile(mountedFilePath, "utf-8")).resolves.toBe(
`import type { HexclaveConfig } from "@hexclave/js";\n\nexport const config: HexclaveConfig = {\n "auth": {\n "allowLocalhost": true\n }\n};\n`
`import type { HexclaveConfig } from "@hexclave/js/config";\n\nexport const config: HexclaveConfig = {\n "auth": {\n "allowLocalhost": true\n }\n};\n`
);
});

@ -1 +1 @@
Subproject commit a815ddcd1354d4bf27042626d1035709c80abdc6
Subproject commit 9f7eb33f1a4bdb38d2aef4e64a42efe423a925ec

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/dashboard",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"scripts": {

View File

@ -34,7 +34,7 @@ import {
} from "@phosphor-icons/react";
import { throwErr } from "@hexclave/shared/dist/utils/errors";
import { runAsynchronously, runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises";
import { ActionDialog, Dialog, DialogContent, DialogTitle, Label, Popover, PopoverContent, PopoverTrigger, Typography, useToast } from "@/components/ui";
import { ActionDialog, Dialog, DialogContent, DialogTitle, Label, Popover, PopoverContent, PopoverTrigger, SimpleTooltip, Typography, useToast } from "@/components/ui";
import { useCallback, useEffect, useMemo, useState } from "react";
import * as yup from "yup";
import Image from "next/image";
@ -178,6 +178,8 @@ const PROVIDERS: ProviderMeta[] = [
},
];
const REMOTE_DEVELOPMENT_ENVIRONMENT_EMAIL_PROVIDER_TOOLTIP = "These email server options are not supported when running dashboard locally.";
function TestSendingDialog(props: { trigger: React.ReactNode }) {
const hexclaveAdminApp = useAdminApp();
const project = hexclaveAdminApp.useProject();
@ -960,18 +962,21 @@ export function DomainSettings() {
const isSelected = serverType === p.value;
const isSaved = savedServerType === p.value;
const isDraft = isSelected && !isSaved;
const isDisabledInDevelopmentEnvironment = project.isDevelopmentEnvironment && p.value !== "shared";
const Icon = p.icon;
return (
const button = (
<button
key={p.value}
type="button"
disabled={isDisabledInDevelopmentEnvironment}
onClick={() => handleSelectProvider(p.value)}
className={cn(
"relative text-left rounded-xl border p-4 transition-all",
"relative h-full w-full text-left rounded-xl border p-4 transition-all disabled:pointer-events-none",
isSaved && "border-green-500/40 bg-green-500/[0.04]",
isDraft && "border-amber-500/50 bg-amber-500/[0.04] ring-1 ring-amber-500/20 border-dashed",
!isSaved && !isDraft && "border-border/60 hover:border-foreground/20 hover:bg-foreground/[0.02]",
isSelected && !isDraft && !isSaved && "border-foreground/40 bg-foreground/[0.03] shadow-sm ring-1 ring-foreground/10",
isDisabledInDevelopmentEnvironment && "cursor-not-allowed opacity-50 hover:border-border/60 hover:bg-transparent",
)}
>
<div className="absolute top-2 right-2 flex items-center gap-1">
@ -994,6 +999,11 @@ export function DomainSettings() {
<div className="text-xs text-muted-foreground mt-1 leading-relaxed">{p.tagline}</div>
</button>
);
return isDisabledInDevelopmentEnvironment ? (
<SimpleTooltip key={p.value} tooltip={REMOTE_DEVELOPMENT_ENVIRONMENT_EMAIL_PROVIDER_TOOLTIP} inline className="block h-full">
{button}
</SimpleTooltip>
) : button;
})}
</div>

View File

@ -53,7 +53,6 @@ import { fromNow } from "@hexclave/shared/dist/utils/dates";
import { captureError, HexclaveAssertionError, throwErr } from '@hexclave/shared/dist/utils/errors';
import { runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises";
import { deindent } from "@hexclave/shared/dist/utils/strings";
import { urlString } from "@hexclave/shared/dist/utils/urls";
import { usePathname, useSearchParams } from "next/navigation";
import { Suspense, useCallback, useEffect, useMemo, useRef, useState, type ReactNode, type RefObject } from "react";
import { createPortal } from "react-dom";
@ -115,7 +114,6 @@ function UserHeader({ user }: UserHeaderProps) {
const [restrictionDialogOpen, setRestrictionDialogOpen] = useState(false);
const [impersonateSnippet, setImpersonateSnippet] = useState<string | null>(null);
const hexclaveAdminApp = useAdminApp();
const router = useRouter();
return (
<div className="flex min-w-0 gap-4 items-center">
@ -136,12 +134,6 @@ function UserHeader({ user }: UserHeaderProps) {
<p>Last active {fromNow(user.lastActiveAt)}</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={() => router.push(`${urlString`/projects/${hexclaveAdminApp.projectId}/conversations`}?userId=${encodeURIComponent(user.id)}`)}
>
Support
</Button>
<DesignMenu
variant="actions"
trigger="icon"

View File

@ -50,7 +50,7 @@ export const config: HexclaveConfig = {
`;
const result = buildUpdatedConfigFileContent(current, { "teams.allowClientTeamCreation": true });
expect(result).toMatchInlineSnapshot(`
"import type { HexclaveConfig } from "@hexclave/next";
"import type { HexclaveConfig } from "@hexclave/next/config";
export const config: HexclaveConfig = {
"teams": {
@ -68,7 +68,7 @@ export const config: HexclaveConfig = {};
`;
const result = buildUpdatedConfigFileContent(current, { "auth.allowSignUp": true });
expect(result).toMatchInlineSnapshot(`
"import type { HexclaveConfig } from "@hexclave/react";
"import type { HexclaveConfig } from "@hexclave/react/config";
export const config: HexclaveConfig = {
"auth": {
@ -104,7 +104,7 @@ export const config: StackConfig = {};
const current = `export const config = {};\n`;
const result = buildUpdatedConfigFileContent(current, { "auth.allowSignUp": true });
expect(result).toMatchInlineSnapshot(`
"import type { HexclaveConfig } from "@hexclave/js";
"import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = {
"auth": {
@ -124,7 +124,7 @@ export const config: HexclaveConfig = {};
"payments.items.todos.customerType": "user",
});
expect(result).toMatchInlineSnapshot(`
"import type { HexclaveConfig } from "@hexclave/js";
"import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = {
"payments": {
@ -150,7 +150,7 @@ export const config: HexclaveConfig = {
"payments.items.todos.displayName": "New",
});
expect(result).toMatchInlineSnapshot(`
"import type { HexclaveConfig } from "@hexclave/js";
"import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = {
"payments": {
@ -226,7 +226,7 @@ export const config: HexclaveConfig = { teams: { allowClientTeamCreation: false
{
"body": {
"branch": "main",
"content": "import type { HexclaveConfig } from "@hexclave/js";
"content": "import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = {
"teams": {
@ -266,7 +266,7 @@ export const config: HexclaveConfig = { teams: { allowClientTeamCreation: false
{
"body": {
"branch": "main",
"content": "import type { HexclaveConfig } from "@hexclave/js";
"content": "import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = {
"auth": {
@ -288,7 +288,7 @@ export const config: HexclaveConfig = { teams: { allowClientTeamCreation: false
});
it("skips the commit when the new rendered file is identical to the old one", async () => {
const same = `import type { HexclaveConfig } from "@hexclave/js";
const same = `import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = {
"teams": {

View File

@ -28,10 +28,13 @@ import {
*/
function detectImportPackage(currentFileContent: string): string | undefined {
// Match `from "@hexclave/<name>"` or `from "@stackframe/<name>"` — single
// or double quotes. Hexclave preferred when both appear.
const hexclave = currentFileContent.match(/from\s+["']@hexclave\/([a-z0-9-]+)["']/i);
// or double quotes, with an optional `/config` subpath suffix (the lightweight
// entrypoint newer config files import from). We return the bare package name;
// the renderer re-appends `/config` for Hexclave packages. Hexclave preferred
// when both appear.
const hexclave = currentFileContent.match(/from\s+["']@hexclave\/([a-z0-9-]+)(?:\/config)?["']/i);
if (hexclave) return `@hexclave/${hexclave[1]}`;
const stackframe = currentFileContent.match(/from\s+["']@stackframe\/([a-z0-9-]+)["']/i);
const stackframe = currentFileContent.match(/from\s+["']@stackframe\/([a-z0-9-]+)(?:\/config)?["']/i);
return stackframe ? `@stackframe/${stackframe[1]}` : undefined;
}

View File

@ -1,14 +1,25 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
import { join } from "path";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
import { afterEach, describe, expect, it, vi } from "vitest";
vi.mock("server-only", () => ({}));
// Root temp config files next to this test file (inside apps/dashboard) rather
// than at process.cwd() (the repo root under vitest's workspace runner). This
// lets jiti resolve workspace packages like `@hexclave/next/config` the same
// way a real user project would — walking up to apps/dashboard/node_modules.
const TEST_FILE_DIR = dirname(fileURLToPath(import.meta.url));
let tempDir: string | undefined;
function createTempDir(): string {
tempDir ??= mkdtempSync(join(TEST_FILE_DIR, ".stack-rde-config-test-"));
return tempDir;
}
function writeTempConfig(content: string): string {
tempDir ??= mkdtempSync(join(process.cwd(), ".stack-rde-config-test-"));
const configPath = join(tempDir, "stack.config.ts");
const configPath = join(createTempDir(), "stack.config.ts");
writeFileSync(configPath, content, "utf-8");
return configPath;
}
@ -24,7 +35,7 @@ afterEach(() => {
describe("remote development environment config file", () => {
it("loads config exports wrapped in defineStackConfig", async () => {
const configPath = writeTempConfig(`
import { defineStackConfig } from "@hexclave/shared/config";
import { defineStackConfig } from "@hexclave/next/config";
export const config = defineStackConfig({
auth: {
@ -49,7 +60,7 @@ describe("remote development environment config file", () => {
it("loads config exports wrapped in defineHexclaveConfig", async () => {
const configPath = writeTempConfig(`
import { defineHexclaveConfig } from "@hexclave/shared/config";
import { defineHexclaveConfig } from "@hexclave/next/config";
export const config = defineHexclaveConfig({
auth: {
@ -155,6 +166,24 @@ describe("remote development environment config file", () => {
`);
});
it("throws a helpful error when the config file imports a module that fails to load", async () => {
// Simulate a heavy framework package (e.g. @stackframe/stack) that throws on import
const dir = createTempDir();
const heavyPackagePath = join(dir, "heavy-package.ts");
writeFileSync(heavyPackagePath, `throw new Error("Cannot load this in a Node.js context");`, "utf-8");
const configPath = join(dir, "stack.config.ts");
writeFileSync(configPath, `
import "${heavyPackagePath}";
export const config = {};
`, "utf-8");
const { readConfigFile } = await import("./config-file");
await expect(readConfigFile(configPath)).rejects.toThrow(
`Failed to load config file ${configPath}. If your config imports a value (e.g. defineHexclaveConfig) from a framework package such as "@hexclave/next", import it from that package's lightweight "/config" entrypoint instead`
);
});
it("rejects modules without a valid config export", async () => {
const configPath = writeTempConfig(`
export const config = () => ({ auth: { allowSignUp: true } });
@ -173,6 +202,10 @@ describe("remote development environment config file", () => {
},
};
`);
// Pin the SDK package the rendered import line points at, so the snapshot
// doesn't depend on which @hexclave/* package the surrounding workspace
// (apps/dashboard) happens to depend on.
writeFileSync(join(createTempDir(), "package.json"), JSON.stringify({ dependencies: { "@hexclave/js": "*" } }), "utf-8");
const { readConfigFile, writeConfigObject } = await import("./config-file");
const current = await readConfigFile(configPath);
@ -182,7 +215,7 @@ describe("remote development environment config file", () => {
});
expect(readFileSync(configPath, "utf-8")).toMatchInlineSnapshot(`
"import type { HexclaveConfig } from "@hexclave/js";
"import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = {
"auth": {

View File

@ -3,6 +3,7 @@ import "server-only";
import { showOnboardingHexclaveConfigValue } from "@hexclave/shared/dist/config-authoring";
import { Config, isValidConfig } from "@hexclave/shared/dist/config/format";
import { detectImportPackageFromDir, renderConfigFileContent } from "@hexclave/shared/dist/config-rendering";
import { captureError } from "@hexclave/shared/dist/utils/errors";
import { createHash } from "crypto";
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
import { createJiti } from "jiti";
@ -62,7 +63,18 @@ export async function readConfigFile(configFilePath: string): Promise<{ config:
};
}
const configModule = await jiti.import<unknown>(configFilePath);
let configModule: unknown;
try {
configModule = await jiti.import<unknown>(configFilePath);
} catch (error) {
// Capture the raw jiti/framework error for diagnostics, but don't attach it as `cause` on the thrown error:
// the dashboard's error formatter (errorToNiceString -> nicify) renders `Error.cause` recursively, which would
// leak the underlying framework stack/internals back into the user-facing message we're deliberately replacing.
captureError("remote-development-environment/readConfigFile", error);
throw new Error(
`Failed to load config file ${configFilePath}. If your config imports a value (e.g. defineHexclaveConfig) from a framework package such as "@hexclave/next", import it from that package's lightweight "/config" entrypoint instead, which doesn't load the framework runtime:\n\n import { defineHexclaveConfig } from "@hexclave/next/config";\n`,
);
}
if (!isConfigModule(configModule)) {
throw new Error(`Invalid config in ${configFilePath}. The file must export a plain \`config\` object or "show-onboarding".`);
}

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/dev-launchpad",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/e2e-tests",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"type": "module",

View File

@ -115,7 +115,7 @@ describe("local emulator config restrictions", () => {
const fileContent = await fs.readFile(filePath, "utf-8");
expect(fileContent).toMatchInlineSnapshot(`
deindent\`
import type { HexclaveConfig } from "@hexclave/js";
import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = {
"teams": {

View File

@ -461,7 +461,7 @@ describe("Stack CLI", () => {
expect(exitCode).toBe(0);
expect(stdout).toContain("Config written to");
const content = fs.readFileSync(configTsPath, "utf-8");
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/js";');
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/js/config";');
expect(content).toContain("export const config: HexclaveConfig");
});
@ -556,7 +556,7 @@ describe("Stack CLI", () => {
expect(stdout).toContain("Config file written to");
const content = fs.readFileSync(path.join(initDir, "stack.config.ts"), "utf-8");
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/js";');
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/js/config";');
expect(content).toContain("export const config: HexclaveConfig");
expect(JSON.parse(extractConfigObjectString(content))).toMatchObject({
apps: {

View File

@ -1,7 +1,7 @@
{
"name": "@hexclave/hosted-components",
"private": true,
"version": "1.0.10",
"version": "1.0.11",
"type": "module",
"scripts": {
"dev": "vite dev --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09",

View File

@ -1,6 +1,10 @@
/// <reference types="vite/client" />
import { StackClientApp, StackProvider, StackTheme } from '@hexclave/react';
import { HexclaveClientApp, HexclaveProvider, HexclaveTheme } from '@hexclave/react';
import { publishableClientKeyNotNecessarySentinel } from '@hexclave/shared/dist/utils/oauth';
import { runAsynchronously } from '@hexclave/shared/dist/utils/promises';
import { validateRedirectUrl } from '@hexclave/shared/dist/utils/redirect-urls';
import { isRelative } from '@hexclave/shared/dist/utils/urls';
import { throwErr } from '@hexclave/shared/dist/utils/errors';
import {
HeadContent,
Outlet,
@ -49,6 +53,27 @@ function getApiBaseUrlFromEnv(): string | undefined {
return import.meta.env.VITE_HEXCLAVE_API_URL ?? import.meta.env.VITE_STACK_API_URL ?? undefined;
}
function isTrustedNavigationTarget(to: string): boolean {
return isRelative(to) || validateRedirectUrl(to, { trustedDomains: [window.location.origin] });
}
function useHostedComponentsNavigate() {
const navigate = useNavigate();
return useMemo(() => (to: string) => {
runAsynchronously(async () => {
if (to.startsWith("#")) {
await navigate({ hash: to.slice(1) });
} else {
if (!isTrustedNavigationTarget(to)) {
throw new Error("Refusing to navigate to an untrusted URL");
}
await navigate({ href: to });
}
});
}, [navigate]);
}
function FullPageError({ title, message }: { title: string, message: string }) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
@ -142,7 +167,7 @@ function RootComponent() {
const hexclaveApp = useMemo(() => {
if (!projectId || !isValidProjectId) return null;
return new StackClientApp({
return new HexclaveClientApp({
projectId,
publishableClientKey: publishableClientKeyNotNecessarySentinel,
tokenStore: "cookie",
@ -155,7 +180,7 @@ function RootComponent() {
afterSignUp: "/",
afterSignOut: "/handler/sign-in",
},
redirectMethod: { useNavigate: useNavigate as any }
redirectMethod: { useNavigate: useHostedComponentsNavigate },
});
}, [isValidProjectId, projectId]);
@ -171,13 +196,15 @@ function RootComponent() {
return <FullPageError title="Something went wrong" message={`Invalid project ID: ${projectId}. Project IDs must be UUIDs.`} />;
}
const app = hexclaveApp ?? throwErr("RootComponent expected a HexclaveClientApp after project ID validation.");
return (
<ErrorBoundary>
<StackProvider app={hexclaveApp!}>
<StackTheme>
<HexclaveProvider app={app}>
<HexclaveTheme>
<Outlet />
</StackTheme>
</StackProvider>
</HexclaveTheme>
</HexclaveProvider>
</ErrorBoundary>
);
}

View File

@ -1,7 +1,7 @@
{
"name": "@hexclave/internal-tool",
"private": true,
"version": "1.0.10",
"version": "1.0.11",
"type": "module",
"scripts": {
"dev": "node scripts/pre-dev.mjs && next dev --turbopack --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}41",

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/mcp",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"type": "module",

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/mock-oauth-server",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"main": "index.js",

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/skills",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"type": "module",

View File

@ -104,6 +104,23 @@ export const hexclaveClientApp = new HexclaveClientApp({
`maskAllInputs` defaults to `true`, so form fields are masked unless you explicitly disable it.
### Disabling Analytics Capture in the SDK
SDK-managed analytics capture is enabled by default. If you don't want the SDK to collect any analytics, pass `analytics: { enabled: false }` when creating your client app:
```ts
import { HexclaveClientApp } from "@hexclave/next";
export const hexclaveClientApp = new HexclaveClientApp({
projectId: process.env.NEXT_PUBLIC_STACK_PROJECT_ID!,
publishableClientKey: process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY!,
tokenStore: "nextjs-cookie",
analytics: { enabled: false },
});
```
This stops the SDK from sending `$page-view` and `$click` events. It also resolves the `ANALYTICS_NOT_ENABLED` warning the SDK logs to the browser console when it tries to send events to a project that hasn't enabled the Analytics app — with capture disabled, the SDK never makes those requests. If you'd rather keep analytics, enable the Analytics app in your dashboard (**Apps -> Analytics**) instead.
## Best Practices
1. **Use Tables for quick incident triage**: the Tables UI is the fastest way to inspect recent `events` rows without writing SQL.

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,7 @@ sidebarTitle: "hexclave.config.ts"
The file exports a static `config` object:
```ts title="hexclave.config.ts"
import type { HexclaveConfig } from "@hexclave/js";
import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = {
auth: {
@ -28,6 +28,22 @@ export const config: HexclaveConfig = {
};
```
<Note>
Always import config helpers from the package's lightweight `/config` entrypoint (e.g. `@hexclave/js/config`, `@hexclave/next/config`) rather than the package root. The `/config` entrypoint contains no framework runtime code, so tooling such as the local dashboard can load your config file in a plain Node context. Importing `defineHexclaveConfig` (or the `HexclaveConfig` type) from the package root instead would pull in the entire SDK and fail to load.
</Note>
To get type-checking and editor autocomplete for your config object, wrap it with `defineHexclaveConfig`:
```ts title="hexclave.config.ts"
import { defineHexclaveConfig } from "@hexclave/js/config";
export const config = defineHexclaveConfig({
auth: {
allowSignUp: true,
},
});
```
If you are running Hexclave with a [local dashboard](/guides/going-further/local-vs-cloud-dashboard), you already have a `hexclave.config.ts` file, and any changes you make on the dashboard will automatically be synced to the config file.
If you are running Hexclave on a [cloud project](/guides/going-further/local-vs-cloud-dashboard) instead, you may need to use the [CLI's `pull` and `push`](/guides/going-further/cli#config-commands) commands to sync your config file with the cloud. In production, you would usually do this in your GitHub Actions or CI/CD pipeline.

View File

@ -25,7 +25,7 @@ Use a development environment when you want to:
The usual setup looks like this:
```ts title="hexclave.config.ts"
import type { HexclaveConfig } from "@hexclave/js";
import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = "show-onboarding";
```

View File

@ -184,6 +184,10 @@ The frameworks and languages with explicit SDK support are:
});
```
<Note>
The SDK auto-captures page-view and click analytics. To turn this off (and silence the `ANALYTICS_NOT_ENABLED` console warning that appears until you enable the Analytics app in your dashboard), pass `analytics: { enabled: false }`.
</Note>
In a backend where you can keep a secret key safe, you can use the `HexclaveServerApp`, which provides access to more sensitive APIs compared to `HexclaveClientApp`:
```ts src/hexclave/server.ts
@ -227,12 +231,14 @@ The frameworks and languages with explicit SDK support are:
First, create a `hexclave.config.ts` configuration file in the root directory of the workspace (or anywhere else):
```ts hexclave.config.ts
import type { HexclaveConfig } from "<the-sdk-from-above>";
import type { HexclaveConfig } from "<the-sdk-from-above>/config";
// default: show-onboarding, which shows the onboarding flow for this project when Hexclave starts
export const config: HexclaveConfig = "show-onboarding";
```
The `/config` entrypoint is lightweight and free of framework runtime code, so it can be safely loaded by tooling such as the local dashboard. If you later switch to a config object and want type-checking, wrap it with `defineHexclaveConfig` imported from the same `<the-sdk-from-above>/config` path (never from `<the-sdk-from-above>` directly, which would pull in the whole SDK and fail to load).
To run your application with Hexclave, you can then start the dev environment and set environment variables expected by your application. Hexclave's CLI has a `dev` command does both of these, so let's install it as a dev dependency and wrap your existing `dev` script in your package.json:
```sh
@ -778,11 +784,13 @@ This setup is for Python backends that do not use the JavaScript SDK. The backen
If this project already has a `hexclave.config.ts` file for another frontend or backend, reuse that same file so the whole project shares one Hexclave config. Otherwise, create a new `hexclave.config.ts` file in your workspace:
```ts hexclave.config.ts
import type { HexclaveConfig } from "@hexclave/js";
import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = "show-onboarding";
```
The `/config` entrypoint is lightweight and free of framework runtime code, so it can be safely loaded by tooling such as the local dashboard. If you later switch to a config object and want type-checking, wrap it with `defineHexclaveConfig` imported from the same `@hexclave/js/config` path (never from `@hexclave/js` directly, which would pull in the whole SDK and fail to load).
Run your backend through the Hexclave CLI so it starts the local dashboard and injects the Hexclave environment variables:
```json package.json
@ -927,11 +935,13 @@ Use this option when your backend is not JavaScript/TypeScript or Python, or whe
If this project already has a `hexclave.config.ts` file for another frontend or backend, reuse that same file so the whole project shares one Hexclave config. Otherwise, create a new `hexclave.config.ts` file in your workspace:
```ts hexclave.config.ts
import type { HexclaveConfig } from "@hexclave/js";
import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = "show-onboarding";
```
The `/config` entrypoint is lightweight and free of framework runtime code, so it can be safely loaded by tooling such as the local dashboard. If you later switch to a config object and want type-checking, wrap it with `defineHexclaveConfig` imported from the same `@hexclave/js/config` path (never from `@hexclave/js` directly, which would pull in the whole SDK and fail to load).
Run your backend through the Hexclave CLI so it starts the local dashboard and injects the Hexclave environment variables:
```json package.json

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/docs-mintlify",
"version": "1.0.10",
"version": "1.0.11",
"private": true,
"scripts": {
"dev": "mint dev --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}04 --no-open",

View File

@ -86,6 +86,10 @@ If you're building a client-only app and don't have a `SECRET_SERVER_KEY`, you c
Redirect URL configuration.
</ParamField>
<ParamField body="analytics" type="object">
Analytics capture configuration. SDK-managed capture is enabled by default; pass `{ enabled: false }` to disable it entirely (which also avoids the `ANALYTICS_NOT_ENABLED` console warning on projects that haven't enabled the Analytics app), or `{ replays: { enabled: true } }` to record session replays.
</ParamField>
<ParamField body="noAutomaticPrefetch" type="boolean">
Disable automatic prefetching.
</ParamField>
@ -106,6 +110,7 @@ If you're building a client-only app and don't have a `SECRET_SERVER_KEY`, you c
projectId?: string;
publishableClientKey?: string;
urls?: object;
analytics?: object;
noAutomaticPrefetch?: boolean;
});
}

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/docs",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"description": "",
"main": "index.js",

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/example-cjs-test",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/convex-example",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/example-demo-app",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"description": "",
"private": true,

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/docs-examples",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"description": "",
"private": true,

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/e-commerce-demo",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/js-example",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"description": "",

View File

@ -1,7 +1,7 @@
{
"name": "@hexclave/lovable-react-18-example",
"private": true,
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"type": "module",
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/example-middleware-demo",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"scripts": {

View File

@ -1,7 +1,7 @@
{
"name": "react-example",
"private": true,
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"type": "module",
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/example-supabase",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/example-tanstack-start-demo",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"description": "TanStack Start demo app for Hexclave",
"private": true,

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/cli",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"description": "The CLI for Hexclave. https://hexclave.com",
"main": "dist/index.js",

View File

@ -278,7 +278,10 @@ export function registerConfigCommand(program: Command) {
const config = parseConfigOverride(configModule.config);
if (config == null) {
const examplePkg = detectImportPackageFromDir(path.dirname(filePath)) ?? "@hexclave/js";
throw new CliError(`Config file must export a plain \`config\` object or "show-onboarding". Example: import type { StackConfig } from "${examplePkg}"; export const config: StackConfig = { ... };`);
// The lightweight `/config` entrypoint only exists on Hexclave-branded packages;
// legacy `@stackframe/*` releases predate it, so import from their root.
const exampleImport = examplePkg.startsWith("@hexclave/") ? `${examplePkg}/config` : examplePkg;
throw new CliError(`Config file must export a plain \`config\` object or "show-onboarding". Example: import type { HexclaveConfig } from "${exampleImport}"; export const config: HexclaveConfig = { ... };`);
}
const source = buildConfigPushSource(opts.configFile, {

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/dashboard-ui-components",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",

View File

@ -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.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"sideEffects": false,
"main": "./dist/index.js",
@ -16,6 +16,15 @@
"default": "./dist/index.js"
}
},
"./config": {
"types": "./dist/config.d.ts",
"import": {
"default": "./dist/esm/config.js"
},
"require": {
"default": "./dist/config.js"
}
},
"./convex.config": {
"types": "./dist/integrations/convex/component/convex.config.d.ts",
"import": {

View File

@ -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.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"sideEffects": false,
"main": "./dist/index.js",
@ -16,6 +16,15 @@
"default": "./dist/index.js"
}
},
"./config": {
"types": "./dist/config.d.ts",
"import": {
"default": "./dist/esm/config.js"
},
"require": {
"default": "./dist/config.js"
}
},
"./convex.config": {
"types": "./dist/integrations/convex/component/convex.config.d.ts",
"import": {

View File

@ -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.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"sideEffects": false,
"main": "./dist/index.js",
@ -16,6 +16,15 @@
"default": "./dist/index.js"
}
},
"./config": {
"types": "./dist/config.d.ts",
"import": {
"default": "./dist/esm/config.js"
},
"require": {
"default": "./dist/config.js"
}
},
"./convex.config": {
"types": "./dist/integrations/convex/component/convex.config.d.ts",
"import": {

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/sc",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"exports": {
"./force-react-server": {

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/shared",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"scripts": {
"build": "rimraf dist && tsdown",

View File

@ -442,11 +442,13 @@ function getRestBackendSetupPrompt(kind: "python" | "rest-api") {
If this project already has a \`hexclave.config.ts\` file for another frontend or backend, reuse that same file so the whole project shares one Hexclave config. Otherwise, create a new \`hexclave.config.ts\` file in your workspace:
\`\`\`ts hexclave.config.ts
import type { HexclaveConfig } from "@hexclave/js";
import type { HexclaveConfig } from "@hexclave/js/config";
export const config: HexclaveConfig = "show-onboarding";
\`\`\`
The \`/config\` entrypoint is lightweight and free of framework runtime code, so it can be safely loaded by tooling such as the local dashboard. If you later switch to a config object and want type-checking, wrap it with \`defineHexclaveConfig\` imported from the same \`@hexclave/js/config\` path (never from \`@hexclave/js\` directly, which would pull in the whole SDK and fail to load).
Run your backend through the Hexclave CLI so it starts the local dashboard and injects the Hexclave environment variables:
\`\`\`json package.json
@ -662,6 +664,10 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
},
});
\`\`\`
<Note>
The SDK auto-captures page-view and click analytics. To turn this off (and silence the \`ANALYTICS_NOT_ENABLED\` console warning that appears until you enable the Analytics app in your dashboard), pass \`analytics: { enabled: false }\`.
</Note>
` : ""}
${isMaybeBackend ? deindent`
@ -719,12 +725,14 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
First, create a \`hexclave.config.ts\` configuration file in the root directory of the workspace (or anywhere else):
\`\`\`ts hexclave.config.ts
import type { HexclaveConfig } from "${packageName}";
import type { HexclaveConfig } from "${packageName}/config";
// default: show-onboarding, which shows the onboarding flow for this project when Hexclave starts
export const config: HexclaveConfig = "show-onboarding";
\`\`\`
The \`/config\` entrypoint is lightweight and free of framework runtime code, so it can be safely loaded by tooling such as the local dashboard. If you later switch to a config object and want type-checking, wrap it with \`defineHexclaveConfig\` imported from the same \`${packageName}/config\` path (never from \`${packageName}\` directly, which would pull in the whole SDK and fail to load).
To run your application with Hexclave, you can then start the dev environment and set environment variables expected by your application. Hexclave's CLI has a \`dev\` command does both of these, so let's install it as a dev dependency and wrap your existing \`dev\` script in your package.json:
\`\`\`sh

View File

@ -13,6 +13,7 @@ export { parseHexclaveConfigFileContent, renderConfigFileContent };
const CONFIG_IMPORT_PACKAGES = [
"@hexclave/next",
"@hexclave/react",
"@hexclave/tanstack-start",
"@hexclave/js",
"@hexclave/template",
"@stackframe/stack",
@ -120,18 +121,26 @@ import.meta.vitest?.test("renderConfigFileContent rejects invalid config exports
import.meta.vitest?.test("renderConfigFileContent uses custom import package", ({ expect }) => {
const content = renderConfigFileContent({}, "@hexclave/next");
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/next";');
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/next/config";');
});
import.meta.vitest?.test("renderConfigFileContent defaults to @hexclave/js", ({ expect }) => {
const content = renderConfigFileContent({});
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/js";');
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/js/config";');
});
import.meta.vitest?.test("renderConfigFileContent keeps legacy @stackframe packages on their root entrypoint", ({ expect }) => {
// The lightweight `/config` subpath only exists on Hexclave-branded packages;
// already-published @stackframe/* releases predate it.
const content = renderConfigFileContent({}, "@stackframe/next");
expect(content).toContain('import type { HexclaveConfig } from "@stackframe/next";');
});
import.meta.vitest?.test("detectConfigImportPackage picks first matching package by priority", ({ expect }) => {
expect(detectConfigImportPackage(["@hexclave/next", "@hexclave/js"])).toBe("@hexclave/next");
expect(detectConfigImportPackage(["@hexclave/react", "@hexclave/js"])).toBe("@hexclave/react");
expect(detectConfigImportPackage(["@hexclave/js"])).toBe("@hexclave/js");
expect(detectConfigImportPackage(["@hexclave/tanstack-start"])).toBe("@hexclave/tanstack-start");
// Hexclave names take priority over legacy stackframe names when both appear.
expect(detectConfigImportPackage(["@stackframe/stack", "@hexclave/next"])).toBe("@hexclave/next");
// Legacy fallback still works for projects pinned to the last @stackframe/* release.

View File

@ -28,7 +28,13 @@ export function renderConfigFileContent(config: unknown, importPackage?: string)
throw new Error(`Config has conflicting keys that would be dropped during normalization: ${droppedKeys.map(k => JSON.stringify(k)).join(", ")}`);
}
const pkg = importPackage ?? DEFAULT_CONFIG_IMPORT_PACKAGE;
const importLine = `import type { HexclaveConfig } from "${pkg}";`;
// Import the `HexclaveConfig` type from the package's lightweight `/config`
// entrypoint, which is free of framework runtime code and therefore safe for
// tooling (e.g. the local dashboard) to load in a plain Node context. Only the
// Hexclave-branded packages expose this subpath; legacy `@stackframe/*`
// releases predate it, so fall back to their package root.
const importSpecifier = pkg.startsWith("@hexclave/") ? `${pkg}/config` : pkg;
const importLine = `import type { HexclaveConfig } from "${importSpecifier}";`;
return `${importLine}\n\nexport const config: HexclaveConfig = ${JSON.stringify(normalizedConfig, null, 2)};\n`;
}

View File

@ -0,0 +1,147 @@
import { describe, expect, it } from "vitest";
import { InternalSession } from "./sessions";
/**
* Builds a decodable (unsigned) access-token JWT with a valid payload. `refreshTokenId` controls the
* `refresh_token_id` claim (the session identifier); `iatOffsetSeconds` lets two tokens for the same session
* differ as strings while sharing a `refresh_token_id`.
*/
function createAccessTokenString(refreshTokenId: string, options?: { iatOffsetSeconds?: number, sub?: string }): string {
const encode = (value: unknown) => Buffer.from(JSON.stringify(value)).toString("base64url");
const nowSeconds = Math.floor(Date.now() / 1000) + (options?.iatOffsetSeconds ?? 0);
return [
encode({ alg: "none", typ: "JWT" }),
encode({
sub: options?.sub ?? "user-id",
exp: nowSeconds + 60,
iat: nowSeconds,
iss: "https://api.example.test",
aud: "project-id",
project_id: "project-id",
branch_id: "main",
refresh_token_id: refreshTokenId,
role: "authenticated",
name: null,
email: null,
email_verified: false,
selected_team_id: null,
signed_up_at: nowSeconds,
is_anonymous: false,
is_restricted: false,
restricted_reason: null,
requires_totp_mfa: false,
}),
"",
].join(".");
}
function createAccessOnlySession(accessToken: string): InternalSession {
return new InternalSession({
refreshAccessTokenCallback: async () => null,
refreshToken: null,
accessToken,
});
}
const currentToken = (session: InternalSession) => session.getAccessTokenIfNotExpiredYet(20_000, null)?.token;
describe("InternalSession.calculateSessionKey", () => {
it("keys by the refresh token when one is present (ignoring any access token)", () => {
expect(InternalSession.calculateSessionKey({ refreshToken: "rt-abc" })).toBe("refresh-rt-abc");
expect(InternalSession.calculateSessionKey({ refreshToken: "rt-abc", accessToken: createAccessTokenString("rtid-1") }))
.toBe("refresh-rt-abc");
});
it("returns not-logged-in when neither token is present", () => {
expect(InternalSession.calculateSessionKey({ refreshToken: null })).toBe("not-logged-in");
expect(InternalSession.calculateSessionKey({ refreshToken: null, accessToken: null })).toBe("not-logged-in");
});
it("keys an access-only session by its refresh_token_id", () => {
expect(InternalSession.calculateSessionKey({ refreshToken: null, accessToken: createAccessTokenString("rtid-1") }))
.toBe("access-session-rtid-1");
});
it("is stable across re-minted access tokens for the same session (the regression this fixes)", () => {
const first = createAccessTokenString("rtid-1", { iatOffsetSeconds: 0 });
const second = createAccessTokenString("rtid-1", { iatOffsetSeconds: 1 });
expect(second).not.toBe(first);
expect(InternalSession.calculateSessionKey({ refreshToken: null, accessToken: second }))
.toBe(InternalSession.calculateSessionKey({ refreshToken: null, accessToken: first }));
});
it("distinguishes access-only sessions with different refresh_token_ids", () => {
expect(InternalSession.calculateSessionKey({ refreshToken: null, accessToken: createAccessTokenString("rtid-1") }))
.not.toBe(InternalSession.calculateSessionKey({ refreshToken: null, accessToken: createAccessTokenString("rtid-2") }));
});
it("falls back to the raw token when the access token can't be decoded", () => {
expect(InternalSession.calculateSessionKey({ refreshToken: null, accessToken: "not-a-jwt" })).toBe("access-not-a-jwt");
});
});
describe("InternalSession#updateAccessToken", () => {
it("installs a fresh token for the same access-only session in place", () => {
const initial = createAccessTokenString("rtid-1", { iatOffsetSeconds: 0 });
const refreshed = createAccessTokenString("rtid-1", { iatOffsetSeconds: 1 });
const session = createAccessOnlySession(initial);
session.updateAccessToken({ accessToken: refreshed, refreshToken: null });
expect(currentToken(session)).toBe(refreshed);
// identity is unchanged — same session key, same object
expect(session.sessionKey).toBe("access-session-rtid-1");
});
it("rejects a token pair belonging to a different access-only session", () => {
const initial = createAccessTokenString("rtid-1");
const foreign = createAccessTokenString("rtid-2", { sub: "other-user" });
const session = createAccessOnlySession(initial);
session.updateAccessToken({ accessToken: foreign, refreshToken: null });
expect(currentToken(session)).toBe(initial);
});
it("is a no-op for an unchanged, null, or undecodable token", () => {
const initial = createAccessTokenString("rtid-1");
const session = createAccessOnlySession(initial);
session.updateAccessToken({ accessToken: initial, refreshToken: null });
session.updateAccessToken({ accessToken: null, refreshToken: null });
session.updateAccessToken({ accessToken: "not-a-jwt", refreshToken: null });
expect(currentToken(session)).toBe(initial);
});
it("never revives an invalidated session", () => {
const session = createAccessOnlySession(createAccessTokenString("rtid-1"));
session.markInvalid();
session.updateAccessToken({ accessToken: createAccessTokenString("rtid-1", { iatOffsetSeconds: 1 }), refreshToken: null });
expect(session.isKnownToBeInvalid()).toBe(true);
expect(currentToken(session)).toBeUndefined();
});
it("updates a refresh-token-backed session's access token in place when the refresh token matches", () => {
const session = new InternalSession({
refreshAccessTokenCallback: async () => null,
refreshToken: "rt-abc",
accessToken: createAccessTokenString("rtid-1"),
});
const refreshed = createAccessTokenString("rtid-2", { iatOffsetSeconds: 1 });
session.updateAccessToken({ accessToken: refreshed, refreshToken: "rt-abc" });
expect(currentToken(session)).toBe(refreshed);
expect(session.sessionKey).toBe("refresh-rt-abc");
});
it("rejects a token pair carrying a different refresh token for a refresh-backed session", () => {
const initial = createAccessTokenString("rtid-1");
const session = new InternalSession({
refreshAccessTokenCallback: async () => null,
refreshToken: "rt-abc",
accessToken: initial,
});
session.updateAccessToken({ accessToken: createAccessTokenString("rtid-2"), refreshToken: "rt-other" });
expect(currentToken(session)).toBe(initial);
});
});

View File

@ -124,6 +124,14 @@ export class InternalSession {
if (ofTokens.refreshToken) {
return `refresh-${ofTokens.refreshToken}`;
} else if (ofTokens.accessToken) {
// Access-only sessions (no refresh token) are keyed by the underlying session's `refresh_token_id`, not the
// access token string: access tokens get re-minted frequently, and keying by the raw token would spawn a new
// session (and cold-invalidate every session-scoped cache) on each refresh. Falls back to the raw token if
// the JWT can't be decoded.
const refreshTokenId = decodeAccessTokenIfValid(ofTokens.accessToken)?.refresh_token_id;
if (refreshTokenId) {
return `access-session-${refreshTokenId}`;
}
return `access-${ofTokens.accessToken}`;
} else {
return "not-logged-in";
@ -210,6 +218,24 @@ export class InternalSession {
return accessToken ? { accessToken, refreshToken: this._refreshToken } : null;
}
/**
* Installs a freshly obtained token pair's access token into this session in place, keeping the session object
* (and therefore every session-scoped cache) stable instead of constructing a new InternalSession. No-op if the
* session is invalid, the access token can't be decoded, it's unchanged, or the pair doesn't map to this session
* (so a foreign token can never be written into this object's cache); never clears an existing token.
*/
updateAccessToken(tokens: { accessToken: string | null, refreshToken: string | null }) {
if (this._knownToBeInvalid.get()) return;
if (!tokens.accessToken) return;
const newAccessToken = AccessToken.createIfValid(tokens.accessToken);
if (!newAccessToken) return;
// Self-enforce the "a session never changes which session it belongs to" invariant: only install a token pair
// that maps to this same session key (validated against the incoming pair, not this session's existing tokens).
if (InternalSession.calculateSessionKey(tokens) !== this.sessionKey) return;
if (this._accessToken.get()?.token === newAccessToken.token) return;
this._accessToken.set(newAccessToken);
}
/**
* Manually mark the access token as expired, even if the date on its payload may still be valid.
*

View File

@ -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.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"sideEffects": false,
"main": "./dist/index.js",
@ -16,6 +16,15 @@
"default": "./dist/index.js"
}
},
"./config": {
"types": "./dist/config.d.ts",
"import": {
"default": "./dist/esm/config.js"
},
"require": {
"default": "./dist/config.js"
}
},
"./tanstack-start-server-context": {
"types": "./dist/tanstack-start-server-context.combined.d.ts",
"import": {

View File

@ -13,7 +13,7 @@
"//": "NEXT_LINE_PLATFORM template",
"private": true,
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"sideEffects": false,
"main": "./dist/index.js",
@ -28,6 +28,15 @@
"default": "./dist/index.js"
}
},
"./config": {
"types": "./dist/config.d.ts",
"import": {
"default": "./dist/esm/config.js"
},
"require": {
"default": "./dist/config.js"
}
},
"//": "IF_PLATFORM tanstack-start",
"./tanstack-start-server-context": {
"types": "./dist/tanstack-start-server-context.combined.d.ts",

View File

@ -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.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"sideEffects": false,
"main": "./dist/index.js",
@ -17,6 +17,15 @@
"default": "./dist/index.js"
}
},
"./config": {
"types": "./dist/config.d.ts",
"import": {
"default": "./dist/esm/config.js"
},
"require": {
"default": "./dist/config.js"
}
},
"./tanstack-start-server-context": {
"types": "./dist/tanstack-start-server-context.combined.d.ts",
"import": {

View File

@ -0,0 +1,13 @@
// Lightweight, side-effect-free entrypoint for authoring `hexclave.config.ts`
// files. Importing from here (e.g. `@hexclave/next/config`) gives you the
// `defineHexclaveConfig` helper and config types WITHOUT pulling in the
// framework runtime (React, server-only, Next.js internals). That matters
// because tooling such as the local dashboard evaluates your config file in a
// plain Node context — importing `defineHexclaveConfig` from the package root
// would drag in the whole SDK and fail to load.
//
// Hexclave aliases and legacy Stack* names — @deprecated JSDoc lives on the
// original declarations in @hexclave/shared/config so it survives dts bundling
// (per-specifier JSDoc on re-exports does not).
export type { HexclaveConfig, StackConfig } from "@hexclave/shared/config";
export { defineHexclaveConfig, defineStackConfig, showOnboardingHexclaveConfigValue } from "@hexclave/shared/config";

View File

@ -1547,10 +1547,14 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
const tokenStore = this._getOrCreateTokenStore(await this._createCookieHelper());
tokenStore.set(tokens);
// Pre-fetch the current user for the new session so the cache is already
// populated when useUser() re-renders, avoiding a stale-cache render cycle.
const newSession = this._getSessionFromTokenStore(tokenStore);
this._currentUserCache.getOrWait([newSession], "write-only").catch(() => {});
// If these tokens resolve to a session we already have (eg. the RDE dashboard re-installing a freshly minted
// access token for the same access-only session), push the new token into it in place; constructing a new
// session here would cold-invalidate every session-scoped cache and suspend the UI on each refresh.
const session = this._getSessionFromTokenStore(tokenStore);
session.updateAccessToken(tokens);
// Pre-fetch the current user so the cache is warm when useUser() re-renders (write-only, so it never suspends).
runAsynchronously(this._currentUserCache.getOrWait([session], "write-only"));
}
protected _getTokenStoreInitForFreshTokens(tokens: { accessToken: string | null, refreshToken: string }): TokenStoreInit | undefined {

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/ui",
"version": "1.0.10",
"version": "1.0.11",
"repository": "https://github.com/hexclave/hexclave",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/swift-sdk",
"version": "1.0.10",
"version": "1.0.11",
"private": true,
"description": "Hexclave Swift SDK",
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/sdk-spec",
"version": "1.0.10",
"version": "1.0.11",
"private": true,
"description": "Hexclave SDK specification files",
"scripts": {}