Merge branch 'dev' into devin/1782934151-remove-product-level-free-trial
Some checks failed
DB migration compat / Check if migrations changed (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled

This commit is contained in:
Vedanta-Gawande 2026-07-13 16:11:02 -04:00 committed by GitHub
commit b055b94af6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 2144 additions and 1597 deletions

View File

@ -132,13 +132,10 @@ jobs:
- name: Backfill Bulldozer from Postgres
run: pnpm run db:backfill-bulldozer-from-prisma
# The built server (node dist/server.mjs) does not auto-load .env files the way
# `next start` used to, so it must be wrapped in with-env:test to pick up
# .env.test.local (mirrors the e2e-fallback-tests workflow).
- name: Start stack-backend in background
uses: JarvusInnovations/background-action@2428e7b970a846423095c79d43f759abf979a635 # v1.0.7
with:
run: pnpm -C apps/backend run with-env:test pnpm run start &
run: pnpm run start:backend --log-order=stream &
wait-on: |
http://localhost:8102
tail: true

View File

@ -126,15 +126,10 @@ jobs:
- name: Backfill Bulldozer from Postgres
run: pnpm run db:backfill-bulldozer-from-prisma
# The built server (node dist/server.mjs) does not auto-load .env files the way
# `next start` used to, so it must be wrapped in with-env:test to pick up
# .env.test.local (mirrors the e2e-fallback-tests workflow). The job-level
# NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX=67 stays in effect because dotenv does not
# override already-set process env vars.
- name: Start stack-backend in background
uses: JarvusInnovations/background-action@2428e7b970a846423095c79d43f759abf979a635 # v1.0.7
with:
run: pnpm -C apps/backend run with-env:test pnpm run start &
run: pnpm run start:backend --log-order=stream &
wait-on: |
http://localhost:6702
tail: true

View File

@ -113,7 +113,7 @@ jobs:
- name: Start stack-backend on fallback port (8110)
uses: JarvusInnovations/background-action@2428e7b970a846423095c79d43f759abf979a635 # v1.0.7
with:
run: PORT=8110 pnpm -C apps/backend run with-env:test pnpm run start &
run: pnpm -C apps/backend run with-env:test next start --port 8110 &
wait-on: |
http://localhost:8110
tail: true

View File

@ -1,4 +0,0 @@
declare module "*/dist/vercel.mjs" {
const app: typeof import("../src/server/vercel").default;
export default app;
}

View File

@ -1,10 +0,0 @@
import app from "../dist/vercel.mjs";
export const config = {
runtime: "nodejs",
maxDuration: 60,
};
export default function handler(request: Request): Response | Promise<Response> {
return app.handle(request);
}

View File

@ -1,11 +1,11 @@
// This file configures the initialization of Sentry on the client.
// The config you add here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/browser/
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
import { getBrowserCompatibilityReport } from "@hexclave/shared/dist/utils/browser-compat";
import { sentryBaseConfig } from "@hexclave/shared/dist/utils/sentry";
import { nicify } from "@hexclave/shared/dist/utils/strings";
import * as Sentry from "@sentry/browser";
Sentry.init({
...sentryBaseConfig,

View File

@ -0,0 +1,99 @@
import { withSentryConfig } from "@sentry/nextjs";
const withConfiguredSentryConfig = (nextConfig) =>
withSentryConfig(
nextConfig,
{
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
widenClientFileUpload: true,
telemetry: false,
},
{
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Transpiles SDK to be compatible with IE11 (increases bundle size)
transpileClientSDK: true,
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
tunnelRoute: "/monitoring",
// Hides source maps from generated client bundles
hideSourceMaps: true,
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
// Enables automatic instrumentation of Vercel Cron Monitors.
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: true,
}
);
/** @type {import('next').NextConfig} */
const nextConfig = {
// optionally set output to "standalone" for Docker builds
// https://nextjs.org/docs/pages/api-reference/next-config-js/output
output: process.env.NEXT_CONFIG_OUTPUT,
// we're open-source, so we can provide source maps
productionBrowserSourceMaps: true,
poweredByHeader: false,
experimental: {
serverMinification: false, // needs to be disabled for oidc-provider to work, which relies on the original constructor names
},
serverExternalPackages: [
'oidc-provider',
],
async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "Cross-Origin-Opener-Policy",
value: "same-origin",
},
{
key: "Permissions-Policy",
value: "",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "X-Frame-Options",
value: "SAMEORIGIN",
},
{
key: "Content-Security-Policy",
value: "",
},
],
},
];
},
};
export default withConfiguredSentryConfig(nextConfig);

View File

