mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Merge branch 'dev' into cl/investigate-dnsimple-zone-delete-error
This commit is contained in:
commit
6ecb6a1ff8
30
.github/workflows/qemu-emulator-build.yaml
vendored
30
.github/workflows/qemu-emulator-build.yaml
vendored
@ -64,17 +64,14 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
|
||||
# Node/pnpm are needed on both arches: arm64 also runs
|
||||
# generate-env-development.mjs inside build-image.sh. amd64 additionally
|
||||
# builds and runs the CLI for the verification steps below.
|
||||
# Node is needed on both arches for generate-env-development.mjs.
|
||||
# pnpm is needed for build-image.sh (runs pnpm install inside Docker
|
||||
# context via the lockfile).
|
||||
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
||||
with:
|
||||
version: 10.23.0
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
@ -145,17 +142,6 @@ jobs:
|
||||
# image to verify it works end-to-end before publishing. arm64 runs
|
||||
# under cross-arch TCG on an amd64 host, which can't reliably boot
|
||||
# Next.js within any sane window — skipped.
|
||||
- name: Build stack-cli (for emulator CLI)
|
||||
if: matrix.arch == 'amd64'
|
||||
run: |
|
||||
# Turbo's task graph for stack-cli#build includes
|
||||
# @hexclave/dashboard#build:rde-standalone, which transitively
|
||||
# depends on @hexclave/next#build (via dashboard → stack).
|
||||
# The pnpm filter must cover the dashboard dep tree too so that
|
||||
# devDependencies like tailwindcss are installed for the build.
|
||||
pnpm install --frozen-lockfile --filter '@hexclave/cli...' --filter '@hexclave/dashboard...'
|
||||
pnpm exec turbo run build --filter='@hexclave/cli...'
|
||||
|
||||
- name: Start emulator and verify
|
||||
if: matrix.arch == 'amd64'
|
||||
env:
|
||||
@ -163,7 +149,9 @@ jobs:
|
||||
EMULATOR_READY_TIMEOUT: 3200
|
||||
EMULATOR_IMAGE_DIR: ${{ env.EMULATOR_IMAGE_DIR }}
|
||||
EMULATOR_RUN_DIR: ${{ env.EMULATOR_RUN_DIR }}
|
||||
run: node packages/stack-cli/dist/index.js emulator start
|
||||
run: |
|
||||
chmod +x docker/local-emulator/qemu/run-emulator.sh
|
||||
docker/local-emulator/qemu/run-emulator.sh start
|
||||
|
||||
- name: Verify services are healthy
|
||||
if: matrix.arch == 'amd64'
|
||||
@ -171,7 +159,7 @@ jobs:
|
||||
EMULATOR_ARCH: ${{ matrix.arch }}
|
||||
EMULATOR_IMAGE_DIR: ${{ env.EMULATOR_IMAGE_DIR }}
|
||||
EMULATOR_RUN_DIR: ${{ env.EMULATOR_RUN_DIR }}
|
||||
run: node packages/stack-cli/dist/index.js emulator status
|
||||
run: docker/local-emulator/qemu/run-emulator.sh status
|
||||
|
||||
- name: Stop emulator
|
||||
if: always() && matrix.arch == 'amd64'
|
||||
@ -179,7 +167,7 @@ jobs:
|
||||
EMULATOR_ARCH: ${{ matrix.arch }}
|
||||
EMULATOR_IMAGE_DIR: ${{ env.EMULATOR_IMAGE_DIR }}
|
||||
EMULATOR_RUN_DIR: ${{ env.EMULATOR_RUN_DIR }}
|
||||
run: node packages/stack-cli/dist/index.js emulator stop
|
||||
run: docker/local-emulator/qemu/run-emulator.sh stop
|
||||
|
||||
- name: Package image
|
||||
run: |
|
||||
@ -259,8 +247,6 @@ jobs:
|
||||
fi
|
||||
|
||||
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
||||
with:
|
||||
version: 10.23.0
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
|
||||
@ -948,12 +948,12 @@ export default function MetricsPage(props: { toSetup: () => void }) {
|
||||
const [customDateRange, setCustomDateRange] = useState<CustomDateRange | null>(null);
|
||||
const user = useUser();
|
||||
|
||||
const displayName = user?.displayName || user?.primaryEmail || "User";
|
||||
const truncatedName = displayName.length > 30 ? `${displayName.slice(0, 30)}...` : displayName;
|
||||
const displayName = user?.displayName || user?.primaryEmail || null;
|
||||
const truncatedName = displayName && displayName.length > 30 ? `${displayName.slice(0, 30)}...` : displayName;
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
title={`Welcome back, ${truncatedName}!`}
|
||||
title={`Welcome back${truncatedName ? `, ${truncatedName}` : ""}!`}
|
||||
actions={
|
||||
<div className="flex items-center gap-2 sm:gap-3 flex-shrink-0">
|
||||
<TimeRangeToggle
|
||||
|
||||
@ -131,7 +131,7 @@ function DashboardDetailContent({
|
||||
const [pendingCode, setPendingCode] = useState<string | null>(null);
|
||||
const [iframeReady, setIframeReady] = useState(hasSource);
|
||||
const [codePhase, setCodePhase] = useState<"typing" | "loading" | "done">("done");
|
||||
const codePhaseTimerRef = useRef<ReturnType<typeof setTimeout>>(null);
|
||||
const codePhaseTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const hasUnsavedChanges = currentTsxSource !== savedTsxSource;
|
||||
const { setNeedConfirm } = useRouterConfirm();
|
||||
const { toast } = useToast();
|
||||
|
||||
@ -81,7 +81,7 @@ async function localEmulatorIsHealthy(): Promise<boolean> {
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
if (!requestHostIsLoopback(req) || !originIsAllowed(req)) {
|
||||
return NextResponse.json({ error: "Development environment health checks only accept loopback requests." }, { status: 403 });
|
||||
return NextResponse.json({ error: "You're accessing the development environment using an unsupported address (such as 'localhost'). Please go to http://127.0.0.1:26700 instead — copy and paste this address into your browser." }, { status: 403 });
|
||||
}
|
||||
|
||||
const isRemoteDevelopmentEnvironment = getPublicEnvVar("NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT") === "true";
|
||||
|
||||
@ -16,12 +16,14 @@ import { DevelopmentPortDisplay } from "./development-port-display";
|
||||
import Loading from "./loading";
|
||||
import { UserIdentity } from "./providers";
|
||||
import { RemoteDevelopmentEnvironmentAuthGate } from "./remote-development-environment-auth-gate";
|
||||
import { WrongAddressScreen } from "./wrong-address-screen";
|
||||
|
||||
const DEV_ENVIRONMENT_HEALTHCHECK_INTERVAL_MS = 2_000;
|
||||
|
||||
type DevEnvironmentHealthSnapshot =
|
||||
| { status: "checking" | "healthy" }
|
||||
| { status: "unhealthy", restartCommand: string };
|
||||
| { status: "unhealthy", restartCommand: string }
|
||||
| { status: "wrong_address", suggestedUrl: string };
|
||||
|
||||
const CHECKING_DEV_ENVIRONMENT_HEALTH_SNAPSHOT: DevEnvironmentHealthSnapshot = { status: "checking" };
|
||||
const HEALTHY_DEV_ENVIRONMENT_HEALTH_SNAPSHOT: DevEnvironmentHealthSnapshot = { status: "healthy" };
|
||||
@ -65,6 +67,18 @@ async function refreshDevEnvironmentHealth() {
|
||||
},
|
||||
});
|
||||
const body: unknown = await response.json();
|
||||
|
||||
// If the health endpoint returns a 403, the user is likely accessing via
|
||||
// an unsupported address (e.g. localhost instead of 127.0.0.1). Extract
|
||||
// the suggested URL from the error and show a dedicated screen.
|
||||
if (response.status === 403 && body != null && typeof body === "object" && "error" in body && typeof body.error === "string") {
|
||||
const match = body.error.match(/http:\/\/127\.0\.0\.1(?::\d+)?/);
|
||||
if (match != null) {
|
||||
setSnapshotIfCurrent({ status: "wrong_address", suggestedUrl: match[0] });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDevEnvironmentHealthResponse(body)) {
|
||||
throw new Error("Development environment health endpoint returned an invalid response.");
|
||||
}
|
||||
@ -149,6 +163,10 @@ function DevEnvironmentHealthGate(props: { children: React.ReactNode }) {
|
||||
return props.children;
|
||||
}
|
||||
|
||||
if (health.status === "wrong_address") {
|
||||
return <WrongAddressScreen suggestedUrl={health.suggestedUrl} />;
|
||||
}
|
||||
|
||||
if (health.status === "unhealthy") {
|
||||
return <DevEnvironmentStoppedScreen restartCommand={health.restartCommand} />;
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import { stackAppInternalsSymbol } from "@/lib/stack-app-internals";
|
||||
import { useStackApp } from "@hexclave/next";
|
||||
import { runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises";
|
||||
import { useEffect, useState } from "react";
|
||||
import { WrongAddressScreen } from "./wrong-address-screen";
|
||||
|
||||
const RDE_ACCESS_TOKEN_MIN_EXPIRATION_MS = 30_000;
|
||||
const RDE_ACCESS_TOKEN_MAX_AGE_MS = 60_000;
|
||||
@ -87,6 +88,17 @@ function shouldRefreshAccessToken(token: RemoteDevelopmentEnvironmentAccessToken
|
||||
);
|
||||
}
|
||||
|
||||
class LoopbackAddressError extends Error {
|
||||
constructor(message: string, public readonly suggestedUrl: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
function extractLoopbackSuggestedUrl(errorMessage: string): string | null {
|
||||
const match = errorMessage.match(/http:\/\/127\.0\.0\.1(?::\d+)?/);
|
||||
return match?.[0] ?? null;
|
||||
}
|
||||
|
||||
async function getRemoteDevelopmentEnvironmentAccessToken(): Promise<RemoteDevelopmentEnvironmentAccessTokenResponse> {
|
||||
const response = await fetch("/api/remote-development-environment/auth", {
|
||||
headers: {
|
||||
@ -94,7 +106,24 @@ async function getRemoteDevelopmentEnvironmentAccessToken(): Promise<RemoteDevel
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to authenticate local remote development environment dashboard (${response.status}): ${await response.text()}`);
|
||||
const responseText = await response.text();
|
||||
// For 403 errors (e.g. accessing via 'localhost' instead of 127.0.0.1),
|
||||
// throw a LoopbackAddressError so the auth gate can show a dedicated screen
|
||||
if (response.status === 403) {
|
||||
try {
|
||||
const errorMessage = JSON.parse(responseText)?.error;
|
||||
if (typeof errorMessage === "string") {
|
||||
const suggestedUrl = extractLoopbackSuggestedUrl(errorMessage);
|
||||
if (suggestedUrl != null) {
|
||||
throw new LoopbackAddressError(errorMessage, suggestedUrl);
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!(e instanceof SyntaxError)) throw e;
|
||||
}
|
||||
}
|
||||
throw new Error(`Failed to authenticate local remote development environment dashboard (${response.status}): ${responseText}`);
|
||||
}
|
||||
|
||||
return parseRemoteDevelopmentEnvironmentAccessTokenResponse(await response.json());
|
||||
@ -112,6 +141,7 @@ async function installRemoteDevelopmentEnvironmentAccessToken(app: unknown): Pro
|
||||
function RemoteDevelopmentEnvironmentAuthGateInner(props: { children: React.ReactNode }) {
|
||||
const app = useStackApp();
|
||||
const [accessTokenInstalled, setAccessTokenInstalled] = useState(false);
|
||||
const [loopbackError, setLoopbackError] = useState<{ suggestedUrl: string } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@ -120,21 +150,31 @@ function RemoteDevelopmentEnvironmentAuthGateInner(props: { children: React.Reac
|
||||
let currentToken: RemoteDevelopmentEnvironmentAccessTokenResponse | undefined;
|
||||
|
||||
const refreshAccessToken = async (): Promise<void> => {
|
||||
const token = await installRemoteDevelopmentEnvironmentAccessToken(app);
|
||||
const currentUser = await app.getUser({
|
||||
or: "anonymous-if-exists[deprecated]",
|
||||
});
|
||||
if (currentUser?.id !== token.userId) {
|
||||
throw new Error("Installed remote development environment token did not match the expected anonymous user.");
|
||||
}
|
||||
if (cancelled) return;
|
||||
currentToken = token;
|
||||
setAccessTokenInstalled(true);
|
||||
try {
|
||||
const token = await installRemoteDevelopmentEnvironmentAccessToken(app);
|
||||
const currentUser = await app.getUser({
|
||||
or: "anonymous-if-exists[deprecated]",
|
||||
});
|
||||
if (currentUser?.id !== token.userId) {
|
||||
throw new Error("Installed remote development environment token did not match the expected anonymous user.");
|
||||
}
|
||||
if (cancelled) return;
|
||||
currentToken = token;
|
||||
setAccessTokenInstalled(true);
|
||||
|
||||
refreshTimeout = setTimeout(() => {
|
||||
refreshPromise = undefined;
|
||||
requestRefresh();
|
||||
}, getRefreshInMillis(token));
|
||||
refreshTimeout = setTimeout(() => {
|
||||
refreshPromise = undefined;
|
||||
requestRefresh();
|
||||
}, getRefreshInMillis(token));
|
||||
} catch (e) {
|
||||
if (e instanceof LoopbackAddressError) {
|
||||
if (!cancelled) {
|
||||
setLoopbackError({ suggestedUrl: e.suggestedUrl });
|
||||
}
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const requestRefresh = (options?: { force?: boolean }) => {
|
||||
@ -170,6 +210,10 @@ function RemoteDevelopmentEnvironmentAuthGateInner(props: { children: React.Reac
|
||||
};
|
||||
}, [app]);
|
||||
|
||||
if (loopbackError != null) {
|
||||
return <WrongAddressScreen suggestedUrl={loopbackError.suggestedUrl} />;
|
||||
}
|
||||
|
||||
if (!accessTokenInstalled) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
28
apps/dashboard/src/app/wrong-address-screen.tsx
Normal file
28
apps/dashboard/src/app/wrong-address-screen.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
export function WrongAddressScreen(props: { suggestedUrl: string }) {
|
||||
return (
|
||||
<div className="relative z-10 min-h-screen bg-background text-foreground flex items-center justify-center px-4">
|
||||
<div className="w-full max-w-lg rounded-2xl border border-black/[0.10] dark:border-white/[0.10] bg-white dark:bg-background p-6 shadow-sm">
|
||||
<div className="mb-3 inline-flex rounded-full bg-amber-500/10 px-3 py-1 text-xs font-medium text-amber-700 dark:text-amber-300">
|
||||
Wrong address
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Use a different address to access this page</h1>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">
|
||||
{"You're accessing the development environment using an address that isn't supported (such as "}
|
||||
<code className="rounded bg-black/[0.04] dark:bg-white/[0.06] px-1 py-0.5 text-xs">localhost</code>
|
||||
{")."}
|
||||
</p>
|
||||
<p className="mt-4 text-sm leading-6 text-muted-foreground">
|
||||
Please open this link instead:
|
||||
</p>
|
||||
<a
|
||||
href={props.suggestedUrl}
|
||||
className="mt-3 block overflow-x-auto rounded-lg bg-black/[0.04] dark:bg-white/[0.06] px-3 py-2 text-sm font-medium text-blue-700 dark:text-blue-300 hover:underline"
|
||||
>
|
||||
{props.suggestedUrl}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
import type { JSX } from 'react';
|
||||
|
||||
type IconProps = { iconSize: number };
|
||||
|
||||
export function Card({ iconSize }: IconProps) {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { JSX } from "react";
|
||||
import { Link } from "@/components/link";
|
||||
import { ChartLineIcon, ChatCircleDotsIcon, ClipboardTextIcon, CodeIcon, CreditCardIcon, EnvelopeSimpleIcon, FingerprintSimpleIcon, KeyIcon, MailboxIcon, MonitorPlayIcon, RocketIcon, ShieldCheckIcon, SparkleIcon, TelevisionSimpleIcon, TriangleIcon, UserGearIcon, UsersIcon, VaultIcon, WebhooksLogoIcon } from "@phosphor-icons/react";
|
||||
import { StackAdminApp } from "@hexclave/next";
|
||||
|
||||
@ -17,6 +17,13 @@ function requestHostIsLoopback(req: NextRequest): boolean {
|
||||
return isLocalhost(`http://${host}`);
|
||||
}
|
||||
|
||||
function loopbackRejectionMessage(req: NextRequest, state: RemoteDevelopmentEnvironmentState): string {
|
||||
const dashboards = localDashboards(state);
|
||||
const port = requestHostPort(req) ?? (dashboards.length > 0 ? dashboards[0].port : null);
|
||||
const suggestedUrl = port != null ? `http://127.0.0.1:${port}` : "http://127.0.0.1:<port>";
|
||||
return `You're accessing the development environment using an unsupported address (such as 'localhost'). Please go to ${suggestedUrl} instead — copy and paste this address into your browser.`;
|
||||
}
|
||||
|
||||
function requestHostUrl(req: NextRequest): URL | null {
|
||||
const host = req.headers.get("host");
|
||||
if (host == null) return null;
|
||||
@ -74,7 +81,7 @@ export function assertRemoteDevelopmentEnvironmentRequest(req: NextRequest): Nex
|
||||
|
||||
const state = readRemoteDevelopmentEnvironmentState();
|
||||
if (!requestHostIsLoopback(req)) {
|
||||
return NextResponse.json({ error: "Remote development environment endpoints only accept loopback requests." }, { status: 403 });
|
||||
return NextResponse.json({ error: loopbackRejectionMessage(req, state) }, { status: 403 });
|
||||
}
|
||||
|
||||
const expectedSecret = localDashboardSecretForRequest(req, state);
|
||||
@ -101,7 +108,7 @@ export function assertRemoteDevelopmentEnvironmentBrowserRequest(req: NextReques
|
||||
}
|
||||
|
||||
if (!requestHostIsLoopback(req) || !browserRequestOriginIsAllowed(req, state)) {
|
||||
return NextResponse.json({ error: "Remote development environment endpoints only accept loopback requests." }, { status: 403 });
|
||||
return NextResponse.json({ error: loopbackRejectionMessage(req, state) }, { status: 403 });
|
||||
}
|
||||
|
||||
const fetchSite = req.headers.get("sec-fetch-site");
|
||||
|
||||
@ -440,7 +440,7 @@
|
||||
if ((app.importance ?? 0) === importance) {
|
||||
// TODO escape HTML
|
||||
appContainer.innerHTML += `
|
||||
<a href="http://${`${stackPortPrefix}` === "81" ? "" : `p${stackPortPrefix}.`}localhost:${withPrefix(app.portSuffix)}${app.path ?? ""}" target="_blank" rel="noopener noreferrer" class="${app.importance === 2 ? "important" : app.importance === 1 ? "" : "unimportant"}">
|
||||
<a href="http://${app.portSuffix === "42" ? "127.0.0.1" : (`${stackPortPrefix}` === "81" ? "localhost" : `p${stackPortPrefix}.localhost`)}:${withPrefix(app.portSuffix)}${app.path ?? ""}" target="_blank" rel="noopener noreferrer" class="${app.importance === 2 ? "important" : app.importance === 1 ? "" : "unimportant"}">
|
||||
<div class="port">:${withPrefix(app.portSuffix)}</div>
|
||||
<div>
|
||||
<img src=${app.img || `//localhost:${withPrefix(app.portSuffix)}/favicon.ico`} />
|
||||
|
||||
@ -16,10 +16,10 @@ RUN apt-get update && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV PNPM_HOME=/pnpm
|
||||
ENV PATH=$PNPM_HOME:$PATH
|
||||
ENV PATH=$PNPM_HOME:$PNPM_HOME/bin:$PATH
|
||||
|
||||
RUN corepack enable
|
||||
RUN corepack prepare pnpm@10.23.0 --activate
|
||||
RUN corepack prepare pnpm@11.5.0 --activate
|
||||
RUN pnpm add -g turbo
|
||||
RUN pnpm add -g tsx
|
||||
|
||||
@ -38,6 +38,9 @@ RUN turbo prune --scope=@hexclave/backend --docker
|
||||
# Build stage
|
||||
FROM base AS builder
|
||||
|
||||
# Skip generate-sdks.ts in preinstall hook (file not available in pruned output)
|
||||
ENV STACK_SKIP_TEMPLATE_GENERATION=true
|
||||
|
||||
COPY --from=pruner /app/out/json/ .
|
||||
COPY --from=pruner /app/out/pnpm-lock.yaml .
|
||||
COPY .gitignore .
|
||||
@ -45,7 +48,7 @@ COPY pnpm-workspace.yaml .
|
||||
COPY turbo.json .
|
||||
COPY configs ./configs
|
||||
COPY --from=pruner /app/scripts/postinstall-patch-next-async-debug-info.mjs ./scripts/
|
||||
RUN STACK_SKIP_TEMPLATE_GENERATION=true pnpm install --frozen-lockfile
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
|
||||
COPY --from=pruner /app/out/full/ .
|
||||
|
||||
@ -55,7 +58,7 @@ COPY docs ./docs
|
||||
ENV NEXT_CONFIG_OUTPUT=standalone
|
||||
|
||||
# Build backend only
|
||||
RUN pnpm turbo run docker-build --filter=@hexclave/backend...
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm turbo run docker-build --filter=@hexclave/backend...
|
||||
|
||||
|
||||
# Final image
|
||||
|
||||
@ -15,10 +15,10 @@ RUN apt-get update && \
|
||||
rm -rf /var/lib/apt/lists
|
||||
|
||||
ENV PNPM_HOME=/pnpm
|
||||
ENV PATH=$PNPM_HOME:$PATH
|
||||
ENV PATH=$PNPM_HOME:$PNPM_HOME/bin:$PATH
|
||||
|
||||
RUN corepack enable
|
||||
RUN corepack prepare pnpm@10.23.0 --activate
|
||||
RUN corepack prepare pnpm@11.5.0 --activate
|
||||
RUN pnpm add -g turbo
|
||||
RUN pnpm add -g tsx
|
||||
|
||||
@ -35,6 +35,9 @@ RUN turbo prune --scope=@hexclave/backend --scope=@hexclave/dashboard --docker
|
||||
|
||||
FROM node-base AS builder
|
||||
|
||||
# Skip generate-sdks.ts in preinstall hook (file not available in pruned output)
|
||||
ENV STACK_SKIP_TEMPLATE_GENERATION=true
|
||||
|
||||
# copy over package.json files and install dependencies
|
||||
COPY --from=pruner /app/out/json/ .
|
||||
COPY --from=pruner /app/out/pnpm-lock.yaml .
|
||||
@ -43,7 +46,7 @@ COPY pnpm-workspace.yaml .
|
||||
COPY turbo.json .
|
||||
COPY configs ./configs
|
||||
COPY --from=pruner /app/scripts/postinstall-patch-next-async-debug-info.mjs ./scripts/
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store STACK_SKIP_TEMPLATE_GENERATION=true pnpm install --frozen-lockfile
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
|
||||
# copy over the rest of the code for the build
|
||||
COPY --from=pruner /app/out/full/ .
|
||||
@ -56,7 +59,7 @@ ENV NEXT_CONFIG_OUTPUT=standalone
|
||||
ENV NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY=pk_test_mock_publishable_key_for_local_emulator
|
||||
|
||||
# Build the backend NextJS app
|
||||
RUN pnpm turbo run docker-build --filter=@hexclave/backend... --filter=@hexclave/dashboard...
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm turbo run docker-build --filter=@hexclave/backend... --filter=@hexclave/dashboard...
|
||||
|
||||
# Build the self-host seed script.
|
||||
# tsdown -> rolldown is multi-threaded Rust; under qemu-user (cross-arch
|
||||
@ -135,7 +138,7 @@ RUN npm install
|
||||
FROM node-base AS mock-oauth-builder
|
||||
WORKDIR /mock-oauth
|
||||
COPY apps/mock-oauth-server/package.json .
|
||||
RUN pnpm install && pnpm add esbuild --save-dev
|
||||
RUN printf 'allowBuilds:\n esbuild: true\n' > pnpm-workspace.yaml && pnpm install && pnpm add esbuild --save-dev
|
||||
COPY apps/mock-oauth-server/src ./src
|
||||
RUN npx esbuild src/index.ts --bundle --platform=node --target=node22 --outfile=dist/index.cjs
|
||||
|
||||
|
||||
@ -10,10 +10,10 @@ RUN apt-get update && \
|
||||
rm -rf /var/lib/apt/lists
|
||||
|
||||
ENV PNPM_HOME=/pnpm
|
||||
ENV PATH=$PNPM_HOME:$PATH
|
||||
ENV PATH=$PNPM_HOME:$PNPM_HOME/bin:$PATH
|
||||
|
||||
RUN corepack enable
|
||||
RUN corepack prepare pnpm@10.23.0 --activate
|
||||
RUN corepack prepare pnpm@11.5.0 --activate
|
||||
RUN pnpm add -g turbo
|
||||
RUN pnpm add -g tsx
|
||||
|
||||
@ -33,6 +33,9 @@ RUN turbo prune --scope=@hexclave/backend --scope=@hexclave/dashboard --docker
|
||||
# Build stage
|
||||
FROM base AS builder
|
||||
|
||||
# Skip generate-sdks.ts in preinstall hook (file not available in pruned output)
|
||||
ENV STACK_SKIP_TEMPLATE_GENERATION=true
|
||||
|
||||
# copy over package.json files and install dependencies
|
||||
COPY --from=pruner /app/out/json/ .
|
||||
COPY --from=pruner /app/out/pnpm-lock.yaml .
|
||||
@ -41,7 +44,7 @@ COPY pnpm-workspace.yaml .
|
||||
COPY turbo.json .
|
||||
COPY configs ./configs
|
||||
COPY --from=pruner /app/scripts/postinstall-patch-next-async-debug-info.mjs ./scripts/
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store STACK_SKIP_TEMPLATE_GENERATION=true pnpm install --frozen-lockfile
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
|
||||
# copy over the rest of the code for the build
|
||||
COPY --from=pruner /app/out/full/ .
|
||||
@ -53,10 +56,10 @@ COPY docs ./docs
|
||||
ENV NEXT_CONFIG_OUTPUT=standalone
|
||||
|
||||
# Build the backend NextJS app
|
||||
RUN pnpm turbo run docker-build --filter=@hexclave/backend... --filter=@hexclave/dashboard...
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm turbo run docker-build --filter=@hexclave/backend... --filter=@hexclave/dashboard...
|
||||
|
||||
# Build the self-host seed script
|
||||
RUN cd apps/backend && pnpm build-self-host-migration-script
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store cd apps/backend && pnpm build-self-host-migration-script
|
||||
|
||||
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -218,7 +218,9 @@ The frameworks and languages with explicit SDK support are:
|
||||
If you already use Hexclave for your product, we recommend you re-use the same project to share your configuration between the two.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Option 1: Running Hexclave's dev environment (recommended)" defaultOpen>
|
||||
<Accordion title="Option 1: Running Hexclave's dashboard locally (recommended)" defaultOpen>
|
||||
This is the strongly recommended option unless the user has explicitly said otherwise, as it allows usage of `stack.config.ts` files and does not require the user to get project IDs or API keys from the dashboard.
|
||||
|
||||
First, create a `stack.config.ts` configuration file in the root directory of the workspace (or anywhere else):
|
||||
|
||||
```ts stack.config.ts
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -19,7 +19,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.17.6",
|
||||
"@types/react": "link:@types/react@18.3.12",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "14.2.3",
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
"react-icons": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "link:@types/react@18.3.12",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"postcss": "^8.4.35",
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.17.6",
|
||||
"@types/react": "link:@types/react@18.3.12",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "14.2.5",
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.17.6",
|
||||
"@types/react": "link:@types/react@18.3.12",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "14.2.3",
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.17.6",
|
||||
"@types/react": "link:@types/react@18.3.12",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
|
||||
25
package.json
25
package.json
@ -35,8 +35,8 @@
|
||||
"emulator:bench": "docker/local-emulator/qemu/run-emulator.sh bench",
|
||||
"stop-deps": "POSTGRES_DELAY_MS=0 pnpm run deps-compose kill && POSTGRES_DELAY_MS=0 pnpm run deps-compose down -v",
|
||||
"wait-until-postgres-is-ready:pg_isready": "until pg_isready -h localhost -p ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}28 && pg_isready -h localhost -p ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}34; do sleep 1; done",
|
||||
"wait-until-postgres-is-ready": "command -v pg_isready >/dev/null 2>&1 && pnpm run wait-until-postgres-is-ready:pg_isready || sleep 10 # not everyone has pg_isready installed, so we fallback to sleeping",
|
||||
"wait-until-clickhouse-is-ready": "pnpm exec wait-on http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}36/ping",
|
||||
"wait-until-postgres-is-ready": "command -v pg_isready >/dev/null 2>&1 && pnpm run wait-until-postgres-is-ready:pg_isready || sleep 10",
|
||||
"wait-until-clickhouse-is-ready": "echo 'Waiting for ClickHouse to be ready...' && pnpm exec wait-on -t 10000 -v http-get://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}36/ping || echo 'ClickHouse is not ready after 10 seconds (this is probably a problem with our wait-on setup TODO fix), continuing regardless...'",
|
||||
"start-deps:no-delay": "pnpm pre && pnpm run deps-compose up --detach --build && pnpm run wait-until-postgres-is-ready && pnpm run wait-until-clickhouse-is-ready && pnpm run db:init && echo \"\\nDependencies started in the background as Docker containers. 'pnpm run stop-deps' to stop them\"",
|
||||
"start-deps": "POSTGRES_DELAY_MS=${POSTGRES_DELAY_MS:-0} pnpm run start-deps:no-delay",
|
||||
"restart-deps": "pnpm pre && pnpm run stop-deps && pnpm run start-deps",
|
||||
@ -112,27 +112,8 @@
|
||||
"vitest": "^1.6.0",
|
||||
"wait-on": "^8.0.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"sharp": "^0.34.5"
|
||||
},
|
||||
"packageExtensions": {
|
||||
"@mintlify/link-rot": {
|
||||
"dependencies": {
|
||||
"react": "*",
|
||||
"react-dom": "*"
|
||||
}
|
||||
}
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"openid-client@5.6.4": "patches/openid-client@5.6.4.patch"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=10.0.0",
|
||||
"node": ">=20.0.0"
|
||||
"node": ">=22.13.0"
|
||||
},
|
||||
"packageManager": "pnpm@11.5.0+sha512.dbfcc4f81cf48597afd4bc391ffdf12c11f1a9fb83a395bfa6b0a2d9cc2fd8ffebafdb1ccbd529632153f793904c2615b7f09fe1a345473fd1c35845172a8eb1",
|
||||
"dependencies": {
|
||||
|
||||
@ -84,8 +84,8 @@
|
||||
"yup": "^1.7.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=19.0.0",
|
||||
"react": ">=19.0.0"
|
||||
"@types/react": ">=18.0.0",
|
||||
"react": ">=18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
|
||||
@ -519,7 +519,9 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
|
||||
` : ""}
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Option 1: Running Hexclave's dev environment (recommended)" defaultOpen>
|
||||
<Accordion title="Option 1: Running Hexclave's dashboard locally (recommended)" defaultOpen>
|
||||
${isAiPrompt ? "This is the strongly recommended option unless the user has explicitly said otherwise, as it allows usage of \`stack.config.ts\` files and does not require the user to get project IDs or API keys from the dashboard." : ""}
|
||||
|
||||
First, create a \`stack.config.ts\` configuration file in the root directory of the workspace (or anywhere else):
|
||||
|
||||
\`\`\`ts stack.config.ts
|
||||
|
||||
@ -90,30 +90,18 @@ import.meta.vitest?.test("getNodeText", ({ expect }) => {
|
||||
// Test with mixed array
|
||||
expect(getNodeText(["hello", 42, null])).toBe("hello42");
|
||||
|
||||
// Test with React element (mocked)
|
||||
const mockElement = {
|
||||
props: {
|
||||
children: "child text"
|
||||
}
|
||||
} as React.ReactElement;
|
||||
// Test with React element
|
||||
const mockElement = React.createElement("span", null, "child text");
|
||||
expect(getNodeText(mockElement)).toBe("child text");
|
||||
|
||||
// Test with nested React elements
|
||||
const nestedElement = {
|
||||
props: {
|
||||
children: {
|
||||
props: {
|
||||
children: "nested text"
|
||||
}
|
||||
} as React.ReactElement
|
||||
}
|
||||
} as React.ReactElement;
|
||||
const nestedElement = React.createElement("div", null, React.createElement("span", null, "nested text"));
|
||||
expect(getNodeText(nestedElement)).toBe("nested text");
|
||||
|
||||
// Test with array of React elements
|
||||
const arrayOfElements = [
|
||||
{ props: { children: "first" } } as React.ReactElement,
|
||||
{ props: { children: "second" } } as React.ReactElement
|
||||
React.createElement("span", null, "first"),
|
||||
React.createElement("span", null, "second"),
|
||||
];
|
||||
expect(getNodeText(arrayOfElements)).toBe("firstsecond");
|
||||
});
|
||||
|
||||
@ -85,11 +85,11 @@
|
||||
"yup": "^1.7.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=19.0.0",
|
||||
"@types/react-dom": ">=19.0.0",
|
||||
"react-dom": ">=19.0.0",
|
||||
"@types/react": ">=18.0.0",
|
||||
"@types/react-dom": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0",
|
||||
"next": ">=14.1 || >=15.0.0-canary.0 || >=15.0.0-rc.0",
|
||||
"react": ">=19.0.0"
|
||||
"react": ">=18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react-dom": {
|
||||
|
||||
@ -95,10 +95,10 @@
|
||||
"yup": "^1.7.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=19.0.0",
|
||||
"@types/react": ">=18.0.0",
|
||||
"@tanstack/react-router": ">=1.100.0",
|
||||
"@tanstack/react-start": ">=1.100.0",
|
||||
"react": ">=19.0.0"
|
||||
"react": ">=18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
|
||||
@ -140,17 +140,17 @@
|
||||
},
|
||||
"//": "IF_PLATFORM react-like",
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=19.0.0",
|
||||
"@types/react": ">=18.0.0",
|
||||
"//": "IF_PLATFORM next",
|
||||
"@types/react-dom": ">=19.0.0",
|
||||
"react-dom": ">=19.0.0",
|
||||
"@types/react-dom": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0",
|
||||
"next": ">=14.1 || >=15.0.0-canary.0 || >=15.0.0-rc.0",
|
||||
"//": "END_PLATFORM",
|
||||
"//": "IF_PLATFORM tanstack-start",
|
||||
"@tanstack/react-router": ">=1.100.0",
|
||||
"@tanstack/react-start": ">=1.100.0",
|
||||
"//": "END_PLATFORM",
|
||||
"react": ">=19.0.0"
|
||||
"react": ">=18.0.0"
|
||||
},
|
||||
"//": "END_PLATFORM",
|
||||
"//": "IF_PLATFORM react-like",
|
||||
|
||||
@ -101,13 +101,13 @@
|
||||
"yup": "^1.7.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=19.0.0",
|
||||
"@types/react-dom": ">=19.0.0",
|
||||
"react-dom": ">=19.0.0",
|
||||
"@types/react": ">=18.0.0",
|
||||
"@types/react-dom": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0",
|
||||
"next": ">=14.1 || >=15.0.0-canary.0 || >=15.0.0-rc.0",
|
||||
"@tanstack/react-router": ">=1.100.0",
|
||||
"@tanstack/react-start": ">=1.100.0",
|
||||
"react": ">=19.0.0"
|
||||
"react": ">=18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react-dom": {
|
||||
|
||||
2023
pnpm-lock.yaml
2023
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -11,8 +11,23 @@ minimumReleaseAge: 10080
|
||||
|
||||
blockExoticSubdeps: true
|
||||
|
||||
overrides:
|
||||
'@types/react': 19.2.7
|
||||
'@types/react-dom': 19.2.3
|
||||
sharp: ^0.34.5
|
||||
|
||||
packageExtensions:
|
||||
'@mintlify/link-rot':
|
||||
dependencies:
|
||||
react: '*'
|
||||
react-dom: '*'
|
||||
|
||||
patchedDependencies:
|
||||
openid-client@5.6.4: patches/openid-client@5.6.4.patch
|
||||
|
||||
allowBuilds:
|
||||
'@prisma/engines': true
|
||||
'@quetzallabs/i18n': false
|
||||
'@sentry/cli': true
|
||||
'@tailwindcss/oxide': true
|
||||
'@vercel/speed-insights': true
|
||||
|
||||
Loading…
Reference in New Issue
Block a user