From 55e8a210ea17c86403c50e4798772e03cac79b10 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Thu, 21 May 2026 14:25:05 -0700 Subject: [PATCH 1/5] =?UTF-8?q?docs:=20add=20Stack=20Auth=20=E2=86=92=20He?= =?UTF-8?q?xclave=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 2/5] 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 3/5] =?UTF-8?q?docs:=20v7=20=E2=80=94=20internal-only=20re?= =?UTF-8?q?names,=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 4/5] 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 5/5] =?UTF-8?q?docs:=20correct=20x-stack-auth=20legacy=20h?= =?UTF-8?q?eader=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)