Merge remote-tracking branch 'origin/dev' into devin/1783369307-cli-auth-app

This commit is contained in:
Devin AI 2026-07-08 17:40:04 +00:00
commit 292859e921
51 changed files with 311 additions and 83 deletions

View File

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

View File

@ -12,9 +12,15 @@ const backendDir = resolve(__dirname, '..');
const packageJson = JSON.parse(readFileSync(resolve(backendDir, 'package.json'), 'utf-8'));
// Packages that must remain as runtime imports (can't be statically bundled)
// Packages that must remain as runtime imports (can't be statically bundled).
// @aws-sdk and @smithy use complex class hierarchies that rolldown mis-scopes,
// causing "StructureSchema$2 is not defined" at runtime. They're unused by the
// migration script anyway (pulled in transitively), and node_modules is present
// in the Docker image, so keeping them external is safe.
const externalPackages = [
'@prisma/client',
'@aws-sdk',
'@smithy',
];
const customNoExternal = new Set([

View File

@ -95,18 +95,24 @@ export const POST = createSmartRouteHandler({
customerId: req.params.customer_id,
});
// Read the current quantity from bulldozer-js BEFORE starting the Prisma
// transaction. getItemQuantityForCustomer reads via HTTP from the
// bulldozer-js server and does not use the Prisma client; keeping the
// call inside the transaction would hold the transaction open for the
// full HTTP round-trip, exhausting the interactive-transaction pool
// under load and causing "Unable to start a transaction" cascades.
const totalQuantity = await getItemQuantityForCustomer({
prisma,
tenancyId: tenancy.id,
itemId: req.params.item_id,
customerId: req.params.customer_id,
customerType: req.params.customer_type,
});
if (!allowNegative && (totalQuantity + req.body.delta < 0)) {
throw new KnownErrors.ItemQuantityInsufficientAmount(req.params.item_id, req.params.customer_id, req.body.delta);
}
const change = await retryTransaction(prisma, async (tx) => {
const totalQuantity = await getItemQuantityForCustomer({
prisma: tx,
tenancyId: tenancy.id,
itemId: req.params.item_id,
customerId: req.params.customer_id,
customerType: req.params.customer_type,
});
if (!allowNegative && (totalQuantity + req.body.delta < 0)) {
throw new KnownErrors.ItemQuantityInsufficientAmount(req.params.item_id, req.params.customer_id, req.body.delta);
}
// dual write - prisma and bulldozer
const change = await tx.itemQuantityChange.create({
data: {
tenancyId: tenancy.id,

View File

@ -105,31 +105,35 @@ export const POST = createSmartRouteHandler({
throw new KnownErrors.VerificationCodeNotFound();
}
// Read the seat cap from bulldozer-js BEFORE starting the Prisma
// transaction. getItemQuantityForCustomer reads via HTTP from the
// bulldozer-js server; keeping the call inside the transaction would
// hold it open for the full HTTP round-trip, risking transaction
// timeouts and connection-pool exhaustion.
let maxDashboardAdmins: number | undefined;
if (auth.tenancy.project.id === "internal" && arePlanLimitsEnforced()) {
try {
maxDashboardAdmins = await getItemQuantityForCustomer({
prisma,
tenancyId: auth.tenancy.id,
customerId: invitationData.team_id,
itemId: "dashboard_admins",
customerType: "team",
});
} catch (error) {
captureError("team-invitations:accept:dashboard-admins-seat-check", error);
maxDashboardAdmins = UNLIMITED_ITEM_CAPACITY;
}
}
await retryTransaction(prisma, async (tx) => {
if (auth.tenancy.project.id === "internal" && arePlanLimitsEnforced()) {
if (maxDashboardAdmins !== undefined) {
const currentMemberCount = await tx.teamMember.count({
where: {
tenancyId: auth.tenancy.id,
teamId: invitationData.team_id,
},
});
// The seat cap lives in bulldozer, but accepting a team invite must not
// hard-depend on it: if bulldozer is unreachable, report it and skip the
// check this once (treat the cap as unlimited) rather than blocking the
// invite. The cap is re-enforced on the next accept once bulldozer is back.
let maxDashboardAdmins: number;
try {
maxDashboardAdmins = await getItemQuantityForCustomer({
prisma: tx,
tenancyId: auth.tenancy.id,
customerId: invitationData.team_id,
itemId: "dashboard_admins",
customerType: "team",
});
} catch (error) {
captureError("team-invitations:accept:dashboard-admins-seat-check", error);
maxDashboardAdmins = UNLIMITED_ITEM_CAPACITY;
}
if (currentMemberCount + 1 > maxDashboardAdmins) {
throw new KnownErrors.ItemQuantityInsufficientAmount("dashboard_admins", invitationData.team_id, -1);
}

View File

@ -1,6 +1,6 @@
import { ITEM_IDS, UNLIMITED } from "@hexclave/shared/dist/plans";
import type { SubscriptionRow } from "./payments/schema/types";
import { buildUsageRow, getNextPlanId, getPlanUsagePeriod } from "./plan-usage";
import { buildUsageRow, getNextPlanId, getPlanUsagePeriod, readBillingSubscriptionMapOrSkip } from "./plan-usage";
import { describe, expect, it } from "vitest";
function createSubscriptionPeriod(startMillis: number, endMillis: number): SubscriptionRow {
@ -134,6 +134,32 @@ describe("plan upgrade targets", () => {
});
});
describe("readBillingSubscriptionMapOrSkip", () => {
it("returns the subscription map when the bulldozer read succeeds", async () => {
const start = Date.UTC(2026, 4, 15);
const end = Date.UTC(2026, 5, 15);
const subMap = { sub_1: createSubscriptionPeriod(start, end) };
const result = await readBillingSubscriptionMapOrSkip(() => Promise.resolve(subMap));
expect(result).toBe(subMap);
});
it("falls back to an empty map (free plan) instead of throwing when bulldozer is down", async () => {
// Regression: plan usage is read on the dashboard's own pages (overview
// limit banners, usage page). A bulldozer outage here used to bubble a 500
// and take the whole dashboard down. It must now degrade to "no
// subscription" (free plan) rather than failing the page.
const result = await readBillingSubscriptionMapOrSkip(() => {
throw new Error("bulldozer unreachable");
});
expect(result).toEqual({});
});
it("also swallows async rejections from the bulldozer read", async () => {
const result = await readBillingSubscriptionMapOrSkip(() => Promise.reject(new Error("bulldozer timed out")));
expect(result).toEqual({});
});
});
describe("billing period selection", () => {
it("uses the subscription period when available", () => {
const start = Date.UTC(2026, 4, 15);

View File

@ -12,7 +12,7 @@ import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch, getTenancy, type Te
import { getPrismaClientForTenancy, getPrismaSchemaForTenancy, globalPrismaClient, sqlQuoteIdent } from "@/prisma-client";
import { BASE_PLAN_IDS_BY_TIER, ITEM_IDS, PLAN_LIMITS, UNLIMITED, type ItemId, type PlanId } from "@hexclave/shared/dist/plans";
import type { PlanUsageResponse } from "@hexclave/shared/dist/interface/admin-interface";
import { HexclaveAssertionError, throwErr } from "@hexclave/shared/dist/utils/errors";
import { captureError, HexclaveAssertionError, throwErr } from "@hexclave/shared/dist/utils/errors";
import { mapWithConcurrency } from "@hexclave/shared/dist/utils/promises";
import type { SubscriptionRow } from "./payments/schema/types";
@ -136,6 +136,27 @@ function getUsageItemLabel(itemId: ItemId): string {
return USAGE_ITEM_LABELS.get(itemId) ?? throwErr(`Missing usage item label for ${itemId}`);
}
/**
* Reads the billing team's subscription map, but never lets a bulldozer outage
* break the caller. Plan usage is surfaced only on the dashboard's own
* (internal project) surfaces the overview/analytics limit banners and the
* usage page so a hard dependency on bulldozer here would take the whole
* dashboard down whenever bulldozer is unreachable. On failure we report the
* error and fall back to an empty subscription map, which resolves to the free
* plan: we skip whatever we would normally bill/enforce rather than failing the
* page. The real plan is re-resolved on the next request once bulldozer is back.
*/
export async function readBillingSubscriptionMapOrSkip(
read: () => Promise<Record<string, SubscriptionRow>>,
): Promise<Record<string, SubscriptionRow>> {
try {
return await read();
} catch (error) {
captureError("plan-usage:subscription-map-unavailable", error);
return {};
}
}
function resolveActivePlanSubscription(subscriptions: Record<string, SubscriptionRow>): SubscriptionRow | null {
const activeSubscriptions = Object.values(subscriptions).filter(isActiveSubscription);
for (const planId of BASE_PLAN_IDS_BY_TIER) {
@ -394,12 +415,12 @@ export async function getPlanUsageForProject(project: UsageSourceProject, now: D
const internalTenancy = await getInternalBillingTenancy();
const internalPrisma = await getPrismaClientForTenancy(internalTenancy);
const subscriptions = await getSubscriptionMapForCustomer({
const subscriptions = await readBillingSubscriptionMapOrSkip(() => getSubscriptionMapForCustomer({
prisma: internalPrisma,
tenancyId: internalTenancy.id,
customerType: "team",
customerId: ownerTeamId,
});
}));
const activePlanSubscription = resolveActivePlanSubscription(subscriptions);
const planId = resolveActivePlanId(activePlanSubscription);
const period = getPlanUsagePeriod(activePlanSubscription, now);

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/bulldozer-js",
"version": "1.0.51",
"version": "1.0.52",
"repository": "https://github.com/hexclave/hexclave",
"private": true,
"type": "module",

View File

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

View File

@ -1,5 +1,6 @@
'use client';
import { useViewerLocation } from '@/hooks/use-viewer-location';
import { hexclaveAppInternalsSymbol } from "@/lib/hexclave-app-internals";
import { cn } from "@/lib/utils";
import { captureError } from "@hexclave/shared/dist/utils/errors";
@ -33,6 +34,7 @@ function GlobeErrorComponent(props: { error: Error }) {
function GlobeSectionWithMetrics({ includeAnonymous, interactive }: { includeAnonymous: boolean, interactive?: boolean }) {
const adminApp = useAdminApp();
const data = (adminApp as any)[hexclaveAppInternalsSymbol].useMetrics(includeAnonymous);
const viewerLocation = useViewerLocation();
return (
<>
@ -42,6 +44,7 @@ function GlobeSectionWithMetrics({ includeAnonymous, interactive }: { includeAno
totalUsers={data.total_users}
activeUsersByCountry={data.active_users_by_country ?? {}}
interactive={interactive}
initialPointOfView={viewerLocation}
/>
</>
);

View File

@ -1,3 +1,4 @@
import { US_CENTER } from '@/hooks/use-viewer-location';
import { useWaitForIdle } from '@/hooks/use-wait-for-idle';
import { useDashboardUser } from '@/lib/dashboard-user';
import { useThemeWatcher } from '@/lib/theme';
@ -359,7 +360,7 @@ type SatelliteHandle = {
lastCountryCheckAt: number,
};
export function GlobeSection({ countryData, totalUsers, activeUsersByCountry, satelliteCount, interactive, children }: {countryData: Record<string, number>, totalUsers: number, activeUsersByCountry?: Record<string, MetricsRecentUser[]>, satelliteCount?: number, interactive?: boolean, children?: React.ReactNode}) {
export function GlobeSection({ countryData, totalUsers, activeUsersByCountry, satelliteCount, interactive, initialPointOfView, children }: {countryData: Record<string, number>, totalUsers: number, activeUsersByCountry?: Record<string, MetricsRecentUser[]>, satelliteCount?: number, interactive?: boolean, initialPointOfView?: { lat: number, lng: number }, children?: React.ReactNode}) {
const hasWaitedForIdle = useWaitForIdle(1000, 5000);
if (!hasWaitedForIdle) {
return <GlobeLoading devReason="waiting for cpu" />;
@ -372,6 +373,7 @@ export function GlobeSection({ countryData, totalUsers, activeUsersByCountry, sa
activeUsersByCountry={activeUsersByCountry ?? {}}
satelliteCount={satelliteCount ?? 2}
interactive={interactive ?? false}
initialPointOfView={initialPointOfView}
/>
</Suspense>
);
@ -466,7 +468,7 @@ function GlobeLoading(props: { devReason: string, className?: string }) {
);
}
function GlobeSectionInner({ countryData, totalUsers, activeUsersByCountry, satelliteCount, interactive, children }: {countryData: Record<string, number>, totalUsers: number, activeUsersByCountry: Record<string, MetricsRecentUser[]>, satelliteCount: number, interactive: boolean, children?: React.ReactNode}) {
function GlobeSectionInner({ countryData, totalUsers, activeUsersByCountry, satelliteCount, interactive, initialPointOfView, children }: {countryData: Record<string, number>, totalUsers: number, activeUsersByCountry: Record<string, MetricsRecentUser[]>, satelliteCount: number, interactive: boolean, initialPointOfView?: { lat: number, lng: number }, children?: React.ReactNode}) {
const countries = use(countriesPromise);
const projectId = useProjectId();
const globeRef = useRef<GlobeMethods | undefined>(undefined);
@ -677,7 +679,6 @@ function GlobeSectionInner({ countryData, totalUsers, activeUsersByCountry, sate
resumeRender();
}, [cameraDistance, shouldShowGlobe, squareSize, interactive]);
const totalUsersInCountries = Object.values(countryData).reduce((acc, curr) => acc + curr, 0);
const totalPopulationInCountries = countries.features.reduce((acc, curr) => acc + curr.properties.POP_EST, 0);
const oneSided95PercentZScore = 1.645;
@ -1156,8 +1157,7 @@ function GlobeSectionInner({ countryData, totalUsers, activeUsersByCountry, sate
controls.enableZoom = interactive;
controls.enableRotate = true;
current.camera().position.z = cameraDistance;
// Little Saint James Island, U.S. Virgin Islands
current.pointOfView({ lat: 18.3076, lng: -64.8267 }, 0);
current.pointOfView({ lat: initialPointOfView?.lat ?? US_CENTER.lat, lng: initialPointOfView?.lng ?? US_CENTER.lng }, 0);
// Fix z-fighting: Enable proper depth testing
const renderer = current.renderer();

View File

@ -18,9 +18,11 @@ import { Alert, AlertDescription, Button } from "@/components/ui";
import { Link } from "@/components/link";
import { useDashboardInternalUser } from "@/lib/dashboard-user";
import { PLAN_LIMITS, resolvePlanId } from "@hexclave/shared/dist/plans";
import { captureError } from "@hexclave/shared/dist/utils/errors";
import { runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises";
import { useVirtualizer } from "@tanstack/react-virtual";
import { useMemo, useRef } from "react";
import { ErrorBoundary } from "next/dist/client/components/error-boundary";
import { Suspense, useEffect, useMemo, useRef, type ReactNode } from "react";
import { useAdminApp } from "../use-admin-app";
// ============================================================================
@ -322,11 +324,45 @@ export function ErrorDisplay({ error, onRetry }: { error: unknown, onRetry: () =
);
}
/**
* The plan-limit banners live on core dashboard pages (overview, analytics,
* session replays) but read the internal project's billing state from
* bulldozer (plan usage, owned products, item quantities). Billing is not
* essential to those pages, so a bulldozer outage must not take them down:
* on any read failure we report it and render nothing rather than letting the
* error escape to the page. `Suspense` keeps the banner from blocking the page
* while its data loads.
*/
function BillingBannerErrorFallback({ location, error }: { location: string, error: unknown }) {
useEffect(() => {
captureError(location, error);
}, [location, error]);
return null;
}
function ResilientBillingBanner(props: { children: ReactNode, location: string }) {
return (
<ErrorBoundary errorComponent={({ error }) => <BillingBannerErrorFallback location={props.location} error={error} />}>
<Suspense fallback={null}>
{props.children}
</Suspense>
</ErrorBoundary>
);
}
/**
* Shows a warning banner when analytics event usage is at 80%+ or 100%.
* Fetches the billing team's analytics_events item and computes usage against the plan's total allocation.
*/
export function AnalyticsEventLimitBanner() {
return (
<ResilientBillingBanner location="analytics-limit-banner:billing-read-failed">
<AnalyticsEventLimitBannerContent />
</ResilientBillingBanner>
);
}
function AnalyticsEventLimitBannerContent() {
const adminApp = useAdminApp();
const project = adminApp.useProject();
const planUsage = adminApp.usePlanUsage();
@ -350,6 +386,14 @@ export function AnalyticsEventLimitBanner() {
* Since the limit is the same across all plans, no upgrade button is shown.
*/
export function SessionReplayLimitBanner() {
return (
<ResilientBillingBanner location="session-replay-limit-banner:billing-read-failed">
<SessionReplayLimitBannerContent />
</ResilientBillingBanner>
);
}
function SessionReplayLimitBannerContent() {
const adminApp = useAdminApp();
const project = adminApp.useProject();
const planUsage = adminApp.usePlanUsage();

View File

@ -37,12 +37,13 @@ import type { CompleteConfig } from "@hexclave/shared/dist/config/schema";
import type { RestrictedReason } from "@hexclave/shared/dist/schema-fields";
import { urlSchema, yupObject, yupString } from "@hexclave/shared/dist/schema-fields";
import { runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises";
import { HexclaveAssertionError, throwErr } from "@hexclave/shared/dist/utils/errors";
import { captureError, HexclaveAssertionError, throwErr } from "@hexclave/shared/dist/utils/errors";
import { allProviders } from "@hexclave/shared/dist/utils/oauth";
import { typedFromEntries, typedEntries } from "@hexclave/shared/dist/utils/objects";
import { resolvePlanId } from "@hexclave/shared/dist/plans";
import { generateUuid } from "@hexclave/shared/dist/utils/uuids";
import { useId, useMemo, useState } from "react";
import { ErrorBoundary } from "next/dist/client/components/error-boundary";
import { Suspense, useEffect, useId, useMemo, useState } from "react";
import { AppEnabledGuard } from "../app-enabled-guard";
import { PageLayout } from "../page-layout";
import { useAdminApp } from "../use-admin-app";
@ -193,7 +194,24 @@ function AddCustomOidcButton({ onClick }: { onClick: () => void }) {
return <AddCustomOidcButtonDisabled onClick={onClick} isTeamPlanOrAbove={false} />;
}
return <AddCustomOidcButtonInner team={ownerTeam} onClick={onClick} />;
// The plan check reads the internal project's owned products from bulldozer.
// That gate must not break the auth-methods page if bulldozer is down: on a
// read failure (or while it loads) we report it and fall back to the locked
// (non-Team) state, which is the same safe default as having no owner team.
return (
<ErrorBoundary errorComponent={({ error }) => <AddCustomOidcButtonPlanReadFailed onClick={onClick} error={error} />}>
<Suspense fallback={<AddCustomOidcButtonDisabled onClick={onClick} isTeamPlanOrAbove={false} />}>
<AddCustomOidcButtonInner team={ownerTeam} onClick={onClick} />
</Suspense>
</ErrorBoundary>
);
}
function AddCustomOidcButtonPlanReadFailed({ onClick, error }: { onClick: () => void, error: unknown }) {
useEffect(() => {
captureError("auth-methods:custom-oidc-plan-gate", error);
}, [error]);
return <AddCustomOidcButtonDisabled onClick={onClick} isTeamPlanOrAbove={false} />;
}
function AddCustomOidcButtonInner({

View File

@ -0,0 +1,33 @@
import { headers } from "next/headers";
import { NextResponse } from "next/server";
export const runtime = "nodejs";
/**
* Returns the dashboard viewer's approximate latitude and longitude,
* derived from IP-based geolocation headers injected by the CDN/proxy
* (Vercel, Cloudflare). No browser permissions are required.
*
* Returns `{ lat, lng }` when geo headers are available, otherwise
* `{ lat: null, lng: null }` so the caller can fall back.
*/
export async function GET() {
const allHeaders = await headers();
// Vercel injects lat/lng headers on every edge/serverless request.
const vercelLat = allHeaders.get("x-vercel-ip-latitude");
const vercelLng = allHeaders.get("x-vercel-ip-longitude");
if (vercelLat != null && vercelLng != null) {
const lat = parseFloat(vercelLat);
const lng = parseFloat(vercelLng);
if (Number.isFinite(lat) && Number.isFinite(lng)) {
return NextResponse.json({ lat, lng });
}
}
// Cloudflare provides a cf-ipcountry header but no lat/lng, so we can't
// do much with it here. Fall back gracefully.
return NextResponse.json({ lat: null, lng: null });
}

View File

@ -0,0 +1,67 @@
"use client";
import { captureError } from "@hexclave/shared/dist/utils/errors";
import { runAsynchronously } from "@hexclave/shared/dist/utils/promises";
import { useEffect, useState } from "react";
type ViewerLocation = { lat: number, lng: number };
// US geographic center fallback
export const US_CENTER: ViewerLocation = { lat: 39.5, lng: -98.35 };
// Cache the result so we only fetch once per page load, even if multiple
// components mount/unmount the hook.
let cachedLocation: ViewerLocation | null = null;
let fetchPromise: Promise<ViewerLocation> | null = null;
function fetchViewerLocation(): Promise<ViewerLocation> {
if (fetchPromise != null) return fetchPromise;
fetchPromise = (async () => {
try {
const res = await fetch("/api/viewer-location");
if (!res.ok) {
fetchPromise = null;
return US_CENTER;
}
const data = await res.json();
if (typeof data.lat === "number" && typeof data.lng === "number") {
return { lat: data.lat, lng: data.lng };
}
} catch (e) {
captureError("viewer-location-fetch", e instanceof Error ? e : new Error(String(e)));
fetchPromise = null;
}
// Header data was missing or malformed; allow retry on next mount.
fetchPromise = null;
return US_CENTER;
})();
return fetchPromise;
}
/**
* Returns the dashboard viewer's approximate location based on IP
* geolocation (via CDN headers), falling back to the US center.
* The fetch runs once per page load and the result is cached.
*/
export function useViewerLocation(): ViewerLocation {
const [location, setLocation] = useState<ViewerLocation>(() => cachedLocation ?? US_CENTER);
useEffect(() => {
if (cachedLocation != null) return;
let cancelled = false;
runAsynchronously(async () => {
const loc = await fetchViewerLocation();
// Only cache a successful (non-fallback) result so transient
// failures can be retried on next mount.
if (loc !== US_CENTER) {
cachedLocation = loc;
}
if (!cancelled) setLocation(loc);
});
return () => {
cancelled = true;
};
}, []);
return location;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{
"//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)",
"name": "@hexclave/js",
"version": "1.0.51",
"version": "1.0.52",
"repository": "https://github.com/hexclave/hexclave",
"sideEffects": false,
"main": "./dist/index.js",

View File

@ -1,7 +1,7 @@
{
"//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)",
"name": "@hexclave/next",
"version": "1.0.51",
"version": "1.0.52",
"repository": "https://github.com/hexclave/hexclave",
"sideEffects": false,
"main": "./dist/index.js",

View File

@ -1,7 +1,7 @@
{
"//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)",
"name": "@hexclave/react",
"version": "1.0.51",
"version": "1.0.52",
"repository": "https://github.com/hexclave/hexclave",
"sideEffects": false,
"main": "./dist/index.js",

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@hexclave/shared-backend",
"version": "1.0.51",
"version": "1.0.52",
"repository": "https://github.com/hexclave/hexclave",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",

View File

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

View File

@ -559,7 +559,7 @@ const appSetupPrompt: Record<PublicAppSetupPromptId, string> =
};
\`\`\`
For large OAuth providers like Google and GitHub, Hexclave automatically provides a client ID and secret, so no extra provider setup is needed.
For some OAuth providers like Google and GitHub, Hexclave automatically provides a client ID and secret, so no extra provider setup is needed.
Then wire the SDK setup above: create the Hexclave App object, wrap React apps in the provider, and add auth pages where your framework needs them. OAuth client IDs/secrets and trusted domains are environment-specific, so leave placeholders or ask the user for those instead of inventing them. See [Auth providers](https://docs.hexclave.com/guides/apps/authentication/auth-providers) and [hexclave.config.ts: Auth](https://docs.hexclave.com/guides/going-further/hexclave-config#auth).
`,

View File

@ -1,7 +1,7 @@
{
"//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)",
"name": "@hexclave/tanstack-start",
"version": "1.0.51",
"version": "1.0.52",
"repository": "https://github.com/hexclave/hexclave",
"sideEffects": false,
"main": "./dist/index.js",

View File

@ -13,7 +13,7 @@
"//": "NEXT_LINE_PLATFORM template",
"private": true,
"version": "1.0.51",
"version": "1.0.52",
"repository": "https://github.com/hexclave/hexclave",
"sideEffects": false,
"main": "./dist/index.js",

View File

@ -2,7 +2,7 @@
"//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)",
"name": "@hexclave/template",
"private": true,
"version": "1.0.51",
"version": "1.0.52",
"repository": "https://github.com/hexclave/hexclave",
"sideEffects": false,
"main": "./dist/index.js",

View File

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

View File

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

View File

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