@ -5,20 +5,20 @@
"private": true,
"type": "module",
"scripts": {
"clean": "rimraf src/generated && rimraf dist && rimraf node_modules",
"clean": "rimraf src/generated && rimraf .next && rimraf node_modules",
"typecheck": "tsc --noEmit",
"with-env": "dotenv -c --",
"with-env:dev": "dotenv -c development --",
"with-env:prod": "dotenv -c production --",
"with-env:test": "dotenv -c test --",
"dev": "BACKEND_PORT=${STACK_DEV_FALLBACK_BACKEND:+${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}10} && BACKEND_PORT=${BACKEND_PORT:-${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02} && concurrently -n \"dev,codegen,prisma-studio,email-queue,cron-jobs,bulldozer-studio\" -k \"STACK_DISABLE_REACT_ASYNC_DEBUG_INFO=${STACK_DISABLE_REACT_ASYNC_DEBUG_INFO:-true} NODE_ENV=development BACKEND_PORT=$BACKEND_PORT dotenv -c development -- tsx watch --clear-screen=false src/server/server.ts ${STACK_BACKEND_DEV_EXTRA_ARGS:-}\" \"pnpm run codegen:watch\" \"pnpm run prisma-studio\" \"pnpm run run-email-queue\" \"pnpm run run-cron-jobs\" \"pnpm run run-bulldozer-studio\"",
"dev": "BACKEND_PORT=${STACK_DEV_FALLBACK_BACKEND:+${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}10} && BACKEND_PORT=${BACKEND_PORT:-${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02} && concurrently -n \"dev,codegen,prisma-studio,email-queue,cron-jobs,bulldozer-studio\" -k \"STACK_DISABLE_REACT_ASYNC_DEBUG_INFO=${STACK_DISABLE_REACT_ASYNC_DEBUG_INFO:-true} next dev --port $BACKEND_PORT ${STACK_BACKEND_DEV_EXTRA_ARGS:-}\" \"pnpm run codegen:watch\" \"pnpm run prisma-studio\" \"pnpm run run-email-queue\" \"pnpm run run-cron-jobs\" \"pnpm run run-bulldozer-studio\"",
"dev:inspect": "STACK_BACKEND_DEV_EXTRA_ARGS=\"--inspect\" pnpm run dev",
"dev:profile": "STACK_BACKEND_DEV_EXTRA_ARGS=\"--experimental-cpu-prof\" pnpm run dev",
"build": "pnpm run codegen && tsdown --config tsdown.config.ts",
"docker-build": "pnpm run codegen && tsdown --config tsdown.config.ts",
"build": "pnpm run codegen && next build",
"docker-build": "pnpm run codegen && next build --experimental-build-mode compile",
"build-self-host-migration-script": "tsdown --config scripts/db-migrations.tsdown.config.ts",
"start": "node dist/server.mjs",
"analyze-bundle": "next experimental-analyze",
"start": "next start --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02",
"codegen-prisma": "HEXCLAVE_DATABASE_CONNECTION_STRING=\"${HEXCLAVE_DATABASE_CONNECTION_STRING:-${STACK_DATABASE_CONNECTION_STRING:-placeholder-database-connection-string}}\" pnpm run prisma generate",
"codegen-prisma:watch": "HEXCLAVE_DATABASE_CONNECTION_STRING=\"${HEXCLAVE_DATABASE_CONNECTION_STRING:-${STACK_DATABASE_CONNECTION_STRING:-placeholder-database-connection-string}}\" pnpm run prisma generate --watch",
"generate-private-sign-up-risk-engine": "pnpm run with-env tsx scripts/generate-private-sign-up-risk-engine.ts",
@ -59,12 +59,9 @@
},
"dependencies": {
"@ai-sdk/mcp": "^1.0.21",
"spacetimedb": "^2.1.0",
"@aws-sdk/client-s3": "^3.855.0",
"@clickhouse/client": "^1.14.0",
"@elysiajs/node": "^1.4.5",
"@hexclave/next": "workspace:*",
"@hexclave/shared": "workspace:*",
"@hexclave/shared-backend": "workspace:*",
"@node-oauth/oauth2-server": "^5.1.0",
"@openrouter/ai-sdk-provider": "2.2.3",
"@opentelemetry/api": "^1.9.0",
@ -76,7 +73,6 @@
"@opentelemetry/instrumentation": "^0.53.0",
"@opentelemetry/resources": "^1.26.0",
"@opentelemetry/sdk-logs": "^0.53.0",
"@opentelemetry/sdk-node": "0.53.0",
"@opentelemetry/sdk-trace-base": "^1.26.0",
"@opentelemetry/sdk-trace-node": "^1.26.0",
"@opentelemetry/semantic-conventions": "^1.27.0",
@ -85,13 +81,14 @@
"@prisma/client": "^7.0.0",
"@prisma/extension-read-replicas": "^0.5.0",
"@prisma/instrumentation": "^7.0.0",
"@sentry/browser": "^10.45.0",
"@sentry/node": "^10.45.0",
"@sentry/rollup-plugin": "^4.6.1",
"@sentry/nextjs": "^10.45.0",
"@simplewebauthn/server": "^13.3.0",
"@sinclair/typebox": "^0.34.49",
"@hexclave/next": "workspace:*",
"@hexclave/shared": "workspace:*",
"@hexclave/shared-backend": "workspace:*",
"@upstash/qstash": "^2.8.2",
"@vercel/functions": "^2.0.0",
"@vercel/otel": "^1.10.4",
"@vercel/sandbox": "^1.2.0",
"ai": "^6.0.0",
"cel-js": "^0.8.2",
@ -99,7 +96,6 @@
"diff": "^8.0.3",
"dotenv": "^16.4.5",
"dotenv-cli": "^7.3.0",
"elysia": "1.4.28",
"emailable": "^3.1.1",
"freestyle": "^0.1.63",
"jiti": "^2.6.1",
@ -116,7 +112,6 @@
"resend": "^6.0.1",
"semver": "^7.6.3",
"sharp": "^0.34.4",
"spacetimedb": "^2.1.0",
"stripe": "^18.3.0",
"svix": "^1.89.0",
"vite": "^6.1.0",

View File

@ -55,9 +55,7 @@ export default defineConfig({
inlineOnly: false,
// Externalize Node.js builtins so they're imported rather than shimmed
external: [...nodeBuiltins, ...externalPackages],
// Docker builds the backend server into the same dist directory before this
// script. Cleaning here deletes dist/server.mjs from the final image.
clean: false,
clean: true,
// Use banner to add createRequire for CommonJS modules that use require() for builtins
// The imported require is used by the shimmed __require2 function
banner: {

View File

@ -2,48 +2,12 @@ import { SmartRouter } from "@/smart-router";
import { writeFileSyncIfChanged } from "@hexclave/shared/dist/utils/fs";
import fs from "fs";
const httpMethodNames = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"] as const;
function stringCompare(a: string, b: string) {
return a < b ? -1 : a > b ? 1 : 0;
}
function routeFilePathToImportPath(filePath: string) {
return `@/${filePath.replace(/^src\//, "").replace(/\.(ts|tsx|js|jsx)$/, "")}`;
}
function generateRouteModules(routes: Awaited<ReturnType<typeof SmartRouter.listRoutes>>) {
const routeFiles = routes
.filter(route => route.isRoute && /\/route\.(ts|tsx|js|jsx)$/.test(route.filePath))
.sort((a, b) => stringCompare(a.normalizedPath, b.normalizedPath) || stringCompare(a.filePath, b.filePath));
const imports = routeFiles.map((route, index) => {
return `import * as r${index} from ${JSON.stringify(routeFilePathToImportPath(route.filePath))};`;
});
const entries = routeFiles.map((route, index) => {
return ` { normalizedPath: ${JSON.stringify(route.normalizedPath)}, module: r${index} },`;
});
return `import type { UnknownRouteModule } from "@/server/registry";
${imports.join("\n")}
export const httpMethodNames = ${JSON.stringify(httpMethodNames)} as const;
export const routeModules: readonly { normalizedPath: string, module: UnknownRouteModule }[] = [
${entries.join("\n")}
];
`;
}
async function main() {
const routes = await SmartRouter.listRoutes();
const apiVersions = await SmartRouter.listApiVersions();
fs.mkdirSync("src/generated", { recursive: true });
writeFileSyncIfChanged("src/generated/routes.json", JSON.stringify(routes, null, 2));
writeFileSyncIfChanged("src/generated/api-versions.json", JSON.stringify(apiVersions, null, 2));
writeFileSyncIfChanged("src/generated/route-modules.ts", generateRouteModules(routes));
console.log("Successfully updated route info");
}
// eslint-disable-next-line no-restricted-syntax

View File

@ -12,8 +12,8 @@ import { KnownErrors } from "@hexclave/shared/dist/known-errors";
import { urlSchema, yupArray, yupNumber, yupObject, yupString, yupUnion } from "@hexclave/shared/dist/schema-fields";
import { getNodeEnvironment } from "@hexclave/shared/dist/utils/env";
import { StatusError } from "@hexclave/shared/dist/utils/errors";
import { cookies } from "@/lib/runtime/headers";
import { redirect } from "@/lib/runtime/navigation";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { generators } from "openid-client";
import type { InferType, Schema } from "yup";

View File

@ -15,8 +15,8 @@ import { KnownError, KnownErrors } from "@hexclave/shared";
import { yupMixed, yupNumber, yupObject, yupString } from "@hexclave/shared/dist/schema-fields";
import { HexclaveAssertionError, StatusError, captureError } from "@hexclave/shared/dist/utils/errors";
import { deindent, extractScopes, mergeScopeStrings } from "@hexclave/shared/dist/utils/strings";
import { cookies } from "@/lib/runtime/headers";
import { redirect } from "@/lib/runtime/navigation";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { oauthResponseToSmartResponse } from "../../oauth-helpers";
/**

View File

@ -3,8 +3,9 @@ import { getSoleTenancyFromProjectBranch } from "@/lib/tenancies";
import { getPrismaClientForTenancy, globalPrismaClient } from "@/prisma-client";
import { VerificationCodeType } from "@/generated/prisma/client";
import { KnownErrors } from "@hexclave/shared/dist/known-errors";
import { NextRequest } from "next/server";
export async function GET(request: Request) {
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
if (!code || code.length !== 45)

View File

@ -4,6 +4,7 @@ import { preprocessProxyBody } from "@/private";
import { handleApiRequest } from "@/route-handlers/smart-route-handler";
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
import { StatusError } from "@hexclave/shared/dist/utils/errors";
import { NextRequest } from "next/server";
const OPENROUTER_BASE_URL = "https://openrouter.ai/api";
const OPENROUTER_DEFAULT_MODEL = "anthropic/claude-sonnet-4.6";
@ -37,7 +38,7 @@ function sanitizeBody(raw: ArrayBuffer): Uint8Array {
return new TextEncoder().encode(JSON.stringify(parsed));
}
async function proxyToOpenRouter(req: Request, options: { params: Promise<{ path?: string[] }> }) {
async function proxyToOpenRouter(req: NextRequest, options: { params: Promise<{ path?: string[] }> }) {
const apiKey = getEnvVariable("STACK_OPENROUTER_API_KEY");
const params = await options.params;
const subpath = params.path?.join("/") ?? "";
@ -48,7 +49,7 @@ async function proxyToOpenRouter(req: Request, options: { params: Promise<{ path
: undefined;
if (apiKey === "FORWARD_TO_PRODUCTION") {
const targetUrl = `${PRODUCTION_AI_PROXY_BASE_URL}/${subpath}${new URL(req.url).search}`;
const targetUrl = `${PRODUCTION_AI_PROXY_BASE_URL}/${subpath}${req.nextUrl.search}`;
const headers: Record<string, string> = {};
if (contentType) {
headers["Content-Type"] = contentType;
@ -69,7 +70,7 @@ async function proxyToOpenRouter(req: Request, options: { params: Promise<{ path
});
}
const targetUrl = `${OPENROUTER_BASE_URL}/${subpath}${new URL(req.url).search}`;
const targetUrl = `${OPENROUTER_BASE_URL}/${subpath}${req.nextUrl.search}`;
const headers: Record<string, string> = {
"Authorization": `Bearer ${apiKey}`,
"anthropic-version": "2023-06-01",

View File

@ -1,7 +1,7 @@
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { yupNever, yupObject, yupString } from "@hexclave/shared/dist/schema-fields";
import { HexclaveAssertionError } from "@hexclave/shared/dist/utils/errors";
import { redirect } from "@/lib/runtime/navigation";
import { redirect } from "next/navigation";
export const GET = createSmartRouteHandler({
metadata: {

View File

@ -2,6 +2,7 @@ import { handleApiRequest } from "@/route-handlers/smart-route-handler";
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
import { HexclaveAssertionError } from "@hexclave/shared/dist/utils/errors";
import { createNodeHttpServerDuplex } from "@hexclave/shared/dist/utils/node-http";
import { NextRequest, NextResponse } from "next/server";
import { createOidcProvider } from "../../../../idp";
export const dynamic = "force-dynamic";
@ -26,7 +27,7 @@ function getOidcCallbackPromise() {
return _oidcCallbackPromiseCache;
}
const handler = handleApiRequest(async (req: Request) => {
const handler = handleApiRequest(async (req: NextRequest) => {
const newUrl = req.url.replace(pathPrefix, "");
if (newUrl === req.url) {
throw new HexclaveAssertionError("No path prefix found in request URL. Is the pathPrefix correct?", { newUrl, url: req.url, pathPrefix });
@ -59,7 +60,7 @@ const handler = handleApiRequest(async (req: Request) => {
// filter out session cookies; we don't want to keep sessions open, every OAuth flow should start a new session
headers = headers.filter(([k, v]) => k !== "set-cookie" || !v.toString().match(/^_session\.?/));
return new Response(body, {
return new NextResponse(body, {
headers: headers,
status: {
// our API never returns 301 or 302 by convention, so transform them to 307 or 308

View File

@ -1,7 +1,7 @@
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { yupNever, yupObject, yupString } from "@hexclave/shared/dist/schema-fields";
import { HexclaveAssertionError } from "@hexclave/shared/dist/utils/errors";
import { redirect } from "@/lib/runtime/navigation";
import { redirect } from "next/navigation";
export const GET = createSmartRouteHandler({
metadata: {

View File

@ -2,6 +2,7 @@ import { handleApiRequest } from "@/route-handlers/smart-route-handler";
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
import { HexclaveAssertionError } from "@hexclave/shared/dist/utils/errors";
import { createNodeHttpServerDuplex } from "@hexclave/shared/dist/utils/node-http";
import { NextRequest, NextResponse } from "next/server";
import { createOidcProvider } from "../../../../idp";
export const dynamic = "force-dynamic";
@ -26,7 +27,7 @@ function getOidcCallbackPromise() {
return _oidcCallbackPromiseCache;
}
const handler = handleApiRequest(async (req: Request) => {
const handler = handleApiRequest(async (req: NextRequest) => {
const newUrl = req.url.replace(pathPrefix, "");
if (newUrl === req.url) {
throw new HexclaveAssertionError("No path prefix found in request URL. Is the pathPrefix correct?", { newUrl, url: req.url, pathPrefix });
@ -59,7 +60,7 @@ const handler = handleApiRequest(async (req: Request) => {
// filter out session cookies; we don't want to keep sessions open, every OAuth flow should start a new session
headers = headers.filter(([k, v]) => k !== "set-cookie" || !v.toString().match(/^_session\.?/));
return new Response(body, {
return new NextResponse(body, {
headers: headers,
status: {
// our API never returns 301 or 302 by convention, so transform them to 307 or 308

View File

@ -5,6 +5,8 @@ import { HexclaveAssertionError } from "@hexclave/shared/dist/utils/errors";
import * as fs from "fs/promises";
import * as path from "path";
const REVALIDATE_SECONDS = 60 * 60;
type ChangeType = "major" | "minor" | "patch";
type ChangelogEntry = {
@ -171,6 +173,9 @@ export const GET = createSmartRouteHandler({
"Accept": "text/plain",
"User-Agent": "stack-auth-backend-changelog",
},
next: {
revalidate: REVALIDATE_SECONDS,
},
});
if (!response.ok) {

View File

@ -2,7 +2,7 @@
// redirect to projects/.well-known/[...route]?include_anonymous=true
import { yupNever, yupObject, yupString } from "@hexclave/shared/dist/schema-fields";
import { redirect } from "@/lib/runtime/navigation";
import { redirect } from "next/navigation";
import { createSmartRouteHandler } from "../../../../../../route-handlers/smart-route-handler";
const handler = createSmartRouteHandler({

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
"use client";
import * as Sentry from "@sentry/nextjs";
import { captureError } from "@hexclave/shared/dist/utils/errors";
import Error from "next/error";
import { useEffect } from "react";
export default function GlobalError({ error }: any) {
useEffect(() => {
captureError("backend-global-error", error);
}, [error]);
return (
<html>
<body>
[An unhandled error occurred.]
<Error
statusCode={500}
/>
</body>
</html>
);
}

View File

@ -0,0 +1,15 @@
"use client";
import { throwErr } from "@hexclave/shared/dist/utils/errors";
export default function Page() {
return <div>
This page is useful for testing error handling.<br />
Your observability platform should pick up on the errors thrown below.<br />
<button onClick={() => throwErr(`Client debug error thrown successfully!`)}>Throw client error</button>
<button onClick={() => {
console.log("Endpoint request", fetch("/health/error-handler-debug/endpoint"));
}}>Throw server error</button>
</div>;
}

View File

@ -1,8 +1,9 @@
import { globalPrismaClient } from "@/prisma-client";
import { HexclaveAssertionError } from "@hexclave/shared/dist/utils/errors";
import { NextRequest } from "next/server";
export async function GET(req: Request) {
if (new URL(req.url).searchParams.get("db")) {
export async function GET(req: NextRequest) {
if (req.nextUrl.searchParams.get("db")) {
const project = await globalPrismaClient.project.findFirst({});
if (!project) {

View File

@ -0,0 +1,22 @@
import type { Metadata } from 'next';
import React from 'react';
import '../polyfills';
export const metadata: Metadata = {
title: 'Hexclave API',
description: 'API endpoint of Hexclave.',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode,
}) {
return (
<html lang="en" suppressHydrationWarning>
<body suppressHydrationWarning>
{children}
</body>
</html>
);
}

View File

@ -0,0 +1,13 @@
import { connection } from "next/server";
export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
export default async function NotFound() {
await connection(); // guarantees we will never prerender
return <div>
404 Not Found
</div>;
}

View File

@ -0,0 +1,22 @@
import { getNodeEnvironment } from "@hexclave/shared/dist/utils/env";
import Link from "next/link";
export default function Home() {
return (
<div>
Welcome to Hexclave&apos;s API endpoint.<br />
<br />
Were you looking for <Link href="https://app.hexclave.com">Hexclave&apos;s dashboard</Link> instead?<br />
<br />
You can also return to <Link href="https://hexclave.com">https://hexclave.com</Link>.<br />
<br />
<Link href="/api/v1">API v1</Link><br />
{getNodeEnvironment() === "development" && (
<>
<br />
<Link href="/dev-stats">Dev Stats</Link><br />
</>
)}
</div>
);
}

View File

@ -1,6 +0,0 @@
import "@/instrument";
import "@/polyfills";
import { app } from "./server/app";
import "./server/env-expand";
export default app;

View File

@ -1,82 +0,0 @@
import { getEnvVariable, getNodeEnvironment } from "@hexclave/shared/dist/utils/env";
import { sentryBaseConfig } from "@hexclave/shared/dist/utils/sentry";
import { nicify } from "@hexclave/shared/dist/utils/strings";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { NodeSDK } from "@opentelemetry/sdk-node";
import { PrismaInstrumentation } from "@prisma/instrumentation";
import * as Sentry from "@sentry/node";
import { initPerfStats } from "./lib/dev-perf-stats";
globalThis.global = globalThis;
// The Elysia process is the Node runtime; set the marker before shared helpers that still ask for Next runtime metadata.
// eslint-disable-next-line no-restricted-syntax
process.env.NEXT_RUNTIME ??= "nodejs";
let registered = false;
export function registerBackendInstrumentation() {
if (registered) {
return;
}
registered = true;
const portPrefix = getEnvVariable("NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX", "81");
const isDevelopment = getNodeEnvironment() === "development";
const sdk = new NodeSDK({
serviceName: "stack-backend",
instrumentations: [
new PrismaInstrumentation(),
...getNodeAutoInstrumentations({
"@opentelemetry/instrumentation-http": {
enabled: false,
},
}),
],
...isDevelopment ? {
traceExporter: new OTLPTraceExporter({
url: `http://localhost:${portPrefix}31/v1/traces`,
}),
} : {},
});
sdk.start();
process.title = `stack-backend:${portPrefix} (node/elysia)`;
initPerfStats();
Sentry.init({
...sentryBaseConfig,
// We run our own OpenTelemetry NodeSDK above (for Prisma + the dev OTLP exporter), which already
// registers the global trace/context/propagation APIs. Without this flag, @sentry/node (v10 is
// OpenTelemetry-native) tries to register them again, logging "Attempted duplicate registration
// of API: trace/propagation/context". Skipping Sentry's OTel setup lets the NodeSDK own it while
// error capture continues to work normally. (Letting Sentry own OTel instead would require migrating
// our OpenTelemetry deps from v1 to v2 to match @sentry/node v10.)
skipOpenTelemetrySetup: true,
dsn: getEnvVariable("NEXT_PUBLIC_SENTRY_DSN", ""),
enabled: getNodeEnvironment() !== "development" && !getEnvVariable("CI", ""),
beforeSend(event, hint) {
const error = hint.originalException;
let nicified;
try {
nicified = nicify(error, { maxDepth: 8 });
} catch (e) {
nicified = `Error occurred during nicification: ${e}`;
}
if (error instanceof Error) {
event.extra = {
...event.extra,
cause: error.cause,
errorProps: {
...error,
},
nicifiedError: nicified,
};
}
return event;
},
});
}
registerBackendInstrumentation();

View File

@ -0,0 +1,73 @@
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { PrismaInstrumentation } from "@prisma/instrumentation";
import * as Sentry from "@sentry/nextjs";
import { getEnvVariable, getNextRuntime, getNodeEnvironment } from "@hexclave/shared/dist/utils/env";
import { sentryBaseConfig } from "@hexclave/shared/dist/utils/sentry";
import { nicify } from "@hexclave/shared/dist/utils/strings";
import { registerOTel } from '@vercel/otel';
import { initPerfStats } from "./lib/dev-perf-stats";
import "./polyfills";
// this is a hack for making prisma instrumentation work
// somehow prisma instrumentation accesses global and it makes edge instrumentation complain
globalThis.global = globalThis;
export async function register() {
registerOTel({
serviceName: 'stack-backend',
instrumentations: [
new PrismaInstrumentation(),
...getNextRuntime() === "nodejs" ? getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-http': {
enabled: false,
},
}) : [],
],
...getNodeEnvironment() === "development" && getNextRuntime() === "nodejs" ? {
traceExporter: new OTLPTraceExporter({
url: `http://localhost:${getEnvVariable("NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX", "81")}31/v1/traces`,
}),
} : {},
});
if (getNextRuntime() === "nodejs") {
(globalThis as any).process.title = `stack-backend:${getEnvVariable("NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX", "81")} (node/nextjs)`;
// Initialize performance stats collection in development
initPerfStats();
}
if (getNextRuntime() === "nodejs" || getNextRuntime() === "edge") {
Sentry.init({
...sentryBaseConfig,
dsn: getEnvVariable("NEXT_PUBLIC_SENTRY_DSN", ""),
enabled: getNodeEnvironment() !== "development" && !getEnvVariable("CI", ""),
// Add exception metadata to the event
beforeSend(event, hint) {
const error = hint.originalException;
let nicified;
try {
nicified = nicify(error, { maxDepth: 8 });
} catch (e) {
nicified = `Error occurred during nicification: ${e}`;
}
if (error instanceof Error) {
event.extra = {
...event.extra,
cause: error.cause,
errorProps: {
...error,
},
nicifiedError: nicified,
};
}
return event;
},
});
}
}

View File

@ -3,7 +3,7 @@ import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
import { HexclaveAssertionError } from "@hexclave/shared/dist/utils/errors";
import { isIpAddress } from "@hexclave/shared/dist/utils/ips";
import { pick } from "@hexclave/shared/dist/utils/objects";
import { headers } from "@/lib/runtime/headers";
import { headers } from "next/headers";
// An end user is a person sitting behind a computer screen.
//

View File

@ -1,128 +0,0 @@
import { getRequestContext, ResponseCookieOptions } from "./request-context";
type CookieSetObject = ResponseCookieOptions & {
name: string,
value: string,
};
type CookieDeleteObject = {
name: string,
};
function encodeCookieComponent(value: string) {
return encodeURIComponent(value);
}
function normalizeSameSite(sameSite: ResponseCookieOptions["sameSite"]) {
if (sameSite === true) {
return "Strict";
}
if (sameSite === "lax") {
return "Lax";
}
if (sameSite === "strict") {
return "Strict";
}
if (sameSite === "none") {
return "None";
}
return undefined;
}
export function serializeSetCookie(name: string, value: string, options: ResponseCookieOptions = {}) {
const parts = [`${encodeCookieComponent(name)}=${encodeCookieComponent(value)}`];
if (options.domain != null) {
parts.push(`Domain=${options.domain}`);
}
parts.push(`Path=${options.path ?? "/"}`);
const expires = options.expires != null
? options.expires instanceof Date ? options.expires : new Date(options.expires)
: options.maxAge != null ? new Date(Date.now() + options.maxAge * 1000) : undefined;
if (expires != null) {
parts.push(`Expires=${expires.toUTCString()}`);
}
if (options.maxAge != null) {
parts.push(`Max-Age=${Math.trunc(options.maxAge)}`);
}
// Emit Secure before HttpOnly to match the attribute order the pre-ElysiaJS
// (Next.js) backend produced. Cookie attribute order is semantically irrelevant
// per RFC 6265, but the e2e suite asserts the exact Set-Cookie string for the
// oauth-inner cookies (`...; Max-Age=N;( Secure;)? HttpOnly`), so keeping this
// order avoids a spurious regression there.
if (options.secure === true) {
parts.push("Secure");
}
if (options.httpOnly === true) {
parts.push("HttpOnly");
}
const sameSite = normalizeSameSite(options.sameSite);
if (sameSite != null) {
parts.push(`SameSite=${sameSite}`);
}
if (options.priority != null) {
parts.push(`Priority=${options.priority}`);
}
return parts.join("; ");
}
export async function headers() {
return getRequestContext().headers;
}
export async function cookies() {
const context = getRequestContext();
return {
get(name: string) {
const pending = [...context.pendingSetCookies].reverse().find(cookie => cookie.name === name);
if (pending != null) {
return {
name,
value: pending.value,
};
}
const value = context.incomingCookies.get(name);
return value == null ? undefined : {
name,
value,
};
},
set(nameOrCookie: string | CookieSetObject, value?: string, options?: ResponseCookieOptions) {
const cookie = typeof nameOrCookie === "string"
? {
name: nameOrCookie,
value: value ?? "",
options: options ?? {},
}
: {
name: nameOrCookie.name,
value: nameOrCookie.value,
options: nameOrCookie,
};
context.pendingSetCookies.push(cookie);
context.incomingCookies.set(cookie.name, cookie.value);
},
delete(nameOrCookie: string | CookieDeleteObject) {
const name = typeof nameOrCookie === "string" ? nameOrCookie : nameOrCookie.name;
const cookie = {
name,
value: "",
options: {
expires: new Date(0),
maxAge: 0,
path: "/",
},
};
context.pendingSetCookies = context.pendingSetCookies.filter(c => c.name !== name);
context.deletedCookies.push(cookie);
context.incomingCookies.delete(name);
},
};
}

View File

@ -1,44 +0,0 @@
export class NextRedirectError extends Error {
digest = "NEXT_REDIRECT";
redirectUrl: string;
redirectStatus: 307 | 308;
constructor(url: string, status: 307 | 308) {
super("NEXT_REDIRECT");
this.redirectUrl = url;
this.redirectStatus = status;
}
}
export class NextNotFoundError extends Error {
digest = "NEXT_NOT_FOUND";
constructor() {
super("NEXT_NOT_FOUND");
}
}
export const RedirectType = {
push: "push",
replace: "replace",
} as const;
export function redirect(url: string, _type?: typeof RedirectType[keyof typeof RedirectType]): never {
throw new NextRedirectError(url, 307);
}
export function permanentRedirect(url: string): never {
throw new NextRedirectError(url, 308);
}
export function notFound(): never {
throw new NextNotFoundError();
}
export function usePathname(): never {
throw new Error("next/navigation usePathname() was called in the backend runtime");
}
export function useSearchParams(): never {
throw new Error("next/navigation useSearchParams() was called in the backend runtime");
}

View File

@ -1,55 +0,0 @@
import { AsyncLocalStorage } from "node:async_hooks";
export type CookieWrite = {
name: string,
value: string,
options: ResponseCookieOptions,
};
export type ResponseCookieOptions = {
domain?: string,
expires?: Date | number | string,
httpOnly?: boolean,
maxAge?: number,
path?: string,
priority?: "low" | "medium" | "high",
sameSite?: boolean | "lax" | "strict" | "none",
secure?: boolean,
};
export type RequestContext = {
headers: Headers,
incomingCookies: Map<string, string>,
pendingSetCookies: CookieWrite[],
deletedCookies: CookieWrite[],
};
export const requestContextALS = new AsyncLocalStorage<RequestContext>();
export function getRequestContext() {
const context = requestContextALS.getStore();
if (context == null) {
throw new Error("Backend request context is only available while handling a backend request");
}
return context;
}
export function parseCookieHeader(cookieHeader: string | null) {
const cookies = new Map<string, string>();
if (cookieHeader == null || cookieHeader === "") {
return cookies;
}
for (const part of cookieHeader.split(";")) {
const separatorIndex = part.indexOf("=");
if (separatorIndex < 0) {
continue;
}
const name = part.slice(0, separatorIndex).trim();
if (name === "") {
continue;
}
const rawValue = part.slice(separatorIndex + 1).trim();
cookies.set(name, rawValue);
}
return cookies;
}

View File

@ -1,4 +1,4 @@
import * as Sentry from "@sentry/node";
import * as Sentry from "@sentry/nextjs";
import { getEnvVariable, getNodeEnvironment } from "@hexclave/shared/dist/utils/env";
import { captureError, registerErrorSink } from "@hexclave/shared/dist/utils/errors";
import * as util from "util";

View File

@ -5,6 +5,8 @@ import apiVersions from './generated/api-versions.json';
import routes from './generated/routes.json';
import './polyfills';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { SmartRouter } from './smart-router';
const DEV_RATE_LIMIT_MAX_REQUESTS = 100;
@ -63,7 +65,7 @@ const corsAllowedRequestHeadersWithAliases = withHexclaveHeaderAliases(corsAllow
const corsAllowedResponseHeadersWithAliases = withHexclaveHeaderAliases(corsAllowedResponseHeaders);
// This function can be marked `async` if using `await` inside
export async function proxy(request: Request) {
export async function proxy(request: NextRequest) {
const url = new URL(request.url);
const delay = +getEnvVariable('STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS', '0');
if (delay) {
@ -96,7 +98,7 @@ export async function proxy(request: Request) {
const waitMs = Math.max(0, DEV_RATE_LIMIT_WINDOW_MS - (now - devRateLimitTimestamps[0]));
const retryAfterSeconds = Math.max(1, Math.ceil(waitMs / 1000));
const response = Response.json({
const response = NextResponse.json({
message: 'Artificial development rate limit triggered. Wait before retrying.',
}, {
status: 429,
@ -171,11 +173,9 @@ export async function proxy(request: Request) {
}
}
const newUrl = new URL(request.url);
const newUrl = request.nextUrl.clone();
newUrl.pathname = pathname;
const rewriteHeaders = new Headers(responseInit?.headers);
rewriteHeaders.set("x-middleware-rewrite", newUrl.toString());
return new Response(null, { ...responseInit, headers: rewriteHeaders });
return NextResponse.rewrite(newUrl, responseInit);
}
// See "Matching Paths" below to learn more

View File

@ -1,9 +1,10 @@
import "../polyfills";
import { yupArray, yupNumber, yupObject, yupString } from "@hexclave/shared/dist/schema-fields";
import { NextRequest } from "next/server";
import { createSmartRouteHandler } from "./smart-route-handler";
export function redirectHandler(redirectPath: string, statusCode: 303 | 307 | 308 = 307): (req: Request, options: any) => Promise<Response> {
export function redirectHandler(redirectPath: string, statusCode: 303 | 307 | 308 = 307): (req: NextRequest, options: any) => Promise<Response> {
return createSmartRouteHandler({
request: yupObject({
url: yupString().defined(),

View File

@ -15,6 +15,7 @@ import { getEnvVariable, getNodeEnvironment } from "@hexclave/shared/dist/utils/
import { HexclaveAssertionError, StatusError, captureError, throwErr } from "@hexclave/shared/dist/utils/errors";
import { deindent } from "@hexclave/shared/dist/utils/strings";
import { traceSpan, withTraceSpan } from "@hexclave/shared/dist/utils/telemetry";
import { NextRequest } from "next/server";
import * as yup from "yup";
const allowedMethods = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] as const;
@ -69,7 +70,7 @@ export type MergeSmartRequest<T, MSQ = SmartRequest> =
)
);
async function validate<T>(obj: SmartRequest, schema: yup.Schema<T>, req: Request | null): Promise<T> {
async function validate<T>(obj: SmartRequest, schema: yup.Schema<T>, req: NextRequest | null): Promise<T> {
try {
return await yupValidate(schema, obj, {
abortEarly: false,
@ -100,7 +101,7 @@ async function validate<T>(obj: SmartRequest, schema: yup.Schema<T>, req: Reques
throw new KnownErrors.SchemaError(
deindent`
Request validation failed on ${req.method} ${new URL(req.url).pathname}:
Request validation failed on ${req.method} ${req.nextUrl.pathname}:
${inners.map(e => deindent`
- ${e.message}
`).join("\n")}
@ -113,7 +114,7 @@ async function validate<T>(obj: SmartRequest, schema: yup.Schema<T>, req: Reques
}
async function parseBody(req: Request, bodyBuffer: ArrayBuffer): Promise<SmartRequest["body"]> {
async function parseBody(req: NextRequest, bodyBuffer: ArrayBuffer): Promise<SmartRequest["body"]> {
const contentType = req.method === "GET" || req.method === "HEAD" ? undefined : req.headers.get("content-type")?.split(";")[0];
const getText = () => {
@ -157,7 +158,7 @@ async function parseBody(req: Request, bodyBuffer: ArrayBuffer): Promise<SmartRe
}
}
const parseAuth = withTraceSpan('smart request parseAuth', async (req: Request): Promise<SmartRequestAuth | null> => {
const parseAuth = withTraceSpan('smart request parseAuth', async (req: NextRequest): Promise<SmartRequestAuth | null> => {
const projectId = req.headers.get("x-stack-project-id");
const branchId = req.headers.get("x-stack-branch-id") ?? DEFAULT_BRANCH_ID;
let requestType = req.headers.get("x-stack-access-type");
@ -336,7 +337,7 @@ const parseAuth = withTraceSpan('smart request parseAuth', async (req: Request):
};
});
export async function createSmartRequest(req: Request, bodyBuffer: ArrayBuffer, options?: { params: Promise<Record<string, string>> }): Promise<SmartRequest> {
export async function createSmartRequest(req: NextRequest, bodyBuffer: ArrayBuffer, options?: { params: Promise<Record<string, string>> }): Promise<SmartRequest> {
return await traceSpan("creating smart request", async () => {
const urlObject = new URL(req.url);
const clientVersionMatch = req.headers.get("x-stack-client-version")?.match(/^(\w+)\s+(@[\w\/]+)@([\d.]+)$/);
@ -362,6 +363,6 @@ export async function createSmartRequest(req: Request, bodyBuffer: ArrayBuffer,
});
}
export async function validateSmartRequest<T extends DeepPartialSmartRequestWithSentinel>(nextReq: Request | null, smartReq: SmartRequest, schema: yup.Schema<T>): Promise<T> {
export async function validateSmartRequest<T extends DeepPartialSmartRequestWithSentinel>(nextReq: NextRequest | null, smartReq: SmartRequest, schema: yup.Schema<T>): Promise<T> {
return await validate(smartReq, schema, nextReq);
}

View File

@ -3,6 +3,7 @@ import { HexclaveAssertionError } from "@hexclave/shared/dist/utils/errors";
import { Json } from "@hexclave/shared/dist/utils/json";
import { deepPlainEquals } from "@hexclave/shared/dist/utils/objects";
import { traceSpan } from "@hexclave/shared/dist/utils/telemetry";
import { NextRequest } from "next/server";
import * as yup from "yup";
import "../polyfills";
import { SmartRequest } from "./smart-request";
@ -41,7 +42,7 @@ export type SmartResponse = {
}
);
export async function validateSmartResponse<T>(req: Request | null, smartReq: SmartRequest, obj: unknown, schema: yup.Schema<T>): Promise<T> {
export async function validateSmartResponse<T>(req: NextRequest | null, smartReq: SmartRequest, obj: unknown, schema: yup.Schema<T>): Promise<T> {
try {
return await yupValidate(schema, obj, {
abortEarly: false,
@ -67,7 +68,7 @@ function isResponseBody(body: unknown): body is Response {
return typeof body === "object" && body !== null && body instanceof Response;
}
export async function createResponse<T extends SmartResponse>(req: Request | null, requestId: string, obj: T): Promise<Response> {
export async function createResponse<T extends SmartResponse>(req: NextRequest | null, requestId: string, obj: T): Promise<Response> {
return await traceSpan("creating HTTP response from smart response", async () => {
let status = obj.statusCode;
const headers = new Map<string, string[]>();

View File

@ -1,7 +1,7 @@
import "../polyfills";
import { recordRequestStats } from "@/lib/dev-request-stats";
import * as Sentry from "@sentry/node";
import * as Sentry from "@sentry/nextjs";
import { EndpointDocumentation } from "@hexclave/shared/dist/crud";
import { KnownError, KnownErrors } from "@hexclave/shared/dist/known-errors";
import { generateSecureRandomString } from "@hexclave/shared/dist/utils/crypto";
@ -9,6 +9,7 @@ import { getNodeEnvironment } from "@hexclave/shared/dist/utils/env";
import { HexclaveAssertionError, StatusError, captureError, errorToNiceString } from "@hexclave/shared/dist/utils/errors";
import { runAsynchronously, wait } from "@hexclave/shared/dist/utils/promises";
import { traceSpan } from "@hexclave/shared/dist/utils/telemetry";
import { NextRequest } from "next/server";
import * as yup from "yup";
import { DeepPartialSmartRequestWithSentinel, MergeSmartRequest, SmartRequest, createSmartRequest, validateSmartRequest } from "./smart-request";
import { SmartResponse, createResponse, validateSmartResponse } from "./smart-response";
@ -64,8 +65,8 @@ let concurrentRequestsInProcess = 0;
* Catches any errors thrown in the handler and returns a 500 response with the thrown error message. Also logs the
* request details.
*/
export function handleApiRequest(handler: (req: Request, options: any, requestId: string) => Promise<Response>): (req: Request, options: any) => Promise<Response> {
return async (req: Request, options: any) => {
export function handleApiRequest(handler: (req: NextRequest, options: any, requestId: string) => Promise<Response>): (req: NextRequest, options: any) => Promise<Response> {
return async (req: NextRequest, options: any) => {
concurrentRequestsInProcess++;
try {
const requestId = generateSecureRandomString(80);
@ -79,13 +80,12 @@ export function handleApiRequest(handler: (req: Request, options: any, requestId
"stack.process.concurrent-requests": concurrentRequestsInProcess,
},
}, async (span) => {
const requestUrl = new URL(req.url);
// Set Sentry scope to include request details
Sentry.setContext("stack-request", {
requestId: requestId,
method: req.method,
url: req.url,
query: Object.fromEntries(requestUrl.searchParams),
query: Object.fromEntries(req.nextUrl.searchParams),
headers: Object.fromEntries(req.headers),
});
@ -121,11 +121,11 @@ export function handleApiRequest(handler: (req: Request, options: any, requestId
...allowedLongRequestPaths,
...allowedLongRequestPaths.map(path => path.replace(/^\/api\/latest\//, "/api/v1/")),
];
const warnAfterSeconds = allAllowedLongRequestPaths.includes(requestUrl.pathname) ? 240 : 12;
const warnAfterSeconds = allAllowedLongRequestPaths.includes(req.nextUrl.pathname) ? 240 : 12;
runAsynchronously(async () => {
await wait(warnAfterSeconds * 1000);
if (!hasRequestFinished) {
captureError("request-timeout-watcher", new Error(`Request with ID ${requestId} to ${req.method} ${requestUrl.pathname} has been running for ${warnAfterSeconds} seconds. Try to keep requests short. The request may be cancelled by the serverless provider if it takes too long.`));
captureError("request-timeout-watcher", new Error(`Request with ID ${requestId} to ${req.method} ${req.nextUrl.pathname} has been running for ${warnAfterSeconds} seconds. Try to keep requests short. The request may be cancelled by the serverless provider if it takes too long.`));
}
});
@ -135,10 +135,10 @@ export function handleApiRequest(handler: (req: Request, options: any, requestId
const time = (performance.now() - timeStart);
// Record request stats for dev-stats page
recordRequestStats(req.method, requestUrl.pathname, time);
recordRequestStats(req.method, req.nextUrl.pathname, time);
if ([301, 302].includes(res.status)) {
throw new HexclaveAssertionError("HTTP status codes 301 and 302 should not be returned by our APIs because the behavior for non-GET methods is inconsistent across implementations. Use 303 (to rewrite method to GET) or 307/308 (to preserve the original method and data) instead.", { status: res.status, url: requestUrl, req, res });
throw new HexclaveAssertionError("HTTP status codes 301 and 302 should not be returned by our APIs because the behavior for non-GET methods is inconsistent across implementations. Use 303 (to rewrite method to GET) or 307/308 (to preserve the original method and data) instead.", { status: res.status, url: req.nextUrl, req, res });
}
if (!disableExtendedLogging) console.log(`[ RES] [${requestId}] ${req.method} ${censoredUrl}: ${res.status} (in ${time.toFixed(0)}ms)`);
return res;
@ -201,7 +201,7 @@ export type SmartRouteHandler<
Req extends DeepPartialSmartRequestWithSentinel = DeepPartialSmartRequestWithSentinel,
Res extends SmartResponse = SmartResponse,
InitArgs extends [readonly OverloadParam[], SmartRouteHandlerOverloadGenerator<OverloadParam, Req, Res>] | [SmartRouteHandlerOverload<Req, Res>] = any,
> = ((req: Request, options: any) => Promise<Response>) & {
> = ((req: NextRequest, options: any) => Promise<Response>) & {
overloads: Map<OverloadParam, SmartRouteHandlerOverload<Req, Res>>,
invoke: (smartRequest: SmartRequest) => Promise<Res>,
initArgs: InitArgs,
@ -247,7 +247,7 @@ export function createSmartRouteHandler<
throw new HexclaveAssertionError("Duplicate overload parameters");
}
const invoke = async (nextRequest: Request | null, requestId: string, smartRequest: SmartRequest, shouldSetContext: boolean = false) => {
const invoke = async (nextRequest: NextRequest | null, requestId: string, smartRequest: SmartRequest, shouldSetContext: boolean = false) => {
const reqsParsed: [[Req, SmartRequest], SmartRouteHandlerOverload<Req, Res>][] = [];
const reqsErrors: unknown[] = [];
for (const [overloadParam, overload] of overloads.entries()) {

View File

@ -1,312 +0,0 @@
import { httpMethodNames } from "@/generated/route-modules";
import { serializeSetCookie } from "@/lib/runtime/headers";
import { NextNotFoundError } from "@/lib/runtime/navigation";
import { parseCookieHeader, requestContextALS, type RequestContext } from "@/lib/runtime/request-context";
import { node } from "@elysiajs/node";
import { getEnvVariable, getNodeEnvironment } from "@hexclave/shared/dist/utils/env";
import { Elysia } from "elysia";
import { runRequestPipeline } from "./middleware";
import { createBackendRequest } from "./backend-request";
import { MalformedRouteParamError, matchRoute } from "./registry";
const globalSecurityHeaders = {
"Cross-Origin-Opener-Policy": "same-origin",
"Permissions-Policy": "",
"Referrer-Policy": "strict-origin-when-cross-origin",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "SAMEORIGIN",
"Content-Security-Policy": "",
};
const knownHttpMethods = new Set<string>(httpMethodNames);
const developmentRequestStartTimes = new WeakMap<Request, number>();
const shouldLogDevelopmentRequests = getNodeEnvironment() === "development";
export const app = new Elysia({
adapter: node(),
})
.onRequest(({ request }) => {
if (shouldLogDevelopmentRequests) {
developmentRequestStartTimes.set(request, performance.now());
}
})
.onAfterResponse(({ request, set }) => {
if (!shouldLogDevelopmentRequests) {
return;
}
const startTime = developmentRequestStartTimes.get(request);
const elapsedMilliseconds = startTime == null ? "unknown" : (performance.now() - startTime).toFixed(1);
const pathname = new URL(request.url).pathname;
console.log(`[Elysia] ${request.method} ${pathname} ${set.status} ${elapsedMilliseconds}ms`);
})
.get("/", () => htmlResponse(homeHtml()))
.get("/dev-stats", () => htmlResponse(devStatsHtml()))
.get("/health/error-handler-debug", () => htmlResponse(errorHandlerDebugHtml()))
.post("/monitoring", async ({ request }) => withGlobalHeaders(await handleMonitoringTunnel(request)), {
parse: "none",
})
.all("/*", async ({ request }) => await dispatch(request), {
parse: "none",
});
export async function dispatch(request: Request) {
const pipeline = await runRequestPipeline(request);
if (pipeline.shortCircuitResponse != null) {
return withGlobalHeaders(pipeline.shortCircuitResponse);
}
let match;
try {
match = matchRoute(pipeline.dispatchPath);
} catch (error) {
if (error instanceof MalformedRouteParamError) {
return withGlobalHeaders(new Response("Bad Request", { status: 400 }));
}
throw error;
}
if (match == null) {
return withGlobalHeaders(new Response("<div>404 Not Found</div>", {
status: 404,
headers: {
"content-type": "text/html; charset=utf-8",
},
}));
}
const method = request.method.toUpperCase();
if (!isHttpMethod(method)) {
return withGlobalHeaders(new Response(null, {
status: 405,
}));
}
const handler = match.methods.get(method) ?? (method === "HEAD" ? match.methods.get("GET") : undefined);
if (handler == null) {
return withGlobalHeaders(new Response(null, {
status: 405,
}));
}
const backendRequest = createBackendRequest(request, pipeline.mergedHeaders, pipeline.originalUrl);
const context: RequestContext = {
headers: pipeline.mergedHeaders,
incomingCookies: parseCookieHeader(pipeline.mergedHeaders.get("cookie")),
pendingSetCookies: [],
deletedCookies: [],
};
const response = await requestContextALS.run(context, async () => {
try {
return await handler(backendRequest, {
params: Promise.resolve(match.params),
});
} catch (error) {
if (isRedirectError(error)) {
return new Response(null, {
status: error.redirectStatus,
headers: {
Location: error.redirectUrl,
},
});
}
if (error instanceof NextNotFoundError) {
return new Response("Not Found", { status: 404 });
}
throw error;
}
});
const finalResponse = method === "HEAD" ? new Response(null, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
}) : response;
for (const cookie of context.pendingSetCookies) {
finalResponse.headers.append("Set-Cookie", serializeSetCookie(cookie.name, cookie.value, cookie.options));
}
for (const cookie of context.deletedCookies) {
finalResponse.headers.append("Set-Cookie", serializeSetCookie(cookie.name, cookie.value, cookie.options));
}
if (pipeline.corsHeadersInit != null) {
for (const [key, value] of Object.entries(pipeline.corsHeadersInit)) {
finalResponse.headers.set(key, value);
}
}
if (pipeline.middlewareRewrite != null) {
finalResponse.headers.set("x-middleware-rewrite", pipeline.middlewareRewrite);
}
return withGlobalHeaders(finalResponse);
}
function isRedirectError(error: unknown): error is { redirectStatus: 307 | 308, redirectUrl: string } {
if (!(error instanceof Error) || !("digest" in error) || !("redirectUrl" in error) || !("redirectStatus" in error)) {
return false;
}
return typeof error.digest === "string"
&& error.digest.startsWith("NEXT_REDIRECT")
&& typeof error.redirectUrl === "string"
&& (error.redirectStatus === 307 || error.redirectStatus === 308);
}
function withGlobalHeaders(response: Response) {
for (const [key, value] of Object.entries(globalSecurityHeaders)) {
response.headers.set(key, value);
}
return response;
}
function htmlResponse(body: string, status = 200) {
return withGlobalHeaders(new Response(body, {
status,
headers: {
"content-type": "text/html; charset=utf-8",
},
}));
}
function homeHtml() {
const devStatsLink = getNodeEnvironment() === "development"
? `<br><a href="/dev-stats">Dev Stats</a><br>`
: "";
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hexclave API</title>
</head>
<body>
<div>
Welcome to Hexclave's API endpoint.<br>
<br>
Were you looking for <a href="https://app.hexclave.com">Hexclave's dashboard</a> instead?<br>
<br>
You can also return to <a href="https://hexclave.com">https://hexclave.com</a>.<br>
<br>
<a href="/api/v1">API v1</a><br>
${devStatsLink}
</div>
</body>
</html>`;
}
function devStatsHtml() {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hexclave Backend Dev Stats</title>
<style>
body { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; margin: 24px; }
button { font: inherit; padding: 4px 8px; }
pre { background: #f6f8fa; border: 1px solid #d0d7de; padding: 12px; overflow: auto; }
</style>
</head>
<body>
<h1>Dev Stats</h1>
<button id="refresh">Refresh</button>
<pre id="output">Loading...</pre>
<script>
async function refresh() {
const output = document.getElementById("output");
const response = await fetch("/dev-stats/api", { headers: { "accept": "application/json" } });
output.textContent = JSON.stringify(await response.json(), null, 2);
}
document.getElementById("refresh").addEventListener("click", refresh);
refresh();
</script>
</body>
</html>`;
}
function errorHandlerDebugHtml() {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Backend Error Debug</title>
</head>
<body>
<div>
This page is useful for testing error handling.<br>
Your observability platform should pick up on the errors thrown below.<br>
<button id="client-error">Throw client error</button>
<button id="server-error">Throw server error</button>
</div>
<script>
document.getElementById("client-error").addEventListener("click", () => {
throw new Error("Client debug error thrown successfully!");
});
document.getElementById("server-error").addEventListener("click", () => {
fetch("/health/error-handler-debug/endpoint").then((response) => console.log("Endpoint response", response));
});
</script>
</body>
</html>`;
}
async function handleMonitoringTunnel(request: Request) {
const allowedDsn = getEnvVariable("NEXT_PUBLIC_SENTRY_DSN", "");
if (allowedDsn === "") {
return new Response(null, { status: 404 });
}
const envelope = await request.text();
const firstLineEnd = envelope.indexOf("\n");
const envelopeHeaderBytes = firstLineEnd === -1 ? envelope : envelope.slice(0, firstLineEnd);
const envelopeDsn = getEnvelopeDsn(envelopeHeaderBytes);
if (envelopeDsn !== allowedDsn) {
return new Response("Invalid Sentry envelope DSN", {
status: 400,
headers: {
"content-type": "text/plain; charset=utf-8",
},
});
}
const sentryDsnUrl = new URL(allowedDsn);
const projectId = sentryDsnUrl.pathname.split("/").filter(Boolean).at(-1);
if (projectId == null) {
return new Response("Invalid configured Sentry DSN", {
status: 500,
headers: {
"content-type": "text/plain; charset=utf-8",
},
});
}
const sentryEnvelopeUrl = new URL(`/api/${projectId}/envelope/`, sentryDsnUrl.origin);
const sentryResponse = await fetch(sentryEnvelopeUrl, {
method: "POST",
body: envelope,
headers: {
"content-type": request.headers.get("content-type") ?? "application/x-sentry-envelope",
},
});
return new Response(sentryResponse.body, {
status: sentryResponse.status,
statusText: sentryResponse.statusText,
headers: sentryResponse.headers,
});
}
function getEnvelopeDsn(envelopeHeaderBytes: string) {
let parsedEnvelopeHeader: unknown;
try {
parsedEnvelopeHeader = JSON.parse(envelopeHeaderBytes);
} catch {
return undefined;
}
if (
parsedEnvelopeHeader == null
|| typeof parsedEnvelopeHeader !== "object"
|| !("dsn" in parsedEnvelopeHeader)
|| typeof parsedEnvelopeHeader.dsn !== "string"
) {
return undefined;
}
return parsedEnvelopeHeader.dsn;
}
function isHttpMethod(method: string): method is typeof httpMethodNames[number] {
return knownHttpMethods.has(method);
}

View File

@ -1,17 +0,0 @@
type NodeRequestInit = RequestInit & {
duplex?: "half",
};
export function createBackendRequest(request: Request, headers: Headers, originalUrl: string): Request {
const init: NodeRequestInit = {
method: request.method,
headers,
};
if (request.method !== "GET" && request.method !== "HEAD") {
init.body = request.body;
init.duplex = "half";
}
return new Request(originalUrl, init);
}

View File

@ -1,36 +0,0 @@
/* eslint-disable no-restricted-syntax -- This bootstrap normalizes process.env before getEnvVariable reads it. */
const envReferencePattern = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-(.*?))?\}|\$([A-Za-z_][A-Za-z0-9_]*)/g;
function expandEnvValue(value: string): string {
return value.replace(envReferencePattern, (_match, bracedName: string | undefined, defaultValue: string | undefined, bareName: string | undefined) => {
const name = bracedName ?? bareName;
if (name == null) {
return "";
}
const referencedValue = process.env[name];
if (bracedName != null && defaultValue != null && (referencedValue == null || referencedValue === "")) {
return defaultValue;
}
return referencedValue ?? "";
});
}
for (let iteration = 0; iteration < 10; iteration++) {
let changed = false;
for (const [key, value] of Object.entries(process.env)) {
if (value == null || !envReferencePattern.test(value)) {
envReferencePattern.lastIndex = 0;
continue;
}
envReferencePattern.lastIndex = 0;
const expandedValue = expandEnvValue(value);
if (expandedValue !== value) {
process.env[key] = expandedValue;
changed = true;
}
}
if (!changed) {
break;
}
}

View File

@ -1,244 +0,0 @@
import apiVersions from "@/generated/api-versions.json";
import routes from "@/generated/routes.json";
import { SmartRouter } from "@/smart-router";
import { getEnvVariable, getNodeEnvironment } from "@hexclave/shared/dist/utils/env";
import { wait } from "@hexclave/shared/dist/utils/promises";
const DEV_RATE_LIMIT_MAX_REQUESTS = 100;
const DEV_RATE_LIMIT_WINDOW_MS = 10_000;
const devRateLimitMarks: number[] = [];
const corsAllowedRequestHeaders = [
"content-type",
"authorization",
"x-stack-project-id",
"x-stack-branch-id",
"x-stack-override-error-status",
"x-stack-random-nonce",
"x-stack-client-version",
"x-stack-disable-artificial-development-delay",
"x-stack-access-type",
"x-stack-publishable-client-key",
"x-stack-secret-server-key",
"x-stack-super-secret-admin-key",
"x-stack-admin-access-token",
"x-stack-refresh-token",
"x-stack-access-token",
"x-stack-allow-restricted-user",
"x-stack-allow-anonymous-user",
"baggage",
"sentry-trace",
"x-vercel-protection-bypass",
"ngrok-skip-browser-warning",
];
const corsAllowedResponseHeaders = [
"content-type",
"x-stack-actual-status",
"x-stack-known-error",
];
function withHexclaveHeaderAliases(headers: string[]): string[] {
return headers.flatMap((header) => header.startsWith("x-stack-")
? [header, `x-hexclave-${header.slice("x-stack-".length)}`]
: [header]);
}
const corsAllowedRequestHeadersWithAliases = withHexclaveHeaderAliases(corsAllowedRequestHeaders);
const corsAllowedResponseHeadersWithAliases = withHexclaveHeaderAliases(corsAllowedResponseHeaders);
export type PipelineResult = {
corsHeadersInit?: HeadersInit,
dispatchPath: string,
mergedHeaders: Headers,
middlewareRewrite?: string,
originalUrl: string,
shortCircuitResponse?: Response,
};
export async function runRequestPipeline(request: Request): Promise<PipelineResult> {
const url = new URL(request.url);
const mergedHeaders = mergeHexclaveHeaderAliases(request.headers);
ensureForwardedForHeader(mergedHeaders, request);
const delay = +getEnvVariable("STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS", "0");
if (delay) {
if (getNodeEnvironment().includes("production")) {
throw new Error("STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS environment variable is only allowed in development");
}
if (!request.headers.get("x-stack-disable-artificial-development-delay")) {
await wait(delay);
}
}
const isApiRequest = url.pathname.startsWith("/api/");
const corsHeadersInit = isApiRequest ? {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
"Access-Control-Max-Age": "86400",
"Access-Control-Allow-Headers": corsAllowedRequestHeadersWithAliases.join(", "),
"Access-Control-Expose-Headers": corsAllowedResponseHeadersWithAliases.join(", "),
"Vary": corsAllowedRequestHeadersWithAliases.join(", "),
} : undefined;
if (isApiRequest && !request.headers.get("x-stack-disable-artificial-development-delay") && getNodeEnvironment() === "development" && request.method !== "OPTIONS" && !request.url.includes(".well-known") && !request.url.includes("/api/latest/internal/external-db-sync/")) {
const now = performance.now();
while (devRateLimitMarks.length > 0 && now - devRateLimitMarks[0] > DEV_RATE_LIMIT_WINDOW_MS) {
devRateLimitMarks.shift();
}
if (devRateLimitMarks.length >= DEV_RATE_LIMIT_MAX_REQUESTS) {
const waitMs = Math.max(0, DEV_RATE_LIMIT_WINDOW_MS - (now - devRateLimitMarks[0]));
const retryAfterSeconds = Math.max(1, Math.ceil(waitMs / 1000));
const response = Response.json({
message: "Artificial development rate limit triggered. Wait before retrying.",
}, {
status: 429,
});
if (Math.random() < 0.5 && corsHeadersInit) {
for (const [key, value] of Object.entries(corsHeadersInit)) {
response.headers.set(key, value);
}
}
if (Math.random() < 0.5) {
response.headers.set("Retry-After", retryAfterSeconds.toString());
}
return {
corsHeadersInit,
dispatchPath: url.pathname,
mergedHeaders,
originalUrl: request.url,
shortCircuitResponse: response,
};
}
devRateLimitMarks.push(now);
}
if (request.method === "OPTIONS" && isApiRequest) {
return {
corsHeadersInit,
dispatchPath: url.pathname,
mergedHeaders,
originalUrl: request.url,
shortCircuitResponse: new Response(null, {
headers: corsHeadersInit,
}),
};
}
const dispatchPath = getDispatchPath(url.pathname);
return {
corsHeadersInit,
dispatchPath,
mergedHeaders,
middlewareRewrite: dispatchPath === url.pathname ? undefined : dispatchPath,
originalUrl: request.url,
};
}
function mergeHexclaveHeaderAliases(headers: Headers) {
const newRequestHeaders = new Headers(headers);
for (const [name, value] of headers) {
if (name.startsWith("x-hexclave-")) {
newRequestHeaders.set(`x-stack-${name.slice("x-hexclave-".length)}`, value);
}
}
return newRequestHeaders;
}
const clientIpForwardingHeaders = ["x-forwarded-for", "x-real-ip", "x-vercel-forwarded-for", "cf-connecting-ip"];
function ensureForwardedForHeader(headers: Headers, request: Request) {
// Direct connections (local dev, proxy-less self-host) arrive without any forwarding
// header, so getEndUserIp() finds no client IP and warns on every request. The Node
// adapter still knows the socket's remote address, so synthesize x-forwarded-for from
// it — mirroring what the old Next.js dev server did. Behind a real proxy (e.g. Vercel)
// one of these headers is already set, so this stays a no-op there.
if (clientIpForwardingHeaders.some((header) => headers.has(header))) {
return;
}
const socketIp = readClientSocketIp(request);
if (socketIp == null) {
return;
}
headers.set("x-forwarded-for", normalizeClientIp(socketIp));
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function readClientSocketIp(request: Request): string | undefined {
// The Node adapter (srvx) augments the web Request with the resolved client IP and
// the underlying Node socket, neither of which exist on the standard Request type.
// Read them defensively at runtime instead of casting; on other runtimes (e.g. the
// Vercel serverless entry) these are simply absent and we fall through to undefined.
const directIp: unknown = Reflect.get(request, "ip");
if (typeof directIp === "string" && directIp !== "") {
return directIp;
}
const runtime: unknown = Reflect.get(request, "runtime");
const node: unknown = isRecord(runtime) ? runtime.node : undefined;
const req: unknown = isRecord(node) ? node.req : undefined;
const socket: unknown = isRecord(req) ? req.socket : undefined;
const remoteAddress: unknown = isRecord(socket) ? socket.remoteAddress : undefined;
return typeof remoteAddress === "string" && remoteAddress !== "" ? remoteAddress : undefined;
}
function normalizeClientIp(ip: string): string {
// Node sockets report IPv4 clients as IPv4-mapped IPv6 (e.g. "::ffff:127.0.0.1").
// Normalize to the plain IPv4 form so it matches what proxies put in x-forwarded-for.
const mapped = ip.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
return mapped ? mapped[1] : ip;
}
import.meta.vitest?.test("ensureForwardedForHeader synthesizes the client IP for direct connections", ({ expect }) => {
// Uses the adapter-provided client IP when no forwarding header is present.
const fromIp = new Headers();
ensureForwardedForHeader(fromIp, Object.assign(new Request("http://localhost/api/v1"), { ip: "203.0.113.7" }));
expect(fromIp.get("x-forwarded-for")).toBe("203.0.113.7");
// Falls back to the raw Node socket and normalizes IPv4-mapped IPv6 to plain IPv4.
const fromSocket = new Headers();
ensureForwardedForHeader(fromSocket, Object.assign(new Request("http://localhost/api/v1"), {
runtime: { node: { req: { socket: { remoteAddress: "::ffff:127.0.0.1" } } } },
}));
expect(fromSocket.get("x-forwarded-for")).toBe("127.0.0.1");
// Never overwrites an existing forwarding header (e.g. behind a trusted proxy like Vercel).
const existing = new Headers({ "x-forwarded-for": "198.51.100.1" });
ensureForwardedForHeader(existing, Object.assign(new Request("http://localhost/api/v1"), { ip: "203.0.113.7" }));
expect(existing.get("x-forwarded-for")).toBe("198.51.100.1");
// No socket info available (e.g. an unusual runtime) → leaves headers untouched.
const none = new Headers();
ensureForwardedForHeader(none, new Request("http://localhost/api/v1"));
expect(none.get("x-forwarded-for")).toBe(null);
});
function getDispatchPath(originalPathname: string) {
let pathname = originalPathname;
outer: for (let i = 0; i < apiVersions.length - 1; i++) {
const version = apiVersions[i];
const nextVersion = apiVersions[i + 1];
if (!nextVersion.migrationFolder) {
throw new Error(`No migration folder found for version ${nextVersion.name}. This is a bug because every version except the first should have a migration folder.`);
}
if ((pathname + "/").startsWith(version.servedRoute + "/")) {
const nextPathname = pathname.replace(version.servedRoute, nextVersion.servedRoute);
const migrationPathname = nextPathname.replace(nextVersion.servedRoute, nextVersion.migrationFolder);
for (const route of routes) {
if (nextVersion.migrationFolder && (route.normalizedPath + "/").startsWith(nextVersion.migrationFolder + "/")) {
if (SmartRouter.matchNormalizedPath(migrationPathname, route.normalizedPath)) {
pathname = migrationPathname;
break outer;
}
}
}
pathname = nextPathname;
}
}
return pathname;
}

View File

@ -1,126 +0,0 @@
import { httpMethodNames, routeModules } from "@/generated/route-modules";
import { SmartRouter } from "@/smart-router";
export type HttpMethod = typeof httpMethodNames[number];
export type RouteParams = Record<string, string | string[]>;
export type RouteHandlerOptions = { params: Promise<RouteParams> };
export type RouteHandler = (request: Request, options: RouteHandlerOptions) => Promise<Response> | Response;
export type UnknownRouteModule = Partial<Record<HttpMethod, unknown>>;
type UnknownRouteFunction = (request: Request, options: RouteHandlerOptions) => unknown;
type RouteEntry = {
methods: Map<HttpMethod, RouteHandler>,
normalizedPath: string,
specificity: number[],
};
export type RouteMatch = {
handler?: RouteHandler,
methods: Map<HttpMethod, RouteHandler>,
normalizedPath: string,
params: Record<string, string | string[]>,
};
export const routeRegistry = buildRouteRegistry();
export function matchRoute(dispatchPath: string): RouteMatch | undefined {
for (const entry of routeRegistry) {
const params = SmartRouter.matchNormalizedPath(dispatchPath, entry.normalizedPath);
if (params !== false) {
return {
methods: entry.methods,
normalizedPath: entry.normalizedPath,
params: decodeRouteParams(params),
};
}
}
return undefined;
}
export class MalformedRouteParamError extends Error {
constructor(param: string) {
super(`Malformed percent-encoding in route parameter: ${param}`);
this.name = "MalformedRouteParamError";
}
}
function strictDecodeURIComponent(value: string): string {
try {
return decodeURIComponent(value);
} catch {
throw new MalformedRouteParamError(value);
}
}
function decodeRouteParams(params: Record<string, string | string[]>): Record<string, string | string[]> {
return Object.fromEntries(
Object.entries(params).map(([key, value]) => [
key,
Array.isArray(value) ? value.map(strictDecodeURIComponent) : strictDecodeURIComponent(value),
]),
);
}
function buildRouteRegistry() {
return routeModules
.map((route) => {
const methods = new Map<HttpMethod, RouteHandler>();
for (const method of httpMethodNames) {
const handler = route.module[method];
if (isRouteFunction(handler)) {
methods.set(method, createRouteHandler(route.normalizedPath, method, handler));
}
}
return {
methods,
normalizedPath: route.normalizedPath,
specificity: getSpecificity(route.normalizedPath),
};
})
.filter((route) => route.methods.size > 0)
.sort(compareRouteEntries);
}
function isRouteFunction(value: unknown): value is UnknownRouteFunction {
return typeof value === "function";
}
function createRouteHandler(normalizedPath: string, method: HttpMethod, handler: UnknownRouteFunction): RouteHandler {
return async (request, options) => {
const result = await handler(request, options);
if (result instanceof Response) {
return result;
}
throw new Error(`Route ${normalizedPath} ${method} did not return a Response`);
};
}
function compareRouteEntries(a: RouteEntry, b: RouteEntry) {
const maxLength = Math.max(a.specificity.length, b.specificity.length);
for (let i = 0; i < maxLength; i++) {
const diff = (b.specificity[i] ?? 0) - (a.specificity[i] ?? 0);
if (diff !== 0) {
return diff;
}
}
return stringCompare(a.normalizedPath, b.normalizedPath);
}
function getSpecificity(normalizedPath: string) {
return normalizedPath.split("/").filter(Boolean).map((segment) => {
if (segment.startsWith("[[...") && segment.endsWith("]]")) {
return 0;
}
if (segment.startsWith("[...") && segment.endsWith("]")) {
return 1;
}
if (segment.startsWith("[") && segment.endsWith("]")) {
return 2;
}
return 3;
});
}
function stringCompare(a: string, b: string) {
return a < b ? -1 : a > b ? 1 : 0;
}

View File

@ -1,21 +0,0 @@
import "@/instrument";
import "@/polyfills";
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
import { runAsynchronously } from "@hexclave/shared/dist/utils/promises";
import { app } from "./app";
import "./env-expand";
const portPrefix = getEnvVariable("NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX", "81");
const port = Number(getEnvVariable("PORT", getEnvVariable("BACKEND_PORT", `${portPrefix}02`)));
const hostname = getEnvVariable("HOSTNAME", "0.0.0.0");
app.listen({
hostname,
port,
});
console.log(`Hexclave backend listening on http://${hostname}:${port}`);
process.once("SIGTERM", () => {
process.exit(0);
});

View File

@ -1,5 +0,0 @@
import "@/instrument";
import "@/polyfills";
import { app } from "./app";
export default app;

View File

@ -103,9 +103,11 @@ function parseApiVersionStringToArray(version: string): [number, number] {
function matchPath(path: string, toMatchWith: string): Record<string, string | string[]> | false {
// get the relative part, and modify it to have a leading slash, without a trailing slash, without ./.., etc.
const url = new URL(path + "/", "http://example.com");
const modifiedPath = url.pathname.slice(1, -1);
// Normalize to relative path segments without a trailing slash. Appending "/" before URL parsing
// turns an already-trailing path into "//", which URL interprets as a protocol-relative URL during
// the final recursive match and rejects because it has no host.
const url = new URL(path === "" ? "/" : path, "http://example.com");
const modifiedPath = url.pathname.slice(1).replace(/\/+$/, "");
const modifiedToMatchWith = toMatchWith.slice(1);
if (modifiedPath === "" && modifiedToMatchWith === "") {
@ -146,6 +148,20 @@ function matchPath(path: string, toMatchWith: string): Record<string, string | s
}
}
import.meta.vitest?.test("matches routes with trailing slashes", ({ expect }) => {
expect({
staticRoute: SmartRouter.matchNormalizedPath("/api/v1/", "/api/v1"),
dynamicRoute: SmartRouter.matchNormalizedPath("/users/user-123/", "/users/[user_id]"),
}).toMatchInlineSnapshot(`
{
"dynamicRoute": {
"user_id": "user-123",
},
"staticRoute": {},
}
`);
});
/**
* Modified from: https://github.com/vercel/next.js/blob/6ff13369bb18045657d0f84ddc86b540340603a1/packages/next/src/shared/lib/router/utils/app-paths.ts#L23
*/

View File

@ -6,7 +6,7 @@
"dom.iterable",
"esnext"
],
"allowJs": false,
"allowJs": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
@ -17,6 +17,11 @@
"jsx": "react-jsx",
"incremental": true,
"noErrorTruncation": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./src/*"
@ -28,15 +33,16 @@
]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.?ts",
"**/*.?tsx"
"**/*.?tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"api",
"node_modules",
".next/dev",
"dist"
".next/dev"
]
}

View File

@ -1,110 +0,0 @@
import { sentryRollupPlugin } from "@sentry/rollup-plugin";
import { readFileSync } from "node:fs";
import { builtinModules } from "node:module";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig, type Rolldown, type UserConfig } from "tsdown";
// @ts-expect-error - this is a workspace tsdown helper imported from source.
import { createBasePlugin } from "../../configs/tsdown/plugins.ts";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const backendDir = __dirname;
const packageJson = JSON.parse(readFileSync(resolve(backendDir, "package.json"), "utf-8"));
const externalPackages = [
"@prisma/client",
"@prisma/adapter-neon",
"@prisma/adapter-pg",
"@prisma/instrumentation",
"@prisma/extension-read-replicas",
"@prisma/client/runtime/library",
"@prisma/client/runtime/client",
"@prisma/client/runtime/edge",
"bcrypt",
"oidc-provider",
"pg",
"sharp",
// @aws-sdk and @smithy use complex class hierarchies that rolldown mis-scopes
// when bundled, emitting references to hoisted classes before they're defined
// (e.g. "ReferenceError: StructureSchema$1 is not defined" when the server
// bundle boots via `node dist/server.mjs`). Keep them external so Node resolves
// them from node_modules at runtime, which is always present alongside the
// bundle (Docker image, CI runner, Vercel NFT trace). Mirrors the same fix in
// scripts/db-migrations.tsdown.config.ts.
"@aws-sdk",
"@smithy",
];
const nodeBuiltins = builtinModules.flatMap((moduleName) => [moduleName, `node:${moduleName}`]);
const customNoExternal = new Set([
...Object.keys(packageJson.dependencies).filter(
(dep) => !externalPackages.some((externalPackage) => dep === externalPackage || dep.startsWith(externalPackage + "/"))
),
]);
function packageNameFromSpecifier(specifier: string) {
if (specifier.startsWith("@")) {
const parts = specifier.split("/");
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;
}
return specifier.split("/")[0] ?? specifier;
}
function shouldBundleDependency(specifier: string) {
return customNoExternal.has(packageNameFromSpecifier(specifier));
}
const basePlugin: Rolldown.Plugin = createBasePlugin({});
// Sentry release names may not contain slashes/whitespace, so sanitize the scoped package name.
const sentryRelease = process.env.SENTRY_RELEASE ?? `${packageJson.name}@${packageJson.version}`.replace(/[/\s]/g, "-");
const shouldUploadSourcemaps = process.env.SENTRY_ORG != null
&& process.env.SENTRY_PROJECT != null
&& process.env.SENTRY_AUTH_TOKEN != null;
const plugins = [
basePlugin,
...(shouldUploadSourcemaps ? [
sentryRollupPlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
release: {
name: sentryRelease,
},
sourcemaps: {
assets: resolve(backendDir, "dist/**/*.map"),
},
}),
] : []),
];
export default defineConfig({
entry: [
resolve(backendDir, "src/server/server.ts"),
resolve(backendDir, "src/server/vercel.ts"),
],
format: ["esm"],
outDir: resolve(backendDir, "dist"),
target: "node22",
platform: "node",
noExternal: shouldBundleDependency,
inlineOnly: false,
external: [...nodeBuiltins, ...externalPackages],
clean: true,
minify: false,
sourcemap: true,
alias: {
"@": resolve(backendDir, "src"),
},
banner: {
js: `import { createRequire as __createRequire } from 'module';
import { fileURLToPath as __fileURLToPath } from 'url';
import { dirname as __dirname_fn } from 'path';
const __filename = __fileURLToPath(import.meta.url);
const __dirname = __dirname_fn(__filename);
const require = __createRequire(import.meta.url);`,
},
plugins,
} satisfies UserConfig);

View File

@ -1,9 +1,5 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": null,
"rewrites": [
{ "source": "/(.*)", "destination": "/api" }
],
"crons": [
{
"path": "/api/latest/internal/email-queue-step",

View File

@ -17,7 +17,7 @@ export default mergeConfig(
},
resolve: {
alias: {
'@': resolve(__dirname, './src'),
'@': resolve(__dirname, './src')
}
},
envDir: __dirname,

View File

@ -10,6 +10,21 @@ describe("without project ID", () => {
it("should load", async ({ expect }) => {
const response = await niceBackendFetch("/api/v1");
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": deindent\`
Welcome to the Hexclave API endpoint! Please refer to the documentation at https://docs.hexclave.com.
Authentication: None
\`,
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should load with a trailing slash", async ({ expect }) => {
const response = await niceBackendFetch("/api/v1/");
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,

View File

@ -52,9 +52,11 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
COPY --from=pruner /app/out/full/ .
# Docs are required for backend OpenAPI/codegen
# Docs are required for the NextJS backend build
COPY docs ./docs
ENV NEXT_CONFIG_OUTPUT=standalone
# Build backend only
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm turbo run docker-build --filter=@hexclave/backend...
@ -69,14 +71,10 @@ RUN apt-get update && \
apt-get install -y --no-install-recommends openssl && \
rm -rf /var/lib/apt/lists/*
# Copy the bundled Elysia server plus the pruned workspace/runtime deps that
# tsdown externalizes for native packages, Prisma, OpenTelemetry, and SDK calls.
COPY --from=builder --chown=node:node /app/node_modules ./node_modules
COPY --from=builder --chown=node:node /app/package.json ./package.json
COPY --from=builder --chown=node:node /app/pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY --from=builder --chown=node:node /app/apps/backend ./apps/backend
COPY --from=builder --chown=node:node /app/packages ./packages
COPY --from=builder --chown=node:node /app/configs ./configs
# Copy Next.js standalone output — this includes a traced, minimal copy of
# node_modules/ and packages/ (only the files the server actually imports).
COPY --from=builder --chown=node:node /app/apps/backend/.next/standalone ./
COPY --from=builder --chown=node:node /app/apps/backend/.next/static ./apps/backend/.next/static
# Prisma schema (needed at runtime by Prisma client)
COPY --from=builder --chown=node:node /app/apps/backend/prisma ./apps/backend/prisma
@ -89,4 +87,4 @@ USER node
EXPOSE 8102
CMD ["node", "apps/backend/dist/server.mjs"]
CMD ["node", "apps/backend/server.js"]

View File

@ -49,13 +49,13 @@ 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/ .
# Docs are required for backend OpenAPI/codegen
# docs are currently required for the NextJS backend build, but won't exist in the final image
COPY docs ./docs
# Dashboard still needs standalone output for its Next.js build
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
ENV NEXT_CONFIG_OUTPUT=standalone
# Build the backend and dashboard
# Build the backend NextJS app
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
@ -74,7 +74,14 @@ RUN apt-get update && \
apt-get install -y openssl postgresql-client socat && \
rm -rf /var/lib/apt/lists
# Copy built dashboard (standalone includes monorepo apps/ structure)
# Copy built backend
COPY --from=builder --chown=node:node /app/apps/backend/.next/standalone ./
COPY --from=builder --chown=node:node /app/apps/backend/.next/static ./apps/backend/.next/static
COPY --from=builder --chown=node:node /app/apps/backend/prisma ./apps/backend/prisma
COPY --from=builder --chown=node:node /app/apps/backend/dist ./apps/backend/dist
COPY --from=builder --chown=node:node /app/apps/backend/node_modules ./apps/backend/node_modules
# Copy built dashboard
COPY --from=builder --chown=node:node /app/apps/dashboard/.next/standalone ./
COPY --from=builder --chown=node:node /app/apps/dashboard/.next/static ./apps/dashboard/.next/static
COPY --from=builder --chown=node:node /app/apps/dashboard/public ./apps/dashboard/public
@ -83,15 +90,6 @@ COPY --from=builder --chown=node:node /app/apps/dashboard/public ./apps/dashboar
COPY --from=builder --chown=node:node /app/node_modules ./node_modules
COPY --from=builder --chown=node:node /app/packages ./packages
# Copy the bundled Elysia backend LAST — the dashboard standalone output includes
# a traced subset of apps/backend/ via pnpm workspace symlinks, and Docker COPY
# creates opaque directory layers that hide files from earlier layers. By copying
# the real backend dist after everything else, its files are guaranteed visible.
COPY --from=builder --chown=node:node /app/apps/backend/dist ./apps/backend/dist
COPY --from=builder --chown=node:node /app/apps/backend/prisma ./apps/backend/prisma
COPY --from=builder --chown=node:node /app/apps/backend/node_modules ./apps/backend/node_modules
COPY --from=builder --chown=node:node /app/apps/backend/package.json ./apps/backend/package.json
# Add the entrypoint script
COPY ./docker/server/entrypoint.sh .
RUN chmod +x entrypoint.sh

View File

@ -204,7 +204,7 @@ fi
echo "Starting backend on port $BACKEND_PORT..."
cd "$WORK_DIR"
PORT=$BACKEND_PORT HOSTNAME=0.0.0.0 node apps/backend/dist/server.mjs &
PORT=$BACKEND_PORT HOSTNAME=0.0.0.0 node apps/backend/server.js &
echo "Starting dashboard on port $DASHBOARD_PORT..."
PORT=$DASHBOARD_PORT HOSTNAME=0.0.0.0 node apps/dashboard/server.js &

View File

@ -114,9 +114,6 @@ importers:
'@clickhouse/client':
specifier: ^1.14.0
version: 1.16.0
'@elysiajs/node':
specifier: ^1.4.5
version: 1.4.5(elysia@1.4.28(@sinclair/typebox@0.34.49)(exact-mirror@1.1.1)(file-type@22.0.1)(openapi-types@12.1.3)(typescript@6.0.3))
'@hexclave/next':
specifier: workspace:*
version: link:../../packages/next
@ -159,9 +156,6 @@ importers:
'@opentelemetry/sdk-logs':
specifier: ^0.53.0
version: 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-node':
specifier: 0.53.0
version: 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base':
specifier: ^1.26.0
version: 1.26.0(@opentelemetry/api@1.9.0)
@ -186,27 +180,21 @@ importers:
'@prisma/instrumentation':
specifier: ^7.0.0
version: 7.1.0(@opentelemetry/api@1.9.0)
'@sentry/browser':
'@sentry/nextjs':
specifier: ^10.45.0
version: 10.45.0
'@sentry/node':
specifier: ^10.45.0
version: 10.45.0
'@sentry/rollup-plugin':
specifier: ^4.6.1
version: 4.9.1(rollup@4.57.1)
version: 10.45.0(@opentelemetry/context-async-hooks@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2))
'@simplewebauthn/server':
specifier: ^13.3.0
version: 13.3.0
'@sinclair/typebox':
specifier: ^0.34.49
version: 0.34.49
'@upstash/qstash':
specifier: ^2.8.2
version: 2.8.2
'@vercel/functions':
specifier: ^2.0.0
version: 2.0.0(@aws-sdk/credential-provider-web-identity@3.972.27)
'@vercel/otel':
specifier: ^1.10.4
version: 1.10.4(@opentelemetry/api-logs@0.53.0)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.53.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-logs@0.53.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))
'@vercel/sandbox':
specifier: ^1.2.0
version: 1.2.0
@ -228,9 +216,6 @@ importers:
dotenv-cli:
specifier: ^7.3.0
version: 7.4.1
elysia:
specifier: 1.4.28
version: 1.4.28(@sinclair/typebox@0.34.49)(exact-mirror@1.1.1)(file-type@22.0.1)(openapi-types@12.1.3)(typescript@6.0.3)
emailable:
specifier: ^3.1.1
version: 3.1.1
@ -1673,10 +1658,10 @@ importers:
version: link:../../packages/next
'@supabase/ssr':
specifier: latest
version: 0.12.0(@supabase/supabase-js@2.110.0)
version: 0.12.0(@supabase/supabase-js@2.108.2)
'@supabase/supabase-js':
specifier: latest
version: 2.110.0
version: 2.108.2
jose:
specifier: ^5.2.2
version: 5.6.3
@ -5866,12 +5851,6 @@ packages:
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/exporter-logs-otlp-grpc@0.53.0':
resolution: {integrity: sha512-x5ygAQgWAQOI+UOhyV3z9eW7QU2dCfnfOuIBiyYmC2AWr74f6x/3JBnP27IAcEx6aihpqBYWKnpoUTztkVPAZw==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': ^1.0.0
'@opentelemetry/exporter-logs-otlp-http@0.200.0':
resolution: {integrity: sha512-KfWw49htbGGp9s8N4KI8EQ9XuqKJ0VG+yVYVYFiCYSjEV32qpQ5qZ9UZBzOZ6xRb+E16SXOSCT3RkqBVSABZ+g==}
engines: {node: ^18.19.0 || >=20.6.0}
@ -5890,12 +5869,6 @@ packages:
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/exporter-logs-otlp-http@0.53.0':
resolution: {integrity: sha512-cSRKgD/n8rb+Yd+Cif6EnHEL/VZg1o8lEcEwFji1lwene6BdH51Zh3feAD9p2TyVoBKrl6Q9Zm2WltSp2k9gWQ==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': ^1.0.0
'@opentelemetry/exporter-logs-otlp-proto@0.200.0':
resolution: {integrity: sha512-GmahpUU/55hxfH4TP77ChOfftADsCq/nuri73I/AVLe2s4NIglvTsaACkFVZAVmnXXyPS00Fk3x27WS3yO07zA==}
engines: {node: ^18.19.0 || >=20.6.0}
@ -5908,12 +5881,6 @@ packages:
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/exporter-logs-otlp-proto@0.53.0':
resolution: {integrity: sha512-jhEcVL1deeWNmTUP05UZMriZPSWUBcfg94ng7JuBb1q2NExgnADQFl1VQQ+xo62/JepK+MxQe4xAwlsDQFbISA==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': ^1.0.0
'@opentelemetry/exporter-metrics-otlp-grpc@0.200.0':
resolution: {integrity: sha512-uHawPRvKIrhqH09GloTuYeq2BjyieYHIpiklOvxm9zhrCL2eRsnI/6g9v2BZTVtGp8tEgIa7rCQ6Ltxw6NBgew==}
engines: {node: ^18.19.0 || >=20.6.0}
@ -5974,12 +5941,6 @@ packages:
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/exporter-trace-otlp-grpc@0.53.0':
resolution: {integrity: sha512-m6KSh6OBDwfDjpzPVbuJbMgMbkoZfpxYH2r262KckgX9cMYvooWXEKzlJYsNDC6ADr28A1rtRoUVRwNfIN4tUg==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': ^1.0.0
'@opentelemetry/exporter-trace-otlp-http@0.200.0':
resolution: {integrity: sha512-Goi//m/7ZHeUedxTGVmEzH19NgqJY+Bzr6zXo1Rni1+hwqaksEyJ44gdlEMREu6dzX1DlAaH/qSykSVzdrdafA==}
engines: {node: ^18.19.0 || >=20.6.0}
@ -6010,18 +5971,6 @@ packages:
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/exporter-trace-otlp-proto@0.53.0':
resolution: {integrity: sha512-T/bdXslwRKj23S96qbvGtaYOdfyew3TjPEKOk5mHjkCmkVl1O9C/YMdejwSsdLdOq2YW30KjR9kVi0YMxZushQ==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': ^1.0.0
'@opentelemetry/exporter-zipkin@1.26.0':
resolution: {integrity: sha512-PW5R34n3SJHO4t0UetyHKiXL6LixIqWN6lWncg3eRXhKuT30x+b7m5sDJS0kEWRfHeS+kG7uCw2vBzmB2lk3Dw==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': ^1.0.0
'@opentelemetry/exporter-zipkin@2.0.0':
resolution: {integrity: sha512-icxaKZ+jZL/NHXX8Aru4HGsrdhK0MLcuRXkX5G5IRmCgoRLw+Br6I/nMVozX2xjGGwV7hw2g+4Slj8K7s4HbVg==}
engines: {node: ^18.19.0 || >=20.6.0}
@ -6491,12 +6440,6 @@ packages:
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/otlp-grpc-exporter-base@0.53.0':
resolution: {integrity: sha512-F7RCN8VN+lzSa4fGjewit8Z5fEUpY/lmMVy5EWn2ZpbAabg3EE3sCLuTNfOiooNGnmvzimUPruoeqeko/5/TzQ==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': ^1.0.0
'@opentelemetry/otlp-transformer@0.200.0':
resolution: {integrity: sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw==}
engines: {node: ^18.19.0 || >=20.6.0}
@ -6675,6 +6618,12 @@ packages:
peerDependencies:
'@opentelemetry/api': '>=1.9.0 <1.10.0'
'@opentelemetry/sdk-metrics@2.8.0':
resolution: {integrity: sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.9.0 <1.10.0'
'@opentelemetry/sdk-node@0.200.0':
resolution: {integrity: sha512-S/YSy9GIswnhYoDor1RusNkmRughipvTCOQrlF1dzI70yQaf68qgf5WMnzUxdlCl3/et/pvaO75xfPfuEmCK5A==}
engines: {node: ^18.19.0 || >=20.6.0}
@ -6687,12 +6636,6 @@ packages:
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.10.0'
'@opentelemetry/sdk-node@0.53.0':
resolution: {integrity: sha512-0hsxfq3BKy05xGktwG8YdGdxV978++x40EAKyKr1CaHZRh8uqVlXnclnl7OMi9xLMJEcXUw7lGhiRlArFcovyg==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.10.0'
'@opentelemetry/sdk-trace-base@1.26.0':
resolution: {integrity: sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==}
engines: {node: '>=14'}
@ -8445,9 +8388,9 @@ packages:
resolution: {integrity: sha512-OuxqBprXRyhe8Pkfyz/4yHQJc5c3lm+TmYWSSx8u48g5yKewSQDOxkiLU5pAk3WnbLPy8XwU/PN+2BG0YFU9Nw==}
engines: {node: '>= 14'}
'@sentry/babel-plugin-component-annotate@4.9.1':
resolution: {integrity: sha512-0gEoi2Lb54MFYPOmdTfxlNKxI7kCOvNV7gP8lxMXJ7nCazF5OqOOZIVshfWjDLrc0QrSV6XdVvwPV9GDn4wBMg==}
engines: {node: '>= 14'}
'@sentry/babel-plugin-component-annotate@5.1.1':
resolution: {integrity: sha512-x2wEpBHwsTyTF2rWsLKJlzrRF1TTIGOfX+ngdE+Yd5DBkoS58HwQv824QOviPGQRla4/ypISqAXzjdDPL/zalg==}
engines: {node: '>= 18'}
'@sentry/browser@10.11.0':
resolution: {integrity: sha512-qemaKCJKJHHCyGBpdLq23xL5u9Xvir20XN7YFTnHcEq4Jvj0GoWsslxKi5cQB2JvpYn62WxTiDgVLeQlleZhSg==}
@ -8461,9 +8404,9 @@ packages:
resolution: {integrity: sha512-dmR4DJhJ4jqVWGWppuTL2blNFqOZZnt4aLkewbD1myFG3KVfUx8CrMQWEmGjkgPOtj5TO6xH9PyTJjXC6o5tnA==}
engines: {node: '>= 14'}
'@sentry/bundler-plugin-core@4.9.1':
resolution: {integrity: sha512-moii+w7N8k8WdvkX7qCDY9iRBlhgHlhTHTUQwF2FNMhBHuqlNpVcSJJqJMjFUQcjYMBDrZgxhfKV18bt5ixwlQ==}
engines: {node: '>= 14'}
'@sentry/bundler-plugin-core@5.1.1':
resolution: {integrity: sha512-F+itpwR9DyQR7gEkrXd2tigREPTvtF5lC8qu6e4anxXYRTui1+dVR0fXNwjpyAZMhIesLfXRN7WY7ggdj7hi0Q==}
engines: {node: '>= 18'}
'@sentry/cli-darwin@2.53.0':
resolution: {integrity: sha512-NNPfpILMwKgpHiyJubHHuauMKltkrgLQ5tvMdxNpxY60jBNdo5VJtpESp4XmXlnidzV4j1z61V4ozU6ttDgt5Q==}
@ -8583,6 +8526,12 @@ packages:
peerDependencies:
next: ^13.2.0 || ^14.0 || ^15.0.0-rc.0
'@sentry/nextjs@10.45.0':
resolution: {integrity: sha512-4LE+UvnfdOYyG8YEb/9TWaJQzMPuGLlph/iqowvsMdxaW6la+mvADiuzNTXly4QfsjeD3KIb7dKlGTqiVV0Ttw==}
engines: {node: '>=18'}
peerDependencies:
next: ^13.2.0 || ^14.0 || ^15.0.0-rc.0 || ^16.0.0-0
'@sentry/node-core@10.11.0':
resolution: {integrity: sha512-dkVZ06F+W5W0CsD47ATTTOTTocmccT/ezrF9idspQq+HVOcjoKSU60WpWo22NjtVNdSYKLnom0q1LKRoaRA/Ww==}
engines: {node: '>=18'}
@ -8656,22 +8605,32 @@ packages:
peerDependencies:
react: ^16.14.0 || 17.x || 18.x || 19.x
'@sentry/rollup-plugin@4.9.1':
resolution: {integrity: sha512-cjr/S4I9Xj4481K0o47PuteGgqFBs3hRRPOFs1Vz1Jql6VeCm5+ghbtsLGQMwB2lCOpWcYggdc03JK67UJT4ZQ==}
engines: {node: '>= 14'}
'@sentry/react@10.45.0':
resolution: {integrity: sha512-jLezuxi4BUIU3raKyAPR5xMbQG/nhwnWmKo5p11NCbLmWzkS+lxoyDTUB4B8TAKZLfdtdkKLOn1S0tFc8vbUHw==}
engines: {node: '>=18'}
peerDependencies:
rollup: '>=3.2.0'
react: ^16.14.0 || 17.x || 18.x || 19.x
'@sentry/vercel-edge@10.11.0':
resolution: {integrity: sha512-jAsJ8RbbF2JWj2wnXfd6BwWxCR6GBITMtlaoWc7pG22HknEtoH15dKsQC3Ew5r/KRcofr2e+ywdnBn5CPr1Pbg==}
engines: {node: '>=18'}
'@sentry/vercel-edge@10.45.0':
resolution: {integrity: sha512-sSF+Ex5NwT60gMinLcP/JNZb3cDaIv0mL1cRjfvN6zN2ZNEw0C9rhdgxa0EdD4G6PCHQ0XnCuAMDsfJ6gnRmfA==}
engines: {node: '>=18'}
'@sentry/webpack-plugin@4.3.0':
resolution: {integrity: sha512-K4nU1SheK/tvyakBws2zfd+MN6hzmpW+wPTbSbDWn1+WL9+g9hsPh8hjFFiVe47AhhUoUZ3YgiH2HyeHXjHflA==}
engines: {node: '>= 14'}
peerDependencies:
webpack: '>=4.40.0'
'@sentry/webpack-plugin@5.1.1':
resolution: {integrity: sha512-XgQg+t2aVrlQDfIiAEizqR/bsy6GtBygwgR+Kw11P/cYczj4W9PZ2IYqQEStBzHqnRTh5DbpyMcUNW2CujdA9A==}
engines: {node: '>= 18'}
peerDependencies:
webpack: '>=5.0.0'
'@shikijs/core@3.14.0':
resolution: {integrity: sha512-qRSeuP5vlYHCNUIrpEBQFO7vSkR7jn7Kv+5X3FO/zBKVDGQbcnlScD3XhkrHi/R8Ltz0kEjvFR9Szp/XMRbFMw==}
@ -9229,37 +9188,37 @@ packages:
resolution: {integrity: sha512-SXuhqhuR5FXaYgKTXzZJeqtVA6JKb9IZWaGeEUxHHiOcFy2p51wccO72bYpXwoK4D5pzQOIYLTuAc7etxyMmwg==}
engines: {node: '>=12.16'}
'@supabase/auth-js@2.110.0':
resolution: {integrity: sha512-Mi288WCTp6wxMFCOu/UgzgHEXODjdl2uVTLqK11eanzGZaldU3RyP8Am+ZbNuVzFP+5+iOvppxzv7N5Ym84xTg==}
engines: {node: '>=22.0.0'}
'@supabase/auth-js@2.108.2':
resolution: {integrity: sha512-tNaQmBgodDZwgB40mRwVbxFy8IDYwjdpcZ0BYrWiwlULCSQoJj4QoG4zgJT7QRPXcqipefNOzvO/qAu4dF98ag==}
engines: {node: '>=20.0.0'}
'@supabase/functions-js@2.110.0':
resolution: {integrity: sha512-Fde5wlY8ZZy+9yqrWlQHo8MacSyUBArBEtN2boB4thJQigPnQD/cc61qZN0n3I1L0gwhWtHYwIMnOBKxSvF6Hw==}
engines: {node: '>=22.0.0'}
'@supabase/functions-js@2.108.2':
resolution: {integrity: sha512-RNUX8EiBy3iLwAX19jtRzLyePnl11/fHcgwDHLnpKcDSXt/5qBnh3LUwAtIjT21Q66QsmNUR2esrHziLCpNubw==}
engines: {node: '>=20.0.0'}
'@supabase/phoenix@0.4.4':
resolution: {integrity: sha512-Gt0pqoXuIqX/8dvG0OKp/wMCobXNH3klNbUPBNyOfN0YA1IswrM3HyWFMOPk1Jy+BRaIyDPcFx4jLBwHNmlyfQ==}
'@supabase/phoenix@0.4.2':
resolution: {integrity: sha512-YSAGnmDAfuleFCVt3CeurQZAhxRfXWeZIIkwp7NhYzQ1UwW6ePSnzsFAiUm/mbCkfoCf70QQHKW/K6RKh52a4A==}
'@supabase/postgrest-js@2.110.0':
resolution: {integrity: sha512-ZbC1QZL3jcvBUfVKjJbgRM27G4Mg3Zzqdm44m5pJafe1e52Cli793EOnwQucomBAGEUDd03Nzaf7XV3ji/XexQ==}
engines: {node: '>=22.0.0'}
'@supabase/postgrest-js@2.108.2':
resolution: {integrity: sha512-GQ28/Y8hk3CFmkb3kXH1h/AQx6JIYSQfO0CJMRVBcEKZoNy6C45cXAZ4fcJvRC5Id0cs6xnkUV0+c0rIocigsw==}
engines: {node: '>=20.0.0'}
'@supabase/realtime-js@2.110.0':
resolution: {integrity: sha512-Wn2AWpneZuDFTkp/65tqctvoh+3JvyTjMam8sTMqVWy5BgkU8zAvFwilPYPPPhkINeKF8NAJKP7FclJ2iGCUMw==}
engines: {node: '>=22.0.0'}
'@supabase/realtime-js@2.108.2':
resolution: {integrity: sha512-aAGxCSUemZvQIibnCdvNvgaKib28I4rfrNjKbQ9cG1uBLwUsI7hVpGXgEbypCCDhLjQlDTAiJlu7rgljYUT73g==}
engines: {node: '>=20.0.0'}
'@supabase/ssr@0.12.0':
resolution: {integrity: sha512-d9XV5XzJvzzZbeAIM7fWTCUYxQJZ2Ru6ny3dJHmHGp/LIrJ+o9FpD7N9Rf/UhhWEvHXSoDe8SI32Z2ouOdMjBg==}
peerDependencies:
'@supabase/supabase-js': ^2.108.0
'@supabase/storage-js@2.110.0':
resolution: {integrity: sha512-71+gU3HrhiylAhftY6FmO5PPdcsScnVcS766CVD+vTYK9qTDLbrx8FhgBYbqGm3iV/wkTfzrNJfjGsMeFRkJRQ==}
engines: {node: '>=22.0.0'}
'@supabase/storage-js@2.108.2':
resolution: {integrity: sha512-TVZPQxXGxY2+A6yTtm77zUHsh70lBhYUEaJL8RQC+BghcX/ygiMG/rmXrNVBce30/WAeNPa8FiG8HbqlGeV05g==}
engines: {node: '>=20.0.0'}
'@supabase/supabase-js@2.110.0':
resolution: {integrity: sha512-8yI84VJiEVW4zxZpLUmxXmjzQ7O2St9X/ymzlBETDHTURPWG3LmvbSiibq+7dqAJmyoUfxZnSfXeM4HCM8s4XQ==}
engines: {node: '>=22.0.0'}
'@supabase/supabase-js@2.108.2':
resolution: {integrity: sha512-hFhnPveb5JQg4a0QYicM0swT253YHMdfeRAl2BKHOlI5VAzuHxUGSr8RbwNLYNPauWOgQMS1H8sz8bvYlgwUfQ==}
engines: {node: '>=20.0.0'}
'@swc/counter@0.1.3':
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
@ -10183,6 +10142,18 @@ packages:
resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==}
engines: {node: '>= 20'}
'@vercel/otel@1.10.4':
resolution: {integrity: sha512-X01cOLU2Aiku4y1Vn9HQ8VmkQEdIQiWhjHa7DEUspcNGNg0Xm7GYAU7ErZ0pH/5spsrvb2wpIIHSBFgs0M5p4g==}
engines: {node: '>=18'}
peerDependencies:
'@opentelemetry/api': ^1.7.0
'@opentelemetry/api-logs': '>=0.46.0 && <1.0.0'
'@opentelemetry/instrumentation': '>=0.46.0 && <1.0.0'
'@opentelemetry/resources': ^1.19.0
'@opentelemetry/sdk-logs': '>=0.46.0 && <1.0.0'
'@opentelemetry/sdk-metrics': ^1.19.0
'@opentelemetry/sdk-trace-base': ^1.19.0
'@vercel/sandbox@1.2.0':
resolution: {integrity: sha512-oq0d2xQuiDq8LyoZOZW+i+QmNkMQ0/ddEGHMukBsF+cxX7ohBEXOV/a0+SopBwEsKCjq9j55QFP56G3MU/iEjA==}
@ -13044,11 +13015,6 @@ packages:
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@10.5.0:
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@13.0.6:
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
engines: {node: 18 || 20 || >=22}
@ -20400,12 +20366,6 @@ snapshots:
elysia: 1.4.28(@sinclair/typebox@0.34.49)(exact-mirror@1.1.1)(file-type@22.0.1)(openapi-types@12.1.3)(typescript@5.9.3)
srvx: 0.11.15
'@elysiajs/node@1.4.5(elysia@1.4.28(@sinclair/typebox@0.34.49)(exact-mirror@1.1.1)(file-type@22.0.1)(openapi-types@12.1.3)(typescript@6.0.3))':
dependencies:
crossws: 0.4.4(srvx@0.11.15)
elysia: 1.4.28(@sinclair/typebox@0.34.49)(exact-mirror@1.1.1)(file-type@22.0.1)(openapi-types@12.1.3)(typescript@6.0.3)
srvx: 0.11.15
'@emnapi/core@1.11.1':
dependencies:
'@emnapi/wasi-threads': 1.2.2
@ -22568,15 +22528,6 @@ snapshots:
'@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-logs': 0.213.0(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-logs-otlp-grpc@0.53.0(@opentelemetry/api@1.9.0)':
dependencies:
'@grpc/grpc-js': 1.14.3
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-grpc-exporter-base': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-transformer': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-logs': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-logs-otlp-http@0.200.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
@ -22604,15 +22555,6 @@ snapshots:
'@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-logs': 0.213.0(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-logs-otlp-http@0.53.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/api-logs': 0.53.0
'@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-exporter-base': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-transformer': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-logs': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-logs-otlp-proto@0.200.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
@ -22635,17 +22577,6 @@ snapshots:
'@opentelemetry/sdk-logs': 0.213.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-logs-otlp-proto@0.53.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/api-logs': 0.53.0
'@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-exporter-base': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-transformer': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-logs': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-metrics-otlp-grpc@0.200.0(@opentelemetry/api@1.9.0)':
dependencies:
'@grpc/grpc-js': 1.14.3
@ -22745,16 +22676,6 @@ snapshots:
'@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-trace-otlp-grpc@0.53.0(@opentelemetry/api@1.9.0)':
dependencies:
'@grpc/grpc-js': 1.14.3
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-grpc-exporter-base': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-transformer': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-trace-otlp-http@0.200.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
@ -22800,23 +22721,6 @@ snapshots:
'@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-trace-otlp-proto@0.53.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-exporter-base': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-transformer': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-zipkin@1.26.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.27.0
'@opentelemetry/exporter-zipkin@2.0.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
@ -23480,14 +23384,6 @@ snapshots:
'@opentelemetry/otlp-exporter-base': 0.213.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-grpc-exporter-base@0.53.0(@opentelemetry/api@1.9.0)':
dependencies:
'@grpc/grpc-js': 1.14.3
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-exporter-base': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-transformer': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-transformer@0.200.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
@ -23688,6 +23584,12 @@ snapshots:
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-node@0.200.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
@ -23746,28 +23648,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@opentelemetry/sdk-node@0.53.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/api-logs': 0.53.0
'@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-logs-otlp-grpc': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-logs-otlp-http': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-logs-otlp-proto': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-trace-otlp-grpc': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-trace-otlp-http': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-trace-otlp-proto': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-zipkin': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-logs': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-metrics': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-node': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.27.0
transitivePeerDependencies:
- supports-color
'@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
@ -26374,6 +26254,18 @@ snapshots:
optionalDependencies:
rollup: 4.50.1
'@rollup/plugin-commonjs@28.0.1(rollup@4.57.1)':
dependencies:
'@rollup/pluginutils': 5.1.0(rollup@4.57.1)
commondir: 1.0.1
estree-walker: 2.0.2
fdir: 6.5.0(picomatch@4.0.3)
is-reference: 1.2.1
magic-string: 0.30.17
picomatch: 4.0.3
optionalDependencies:
rollup: 4.57.1
'@rollup/pluginutils@5.1.0(rollup@4.50.1)':
dependencies:
'@types/estree': 1.0.8
@ -26382,6 +26274,14 @@ snapshots:
optionalDependencies:
rollup: 4.50.1
'@rollup/pluginutils@5.1.0(rollup@4.57.1)':
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
picomatch: 2.3.1
optionalDependencies:
rollup: 4.57.1
'@rollup/rollup-android-arm-eabi@4.34.8':
optional: true
@ -26630,7 +26530,7 @@ snapshots:
'@sentry/babel-plugin-component-annotate@4.3.0': {}
'@sentry/babel-plugin-component-annotate@4.9.1': {}
'@sentry/babel-plugin-component-annotate@5.1.1': {}
'@sentry/browser@10.11.0':
dependencies:
@ -26662,16 +26562,15 @@ snapshots:
- encoding
- supports-color
'@sentry/bundler-plugin-core@4.9.1':
'@sentry/bundler-plugin-core@5.1.1':
dependencies:
'@babel/core': 7.29.0
'@sentry/babel-plugin-component-annotate': 4.9.1
'@sentry/babel-plugin-component-annotate': 5.1.1
'@sentry/cli': 2.58.5
dotenv: 16.6.1
find-up: 5.0.0
glob: 10.5.0
magic-string: 0.30.8
unplugin: 1.0.1
glob: 13.0.6
magic-string: 0.30.17
transitivePeerDependencies:
- encoding
- supports-color
@ -26795,6 +26694,31 @@ snapshots:
- supports-color
- webpack
'@sentry/nextjs@10.45.0(@opentelemetry/context-async-hooks@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2))':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/semantic-conventions': 1.40.0
'@rollup/plugin-commonjs': 28.0.1(rollup@4.57.1)
'@sentry-internal/browser-utils': 10.45.0
'@sentry/bundler-plugin-core': 5.1.1
'@sentry/core': 10.45.0
'@sentry/node': 10.45.0
'@sentry/opentelemetry': 10.45.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)
'@sentry/react': 10.45.0(react@19.2.3)
'@sentry/vercel-edge': 10.45.0
'@sentry/webpack-plugin': 5.1.1(webpack@5.92.0(esbuild@0.24.2))
next: 16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
rollup: 4.57.1
stacktrace-parser: 0.1.11
transitivePeerDependencies:
- '@opentelemetry/context-async-hooks'
- '@opentelemetry/core'
- '@opentelemetry/sdk-trace-base'
- encoding
- react
- supports-color
- webpack
'@sentry/node-core@10.11.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)':
dependencies:
'@opentelemetry/api': 1.9.0
@ -26920,6 +26844,15 @@ snapshots:
'@opentelemetry/semantic-conventions': 1.40.0
'@sentry/core': 10.11.0
'@sentry/opentelemetry@10.45.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/context-async-hooks': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.40.0
'@sentry/core': 10.45.0
'@sentry/opentelemetry@10.45.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)':
dependencies:
'@opentelemetry/api': 1.9.0
@ -26936,14 +26869,11 @@ snapshots:
hoist-non-react-statics: 3.3.2
react: 19.2.3
'@sentry/rollup-plugin@4.9.1(rollup@4.57.1)':
'@sentry/react@10.45.0(react@19.2.3)':
dependencies:
'@sentry/bundler-plugin-core': 4.9.1
rollup: 4.57.1
unplugin: 1.0.1
transitivePeerDependencies:
- encoding
- supports-color
'@sentry/browser': 10.45.0
'@sentry/core': 10.45.0
react: 19.2.3
'@sentry/vercel-edge@10.11.0':
dependencies:
@ -26951,6 +26881,12 @@ snapshots:
'@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.0)
'@sentry/core': 10.11.0
'@sentry/vercel-edge@10.45.0':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.0)
'@sentry/core': 10.45.0
'@sentry/webpack-plugin@4.3.0(webpack@5.92.0(esbuild@0.24.2))':
dependencies:
'@sentry/bundler-plugin-core': 4.3.0
@ -26961,6 +26897,15 @@ snapshots:
- encoding
- supports-color
'@sentry/webpack-plugin@5.1.1(webpack@5.92.0(esbuild@0.24.2))':
dependencies:
'@sentry/bundler-plugin-core': 5.1.1
uuid: 9.0.1
webpack: 5.92.0(esbuild@0.24.2)
transitivePeerDependencies:
- encoding
- supports-color
'@shikijs/core@3.14.0':
dependencies:
'@shikijs/types': 3.14.0
@ -27852,42 +27797,42 @@ snapshots:
'@stripe/stripe-js@7.7.0': {}
'@supabase/auth-js@2.110.0':
'@supabase/auth-js@2.108.2':
dependencies:
tslib: 2.8.1
'@supabase/functions-js@2.110.0':
'@supabase/functions-js@2.108.2':
dependencies:
tslib: 2.8.1
'@supabase/phoenix@0.4.4': {}
'@supabase/phoenix@0.4.2': {}
'@supabase/postgrest-js@2.110.0':
'@supabase/postgrest-js@2.108.2':
dependencies:
tslib: 2.8.1
'@supabase/realtime-js@2.110.0':
'@supabase/realtime-js@2.108.2':
dependencies:
'@supabase/phoenix': 0.4.4
'@supabase/phoenix': 0.4.2
tslib: 2.8.1
'@supabase/ssr@0.12.0(@supabase/supabase-js@2.110.0)':
'@supabase/ssr@0.12.0(@supabase/supabase-js@2.108.2)':
dependencies:
'@supabase/supabase-js': 2.110.0
'@supabase/supabase-js': 2.108.2
cookie: 1.0.2
'@supabase/storage-js@2.110.0':
'@supabase/storage-js@2.108.2':
dependencies:
iceberg-js: 0.8.1
tslib: 2.8.1
'@supabase/supabase-js@2.110.0':
'@supabase/supabase-js@2.108.2':
dependencies:
'@supabase/auth-js': 2.110.0
'@supabase/functions-js': 2.110.0
'@supabase/postgrest-js': 2.110.0
'@supabase/realtime-js': 2.110.0
'@supabase/storage-js': 2.110.0
'@supabase/auth-js': 2.108.2
'@supabase/functions-js': 2.108.2
'@supabase/postgrest-js': 2.108.2
'@supabase/realtime-js': 2.108.2
'@supabase/storage-js': 2.108.2
'@swc/counter@0.1.3': {}
@ -29314,6 +29259,16 @@ snapshots:
'@vercel/oidc@3.1.0': {}
'@vercel/otel@1.10.4(@opentelemetry/api-logs@0.53.0)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.53.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-logs@0.53.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/api-logs': 0.53.0
'@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-logs': 0.53.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-metrics': 2.8.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 1.26.0(@opentelemetry/api@1.9.0)
'@vercel/sandbox@1.2.0':
dependencies:
'@vercel/oidc': 3.1.0
@ -31313,18 +31268,6 @@ snapshots:
optionalDependencies:
typescript: 5.9.3
elysia@1.4.28(@sinclair/typebox@0.34.49)(exact-mirror@1.1.1)(file-type@22.0.1)(openapi-types@12.1.3)(typescript@6.0.3):
dependencies:
'@sinclair/typebox': 0.34.49
cookie: 1.1.1
exact-mirror: 1.1.1
fast-decode-uri-component: 1.0.1
file-type: 22.0.1
memoirist: 0.4.0
openapi-types: 12.1.3
optionalDependencies:
typescript: 6.0.3
emailable@3.1.1:
dependencies:
axios: 1.7.7
@ -33208,15 +33151,6 @@ snapshots:
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
glob@10.5.0:
dependencies:
foreground-child: 3.3.1
jackspeak: 3.4.3
minimatch: 9.0.5
minipass: 7.1.3
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
glob@13.0.6:
dependencies:
minimatch: 10.2.4