From 55e8a210ea17c86403c50e4798772e03cac79b10 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Thu, 21 May 2026 14:25:05 -0700 Subject: [PATCH 01/28] =?UTF-8?q?docs:=20add=20Stack=20Auth=20=E2=86=92=20?= =?UTF-8?q?Hexclave=20rebrand=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Captures the v6 rebrand plan: dual-support strategy for wire identifiers (HTTP headers, cookies, JWT issuers, Bearer prefix, OAuth state), SDK alias re-exports, npm dual-publish via rewrite-then-republish, Swift split into separate StackAuth/Hexclave packages, env var taxonomy, verification matrix, and 2-PR rollout. --- RENAME-TO-HEXCLAVE.md | 922 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 922 insertions(+) create mode 100644 RENAME-TO-HEXCLAVE.md diff --git a/RENAME-TO-HEXCLAVE.md b/RENAME-TO-HEXCLAVE.md new file mode 100644 index 0000000000..9342e8f4c5 --- /dev/null +++ b/RENAME-TO-HEXCLAVE.md @@ -0,0 +1,922 @@ +# Stack Auth → Hexclave Rename Plan (v6) + +Rebrand rollout with backwards compatibility. Organized by wire-compatibility risk: what breaks existing users vs. what's purely cosmetic. + +**Rollout strategy:** one large additive PR that introduces Hexclave naming everywhere without breaking anything, then a much-later cleanup PR that removes only the safely-removable fallbacks (cookies, customer SDK env vars, MCP legacy tool, non-essential DNS). + +## Locked-in decisions + +- GitHub canonical repo: **`hexclave/hexclave`** (was `hexclave/stack-auth`) +- Read-only legacy identifiers — kept indefinitely (no Hexclave writer needed because nothing emits them in new code): + - `x-stack-auth` (legacy JSON-encoded auth header) +- **Symmetric dual-support** (old kept indefinitely, new is preferred / emitted by new code): + - All `x-stack-*` request headers ↔ `x-hexclave-*` equivalents (dual-accept) + - All `x-stack-*` response headers ↔ `x-hexclave-*` equivalents (dual-emit) + - `Bearer stackauth_*` ↔ `Bearer hexclave_*` + - All `stack-*` cookies (auth, OAuth state, low-risk UI) ↔ `hexclave-*` equivalents (dual-write) + - All three `stack-auth.com` JWT issuer variants ↔ `hexclave.com` (validator accepts both) + - `stack.config.ts` ↔ `hexclave.config.ts` (discovery prefers new, falls back to old) + - `stack-auth-mobile-oauth-url://` ↔ `hexclave-mobile-oauth-url://` (backend accepts both schemes; new Swift SDK registers the new one) + - `Symbol.for("StackAuth--app-internals")` ↔ `Symbol.for("Hexclave--app-internals")` (attach under both, look up both) + - JS `Stack*` exports stay canonical; `Hexclave*` added as aliases — both kept indefinitely +- **Swift SDK: separate package**, not typealiases. `StackAuth` Swift package frozen at existing git URL — old users keep `import StackAuth`. New `Hexclave` Swift package (new git URL) is the canonical going-forward SDK with real `Hexclave*` symbols. Breaking changes allowed between versions; old package remains installable but unmaintained. +- New docs teach **Hexclave-only** names; old names appear only in explicitly-marked compatibility notes +- Self-host operator env vars (Category C) are **out of scope** — stay as `STACK_*`, no aliasing +- `@hexclave/*` packages dual-published via rewrite step in `.github/workflows/npm-publish.yaml` (see Tier 2) +- **Sentry / PostHog / observability DSNs** — out of scope. Existing DSNs continue unchanged. No project renames in either tool. +- **`skill.stack-auth.com`** — DNS redirects to `skill.hexclave.com`; both URLs serve identical content indefinitely. Customers with cached MCP configs pointing at old domain keep working. +- **`StackAssertionError`** — class gets `HexclaveAssertionError` alias (per Tier 1 pattern); error message string updates from "This is likely an error in Stack." → "This is likely an error in Hexclave." in PR 1. +- **CHANGELOG title** — becomes "Hexclave Changelog" in PR 1. History continuity preserved through commit log, not title. +- **Test assertion updates** — every test that asserts on header names, cookie names, error message prefixes, etc. updates in lockstep with implementation in PR 1. +- **Deprecation warning text** — exact wording is an implementation-time decision; SDK init logs `console.warn` once per process. +- **Docker registry path / image naming** — not part of this rebrand; existing image tags continue. +- Telemetry is deferred; not blocking PR 1 + +## Scale at a glance + +| Surface | Count | +|---|---| +| HTTP headers (`x-stack-*`) | 21 | +| Cookies | ~12 | +| Customer-facing env vars | ~20+ | +| NPM packages | 11 | +| Public SDK classes/components/hooks (JS) | ~12 | +| Swift module + symbols | 1 module, ~10 symbols | +| Domain references | 625+ | +| Total brand string references | ~1,000+ | + +--- + +## Tier 0 — Wire identifiers (dual-accept indefinitely) + +These travel between SDK and backend, or get baked into third-party systems. **Alias, never replace.** + +### Read-only legacy identifiers (no Hexclave writer needed) + +These have no Hexclave equivalent because nothing in new code emits them. Backend keeps parsing them indefinitely as a compatibility path. + +| Identifier | What | Why no Hexclave equivalent | +|---|---|---| +| `x-stack-auth: { accessToken, refreshToken }` | Legacy JSON-encoded auth header | Newer SDKs use split `x-stack-access-token` + `x-stack-refresh-token` (which DO get `x-hexclave-*` aliases). This older header has no current writer to add a new format to. | + +### Symmetric dual-support (old kept, new is canonical) + +These follow the same pattern as request headers: old form continues to work indefinitely; new form is preferred and emitted by new code. + +| Concept | Old (read indefinitely) | New (canonical, written by new code) | +|---|---|---| +| Bearer auth prefix | `Authorization: Bearer stackauth_` | `Authorization: Bearer hexclave_` | +| Response/protocol headers | `x-stack-actual-status`, `x-stack-known-error`, `x-stack-request-id` | `x-hexclave-actual-status`, `x-hexclave-known-error`, `x-hexclave-request-id` (dual-emitted) | +| Config filename | `stack.config.ts` | `hexclave.config.ts` | +| Mobile OAuth URL scheme | `stack-auth-mobile-oauth-url://` | `hexclave-mobile-oauth-url://` | + +**Bearer prefix details.** Backend's Authorization parser checks both `stackauth_` and `hexclave_` prefixes (one extra string-prefix check). New SDKs construct tokens with the `hexclave_` prefix; old SDKs keep working unchanged. Anyone debugging a request sees the brand-consistent prefix on new traffic. + +**Response header details.** Backend emits both `x-stack-*` AND `x-hexclave-*` versions of `actual-status`, `known-error`, `request-id` on every response (~60 extra bytes total — negligible). New SDKs read `x-hexclave-*` first, fall back to `x-stack-*`. Old SDKs continue to read `x-stack-*` only. + +**Config filename details.** +- **Discovery order:** CLI/dashboard look for `hexclave.config.ts` first; fall back to `stack.config.ts`. +- **`hexclave init`** generates `hexclave.config.ts` for new projects. +- **Existing projects** with `stack.config.ts` keep working without migration — the DB row pointing at that filename still resolves. +- **GitHub config push** writes back to whichever filename already exists in the customer's repo; defaults to `hexclave.config.ts` for new repos. +- **Tests** updated to expect new default; old-filename tests retained as compat coverage. + +**Mobile OAuth URL scheme details.** +- **Backend acceptance check** at [apps/backend/src/lib/redirect-urls.tsx:78](apps/backend/src/lib/redirect-urls.tsx:78) currently reads `url.protocol === 'stack-auth-mobile-oauth-url:'`. Update to accept either protocol: `url.protocol === 'stack-auth-mobile-oauth-url:' || url.protocol === 'hexclave-mobile-oauth-url:'`. +- **Frozen `StackAuth` Swift SDK** keeps registering `stack-auth-mobile-oauth-url` in `Info.plist` and using `stack-auth-mobile-oauth-url://success` / `…://error` as callback URLs. Existing App-Store-shipped customer apps keep working unchanged. +- **New `Hexclave` Swift SDK** registers `hexclave-mobile-oauth-url` in `Info.plist`, uses `hexclave-mobile-oauth-url://success` / `…://error` callbacks. +- **Spec update:** `sdks/spec/src/apps/client-app.spec.md` documents both schemes; canonical for new code is the Hexclave scheme. +- **Tests:** add `isAcceptedNativeAppUrl('hexclave-mobile-oauth-url://success')` etc. alongside the existing assertions in `apps/backend/src/lib/redirect-urls.test.tsx`. + +### HTTP request headers (dual-accept) + +Server reads both `x-stack-*` and `x-hexclave-*` via a single helper. New SDKs emit `x-hexclave-*`; existing SDKs keep working unchanged. + +**Read paths:** `apps/backend/src/route-handlers/smart-request.tsx`, `apps/backend/src/proxy.tsx` + +| Old (accepted indefinitely) | New (preferred) | +|---|---| +| `x-stack-access-token` | `x-hexclave-access-token` | +| `x-stack-refresh-token` | `x-hexclave-refresh-token` | +| `x-stack-project-id` | `x-hexclave-project-id` | +| `x-stack-access-type` | `x-hexclave-access-type` | +| `x-stack-api-key` | `x-hexclave-api-key` | +| `x-stack-request-type` | `x-hexclave-request-type` | +| `x-stack-publishable-client-key` | `x-hexclave-publishable-client-key` | +| `x-stack-secret-server-key` | `x-hexclave-secret-server-key` | +| `x-stack-super-secret-admin-key` | `x-hexclave-super-secret-admin-key` | +| `x-stack-admin-access-token` | `x-hexclave-admin-access-token` | +| `x-stack-branch-id` | `x-hexclave-branch-id` | +| `x-stack-allow-anonymous-user` | `x-hexclave-allow-anonymous-user` | +| `x-stack-allow-restricted-user` | `x-hexclave-allow-restricted-user` | +| `x-stack-client-version` | `x-hexclave-client-version` | +| `x-stack-development-override-key` | `x-hexclave-development-override-key` | +| `x-stack-override-error-status` | `x-hexclave-override-error-status` | +| `x-stack-disable-artificial-development-delay` | `x-hexclave-disable-artificial-development-delay` | +| `x-stack-development-disable-extended-logging` | `x-hexclave-development-disable-extended-logging` | +| `x-stack-random-nonce` | `x-hexclave-random-nonce` | +| `x-stack-bulldozer-studio-token` | `x-hexclave-bulldozer-studio-token` | + +**Implementation pattern:** `readDualHeader(req, "x-hexclave-foo", "x-stack-foo")` at the parse layer. Zero per-route changes. + +**CORS sync requirement.** `apps/backend/src/proxy.tsx` maintains explicit allowlists for request headers (lines 16-54) and response headers (lines 50-54) used by CORS preflight. Every old + new header pair must appear in both allowlists or preflight will fail. Easy to miss. + +### HTTP response/protocol headers (dual-emit) + +These flow backend → client. Covered in the symmetric dual-support table above. Backend emits both `x-stack-*` and `x-hexclave-*` versions of `actual-status`, `known-error`, `request-id` on every response. New SDKs read `x-hexclave-*` first, fall back to `x-stack-*`. + +> **Note on `x-stack-override-error-status`:** this is a **request** header (client tells backend to override response status before backend emits `x-stack-actual-status`). It's in the request-header table above, dual-accepted as `x-hexclave-override-error-status`. + +### Authorization Bearer formats + +Covered in the symmetric dual-support table above. Backend accepts both `Bearer stackauth_*` and `Bearer hexclave_*`. New SDKs emit `Bearer hexclave_*`. + +### Cookies (dual-write, dual-read across the board) + +Every cookie containing "stack" gets a `hexclave-*` equivalent dual-written. Reads prefer new, fall back to old. Old cookies expire naturally as users re-authenticate or as their TTL passes. + +**Main auth cookies** (`packages/template/src/lib/cookie.ts`, dashboard manual setters): + +| Old (read for compat) | New (canonical, written by PR 1+) | +|---|---| +| `stack-access` | `hexclave-access` | +| `stack-refresh-{projectId}--default` | `hexclave-refresh-{projectId}--default` | +| `stack-refresh-{projectId}--custom-{encoded}` | `hexclave-refresh-{projectId}--custom-{encoded}` | +| `__Host-stack-refresh-internal--*` | `__Host-hexclave-refresh-internal--*` | +| `stack-refresh` (legacy, pre-projectId scheme) | continue reading + deleting on sign-out, do not write | + +**OAuth state cookies** (`apps/backend/.../oauth/authorize/[provider_id]/route.tsx`, `packages/template/src/lib/cookie.ts`): + +| Old (read for compat) | New (canonical, written by PR 1+) | +|---|---| +| `stack-oauth-inner-{state}` (backend-set, deleted on callback) | `hexclave-oauth-inner-{state}` | +| `stack-oauth-outer-{state}` (SDK-set PKCE verifier, 60min TTL) | `hexclave-oauth-outer-{state}` | + +**Low-risk cookies** (low TTL or UI-only — same dual-write pattern for consistency): + +| Old | New | +|---|---| +| `stack-is-https` | `hexclave-is-https` | +| `stack-last-seen-changelog-version` | `hexclave-last-seen-changelog-version` | +| `stack-cli-auth-confirmed` | `hexclave-cli-auth-confirmed` | + +**CHIPS test cookies — keep as `stack-*` indefinitely** (internal, never user-visible, no functional reason to rename): + +- `__Host-stack-temporary-chips-test-*` + +Additional surfaces that set/read cookies and need updating in PR 1: + +- Dashboard remote development environment auth route (deletes internal project cookies) +- Dashboard user impersonation/debug flows (manually set refresh cookies) +- Backend OAuth callback routes (set + delete OAuth state cookies) + +### Customer-facing env vars — see "Env var taxonomy" section below + +The env var question is large enough to warrant its own section. + +### OAuth callback paths + +`/handler/oauth-callback` and `/handler/*` are registered with Google, GitHub, Discord, Apple, etc. as fixed strings. + +**Decision: do NOT rename.** Keep these paths stable indefinitely. New docs teach the existing URLs; do not invent `/hexclave-handler/*`. + +Note: Apple sign-in setup docs require `api.stack-auth.com` as the configured domain for Apple's relay service. This is one more reason `api.stack-auth.com` cannot be deprecated. + +### JWT issuer / audience + +Encoded into already-issued tokens. Validator must accept old + new indefinitely. Three issuer variants: + +| Old | New | +|---|---| +| `iss: https://api.stack-auth.com/api/v1/projects/{projectId}` | `iss: https://api.hexclave.com/api/v1/projects/{projectId}` | +| `iss: https://api.stack-auth.com/api/v1/projects-anonymous-users/{projectId}` | `iss: https://api.hexclave.com/api/v1/projects-anonymous-users/{projectId}` | +| `iss: https://api.stack-auth.com/api/v1/projects-restricted-users/{projectId}` | `iss: https://api.hexclave.com/api/v1/projects-restricted-users/{projectId}` | +| `aud: https://idp-jwk-audience.stack-auth.com/{idpId}` | `aud: https://idp-jwk-audience.hexclave.com/{idpId}` | + +**Files:** `packages/template/src/integrations/convex.ts`, `apps/backend/src/app/api/latest/integrations/idp.ts:167` + +**Strategy:** +- Validator accepts both domains for all three issuer types +- JWKS docs teach Hexclave issuer URLs as canonical +- Convex provider config exposes new issuer URLs by default; old tokens remain valid +- New tokens sign with new domain when the API is served from the new domain (driven by configured base URL, not a separate flag) + +### Dashboard "Create-a-Dashboard" sandbox · iframe protocol + window globals + +The dashboard's AI-generated mini-dashboards run in an iframe sandbox host that exposes SDK globals and a postMessage protocol with `stack-*` identifiers. Generated dashboards saved by customers reference these names — renaming naively breaks every saved dashboard. + +**Window globals** (`apps/dashboard/src/components/commands/create-dashboard/dashboard-sandbox-host.tsx:84-93, 171-173`): + +| Old (kept) | New (set alongside) | +|---|---| +| `window.StackAdminApp` | `window.HexclaveAdminApp` | +| `window.StackServerApp` | `window.HexclaveServerApp` | +| `window.StackSDK` | `window.HexclaveSDK` | + +Sandbox sets both globals; saved dashboards using either reference resolve. + +**iframe postMessage types** (`apps/dashboard/.../dashboard-sandbox-host.tsx:405, 419, 778`): + +| Old (kept) | New (accepted alongside) | +|---|---| +| `stack-access-token-request` | `hexclave-access-token-request` | +| `stack-access-token-response` | `hexclave-access-token-response` | + +Sandbox listens for both message types and responds with both. AI prompts for new dashboards generate Hexclave-named messages; saved dashboards continue using the old names. + +### `@stackframe/emails` virtual module · customer email templates + +Customer-authored email templates import from a virtual `@stackframe/emails` module. This is a public API surface that the plan previously missed. + +**Renderer:** `apps/backend/src/lib/email-rendering.tsx:89` maps the virtual import — currently only `@stackframe/emails`. Update to map both `@stackframe/emails` and `@hexclave/emails` to the same backing module. + +**AI tools:** `apps/backend/src/lib/ai/tools/create-email-template.ts:22,33` and `create-email-draft.ts:23` instruct the model to import from `@stackframe/emails`. Update prompts to teach `@hexclave/emails`; accept either in validation. + +**Monaco editor typings:** `apps/dashboard/src/components/vibe-coding/code-editor.tsx:95` declares the module to the editor. Declare both. + +**Error messages:** `apps/backend/.../email-templates/[templateId]/route.tsx:61` tells users to import from the old name in error text. Update to suggest `@hexclave/emails`. + +**Default templates / E2E fixtures:** find any seeded customer templates that import the old name; new defaults use new name; existing seeded data left alone (works via dual-mapping). + +### MCP tool name + +AI clients (Claude, Cursor, etc.) have `ask_stack_auth` baked into their MCP configs. + +**File:** `apps/mcp/src/mcp-handler.ts:107` + +**Strategy:** register `ask_hexclave` as a new tool; keep `ask_stack_auth` indefinitely as a thin proxy. Setup pages generated by `apps/mcp/src/setup-page.ts` teach the new tool name. + +### Storage keys + +`sessionStorage` / `localStorage` keys. Dual-write old + new names; reads prefer new. + +| Old (read for compat) | New (canonical) | +|---|---| +| `stack-docs-selected-platform` (sessionStorage) | `hexclave-docs-selected-platform` | +| `stack-docs-selected-frameworks` (sessionStorage) | `hexclave-docs-selected-frameworks` | +| `stack_mfa_attempt_code` (sessionStorage, underscore-delimited) | `hexclave_mfa_attempt_code` | + +Note the third key uses underscores instead of hyphens — preserve the existing convention for the new name to keep the access pattern identical. + +--- + +## Tier 1 — Public SDK API (alias via re-exports) + +### JS / React / Next.js / TanStack SDKs + +Codegen makes this clean. `scripts/generate-sdks.ts` copies `packages/template` → `packages/{js,stack,react,tanstack-start}`. Add re-exports once in template; all generated packages get both names. + +Dual-export every public Stack* symbol: + +| Old (kept) | New (alias added) | +|---|---| +| `StackClientApp` | `HexclaveClientApp` | +| `StackServerApp` | `HexclaveServerApp` | +| `StackAdminApp` | `HexclaveAdminApp` | +| `StackProvider` | `HexclaveProvider` | +| `StackHandler` | `HexclaveHandler` | +| `StackTheme` | `HexclaveTheme` | +| `useStackApp()` | `useHexclaveApp()` | +| `StackClientInterface` | `HexclaveClientInterface` | +| `StackServerInterface` | `HexclaveServerInterface` | +| `StackAdminInterface` | `HexclaveAdminInterface` | +| `StackAssertionError` | `HexclaveAssertionError` (plus: error message text updates from "This is likely an error in Stack." → "This is likely an error in Hexclave." in `packages/stack-shared/src/utils/errors.tsx`) | +| `StackConfig` | `HexclaveConfig` | +| `defineStackConfig()` | `defineHexclaveConfig()` | +| `Stack*ConstructorOptions` | `Hexclave*ConstructorOptions` | +| `Stack{Client,Server,Admin}AppConstructor` | `Hexclave{Client,Server,Admin}AppConstructor` | +| `StackClientAppJson` | `HexclaveClientAppJson` | + +**Pattern:** `export { StackClientApp as HexclaveClientApp }`. Same class, both names. Users can mix freely. + +**Canonicality:** `Stack*` is the internal/canonical class name; `Hexclave*` is the alias. This means PR 2 cannot "remove Stack* aliases" — they're the originals. Both names stay indefinitely. If a future effort wants to flip canonicality so `Hexclave*` is the real class and `Stack*` is the alias, that's a separate, optional follow-up — not part of this rebrand. + +`stack.config.ts` filename stays (locked decision). `showOnboardingStackConfigValue` stays internal — no alias needed. + +Page components (`SignIn`, `SignUp`, `AuthPage`, `AccountSettings`, `UserButton`, `TeamSwitcher`, `OAuthButton`, `PasswordReset`, `EmailVerification`, `ForgotPassword`, `MessageCard`, `CliAuthConfirmation`) don't carry the brand — leave alone. + +**Internal `Symbol.for(...)` keying** — dual-symbol pattern. Three distinct `Symbol.for()` strings with "stack" in them across the codebase. All get the dual-attach treatment for consistency. + +| Old (kept for cross-version coexistence) | New (canonical) | Location | +|---|---|---| +| `Symbol.for("StackAuth--DO-NOT-USE-OR-YOU-WILL-BE-FIRED--StackAppInternals")` | `Symbol.for("Hexclave--app-internals")` | `packages/template/src/lib/stack-app/common.ts:213` | +| `Symbol.for("__stack-globals")` | `Symbol.for("__hexclave-globals")` | `packages/stack-shared/src/utils/globals.tsx` | +| `Symbol.for("__stack_email_queue_first_run_completed")` | `Symbol.for("__hexclave_email_queue_first_run_completed")` | `apps/backend/src/lib/email-queue-step.tsx` | + +On attach: write internals under BOTH symbols. On lookup: try new first, fall back to old. Mixed-version setups (a customer with two SDK majors loaded in one page) keep working. + +### Swift SDK — separate package, not typealiases + +The Swift SDK is niche enough that breaking changes between versions are acceptable as long as old SDK versions remain installable. So the cleanest split is **two separate Swift packages**: + +| Package | Status | Module | Symbols | +|---|---|---|---| +| `StackAuth` (existing git URL) | **Frozen**. Bug fixes only; no new features. Existing SPM consumers keep working with no change. | `import StackAuth` | `StackClientApp`, `StackServerApp`, `StackAuthError`, ... | +| `Hexclave` (new git URL / new repo) | **Canonical going forward**. All new development happens here. | `import Hexclave` | `HexclaveClientApp`, `HexclaveServerApp`, `HexclaveError`, ... — these are *real types*, not typealiases | + +Notes: +- Old code (`import StackAuth; let app = StackClientApp(...)`) keeps working indefinitely from the existing SPM URL — but doesn't get new features +- New code uses `import Hexclave; let app = HexclaveClientApp(...)` — Hexclave-only, no Stack visible anywhere +- Default base URL in the new `Hexclave` package is `https://api.hexclave.com` +- No typealiases, no dual-export inside one module — the two packages are independent + +**`sdks/spec`** describes Hexclave naming as canonical; the spec for the legacy `StackAuth` package is preserved at the existing path but flagged as frozen. + +**Files in scope:** +- Existing `sdks/implementations/swift/` — frozen as-is, keeps publishing `StackAuth` package from existing URL +- New Hexclave Swift package — new directory (e.g. `sdks/implementations/swift-hexclave/`) or new repo, TBD by Swift maintainer +- `sdks/spec/` updated to describe Hexclave canonical Swift API + +**Files in scope:** +- `sdks/implementations/swift/Package.swift` +- `sdks/implementations/swift/Sources/StackAuth/` +- `sdks/implementations/swift/Tests/StackAuthTests/` +- `sdks/implementations/swift/Examples/StackAuthiOS/` +- `sdks/implementations/swift/Examples/StackAuthMacOS/` +- `sdks/spec/src/` +- `sdks/spec/README.md` + +Per AGENTS.md, SDK implementation changes must update `sdks/spec` — bake this into the PR 1 checklist. + +--- + +## Tier 2 — NPM packages (dual-publish) + +Keep `@stackframe/*` published indefinitely. Add `@hexclave/*` mirrors. + +### Publishing mechanics + +**Decision: rewrite-then-republish in `.github/workflows/npm-publish.yaml`.** Workspace stays `@stackframe/*`-keyed; no duplicate source dirs. + +Concrete change to the existing workflow: + +```yaml +# Existing steps: +- name: Build packages + run: pnpm build:packages +- name: Publish @stackframe/* packages + run: pnpm publish -r --no-git-checks --access public + env: + NPM_CONFIG_PROVENANCE: true + +# New steps appended: +- name: Rewrite package names to @hexclave/* + run: pnpm tsx scripts/rewrite-packages-to-hexclave.ts +- name: Publish @hexclave/* packages + run: pnpm publish -r --no-git-checks --access public + env: + NPM_CONFIG_PROVENANCE: true +``` + +`scripts/rewrite-packages-to-hexclave.ts` does, for each publishable package per the mapping table below: +- Read `package.json` +- Rewrite `name`: `@stackframe/foo` → `@hexclave/foo` +- Rewrite all `dependencies` / `peerDependencies` entries from `@stackframe/X` → `@hexclave/X` with the version of the just-published artifact +- Update `bin` entries where relevant (e.g. `@hexclave/cli` registers `hexclave` binary alongside the existing `stack`) +- Leave built `dist/` artifacts untouched (no rebuild needed) + +`pnpm publish` skips versions already on npm, so reruns are safe. The workflow runs on a clean checkout each time, so no revert is needed. + +Notes: +- Workspace remains `@stackframe/*`-keyed; `pnpm-workspace.yaml`, Turbo filters, and lockfile are unchanged +- Source maps, type declarations, `exports`, `typesVersions` resolve under both names because they're the same built artifacts +- The rewrite step only runs in CI; local development keeps using `@stackframe/*` names + +### 10 mirrored packages + +| Old (kept) | New (mirrored) | +|---|---| +| `@stackframe/react` | `@hexclave/react` | +| `@stackframe/stack` | `@hexclave/stack` | +| `@stackframe/js` | `@hexclave/js` | +| `@stackframe/stack-shared` | `@hexclave/shared` | +| `@stackframe/stack-ui` | `@hexclave/ui` | +| `@stackframe/stack-sc` | `@hexclave/sc` | +| `@stackframe/init-stack` | `@hexclave/init` | +| `@stackframe/stack-cli` | `@hexclave/cli` | +| `@stackframe/tanstack-start` | `@hexclave/tanstack-start` | +| `@stackframe/dashboard-ui-components` | `@hexclave/dashboard-ui-components` | + +**`@stackframe/dashboard-ui-components` is publishable.** Earlier plan versions marked it "internal only" — that was wrong. It's loaded at runtime via esm.sh by the dashboard's create-dashboard sandbox host ([apps/dashboard/.../dashboard-sandbox-host.tsx](apps/dashboard/src/components/commands/create-dashboard/dashboard-sandbox-host.tsx)) plus served locally as an IIFE bundle (`dashboard-ui-components.iife.js`). Mirror it like the other public packages. The IIFE bundle filename also gets dual-served — both `dashboard-ui-components.iife.js` and a future Hexclave-branded path (TBD) until generated dashboards stored with the old filename can be updated. + +**Not mirrored — internal:** `@stackframe/template` (codegen source). + +**Not publishable, stay `@stackframe/*`:** `@stackframe/monorepo`, backend, dashboard, docs, mcp, hosted-components, skills, mock-oauth-server, e2e, internal-tool, dev-launchpad. + +### CLI / init wizard + +| Old (kept) | New | +|---|---| +| `npx @stackframe/init-stack` | `npx @hexclave/init` | +| `stack` binary | `hexclave` binary alias | +| `~/.config/stack-auth/credentials.json` | `~/.config/hexclave/credentials.json` | +| `stack.config.ts` (fallback) | `hexclave.config.ts` (preferred default) | + +CLI reads both config paths; writes new path. Old path silently migrates on next run. For project config: `init` generates `hexclave.config.ts` in new projects; discovery prefers `hexclave.config.ts` and falls back to `stack.config.ts` for existing projects (see Tier 0 details). + +--- + +## Env var taxonomy + +Replaces the flat env var table from v1. Different categories warrant different treatment. + +### Table shape + +For each *concept* (e.g. "Project ID"), the repo may already have multiple env var aliases (Vite vs. Next, BROWSER prefix vs. suffix, etc.). The plan picks **one canonical Hexclave name per concept**; all currently-recognized old names continue to be read as compat aliases. A grep-based pass over Category A and B old-name aliases should be done before implementation to confirm the list below matches what's actually in the repo. + +### A. Customer SDK env vars (dual-read, prefer Hexclave) + +Customer-set in their own projects. SDK init reads any old alias for compat; warns if no canonical Hexclave name is set; new canonical is the only name documented. + +| Concept | Old accepted (compat) | New canonical | +|---|---|---| +| Project ID (Next.js client) | `NEXT_PUBLIC_STACK_PROJECT_ID` | `NEXT_PUBLIC_HEXCLAVE_PROJECT_ID` | +| Publishable client key (Next.js client) | `NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY` | `NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY` | +| API URL (Next.js client) | `NEXT_PUBLIC_STACK_API_URL` | `NEXT_PUBLIC_HEXCLAVE_API_URL` | +| Dashboard URL (Next.js client) | `NEXT_PUBLIC_STACK_DASHBOARD_URL` | `NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL` | +| Stack base URL (Next.js client) | `NEXT_PUBLIC_STACK_URL` | `NEXT_PUBLIC_HEXCLAVE_URL` | +| Hosted handler domain suffix | `NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX` | `NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_DOMAIN_SUFFIX` | +| Hosted handler URL template | `NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE` | `NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_URL_TEMPLATE` | +| Extra request headers (client) | `NEXT_PUBLIC_STACK_EXTRA_REQUEST_HEADERS` | `NEXT_PUBLIC_HEXCLAVE_EXTRA_REQUEST_HEADERS` | +| Project ID (server) | `STACK_PROJECT_ID` | `HEXCLAVE_PROJECT_ID` | +| Publishable client key (server) | `STACK_PUBLISHABLE_CLIENT_KEY` | `HEXCLAVE_PUBLISHABLE_CLIENT_KEY` | +| Secret server key | `STACK_SECRET_SERVER_KEY` | `HEXCLAVE_SECRET_SERVER_KEY` | +| Super secret admin key | `STACK_SUPER_SECRET_ADMIN_KEY` | `HEXCLAVE_SUPER_SECRET_ADMIN_KEY` | +| API URL (server, generic) | `STACK_API_URL` | `HEXCLAVE_API_URL` | +| API URL (server, browser-context override) | `STACK_API_URL_BROWSER` | `HEXCLAVE_API_URL_BROWSER` | +| API URL (server, server-context override) | `STACK_API_URL_SERVER` | `HEXCLAVE_API_URL_SERVER` | +| Dashboard URL (server) | `STACK_DASHBOARD_URL` | `HEXCLAVE_DASHBOARD_URL` | +| Dashboard base URL (server) | `STACK_DASHBOARD_BASE_URL` | `HEXCLAVE_DASHBOARD_BASE_URL` | +| Extra request headers (server) | `STACK_EXTRA_REQUEST_HEADERS` | `HEXCLAVE_EXTRA_REQUEST_HEADERS` | +| Project ID (Vite client) | `VITE_STACK_PROJECT_ID` | `VITE_HEXCLAVE_PROJECT_ID` | +| Publishable client key (Vite client) | `VITE_STACK_PUBLISHABLE_CLIENT_KEY` | `VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY` | +| API URL (Vite client) | `VITE_STACK_API_URL` | `VITE_HEXCLAVE_API_URL` | + +### B. Framework / internal URL env vars (dual-read at app runtime) + +Used by dashboard/backend/local-dev tooling. Some concepts have multiple historical aliases; pick one canonical Hexclave name, accept all old aliases. + +| Concept | Old accepted (compat) | New canonical | +|---|---|---| +| Browser API URL (framework runtime) | `NEXT_PUBLIC_BROWSER_STACK_API_URL`, `NEXT_PUBLIC_STACK_API_URL_BROWSER` | `NEXT_PUBLIC_HEXCLAVE_API_URL_BROWSER` | +| Server API URL (framework runtime) | `NEXT_PUBLIC_SERVER_STACK_API_URL`, `NEXT_PUBLIC_STACK_API_URL_SERVER` | `NEXT_PUBLIC_HEXCLAVE_API_URL_SERVER` | +| Browser Dashboard URL | `NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL`, `NEXT_PUBLIC_STACK_DASHBOARD_URL_BROWSER` | `NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL_BROWSER` | +| Server Dashboard URL | `NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL`, `NEXT_PUBLIC_STACK_DASHBOARD_URL_SERVER` | `NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL_SERVER` | +| Is local emulator | `NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR` | `NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR` | +| Is remote dev env | `NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT` | `NEXT_PUBLIC_HEXCLAVE_IS_REMOTE_DEVELOPMENT_ENVIRONMENT` | +| Is preview | `NEXT_PUBLIC_STACK_IS_PREVIEW` | `NEXT_PUBLIC_HEXCLAVE_IS_PREVIEW` | + +> The exact list of "Old accepted" aliases above is best-effort and **must be validated** against a repo-wide grep before implementation. The reviewer flagged that prior versions of this plan listed aspirational names (`NEXT_PUBLIC_STACK_BROWSER_API_URL`) that don't actually exist in the repo. + +**Exception — keep indefinitely:** `NEXT_PUBLIC_STACK_PORT_PREFIX`. Baked into every dev's local Docker/`.env`; renaming has zero user-facing value and breaks local setups. + +### C. Self-host / operator env vars — out of scope + +These remain as `STACK_*` indefinitely. Not part of the rebrand. + +- `STACK_DATABASE_CONNECTION_STRING`, `STACK_SERVER_SECRET`, `STACK_EMAIL_*`, `STACK_S3_*`, `STACK_SVIX_*`, `STACK_QSTASH_*`, `STACK_STRIPE_*`, `STACK_FREESTYLE_*`, `STACK_OPENROUTER_API_KEY`, `STACK_MCP_LOG_TOKEN`, `STACK_CLICKHOUSE_*`, `STACK_RUN_MIGRATIONS`, `STACK_RUN_SEED_SCRIPT`, `STACK_SEED_INTERNAL_PROJECT_*`, local emulator + QEMU vars + +Self-hosters keep their existing `.env` files unchanged. No deprecation warnings, no docs migration, no `.env.example` rewrite. Operators are not affected by the rebrand at the env-var layer. + +### D. GitHub onboarding workflow + +Two different things to keep straight, conflated in prior plan versions: + +**GitHub Actions secret names** in the customer's repo (`secrets.STACK_AUTH_*`): + +| Old (kept supported) | New (emitted by new workflows) | +|---|---| +| `secrets.STACK_AUTH_PROJECT_ID` | `secrets.HEXCLAVE_PROJECT_ID` | +| `secrets.STACK_AUTH_SECRET_SERVER_KEY` | `secrets.HEXCLAVE_SECRET_SERVER_KEY` | +| `secrets.STACK_AUTH_CONFIG_PATH` | `secrets.HEXCLAVE_CONFIG_PATH` | +| `secrets.STACK_AUTH_SOURCE_REPO` | `secrets.HEXCLAVE_SOURCE_REPO` | +| `secrets.STACK_AUTH_SOURCE_WORKFLOW_PATH` | `secrets.HEXCLAVE_SOURCE_WORKFLOW_PATH` | + +**Process env vars** exported from those secrets and consumed by the CLI inside the workflow runner (`apps/dashboard/src/lib/onboarding/link-existing-onboarding-workflow.ts:51-53` exports them; `packages/stack-cli/src/lib/auth.ts:55,93` reads them): + +| Old (kept supported) | New (emitted by new workflows) | +|---|---| +| `STACK_PROJECT_ID` | `HEXCLAVE_PROJECT_ID` | +| `STACK_SECRET_SERVER_KEY` | `HEXCLAVE_SECRET_SERVER_KEY` | + +These are the **same env vars** customers set in their own apps (Category A) — the workflow just reads the same names. CLI dual-read in Category A automatically covers the workflow runner case. The dashboard's workflow generator must emit both old + new export lines until the CLI dual-read ships. + +New generated workflows emit `HEXCLAVE_*`. Existing customer workflows with `STACK_AUTH_*` secrets / `STACK_*` process env vars keep working. Generated-workflow tests cover both shapes. + +### E. Build / dev / test env vars (keep as `STACK_*`) + +Classified as internal. Not part of the brand rebrand. + +- `STACK_SKIP_TEMPLATE_GENERATION` +- `STACK_DISABLE_REACT_ASYNC_DEBUG_INFO` +- `STACK_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING` +- `STACK_RUN_SETUP_WIZARD_TESTS` +- `STACK_TEST_SDK_FALLBACK` + +Add to `turbo.json` `globalEnv`: `HEXCLAVE_*` alongside existing `STACK_*`. + +--- + +## Tier 3 — Persistent data (idempotent migrations in PR 1) + +### Internal project display name + +**File:** `apps/backend/prisma/seed.ts` + +``` +Project { id: 'internal', displayName: 'Stack Dashboard' } + → displayName: 'Hexclave Dashboard' +``` + +**Migration shape:** +- Idempotent forward migration: `UPDATE Project SET displayName='Hexclave Dashboard' WHERE id='internal' AND displayName='Stack Dashboard'` +- Custom user-modified display names (where someone renamed the internal project) are **not overwritten** +- Missing row no-ops safely +- Migration tests cover all three cases + +Project ID `'internal'` stays — code constant, not brand string. + +### IdP audience URL (stored OAuth configs) + +**File:** `apps/backend/src/app/api/latest/integrations/idp.ts:167` + +Validator accepts both `stack-auth.com` and `hexclave.com` domains. Leave existing DB rows untouched; new configs use the new domain. + +### Email config name + +**Files:** `apps/backend/src/lib/emails.tsx`, `apps/backend/prisma/seed.ts` + +Update `getSharedEmailConfig("Stack Auth")` → `getSharedEmailConfig("Hexclave")`. Bundled with the seed migration; same idempotency rules. + +### Things NOT migrated (locked) + +- Clickhouse `analytics_internal` database name — never user-visible +- Postgres DB name `stackframe` — would orphan every dev's local volume +- Prisma schema tables/columns — no "stack" in them, nothing to rename +- Historical migration filenames — already applied + +--- + +## Tier 4 — Brand strings (mechanical sweep, no compat needed) + +### GitHub repo slug + +Canonical repo becomes **`hexclave/hexclave`** (was `hexclave/stack-auth`). GitHub will redirect old URLs for browser/git usage, but all newly-generated content uses the canonical URL. + +Surfaces to update: +- `repository` fields in all `package.json` files (root + every package + every example) +- `homepage` fields where present +- README, CONTRIBUTING, SECURITY links +- Docs links to GitHub source files +- Mintlify navbar GitHub link in `docs-mintlify/docs.json` +- Generated setup prompts +- Example projects +- GitHub issue/PR templates +- Workflow file repo references +- Raw GitHub asset URLs in CHANGELOG (`raw.githubusercontent.com/stack-auth/stack-auth/`) +- `.github/workflows/swift-sdk-publish.yaml` — currently references `stack-auth/swift-sdk-prerelease`; decide its new home (likely `hexclave/swift-sdk-prerelease` or fold into main repo) + +### Domain inventory + +Complete old→new table. All old domains keep resolving/redirecting indefinitely. + +| Old | New | Notes | +|---|---|---| +| `api.stack-auth.com` | `api.hexclave.com` | Apple sign-in setup requires old domain — keep working indefinitely | +| `app.stack-auth.com` | `app.hexclave.com` | | +| `stack-auth.com` | `hexclave.com` | Marketing root | +| `docs.stack-auth.com` | `docs.hexclave.com` | | +| `discord.stack-auth.com` | `discord.hexclave.com` | | +| `demo.stack-auth.com` | `demo.hexclave.com` | | +| `mcp.stack-auth.com` | `mcp.hexclave.com` | MCP server endpoint | +| `skill.stack-auth.com` | `skill.hexclave.com` | Skill resource server | +| `built-with-stack-auth.com` | `built-with-hexclave.com` | Hosted-component subdomain pattern | +| `r.stack-auth.com` | `r.hexclave.com` | Analytics/replay endpoint | +| `feedback.stack-auth.com` | `feedback.hexclave.com` | | +| `test.stack-auth.com` | `test.hexclave.com` | | +| `preview.stack-auth.com` | `preview.hexclave.com` | | +| `api2.stack-auth.com` | `api2.hexclave.com` | | +| `api.staging.stack-auth.com` | `api.staging.hexclave.com` | | +| `idp-jwk-audience.stack-auth.com` | `idp-jwk-audience.hexclave.com` | See JWT section | + +**OAuth callback URLs in provider setup docs:** teach new Hexclave callbacks; include a compatibility note that old callback URLs registered with providers continue to work. + +### Emails + +| Old | New | +|---|---| +| `noreply@stackframe.co` | `noreply@hexclave.com` | +| `security@stack-auth.com` | `security@hexclave.com` | +| `team@stack-auth.com` | `team@hexclave.com` | + +Set up new mailboxes; forward old → new during transition. + +### Page titles and metadata + +| Old | New | Where | +|---|---|---| +| "Stack Auth Dashboard" | "Hexclave Dashboard" | `apps/dashboard/src/app/layout.tsx` | +| "Stack Auth API" | "Hexclave API" | `apps/backend/src/app/layout.tsx` | +| "Stack REST API" | "Hexclave REST API" | `docs-mintlify/openapi/{server,admin,client}.json` | +| "Stack Webhooks API" | "Hexclave Webhooks API" | `docs-mintlify/openapi/webhooks.json` | +| "Stack Auth Documentation" | "Hexclave Documentation" | `docs-mintlify/docs.json` | + +### Generated content / AI / MCP / skills + +These are AI-generated or template-generated; **update the generator first, then regenerate outputs**. Verify no generated file reintroduces "Stack Auth" branding unintentionally. + +Source generators to update: +- `docs-mintlify/snippets/home-prompt-island.jsx` +- Setup prompt generation scripts (`scripts/generate-setup-prompt-docs.ts` or similar) +- `packages/stack-shared/src/ai/prompts.ts` +- `packages/stack-shared/src/helpers/init-prompt.ts` +- `apps/backend/src/lib/ai/prompts.ts` +- `apps/mcp/src/setup-page.ts` +- `apps/skills/src/app/route.ts` +- `skills/stack-auth/SKILL.md` (consider renaming dir to `skills/hexclave/`) + +Generated artifacts to regenerate after generator updates: +- Docs MDX under `docs-mintlify/` +- OpenAPI `servers`, `x-full-url`, titles +- Setup prompts +- Hosted skill outputs +- MCP browser references + +### OpenAPI schema header documentation + +**Decision: Hexclave-only canonical, with a compatibility note.** +- OpenAPI documents `X-Hexclave-*` request headers as canonical +- A single compatibility note in the OpenAPI description explains that `X-Stack-*` aliases are accepted on every endpoint +- Backend schema routes that explicitly enumerate `X-Stack-*` get dual schema entries (both names accepted, only new name documented as primary) +- Response headers documented under `X-Hexclave-*` as canonical; compat note explains that `X-Stack-*` equivalents are emitted in parallel and read by older clients + +### Visual / branding assets + +Asset filenames can stay or be renamed; the contents are what matter. Update: +- `.github/assets/logo.png` (+ other logo/screenshot assets) +- Docs logos: `docs-mintlify/images/logo-{dark,light}.svg`, OG images +- Favicons across apps +- Dashboard logo/wordmark components +- README screenshots/GIFs (rerun capture) +- Package README badges +- App icons under `docs-mintlify/images/app-icons/` +- Social cards (Twitter/OpenGraph) + +### Known-error message templates (user-visible) + +`packages/stack-shared/src/known-errors.tsx` has user-facing error message templates that reference specific header names and docs URLs. Lines 246, 256, 269, 286, 299, 710 reference `x-stack-access-type`, `x-stack-project-id`, `x-stack-publishable-client-key`, etc. Lines 271, 288 link `docs.stack-auth.com`. Update messages to lead with the canonical `x-hexclave-*` header name and the new docs domain; keep mentions of `x-stack-*` only as a compat alias note. Test assertions on these message strings must update in lockstep. + +### Email strings (subjects + body content) + +Not just subjects — body strings too. Hardcoded in source, not in DB templates. Search exhaustively for "Stack Auth" inside email-related files. + +- "Test Email from Stack Auth" — `apps/backend/src/app/api/latest/internal/send-test-email/route.tsx` +- "Thank you for using Stack Auth!" — `apps/backend/src/app/api/latest/internal/failed-emails-digest/route.ts` +- "Stack Auth User" default passkey display name +- Any other hardcoded subject/body containing "Stack Auth" — grep before PR 1 + +### CHANGELOG title flip + +`CHANGELOG.md` title becomes "Hexclave Changelog" in PR 1. Existing entries' commit-by-commit context preserves continuity; no need to dual-name the title. + +### Contributor / agent guidance + +- `AGENTS.md` currently says: *"Any environment variables you create should be prefixed with `STACK_`"*. Flip to prefer `HEXCLAVE_*` for Category A/B; document that Category C/E vars stay `STACK_*`. +- Update any other contributor guidance referencing brand strings. + +### Other Tier 4 sweeps (same PR) + +- README.md, CONTRIBUTING.md, CHANGELOG.md (title flip per above), AGENTS.md (env var guidance per above) +- 49 docs files referencing `Stack*` class names in code examples (Hexclave-only after the rewrite; one compat note per page where relevant) +- 72 docs files referencing `@stackframe/*` package names in install snippets +- 11 example projects (`examples/`) — including hardcoded `https://app.stack-auth.com` links in their UIs and `.env` comments +- `.github/SECURITY.md`, PR template, workflow file refs +- `skills/stack-auth/SKILL.md` (consider directory rename to `skills/hexclave/`; old directory can stay as a pointer if needed) +- Dashboard setup-page snippets (`apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx`) — copy-pasteable code blocks shown to customers +- Init wizard prompts (`packages/init-stack/`) — user-facing CLI messaging + +--- + +## Do not rename — `stack-*` literals kept indefinitely + +Items that contain "stack" in their literal name and intentionally stay that way. No Hexclave equivalent will exist. + +| What | Why | +|---|---| +| `x-stack-auth` legacy JSON-encoded header | No current writer; pure read-only compat path | +| `__Host-stack-temporary-chips-test-*` cookies | Internal, never user-visible, no functional reason to rename | +| `NEXT_PUBLIC_STACK_PORT_PREFIX` | Baked into every dev's local Docker setup | +| `POSTGRES_DB: stackframe` | Would orphan every dev's local volume | +| Self-host `STACK_*` env vars (Category C) | Out of scope; self-hosters unaffected | +| Build/dev/test env vars (`STACK_SKIP_TEMPLATE_GENERATION`, `STACK_TEST_SDK_FALLBACK`, etc.) | Internal-only, not user-facing | +| Swift legacy `StackAuth` package | Frozen but installable; new SDK lives in separate `Hexclave` package | + +### Not in scope — never had "stack" branding to begin with + +These are listed once for completeness so reviewers don't worry about them. The rebrand never touches them. + +- Webhook event types (`user.created`, `team.updated`, etc.) — already generic +- Clickhouse `analytics_internal` database name +- `'internal'` project ID literal — a code constant, not a brand string +- `/handler/*` OAuth callback routes +- Prisma schema (tables / columns / enums) +- API key prefixes (`pck_`, `ssk_`) — opaque to users +- Historical migration filenames — already applied + +--- + +## Implementation realities (architecture observations from pre-PR-1 review) + +These aren't decisions — they're things the implementer should know before starting. Each comes from grepping the actual codebase. + +1. **No `readDualHeader` helper exists — AND it's insufficient on its own.** [smart-request.tsx](apps/backend/src/route-handlers/smart-request.tsx) reads auth-level headers via individual `req.headers.get()` calls (~10 sites). But route handlers ALSO destructure header names directly from yup-validated schemas — e.g. [refresh/route.tsx:19,29](apps/backend/src/app/api/latest/auth/sessions/current/refresh/route.tsx:19) declares `"x-stack-refresh-token"` in the schema and destructures it from `headers` in the handler, and [password/update/route.tsx:27,34](apps/backend/src/app/api/latest/auth/password/update/route.tsx:27) does the same. **A helper at the auth-parse layer alone won't cover these.** PR 1 must either (a) add a header-name normalization step *before* yup schema validation that populates both old + new keys into `headers`, or (b) update every route schema that names a `x-stack-*` header to accept both names. (a) is mechanically smaller. CORS allowlist in `apps/backend/src/proxy.tsx` also needs both old + new names. + +2. **JWT issuer validation is URL-built, not domain-matched.** [apps/backend/src/lib/tokens.tsx:58-104](apps/backend/src/lib/tokens.tsx:58) constructs allowed issuer URLs from `NEXT_PUBLIC_STACK_API_URL` and passes them as an exact-match array to `verifyJWT()`. There's no domain-substring check. Implementation must build **two arrays** (one per domain) and concatenate, OR refactor `getIssuer()` to return both variants. + +3. **Cookie helper isn't fully centralized.** `stack-is-https` is written in at least 4 places that bypass the central helper (cookie.ts:280, 355; TanStack integration:198; backend OAuth setters). Dual-write requires refactoring those to use shared constants, not just editing one helper. + +4. **Bearer prefix parser location TBD.** The agent review couldn't pinpoint where `Bearer stackauth_*` is actually parsed — likely JWT validation in `packages/stack-shared/src/utils/jwt.tsx` or in middleware, not in `smart-request.tsx`. Locating is a PR 1 prerequisite. + +5. **NPM dual-publish needs the copy-to-temp pattern, not in-place rewrite.** The plan's original "rewrite package.json names, then `pnpm publish -r` again" approach won't work — pnpm uses a shared lockfile, so after rewrite it can't resolve `@hexclave/X` workspace refs. **Concrete fix:** the rewrite script copies `dist/` artifacts and each package.json into a temp directory, rewrites the temp copies (names + deps), and publishes from temp. Workspace lockfile stays untouched. + + ```yaml + - name: Rewrite to @hexclave/* in temp dir + run: pnpm tsx scripts/rewrite-packages-to-hexclave.ts --out /tmp/hexclave-pkgs + - name: Publish @hexclave/* packages + run: pnpm publish --no-git-checks --access public --recursive /tmp/hexclave-pkgs + ``` + +6. **Config discovery is not one function.** `init` (`packages/stack-cli/src/commands/init.ts`) hardcodes `stack.config.ts` output; `dev` requires explicit `--config-file`; dashboard local-dev linking discovers separately. Adding "prefer `hexclave.config.ts`, fall back to `stack.config.ts`" requires updating each discovery site. + +7. **Symbol attach-and-lookup sites unknown.** `stackAppInternalsSymbol` is defined in [common.ts:213](packages/template/src/lib/stack-app/common.ts:213). Every site that does `app[stackAppInternalsSymbol] = …` and every `app[stackAppInternalsSymbol]` read needs dual treatment. Estimated 5-20 sites; enumerate during PR 1. + +8. **Snapshot serializer hardcodes `"stack-oauth-inner-"`** at [apps/e2e/tests/snapshot-serializer.ts:119](apps/e2e/tests/snapshot-serializer.ts:119). Update to also recognize `"hexclave-oauth-inner-"` or snapshots will go noisy during dual-write. + +9. **Test assertion sweep.** Roughly 7+ e2e tests assert on exact `"Stack Auth: …"` error message prefixes and specific header names (`expect(...).toEqual({"x-stack-auth": ...})`, etc.). Update in lockstep with implementation in PR 1. + +10. **CLI Sentry DSN compile-time bake.** `packages/stack-cli/tsdown.config.ts` embeds `__STACK_CLI_SENTRY_DSN__`. Existing DSN stays (per locked decision); just be aware that old released CLI versions will keep emitting under their old DSN indefinitely — that's intentional. + +--- + +## PR 1 verification matrix + +Compatibility-sensitive enough to be part of the implementation plan, not implicit. + +### Auth wire +- [ ] Backend accepts every `x-stack-*` request header (incl. `x-stack-api-key`, `x-stack-request-type`, `x-stack-override-error-status`) +- [ ] Backend accepts every `x-hexclave-*` request header (incl. `x-hexclave-api-key`, `x-hexclave-request-type`, `x-hexclave-override-error-status`) +- [ ] Both header sets mixed in same request work +- [ ] CORS preflight allowlist in `proxy.tsx` includes both old + new names for request AND response headers +- [ ] New SDK emits `x-hexclave-*` by default +- [ ] Old SDK (unchanged) authenticates successfully +- [ ] Backend accepts `Authorization: Bearer stackauth_*` +- [ ] Backend accepts `Authorization: Bearer hexclave_*` +- [ ] New SDK constructs tokens with `Bearer hexclave_*` prefix +- [ ] `x-stack-auth: {...}` legacy header continues to be parsed (no Hexclave equivalent emitted) +- [ ] Backend emits BOTH `x-stack-*` and `x-hexclave-*` response headers (`actual-status`, `known-error`, `request-id`) +- [ ] New SDK reads `x-hexclave-*` response headers, falls back to `x-stack-*` +- [ ] Old SDK still reads `x-stack-*` response headers correctly + +### Cookies +- [ ] Sign-in with old `stack-access` / `stack-refresh-*` only → succeeds +- [ ] Sign-in with new `hexclave-access` / `hexclave-refresh-*` only → succeeds +- [ ] Both old + new cookies present → no conflict, new preferred +- [ ] Sign-out clears both old + new names +- [ ] Legacy `stack-refresh` (pre-projectId) still readable and deletable on sign-out +- [ ] OAuth flow dual-writes `stack-oauth-{inner,outer}-*` and `hexclave-oauth-{inner,outer}-*` +- [ ] OAuth callback reads either cookie name and completes flow +- [ ] Low-risk cookies (`stack-is-https`, changelog, cli-auth-confirmed) dual-written under both names +- [ ] CHIPS test cookies still under `__Host-stack-temporary-chips-test-*` (not renamed) +- [ ] Mobile OAuth callback (`stack-auth-mobile-oauth-url://`) unchanged + +### Env vars +- [ ] Old env only (every customer-facing var in Category A) → SDK initializes, deprecation warning emitted +- [ ] New env only → SDK initializes, no warning +- [ ] Both envs with different values → new wins, deprecation warning emitted +- [ ] Multi-alias Category B vars: all historical aliases readable, new canonical preferred +- [ ] `NEXT_PUBLIC_STACK_PORT_PREFIX` still works under that exact name (not renamed) +- [ ] Generated GitHub workflow with `STACK_AUTH_*` vars continues to authenticate +- [ ] Newly generated workflow emits `HEXCLAVE_*` and authenticates +- [ ] Self-host vars (Category C) untouched — `.env` files for operators unchanged +- [ ] Build/dev/test vars (Category E) still under `STACK_*` — no rename attempted + +### JWT +- [ ] Old normal issuer (`api.stack-auth.com/.../projects/{id}`) validates +- [ ] Old anonymous issuer validates +- [ ] Old restricted issuer validates +- [ ] New equivalents (all three) validate +- [ ] Convex provider config exposes new issuer URLs + +### MCP +- [ ] `ask_hexclave` tool works +- [ ] `ask_stack_auth` tool still works +- [ ] Setup pages teach new tool name + +### CLI +- [ ] `stack` binary still works +- [ ] `hexclave` binary works +- [ ] Old `~/.config/stack-auth/credentials.json` read on first run +- [ ] After first run, `~/.config/hexclave/credentials.json` exists +- [ ] Project config discovery: `hexclave.config.ts` preferred; falls back to `stack.config.ts` +- [ ] `hexclave init` generates `hexclave.config.ts` for new projects +- [ ] Existing project with only `stack.config.ts` works without migration +- [ ] `hexclave dev --config-file ./stack.config.ts` works (explicit override) +- [ ] GitHub config push writes to whichever filename already exists in customer repo + +### Packages +- [ ] `npm install @stackframe/stack` → imports `StackClientApp` AND `HexclaveClientApp` +- [ ] `npm install @hexclave/stack` → same, both aliases available +- [ ] Generated `.d.ts` exposes both names +- [ ] Source maps resolve +- [ ] Both packages can be installed side-by-side without conflicts +- [ ] `npm-publish.yaml` runs build → publish @stackframe → rewrite → publish @hexclave with no failures +- [ ] Rewrite script correctly updates `dependencies` / `peerDependencies` to `@hexclave/*` versions +- [ ] `@hexclave/cli` package registers `hexclave` binary +- [ ] `Symbol.for("StackAuth--app-internals")` and `Symbol.for("Hexclave--app-internals")` both resolve to the same internals + +### Swift +- [ ] Existing `StackAuth` SPM package still installable from its existing git URL +- [ ] Existing `import StackAuth` code continues to work unchanged +- [ ] New `Hexclave` SPM package installable from new URL +- [ ] `import Hexclave; let app = HexclaveClientApp(...)` works +- [ ] New `Hexclave` package default base URL is `api.hexclave.com` + +### Docs +- [ ] No unintended "Stack Auth" brand strings in new docs (lint pass) +- [ ] Old names appear only in compatibility sections +- [ ] Link checker passes against new GitHub slug + new domains +- [ ] OpenAPI shows `X-Hexclave-*` as canonical with compat note + +### Migrations +- [ ] Default `Stack Dashboard` → `Hexclave Dashboard` updates +- [ ] User-modified display names not overwritten +- [ ] Missing row no-ops safely + +### Tests + CI +- [ ] All test assertions on header names updated (search for `"x-stack-"` in `apps/e2e/tests/`) +- [ ] All test assertions on cookie names updated (incl. `expect(...).toMatch(/stack-oauth-inner-/)` patterns) +- [ ] Snapshot serializer (`apps/e2e/tests/snapshot-serializer.ts`) handles both `stack-oauth-inner-*` AND `hexclave-oauth-inner-*` prefixes +- [ ] Existing snapshot files regenerated cleanly (only the 1 known snapshot file) +- [ ] Test error messages updated for "Stack Auth: …" → "Hexclave: …" pattern +- [ ] `HexclaveAssertionError` message reads "This is likely an error in Hexclave." +- [ ] CI workflows pass with both old and new package names installable + +--- + +## Rollout — 2 PRs + +### PR 1: "Rebrand to Hexclave (additive)" — now + +One large additive PR. Nothing deleted. Existing users continue working untouched. Verification matrix above must pass. **Prerequisite (not in the PR itself): exhaustive operator env var inventory** — see Category C. + +Major work items: +- Header / cookie / env var dual-accept (Tier 0) +- Template re-exports propagating to generated SDKs (Tier 1 JS) +- Swift: stand up new `Hexclave` SPM package with real `Hexclave*` symbols + `api.hexclave.com` base URL; freeze existing `StackAuth` package +- `sdks/spec` update +- Publish-time mirror artifacts for `@hexclave/*` packages (Tier 2) +- CLI dual config paths + binary alias +- JWT validator accepts both domains for all 3 issuer types +- MCP dual tool registration +- Idempotent seed/data migration with tests +- Mechanical sweep: domains (full inventory), repo slug, page titles, OpenAPI titles, generated content (after generator updates), examples, README family, assets +- DNS: stand up all `*.hexclave.com` subdomains; redirect from `*.stack-auth.com` +- `turbo.json` `globalEnv` adds `HEXCLAVE_*` + +### PR 2: "Remove non-essential Stack Auth fallbacks" — 12+ months later + +Only after operational evidence shows the targeted fallbacks are unused (telemetry decision deferred from PR 1). + +Pure deletion, but **narrowly scoped**. Wire identifiers (request headers, response headers, JWT issuers, Bearer prefix, OAuth state cookies, mobile URL scheme, SDK class aliases) stay indefinitely and are NOT touched by PR 2. The legacy `StackAuth` Swift package stays installable but unmaintained — also untouched. Same goes for everything in "Do not rename". + +Safely removable in PR 2: + +- Stop dual-writing main auth cookies under their old `stack-*` names (old cookies have long expired naturally; reads of old names can also be dropped) +- Stop reading `STACK_*` customer SDK env vars (or hard-error with a migration message) — only after operator dashboards confirm low usage +- Remove `ask_stack_auth` MCP tool — only after AI client adoption of `ask_hexclave` is high +- Tear down non-essential `*.stack-auth.com` subdomains (keep `api.stack-auth.com` indefinitely — Apple sign-in setup depends on it) +- `@stackframe/*` published packages: leave on npm with a "moved to `@hexclave/*`" README; do not unpublish (npm unpublishing breaks the ecosystem) + +**Explicitly NOT removed in PR 2:** + +- `x-stack-*` request headers (kept dual-accepted indefinitely) +- `x-stack-*` response headers (kept dual-emitted indefinitely) +- `Bearer stackauth_*` prefix (kept dual-accepted indefinitely) +- `x-stack-auth` legacy header (still parsed) +- JWT validator's acceptance of all three `stack-auth.com` issuer variants and the IdP audience +- JS `Stack*` exports — they're the canonical class names, not aliases +- Legacy `StackAuth` Swift package (frozen but installable from existing SPM URL) +- OAuth state cookies (`stack-oauth-*`), CHIPS test cookies, `stack-auth-mobile-oauth-url://` +- `stack.config.ts` filename (still readable as fallback) +- Everything in the "Do not rename" table + +--- + +## Open questions still worth answering before implementation + +- **Operator env var inventory:** must be produced before PR 1 implementation begins. Once produced, decide whether category C scope fits in PR 1 or needs to be deferred to a follow-up. Current plan: include in PR 1 if scope is manageable. +- **SDK request header emission:** new SDKs emit `x-hexclave-*` for every request header (current plan), or skip the most stable ones (e.g. `branch-id`) to reduce churn? Current plan: emit all. +- **DNS infrastructure:** ops team confirmation on indefinite redirect maintenance capacity for 16 subdomains. +- **Future canonicality flip (post PR 2):** is there any reason to ever make `Hexclave*` the canonical class name in JS or Swift, with `Stack*` as the alias? Current plan: no — coexistence indefinitely, neither is "more canonical". +- `hexclave.config.ts` is the new canonical config filename; `stack.config.ts` read-fallback stays in discovery indefinitely. We'd only drop the fallback after telemetry shows essentially no projects rely on it. +- DNS infrastructure ownership for redirects — operations team needs to confirm capacity for indefinite redirect maintenance. From 0bef15f99e1f8b8c24e9cf7e66b2ffcefea399e9 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Thu, 21 May 2026 18:00:07 -0700 Subject: [PATCH 02/28] docs: cover query params, storage keys, custom events, and dev tool in rebrand plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review gaps in the Hexclave rename plan: - Storage keys: add the missed localStorage keys `_STACK_AUTH.lastUsed`, `stack:session-replay:v1:*`, `__stack-dev-tool-state`, and `stack-devtool-trigger-position`, with per-key compat risk notes. - Query parameters: new Tier 0 category — `stack_response_mode`, the four `stack_cross_domain_*` handoff params, and `stack-init-id` are wire-compat-sensitive and were previously uncovered. - Custom DOM events: `stack-platform-change` / `stack-framework-change`. - Dev tool: dev-tool-core.ts is its own brand surface (storage keys, header-emit site, DOM identifiers, brand strings). --- RENAME-TO-HEXCLAVE.md | 49 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/RENAME-TO-HEXCLAVE.md b/RENAME-TO-HEXCLAVE.md index 9342e8f4c5..f431801e78 100644 --- a/RENAME-TO-HEXCLAVE.md +++ b/RENAME-TO-HEXCLAVE.md @@ -255,8 +255,55 @@ AI clients (Claude, Cursor, etc.) have `ask_stack_auth` baked into their MCP con | `stack-docs-selected-platform` (sessionStorage) | `hexclave-docs-selected-platform` | | `stack-docs-selected-frameworks` (sessionStorage) | `hexclave-docs-selected-frameworks` | | `stack_mfa_attempt_code` (sessionStorage, underscore-delimited) | `hexclave_mfa_attempt_code` | +| `_STACK_AUTH.lastUsed` (localStorage, dot-delimited — `packages/template/src/components/oauth-button.tsx`) | `_HEXCLAVE.lastUsed` | +| `stack:session-replay:v1:{projectId}` (localStorage, colon-delimited versioned prefix — `packages/template/src/lib/stack-app/apps/implementations/session-replay.ts`) | `hexclave:session-replay:v1:{projectId}` | +| `__stack-dev-tool-state` (localStorage — `packages/template/src/dev-tool/dev-tool-core.ts`) | `__hexclave-dev-tool-state` | +| `stack-devtool-trigger-position` (localStorage — `packages/template/src/dev-tool/dev-tool-core.ts`) | `hexclave-devtool-trigger-position` | -Note the third key uses underscores instead of hyphens — preserve the existing convention for the new name to keep the access pattern identical. +Delimiter conventions are inconsistent across these keys (hyphen, underscore, dot, colon) — preserve each key's existing convention for its new name so the access pattern stays identical. + +**Per-key risk.** The docs `sessionStorage` keys and `stack_mfa_attempt_code` follow the dual-write / prefer-new pattern. The dev-tool keys (`__stack-dev-tool-state`, `stack-devtool-trigger-position`) and the OAuth `_STACK_AUTH.lastUsed` last-provider hint are UI-only local preferences — a one-time reset on rename is harmless, so a straight rename is acceptable. `stack:session-replay:v1:*` holds an in-progress recording session ID; dual-read the old key so a recording session active across the SDK upgrade is not orphaned. + +### Query parameters (dual-accept) + +`stack_*` / `stack-*` URL query parameters travel between SDK and backend, or across domains during auth handoffs — the same wire-compatibility risk class as headers. Earlier plan versions had no query-parameter category. **Alias, never replace:** the reader accepts both names; new code emits the new name. + +| Old (accepted indefinitely) | New (preferred) | Flow | +|---|---|---| +| `stack_response_mode` | `hexclave_response_mode` | SDK → backend (OAuth authorize) | +| `stack_cross_domain_auth` | `hexclave_cross_domain_auth` | cross-domain handoff (SDK ↔ SDK across domains) | +| `stack_cross_domain_state` | `hexclave_cross_domain_state` | cross-domain handoff | +| `stack_cross_domain_code_challenge` | `hexclave_cross_domain_code_challenge` | cross-domain handoff | +| `stack_cross_domain_after_callback_redirect_url` | `hexclave_cross_domain_after_callback_redirect_url` | cross-domain handoff | +| `stack-init-id` | `hexclave-init-id` | init CLI → dashboard wizard-congrats page | + +**`stack_response_mode`** — emitted by `packages/stack-shared/src/interface/client-interface.ts:1419`, read by the yup query schema at `apps/backend/src/app/api/latest/auth/oauth/authorize/[provider_id]/route.tsx:42`. The backend schema must accept both keys (prefer new). This needs genuine dual-accept, not a rename: if the param is dropped silently the backend falls back to `responseMode: "redirect"` and the SDK can no longer intercept bot challenges before navigating. + +**`stack_cross_domain_*`** — the four param names are defined together in `packages/template/src/lib/stack-app/apps/implementations/redirect-page-urls.ts:6-9`; the `stack_cross_domain_auth === "1"` marker is read at `packages/template/src/components-page/stack-handler-client.tsx:267`. Writer and reader are both in the SDK, so a handoff between two different SDK majors (one per domain) must still resolve: dual-emit both param sets into the redirect URL, and accept either on read. + +**`stack-init-id`** — emitted by `packages/init-stack/src/index.ts:452`, read by `apps/dashboard/src/app/(main)/wizard-congrats/posthog.tsx:12`. Dashboard reads either key; new `init` CLI emits the new one. Low-stakes (PostHog distinct-id correlation only) but follows the same pattern. Note the hyphen delimiter — the cross-domain and response-mode params use underscores; preserve each. + +A repo-wide grep for `stack_` / `stack-` query keys should confirm this list before PR 1. + +### Custom DOM events + +The docs site syncs platform/framework selection across components via `window`-dispatched `CustomEvent`s with `stack-`-prefixed names. They pair with the `stack-docs-selected-*` sessionStorage keys above. + +| Old | New | +|---|---| +| `stack-platform-change` | `hexclave-platform-change` | +| `stack-framework-change` | `hexclave-framework-change` | + +**Files:** `docs/src/components/layouts/platform-indicator.tsx` and `docs/src/components/mdx/platform-codeblock.tsx` (both dispatch and listen). These events are dispatched and consumed entirely within a single docs-site page load — no cross-version or persistence concern. Straight rename; update dispatch and listener sites in lockstep. + +### Dev tool (`packages/template/src/dev-tool/dev-tool-core.ts`) + +The in-app dev tool ships inside the SDK (`packages/template`, propagated to every generated SDK) and is its own brand surface, missed by earlier plan versions. It spans tiers: + +- **localStorage keys** — `__stack-dev-tool-state`, `stack-devtool-trigger-position` (in the Storage keys table above). +- **Header-emit site** — the AI tab builds a `fetch` to the AI endpoint with hand-written `X-Stack-Access-Type`, `X-Stack-Project-Id`, `X-Stack-Publishable-Client-Key` headers, bypassing the normal client interface. The request-header table covers the backend *accept* side; SDK header *emit* sites like this one must be switched to `x-hexclave-*` and enumerated during PR 1. +- **DOM identifiers** — element id `__stack-dev-tool-root`, global key `__stack-dev-tool-instance`, attribute `data-stack-devtool-trigger`. Internal, no compat needed — straight rename. +- **Brand strings / domains** — many "Stack Auth" UI strings and `docs.stack-auth.com` / `app.stack-auth.com` / `test.stack-auth.com` references; covered by the Tier 4 sweep + domain inventory, but the file must be on the sweep list. --- From 1843a37ab41b2d30f1bd61494ae9e246acd6b57c Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Fri, 22 May 2026 11:23:59 -0700 Subject: [PATCH 03/28] =?UTF-8?q?docs:=20v7=20=E2=80=94=20internal-only=20?= =?UTF-8?q?renames,=20env=20var=20dual-read,=203-PR=20rollout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tier 1: internal-only symbols (SDK interfaces, StackAssertionError) renamed outright with no alias; user-facing symbols keep aliases - Tier 2: @hexclave/* starts at 1.0.0, lockstep versioning; add npm deprecate + runtime warn for old packages; drop @hexclave/init - Env vars: all categories dual-read (incl. operator/internal); NEXT_PUBLIC_STACK_PORT_PREFIX renamed outright - Emails: noreply moves to sent-with-hexclave.com sending domain - CHIPS test cookies out of scope (unused feature) - Rollout split into 3 PRs: invisible compat layer, visible rebrand, far-future fallback removal --- RENAME-TO-HEXCLAVE.md | 222 +++++++++++++++++++++++++----------------- 1 file changed, 132 insertions(+), 90 deletions(-) diff --git a/RENAME-TO-HEXCLAVE.md b/RENAME-TO-HEXCLAVE.md index f431801e78..c77ada055f 100644 --- a/RENAME-TO-HEXCLAVE.md +++ b/RENAME-TO-HEXCLAVE.md @@ -1,8 +1,8 @@ -# Stack Auth → Hexclave Rename Plan (v6) +# Stack Auth → Hexclave Rename Plan (v7) Rebrand rollout with backwards compatibility. Organized by wire-compatibility risk: what breaks existing users vs. what's purely cosmetic. -**Rollout strategy:** one large additive PR that introduces Hexclave naming everywhere without breaking anything, then a much-later cleanup PR that removes only the safely-removable fallbacks (cookies, customer SDK env vars, MCP legacy tool, non-essential DNS). +**Rollout strategy:** three PRs. **PR 1** is invisible — wire-level dual-accept/dual-write, SDK export aliases, and internal renames, all shipped inside the existing `@stackframe/*` packages and existing deploys; nothing breaks and no Hexclave branding is yet visible to any user. **PR 2** makes the brand public — new `@hexclave/*` and Swift packages, `@deprecated` markers on old names, and every user-facing string, domain, title, and doc. **PR 3** (12+ months later) removes only the safely-removable fallbacks once telemetry confirms they're unused. See [Rollout](#rollout--3-prs). ## Locked-in decisions @@ -18,17 +18,19 @@ Rebrand rollout with backwards compatibility. Organized by wire-compatibility ri - `stack.config.ts` ↔ `hexclave.config.ts` (discovery prefers new, falls back to old) - `stack-auth-mobile-oauth-url://` ↔ `hexclave-mobile-oauth-url://` (backend accepts both schemes; new Swift SDK registers the new one) - `Symbol.for("StackAuth--app-internals")` ↔ `Symbol.for("Hexclave--app-internals")` (attach under both, look up both) - - JS `Stack*` exports stay canonical; `Hexclave*` added as aliases — both kept indefinitely + - JS public `Stack*` exports stay canonical; `Hexclave*` added as aliases — both kept indefinitely. `Stack*` names get `@deprecated` JSDoc in PR 2. **Internal-only** symbols (the three SDK interfaces, `StackAssertionError`) are renamed outright with no alias — see Tier 1 - **Swift SDK: separate package**, not typealiases. `StackAuth` Swift package frozen at existing git URL — old users keep `import StackAuth`. New `Hexclave` Swift package (new git URL) is the canonical going-forward SDK with real `Hexclave*` symbols. Breaking changes allowed between versions; old package remains installable but unmaintained. - New docs teach **Hexclave-only** names; old names appear only in explicitly-marked compatibility notes -- Self-host operator env vars (Category C) are **out of scope** — stay as `STACK_*`, no aliasing +- All env vars are **dual-read** (`HEXCLAVE_*` accepted alongside `STACK_*`, new name preferred) across every category — including self-host/operator (Category C) and build/dev/test (Category E) vars. Sole exception: `NEXT_PUBLIC_STACK_PORT_PREFIX`, renamed outright (dev-only, no dual-accept) - `@hexclave/*` packages dual-published via rewrite step in `.github/workflows/npm-publish.yaml` (see Tier 2) - **Sentry / PostHog / observability DSNs** — out of scope. Existing DSNs continue unchanged. No project renames in either tool. - **`skill.stack-auth.com`** — DNS redirects to `skill.hexclave.com`; both URLs serve identical content indefinitely. Customers with cached MCP configs pointing at old domain keep working. -- **`StackAssertionError`** — class gets `HexclaveAssertionError` alias (per Tier 1 pattern); error message string updates from "This is likely an error in Stack." → "This is likely an error in Hexclave." in PR 1. -- **CHANGELOG title** — becomes "Hexclave Changelog" in PR 1. History continuity preserved through commit log, not title. -- **Test assertion updates** — every test that asserts on header names, cookie names, error message prefixes, etc. updates in lockstep with implementation in PR 1. -- **Deprecation warning text** — exact wording is an implementation-time decision; SDK init logs `console.warn` once per process. +- **`StackAssertionError`** — internal-only (not exported from any customer SDK entrypoint), so it is renamed to `HexclaveAssertionError` with **no alias** (see Tier 1). Its error message string updates from "This is likely an error in Stack." → "This is likely an error in Hexclave." as part of the Tier 4 brand-string sweep (PR 2). +- **CHANGELOG title** — becomes "Hexclave Changelog" in PR 2 (Tier 4 brand string). History continuity preserved through commit log, not title. +- **Test assertion updates** — every test that asserts on header names, cookie names, error message prefixes, etc. updates in lockstep with the implementation, in whichever PR changes that identifier (wire identifiers in PR 1, brand strings in PR 2). +- **`@hexclave/*` package versions** start at `1.0.0` and bump in lockstep with `@stackframe/*` releases — one `@hexclave` publish per `@stackframe` publish; absolute version numbers stay offset (`@stackframe/*` is currently `2.8.92`). +- **Old-package deprecation (PR 2)** — `@stackframe/*` packages are marked deprecated on npm via `npm deprecate`; the SDK additionally logs a `console.warn` once per process recommending the `@hexclave/*` equivalent. All `Stack*`-named public exports get `@deprecated` JSDoc. Exact warning wording is an implementation-time decision. +- **CHIPS partitioned-cookie test** — the feature is no longer used and will be removed in a separate change; this rebrand ignores it entirely (no rename, no dual-write). - **Docker registry path / image naming** — not part of this rebrand; existing image tags continue. - Telemetry is deferred; not blocking PR 1 @@ -39,7 +41,7 @@ Rebrand rollout with backwards compatibility. Organized by wire-compatibility ri | HTTP headers (`x-stack-*`) | 21 | | Cookies | ~12 | | Customer-facing env vars | ~20+ | -| NPM packages | 11 | +| `@hexclave/*` mirror packages | 9 | | Public SDK classes/components/hooks (JS) | ~12 | | Swift module + symbols | 1 module, ~10 symbols | | Domain references | 625+ | @@ -160,9 +162,7 @@ Every cookie containing "stack" gets a `hexclave-*` equivalent dual-written. Rea | `stack-last-seen-changelog-version` | `hexclave-last-seen-changelog-version` | | `stack-cli-auth-confirmed` | `hexclave-cli-auth-confirmed` | -**CHIPS test cookies — keep as `stack-*` indefinitely** (internal, never user-visible, no functional reason to rename): - -- `__Host-stack-temporary-chips-test-*` +**CHIPS test cookies — out of scope.** The `__Host-stack-temporary-chips-test-*` probe cookies belong to the partitioned-cookie support test (`_internalShouldSetPartitionedClient` in `packages/template/src/lib/cookie.ts`). These are ephemeral — set and deleted within the same synchronous call, never persisted. The feature is no longer used and is slated for removal in a separate change, so this rebrand does not touch it: no rename, no dual-write. Additional surfaces that set/read cookies and need updating in PR 1: @@ -307,15 +307,17 @@ The in-app dev tool ships inside the SDK (`packages/template`, propagated to eve --- -## Tier 1 — Public SDK API (alias via re-exports) +## Tier 1 — Public SDK API (aliases for user-facing symbols, outright rename for internal ones) ### JS / React / Next.js / TanStack SDKs Codegen makes this clean. `scripts/generate-sdks.ts` copies `packages/template` → `packages/{js,stack,react,tanstack-start}`. Add re-exports once in template; all generated packages get both names. -Dual-export every public Stack* symbol: +**Classification rule:** a symbol gets a `Hexclave*` alias only if it is **user-facing** — reachable from a customer SDK entrypoint (`@stackframe/stack` / `@stackframe/js` / `@stackframe/react`). Symbols that are internal-only — not in any customer SDK entrypoint — are renamed outright with no alias (next subsection). + +Dual-export every user-facing `Stack*` symbol: -| Old (kept) | New (alias added) | +| Old (kept, `@deprecated` in PR 2) | New (alias added) | |---|---| | `StackClientApp` | `HexclaveClientApp` | | `StackServerApp` | `HexclaveServerApp` | @@ -324,19 +326,30 @@ Dual-export every public Stack* symbol: | `StackHandler` | `HexclaveHandler` | | `StackTheme` | `HexclaveTheme` | | `useStackApp()` | `useHexclaveApp()` | -| `StackClientInterface` | `HexclaveClientInterface` | -| `StackServerInterface` | `HexclaveServerInterface` | -| `StackAdminInterface` | `HexclaveAdminInterface` | -| `StackAssertionError` | `HexclaveAssertionError` (plus: error message text updates from "This is likely an error in Stack." → "This is likely an error in Hexclave." in `packages/stack-shared/src/utils/errors.tsx`) | | `StackConfig` | `HexclaveConfig` | | `defineStackConfig()` | `defineHexclaveConfig()` | | `Stack*ConstructorOptions` | `Hexclave*ConstructorOptions` | | `Stack{Client,Server,Admin}AppConstructor` | `Hexclave{Client,Server,Admin}AppConstructor` | | `StackClientAppJson` | `HexclaveClientAppJson` | -**Pattern:** `export { StackClientApp as HexclaveClientApp }`. Same class, both names. Users can mix freely. +The type cluster (`Stack*ConstructorOptions`, `Stack{Client,Server,Admin}AppConstructor`, `StackClientAppJson`) is obscure but *is* exported from the customer SDK index ([`packages/template/src/lib/stack-app/index.ts`](packages/template/src/lib/stack-app/index.ts)). Aliasing is free (`export type { X as Y }`) and a wrong rename would be breaking, so these keep aliases. + +**Pattern:** `export { StackClientApp as HexclaveClientApp }`. Same class, both names. Users can mix freely. Adding the aliases is non-breaking and ships in **PR 1**; the `@deprecated` JSDoc on the `Stack*` names is IDE-visible (strikethrough) and ships in **PR 2**. + +### Internal-only symbols — renamed outright, no alias + +These are **not exported from any customer SDK entrypoint** (`@stackframe/stack` / `js` / `react`). The three interfaces are exported only from the low-level `@stackframe/stack-shared` package's index — an implementation-detail package, not a customer-facing API; `StackAssertionError` is not exported from any public index at all. Per the "internal-only → rename, no alias" rule they are renamed in place — every reference updates in lockstep, no `Stack*` name survives. Non-user-facing, so this lands in **PR 1**. + +| Old (removed, no alias) | New | +|---|---| +| `StackClientInterface` | `HexclaveClientInterface` | +| `StackServerInterface` | `HexclaveServerInterface` | +| `StackAdminInterface` | `HexclaveAdminInterface` | +| `StackAssertionError` | `HexclaveAssertionError` (the class rename is PR 1; its user-visible message text "This is likely an error in Stack." → "Hexclave" is a Tier 4 brand string, PR 2) | + +> If a pre-implementation grep finds any of these re-exported from a customer SDK entrypoint after all, that symbol moves back to the alias table — the rule is "internal *and* not directly reachable by users." -**Canonicality:** `Stack*` is the internal/canonical class name; `Hexclave*` is the alias. This means PR 2 cannot "remove Stack* aliases" — they're the originals. Both names stay indefinitely. If a future effort wants to flip canonicality so `Hexclave*` is the real class and `Stack*` is the alias, that's a separate, optional follow-up — not part of this rebrand. +**Canonicality.** For the user-facing classes, `Stack*` remains the underlying class name and `Hexclave*` is the alias; both stay indefinitely (**PR 3 does not remove the `Stack*` names** — they're the originals). `Stack*` is marked `@deprecated` to steer new code toward `Hexclave*`, but "deprecated" here means "discouraged," not "scheduled for removal." A future effort could flip canonicality so `Hexclave*` is the real class — separate, optional, out of scope here. `stack.config.ts` filename stays (locked decision). `showOnboardingStackConfigValue` stays internal — no alias needed. @@ -383,7 +396,7 @@ Notes: - `sdks/spec/src/` - `sdks/spec/README.md` -Per AGENTS.md, SDK implementation changes must update `sdks/spec` — bake this into the PR 1 checklist. +Per AGENTS.md, SDK implementation changes must update `sdks/spec` — bake this into the PR 2 checklist (the new Swift package is part of PR 2). --- @@ -418,7 +431,8 @@ Concrete change to the existing workflow: `scripts/rewrite-packages-to-hexclave.ts` does, for each publishable package per the mapping table below: - Read `package.json` - Rewrite `name`: `@stackframe/foo` → `@hexclave/foo` -- Rewrite all `dependencies` / `peerDependencies` entries from `@stackframe/X` → `@hexclave/X` with the version of the just-published artifact +- Set `version`: `@hexclave/*` packages carry their **own** version line, starting at `1.0.0` and bumped once per `@stackframe/*` release (lockstep cadence — absolute numbers stay offset from `@stackframe/*`, currently `2.8.92`). The script reads the target `@hexclave` version from a single source (a `HEXCLAVE_VERSION` file or workflow input); all mirror packages share one version. +- Rewrite all `dependencies` / `peerDependencies` entries `@stackframe/X` → `@hexclave/X`, pinned to the **`@hexclave` version being published** (not the `@stackframe` version) — since all mirror packages share one version this is a single substitution - Update `bin` entries where relevant (e.g. `@hexclave/cli` registers `hexclave` binary alongside the existing `stack`) - Leave built `dist/` artifacts untouched (no rebuild needed) @@ -429,7 +443,7 @@ Notes: - Source maps, type declarations, `exports`, `typesVersions` resolve under both names because they're the same built artifacts - The rewrite step only runs in CI; local development keeps using `@stackframe/*` names -### 10 mirrored packages +### 9 mirrored packages | Old (kept) | New (mirrored) | |---|---| @@ -439,37 +453,49 @@ Notes: | `@stackframe/stack-shared` | `@hexclave/shared` | | `@stackframe/stack-ui` | `@hexclave/ui` | | `@stackframe/stack-sc` | `@hexclave/sc` | -| `@stackframe/init-stack` | `@hexclave/init` | | `@stackframe/stack-cli` | `@hexclave/cli` | | `@stackframe/tanstack-start` | `@hexclave/tanstack-start` | | `@stackframe/dashboard-ui-components` | `@hexclave/dashboard-ui-components` | **`@stackframe/dashboard-ui-components` is publishable.** Earlier plan versions marked it "internal only" — that was wrong. It's loaded at runtime via esm.sh by the dashboard's create-dashboard sandbox host ([apps/dashboard/.../dashboard-sandbox-host.tsx](apps/dashboard/src/components/commands/create-dashboard/dashboard-sandbox-host.tsx)) plus served locally as an IIFE bundle (`dashboard-ui-components.iife.js`). Mirror it like the other public packages. The IIFE bundle filename also gets dual-served — both `dashboard-ui-components.iife.js` and a future Hexclave-branded path (TBD) until generated dashboards stored with the old filename can be updated. -**Not mirrored — internal:** `@stackframe/template` (codegen source). +**Not mirrored:** +- `@stackframe/template` — codegen source, internal. +- `@stackframe/init-stack` — the standalone init wizard. Stays published under its existing name (existing `npx @stackframe/init-stack` users keep working), but gets **no `@hexclave` mirror**: new-user onboarding moves to the CLI's `init` subcommand (`npx @hexclave/cli@latest init`). See CLI section. **Not publishable, stay `@stackframe/*`:** `@stackframe/monorepo`, backend, dashboard, docs, mcp, hosted-components, skills, mock-oauth-server, e2e, internal-tool, dev-launchpad. +### Deprecating the `@stackframe/*` packages (PR 2) + +`@stackframe/*` packages stay published and fully functional indefinitely — but once the `@hexclave/*` mirrors exist, new installs should be steered to them. Two layers, both in PR 2: + +- **npm-level:** run `npm deprecate "@stackframe/@*" "Renamed to @hexclave/ — see "` for each mirrored package. npm surfaces this on every `npm install`. +- **Runtime:** the SDK logs a `console.warn` once per process on init when it was loaded from a `@stackframe/*` package, recommending the `@hexclave/*` equivalent. (How the SDK knows which name it shipped under is an implementation detail — e.g. a build-time constant stamped by the rewrite script.) + +Separately, every `Stack*`-named public export gets `@deprecated` JSDoc (see Tier 1). Because `@stackframe/*` and `@hexclave/*` are generated from the same `packages/template` source, the `@deprecated` tag lands in **both** packages — that is intended: `Stack*` is the old brand regardless of which package ships it, and `Hexclave*` is the name to prefer everywhere. The npm-level `npm deprecate` is the only piece scoped to `@stackframe/*` specifically. + ### CLI / init wizard | Old (kept) | New | |---|---| -| `npx @stackframe/init-stack` | `npx @hexclave/init` | +| `npx @stackframe/init-stack` | `npx @hexclave/cli@latest init` — onboarding moves to the CLI's `init` subcommand | | `stack` binary | `hexclave` binary alias | | `~/.config/stack-auth/credentials.json` | `~/.config/hexclave/credentials.json` | | `stack.config.ts` (fallback) | `hexclave.config.ts` (preferred default) | CLI reads both config paths; writes new path. Old path silently migrates on next run. For project config: `init` generates `hexclave.config.ts` in new projects; discovery prefers `hexclave.config.ts` and falls back to `stack.config.ts` for existing projects (see Tier 0 details). +The canonical onboarding command everywhere — docs, dashboard setup snippets, generated prompts — becomes `npx @hexclave/cli@latest init`. `npx @stackframe/stack-cli@latest init` is the byte-identical command under the old package name and keeps working. The standalone `@stackframe/init-stack` package is no longer the taught entrypoint and is not mirrored as `@hexclave/*`. + --- ## Env var taxonomy -Replaces the flat env var table from v1. Different categories warrant different treatment. +Replaces the flat env var table from v1. **Every category is dual-read** — `HEXCLAVE_*` accepted alongside `STACK_*`, new name preferred and documented. Categories differ only in audience and which docs change. Sole exception: the dev-only port-prefix var, renamed outright (see Category B). ### Table shape -For each *concept* (e.g. "Project ID"), the repo may already have multiple env var aliases (Vite vs. Next, BROWSER prefix vs. suffix, etc.). The plan picks **one canonical Hexclave name per concept**; all currently-recognized old names continue to be read as compat aliases. A grep-based pass over Category A and B old-name aliases should be done before implementation to confirm the list below matches what's actually in the repo. +For each *concept* (e.g. "Project ID"), the repo may already have multiple env var aliases (Vite vs. Next, BROWSER prefix vs. suffix, etc.). The plan picks **one canonical Hexclave name per concept**; all currently-recognized old names continue to be read as compat aliases. A grep-based pass over Category A, B, and C old-name aliases should be done before implementation to confirm the list below matches what's actually in the repo. ### A. Customer SDK env vars (dual-read, prefer Hexclave) @@ -515,15 +541,15 @@ Used by dashboard/backend/local-dev tooling. Some concepts have multiple histori > The exact list of "Old accepted" aliases above is best-effort and **must be validated** against a repo-wide grep before implementation. The reviewer flagged that prior versions of this plan listed aspirational names (`NEXT_PUBLIC_STACK_BROWSER_API_URL`) that don't actually exist in the repo. -**Exception — keep indefinitely:** `NEXT_PUBLIC_STACK_PORT_PREFIX`. Baked into every dev's local Docker/`.env`; renaming has zero user-facing value and breaks local setups. +**Exception — renamed outright, no dual-accept:** `NEXT_PUBLIC_STACK_PORT_PREFIX` → `NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX`. A dev-only var baked into local Docker/`.env` setups; unlike every other env var it is *not* dual-read — it's a straight rename. Each developer updates their local `.env` once (internal-only churn, no customer impact). -### C. Self-host / operator env vars — out of scope +### C. Self-host / operator env vars (dual-read, prefer Hexclave) -These remain as `STACK_*` indefinitely. Not part of the rebrand. +Used by operators running their own instance. Previously scoped out; **now dual-read** like every other category — the runtime accepts `HEXCLAVE_*` alongside the existing `STACK_*` name, prefers `HEXCLAVE_*`, and docs / `.env.example` teach the new name. -- `STACK_DATABASE_CONNECTION_STRING`, `STACK_SERVER_SECRET`, `STACK_EMAIL_*`, `STACK_S3_*`, `STACK_SVIX_*`, `STACK_QSTASH_*`, `STACK_STRIPE_*`, `STACK_FREESTYLE_*`, `STACK_OPENROUTER_API_KEY`, `STACK_MCP_LOG_TOKEN`, `STACK_CLICKHOUSE_*`, `STACK_RUN_MIGRATIONS`, `STACK_RUN_SEED_SCRIPT`, `STACK_SEED_INTERNAL_PROJECT_*`, local emulator + QEMU vars +- `STACK_DATABASE_CONNECTION_STRING`, `STACK_SERVER_SECRET`, `STACK_EMAIL_*`, `STACK_S3_*`, `STACK_SVIX_*`, `STACK_QSTASH_*`, `STACK_STRIPE_*`, `STACK_FREESTYLE_*`, `STACK_OPENROUTER_API_KEY`, `STACK_MCP_LOG_TOKEN`, `STACK_CLICKHOUSE_*`, `STACK_RUN_MIGRATIONS`, `STACK_RUN_SEED_SCRIPT`, `STACK_SEED_INTERNAL_PROJECT_*`, local emulator + QEMU vars — each gets a `HEXCLAVE_*` equivalent. -Self-hosters keep their existing `.env` files unchanged. No deprecation warnings, no docs migration, no `.env.example` rewrite. Operators are not affected by the rebrand at the env-var layer. +Existing operator `.env` files keep working unchanged (old names still read). This makes the exhaustive operator-var inventory (see Open questions) **in-scope, required work** — not a deferred prerequisite — since every operator var now needs a dual-read site, a docs update, and an `.env.example` entry. ### D. GitHub onboarding workflow @@ -550,9 +576,9 @@ These are the **same env vars** customers set in their own apps (Category A) — New generated workflows emit `HEXCLAVE_*`. Existing customer workflows with `STACK_AUTH_*` secrets / `STACK_*` process env vars keep working. Generated-workflow tests cover both shapes. -### E. Build / dev / test env vars (keep as `STACK_*`) +### E. Build / dev / test env vars (dual-read, prefer Hexclave) -Classified as internal. Not part of the brand rebrand. +Internal tooling vars. **Dual-read** like every other category. - `STACK_SKIP_TEMPLATE_GENERATION` - `STACK_DISABLE_REACT_ASYNC_DEBUG_INFO` @@ -560,11 +586,13 @@ Classified as internal. Not part of the brand rebrand. - `STACK_RUN_SETUP_WIZARD_TESTS` - `STACK_TEST_SDK_FALLBACK` -Add to `turbo.json` `globalEnv`: `HEXCLAVE_*` alongside existing `STACK_*`. +Each gets a `HEXCLAVE_*` equivalent, dual-read at its use site. Add the `HEXCLAVE_*` form of every env var (Categories A–E) to `turbo.json` `globalEnv` alongside the existing `STACK_*` form. --- -## Tier 3 — Persistent data (idempotent migrations in PR 1) +## Tier 3 — Persistent data (idempotent migrations) + +The display-name and email-config migrations change user-visible data → **PR 2**. The IdP-audience validator change is compatibility-only → **PR 1**. ### Internal project display name @@ -650,13 +678,13 @@ Complete old→new table. All old domains keep resolving/redirecting indefinitel ### Emails -| Old | New | -|---|---| -| `noreply@stackframe.co` | `noreply@hexclave.com` | -| `security@stack-auth.com` | `security@hexclave.com` | -| `team@stack-auth.com` | `team@hexclave.com` | +| Old | New | Notes | +|---|---|---| +| `noreply@stackframe.co` | `noreply@sent-with-hexclave.com` | Transactional/bulk sender on a **separate domain** — see below | +| `security@stack-auth.com` | `security@hexclave.com` | Inbound mailbox | +| `team@stack-auth.com` | `team@hexclave.com` | Inbound mailbox | -Set up new mailboxes; forward old → new during transition. +**Transactional sender uses a dedicated domain.** The bulk/transactional sender (`noreply@`) moves to a separate registrable domain — `sent-with-hexclave.com` or similar (exact name TBD) — *not* `noreply@hexclave.com`. This is a deliberate split: it isolates bulk-email deliverability problems from the primary `hexclave.com` domain's reputation. That domain must be registered and configured with SPF/DKIM/DMARC. It is **not** in the domain inventory above — it has no `stack-auth.com` predecessor (the old sender was on `stackframe.co`). The inbound human mailboxes (`security@`, `team@`) carry no reputation risk and move to `hexclave.com` as normal. Set up new mailboxes; forward old → new during transition. ### Page titles and metadata @@ -720,18 +748,18 @@ Not just subjects — body strings too. Hardcoded in source, not in DB templates - "Test Email from Stack Auth" — `apps/backend/src/app/api/latest/internal/send-test-email/route.tsx` - "Thank you for using Stack Auth!" — `apps/backend/src/app/api/latest/internal/failed-emails-digest/route.ts` - "Stack Auth User" default passkey display name -- Any other hardcoded subject/body containing "Stack Auth" — grep before PR 1 +- Any other hardcoded subject/body containing "Stack Auth" — grep before PR 2 ### CHANGELOG title flip -`CHANGELOG.md` title becomes "Hexclave Changelog" in PR 1. Existing entries' commit-by-commit context preserves continuity; no need to dual-name the title. +`CHANGELOG.md` title becomes "Hexclave Changelog" in PR 2. Existing entries' commit-by-commit context preserves continuity; no need to dual-name the title. ### Contributor / agent guidance - `AGENTS.md` currently says: *"Any environment variables you create should be prefixed with `STACK_`"*. Flip to prefer `HEXCLAVE_*` for Category A/B; document that Category C/E vars stay `STACK_*`. - Update any other contributor guidance referencing brand strings. -### Other Tier 4 sweeps (same PR) +### Other Tier 4 sweeps (PR 2) - README.md, CONTRIBUTING.md, CHANGELOG.md (title flip per above), AGENTS.md (env var guidance per above) - 49 docs files referencing `Stack*` class names in code examples (Hexclave-only after the rewrite; one compat note per page where relevant) @@ -740,7 +768,7 @@ Not just subjects — body strings too. Hardcoded in source, not in DB templates - `.github/SECURITY.md`, PR template, workflow file refs - `skills/stack-auth/SKILL.md` (consider directory rename to `skills/hexclave/`; old directory can stay as a pointer if needed) - Dashboard setup-page snippets (`apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx`) — copy-pasteable code blocks shown to customers -- Init wizard prompts (`packages/init-stack/`) — user-facing CLI messaging +- Init wizard prompts — user-facing CLI messaging in `packages/stack-cli/` (the `init` command, the new taught entrypoint) and the still-published `packages/init-stack/` --- @@ -751,13 +779,14 @@ Items that contain "stack" in their literal name and intentionally stay that way | What | Why | |---|---| | `x-stack-auth` legacy JSON-encoded header | No current writer; pure read-only compat path | -| `__Host-stack-temporary-chips-test-*` cookies | Internal, never user-visible, no functional reason to rename | -| `NEXT_PUBLIC_STACK_PORT_PREFIX` | Baked into every dev's local Docker setup | | `POSTGRES_DB: stackframe` | Would orphan every dev's local volume | -| Self-host `STACK_*` env vars (Category C) | Out of scope; self-hosters unaffected | -| Build/dev/test env vars (`STACK_SKIP_TEMPLATE_GENERATION`, `STACK_TEST_SDK_FALLBACK`, etc.) | Internal-only, not user-facing | | Swift legacy `StackAuth` package | Frozen but installable; new SDK lives in separate `Hexclave` package | +Two items that earlier plan versions listed here have moved: +- **`NEXT_PUBLIC_STACK_PORT_PREFIX`** is now **renamed outright** to `NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX` (no dual-accept) — see Env var taxonomy. +- **Self-host (Category C) and build/dev/test (Category E) `STACK_*` env vars** are now **dual-read** — `HEXCLAVE_*` accepted alongside `STACK_*` — see Env var taxonomy. +- **`__Host-stack-temporary-chips-test-*` cookies** belong to an unused feature being removed in a separate change; this rebrand ignores them entirely (see Tier 0 cookies). + ### Not in scope — never had "stack" branding to begin with These are listed once for completeness so reviewers don't worry about them. The rebrand never touches them. @@ -799,15 +828,15 @@ These aren't decisions — they're things the implementer should know before sta 8. **Snapshot serializer hardcodes `"stack-oauth-inner-"`** at [apps/e2e/tests/snapshot-serializer.ts:119](apps/e2e/tests/snapshot-serializer.ts:119). Update to also recognize `"hexclave-oauth-inner-"` or snapshots will go noisy during dual-write. -9. **Test assertion sweep.** Roughly 7+ e2e tests assert on exact `"Stack Auth: …"` error message prefixes and specific header names (`expect(...).toEqual({"x-stack-auth": ...})`, etc.). Update in lockstep with implementation in PR 1. +9. **Test assertion sweep.** Roughly 7+ e2e tests assert on exact `"Stack Auth: …"` error message prefixes and specific header names (`expect(...).toEqual({"x-stack-auth": ...})`, etc.). Update in lockstep with the implementation — header-name assertions in PR 1, error-message-prefix assertions in PR 2. 10. **CLI Sentry DSN compile-time bake.** `packages/stack-cli/tsdown.config.ts` embeds `__STACK_CLI_SENTRY_DSN__`. Existing DSN stays (per locked decision); just be aware that old released CLI versions will keep emitting under their old DSN indefinitely — that's intentional. --- -## PR 1 verification matrix +## Verification matrix -Compatibility-sensitive enough to be part of the implementation plan, not implicit. +Compatibility-sensitive enough to be part of the implementation plan, not implicit. Each item is verified in whichever PR introduces it — PR 1 for wire/compat behavior, PR 2 for the visible rebrand. ### Auth wire - [ ] Backend accepts every `x-stack-*` request header (incl. `x-stack-api-key`, `x-stack-request-type`, `x-stack-override-error-status`) @@ -833,7 +862,7 @@ Compatibility-sensitive enough to be part of the implementation plan, not implic - [ ] OAuth flow dual-writes `stack-oauth-{inner,outer}-*` and `hexclave-oauth-{inner,outer}-*` - [ ] OAuth callback reads either cookie name and completes flow - [ ] Low-risk cookies (`stack-is-https`, changelog, cli-auth-confirmed) dual-written under both names -- [ ] CHIPS test cookies still under `__Host-stack-temporary-chips-test-*` (not renamed) +- [ ] CHIPS test cookies untouched (unused feature, out of scope for this rebrand) - [ ] Mobile OAuth callback (`stack-auth-mobile-oauth-url://`) unchanged ### Env vars @@ -841,11 +870,11 @@ Compatibility-sensitive enough to be part of the implementation plan, not implic - [ ] New env only → SDK initializes, no warning - [ ] Both envs with different values → new wins, deprecation warning emitted - [ ] Multi-alias Category B vars: all historical aliases readable, new canonical preferred -- [ ] `NEXT_PUBLIC_STACK_PORT_PREFIX` still works under that exact name (not renamed) +- [ ] `NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX` works; `NEXT_PUBLIC_STACK_PORT_PREFIX` renamed everywhere (straight rename, no dual-accept) - [ ] Generated GitHub workflow with `STACK_AUTH_*` vars continues to authenticate - [ ] Newly generated workflow emits `HEXCLAVE_*` and authenticates -- [ ] Self-host vars (Category C) untouched — `.env` files for operators unchanged -- [ ] Build/dev/test vars (Category E) still under `STACK_*` — no rename attempted +- [ ] Self-host vars (Category C) dual-read — both `STACK_*` and `HEXCLAVE_*` work; existing operator `.env` files unchanged +- [ ] Build/dev/test vars (Category E) dual-read — both `STACK_*` and `HEXCLAVE_*` work ### JWT - [ ] Old normal issuer (`api.stack-auth.com/.../projects/{id}`) validates @@ -910,50 +939,63 @@ Compatibility-sensitive enough to be part of the implementation plan, not implic --- -## Rollout — 2 PRs +## Rollout — 3 PRs + +The three PRs separate *invisible* changes from *visible* ones. PR 1 can merge fast with low review risk because nothing in it is observable to any user — it breaks nothing and reveals no Hexclave branding. PR 2 is the actual public rebrand. PR 3 is far-future cleanup. + +The Tier sections above describe **what** changes; this section assigns **when**. Several surfaces split across PR 1 and PR 2 — the wire/code half lands in PR 1, the visible half in PR 2 (e.g. MCP: register `ask_hexclave` in PR 1, change the setup-page text in PR 2). "No user-facing changes" in PR 1 means **no breaking changes and no rebranded UI/text/docs** — not byte-identical behavior (new SDK code does start emitting `x-hexclave-*` etc., which is visible on the wire but harmless). + +**Deploy ordering within PR 1:** the backend dual-accept must be deployed everywhere — including self-hosted instances — *before* SDKs that emit the new identifiers reach users, or a new SDK against an un-updated backend would fail. Sequence the backend changes ahead of the SDK emit switch. -### PR 1: "Rebrand to Hexclave (additive)" — now +### PR 1: "Hexclave compatibility layer (invisible)" — now -One large additive PR. Nothing deleted. Existing users continue working untouched. Verification matrix above must pass. **Prerequisite (not in the PR itself): exhaustive operator env var inventory** — see Category C. +Purely additive, ships entirely inside the existing `@stackframe/*` packages and existing deploys. Nothing is deleted; nothing breaks; no Hexclave branding becomes visible to any user. Safe to merge quickly. **Prerequisite (not code in the PR): the exhaustive operator env var inventory** — now required since Category C is dual-read (see Category C). -Major work items: -- Header / cookie / env var dual-accept (Tier 0) -- Template re-exports propagating to generated SDKs (Tier 1 JS) -- Swift: stand up new `Hexclave` SPM package with real `Hexclave*` symbols + `api.hexclave.com` base URL; freeze existing `StackAuth` package -- `sdks/spec` update -- Publish-time mirror artifacts for `@hexclave/*` packages (Tier 2) -- CLI dual config paths + binary alias -- JWT validator accepts both domains for all 3 issuer types -- MCP dual tool registration -- Idempotent seed/data migration with tests -- Mechanical sweep: domains (full inventory), repo slug, page titles, OpenAPI titles, generated content (after generator updates), examples, README family, assets -- DNS: stand up all `*.hexclave.com` subdomains; redirect from `*.stack-auth.com` -- `turbo.json` `globalEnv` adds `HEXCLAVE_*` +Scope: +- **Wire dual-accept / dual-emit:** request headers, response headers, `Bearer` prefix, query parameters, JWT issuer/audience validator — backend accepts old + new; new SDK code emits the new form (Tier 0). +- **Cookies & storage:** dual-write / dual-read all auth, OAuth-state, and low-risk cookies and storage keys (Tier 0). +- **Env vars:** dual-read every category (A–E); rename `NEXT_PUBLIC_STACK_PORT_PREFIX` outright; `turbo.json` `globalEnv` gains the `HEXCLAVE_*` forms. +- **SDK export aliases:** add `Hexclave*` aliases in `packages/template`, propagated by codegen to every JS SDK (Tier 1). No `@deprecated` markers yet. +- **Internal renames:** the three SDK interfaces and `StackAssertionError` renamed outright (no alias); dev-tool DOM identifiers; `Symbol.for(...)` dual-attach (Tier 1). +- **MCP:** register the `ask_hexclave` tool (additive); `ask_stack_auth` keeps working. +- **Config discovery:** CLI / dashboard accept `hexclave.config.ts` and the `~/.config/hexclave/` credentials path alongside the old ones. +- **Tests:** assertions on wire identifiers updated in lockstep; snapshot serializer handles both prefixes. -### PR 2: "Remove non-essential Stack Auth fallbacks" — 12+ months later +### PR 2: "Rebrand to Hexclave (visible)" — after PR 1 -Only after operational evidence shows the targeted fallbacks are unused (telemetry decision deferred from PR 1). +Where the brand goes public. Everything user-visible. -Pure deletion, but **narrowly scoped**. Wire identifiers (request headers, response headers, JWT issuers, Bearer prefix, OAuth state cookies, mobile URL scheme, SDK class aliases) stay indefinitely and are NOT touched by PR 2. The legacy `StackAuth` Swift package stays installable but unmaintained — also untouched. Same goes for everything in "Do not rename". +Scope: +- **New packages:** publish the `@hexclave/*` npm mirrors (starting at `1.0.0`); stand up the new `Hexclave` Swift package with real `Hexclave*` symbols and `api.hexclave.com` base URL; freeze the existing `StackAuth` Swift package; update `sdks/spec`. +- **Deprecation:** `npm deprecate` the `@stackframe/*` packages; `@deprecated` JSDoc on every `Stack*` public export; SDK runtime `console.warn` recommending `@hexclave/*`. +- **Brand strings (Tier 4):** domains (full inventory), GitHub repo slug, page titles, OpenAPI titles, known-error message templates, email subjects/bodies, `StackAssertionError` message text, CHANGELOG title, contributor guidance, README family, visual assets. +- **Generated content:** update generators (AI prompts, setup prompts, MCP setup page, skills), then regenerate outputs. +- **Docs:** rewrite to teach Hexclave-only names; old names only in compat notes; onboarding command becomes `npx @hexclave/cli@latest init`. +- **Data migration:** idempotent seed migration `Stack Dashboard` → `Hexclave Dashboard` and the email config name (user-visible). +- **CLI:** `hexclave` binary alias; `hexclave init` generates `hexclave.config.ts`; dashboard setup snippets teach the new commands. +- **DNS:** stand up all `*.hexclave.com` subdomains and the `sent-with-hexclave.com` sending domain; redirect from `*.stack-auth.com`. +- **Tests:** assertions on brand strings / error message prefixes updated in lockstep. -Safely removable in PR 2: +### PR 3: "Remove non-essential Stack Auth fallbacks" — 12+ months later -- Stop dual-writing main auth cookies under their old `stack-*` names (old cookies have long expired naturally; reads of old names can also be dropped) -- Stop reading `STACK_*` customer SDK env vars (or hard-error with a migration message) — only after operator dashboards confirm low usage -- Remove `ask_stack_auth` MCP tool — only after AI client adoption of `ask_hexclave` is high -- Tear down non-essential `*.stack-auth.com` subdomains (keep `api.stack-auth.com` indefinitely — Apple sign-in setup depends on it) -- `@stackframe/*` published packages: leave on npm with a "moved to `@hexclave/*`" README; do not unpublish (npm unpublishing breaks the ecosystem) +(Formerly "PR 2" in earlier plan versions.) Pure deletion, **narrowly scoped**, only after operational evidence / telemetry shows the targeted fallbacks are unused. -**Explicitly NOT removed in PR 2:** +Safely removable in PR 3: +- Stop dual-writing main auth cookies under their old `stack-*` names (old cookies have long expired naturally; reads of old names can also be dropped). +- Stop reading `STACK_*` customer SDK env vars (or hard-error with a migration message) — only after operator dashboards confirm low usage. +- Remove the `ask_stack_auth` MCP tool — only after AI-client adoption of `ask_hexclave` is high. +- Tear down non-essential `*.stack-auth.com` subdomains (keep `api.stack-auth.com` indefinitely — Apple sign-in setup depends on it). +- `@stackframe/*` published packages: leave on npm with a "moved to `@hexclave/*`" README; do not unpublish (npm unpublishing breaks the ecosystem). +**Explicitly NOT removed in PR 3:** - `x-stack-*` request headers (kept dual-accepted indefinitely) - `x-stack-*` response headers (kept dual-emitted indefinitely) - `Bearer stackauth_*` prefix (kept dual-accepted indefinitely) - `x-stack-auth` legacy header (still parsed) - JWT validator's acceptance of all three `stack-auth.com` issuer variants and the IdP audience -- JS `Stack*` exports — they're the canonical class names, not aliases +- JS `Stack*` exports — they're the canonical class names, not aliases (deprecated, not removed) - Legacy `StackAuth` Swift package (frozen but installable from existing SPM URL) -- OAuth state cookies (`stack-oauth-*`), CHIPS test cookies, `stack-auth-mobile-oauth-url://` +- OAuth state cookies (`stack-oauth-*`), `stack-auth-mobile-oauth-url://` - `stack.config.ts` filename (still readable as fallback) - Everything in the "Do not rename" table @@ -961,9 +1003,9 @@ Safely removable in PR 2: ## Open questions still worth answering before implementation -- **Operator env var inventory:** must be produced before PR 1 implementation begins. Once produced, decide whether category C scope fits in PR 1 or needs to be deferred to a follow-up. Current plan: include in PR 1 if scope is manageable. +- **Operator env var inventory:** must be produced before PR 1 implementation begins. Category C is now dual-read and **in-scope for PR 1** (no longer deferrable) — the inventory is required so every operator var gets a dual-read site, a docs update, and an `.env.example` entry. - **SDK request header emission:** new SDKs emit `x-hexclave-*` for every request header (current plan), or skip the most stable ones (e.g. `branch-id`) to reduce churn? Current plan: emit all. -- **DNS infrastructure:** ops team confirmation on indefinite redirect maintenance capacity for 16 subdomains. -- **Future canonicality flip (post PR 2):** is there any reason to ever make `Hexclave*` the canonical class name in JS or Swift, with `Stack*` as the alias? Current plan: no — coexistence indefinitely, neither is "more canonical". +- **DNS infrastructure:** ops team confirmation on indefinite redirect maintenance capacity for 16 subdomains, plus registration + SPF/DKIM/DMARC for the separate `sent-with-hexclave.com` transactional sending domain. +- **Future canonicality flip (post PR 3):** is there any reason to ever make `Hexclave*` the canonical class name in JS or Swift, with `Stack*` as the alias? Current plan: no — coexistence indefinitely, neither is "more canonical". - `hexclave.config.ts` is the new canonical config filename; `stack.config.ts` read-fallback stays in discovery indefinitely. We'd only drop the fallback after telemetry shows essentially no projects rely on it. - DNS infrastructure ownership for redirects — operations team needs to confirm capacity for indefinite redirect maintenance. From a1d043b3d769b97781bb8be2f55eaeb19927fadf Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Fri, 22 May 2026 11:57:14 -0700 Subject: [PATCH 04/28] docs: fold PR 1 discovery findings into the plan - Add "PR 1 implementation guide" section resolving every open item with concrete file:line references and chosen approach per work-area - Correction: Bearer stackauth_ prefix is SDK-internal, never parsed by the backend (was wrongly listed as a backend wire identifier) - Request headers: normalize at the existing empty proxy.tsx:114 hook (no readDualHeader helper, no per-route schema edits) - Env vars: hybrid dual-read (central getEnvVariable transform + two client files + per-site tail) - Symbol.for: four symbols, not three; only one needs dual-attach - Query params: add the two nested cross-domain params --- RENAME-TO-HEXCLAVE.md | 129 +++++++++++++++++++++++++++++------------- 1 file changed, 89 insertions(+), 40 deletions(-) diff --git a/RENAME-TO-HEXCLAVE.md b/RENAME-TO-HEXCLAVE.md index c77ada055f..d3d279554a 100644 --- a/RENAME-TO-HEXCLAVE.md +++ b/RENAME-TO-HEXCLAVE.md @@ -72,7 +72,7 @@ These follow the same pattern as request headers: old form continues to work ind | Config filename | `stack.config.ts` | `hexclave.config.ts` | | Mobile OAuth URL scheme | `stack-auth-mobile-oauth-url://` | `hexclave-mobile-oauth-url://` | -**Bearer prefix details.** Backend's Authorization parser checks both `stackauth_` and `hexclave_` prefixes (one extra string-prefix check). New SDKs construct tokens with the `hexclave_` prefix; old SDKs keep working unchanged. Anyone debugging a request sees the brand-consistent prefix on new traffic. +**Bearer prefix details.** *Discovery correction:* the `Bearer stackauth_` token is **not** a backend wire identifier — the Stack backend never parses it. It is an SDK-internal serialization of `{ accessToken, refreshToken }` for the `tokenStore: { headers }` init path; the SDK decodes it itself and then sends `x-stack-access-token` / `x-stack-refresh-token` to the backend. Dual-support lives entirely in `packages/template` (`client-app-impl.ts`): accept either prefix on parse, emit `hexclave_`. No backend change. See the [PR 1 implementation guide](#pr-1-implementation-guide-resolved-from-codebase-discovery). **Response header details.** Backend emits both `x-stack-*` AND `x-hexclave-*` versions of `actual-status`, `known-error`, `request-id` on every response (~60 extra bytes total — negligible). New SDKs read `x-hexclave-*` first, fall back to `x-stack-*`. Old SDKs continue to read `x-stack-*` only. @@ -119,9 +119,9 @@ Server reads both `x-stack-*` and `x-hexclave-*` via a single helper. New SDKs e | `x-stack-random-nonce` | `x-hexclave-random-nonce` | | `x-stack-bulldozer-studio-token` | `x-hexclave-bulldozer-studio-token` | -**Implementation pattern:** `readDualHeader(req, "x-hexclave-foo", "x-stack-foo")` at the parse layer. Zero per-route changes. +**Implementation pattern:** normalize `x-hexclave-*` → `x-stack-*` at the existing (currently-empty) request-header hook in `apps/backend/src/proxy.tsx:114`, before routing and yup validation — so `smart-request.tsx` and every route schema keep working unchanged. No `readDualHeader` helper, no per-route edits. Details and the exact reader sites are in the [PR 1 implementation guide](#pr-1-implementation-guide-resolved-from-codebase-discovery). -**CORS sync requirement.** `apps/backend/src/proxy.tsx` maintains explicit allowlists for request headers (lines 16-54) and response headers (lines 50-54) used by CORS preflight. Every old + new header pair must appear in both allowlists or preflight will fail. Easy to miss. +**CORS sync requirement.** `apps/backend/src/proxy.tsx` maintains explicit allowlists — `corsAllowedRequestHeaders` (lines 16-48) and `corsAllowedResponseHeaders` (lines 50-54). `apps/dashboard/src/proxy.tsx` has a near-duplicate pair (lines 13-34). Every old + new header name must appear in all of them or CORS preflight fails. Easy to miss. ### HTTP response/protocol headers (dual-emit) @@ -131,7 +131,7 @@ These flow backend → client. Covered in the symmetric dual-support table above ### Authorization Bearer formats -Covered in the symmetric dual-support table above. Backend accepts both `Bearer stackauth_*` and `Bearer hexclave_*`. New SDKs emit `Bearer hexclave_*`. +**SDK-internal — not a backend identifier.** The `Bearer stackauth_*` token is parsed and emitted entirely within the SDK (`packages/template`); the Stack backend never sees it. The SDK's token parser accepts both `stackauth_` and `hexclave_` prefixes; new SDK code emits `hexclave_`. Exact functions and line numbers in the [PR 1 implementation guide](#pr-1-implementation-guide-resolved-from-codebase-discovery). ### Cookies (dual-write, dual-read across the board) @@ -270,21 +270,21 @@ Delimiter conventions are inconsistent across these keys (hyphen, underscore, do | Old (accepted indefinitely) | New (preferred) | Flow | |---|---|---| -| `stack_response_mode` | `hexclave_response_mode` | SDK → backend (OAuth authorize) | -| `stack_cross_domain_auth` | `hexclave_cross_domain_auth` | cross-domain handoff (SDK ↔ SDK across domains) | -| `stack_cross_domain_state` | `hexclave_cross_domain_state` | cross-domain handoff | -| `stack_cross_domain_code_challenge` | `hexclave_cross_domain_code_challenge` | cross-domain handoff | -| `stack_cross_domain_after_callback_redirect_url` | `hexclave_cross_domain_after_callback_redirect_url` | cross-domain handoff | +| `stack_response_mode` | `hexclave_response_mode` | SDK → backend (OAuth authorize) — **the only query param the backend reads** | +| `stack_cross_domain_auth` (marker, value `"1"`) | `hexclave_cross_domain_auth` | cross-domain handoff (SDK ↔ SDK), SDK-internal | +| `stack_cross_domain_state` | `hexclave_cross_domain_state` | cross-domain handoff, SDK-internal | +| `stack_cross_domain_code_challenge` | `hexclave_cross_domain_code_challenge` | cross-domain handoff, SDK-internal | +| `stack_cross_domain_after_callback_redirect_url` | `hexclave_cross_domain_after_callback_redirect_url` | cross-domain handoff, SDK-internal | +| `stack_nested_cross_domain_auth_refresh_token_id` | `hexclave_nested_cross_domain_auth_refresh_token_id` | nested cross-domain handoff, SDK-internal | +| `stack_nested_cross_domain_auth_callback_url` | `hexclave_nested_cross_domain_auth_callback_url` | nested cross-domain handoff, SDK-internal | | `stack-init-id` | `hexclave-init-id` | init CLI → dashboard wizard-congrats page | -**`stack_response_mode`** — emitted by `packages/stack-shared/src/interface/client-interface.ts:1419`, read by the yup query schema at `apps/backend/src/app/api/latest/auth/oauth/authorize/[provider_id]/route.tsx:42`. The backend schema must accept both keys (prefer new). This needs genuine dual-accept, not a rename: if the param is dropped silently the backend falls back to `responseMode: "redirect"` and the SDK can no longer intercept bot challenges before navigating. +**`stack_response_mode`** — emitted by `packages/stack-shared/src/interface/client-interface.ts:1419`, read by the yup query schema at `apps/backend/src/app/api/latest/auth/oauth/authorize/[provider_id]/route.tsx:42` (used at :160,:166). The backend schema must accept both keys (prefer new). This needs genuine dual-accept, not a rename: if the param is dropped silently the backend falls back to `responseMode: "redirect"` and the SDK can no longer intercept bot challenges before navigating. **This is the only `stack_*` query param the backend itself parses** — all the others below are SDK↔SDK only. -**`stack_cross_domain_*`** — the four param names are defined together in `packages/template/src/lib/stack-app/apps/implementations/redirect-page-urls.ts:6-9`; the `stack_cross_domain_auth === "1"` marker is read at `packages/template/src/components-page/stack-handler-client.tsx:267`. Writer and reader are both in the SDK, so a handoff between two different SDK majors (one per domain) must still resolve: dual-emit both param sets into the redirect URL, and accept either on read. +**`stack_cross_domain_*`** — the four param names are the `crossDomainAuthQueryParams` const at `packages/template/src/lib/stack-app/apps/implementations/redirect-page-urls.ts:5-10`; the `stack_cross_domain_auth === "1"` marker is read at `packages/template/src/components-page/stack-handler-client.tsx:267`. The two `stack_nested_cross_domain_auth_*` params are the `nestedCrossDomainAuthQueryParams` object at `client-app-impl.ts:89-97` (written :847,:849; read :860,:863). Writer and reader are both in the SDK, so a handoff between two SDK majors (one per domain) must still resolve: dual-emit both param sets into the redirect URL, and accept either on read. (The non-prefixed OAuth params in `nestedCrossDomainAuthQueryParams` — `redirect_uri`, `state`, `code_challenge`, etc. — are standard OAuth and are **not** rebranded.) **`stack-init-id`** — emitted by `packages/init-stack/src/index.ts:452`, read by `apps/dashboard/src/app/(main)/wizard-congrats/posthog.tsx:12`. Dashboard reads either key; new `init` CLI emits the new one. Low-stakes (PostHog distinct-id correlation only) but follows the same pattern. Note the hyphen delimiter — the cross-domain and response-mode params use underscores; preserve each. -A repo-wide grep for `stack_` / `stack-` query keys should confirm this list before PR 1. - ### Custom DOM events The docs site syncs platform/framework selection across components via `window`-dispatched `CustomEvent`s with `stack-`-prefixed names. They pair with the `stack-docs-selected-*` sessionStorage keys above. @@ -355,15 +355,16 @@ These are **not exported from any customer SDK entrypoint** (`@stackframe/stack` Page components (`SignIn`, `SignUp`, `AuthPage`, `AccountSettings`, `UserButton`, `TeamSwitcher`, `OAuthButton`, `PasswordReset`, `EmailVerification`, `ForgotPassword`, `MessageCard`, `CliAuthConfirmation`) don't carry the brand — leave alone. -**Internal `Symbol.for(...)` keying** — dual-symbol pattern. Three distinct `Symbol.for()` strings with "stack" in them across the codebase. All get the dual-attach treatment for consistency. +**Internal `Symbol.for(...)` keying.** Discovery found **four** distinct `Symbol.for()` strings containing "stack" (earlier plan versions listed three). Only the first is customer-visible — part of `StackClientApp`'s type surface, accessed by dashboard + example code — and needs dual-attach; the other three are file-private with no cross-version concern and are renamed outright. -| Old (kept for cross-version coexistence) | New (canonical) | Location | +| Old | New | Scope / treatment | |---|---|---| -| `Symbol.for("StackAuth--DO-NOT-USE-OR-YOU-WILL-BE-FIRED--StackAppInternals")` | `Symbol.for("Hexclave--app-internals")` | `packages/template/src/lib/stack-app/common.ts:213` | -| `Symbol.for("__stack-globals")` | `Symbol.for("__hexclave-globals")` | `packages/stack-shared/src/utils/globals.tsx` | -| `Symbol.for("__stack_email_queue_first_run_completed")` | `Symbol.for("__hexclave_email_queue_first_run_completed")` | `apps/backend/src/lib/email-queue-step.tsx` | +| `Symbol.for("StackAuth--DO-NOT-USE-OR-YOU-WILL-BE-FIRED--StackAppInternals")` | `Symbol.for("Hexclave--app-internals")` | Customer-visible. **3 definition sites:** `packages/template/src/lib/stack-app/common.ts:213`, `apps/dashboard/src/lib/stack-app-internals.ts:8`, `apps/dashboard/.../external-db-sync/page-client.tsx:24`. **Dual-attach.** | +| `Symbol.for("__stack-globals")` | `Symbol.for("__hexclave-globals")` | SDK-internal, file-private to `packages/stack-shared/src/utils/globals.tsx`. Straight rename. | +| `Symbol.for("stack-smartRouteHandler")` | `Symbol.for("hexclave-smartRouteHandler")` | Backend-only, file-private to `apps/backend/src/route-handlers/smart-route-handler.tsx`. Straight rename. | +| `Symbol.for("__stack_email_queue_first_run_completed")` | `Symbol.for("__hexclave_email_queue_first_run_completed")` | Backend-only, file-private to `apps/backend/src/lib/email-queue-step.tsx`. Straight rename. | -On attach: write internals under BOTH symbols. On lookup: try new first, fall back to old. Mixed-version setups (a customer with two SDK majors loaded in one page) keep working. +For the customer-visible symbol: on attach, write internals under BOTH symbols; on lookup, try new then old — so a page with two SDK majors keeps working. The other three are read and written within a single file, so a plain rename of the string is sufficient. ### Swift SDK — separate package, not typealiases @@ -801,19 +802,76 @@ These are listed once for completeness so reviewers don't worry about them. The --- -## Implementation realities (architecture observations from pre-PR-1 review) +## PR 1 implementation guide (resolved from codebase discovery) + +A discovery pass against the codebase resolved the open items from earlier plan versions. Each work-area below gives the chosen approach, the concrete files/lines, and the gotchas — enough to implement PR 1 without further investigation. + +### Request headers — normalize at the proxy + +There is an **existing, currently-empty hook** at `apps/backend/src/proxy.tsx:114` (`const newRequestHeaders = new Headers(request.headers); // here we could update the request headers (currently we don't)`). Insert the normalization here: for each `x-hexclave-*` request header, copy its value onto the matching `x-stack-*` name. It runs before routing, `createSmartRequest`, and yup validation, so **every downstream reader keeps working unchanged** — `parseAuth`'s ~12 `req.headers.get("x-stack-*")` calls (`smart-request.tsx:162-172,348`), the route handlers that destructure header names from yup schemas (`auth/password/update/route.tsx:27,34`; `auth/sessions/current/refresh/route.tsx:19,29`; `auth/oauth/cross-domain/authorize/route.tsx:110-160`), `smart-response.tsx:144`, `smart-route-handler.tsx:92`, `proxy.tsx:64,81`. No `readDualHeader` helper, no per-route schema edits. Apply the same normalization in `apps/dashboard/src/proxy.tsx` (it has its own header handling). + +### Response headers + +`apps/backend/src/route-handlers/smart-response.tsx` sets `x-stack-request-id` (:136, always) and `x-stack-actual-status` (:146); `x-stack-known-error` comes from `KnownError.getHeaders()` (`packages/stack-shared/src/known-errors.tsx:49-53`) and is copied on at `smart-response.tsx:150-152`. Dual-emit = set the `x-hexclave-*` copy at each site. The SDK reads `x-stack-actual-status` (`client-interface.ts:794-795`) and `x-stack-known-error` (`:807-810`); `x-stack-request-id` is emit-only, never read. Dual-read = check `x-hexclave-*` first. + +### Bearer token prefix — SDK-internal, not a backend identifier + +The `Bearer stackauth_` token is **never parsed by the Stack backend**. It is an SDK-internal serialization of `{ accessToken, refreshToken }` for the `tokenStore: { headers }` init path; the SDK decodes it and then sends `x-stack-access-token` / `x-stack-refresh-token`. All in `packages/template/.../lib/stack-app/apps/implementations/client-app-impl.ts`: constant `STACK_AUTHORIZATION_VALUE_PREFIX = "stackauth_"` (:102), emit `getAuthorizationHeaderValueFromAuthJson()` (:104-111), parse `getAuthJsonFromAuthorizationHeaderValue()` (:113-154; prefix check :120; hardcoded error strings :126,134,138,144,147). Add `HEXCLAVE_AUTHORIZATION_VALUE_PREFIX = "hexclave_"`, accept either on parse, emit `hexclave_`. No backend change. + +### JWT issuer / audience + +`apps/backend/src/lib/tokens.tsx:58-104` builds allowed issuer URLs from the configured API URL and passes an exact-match array to `verifyJWT()` — no domain-substring check. Build two arrays (one per domain) and concatenate, or have `getIssuer()` return both variants. Also: `packages/template/src/integrations/convex.ts`, `apps/backend/src/app/api/latest/integrations/idp.ts:167`. + +### Cookies — central helper + enumerated bypass sites + +Auth cookies (`stack-access`, `stack-refresh-*`, `stack-oauth-outer-*`) flow through `packages/template/src/lib/cookie.ts`; their names have a single point of truth in `client-app-impl.ts` getters — `_accessTokenCookieName` (:1083), `_refreshTokenCookieName` / `_legacyRefreshTokenCookieName` (:969-975), `_getRefreshTokenCookieNamePatterns()` (:1091-1098). Dual-write / dual-read by extending those. **Bypass sites to patch individually:** +- `stack-oauth-inner-*` — backend raw `cookies()`: set at `auth/oauth/authorize/[provider_id]/route.tsx:180-188`, read + delete at `auth/oauth/callback/[provider_id]/route.tsx:119-120`. +- `stack-access` / `stack-refresh-*` delete — `apps/dashboard/.../api/remote-development-environment/auth/route.ts:10-21` (update `isInternalProjectRefreshCookieName()` at :10-13 to match new names too). +- `stack-is-https` — three write sites, all inside `cookie.ts` (:198-203, :280, :355). +- `stack-last-seen-changelog-version` — raw `document.cookie`: `stack-companion.tsx:223`, `changelog-widget.tsx:47` (reads :192,:231). +- Impersonation snippet strings — `users/[userId]/page-client.tsx:161`, `user-table.tsx:399` (code shown to users to paste in a console). +- Snapshot serializer — add `"hexclave-oauth-inner-"` to `keyedCookieNamePrefixes` at `apps/e2e/tests/snapshot-serializer.ts:119`. + +### Storage keys + +Single-constant keys — change the constant: `CLI_AUTH_CONFIRMED_KEY` (`cli-auth-confirm.tsx:31`), `LOCAL_STORAGE_PREFIX` (`session-replay.ts:90`), `STORAGE_KEY` / `TRIGGER_POS_KEY` (`dev-tool-core.ts:51-52`), `OVERRIDE_KEY` (`dev-tool/index.ts:9`). Hardcoded strings to wrap then change: `stack_mfa_attempt_code` (`client-app-impl.ts:3288`, `mfa.tsx:37,70`, `page-component-versions.ts:1510,1519`), `_STACK_AUTH.lastUsed` (`oauth-button.tsx:37,190`), docs keys (`platform-codeblock.tsx`, `platform-indicator.tsx`). Dual-read where the value must survive an SDK upgrade (`session-replay`); straight rename for the UI-only dev-tool / docs keys. -These aren't decisions — they're things the implementer should know before starting. Each comes from grepping the actual codebase. +### Env vars — hybrid: one central transform + two client files + a per-site tail -1. **No `readDualHeader` helper exists — AND it's insufficient on its own.** [smart-request.tsx](apps/backend/src/route-handlers/smart-request.tsx) reads auth-level headers via individual `req.headers.get()` calls (~10 sites). But route handlers ALSO destructure header names directly from yup-validated schemas — e.g. [refresh/route.tsx:19,29](apps/backend/src/app/api/latest/auth/sessions/current/refresh/route.tsx:19) declares `"x-stack-refresh-token"` in the schema and destructures it from `headers` in the handler, and [password/update/route.tsx:27,34](apps/backend/src/app/api/latest/auth/password/update/route.tsx:27) does the same. **A helper at the auth-parse layer alone won't cover these.** PR 1 must either (a) add a header-name normalization step *before* yup schema validation that populates both old + new keys into `headers`, or (b) update every route schema that names a `x-stack-*` header to accept both names. (a) is mechanically smaller. CORS allowlist in `apps/backend/src/proxy.tsx` also needs both old + new names. +`getEnvVariable` in `packages/stack-shared/src/utils/env.tsx` already has an `ENV_VAR_RENAME` table — the dual-read primitive exists. +- **Server-side (~150 call sites, zero call-site edits):** add a `STACK_*`→`HEXCLAVE_*` (and `NEXT_PUBLIC_STACK_*`→`NEXT_PUBLIC_HEXCLAVE_*`, infix variants, `STACK_AUTH_*`) prefix transform inside `getEnvVariable` / `getEnvBoolean` / `getProcessEnv`: try the `HEXCLAVE_*` name, then `STACK_*`. This automatically covers the dynamically-built OAuth credential names (`STACK_${provider}_CLIENT_ID` at `apps/backend/src/oauth/index.tsx:40-41`). +- **Dashboard client (~70 sites):** `apps/dashboard/src/lib/env.tsx` `getPublicEnvVar` keys into a static `_inlineEnvVars` map of literal `process.env.NEXT_PUBLIC_*` (build-time inlined — cannot be made dynamic). Add a parallel `NEXT_PUBLIC_HEXCLAVE_*` literal as the preferred operand in each entry; add matching `HEXCLAVE` sentinels to `_postBuildEnvVars` and the Docker post-build substitution. +- **Customer SDK:** `packages/template/src/lib/env.ts` — each getter is a literal `process.env.X`; add the `HEXCLAVE` literal as the preferred operand inside each. Run `generate-sdks` after. +- **Per-site tail (unavoidable):** Vite examples (`VITE_STACK_*` via `import.meta.env`), raw `process.env` in `next.config.mjs` / `prisma/seed.ts` / `stack-cli` / `mock-oauth-server`, the `STACK_*` glob in `turbo.json`, and `.env*` / `.env.example`. +- Resolve (don't mechanically prefix-swap) the three alias clusters — API URL, and the browser/server infix-vs-suffix forms; `apps/dashboard/src/lib/env.tsx` already carries TODOs for them. `NEXT_PUBLIC_STACK_PORT_PREFIX` (~25 sites) is the rename-outright exception. -2. **JWT issuer validation is URL-built, not domain-matched.** [apps/backend/src/lib/tokens.tsx:58-104](apps/backend/src/lib/tokens.tsx:58) constructs allowed issuer URLs from `NEXT_PUBLIC_STACK_API_URL` and passes them as an exact-match array to `verifyJWT()`. There's no domain-substring check. Implementation must build **two arrays** (one per domain) and concatenate, OR refactor `getIssuer()` to return both variants. +### SDK export aliases — concrete targets -3. **Cookie helper isn't fully centralized.** `stack-is-https` is written in at least 4 places that bypass the central helper (cookie.ts:280, 355; TanStack integration:198; backend OAuth setters). Dual-write requires refactoring those to use shared constants, not just editing one helper. +Add to `packages/template/src/index.ts`: `StackConfig`, `defineStackConfig`, `StackHandler`, `StackProvider`, `StackTheme`, `useStackApp`. Add to `packages/template/src/lib/stack-app/index.ts`: `StackClientApp` / `StackServerApp` / `StackAdminApp` (values) and the types `Stack{Client,Server,Admin}AppConstructor`, `Stack{Client,Server,Admin}AppConstructorOptions`, `StackClientAppJson`. `StackHandler` / `StackProvider` are `export { default as ... }` re-exports — alias as `export { StackHandler as HexclaveHandler }` (not another default). Type exports use `export type { X as HexclaveX }`. Run `generate-sdks` after. -4. **Bearer prefix parser location TBD.** The agent review couldn't pinpoint where `Bearer stackauth_*` is actually parsed — likely JWT validation in `packages/stack-shared/src/utils/jwt.tsx` or in middleware, not in `smart-request.tsx`. Locating is a PR 1 prerequisite. +### Internal renames -5. **NPM dual-publish needs the copy-to-temp pattern, not in-place rewrite.** The plan's original "rewrite package.json names, then `pnpm publish -r` again" approach won't work — pnpm uses a shared lockfile, so after rewrite it can't resolve `@hexclave/X` workspace refs. **Concrete fix:** the rewrite script copies `dist/` artifacts and each package.json into a temp directory, rewrites the temp copies (names + deps), and publishes from temp. Workspace lockfile stays untouched. +`StackClientInterface` / `StackServerInterface` / `StackAdminInterface` — defined in `packages/stack-shared/src/interface/{client,server,admin}-interface.ts`, exported only from `@stackframe/stack-shared`'s index (no customer SDK). ~34 references across ~14 files. `StackAssertionError` — `packages/stack-shared/src/utils/errors.tsx:69`, not exported from any public index, ~344 references. Both rename outright; mechanical, grep-driven. + +### Config discovery — ~15 sites + +CLI: `stack-cli/src/commands/config-file.ts:202-205`, `init.ts:195,390`, `dev.ts:495`. Dashboard local-dev: `lib/remote-development-environment/config-file.ts:18,55`, `link-existing-onboarding.tsx:146,445,500`, `projects/page-client.tsx` (~8 UI strings), `development-environment/health/route.ts:47`, `layout-client.tsx:78`. Backend emulator: `internal/local-emulator/project/route.tsx:43,305`. Each: prefer `hexclave.config.ts`, fall back to `stack.config.ts`. CLI credentials path: `stack-cli/src/lib/config.ts:5` (`~/.config/stack-auth/credentials.json`, override `STACK_CLI_CONFIG_PATH`) — dual-read old path, write new. + +### MCP + +`apps/mcp/src/mcp-handler.ts` — `server.tool("ask_stack_auth", …)` at :107-172. Add `server.tool("ask_hexclave", …)` delegating to the same handler; pass `toolName: "ask_hexclave"` (:149), adjust the hint text (:169) and `instructions` (:179). `apps/e2e/tests/backend/.../mcp.test.ts` inline snapshots (:37-99) grow and must be updated. + +### Test sweep (PR 1 — wire identifiers only) + +Header-name assertions: `js/auth-like.test.ts:404,419,470,541`, `render-email.test.ts:180,217`, `internal/projects.test.ts:51`, `neon/.../provision.test.ts:224`, `backend-helpers.ts:197-201`. Cookie-name: `backend-helpers.ts:803`, `oauth/{authorize,callback,merge-strategy}.test.ts`, `sign-up-rules.test.ts:36`, `js/cookies.test.ts:200`, `cross-domain-auth.test.ts:190`. Snapshot serializer: `snapshot-serializer.ts:119`. Many `x-stack-known-error` values live in inline snapshots and regenerate automatically. (Error-message-string and docs-URL assertions are PR 2.) + +### Still unverified + +- The backend **read** site for the legacy `x-stack-auth` JSON header was not located in `smart-request.tsx`'s auth parser. The SDK emit side is `client-app-impl.ts` (`getAuthHeaders()` / `useAuthHeaders()`). Confirm whether and where the backend consumes it before relying on the "kept indefinitely" claim. + +### Retained from earlier review + +- **NPM dual-publish needs the copy-to-temp pattern**, not an in-place rewrite — pnpm's shared lockfile means an in-place rename can't resolve `@hexclave/X` workspace refs. The rewrite script copies `dist/` artifacts and each `package.json` into a temp directory, rewrites the temp copies (names + deps + version), and publishes from temp: ```yaml - name: Rewrite to @hexclave/* in temp dir @@ -822,15 +880,7 @@ These aren't decisions — they're things the implementer should know before sta run: pnpm publish --no-git-checks --access public --recursive /tmp/hexclave-pkgs ``` -6. **Config discovery is not one function.** `init` (`packages/stack-cli/src/commands/init.ts`) hardcodes `stack.config.ts` output; `dev` requires explicit `--config-file`; dashboard local-dev linking discovers separately. Adding "prefer `hexclave.config.ts`, fall back to `stack.config.ts`" requires updating each discovery site. - -7. **Symbol attach-and-lookup sites unknown.** `stackAppInternalsSymbol` is defined in [common.ts:213](packages/template/src/lib/stack-app/common.ts:213). Every site that does `app[stackAppInternalsSymbol] = …` and every `app[stackAppInternalsSymbol]` read needs dual treatment. Estimated 5-20 sites; enumerate during PR 1. - -8. **Snapshot serializer hardcodes `"stack-oauth-inner-"`** at [apps/e2e/tests/snapshot-serializer.ts:119](apps/e2e/tests/snapshot-serializer.ts:119). Update to also recognize `"hexclave-oauth-inner-"` or snapshots will go noisy during dual-write. - -9. **Test assertion sweep.** Roughly 7+ e2e tests assert on exact `"Stack Auth: …"` error message prefixes and specific header names (`expect(...).toEqual({"x-stack-auth": ...})`, etc.). Update in lockstep with the implementation — header-name assertions in PR 1, error-message-prefix assertions in PR 2. - -10. **CLI Sentry DSN compile-time bake.** `packages/stack-cli/tsdown.config.ts` embeds `__STACK_CLI_SENTRY_DSN__`. Existing DSN stays (per locked decision); just be aware that old released CLI versions will keep emitting under their old DSN indefinitely — that's intentional. +- **CLI Sentry DSN compile-time bake.** `packages/stack-cli/tsdown.config.ts` embeds `__STACK_CLI_SENTRY_DSN__`. Existing DSN stays (per locked decision); old released CLI versions keep emitting under their old DSN indefinitely — intentional. --- @@ -845,9 +895,8 @@ Compatibility-sensitive enough to be part of the implementation plan, not implic - [ ] CORS preflight allowlist in `proxy.tsx` includes both old + new names for request AND response headers - [ ] New SDK emits `x-hexclave-*` by default - [ ] Old SDK (unchanged) authenticates successfully -- [ ] Backend accepts `Authorization: Bearer stackauth_*` -- [ ] Backend accepts `Authorization: Bearer hexclave_*` -- [ ] New SDK constructs tokens with `Bearer hexclave_*` prefix +- [ ] SDK token parser accepts `Bearer stackauth_*` AND `Bearer hexclave_*` (SDK-internal — the backend is not involved) +- [ ] New SDK constructs `tokenStore: { headers }` tokens with the `hexclave_` prefix - [ ] `x-stack-auth: {...}` legacy header continues to be parsed (no Hexclave equivalent emitted) - [ ] Backend emits BOTH `x-stack-*` and `x-hexclave-*` response headers (`actual-status`, `known-error`, `request-id`) - [ ] New SDK reads `x-hexclave-*` response headers, falls back to `x-stack-*` @@ -949,7 +998,7 @@ The Tier sections above describe **what** changes; this section assigns **when** ### PR 1: "Hexclave compatibility layer (invisible)" — now -Purely additive, ships entirely inside the existing `@stackframe/*` packages and existing deploys. Nothing is deleted; nothing breaks; no Hexclave branding becomes visible to any user. Safe to merge quickly. **Prerequisite (not code in the PR): the exhaustive operator env var inventory** — now required since Category C is dual-read (see Category C). +Purely additive, ships entirely inside the existing `@stackframe/*` packages and existing deploys. Nothing is deleted; nothing breaks; no Hexclave branding becomes visible to any user. Safe to merge quickly. The discovery pass resolved every prerequisite — see the [PR 1 implementation guide](#pr-1-implementation-guide-resolved-from-codebase-discovery) for the concrete files, line numbers, and chosen approach per work-area. Scope: - **Wire dual-accept / dual-emit:** request headers, response headers, `Bearer` prefix, query parameters, JWT issuer/audience validator — backend accepts old + new; new SDK code emits the new form (Tier 0). @@ -1003,7 +1052,7 @@ Safely removable in PR 3: ## Open questions still worth answering before implementation -- **Operator env var inventory:** must be produced before PR 1 implementation begins. Category C is now dual-read and **in-scope for PR 1** (no longer deferrable) — the inventory is required so every operator var gets a dual-read site, a docs update, and an `.env.example` entry. +- **Operator env var inventory:** the central `getEnvVariable` prefix-transform (see PR 1 implementation guide) makes server-side dual-read automatic with no per-var code changes — so an exhaustive inventory is *not* a code prerequisite for PR 1. It is still needed for the per-site tail (raw `process.env`, `import.meta.env`, `turbo.json`, `.env*`) and for the PR 2 docs / `.env.example` rewrite; produce it before PR 2. Discovery found ~140 distinct `STACK`-named vars across categories A–E. - **SDK request header emission:** new SDKs emit `x-hexclave-*` for every request header (current plan), or skip the most stable ones (e.g. `branch-id`) to reduce churn? Current plan: emit all. - **DNS infrastructure:** ops team confirmation on indefinite redirect maintenance capacity for 16 subdomains, plus registration + SPF/DKIM/DMARC for the separate `sent-with-hexclave.com` transactional sending domain. - **Future canonicality flip (post PR 3):** is there any reason to ever make `Hexclave*` the canonical class name in JS or Swift, with `Stack*` as the alias? Current plan: no — coexistence indefinitely, neither is "more canonical". From 8b7ccfc7ea6621a4652ebacb7ed5eff4552b896d Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Fri, 22 May 2026 12:03:56 -0700 Subject: [PATCH 05/28] =?UTF-8?q?docs:=20correct=20x-stack-auth=20legacy?= =?UTF-8?q?=20header=20=E2=80=94=20SDK-internal,=20not=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Grep confirms x-stack-auth has zero references in apps/backend and packages/stack-shared. It is produced by the deprecated getAuthHeaders()/ useAuthHeaders() SDK methods and consumed by the SDK tokenStore parser (client-app-impl.ts) — the Stack backend never parses it. Reframed from "backend read-only wire identifier" to "SDK-internal legacy identifier", corrected the false "no current writer" claim, and resolved the open verification item. --- RENAME-TO-HEXCLAVE.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/RENAME-TO-HEXCLAVE.md b/RENAME-TO-HEXCLAVE.md index d3d279554a..f0eac2df13 100644 --- a/RENAME-TO-HEXCLAVE.md +++ b/RENAME-TO-HEXCLAVE.md @@ -7,8 +7,8 @@ Rebrand rollout with backwards compatibility. Organized by wire-compatibility ri ## Locked-in decisions - GitHub canonical repo: **`hexclave/hexclave`** (was `hexclave/stack-auth`) -- Read-only legacy identifiers — kept indefinitely (no Hexclave writer needed because nothing emits them in new code): - - `x-stack-auth` (legacy JSON-encoded auth header) +- SDK-internal legacy identifiers — kept readable indefinitely, no Hexclave variant added: + - `x-stack-auth` (legacy JSON-encoded auth header) — produced by the deprecated `getAuthHeaders()` / `useAuthHeaders()` SDK methods, read by the SDK's `tokenStore: { headers }` parser. Never travels to the Stack backend. Frozen because its producers are `@deprecated` in favor of `getAuthorizationHeader()` (the Bearer path). - **Symmetric dual-support** (old kept indefinitely, new is preferred / emitted by new code): - All `x-stack-*` request headers ↔ `x-hexclave-*` equivalents (dual-accept) - All `x-stack-*` response headers ↔ `x-hexclave-*` equivalents (dual-emit) @@ -53,13 +53,13 @@ Rebrand rollout with backwards compatibility. Organized by wire-compatibility ri These travel between SDK and backend, or get baked into third-party systems. **Alias, never replace.** -### Read-only legacy identifiers (no Hexclave writer needed) +### SDK-internal legacy identifiers (no Hexclave variant) -These have no Hexclave equivalent because nothing in new code emits them. Backend keeps parsing them indefinitely as a compatibility path. +*Discovery correction:* `x-stack-auth` is **not** a backend wire identifier — `apps/backend` and `packages/stack-shared` contain zero references to it. Like the `Bearer stackauth_` prefix, it lives entirely in the SDK (`packages/template`). -| Identifier | What | Why no Hexclave equivalent | +| Identifier | What | Treatment | |---|---|---| -| `x-stack-auth: { accessToken, refreshToken }` | Legacy JSON-encoded auth header | Newer SDKs use split `x-stack-access-token` + `x-stack-refresh-token` (which DO get `x-hexclave-*` aliases). This older header has no current writer to add a new format to. | +| `x-stack-auth: { accessToken, refreshToken }` | Legacy JSON-encoded auth header | **Produced** by the SDK's `getAuthHeaders()` / `useAuthHeaders()` methods (`client-app-impl.ts:1640,3471` — both `@deprecated` in favor of `getAuthorizationHeader()`). **Consumed** by the SDK's `tokenStore: { headers }` parser at `client-app-impl.ts:1098-1113`. The flow is client SDK → the developer's own server → a server-side Stack SDK; the Stack backend is never in the path. Frozen: the producing methods are deprecated, so no `x-hexclave-auth` variant is added — the parser keeps reading `x-stack-auth` indefinitely. New code uses `getAuthorizationHeader()` (the `hexclave_` Bearer path). | ### Symmetric dual-support (old kept, new is canonical) @@ -779,7 +779,7 @@ Items that contain "stack" in their literal name and intentionally stay that way | What | Why | |---|---| -| `x-stack-auth` legacy JSON-encoded header | No current writer; pure read-only compat path | +| `x-stack-auth` legacy JSON-encoded header | SDK-internal — produced by deprecated `getAuthHeaders()`, read by the SDK `tokenStore` parser; never a backend identifier, no Hexclave variant | | `POSTGRES_DB: stackframe` | Would orphan every dev's local volume | | Swift legacy `StackAuth` package | Frozen but installable; new SDK lives in separate `Hexclave` package | @@ -865,9 +865,9 @@ CLI: `stack-cli/src/commands/config-file.ts:202-205`, `init.ts:195,390`, `dev.ts Header-name assertions: `js/auth-like.test.ts:404,419,470,541`, `render-email.test.ts:180,217`, `internal/projects.test.ts:51`, `neon/.../provision.test.ts:224`, `backend-helpers.ts:197-201`. Cookie-name: `backend-helpers.ts:803`, `oauth/{authorize,callback,merge-strategy}.test.ts`, `sign-up-rules.test.ts:36`, `js/cookies.test.ts:200`, `cross-domain-auth.test.ts:190`. Snapshot serializer: `snapshot-serializer.ts:119`. Many `x-stack-known-error` values live in inline snapshots and regenerate automatically. (Error-message-string and docs-URL assertions are PR 2.) -### Still unverified +### `x-stack-auth` legacy header — resolved -- The backend **read** site for the legacy `x-stack-auth` JSON header was not located in `smart-request.tsx`'s auth parser. The SDK emit side is `client-app-impl.ts` (`getAuthHeaders()` / `useAuthHeaders()`). Confirm whether and where the backend consumes it before relying on the "kept indefinitely" claim. +A repo-wide grep confirms `x-stack-auth` has **zero references in `apps/backend` or `packages/stack-shared`** — the backend never parses it. It is entirely SDK-internal: produced by the `@deprecated` `getAuthHeaders()` / `useAuthHeaders()` methods (`client-app-impl.ts:1640,3471`) and consumed by the `tokenStore: { headers }` parser (`client-app-impl.ts:1098-1113`) — the same parser path that handles the `Bearer stackauth_` prefix. No backend change, and no `x-hexclave-auth` variant: the producing methods are deprecated in favor of `getAuthorizationHeader()`, so the header is frozen and the parser keeps reading `x-stack-auth` indefinitely. ### Retained from earlier review @@ -897,7 +897,7 @@ Compatibility-sensitive enough to be part of the implementation plan, not implic - [ ] Old SDK (unchanged) authenticates successfully - [ ] SDK token parser accepts `Bearer stackauth_*` AND `Bearer hexclave_*` (SDK-internal — the backend is not involved) - [ ] New SDK constructs `tokenStore: { headers }` tokens with the `hexclave_` prefix -- [ ] `x-stack-auth: {...}` legacy header continues to be parsed (no Hexclave equivalent emitted) +- [ ] `x-stack-auth` legacy header still accepted by the SDK's `tokenStore: { headers }` parser (SDK-internal; no `x-hexclave-auth` variant; producers are deprecated) - [ ] Backend emits BOTH `x-stack-*` and `x-hexclave-*` response headers (`actual-status`, `known-error`, `request-id`) - [ ] New SDK reads `x-hexclave-*` response headers, falls back to `x-stack-*` - [ ] Old SDK still reads `x-stack-*` response headers correctly @@ -1040,7 +1040,7 @@ Safely removable in PR 3: - `x-stack-*` request headers (kept dual-accepted indefinitely) - `x-stack-*` response headers (kept dual-emitted indefinitely) - `Bearer stackauth_*` prefix (kept dual-accepted indefinitely) -- `x-stack-auth` legacy header (still parsed) +- `x-stack-auth` legacy header (SDK-internal; still parsed by the SDK `tokenStore` parser) - JWT validator's acceptance of all three `stack-auth.com` issuer variants and the IdP audience - JS `Stack*` exports — they're the canonical class names, not aliases (deprecated, not removed) - Legacy `StackAuth` Swift package (frozen but installable from existing SPM URL) From e60550a2e3919eb4e50826712dbf4546f942d9ce Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Fri, 22 May 2026 12:31:43 -0700 Subject: [PATCH 06/28] feat(hexclave): add Hexclave* SDK export aliases Adds Hexclave* aliases for the user-facing Stack* exports (apps, provider, handler, theme, useStackApp, config) in packages/template; codegen propagates them to the generated SDKs. Additive and non-breaking. PR 1 of the Hexclave rebrand (see RENAME-TO-HEXCLAVE.md, Tier 1). --- packages/template/src/index.ts | 8 ++++++++ packages/template/src/lib/stack-app/index.ts | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/packages/template/src/index.ts b/packages/template/src/index.ts index a948d93ac1..6801587fb3 100644 --- a/packages/template/src/index.ts +++ b/packages/template/src/index.ts @@ -2,6 +2,9 @@ export * from './lib/stack-app'; export { getConvexProvidersConfig } from "./integrations/convex"; export type { StackConfig } from "@stackframe/stack-shared/config"; export { defineStackConfig } from "@stackframe/stack-shared/config"; +// Hexclave aliases — same symbols under the new brand name (see RENAME-TO-HEXCLAVE.md, Tier 1) +export type { StackConfig as HexclaveConfig } from "@stackframe/stack-shared/config"; +export { defineStackConfig as defineHexclaveConfig } from "@stackframe/stack-shared/config"; // IF_PLATFORM react-like export type { AnalyticsOptions, AnalyticsReplayOptions } from "./lib/stack-app/apps/implementations/session-replay"; @@ -9,6 +12,11 @@ export { default as StackHandler } from "./components-page/stack-handler"; export { useStackApp, useUser } from "./lib/hooks"; export { default as StackProvider } from "./providers/stack-provider"; export { StackTheme } from './providers/theme-provider'; +// Hexclave aliases — same symbols under the new brand name (see RENAME-TO-HEXCLAVE.md, Tier 1) +export { default as HexclaveHandler } from "./components-page/stack-handler"; +export { useStackApp as useHexclaveApp } from "./lib/hooks"; +export { default as HexclaveProvider } from "./providers/stack-provider"; +export { StackTheme as HexclaveTheme } from './providers/theme-provider'; export { AccountSettings } from "./components-page/account-settings"; export { AuthPage } from "./components-page/auth-page"; diff --git a/packages/template/src/lib/stack-app/index.ts b/packages/template/src/lib/stack-app/index.ts index 0167740088..cd621eba1d 100644 --- a/packages/template/src/lib/stack-app/index.ts +++ b/packages/template/src/lib/stack-app/index.ts @@ -11,6 +11,21 @@ export type { StackServerAppConstructor, StackServerAppConstructorOptions } from "./apps"; +// Hexclave aliases — same symbols under the new brand name (see RENAME-TO-HEXCLAVE.md, Tier 1) +export { + StackAdminApp as HexclaveAdminApp, + StackClientApp as HexclaveClientApp, + StackServerApp as HexclaveServerApp +} from "./apps"; +export type { + StackAdminAppConstructor as HexclaveAdminAppConstructor, + StackAdminAppConstructorOptions as HexclaveAdminAppConstructorOptions, + StackClientAppConstructor as HexclaveClientAppConstructor, + StackClientAppConstructorOptions as HexclaveClientAppConstructorOptions, + StackClientAppJson as HexclaveClientAppJson, + StackServerAppConstructor as HexclaveServerAppConstructor, + StackServerAppConstructorOptions as HexclaveServerAppConstructorOptions +} from "./apps"; export type { EmailOutboxListOptions, From fc781def3f325744fbd2d2f87c8ab9bd421107af Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Fri, 22 May 2026 12:31:43 -0700 Subject: [PATCH 07/28] feat(hexclave): JWT validator accepts both stack-auth.com and hexclave.com issuers decodeAccessToken now builds allowed issuers for both api.stack-auth.com and api.hexclave.com so tokens issued under either host keep validating across the domain transition. Signing is unchanged (follows the configured API URL). --- apps/backend/src/lib/tokens.tsx | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/lib/tokens.tsx b/apps/backend/src/lib/tokens.tsx index f834513f51..d97382909e 100644 --- a/apps/backend/src/lib/tokens.tsx +++ b/apps/backend/src/lib/tokens.tsx @@ -60,6 +60,22 @@ const getIssuer = (projectId: string, userType: UserType) => { const url = new URL(`/api/v1/projects${suffix}/${projectId}`, getEnvVariable("NEXT_PUBLIC_STACK_API_URL")); return url.toString(); }; +// Hexclave rebrand: api.stack-auth.com ↔ api.hexclave.com. During the domain transition a +// backend served from one host must keep validating tokens issued under the other, so the +// validator accepts the issuer under both hosts. Signing always uses getIssuer() (the +// configured host), so new tokens follow the deployment. See RENAME-TO-HEXCLAVE.md (Tier 0, JWT). +const issuerHostAliases: Record = { + "api.stack-auth.com": "api.hexclave.com", + "api.hexclave.com": "api.stack-auth.com", +}; +const getAllowedIssuers = (projectId: string, userType: UserType): string[] => { + const issuer = getIssuer(projectId, userType); + const aliasHost = issuerHostAliases[new URL(issuer).host]; + if (!aliasHost) return [issuer]; + const aliasedUrl = new URL(issuer); + aliasedUrl.host = aliasHost; + return [issuer, aliasedUrl.toString()]; +}; const getAudience = (projectId: string, userType: UserType) => { // TODO: make the audience a URL, and encode the user type in a better way return userType === 'anonymous' ? `${projectId}:anon` : userType === 'restricted' ? `${projectId}:restricted` : projectId; @@ -98,9 +114,9 @@ export async function decodeAccessToken(accessToken: string, { allowAnonymous, a // Determine allowed issuers based on what types of tokens we accept const projectId = aud.split(":")[0]; const allowedIssuers = [ - getIssuer(projectId, 'normal'), - ...(allowRestricted ? [getIssuer(projectId, 'restricted')] : []), - ...(allowAnonymous ? [getIssuer(projectId, 'anonymous')] : []), + ...getAllowedIssuers(projectId, 'normal'), + ...(allowRestricted ? getAllowedIssuers(projectId, 'restricted') : []), + ...(allowAnonymous ? getAllowedIssuers(projectId, 'anonymous') : []), ]; payload = await verifyJWT({ From 2a056eac32724e2039a198052c871f8ce1a72bc5 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Fri, 22 May 2026 12:31:43 -0700 Subject: [PATCH 08/28] feat(hexclave): dual-accept x-hexclave-* request headers Backend and dashboard proxies normalize each x-hexclave-* request header onto its x-stack-* equivalent before routing/validation, and add the x-hexclave-* names to the CORS allowlists. Existing x-stack-* clients are unaffected. --- apps/backend/src/proxy.tsx | 26 ++++++++++++++++++++++---- apps/dashboard/src/proxy.tsx | 29 ++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/apps/backend/src/proxy.tsx b/apps/backend/src/proxy.tsx index 094340ff7a..2697fc3cd7 100644 --- a/apps/backend/src/proxy.tsx +++ b/apps/backend/src/proxy.tsx @@ -53,6 +53,16 @@ const corsAllowedResponseHeaders = [ 'x-stack-known-error', ]; +// Hexclave rebrand: every `x-stack-*` header is dual-accepted under its `x-hexclave-*` equivalent. +// Derive the alias names so the CORS allowlists never drift. See RENAME-TO-HEXCLAVE.md (Tier 0). +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); + // This function can be marked `async` if using `await` inside export async function proxy(request: NextRequest) { const url = new URL(request.url); @@ -72,9 +82,9 @@ export async function proxy(request: NextRequest) { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS", "Access-Control-Max-Age": "86400", // 1 day (capped to lower values, eg. 10min, by some browsers) - "Access-Control-Allow-Headers": corsAllowedRequestHeaders.join(', '), - "Access-Control-Expose-Headers": corsAllowedResponseHeaders.join(', '), - "Vary": corsAllowedRequestHeaders.join(', '), + "Access-Control-Allow-Headers": corsAllowedRequestHeadersWithAliases.join(', '), + "Access-Control-Expose-Headers": corsAllowedResponseHeadersWithAliases.join(', '), + "Vary": corsAllowedRequestHeadersWithAliases.join(', '), } : undefined; // ensure our clients can handle 429 responses @@ -112,7 +122,15 @@ export async function proxy(request: NextRequest) { } const newRequestHeaders = new Headers(request.headers); - // here we could update the request headers (currently we don't) + // Hexclave rebrand: dual-accept request headers. New SDKs emit `x-hexclave-*`; copy each onto its + // `x-stack-*` equivalent here — before routing and yup validation — so downstream auth parsing + // and route schemas (which read `x-stack-*`) keep working unchanged. The new form wins when both + // are present. See RENAME-TO-HEXCLAVE.md (Tier 0, HTTP request headers). + for (const [name, value] of request.headers) { + if (name.startsWith('x-hexclave-')) { + newRequestHeaders.set(`x-stack-${name.slice('x-hexclave-'.length)}`, value); + } + } const responseInit = isApiRequest ? { request: { diff --git a/apps/dashboard/src/proxy.tsx b/apps/dashboard/src/proxy.tsx index 4a0eeca498..51312d0e6b 100644 --- a/apps/dashboard/src/proxy.tsx +++ b/apps/dashboard/src/proxy.tsx @@ -34,6 +34,16 @@ const corsAllowedResponseHeaders = [ 'x-stack-known-error', ]; +// Hexclave rebrand: every `x-stack-*` header is dual-accepted under its `x-hexclave-*` equivalent. +// Derive the alias names so the CORS allowlists never drift. See RENAME-TO-HEXCLAVE.md (Tier 0). +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 async function proxy(request: NextRequest) { const delay = Number.parseInt(getEnvVariable('STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS', '0')); if (delay) { @@ -48,15 +58,28 @@ export async function proxy(request: NextRequest) { const url = new URL(request.url); const isApiRequest = url.pathname.startsWith('/api/'); + // Hexclave rebrand: dual-accept request headers — copy each `x-hexclave-*` onto its `x-stack-*` + // equivalent so downstream API routes that read `x-stack-*` keep working unchanged. The new form + // wins when both are present. See RENAME-TO-HEXCLAVE.md (Tier 0, HTTP request headers). + const newRequestHeaders = new Headers(request.headers); + for (const [name, value] of request.headers) { + if (name.startsWith('x-hexclave-')) { + newRequestHeaders.set(`x-stack-${name.slice('x-hexclave-'.length)}`, value); + } + } + // default headers - const responseInit: ResponseInit = { + const responseInit = { + request: { + headers: newRequestHeaders, + }, headers: { // CORS headers ...(!isApiRequest ? {} : { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", - "Access-Control-Allow-Headers": corsAllowedRequestHeaders.join(', '), - "Access-Control-Expose-Headers": corsAllowedResponseHeaders.join(', '), + "Access-Control-Allow-Headers": corsAllowedRequestHeadersWithAliases.join(', '), + "Access-Control-Expose-Headers": corsAllowedResponseHeadersWithAliases.join(', '), }), }, }; From 30ffd604c7a859171f0e6bbb04705e6deb71973d Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Fri, 22 May 2026 12:31:43 -0700 Subject: [PATCH 09/28] feat(hexclave): register ask_hexclave MCP tool alongside ask_stack_auth Both tools register identical schema/behavior via a shared helper; ask_stack_auth keeps working unchanged. --- apps/mcp/src/mcp-handler.ts | 129 +++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 61 deletions(-) diff --git a/apps/mcp/src/mcp-handler.ts b/apps/mcp/src/mcp-handler.ts index fa1b295a76..b9c277e22a 100644 --- a/apps/mcp/src/mcp-handler.ts +++ b/apps/mcp/src/mcp-handler.ts @@ -104,72 +104,79 @@ export function createStackMcpHandler(config: { streamableHttpEndpoint: string } }), ); - server.tool( - "ask_stack_auth", - "Ask the Stack Auth documentation assistant. Use this for any question about Stack Auth: setup, APIs, SDK usage, configuration, or troubleshooting. The assistant searches official documentation and answers with citations. Always set `reason` to a short explanation of why you are calling this tool (for product analytics and debugging).", - { - question: z.string().describe("The full question to ask about Stack Auth."), - reason: z - .string() - .min(1) - .describe( - "Why the agent invoked this tool (e.g. user asked about OAuth setup, need Stack Auth API headers). Used for analytics, not sent to the model.", - ), - userPrompt: z - .string() - .min(1) - .describe( - "The original user message/prompt that triggered this tool call. Copy the user's exact words. Don't include any sensitive information.", - ), - conversationId: z - .string() - .optional() - .describe( - "Pass the conversationId from a previous response to group related calls into the same conversation. Omit on the first call - the server will generate one and return it.", - ), - }, - async ({ question, reason, userPrompt, conversationId }) => { - await withPostHog(async (posthog) => { - posthog.capture({ - event: "ask_stack_auth_mcp", - properties: { question, reason }, - distinctId: "mcp-handler", + // Hexclave rebrand: `ask_hexclave` is the canonical going-forward tool name; `ask_stack_auth` + // is kept indefinitely as a legacy alias. Both register identical schema/behavior via this + // helper (see RENAME-TO-HEXCLAVE.md, Tier 0, MCP tool name). + const registerAskTool = (toolName: "ask_stack_auth" | "ask_hexclave", brand: string) => { + server.tool( + toolName, + `Ask the ${brand} documentation assistant. Use this for any question about ${brand}: setup, APIs, SDK usage, configuration, or troubleshooting. The assistant searches official documentation and answers with citations. Always set \`reason\` to a short explanation of why you are calling this tool (for product analytics and debugging).`, + { + question: z.string().describe(`The full question to ask about ${brand}.`), + reason: z + .string() + .min(1) + .describe( + `Why the agent invoked this tool (e.g. user asked about OAuth setup, need ${brand} API headers). Used for analytics, not sent to the model.`, + ), + userPrompt: z + .string() + .min(1) + .describe( + "The original user message/prompt that triggered this tool call. Copy the user's exact words. Don't include any sensitive information.", + ), + conversationId: z + .string() + .optional() + .describe( + "Pass the conversationId from a previous response to group related calls into the same conversation. Omit on the first call - the server will generate one and return it.", + ), + }, + async ({ question, reason, userPrompt, conversationId }) => { + await withPostHog(async (posthog) => { + posthog.capture({ + event: `${toolName}_mcp`, + properties: { question, reason }, + distinctId: "mcp-handler", + }); }); - }); - - const res = await fetch(`${getBackendApiBaseUrl()}/api/latest/ai/query/generate`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - quality: "smart", - speed: "fast", - tools: ["docs"], - systemPrompt: "docs-ask-ai", - messages: [{ role: "user", content: question }], - mcpCallMetadata: { toolName: "ask_stack_auth", reason, userPrompt, conversationId }, - }), - }); - - if (!res.ok) { - const errText = await res.text(); - return { - content: [{ type: "text", text: `Stack Auth AI error (${res.status}): ${errText}` }], - isError: true, - }; - } - const body = parseAiQueryResponse(await res.json()); + const res = await fetch(`${getBackendApiBaseUrl()}/api/latest/ai/query/generate`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + quality: "smart", + speed: "fast", + tools: ["docs"], + systemPrompt: "docs-ask-ai", + messages: [{ role: "user", content: question }], + mcpCallMetadata: { toolName, reason, userPrompt, conversationId }, + }), + }); - const contentText = body.content?.map((c) => c.text).join("\n\n"); - const text = body.finalText ?? contentText ?? ""; + if (!res.ok) { + const errText = await res.text(); + return { + content: [{ type: "text", text: `${brand} AI error (${res.status}): ${errText}` }], + isError: true, + }; + } - const responseConversationId = body.conversationId ?? conversationId ?? ""; + const body = parseAiQueryResponse(await res.json()); - return { - content: [{ type: "text", text: `${text.length > 0 ? text : "(empty response)"}\n\n[conversationId: ${responseConversationId} - pass this value as the conversationId parameter in your next ask_stack_auth call to continue this conversation]` }], - }; - }, - ); + const contentText = body.content?.map((c) => c.text).join("\n\n"); + const text = body.finalText ?? contentText ?? ""; + + const responseConversationId = body.conversationId ?? conversationId ?? ""; + + return { + content: [{ type: "text", text: `${text.length > 0 ? text : "(empty response)"}\n\n[conversationId: ${responseConversationId} - pass this value as the conversationId parameter in your next ${toolName} call to continue this conversation]` }], + }; + }, + ); + }; + registerAskTool("ask_stack_auth", "Stack Auth"); + registerAskTool("ask_hexclave", "Hexclave"); }, { serverInfo: { From 7fed864a08f2c7a092c1be0d6a78f5f764e331d9 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Fri, 22 May 2026 13:36:50 -0700 Subject: [PATCH 10/28] feat(hexclave): env vars, cookies, bearer, symbols, query params, internal renames Completes the PR 1 wire/compat layer for the Hexclave rebrand: - Env vars: central getEnvVariable HEXCLAVE_* prefix-transform (dual-read); dashboard + template client env files dual-read; turbo.json globalEnv; NEXT_PUBLIC_STACK_PORT_PREFIX renamed outright (incl. docker entrypoint). - Config discovery: CLI/dashboard/backend prefer hexclave.config.ts, fall back to stack.config.ts; CLI credentials path dual-read. - Response headers: backend dual-emits x-hexclave-*; SDK reads new first. - SDK request headers: interfaces + dashboard api-headers emit x-hexclave-*. - Bearer prefix: SDK token parser accepts stackauth_ and hexclave_. - Cookies: dual-write/dual-read auth, OAuth-state and low-risk cookies. - Storage keys + query params dual-handled; cross-domain params dual-emit. - Symbols: app-internals symbol dual-attached; 3 file-private symbols renamed. - Internal-only symbols renamed outright (no alias): StackAssertionError, Stack{Client,Server,Admin}Interface. Verified: pnpm typecheck and pnpm lint pass (pre-existing fresh-worktree codegen gaps in stack-docs aside). e2e snapshot regeneration pending a CI run. --- .../e2e-custom-base-port-api-tests.yaml | 2 +- .../setup-tests-with-custom-base-port.yaml | 2 +- apps/backend/.env.development | 30 +-- apps/backend/package.json | 8 +- .../scripts/backfill-internal-free-plans.ts | 6 +- .../regen-internal-subscriptions-to-latest.ts | 6 +- apps/backend/scripts/run-bulldozer-studio.ts | 34 +-- apps/backend/scripts/run-cron-jobs.ts | 6 +- apps/backend/scripts/run-email-queue.ts | 6 +- .../scripts/verify-data-integrity/api.ts | 12 +- .../clickhouse-sync-verifier.ts | 8 +- .../scripts/verify-data-integrity/index.ts | 8 +- .../payments-verifier.ts | 6 +- .../stripe-payout-integrity.ts | 6 +- .../app/api/latest/(api-keys)/handlers.tsx | 10 +- .../mfa/sign-in/verification-code-handler.tsx | 4 +- .../oauth/authorize/[provider_id]/route.tsx | 16 +- .../oauth/callback/[provider_id]/route.tsx | 22 +- .../oauth/cross-domain/authorize/route.tsx | 4 +- .../api/latest/auth/oauth/oauth-helpers.tsx | 8 +- .../register/verification-code-handler.tsx | 8 +- .../sign-in/verification-code-handler.tsx | 4 +- .../api/latest/auth/password/set/route.tsx | 4 +- .../latest/auth/password/sign-in/route.tsx | 4 +- .../api/latest/auth/password/update/route.tsx | 4 +- .../latest/check-feature-support/route.tsx | 4 +- .../access-token-helpers.tsx | 10 +- .../data-vault/stores/[id]/get/route.tsx | 4 +- .../src/app/api/latest/emails/outbox/crud.tsx | 8 +- .../credential-scanning/revoke/route.tsx | 12 +- .../custom/oauth/authorize/route.tsx | 4 +- .../custom/oauth/idp/[[...route]]/route.tsx | 4 +- .../integrations/custom/oauth/token/route.tsx | 6 +- .../confirm/verification-code-handler.tsx | 4 +- .../src/app/api/latest/integrations/idp.ts | 10 +- .../neon/oauth/authorize/route.tsx | 4 +- .../neon/oauth/idp/[[...route]]/route.tsx | 4 +- .../integrations/neon/oauth/token/route.tsx | 6 +- .../confirm/verification-code-handler.tsx | 4 +- .../integrations/resend/webhooks/route.tsx | 4 +- .../integrations/stripe/webhooks/route.tsx | 40 ++-- .../latest/internal/analytics/query/route.ts | 4 +- .../latest/internal/backend-urls/route.tsx | 12 +- .../config/override/[level]/route.tsx | 4 +- .../internal/email-themes/[id]/route.tsx | 4 +- .../internal/external-db-sync/poller/route.ts | 6 +- .../external-db-sync/sequencer/route.ts | 4 +- .../internal/external-db-sync/status/route.ts | 2 +- .../internal/failed-emails-digest/route.ts | 4 +- .../[featureRequestId]/upvote/route.tsx | 8 +- .../internal/feature-requests/route.tsx | 6 +- .../internal/local-emulator/project/route.tsx | 45 +++- .../app/api/latest/internal/metrics/route.tsx | 12 +- .../internal/payments/method-configs/route.ts | 4 +- .../test-mode-purchase-session/route.tsx | 4 +- .../payments/transactions/refund/route.tsx | 16 +- .../internal/payments/transactions/route.tsx | 84 ++++---- .../internal/projects-metrics/route.tsx | 4 +- .../app/api/latest/internal/projects/crud.tsx | 4 +- .../latest/internal/send-test-email/route.tsx | 4 +- .../internal/send-test-webhook/route.tsx | 4 +- .../chunks/[chunk_id]/events/route.tsx | 10 +- .../[session_replay_id]/events/route.tsx | 10 +- .../latest/internal/user-activity/route.tsx | 4 +- .../[customer_id]/switch/route.ts | 6 +- .../purchases/purchase-session/route.tsx | 6 +- .../payments/purchases/validate-code/route.ts | 4 +- .../backend/src/app/api/latest/users/crud.tsx | 20 +- apps/backend/src/app/health/email/route.tsx | 8 +- .../error-handler-debug/endpoint/route.tsx | 4 +- apps/backend/src/app/health/route.tsx | 4 +- apps/backend/src/auto-migrations/index.tsx | 10 +- apps/backend/src/instrumentation.ts | 4 +- apps/backend/src/lib/ai/mcp-logger.ts | 4 +- apps/backend/src/lib/ai/models.ts | 6 +- apps/backend/src/lib/bulldozer/db/index.ts | 4 +- .../db/row-change-trigger-dispatch.ts | 8 +- .../lib/bulldozer/db/tables/concat-table.ts | 8 +- .../backend/src/lib/bulldozer/db/utilities.ts | 4 +- apps/backend/src/lib/cache.tsx | 6 +- apps/backend/src/lib/clickhouse-errors.ts | 6 +- apps/backend/src/lib/clickhouse.tsx | 8 +- apps/backend/src/lib/config.tsx | 38 ++-- apps/backend/src/lib/contact-channel.tsx | 6 +- apps/backend/src/lib/email-delivery-stats.tsx | 4 +- apps/backend/src/lib/email-queue-step.tsx | 27 +-- apps/backend/src/lib/email-rendering.tsx | 12 +- .../backend/src/lib/email-template-rewrite.ts | 4 +- apps/backend/src/lib/emailable.tsx | 16 +- apps/backend/src/lib/emails-low-level.tsx | 8 +- apps/backend/src/lib/emails.tsx | 12 +- apps/backend/src/lib/end-users.tsx | 6 +- apps/backend/src/lib/events.tsx | 20 +- .../backend/src/lib/external-db-sync-queue.ts | 4 +- apps/backend/src/lib/external-db-sync.ts | 74 +++---- apps/backend/src/lib/featurebase.tsx | 4 +- apps/backend/src/lib/internal-api-keys.tsx | 10 +- .../src/lib/internal-feedback-emails.tsx | 4 +- apps/backend/src/lib/js-execution.tsx | 30 +-- .../backend/src/lib/managed-email-domains.tsx | 6 +- .../src/lib/managed-email-onboarding.tsx | 24 +-- .../src/lib/notification-categories.ts | 4 +- apps/backend/src/lib/oauth.tsx | 10 +- apps/backend/src/lib/openapi.tsx | 28 +-- apps/backend/src/lib/payments.tsx | 6 +- .../src/lib/payments/ensure-free-plan.ts | 8 +- apps/backend/src/lib/plan-entitlements.ts | 8 +- apps/backend/src/lib/product-versions.tsx | 4 +- apps/backend/src/lib/projects.tsx | 8 +- apps/backend/src/lib/redirect-urls.tsx | 4 +- apps/backend/src/lib/sign-up-rules.ts | 6 +- apps/backend/src/lib/stripe.tsx | 30 +-- apps/backend/src/lib/telegram.tsx | 4 +- apps/backend/src/lib/tenancies.tsx | 20 +- apps/backend/src/lib/tokens.tsx | 22 +- apps/backend/src/lib/turnstile.tsx | 14 +- apps/backend/src/lib/webhooks.tsx | 4 +- apps/backend/src/oauth/index.tsx | 4 +- apps/backend/src/oauth/model.tsx | 2 +- apps/backend/src/oauth/providers/apple.tsx | 4 +- apps/backend/src/oauth/providers/base.tsx | 16 +- apps/backend/src/oauth/providers/github.tsx | 8 +- apps/backend/src/oauth/providers/x.tsx | 4 +- apps/backend/src/polyfills.tsx | 4 +- apps/backend/src/prisma-client.tsx | 16 +- apps/backend/src/proxy.tsx | 6 +- .../src/route-handlers/crud-handler.tsx | 12 +- .../src/route-handlers/cud-handler.tsx | 18 +- .../src/route-handlers/smart-request.tsx | 8 +- .../src/route-handlers/smart-response.tsx | 10 +- .../route-handlers/smart-route-handler.tsx | 13 +- .../verification-code-handler.tsx | 4 +- apps/backend/src/s3.tsx | 18 +- apps/backend/src/smart-router.tsx | 12 +- apps/backend/src/utils/telemetry.tsx | 4 +- apps/dashboard/.env.development | 8 +- apps/dashboard/package.json | 4 +- .../link-existing-onboarding.tsx | 5 +- .../projects/preview-project-redirect.tsx | 4 +- .../[projectId]/apps/[appId]/page-client.tsx | 4 +- .../[projectId]/auth-methods/page-client.tsx | 6 +- .../[projectId]/domains/page-client.tsx | 4 +- .../[projectId]/sign-up-rules/page-client.tsx | 14 +- .../teams/[teamId]/page-client.tsx | 4 +- .../projects/[projectId]/use-admin-app.tsx | 4 +- .../users/[userId]/page-client.tsx | 10 +- .../widget-playground/page-client.tsx | 70 +++---- .../neon-transfer-confirm-page.tsx | 4 +- .../integrations/oauth-confirm-page.tsx | 4 +- .../integrations/transfer-confirm-page.tsx | 4 +- .../app/(main)/wizard-congrats/posthog.tsx | 6 +- .../auth/route.ts | 8 +- .../src/app/development-port-display.tsx | 2 +- .../error-handler-debug/endpoint/route.tsx | 4 +- .../project-transfer-confirm-view.tsx | 8 +- apps/dashboard/src/components/smart-form.tsx | 6 +- .../src/components/stack-companion.tsx | 25 ++- .../stack-companion/changelog-widget.tsx | 5 +- .../stack-companion/feature-request-board.tsx | 6 +- .../src/components/ui/brand-icons.tsx | 4 +- apps/dashboard/src/components/ui/form.tsx | 4 +- apps/dashboard/src/instrumentation.ts | 2 +- apps/dashboard/src/lib/api-headers.ts | 9 +- apps/dashboard/src/lib/cel-visual-parser.ts | 6 +- apps/dashboard/src/lib/config-update.tsx | 4 +- apps/dashboard/src/lib/env.tsx | 60 +++--- .../config-file.ts | 16 +- .../lib/remote-development-environment/env.ts | 4 +- apps/dashboard/src/lib/risk-score-utils.ts | 4 +- apps/dashboard/src/lib/stack-app-internals.ts | 14 +- apps/dashboard/src/lib/transfer-utils.ts | 6 +- apps/dashboard/src/polyfills.tsx | 4 +- apps/dashboard/src/proxy.tsx | 4 +- apps/dashboard/src/stack/server.tsx | 4 +- apps/dev-launchpad/package.json | 2 +- apps/dev-launchpad/public/index.html | 4 +- .../dev-launchpad/scripts/write-env-config.js | 4 +- apps/e2e/.env.development | 14 +- apps/e2e/tests/backend/backend-helpers.ts | 32 +-- .../endpoints/api/migration-tests.test.ts | 2 +- .../api/v1/analytics-events-batch.test.ts | 4 +- .../endpoints/api/v1/analytics-query.test.ts | 8 +- .../api/v1/auth/oauth/authorize.test.ts | 6 +- .../api/v1/auth/password/reset.test.ts | 4 +- .../api/v1/auth/sessions/index.test.ts | 4 +- .../api/v1/emails/email-queue.test.ts | 8 +- .../api/v1/emails/outbox-api.test.ts | 6 +- .../api/v1/external-db-sync-advanced.test.ts | 4 +- .../api/v1/external-db-sync-basics.test.ts | 10 +- .../api/v1/external-db-sync-race.test.ts | 2 +- .../api/v1/external-db-sync-utils.ts | 2 +- .../backend/endpoints/api/v1/index.test.ts | 4 +- .../api/v1/integrations/custom/oauth.test.ts | 18 +- .../custom/projects/transfer.test.ts | 2 +- .../api/v1/integrations/neon/oauth.test.ts | 18 +- .../neon/projects/transfer.test.ts | 2 +- .../api/v1/internal/projects.test.ts | 2 +- .../endpoints/api/v1/send-email.test.ts | 4 +- .../backend/endpoints/api/v1/teams.test.ts | 4 +- .../endpoints/api/v1/unsubscribe-link.test.ts | 2 +- .../backend/endpoints/api/v1/users.test.ts | 4 +- .../tests/backend/payment-quota-helpers.ts | 10 +- apps/e2e/tests/helpers.ts | 26 +-- apps/e2e/tests/helpers/ports.ts | 2 +- apps/e2e/tests/js/app.test.ts | 2 +- apps/e2e/tests/snapshot-serializer.ts | 10 +- apps/hosted-components/.env.development | 2 +- apps/hosted-components/package.json | 2 +- apps/hosted-components/vite.config.ts | 2 +- apps/internal-tool/.env.development | 6 +- apps/internal-tool/package.json | 4 +- .../scripts/spacetime-publish.mjs | 2 +- apps/internal-tool/src/lib/mcp-review-api.ts | 2 +- apps/internal-tool/src/stack.ts | 2 +- apps/mcp/.env.development | 2 +- apps/mcp/package.json | 4 +- apps/mock-oauth-server/src/index.ts | 2 +- apps/skills/package.json | 4 +- docker/dependencies/docker.compose.yaml | 46 ++-- docs-mintlify/package.json | 2 +- docs/.env.development | 2 +- docs/package.json | 4 +- .../components/layouts/platform-indicator.tsx | 26 +-- .../src/components/mdx/platform-codeblock.tsx | 23 +- examples/cjs-test/.env.development | 2 +- examples/cjs-test/package.json | 4 +- examples/convex/.env.development | 2 +- examples/convex/package.json | 6 +- examples/demo/.env.development | 4 +- examples/demo/package.json | 4 +- examples/demo/src/app/payments-demo/page.tsx | 2 +- examples/docs-examples/.env.development | 2 +- examples/docs-examples/package.json | 4 +- examples/e-commerce/.env.development | 2 +- examples/e-commerce/package.json | 4 +- examples/js-example/.env.development | 4 +- examples/js-example/package.json | 2 +- .../lovable-react-18-example/.env.development | 2 +- .../lovable-react-18-example/vite.config.ts | 2 +- examples/middleware/.env.development | 2 +- examples/middleware/package.json | 4 +- examples/react-example/.env.development | 2 +- examples/react-example/package.json | 2 +- examples/supabase/.env.development | 2 +- examples/supabase/package.json | 4 +- examples/tanstack-start-demo/.env.development | 2 +- examples/tanstack-start-demo/package.json | 2 +- examples/tanstack-start-demo/src/stack.ts | 4 +- examples/tanstack-start-demo/vite.config.ts | 2 +- package.json | 10 +- packages/init-stack/package.json | 2 +- packages/init-stack/src/index.ts | 3 +- .../stack-cli/src/commands/config-file.ts | 11 +- packages/stack-cli/src/commands/emulator.ts | 2 +- packages/stack-cli/src/commands/init.ts | 3 +- packages/stack-cli/src/lib/config.ts | 32 ++- packages/stack-shared/src/config/format.ts | 6 +- .../migrate-catalogs-to-product-lines.ts | 4 +- .../src/config/schema-fuzzer.test.ts | 4 +- packages/stack-shared/src/config/schema.ts | 12 +- .../src/helpers/production-mode.ts | 4 +- packages/stack-shared/src/index.ts | 6 +- .../src/interface/admin-interface.ts | 7 +- .../src/interface/client-interface.test.ts | 8 +- .../src/interface/client-interface.ts | 89 ++++---- .../src/interface/page-component-versions.ts | 5 +- .../src/interface/server-interface.ts | 23 +- packages/stack-shared/src/known-errors.tsx | 6 +- packages/stack-shared/src/schema-fields.ts | 18 +- packages/stack-shared/src/sessions.ts | 10 +- packages/stack-shared/src/utils/api-keys.tsx | 8 +- packages/stack-shared/src/utils/bytes.tsx | 20 +- packages/stack-shared/src/utils/crypto.tsx | 8 +- .../stack-shared/src/utils/currencies.tsx | 4 +- packages/stack-shared/src/utils/dates.tsx | 6 +- packages/stack-shared/src/utils/env.tsx | 30 ++- packages/stack-shared/src/utils/errors.tsx | 8 +- packages/stack-shared/src/utils/esbuild.tsx | 12 +- .../stack-shared/src/utils/featurebase.tsx | 28 +-- packages/stack-shared/src/utils/globals.tsx | 3 +- packages/stack-shared/src/utils/hashes.tsx | 6 +- packages/stack-shared/src/utils/jwt.tsx | 6 +- packages/stack-shared/src/utils/objects.tsx | 24 +-- .../src/utils/paginated-lists.tsx | 6 +- packages/stack-shared/src/utils/promises.tsx | 10 +- packages/stack-shared/src/utils/strings.tsx | 18 +- packages/stack-shared/src/utils/telemetry.tsx | 4 +- .../src/utils/turnstile-browser.ts | 4 +- .../stack-shared/src/utils/turnstile-flow.ts | 12 +- .../stack-shared/src/utils/typed-arrays.tsx | 4 +- packages/stack-shared/src/utils/unicode.tsx | 4 +- .../stack-ui/src/components/brand-icons.tsx | 4 +- packages/stack-ui/src/components/ui/form.tsx | 4 +- packages/template/scripts/process-css.ts | 4 +- .../teams/team-api-keys-section.tsx | 4 +- .../src/components-page/cli-auth-confirm.tsx | 3 +- packages/template/src/components-page/mfa.tsx | 5 +- .../components-page/stack-handler-client.tsx | 8 +- .../template/src/components/oauth-button.tsx | 6 +- .../template/src/components/team-switcher.tsx | 4 +- .../template/src/dev-tool/dev-tool-core.ts | 5 +- packages/template/src/dev-tool/index.ts | 3 +- packages/template/src/lib/auth.test.ts | 4 +- packages/template/src/lib/auth.ts | 12 +- packages/template/src/lib/cookie.ts | 47 +++-- packages/template/src/lib/env.ts | 56 ++--- .../apps/implementations/admin-app-impl.ts | 23 +- .../apps/implementations/client-app-impl.ts | 197 ++++++++++++------ .../stack-app/apps/implementations/common.ts | 8 +- .../implementations/redirect-page-urls.ts | 96 ++++++--- .../apps/implementations/server-app-impl.ts | 26 +-- .../apps/implementations/session-replay.ts | 25 ++- packages/template/src/lib/stack-app/common.ts | 4 + packages/template/src/lib/stack-app/index.ts | 4 +- .../src/lib/stack-app/url-targets.test.ts | 4 +- .../template/src/lib/stack-app/url-targets.ts | 10 +- packages/template/src/utils/url.ts | 4 +- scripts/set-process-title.js | 2 +- turbo.json | 1 + 319 files changed, 1787 insertions(+), 1462 deletions(-) diff --git a/.github/workflows/e2e-custom-base-port-api-tests.yaml b/.github/workflows/e2e-custom-base-port-api-tests.yaml index f1b4c76251..497c1eca6a 100644 --- a/.github/workflows/e2e-custom-base-port-api-tests.yaml +++ b/.github/workflows/e2e-custom-base-port-api-tests.yaml @@ -18,7 +18,7 @@ jobs: NODE_ENV: test STACK_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING: yes STACK_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:6728/stackframe" - NEXT_PUBLIC_STACK_PORT_PREFIX: "67" + NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX: "67" STACK_EXTERNAL_DB_SYNC_MAX_DURATION_MS: "20000" STACK_EXTERNAL_DB_SYNC_DIRECT: "false" diff --git a/.github/workflows/setup-tests-with-custom-base-port.yaml b/.github/workflows/setup-tests-with-custom-base-port.yaml index b6f511ecea..395a60ee6e 100644 --- a/.github/workflows/setup-tests-with-custom-base-port.yaml +++ b/.github/workflows/setup-tests-with-custom-base-port.yaml @@ -19,7 +19,7 @@ jobs: if: ${{ (github.head_ref || github.ref_name) == 'dev' }} runs-on: ubicloud-standard-16 env: - NEXT_PUBLIC_STACK_PORT_PREFIX: "69" + NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX: "69" STACK_EXTERNAL_DB_SYNC_MAX_DURATION_MS: "20000" STACK_EXTERNAL_DB_SYNC_DIRECT: "false" diff --git a/apps/backend/.env.development b/apps/backend/.env.development index 8266efbc2d..ff9ea83dfb 100644 --- a/apps/backend/.env.development +++ b/apps/backend/.env.development @@ -1,6 +1,6 @@ -NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02 -NEXT_PUBLIC_STACK_DASHBOARD_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01 -NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX=.localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}09 +NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02 +NEXT_PUBLIC_STACK_DASHBOARD_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}01 +NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX=.localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09 NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=false STACK_SERVER_SECRET=23-wuNpik0gIW4mruTz25rbIvhuuvZFrLOLtL7J4tyo @@ -17,8 +17,8 @@ STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=this-super-secret-admin-key-is-for-local-development-only -STACK_OAUTH_MOCK_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}14 -STACK_TURNSTILE_SITEVERIFY_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}14/turnstile/siteverify +STACK_OAUTH_MOCK_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}14 +STACK_TURNSTILE_SITEVERIFY_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}14/turnstile/siteverify # Cloudflare Turnstile test keys — always-pass widgets, no real challenges # See https://developers.cloudflare.com/turnstile/troubleshooting/testing/ @@ -49,12 +49,12 @@ STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS=true # apps/backend/src/lib/plan-entitlements.ts:arePlanLimitsEnforced. STACK_DISABLE_PLAN_LIMITS=false -STACK_DATABASE_CONNECTION_STRING=postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}28/stackframe -STACK_DATABASE_REPLICA_CONNECTION_STRING=postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}34/stackframe +STACK_DATABASE_CONNECTION_STRING=postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}28/stackframe +STACK_DATABASE_REPLICA_CONNECTION_STRING=postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}34/stackframe STACK_DATABASE_REPLICATION_WAIT_STRATEGY=pg-stat-replication STACK_EMAIL_HOST=127.0.0.1 -STACK_EMAIL_PORT=${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}29 +STACK_EMAIL_PORT=${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}29 STACK_EMAIL_SECURE=false STACK_EMAIL_USERNAME="does not matter, ignored by Inbucket" STACK_EMAIL_PASSWORD="does not matter, ignored by Inbucket" @@ -64,7 +64,7 @@ STACK_ACCESS_TOKEN_EXPIRATION_TIME=60s STACK_DEFAULT_EMAIL_CAPACITY_PER_HOUR=100000 -STACK_SVIX_SERVER_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}13 +STACK_SVIX_SERVER_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}13 STACK_SVIX_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NTUxNDA2MzksImV4cCI6MTk3MDUwMDYzOSwibmJmIjoxNjU1MTQwNjM5LCJpc3MiOiJzdml4LXNlcnZlciIsInN1YiI6Im9yZ18yM3JiOFlkR3FNVDBxSXpwZ0d3ZFhmSGlyTXUifQ.En8w77ZJWbd0qrMlHHupHUB-4cx17RfzFykseg95SUk # Trusted reverse proxy for reading real client IP addresses. @@ -91,7 +91,7 @@ STACK_EMAIL_MONITOR_PROJECT_ID=internal STACK_EMAIL_MONITOR_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only STACK_EMAIL_MONITOR_RESEND_EMAIL_DOMAIN=stack-generated.example.com STACK_EMAIL_MONITOR_RESEND_EMAIL_API_KEY=this-is-a-fake-key -STACK_EMAIL_MONITOR_INBUCKET_API_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}05 +STACK_EMAIL_MONITOR_INBUCKET_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}05 STACK_EMAIL_MONITOR_USE_INBUCKET=true STACK_EMAIL_MONITOR_SECRET_TOKEN=this-secret-token-is-for-local-development-only @@ -100,7 +100,7 @@ STACK_EMAILABLE_API_KEY= STACK_INTERNAL_FEEDBACK_RECIPIENTS=team@stack-auth.com # S3 Configuration for local development using s3mock -STACK_S3_ENDPOINT=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}21 +STACK_S3_ENDPOINT=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}21 STACK_S3_REGION=us-east-1 STACK_S3_ACCESS_KEY_ID=s3mockroot STACK_S3_SECRET_ACCESS_KEY=s3mockroot @@ -109,23 +109,23 @@ STACK_S3_PRIVATE_BUCKET=stack-storage-private # AWS region defaults to LocalStack STACK_AWS_REGION=us-east-1 -STACK_AWS_KMS_ENDPOINT=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}24 +STACK_AWS_KMS_ENDPOINT=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}24 STACK_AWS_ACCESS_KEY_ID=test STACK_AWS_SECRET_ACCESS_KEY=test # Upstash defaults to one of the pre-build test users of the local emulator -STACK_QSTASH_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}25 +STACK_QSTASH_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}25 STACK_QSTASH_TOKEN=eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0= STACK_QSTASH_CURRENT_SIGNING_KEY=sig_7kYjw48mhY7kAjqNGcy6cr29RJ6r STACK_QSTASH_NEXT_SIGNING_KEY=sig_5ZB6DVzB1wjE8S6rZ7eenA8Pdnhs # MCP review tool (SpacetimeDB) -STACK_SPACETIMEDB_URI=ws://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}39 +STACK_SPACETIMEDB_URI=ws://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}39 STACK_SPACETIMEDB_DB_NAME=stack-auth-llm STACK_MCP_LOG_TOKEN=change-me # Clickhouse -STACK_CLICKHOUSE_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}36 +STACK_CLICKHOUSE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}36 STACK_CLICKHOUSE_ADMIN_USER=stackframe STACK_CLICKHOUSE_ADMIN_PASSWORD=PASSWORD-PLACEHOLDER--9gKyMxJeMx STACK_CLICKHOUSE_EXTERNAL_PASSWORD=PASSWORD-PLACEHOLDER--EZeHscBMzE diff --git a/apps/backend/package.json b/apps/backend/package.json index e42cd0cf9c..9205c1096a 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -11,14 +11,14 @@ "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_STACK_PORT_PREFIX:-81}10} && BACKEND_PORT=${BACKEND_PORT:-${NEXT_PUBLIC_STACK_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": "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 && 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", "analyze-bundle": "next experimental-analyze", - "start": "next start --port ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02", + "start": "next start --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02", "codegen-prisma": "STACK_DATABASE_CONNECTION_STRING=\"${STACK_DATABASE_CONNECTION_STRING:-placeholder-database-connection-string}\" pnpm run prisma generate", "codegen-prisma:watch": "STACK_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", @@ -28,9 +28,9 @@ "codegen": "pnpm run with-env pnpm run generate-migration-imports && pnpm run with-env bash -c 'if [ \"$STACK_ACCELERATE_ENABLED\" = \"true\" ]; then pnpm run prisma generate --no-engine; else pnpm run codegen-prisma; fi' && pnpm run generate-private-sign-up-risk-engine && pnpm run codegen-docs && pnpm run codegen-route-info", "codegen:watch": "pnpm run generate-private-sign-up-risk-engine && concurrently -n \"prisma,private-risk-engine,docs,route-info,migration-imports\" -k \"pnpm run codegen-prisma:watch\" \"pnpm run generate-private-sign-up-risk-engine:watch\" \"pnpm run codegen-docs:watch\" \"pnpm run codegen-route-info:watch\" \"pnpm run generate-migration-imports:watch\"", "psql-inner": "psql $(echo $STACK_DATABASE_CONNECTION_STRING | sed 's/\\?.*$//')", - "clickhouse": "pnpm run with-env clickhouse-client --host localhost --port ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}37 --user stackframe --password PASSWORD-PLACEHOLDER--9gKyMxJeMx", + "clickhouse": "pnpm run with-env clickhouse-client --host localhost --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}37 --user stackframe --password PASSWORD-PLACEHOLDER--9gKyMxJeMx", "psql": "pnpm run with-env:dev pnpm run psql-inner", - "prisma-studio": "pnpm run with-env:dev prisma studio --port ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}06 --browser none", + "prisma-studio": "pnpm run with-env:dev prisma studio --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}06 --browser none", "prisma:dev": "pnpm run with-env:dev prisma", "prisma": "pnpm run with-env prisma", "db:migration-gen": "pnpm run with-env:dev tsx scripts/db-migrations.ts generate-migration-file", diff --git a/apps/backend/scripts/backfill-internal-free-plans.ts b/apps/backend/scripts/backfill-internal-free-plans.ts index 89cbbf3fc9..c0cc4a866d 100644 --- a/apps/backend/scripts/backfill-internal-free-plans.ts +++ b/apps/backend/scripts/backfill-internal-free-plans.ts @@ -17,7 +17,7 @@ import { ensureFreePlanForBillingTeam } from "@/lib/payments/ensure-free-plan"; // eslint-disable-next-line @typescript-eslint/no-deprecated -- idiomatic way to get the internal tenancy today (see plan-entitlements.ts) import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch, type Tenancy } from "@/lib/tenancies"; import { globalPrismaClient } from "@/prisma-client"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { getOrUndefined } from "@stackframe/stack-shared/dist/utils/objects"; // Page size for streaming teams. Big enough to amortise round-trips, @@ -64,7 +64,7 @@ export async function runBackfillInternalFreePlans(): Promise<{ log("Starting..."); const internalTenancy = await getSoleTenancyFromProjectBranch("internal", DEFAULT_BRANCH_ID, true); if (internalTenancy == null) { - throw new StackAssertionError("Internal billing tenancy not found", { + throw new HexclaveAssertionError("Internal billing tenancy not found", { billingProjectId: "internal", branchId: DEFAULT_BRANCH_ID, }); @@ -79,7 +79,7 @@ export async function runBackfillInternalFreePlans(): Promise<{ || freePlanProduct.customerType !== "team" || freePlanProduct.productLineId == null ) { - throw new StackAssertionError( + throw new HexclaveAssertionError( "Internal tenancy `free` product is not configured as a team-typed, product-line-tagged plan; cannot run backfill", { freePlanProduct }, ); diff --git a/apps/backend/scripts/regen-internal-subscriptions-to-latest.ts b/apps/backend/scripts/regen-internal-subscriptions-to-latest.ts index 5bf6dfc24f..757a406b90 100644 --- a/apps/backend/scripts/regen-internal-subscriptions-to-latest.ts +++ b/apps/backend/scripts/regen-internal-subscriptions-to-latest.ts @@ -22,7 +22,7 @@ import { getStripeForAccount } from "@/lib/stripe"; // eslint-disable-next-line @typescript-eslint/no-deprecated -- idiomatic way to get the internal tenancy today (see plan-entitlements.ts) import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch, type Tenancy } from "@/lib/tenancies"; import { getPrismaClientForTenancy, globalPrismaClient, retryTransaction } from "@/prisma-client"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { getOrUndefined } from "@stackframe/stack-shared/dist/utils/objects"; import type Stripe from "stripe"; @@ -136,7 +136,7 @@ export async function runRegenInternalSubscriptionsToLatest(options: { log("Starting..."); const internalTenancy = await getSoleTenancyFromProjectBranch("internal", DEFAULT_BRANCH_ID, true); if (internalTenancy == null) { - throw new StackAssertionError("Internal billing tenancy not found", { + throw new HexclaveAssertionError("Internal billing tenancy not found", { billingProjectId: "internal", branchId: DEFAULT_BRANCH_ID, }); @@ -242,7 +242,7 @@ export async function regenSingleSubscription(args: { const isStripeBacked = needsStripeMetadataRebase(sub); if (isStripeBacked && stripe == null) { - throw new StackAssertionError( + throw new HexclaveAssertionError( "regenSingleSubscription called for Stripe-backed sub without a stripe client", { subId: sub.id, stripeSubscriptionId: sub.stripeSubscriptionId, creationSource: sub.creationSource }, ); diff --git a/apps/backend/scripts/run-bulldozer-studio.ts b/apps/backend/scripts/run-bulldozer-studio.ts index ec2392f37c..943cf33989 100644 --- a/apps/backend/scripts/run-bulldozer-studio.ts +++ b/apps/backend/scripts/run-bulldozer-studio.ts @@ -1,5 +1,5 @@ import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { deindent, stringCompare } from "@stackframe/stack-shared/dist/utils/strings"; import ELK from "elkjs/lib/elk.bundled.js"; import http from "node:http"; @@ -111,7 +111,7 @@ type StudioTableRecord = { table: StudioTable, }; -const STUDIO_PORT = Number(`${getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81")}40`); +const STUDIO_PORT = Number(`${getEnvVariable("NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX", "81")}40`); const STUDIO_HOST = "127.0.0.1"; const BULLDOZER_LOCK_ID = 7857391; const STUDIO_INSTANCE_ID = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; @@ -153,18 +153,18 @@ function isStudioStoredTable(value: StudioTable): value is StudioStoredTable { } function requireRecord(value: unknown, errorMessage: string): Record { - if (!isRecord(value)) throw new StackAssertionError(errorMessage); + if (!isRecord(value)) throw new HexclaveAssertionError(errorMessage); return value; } function requireString(value: unknown, errorMessage: string): string { - if (typeof value !== "string") throw new StackAssertionError(errorMessage); + if (typeof value !== "string") throw new HexclaveAssertionError(errorMessage); return value; } function requireStringArray(value: unknown, errorMessage: string): string[] { if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) { - throw new StackAssertionError(errorMessage); + throw new HexclaveAssertionError(errorMessage); } return value; } @@ -189,7 +189,7 @@ function isJsonValue(value: unknown): value is JsonValue { function requireJsonValue(value: unknown, errorMessage: string): JsonValue { if (!isJsonValue(value)) { - throw new StackAssertionError(errorMessage); + throw new HexclaveAssertionError(errorMessage); } return value; } @@ -500,7 +500,7 @@ function createTableRegistry(schema: Record): { walk(schema, ""); if (tables.length === 0) { - throw new StackAssertionError("No studio-compatible tables found in schema object."); + throw new HexclaveAssertionError("No studio-compatible tables found in schema object."); } const categories: CategoryRecord[] = []; @@ -536,7 +536,7 @@ let registry = createTableRegistry( function switchSchema(name: string): void { const factory = Reflect.get(AVAILABLE_SCHEMAS, name); if (typeof factory !== "function") { - throw new StackAssertionError(`Unknown schema "${name}". Available: ${Object.keys(AVAILABLE_SCHEMAS).join(", ")}`); + throw new HexclaveAssertionError(`Unknown schema "${name}". Available: ${Object.keys(AVAILABLE_SCHEMAS).join(", ")}`); } currentSchemaName = name; registry = createTableRegistry(factory()); @@ -886,7 +886,7 @@ async function queryRows(query: SqlQuery): Promise { const rows = await retryTransaction(globalPrismaClient, async (tx) => { return await tx.$queryRawUnsafe(toQueryableSqlQuery(query)); }); - if (!Array.isArray(rows)) throw new StackAssertionError("Expected SQL query to return an array of rows."); + if (!Array.isArray(rows)) throw new HexclaveAssertionError("Expected SQL query to return an array of rows."); return rows; } @@ -895,7 +895,7 @@ async function readBoolean(expression: SqlExpression): Promise return await tx.$queryRawUnsafe>>(`SELECT (${expression.sql}) AS "value"`); }); if (!Array.isArray(rows) || rows.length === 0 || !isRecord(rows[0])) { - throw new StackAssertionError("Expected boolean expression query to return one row."); + throw new HexclaveAssertionError("Expected boolean expression query to return one row."); } return Reflect.get(rows[0], "value") === true; } @@ -1256,7 +1256,7 @@ async function readRequestBody(request: http.IncomingMessage): Promise { const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); totalBytes += chunkBuffer.byteLength; if (totalBytes > MAX_REQUEST_BODY_BYTES) { - throw new StackAssertionError("Request body exceeds maximum size.", { + throw new HexclaveAssertionError("Request body exceeds maximum size.", { maxRequestBodyBytes: MAX_REQUEST_BODY_BYTES, receivedBytes: totalBytes, }); @@ -1299,7 +1299,7 @@ function requireAuthorizedMutationRequest(request: http.IncomingMessage, request const authHeader = request.headers[STUDIO_AUTH_HEADER]; const token = typeof authHeader === "string" ? authHeader : null; if (token !== STUDIO_AUTH_TOKEN) { - throw new StackAssertionError("Invalid or missing studio mutation token."); + throw new HexclaveAssertionError("Invalid or missing studio mutation token."); } const originHeader = request.headers.origin; @@ -1308,7 +1308,7 @@ function requireAuthorizedMutationRequest(request: http.IncomingMessage, request try { originUrl = new URL(originHeader); } catch { - throw new StackAssertionError("Mutation origin is not allowed.", { + throw new HexclaveAssertionError("Mutation origin is not allowed.", { origin: originHeader, path: requestUrl.pathname, }); @@ -1321,7 +1321,7 @@ function requireAuthorizedMutationRequest(request: http.IncomingMessage, request || hostname === "::1" || hostname.endsWith(".localhost"); if (!portMatches || !hostnameAllowed) { - throw new StackAssertionError("Mutation origin is not allowed.", { + throw new HexclaveAssertionError("Mutation origin is not allowed.", { origin: originHeader, path: requestUrl.pathname, }); @@ -4151,7 +4151,7 @@ function getStudioPageHtml(): string { async function handleRequest(request: http.IncomingMessage, response: http.ServerResponse): Promise { if (!isLoopbackAddress(request.socket.remoteAddress)) { - throw new StackAssertionError("Bulldozer Studio only accepts loopback requests.", { + throw new HexclaveAssertionError("Bulldozer Studio only accepts loopback requests.", { remoteAddress: request.socket.remoteAddress, }); } @@ -4252,7 +4252,7 @@ async function handleRequest(request: http.IncomingMessage, response: http.Serve const rowIdentifier = requireString(Reflect.get(body, "rowIdentifier"), "rowIdentifier must be a string."); const rowData = requireJsonValue(Reflect.get(body, "rowData"), "rowData must be valid JSON."); if (!isRecord(rowData)) { - throw new StackAssertionError("rowData must be a JSON object."); + throw new HexclaveAssertionError("rowData must be a JSON object."); } const executionContext = createBulldozerExecutionContext(); const metrics = await executeStatements(record.table.setRow( @@ -4326,7 +4326,7 @@ async function handleRequest(request: http.IncomingMessage, response: http.Serve pathSegments.length === 0 || (pathSegments.length === 1 && pathSegments[0] === "table") ) { - throw new StackAssertionError("Deleting reserved root paths is not allowed."); + throw new HexclaveAssertionError("Deleting reserved root paths is not allowed."); } await retryTransaction(globalPrismaClient, async (tx) => { await tx.$executeRawUnsafe(`SET LOCAL jit = off`); diff --git a/apps/backend/scripts/run-cron-jobs.ts b/apps/backend/scripts/run-cron-jobs.ts index 0eea97a0ff..8cf1de6c31 100644 --- a/apps/backend/scripts/run-cron-jobs.ts +++ b/apps/backend/scripts/run-cron-jobs.ts @@ -1,5 +1,5 @@ import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { captureError, StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { captureError, HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { runAsynchronously, wait } from "@stackframe/stack-shared/dist/utils/promises"; import { Result } from "@stackframe/stack-shared/dist/utils/results"; @@ -18,14 +18,14 @@ async function main() { console.log("Starting cron jobs..."); const cronSecret = getEnvVariable('CRON_SECRET'); - const baseUrl = `http://localhost:${getEnvVariable('NEXT_PUBLIC_STACK_PORT_PREFIX', '81')}02`; + const baseUrl = `http://localhost:${getEnvVariable('NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX', '81')}02`; const run = async (endpoint: string) => { console.log(`Running ${endpoint}...`); const res = await fetch(`${baseUrl}${endpoint}`, { headers: { 'Authorization': `Bearer ${cronSecret}` }, }); - if (!res.ok) throw new StackAssertionError(`Failed to call ${endpoint}: ${res.status} ${res.statusText}\n${await res.text()}`, { res }); + if (!res.ok) throw new HexclaveAssertionError(`Failed to call ${endpoint}: ${res.status} ${res.statusText}\n${await res.text()}`, { res }); console.log(`${endpoint} completed.`); }; diff --git a/apps/backend/scripts/run-email-queue.ts b/apps/backend/scripts/run-email-queue.ts index de68726962..f08e85a31a 100644 --- a/apps/backend/scripts/run-email-queue.ts +++ b/apps/backend/scripts/run-email-queue.ts @@ -1,12 +1,12 @@ import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { runAsynchronously, wait } from "@stackframe/stack-shared/dist/utils/promises"; async function main() { console.log("Starting email queue processor..."); const cronSecret = getEnvVariable('CRON_SECRET'); - const baseUrl = `http://localhost:${getEnvVariable('NEXT_PUBLIC_STACK_PORT_PREFIX', '81')}02`; + const baseUrl = `http://localhost:${getEnvVariable('NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX', '81')}02`; // Wait a few seconds to make sure the server is fully started await wait(5_000); @@ -21,7 +21,7 @@ async function main() { method: "GET", headers: { 'Authorization': `Bearer ${cronSecret}` }, }); - if (!res.ok) throw new StackAssertionError(`Failed to call email queue step: ${res.status} ${res.statusText}\n${await res.text()}`, { res }); + if (!res.ok) throw new HexclaveAssertionError(`Failed to call email queue step: ${res.status} ${res.statusText}\n${await res.text()}`, { res }); console.log("Email queue step completed."); const endTime = performance.now(); diff --git a/apps/backend/scripts/verify-data-integrity/api.ts b/apps/backend/scripts/verify-data-integrity/api.ts index ad5f5945c8..27ca31ebd8 100644 --- a/apps/backend/scripts/verify-data-integrity/api.ts +++ b/apps/backend/scripts/verify-data-integrity/api.ts @@ -1,6 +1,6 @@ import fs from "fs"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { deepPlainEquals, filterUndefined } from "@stackframe/stack-shared/dist/utils/objects"; import { deindent } from "@stackframe/stack-shared/dist/utils/strings"; @@ -76,19 +76,19 @@ export function createApiHelpers(options: { if (targetOutputData) { const targetEndpointOutputs = targetOutputData.get(endpoint); if (!targetEndpointOutputs) { - throw new StackAssertionError(deindent` + throw new HexclaveAssertionError(deindent` Output data mismatch for endpoint ${endpoint}: Expected ${endpoint} to be in targetOutputData, but it is not. `, { endpoint }); } if (targetEndpointOutputs.length < count) { - throw new StackAssertionError(deindent` + throw new HexclaveAssertionError(deindent` Output data mismatch for endpoint ${endpoint}: Expected ${targetEndpointOutputs.length} outputs but got at least ${count}. `, { endpoint }); } if (!(deepPlainEquals(targetEndpointOutputs[count - 1], output))) { - throw new StackAssertionError(deindent` + throw new HexclaveAssertionError(deindent` Output data mismatch for endpoint ${endpoint}: Expected output[${JSON.stringify(endpoint)}][${count - 1}] to be: ${JSON.stringify(targetEndpointOutputs[count - 1], null, 2)} @@ -108,7 +108,7 @@ export function createApiHelpers(options: { for (const [endpoint, expectedOutputs] of targetOutputData) { const actualCount = outputCountByEndpoint.get(endpoint) ?? 0; if (actualCount !== expectedOutputs.length) { - throw new StackAssertionError(deindent` + throw new HexclaveAssertionError(deindent` Output data mismatch for endpoint ${endpoint}: Expected ${expectedOutputs.length} outputs but got ${actualCount}. `, { endpoint, expectedCount: expectedOutputs.length, actualCount }); @@ -136,7 +136,7 @@ export function createApiHelpers(options: { const responseText = await response.text(); if (response.status !== expectedStatusCode) { - throw new StackAssertionError(deindent` + throw new HexclaveAssertionError(deindent` Expected status code ${expectedStatusCode} but got ${response.status} for ${endpoint}: ${responseText} diff --git a/apps/backend/scripts/verify-data-integrity/clickhouse-sync-verifier.ts b/apps/backend/scripts/verify-data-integrity/clickhouse-sync-verifier.ts index dfa48f8707..08bb316814 100644 --- a/apps/backend/scripts/verify-data-integrity/clickhouse-sync-verifier.ts +++ b/apps/backend/scripts/verify-data-integrity/clickhouse-sync-verifier.ts @@ -3,7 +3,7 @@ import { CLICKHOUSE_COLUMN_NORMALIZERS } from "@/lib/external-db-sync"; import type { Tenancy } from "@/lib/tenancies"; import { getPrismaClientForTenancy } from "@/prisma-client"; import { DEFAULT_DB_SYNC_MAPPINGS } from "@stackframe/stack-shared/dist/config/db-sync-mappings"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { deindent } from "@stackframe/stack-shared/dist/utils/strings"; import type { RecurseFunction } from "./recurse"; @@ -153,7 +153,7 @@ export async function verifyClickhouseSync(options: { if (!fetchQuery) return; if (!(mappingName in SORT_KEYS)) { - throw new StackAssertionError(`No sort keys defined for mapping ${mappingName}`); + throw new HexclaveAssertionError(`No sort keys defined for mapping ${mappingName}`); } const sortKeys = SORT_KEYS[mappingName as keyof typeof SORT_KEYS]; @@ -210,7 +210,7 @@ export async function verifyClickhouseSync(options: { // Compare row counts if (pgRows.length !== chRows.length) { - throw new StackAssertionError(deindent` + throw new HexclaveAssertionError(deindent` ClickHouse sync row count mismatch for ${mappingName}. Postgres: ${pgRows.length} rows, ClickHouse: ${chRows.length} rows. `); @@ -231,7 +231,7 @@ export async function verifyClickhouseSync(options: { if (!deepEqual(normalizedPg, normalizedCh)) { const diffs = findDifferences(normalizedPg, normalizedCh); const keyValues = fullSortKeys.map(k => `${k}=${pgRows[i][k]}`).join(", "); - throw new StackAssertionError(deindent` + throw new HexclaveAssertionError(deindent` ClickHouse sync data mismatch for ${mappingName} at row ${keyValues}. Differences: ${diffs.join("; ")} `); diff --git a/apps/backend/scripts/verify-data-integrity/index.ts b/apps/backend/scripts/verify-data-integrity/index.ts index a4186b0239..72050e64ad 100644 --- a/apps/backend/scripts/verify-data-integrity/index.ts +++ b/apps/backend/scripts/verify-data-integrity/index.ts @@ -6,7 +6,7 @@ import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch } from "@/lib/tenanc import { getPrismaClientForTenancy, globalPrismaClient } from "@/prisma-client"; import type { OrganizationRenderedConfig } from "@stackframe/stack-shared/dist/config/schema"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { omit } from "@stackframe/stack-shared/dist/utils/objects"; import { wait } from "@stackframe/stack-shared/dist/utils/promises"; import { deindent } from "@stackframe/stack-shared/dist/utils/strings"; @@ -179,7 +179,7 @@ async function main() { await recurse(`[bulldozer table] ${label}`, async () => { const errors = await prismaClient.$queryRawUnsafe(toQueryableSqlQuery(table.verifyDataIntegrity(executionContext))); if (errors.length > 0) { - throw new StackAssertionError(deindent` + throw new HexclaveAssertionError(deindent` Bulldozer data integrity violation in table ${label}: found ${errors.length} error row(s). `, { errors }); } @@ -315,7 +315,7 @@ async function main() { // `any` because these endpoint response types aren't imported here, // and this script is intentionally tolerant of response shape changes. if (!projectPermissionDefinitions.items.some((p: any) => p.id === projectPermission.id)) { - throw new StackAssertionError(deindent` + throw new HexclaveAssertionError(deindent` Project permission ${projectPermission.id} not found in project permission definitions. `); } @@ -344,7 +344,7 @@ async function main() { // `any` because these endpoint response types aren't imported here, // and this script is intentionally tolerant of response shape changes. if (!teamPermissionDefinitions.items.some((p: any) => p.id === teamPermission.id)) { - throw new StackAssertionError(deindent` + throw new HexclaveAssertionError(deindent` Team permission ${teamPermission.id} not found in team permission definitions. `); } diff --git a/apps/backend/scripts/verify-data-integrity/payments-verifier.ts b/apps/backend/scripts/verify-data-integrity/payments-verifier.ts index e70901eed5..baabc55148 100644 --- a/apps/backend/scripts/verify-data-integrity/payments-verifier.ts +++ b/apps/backend/scripts/verify-data-integrity/payments-verifier.ts @@ -6,7 +6,7 @@ import type { OrganizationRenderedConfig } from "@stackframe/stack-shared/dist/c import type { TransactionEntry } from "@stackframe/stack-shared/dist/interface/crud/transactions"; import { FAR_FUTURE_DATE, addInterval, getIntervalsElapsed, type DayInterval } from "@stackframe/stack-shared/dist/utils/dates"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { deepPlainEquals } from "@stackframe/stack-shared/dist/utils/objects"; import { deindent, stringCompare, typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings"; import { urlString } from "@stackframe/stack-shared/dist/utils/urls"; @@ -603,7 +603,7 @@ export async function createPaymentsVerifier(options: { customerType: customer.customerType, }); if (dbQuantity !== response.quantity) { - throw new StackAssertionError(deindent` + throw new HexclaveAssertionError(deindent` Item quantity mismatch for ${customer.customerType} ${customer.customerId} item ${itemId}. Expected ${expectedQuantity} but got ${response.quantity}. `, { expectedQuantity, actualQuantity: response.quantity, dbQuantity }); @@ -631,7 +631,7 @@ export async function createPaymentsVerifier(options: { const normalizedActual = normalizeOwnedProducts(actualProducts); if (!deepPlainEquals(normalizedExpected, normalizedActual)) { - throw new StackAssertionError(deindent` + throw new HexclaveAssertionError(deindent` Owned products mismatch for ${customer.customerType} ${customer.customerId}. Expected: ${JSON.stringify(normalizedExpected, null, 2)} diff --git a/apps/backend/scripts/verify-data-integrity/stripe-payout-integrity.ts b/apps/backend/scripts/verify-data-integrity/stripe-payout-integrity.ts index 674eb65726..70e09f2625 100644 --- a/apps/backend/scripts/verify-data-integrity/stripe-payout-integrity.ts +++ b/apps/backend/scripts/verify-data-integrity/stripe-payout-integrity.ts @@ -2,7 +2,7 @@ import type { Tenancy } from "@/lib/tenancies"; import { getStripeForAccount } from "@/lib/stripe"; import type { Transaction } from "@stackframe/stack-shared/dist/interface/crud/transactions"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { deindent } from "@stackframe/stack-shared/dist/utils/strings"; import { urlString } from "@stackframe/stack-shared/dist/utils/urls"; @@ -37,7 +37,7 @@ export async function fetchAllTransactionsForProject(options: { function parseMoneyAmountToMinorUnits(amount: string, decimals: number): bigint { const [wholePart, fractionalPart = ""] = amount.split("."); if (fractionalPart.length > decimals) { - throw new StackAssertionError("Money amount has too many decimals", { amount, decimals }); + throw new HexclaveAssertionError("Money amount has too many decimals", { amount, decimals }); } const paddedFraction = fractionalPart.padEnd(decimals, "0"); return BigInt(`${wholePart}${paddedFraction}`); @@ -130,7 +130,7 @@ export async function verifyStripePayoutIntegrity(options: { stripeAccountId: options.stripeAccountId, }); if (moneyTransferTotalUsdMinor !== stripeBalanceTransactionTotalUsdMinor) { - throw new StackAssertionError(deindent` + throw new HexclaveAssertionError(deindent` Stripe balance transaction mismatch for project ${options.projectId}. Money transfers total USD ${formatMinorUnitsToMoneyString(moneyTransferTotalUsdMinor, 2)} vs Stripe balance transactions USD ${formatMinorUnitsToMoneyString(stripeBalanceTransactionTotalUsdMinor, 2)}. `, { diff --git a/apps/backend/src/app/api/latest/(api-keys)/handlers.tsx b/apps/backend/src/app/api/latest/(api-keys)/handlers.tsx index 7fe624ce96..53ea28f464 100644 --- a/apps/backend/src/app/api/latest/(api-keys)/handlers.tsx +++ b/apps/backend/src/app/api/latest/(api-keys)/handlers.tsx @@ -9,7 +9,7 @@ import { KnownErrors } from "@stackframe/stack-shared"; import { TeamApiKeysCrud, UserApiKeysCrud, teamApiKeysCreateInputSchema, teamApiKeysCreateOutputSchema, teamApiKeysCrud, userApiKeysCreateInputSchema, userApiKeysCreateOutputSchema, userApiKeysCrud } from "@stackframe/stack-shared/dist/interface/crud/project-api-keys"; import { adaptSchema, clientOrHigherAuthTypeSchema, serverOrHigherAuthTypeSchema, userIdOrMeSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; import { createProjectApiKey } from "@stackframe/stack-shared/dist/utils/api-keys"; -import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids"; @@ -114,14 +114,14 @@ async function prismaToCrud(prisma: ProjectApiKey, | TeamApiKeysCrud["Admin"]["Read"] > { if ((prisma.projectUserId == null) === (prisma.teamId == null)) { - throw new StackAssertionError("Exactly one of projectUserId or teamId must be set", { prisma }); + throw new HexclaveAssertionError("Exactly one of projectUserId or teamId must be set", { prisma }); } if (type === "user" && prisma.projectUserId == null) { - throw new StackAssertionError("projectUserId must be set for user API keys", { prisma }); + throw new HexclaveAssertionError("projectUserId must be set for user API keys", { prisma }); } if (type === "team" && prisma.teamId == null) { - throw new StackAssertionError("teamId must be set for team API keys", { prisma }); + throw new HexclaveAssertionError("teamId must be set for team API keys", { prisma }); } return { @@ -184,7 +184,7 @@ function createApiKeyHandlers(type: Type) { /* const userPrefix = body.prefix ?? (isPublic ? "pk" : "sk"); if (!userPrefix.match(/^[a-zA-Z0-9_]+$/)) { - throw new StackAssertionError("userPrefix must contain only alphanumeric characters and underscores. This is so we can register the API key with security scanners. This should've been checked in the creation schema"); + throw new HexclaveAssertionError("userPrefix must contain only alphanumeric characters and underscores. This is so we can register the API key with security scanners. This should've been checked in the creation schema"); } */ const isCloudVersion = new URL(url).hostname === "api.stack-auth.com"; // we only want to enable secret scanning on the cloud version diff --git a/apps/backend/src/app/api/latest/auth/mfa/sign-in/verification-code-handler.tsx b/apps/backend/src/app/api/latest/auth/mfa/sign-in/verification-code-handler.tsx index 798e0a76e7..9b5ee075eb 100644 --- a/apps/backend/src/app/api/latest/auth/mfa/sign-in/verification-code-handler.tsx +++ b/apps/backend/src/app/api/latest/auth/mfa/sign-in/verification-code-handler.tsx @@ -6,7 +6,7 @@ import { VerificationCodeType } from "@/generated/prisma/client"; import { KnownErrors } from "@stackframe/stack-shared"; import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; import { signInResponseSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; export const mfaVerificationCodeHandler = createVerificationCodeHandler({ metadata: { @@ -48,7 +48,7 @@ export const mfaVerificationCodeHandler = createVerificationCodeHandler({ }); const totpSecret = user.totpSecret; if (!totpSecret) { - throw new StackAssertionError("User does not have a TOTP secret", { user }); + throw new HexclaveAssertionError("User does not have a TOTP secret", { user }); } const isTotpValid = verifyTOTP(totpSecret, 30, 6, body.totp); if (!isTotpValid) { diff --git a/apps/backend/src/app/api/latest/auth/oauth/authorize/[provider_id]/route.tsx b/apps/backend/src/app/api/latest/auth/oauth/authorize/[provider_id]/route.tsx index a7d6cacea9..cf39219596 100644 --- a/apps/backend/src/app/api/latest/auth/oauth/authorize/[provider_id]/route.tsx +++ b/apps/backend/src/app/api/latest/auth/oauth/authorize/[provider_id]/route.tsx @@ -39,7 +39,12 @@ export const GET = createSmartRouteHandler({ error_redirect_url: urlSchema.optional().meta({ openapiField: { hidden: true } }), error_redirect_uri: urlSchema.optional(), after_callback_redirect_url: urlSchema.optional(), - stack_response_mode: yupString().oneOf(["json", "redirect"]).default("redirect"), + // Hexclave rebrand: the SDK now emits `hexclave_response_mode`; the legacy + // `stack_response_mode` name is still accepted. Neither carries a yup default + // so the handler can tell "neither set" apart from an explicit value and only + // then fall back to "redirect". + stack_response_mode: yupString().oneOf(["json", "redirect"]).optional(), + hexclave_response_mode: yupString().oneOf(["json", "redirect"]).optional(), ...botChallengeFlowRequestSchemaFields, // oauth parameters @@ -56,7 +61,7 @@ export const GET = createSmartRouteHandler({ }), response: yupUnion( yupObject({ - // The SDK uses stack_response_mode=json so it can intercept bot challenges before navigating. + // The SDK uses hexclave_response_mode=json (legacy: stack_response_mode=json) so it can intercept bot challenges before navigating. // The redirect path (default) is the legacy browser-direct flow. statusCode: yupNumber().oneOf([200]).defined(), bodyType: yupString().oneOf(["json"]).defined(), @@ -129,6 +134,9 @@ export const GET = createSmartRouteHandler({ const innerCodeVerifier = generators.codeVerifier(); const innerState = generators.state(); + // Hexclave rebrand: prefer the new query param name, accept the legacy one, + // and only fall back to "redirect" when neither was provided. + const responseMode = query.hexclave_response_mode ?? query.stack_response_mode ?? "redirect"; const providerObj = await getProvider(provider); const oauthUrl = providerObj.getAuthorizationUrl({ codeVerifier: innerCodeVerifier, @@ -157,13 +165,13 @@ export const GET = createSmartRouteHandler({ afterCallbackRedirectUrl: query.after_callback_redirect_url, turnstileResult: turnstileAssessment.status, turnstileVisibleChallengeResult: turnstileAssessment.visibleChallengeResult, - responseMode: query.stack_response_mode, + responseMode, } satisfies InferType, expiresAt: new Date(Date.now() + 1000 * 60 * outerOAuthFlowExpirationInMinutes), }, }); - if (query.stack_response_mode === "json") { + if (responseMode === "json") { // In JSON mode the client controls the flow programmatically and PKCE // already prevents CSRF, so we skip the cookie (which would require // credentials: "include" and a non-wildcard CORS origin). diff --git a/apps/backend/src/app/api/latest/auth/oauth/callback/[provider_id]/route.tsx b/apps/backend/src/app/api/latest/auth/oauth/callback/[provider_id]/route.tsx index 9f3db0ec17..d6195fada0 100644 --- a/apps/backend/src/app/api/latest/auth/oauth/callback/[provider_id]/route.tsx +++ b/apps/backend/src/app/api/latest/auth/oauth/callback/[provider_id]/route.tsx @@ -12,7 +12,7 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { InvalidClientError, InvalidScopeError, Request as OAuthRequest, Response as OAuthResponse } from "@node-oauth/oauth2-server"; import { KnownError, KnownErrors } from "@stackframe/stack-shared"; import { yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; import { deindent, extractScopes } from "@stackframe/stack-shared/dist/utils/strings"; import { cookies } from "next/headers"; import { redirect } from "next/navigation"; @@ -110,14 +110,18 @@ const handler = createSmartRouteHandler({ try { outerInfo = await oauthCookieSchema.validate(outerInfoDB.info); } catch (error) { - throw new StackAssertionError("Invalid outer info"); + throw new HexclaveAssertionError("Invalid outer info"); } // JSON-mode requests use PKCE for CSRF protection and don't set a cookie. // Only check the CSRF cookie for browser-redirect mode requests. if (outerInfo.responseMode !== 'json') { - const cookieInfo = (await cookies()).get("stack-oauth-inner-" + innerState); - (await cookies()).delete("stack-oauth-inner-" + innerState); + // Hexclave rebrand: read whichever inner-OAuth cookie name is present (prefer the new name), and delete both. + const cookieStore = await cookies(); + const cookieInfo = cookieStore.get("hexclave-oauth-inner-" + innerState) + ?? cookieStore.get("stack-oauth-inner-" + innerState); + cookieStore.delete("hexclave-oauth-inner-" + innerState); + cookieStore.delete("stack-oauth-inner-" + innerState); if (cookieInfo?.value !== 'true') { throw new StatusError(StatusError.BadRequest, "Inner OAuth cookie not found. This is likely because you refreshed the page during the OAuth sign in process. Please try signing in again"); @@ -137,7 +141,7 @@ const handler = createSmartRouteHandler({ const tenancy = await getTenancy(tenancyId); if (!tenancy) { - throw new StackAssertionError("Tenancy in outerInfo not found; has it been deleted?", { tenancyId }); + throw new HexclaveAssertionError("Tenancy in outerInfo not found; has it been deleted?", { tenancyId }); } const prisma = await getPrismaClientForTenancy(tenancy); @@ -183,7 +187,7 @@ const handler = createSmartRouteHandler({ if (type === "link") { if (!projectUserId) { - throw new StackAssertionError("projectUserId not found in cookie when authorizing signed in user"); + throw new HexclaveAssertionError("projectUserId not found in cookie when authorizing signed in user"); } const user = await prisma.projectUser.findUnique({ @@ -198,7 +202,7 @@ const handler = createSmartRouteHandler({ } }); if (!user) { - throw new StackAssertionError("User not found"); + throw new HexclaveAssertionError("User not found"); } } @@ -263,7 +267,7 @@ const handler = createSmartRouteHandler({ // This flow is when a signed-in user wants to connect an OAuth account if (type === "link") { if (!projectUserId) { - throw new StackAssertionError("projectUserId not found in cookie when authorizing signed in user"); + throw new HexclaveAssertionError("projectUserId not found in cookie when authorizing signed in user"); } if (oldAccount) { @@ -403,7 +407,7 @@ const handler = createSmartRouteHandler({ // which scopes are being requested, and by whom? // I think this is a bug in the client? But just to be safe, let's log an error to make sure that it is not our fault // TODO: remove the captureError once you see in production that our own clients never trigger this - captureError("outer-oauth-callback-invalid-scope", new StackAssertionError(deindent` + captureError("outer-oauth-callback-invalid-scope", new HexclaveAssertionError(deindent` A client requested an invalid scope. Is this a bug in the client, or our fault? Scopes requested: ${oauthRequest.query?.scope} diff --git a/apps/backend/src/app/api/latest/auth/oauth/cross-domain/authorize/route.tsx b/apps/backend/src/app/api/latest/auth/oauth/cross-domain/authorize/route.tsx index 972bd52358..bd9ad25f7b 100644 --- a/apps/backend/src/app/api/latest/auth/oauth/cross-domain/authorize/route.tsx +++ b/apps/backend/src/app/api/latest/auth/oauth/cross-domain/authorize/route.tsx @@ -7,7 +7,7 @@ import { globalPrismaClient } from "@/prisma-client"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { KnownErrors } from "@stackframe/stack-shared"; import { adaptSchema, clientOrHigherAuthTypeSchema, urlSchema, yupNumber, yupObject, yupString, yupTuple } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { publishableClientKeyNotNecessarySentinel } from "@stackframe/stack-shared/dist/utils/oauth"; import { InvalidClientError, InvalidScopeError, Request as OAuthRequest, Response as OAuthResponse } from "@node-oauth/oauth2-server"; @@ -86,7 +86,7 @@ export async function createCrossDomainAuthorizeRedirect(options: { const redirectUrl = oauthResponse.headers?.location; if (typeof redirectUrl !== "string") { - throw new StackAssertionError("Cross-domain authorization response is missing redirect location", { + throw new HexclaveAssertionError("Cross-domain authorization response is missing redirect location", { oauthResponse, }); } diff --git a/apps/backend/src/app/api/latest/auth/oauth/oauth-helpers.tsx b/apps/backend/src/app/api/latest/auth/oauth/oauth-helpers.tsx index 9499eee603..4c4bf61308 100644 --- a/apps/backend/src/app/api/latest/auth/oauth/oauth-helpers.tsx +++ b/apps/backend/src/app/api/latest/auth/oauth/oauth-helpers.tsx @@ -1,12 +1,12 @@ import { SmartResponse } from "@/route-handlers/smart-response"; import { Response as OAuthResponse } from "@node-oauth/oauth2-server"; -import { StackAssertionError, StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; export function oauthResponseToSmartResponse(oauthResponse: OAuthResponse) { if (!oauthResponse.status) { - throw new StackAssertionError(`OAuth response status is missing`, { oauthResponse }); + throw new HexclaveAssertionError(`OAuth response status is missing`, { oauthResponse }); } else if (oauthResponse.status >= 500 && oauthResponse.status < 600) { - throw new StackAssertionError(`OAuth server error: ${JSON.stringify(oauthResponse.body)}`, { oauthResponse }); + throw new HexclaveAssertionError(`OAuth server error: ${JSON.stringify(oauthResponse.body)}`, { oauthResponse }); } else if (oauthResponse.status >= 200 && oauthResponse.status < 500) { return { statusCode: { @@ -17,7 +17,7 @@ export function oauthResponseToSmartResponse(oauthResponse: OAuthResponse) { headers: Object.fromEntries(Object.entries(oauthResponse.headers || {}).map(([k, v]) => ([k, [v]]))), } as const satisfies SmartResponse; } else { - throw new StackAssertionError(`Invalid OAuth response status code: ${oauthResponse.status}`, { oauthResponse }); + throw new HexclaveAssertionError(`Invalid OAuth response status code: ${oauthResponse.status}`, { oauthResponse }); } } diff --git a/apps/backend/src/app/api/latest/auth/passkey/register/verification-code-handler.tsx b/apps/backend/src/app/api/latest/auth/passkey/register/verification-code-handler.tsx index 0dd7842fe0..95e93c6e4a 100644 --- a/apps/backend/src/app/api/latest/auth/passkey/register/verification-code-handler.tsx +++ b/apps/backend/src/app/api/latest/auth/passkey/register/verification-code-handler.tsx @@ -6,7 +6,7 @@ import { verifyRegistrationResponse } from "@simplewebauthn/server"; import { decodeClientDataJSON } from "@simplewebauthn/server/helpers"; import { KnownErrors } from "@stackframe/stack-shared"; import { yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { RegistrationResponseJSON } from "@stackframe/stack-shared/dist/utils/passkey"; export const registerVerificationCodeHandler = createVerificationCodeHandler({ @@ -37,7 +37,7 @@ export const registerVerificationCodeHandler = createVerificationCodeHandler({ }), }), async send() { - throw new StackAssertionError("send() called on a Passkey registration verification code handler"); + throw new HexclaveAssertionError("send() called on a Passkey registration verification code handler"); }, async handler(tenancy, _, { challenge }, { credential }, user) { if (!tenancy.config.auth.passkey.allowSignIn) { @@ -45,7 +45,7 @@ export const registerVerificationCodeHandler = createVerificationCodeHandler({ } if (!user) { - throw new StackAssertionError("User not found", { + throw new HexclaveAssertionError("User not found", { tenancyId: tenancy.id, }); } @@ -92,7 +92,7 @@ export const registerVerificationCodeHandler = createVerificationCodeHandler({ if (authMethods.length > 1) { // We do not support multiple passkeys per user yet - throw new StackAssertionError("User has multiple passkey auth methods.", { + throw new HexclaveAssertionError("User has multiple passkey auth methods.", { tenancyId: tenancy.id, projectUserId: user.id, }); diff --git a/apps/backend/src/app/api/latest/auth/passkey/sign-in/verification-code-handler.tsx b/apps/backend/src/app/api/latest/auth/passkey/sign-in/verification-code-handler.tsx index e58e571ba8..698bdf4ae3 100644 --- a/apps/backend/src/app/api/latest/auth/passkey/sign-in/verification-code-handler.tsx +++ b/apps/backend/src/app/api/latest/auth/passkey/sign-in/verification-code-handler.tsx @@ -7,7 +7,7 @@ import { verifyAuthenticationResponse } from "@simplewebauthn/server"; import { decodeClientDataJSON } from "@simplewebauthn/server/helpers"; import { KnownErrors } from "@stackframe/stack-shared"; import { signInResponseSchema, yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { AuthenticationResponseJSON } from "@stackframe/stack-shared/dist/utils/passkey"; import { createMfaRequiredError } from "../../mfa/sign-in/verification-code-handler"; @@ -35,7 +35,7 @@ export const passkeySignInVerificationCodeHandler = createVerificationCodeHandle body: signInResponseSchema.defined(), }), async send() { - throw new StackAssertionError("send() called on a Passkey sign in verification code handler"); + throw new HexclaveAssertionError("send() called on a Passkey sign in verification code handler"); }, async handler(tenancy, _, { challenge }, { authentication_response }) { diff --git a/apps/backend/src/app/api/latest/auth/password/set/route.tsx b/apps/backend/src/app/api/latest/auth/password/set/route.tsx index 067a94b5ab..1bb7fddeda 100644 --- a/apps/backend/src/app/api/latest/auth/password/set/route.tsx +++ b/apps/backend/src/app/api/latest/auth/password/set/route.tsx @@ -3,7 +3,7 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { KnownErrors } from "@stackframe/stack-shared"; import { getPasswordError } from "@stackframe/stack-shared/dist/helpers/password"; import { adaptSchema, clientOrHigherAuthTypeSchema, passwordSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { hashPassword } from "@stackframe/stack-shared/dist/utils/hashes"; export const POST = createSmartRouteHandler({ @@ -47,7 +47,7 @@ export const POST = createSmartRouteHandler({ }); if (authMethods.length > 1) { - throw new StackAssertionError("User has multiple password auth methods.", { + throw new HexclaveAssertionError("User has multiple password auth methods.", { tenancyId: tenancy.id, projectUserId: user.id, }); diff --git a/apps/backend/src/app/api/latest/auth/password/sign-in/route.tsx b/apps/backend/src/app/api/latest/auth/password/sign-in/route.tsx index 98eaa2742d..47c1b4ff3c 100644 --- a/apps/backend/src/app/api/latest/auth/password/sign-in/route.tsx +++ b/apps/backend/src/app/api/latest/auth/password/sign-in/route.tsx @@ -4,7 +4,7 @@ import { getPrismaClientForTenancy } from "@/prisma-client"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { KnownErrors } from "@stackframe/stack-shared"; import { adaptSchema, clientOrHigherAuthTypeSchema, emailSchema, passwordSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { comparePassword } from "@stackframe/stack-shared/dist/utils/hashes"; import { createMfaRequiredError } from "../../mfa/sign-in/verification-code-handler"; @@ -56,7 +56,7 @@ export const POST = createSmartRouteHandler({ } if (!contactChannel || !passwordAuthMethod) { - throw new StackAssertionError("This should never happen (the comparePassword call should've already caused this to fail)"); + throw new HexclaveAssertionError("This should never happen (the comparePassword call should've already caused this to fail)"); } if (contactChannel.projectUser.requiresTotpMfa) { diff --git a/apps/backend/src/app/api/latest/auth/password/update/route.tsx b/apps/backend/src/app/api/latest/auth/password/update/route.tsx index 75756e4783..2b47481203 100644 --- a/apps/backend/src/app/api/latest/auth/password/update/route.tsx +++ b/apps/backend/src/app/api/latest/auth/password/update/route.tsx @@ -4,7 +4,7 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { KnownErrors } from "@stackframe/stack-shared"; import { getPasswordError } from "@stackframe/stack-shared/dist/helpers/password"; import { adaptSchema, clientOrHigherAuthTypeSchema, passwordSchema, yupNumber, yupObject, yupString, yupTuple } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { comparePassword, hashPassword } from "@stackframe/stack-shared/dist/utils/hashes"; export const POST = createSmartRouteHandler({ @@ -51,7 +51,7 @@ export const POST = createSmartRouteHandler({ }); if (authMethods.length > 1) { - throw new StackAssertionError("User has multiple password auth methods.", { + throw new HexclaveAssertionError("User has multiple password auth methods.", { tenancyId: tenancy.id, projectUserId: user.id, }); diff --git a/apps/backend/src/app/api/latest/check-feature-support/route.tsx b/apps/backend/src/app/api/latest/check-feature-support/route.tsx index 97b8eab741..b414018d4c 100644 --- a/apps/backend/src/app/api/latest/check-feature-support/route.tsx +++ b/apps/backend/src/app/api/latest/check-feature-support/route.tsx @@ -1,6 +1,6 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; import { deindent } from "@stackframe/stack-shared/dist/utils/strings"; export const POST = createSmartRouteHandler({ @@ -25,7 +25,7 @@ export const POST = createSmartRouteHandler({ const featureName = req.body?.name as unknown; const expectedUnsupportedFeatures: unknown[] = ["rsc-handler-signIn"]; if (!expectedUnsupportedFeatures.includes(featureName)) { - captureError("check-feature-support", new StackAssertionError(`${req.auth?.user?.primaryEmail || "User"} tried to check support of unsupported feature: ${JSON.stringify(req.body, null, 2)}`, { req })); + captureError("check-feature-support", new HexclaveAssertionError(`${req.auth?.user?.primaryEmail || "User"} tried to check support of unsupported feature: ${JSON.stringify(req.body, null, 2)}`, { req })); } return { statusCode: 200, diff --git a/apps/backend/src/app/api/latest/connected-accounts/access-token-helpers.tsx b/apps/backend/src/app/api/latest/connected-accounts/access-token-helpers.tsx index eb896b5dc5..9286f1baa3 100644 --- a/apps/backend/src/app/api/latest/connected-accounts/access-token-helpers.tsx +++ b/apps/backend/src/app/api/latest/connected-accounts/access-token-helpers.tsx @@ -3,7 +3,7 @@ import type { OAuthAccessTokenRefreshError } from "@/oauth/providers/base"; import { getPrismaClientForTenancy } from "@/prisma-client"; import { KnownErrors } from "@stackframe/stack-shared"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; import { extractScopes } from "@stackframe/stack-shared/dist/utils/strings"; function captureOAuthAccessTokenRefreshIssue(options: { @@ -15,7 +15,7 @@ function captureOAuthAccessTokenRefreshIssue(options: { }) { const providerId = typeof options.errorContext.providerId === "string" ? options.errorContext.providerId : "unknown"; const providerClass = options.providerInstance.constructor.name; - captureError(options.location, new StackAssertionError( + captureError(options.location, new HexclaveAssertionError( `${options.message} (providerId: ${providerId}, providerClass: ${providerClass}, attempts: ${options.refreshError.attempts}, retries: ${options.refreshError.retryCount})`, { cause: options.refreshError.cause, @@ -195,7 +195,7 @@ export async function retrieveOrRefreshAccessToken(options: { errorContext, refreshError: tokenSetResult.error, }); - const assertionError = new StackAssertionError('Unexpected error refreshing access token — this may indicate a bug or misconfiguration', { + const assertionError = new HexclaveAssertionError('Unexpected error refreshing access token — this may indicate a bug or misconfiguration', { cause: tokenSetResult.error.cause, providerClass: providerInstance.constructor.name, refreshErrorType: tokenSetResult.error.type, @@ -209,7 +209,7 @@ export async function retrieveOrRefreshAccessToken(options: { } default: { const _: never = tokenSetResult.error; - throw new StackAssertionError("Unhandled OAuth access token refresh error", { cause: _ }); + throw new HexclaveAssertionError("Unhandled OAuth access token refresh error", { cause: _ }); } } } @@ -243,7 +243,7 @@ export async function retrieveOrRefreshAccessToken(options: { return { access_token: tokenSet.accessToken }; } else { - throw new StackAssertionError("No access token returned"); + throw new HexclaveAssertionError("No access token returned"); } } diff --git a/apps/backend/src/app/api/latest/data-vault/stores/[id]/get/route.tsx b/apps/backend/src/app/api/latest/data-vault/stores/[id]/get/route.tsx index ac3b0ec8cd..2d01f6b01c 100644 --- a/apps/backend/src/app/api/latest/data-vault/stores/[id]/get/route.tsx +++ b/apps/backend/src/app/api/latest/data-vault/stores/[id]/get/route.tsx @@ -3,7 +3,7 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { KnownErrors } from "@stackframe/stack-shared"; import { decryptWithKms } from "@stackframe/stack-shared/dist/helpers/vault/server-side"; import { adaptSchema, serverOrHigherAuthTypeSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; export const POST = createSmartRouteHandler({ metadata: { @@ -55,7 +55,7 @@ export const POST = createSmartRouteHandler({ const encryptedData = entry.encrypted as { edkBase64?: string, ciphertextBase64?: string }; if (!encryptedData.edkBase64 || !encryptedData.ciphertextBase64) { - throw new StackAssertionError("Corrupted encrypted data", encryptedData); + throw new HexclaveAssertionError("Corrupted encrypted data", encryptedData); } const decryptedValue = await decryptWithKms({ diff --git a/apps/backend/src/app/api/latest/emails/outbox/crud.tsx b/apps/backend/src/app/api/latest/emails/outbox/crud.tsx index 73ec227026..a1b0beb314 100644 --- a/apps/backend/src/app/api/latest/emails/outbox/crud.tsx +++ b/apps/backend/src/app/api/latest/emails/outbox/crud.tsx @@ -6,7 +6,7 @@ import { createCrudHandlers } from "@/route-handlers/crud-handler"; import { KnownErrors } from "@stackframe/stack-shared"; import { emailOutboxCrud, EmailOutboxCrud } from "@stackframe/stack-shared/dist/interface/crud/email-outbox"; import { yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError, StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { Json } from "@stackframe/stack-shared/dist/utils/json"; import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; @@ -17,7 +17,7 @@ import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; */ function apiRecipientToDb(apiRecipient: EmailOutboxCrud["Server"]["Update"]["to"]): EmailOutboxRecipient { if (!apiRecipient) { - throw new StackAssertionError("Recipient is required"); + throw new HexclaveAssertionError("Recipient is required"); } switch (apiRecipient.type) { case "user-primary-email": { @@ -30,7 +30,7 @@ function apiRecipientToDb(apiRecipient: EmailOutboxCrud["Server"]["Update"]["to" return { type: "custom-emails", emails: apiRecipient.emails }; } default: { - throw new StackAssertionError("Unknown recipient type", { apiRecipient }); + throw new HexclaveAssertionError("Unknown recipient type", { apiRecipient }); } } } @@ -284,7 +284,7 @@ function prismaModelToCrud(prismaModel: EmailOutbox): EmailOutboxCrud["Server"][ }; } } - throw new StackAssertionError(`Unknown email outbox status: ${status}`, { status }); + throw new HexclaveAssertionError(`Unknown email outbox status: ${status}`, { status }); } const MAX_LIMIT = 100; diff --git a/apps/backend/src/app/api/latest/integrations/credential-scanning/revoke/route.tsx b/apps/backend/src/app/api/latest/integrations/credential-scanning/revoke/route.tsx index a3946b58a9..5c830af3a9 100644 --- a/apps/backend/src/app/api/latest/integrations/credential-scanning/revoke/route.tsx +++ b/apps/backend/src/app/api/latest/integrations/credential-scanning/revoke/route.tsx @@ -7,7 +7,7 @@ import { getPrismaClientForTenancy, globalPrismaClient, retryTransaction } from import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { KnownErrors } from "@stackframe/stack-shared"; import { yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { escapeHtml } from "@stackframe/stack-shared/dist/utils/html"; export const POST = createSmartRouteHandler({ @@ -83,7 +83,7 @@ export const POST = createSmartRouteHandler({ // For user API keys, notify the user const tenancy = await getTenancy(updatedApiKey.tenancyId); if (!tenancy) { - throw new StackAssertionError("Tenancy not found"); + throw new HexclaveAssertionError("Tenancy not found"); } const prisma = await getPrismaClientForTenancy(tenancy); @@ -101,7 +101,7 @@ export const POST = createSmartRouteHandler({ if (!projectUser) { // This should never happen - throw new StackAssertionError("Project user not found"); + throw new HexclaveAssertionError("Project user not found"); } // We might have other types besides email, so we disable this rule // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -113,14 +113,14 @@ export const POST = createSmartRouteHandler({ // For team API keys, notify users with manage_api_keys permission const tenancy = await getTenancy(updatedApiKey.tenancyId); if (!tenancy) { - throw new StackAssertionError("Tenancy not found"); + throw new HexclaveAssertionError("Tenancy not found"); } const prisma = await getPrismaClientForTenancy(tenancy); const userIdsWithManageApiKeysPermission = await retryTransaction(prisma, async (tx) => { if (!updatedApiKey.teamId) { - throw new StackAssertionError("Team ID not specified in team API key"); + throw new HexclaveAssertionError("Team ID not specified in team API key"); } const permissions = await listPermissions(tx, { @@ -159,7 +159,7 @@ export const POST = createSmartRouteHandler({ const tenancy = await getTenancy(updatedApiKey.tenancyId); if (!tenancy) { - throw new StackAssertionError("Tenancy not found"); + throw new HexclaveAssertionError("Tenancy not found"); } // Create email content diff --git a/apps/backend/src/app/api/latest/integrations/custom/oauth/authorize/route.tsx b/apps/backend/src/app/api/latest/integrations/custom/oauth/authorize/route.tsx index 7ec3e7c410..62f5339ccd 100644 --- a/apps/backend/src/app/api/latest/integrations/custom/oauth/authorize/route.tsx +++ b/apps/backend/src/app/api/latest/integrations/custom/oauth/authorize/route.tsx @@ -1,6 +1,6 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { yupNever, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { redirect } from "next/navigation"; export const GET = createSmartRouteHandler({ @@ -22,7 +22,7 @@ export const GET = createSmartRouteHandler({ handler: async (req) => { const url = new URL(req.url); if (url.pathname !== "/api/v1/integrations/custom/oauth/authorize") { - throw new StackAssertionError(`Expected pathname to be authorize endpoint but got ${JSON.stringify(url.pathname)}`, { url }); + throw new HexclaveAssertionError(`Expected pathname to be authorize endpoint but got ${JSON.stringify(url.pathname)}`, { url }); } url.pathname = "/api/v1/integrations/custom/oauth/idp/auth"; url.search = new URLSearchParams({ ...req.query, scope: "openid" }).toString(); diff --git a/apps/backend/src/app/api/latest/integrations/custom/oauth/idp/[[...route]]/route.tsx b/apps/backend/src/app/api/latest/integrations/custom/oauth/idp/[[...route]]/route.tsx index bef44f5909..a7e4ebd6f9 100644 --- a/apps/backend/src/app/api/latest/integrations/custom/oauth/idp/[[...route]]/route.tsx +++ b/apps/backend/src/app/api/latest/integrations/custom/oauth/idp/[[...route]]/route.tsx @@ -1,6 +1,6 @@ import { handleApiRequest } from "@/route-handlers/smart-route-handler"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { createNodeHttpServerDuplex } from "@stackframe/stack-shared/dist/utils/node-http"; import { NextRequest, NextResponse } from "next/server"; import { createOidcProvider } from "../../../../idp"; @@ -30,7 +30,7 @@ function getOidcCallbackPromise() { const handler = handleApiRequest(async (req: NextRequest) => { const newUrl = req.url.replace(pathPrefix, ""); if (newUrl === req.url) { - throw new StackAssertionError("No path prefix found in request URL. Is the pathPrefix correct?", { newUrl, url: req.url, pathPrefix }); + throw new HexclaveAssertionError("No path prefix found in request URL. Is the pathPrefix correct?", { newUrl, url: req.url, pathPrefix }); } const newHeaders = new Headers(req.headers); const incomingBody = new Uint8Array(await req.arrayBuffer()); diff --git a/apps/backend/src/app/api/latest/integrations/custom/oauth/token/route.tsx b/apps/backend/src/app/api/latest/integrations/custom/oauth/token/route.tsx index 0db2d84196..25597c063c 100644 --- a/apps/backend/src/app/api/latest/integrations/custom/oauth/token/route.tsx +++ b/apps/backend/src/app/api/latest/integrations/custom/oauth/token/route.tsx @@ -2,7 +2,7 @@ import { globalPrismaClient } from "@/prisma-client"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { neonAuthorizationHeaderSchema, yupMixed, yupNumber, yupObject, yupString, yupTuple, yupUnion } from "@stackframe/stack-shared/dist/schema-fields"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; export const POST = createSmartRouteHandler({ metadata: { @@ -62,7 +62,7 @@ export const POST = createSmartRouteHandler({ }); if (!userInfoResponse.ok) { const text = await userInfoResponse.text(); - throw new StackAssertionError("Failed to fetch user info? This should never happen", { text, userInfoResponse }); + throw new HexclaveAssertionError("Failed to fetch user info? This should never happen", { text, userInfoResponse }); } const userInfoResponseBody = await userInfoResponse.json(); @@ -74,7 +74,7 @@ export const POST = createSmartRouteHandler({ }, }); if (!mapping) { - throw new StackAssertionError("No mapping found for account", { accountId }); + throw new HexclaveAssertionError("No mapping found for account", { accountId }); } return { diff --git a/apps/backend/src/app/api/latest/integrations/custom/projects/transfer/confirm/verification-code-handler.tsx b/apps/backend/src/app/api/latest/integrations/custom/projects/transfer/confirm/verification-code-handler.tsx index 7825ca3693..166da03dcc 100644 --- a/apps/backend/src/app/api/latest/integrations/custom/projects/transfer/confirm/verification-code-handler.tsx +++ b/apps/backend/src/app/api/latest/integrations/custom/projects/transfer/confirm/verification-code-handler.tsx @@ -6,7 +6,7 @@ import { createVerificationCodeHandler } from "@/route-handlers/verification-cod import { VerificationCodeType } from "@/generated/prisma/client"; import { KnownErrors } from "@stackframe/stack-shared"; import { yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError, StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; export const integrationProjectTransferCodeHandler = createVerificationCodeHandler({ metadata: { @@ -44,7 +44,7 @@ export const integrationProjectTransferCodeHandler = createVerificationCodeHandl }, async handler(tenancy, method, data, body, user) { - if (tenancy.project.id !== "internal") throw new StackAssertionError("This endpoint is only available for internal projects, why is it being called for a non-internal project?"); + if (tenancy.project.id !== "internal") throw new HexclaveAssertionError("This endpoint is only available for internal projects, why is it being called for a non-internal project?"); if (!user) throw new KnownErrors.UserAuthenticationRequired; const provisionedProject = await globalPrismaClient.provisionedProject.deleteMany({ diff --git a/apps/backend/src/app/api/latest/integrations/idp.ts b/apps/backend/src/app/api/latest/integrations/idp.ts index 36a5944123..a1908493a7 100644 --- a/apps/backend/src/app/api/latest/integrations/idp.ts +++ b/apps/backend/src/app/api/latest/integrations/idp.ts @@ -2,7 +2,7 @@ import { globalPrismaClient, retryTransaction } from '@/prisma-client'; import { Prisma } from '@/generated/prisma/client'; import { decodeBase64OrBase64Url, toHexString } from '@stackframe/stack-shared/dist/utils/bytes'; import { getEnvVariable } from '@stackframe/stack-shared/dist/utils/env'; -import { StackAssertionError, captureError, throwErr } from '@stackframe/stack-shared/dist/utils/errors'; +import { HexclaveAssertionError, captureError, throwErr } from '@stackframe/stack-shared/dist/utils/errors'; import { sha512 } from '@stackframe/stack-shared/dist/utils/hashes'; import { getPrivateJwks, getPublicJwkSet } from '@stackframe/stack-shared/dist/utils/jwt'; import { deindent } from '@stackframe/stack-shared/dist/utils/strings'; @@ -40,7 +40,7 @@ function createAdapter(options: { constructor(model: string) { this.model = model; if (!model) { - throw new StackAssertionError(deindent` + throw new HexclaveAssertionError(deindent` model must be non-empty. oidc-provider should never call the constructor with an empty string. However, it relies on 'constructor.name' in some locations, causing it to fail when class name minification is enabled. Make sure that server-side class names are not minified, for example by disabling serverMinification in next.config.mjs. @@ -50,9 +50,9 @@ function createAdapter(options: { async upsert(id: string, payload: AdapterPayload, expiresInSeconds: number): Promise { // if one of these assertions is triggered, make sure you're not minifying class names (see the constructor) - if (expiresInSeconds < 0) throw new StackAssertionError(`expiresInSeconds of ${this.model}:${id} must be non-negative, got ${expiresInSeconds}`, { expiresInSeconds, model: this.model, id, payload }); - if (expiresInSeconds > 60 * 60 * 24 * 365 * 100) throw new StackAssertionError(`expiresInSeconds of ${this.model}:${id} must be less than 100 years, got ${expiresInSeconds}`, { expiresInSeconds, model: this.model, id, payload }); - if (!Number.isFinite(expiresInSeconds)) throw new StackAssertionError(`expiresInSeconds of ${this.model}:${id} must be a finite number, got ${expiresInSeconds}`, { expiresInSeconds, model: this.model, id, payload }); + if (expiresInSeconds < 0) throw new HexclaveAssertionError(`expiresInSeconds of ${this.model}:${id} must be non-negative, got ${expiresInSeconds}`, { expiresInSeconds, model: this.model, id, payload }); + if (expiresInSeconds > 60 * 60 * 24 * 365 * 100) throw new HexclaveAssertionError(`expiresInSeconds of ${this.model}:${id} must be less than 100 years, got ${expiresInSeconds}`, { expiresInSeconds, model: this.model, id, payload }); + if (!Number.isFinite(expiresInSeconds)) throw new HexclaveAssertionError(`expiresInSeconds of ${this.model}:${id} must be a finite number, got ${expiresInSeconds}`, { expiresInSeconds, model: this.model, id, payload }); await niceUpdate(this.model, id, () => ({ payload, expiresAt: new Date(Date.now() + expiresInSeconds * 1000) })); } diff --git a/apps/backend/src/app/api/latest/integrations/neon/oauth/authorize/route.tsx b/apps/backend/src/app/api/latest/integrations/neon/oauth/authorize/route.tsx index 68707322fe..6d5b0b2218 100644 --- a/apps/backend/src/app/api/latest/integrations/neon/oauth/authorize/route.tsx +++ b/apps/backend/src/app/api/latest/integrations/neon/oauth/authorize/route.tsx @@ -1,6 +1,6 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { yupNever, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { redirect } from "next/navigation"; export const GET = createSmartRouteHandler({ @@ -22,7 +22,7 @@ export const GET = createSmartRouteHandler({ handler: async (req) => { const url = new URL(req.url); if (url.pathname !== "/api/v1/integrations/neon/oauth/authorize") { - throw new StackAssertionError(`Expected pathname to be authorize endpoint but got ${JSON.stringify(url.pathname)}`, { url }); + throw new HexclaveAssertionError(`Expected pathname to be authorize endpoint but got ${JSON.stringify(url.pathname)}`, { url }); } url.pathname = "/api/v1/integrations/neon/oauth/idp/auth"; url.search = new URLSearchParams({ ...req.query, scope: "openid" }).toString(); diff --git a/apps/backend/src/app/api/latest/integrations/neon/oauth/idp/[[...route]]/route.tsx b/apps/backend/src/app/api/latest/integrations/neon/oauth/idp/[[...route]]/route.tsx index 66d4bb2883..50ea480b51 100644 --- a/apps/backend/src/app/api/latest/integrations/neon/oauth/idp/[[...route]]/route.tsx +++ b/apps/backend/src/app/api/latest/integrations/neon/oauth/idp/[[...route]]/route.tsx @@ -1,6 +1,6 @@ import { handleApiRequest } from "@/route-handlers/smart-route-handler"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { createNodeHttpServerDuplex } from "@stackframe/stack-shared/dist/utils/node-http"; import { NextRequest, NextResponse } from "next/server"; import { createOidcProvider } from "../../../../idp"; @@ -30,7 +30,7 @@ function getOidcCallbackPromise() { const handler = handleApiRequest(async (req: NextRequest) => { const newUrl = req.url.replace(pathPrefix, ""); if (newUrl === req.url) { - throw new StackAssertionError("No path prefix found in request URL. Is the pathPrefix correct?", { newUrl, url: req.url, pathPrefix }); + throw new HexclaveAssertionError("No path prefix found in request URL. Is the pathPrefix correct?", { newUrl, url: req.url, pathPrefix }); } const newHeaders = new Headers(req.headers); const incomingBody = new Uint8Array(await req.arrayBuffer()); diff --git a/apps/backend/src/app/api/latest/integrations/neon/oauth/token/route.tsx b/apps/backend/src/app/api/latest/integrations/neon/oauth/token/route.tsx index ff904bcf6b..95e5129969 100644 --- a/apps/backend/src/app/api/latest/integrations/neon/oauth/token/route.tsx +++ b/apps/backend/src/app/api/latest/integrations/neon/oauth/token/route.tsx @@ -2,7 +2,7 @@ import { globalPrismaClient } from "@/prisma-client"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { neonAuthorizationHeaderSchema, yupMixed, yupNumber, yupObject, yupString, yupTuple, yupUnion } from "@stackframe/stack-shared/dist/schema-fields"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; export const POST = createSmartRouteHandler({ metadata: { @@ -62,7 +62,7 @@ export const POST = createSmartRouteHandler({ }); if (!userInfoResponse.ok) { const text = await userInfoResponse.text(); - throw new StackAssertionError("Failed to fetch user info? This should never happen", { text, userInfoResponse }); + throw new HexclaveAssertionError("Failed to fetch user info? This should never happen", { text, userInfoResponse }); } const userInfoResponseBody = await userInfoResponse.json(); @@ -74,7 +74,7 @@ export const POST = createSmartRouteHandler({ }, }); if (!mapping) { - throw new StackAssertionError("No mapping found for account", { accountId }); + throw new HexclaveAssertionError("No mapping found for account", { accountId }); } return { diff --git a/apps/backend/src/app/api/latest/integrations/neon/projects/transfer/confirm/verification-code-handler.tsx b/apps/backend/src/app/api/latest/integrations/neon/projects/transfer/confirm/verification-code-handler.tsx index 2bc14e985e..ca020ff837 100644 --- a/apps/backend/src/app/api/latest/integrations/neon/projects/transfer/confirm/verification-code-handler.tsx +++ b/apps/backend/src/app/api/latest/integrations/neon/projects/transfer/confirm/verification-code-handler.tsx @@ -4,7 +4,7 @@ import { createVerificationCodeHandler } from "@/route-handlers/verification-cod import { VerificationCodeType } from "@/generated/prisma/client"; import { KnownErrors } from "@stackframe/stack-shared"; import { yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; export const neonIntegrationProjectTransferCodeHandler = createVerificationCodeHandler({ metadata: { @@ -42,7 +42,7 @@ export const neonIntegrationProjectTransferCodeHandler = createVerificationCodeH }, async handler(tenancy, method, data, body, user) { - if (tenancy.project.id !== "internal") throw new StackAssertionError("This endpoint is only available for internal projects, why is it being called for a non-internal project?"); + if (tenancy.project.id !== "internal") throw new HexclaveAssertionError("This endpoint is only available for internal projects, why is it being called for a non-internal project?"); if (!user) throw new KnownErrors.UserAuthenticationRequired; const provisionedProject = await globalPrismaClient.provisionedProject.deleteMany({ diff --git a/apps/backend/src/app/api/latest/integrations/resend/webhooks/route.tsx b/apps/backend/src/app/api/latest/integrations/resend/webhooks/route.tsx index ba43ca90f3..25034fd143 100644 --- a/apps/backend/src/app/api/latest/integrations/resend/webhooks/route.tsx +++ b/apps/backend/src/app/api/latest/integrations/resend/webhooks/route.tsx @@ -2,7 +2,7 @@ import { processResendDomainWebhookEvent } from "@/lib/managed-email-onboarding" import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { yupBoolean, yupMixed, yupNumber, yupObject, yupString, yupTuple } from "@stackframe/stack-shared/dist/schema-fields"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { Result } from "@stackframe/stack-shared/dist/utils/results"; import { Webhook } from "svix"; @@ -78,7 +78,7 @@ export const POST = createSmartRouteHandler({ const domainId = payload.data?.id; const providerStatusRaw = payload.data?.status; if (domainId == null || providerStatusRaw == null) { - throw new StackAssertionError("Resend webhook payload missing required domain fields", { + throw new HexclaveAssertionError("Resend webhook payload missing required domain fields", { payload, }); } diff --git a/apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx b/apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx index 812f224081..c444e6f362 100644 --- a/apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx +++ b/apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx @@ -11,7 +11,7 @@ import { DEFAULT_TEMPLATE_IDS } from "@stackframe/stack-shared/dist/helpers/emai import { yupMixed, yupNumber, yupObject, yupString, yupTuple } from "@stackframe/stack-shared/dist/schema-fields"; import { typedIncludes } from '@stackframe/stack-shared/dist/utils/arrays'; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; import { getOrUndefined } from "@stackframe/stack-shared/dist/utils/objects"; import { typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings"; import Stripe from "stripe"; @@ -115,11 +115,11 @@ async function getTenancyForStripeAccountId(accountId: string, mockData?: Stripe const account = await stripe.accounts.retrieve(accountId); const tenancyId = account.metadata?.tenancyId; if (!tenancyId) { - throw new StackAssertionError("Stripe account metadata missing tenancyId", { accountId }); + throw new HexclaveAssertionError("Stripe account metadata missing tenancyId", { accountId }); } const tenancy = await getTenancy(tenancyId); if (!tenancy) { - throw new StackAssertionError("Tenancy not found", { accountId, tenancyId }); + throw new HexclaveAssertionError("Tenancy not found", { accountId, tenancyId }); } return tenancy; } @@ -159,7 +159,7 @@ async function sendDefaultTemplateEmail(options: { const templateId = DEFAULT_TEMPLATE_IDS[options.templateType]; const template = getOrUndefined(options.tenancy.config.emails.templates, templateId); if (!template) { - throw new StackAssertionError(`Default email template not found: ${options.templateType}`, { templateId }); + throw new HexclaveAssertionError(`Default email template not found: ${options.templateType}`, { templateId }); } await sendEmailToMany({ tenancy: options.tenancy, @@ -183,7 +183,7 @@ async function processStripeWebhookEvent(event: Stripe.Event): Promise { const metadata = paymentIntent.metadata; const accountId = event.account; if (!accountId) { - throw new StackAssertionError("Stripe webhook account id missing", { event }); + throw new HexclaveAssertionError("Stripe webhook account id missing", { event }); } const tenancy = await getTenancyForStripeAccountId(accountId, mockData); const prisma = await getPrismaClientForTenancy(tenancy); @@ -198,11 +198,11 @@ async function processStripeWebhookEvent(event: Stripe.Event): Promise { const qty = Math.max(1, Number(metadata.purchaseQuantity || 1)); const stripePaymentIntentId = paymentIntent.id; if (!metadata.customerId || !metadata.customerType) { - throw new StackAssertionError("Missing customer metadata for one-time purchase", { event }); + throw new HexclaveAssertionError("Missing customer metadata for one-time purchase", { event }); } const customerType = normalizeCustomerType(metadata.customerType); if (!customerType) { - throw new StackAssertionError("Invalid customer type for one-time purchase", { event }); + throw new HexclaveAssertionError("Invalid customer type for one-time purchase", { event }); } // dual write - prisma and bulldozer const upsertedPurchase = await prisma.oneTimePurchase.upsert({ @@ -260,16 +260,16 @@ async function processStripeWebhookEvent(event: Stripe.Event): Promise { const metadata = paymentIntent.metadata; const accountId = event.account; if (!accountId) { - throw new StackAssertionError("Stripe webhook account id missing", { event }); + throw new HexclaveAssertionError("Stripe webhook account id missing", { event }); } const tenancy = await getTenancyForStripeAccountId(accountId, mockData); const prisma = await getPrismaClientForTenancy(tenancy); if (!metadata.customerId || !metadata.customerType) { - throw new StackAssertionError("Missing customer metadata for one-time purchase failure", { event }); + throw new HexclaveAssertionError("Missing customer metadata for one-time purchase failure", { event }); } const customerType = normalizeCustomerType(metadata.customerType); if (!customerType) { - throw new StackAssertionError("Invalid customer type for one-time purchase failure", { event }); + throw new HexclaveAssertionError("Invalid customer type for one-time purchase failure", { event }); } const recipients = await getPaymentRecipients({ tenancy, @@ -306,7 +306,7 @@ async function processStripeWebhookEvent(event: Stripe.Event): Promise { } const accountId = event.account; if (!accountId) { - throw new StackAssertionError("Stripe webhook account id missing", { event }); + throw new HexclaveAssertionError("Stripe webhook account id missing", { event }); } const dispute = event.data.object as Stripe.Dispute; const tenancy = await getTenancyForStripeAccountId(accountId, mockData); @@ -325,10 +325,10 @@ async function processStripeWebhookEvent(event: Stripe.Event): Promise { const accountId = event.account; const customerId = event.data.object.customer; if (!accountId) { - throw new StackAssertionError("Stripe webhook account id missing", { event }); + throw new HexclaveAssertionError("Stripe webhook account id missing", { event }); } if (typeof customerId !== 'string') { - throw new StackAssertionError("Stripe webhook bad customer id", { event }); + throw new HexclaveAssertionError("Stripe webhook bad customer id", { event }); } const stripe = await getStripeForAccount({ accountId }, mockData); await syncStripeSubscriptions(stripe, accountId, customerId); @@ -345,15 +345,15 @@ async function processStripeWebhookEvent(event: Stripe.Event): Promise { const prisma = await getPrismaClientForTenancy(tenancy); const stripeCustomerId = invoice.customer; if (typeof stripeCustomerId !== "string") { - throw new StackAssertionError("Stripe invoice customer id missing", { event }); + throw new HexclaveAssertionError("Stripe invoice customer id missing", { event }); } const stripeCustomer = await stripe.customers.retrieve(stripeCustomerId); if (stripeCustomer.deleted) { - throw new StackAssertionError("Stripe invoice customer deleted", { event }); + throw new HexclaveAssertionError("Stripe invoice customer deleted", { event }); } const customerType = normalizeCustomerType(stripeCustomer.metadata.customerType); if (!stripeCustomer.metadata.customerId || !customerType) { - throw new StackAssertionError("Stripe invoice customer metadata missing customerId or customerType", { event }); + throw new HexclaveAssertionError("Stripe invoice customer metadata missing customerId or customerType", { event }); } const recipients = await getPaymentRecipients({ tenancy, @@ -391,15 +391,15 @@ async function processStripeWebhookEvent(event: Stripe.Event): Promise { const prisma = await getPrismaClientForTenancy(tenancy); const stripeCustomerId = invoice.customer; if (typeof stripeCustomerId !== "string") { - throw new StackAssertionError("Stripe invoice customer id missing", { event }); + throw new HexclaveAssertionError("Stripe invoice customer id missing", { event }); } const stripeCustomer = await stripe.customers.retrieve(stripeCustomerId); if (stripeCustomer.deleted) { - throw new StackAssertionError("Stripe invoice customer deleted", { event }); + throw new HexclaveAssertionError("Stripe invoice customer deleted", { event }); } const customerType = normalizeCustomerType(stripeCustomer.metadata.customerType); if (!stripeCustomer.metadata.customerId || !customerType) { - throw new StackAssertionError("Stripe invoice customer metadata missing customerId or customerType", { event }); + throw new HexclaveAssertionError("Stripe invoice customer metadata missing customerId or customerType", { event }); } const recipients = await getPaymentRecipients({ tenancy, @@ -431,7 +431,7 @@ async function processStripeWebhookEvent(event: Stripe.Event): Promise { return; } else { - throw new StackAssertionError("Unknown stripe webhook type received: " + event.type, { event }); + throw new HexclaveAssertionError("Unknown stripe webhook type received: " + event.type, { event }); } } diff --git a/apps/backend/src/app/api/latest/internal/analytics/query/route.ts b/apps/backend/src/app/api/latest/internal/analytics/query/route.ts index 9e8d4526a2..25142338da 100644 --- a/apps/backend/src/app/api/latest/internal/analytics/query/route.ts +++ b/apps/backend/src/app/api/latest/internal/analytics/query/route.ts @@ -6,7 +6,7 @@ import { getStackServerApp } from "@/stack"; import { KnownErrors } from "@stackframe/stack-shared"; import { ITEM_IDS, PLAN_LIMITS } from "@stackframe/stack-shared/dist/plans"; import { adaptSchema, adminAuthTypeSchema, jsonSchema, yupBoolean, yupMixed, yupNumber, yupObject, yupRecord, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { Result } from "@stackframe/stack-shared/dist/utils/results"; import { randomUUID } from "crypto"; @@ -37,7 +37,7 @@ export const POST = createSmartRouteHandler({ }), async handler({ body, auth }) { if (body.include_all_branches) { - throw new StackAssertionError("include_all_branches is not supported yet"); + throw new HexclaveAssertionError("include_all_branches is not supported yet"); } let effectiveTimeoutMs = body.timeout_ms; diff --git a/apps/backend/src/app/api/latest/internal/backend-urls/route.tsx b/apps/backend/src/app/api/latest/internal/backend-urls/route.tsx index 8f50911cc7..f6320a2d1b 100644 --- a/apps/backend/src/app/api/latest/internal/backend-urls/route.tsx +++ b/apps/backend/src/app/api/latest/internal/backend-urls/route.tsx @@ -1,6 +1,6 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { urlSchema, yupArray, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; import { getDefaultApiUrls } from "@stackframe/stack-shared/dist/utils/urls"; @@ -19,25 +19,25 @@ const urlsArraySchema = yupArray(urlSchema.defined()).min(1).defined(); export function parseAndValidateConfig(raw: unknown): Array<{ probability: number, urls: string[] }> { if (typeof raw !== "object" || raw === null || Array.isArray(raw)) { - throw new StackAssertionError("STACK_BACKEND_URLS_CONFIG must be a JSON object mapping probability strings to URL arrays"); + throw new HexclaveAssertionError("STACK_BACKEND_URLS_CONFIG must be a JSON object mapping probability strings to URL arrays"); } const entries = Object.entries(raw as Record).map(([key, value]) => { const probability = Number(key); if (isNaN(probability) || probability < 0 || probability > 1) { - throw new StackAssertionError(`Invalid probability key "${key}": must be a number between 0 and 1`); + throw new HexclaveAssertionError(`Invalid probability key "${key}": must be a number between 0 and 1`); } const urls = urlsArraySchema.validateSync(value); return { probability, urls }; }); if (entries.length === 0) { - throw new StackAssertionError("STACK_BACKEND_URLS_CONFIG must have at least one entry"); + throw new HexclaveAssertionError("STACK_BACKEND_URLS_CONFIG must have at least one entry"); } const sum = entries.reduce((acc, e) => acc + e.probability, 0); if (sum > 1 + 1e-9) { - throw new StackAssertionError(`Probabilities sum to ${sum}, which exceeds 1`); + throw new HexclaveAssertionError(`Probabilities sum to ${sum}, which exceeds 1`); } return entries; @@ -52,7 +52,7 @@ function getCachedConfig() { try { parsed = JSON.parse(rawEnv); } catch (e) { - throw new StackAssertionError(`STACK_BACKEND_URLS_CONFIG is not valid JSON: ${e}`); + throw new HexclaveAssertionError(`STACK_BACKEND_URLS_CONFIG is not valid JSON: ${e}`); } cachedEntries = parseAndValidateConfig(parsed); } else { diff --git a/apps/backend/src/app/api/latest/internal/config/override/[level]/route.tsx b/apps/backend/src/app/api/latest/internal/config/override/[level]/route.tsx index 8e33ff4313..beb1b92772 100644 --- a/apps/backend/src/app/api/latest/internal/config/override/[level]/route.tsx +++ b/apps/backend/src/app/api/latest/internal/config/override/[level]/route.tsx @@ -18,7 +18,7 @@ import { globalPrismaClient, rawQuery } from "@/prisma-client"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { branchConfigSchema, environmentConfigSchema, getConfigOverrideErrors, migrateConfigOverride, projectConfigSchema } from "@stackframe/stack-shared/dist/config/schema"; import { adaptSchema, branchConfigSourceSchema, serverOrHigherAuthTypeSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; import * as yup from "yup"; type BranchConfigSourceApi = yup.InferType; @@ -241,7 +241,7 @@ async function warnOnValidationFailure( captureError("config-override-validation-warning", `Config override validation warning for project ${options.projectId} (this may not be a logic error, but rather a client/implementation issue — e.g. dot notation into non-existent record entries): ${validationResult.error}`); } } catch (e) { - captureError("config-override-validation-check-failed", new StackAssertionError("Config override validation check failed. This may be really bad! Make sure to check the error and the config.", { cause: e, options })); + captureError("config-override-validation-check-failed", new HexclaveAssertionError("Config override validation check failed. This may be really bad! Make sure to check the error and the config.", { cause: e, options })); } } diff --git a/apps/backend/src/app/api/latest/internal/email-themes/[id]/route.tsx b/apps/backend/src/app/api/latest/internal/email-themes/[id]/route.tsx index 415c348aa5..8689918bea 100644 --- a/apps/backend/src/app/api/latest/internal/email-themes/[id]/route.tsx +++ b/apps/backend/src/app/api/latest/internal/email-themes/[id]/route.tsx @@ -1,6 +1,6 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { adaptSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { internalEmailThemesCudHandlers } from "../cud"; export const GET = internalEmailThemesCudHandlers.readHandler; @@ -40,7 +40,7 @@ export const PATCH = createSmartRouteHandler({ const updated = result.items.find((t) => t.id === id); if (!updated) { - throw new StackAssertionError("Theme was updated but could not be found afterwards", { id }); + throw new HexclaveAssertionError("Theme was updated but could not be found afterwards", { id }); } return { statusCode: 200, diff --git a/apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts b/apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts index 831e93e42b..6f25634c49 100644 --- a/apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts +++ b/apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts @@ -13,7 +13,7 @@ import { yupTuple, } from "@stackframe/stack-shared/dist/schema-fields"; import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; -import { captureError, StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { captureError, HexclaveAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { wait } from "@stackframe/stack-shared/dist/utils/promises"; import type { PublishBatchRequest } from "@upstash/qstash"; @@ -41,7 +41,7 @@ function getPollerClaimLimit(): number { if (!rawValue) return DEFAULT_POLL_CLAIM_LIMIT; const parsed = Number.parseInt(rawValue, 10); if (!Number.isFinite(parsed) || parsed <= 0) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `${POLLER_CLAIM_LIMIT_ENV} must be a positive integer. Received: ${JSON.stringify(rawValue)}` ); } @@ -136,7 +136,7 @@ export const GET = createSmartRouteHandler({ const ID_SAMPLE_LIMIT = 10; captureError( "poller-stale-outgoing-requests", - new StackAssertionError( + new HexclaveAssertionError( [ `Recovered ${total} stale outgoing request(s) (reset=${resetIds.length}, deleted=${deletedIds.length}) older than ${STALE_REQUEST_THRESHOLD_MS}ms.`, `Stale rows are claims that never got cleared after publishing — the most likely cause is a poller lambda dying between the UPDATE that set startedFulfillingAt and the DELETE that should have removed the row.`, diff --git a/apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts b/apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts index 9130e30c0d..ed5b0411a2 100644 --- a/apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts +++ b/apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts @@ -12,7 +12,7 @@ import { yupTuple, } from "@stackframe/stack-shared/dist/schema-fields"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { captureError, StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { captureError, HexclaveAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { wait } from "@stackframe/stack-shared/dist/utils/promises"; const DEFAULT_MAX_DURATION_MS = 3 * 60 * 1000; @@ -33,7 +33,7 @@ function getSequencerBatchSize(): number { if (!rawValue) return DEFAULT_BATCH_SIZE; const parsed = Number.parseInt(rawValue, 10); if (!Number.isFinite(parsed) || parsed <= 0) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `${SEQUENCER_BATCH_SIZE_ENV} must be a positive integer. Received: ${JSON.stringify(rawValue)}` ); } diff --git a/apps/backend/src/app/api/latest/internal/external-db-sync/status/route.ts b/apps/backend/src/app/api/latest/internal/external-db-sync/status/route.ts index 88845389e6..f3fd069aed 100644 --- a/apps/backend/src/app/api/latest/internal/external-db-sync/status/route.ts +++ b/apps/backend/src/app/api/latest/internal/external-db-sync/status/route.ts @@ -13,7 +13,7 @@ import { yupString, } from "@stackframe/stack-shared/dist/schema-fields"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { errorToNiceString, StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { errorToNiceString, HexclaveAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { Result } from "@stackframe/stack-shared/dist/utils/results"; import { Client } from "pg"; import { KnownErrors } from "@stackframe/stack-shared"; diff --git a/apps/backend/src/app/api/latest/internal/failed-emails-digest/route.ts b/apps/backend/src/app/api/latest/internal/failed-emails-digest/route.ts index 0ed66f3eba..d6ec4a42f7 100644 --- a/apps/backend/src/app/api/latest/internal/failed-emails-digest/route.ts +++ b/apps/backend/src/app/api/latest/internal/failed-emails-digest/route.ts @@ -3,7 +3,7 @@ import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch } from "@/lib/tenanc import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { yupArray, yupBoolean, yupNumber, yupObject, yupString, yupTuple } from "@stackframe/stack-shared/dist/schema-fields"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; import { escapeHtml } from "@stackframe/stack-shared/dist/utils/html"; import { getFailedEmailsByTenancy } from "./crud"; @@ -69,7 +69,7 @@ export const POST = createSmartRouteHandler({ `; if (query.dry_run !== "true") { try { - throw new StackAssertionError("Failed emails digest is currently disabled!"); + throw new HexclaveAssertionError("Failed emails digest is currently disabled!"); } catch (error) { anyDigestsFailedToSend = true; captureError("send-failed-emails-digest", error); diff --git a/apps/backend/src/app/api/latest/internal/feature-requests/[featureRequestId]/upvote/route.tsx b/apps/backend/src/app/api/latest/internal/feature-requests/[featureRequestId]/upvote/route.tsx index 5611bb93fd..d381bced9f 100644 --- a/apps/backend/src/app/api/latest/internal/feature-requests/[featureRequestId]/upvote/route.tsx +++ b/apps/backend/src/app/api/latest/internal/feature-requests/[featureRequestId]/upvote/route.tsx @@ -1,7 +1,7 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { getOrCreateFeaturebaseUserFromAuth, requireFeaturebaseApiKey } from "@/lib/featurebase"; import { adaptSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; // POST /api/latest/internal/feature-requests/[featureRequestId]/upvote export const POST = createSmartRouteHandler({ @@ -52,14 +52,14 @@ export const POST = createSmartRouteHandler({ try { data = await response.json(); } catch (error) { - if (error instanceof StackAssertionError) { + if (error instanceof HexclaveAssertionError) { throw error; } - throw new StackAssertionError("Failed to parse Featurebase upvote response", { cause: error }); + throw new HexclaveAssertionError("Failed to parse Featurebase upvote response", { cause: error }); } if (!response.ok) { - throw new StackAssertionError(`Featurebase upvote API error: ${data.error || 'Failed to toggle upvote'}`, { data }); + throw new HexclaveAssertionError(`Featurebase upvote API error: ${data.error || 'Failed to toggle upvote'}`, { data }); } return { diff --git a/apps/backend/src/app/api/latest/internal/feature-requests/route.tsx b/apps/backend/src/app/api/latest/internal/feature-requests/route.tsx index c43c50c21f..3370bae24a 100644 --- a/apps/backend/src/app/api/latest/internal/feature-requests/route.tsx +++ b/apps/backend/src/app/api/latest/internal/feature-requests/route.tsx @@ -1,7 +1,7 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { getOrCreateFeaturebaseUserFromAuth, requireFeaturebaseApiKey } from "@/lib/featurebase"; import { adaptSchema, yupArray, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; // Typed subset of the Featurebase v2 API responses; fields we don't use are omitted. // The response schema validated by yup on output acts as the runtime safety net. @@ -75,7 +75,7 @@ export const GET = createSmartRouteHandler({ const data: FeaturebaseListResponse = await response.json(); if (!response.ok) { - throw new StackAssertionError(`Featurebase API error: ${data.error || 'Failed to fetch feature requests'}`, { + throw new HexclaveAssertionError(`Featurebase API error: ${data.error || 'Failed to fetch feature requests'}`, { details: { response: response, responseData: data, @@ -195,7 +195,7 @@ export const POST = createSmartRouteHandler({ const data = await response.json(); if (!response.ok) { - throw new StackAssertionError(`Featurebase API error: ${data.error || 'Failed to create feature request'}`, { data }); + throw new HexclaveAssertionError(`Featurebase API error: ${data.error || 'Failed to create feature request'}`, { data }); } return { diff --git a/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx b/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx index 7c85618c0b..ed6a8c5458 100644 --- a/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx +++ b/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx @@ -25,7 +25,7 @@ import { yupString, } from "@stackframe/stack-shared/dist/schema-fields"; import { generateSecureRandomString } from "@stackframe/stack-shared/dist/utils/crypto"; -import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids"; import fs from "fs/promises"; import * as path from "path"; @@ -40,7 +40,10 @@ function isProjectOnboardingStatus(value: string): value is ProjectOnboardingSta function deriveDisplayLabel(absoluteFilePath: string): string { const base = path.basename(absoluteFilePath); - if (base.toLowerCase() === "stack.config.ts") { + // Hexclave rebrand: recognize both the new `hexclave.config.ts` filename and + // the legacy `stack.config.ts` so the directory name is used as the label. + const lowerBase = base.toLowerCase(); + if (lowerBase === "hexclave.config.ts" || lowerBase === "stack.config.ts") { return path.basename(path.dirname(absoluteFilePath)) || base; } return base; @@ -62,7 +65,7 @@ async function assertLocalEmulatorOwnerTeamReadiness() { }, }); if (!ownerTeam) { - throw new StackAssertionError("Local emulator owner team is missing. Run the seed script before requesting local emulator project credentials."); + throw new HexclaveAssertionError("Local emulator owner team is missing. Run the seed script before requesting local emulator project credentials."); } const ownerMembership = await internalPrisma.teamMember.findUnique({ @@ -78,7 +81,7 @@ async function assertLocalEmulatorOwnerTeamReadiness() { }, }); if (!ownerMembership) { - throw new StackAssertionError("Local emulator user is not a member of the local emulator owner team. Run the seed script before requesting local emulator project credentials."); + throw new HexclaveAssertionError("Local emulator user is not a member of the local emulator owner team. Run the seed script before requesting local emulator project credentials."); } } @@ -173,7 +176,7 @@ async function getOrCreateCredentials(projectId: string) { }); if (!keySet.publishableClientKey || !keySet.secretServerKey || !keySet.superSecretAdminKey) { - throw new StackAssertionError("Local emulator key set is missing required keys.", { + throw new HexclaveAssertionError("Local emulator key set is missing required keys.", { projectId, keySetId: keySet.id, }); @@ -206,10 +209,10 @@ async function syncLocalEmulatorOnboardingStatus(projectId: string, showOnboardi `); const row = rows.length > 0 ? rows[0] : undefined; if (!row) { - throw new StackAssertionError("Local emulator project not found while syncing onboarding state.", { projectId }); + throw new HexclaveAssertionError("Local emulator project not found while syncing onboarding state.", { projectId }); } if (!isProjectOnboardingStatus(row.onboardingStatus)) { - throw new StackAssertionError("Project onboarding status in DB is invalid.", { + throw new HexclaveAssertionError("Project onboarding status in DB is invalid.", { projectId, onboardingStatus: row.onboardingStatus, }); @@ -301,9 +304,31 @@ export const POST = createSmartRouteHandler({ } const looksLikeConfigFile = /\.(ts|js|mjs)$/i.test(inputPath); - const absoluteFilePath = (inputStat?.isDirectory() || (!inputStat && !looksLikeConfigFile)) - ? path.join(inputPath, "stack.config.ts") - : inputPath; + let absoluteFilePath: string; + if (inputStat?.isDirectory() || (!inputStat && !looksLikeConfigFile)) { + // Hexclave rebrand: prefer the new `hexclave.config.ts` filename inside + // the directory; fall back to the legacy `stack.config.ts` when that one + // exists on disk so existing projects keep working without migration. + const hexclaveCandidate = path.join(inputPath, "hexclave.config.ts"); + const legacyCandidate = path.join(inputPath, "stack.config.ts"); + let hexclaveExists = false; + try { + await fs.access(resolveEmulatorPath(hexclaveCandidate)); + hexclaveExists = true; + } catch { + hexclaveExists = false; + } + let legacyExists = false; + try { + await fs.access(resolveEmulatorPath(legacyCandidate)); + legacyExists = true; + } catch { + legacyExists = false; + } + absoluteFilePath = (!hexclaveExists && legacyExists) ? legacyCandidate : hexclaveCandidate; + } else { + absoluteFilePath = inputPath; + } const resolvedFilePath = resolveEmulatorPath(absoluteFilePath); let fileExists: boolean; diff --git a/apps/backend/src/app/api/latest/internal/metrics/route.tsx b/apps/backend/src/app/api/latest/internal/metrics/route.tsx index 959a60aad7..285a5a7fef 100644 --- a/apps/backend/src/app/api/latest/internal/metrics/route.tsx +++ b/apps/backend/src/app/api/latest/internal/metrics/route.tsx @@ -20,7 +20,7 @@ import { MetricsPaymentsOverviewSchema, MetricsRecentUserSchema, } from "@stackframe/stack-shared/dist/interface/admin-metrics"; -import { captureError, StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { captureError, HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { adaptSchema, adminAuthTypeSchema, yupArray, yupNumber, yupObject, yupRecord, yupString } from "@stackframe/stack-shared/dist/schema-fields"; import { userFullInclude, userPrismaToCrud, usersCrudHandlers } from "../../users/crud"; @@ -305,7 +305,7 @@ async function loadLiveUsersCount( const captureId = error instanceof ClickHouseError ? "internal-metrics-load-live-users-count-clickhouse-error" : "internal-metrics-load-live-users-count-unexpected-error"; - captureError(captureId, new StackAssertionError( + captureError(captureId, new HexclaveAssertionError( "Failed to load live users count for internal metrics.", { cause: error, @@ -623,7 +623,7 @@ async function loadAnonymousVisitorsFromTokenRefresh( const captureId = error instanceof ClickHouseError ? "internal-metrics-load-anonymous-visitors-fallback-clickhouse-error" : "internal-metrics-load-anonymous-visitors-fallback-unexpected-error"; - captureError(captureId, new StackAssertionError( + captureError(captureId, new HexclaveAssertionError( "Failed to load anonymous visitors fallback for internal metrics.", { cause: error, @@ -678,7 +678,7 @@ async function loadMonthlyActiveUsers(tenancy: Tenancy, now: Date, includeAnonym if (!(error instanceof ClickHouseError)) { throw error; } - captureError("internal-metrics-load-monthly-active-users-failed", new StackAssertionError( + captureError("internal-metrics-load-monthly-active-users-failed", new HexclaveAssertionError( "Failed to load monthly active users for internal metrics.", { cause: error, @@ -960,7 +960,7 @@ async function loadEmailOverview(tenancy: Tenancy, now: Date) { } default: { const _exhaustiveCheck: never = status; - captureError("internal-metrics-unknown-email-simple-status", new StackAssertionError( + captureError("internal-metrics-unknown-email-simple-status", new HexclaveAssertionError( `Unknown EmailOutboxSimpleStatus value: ${String(_exhaustiveCheck)}`, { status: _exhaustiveCheck }, )); @@ -1301,7 +1301,7 @@ async function loadAnalyticsOverview(tenancy: Tenancy, now: Date, includeAnonymo if (!(error instanceof ClickHouseError)) { throw error; } - captureError("internal-metrics-analytics-overview-clickhouse-fallback", new StackAssertionError( + captureError("internal-metrics-analytics-overview-clickhouse-fallback", new HexclaveAssertionError( "Falling back to empty analytics overview due to ClickHouse query failure.", { cause: error, diff --git a/apps/backend/src/app/api/latest/internal/payments/method-configs/route.ts b/apps/backend/src/app/api/latest/internal/payments/method-configs/route.ts index 7f77934c8c..94375080e8 100644 --- a/apps/backend/src/app/api/latest/internal/payments/method-configs/route.ts +++ b/apps/backend/src/app/api/latest/internal/payments/method-configs/route.ts @@ -5,7 +5,7 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { KnownErrors } from "@stackframe/stack-shared"; import { getAllPaymentMethodIds, getAllPaymentMethodNames, getPaymentMethodName, isKnownPaymentMethod } from "@stackframe/stack-shared/dist/payments/payment-methods"; import { adaptSchema, adminAuthTypeSchema, yupArray, yupBoolean, yupNumber, yupObject, yupRecord, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { stringCompare } from "@stackframe/stack-shared/dist/utils/strings"; const METADATA_FIELDS = new Set([ @@ -71,7 +71,7 @@ export const GET = createSmartRouteHandler({ const platformConfig = configs.data.find(c => c.application || c.parent); const defaultConfig = platformConfig || configs.data.find(c => c.is_default); if (!defaultConfig) { - throw new StackAssertionError("No payment method configuration found for Stripe account", { + throw new HexclaveAssertionError("No payment method configuration found for Stripe account", { stripeAccountId: project.stripeAccountId, configCount: configs.data.length, }); diff --git a/apps/backend/src/app/api/latest/internal/payments/test-mode-purchase-session/route.tsx b/apps/backend/src/app/api/latest/internal/payments/test-mode-purchase-session/route.tsx index a476350362..63d0da1562 100644 --- a/apps/backend/src/app/api/latest/internal/payments/test-mode-purchase-session/route.tsx +++ b/apps/backend/src/app/api/latest/internal/payments/test-mode-purchase-session/route.tsx @@ -6,7 +6,7 @@ import { getPrismaClientForTenancy } from "@/prisma-client"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { KnownErrors } from "@stackframe/stack-shared"; import { yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; export const POST = createSmartRouteHandler({ metadata: { @@ -29,7 +29,7 @@ export const POST = createSmartRouteHandler({ const tenancy = await getTenancy(data.tenancyId); if (!tenancy) { - throw new StackAssertionError("Tenancy not found for test mode purchase session"); + throw new HexclaveAssertionError("Tenancy not found for test mode purchase session"); } if (tenancy.config.payments.blockNewPurchases) { throw new KnownErrors.NewPurchasesBlocked(); diff --git a/apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx b/apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx index 0ef5401b38..2217581e6c 100644 --- a/apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx +++ b/apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx @@ -17,7 +17,7 @@ import { KnownErrors } from "@stackframe/stack-shared/dist/known-errors"; import { adaptSchema, adminAuthTypeSchema, moneyAmountSchema, productSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; import { moneyAmountToStripeUnits } from "@stackframe/stack-shared/dist/utils/currencies"; import { SUPPORTED_CURRENCIES, type MoneyAmount } from "@stackframe/stack-shared/dist/utils/currency-constants"; -import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import type Stripe from "stripe"; import { InferType } from "yup"; @@ -51,7 +51,7 @@ export function buildStripeRefundParams(args: { */ function stripeUnitsToMoneyAmount(stripeUnits: number): string { if (!Number.isFinite(stripeUnits) || Math.trunc(stripeUnits) !== stripeUnits) { - throw new StackAssertionError("Stripe units must be an integer", { stripeUnits }); + throw new HexclaveAssertionError("Stripe units must be an integer", { stripeUnits }); } const absolute = Math.abs(stripeUnits); const decimals = USD_CURRENCY.decimals; @@ -77,7 +77,7 @@ function getTotalUsdStripeUnits(options: { throw new KnownErrors.SchemaError("Refunds are only supported for USD-priced purchases."); } if (!Number.isFinite(options.quantity) || Math.trunc(options.quantity) !== options.quantity) { - throw new StackAssertionError("Purchase quantity is not an integer", { quantity: options.quantity }); + throw new HexclaveAssertionError("Purchase quantity is not an integer", { quantity: options.quantity }); } return moneyAmountToStripeUnits(usdPrice as MoneyAmount, USD_CURRENCY) * options.quantity; } @@ -365,15 +365,15 @@ async function resolveInvoicePaymentIntentId(stripe: Stripe, stripeInvoiceId: st const invoice = await stripe.invoices.retrieve(stripeInvoiceId, { expand: ["payments"] }); const payments = invoice.payments?.data; if (!payments || payments.length === 0) { - throw new StackAssertionError("Invoice has no payments", { stripeInvoiceId }); + throw new HexclaveAssertionError("Invoice has no payments", { stripeInvoiceId }); } const paidPayment = payments.find((payment) => payment.status === "paid"); if (!paidPayment) { - throw new StackAssertionError("Invoice has no paid payment", { stripeInvoiceId }); + throw new HexclaveAssertionError("Invoice has no paid payment", { stripeInvoiceId }); } const paymentIntentId = paidPayment.payment.payment_intent; if (!paymentIntentId || typeof paymentIntentId !== "string") { - throw new StackAssertionError("Payment has no payment intent", { stripeInvoiceId }); + throw new HexclaveAssertionError("Payment has no payment intent", { stripeInvoiceId }); } return paymentIntentId; } @@ -646,7 +646,7 @@ async function handleSubscriptionRefund(options: { throw new KnownErrors.SubscriptionInvoiceNotFound(subscription.id); } if (startInvoices.length > 1) { - throw new StackAssertionError("Multiple subscription creation invoices found for subscription", { subscriptionId: subscription.id }); + throw new HexclaveAssertionError("Multiple subscription creation invoices found for subscription", { subscriptionId: subscription.id }); } const startInvoice = startInvoices[0]; invoice = { id: startInvoice.id, stripeInvoiceId: startInvoice.stripeInvoiceId, amountTotal: startInvoice.amountTotal }; @@ -971,7 +971,7 @@ async function handleOneTimePurchaseRefund(options: { // ── Stripe side ─────────────────────────────────────────────────────── if (options.amountStripeUnits > 0 && !isTestMode) { if (!purchase.stripePaymentIntentId) { - throw new StackAssertionError("Live-mode one-time purchase missing stripePaymentIntentId", { purchaseId: purchase.id }); + throw new HexclaveAssertionError("Live-mode one-time purchase missing stripePaymentIntentId", { purchaseId: purchase.id }); } const stripe = await getStripeForAccount({ tenancy }); await stripe.refunds.create( diff --git a/apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx b/apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx index 06a8d561a7..4e4616fdd3 100644 --- a/apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx +++ b/apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx @@ -8,7 +8,7 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { TRANSACTION_TYPES, transactionSchema, type Transaction, type TransactionEntry, type TransactionType } from "@stackframe/stack-shared/dist/interface/crud/transactions"; import { adaptSchema, adminAuthTypeSchema, yupArray, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; import { SUPPORTED_CURRENCIES } from "@stackframe/stack-shared/dist/utils/currency-constants"; -import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; const schema = paymentsSchema; @@ -114,7 +114,7 @@ function isRecord(value: unknown): value is Record { function readLedgerTransactionRow(rowData: unknown): LedgerTransactionRow { if (!isRecord(rowData)) { - throw new StackAssertionError("Ledger transaction rowData is not an object", { rowData }); + throw new HexclaveAssertionError("Ledger transaction rowData is not an object", { rowData }); } const txnId = Reflect.get(rowData, "txnId"); const type = Reflect.get(rowData, "type"); @@ -127,10 +127,10 @@ function readLedgerTransactionRow(rowData: unknown): LedgerTransactionRow { const refundedAtMillis = refundedAtMillisValue === undefined ? null : refundedAtMillisValue; if (typeof txnId !== "string" || txnId.length === 0) { - throw new StackAssertionError("Ledger transaction row is missing txnId", { rowData }); + throw new HexclaveAssertionError("Ledger transaction row is missing txnId", { rowData }); } if (typeof customerId !== "string" || customerId.length === 0) { - throw new StackAssertionError("Ledger transaction row is missing customerId", { rowData }); + throw new HexclaveAssertionError("Ledger transaction row is missing customerId", { rowData }); } if ( type !== "subscription-start" && @@ -139,22 +139,22 @@ function readLedgerTransactionRow(rowData: unknown): LedgerTransactionRow { type !== "subscription-renewal" && type !== "refund" ) { - throw new StackAssertionError("Unexpected ledger transaction type", { rowData }); + throw new HexclaveAssertionError("Unexpected ledger transaction type", { rowData }); } if (typeof effectiveAtMillis !== "number" || !Number.isInteger(effectiveAtMillis) || effectiveAtMillis < 0) { - throw new StackAssertionError("Ledger transaction row has invalid effectiveAtMillis", { rowData }); + throw new HexclaveAssertionError("Ledger transaction row has invalid effectiveAtMillis", { rowData }); } if (typeof createdAtMillis !== "number" || !Number.isInteger(createdAtMillis) || createdAtMillis < 0) { - throw new StackAssertionError("Ledger transaction row has invalid createdAtMillis", { rowData }); + throw new HexclaveAssertionError("Ledger transaction row has invalid createdAtMillis", { rowData }); } if (!Array.isArray(entries)) { - throw new StackAssertionError("Ledger transaction row has invalid entries", { rowData }); + throw new HexclaveAssertionError("Ledger transaction row has invalid entries", { rowData }); } if (paymentProvider !== null && paymentProvider !== "test_mode" && paymentProvider !== "stripe") { - throw new StackAssertionError("Ledger transaction row has invalid paymentProvider", { rowData }); + throw new HexclaveAssertionError("Ledger transaction row has invalid paymentProvider", { rowData }); } if (refundedAtMillis !== null && (typeof refundedAtMillis !== "number" || !Number.isInteger(refundedAtMillis) || refundedAtMillis < 0)) { - throw new StackAssertionError("Ledger transaction row has invalid refundedAtMillis", { rowData }); + throw new HexclaveAssertionError("Ledger transaction row has invalid refundedAtMillis", { rowData }); } return { @@ -173,19 +173,19 @@ function readLedgerTransactionRow(rowData: unknown): LedgerTransactionRow { function parseSourceId(row: LedgerTransactionRow): string { if (row.type === "subscription-start") { if (!row.txnId.startsWith("sub-start:")) { - throw new StackAssertionError("subscription-start transaction id has invalid prefix", { txnId: row.txnId }); + throw new HexclaveAssertionError("subscription-start transaction id has invalid prefix", { txnId: row.txnId }); } return row.txnId.slice("sub-start:".length); } if (row.type === "one-time-purchase") { if (!row.txnId.startsWith("otp:")) { - throw new StackAssertionError("one-time-purchase transaction id has invalid prefix", { txnId: row.txnId }); + throw new HexclaveAssertionError("one-time-purchase transaction id has invalid prefix", { txnId: row.txnId }); } return row.txnId.slice("otp:".length); } if (row.type === "manual-item-quantity-change") { if (!row.txnId.startsWith("miqc:")) { - throw new StackAssertionError("manual-item-quantity-change transaction id has invalid prefix", { txnId: row.txnId }); + throw new HexclaveAssertionError("manual-item-quantity-change transaction id has invalid prefix", { txnId: row.txnId }); } return row.txnId.slice("miqc:".length); } @@ -197,7 +197,7 @@ function parseSourceId(row: LedgerTransactionRow): string { return row.txnId; } if (!row.txnId.startsWith("sub-renewal:")) { - throw new StackAssertionError("subscription-renewal transaction id has invalid prefix", { txnId: row.txnId }); + throw new HexclaveAssertionError("subscription-renewal transaction id has invalid prefix", { txnId: row.txnId }); } return row.txnId.slice("sub-renewal:".length); } @@ -206,12 +206,12 @@ function readCustomerType(value: unknown, context: string): "user" | "team" | "c if (value === "user" || value === "team" || value === "custom") { return value; } - throw new StackAssertionError(`Invalid customerType for ${context}`, { value }); + throw new HexclaveAssertionError(`Invalid customerType for ${context}`, { value }); } function readDayInterval(value: unknown, context: string): [number, "day" | "week" | "month" | "year"] { if (!Array.isArray(value) || value.length !== 2) { - throw new StackAssertionError(`Invalid day interval for ${context}`, { value }); + throw new HexclaveAssertionError(`Invalid day interval for ${context}`, { value }); } const count = value[0]; const unit = value[1]; @@ -221,7 +221,7 @@ function readDayInterval(value: unknown, context: string): [number, "day" | "wee count < 0 || (unit !== "day" && unit !== "week" && unit !== "month" && unit !== "year") ) { - throw new StackAssertionError(`Invalid day interval for ${context}`, { value }); + throw new HexclaveAssertionError(`Invalid day interval for ${context}`, { value }); } return [count, unit]; } @@ -230,22 +230,22 @@ type InlineProduct = Extract["produ function mapProductSnapshotToInlineProduct(product: unknown): InlineProduct { if (!isRecord(product)) { - throw new StackAssertionError("Invalid product snapshot", { product }); + throw new HexclaveAssertionError("Invalid product snapshot", { product }); } const customerType = readCustomerType(product.customerType, "product snapshot"); const includedItemsRaw = product.includedItems; if (!isRecord(includedItemsRaw)) { - throw new StackAssertionError("Invalid includedItems in product snapshot", { product }); + throw new HexclaveAssertionError("Invalid includedItems in product snapshot", { product }); } const includedItems: InlineProduct["included_items"] = {}; for (const [itemId, value] of Object.entries(includedItemsRaw)) { if (!isRecord(value)) { - throw new StackAssertionError("Invalid included item config", { itemId, value }); + throw new HexclaveAssertionError("Invalid included item config", { itemId, value }); } const quantity = value.quantity; if (typeof quantity !== "number") { - throw new StackAssertionError("Invalid included item quantity", { itemId, value }); + throw new HexclaveAssertionError("Invalid included item quantity", { itemId, value }); } const repeat = value.repeat; const parsedRepeat = @@ -262,7 +262,7 @@ function mapProductSnapshotToInlineProduct(product: unknown): InlineProduct { expires !== "when-purchase-expires" && expires !== "when-repeated" ) { - throw new StackAssertionError("Invalid included item expires value", { itemId, value }); + throw new HexclaveAssertionError("Invalid included item expires value", { itemId, value }); } includedItems[itemId] = { quantity, @@ -273,11 +273,11 @@ function mapProductSnapshotToInlineProduct(product: unknown): InlineProduct { const prices: InlineProduct["prices"] = {}; if (!isRecord(product.prices)) { - throw new StackAssertionError("Invalid prices in product snapshot", { product }); + throw new HexclaveAssertionError("Invalid prices in product snapshot", { product }); } for (const [priceId, value] of Object.entries(product.prices)) { if (!isRecord(value)) { - throw new StackAssertionError("Invalid price config in product snapshot", { priceId, value }); + throw new HexclaveAssertionError("Invalid price config in product snapshot", { priceId, value }); } const mappedPrice: InlineProduct["prices"][string] = {}; for (const currency of SUPPORTED_CURRENCIES) { @@ -337,25 +337,25 @@ type LedgerItemQuantityChangeEntry = { function readProductGrantEntry(entry: Record): LedgerProductGrantEntry { if (typeof entry.customerId !== "string") { - throw new StackAssertionError("Invalid product-grant customerId", { entry }); + throw new HexclaveAssertionError("Invalid product-grant customerId", { entry }); } if (entry.productId !== null && typeof entry.productId !== "string") { - throw new StackAssertionError("Invalid product-grant productId", { entry }); + throw new HexclaveAssertionError("Invalid product-grant productId", { entry }); } if (!isRecord(entry.product)) { - throw new StackAssertionError("Invalid product-grant product snapshot", { entry }); + throw new HexclaveAssertionError("Invalid product-grant product snapshot", { entry }); } if (typeof entry.quantity !== "number") { - throw new StackAssertionError("Invalid product-grant quantity", { entry }); + throw new HexclaveAssertionError("Invalid product-grant quantity", { entry }); } if (entry.priceId !== undefined && entry.priceId !== null && typeof entry.priceId !== "string") { - throw new StackAssertionError("Invalid product-grant priceId", { entry }); + throw new HexclaveAssertionError("Invalid product-grant priceId", { entry }); } if (entry.subscriptionId !== undefined && entry.subscriptionId !== null && typeof entry.subscriptionId !== "string") { - throw new StackAssertionError("Invalid product-grant subscriptionId", { entry }); + throw new HexclaveAssertionError("Invalid product-grant subscriptionId", { entry }); } if (entry.oneTimePurchaseId !== undefined && entry.oneTimePurchaseId !== null && typeof entry.oneTimePurchaseId !== "string") { - throw new StackAssertionError("Invalid product-grant oneTimePurchaseId", { entry }); + throw new HexclaveAssertionError("Invalid product-grant oneTimePurchaseId", { entry }); } return { type: "product-grant", @@ -372,10 +372,10 @@ function readProductGrantEntry(entry: Record): LedgerProductGra function readMoneyTransferEntry(entry: Record): LedgerMoneyTransferEntry { if (typeof entry.customerId !== "string") { - throw new StackAssertionError("Invalid money-transfer customerId", { entry }); + throw new HexclaveAssertionError("Invalid money-transfer customerId", { entry }); } if (!isRecord(entry.chargedAmount)) { - throw new StackAssertionError("Invalid money-transfer chargedAmount", { entry }); + throw new HexclaveAssertionError("Invalid money-transfer chargedAmount", { entry }); } const chargedAmount: Record = {}; @@ -395,7 +395,7 @@ function readMoneyTransferEntry(entry: Record): LedgerMoneyTran function readItemQuantityChangeEntry(entry: Record): LedgerItemQuantityChangeEntry { if (typeof entry.customerId !== "string" || typeof entry.itemId !== "string" || typeof entry.quantity !== "number") { - throw new StackAssertionError("Invalid item-quantity-change entry", { entry }); + throw new HexclaveAssertionError("Invalid item-quantity-change entry", { entry }); } return { @@ -455,11 +455,11 @@ function mapItemQuantityChangeEntry(entry: LedgerItemQuantityChangeEntry): Extra function mapLedgerEntry(entry: unknown): TransactionEntry | null { if (!isRecord(entry)) { - throw new StackAssertionError("Invalid ledger entry value", { entry }); + throw new HexclaveAssertionError("Invalid ledger entry value", { entry }); } const type = entry.type; if (typeof type !== "string") { - throw new StackAssertionError("Missing ledger entry type", { entry }); + throw new HexclaveAssertionError("Missing ledger entry type", { entry }); } if (type === "money-transfer") { @@ -482,7 +482,7 @@ function mapLedgerEntry(entry: unknown): TransactionEntry | null { adjustedEntryIndex < 0 || typeof quantity !== "number" ) { - throw new StackAssertionError("Invalid product-revocation entry", { entry }); + throw new HexclaveAssertionError("Invalid product-revocation entry", { entry }); } return { type: "product_revocation", @@ -502,7 +502,7 @@ function mapLedgerEntry(entry: unknown): TransactionEntry | null { adjustedEntryIndex < 0 || typeof quantity !== "number" ) { - throw new StackAssertionError("Invalid product-revocation-reversal entry", { entry }); + throw new HexclaveAssertionError("Invalid product-revocation-reversal entry", { entry }); } return { type: "product_revocation_reversal", @@ -523,7 +523,7 @@ function mapLedgerEntry(entry: unknown): TransactionEntry | null { return null; } - throw new StackAssertionError("Unexpected ledger entry type", { entry }); + throw new HexclaveAssertionError("Unexpected ledger entry type", { entry }); } function mapLedgerTransactionTypeToApiType(type: LedgerTransactionType): Transaction["type"] { @@ -569,11 +569,11 @@ function buildAdjustedByLookupFromRefundRows(rows: unknown[]): Map(); for (const row of parsedRows) { if (seenTxnIds.has(row.txnId)) { - throw new StackAssertionError("Duplicate transaction id returned from grouped transactions table", { + throw new HexclaveAssertionError("Duplicate transaction id returned from grouped transactions table", { txnId: row.txnId, tenancyId: options.tenancyId, }); diff --git a/apps/backend/src/app/api/latest/internal/projects-metrics/route.tsx b/apps/backend/src/app/api/latest/internal/projects-metrics/route.tsx index e8b2797914..bda19a0bbf 100644 --- a/apps/backend/src/app/api/latest/internal/projects-metrics/route.tsx +++ b/apps/backend/src/app/api/latest/internal/projects-metrics/route.tsx @@ -5,7 +5,7 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { KnownErrors } from "@stackframe/stack-shared"; import { MetricsDataPointsSchema } from "@stackframe/stack-shared/dist/interface/admin-metrics"; import { adaptSchema, clientOrHigherAuthTypeSchema, yupNumber, yupObject, yupRecord, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; const SIGNUPS_WINDOW_DAYS = 30; const ONE_DAY_MS = 24 * 60 * 60 * 1000; @@ -148,7 +148,7 @@ export const GET = createSmartRouteHandler({ ), ]); } catch (cause) { - throw new StackAssertionError("Failed to load project metrics.", { + throw new HexclaveAssertionError("Failed to load project metrics.", { cause, userId: req.auth.user.id, projectIds, diff --git a/apps/backend/src/app/api/latest/internal/projects/crud.tsx b/apps/backend/src/app/api/latest/internal/projects/crud.tsx index c0bb47c61d..ea3fce8741 100644 --- a/apps/backend/src/app/api/latest/internal/projects/crud.tsx +++ b/apps/backend/src/app/api/latest/internal/projects/crud.tsx @@ -7,7 +7,7 @@ import { createCrudHandlers } from "@/route-handlers/crud-handler"; import { KnownErrors } from "@stackframe/stack-shared"; import { adminUserProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; import { projectIdSchema, yupObject } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { isNotNull, typedEntries, typedFromEntries } from "@stackframe/stack-shared/dist/utils/objects"; import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; @@ -58,7 +58,7 @@ export const adminUserProjectsCrudHandlers = createLazyProxy(() => createCrudHan const projects = (await Promise.all(typedEntries(projectsRecord).map(async ([_, project]) => await project))).filter(isNotNull); if (projects.length !== projectIds.length) { - throw new StackAssertionError('Failed to fetch all projects of a user'); + throw new HexclaveAssertionError('Failed to fetch all projects of a user'); } const projectsWithConfig = await Promise.all(projects.map(async (project) => { diff --git a/apps/backend/src/app/api/latest/internal/send-test-email/route.tsx b/apps/backend/src/app/api/latest/internal/send-test-email/route.tsx index a243a71633..7c5f45fa47 100644 --- a/apps/backend/src/app/api/latest/internal/send-test-email/route.tsx +++ b/apps/backend/src/app/api/latest/internal/send-test-email/route.tsx @@ -6,7 +6,7 @@ import { KnownErrors } from "@stackframe/stack-shared"; import { ITEM_IDS } from "@stackframe/stack-shared/dist/plans"; import * as schemaFields from "@stackframe/stack-shared/dist/schema-fields"; import { adaptSchema, adminAuthTypeSchema, emailSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; import { timeout } from "@stackframe/stack-shared/dist/utils/promises"; import { Result } from "@stackframe/stack-shared/dist/utils/results"; @@ -91,7 +91,7 @@ export const POST = createSmartRouteHandler({ } else if (result.error.rawError.code === "EMESSAGE") { errorMessage = "Email server rejected the email: " + result.error.rawError.message; } else { - captureError("send-test-email", new StackAssertionError("Unknown error while sending test email. We should add a better error description for the user.", { + captureError("send-test-email", new HexclaveAssertionError("Unknown error while sending test email. We should add a better error description for the user.", { cause: result.error, recipient_email: body.recipient_email, email_config: body.email_config, diff --git a/apps/backend/src/app/api/latest/internal/send-test-webhook/route.tsx b/apps/backend/src/app/api/latest/internal/send-test-webhook/route.tsx index c0b42dc743..78d5f6b94a 100644 --- a/apps/backend/src/app/api/latest/internal/send-test-webhook/route.tsx +++ b/apps/backend/src/app/api/latest/internal/send-test-webhook/route.tsx @@ -1,7 +1,7 @@ import { getSvixClient } from "@/lib/webhooks"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { adaptSchema, adminAuthTypeSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { captureError, StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { captureError, HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { Result } from "@stackframe/stack-shared/dist/utils/results"; import { MessageStatus } from "svix"; @@ -60,7 +60,7 @@ export const POST = createSmartRouteHandler({ )); if (messageResult.status === "error") { const errorMessage = messageResult.error instanceof Error ? messageResult.error.message : "Unknown error while sending the test webhook."; - captureError("send-test-webhook", new StackAssertionError("Failed to send test webhook", { + captureError("send-test-webhook", new HexclaveAssertionError("Failed to send test webhook", { cause: messageResult.error, project_id: projectId, endpoint_id: body.endpoint_id, diff --git a/apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/chunks/[chunk_id]/events/route.tsx b/apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/chunks/[chunk_id]/events/route.tsx index 3b47cd4cb2..eaa40ada27 100644 --- a/apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/chunks/[chunk_id]/events/route.tsx +++ b/apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/chunks/[chunk_id]/events/route.tsx @@ -2,7 +2,7 @@ import { getPrismaClientForTenancy } from "@/prisma-client"; import { downloadBytes } from "@/s3"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { KnownErrors } from "@stackframe/stack-shared"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { adaptSchema, adminAuthTypeSchema, yupArray, yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; import { promisify } from "node:util"; import { gunzip as gunzipCb } from "node:zlib"; @@ -64,20 +64,20 @@ export const GET = createSmartRouteHandler({ try { parsed = JSON.parse(new TextDecoder().decode(unzipped)); } catch (e) { - throw new StackAssertionError("Failed to decode session replay chunk JSON", { cause: e }); + throw new HexclaveAssertionError("Failed to decode session replay chunk JSON", { cause: e }); } if (typeof parsed !== "object" || parsed === null) { - throw new StackAssertionError("Decoded session replay chunk is not an object"); + throw new HexclaveAssertionError("Decoded session replay chunk is not an object"); } if (parsed.session_replay_id !== sessionReplayId) { - throw new StackAssertionError("Decoded session replay chunk session_replay_id mismatch", { + throw new HexclaveAssertionError("Decoded session replay chunk session_replay_id mismatch", { expected: sessionReplayId, actual: parsed.session_replay_id, }); } if (!Array.isArray(parsed.events)) { - throw new StackAssertionError("Decoded session replay chunk events is not an array"); + throw new HexclaveAssertionError("Decoded session replay chunk events is not an array"); } return { diff --git a/apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/events/route.tsx b/apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/events/route.tsx index 9b743763af..8d542cb2a8 100644 --- a/apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/events/route.tsx +++ b/apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/events/route.tsx @@ -3,7 +3,7 @@ import { getPrismaClientForTenancy } from "@/prisma-client"; import { downloadBytes } from "@/s3"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { KnownErrors } from "@stackframe/stack-shared"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { adaptSchema, adminAuthTypeSchema, yupArray, yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; import { promisify } from "node:util"; import { gunzip as gunzipCb } from "node:zlib"; @@ -117,20 +117,20 @@ export const GET = createSmartRouteHandler({ try { parsed = JSON.parse(new TextDecoder().decode(unzipped)); } catch (e) { - throw new StackAssertionError("Failed to decode session replay chunk JSON", { cause: e }); + throw new HexclaveAssertionError("Failed to decode session replay chunk JSON", { cause: e }); } if (typeof parsed !== "object" || parsed === null) { - throw new StackAssertionError("Decoded session replay chunk is not an object"); + throw new HexclaveAssertionError("Decoded session replay chunk is not an object"); } if (parsed.session_replay_id !== sessionReplayId) { - throw new StackAssertionError("Decoded session replay chunk session_replay_id mismatch", { + throw new HexclaveAssertionError("Decoded session replay chunk session_replay_id mismatch", { expected: sessionReplayId, actual: parsed.session_replay_id, }); } if (!Array.isArray(parsed.events)) { - throw new StackAssertionError("Decoded session replay chunk events is not an array"); + throw new HexclaveAssertionError("Decoded session replay chunk events is not an array"); } events = parsed.events as any[]; } diff --git a/apps/backend/src/app/api/latest/internal/user-activity/route.tsx b/apps/backend/src/app/api/latest/internal/user-activity/route.tsx index c1bc212897..8243429a06 100644 --- a/apps/backend/src/app/api/latest/internal/user-activity/route.tsx +++ b/apps/backend/src/app/api/latest/internal/user-activity/route.tsx @@ -3,7 +3,7 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { ClickHouseError } from "@clickhouse/client"; import { UserActivityResponseBodySchema } from "@stackframe/stack-shared/dist/interface/admin-metrics"; import { adaptSchema, adminAuthTypeSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; // Per-user activity heatmap window. Sized to match the 22×16 dashboard grid // so every cell maps to exactly one day and we never truncate or pad awkwardly @@ -75,7 +75,7 @@ export const GET = createSmartRouteHandler({ if (!(error instanceof ClickHouseError)) { throw error; } - captureError("internal-user-activity-clickhouse-fallback", new StackAssertionError( + captureError("internal-user-activity-clickhouse-fallback", new HexclaveAssertionError( "Failed to load user activity due to ClickHouse query failure.", { cause: error, diff --git a/apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/switch/route.ts b/apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/switch/route.ts index f015004b45..2f6929431d 100644 --- a/apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/switch/route.ts +++ b/apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/switch/route.ts @@ -10,7 +10,7 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { KnownErrors } from "@stackframe/stack-shared"; import { adaptSchema, clientOrHigherAuthTypeSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; import { SUPPORTED_CURRENCIES } from "@stackframe/stack-shared/dist/utils/currency-constants"; -import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { getOrUndefined, typedEntries } from "@stackframe/stack-shared/dist/utils/objects"; import { typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings"; import Stripe from "stripe"; @@ -231,7 +231,7 @@ export const POST = createSmartRouteHandler({ if (existingSub?.stripeSubscriptionId) { const existingStripeSub = await stripe.subscriptions.retrieve(existingSub.stripeSubscriptionId); if (existingStripeSub.items.data.length === 0) { - throw new StackAssertionError("Stripe subscription has no items", { subscriptionId: existingSub.id }); + throw new HexclaveAssertionError("Stripe subscription has no items", { subscriptionId: existingSub.id }); } const existingItem = existingStripeSub.items.data[0]; // Intentional: switching an existing (possibly pre-platform-fee) @@ -323,7 +323,7 @@ export const POST = createSmartRouteHandler({ }); const createdSubscription = created as Stripe.Subscription; if (createdSubscription.items.data.length === 0) { - throw new StackAssertionError("Stripe subscription has no items", { stripeSubscriptionId: createdSubscription.id }); + throw new HexclaveAssertionError("Stripe subscription has no items", { stripeSubscriptionId: createdSubscription.id }); } const createdItem = createdSubscription.items.data[0]; const sanitizedCreateDates = sanitizeStripePeriodDates( diff --git a/apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx b/apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx index a54c3cd592..478da7b6e0 100644 --- a/apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx +++ b/apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx @@ -10,7 +10,7 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { KnownErrors } from "@stackframe/stack-shared"; import { getStripeOneTimeMinAmount } from "@stackframe/stack-shared/dist/payments/stripe-limits"; import { yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError, StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { purchaseUrlVerificationCodeHandler } from "../verification-code-handler"; export const POST = createSmartRouteHandler({ @@ -59,7 +59,7 @@ export const POST = createSmartRouteHandler({ const { data, id: codeId } = await purchaseUrlVerificationCodeHandler.validateCode(full_code); const tenancy = await getTenancy(data.tenancyId); if (!tenancy) { - throw new StackAssertionError("No tenancy found from purchase code data tenancy id. This should never happen."); + throw new HexclaveAssertionError("No tenancy found from purchase code data tenancy id. This should never happen."); } if (tenancy.config.payments.blockNewPurchases) { throw new KnownErrors.NewPurchasesBlocked(); @@ -77,7 +77,7 @@ export const POST = createSmartRouteHandler({ quantity, }); if (!selectedPrice) { - throw new StackAssertionError("Price not resolved for purchase session"); + throw new HexclaveAssertionError("Price not resolved for purchase session"); } // Validate the price amount up-front so a malformed config can't slip past diff --git a/apps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts b/apps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts index 1bf383bbae..3c8c4825ce 100644 --- a/apps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts +++ b/apps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts @@ -6,7 +6,7 @@ import { getPrismaClientForTenancy } from "@/prisma-client"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { KnownErrors } from "@stackframe/stack-shared"; import { inlineProductSchema, urlSchema, yupArray, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { purchaseUrlVerificationCodeHandler } from "../verification-code-handler"; export const POST = createSmartRouteHandler({ @@ -53,7 +53,7 @@ export const POST = createSmartRouteHandler({ const verificationCode = await purchaseUrlVerificationCodeHandler.validateCode(body.full_code); const tenancy = await getTenancy(verificationCode.data.tenancyId); if (!tenancy) { - throw new StackAssertionError(`No tenancy found for given tenancyId`); + throw new HexclaveAssertionError(`No tenancy found for given tenancyId`); } if (body.return_url && !validateRedirectUrl(body.return_url, tenancy)) { throw new KnownErrors.RedirectUrlNotWhitelisted(); diff --git a/apps/backend/src/app/api/latest/users/crud.tsx b/apps/backend/src/app/api/latest/users/crud.tsx index d6d23aa6c3..782fcc9fdd 100644 --- a/apps/backend/src/app/api/latest/users/crud.tsx +++ b/apps/backend/src/app/api/latest/users/crud.tsx @@ -22,7 +22,7 @@ import { userIdOrMeSchema, yupNumber, yupObject, yupString } from "@stackframe/s import { validateBase64Image } from "@stackframe/stack-shared/dist/utils/base64"; import { decodeBase64 } from "@stackframe/stack-shared/dist/utils/bytes"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, StatusError, captureError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError, captureError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { hashPassword, isPasswordHashValid } from "@stackframe/stack-shared/dist/utils/hashes"; import { has } from "@stackframe/stack-shared/dist/utils/objects"; import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; @@ -143,7 +143,7 @@ export const userPrismaToCrud = ( const lastActiveAtMillis = prisma.lastActiveAt.getTime(); const selectedTeamMembers = prisma.teamMembers; if (selectedTeamMembers.length > 1) { - throw new StackAssertionError("User cannot have more than one selected team; this should never happen"); + throw new HexclaveAssertionError("User cannot have more than one selected team; this should never happen"); } // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -242,7 +242,7 @@ async function checkAuthData( if (!data.primaryEmailAuthEnabled) return; if (!data.oldPrimaryEmail || data.oldPrimaryEmail !== data.primaryEmail) { if (!data.primaryEmail) { - throw new StackAssertionError("primary_email_auth_enabled cannot be true without primary_email"); + throw new HexclaveAssertionError("primary_email_auth_enabled cannot be true without primary_email"); } const existingChannelUsedForAuth = await tx.contactChannel.findFirst({ where: { @@ -278,7 +278,7 @@ async function checkAuthUsersSoftLimit(tenancy: Tenancy) { const usage = await getTeamWideNonAnonymousUserCount(billingTeamId); const capacity = await getTeamWideAuthUsersCapacity(billingTeamId); if (usage > capacity) { - captureError("auth-users-plan-soft-limit-exceeded", new StackAssertionError( + captureError("auth-users-plan-soft-limit-exceeded", new HexclaveAssertionError( "Auth users soft limit exceeded for billing team", { ownerTeamId: billingTeamId, usage, capacity, projectId: tenancy.project.id }, )); @@ -377,7 +377,7 @@ export function getUserQuery(projectId: string, branchId: string, userId: string `, postProcess: (queryResult) => { if (queryResult.length !== 1) { - throw new StackAssertionError(`Expected 1 user with id ${userId} in project ${projectId}, got ${queryResult.length}`, { queryResult }); + throw new HexclaveAssertionError(`Expected 1 user with id ${userId} in project ${projectId}, got ${queryResult.length}`, { queryResult }); } const row = queryResult[0].row_data_json; @@ -392,7 +392,7 @@ export function getUserQuery(projectId: string, branchId: string, userId: string if (row.SelectedTeamMember && !row.SelectedTeamMember.Team) { // This seems to happen in production much more often than it should, so let's log some information for debugging - captureError("selected-team-member-and-team-consistency", new StackAssertionError("Selected team member has no team? Ignoring it", { row })); + captureError("selected-team-member-and-team-consistency", new HexclaveAssertionError("Selected team member has no team? Ignoring it", { row })); row.SelectedTeamMember = null; } @@ -817,7 +817,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC }); if (!user) { - throw new StackAssertionError("User was created but not found", newUser); + throw new HexclaveAssertionError("User was created but not found", newUser); } return userPrismaToCrud(user, auth.tenancy.config); @@ -886,7 +886,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC projectUserId: params.user_id, } }); - throw new StackAssertionError("Failed to update team member", { + throw new HexclaveAssertionError("Failed to update team member", { cause: e, tenancy_id: auth.tenancy.id, user_id: params.user_id, @@ -908,7 +908,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC }); if (!oldUser) { - throw new StackAssertionError("User not found"); + throw new HexclaveAssertionError("User not found"); } // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -1134,7 +1134,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC }); if (!primaryEmailChannel) { - throw new StackAssertionError("password is set but primary_email is not set"); + throw new HexclaveAssertionError("password is set but primary_email is not set"); } if (!config.auth.password.allowSignIn) { diff --git a/apps/backend/src/app/health/email/route.tsx b/apps/backend/src/app/health/email/route.tsx index 90eafada78..1a3d983404 100644 --- a/apps/backend/src/app/health/email/route.tsx +++ b/apps/backend/src/app/health/email/route.tsx @@ -3,7 +3,7 @@ import { traceSpan } from "@/utils/telemetry"; import { yupNumber, yupObject, yupString, yupTuple } from "@stackframe/stack-shared/dist/schema-fields"; import { generateSecureRandomString } from "@stackframe/stack-shared/dist/utils/crypto"; import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { wait } from "@stackframe/stack-shared/dist/utils/promises"; type ResendEmail = { @@ -83,7 +83,7 @@ const performSignUp = async (email: string, password: string) => { const responseBody = await response.text(); if (!response.ok) { - throw new StackAssertionError(`Sign-up failed: ${response.status} - ${responseBody}`, { + throw new HexclaveAssertionError(`Sign-up failed: ${response.status} - ${responseBody}`, { responseBody, }); } @@ -128,7 +128,7 @@ const waitForVerificationEmail = async (testEmail: string, useInbucket: boolean) } } - throw new StackAssertionError(`Couldn't find verification email in time limit`, { recipient_email: testEmail, max_poll_attempts: MAX_POLL_ATTEMPTS, poll_interval_ms: POLL_INTERVAL_MS }); + throw new HexclaveAssertionError(`Couldn't find verification email in time limit`, { recipient_email: testEmail, max_poll_attempts: MAX_POLL_ATTEMPTS, poll_interval_ms: POLL_INTERVAL_MS }); }); }; @@ -156,7 +156,7 @@ export const POST = createSmartRouteHandler({ const useInbucket = getEnvVariable("STACK_EMAIL_MONITOR_USE_INBUCKET") === "true"; if (useInbucket && getNodeEnvironment().includes("prod")) { - throw new StackAssertionError("Inbucket is not supported as the email monitor inbox in production"); + throw new HexclaveAssertionError("Inbucket is not supported as the email monitor inbox in production"); } const uniqueId = generateSecureRandomString(); diff --git a/apps/backend/src/app/health/error-handler-debug/endpoint/route.tsx b/apps/backend/src/app/health/error-handler-debug/endpoint/route.tsx index 1d5d116e92..a722469e08 100644 --- a/apps/backend/src/app/health/error-handler-debug/endpoint/route.tsx +++ b/apps/backend/src/app/health/error-handler-debug/endpoint/route.tsx @@ -1,6 +1,6 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; export const dynamic = "force-dynamic"; @@ -11,6 +11,6 @@ export const GET = createSmartRouteHandler({ bodyType: yupString().oneOf(["success"]).defined(), }), handler: async (req) => { - throw new StackAssertionError(`Server debug error thrown successfully!`); + throw new HexclaveAssertionError(`Server debug error thrown successfully!`); }, }); diff --git a/apps/backend/src/app/health/route.tsx b/apps/backend/src/app/health/route.tsx index 5ec7d84f14..ddb63f2733 100644 --- a/apps/backend/src/app/health/route.tsx +++ b/apps/backend/src/app/health/route.tsx @@ -1,5 +1,5 @@ import { globalPrismaClient } from "@/prisma-client"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { NextRequest } from "next/server"; export async function GET(req: NextRequest) { @@ -7,7 +7,7 @@ export async function GET(req: NextRequest) { const project = await globalPrismaClient.project.findFirst({}); if (!project) { - throw new StackAssertionError("No project found"); + throw new HexclaveAssertionError("No project found"); } } diff --git a/apps/backend/src/auto-migrations/index.tsx b/apps/backend/src/auto-migrations/index.tsx index 40ce1bb7f0..bb05cfdb04 100644 --- a/apps/backend/src/auto-migrations/index.tsx +++ b/apps/backend/src/auto-migrations/index.tsx @@ -1,6 +1,6 @@ import { Prisma, PrismaClient } from '@/generated/prisma/client'; import { sqlQuoteIdent, sqlQuoteIdentToString } from '@/prisma-client'; -import { StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors'; +import { HexclaveAssertionError } from '@stackframe/stack-shared/dist/utils/errors'; import { wait } from '@stackframe/stack-shared/dist/utils/promises'; import { MIGRATION_FILES } from './../generated/migration-files'; @@ -138,7 +138,7 @@ export async function applyMigrations(options: { const isConditionallyRepeatMigration = statement.includes('CONDITIONALLY_REPEAT_MIGRATION_SENTINEL'); if (isConditionallyRepeatMigration && !isSingleStatement) { - throw new StackAssertionError("CONDITIONALLY_REPEAT_MIGRATION_SENTINEL requires SINGLE_STATEMENT_SENTINEL", { statement }); + throw new HexclaveAssertionError("CONDITIONALLY_REPEAT_MIGRATION_SENTINEL requires SINGLE_STATEMENT_SENTINEL", { statement }); } log(` |> Running statement${isSingleStatement ? "" : "s"}${runOutside ? " outside of transaction" : ""}: ${statement.replace(/(\n|\s)/gm, " ").slice(0, 20)}...`); @@ -148,14 +148,14 @@ export async function applyMigrations(options: { const res = await txOrPrismaClient.$queryRaw`${Prisma.raw(statement)}`; if (isConditionallyRepeatMigration) { if (!Array.isArray(res)) { - throw new StackAssertionError("Expected an array as a return value of repeat condition", { res }); + throw new HexclaveAssertionError("Expected an array as a return value of repeat condition", { res }); } if (res.length > 0) { if (!("should_repeat_migration" in res[0])) { - throw new StackAssertionError("Expected should_repeat_migration column in return value of repeat condition", { res }); + throw new HexclaveAssertionError("Expected should_repeat_migration column in return value of repeat condition", { res }); } if (typeof res[0].should_repeat_migration !== 'boolean') { - throw new StackAssertionError("Expected should_repeat_migration column in return value of repeat condition to be a boolean (found: " + typeof res[0].should_repeat_migration + ")", { res }); + throw new HexclaveAssertionError("Expected should_repeat_migration column in return value of repeat condition to be a boolean (found: " + typeof res[0].should_repeat_migration + ")", { res }); } if (res[0].should_repeat_migration) { log(` |> Migration ${migration.migrationName} requested to be repeated. This is normal and *not* indicative of a problem.`); diff --git a/apps/backend/src/instrumentation.ts b/apps/backend/src/instrumentation.ts index 16562290e7..10d256d26e 100644 --- a/apps/backend/src/instrumentation.ts +++ b/apps/backend/src/instrumentation.ts @@ -26,13 +26,13 @@ export async function register() { ], ...getNodeEnvironment() === "development" && getNextRuntime() === "nodejs" ? { traceExporter: new OTLPTraceExporter({ - url: `http://localhost:${getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81")}31/v1/traces`, + 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_STACK_PORT_PREFIX", "81")} (node/nextjs)`; + (globalThis as any).process.title = `stack-backend:${getEnvVariable("NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX", "81")} (node/nextjs)`; // Initialize performance stats collection in development initPerfStats(); diff --git a/apps/backend/src/lib/ai/mcp-logger.ts b/apps/backend/src/lib/ai/mcp-logger.ts index f8a9a3d045..4028b610f2 100644 --- a/apps/backend/src/lib/ai/mcp-logger.ts +++ b/apps/backend/src/lib/ai/mcp-logger.ts @@ -1,5 +1,5 @@ import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; import { DbConnection } from "./spacetimedb-bindings"; import type { LogMcpCallParams } from "./spacetimedb-bindings/types/reducers"; @@ -40,7 +40,7 @@ export async function getConnection(): Promise { export async function getConnectionOrThrow(): Promise { const conn = await getConnection(); if (!conn) { - throw new StackAssertionError("SpacetimeDB connection unavailable"); + throw new HexclaveAssertionError("SpacetimeDB connection unavailable"); } return conn; } diff --git a/apps/backend/src/lib/ai/models.ts b/apps/backend/src/lib/ai/models.ts index 34e8df13a3..5787629903 100644 --- a/apps/backend/src/lib/ai/models.ts +++ b/apps/backend/src/lib/ai/models.ts @@ -1,7 +1,7 @@ import { isLocalEmulatorEnabled } from "@/lib/local-emulator"; import { createOpenRouter } from "@openrouter/ai-sdk-provider"; import { getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; export const MODEL_QUALITIES = ["dumb", "smart", "smartest"] as const; export const MODEL_SPEEDS = ["slow", "fast"] as const; @@ -78,8 +78,8 @@ export function selectModel( isAuthenticated: boolean, directApiKey?: string, ) { - if (!MODEL_QUALITIES.includes(quality)) throw new StackAssertionError("Invalid quality"); - if (!MODEL_SPEEDS.includes(speed)) throw new StackAssertionError("Invalid speed"); + if (!MODEL_QUALITIES.includes(quality)) throw new HexclaveAssertionError("Invalid quality"); + if (!MODEL_SPEEDS.includes(speed)) throw new HexclaveAssertionError("Invalid speed"); const config = MODEL_SELECTION_MATRIX[quality][speed][isAuthenticated ? "authenticated" : "unauthenticated"]; diff --git a/apps/backend/src/lib/bulldozer/db/index.ts b/apps/backend/src/lib/bulldozer/db/index.ts index 4b2a34893f..99ae1abaca 100644 --- a/apps/backend/src/lib/bulldozer/db/index.ts +++ b/apps/backend/src/lib/bulldozer/db/index.ts @@ -1,4 +1,4 @@ -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { deindent } from "@stackframe/stack-shared/dist/utils/strings"; import { BULLDOZER_SORT_HELPERS_SQL } from "./bulldozer-sort-helpers-sql"; @@ -206,7 +206,7 @@ function chooseSafeDollarQuoteTag( return tag; } } - throw new StackAssertionError("Could not find a safe deterministic dollar-quote tag", { tagPrefix }); + throw new HexclaveAssertionError("Could not find a safe deterministic dollar-quote tag", { tagPrefix }); } /** diff --git a/apps/backend/src/lib/bulldozer/db/row-change-trigger-dispatch.ts b/apps/backend/src/lib/bulldozer/db/row-change-trigger-dispatch.ts index 309a25e393..de6b705636 100644 --- a/apps/backend/src/lib/bulldozer/db/row-change-trigger-dispatch.ts +++ b/apps/backend/src/lib/bulldozer/db/row-change-trigger-dispatch.ts @@ -1,4 +1,4 @@ -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { stringCompare } from "@stackframe/stack-shared/dist/utils/strings"; import type { BulldozerExecutionContext } from "./execution-context"; import { getBulldozerExecutionContext } from "./execution-context"; @@ -97,7 +97,7 @@ export function attachRowChangeTriggerMetadata( .map((statement) => statement.outputName) .find((statementOutputName): statementOutputName is string => typeof statementOutputName === "string"); if (outputName == null) { - throw new StackAssertionError("Row change trigger did not produce an output changes table.", { + throw new HexclaveAssertionError("Row change trigger did not produce an output changes table.", { targetTableId: metadata.targetTableId, }); } @@ -223,7 +223,7 @@ export function collectRowChangeTriggerStatements(ctx: BulldozerExecutionContext const missing = [...discoveredTableIds] .filter((tableId) => !topologicalOrder.includes(tableId)) .sort(stringCompare); - throw new StackAssertionError("Cycle detected in trigger dependency graph — topological sort could not order all tables", { + throw new HexclaveAssertionError("Cycle detected in trigger dependency graph — topological sort could not order all tables", { sourceTableId: options.sourceTableId, cyclicTableIds: missing, orderedTableIds: topologicalOrder, @@ -262,7 +262,7 @@ export function collectRowChangeTriggerStatements(ctx: BulldozerExecutionContext statements.push(...execution.statements); if (trigger.targetTableId == null) continue; if (execution.outputChangesTable == null) { - throw new StackAssertionError("Row change trigger did not emit output changes table.", { + throw new HexclaveAssertionError("Row change trigger did not emit output changes table.", { sourceTableId, targetTableId: trigger.targetTableId, }); diff --git a/apps/backend/src/lib/bulldozer/db/tables/concat-table.ts b/apps/backend/src/lib/bulldozer/db/tables/concat-table.ts index 38a57eb55c..6c0baeb947 100644 --- a/apps/backend/src/lib/bulldozer/db/tables/concat-table.ts +++ b/apps/backend/src/lib/bulldozer/db/tables/concat-table.ts @@ -1,4 +1,4 @@ -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { deindent } from "@stackframe/stack-shared/dist/utils/strings"; import type { Table } from ".."; import { getBulldozerExecutionContext } from "../execution-context"; @@ -29,13 +29,13 @@ export function declareConcatTable< }): Table { const tables = [...options.tables]; const firstTable = tables[0] ?? (() => { - throw new StackAssertionError("declareConcatTable requires at least one input table", { tableId: options.tableId }); + throw new HexclaveAssertionError("declareConcatTable requires at least one input table", { tableId: options.tableId }); })(); const referenceCompareGroupKeysSql = firstTable.compareGroupKeys(sqlExpression`$1`, sqlExpression`$2`).sql; for (const table of tables) { const compareGroupKeysSql = table.compareGroupKeys(sqlExpression`$1`, sqlExpression`$2`).sql; if (compareGroupKeysSql !== referenceCompareGroupKeysSql) { - throw new StackAssertionError("declareConcatTable requires group-comparator-compatible input tables", { + throw new HexclaveAssertionError("declareConcatTable requires group-comparator-compatible input tables", { tableId: options.tableId, tableDebugId: tableIdToDebugString(table.tableId), }); @@ -87,7 +87,7 @@ export function declareConcatTable< `; } const groupKey = queryOptions.groupKey ?? (() => { - throw new StackAssertionError("declareConcatTable specific-group query requires a group key"); + throw new HexclaveAssertionError("declareConcatTable specific-group query requires a group key"); })(); return deindent` SELECT diff --git a/apps/backend/src/lib/bulldozer/db/utilities.ts b/apps/backend/src/lib/bulldozer/db/utilities.ts index 6856777094..3453e06327 100644 --- a/apps/backend/src/lib/bulldozer/db/utilities.ts +++ b/apps/backend/src/lib/bulldozer/db/utilities.ts @@ -1,4 +1,4 @@ -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { templateIdentity } from "@stackframe/stack-shared/dist/utils/strings"; const sqlTemplateLiteral = (type: T) => (strings: TemplateStringsArray, ...values: { sql: string }[]) => ({ type, sql: templateIdentity(strings, ...values.map(v => v.sql)) }); @@ -34,7 +34,7 @@ export type TableId = string | { "tableType": "internal", "internalId": string, export function quoteSqlIdentifier(input: string): SqlExpression { if (input.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/) == null) { - throw new StackAssertionError("Invalid SQL identifier", { input }); + throw new HexclaveAssertionError("Invalid SQL identifier", { input }); } return { type: "expression", sql: `"${input}"` }; } diff --git a/apps/backend/src/lib/cache.tsx b/apps/backend/src/lib/cache.tsx index 5dcf77acdf..d35bbb6fea 100644 --- a/apps/backend/src/lib/cache.tsx +++ b/apps/backend/src/lib/cache.tsx @@ -1,6 +1,6 @@ import { Prisma } from "@/generated/prisma/client"; import { PrismaClientTransaction } from "@/prisma-client"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; export type CacheGetOrSetOptions = { namespace: string, @@ -19,10 +19,10 @@ function computeExpiry(ttlMs: number): Date { export async function getOrSetCacheValue(options: CacheGetOrSetOptions): Promise { if (!options.namespace) { - throw new StackAssertionError("Cache namespace must be a non-empty string."); + throw new HexclaveAssertionError("Cache namespace must be a non-empty string."); } if (!options.cacheKey) { - throw new StackAssertionError("Cache key must be a non-empty string."); + throw new HexclaveAssertionError("Cache key must be a non-empty string."); } const existing = await options.prisma.cacheEntry.findUnique({ diff --git a/apps/backend/src/lib/clickhouse-errors.ts b/apps/backend/src/lib/clickhouse-errors.ts index dbac2f2865..680f1719c0 100644 --- a/apps/backend/src/lib/clickhouse-errors.ts +++ b/apps/backend/src/lib/clickhouse-errors.ts @@ -1,5 +1,5 @@ import { getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; -import { captureError, StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { captureError, HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; const SAFE_CLICKHOUSE_ERROR_CODES = [ 62, // SYNTAX_ERROR @@ -23,7 +23,7 @@ const DEFAULT_CLICKHOUSE_ERROR_MESSAGE = "Error during execution of this query." export function getSafeClickhouseErrorMessage(error: unknown, query: string) { if (typeof error !== "object" || error === null || !("code" in error) || typeof error.code !== "string" || isNaN(Number(error.code)) || !("message" in error) || typeof error.message !== "string") { - captureError("unknown-clickhouse-error-for-query-not-clickhouse-error", new StackAssertionError("Unknown error from Clickhouse is not a Clickhouse error", { cause: error, query: query })); + captureError("unknown-clickhouse-error-for-query-not-clickhouse-error", new HexclaveAssertionError("Unknown error from Clickhouse is not a Clickhouse error", { cause: error, query: query })); return DEFAULT_CLICKHOUSE_ERROR_MESSAGE; } @@ -34,7 +34,7 @@ export function getSafeClickhouseErrorMessage(error: unknown, query: string) { } const isKnown = UNSAFE_CLICKHOUSE_ERROR_CODES.includes(errorCode); if (!isKnown) { - captureError("unknown-clickhouse-error-for-query", new StackAssertionError(`Unknown Clickhouse error: code ${errorCode} not in safe or unsafe codes`, { cause: error, query: query })); + captureError("unknown-clickhouse-error-for-query", new HexclaveAssertionError(`Unknown Clickhouse error: code ${errorCode} not in safe or unsafe codes`, { cause: error, query: query })); } if (getNodeEnvironment() === "development" || getNodeEnvironment() === "test") { diff --git a/apps/backend/src/lib/clickhouse.tsx b/apps/backend/src/lib/clickhouse.tsx index 19ff78aeb4..c59b9e1420 100644 --- a/apps/backend/src/lib/clickhouse.tsx +++ b/apps/backend/src/lib/clickhouse.tsx @@ -1,6 +1,6 @@ import { createClient, type ClickHouseClient, type ClickHouseSettings } from "@clickhouse/client"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; function getAdminAuth() { return { @@ -97,14 +97,14 @@ export const getQueryTimingStats = async (client: ClickHouseClient, queryId: str return stats.data[0]; } if (stats.data.length > 1) { - throw new StackAssertionError(`Unexpected number of query log results: ${stats.data.length}`, { data: stats.data }); + throw new HexclaveAssertionError(`Unexpected number of query log results: ${stats.data.length}`, { data: stats.data }); } if (attempt < retryDelaysMs.length) { await new Promise((resolve) => setTimeout(resolve, retryDelaysMs[attempt])); } } - throw new StackAssertionError("Unexpected number of query log results: 0", { data: [] }); + throw new HexclaveAssertionError("Unexpected number of query log results: 0", { data: [] }); }; export const getQueryTimingStatsForProject = async ( @@ -143,7 +143,7 @@ export const getQueryTimingStatsForProject = async ( return stats.data[0]; } if (stats.data.length > 1) { - throw new StackAssertionError(`Unexpected number of query log results: ${stats.data.length}`, { data: stats.data }); + throw new HexclaveAssertionError(`Unexpected number of query log results: ${stats.data.length}`, { data: stats.data }); } if (attempt < retryDelaysMs.length) { await new Promise((resolve) => setTimeout(resolve, retryDelaysMs[attempt])); diff --git a/apps/backend/src/lib/config.tsx b/apps/backend/src/lib/config.tsx index 9c535d095c..82f8aa2c2b 100644 --- a/apps/backend/src/lib/config.tsx +++ b/apps/backend/src/lib/config.tsx @@ -5,7 +5,7 @@ import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/proje import { branchConfigSourceSchema, yupBoolean, yupMixed, yupObject, yupRecord, yupString, yupUnion } from "@stackframe/stack-shared/dist/schema-fields"; import { isTruthy } from "@stackframe/stack-shared/dist/utils/booleans"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; import { filterUndefined, typedEntries } from "@stackframe/stack-shared/dist/utils/objects"; import { Result } from "@stackframe/stack-shared/dist/utils/results"; import { deindent, stringCompare } from "@stackframe/stack-shared/dist/utils/strings"; @@ -138,7 +138,7 @@ export function getProjectConfigOverrideQuery(options: ProjectOptions): RawQuery `, postProcess: async (queryResult) => { if (queryResult.length > 1) { - throw new StackAssertionError(`Expected 0 or 1 project config overrides for project ${options.projectId}, got ${queryResult.length}`, { queryResult }); + throw new HexclaveAssertionError(`Expected 0 or 1 project config overrides for project ${options.projectId}, got ${queryResult.length}`, { queryResult }); } return migrateConfigOverride("project", queryResult[0]?.projectConfigOverride ?? {}); }, @@ -157,7 +157,7 @@ export function getBranchConfigOverrideQuery(options: BranchOptions): RawQuery

{ if (queryResult.length > 1) { - throw new StackAssertionError(`Expected 0 or 1 branch config overrides for project ${options.projectId} and branch ${options.branchId}, got ${queryResult.length}`, { queryResult }); + throw new HexclaveAssertionError(`Expected 0 or 1 branch config overrides for project ${options.projectId} and branch ${options.branchId}, got ${queryResult.length}`, { queryResult }); } return migrateConfigOverride("branch", queryResult[0]?.config ?? {}); }, @@ -204,7 +204,7 @@ export function getEnvironmentConfigOverrideQuery(options: EnvironmentOptions): `, postProcess: async (queryResult) => { if (queryResult.length > 1) { - throw new StackAssertionError(`Expected 0 or 1 environment config overrides for project ${options.projectId} and branch ${options.branchId}, got ${queryResult.length}`, { queryResult }); + throw new HexclaveAssertionError(`Expected 0 or 1 environment config overrides for project ${options.projectId} and branch ${options.branchId}, got ${queryResult.length}`, { queryResult }); } const storedConfigOverride = migrateConfigOverride("environment", queryResult[0]?.config ?? {}); if (queryResult[0]?.isDevelopmentEnvironment === true) { @@ -218,7 +218,7 @@ export function getEnvironmentConfigOverrideQuery(options: EnvironmentOptions): export function getOrganizationConfigOverrideQuery(options: OrganizationOptions): RawQuery> { // fetch organization config from DB (either our own, or the source of truth one) if (!("forUserId" in options) && options.organizationId !== null) { - throw new StackAssertionError('Non-null organization ID is not implemented'); + throw new HexclaveAssertionError('Non-null organization ID is not implemented'); } return { @@ -252,15 +252,15 @@ export async function setProjectConfigOverride(options: { // large configs make our DB slow; let's prevent them early const newConfigString = JSON.stringify(newConfig); if (newConfigString.length > 1_000_000) { - captureError("set-project-config-too-large", new StackAssertionError(`Project config override for ${options.projectId} is ${(newConfigString.length/1_000_000).toFixed(1)}MB long!`)); + captureError("set-project-config-too-large", new HexclaveAssertionError(`Project config override for ${options.projectId} is ${(newConfigString.length/1_000_000).toFixed(1)}MB long!`)); } if (newConfigString.length > 5_000_000) { - throw new StackAssertionError(`Project config override for ${options.projectId} is too large.`); + throw new HexclaveAssertionError(`Project config override for ${options.projectId} is too large.`); } const overrideErrors = await getConfigOverrideErrors(projectConfigSchema, newConfig); if (overrideErrors.status === "error") { - captureError("setProjectConfigOverride", new StackAssertionError(`Config override is invalid — at a place where it should have already been validated! ${overrideErrors.error}`, { projectId: options.projectId })); + captureError("setProjectConfigOverride", new HexclaveAssertionError(`Config override is invalid — at a place where it should have already been validated! ${overrideErrors.error}`, { projectId: options.projectId })); } await globalPrismaClient.project.update({ where: { @@ -290,15 +290,15 @@ export async function setBranchConfigOverride(options: { // large configs make our DB slow; let's prevent them early const newConfigString = JSON.stringify(newConfig); if (newConfigString.length > 1_000_000) { - captureError("set-branch-config-too-large", new StackAssertionError(`Branch config override for ${options.projectId}/${options.branchId} is ${(newConfigString.length/1_000_000).toFixed(1)}MB long!`)); + captureError("set-branch-config-too-large", new HexclaveAssertionError(`Branch config override for ${options.projectId}/${options.branchId} is ${(newConfigString.length/1_000_000).toFixed(1)}MB long!`)); } if (newConfigString.length > 5_000_000) { - throw new StackAssertionError(`Branch config override for ${options.projectId}/${options.branchId} is too large.`); + throw new HexclaveAssertionError(`Branch config override for ${options.projectId}/${options.branchId} is too large.`); } const overrideErrors = await getConfigOverrideErrors(branchConfigSchema, newConfig); if (overrideErrors.status === "error") { - captureError("setBranchConfigOverride", new StackAssertionError(`Config override is invalid — at a place where it should have already been validated! ${overrideErrors.error}`, { projectId: options.projectId, branchId: options.branchId })); + captureError("setBranchConfigOverride", new HexclaveAssertionError(`Config override is invalid — at a place where it should have already been validated! ${overrideErrors.error}`, { projectId: options.projectId, branchId: options.branchId })); } await globalPrismaClient.branchConfigOverride.upsert({ where: { @@ -394,7 +394,7 @@ export async function setEnvironmentConfigOverride(options: { }): Promise { const blockReason = await getEnvironmentConfigWriteBlockReason(options.projectId); if (blockReason != null) { - throw new StackAssertionError(blockReason, { + throw new HexclaveAssertionError(blockReason, { projectId: options.projectId, branchId: options.branchId, }); @@ -405,15 +405,15 @@ export async function setEnvironmentConfigOverride(options: { // large configs make our DB slow; let's prevent them early const newConfigString = JSON.stringify(newConfig); if (newConfigString.length > 1_000_000) { - captureError("set-environment-config-too-large", new StackAssertionError(`Environment config override for ${options.projectId}/${options.branchId} is ${(newConfigString.length/1_000_000).toFixed(1)}MB long!`)); + captureError("set-environment-config-too-large", new HexclaveAssertionError(`Environment config override for ${options.projectId}/${options.branchId} is ${(newConfigString.length/1_000_000).toFixed(1)}MB long!`)); } if (newConfigString.length > 5_000_000) { - throw new StackAssertionError(`Environment config override for ${options.projectId}/${options.branchId} is too large.`); + throw new HexclaveAssertionError(`Environment config override for ${options.projectId}/${options.branchId} is too large.`); } const overrideErrors = await getConfigOverrideErrors(environmentConfigSchema, newConfig); if (overrideErrors.status === "error") { - captureError("setEnvironmentConfigOverride", new StackAssertionError(`Config override is invalid — at a place where it should have already been validated! ${overrideErrors.error}`, { projectId: options.projectId, branchId: options.branchId })); + captureError("setEnvironmentConfigOverride", new HexclaveAssertionError(`Config override is invalid — at a place where it should have already been validated! ${overrideErrors.error}`, { projectId: options.projectId, branchId: options.branchId })); } await globalPrismaClient.environmentConfigOverride.upsert({ where: { @@ -440,7 +440,7 @@ export function setOrganizationConfigOverride(options: { organizationConfigOverride: OrganizationConfigOverride, }): Promise { // save organization config override on DB (either our own, or the source of truth one) - throw new StackAssertionError('Not implemented'); + throw new HexclaveAssertionError('Not implemented'); } @@ -521,7 +521,7 @@ export function overrideOrganizationConfigOverride(options: { organizationConfigOverrideOverride: OrganizationConfigOverrideOverride, }): Promise { // save organization config override on DB (either our own, or the source of truth one) - throw new StackAssertionError('Not implemented'); + throw new HexclaveAssertionError('Not implemented'); } @@ -656,7 +656,7 @@ function makeUnsanitizedIncompleteConfigQuery(options: { previous?: RawQue const over = await overPromise; const overrideErrors = await getConfigOverrideErrors(options.schema, over); if (overrideErrors.status === "error") { - captureError("config-override-validation-error", new StackAssertionError(`Config override is invalid — at a place where it should have already been validated! ${overrideErrors.error}`, { extraInfo: options.extraInfo })); + captureError("config-override-validation-error", new HexclaveAssertionError(`Config override is invalid — at a place where it should have already been validated! ${overrideErrors.error}`, { extraInfo: options.extraInfo })); } return override(prev, over); }, @@ -1086,7 +1086,7 @@ import.meta.vitest?.test('_validateConfigOverrideSchemaImpl(...)', async ({ expe import.meta.vitest?.test('setEnvironmentConfigOverride blocks writes for development environment projects', async ({ expect }) => { const vi = import.meta.vitest?.vi; if (!vi) { - throw new StackAssertionError("Vitest context is required for in-source tests."); + throw new HexclaveAssertionError("Vitest context is required for in-source tests."); } const developmentEnvironment = await import("./development-environment"); diff --git a/apps/backend/src/lib/contact-channel.tsx b/apps/backend/src/lib/contact-channel.tsx index 1a8836a1e8..db5a5a13c1 100644 --- a/apps/backend/src/lib/contact-channel.tsx +++ b/apps/backend/src/lib/contact-channel.tsx @@ -1,6 +1,6 @@ import { BooleanTrue, ContactChannelType } from "@/generated/prisma/client"; import { markProjectUserForExternalDbSync, withExternalDbSyncUpdate } from "@/lib/external-db-sync"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { normalizeEmail } from "./emails"; import { PrismaTransaction } from "./types"; @@ -75,7 +75,7 @@ export async function setContactChannelAsPrimaryById( }); if (!targetChannel) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Contact channel not found with id ${options.contactChannelId} for user ${options.projectUserId} in tenancy ${options.tenancyId}`, { options } ); @@ -83,7 +83,7 @@ export async function setContactChannelAsPrimaryById( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (targetChannel.type !== options.type) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Contact channel type mismatch: expected ${options.type}, got ${targetChannel.type}`, { options, actualType: targetChannel.type } ); diff --git a/apps/backend/src/lib/email-delivery-stats.tsx b/apps/backend/src/lib/email-delivery-stats.tsx index c3d7153cfb..f3cbe36c1b 100644 --- a/apps/backend/src/lib/email-delivery-stats.tsx +++ b/apps/backend/src/lib/email-delivery-stats.tsx @@ -1,7 +1,7 @@ import { Prisma } from "@/generated/prisma/client"; import { globalPrismaClient, PrismaClientTransaction, RawQuery, rawQuery } from "@/prisma-client"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; export type EmailDeliveryWindowStats = { sent: number, @@ -27,7 +27,7 @@ export function calculatePenaltyFactor(sent: number, bounced: number, spam: numb const defaultEmailCapacityPerHour = Number.parseInt(getEnvVariable("STACK_DEFAULT_EMAIL_CAPACITY_PER_HOUR", "200")); if (!Number.isFinite(defaultEmailCapacityPerHour)) { - throw new StackAssertionError(`Invalid STACK_DEFAULT_EMAIL_CAPACITY_PER_HOUR environment variable: ${getEnvVariable("STACK_DEFAULT_EMAIL_CAPACITY_PER_HOUR", "")}`); + throw new HexclaveAssertionError(`Invalid STACK_DEFAULT_EMAIL_CAPACITY_PER_HOUR environment variable: ${getEnvVariable("STACK_DEFAULT_EMAIL_CAPACITY_PER_HOUR", "")}`); } const BOOST_MULTIPLIER = 4; diff --git a/apps/backend/src/lib/email-queue-step.tsx b/apps/backend/src/lib/email-queue-step.tsx index 09e857b2f2..37e7cfe065 100644 --- a/apps/backend/src/lib/email-queue-step.tsx +++ b/apps/backend/src/lib/email-queue-step.tsx @@ -12,7 +12,7 @@ import { allPromisesAndWaitUntilEach } from "@/utils/background-tasks"; import { withTraceSpan } from "@/utils/telemetry"; import { groupBy } from "@stackframe/stack-shared/dist/utils/arrays"; import { getEnvBoolean, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; -import { captureError, errorToNiceString, StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { captureError, errorToNiceString, HexclaveAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { Json } from "@stackframe/stack-shared/dist/utils/json"; import { filterUndefined } from "@stackframe/stack-shared/dist/utils/objects"; import { Result } from "@stackframe/stack-shared/dist/utils/results"; @@ -59,7 +59,8 @@ const appendSendAttemptError =( }; // Track if email queue has run at least once since server start (used to suppress first-run delta warnings in dev) -const emailQueueFirstRunKey = Symbol.for("__stack_email_queue_first_run_completed"); +// Hexclave rebrand: file-private symbol key, renamed outright (no cross-version compat needed). +const emailQueueFirstRunKey = Symbol.for("__hexclave_email_queue_first_run_completed"); async function verifyEmailDeliverability( email: string, @@ -131,7 +132,7 @@ async function retryEmailsStuckInRendering(): Promise { }, }); if (res.length > 0) { - captureError("email-queue-step-stuck-in-rendering", new StackAssertionError(`${res.length} emails stuck in rendering! This should never happen. Resetting them to be re-rendered.`, { + captureError("email-queue-step-stuck-in-rendering", new HexclaveAssertionError(`${res.length} emails stuck in rendering! This should never happen. Resetting them to be re-rendered.`, { emails: res.map(e => e.id), })); } @@ -191,7 +192,7 @@ async function failEmailsStuckInSending(additionalWhere?: Prisma.EmailOutboxWher if (failed.length > 0) { captureError( "email-queue-step-stuck-in-sending", - new StackAssertionError( + new HexclaveAssertionError( `${failed.length} emails were stuck in sending and were marked as failed (terminal server errors; delivery status unknown, not retried to avoid duplicate sends). Manual investigation is recommended.`, { emails: failed.map(({ id, tenancyId, startedSendingAt }) => ({ id, tenancyId, startedSendingAt })) }, ), @@ -271,7 +272,7 @@ async function updateLastExecutionTime(): Promise { // In development, the first run after server start often has a large delta because the server wasn't running console.log(`[email-queue] Skipping delta warning on first run (delta: ${delta.toFixed(2)}s) — this is normal after server restart`); } else { - captureError("email-queue-step-delta-too-large", new StackAssertionError(`Email queue step delta is too large: ${delta}. Either the previous step took too long, or something is wrong.`)); + captureError("email-queue-step-delta-too-large", new HexclaveAssertionError(`Email queue step delta is too large: ${delta}. Either the previous step took too long, or something is wrong.`)); } } (globalThis as any)[emailQueueFirstRunKey] = true; @@ -785,7 +786,7 @@ async function processSingleEmail(context: TenancyProcessingContext, row: EmailO const failureReason = isAttemptsExhausted ? "attempts_exhausted" : "permanent_error"; if (isAttemptsExhausted) { - captureError("email-queue-step-retries-exhausted", new StackAssertionError(`Email failed after ${newAttemptCount} attempts`, { + captureError("email-queue-step-retries-exhausted", new HexclaveAssertionError(`Email failed after ${newAttemptCount} attempts`, { cause: result.error.rawError, emailId: row.id, tenancyId: row.tenancyId, @@ -925,7 +926,7 @@ async function shouldSendEmail( ): Promise { const category = getNotificationCategoryById(categoryId); if (!category) { - throw new StackAssertionError("Invalid notification category id, we should have validated this before calling shouldSendEmail", { categoryId, userId }); + throw new HexclaveAssertionError("Invalid notification category id, we should have validated this before calling shouldSendEmail", { categoryId, userId }); } if (!category.can_disable) { return true; @@ -975,21 +976,21 @@ export function serializeRecipient(recipient: EmailOutboxRecipient): Json { }; } default: { - throw new StackAssertionError("Unknown EmailOutbox recipient type", { recipient }); + throw new HexclaveAssertionError("Unknown EmailOutbox recipient type", { recipient }); } } } export function deserializeRecipient(raw: Json): EmailOutboxRecipient { if (raw === null || typeof raw !== "object" || Array.isArray(raw)) { - throw new StackAssertionError("Malformed EmailOutbox recipient payload", { raw }); + throw new HexclaveAssertionError("Malformed EmailOutbox recipient payload", { raw }); } const base = raw as Record; const type = base.type; if (type === "user-primary-email") { const userId = base.userId; if (typeof userId !== "string") { - throw new StackAssertionError("Expected userId to be present for user-primary-email recipient", { raw }); + throw new HexclaveAssertionError("Expected userId to be present for user-primary-email recipient", { raw }); } return { type, userId }; } @@ -997,16 +998,16 @@ export function deserializeRecipient(raw: Json): EmailOutboxRecipient { const userId = base.userId; const emails = base.emails; if (typeof userId !== "string" || !Array.isArray(emails) || !emails.every((item) => typeof item === "string")) { - throw new StackAssertionError("Invalid user-custom-emails recipient payload", { raw }); + throw new HexclaveAssertionError("Invalid user-custom-emails recipient payload", { raw }); } return { type, userId, emails: emails as string[] }; } if (type === "custom-emails") { const emails = base.emails; if (!Array.isArray(emails) || !emails.every((item) => typeof item === "string")) { - throw new StackAssertionError("Invalid custom-emails recipient payload", { raw }); + throw new HexclaveAssertionError("Invalid custom-emails recipient payload", { raw }); } return { type, emails: emails as string[] }; } - throw new StackAssertionError("Unknown EmailOutbox recipient type", { raw }); + throw new HexclaveAssertionError("Unknown EmailOutbox recipient type", { raw }); } diff --git a/apps/backend/src/lib/email-rendering.tsx b/apps/backend/src/lib/email-rendering.tsx index 425497c793..6344597e4a 100644 --- a/apps/backend/src/lib/email-rendering.tsx +++ b/apps/backend/src/lib/email-rendering.tsx @@ -1,6 +1,6 @@ import { executeJavascript, type ExecuteResult } from '@/lib/js-execution'; import { emptyEmailTheme } from '@stackframe/stack-shared/dist/helpers/emails'; -import { StackAssertionError, captureError } from '@stackframe/stack-shared/dist/utils/errors'; +import { HexclaveAssertionError, captureError } from '@stackframe/stack-shared/dist/utils/errors'; import { bundleJavaScript } from '@stackframe/stack-shared/dist/utils/esbuild'; import { get, has } from '@stackframe/stack-shared/dist/utils/objects'; import { @@ -16,7 +16,7 @@ export function getActiveEmailTheme(tenancy: Tenancy) { const themeList = tenancy.config.emails.themes; const currentActiveTheme = tenancy.config.emails.selectedThemeId; if (!(has(themeList, currentActiveTheme))) { - throw new StackAssertionError("No active email theme found", { + throw new HexclaveAssertionError("No active email theme found", { themeList, currentActiveTheme, }); @@ -128,10 +128,10 @@ export async function renderEmailWithTemplate( const user = (previewMode && !options.user) ? { displayName: "John Doe" } : options.user; const project = (previewMode && !options.project) ? { displayName: "My Project" } : options.project; if (!user) { - throw new StackAssertionError("User is required when not in preview mode", { user, project, variables }); + throw new HexclaveAssertionError("User is required when not in preview mode", { user, project, variables }); } if (!project) { - throw new StackAssertionError("Project is required when not in preview mode", { user, project, variables }); + throw new HexclaveAssertionError("Project is required when not in preview mode", { user, project, variables }); } // Process editable markers if requested @@ -152,7 +152,7 @@ export async function renderEmailWithTemplate( } catch (e) { // If transpilation fails, fall back to original source // This can happen with complex or invalid JSX - captureError("email-transpilation-template-error", new StackAssertionError( + captureError("email-transpilation-template-error", new HexclaveAssertionError( "Failed to transpile template for editable markers", { cause: e } )); @@ -167,7 +167,7 @@ export async function renderEmailWithTemplate( editableRegions = { ...editableRegions, ...themeResult.editableRegions }; } catch (e) { // If transpilation fails, fall back to original source - captureError("email-transpilation-theme-error", new StackAssertionError( + captureError("email-transpilation-theme-error", new HexclaveAssertionError( "Failed to transpile theme for editable markers", { cause: e } )); diff --git a/apps/backend/src/lib/email-template-rewrite.ts b/apps/backend/src/lib/email-template-rewrite.ts index 3960dc4b36..9d9dcf3f40 100644 --- a/apps/backend/src/lib/email-template-rewrite.ts +++ b/apps/backend/src/lib/email-template-rewrite.ts @@ -1,7 +1,7 @@ import { renderEmailWithTemplate } from "@/lib/email-rendering"; import { emptyEmailTheme } from "@stackframe/stack-shared/dist/helpers/emails"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; import { Result } from "@stackframe/stack-shared/dist/utils/results"; const MOCK_API_KEY_SENTINEL = "mock-openrouter-api-key"; @@ -146,7 +146,7 @@ export async function rewriteTemplateSourceWithAI(templateTsxSource: string): Pr lastError = renderResult.error; } - captureError("email-template-rewrite-failed-after-retries", new StackAssertionError( + captureError("email-template-rewrite-failed-after-retries", new HexclaveAssertionError( "Template rewrite failed after all retries", { isMockMode: isMockMode(), diff --git a/apps/backend/src/lib/emailable.tsx b/apps/backend/src/lib/emailable.tsx index bd41e79d9f..14d8f5d672 100644 --- a/apps/backend/src/lib/emailable.tsx +++ b/apps/backend/src/lib/emailable.tsx @@ -1,5 +1,5 @@ import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; -import { captureError, StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { captureError, HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { wait } from "@stackframe/stack-shared/dist/utils/promises"; import { traceSpan } from "@stackframe/stack-shared/dist/utils/telemetry"; import createEmailableClient from "emailable"; @@ -28,12 +28,12 @@ function isReservedTestDomain(emailDomain: string): boolean { function validateVerifyResponse(value: unknown) { if (value == null || typeof value !== "object" || Array.isArray(value)) { - throw new StackAssertionError("Emailable returned a non-object response body", { value }); + throw new HexclaveAssertionError("Emailable returned a non-object response body", { value }); } const response = Object.assign(Object.create(null), value) as Record; const { state, disposable, score } = response; if (typeof state !== "string" || !VERIFY_STATES.some(s => s === state)) { - throw new StackAssertionError("Emailable verify response has invalid or missing state", { response }); + throw new HexclaveAssertionError("Emailable verify response has invalid or missing state", { response }); } const parsedScore = typeof score === "number" && score >= 0 && score <= 100 ? score : null; return { ...response, state, disposable: disposable === true, score: parsedScore }; @@ -47,17 +47,17 @@ async function verifyWithRetries(verifyFn: () => Promise, maxAttempts: await wait((Math.random() + 0.5) * delayBaseMs * (2 ** i)); continue; } - throw new StackAssertionError("Emailable returned an unexpected response body", { response: res }); + throw new HexclaveAssertionError("Emailable returned an unexpected response body", { response: res }); } return res; } - throw new StackAssertionError("Timed out while verifying email address with Emailable"); + throw new HexclaveAssertionError("Timed out while verifying email address with Emailable"); } function buildTestUndeliverableResponse(email: string) { const match = email.match(/^([^@]+)@([^@]+)$/); if (!match) { - throw new StackAssertionError("Expected a valid email before creating the Emailable test-mode response", { email }); + throw new HexclaveAssertionError("Expected a valid email before creating the Emailable test-mode response", { email }); } return { accept_all: false, did_you_mean: null, disposable: false, domain: match[2], @@ -93,7 +93,7 @@ export async function checkEmailWithEmailable( if (["development", "test"].includes(getNodeEnvironment())) { return { status: "deliverable", emailableScore: null }; } - throw new StackAssertionError("STACK_EMAILABLE_API_KEY must not be empty; set it to 'disable_email_validation' to disable email validation"); + throw new HexclaveAssertionError("STACK_EMAILABLE_API_KEY must not be empty; set it to 'disable_email_validation' to disable email validation"); } const apiKey = rawApiKey === "disable_email_validation" ? "" : rawApiKey; @@ -116,7 +116,7 @@ export async function checkEmailWithEmailable( return { status: "deliverable", emailableScore: response.score }; }); } catch (error) { - captureError("emailable-api-error", new StackAssertionError("Error while checking email address with Emailable", { cause: error, email, options })); + captureError("emailable-api-error", new HexclaveAssertionError("Error while checking email address with Emailable", { cause: error, email, options })); // If there's an error, let's pretend the email is deliverable, albeit with the score unavailable return { status: "deliverable", emailableScore: null }; } diff --git a/apps/backend/src/lib/emails-low-level.tsx b/apps/backend/src/lib/emails-low-level.tsx index ba58de12a0..cd441941ab 100644 --- a/apps/backend/src/lib/emails-low-level.tsx +++ b/apps/backend/src/lib/emails-low-level.tsx @@ -4,7 +4,7 @@ * providers. You probably shouldn't use this and should instead use the functions in emails.tsx. */ -import { StackAssertionError, captureError } from '@stackframe/stack-shared/dist/utils/errors'; +import { HexclaveAssertionError, captureError } from '@stackframe/stack-shared/dist/utils/errors'; import { omit, pick } from '@stackframe/stack-shared/dist/utils/objects'; import { runAsynchronously, wait } from '@stackframe/stack-shared/dist/utils/promises'; import { Result } from '@stackframe/stack-shared/dist/utils/results'; @@ -55,7 +55,7 @@ async function _lowLevelSendEmailWithoutRetries(options: LowLevelSendEmailOption runAsynchronously(async () => { await wait(15_000); if (!finished) { - captureError("email-send-timeout", new StackAssertionError("Email send took longer than 15s; maybe the email service is too slow?", { + captureError("email-send-timeout", new HexclaveAssertionError("Email send took longer than 15s; maybe the email service is too slow?", { config: options.emailConfig.type === 'shared' ? "shared" : pick(options.emailConfig, ['host', 'port', 'username', 'senderEmail', 'senderName']), to: options.to, subject: options.subject, @@ -209,7 +209,7 @@ async function _lowLevelSendEmailWithoutRetries(options: LowLevelSendEmailOption } // ============ unknown error ============ - captureError("unknown-email-send-error", new StackAssertionError("Unknown error while sending email. We should add a better error description for the user.", { cause: error })); + captureError("unknown-email-send-error", new HexclaveAssertionError("Unknown error while sending email. We should add a better error description for the user.", { cause: error })); return Result.error({ rawError: error, errorType: 'UNKNOWN', @@ -230,7 +230,7 @@ export async function lowLevelSendEmailDirectWithoutRetries(options: LowLevelSen message?: string, }>> { if (!options.to) { - throw new StackAssertionError("No recipient email address provided to sendEmail", omit(options, ['emailConfig'])); + throw new HexclaveAssertionError("No recipient email address provided to sendEmail", omit(options, ['emailConfig'])); } const result = await _lowLevelSendEmailWithoutRetries(options); diff --git a/apps/backend/src/lib/emails.tsx b/apps/backend/src/lib/emails.tsx index 5358b9b00a..de7282fbf1 100644 --- a/apps/backend/src/lib/emails.tsx +++ b/apps/backend/src/lib/emails.tsx @@ -4,7 +4,7 @@ import { EmailOutboxCreatedWith } from '@/generated/prisma/client'; import { DEFAULT_TEMPLATE_IDS } from '@stackframe/stack-shared/dist/helpers/emails'; import { UsersCrud } from '@stackframe/stack-shared/dist/interface/crud/users'; import { getEnvBoolean, getEnvVariable } from '@stackframe/stack-shared/dist/utils/env'; -import { StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors'; +import { HexclaveAssertionError } from '@stackframe/stack-shared/dist/utils/errors'; import { Json } from '@stackframe/stack-shared/dist/utils/json'; import { runEmailQueueStep, serializeRecipient } from './email-queue-step'; import { LowLevelEmailConfig, isSecureEmailPort } from './emails-low-level'; @@ -30,11 +30,11 @@ function getDefaultEmailTemplate(tenancy: Tenancy, type: keyof typeof DEFAULT_TE if (defaultTemplateId) { const template = templateList.get(defaultTemplateId); if (!template) { - throw new StackAssertionError(`Default email template not found: ${type}`); + throw new HexclaveAssertionError(`Default email template not found: ${type}`); } return template; } - throw new StackAssertionError(`Unknown email template type: ${type}`); + throw new HexclaveAssertionError(`Unknown email template type: ${type}`); } export async function sendEmailToMany(options: { @@ -106,7 +106,7 @@ export async function getEmailConfig(tenancy: Tenancy): Promise( for (const eventType of eventTypes) { if (eventType.id.startsWith("$")) { if (!systemEventTypesById.has(eventType.id as any)) { - throw new StackAssertionError(`Invalid system event type: ${eventType.id}`, { eventType }); + throw new HexclaveAssertionError(`Invalid system event type: ${eventType.id}`, { eventType }); } } else { - throw new StackAssertionError(`Non-system event types are not supported yet`, { eventType }); + throw new HexclaveAssertionError(`Non-system event types are not supported yet`, { eventType }); } } @@ -247,7 +247,7 @@ export async function logEvent( data = await eventType.dataSchema.validate(data, { strict: true, stripUnknown: false }); } catch (error) { if (error instanceof yup.ValidationError) { - throw new StackAssertionError(`Invalid event data for event type: ${eventType.id}`, { eventType, data, originalData, originalEventTypes: eventTypes, cause: error }); + throw new HexclaveAssertionError(`Invalid event data for event type: ${eventType.id}`, { eventType, data, originalData, originalEventTypes: eventTypes, cause: error }); } throw error; } @@ -318,11 +318,11 @@ export async function logEvent( const refreshTokenId = typeof dataRecord === "object" && dataRecord && typeof dataRecord.refreshTokenId === "string" ? dataRecord.refreshTokenId - : throwErr(new StackAssertionError("refreshTokenId is required for $token-refresh ClickHouse event", { dataRecord })); + : throwErr(new HexclaveAssertionError("refreshTokenId is required for $token-refresh ClickHouse event", { dataRecord })); const isAnonymous = typeof dataRecord === "object" && dataRecord && typeof dataRecord.isAnonymous === "boolean" ? dataRecord.isAnonymous - : throwErr(new StackAssertionError("isAnonymous is required for $token-refresh ClickHouse event", { dataRecord })); + : throwErr(new HexclaveAssertionError("isAnonymous is required for $token-refresh ClickHouse event", { dataRecord })); const ipInfo = typeof dataRecord === "object" && dataRecord ? (dataRecord.ipInfo as EndUserIpInfo | null | undefined) @@ -336,11 +336,11 @@ export async function logEvent( const ruleId = typeof dataRecord === "object" && dataRecord && typeof dataRecord.ruleId === "string" ? dataRecord.ruleId - : throwErr(new StackAssertionError("ruleId is required for $sign-up-rule-trigger ClickHouse event", { dataRecord })); + : throwErr(new HexclaveAssertionError("ruleId is required for $sign-up-rule-trigger ClickHouse event", { dataRecord })); const action = typeof dataRecord === "object" && dataRecord && typeof dataRecord.action === "string" ? dataRecord.action - : throwErr(new StackAssertionError("action is required for $sign-up-rule-trigger ClickHouse event", { dataRecord })); + : throwErr(new HexclaveAssertionError("action is required for $sign-up-rule-trigger ClickHouse event", { dataRecord })); const email = typeof dataRecord === "object" && dataRecord ? (dataRecord.email as string | null | undefined) ?? null @@ -361,11 +361,11 @@ export async function logEvent( oauth_provider: oauthProvider, }; } else { - throw new StackAssertionError(`Unhandled ClickHouse event type: ${matchingEventType.id}`, { matchingEventType }); + throw new HexclaveAssertionError(`Unhandled ClickHouse event type: ${matchingEventType.id}`, { matchingEventType }); } if (!projectId) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `projectId is required for ClickHouse event insertion (${matchingEventType.id})`, { matchingEventType, dataRecord } ); diff --git a/apps/backend/src/lib/external-db-sync-queue.ts b/apps/backend/src/lib/external-db-sync-queue.ts index 0e7fcc51b8..735167191f 100644 --- a/apps/backend/src/lib/external-db-sync-queue.ts +++ b/apps/backend/src/lib/external-db-sync-queue.ts @@ -1,11 +1,11 @@ import { globalPrismaClient } from "@/prisma-client"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; function assertUuid(value: unknown, label: string): asserts value is string { if (typeof value !== "string" || value.trim().length === 0 || !UUID_REGEX.test(value)) { - throw new StackAssertionError(`${label} must be a valid UUID. Received: ${JSON.stringify(value)}`); + throw new HexclaveAssertionError(`${label} must be a valid UUID. Received: ${JSON.stringify(value)}`); } } diff --git a/apps/backend/src/lib/external-db-sync.ts b/apps/backend/src/lib/external-db-sync.ts index d638911a86..0abb5982aa 100644 --- a/apps/backend/src/lib/external-db-sync.ts +++ b/apps/backend/src/lib/external-db-sync.ts @@ -5,7 +5,7 @@ import { Prisma } from "@/generated/prisma/client"; import { getClickhouseAdminClient } from "@/lib/clickhouse"; import { DEFAULT_DB_SYNC_MAPPINGS } from "@stackframe/stack-shared/dist/config/db-sync-mappings"; import type { CompleteConfig } from "@stackframe/stack-shared/dist/config/schema"; -import { captureError, StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { captureError, HexclaveAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; import { omit } from "@stackframe/stack-shared/dist/utils/objects"; import { Result } from "@stackframe/stack-shared/dist/utils/results"; @@ -17,14 +17,14 @@ const MAX_BATCHES_PER_MAPPING_ENV = "STACK_EXTERNAL_DB_SYNC_MAX_BATCHES_PER_MAPP function assertNonEmptyString(value: unknown, label: string): asserts value is string { if (typeof value !== "string" || value.trim().length === 0) { - throw new StackAssertionError(`${label} must be a non-empty string.`); + throw new HexclaveAssertionError(`${label} must be a non-empty string.`); } } function assertUuid(value: unknown, label: string): asserts value is string { assertNonEmptyString(value, label); if (!UUID_REGEX.test(value)) { - throw new StackAssertionError(`${label} must be a valid UUID. Received: ${JSON.stringify(value)}`); + throw new HexclaveAssertionError(`${label} must be a valid UUID. Received: ${JSON.stringify(value)}`); } } @@ -151,7 +151,7 @@ export async function recordExternalDbSyncDeletion( `); if (insertedCount !== 1) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Expected to insert 1 DeletedRow entry for ProjectUser, got ${insertedCount}.` ); } @@ -194,7 +194,7 @@ export async function recordExternalDbSyncDeletion( `); if (insertedCount !== 1) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Expected to insert 1 DeletedRow entry for ContactChannel, got ${insertedCount}.` ); } @@ -228,7 +228,7 @@ export async function recordExternalDbSyncDeletion( `); if (insertedCount !== 1) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Expected to insert 1 DeletedRow entry for Team, got ${insertedCount}.` ); } @@ -264,7 +264,7 @@ export async function recordExternalDbSyncDeletion( `); if (insertedCount !== 1) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Expected to insert 1 DeletedRow entry for TeamMember, got ${insertedCount}.` ); } @@ -303,7 +303,7 @@ export async function recordExternalDbSyncDeletion( `); if (insertedCount !== 1) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Expected to insert 1 DeletedRow entry for TeamMemberDirectPermission, got ${insertedCount}.` ); } @@ -341,7 +341,7 @@ export async function recordExternalDbSyncDeletion( `); if (insertedCount !== 1) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Expected to insert 1 DeletedRow entry for ProjectUserDirectPermission, got ${insertedCount}.` ); } @@ -378,7 +378,7 @@ export async function recordExternalDbSyncDeletion( `); if (insertedCount !== 1) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Expected to insert 1 DeletedRow entry for UserNotificationPreference, got ${insertedCount}.` ); } @@ -412,7 +412,7 @@ export async function recordExternalDbSyncDeletion( `); if (insertedCount !== 1) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Expected to insert 1 DeletedRow entry for ProjectUserRefreshToken, got ${insertedCount}.` ); } @@ -446,7 +446,7 @@ export async function recordExternalDbSyncDeletion( `); if (insertedCount !== 1) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Expected to insert 1 DeletedRow entry for ProjectUserOAuthAccount, got ${insertedCount}.` ); } @@ -488,7 +488,7 @@ export async function recordExternalDbSyncDeletion( `); if (insertedCount !== 1) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Expected to insert 1 DeletedRow entry for VerificationCode_TEAM_INVITATION, got ${insertedCount}.` ); } @@ -947,7 +947,7 @@ function getMaxBatchesPerMapping(): number | null { if (!rawValue) return null; const parsed = Number.parseInt(rawValue, 10); if (!Number.isFinite(parsed) || parsed <= 0) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `${MAX_BATCHES_PER_MAPPING_ENV} must be a positive integer. Received: ${JSON.stringify(rawValue)}` ); } @@ -981,7 +981,7 @@ async function ensureExternalSchema( return; } - throw new StackAssertionError( + throw new HexclaveAssertionError( `Schema creation error while creating table ${JSON.stringify(tableName)}, but table does not exist.` ); } @@ -999,7 +999,7 @@ async function pushRowsToExternalDb( assertNonEmptyString(mappingId, "mappingId"); assertUuid(expectedTenancyId, "expectedTenancyId"); if (!Array.isArray(newRows)) { - throw new StackAssertionError(`newRows must be an array for table ${JSON.stringify(tableName)}.`); + throw new HexclaveAssertionError(`newRows must be an array for table ${JSON.stringify(tableName)}.`); } if (newRows.length === 0) return; // Just for our own sanity, make sure that we have the right number of positional parameters @@ -1010,7 +1010,7 @@ async function pushRowsToExternalDb( const orderedKeys = Object.keys(omit(sampleRow, ["tenancyId"])); // +1 for mapping_name parameter which is appended if (orderedKeys.length + 1 !== expectedParamCount) { - throw new StackAssertionError(` + throw new HexclaveAssertionError(` Column count mismatch for table ${JSON.stringify(tableName)} → upsertQuery expects ${expectedParamCount} parameters (last one should be mapping_name). → internalDbFetchQuery returned ${orderedKeys.length} columns (excluding tenancyId) + 1 for mapping_name = ${orderedKeys.length + 1}. @@ -1023,7 +1023,7 @@ async function pushRowsToExternalDb( // Validate that all rows belong to the expected tenant if (tenancyId !== expectedTenancyId) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Row has unexpected tenancyId. Expected ${expectedTenancyId}, got ${tenancyId}. ` + `This indicates a bug in the internalDbFetchQuery.` ); @@ -1036,7 +1036,7 @@ async function pushRowsToExternalDb( rowKeys.every((k, i) => k === orderedKeys[i]); if (!validShape) { - throw new StackAssertionError( + throw new HexclaveAssertionError( ` Row shape mismatch for table "${tableName}".\n` + `Expected column order: [${orderedKeys.join(", ")}]\n` + `Received column order: [${rowKeys.join(", ")}]\n` + @@ -1065,7 +1065,7 @@ function normalizeClickhouseBoolean(value: unknown, label: string): number { if (value === 0 || value === 1) { return value; } - throw new StackAssertionError(`${label} must be a boolean or 0/1. Received: ${JSON.stringify(value)}`); + throw new HexclaveAssertionError(`${label} must be a boolean or 0/1. Received: ${JSON.stringify(value)}`); } function normalizeClickhouseNullableBoolean(value: unknown, label: string): number | null { @@ -1081,7 +1081,7 @@ function parseSequenceId(value: unknown, mappingId: string): number | null { } const seqNum = Number(value); if (!Number.isFinite(seqNum)) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Invalid sequence_id for mapping ${mappingId}: ${JSON.stringify(value)}` ); } @@ -1173,7 +1173,7 @@ async function pushRowsToClickhouse( assertNonEmptyString(mappingId, "mappingId"); assertUuid(expectedTenancyId, "expectedTenancyId"); if (!Array.isArray(newRows)) { - throw new StackAssertionError(`newRows must be an array for table ${JSON.stringify(tableName)}.`); + throw new HexclaveAssertionError(`newRows must be an array for table ${JSON.stringify(tableName)}.`); } if (newRows.length === 0) return; @@ -1187,12 +1187,12 @@ async function pushRowsToClickhouse( const normalizedRows = newRows.map((row) => { const tenancyIdValue = row.tenancyId; if (typeof tenancyIdValue !== "string") { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Row has invalid tenancyId. Expected ${expectedTenancyId}, got ${JSON.stringify(tenancyIdValue)}.` ); } if (tenancyIdValue !== expectedTenancyId) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Row has unexpected tenancyId. Expected ${expectedTenancyId}, got ${tenancyIdValue}. ` + `This indicates a bug in the internalDbFetchQuery.` ); @@ -1206,7 +1206,7 @@ async function pushRowsToClickhouse( rowKeys.every((key, index) => key === orderedKeys[index]); if (!validShape) { - throw new StackAssertionError( + throw new HexclaveAssertionError( ` Row shape mismatch for table "${tableName}".\n` + `Expected column order: [${orderedKeys.join(", ")}]\n` + `Received column order: [${rowKeys.join(", ")}]\n` + @@ -1217,7 +1217,7 @@ async function pushRowsToClickhouse( const sequenceId = parseSequenceId(rest.sync_sequence_id, mappingId); if (sequenceId === null) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `sync_sequence_id must be defined for ClickHouse sync. Mapping: ${mappingId}` ); } @@ -1283,7 +1283,7 @@ async function getClickhouseLastSyncedSequenceId( } const parsed = Number(result[0]?.last_synced_sequence_id); if (!Number.isFinite(parsed)) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Invalid last_synced_sequence_id for mapping ${mappingId}: ${JSON.stringify(result[0]?.last_synced_sequence_id)}` ); } @@ -1299,7 +1299,7 @@ async function updateClickhouseSyncMetadata( assertUuid(tenancyId, "tenancyId"); assertNonEmptyString(mappingId, "mappingId"); if (!Number.isFinite(lastSequenceId)) { - throw new StackAssertionError(`lastSequenceId must be a finite number for mapping ${mappingId}.`); + throw new HexclaveAssertionError(`lastSequenceId must be a finite number for mapping ${mappingId}.`); } await client.insert({ table: "analytics_internal._stack_sync_metadata", @@ -1333,7 +1333,7 @@ async function syncPostgresMapping( assertNonEmptyString(fetchQuery, "internalDbFetchQuery"); assertNonEmptyString(updateQuery, "externalDbUpdateQueries"); if (!fetchQuery.includes("$1") || !fetchQuery.includes("$2")) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `internalDbFetchQuery must reference $1 (tenancyId) and $2 (lastSequenceId). Mapping: ${mappingId}` ); } @@ -1350,7 +1350,7 @@ async function syncPostgresMapping( lastSequenceId = Number(metadataResult.rows[0].last_synced_sequence_id); } if (!Number.isFinite(lastSequenceId)) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Invalid last_synced_sequence_id for mapping ${mappingId}: ${JSON.stringify(metadataResult.rows[0]?.last_synced_sequence_id)}` ); } @@ -1363,7 +1363,7 @@ async function syncPostgresMapping( while (true) { assertUuid(tenancyId, "tenancyId"); if (!Number.isFinite(lastSequenceId)) { - throw new StackAssertionError(`lastSequenceId must be a finite number for mapping ${mappingId}.`); + throw new HexclaveAssertionError(`lastSequenceId must be a finite number for mapping ${mappingId}.`); } const rows = await internalPrisma.$replica().$queryRawUnsafe(fetchQuery, tenancyId, lastSequenceId); @@ -1418,15 +1418,15 @@ async function syncClickhouseMapping( assertUuid(tenancyId, "tenancyId"); const fetchQuery = mapping.internalDbFetchQueries.clickhouse; if (!fetchQuery) { - throw new StackAssertionError(`Missing ClickHouse fetch query for mapping ${mappingId}.`); + throw new HexclaveAssertionError(`Missing ClickHouse fetch query for mapping ${mappingId}.`); } const tableSchema = mapping.targetTableSchemas.clickhouse; if (!tableSchema) { - throw new StackAssertionError(`Missing ClickHouse table schema for mapping ${mappingId}.`); + throw new HexclaveAssertionError(`Missing ClickHouse table schema for mapping ${mappingId}.`); } assertNonEmptyString(fetchQuery, "internalDbFetchQuery"); if (!fetchQuery.includes("$1") || !fetchQuery.includes("$2")) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `internalDbFetchQuery must reference $1 (tenancyId) and $2 (lastSequenceId). Mapping: ${mappingId}` ); } @@ -1442,7 +1442,7 @@ async function syncClickhouseMapping( while (true) { assertUuid(tenancyId, "tenancyId"); if (!Number.isFinite(lastSequenceId)) { - throw new StackAssertionError(`lastSequenceId must be a finite number for mapping ${mappingId}.`); + throw new HexclaveAssertionError(`lastSequenceId must be a finite number for mapping ${mappingId}.`); } const rows = await internalPrisma.$replica().$queryRawUnsafe[]>(fetchQuery, tenancyId, lastSequenceId); @@ -1497,7 +1497,7 @@ async function syncDatabase( const dbType = dbConfig.type; if (dbType === "postgres") { if (!dbConfig.connectionString) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Invalid configuration for external DB ${dbId}: 'connectionString' is missing.` ); } @@ -1541,7 +1541,7 @@ async function syncDatabase( return needsResync; } - throw new StackAssertionError( + throw new HexclaveAssertionError( `Unsupported database type '${String(dbType)}' for external DB ${dbId}.` ); } diff --git a/apps/backend/src/lib/featurebase.tsx b/apps/backend/src/lib/featurebase.tsx index 261569e6c3..2125c6bc2c 100644 --- a/apps/backend/src/lib/featurebase.tsx +++ b/apps/backend/src/lib/featurebase.tsx @@ -1,6 +1,6 @@ import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { getOrCreateFeaturebaseUser as getOrCreateFeaturebaseUserShared, StackAuthUser } from "@stackframe/stack-shared/dist/utils/featurebase"; export function getFeaturebaseApiKey(): string { @@ -10,7 +10,7 @@ export function getFeaturebaseApiKey(): string { export function requireFeaturebaseApiKey(): string { const key = getFeaturebaseApiKey(); if (!key) { - throw new StackAssertionError("STACK_FEATUREBASE_API_KEY environment variable is not set"); + throw new HexclaveAssertionError("STACK_FEATUREBASE_API_KEY environment variable is not set"); } return key; } diff --git a/apps/backend/src/lib/internal-api-keys.tsx b/apps/backend/src/lib/internal-api-keys.tsx index 6e21178398..ccecd27643 100644 --- a/apps/backend/src/lib/internal-api-keys.tsx +++ b/apps/backend/src/lib/internal-api-keys.tsx @@ -7,7 +7,7 @@ import { yupString } from '@stackframe/stack-shared/dist/schema-fields'; import { typedIncludes } from '@stackframe/stack-shared/dist/utils/arrays'; import { generateSecureRandomString } from '@stackframe/stack-shared/dist/utils/crypto'; import { KnownError, KnownErrors } from '@stackframe/stack-shared/dist/known-errors'; -import { StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors'; +import { HexclaveAssertionError } from '@stackframe/stack-shared/dist/utils/errors'; import { publishableClientKeyNotNecessarySentinel } from '@stackframe/stack-shared/dist/utils/oauth'; import { Result } from '@stackframe/stack-shared/dist/utils/results'; import { generateUuid } from '@stackframe/stack-shared/dist/utils/uuids'; @@ -78,18 +78,18 @@ type KeyType = function validateKeyType(obj: any): KeyType { if (typeof obj !== 'object' || obj === null) { - throw new StackAssertionError('Invalid key type', { obj }); + throw new HexclaveAssertionError('Invalid key type', { obj }); } const entries = Object.entries(obj); if (entries.length !== 1) { - throw new StackAssertionError('Invalid key type; must have exactly one entry', { obj }); + throw new HexclaveAssertionError('Invalid key type; must have exactly one entry', { obj }); } const [key, value] = entries[0]; if (!typedIncludes(['publishableClientKey', 'secretServerKey', 'superSecretAdminKey'], key)) { - throw new StackAssertionError('Invalid key type; field must be one of the three key types', { obj }); + throw new HexclaveAssertionError('Invalid key type; field must be one of the three key types', { obj }); } if (typeof value !== 'string') { - throw new StackAssertionError('Invalid key type; field must be a string', { obj }); + throw new HexclaveAssertionError('Invalid key type; field must be a string', { obj }); } return { [key]: value, diff --git a/apps/backend/src/lib/internal-feedback-emails.tsx b/apps/backend/src/lib/internal-feedback-emails.tsx index 30db8a0e54..87727373e9 100644 --- a/apps/backend/src/lib/internal-feedback-emails.tsx +++ b/apps/backend/src/lib/internal-feedback-emails.tsx @@ -5,7 +5,7 @@ import { getNotificationCategoryByName } from "@/lib/notification-categories"; import { Tenancy } from "@/lib/tenancies"; import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { escapeHtml } from "@stackframe/stack-shared/dist/utils/html"; const transactionalCategoryId = getNotificationCategoryByName("Transactional")?.id ?? throwErr("Transactional notification category not found"); @@ -50,7 +50,7 @@ export function getInternalFeedbackRecipients(): string[] { const recipients = rawRecipients.split(",").map((recipient) => recipient.trim()); if (recipients.some((recipient) => recipient.length === 0)) { - throw new StackAssertionError("STACK_INTERNAL_FEEDBACK_RECIPIENTS contains an empty recipient", { + throw new HexclaveAssertionError("STACK_INTERNAL_FEEDBACK_RECIPIENTS contains an empty recipient", { rawRecipients, }); } diff --git a/apps/backend/src/lib/js-execution.tsx b/apps/backend/src/lib/js-execution.tsx index f6a97ce931..a5fe9def5d 100644 --- a/apps/backend/src/lib/js-execution.tsx +++ b/apps/backend/src/lib/js-execution.tsx @@ -2,7 +2,7 @@ import { traceSpan } from '@/utils/telemetry'; import { runAsynchronouslyAndWaitUntil } from '@/utils/background-tasks'; import { getEnvVariable, getNodeEnvironment } from '@stackframe/stack-shared/dist/utils/env'; import { isLocalEmulatorEnabled } from "@/lib/local-emulator"; -import { StackAssertionError, captureError } from '@stackframe/stack-shared/dist/utils/errors'; +import { HexclaveAssertionError, captureError } from '@stackframe/stack-shared/dist/utils/errors'; import { Result } from '@stackframe/stack-shared/dist/utils/results'; import { Sandbox } from '@vercel/sandbox'; import { Freestyle as FreestyleClient } from 'freestyle-sandboxes'; @@ -29,10 +29,10 @@ function createFreestyleEngine(): JsEngine { if (apiKey === "mock_stack_freestyle_key") { if (!["development", "test"].includes(getNodeEnvironment()) && !isLocalEmulatorEnabled()) { - throw new StackAssertionError("Mock Freestyle key used in production; please set the STACK_FREESTYLE_API_KEY environment variable."); + throw new HexclaveAssertionError("Mock Freestyle key used in production; please set the STACK_FREESTYLE_API_KEY environment variable."); } if (!baseUrl) { - const prefix = getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81"); + const prefix = getEnvVariable("NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX", "81"); baseUrl = `http://localhost:${prefix}22`; } } @@ -48,7 +48,7 @@ function createFreestyleEngine(): JsEngine { }); if (response.result === undefined) { - throw new StackAssertionError("Freestyle execution returned undefined result", { response, innerCode: code, innerOptions: options }); + throw new HexclaveAssertionError("Freestyle execution returned undefined result", { response, innerCode: code, innerOptions: options }); } return response.result as ExecuteResult; @@ -81,7 +81,7 @@ function createVercelSandboxEngine(): JsEngine { const installResult = await sandbox.runCommand('npm', ['install', '--no-save', ...packages]); if (installResult.exitCode !== 0) { - throw new StackAssertionError("Failed to install packages in Vercel Sandbox", { exitCode: installResult.exitCode, innerCode: code, innerOptions: options }); + throw new HexclaveAssertionError("Failed to install packages in Vercel Sandbox", { exitCode: installResult.exitCode, innerCode: code, innerOptions: options }); } } @@ -102,19 +102,19 @@ function createVercelSandboxEngine(): JsEngine { const runResult = await sandbox.runCommand('node', ['/vercel/sandbox/runner.mjs']); if (runResult.exitCode !== 0) { - throw new StackAssertionError("Vercel Sandbox runner exited with non-zero code", { innerCode: code, innerOptions: options, exitCode: runResult.exitCode }); + throw new HexclaveAssertionError("Vercel Sandbox runner exited with non-zero code", { innerCode: code, innerOptions: options, exitCode: runResult.exitCode }); } const resultBuffer = await sandbox.readFileToBuffer({ path: resultPath }); if (resultBuffer === null) { - throw new StackAssertionError("Result file not found in Vercel Sandbox", { resultPath, innerCode: code, innerOptions: options }); + throw new HexclaveAssertionError("Result file not found in Vercel Sandbox", { resultPath, innerCode: code, innerOptions: options }); } const resultJson = resultBuffer.toString(); try { return JSON.parse(resultJson); } catch (e: any) { - throw new StackAssertionError("Failed to parse result from Vercel Sandbox", { resultJson, cause: e, innerCode: code, innerOptions: options }); + throw new HexclaveAssertionError("Failed to parse result from Vercel Sandbox", { resultJson, cause: e, innerCode: code, innerOptions: options }); } } finally { await sandbox.stop(); @@ -151,7 +151,7 @@ export async function executeJavascript(code: string, options: ExecuteJavascript return await runWithFallback(code, options); } else { if (getNodeEnvironment().includes("prod") && !isLocalEmulatorEnabled()) { - throw new StackAssertionError("STACK_VERCEL_SANDBOX_TOKEN is set to the disabled sentinel value in production. Please configure a real Vercel Sandbox token."); + throw new HexclaveAssertionError("STACK_VERCEL_SANDBOX_TOKEN is set to the disabled sentinel value in production. Please configure a real Vercel Sandbox token."); } return await runWithoutFallback(code, options); @@ -192,7 +192,7 @@ async function runSanityTest(code: string, options: ExecuteJavascriptOptions) { } if (failures.length > 0) { - captureError("js-execution-sanity-test-failures", new StackAssertionError( + captureError("js-execution-sanity-test-failures", new HexclaveAssertionError( `JS execution sanity test: ${failures.length} engine(s) failed`, { failures, successfulEngines: results.map(r => r.engine), innerCode: code, innerOptions: options } )); @@ -205,7 +205,7 @@ async function runSanityTest(code: string, options: ExecuteJavascriptOptions) { const referenceResult = results[0].result as ExecuteResult; const allEqual = results.every(r => areResultsEqual(r.result as ExecuteResult, referenceResult)); if (!allEqual) { - captureError("js-execution-sanity-test-mismatch", new StackAssertionError( + captureError("js-execution-sanity-test-mismatch", new HexclaveAssertionError( "JS execution sanity test: engines returned different results", { results, innerCode: code, innerOptions: options } )); @@ -234,7 +234,7 @@ async function runWithFallback(code: string, options: ExecuteJavascriptOptions): return retryResult.data; } - captureError(`js-execution-freestyle-failed`, new StackAssertionError( + captureError(`js-execution-freestyle-failed`, new HexclaveAssertionError( `JS execution freestyle engine failed, falling back to vercel sandbox engine`, { cause: retryResult.error, innerCode: code, innerOptions: options } )); @@ -243,11 +243,11 @@ async function runWithFallback(code: string, options: ExecuteJavascriptOptions): const result = await vercelSandboxEngine.execute(code, options); return result; } catch (error){ - captureError(`js-execution-vercel-sandbox-failed`, new StackAssertionError( + captureError(`js-execution-vercel-sandbox-failed`, new HexclaveAssertionError( `JS execution vercel sandbox engine failed after fallback from freestyle engine`, { cause: error, innerCode: code, innerOptions: options } )); - throw new StackAssertionError("Vercel Sandbox service unavailable", { cause: error, innerCode: code, innerOptions: options }); + throw new HexclaveAssertionError("Vercel Sandbox service unavailable", { cause: error, innerCode: code, innerOptions: options }); } } @@ -257,6 +257,6 @@ async function runWithoutFallback(code: string, options: ExecuteJavascriptOption const result = await freestyleEngine.execute(code, options); return result; } catch (error) { - throw new StackAssertionError("Freestyle rendering service unavailable when running without fallback", { cause: error, innerCode: code, innerOptions: options }); + throw new HexclaveAssertionError("Freestyle rendering service unavailable when running without fallback", { cause: error, innerCode: code, innerOptions: options }); } } diff --git a/apps/backend/src/lib/managed-email-domains.tsx b/apps/backend/src/lib/managed-email-domains.tsx index c31c3960c6..626ff97d92 100644 --- a/apps/backend/src/lib/managed-email-domains.tsx +++ b/apps/backend/src/lib/managed-email-domains.tsx @@ -1,6 +1,6 @@ import { Prisma } from "@/generated/prisma/client"; import { globalPrismaClient } from "@/prisma-client"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; export type ManagedEmailDomainStatus = "pending_dns" | "pending_verification" | "verified" | "applied" | "failed"; @@ -70,7 +70,7 @@ function statusToDbStatus(status: ManagedEmailDomainStatus): ManagedEmailDomainR function parseNameServerRecords(value: unknown): string[] { if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) { - throw new StackAssertionError("ManagedEmailDomain.nameServerRecords stored invalid JSON", { + throw new HexclaveAssertionError("ManagedEmailDomain.nameServerRecords stored invalid JSON", { nameServerRecords: value, }); } @@ -208,7 +208,7 @@ export async function markManagedEmailDomainApplied(id: string): Promise(response: Response, errorContext: string): Promise { if (!response.ok) { const responseBody = await response.text(); - throw new StackAssertionError(errorContext, { + throw new HexclaveAssertionError(errorContext, { status: response.status, responseBody, }); @@ -155,7 +155,7 @@ type DnsimpleDnsRecord = { async function parseDnsimpleJsonOrThrow(response: Response, errorContext: string): Promise { const body = await parseJsonOrThrow>(response, errorContext); if (!body.data) { - throw new StackAssertionError(errorContext, { + throw new HexclaveAssertionError(errorContext, { dnsimpleResponseBody: body, }); } @@ -228,7 +228,7 @@ async function createDnsimpleZone(subdomain: string): Promise { async function createOrReuseDnsimpleZone(subdomain: string): Promise { const existingZones = await listDnsimpleZones(subdomain); if (existingZones.length > 1) { - throw new StackAssertionError("Multiple DNSimple zones found for managed email onboarding subdomain", { + throw new HexclaveAssertionError("Multiple DNSimple zones found for managed email onboarding subdomain", { subdomain, zoneIds: existingZones.map((zone) => `${zone.id}`), }); @@ -250,7 +250,7 @@ async function getDnsimpleZoneNameServers(zoneName: string): Promise { ); const rawZoneFile = zoneFile.zone; if (!rawZoneFile) { - throw new StackAssertionError("DNSimple zone file response did not include zone contents", { + throw new HexclaveAssertionError("DNSimple zone file response did not include zone contents", { zoneName, zoneFile, }); @@ -292,7 +292,7 @@ function toDnsimpleRecordName(recordName: string, zoneName: string) { if (normalizedRecordName.endsWith(`.${normalizedZoneName}`)) { return normalizedRecordName.slice(0, -(normalizedZoneName.length + 1)); } - throw new StackAssertionError("DNS record name is not inside zone", { + throw new HexclaveAssertionError("DNS record name is not inside zone", { recordName, zoneName, }); @@ -390,14 +390,14 @@ async function upsertDnsimpleResendRecords(zoneName: string, subdomain: string, return existingType === "CNAME"; }); if (hasCnameConflict) { - throw new StackAssertionError("Cannot create DNSimple DNS record because of CNAME conflict", { + throw new HexclaveAssertionError("Cannot create DNSimple DNS record because of CNAME conflict", { zoneName, desiredRecord, }); } if (desiredRecord.type === "CNAME" && recordsWithSameName.some((existingRecord) => existingRecord.type.toUpperCase() === "CNAME")) { - throw new StackAssertionError("DNSimple CNAME record already exists with different content", { + throw new HexclaveAssertionError("DNSimple CNAME record already exists with different content", { zoneName, desiredRecord, existingRecords: recordsWithSameName, @@ -438,7 +438,7 @@ async function createResendDomain(subdomain: string): Promise { if ((response.status === 403 || response.status === 409) && isResendDomainAlreadyExistsResponse(responseBody)) { throw new StatusError(409, "This subdomain already exists in Resend. If this is from another project, choose a different subdomain."); } - throw new StackAssertionError("Failed to create Resend domain for managed email onboarding", { + throw new HexclaveAssertionError("Failed to create Resend domain for managed email onboarding", { status: response.status, responseBody, }); @@ -455,7 +455,7 @@ async function createResendDomain(subdomain: string): Promise { }); if (!verifyResponse.ok) { const verifyResponseBody = await verifyResponse.text(); - throw new StackAssertionError("Failed to trigger Resend domain verification for managed email onboarding", { + throw new HexclaveAssertionError("Failed to trigger Resend domain verification for managed email onboarding", { status: verifyResponse.status, responseBody: verifyResponseBody, domainId: body.id, @@ -489,7 +489,7 @@ async function createResendScopedKey(options: { subdomain: string, domainId: str "Failed to create scoped Resend API key while applying managed email domain", ); if (!body.token) { - throw new StackAssertionError("Resend did not return an API key token for managed onboarding", { + throw new HexclaveAssertionError("Resend did not return an API key token for managed onboarding", { domainId: options.domainId, tenancyId: options.tenancyId, subdomain: options.subdomain, @@ -565,7 +565,7 @@ export async function setupManagedEmailProvider(options: { subdomain: string, se const zoneNameServers = await getDnsimpleZoneNameServers(dnsimpleZone.name); if (zoneNameServers.length === 0) { - throw new StackAssertionError("DNSimple zone was created without nameservers for managed email onboarding", { + throw new HexclaveAssertionError("DNSimple zone was created without nameservers for managed email onboarding", { zoneId: dnsimpleZone.id, subdomain: normalizedSubdomain, }); diff --git a/apps/backend/src/lib/notification-categories.ts b/apps/backend/src/lib/notification-categories.ts index e04ce5e750..520e970d8f 100644 --- a/apps/backend/src/lib/notification-categories.ts +++ b/apps/backend/src/lib/notification-categories.ts @@ -1,7 +1,7 @@ import { Tenancy } from "@/lib/tenancies"; import { getPrismaClientForTenancy } from "@/prisma-client"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { unsubscribeLinkVerificationCodeHandler } from "../app/api/latest/emails/unsubscribe-link/verification-handler"; // For now, we only have two hardcoded notification categories. TODO: query from database instead and create UI to manage them in dashboard @@ -33,7 +33,7 @@ export const getNotificationCategoryById = (id: string) => { export const hasNotificationEnabled = async (tenancy: Tenancy, userId: string, notificationCategoryId: string) => { const notificationCategory = listNotificationCategories().find((category) => category.id === notificationCategoryId); if (!notificationCategory) { - throw new StackAssertionError('Invalid notification category id', { notificationCategoryId }); + throw new HexclaveAssertionError('Invalid notification category id', { notificationCategoryId }); } const prisma = await getPrismaClientForTenancy(tenancy); diff --git a/apps/backend/src/lib/oauth.tsx b/apps/backend/src/lib/oauth.tsx index 3b796771a6..0bf9041f80 100644 --- a/apps/backend/src/lib/oauth.tsx +++ b/apps/backend/src/lib/oauth.tsx @@ -4,13 +4,13 @@ import { createOrUpgradeAnonymousUserWithRules, SignUpRuleOptions } from "@/lib/ import { PrismaClientTransaction } from "@/prisma-client"; import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users"; import { KnownErrors } from "@stackframe/stack-shared/dist/known-errors"; -import { captureError, StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { captureError, HexclaveAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; /** * Find an existing OAuth account for sign-in. * * @returns The existing account if found, or null if no account exists - * @throws StackAssertionError if multiple accounts are found (should never happen) + * @throws HexclaveAssertionError if multiple accounts are found (should never happen) */ export async function findExistingOAuthAccount( prisma: PrismaClientTransaction, @@ -28,7 +28,7 @@ export async function findExistingOAuthAccount( }); if (existingAccounts.length > 1) { - throw new StackAssertionError("Multiple accounts found for the same provider and account ID", { + throw new HexclaveAssertionError("Multiple accounts found for the same provider and account ID", { providerId, providerAccountId, }); @@ -45,7 +45,7 @@ export function getProjectUserIdFromOAuthAccount( account: Awaited> ): string { if (!account) { - throw new StackAssertionError("OAuth account is null"); + throw new HexclaveAssertionError("OAuth account is null"); } return account.projectUserId ?? throwErr("OAuth account exists but has no associated user"); } @@ -88,7 +88,7 @@ export async function handleOAuthEmailMergeStrategy( if (!emailVerified) { // TODO: Handle this case - const err = new StackAssertionError( + const err = new HexclaveAssertionError( "OAuth account merge strategy is set to link_method, but the NEW email is not verified. This is an edge case that we don't handle right now", { existingContactChannel, email, emailVerified } ); diff --git a/apps/backend/src/lib/openapi.tsx b/apps/backend/src/lib/openapi.tsx index b79739995e..2fe6bf89c4 100644 --- a/apps/backend/src/lib/openapi.tsx +++ b/apps/backend/src/lib/openapi.tsx @@ -2,7 +2,7 @@ import { SmartRouteHandler } from '@/route-handlers/smart-route-handler'; import { CrudlOperation, EndpointDocumentation } from '@stackframe/stack-shared/dist/crud'; import { WebhookEvent } from '@stackframe/stack-shared/dist/interface/webhooks'; import { yupNumber, yupObject, yupString } from '@stackframe/stack-shared/dist/schema-fields'; -import { StackAssertionError, throwErr } from '@stackframe/stack-shared/dist/utils/errors'; +import { HexclaveAssertionError, throwErr } from '@stackframe/stack-shared/dist/utils/errors'; import { HttpMethod } from '@stackframe/stack-shared/dist/utils/http'; import { typedEntries, typedFromEntries } from '@stackframe/stack-shared/dist/utils/objects'; import { deindent, stringCompare } from '@stackframe/stack-shared/dist/utils/strings'; @@ -146,7 +146,7 @@ function parseRouteHandler(options: { } if (result) { - throw new StackAssertionError(deindent` + throw new HexclaveAssertionError(deindent` OpenAPI generator matched multiple overloads for audience ${options.audience} on endpoint ${options.method} ${options.path}. This does not necessarily mean there is a bug in the endpoint; the OpenAPI generator uses a heuristic to pick the allowed overloads, and may pick too many. Currently, this heuristic checks whether the request.auth.type property in the schema is a yup.string.oneOf(...) and matches it to the expected audience of the schema. If there are multiple overloads matching a single audience, for example because none of the overloads specify request.auth.type, the OpenAPI generator will not know which overload to generate specs for, and hence fails. @@ -253,7 +253,7 @@ function getFieldSchema(field: yup.SchemaFieldDescription, crudOperation?: Capit function toParameters(description: yup.SchemaFieldDescription, crudOperation?: Capitalize, path?: string) { const pathParams: string[] = path ? path.match(/{[^}]+}/g) || [] : []; if (!isSchemaObjectDescription(description)) { - throw new StackAssertionError('Parameters field must be an object schema', { actual: description }); + throw new HexclaveAssertionError('Parameters field must be an object schema', { actual: description }); } return Object.entries(description.fields).map(([key, field]) => { @@ -275,15 +275,15 @@ function toParameters(description: yup.SchemaFieldDescription, crudOperation?: C function toHeaderParameters(description: yup.SchemaFieldDescription, crudOperation?: Capitalize) { if (!isSchemaObjectDescription(description)) { - throw new StackAssertionError('Parameters field must be an object schema', { actual: description }); + throw new HexclaveAssertionError('Parameters field must be an object schema', { actual: description }); } return Object.entries(description.fields).map(([key, tupleField]) => { if (!isSchemaTupleDescription(tupleField)) { - throw new StackAssertionError('Header field must be a tuple schema', { actual: tupleField, key }); + throw new HexclaveAssertionError('Header field must be a tuple schema', { actual: tupleField, key }); } if (tupleField.innerType.length !== 1) { - throw new StackAssertionError('Header fields of length !== 1 not currently supported', { actual: tupleField, key }); + throw new HexclaveAssertionError('Header fields of length !== 1 not currently supported', { actual: tupleField, key }); } const field = tupleField.innerType[0]; const meta = "meta" in field ? field.meta : {}; @@ -313,7 +313,7 @@ function toSchema(description: yup.SchemaFieldDescription, crudOperation?: Capit items: toSchema(description.innerType, crudOperation), }; } else { - throw new StackAssertionError(`Unsupported schema type in toSchema: ${description.type}`, { actual: description }); + throw new HexclaveAssertionError(`Unsupported schema type in toSchema: ${description.type}`, { actual: description }); } } @@ -326,7 +326,7 @@ function toRequired(description: yup.SchemaFieldDescription, crudOperation?: Cap } else if (isSchemaArrayDescription(description)) { res = []; } else { - throw new StackAssertionError(`Unsupported schema type in toRequired: ${description.type}`, { actual: description }); + throw new HexclaveAssertionError(`Unsupported schema type in toRequired: ${description.type}`, { actual: description }); } if (res.length === 0) return undefined; return res; @@ -334,7 +334,7 @@ function toRequired(description: yup.SchemaFieldDescription, crudOperation?: Cap function toExamples(description: yup.SchemaFieldDescription, crudOperation?: Capitalize) { if (!isSchemaObjectDescription(description)) { - throw new StackAssertionError('Examples field must be an object schema', { actual: description }); + throw new HexclaveAssertionError('Examples field must be an object schema', { actual: description }); } return Object.entries(description.fields).reduce((acc, [key, field]) => { @@ -391,15 +391,15 @@ export function parseOverload(options: { for (const { responseDesc, responseTypeDesc, statusCodeDesc } of options.responseVariants) { if (!isSchemaStringDescription(responseTypeDesc)) { - throw new StackAssertionError(`Expected response type to be a string`, { actual: responseTypeDesc, options }); + throw new HexclaveAssertionError(`Expected response type to be a string`, { actual: responseTypeDesc, options }); } if (responseTypeDesc.oneOf.length !== 1) { - throw new StackAssertionError(`Expected response type to have exactly one value`, { actual: responseTypeDesc, options }); + throw new HexclaveAssertionError(`Expected response type to have exactly one value`, { actual: responseTypeDesc, options }); } const bodyType = responseTypeDesc.oneOf[0]; if (!isSchemaNumberDescription(statusCodeDesc)) { - throw new StackAssertionError('Expected status code to be a number', { actual: statusCodeDesc, options }); + throw new HexclaveAssertionError('Expected status code to be a number', { actual: statusCodeDesc, options }); } // Get all status codes or use 200 as default if none specified @@ -425,7 +425,7 @@ export function parseOverload(options: { } case 'text': { if (!responseDesc || !isSchemaStringDescription(responseDesc)) { - throw new StackAssertionError('Expected response body of bodyType=="text" to be a string schema', { actual: responseDesc }); + throw new HexclaveAssertionError('Expected response body of bodyType=="text" to be a string schema', { actual: responseDesc }); } allResponses[status] = { description: 'Successful response', @@ -468,7 +468,7 @@ export function parseOverload(options: { break; } default: { - throw new StackAssertionError(`Unsupported body type: ${bodyType}`); + throw new HexclaveAssertionError(`Unsupported body type: ${bodyType}`); } } } diff --git a/apps/backend/src/lib/payments.tsx b/apps/backend/src/lib/payments.tsx index 08b1f40def..0bf7d6c47c 100644 --- a/apps/backend/src/lib/payments.tsx +++ b/apps/backend/src/lib/payments.tsx @@ -9,7 +9,7 @@ import type { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/use import type { inlineProductSchema, productSchema, productSchemaWithMetadata } from "@stackframe/stack-shared/dist/schema-fields"; import { SUPPORTED_CURRENCIES } from "@stackframe/stack-shared/dist/utils/currency-constants"; import { addInterval } from "@stackframe/stack-shared/dist/utils/dates"; -import { StackAssertionError, StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { filterUndefined, getOrUndefined, has, typedEntries, typedFromEntries, typedKeys, typedValues } from "@stackframe/stack-shared/dist/utils/objects"; import { typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings"; import { isUuid } from "@stackframe/stack-shared/dist/utils/uuids"; @@ -82,7 +82,7 @@ export async function ensureProductIdOrInlineProduct( return product; } else { if (!inlineProduct) { - throw new StackAssertionError("Inline product does not exist, this should never happen", { inlineProduct, productId }); + throw new HexclaveAssertionError("Inline product does not exist, this should never happen", { inlineProduct, productId }); } return { productLineId: undefined, @@ -239,7 +239,7 @@ export async function getStripeCustomerForCustomerOrNull(options: { } if (matches.length > 1) { - throw new StackAssertionError("Multiple Stripe customers found for customerId; customerType filtering was ambiguous", { + throw new HexclaveAssertionError("Multiple Stripe customers found for customerId; customerType filtering was ambiguous", { customerId: options.customerId, customerType: options.customerType, stripeCustomerIds: matches.map((c) => c.id), diff --git a/apps/backend/src/lib/payments/ensure-free-plan.ts b/apps/backend/src/lib/payments/ensure-free-plan.ts index 6661a637af..668fe77ac2 100644 --- a/apps/backend/src/lib/payments/ensure-free-plan.ts +++ b/apps/backend/src/lib/payments/ensure-free-plan.ts @@ -7,7 +7,7 @@ import type { ProductSnapshot } from "@/lib/payments/schema/types"; import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch, type Tenancy } from "@/lib/tenancies"; import { getPrismaClientForTenancy, retryTransaction, type PrismaClientTransaction } from "@/prisma-client"; import { addInterval } from "@stackframe/stack-shared/dist/utils/dates"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { getOrUndefined, typedEntries } from "@stackframe/stack-shared/dist/utils/objects"; /** @@ -27,7 +27,7 @@ import { getOrUndefined, typedEntries } from "@stackframe/stack-shared/dist/util async function getInternalBillingTenancy(): Promise { const tenancy = await getSoleTenancyFromProjectBranch("internal", DEFAULT_BRANCH_ID, true); if (tenancy == null) { - throw new StackAssertionError("Internal billing tenancy not found"); + throw new HexclaveAssertionError("Internal billing tenancy not found"); } return tenancy; } @@ -56,7 +56,7 @@ export async function createFreePlanSubscriptionRow(options: { const { prisma, internalTenancy, billingTeamId, creationSource } = options; const freePlanProduct = getOrUndefined(internalTenancy.config.payments.products, "free"); if (freePlanProduct == null || freePlanProduct.customerType !== "team" || freePlanProduct.productLineId == null) { - throw new StackAssertionError( + throw new HexclaveAssertionError( "Internal tenancy `free` product is not configured as a team-typed, product-line-tagged plan; cannot grant", { freePlanProduct }, ); @@ -67,7 +67,7 @@ export async function createFreePlanSubscriptionRow(options: { // non-undefined (no noUncheckedIndexedAccess in our tsconfig). const priceEntries = typedEntries(freePlanProduct.prices); if (priceEntries.length === 0) { - throw new StackAssertionError("Free plan has no prices configured"); + throw new HexclaveAssertionError("Free plan has no prices configured"); } const [firstPriceId, firstPrice] = priceEntries[0]; const priceInterval = firstPrice.interval; diff --git a/apps/backend/src/lib/plan-entitlements.ts b/apps/backend/src/lib/plan-entitlements.ts index 65549dfa03..fe558fca14 100644 --- a/apps/backend/src/lib/plan-entitlements.ts +++ b/apps/backend/src/lib/plan-entitlements.ts @@ -2,7 +2,7 @@ import { getItemQuantityForCustomer } from "@/lib/payments/customer-data"; import { getPrismaClientForTenancy, globalPrismaClient } from "@/prisma-client"; import { ITEM_IDS } from "@stackframe/stack-shared/dist/plans"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch, type Tenancy } from "./tenancies"; /** @@ -62,7 +62,7 @@ export function getBillingTeamId(project: { id: string, ownerTeamId?: string | n async function getInternalBillingTenancy(): Promise { const tenancy = await getSoleTenancyFromProjectBranch("internal", DEFAULT_BRANCH_ID, true); if (tenancy == null) { - throw new StackAssertionError("Internal billing tenancy not found", { + throw new HexclaveAssertionError("Internal billing tenancy not found", { billingProjectId: "internal", branchId: DEFAULT_BRANCH_ID, }); @@ -138,7 +138,7 @@ async function getTeamWideItemCapacity( ): Promise { // Capacity metric: entitlement from Stack Auth payments for a specific item. if (!TEAM_WIDE_CAPACITY_ITEM_IDS.has(itemId)) { - throw new StackAssertionError("Unsupported team-wide capacity item id", { itemId }); + throw new HexclaveAssertionError("Unsupported team-wide capacity item id", { itemId }); } const internalBillingTenancy = await getInternalBillingTenancy(); const billingPrisma = await readers.getPrismaForTenancy(internalBillingTenancy); @@ -176,7 +176,7 @@ export async function getTeamWideAuthUsersCapacityForProjectTenancy( ): Promise { const billingTeamId = getBillingTeamId(projectTenancy.project); if (billingTeamId == null) { - throw new StackAssertionError("Project owner team missing; cannot resolve billing team", { + throw new HexclaveAssertionError("Project owner team missing; cannot resolve billing team", { projectId: projectTenancy.project.id, }); } diff --git a/apps/backend/src/lib/product-versions.tsx b/apps/backend/src/lib/product-versions.tsx index c730bc5477..469a7c588c 100644 --- a/apps/backend/src/lib/product-versions.tsx +++ b/apps/backend/src/lib/product-versions.tsx @@ -1,6 +1,6 @@ import { PrismaClientTransaction } from "@/prisma-client"; import { encodeBase64 } from "@stackframe/stack-shared/dist/utils/bytes"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import crypto from "crypto"; /** @@ -85,7 +85,7 @@ export async function getProductVersion(options: { }); if (!version) { - throw new StackAssertionError( + throw new HexclaveAssertionError( "ProductVersion not found. This may indicate a race condition or deleted record.", { tenancyId: options.tenancyId, diff --git a/apps/backend/src/lib/projects.tsx b/apps/backend/src/lib/projects.tsx index b5503923f1..489086d71d 100644 --- a/apps/backend/src/lib/projects.tsx +++ b/apps/backend/src/lib/projects.tsx @@ -5,7 +5,7 @@ import { CompleteConfig, EnvironmentConfigOverrideOverride, ProjectConfigOverrid import { AdminUserProjectsCrud, ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { filterUndefined, typedFromEntries } from "@stackframe/stack-shared/dist/utils/objects"; import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids"; import { RawQuery, getPrismaClientForTenancy, globalPrismaClient, rawQuery, retryTransaction } from "../prisma-client"; @@ -49,7 +49,7 @@ export function getProjectQuery(projectId: string): RawQuery { if (queryResult.length > 1) { - throw new StackAssertionError(`Expected 0 or 1 projects with id ${projectId}, got ${queryResult.length}`, { queryResult }); + throw new HexclaveAssertionError(`Expected 0 or 1 projects with id ${projectId}, got ${queryResult.length}`, { queryResult }); } if (queryResult.length === 0) { return null; @@ -57,7 +57,7 @@ export function getProjectQuery(projectId: string): RawQuery[0]['prisma'], @@ -97,7 +97,7 @@ export async function resolveProductFromStripeMetadata(options: { try { return JSON.parse(productString) as StripeMetadataProduct; } catch (error) { - throw new StackAssertionError( + throw new HexclaveAssertionError( "Failed to parse product JSON from Stripe metadata. The 'product' or 'offer' field contains invalid JSON.", { ...options.context, @@ -110,7 +110,7 @@ export async function resolveProductFromStripeMetadata(options: { } } - throw new StackAssertionError( + throw new HexclaveAssertionError( "Stripe metadata is missing product information. Expected one of: 'productVersionId' (current), 'product' (legacy), or 'offer' (oldest). This may indicate the purchase was created before product tracking was implemented, or the metadata was corrupted.", { ...options.context, @@ -189,20 +189,20 @@ import.meta.vitest?.describe("resolveProductFromStripeMetadata", (test) => { }); export const getStackStripe = (overrides?: StripeOverridesMap) => { if (!stripeSecretKey) { - throw new StackAssertionError("STACK_STRIPE_SECRET_KEY environment variable is not set"); + throw new HexclaveAssertionError("STACK_STRIPE_SECRET_KEY environment variable is not set"); } if (overrides && !useStripeMock) { - throw new StackAssertionError("Stripe overrides are not supported in production"); + throw new HexclaveAssertionError("Stripe overrides are not supported in production"); } return createStripeProxy(new Stripe(stripeSecretKey, stripeConfig), overrides); }; export const getStripeForAccount = async (options: { tenancy?: Tenancy, accountId?: string }, overrides?: StripeOverridesMap) => { if (!stripeSecretKey) { - throw new StackAssertionError("STACK_STRIPE_SECRET_KEY environment variable is not set"); + throw new HexclaveAssertionError("STACK_STRIPE_SECRET_KEY environment variable is not set"); } if (overrides && !useStripeMock) { - throw new StackAssertionError("Stripe overrides are not supported in production"); + throw new HexclaveAssertionError("Stripe overrides are not supported in production"); } if (!options.tenancy && !options.accountId) { throwErr(400, "Either tenancy or stripeAccountId must be provided"); @@ -227,11 +227,11 @@ export const getStripeForAccount = async (options: { tenancy?: Tenancy, accountI const getTenancyFromStripeAccountIdOrThrow = async (stripe: Stripe, stripeAccountId: string) => { const account = await stripe.accounts.retrieve(stripeAccountId); if (!account.metadata?.tenancyId || typeof account.metadata.tenancyId !== "string") { - throw new StackAssertionError("Stripe account metadata missing tenancyId", { accountId: stripeAccountId }); + throw new HexclaveAssertionError("Stripe account metadata missing tenancyId", { accountId: stripeAccountId }); } const tenancy = await getTenancy(account.metadata.tenancyId); if (!tenancy) { - throw new StackAssertionError("Tenancy not found", { accountId: stripeAccountId }); + throw new HexclaveAssertionError("Tenancy not found", { accountId: stripeAccountId }); } return tenancy; }; @@ -272,10 +272,10 @@ export async function syncStripeSubscriptions(stripe: Stripe, stripeAccountId: s const customerId = stripeCustomer.metadata.customerId; const customerType = stripeCustomer.metadata.customerType; if (!customerId || !customerType) { - throw new StackAssertionError("Stripe customer metadata missing customerId or customerType"); + throw new HexclaveAssertionError("Stripe customer metadata missing customerId or customerType"); } if (!typedIncludes(Object.values(CustomerType), customerType)) { - throw new StackAssertionError("Stripe customer metadata has invalid customerType"); + throw new HexclaveAssertionError("Stripe customer metadata has invalid customerType"); } const prisma = await getPrismaClientForTenancy(tenancy); const subscriptions = await stripe.subscriptions.list({ @@ -359,7 +359,7 @@ export async function upsertStripeInvoice(stripe: Stripe, stripeAccountId: strin return; } if (invoiceSubscriptionIds.length > 1) { - throw new StackAssertionError( + throw new HexclaveAssertionError( "Multiple subscription line items found in single invoice", { stripeAccountId, invoiceId: invoice.id } ); diff --git a/apps/backend/src/lib/telegram.tsx b/apps/backend/src/lib/telegram.tsx index df5995811e..19a9eaa6d3 100644 --- a/apps/backend/src/lib/telegram.tsx +++ b/apps/backend/src/lib/telegram.tsx @@ -1,5 +1,5 @@ import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; const TELEGRAM_HOSTNAME = "api.telegram.org"; const TELEGRAM_ENDPOINT_PATH = "/sendMessage"; @@ -33,7 +33,7 @@ export async function sendTelegramMessage(options: TelegramConfig & { message: s if (!response.ok) { const body = await response.text(); - throw new StackAssertionError("Failed to send Telegram notification.", { + throw new HexclaveAssertionError("Failed to send Telegram notification.", { status: response.status, body, }); diff --git a/apps/backend/src/lib/tenancies.tsx b/apps/backend/src/lib/tenancies.tsx index 02e18f91e4..be5c1f05f1 100644 --- a/apps/backend/src/lib/tenancies.tsx +++ b/apps/backend/src/lib/tenancies.tsx @@ -2,7 +2,7 @@ import { globalPrismaClient, RawQuery, rawQuery } from "@/prisma-client"; import { Prisma } from "@/generated/prisma/client"; import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; import { getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { deepPlainEquals } from "@stackframe/stack-shared/dist/utils/objects"; import { getRenderedOrganizationConfigQuery } from "./config"; import { getProject, getProjectQuery } from "./projects"; @@ -24,10 +24,10 @@ export const DEFAULT_BRANCH_ID = "main"; */ async function tenancyPrismaToCrudUnused(prisma: Prisma.TenancyGetPayload<{}>) { if (prisma.hasNoOrganization && prisma.organizationId !== null) { - throw new StackAssertionError("Organization ID is not null for a tenancy with hasNoOrganization", { tenancyId: prisma.id, prisma }); + throw new HexclaveAssertionError("Organization ID is not null for a tenancy with hasNoOrganization", { tenancyId: prisma.id, prisma }); } if (!prisma.hasNoOrganization && prisma.organizationId === null) { - throw new StackAssertionError("Organization ID is null for a tenancy without hasNoOrganization", { tenancyId: prisma.id, prisma }); + throw new HexclaveAssertionError("Organization ID is null for a tenancy without hasNoOrganization", { tenancyId: prisma.id, prisma }); } const projectCrud = await getProject(prisma.projectId) ?? throwErr("Project in tenancy not found"); @@ -66,7 +66,7 @@ export async function getSoleTenancyFromProjectBranch(projectOrId: Omit { if (queryResult.length > 1) { - throw new StackAssertionError( + throw new HexclaveAssertionError( `Expected 0 or 1 tenancies for project ${projectId}, branch ${branchId}, organization ${organizationId}, got ${queryResult.length}`, { queryResult } ); @@ -142,18 +142,18 @@ function getTenancyFromProjectQuery(projectId: string, branchId: string, organiz ]); if (!projectResult) { - throw new StackAssertionError("Project in tenancy not found", { projectId, tenancyId: tenancyResult.id }); + throw new HexclaveAssertionError("Project in tenancy not found", { projectId, tenancyId: tenancyResult.id }); } // Validate tenancy consistency if (tenancyResult.hasNoOrganization && tenancyResult.organizationId !== null) { - throw new StackAssertionError("Organization ID is not null for a tenancy with hasNoOrganization", { + throw new HexclaveAssertionError("Organization ID is not null for a tenancy with hasNoOrganization", { tenancyId: tenancyResult.id, tenancy: tenancyResult }); } if (!tenancyResult.hasNoOrganization && tenancyResult.organizationId === null) { - throw new StackAssertionError("Organization ID is null for a tenancy without hasNoOrganization", { + throw new HexclaveAssertionError("Organization ID is null for a tenancy without hasNoOrganization", { tenancyId: tenancyResult.id, tenancy: tenancyResult }); @@ -207,7 +207,7 @@ export async function getTenancyFromProject(projectId: string, branchId: string, // Compare the two results if (!deepPlainEquals(result, oldResult)) { - throw new StackAssertionError("getTenancyFromProject: new implementation does not match old implementation", { + throw new HexclaveAssertionError("getTenancyFromProject: new implementation does not match old implementation", { projectId, branchId, organizationId, diff --git a/apps/backend/src/lib/tokens.tsx b/apps/backend/src/lib/tokens.tsx index d97382909e..5e159bb22d 100644 --- a/apps/backend/src/lib/tokens.tsx +++ b/apps/backend/src/lib/tokens.tsx @@ -7,7 +7,7 @@ import { restrictedReasonSchema, yupBoolean, yupNumber, yupObject, yupString } f import { AccessTokenPayload } from '@stackframe/stack-shared/dist/sessions'; import { generateSecureRandomString } from '@stackframe/stack-shared/dist/utils/crypto'; import { getEnvVariable } from '@stackframe/stack-shared/dist/utils/env'; -import { captureError, StackAssertionError, throwErr } from '@stackframe/stack-shared/dist/utils/errors'; +import { captureError, HexclaveAssertionError, throwErr } from '@stackframe/stack-shared/dist/utils/errors'; import { getPrivateJwks, getPublicJwkSet, signJWT, verifyJWT } from '@stackframe/stack-shared/dist/utils/jwt'; import { Result } from '@stackframe/stack-shared/dist/utils/results'; import { traceSpan } from '@stackframe/stack-shared/dist/utils/telemetry'; @@ -100,7 +100,7 @@ export async function decodeAccessToken(accessToken: string, { allowAnonymous, a return await traceSpan("decoding access token", async (span) => { if (allowAnonymous && !allowRestricted) { - throw new StackAssertionError("If allowAnonymous is true, allowRestricted must also be true"); + throw new HexclaveAssertionError("If allowAnonymous is true, allowRestricted must also be true"); } let payload: jose.JWTPayload; @@ -150,33 +150,33 @@ export async function decodeAccessToken(accessToken: string, { allowAnonymous, a // Anonymous users must be restricted if (isAnonymous && !isRestricted) { - throw new StackAssertionError("Unparsable access token. User is anonymous but not restricted.", { accessToken, payload }); + throw new HexclaveAssertionError("Unparsable access token. User is anonymous but not restricted.", { accessToken, payload }); } // Enforce consistency between isRestricted and restrictedReason if (isRestricted && !restrictedReason) { - throw new StackAssertionError("Unparsable access token. User is restricted but restrictedReason is missing.", { accessToken, payload }); + throw new HexclaveAssertionError("Unparsable access token. User is restricted but restrictedReason is missing.", { accessToken, payload }); } if (!isRestricted && restrictedReason) { - throw new StackAssertionError("Unparsable access token. User is not restricted but restrictedReason is present.", { accessToken, payload }); + throw new HexclaveAssertionError("Unparsable access token. User is not restricted but restrictedReason is present.", { accessToken, payload }); } // Validate audience matches the user type if (aud.endsWith(":anon") && !isAnonymous) { - throw new StackAssertionError("Unparsable access token. Audience is an anonymous audience, but user is not anonymous.", { accessToken, payload }); + throw new HexclaveAssertionError("Unparsable access token. Audience is an anonymous audience, but user is not anonymous.", { accessToken, payload }); } else if (!aud.endsWith(":anon") && isAnonymous) { - throw new StackAssertionError("Unparsable access token. Audience is not an anonymous audience, but user is anonymous.", { accessToken, payload }); + throw new HexclaveAssertionError("Unparsable access token. Audience is not an anonymous audience, but user is anonymous.", { accessToken, payload }); } if (aud.endsWith(":restricted") && !isRestricted) { - throw new StackAssertionError("Unparsable access token. User is not restricted, but audience is a restricted audience.", { accessToken, payload }); + throw new HexclaveAssertionError("Unparsable access token. User is not restricted, but audience is a restricted audience.", { accessToken, payload }); } else if (!aud.endsWith(":restricted") && isRestricted && !isAnonymous) { - throw new StackAssertionError("Unparsable access token. Audience is not a restricted audience, but user is restricted.", { accessToken, payload }); + throw new HexclaveAssertionError("Unparsable access token. Audience is not a restricted audience, but user is restricted.", { accessToken, payload }); } const branchId = payload.branch_id ?? payload.branchId; if (branchId !== "main") { // TODO instead, we should check here that the aud is `projectId#branch` instead - throw new StackAssertionError("Branch ID !== main not currently supported."); + throw new HexclaveAssertionError("Branch ID !== main not currently supported."); } const result = await accessTokenSchema.validate({ @@ -361,7 +361,7 @@ export async function generateAccessTokenFromRefreshTokenIfValid(options: Refres restrictedReason: user.restricted_reason, }); } catch (error) { - captureError("generated-access-token-payload-does-not-fit-the-access-token-schema", new StackAssertionError("Generated access token payload does not fit the accessTokenSchema. This is a bug — the token data is inconsistent.", { cause: error, payload })); + captureError("generated-access-token-payload-does-not-fit-the-access-token-schema", new HexclaveAssertionError("Generated access token payload does not fit the accessTokenSchema. This is a bug — the token data is inconsistent.", { cause: error, payload })); } const userType = getUserType(user.is_anonymous, user.is_restricted); diff --git a/apps/backend/src/lib/turnstile.tsx b/apps/backend/src/lib/turnstile.tsx index 8dbe37b3bd..bfea592b5c 100644 --- a/apps/backend/src/lib/turnstile.tsx +++ b/apps/backend/src/lib/turnstile.tsx @@ -1,7 +1,7 @@ import { KnownErrors } from "@stackframe/stack-shared"; import { yupString } from "@stackframe/stack-shared/dist/schema-fields"; import { getEnvBoolean, getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; -import { captureError, StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { captureError, HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { Result } from "@stackframe/stack-shared/dist/utils/results"; import { TurnstileAction, @@ -111,7 +111,7 @@ async function fetchSiteverify(token: string, remoteIp: string | null, secretKey }); if (!response.ok) { - throw new StackAssertionError("Turnstile siteverify request failed", { + throw new HexclaveAssertionError("Turnstile siteverify request failed", { status: response.status, statusText: response.statusText, }); @@ -119,7 +119,7 @@ async function fetchSiteverify(token: string, remoteIp: string | null, secretKey const json = await response.json(); if (!isSiteverifyResponse(json)) { - throw new StackAssertionError("Turnstile siteverify response missing required fields", { json }); + throw new HexclaveAssertionError("Turnstile siteverify response missing required fields", { json }); } return json; @@ -146,7 +146,7 @@ export async function verifyTurnstileToken(params: { ); if (result.status === "error") { - captureError("turnstile-siteverify-error", new StackAssertionError("Turnstile siteverify request failed", { + captureError("turnstile-siteverify-error", new HexclaveAssertionError("Turnstile siteverify request failed", { cause: result.error, expectedAction: params.expectedAction, })); @@ -157,7 +157,7 @@ export async function verifyTurnstileToken(params: { if (!data.success) { if (params.captureRejectedAsError ?? true) { - captureError("turnstile-siteverify-rejected", new StackAssertionError("Turnstile siteverify returned success=false", { + captureError("turnstile-siteverify-rejected", new HexclaveAssertionError("Turnstile siteverify returned success=false", { errorCodes: data["error-codes"], expectedAction: params.expectedAction, receivedAction: data.action, @@ -168,7 +168,7 @@ export async function verifyTurnstileToken(params: { } if (data.hostname != null && params.isAllowedHostname != null && !params.isAllowedHostname(data.hostname)) { - captureError("turnstile-hostname-mismatch", new StackAssertionError("Turnstile hostname does not match any allowed domain", { + captureError("turnstile-hostname-mismatch", new HexclaveAssertionError("Turnstile hostname does not match any allowed domain", { receivedHostname: data.hostname, })); return { status: "invalid" }; @@ -197,7 +197,7 @@ export async function verifyTurnstileTokenWithOptionalVisibleChallenge(params: { const phase = params.phase; if (params.challengeUnavailable) { if (params.token != null || phase != null) { - throw new StackAssertionError("challengeUnavailable cannot be combined with a bot challenge token or phase"); + throw new HexclaveAssertionError("challengeUnavailable cannot be combined with a bot challenge token or phase"); } return { status: "error", visibleChallengeResult: "error" }; } diff --git a/apps/backend/src/lib/webhooks.tsx b/apps/backend/src/lib/webhooks.tsx index 070d036ad1..8f1067da27 100644 --- a/apps/backend/src/lib/webhooks.tsx +++ b/apps/backend/src/lib/webhooks.tsx @@ -5,7 +5,7 @@ import { teamCreatedWebhookEvent, teamDeletedWebhookEvent, teamUpdatedWebhookEve import { userCreatedWebhookEvent, userDeletedWebhookEvent, userUpdatedWebhookEvent } from "@stackframe/stack-shared/dist/interface/crud/users"; import { WebhookEvent } from "@stackframe/stack-shared/dist/interface/webhooks"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; import { Result } from "@stackframe/stack-shared/dist/utils/results"; import { Svix } from "svix"; import * as yup from "yup"; @@ -63,7 +63,7 @@ function createWebhookSender(event: WebhookEvent) { // Rate limit. Let's retry later return Result.error(e); } - throw new StackAssertionError("Error sending Svix webhook!", { event: event.type, data: options.data, cause: e }); + throw new HexclaveAssertionError("Error sending Svix webhook!", { event: event.type, data: options.data, cause: e }); } }, 5); }; diff --git a/apps/backend/src/oauth/index.tsx b/apps/backend/src/oauth/index.tsx index d7165a2c11..c2c5c50337 100644 --- a/apps/backend/src/oauth/index.tsx +++ b/apps/backend/src/oauth/index.tsx @@ -2,7 +2,7 @@ import { DEFAULT_BRANCH_ID, Tenancy } from "@/lib/tenancies"; import { DiscordProvider } from "@/oauth/providers/discord"; import OAuth2Server from "@node-oauth/oauth2-server"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { OAuthModel } from "./model"; import { AppleProvider } from "./providers/apple"; import { OAuthBaseProvider } from "./providers/base"; @@ -63,7 +63,7 @@ export async function getProvider(provider: Tenancy['config']['auth']['oauth'][' const clientSecret = _getEnvForProvider(providerType).clientSecret; if (clientId === "MOCK") { if (clientSecret !== "MOCK") { - throw new StackAssertionError("If OAuth provider client ID is set to MOCK, then client secret must also be set to MOCK"); + throw new HexclaveAssertionError("If OAuth provider client ID is set to MOCK, then client secret must also be set to MOCK"); } return await mockProvider.create(providerType); } else { diff --git a/apps/backend/src/oauth/model.tsx b/apps/backend/src/oauth/model.tsx index ab021bd6d8..cff1e6cac3 100644 --- a/apps/backend/src/oauth/model.tsx +++ b/apps/backend/src/oauth/model.tsx @@ -9,7 +9,7 @@ import { createRefreshTokenObj, decodeAccessToken, generateAccessTokenFromRefres import { getPrismaClientForTenancy, globalPrismaClient } from "@/prisma-client"; import { AuthorizationCode, AuthorizationCodeModel, Client, Falsey, RefreshToken, Token, User } from "@node-oauth/oauth2-server"; import { KnownErrors } from "@stackframe/stack-shared"; -import { StackAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; import { getProjectBranchFromClientId } from "."; const PrismaClientKnownRequestError = Prisma.PrismaClientKnownRequestError; diff --git a/apps/backend/src/oauth/providers/apple.tsx b/apps/backend/src/oauth/providers/apple.tsx index 7fb0af99ac..f4fca6be2d 100644 --- a/apps/backend/src/oauth/providers/apple.tsx +++ b/apps/backend/src/oauth/providers/apple.tsx @@ -1,5 +1,5 @@ import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { decodeJwt } from 'jose'; import { OAuthUserInfo, validateUserInfo } from "../utils"; import { OAuthBaseProvider, TokenSet } from "./base"; @@ -35,7 +35,7 @@ export class AppleProvider extends OAuthBaseProvider { try { payload = decodeJwt(idToken); } catch (error) { - throw new StackAssertionError("Error decoding Apple ID token", { cause: error }); + throw new HexclaveAssertionError("Error decoding Apple ID token", { cause: error }); } return validateUserInfo({ diff --git a/apps/backend/src/oauth/providers/base.tsx b/apps/backend/src/oauth/providers/base.tsx index f3b1141e33..17d4e2aa30 100644 --- a/apps/backend/src/oauth/providers/base.tsx +++ b/apps/backend/src/oauth/providers/base.tsx @@ -1,5 +1,5 @@ import { KnownErrors } from "@stackframe/stack-shared"; -import { StackAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; import { wait } from "@stackframe/stack-shared/dist/utils/promises"; import { Result } from "@stackframe/stack-shared/dist/utils/results"; import { mergeScopeStrings } from "@stackframe/stack-shared/dist/utils/strings"; @@ -219,7 +219,7 @@ export function getOAuthAccessTokenRefreshError(error: unknown, options: { function processTokenSet(providerName: string, tokenSet: OIDCTokenSet, defaultAccessTokenExpiresInMillis?: number): TokenSet { if (!tokenSet.access_token) { - throw new StackAssertionError(`No access token received from ${providerName}.`, { tokenSet, providerName }); + throw new HexclaveAssertionError(`No access token received from ${providerName}.`, { tokenSet, providerName }); } // if expires_in or expires_at provided, use that @@ -227,7 +227,7 @@ function processTokenSet(providerName: string, tokenSet: OIDCTokenSet, defaultAc // otherwise, use 1h, and log an error if (!tokenSet.expires_in && !tokenSet.expires_at && !defaultAccessTokenExpiresInMillis) { - captureError("processTokenSet", new StackAssertionError(`No expires_in or expires_at received from OAuth provider ${providerName}. Falling back to 1h`, { tokenSetKeys: Object.keys(tokenSet) })); + captureError("processTokenSet", new HexclaveAssertionError(`No expires_in or expires_at received from OAuth provider ${providerName}. Falling back to 1h`, { tokenSetKeys: Object.keys(tokenSet) })); } return { @@ -386,7 +386,7 @@ export abstract class OAuthBaseProvider { throw new StatusError(400, `Invalid client credentials for this OAuth provider. Please ensure the configuration in the Stack Auth dashboard is correct.`); } if (isRetryableOAuthUserInfoError(error)) { - captureError("inner-oauth-callback-retryable-error", new StackAssertionError("Transient OAuth provider failure during callback exchange.", { + captureError("inner-oauth-callback-retryable-error", new HexclaveAssertionError("Transient OAuth provider failure during callback exchange.", { provider: this.constructor.name, params, cause: error, @@ -398,11 +398,11 @@ export abstract class OAuthBaseProvider { const missingScope = scopeMatch ? scopeMatch[1] : null; throw new StatusError(400, `The OAuth provider does not allow the requested scope${missingScope ? ` "${missingScope}"` : ""}. Please ensure the scope is configured correctly in the provider's dashboard.`); } - throw new StackAssertionError(`Inner OAuth callback failed due to error: ${error}`, { params, cause: error }); + throw new HexclaveAssertionError(`Inner OAuth callback failed due to error: ${error}`, { params, cause: error }); } if ('error' in tokenSet) { - throw new StackAssertionError(`Inner OAuth callback failed due to error: ${tokenSet.error}, ${tokenSet.error_description}`, { params, tokenSet }); + throw new HexclaveAssertionError(`Inner OAuth callback failed due to error: ${tokenSet.error}, ${tokenSet.error_description}`, { params, tokenSet }); } tokenSet = processTokenSet(this.constructor.name, tokenSet, this.defaultAccessTokenExpiresInMillis); @@ -420,7 +420,7 @@ export abstract class OAuthBaseProvider { }); if (userInfoResult.status === "error") { - captureError("oauth-userinfo-retry-exhausted", new StackAssertionError("Failed to fetch OAuth user info after retries.", { + captureError("oauth-userinfo-retry-exhausted", new HexclaveAssertionError("Failed to fetch OAuth user info after retries.", { attempts: userInfoResult.attempts, provider: this.constructor.name, cause: userInfoResult.error, @@ -475,7 +475,7 @@ export abstract class OAuthBaseProvider { } } - throw new StackAssertionError("OAuth access token refresh finished without a result. This should never happen because the refresh loop either returns a result or throws."); + throw new HexclaveAssertionError("OAuth access token refresh finished without a result. This should never happen because the refresh loop either returns a result or throws."); } // If the token can be revoked before it expires, override this method to make an API call to the provider to check if the token is valid diff --git a/apps/backend/src/oauth/providers/github.tsx b/apps/backend/src/oauth/providers/github.tsx index e913965f17..69c550db65 100644 --- a/apps/backend/src/oauth/providers/github.tsx +++ b/apps/backend/src/oauth/providers/github.tsx @@ -1,5 +1,5 @@ import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { getJwtInfo } from "@stackframe/stack-shared/dist/utils/jwt"; import { OAuthUserInfo, validateUserInfo } from "../utils"; import { OAuthBaseProvider, TokenSet } from "./base"; @@ -39,7 +39,7 @@ export class GithubProvider extends OAuthBaseProvider { }, }); if (!rawUserInfoRes.ok) { - throw new StackAssertionError("Error fetching user info from GitHub provider: Status code " + rawUserInfoRes.status, { + throw new HexclaveAssertionError("Error fetching user info from GitHub provider: Status code " + rawUserInfoRes.status, { rawUserInfoRes, rawUserInfoResText: await rawUserInfoRes.text(), hasAccessToken: !!tokenSet.accessToken, @@ -62,14 +62,14 @@ export class GithubProvider extends OAuthBaseProvider { if (emailsRes.status === 403) { throw new StatusError(StatusError.BadRequest, `GitHub returned a 403 error when fetching user emails. \nDeveloper information: This is likely due to not having the correct permission "Email addresses" in your GitHub app. Please check your GitHub app settings and try again.`); } - throw new StackAssertionError("Error fetching user emails from GitHub: Status code " + emailsRes.status, { + throw new HexclaveAssertionError("Error fetching user emails from GitHub: Status code " + emailsRes.status, { emailsRes, rawUserInfo, }); } const emails = await emailsRes.json(); if (!Array.isArray(emails)) { - throw new StackAssertionError("Error fetching user emails from GitHub: Invalid response", { + throw new HexclaveAssertionError("Error fetching user emails from GitHub: Invalid response", { emails, emailsRes, rawUserInfo, diff --git a/apps/backend/src/oauth/providers/x.tsx b/apps/backend/src/oauth/providers/x.tsx index e3e7baa933..61d2059013 100644 --- a/apps/backend/src/oauth/providers/x.tsx +++ b/apps/backend/src/oauth/providers/x.tsx @@ -1,5 +1,5 @@ import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { OAuthUserInfo, validateUserInfo } from "../utils"; import { OAuthBaseProvider, TokenSet } from "./base"; @@ -34,7 +34,7 @@ export class XProvider extends OAuthBaseProvider { ); if (!fetchRes.ok) { const text = await fetchRes.text(); - throw new StackAssertionError(`Failed to fetch user info from X: ${fetchRes.status} ${text}`, { + throw new HexclaveAssertionError(`Failed to fetch user info from X: ${fetchRes.status} ${text}`, { status: fetchRes.status, text, }); diff --git a/apps/backend/src/polyfills.tsx b/apps/backend/src/polyfills.tsx index 510535033e..cc42cddde7 100644 --- a/apps/backend/src/polyfills.tsx +++ b/apps/backend/src/polyfills.tsx @@ -6,8 +6,8 @@ import { runAsynchronouslyAndWaitUntil } from "./utils/background-tasks"; function expandStackPortPrefix(value?: string | null) { if (!value) return value ?? undefined; - const prefix = getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81"); - return prefix ? value.replace(/\$\{NEXT_PUBLIC_STACK_PORT_PREFIX:-81\}/g, prefix) : value; + const prefix = getEnvVariable("NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX", "81"); + return prefix ? value.replace(/\$\{NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81\}/g, prefix) : value; } const sentryErrorSink = (location: string, error: unknown) => { diff --git a/apps/backend/src/prisma-client.tsx b/apps/backend/src/prisma-client.tsx index 53c18cd084..ba2cfa7526 100644 --- a/apps/backend/src/prisma-client.tsx +++ b/apps/backend/src/prisma-client.tsx @@ -5,7 +5,7 @@ import { readReplicas } from '@prisma/extension-read-replicas'; import { CompleteConfig } from "@stackframe/stack-shared/dist/config/schema"; import { yupObject, yupValidate } from "@stackframe/stack-shared/dist/schema-fields"; import { getEnvVariable, getNodeEnvironment } from '@stackframe/stack-shared/dist/utils/env'; -import { captureError, StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { captureError, HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { globalVar } from "@stackframe/stack-shared/dist/utils/globals"; import { deepPlainEquals, filterUndefined, typedFromEntries, typedKeys } from "@stackframe/stack-shared/dist/utils/objects"; import { concatStacktracesIfRejected, ignoreUnhandledRejection, runAsynchronously, wait } from "@stackframe/stack-shared/dist/utils/promises"; @@ -212,7 +212,7 @@ async function waitForReplication(replicas: PrismaClient[], target: string, time if (strategy === "pg-stat-replication") { if (!/^[0-9A-Fa-f]+\/[0-9A-Fa-f]+$/.test(target)) { - throw new StackAssertionError(`Invalid pg_lsn format: ${target}`); + throw new HexclaveAssertionError(`Invalid pg_lsn format: ${target}`); } checkCaughtUp = async (replica) => { return await traceSpan({ @@ -233,7 +233,7 @@ async function waitForReplication(replicas: PrismaClient[], target: string, time }; } else if (strategy === "aurora") { if (!/^\d+$/.test(target)) { - throw new StackAssertionError(`Invalid bigint format for Aurora durable_lsn: ${target}`); + throw new HexclaveAssertionError(`Invalid bigint format for Aurora durable_lsn: ${target}`); } const targetBigInt = BigInt(target); checkCaughtUp = async (replica) => { @@ -261,7 +261,7 @@ async function waitForReplication(replicas: PrismaClient[], target: string, time }); }; } else { - throw new StackAssertionError(`Unknown replication wait strategy: ${strategy}`); + throw new HexclaveAssertionError(`Unknown replication wait strategy: ${strategy}`); } // Wait for all replicas in parallel with timeout and exponential backoff @@ -329,7 +329,7 @@ function extendWithReplicationWait(primary: T, replicaCl `; target = durable_lsn.toString(); } else { - throw new StackAssertionError(`Unknown replication wait strategy: ${strategy}`); + throw new HexclaveAssertionError(`Unknown replication wait strategy: ${strategy}`); } span.setAttribute('stack.db-replication.target', target); @@ -337,11 +337,11 @@ function extendWithReplicationWait(primary: T, replicaCl const caughtUp = await waitForReplication(replicaClients, target, 2000); if (!caughtUp) { span.setAttribute('stack.db-replication.timeout', true); - captureError("prisma-client-replication-timeout", new StackAssertionError("Replication wait timed out after 2s. The replica may be behind, or something weird is going on!")); + captureError("prisma-client-replication-timeout", new HexclaveAssertionError("Replication wait timed out after 2s. The replica may be behind, or something weird is going on!")); } } catch (e) { span.setAttribute('stack.db-replication.error', `${e}`); - captureError("prisma-client-replication-error", new StackAssertionError("Error getting replication target and waiting. We'll just wait 1000ms instead, but please fix this as the replication may not be working.", { cause: e })); + captureError("prisma-client-replication-error", new HexclaveAssertionError("Error getting replication target and waiting. We'll just wait 1000ms instead, but please fix this as the replication may not be working.", { cause: e })); await wait(1_000); } }); @@ -583,7 +583,7 @@ export const RawQuery = { return acc.filter(c => q.supportedPrismaClients.includes(c)); }, allSupportedPrismaClients as RawQuery["supportedPrismaClients"]); if (supportedPrismaClients.length === 0) { - throw new StackAssertionError("The queries must have at least one overlapping supported Prisma client"); + throw new HexclaveAssertionError("The queries must have at least one overlapping supported Prisma client"); } // Only mark combined query as read-only if all individual queries are read-only diff --git a/apps/backend/src/proxy.tsx b/apps/backend/src/proxy.tsx index 2697fc3cd7..eb42d94e3f 100644 --- a/apps/backend/src/proxy.tsx +++ b/apps/backend/src/proxy.tsx @@ -1,5 +1,5 @@ import { getEnvVariable, getNodeEnvironment } from '@stackframe/stack-shared/dist/utils/env'; -import { StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors'; +import { HexclaveAssertionError } from '@stackframe/stack-shared/dist/utils/errors'; import { wait } from '@stackframe/stack-shared/dist/utils/promises'; import apiVersions from './generated/api-versions.json'; import routes from './generated/routes.json'; @@ -69,7 +69,7 @@ export async function proxy(request: NextRequest) { const delay = +getEnvVariable('STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS', '0'); if (delay) { if (getNodeEnvironment().includes('production')) { - throw new StackAssertionError('STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS environment variable is only allowed in development'); + throw new HexclaveAssertionError('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); @@ -151,7 +151,7 @@ export async function proxy(request: NextRequest) { const version = apiVersions[i]; const nextVersion = apiVersions[i + 1]; if (!nextVersion.migrationFolder) { - throw new StackAssertionError(`No migration folder found for version ${nextVersion.name}. This is a bug because every version except the first should have a migration folder.`); + throw new HexclaveAssertionError(`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); diff --git a/apps/backend/src/route-handlers/crud-handler.tsx b/apps/backend/src/route-handlers/crud-handler.tsx index 0049693a1d..eeef642ccc 100644 --- a/apps/backend/src/route-handlers/crud-handler.tsx +++ b/apps/backend/src/route-handlers/crud-handler.tsx @@ -6,7 +6,7 @@ import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/proje import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users"; import { yupArray, yupBoolean, yupMixed, yupNumber, yupObject, yupString, yupValidate } from "@stackframe/stack-shared/dist/schema-fields"; import { typedIncludes } from "@stackframe/stack-shared/dist/utils/arrays"; -import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { FilterUndefined } from "@stackframe/stack-shared/dist/utils/objects"; import { deindent, typedToLowercase } from "@stackframe/stack-shared/dist/utils/strings"; import { traceSpan } from "@stackframe/stack-shared/dist/utils/telemetry"; @@ -251,17 +251,17 @@ export function createCrudHandlers< }) => { if (tenancy) { if (project || branchId) { - throw new StackAssertionError("Must specify either project and branchId or tenancy, not both"); + throw new HexclaveAssertionError("Must specify either project and branchId or tenancy, not both"); } project = tenancy.project; branchId = tenancy.branchId; } else if (project) { if (!branchId) { - throw new StackAssertionError("Must specify branchId when specifying project"); + throw new HexclaveAssertionError("Must specify branchId when specifying project"); } tenancy = await getSoleTenancyFromProjectBranch(project.id, branchId); } else { - throw new StackAssertionError("Must specify either project and branchId or tenancy"); + throw new HexclaveAssertionError("Must specify either project and branchId or tenancy"); } try { @@ -280,7 +280,7 @@ export function createCrudHandlers< }); }); } catch (error) { - if (allowedErrorTypes?.some((a) => error instanceof a) || error instanceof StackAssertionError) { + if (allowedErrorTypes?.some((a) => error instanceof a) || error instanceof HexclaveAssertionError) { throw error; } throw new CrudHandlerInvocationError(error); @@ -308,7 +308,7 @@ async function validate(obj: unknown, schema: yup.ISchema, currentUser: Us }); } catch (error) { if (error instanceof yup.ValidationError) { - throw new StackAssertionError( + throw new HexclaveAssertionError( deindent` ${validationDescription} failed in CRUD handler. diff --git a/apps/backend/src/route-handlers/cud-handler.tsx b/apps/backend/src/route-handlers/cud-handler.tsx index 3be5409c18..bafca6e271 100644 --- a/apps/backend/src/route-handlers/cud-handler.tsx +++ b/apps/backend/src/route-handlers/cud-handler.tsx @@ -6,7 +6,7 @@ import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/proje import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users"; import { yupArray, yupBoolean, yupMixed, yupNumber, yupObject, yupString, yupValidate } from "@stackframe/stack-shared/dist/schema-fields"; import { typedIncludes } from "@stackframe/stack-shared/dist/utils/arrays"; -import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { FilterUndefined } from "@stackframe/stack-shared/dist/utils/objects"; import { deindent, typedToLowercase } from "@stackframe/stack-shared/dist/utils/strings"; import { traceSpan } from "@stackframe/stack-shared/dist/utils/telemetry"; @@ -170,7 +170,7 @@ export function createCudHandlers< }); if (availableAccessTypes.length === 0) { - throw new StackAssertionError(`No access types available for operation ${operation} in CUD handler; check that the corresponding schemas are defined in the CrudSchema`); + throw new HexclaveAssertionError(`No access types available for operation ${operation} in CUD handler; check that the corresponding schemas are defined in the CrudSchema`); } // Build invoke helpers per access type to power both route handlers and direct calls. @@ -232,10 +232,10 @@ export function createCudHandlers< }) as any; if (listResult.is_paginated) { - throw new StackAssertionError("Read operation returned a paginated list; reads must return exactly one item"); + throw new HexclaveAssertionError("Read operation returned a paginated list; reads must return exactly one item"); } if (listResult.items.length !== 1) { - throw new StackAssertionError(`Read operation returned ${listResult.items.length} items; reads must return exactly one item`); + throw new HexclaveAssertionError(`Read operation returned ${listResult.items.length} items; reads must return exactly one item`); } return listResult.items[0]; }; @@ -318,18 +318,18 @@ export function createCudHandlers< ) => { if (tenancy) { if (project || branchId) { - throw new StackAssertionError("Must specify either project and branchId or tenancy, not both"); + throw new HexclaveAssertionError("Must specify either project and branchId or tenancy, not both"); } return { project: tenancy.project, branchId: tenancy.branchId, tenancy }; } if (project) { if (!branchId) { - throw new StackAssertionError("Must specify branchId when specifying project"); + throw new HexclaveAssertionError("Must specify branchId when specifying project"); } const resolvedTenancy = await getSoleTenancyFromProjectBranch(project.id, branchId); return { project, branchId, tenancy: resolvedTenancy }; } - throw new StackAssertionError("Must specify either project and branchId or tenancy"); + throw new HexclaveAssertionError("Must specify either project and branchId or tenancy"); }; const makeDirectInvoke = (entry: (typeof accessTypeEntries) extends Map ? V : never, accessType: "client" | "server" | "admin", directOperation: CudOperation) => { @@ -362,7 +362,7 @@ export function createCudHandlers< }); }); } catch (error) { - if (allowedErrorTypes?.some((a: any) => error instanceof a) || error instanceof StackAssertionError) { + if (allowedErrorTypes?.some((a: any) => error instanceof a) || error instanceof HexclaveAssertionError) { throw error; } throw new CudHandlerInvocationError(error); @@ -399,7 +399,7 @@ async function validate(obj: unknown, schema: yup.ISchema, currentUser: Us }); } catch (error) { if (error instanceof yup.ValidationError) { - throw new StackAssertionError( + throw new HexclaveAssertionError( deindent` ${validationDescription} failed in CUD handler. diff --git a/apps/backend/src/route-handlers/smart-request.tsx b/apps/backend/src/route-handlers/smart-request.tsx index 2562c599dc..9d9f508797 100644 --- a/apps/backend/src/route-handlers/smart-request.tsx +++ b/apps/backend/src/route-handlers/smart-request.tsx @@ -12,7 +12,7 @@ import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users"; import { StackAdaptSentinel, yupValidate } from "@stackframe/stack-shared/dist/schema-fields"; import { groupBy, typedIncludes } from "@stackframe/stack-shared/dist/utils/arrays"; import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, StatusError, captureError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError, captureError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { deindent } from "@stackframe/stack-shared/dist/utils/strings"; import { traceSpan, withTraceSpan } from "@stackframe/stack-shared/dist/utils/telemetry"; import { NextRequest } from "next/server"; @@ -83,7 +83,7 @@ async function validate(obj: SmartRequest, schema: yup.Schema, req: NextRe if (error instanceof yup.ValidationError) { if (req === null) { // we weren't called by a HTTP request, so it must be a logical error in a manual invocation - throw new StackAssertionError("Request validation failed", { cause: error }); + throw new HexclaveAssertionError("Request validation failed", { cause: error }); } else { const inners = error.inner.length ? error.inner : [error]; const description = schema.describe(); @@ -234,7 +234,7 @@ const parseAuth = withTraceSpan('smart request parseAuth', async (req: NextReque if (!user) { // this is the case when access token is still valid, but the user is deleted from the database // this should be very rare, let's log it on Sentry when it happens - captureError("admin-access-token-expiration", new StackAssertionError("User not found for admin access token. This may not be a bug, but it's worth investigating")); + captureError("admin-access-token-expiration", new HexclaveAssertionError("User not found for admin access token. This may not be a bug, but it's worth investigating")); throw new StatusError(401, "The user associated with the admin access token is no longer valid. Please refresh the admin access token and try again."); } @@ -315,7 +315,7 @@ const parseAuth = withTraceSpan('smart request parseAuth', async (req: NextReque break; } default: { - throw new StackAssertionError(`Unexpected request type: ${requestType}. This should never happen because we should've filtered this earlier`); + throw new HexclaveAssertionError(`Unexpected request type: ${requestType}. This should never happen because we should've filtered this earlier`); } } } diff --git a/apps/backend/src/route-handlers/smart-response.tsx b/apps/backend/src/route-handlers/smart-response.tsx index bf5bb5aa02..36b45df95d 100644 --- a/apps/backend/src/route-handlers/smart-response.tsx +++ b/apps/backend/src/route-handlers/smart-response.tsx @@ -1,5 +1,5 @@ import { yupValidate } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { Json } from "@stackframe/stack-shared/dist/utils/json"; import { deepPlainEquals } from "@stackframe/stack-shared/dist/utils/objects"; import { traceSpan } from "@stackframe/stack-shared/dist/utils/telemetry"; @@ -52,7 +52,7 @@ export async function validateSmartResponse(req: NextRequest | null, smartReq currentUserId: smartReq.auth?.user?.id ?? null, }); } catch (error) { - throw new StackAssertionError(`Error occurred during ${req ? `${req.method} ${req.url}` : "a custom endpoint invocation's"} response validation: ${error}`, { obj, schema, cause: error }); + throw new HexclaveAssertionError(`Error occurred during ${req ? `${req.method} ${req.url}` : "a custom endpoint invocation's"} response validation: ${error}`, { obj, schema, cause: error }); } } @@ -92,7 +92,7 @@ export async function createResponse(req: NextRequest | } case "json": { if (obj.body === undefined || !deepPlainEquals(obj.body, JSON.parse(JSON.stringify(obj.body)), { ignoreUndefinedValues: true })) { - throw new StackAssertionError("Invalid JSON body is not JSON", { body: obj.body }); + throw new HexclaveAssertionError("Invalid JSON body is not JSON", { body: obj.body }); } headers.set("content-type", ["application/json; charset=utf-8"]); arrayBufferBody = new TextEncoder().encode(JSON.stringify(obj.body, null, jsonIndent)); @@ -133,7 +133,9 @@ export async function createResponse(req: NextRequest | // Add the request ID to the response headers + // Hexclave rebrand: dual-emit both x-hexclave-* and x-stack-* so old and new SDKs can both read it. headers.set("x-stack-request-id", [requestId]); + headers.set("x-hexclave-request-id", [requestId]); // Disable caching by default @@ -143,7 +145,9 @@ export async function createResponse(req: NextRequest | // If the x-stack-override-error-status header is given, override 4xx statuses to 200. if (req?.headers.has("x-stack-override-error-status") && status >= 400 && status < 500) { status = 200; + // Hexclave rebrand: dual-emit both x-hexclave-* and x-stack-* so old and new SDKs can both read it. headers.set("x-stack-actual-status", [obj.statusCode.toString()]); + headers.set("x-hexclave-actual-status", [obj.statusCode.toString()]); } // set all headers from the smart response (considering case insensitivity) diff --git a/apps/backend/src/route-handlers/smart-route-handler.tsx b/apps/backend/src/route-handlers/smart-route-handler.tsx index dfd9d21863..fc1e0146bd 100644 --- a/apps/backend/src/route-handlers/smart-route-handler.tsx +++ b/apps/backend/src/route-handlers/smart-route-handler.tsx @@ -6,7 +6,7 @@ import { EndpointDocumentation } from "@stackframe/stack-shared/dist/crud"; import { KnownError, KnownErrors } from "@stackframe/stack-shared/dist/known-errors"; import { generateSecureRandomString } from "@stackframe/stack-shared/dist/utils/crypto"; import { getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, StatusError, captureError, errorToNiceString } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError, captureError, errorToNiceString } from "@stackframe/stack-shared/dist/utils/errors"; import { runAsynchronously, wait } from "@stackframe/stack-shared/dist/utils/promises"; import { traceSpan } from "@stackframe/stack-shared/dist/utils/telemetry"; import { NextRequest } from "next/server"; @@ -135,7 +135,7 @@ export function handleApiRequest(handler: (req: NextRequest, options: any, reque recordRequestStats(req.method, req.nextUrl.pathname, time); if ([301, 302].includes(res.status)) { - throw new StackAssertionError("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 }); + 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; @@ -205,7 +205,8 @@ export type SmartRouteHandler< } function getSmartRouteHandlerSymbol() { - return Symbol.for("stack-smartRouteHandler"); + // Hexclave rebrand: file-private symbol key, renamed outright (no cross-version compat needed). + return Symbol.for("hexclave-smartRouteHandler"); } export function isSmartRouteHandler(handler: any): handler is SmartRouteHandler { @@ -240,7 +241,7 @@ export function createSmartRouteHandler< overloadGenerator(overloadParam), ])); if (overloads.size !== overloadParams.length) { - throw new StackAssertionError("Duplicate overload parameters"); + throw new HexclaveAssertionError("Duplicate overload parameters"); } const invoke = async (nextRequest: NextRequest | null, requestId: string, smartRequest: SmartRequest, shouldSetContext: boolean = false) => { @@ -330,9 +331,9 @@ const mergeErrorPriority = [ function mergeOverloadErrors(errors: StatusError[]): StatusError[] { if (errors.length > 6) { // TODO fix this - throw new StackAssertionError("Too many overloads failed, refusing to trying to merge them as it would be computationally expensive and could be used for a DoS attack. Fix this if we ever have an endpoint with > 8 overloads"); + throw new HexclaveAssertionError("Too many overloads failed, refusing to trying to merge them as it would be computationally expensive and could be used for a DoS attack. Fix this if we ever have an endpoint with > 8 overloads"); } else if (errors.length === 0) { - throw new StackAssertionError("No errors to merge"); + throw new HexclaveAssertionError("No errors to merge"); } else if (errors.length === 1) { return [errors[0]]; } else if (errors.length === 2) { diff --git a/apps/backend/src/route-handlers/verification-code-handler.tsx b/apps/backend/src/route-handlers/verification-code-handler.tsx index 062e2e4687..8c1796c272 100644 --- a/apps/backend/src/route-handlers/verification-code-handler.tsx +++ b/apps/backend/src/route-handlers/verification-code-handler.tsx @@ -8,7 +8,7 @@ import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/proje import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users"; import { adaptSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; import { generateSecureRandomString } from "@stackframe/stack-shared/dist/utils/crypto"; -import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { DeepPartial } from "@stackframe/stack-shared/dist/utils/objects"; import * as yup from "yup"; import { SmartRequest } from "./smart-request"; @@ -256,7 +256,7 @@ export function createVerificationCodeHandler< async sendCode(createOptions, sendOptions) { const codeObj = await this.createCode(createOptions); if (!options.send) { - throw new StackAssertionError("Cannot use sendCode on this verification code handler because it doesn't have a send function"); + throw new HexclaveAssertionError("Cannot use sendCode on this verification code handler because it doesn't have a send function"); } return await options.send(codeObj, parseProjectBranchCombo(createOptions), sendOptions); }, diff --git a/apps/backend/src/s3.tsx b/apps/backend/src/s3.tsx index 1a29687618..8ee39cc351 100644 --- a/apps/backend/src/s3.tsx +++ b/apps/backend/src/s3.tsx @@ -1,6 +1,6 @@ import { GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { ImageProcessingError, parseBase64Image } from "./lib/images"; const S3_REGION = getEnvVariable("STACK_S3_REGION", ""); @@ -47,12 +47,12 @@ export async function uploadBytes(options: { private?: boolean, }) { if (!s3Client) { - throw new StackAssertionError("S3 is not configured"); + throw new HexclaveAssertionError("S3 is not configured"); } const bucket = options.private ? S3_PRIVATE_BUCKET : S3_BUCKET; if (!bucket) { - throw new StackAssertionError(options.private ? "S3 private bucket is not configured" : "S3 bucket is not configured"); + throw new HexclaveAssertionError(options.private ? "S3 private bucket is not configured" : "S3 bucket is not configured"); } const command = new PutObjectCommand({ @@ -88,23 +88,23 @@ async function readBodyToBytes(body: unknown): Promise { } else if (Buffer.isBuffer(chunk)) { chunks.push(chunk); } else { - throw new StackAssertionError("Unexpected S3 body chunk type"); + throw new HexclaveAssertionError("Unexpected S3 body chunk type"); } } return new Uint8Array(Buffer.concat(chunks)); } - throw new StackAssertionError("Unexpected S3 body type"); + throw new HexclaveAssertionError("Unexpected S3 body type"); } export async function downloadBytes(options: { key: string, private?: boolean }): Promise { if (!s3Client) { - throw new StackAssertionError("S3 is not configured"); + throw new HexclaveAssertionError("S3 is not configured"); } const bucket = options.private ? S3_PRIVATE_BUCKET : S3_BUCKET; if (!bucket) { - throw new StackAssertionError(options.private ? "S3 private bucket is not configured" : "S3 bucket is not configured"); + throw new HexclaveAssertionError(options.private ? "S3 private bucket is not configured" : "S3 bucket is not configured"); } const command = new GetObjectCommand({ @@ -114,7 +114,7 @@ export async function downloadBytes(options: { key: string, private?: boolean }) const res = await s3Client.send(command); if (!res.Body) { - throw new StackAssertionError("S3 getObject returned empty body"); + throw new HexclaveAssertionError("S3 getObject returned empty body"); } return await readBodyToBytes(res.Body); @@ -130,7 +130,7 @@ async function uploadBase64Image({ folderName: string, }) { if (!s3Client) { - throw new StackAssertionError("S3 is not configured"); + throw new HexclaveAssertionError("S3 is not configured"); } let buffer: Buffer; diff --git a/apps/backend/src/smart-router.tsx b/apps/backend/src/smart-router.tsx index 5640ad0e99..4b138b958f 100644 --- a/apps/backend/src/smart-router.tsx +++ b/apps/backend/src/smart-router.tsx @@ -1,5 +1,5 @@ import { isTruthy } from "@stackframe/stack-shared/dist/utils/booleans"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { numberCompare } from "@stackframe/stack-shared/dist/utils/numbers"; @@ -76,10 +76,10 @@ export const SmartRouter = { } as const; } else { if (!allFiles.includes(`src/app/api/migrations/${version}/beta-changes.txt`)) { - throw new StackAssertionError(`API version ${version} does not have a beta-changes.txt file. The beta-changes.txt file should contain the changes since the last beta release.`); + throw new HexclaveAssertionError(`API version ${version} does not have a beta-changes.txt file. The beta-changes.txt file should contain the changes since the last beta release.`); } if (!version.includes("beta") && !allFiles.includes(`src/app/api/migrations/${version}/changes.txt`)) { - throw new StackAssertionError(`API version ${version} does not have a changes.txt file. The changes.txt file should contain the changes since the last full (non-beta) release.`); + throw new HexclaveAssertionError(`API version ${version} does not have a changes.txt file. The changes.txt file should contain the changes since the last full (non-beta) release.`); } return { name: version, @@ -97,7 +97,7 @@ export const SmartRouter = { function parseApiVersionStringToArray(version: string): [number, number] { const matchResult = version.match(/^v(\d+)(?:beta(\d+))?$/); - if (!matchResult) throw new StackAssertionError(`Invalid API version string: ${version}`); + if (!matchResult) throw new HexclaveAssertionError(`Invalid API version string: ${version}`); return [+matchResult[1], matchResult[2] === "" ? Number.POSITIVE_INFINITY : +matchResult[2]]; } @@ -118,14 +118,14 @@ function matchPath(path: string, toMatchWith: string): Record path.endsWith("/stack.config.ts") || path.endsWith("/stack.config.js") || path === "stack.config.ts" || path === "stack.config.js") + .filter((path) => configFileNames.some((name) => path === name || path.endsWith(`/${name}`))) .sort((a, b) => stringCompare(a, b)); } diff --git a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/preview-project-redirect.tsx b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/preview-project-redirect.tsx index 5dd8bb598f..609ffcd65e 100644 --- a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/preview-project-redirect.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/preview-project-redirect.tsx @@ -4,7 +4,7 @@ import Loading from "@/app/loading"; import { useRouter } from "@/components/router"; import { stackAppInternalsSymbol } from "@/lib/stack-app-internals"; import { useStackApp, useUser } from "@stackframe/stack"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; import { useEffect, useMemo, useRef } from "react"; @@ -41,7 +41,7 @@ export default function PreviewProjectRedirect() { if (!response.ok) { const text = await response.text(); - throw new StackAssertionError(`Failed to create preview project: ${response.status} ${text}`); + throw new HexclaveAssertionError(`Failed to create preview project: ${response.status} ${text}`); } const body = await response.json(); diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/[appId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/[appId]/page-client.tsx index 895f49282b..9a77eb63e9 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/[appId]/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/[appId]/page-client.tsx @@ -6,7 +6,7 @@ import { useRouter } from "@/components/router"; import { useUpdateConfig } from "@/lib/config-update"; import { ALL_APPS_FRONTEND, getAppPath, getDocumentationHref, isSubApp, type AppId } from "@/lib/apps-frontend"; import { isAppEnabled } from "@/lib/apps-utils"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; import { PageLayout } from "../../page-layout"; @@ -22,7 +22,7 @@ export default function AppDetailsPageClient({ appId }: { appId: AppId }) { const appFrontend = ALL_APPS_FRONTEND[appId]; if (!(appFrontend as any)) { - throw new StackAssertionError(`App frontend not found for appId: ${appId}`, { appId }); + throw new HexclaveAssertionError(`App frontend not found for appId: ${appId}`, { appId }); } const parentAppId = isSubApp(appFrontend) ? appFrontend.parentAppId : null; const parentAppFrontend = parentAppId == null ? null : ALL_APPS_FRONTEND[parentAppId]; diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx index 41b4bd9c5b..887e0dedfc 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx @@ -32,7 +32,7 @@ import { AdminProject, AuthPage } from "@stackframe/stack"; import type { CompleteConfig } from "@stackframe/stack-shared/dist/config/schema"; import type { RestrictedReason } from "@stackframe/stack-shared/dist/schema-fields"; import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { allProviders } from "@stackframe/stack-shared/dist/utils/oauth"; import { typedFromEntries } from "@stackframe/stack-shared/dist/utils/objects"; import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids"; @@ -133,7 +133,7 @@ function adminProviderToConfigProvider(provider: AdminOAuthProviderConfig): Comp }; } default: { - throw new StackAssertionError(`Unknown provider type: ${(provider as { type: unknown }).type}`); + throw new HexclaveAssertionError(`Unknown provider type: ${(provider as { type: unknown }).type}`); } } } @@ -657,7 +657,7 @@ export default function PageClient() { }; const onMergeStrategyChange = (value: string) => { if (value !== "link_method" && value !== "raise_error" && value !== "allow_duplicates") { - throw new StackAssertionError(`Unknown OAuth account merge strategy: ${value}`); + throw new HexclaveAssertionError(`Unknown OAuth account merge strategy: ${value}`); } const next: OAuthAccountMergeStrategy = value; setLocalMergeStrategy(next === config.auth.oauth.accountMergeStrategy ? undefined : next); diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx index 74add1e4e7..ad3f26c731 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx @@ -7,7 +7,7 @@ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, ActionCel import { DataGrid, useDataGridUrlState, useDataSource, type DataGridColumnDef } from "@stackframe/dashboard-ui-components"; import { useUpdateConfig } from "@/lib/config-update"; import { yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { typedEntries } from "@stackframe/stack-shared/dist/utils/objects"; import { isValidHostnameWithWildcards, isValidUrl } from "@stackframe/stack-shared/dist/utils/urls"; import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids"; @@ -168,7 +168,7 @@ function EditDialog(props: { } catch (error) { // this piece of code fails a lot, so let's add some additional information to the error // TODO: remove this error once we're confident this is no longer happening - throw new StackAssertionError( + throw new HexclaveAssertionError( `Failed to update domains: ${error}`, { cause: error, diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx index 20b687cc1c..a8434f1490 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx @@ -67,7 +67,7 @@ import type { CompleteConfig } from "@stackframe/stack-shared/dist/config/schema import { useAsyncCallback } from "@stackframe/stack-shared/dist/hooks/use-async-callback"; import type { SignUpRule, SignUpRuleAction } from "@stackframe/stack-shared/dist/interface/crud/sign-up-rules"; import { isValidCountryCode, normalizeCountryCode } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { standardProviders } from "@stackframe/stack-shared/dist/utils/oauth"; import { typedEntries } from "@stackframe/stack-shared/dist/utils/objects"; import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; @@ -259,7 +259,7 @@ function parseRuleTriggerRows(resultRows: Record[]): RuleTrigge return resultRows.map((row) => { const triggeredAt = row.triggered_at; if (typeof triggeredAt !== "string") { - throw new StackAssertionError("Expected sign-up rule trigger row to include triggered_at:string", { row }); + throw new HexclaveAssertionError("Expected sign-up rule trigger row to include triggered_at:string", { row }); } const emailRaw = row.email; @@ -270,7 +270,7 @@ function parseRuleTriggerRows(resultRows: Record[]): RuleTrigge return { id: generateUuid(), triggeredAt, email: emailRaw }; } - throw new StackAssertionError("Expected sign-up rule trigger row to include email:null|string", { row }); + throw new HexclaveAssertionError("Expected sign-up rule trigger row to include email:null|string", { row }); }); } @@ -679,7 +679,7 @@ function ActionDropdown({ state, size = "sm", className }: { state: RuleEditorSt value={state.actionType} onValueChange={(v) => { if (!isActionType(v)) { - throw new StackAssertionError(`Unexpected sign-up rule action type: ${v}`); + throw new HexclaveAssertionError(`Unexpected sign-up rule action type: ${v}`); } state.setActionType(v); }} @@ -962,7 +962,7 @@ function DefaultActionRow({ value={value} onValueChange={(v) => { if (!isDefaultAction(v)) { - throw new StackAssertionError(`Unexpected default sign-up rule action: ${v}`); + throw new HexclaveAssertionError(`Unexpected default sign-up rule action: ${v}`); } onChange(v); }} @@ -1061,7 +1061,7 @@ function useTestRulesState(stackAdminApp: ReturnType) { ); if (!response.ok) { - throw new StackAssertionError(`Failed to test sign-up rules: ${response.status} ${response.statusText}`); + throw new HexclaveAssertionError(`Failed to test sign-up rules: ${response.status} ${response.statusText}`); } const data = await response.json(); @@ -1580,7 +1580,7 @@ function useSignUpRulesAnalytics() { if (cancelled) return; if (!response.ok) { - throw new StackAssertionError(`Failed to fetch sign-up rules stats: ${response.status} ${response.statusText}`); + throw new HexclaveAssertionError(`Failed to fetch sign-up rules stats: ${response.status} ${response.statusText}`); } const data = await response.json(); diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/teams/[teamId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/teams/[teamId]/page-client.tsx index 721c1d3911..90f08e850b 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/teams/[teamId]/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/teams/[teamId]/page-client.tsx @@ -15,7 +15,7 @@ import { DatabaseIcon, PlusIcon } from "@phosphor-icons/react"; import { ServerTeam } from '@stackframe/stack'; import { AppId } from "@stackframe/stack-shared/dist/apps/apps-config"; import { strictEmailSchema, yupObject } from '@stackframe/stack-shared/dist/schema-fields'; -import { StackAssertionError, throwErr } from '@stackframe/stack-shared/dist/utils/errors'; +import { HexclaveAssertionError, throwErr } from '@stackframe/stack-shared/dist/utils/errors'; import { runAsynchronouslyWithAlert } from '@stackframe/stack-shared/dist/utils/promises'; import { notFound, usePathname, useSearchParams } from 'next/navigation'; import { Suspense, useCallback, useMemo, useState } from 'react'; @@ -237,7 +237,7 @@ function TeamPage({ team }: { team: ServerTeam }) { selectedCategory={activeTab} onSelect={(id) => { if (!isTeamPageTab(id)) { - throw new StackAssertionError(`Unknown team page tab selected: ${id}`); + throw new HexclaveAssertionError(`Unknown team page tab selected: ${id}`); } setSelectedTab(id); }} diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx index e78b89c855..6f921fb66f 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx @@ -2,7 +2,7 @@ import { useDashboardInternalUser } from "@/lib/dashboard-user"; import { StackAdminApp } from "@stackframe/stack"; -import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { notFound, usePathname } from "next/navigation"; import React from "react"; @@ -47,7 +47,7 @@ export function useAdminApp(projectId?: string) { export function useProjectId() { const pathname = usePathname(); if (!pathname.startsWith("/projects/")) { - throw new StackAssertionError("useProjectId must be used within a project route"); + throw new HexclaveAssertionError("useProjectId must be used within a project route"); } const projectId = pathname.split("/")[2]; return projectId; diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx index cb515dbed7..ae02dd16f7 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx @@ -50,7 +50,7 @@ import { KnownErrors } from "@stackframe/stack-shared"; import { AppId } from "@stackframe/stack-shared/dist/apps/apps-config"; import { normalizeCountryCode } from "@stackframe/stack-shared/dist/schema-fields"; import { fromNow } from "@stackframe/stack-shared/dist/utils/dates"; -import { captureError, StackAssertionError, throwErr } from '@stackframe/stack-shared/dist/utils/errors'; +import { captureError, HexclaveAssertionError, throwErr } from '@stackframe/stack-shared/dist/utils/errors'; import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; import { deindent } from "@stackframe/stack-shared/dist/utils/strings"; import { urlString } from "@stackframe/stack-shared/dist/utils/urls"; @@ -276,7 +276,7 @@ function RestrictionDialog({ toast({ title: "User restricted", variant: "success" }); onOpenChange(false); } catch (error) { - captureError(`user-restriction-save-and-restrict-error`, new StackAssertionError(`Failed to save and restrict user ${user.id}`, { cause: error })); + captureError(`user-restriction-save-and-restrict-error`, new HexclaveAssertionError(`Failed to save and restrict user ${user.id}`, { cause: error })); setSubmitError(error instanceof Error && error.message ? error.message : "Failed to save the restriction. Please try again."); } finally { setIsSaving(false); @@ -295,7 +295,7 @@ function RestrictionDialog({ toast({ title: "Restriction removed", variant: "success" }); onOpenChange(false); } catch (error) { - captureError(`user-restriction-remove-error`, new StackAssertionError(`Failed to remove restriction for user ${user.id}`, { cause: error })); + captureError(`user-restriction-remove-error`, new HexclaveAssertionError(`Failed to remove restriction for user ${user.id}`, { cause: error })); setSubmitError(error instanceof Error && error.message ? error.message : "Failed to remove the restriction. Please try again."); } finally { setIsSaving(false); @@ -1375,7 +1375,7 @@ function OAuthProviderDialog(props: OAuthProviderDialogProps) { const providerConfig = availableProviders.find((p: any) => p.id === values.providerId); if (!providerConfig) { - throw new StackAssertionError(`Provider config not found for ${values.providerId}`); + throw new HexclaveAssertionError(`Provider config not found for ${values.providerId}`); } result = await stackAdminApp.createOAuthProvider({ @@ -1963,7 +1963,7 @@ function UserPage({ user }: { user: ServerUser }) { selectedCategory={activeTab} onSelect={(id) => { if (!isUserPageTab(id)) { - throw new StackAssertionError(`Unknown user page tab selected: ${id}`); + throw new HexclaveAssertionError(`Unknown user page tab selected: ${id}`); } setSelectedTab(id); }} diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/widget-playground/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/widget-playground/page-client.tsx index 276140765f..d5c1cb14c7 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/widget-playground/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/widget-playground/page-client.tsx @@ -5,7 +5,7 @@ import { Button, ButtonProps, Dialog, DialogBody, DialogContent, DialogFooter, D import { DndContext, closestCenter, pointerWithin, useDraggable, useDroppable } from '@dnd-kit/core'; import useResizeObserver from '@react-hook/resize-observer'; import { range } from '@stackframe/stack-shared/dist/utils/arrays'; -import { StackAssertionError, errorToNiceString, throwErr } from '@stackframe/stack-shared/dist/utils/errors'; +import { HexclaveAssertionError, errorToNiceString, throwErr } from '@stackframe/stack-shared/dist/utils/errors'; import { bundleJavaScript } from '@stackframe/stack-shared/dist/utils/esbuild'; import { Json, isJsonSerializable } from '@stackframe/stack-shared/dist/utils/json'; import { deepPlainEquals, filterUndefined, isNotNull } from '@stackframe/stack-shared/dist/utils/objects'; @@ -207,10 +207,10 @@ export function serializeWidgetInstance(widgetInstance: WidgetInstance export function deserializeWidgetInstance(widgets: Widget[], serialized: Json): WidgetInstance { const serializedAny: any = serialized; if (typeof serializedAny !== "object" || serializedAny === null) { - throw new StackAssertionError(`Serialized widget instance is not an object!`, { serialized }); + throw new HexclaveAssertionError(`Serialized widget instance is not an object!`, { serialized }); } if (typeof serializedAny.id !== "string") { - throw new StackAssertionError(`Serialized widget instance id is not a string!`, { serialized }); + throw new HexclaveAssertionError(`Serialized widget instance id is not a string!`, { serialized }); } return { id: serializedAny.id, @@ -253,42 +253,42 @@ class WidgetInstanceGrid { const allInstanceIds = new Set(); const checkInstance = (instance: WidgetInstance) => { if (allInstanceIds.has(instance.id)) { - throw new StackAssertionError(`Widget instance ${instance.id} is duplicated!`, { instance }); + throw new HexclaveAssertionError(`Widget instance ${instance.id} is duplicated!`, { instance }); } allInstanceIds.add(instance.id); const settings = getSettings(instance); const state = getState(instance); if (!isJsonSerializable(settings)) { - throw new StackAssertionError(`Settings must be JSON serializable`, { instance, settings }); + throw new HexclaveAssertionError(`Settings must be JSON serializable`, { instance, settings }); } if (!isJsonSerializable(state)) { - throw new StackAssertionError(`State must be JSON serializable`, { instance, state }); + throw new HexclaveAssertionError(`State must be JSON serializable`, { instance, state }); } }; for (const element of this._nonEmptyElements) { if (element.instance === null) { - throw new StackAssertionError(`Non-empty element instance is null!`, { element }); + throw new HexclaveAssertionError(`Non-empty element instance is null!`, { element }); } if (element.width < WidgetInstanceGrid.MIN_ELEMENT_WIDTH) { - throw new StackAssertionError(`Width must be at least ${WidgetInstanceGrid.MIN_ELEMENT_WIDTH}`, { width: element.width, element }); + throw new HexclaveAssertionError(`Width must be at least ${WidgetInstanceGrid.MIN_ELEMENT_WIDTH}`, { width: element.width, element }); } if (element.height < WidgetInstanceGrid.MIN_ELEMENT_HEIGHT) { - throw new StackAssertionError(`Height must be at least ${WidgetInstanceGrid.MIN_ELEMENT_HEIGHT}`, { height: element.height, element }); + throw new HexclaveAssertionError(`Height must be at least ${WidgetInstanceGrid.MIN_ELEMENT_HEIGHT}`, { height: element.height, element }); } if (element.x + element.width > width) { - throw new StackAssertionError(`Element ${element.instance.id} is out of bounds: ${element.x + element.width} > ${width}`, { width, element }); + throw new HexclaveAssertionError(`Element ${element.instance.id} is out of bounds: ${element.x + element.width} > ${width}`, { width, element }); } if (this._fixedHeight !== "auto" && element.y + element.height > this._fixedHeight) { - throw new StackAssertionError(`Element ${element.instance.id} is out of bounds: ${element.y + element.height} > ${this._fixedHeight}`, { height: this._fixedHeight, element }); + throw new HexclaveAssertionError(`Element ${element.instance.id} is out of bounds: ${element.y + element.height} > ${this._fixedHeight}`, { height: this._fixedHeight, element }); } if (element.instance.widget.isHeightVariable) { - throw new StackAssertionError(`Element ${element.instance.id} is passed in as a grid element, but has a variable height!`, { element }); + throw new HexclaveAssertionError(`Element ${element.instance.id} is passed in as a grid element, but has a variable height!`, { element }); } checkInstance(element.instance); } for (const [y, instances] of this._varHeights) { if (instances.length === 0) { - throw new StackAssertionError(`No variable height widgets found at y = ${y}!`, { varHeights: this._varHeights }); + throw new HexclaveAssertionError(`No variable height widgets found at y = ${y}!`, { varHeights: this._varHeights }); } for (const instance of instances) { checkInstance(instance); @@ -351,7 +351,7 @@ class WidgetInstanceGrid { // as a sanity check, let's serialize as JSON just to make sure it's JSON-serializable const afterJsonSerialization = JSON.parse(JSON.stringify(res)); if (!deepPlainEquals(afterJsonSerialization, res)) { - throw new StackAssertionError(`WidgetInstanceGrid serialization is not JSON-serializable!`, { + throw new HexclaveAssertionError(`WidgetInstanceGrid serialization is not JSON-serializable!`, { beforeJsonSerialization: res, afterJsonSerialization, }); @@ -362,10 +362,10 @@ class WidgetInstanceGrid { public static fromSerialized(serialized: Json): WidgetInstanceGrid { if (typeof serialized !== "object" || serialized === null) { - throw new StackAssertionError(`WidgetInstanceGrid serialization is not an object or is null!`, { serialized }); + throw new HexclaveAssertionError(`WidgetInstanceGrid serialization is not an object or is null!`, { serialized }); } if (!("className" in serialized) || typeof serialized.className !== "string" || serialized.className !== "WidgetInstanceGrid") { - throw new StackAssertionError(`WidgetInstanceGrid serialization is not a WidgetInstanceGrid!`, { serialized }); + throw new HexclaveAssertionError(`WidgetInstanceGrid serialization is not a WidgetInstanceGrid!`, { serialized }); } const serializedAny = serialized as any; @@ -382,7 +382,7 @@ class WidgetInstanceGrid { return new WidgetInstanceGrid(nonEmptyElements, varHeights, serializedAny.width, serializedAny.fixedHeight); } default: { - throw new StackAssertionError(`Unknown WidgetInstanceGrid version ${serializedAny.version}!`, { + throw new HexclaveAssertionError(`Unknown WidgetInstanceGrid version ${serializedAny.version}!`, { serialized, }); } @@ -454,7 +454,7 @@ class WidgetInstanceGrid { const array = new Array(this.width).fill(null).map(() => new Array(this.height).fill(null)); [...this._nonEmptyElements].forEach(({ x, y, width, height, instance }) => { if (x + width > this.width) { - throw new StackAssertionError(`Widget instance ${instance?.id} is out of bounds: ${x + width} > ${this.width}`); + throw new HexclaveAssertionError(`Widget instance ${instance?.id} is out of bounds: ${x + width} > ${this.width}`); } for (let i = 0; i < width; i++) { for (let j = 0; j < height; j++) { @@ -467,7 +467,7 @@ class WidgetInstanceGrid { public getElementAt(x: number, y: number): GridElement { if (x < 0 || x >= this.width || y < 0 || y >= this.height) { - throw new StackAssertionError(`Invalid coordinates for getElementAt: ${x}, ${y}`); + throw new HexclaveAssertionError(`Invalid coordinates for getElementAt: ${x}, ${y}`); } return [...this.elements()].find((element) => x >= element.x && x < element.x + element.width && y >= element.y && y < element.y + element.height) ?? throwErr(`No element found at ${x}, ${y}`); } @@ -497,10 +497,10 @@ class WidgetInstanceGrid { } const minSize = this.getMinResizableSize(); if (width < minSize.width) { - throw new StackAssertionError(`Width must be at least ${minSize.width}`, { width }); + throw new HexclaveAssertionError(`Width must be at least ${minSize.width}`, { width }); } if (height !== "auto" && height < minSize.height) { - throw new StackAssertionError(`Height must be at least ${minSize.height}`, { height }); + throw new HexclaveAssertionError(`Height must be at least ${minSize.height}`, { height }); } return new WidgetInstanceGrid(this._nonEmptyElements, this._varHeights, width, height); } @@ -510,7 +510,7 @@ class WidgetInstanceGrid { if (element.instance?.widget.calculateMinSize) { const minSize = element.instance.widget.calculateMinSize({ settings: element.instance.settingsOrUndefined, state: element.instance.stateOrUndefined }); if (minSize.widthInGridUnits > element.width || minSize.heightInGridUnits > element.height) { - throw new StackAssertionError(`Widget ${element.instance.widget.id} has a size of ${element.width}x${element.height}, but calculateMinSize returned a smaller value (${minSize.widthInGridUnits}x${minSize.heightInGridUnits}).`); + throw new HexclaveAssertionError(`Widget ${element.instance.widget.id} has a size of ${element.width}x${element.height}, but calculateMinSize returned a smaller value (${minSize.widthInGridUnits}x${minSize.heightInGridUnits}).`); } res.width = Math.max(res.width, minSize.widthInGridUnits); res.height = Math.max(res.height, minSize.heightInGridUnits); @@ -541,7 +541,7 @@ class WidgetInstanceGrid { public withSwappedElements(x1: number, y1: number, x2: number, y2: number) { if (!this.canSwap(x1, y1, x2, y2)) { - throw new StackAssertionError(`Cannot swap elements at ${x1}, ${y1} and ${x2}, ${y2}`); + throw new HexclaveAssertionError(`Cannot swap elements at ${x1}, ${y1} and ${x2}, ${y2}`); } const elementsToSwap = [this.getElementAt(x1, y1), this.getElementAt(x2, y2)]; @@ -630,7 +630,7 @@ class WidgetInstanceGrid { public withResizedElement(x: number, y: number, edgesDelta: { top: number, left: number, bottom: number, right: number }) { const clamped = this.clampElementResize(x, y, edgesDelta); if (!deepPlainEquals(clamped, edgesDelta)) { - throw new StackAssertionError(`Resize is not allowed: ${JSON.stringify(edgesDelta)} requested, but only ${JSON.stringify(clamped)} allowed`); + throw new HexclaveAssertionError(`Resize is not allowed: ${JSON.stringify(edgesDelta)} requested, but only ${JSON.stringify(clamped)} allowed`); } // performance optimization: if there is no change, return the same grid @@ -678,14 +678,14 @@ class WidgetInstanceGrid { public withUpdatedElementSettings(x: number, y: number, newSettings: any) { if (!isJsonSerializable(newSettings)) { - throw new StackAssertionError(`New settings are not JSON serializable: ${JSON.stringify(newSettings)}`, { newSettings }); + throw new HexclaveAssertionError(`New settings are not JSON serializable: ${JSON.stringify(newSettings)}`, { newSettings }); } return this._withUpdatedElementInstance(x, y, (element) => element.instance ? { ...element.instance, settingsOrUndefined: newSettings } : throwErr(`No widget instance at ${x}, ${y}`)); } public withUpdatedElementState(x: number, y: number, newState: any) { if (!isJsonSerializable(newState)) { - throw new StackAssertionError(`New state are not JSON serializable: ${JSON.stringify(newState)}`, { newState }); + throw new HexclaveAssertionError(`New state are not JSON serializable: ${JSON.stringify(newState)}`, { newState }); } return this._withUpdatedElementInstance(x, y, (element) => element.instance ? { ...element.instance, stateOrUndefined: newState } : throwErr(`No widget instance at ${x}, ${y}`)); } @@ -745,7 +745,7 @@ class WidgetInstanceGrid { public withAddedVarHeightAtEndOf(y: number, instance: WidgetInstance) { if (!this.canAddVarHeight(y)) { - throw new StackAssertionError(`Cannot add var height instance at ${y}`, { y, instance }); + throw new HexclaveAssertionError(`Cannot add var height instance at ${y}`, { y, instance }); } const newVarHeights = new Map(this._varHeights); newVarHeights.set(y, [...(newVarHeights.get(y) ?? []), instance]); @@ -1112,7 +1112,7 @@ function SwappableWidgetInstanceGrid(props: { gridRef: RefState {[...(varHeights.get(y) ?? []), null].map((instance, i) => { if (instance !== null && !props.allowVariableHeight) { - throw new StackAssertionError("Variable height widgets are not allowed in this component", { instance }); + throw new HexclaveAssertionError("Variable height widgets are not allowed in this component", { instance }); } const location = instance ? ["before", instance.id] as const: ["end-of", y] as const; const isOverVarHeightSlot = deepPlainEquals(overVarHeightSlot, location); @@ -1155,7 +1155,7 @@ function SwappableWidgetInstanceGrid(props: { gridRef: RefState { - throw new StackAssertionError("Cannot resize a var-height widget!"); + throw new HexclaveAssertionError("Cannot resize a var-height widget!"); }} x={0} y={y} @@ -1207,7 +1207,7 @@ function SwappableWidgetInstanceGrid(props: { gridRef: RefState instance?.id === widgetId); if (!widgetElement) { - throw new StackAssertionError(`Widget instance ${widgetId} not found in grid`); + throw new HexclaveAssertionError(`Widget instance ${widgetId} not found in grid`); } if (event.over) { const overCoordinates = JSON.parse(`${event.over.id}`) as [number, number]; @@ -1224,13 +1224,13 @@ function SwappableWidgetInstanceGrid(props: { gridRef: RefState instance?.id === widgetId); if (!widgetElement) { - throw new StackAssertionError(`Widget instance ${widgetId} not found in grid`); + throw new HexclaveAssertionError(`Widget instance ${widgetId} not found in grid`); } if (event.over) { if (!event.active.rect.current.initial) { // not sure when this happens, if ever. let's ignore it in prod, throw in dev if (process.env.NODE_ENV === 'development') { - throw new StackAssertionError("Active element has no initial rect. Not sure when this happens, so please report it"); + throw new HexclaveAssertionError("Active element has no initial rect. Not sure when this happens, so please report it"); } } else { const overCoordinates = JSON.parse(`${event.over.id}`) as [number, number]; @@ -1488,13 +1488,13 @@ function Draggable(props: { if (!settingsOpenAnimationDetails) { runAsynchronouslyWithAlert(async () => { // we want to wait asynchronously with starting the animations until the dialog is mounted, otherwise we can't sync up the animations - if (!draggableContainerRef.current) throw new StackAssertionError("Draggable container not found", { draggableContainerRef }); + if (!draggableContainerRef.current) throw new HexclaveAssertionError("Draggable container not found", { draggableContainerRef }); for (let i = 0; i < 100; i++) { if (cancelled) return; if (dialogRef.current) break; await wait(10 + 3 * i); } - if (!dialogRef.current) throw new StackAssertionError("Dialog ref not found even after waiting", { dialogRef }); + if (!dialogRef.current) throw new HexclaveAssertionError("Dialog ref not found even after waiting", { dialogRef }); if (cancelled) return; const draggableContainerRect = draggableContainerRef.current.getBoundingClientRect(); @@ -1833,7 +1833,7 @@ function ResizeHandle({ widgetInstance, x, y, ...props }: { }) { const dragBaseCoordinates = useRefState<[number, number] | null>(null); if (![ -1, 0, 1 ].includes(x) || ![ -1, 0, 1 ].includes(y)) { - throw new StackAssertionError(`Invalid resize handle coordinates, must be -1, 0, or 1: ${x}, ${y}`); + throw new HexclaveAssertionError(`Invalid resize handle coordinates, must be -1, 0, or 1: ${x}, ${y}`); } const isCorner = x !== 0 && y !== 0; diff --git a/apps/dashboard/src/app/(main)/integrations/neon-transfer-confirm-page.tsx b/apps/dashboard/src/app/(main)/integrations/neon-transfer-confirm-page.tsx index 0a83b2b2ff..a592b97702 100644 --- a/apps/dashboard/src/app/(main)/integrations/neon-transfer-confirm-page.tsx +++ b/apps/dashboard/src/app/(main)/integrations/neon-transfer-confirm-page.tsx @@ -5,7 +5,7 @@ import { useRouter } from "@/components/router"; import { Button, Card, CardContent, CardFooter, CardHeader, Input, Typography } from "@/components/ui"; import { buildTransferSignUpUrl, getStackAppInternals } from "@/lib/transfer-utils"; import { useStackApp, useUser } from "@stackframe/stack"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { runAsynchronously, runAsynchronouslyWithAlert, wait } from "@stackframe/stack-shared/dist/utils/promises"; import Image from "next/image"; import { useSearchParams } from "next/navigation"; @@ -144,7 +144,7 @@ export default function NeonIntegrationProjectTransferConfirmPageClient() { }); const confirmResJson = await confirmRes.json(); if (typeof confirmResJson?.project_id !== "string") { - throw new StackAssertionError("Neon project transfer confirm response is missing `project_id`", { confirmResJson }); + throw new HexclaveAssertionError("Neon project transfer confirm response is missing `project_id`", { confirmResJson }); } router.push(`/projects/${confirmResJson.project_id}`); await wait(3000); diff --git a/apps/dashboard/src/app/(main)/integrations/oauth-confirm-page.tsx b/apps/dashboard/src/app/(main)/integrations/oauth-confirm-page.tsx index 3c635138e5..45bd1cd0c9 100644 --- a/apps/dashboard/src/app/(main)/integrations/oauth-confirm-page.tsx +++ b/apps/dashboard/src/app/(main)/integrations/oauth-confirm-page.tsx @@ -1,6 +1,6 @@ import { getStackServerApp } from "@/stack/server"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { redirect } from "next/navigation"; import ConfirmCard from "./oauth-confirm-card"; @@ -43,7 +43,7 @@ export default async function IntegrationConfirmPage(props: { }); if (!response.ok) { const text = await response.text(); - throw new StackAssertionError(`Failed to confirm integration: ${response.status} ${text}`, { response, text }); + throw new HexclaveAssertionError(`Failed to confirm integration: ${response.status} ${text}`, { response, text }); } const json = await response.json(); const authorizationCode = json.authorization_code; diff --git a/apps/dashboard/src/app/(main)/integrations/transfer-confirm-page.tsx b/apps/dashboard/src/app/(main)/integrations/transfer-confirm-page.tsx index 52d2e8eca7..2b89ada25d 100644 --- a/apps/dashboard/src/app/(main)/integrations/transfer-confirm-page.tsx +++ b/apps/dashboard/src/app/(main)/integrations/transfer-confirm-page.tsx @@ -4,7 +4,7 @@ import { ProjectTransferConfirmView, type ProjectTransferConfirmUiState } from " import { useRouter } from "@/components/router"; import { buildTransferSignUpUrl, getStackAppInternals } from "@/lib/transfer-utils"; import { useStackApp, useUser } from "@stackframe/stack"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { runAsynchronously, wait } from "@stackframe/stack-shared/dist/utils/promises"; import { useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; @@ -67,7 +67,7 @@ export default function CustomIntegrationProjectTransferConfirmPageClient() { }); const confirmResJson = await confirmRes.json(); if (typeof confirmResJson?.project_id !== "string") { - throw new StackAssertionError("Project transfer confirm response is missing `project_id`", { confirmResJson }); + throw new HexclaveAssertionError("Project transfer confirm response is missing `project_id`", { confirmResJson }); } router.push(`/projects/${confirmResJson.project_id}`); await wait(3000); diff --git a/apps/dashboard/src/app/(main)/wizard-congrats/posthog.tsx b/apps/dashboard/src/app/(main)/wizard-congrats/posthog.tsx index dfd95ae783..dd6c3ca6d5 100644 --- a/apps/dashboard/src/app/(main)/wizard-congrats/posthog.tsx +++ b/apps/dashboard/src/app/(main)/wizard-congrats/posthog.tsx @@ -9,7 +9,9 @@ export default function PostHog() { const searchParams = useSearchParams(); const router = useRouter(); useEffect(() => { - const distinctId = searchParams.get("stack-init-id"); + // Hexclave rebrand: prefer the new query param name, fall back to the legacy one. + const initIdKey = searchParams.has("hexclave-init-id") ? "hexclave-init-id" : "stack-init-id"; + const distinctId = searchParams.get(initIdKey); if (distinctId) { posthog.capture('$merge_dangerously', { @@ -17,7 +19,7 @@ export default function PostHog() { }); const newSearchParams = new URLSearchParams(); searchParams.forEach((value, key) => { - if (key !== "stack-init-id") { + if (key !== "hexclave-init-id" && key !== "stack-init-id") { newSearchParams.append(key, value); } }); diff --git a/apps/dashboard/src/app/api/remote-development-environment/auth/route.ts b/apps/dashboard/src/app/api/remote-development-environment/auth/route.ts index 2d9d6635b8..81b5934641 100644 --- a/apps/dashboard/src/app/api/remote-development-environment/auth/route.ts +++ b/apps/dashboard/src/app/api/remote-development-environment/auth/route.ts @@ -6,15 +6,21 @@ export const runtime = "nodejs"; const INTERNAL_PROJECT_ID = "internal"; function isInternalProjectRefreshCookieName(name: string): boolean { + // Hexclave rebrand: match refresh cookies under both the `stack-refresh-*` and `hexclave-refresh-*` bases. return ( name === "stack-refresh" || name === `stack-refresh-${INTERNAL_PROJECT_ID}` || name.startsWith(`stack-refresh-${INTERNAL_PROJECT_ID}--`) || - name.startsWith(`__Host-stack-refresh-${INTERNAL_PROJECT_ID}--`) + name.startsWith(`__Host-stack-refresh-${INTERNAL_PROJECT_ID}--`) || + name === `hexclave-refresh-${INTERNAL_PROJECT_ID}` || + name.startsWith(`hexclave-refresh-${INTERNAL_PROJECT_ID}--`) || + name.startsWith(`__Host-hexclave-refresh-${INTERNAL_PROJECT_ID}--`) ); } function deleteInternalProjectAuthCookies(req: NextRequest, response: NextResponse): void { + // Hexclave rebrand: delete the access cookie under both names. + response.cookies.delete("hexclave-access"); response.cookies.delete("stack-access"); for (const cookie of req.cookies.getAll()) { if (isInternalProjectRefreshCookieName(cookie.name)) { diff --git a/apps/dashboard/src/app/development-port-display.tsx b/apps/dashboard/src/app/development-port-display.tsx index edf346101a..ae18e4fff6 100644 --- a/apps/dashboard/src/app/development-port-display.tsx +++ b/apps/dashboard/src/app/development-port-display.tsx @@ -6,7 +6,7 @@ import { getPublicEnvVar } from "../lib/env"; export function DevelopmentPortDisplay() { const [isVisible, setIsVisible] = useState(true); - const prefix = getPublicEnvVar("NEXT_PUBLIC_STACK_PORT_PREFIX"); + const prefix = getPublicEnvVar("NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX"); if (!prefix || !isVisible || !process.env.NODE_ENV.includes("development")) return null; const color = ({ "91": "#eee", diff --git a/apps/dashboard/src/app/health/error-handler-debug/endpoint/route.tsx b/apps/dashboard/src/app/health/error-handler-debug/endpoint/route.tsx index f44a32ddd6..94402a2f6a 100644 --- a/apps/dashboard/src/app/health/error-handler-debug/endpoint/route.tsx +++ b/apps/dashboard/src/app/health/error-handler-debug/endpoint/route.tsx @@ -1,7 +1,7 @@ -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; export const dynamic = "force-dynamic"; export function GET() { - throw new StackAssertionError(`Server debug error thrown successfully!`); + throw new HexclaveAssertionError(`Server debug error thrown successfully!`); } diff --git a/apps/dashboard/src/components/project-transfer-confirm-view.tsx b/apps/dashboard/src/components/project-transfer-confirm-view.tsx index e593a0ae56..f1151246ee 100644 --- a/apps/dashboard/src/components/project-transfer-confirm-view.tsx +++ b/apps/dashboard/src/components/project-transfer-confirm-view.tsx @@ -7,7 +7,7 @@ import { DesignInput } from "@/components/design-components/input"; import { Logo } from "@/components/logo"; import { Spinner } from "@/components/ui"; import { ArrowsLeftRightIcon } from "@phosphor-icons/react"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; export type ProjectTransferConfirmUiState = "loading" | "success" | { type: "error", message: string }; @@ -38,15 +38,15 @@ export function ProjectTransferConfirmView(props: ProjectTransferConfirmViewProp if (state === "success" || isErrorState) { if (onCancel == null) { - throw new StackAssertionError("ProjectTransferConfirmView requires `onCancel` in the success and error states"); + throw new HexclaveAssertionError("ProjectTransferConfirmView requires `onCancel` in the success and error states"); } } if (state === "success") { if (onPrimary == null) { - throw new StackAssertionError("ProjectTransferConfirmView requires `onPrimary` in the success state"); + throw new HexclaveAssertionError("ProjectTransferConfirmView requires `onPrimary` in the success state"); } if (signedIn && (signedInAsLabel == null || onSwitchAccount == null)) { - throw new StackAssertionError("ProjectTransferConfirmView requires `signedInAsLabel` and `onSwitchAccount` when `signedIn` is true in the success state"); + throw new HexclaveAssertionError("ProjectTransferConfirmView requires `signedInAsLabel` and `onSwitchAccount` when `signedIn` is true in the success state"); } } diff --git a/apps/dashboard/src/components/smart-form.tsx b/apps/dashboard/src/components/smart-form.tsx index 1d7a375bff..d221e5b304 100644 --- a/apps/dashboard/src/components/smart-form.tsx +++ b/apps/dashboard/src/components/smart-form.tsx @@ -1,7 +1,7 @@ "use client"; import { yupResolver } from "@hookform/resolvers/yup"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; import { Form } from "@/components/ui"; import React, { useCallback, useEffect, useRef, useState } from "react"; @@ -108,7 +108,7 @@ function SmartFormField(props: { return null; } if (!("oneOf" in props.description)) { - throw new StackAssertionError(`Unsupported yup field ${props.id}; can't create form automatically from lazy yup schema`); + throw new HexclaveAssertionError(`Unsupported yup field ${props.id}; can't create form automatically from lazy yup schema`); } switch (props.description.type) { @@ -130,5 +130,5 @@ function SmartFormField(props: { } } - throw new StackAssertionError(`Unsupported yup field ${props.id}; can't create form automatically from schema of type ${JSON.stringify(props.description.type)}. Maybe you need to implement it, or add a stackFormFieldRender meta property to the schema.`); + throw new HexclaveAssertionError(`Unsupported yup field ${props.id}; can't create form automatically from schema of type ${JSON.stringify(props.description.type)}. Maybe you need to implement it, or add a stackFormFieldRender meta property to the schema.`); } diff --git a/apps/dashboard/src/components/stack-companion.tsx b/apps/dashboard/src/components/stack-companion.tsx index 0741ba0918..c877ba4ae3 100644 --- a/apps/dashboard/src/components/stack-companion.tsx +++ b/apps/dashboard/src/components/stack-companion.tsx @@ -189,10 +189,12 @@ export function StackCompanion({ className, glassBg = false }: { className?: str setChangelogData(entries); // Check for new versions - const lastSeenRaw = document.cookie - .split('; ') - .find(row => row.startsWith('stack-last-seen-changelog-version=')) - ?.split('=')[1] || ''; + // Hexclave rebrand: dual-read the changelog cookie, preferring the new name. + const cookieRows = document.cookie.split('; '); + const lastSeenRaw = ( + cookieRows.find(row => row.startsWith('hexclave-last-seen-changelog-version=')) + ?? cookieRows.find(row => row.startsWith('stack-last-seen-changelog-version=')) + )?.split('=')[1] || ''; const lastSeen = lastSeenRaw ? decodeURIComponent(lastSeenRaw) : ''; setLastSeenVersion(lastSeen); @@ -220,7 +222,10 @@ export function StackCompanion({ className, glassBg = false }: { className?: str if (changelogData && changelogData.length > 0) { const latestReleasedEntry = changelogData.find(entry => !entry.isUnreleased); if (latestReleasedEntry) { - document.cookie = `stack-last-seen-changelog-version=${sanitizeCookieValue(latestReleasedEntry.version)}; path=/; max-age=31536000`; // 1 year + // Hexclave rebrand: dual-write the changelog cookie under both names. 1 year max-age. + const sanitizedVersion = sanitizeCookieValue(latestReleasedEntry.version); + document.cookie = `hexclave-last-seen-changelog-version=${sanitizedVersion}; path=/; max-age=31536000`; + document.cookie = `stack-last-seen-changelog-version=${sanitizedVersion}; path=/; max-age=31536000`; setLastSeenVersion(latestReleasedEntry.version); } } @@ -228,10 +233,12 @@ export function StackCompanion({ className, glassBg = false }: { className?: str setHasNewVersions(false); } else if (activeItem === null) { // When closed, re-check if there are new versions - const lastSeenRaw = document.cookie - .split('; ') - .find(row => row.startsWith('stack-last-seen-changelog-version=')) - ?.split('=')[1] || ''; + // Hexclave rebrand: dual-read the changelog cookie, preferring the new name. + const cookieRows = document.cookie.split('; '); + const lastSeenRaw = ( + cookieRows.find(row => row.startsWith('hexclave-last-seen-changelog-version=')) + ?? cookieRows.find(row => row.startsWith('stack-last-seen-changelog-version=')) + )?.split('=')[1] || ''; const lastSeen = lastSeenRaw ? decodeURIComponent(lastSeenRaw) : ''; diff --git a/apps/dashboard/src/components/stack-companion/changelog-widget.tsx b/apps/dashboard/src/components/stack-companion/changelog-widget.tsx index 5eed5f493a..934df5fa83 100644 --- a/apps/dashboard/src/components/stack-companion/changelog-widget.tsx +++ b/apps/dashboard/src/components/stack-companion/changelog-widget.tsx @@ -44,7 +44,10 @@ function markLatestVersionSeen(entries: ApiChangelogEntry[]) { // Find the first released version (skip unreleased to avoid breaking version comparison) const latestReleasedEntry = entries.find(entry => !entry.isUnreleased); if (latestReleasedEntry) { - document.cookie = `stack-last-seen-changelog-version=${encodeURIComponent(latestReleasedEntry.version)}; path=/; max-age=31536000`; + // Hexclave rebrand: dual-write the changelog cookie under both names. + const encodedVersion = encodeURIComponent(latestReleasedEntry.version); + document.cookie = `hexclave-last-seen-changelog-version=${encodedVersion}; path=/; max-age=31536000`; + document.cookie = `stack-last-seen-changelog-version=${encodedVersion}; path=/; max-age=31536000`; } } diff --git a/apps/dashboard/src/components/stack-companion/feature-request-board.tsx b/apps/dashboard/src/components/stack-companion/feature-request-board.tsx index ec86807a33..d872dbbbda 100644 --- a/apps/dashboard/src/components/stack-companion/feature-request-board.tsx +++ b/apps/dashboard/src/components/stack-companion/feature-request-board.tsx @@ -6,7 +6,7 @@ import { getInternalProjectHeaders } from '@/lib/internal-project-headers'; import { cn } from '@/lib/utils'; import { CaretUpIcon, CircleNotchIcon, LightbulbIcon, PaperPlaneTiltIcon, PlusIcon, XIcon } from '@phosphor-icons/react'; import { useUser } from '@stackframe/stack'; -import { StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors'; +import { HexclaveAssertionError } from '@stackframe/stack-shared/dist/utils/errors'; import { htmlToText } from '@stackframe/stack-shared/dist/utils/html'; import { runAsynchronously, runAsynchronouslyWithAlert } from '@stackframe/stack-shared/dist/utils/promises'; import { useCallback, useEffect, useState } from 'react'; @@ -96,7 +96,7 @@ export function FeatureRequestBoard({}: FeatureRequestBoardProps) { }); setUserUpvotes(upvotedPosts); } else { - throw new StackAssertionError('Fetch response is not OK', { + throw new HexclaveAssertionError('Fetch response is not OK', { details: { response: response, responseText: await response.text(), @@ -159,7 +159,7 @@ export function FeatureRequestBoard({}: FeatureRequestBoardProps) { return; } - throw new StackAssertionError('Failed to upvote feature request', { + throw new HexclaveAssertionError('Failed to upvote feature request', { status: response.status, responseText: await response.text(), }); diff --git a/apps/dashboard/src/components/ui/brand-icons.tsx b/apps/dashboard/src/components/ui/brand-icons.tsx index 3406f6adba..3c58df5acc 100644 --- a/apps/dashboard/src/components/ui/brand-icons.tsx +++ b/apps/dashboard/src/components/ui/brand-icons.tsx @@ -1,4 +1,4 @@ -import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; export function Google({ iconSize }: { iconSize: number }) { return ( @@ -240,7 +240,7 @@ export function Mapping({ return ; } default: { - throw new StackAssertionError(`Icon not found for provider: ${provider}`); + throw new HexclaveAssertionError(`Icon not found for provider: ${provider}`); } } } diff --git a/apps/dashboard/src/components/ui/form.tsx b/apps/dashboard/src/components/ui/form.tsx index 44afcb0eed..1c3f293359 100644 --- a/apps/dashboard/src/components/ui/form.tsx +++ b/apps/dashboard/src/components/ui/form.tsx @@ -13,7 +13,7 @@ import { useFormContext, } from "react-hook-form"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { cn } from "@/lib/utils"; import { SpanLabel } from "./label"; @@ -47,7 +47,7 @@ const useFormField = () => { const { getFieldState, formState } = useFormContext(); if (!fieldContext) { - throw new StackAssertionError("useFormField should be used within "); + throw new HexclaveAssertionError("useFormField should be used within "); } const fieldState = getFieldState(fieldContext.name, formState); diff --git a/apps/dashboard/src/instrumentation.ts b/apps/dashboard/src/instrumentation.ts index 5962696e9a..cb353e4996 100644 --- a/apps/dashboard/src/instrumentation.ts +++ b/apps/dashboard/src/instrumentation.ts @@ -18,7 +18,7 @@ export async function register() { if (getEnvBoolean("NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT")) { globalThis.process.title = `Stack Auth — Development Server (port ${getEnvVariable("PORT", "?")})`; } else { - globalThis.process.title = `stack-dashboard:${getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81")} (node/nextjs)`; + globalThis.process.title = `stack-dashboard:${getEnvVariable("NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX", "81")} (node/nextjs)`; } await startRemoteDevelopmentEnvironmentLifecycleIfNeeded(); } diff --git a/apps/dashboard/src/lib/api-headers.ts b/apps/dashboard/src/lib/api-headers.ts index 3a98542c42..628d8be173 100644 --- a/apps/dashboard/src/lib/api-headers.ts +++ b/apps/dashboard/src/lib/api-headers.ts @@ -11,10 +11,11 @@ export async function buildStackAuthHeaders( ): Promise> { const accessToken = currentUser ? await currentUser.getAccessToken() : null; + // Hexclave rebrand: emit x-hexclave-* request headers; the backend proxy dual-accepts both names. return { - "x-stack-access-type": "client", - "x-stack-project-id": "internal", - "x-stack-publishable-client-key": getPublicEnvVar("NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY") ?? "", - ...(accessToken ? { "x-stack-access-token": accessToken } : {}), + "x-hexclave-access-type": "client", + "x-hexclave-project-id": "internal", + "x-hexclave-publishable-client-key": getPublicEnvVar("NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY") ?? "", + ...(accessToken ? { "x-hexclave-access-token": accessToken } : {}), }; } diff --git a/apps/dashboard/src/lib/cel-visual-parser.ts b/apps/dashboard/src/lib/cel-visual-parser.ts index 3c140c7dad..0afcf77bdd 100644 --- a/apps/dashboard/src/lib/cel-visual-parser.ts +++ b/apps/dashboard/src/lib/cel-visual-parser.ts @@ -17,7 +17,7 @@ import { normalizeCountryCode } from "@stackframe/stack-shared/dist/schema-fields"; import { type ConditionField, type ConditionOperator, conditionFields, escapeCelString, fieldMetadata, isNumericField, unescapeCelString, validateNumericFieldValue } from "@stackframe/stack-shared/dist/utils/cel-fields"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; export type { ConditionField, ConditionOperator } from "@stackframe/stack-shared/dist/utils/cel-fields"; @@ -79,7 +79,7 @@ export function visualTreeToCel(node: RuleNode): string { function normalizeConditionValue(condition: ConditionNode): ConditionNode['value'] { if (condition.field !== 'countryCode') return condition.value; if (typeof condition.value === 'number') { - throw new StackAssertionError(`Invalid numeric value for countryCode: ${condition.value}. Country codes must be strings.`); + throw new HexclaveAssertionError(`Invalid numeric value for countryCode: ${condition.value}. Country codes must be strings.`); } return Array.isArray(condition.value) ? condition.value.map(normalizeCountryCode) @@ -93,7 +93,7 @@ function conditionToCel(condition: ConditionNode): string { // Numeric comparisons: field >= 42 if (operator in comparisonSymbols && isNumericField(field)) { const err = validateNumericFieldValue(field, String(value)); - if (err) throw new StackAssertionError(err); + if (err) throw new HexclaveAssertionError(err); return `${field} ${comparisonSymbols[operator]} ${typeof value === 'number' ? value : Number(value)}`; } diff --git a/apps/dashboard/src/lib/config-update.tsx b/apps/dashboard/src/lib/config-update.tsx index be1a04c70c..65a23b0b06 100644 --- a/apps/dashboard/src/lib/config-update.tsx +++ b/apps/dashboard/src/lib/config-update.tsx @@ -6,7 +6,7 @@ import { useDashboardInternalUser } from "@/lib/dashboard-user"; import { getPublicEnvVar } from "@/lib/env"; import type { OAuthConnection, PushedConfigSource, StackAdminApp } from "@stackframe/stack"; import type { EnvironmentConfigOverrideOverride } from "@stackframe/stack-shared/dist/config/schema"; -import { StackAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; import React, { createContext, Suspense, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; @@ -566,7 +566,7 @@ export function useUpdateConfig() { if (getPublicEnvVar("NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT") === "true") { if (!pushable) { - throw new StackAssertionError("These settings are read-only in a development environment. Update them in your production deployment instead."); + throw new HexclaveAssertionError("These settings are read-only in a development environment. Update them in your production deployment instead."); } const project = await adminApp.getProject(); diff --git a/apps/dashboard/src/lib/env.tsx b/apps/dashboard/src/lib/env.tsx index fc9301f248..6f98db8d42 100644 --- a/apps/dashboard/src/lib/env.tsx +++ b/apps/dashboard/src/lib/env.tsx @@ -3,44 +3,55 @@ export function expandStackPortPrefix(value?: string | null) { if (!value) return value ?? undefined; - const prefix = getPublicEnvVar("NEXT_PUBLIC_STACK_PORT_PREFIX") ?? "81"; - return prefix ? value.replace(/\$\{NEXT_PUBLIC_STACK_PORT_PREFIX:-81\}/g, prefix as string) : value; + // Hexclave rebrand: port-prefix var renamed outright (no dual-read). + const prefix = getPublicEnvVar("NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX") ?? "81"; + return prefix ? value.replace(/\$\{NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81\}/g, prefix as string) : value; } +// Hexclave rebrand: each entry prefers the NEXT_PUBLIC_HEXCLAVE_* literal, falling back +// to the legacy NEXT_PUBLIC_*STACK_* literal. Both operands must stay literal +// `process.env.NEXT_PUBLIC_*` references so Next.js can inline them at build time. const _inlineEnvVars = { - NEXT_PUBLIC_STACK_API_URL: process.env.NEXT_PUBLIC_STACK_API_URL, - NEXT_PUBLIC_STACK_DASHBOARD_URL: process.env.NEXT_PUBLIC_STACK_DASHBOARD_URL, - NEXT_PUBLIC_STACK_SVIX_SERVER_URL: process.env.NEXT_PUBLIC_STACK_SVIX_SERVER_URL, - NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR: process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR, - NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT: process.env.NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT, - NEXT_PUBLIC_STACK_IS_PREVIEW: process.env.NEXT_PUBLIC_STACK_IS_PREVIEW, - NEXT_PUBLIC_STACK_PROJECT_ID: process.env.NEXT_PUBLIC_STACK_PROJECT_ID, - NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY: process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY, - NEXT_PUBLIC_STACK_URL: process.env.NEXT_PUBLIC_STACK_URL, - NEXT_PUBLIC_STACK_INBUCKET_WEB_URL: process.env.NEXT_PUBLIC_STACK_INBUCKET_WEB_URL, - NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS: process.env.NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS, - NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY, - NEXT_PUBLIC_STACK_PORT_PREFIX: process.env.NEXT_PUBLIC_STACK_PORT_PREFIX, - NEXT_PUBLIC_STACK_DOCS_BASE_URL: process.env.NEXT_PUBLIC_STACK_DOCS_BASE_URL, - NEXT_PUBLIC_STACK_ENABLE_REACT_SCAN_IN_DEVELOPMENT: process.env.NEXT_PUBLIC_STACK_ENABLE_REACT_SCAN_IN_DEVELOPMENT, + NEXT_PUBLIC_STACK_API_URL: process.env.NEXT_PUBLIC_HEXCLAVE_API_URL ?? process.env.NEXT_PUBLIC_STACK_API_URL, + NEXT_PUBLIC_STACK_DASHBOARD_URL: process.env.NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL ?? process.env.NEXT_PUBLIC_STACK_DASHBOARD_URL, + NEXT_PUBLIC_STACK_SVIX_SERVER_URL: process.env.NEXT_PUBLIC_HEXCLAVE_SVIX_SERVER_URL ?? process.env.NEXT_PUBLIC_STACK_SVIX_SERVER_URL, + NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR: process.env.NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR ?? process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR, + NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT: process.env.NEXT_PUBLIC_HEXCLAVE_IS_REMOTE_DEVELOPMENT_ENVIRONMENT ?? process.env.NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT, + NEXT_PUBLIC_STACK_IS_PREVIEW: process.env.NEXT_PUBLIC_HEXCLAVE_IS_PREVIEW ?? process.env.NEXT_PUBLIC_STACK_IS_PREVIEW, + NEXT_PUBLIC_STACK_PROJECT_ID: process.env.NEXT_PUBLIC_HEXCLAVE_PROJECT_ID ?? process.env.NEXT_PUBLIC_STACK_PROJECT_ID, + NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY: process.env.NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY ?? process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY, + NEXT_PUBLIC_STACK_URL: process.env.NEXT_PUBLIC_HEXCLAVE_URL ?? process.env.NEXT_PUBLIC_STACK_URL, + NEXT_PUBLIC_STACK_INBUCKET_WEB_URL: process.env.NEXT_PUBLIC_HEXCLAVE_INBUCKET_WEB_URL ?? process.env.NEXT_PUBLIC_STACK_INBUCKET_WEB_URL, + NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS: process.env.NEXT_PUBLIC_HEXCLAVE_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS ?? process.env.NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS, + NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_HEXCLAVE_STRIPE_PUBLISHABLE_KEY ?? process.env.NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY, + // Hexclave rebrand: port-prefix var renamed outright (no dual-read). + NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX: process.env.NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX, + NEXT_PUBLIC_STACK_DOCS_BASE_URL: process.env.NEXT_PUBLIC_HEXCLAVE_DOCS_BASE_URL ?? process.env.NEXT_PUBLIC_STACK_DOCS_BASE_URL, + NEXT_PUBLIC_STACK_ENABLE_REACT_SCAN_IN_DEVELOPMENT: process.env.NEXT_PUBLIC_HEXCLAVE_ENABLE_REACT_SCAN_IN_DEVELOPMENT ?? process.env.NEXT_PUBLIC_STACK_ENABLE_REACT_SCAN_IN_DEVELOPMENT, // TODO: NEXT_PUBLIC_BROWSER_STACK_API_URL should be renamed to NEXT_PUBLIC_STACK_BROWSER_API_URL - NEXT_PUBLIC_BROWSER_STACK_API_URL: process.env.NEXT_PUBLIC_BROWSER_STACK_API_URL, + NEXT_PUBLIC_BROWSER_STACK_API_URL: process.env.NEXT_PUBLIC_BROWSER_HEXCLAVE_API_URL ?? process.env.NEXT_PUBLIC_BROWSER_STACK_API_URL, // TODO: NEXT_PUBLIC_SERVER_STACK_API_URL should be renamed to NEXT_PUBLIC_STACK_SERVER_API_URL - NEXT_PUBLIC_SERVER_STACK_API_URL: process.env.NEXT_PUBLIC_SERVER_STACK_API_URL, + NEXT_PUBLIC_SERVER_STACK_API_URL: process.env.NEXT_PUBLIC_SERVER_HEXCLAVE_API_URL ?? process.env.NEXT_PUBLIC_SERVER_STACK_API_URL, // TODO: NEXT_PUBLIC_SENTRY_DSN should be renamed to NEXT_PUBLIC_STACK_SENTRY_DSN NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN, // TODO: NEXT_PUBLIC_VERSION_ALERTER_SEVERE_ONLY should be renamed to NEXT_PUBLIC_STACK_VERSION_ALERTER_SEVERE_ONLY NEXT_PUBLIC_VERSION_ALERTER_SEVERE_ONLY: process.env.NEXT_PUBLIC_VERSION_ALERTER_SEVERE_ONLY, // TODO: NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL should be renamed to NEXT_PUBLIC_STACK_BROWSER_DASHBOARD_URL - NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL: process.env.NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL, + NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL: process.env.NEXT_PUBLIC_BROWSER_HEXCLAVE_DASHBOARD_URL ?? process.env.NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL, // TODO: NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL should be renamed to NEXT_PUBLIC_STACK_SERVER_DASHBOARD_URL - NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL: process.env.NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL, + NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL: process.env.NEXT_PUBLIC_SERVER_HEXCLAVE_DASHBOARD_URL ?? process.env.NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL, // TODO: NEXT_PUBLIC_POSTHOG_KEY should be renamed to NEXT_PUBLIC_STACK_POSTHOG_KEY NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, } as const; -// This will be replaced with the actual env vars after a docker build +// This will be replaced with the actual env vars after a docker build. +// Hexclave rebrand: sentinel STRING values intentionally keep their legacy +// NEXT_PUBLIC_STACK_* names. docker/server/entrypoint.sh derives the env var to +// substitute via `${sentinel#STACK_ENV_VAR_SENTINEL_}` and only exports the +// NEXT_PUBLIC_STACK_* names — renaming the sentinels here would break docker +// self-host substitution. Dual-read in the docker post-build path needs an +// entrypoint.sh change (separate work-area). Only the port-prefix KEY is renamed. const _postBuildEnvVars = { NEXT_PUBLIC_STACK_API_URL: "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_STACK_API_URL", NEXT_PUBLIC_BROWSER_STACK_API_URL: "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_BROWSER_STACK_API_URL", @@ -61,7 +72,7 @@ const _postBuildEnvVars = { NEXT_PUBLIC_STACK_INBUCKET_WEB_URL: "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_STACK_INBUCKET_WEB_URL", NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS: "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS", NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY: "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY", - NEXT_PUBLIC_STACK_PORT_PREFIX: "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_STACK_PORT_PREFIX", + NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX: "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX", NEXT_PUBLIC_STACK_DOCS_BASE_URL: "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_STACK_DOCS_BASE_URL", NEXT_PUBLIC_STACK_ENABLE_REACT_SCAN_IN_DEVELOPMENT: "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_STACK_ENABLE_REACT_SCAN_IN_DEVELOPMENT", } satisfies typeof _inlineEnvVars; @@ -78,5 +89,6 @@ export function getPublicEnvVar(name: keyof typeof _inlineEnvVars): string | und if (_usePostBuildEnvVars.slice(0) === 'true' && value && value.startsWith('STACK_ENV_VAR_SENTINEL')) { return undefined; } - return name === 'NEXT_PUBLIC_STACK_PORT_PREFIX' || !name.startsWith('NEXT_PUBLIC_STACK_') ? value : expandStackPortPrefix(value); + // Hexclave rebrand: port-prefix var renamed outright; skip port-prefix expansion for it. + return name === 'NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX' || !name.startsWith('NEXT_PUBLIC_STACK_') ? value : expandStackPortPrefix(value); } diff --git a/apps/dashboard/src/lib/remote-development-environment/config-file.ts b/apps/dashboard/src/lib/remote-development-environment/config-file.ts index 32ee5ee2bc..91acbb429e 100644 --- a/apps/dashboard/src/lib/remote-development-environment/config-file.ts +++ b/apps/dashboard/src/lib/remote-development-environment/config-file.ts @@ -15,7 +15,21 @@ export function sha256String(value: string): string { export function resolveConfigFilePath(inputPath: string): string { const resolved = path.resolve(inputPath); const looksLikeConfigFile = /\.(ts|js|mjs|cjs)$/i.test(resolved); - return looksLikeConfigFile ? resolved : path.join(resolved, "stack.config.ts"); + if (looksLikeConfigFile) { + return resolved; + } + // Hexclave rebrand: prefer the new `hexclave.config.ts` filename inside the + // directory, falling back to the legacy `stack.config.ts` for existing + // projects. If neither exists, default to the new filename. + const hexclaveCandidate = path.join(resolved, "hexclave.config.ts"); + const legacyCandidate = path.join(resolved, "stack.config.ts"); + if (existsSync(hexclaveCandidate)) { + return hexclaveCandidate; + } + if (existsSync(legacyCandidate)) { + return legacyCandidate; + } + return hexclaveCandidate; } export function ensureConfigFileExists(configFilePath: string): void { diff --git a/apps/dashboard/src/lib/remote-development-environment/env.ts b/apps/dashboard/src/lib/remote-development-environment/env.ts index 527855c478..72ee8b7edd 100644 --- a/apps/dashboard/src/lib/remote-development-environment/env.ts +++ b/apps/dashboard/src/lib/remote-development-environment/env.ts @@ -1,6 +1,6 @@ import "server-only"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; export const REMOTE_DEVELOPMENT_ENVIRONMENT_ENABLED_ENV = "NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT"; @@ -10,6 +10,6 @@ export function isRemoteDevelopmentEnvironmentEnabled(): boolean { export function assertRemoteDevelopmentEnvironmentEnabled(): void { if (!isRemoteDevelopmentEnvironmentEnabled()) { - throw new StackAssertionError(`${REMOTE_DEVELOPMENT_ENVIRONMENT_ENABLED_ENV}=true is required to use remote development environment internals.`); + throw new HexclaveAssertionError(`${REMOTE_DEVELOPMENT_ENVIRONMENT_ENABLED_ENV}=true is required to use remote development environment internals.`); } } diff --git a/apps/dashboard/src/lib/risk-score-utils.ts b/apps/dashboard/src/lib/risk-score-utils.ts index 2b4dd330a3..57044ef212 100644 --- a/apps/dashboard/src/lib/risk-score-utils.ts +++ b/apps/dashboard/src/lib/risk-score-utils.ts @@ -1,4 +1,4 @@ -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; export const RISK_SCORE_REGEX = /^(100|[1-9]?[0-9])$/; @@ -8,7 +8,7 @@ export function validateRiskScore(value: string | null | undefined): boolean { export function parseRiskScore(value: string): number { if (!RISK_SCORE_REGEX.test(value)) { - throw new StackAssertionError("Risk scores must be integers between 0 and 100"); + throw new HexclaveAssertionError("Risk scores must be integers between 0 and 100"); } return Number(value); } diff --git a/apps/dashboard/src/lib/stack-app-internals.ts b/apps/dashboard/src/lib/stack-app-internals.ts index 9e682345d9..0ae45add27 100644 --- a/apps/dashboard/src/lib/stack-app-internals.ts +++ b/apps/dashboard/src/lib/stack-app-internals.ts @@ -3,7 +3,7 @@ import { type MetricsUserCounts, type UserActivityResponse, } from "@stackframe/stack-shared/dist/interface/admin-metrics"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; export const stackAppInternalsSymbol = Symbol.for("StackAuth--DO-NOT-USE-OR-YOU-WILL-BE-FIRED--StackAppInternals"); @@ -38,12 +38,12 @@ export type { export function useMetricsOrThrow(adminApp: object, includeAnonymous: boolean): MetricsResponse { const internals = Reflect.get(adminApp, stackAppInternalsSymbol); if (typeof internals !== "object" || internals == null || !("useMetrics" in internals)) { - throw new StackAssertionError("Admin app internals are unavailable: missing useMetrics"); + throw new HexclaveAssertionError("Admin app internals are unavailable: missing useMetrics"); } const useMetrics = internals.useMetrics; if (typeof useMetrics !== "function") { - throw new StackAssertionError("Admin app internals are unavailable: useMetrics is not callable"); + throw new HexclaveAssertionError("Admin app internals are unavailable: useMetrics is not callable"); } return useMetrics(includeAnonymous) as MetricsResponse; @@ -58,12 +58,12 @@ export function useMetricsOrThrow(adminApp: object, includeAnonymous: boolean): export function useUserActivityOrThrow(adminApp: object, userId: string): UserActivityResponse { const internals = Reflect.get(adminApp, stackAppInternalsSymbol); if (typeof internals !== "object" || internals == null || !("useUserActivity" in internals)) { - throw new StackAssertionError("Admin app internals are unavailable: missing useUserActivity"); + throw new HexclaveAssertionError("Admin app internals are unavailable: missing useUserActivity"); } const useUserActivity = internals.useUserActivity; if (typeof useUserActivity !== "function") { - throw new StackAssertionError("Admin app internals are unavailable: useUserActivity is not callable"); + throw new HexclaveAssertionError("Admin app internals are unavailable: useUserActivity is not callable"); } return useUserActivity(userId) as UserActivityResponse; @@ -72,12 +72,12 @@ export function useUserActivityOrThrow(adminApp: object, userId: string): UserAc export function useMetricsUserCountsOrThrow(adminApp: object): MetricsUserCounts { const internals = Reflect.get(adminApp, stackAppInternalsSymbol); if (typeof internals !== "object" || internals == null || !("useMetricsUserCounts" in internals)) { - throw new StackAssertionError("Admin app internals are unavailable: missing useMetricsUserCounts"); + throw new HexclaveAssertionError("Admin app internals are unavailable: missing useMetricsUserCounts"); } const useMetricsUserCounts = internals.useMetricsUserCounts; if (typeof useMetricsUserCounts !== "function") { - throw new StackAssertionError("Admin app internals are unavailable: useMetricsUserCounts is not callable"); + throw new HexclaveAssertionError("Admin app internals are unavailable: useMetricsUserCounts is not callable"); } return useMetricsUserCounts() as MetricsUserCounts; diff --git a/apps/dashboard/src/lib/transfer-utils.ts b/apps/dashboard/src/lib/transfer-utils.ts index 3cb2c970f2..2122dc7130 100644 --- a/apps/dashboard/src/lib/transfer-utils.ts +++ b/apps/dashboard/src/lib/transfer-utils.ts @@ -1,5 +1,5 @@ import { stackAppInternalsSymbol } from "@/lib/stack-app-internals"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; export function buildTransferSignUpUrl(): string { const currentUrl = new URL(window.location.href); @@ -21,11 +21,11 @@ type StackAppInternals = { export function getStackAppInternals(app: unknown): StackAppInternals { if (typeof app !== "object" || app === null) { - throw new StackAssertionError("getStackAppInternals: expected an app object", { app }); + throw new HexclaveAssertionError("getStackAppInternals: expected an app object", { app }); } const internals = (app as Record)[stackAppInternalsSymbol]; if (internals == null || typeof (internals as StackAppInternals).sendRequest !== "function") { - throw new StackAssertionError("getStackAppInternals: app is missing stackAppInternalsSymbol or sendRequest", { app }); + throw new HexclaveAssertionError("getStackAppInternals: app is missing stackAppInternalsSymbol or sendRequest", { app }); } return internals as StackAppInternals; } diff --git a/apps/dashboard/src/polyfills.tsx b/apps/dashboard/src/polyfills.tsx index f62bdb3c3a..4830829f39 100644 --- a/apps/dashboard/src/polyfills.tsx +++ b/apps/dashboard/src/polyfills.tsx @@ -6,8 +6,8 @@ import { getPublicEnvVar } from "./lib/env"; function expandStackPortPrefix(value?: string | null) { if (!value) return value ?? undefined; - const prefix = getPublicEnvVar("NEXT_PUBLIC_STACK_PORT_PREFIX") ?? "81"; - return prefix ? value.replace(/\$\{NEXT_PUBLIC_STACK_PORT_PREFIX:-81\}/g, prefix as string) : value; + const prefix = getPublicEnvVar("NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX") ?? "81"; + return prefix ? value.replace(/\$\{NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81\}/g, prefix as string) : value; } const sentryErrorSink = (location: string, error: unknown) => { diff --git a/apps/dashboard/src/proxy.tsx b/apps/dashboard/src/proxy.tsx index 51312d0e6b..9e6ba6ca3f 100644 --- a/apps/dashboard/src/proxy.tsx +++ b/apps/dashboard/src/proxy.tsx @@ -1,7 +1,7 @@ import { getEnvVariable, getNodeEnvironment } from '@stackframe/stack-shared/dist/utils/env'; import './polyfills'; -import { StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors'; +import { HexclaveAssertionError } from '@stackframe/stack-shared/dist/utils/errors'; import { wait } from '@stackframe/stack-shared/dist/utils/promises'; import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; @@ -48,7 +48,7 @@ export async function proxy(request: NextRequest) { const delay = Number.parseInt(getEnvVariable('STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS', '0')); if (delay) { if (getNodeEnvironment().includes('production')) { - throw new StackAssertionError('STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS is only allowed in development'); + throw new HexclaveAssertionError('STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS is only allowed in development'); } if (!request.headers.get('x-stack-disable-artificial-development-delay')) { await wait(delay); diff --git a/apps/dashboard/src/stack/server.tsx b/apps/dashboard/src/stack/server.tsx index 2185c2ac0c..1fe5e88b14 100644 --- a/apps/dashboard/src/stack/server.tsx +++ b/apps/dashboard/src/stack/server.tsx @@ -2,7 +2,7 @@ import "server-only"; import { isRemoteDevelopmentEnvironmentEnabled } from "@/lib/remote-development-environment/env"; import { StackServerApp } from "@stackframe/stack"; -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { stackClientApp } from "./client"; type InternalServerApp = StackServerApp; @@ -11,7 +11,7 @@ let _stackServerApp: InternalServerApp | undefined; export function getStackServerApp(): InternalServerApp { if (!_stackServerApp) { if (isRemoteDevelopmentEnvironmentEnabled()) { - throw new StackAssertionError("stackServerApp is not available in the local remote development environment dashboard."); + throw new HexclaveAssertionError("stackServerApp is not available in the local remote development environment dashboard."); } _stackServerApp = new StackServerApp({ inheritsFrom: stackClientApp, diff --git a/apps/dev-launchpad/package.json b/apps/dev-launchpad/package.json index 6f533d55d3..623c76d956 100644 --- a/apps/dev-launchpad/package.json +++ b/apps/dev-launchpad/package.json @@ -4,7 +4,7 @@ "repository": "https://github.com/hexclave/stack-auth", "private": true, "scripts": { - "dev": "node scripts/write-env-config.js && serve -p ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}00 -s public", + "dev": "node scripts/write-env-config.js && serve -p ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}00 -s public", "clean": "rimraf node_modules" }, "dependencies": { diff --git a/apps/dev-launchpad/public/index.html b/apps/dev-launchpad/public/index.html index 835fc3e303..fc20478dc1 100644 --- a/apps/dev-launchpad/public/index.html +++ b/apps/dev-launchpad/public/index.html @@ -108,8 +108,8 @@

Background services

if (!port || port.length < 2) return "81"; return port.slice(0, -2); }; - const stackPortPrefix = window.NEXT_PUBLIC_STACK_PORT_PREFIX || derivePrefixFromLocation(); - window.NEXT_PUBLIC_STACK_PORT_PREFIX = stackPortPrefix; + const stackPortPrefix = window.NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX || derivePrefixFromLocation(); + window.NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX = stackPortPrefix; const withPrefix = (suffix) => `${stackPortPrefix}${suffix}`; // Depending on the port prefix, set the color to light grey (port 91), light purple (port 92), papyrus yellow (port 93), or default otherwise diff --git a/apps/dev-launchpad/scripts/write-env-config.js b/apps/dev-launchpad/scripts/write-env-config.js index d360d7fa13..c2fc2703a9 100644 --- a/apps/dev-launchpad/scripts/write-env-config.js +++ b/apps/dev-launchpad/scripts/write-env-config.js @@ -3,9 +3,9 @@ const fs = require("fs"); const path = require("path"); -const prefix = process.env.NEXT_PUBLIC_STACK_PORT_PREFIX ?? "81"; +const prefix = process.env.NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX ?? "81"; const targetPath = path.join(__dirname, "..", "public", "env-config.js"); -const fileContents = `window.NEXT_PUBLIC_STACK_PORT_PREFIX = ${JSON.stringify(prefix)};\n`; +const fileContents = `window.NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX = ${JSON.stringify(prefix)};\n`; fs.writeFileSync(targetPath, fileContents); console.log(`[dev-launchpad] Wrote env-config.js with prefix ${prefix}`); diff --git a/apps/e2e/.env.development b/apps/e2e/.env.development index b4acbfaf93..830e2df1a8 100644 --- a/apps/e2e/.env.development +++ b/apps/e2e/.env.development @@ -1,15 +1,15 @@ -STACK_DASHBOARD_BASE_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01 -STACK_BACKEND_BASE_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02 -STACK_MCP_BASE_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}44 +STACK_DASHBOARD_BASE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}01 +STACK_BACKEND_BASE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02 +STACK_MCP_BASE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}44 STACK_INTERNAL_PROJECT_ID=internal STACK_INTERNAL_PROJECT_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only STACK_INTERNAL_PROJECT_SERVER_KEY=this-secret-server-key-is-for-local-development-only STACK_INTERNAL_PROJECT_ADMIN_KEY=this-super-secret-admin-key-is-for-local-development-only -NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX=.localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}09 -STACK_DATABASE_CONNECTION_STRING=postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}28/stackframe +NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX=.localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09 +STACK_DATABASE_CONNECTION_STRING=postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}28/stackframe -STACK_INBUCKET_API_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}05 -STACK_SVIX_SERVER_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}13 +STACK_INBUCKET_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}05 +STACK_SVIX_SERVER_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}13 STACK_EMAIL_MONITOR_SECRET_TOKEN=this-secret-token-is-for-local-development-only diff --git a/apps/e2e/tests/backend/backend-helpers.ts b/apps/e2e/tests/backend/backend-helpers.ts index 09da1f189c..909ccb52fe 100644 --- a/apps/e2e/tests/backend/backend-helpers.ts +++ b/apps/e2e/tests/backend/backend-helpers.ts @@ -2,7 +2,7 @@ import type { ProjectConfigOverride } from "@stackframe/stack-shared/dist/config import { AdminUserProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; import { encodeBase64 } from "@stackframe/stack-shared/dist/utils/bytes"; import { generateSecureRandomString } from "@stackframe/stack-shared/dist/utils/crypto"; -import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { HexclaveAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { publishableClientKeyNotNecessarySentinel } from "@stackframe/stack-shared/dist/utils/oauth"; import { filterUndefined, omit } from "@stackframe/stack-shared/dist/utils/objects"; import { wait } from "@stackframe/stack-shared/dist/utils/promises"; @@ -46,10 +46,10 @@ export const backendContext = new Context { if ("defaultProjectKeys" in update) { - throw new StackAssertionError("Cannot set defaultProjectKeys"); + throw new HexclaveAssertionError("Cannot set defaultProjectKeys"); } if ("mailbox" in update && !(update.mailbox instanceof Mailbox)) { - throw new StackAssertionError("Must create a mailbox with createMailbox()!"); + throw new HexclaveAssertionError("Must create a mailbox with createMailbox()!"); } return { ...acc, @@ -63,7 +63,7 @@ export function createMailbox(email?: string): Mailbox { backendContext.set({ generatedMailboxNamesCount: backendContext.value.generatedMailboxNamesCount + 1 }); email = `mailbox-${backendContext.value.generatedMailboxNamesCount}--${randomUUID()}${generatedEmailSuffix}`; } - if (!email.includes("@")) throw new StackAssertionError(`Invalid mailbox email: ${email}`); + if (!email.includes("@")) throw new HexclaveAssertionError(`Invalid mailbox email: ${email}`); return new Mailbox("(we can ignore the disclaimer here)" as any, email); } @@ -118,7 +118,7 @@ function expectSnakeCase(obj: unknown, path: string): void { } else { for (const [key, value] of Object.entries(obj)) { if (key.match(/^[a-z0-9][A-Z][a-z0-9]+$/) && !key.includes("_") && !["newUser", "afterCallbackRedirectUrl"].includes(key)) { - throw new StackAssertionError(`Object has camelCase key (expected snake_case): ${path}.${key}`); + throw new HexclaveAssertionError(`Object has camelCase key (expected snake_case): ${path}.${key}`); } if (["client_metadata", "server_metadata", "options_json", "credential", "authentication_response", "metadata", "variables", "skipped_details"].includes(key)) continue; // because email templates @@ -143,10 +143,10 @@ export async function niceBackendFetch(url: string | URL, options?: Omit { const { body, rawBody, rawContentType, headers, accessType, omitPublishableClientKey, userAuth: userAuthOverride, ...otherOptions } = options ?? {}; if (body !== undefined && rawBody !== undefined) { - throw new StackAssertionError("niceBackendFetch: pass either body or rawBody, not both"); + throw new HexclaveAssertionError("niceBackendFetch: pass either body or rawBody, not both"); } if (rawContentType !== undefined && rawBody === undefined) { - throw new StackAssertionError("niceBackendFetch: rawContentType only makes sense with rawBody"); + throw new HexclaveAssertionError("niceBackendFetch: rawContentType only makes sense with rawBody"); } if (typeof body === "object") { expectSnakeCase(body, "req.body"); @@ -154,8 +154,8 @@ export async function niceBackendFetch(url: string | URL, options?: Omit= 500 && res.status < 600) { - throw new StackAssertionError(`API threw ISE in ${otherOptions.method ?? "GET"} ${url}: ${res.status} ${typeof res.body === "string" ? res.body : nicify(res.body)}`); + throw new HexclaveAssertionError(`API threw ISE in ${otherOptions.method ?? "GET"} ${url}: ${res.status} ${typeof res.body === "string" ? res.body : nicify(res.body)}`); } if (res.headers.has("x-stack-known-error")) { expect(res.status).toBeGreaterThanOrEqual(400); @@ -262,7 +262,7 @@ export async function waitForOutboxEmailWithStatus(subject: string, status: stri } await wait(500); } - throw new StackAssertionError( + throw new HexclaveAssertionError( `Timeout waiting for outbox email with subject "${subject}" and status "${status}"`, { foundEmails: emails } ); @@ -361,7 +361,7 @@ export namespace Auth { export async function expectSessionToBeValid() { const response = await niceBackendFetch("/api/v1/auth/sessions/current/refresh", { method: "POST", accessType: "client" }); if (response.status !== 200) { - throw new StackAssertionError("Expected session to be valid, but was actually invalid.", { response }); + throw new HexclaveAssertionError("Expected session to be valid, but was actually invalid.", { response }); } expect(response).toMatchObject({ status: 200, @@ -391,7 +391,7 @@ export namespace Auth { await ensureParsableAccessToken(); const response = await niceBackendFetch("/api/v1/users/me", { accessType: "client" }); if (response.status === 200) { - throw new StackAssertionError("Expected access token to be invalid, but was actually valid.", { response }); + throw new HexclaveAssertionError("Expected access token to be invalid, but was actually valid.", { response }); } } @@ -405,7 +405,7 @@ export namespace Auth { await ensureParsableAccessToken(); const response = await niceBackendFetch("/api/v1/users/me", { accessType: "client" }); if (response.status !== 200) { - throw new StackAssertionError("Expected access token to be valid, but was actually invalid.", { response }); + throw new HexclaveAssertionError("Expected access token to be valid, but was actually invalid.", { response }); } } @@ -493,7 +493,7 @@ export namespace Auth { break; } if (performance.now() >= deadline) { - throw new StackAssertionError(`Sign-in code message not found within ${deadlineMs}ms`, { + throw new HexclaveAssertionError(`Sign-in code message not found within ${deadlineMs}ms`, { response, messages: messages.map(m => ({ ...m, body: m.body && omit(m.body, ["html"]) })), }); @@ -524,7 +524,7 @@ export namespace Auth { export async function signInWithCode(signInCode: string) { const projectKeys = backendContext.value.projectKeys; - if (projectKeys === "no-project") throw new StackAssertionError("Must provide project keys in the backend context before calling signInWithCode"); + if (projectKeys === "no-project") throw new HexclaveAssertionError("Must provide project keys in the backend context before calling signInWithCode"); const response = await niceBackendFetch("/api/v1/auth/otp/sign-in", { method: "POST", diff --git a/apps/e2e/tests/backend/endpoints/api/migration-tests.test.ts b/apps/e2e/tests/backend/endpoints/api/migration-tests.test.ts index abe8884654..3a32009c75 100644 --- a/apps/e2e/tests/backend/endpoints/api/migration-tests.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/migration-tests.test.ts @@ -22,7 +22,7 @@ describe("SmartRouteHandler", () => { Please see the API documentation at https://docs.stack-auth.com, or visit the Stack Auth dashboard at https://app.stack-auth.com. - URL: http://localhost:<$NEXT_PUBLIC_STACK_PORT_PREFIX>02/api/v1/migration-tests/smart-route-handler + URL: http://localhost:<$NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX>02/api/v1/migration-tests/smart-route-handler \`, "headers": Headers {