mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
## What
Continues the **Stack Auth → Hexclave** rename for a set of safe,
internal-only surfaces. This intentionally avoids public-contract names.
### Changes
- **Examples** — renamed the user-facing config module
`stack.ts`/`stack.tsx` (and the `convex` / `lovable` `stack/`
directories) to `hexclave`, and updated every importer across
`.ts`/`.tsx`/`.jsx`. The public `app/handler/[...stack]/` route segment
is left unchanged.
- **apps/{dashboard,backend,internal-tool}** — renamed app-local
SDK-init symbols `stackClientApp → hexclaveClientApp` and
`getStackServerApp → getHexclaveServerApp`, and the dashboard
`StackCompanion` component → `HexclaveCompanion` (incl.
`useStackCompanion`, context types). The public
`StackClientApp`/`StackServerApp` SDK classes are **unchanged**.
- **packages/stack-shared** — added comments to the crypto / JWT / vault
`stack-*` literals documenting that they must **not** be renamed (key
derivation / JWKS / KMS-alias stability). The literals are
byte-identical.
### Deliberately excluded
- **`STACK_*` → `HEXCLAVE_*` env-var rename** — `HEXCLAVE_*` already
resolves via the dual-read layers (SDK env, dashboard `_inlineEnvVars`,
`getEnvVariable`). The remaining holdout is the docker post-build
sentinel path, which the codebase authors explicitly deferred and which
is tightly coupled to `entrypoint.sh` + untestable here. A blind rename
there risks silently breaking self-host/emulator bootstrap for ~zero
functional gain.
- **All public-contract names** — SDK class names, env vars, HTTP
headers (`x-stack-*`), and the `/handler` route convention.
## Verification
- `pnpm lint` — **29/29 passing**.
- `pnpm typecheck` — **28/29 passing**; the only failure is
`@hexclave/docs` (pre-existing missing fumadocs `.source` codegen,
untouched by this PR).
- Two rounds of adversarial multi-agent review; findings fixed:
string-literal collateral from the symbol sweep (CLI test fixtures + an
AI-prompt template) reverted, and a missed `.jsx` importer in
`examples/cjs-test` corrected.
## Notes
- Based on a `dev` snapshot from when the branch was cut (a couple
commits behind tip); the diff contains only the changes above.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Complete the internal “Stack” → “Hexclave” rename across examples,
app-local code, config tooling, and setup docs, and standardize env
output to HEXCLAVE_* with correct default API URL handling. Public SDK
classes, handler routes, and legacy env names keep working.
- **Refactors**
- Examples/config: `stack.*` files and `stack/` dirs →
`hexclave.*`/`hexclave/`; imports updated; keep `app/handler/[...stack]`
route.
- Apps: backend/dashboard/internal-tool now use `getHexclaveServerApp`
and `hexclaveClientApp`; dashboard `StackCompanion` →
`HexclaveCompanion`. Public `StackClientApp`/`StackServerApp` unchanged.
- Env/setup: Next.js and CLI generators write HEXCLAVE_* and omit API
URL when using https://api.stack-auth.com; CLI `doctor` and auth
resolution prefer HEXCLAVE_* (e.g. `HEXCLAVE_SECRET_SERVER_KEY`,
`HEXCLAVE_PROJECT_ID`) with `STACK_*` fallback.
- Config tooling: `stack-config-file` → `hexclave-config-file`, emitting
`HexclaveConfig`; imports updated across backend/dashboard/tooling.
- Shared/docs: added “do not rename” notes for crypto/JWT/vault
`stack-*` literals; regenerated setup prompt/docs to use
`hexclave.config.ts`, `hexclave dev`, and `src/hexclave/`.
- Tests: updated snapshots/assertions to expect `HexclaveConfig` and
HEXCLAVE_* env names.
- **Migration**
- No action required. SDK and CLI read both HEXCLAVE_* and STACK_*.
<sup>Written for commit 8a891b4f6c.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1534?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Refactor**
* Renamed internal app/client/server instances and companion/provider
components to the new product name across backend, dashboard, examples,
and tooling; imports updated accordingly.
* Updated generated environment variable names and CLI init/doctor
outputs to prefer the new product prefix.
* **Documentation**
* Added clarifying notes about vault/encryption and JWT/key labels to
avoid breaking existing encrypted data.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
121 lines
4.6 KiB
TypeScript
121 lines
4.6 KiB
TypeScript
import * as parser from "@babel/parser";
|
|
import * as t from "@babel/types";
|
|
import { isValidConfig, normalize } from "./config/format";
|
|
|
|
export const showOnboardingStackConfigValue = "show-onboarding";
|
|
|
|
const DEFAULT_CONFIG_IMPORT_PACKAGE = "@hexclave/js";
|
|
|
|
/**
|
|
* Renders a config object into the source text of a `stack.config.ts` file.
|
|
*
|
|
* Browser-safe: kept here (next to `parseStackConfigFileContent`) instead of in
|
|
* `config-rendering.ts` so dashboard client code can render config files
|
|
* without pulling in `fs` / `path`.
|
|
*/
|
|
export function renderConfigFileContent(config: unknown, importPackage?: string): string {
|
|
if (!isValidConfig(config)) {
|
|
throw new Error("Invalid config: expected a plain object.");
|
|
}
|
|
|
|
const droppedKeys: string[] = [];
|
|
const normalizedConfig = normalize(config, {
|
|
onDotIntoNonObject: "ignore",
|
|
onDotIntoNull: "empty-object",
|
|
droppedKeys,
|
|
});
|
|
if (droppedKeys.length > 0) {
|
|
throw new Error(`Config has conflicting keys that would be dropped during normalization: ${droppedKeys.map(k => JSON.stringify(k)).join(", ")}`);
|
|
}
|
|
const pkg = importPackage ?? DEFAULT_CONFIG_IMPORT_PACKAGE;
|
|
const importLine = `import type { HexclaveConfig } from "${pkg}";`;
|
|
return `${importLine}\n\nexport const config: HexclaveConfig = ${JSON.stringify(normalizedConfig, null, 2)};\n`;
|
|
}
|
|
|
|
type ParsedStackConfig = Record<string, unknown> | typeof showOnboardingStackConfigValue;
|
|
|
|
function unwrapStaticConfigExpression(expression: t.Expression): t.Expression {
|
|
if (
|
|
t.isTSAsExpression(expression)
|
|
|| t.isTSSatisfiesExpression(expression)
|
|
|| t.isTSTypeAssertion(expression)
|
|
|| t.isTSNonNullExpression(expression)
|
|
) {
|
|
return unwrapStaticConfigExpression(expression.expression);
|
|
}
|
|
return expression;
|
|
}
|
|
|
|
function evaluateStaticConfigExpression(expression: t.Expression): unknown {
|
|
const unwrapped = unwrapStaticConfigExpression(expression);
|
|
if (t.isStringLiteral(unwrapped)) return unwrapped.value;
|
|
if (t.isBooleanLiteral(unwrapped)) return unwrapped.value;
|
|
if (t.isNumericLiteral(unwrapped)) return unwrapped.value;
|
|
if (t.isNullLiteral(unwrapped)) return null;
|
|
if (t.isIdentifier(unwrapped) && unwrapped.name === "undefined") return undefined;
|
|
if (t.isUnaryExpression(unwrapped) && unwrapped.operator === "-" && t.isNumericLiteral(unwrapped.argument)) {
|
|
return -unwrapped.argument.value;
|
|
}
|
|
if (t.isArrayExpression(unwrapped)) {
|
|
return unwrapped.elements.map((element) => {
|
|
if (element == null || t.isSpreadElement(element)) {
|
|
throw new Error("Config arrays cannot contain holes or spreads.");
|
|
}
|
|
return evaluateStaticConfigExpression(element);
|
|
});
|
|
}
|
|
if (t.isObjectExpression(unwrapped)) {
|
|
const result: Record<string, unknown> = {};
|
|
for (const property of unwrapped.properties) {
|
|
if (t.isSpreadElement(property)) {
|
|
throw new Error("Config objects cannot contain spreads.");
|
|
}
|
|
if (property.computed) {
|
|
throw new Error("Config object keys cannot be computed.");
|
|
}
|
|
const key = t.isIdentifier(property.key)
|
|
? property.key.name
|
|
: t.isStringLiteral(property.key) || t.isNumericLiteral(property.key)
|
|
? String(property.key.value)
|
|
: null;
|
|
if (key == null) {
|
|
throw new Error("Unsupported config object key.");
|
|
}
|
|
if (t.isObjectMethod(property)) {
|
|
throw new Error("Config objects cannot contain methods.");
|
|
}
|
|
if (!t.isExpression(property.value)) {
|
|
throw new Error("Unsupported config object value.");
|
|
}
|
|
result[key] = evaluateStaticConfigExpression(property.value);
|
|
}
|
|
return result;
|
|
}
|
|
throw new Error(`Unsupported config expression: ${unwrapped.type}`);
|
|
}
|
|
|
|
export function parseStackConfigFileContent(content: string, filePath: string): ParsedStackConfig {
|
|
if (content.trim() === "") return {};
|
|
const ast = parser.parse(content, {
|
|
sourceType: "module",
|
|
plugins: ["typescript"],
|
|
});
|
|
|
|
for (const statement of ast.program.body) {
|
|
if (!t.isExportNamedDeclaration(statement) || !t.isVariableDeclaration(statement.declaration)) {
|
|
continue;
|
|
}
|
|
for (const declaration of statement.declaration.declarations) {
|
|
if (!t.isIdentifier(declaration.id) || declaration.id.name !== "config") {
|
|
continue;
|
|
}
|
|
if (declaration.init == null || !t.isExpression(declaration.init)) {
|
|
throw new Error(`Config export in ${filePath} must have an initializer.`);
|
|
}
|
|
return evaluateStaticConfigExpression(declaration.init) as ParsedStackConfig;
|
|
}
|
|
}
|
|
|
|
throw new Error(`Invalid config in ${filePath}. The file must export a plain \`config\` object or "show-onboarding".`);
|
|
}
|