mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-27 21:01:03 +08:00
Rename STACK_* env vars to HEXCLAVE_* in env templates, with legacy dual-read (#1588)
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migration compat / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migration compat / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
## Summary
Completes the env-var side of the Hexclave rebrand: every
`STACK_*`-prefixed variable (including `NEXT_PUBLIC_STACK_*` and
`VITE_STACK_*`) is renamed to `HEXCLAVE_*` across all checked-in `.env`,
`.env.development`, and `.env.example` files (30 files, ~135 keys).
Legacy `STACK_*` names keep working everywhere via dual-read, so
**existing deployments, `.env.local` files, and self-hosted setups need
no immediate migration**.
## How legacy names keep working
- **Server code** already resolves `HEXCLAVE_*` first with `STACK_*`
fallback via `getEnvVariable`. Direct `process.env.STACK_X` readers fed
by the renamed files (prisma seed, e2e tests/helpers, internal-tool
scripts, examples, `prisma.config.ts`) now read `HEXCLAVE_X || STACK_X`.
- **Client code** (Next.js build-time inlining) uses literal dual-read
expressions; the dashboard's `_inlineEnvVars` already had them.
- **Docker/self-hosting**: `docker/server/entrypoint.sh` (shared by the
server and local-emulator images) gets a generic two-way
`HEXCLAVE_`↔`STACK_` env mirror — runs at startup and again before
sentinel replacement — replacing the previous URL-trio-only mirror.
Operators can use either prefix.
## The empty-placeholder trap (`||` vs `??`)
The checked-in templates define empty placeholders (`HEXCLAVE_X=#
comment` parses to `""` via dotenv). With `?? `-based fallbacks, that
empty string would silently shadow a real value under the legacy name —
including legacy vars set in Vercel/CI env at build time, since the
tracked `.env` is present during builds. All fallback chains therefore
treat empty-as-unset (`||`):
- `getEnvVariable` and `getProcessEnv` in `packages/shared`
- the dashboard/docs/example literal dual-reads
- the generated SDK env getters (via
`packages/template/scripts/generate-env.ts`; the generated
`src/generated/env.ts` files are gitignored and regenerate at build)
## Other notable changes
- Tests that override env now set the canonical `HEXCLAVE_*` name (it
wins over `STACK_*`): e2e `cross-domain-auth`, backend
`internal-feedback-emails` in-source test.
- e2e `helpers.ts` port-prefix expansion loop also matches the
`HEXCLAVE_` prefixes.
- `docker/local-emulator/generate-env-development.mjs` reads source keys
canonically (legacy fallback) and emits canonical keys; regenerated
output matches.
- `rotate-secrets.sh` falls back to
`HEXCLAVE_DATABASE_CONNECTION_STRING`.
- Docs code snippets (`docs/code-examples`) renamed outright to
canonical names, consistent with #1571.
- OAuth callback `console.warn` in `packages/template/src/lib/auth.ts`
now says Hexclave.
## Migration note for the team
Local `.env.local` files with legacy `STACK_*` overrides keep working
**unless** the override targets a var that `.env.development` now sets
to a real (non-empty) `HEXCLAVE_*` value — the canonical name wins over
file precedence. Rename those keys in your `.env.local` once.
## Verification
- `typecheck` + `lint` pass on every touched package (shared, backend,
dashboard, e2e, internal-tool, cli, docs, template). Pre-existing
failures on dev (`admin-app-impl.ts` typecheck, dashboard metrics-page
errors) are unchanged (identical error counts with/without this change).
- `getEnvVariable`/`getProcessEnv` fallback semantics smoke-tested
directly (empty-HEXCLAVE → legacy fallback, HEXCLAVE wins when set,
defaults intact).
- `internal-feedback-emails` in-source vitest passes; emulator env
generator `--check` passes; `bash -n` on touched shell scripts.
- Two independent review agents audited the diff for correctness bugs
and coverage gaps; all confirmed findings are fixed in the third commit.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Renamed all `STACK_*` env vars (including
`NEXT_PUBLIC_STACK_*`/`VITE_STACK_*`) to `HEXCLAVE_*` across env
templates and code, with dual‑read that treats empty as unset, detects
conflicts, ignores post‑build sentinels, and falls back to legacy names.
All GitHub Actions now use `HEXCLAVE_*`; local‑emulator e2e is fixed by
setting `NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR` in CI.
- **Refactors**
- Added conflict‑aware dual‑read helpers (prefer `HEXCLAVE_*`,
empty‑as‑unset, ignore post‑build sentinels, preserve empty passthrough)
and used them across `packages/shared` (resolver + tests),
`apps/dashboard` inline/public envs (with tests), `apps/backend` Prisma
config/seed and vitest (accept both prefixes), `packages/cli`
(API/Dashboard URLs, project ID, `HEXCLAVE_EMULATOR_HOME`; tests),
Docker (`entrypoint.sh` mirroring + `rotate-secrets.sh` DB URL),
docs/components (`docs/src/lib/env.ts`), and examples; hosted/Vite apps
now error if both spellings differ.
- Port‑prefix expansion includes `HEXCLAVE_*`; backend tests use a new
helper to resolve DB connection strings; Prisma prefers
`HEXCLAVE_DATABASE_CONNECTION_STRING` with legacy fallback.
- Generated SDK env getters use plain `HEXCLAVE_*` || `STACK_*` (no
conflict throw); dashboard inline resolver preserves empty/sentinel
passthrough to avoid build failures; docs/examples include dual‑read
utilities.
- Tests now stub canonical `HEXCLAVE_*` flags (e.g., plan limits, bot
challenge, OAuth tokens, hosted handler) to avoid shadowing/conflict
with committed defaults.
- **Migration**
- No immediate action; legacy `STACK_*` names still work.
- If both names are set with different values, builds/scripts error. Set
only `HEXCLAVE_*` or make both equal.
- SDK consumers won’t see conflict throws; update env names to
`HEXCLAVE_*` over time.
<sup>Written for commit 7539fb9fbf.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1588?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.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
* **Chores**
* Migrated environment variable names from the legacy `STACK_*` prefix
to the new `HEXCLAVE_*` prefix across backend, dashboard, tooling,
Docker, and examples.
* Updated environment/config resolution to prefer `HEXCLAVE_*`, treat
empty strings as unset, and detect conflicts when both `STACK_*` and
`HEXCLAVE_*` are set to different values.
* Updated local emulator, server startup, and env-generation workflows
to use the new names (with legacy fallback where applicable).
* **Documentation**
* Updated docs and code examples to reference `HEXCLAVE_*` variables.
* **Tests**
* Refreshed unit and e2e coverage to validate dual-read behavior,
conflict detection, and empty-value handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
b7f145e2db
commit
38ae913fc9
@ -54,8 +54,8 @@ jobs:
|
||||
runs-on: ubicloud-standard-8
|
||||
env:
|
||||
NODE_ENV: test
|
||||
STACK_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING: yes
|
||||
STACK_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:8128/stackframe"
|
||||
HEXCLAVE_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING: yes
|
||||
HEXCLAVE_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:8128/stackframe"
|
||||
|
||||
steps:
|
||||
# First, checkout the current branch to get its migrations
|
||||
@ -255,8 +255,8 @@ jobs:
|
||||
runs-on: ubicloud-standard-8
|
||||
env:
|
||||
NODE_ENV: test
|
||||
STACK_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING: yes
|
||||
STACK_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:8128/stackframe"
|
||||
HEXCLAVE_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING: yes
|
||||
HEXCLAVE_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:8128/stackframe"
|
||||
|
||||
steps:
|
||||
# First, checkout the base branch to get its migrations
|
||||
|
||||
@ -17,11 +17,11 @@ jobs:
|
||||
runs-on: ubicloud-standard-8
|
||||
env:
|
||||
NODE_ENV: test
|
||||
NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR: "true"
|
||||
STACK_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING: yes
|
||||
STACK_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:8128/stackframe"
|
||||
STACK_EXTERNAL_DB_SYNC_MAX_DURATION_MS: "20000"
|
||||
STACK_EXTERNAL_DB_SYNC_DIRECT: "false"
|
||||
NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR: "true"
|
||||
HEXCLAVE_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING: yes
|
||||
HEXCLAVE_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:8128/stackframe"
|
||||
HEXCLAVE_EXTERNAL_DB_SYNC_MAX_DURATION_MS: "20000"
|
||||
HEXCLAVE_EXTERNAL_DB_SYNC_DIRECT: "false"
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
10
.github/workflows/e2e-api-tests.yaml
vendored
10
.github/workflows/e2e-api-tests.yaml
vendored
@ -17,10 +17,10 @@ jobs:
|
||||
runs-on: ubicloud-standard-8
|
||||
env:
|
||||
NODE_ENV: test
|
||||
STACK_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING: yes
|
||||
STACK_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:8128/stackframe"
|
||||
STACK_EXTERNAL_DB_SYNC_MAX_DURATION_MS: "20000"
|
||||
STACK_EXTERNAL_DB_SYNC_DIRECT: "false"
|
||||
HEXCLAVE_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING: yes
|
||||
HEXCLAVE_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:8128/stackframe"
|
||||
HEXCLAVE_EXTERNAL_DB_SYNC_MAX_DURATION_MS: "20000"
|
||||
HEXCLAVE_EXTERNAL_DB_SYNC_DIRECT: "false"
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
@ -59,7 +59,7 @@ jobs:
|
||||
- name: Override Freestyle API key for prod mode
|
||||
if: matrix.freestyle-mode == 'prod'
|
||||
run: |
|
||||
echo "STACK_FREESTYLE_API_KEY=${{ secrets.STACK_FREESTYLE_REAL_API_KEY }}" >> apps/backend/.env.test.local
|
||||
echo "HEXCLAVE_FREESTYLE_API_KEY=${{ secrets.STACK_FREESTYLE_REAL_API_KEY }}" >> apps/backend/.env.test.local
|
||||
|
||||
- name: Create .env.test.local file for apps/dashboard
|
||||
run: cp apps/dashboard/.env.development apps/dashboard/.env.test.local
|
||||
|
||||
@ -16,11 +16,11 @@ jobs:
|
||||
runs-on: ubicloud-standard-8
|
||||
env:
|
||||
NODE_ENV: test
|
||||
STACK_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING: yes
|
||||
STACK_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:6728/stackframe"
|
||||
HEXCLAVE_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING: yes
|
||||
HEXCLAVE_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:6728/stackframe"
|
||||
NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX: "67"
|
||||
STACK_EXTERNAL_DB_SYNC_MAX_DURATION_MS: "20000"
|
||||
STACK_EXTERNAL_DB_SYNC_DIRECT: "false"
|
||||
HEXCLAVE_EXTERNAL_DB_SYNC_MAX_DURATION_MS: "20000"
|
||||
HEXCLAVE_EXTERNAL_DB_SYNC_DIRECT: "false"
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
12
.github/workflows/e2e-fallback-tests.yaml
vendored
12
.github/workflows/e2e-fallback-tests.yaml
vendored
@ -19,14 +19,14 @@ jobs:
|
||||
runs-on: ubicloud-standard-8
|
||||
env:
|
||||
NODE_ENV: test
|
||||
STACK_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING: yes
|
||||
STACK_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:8128/stackframe"
|
||||
STACK_EXTERNAL_DB_SYNC_MAX_DURATION_MS: "20000"
|
||||
STACK_EXTERNAL_DB_SYNC_DIRECT: "false"
|
||||
HEXCLAVE_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING: yes
|
||||
HEXCLAVE_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:8128/stackframe"
|
||||
HEXCLAVE_EXTERNAL_DB_SYNC_MAX_DURATION_MS: "20000"
|
||||
HEXCLAVE_EXTERNAL_DB_SYNC_DIRECT: "false"
|
||||
# SDK reads this as the primary URL, discovers hardcoded fallback to port 8110
|
||||
NEXT_PUBLIC_STACK_API_URL: "http://localhost:8102"
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL: "http://localhost:8102"
|
||||
# Tells js-helpers to omit explicit baseUrl so the SDK exercises fallback logic
|
||||
STACK_TEST_SDK_FALLBACK: "true"
|
||||
HEXCLAVE_TEST_SDK_FALLBACK: "true"
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
14
.github/workflows/qemu-emulator-build.yaml
vendored
14
.github/workflows/qemu-emulator-build.yaml
vendored
@ -26,12 +26,12 @@ env:
|
||||
EMULATOR_IMAGE_DIR: ${{ github.workspace }}/docker/local-emulator/qemu/images
|
||||
EMULATOR_RUN_DIR: ${{ github.workspace }}/docker/local-emulator/qemu/run
|
||||
# The stack-cli ignores EMULATOR_IMAGE_DIR/RUN_DIR and derives its own paths
|
||||
# from STACK_EMULATOR_HOME. Point it at the same workspace so `emulator
|
||||
# from HEXCLAVE_EMULATOR_HOME. Point it at the same workspace so `emulator
|
||||
# start` finds the freshly-built qcow2 from build-image.sh and cold-boots
|
||||
# it, instead of auto-pulling from a prior release. CI doesn't capture a
|
||||
# savevm (EMULATOR_CAPTURE_SAVEVM defaults to 0); users capture locally
|
||||
# on first `stack emulator pull`.
|
||||
STACK_EMULATOR_HOME: ${{ github.workspace }}/docker/local-emulator/qemu
|
||||
HEXCLAVE_EMULATOR_HOME: ${{ github.workspace }}/docker/local-emulator/qemu
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@ -265,11 +265,11 @@ jobs:
|
||||
name: qemu-emulator-${{ matrix.arch }}
|
||||
path: ${{ github.workspace }}/.stack-emulator-images/
|
||||
|
||||
- name: Place qcow2 into STACK_EMULATOR_HOME layout
|
||||
- name: Place qcow2 into HEXCLAVE_EMULATOR_HOME layout
|
||||
run: |
|
||||
mkdir -p "$STACK_EMULATOR_HOME/images"
|
||||
cp "${{ github.workspace }}/.stack-emulator-images/stack-emulator-${{ matrix.arch }}.qcow2" "$STACK_EMULATOR_HOME/images/"
|
||||
ls -lh "$STACK_EMULATOR_HOME/images/"
|
||||
mkdir -p "$HEXCLAVE_EMULATOR_HOME/images"
|
||||
cp "${{ github.workspace }}/.stack-emulator-images/stack-emulator-${{ matrix.arch }}.qcow2" "$HEXCLAVE_EMULATOR_HOME/images/"
|
||||
ls -lh "$HEXCLAVE_EMULATOR_HOME/images/"
|
||||
|
||||
# No savevm.zst artifact (users capture locally via `emulator pull`),
|
||||
# so `emulator start` cold-boots the qcow2. Budget accordingly.
|
||||
@ -300,7 +300,7 @@ jobs:
|
||||
|
||||
- name: Print serial log on failure
|
||||
if: failure()
|
||||
run: tail -100 "$STACK_EMULATOR_HOME/run/vm/serial.log" 2>/dev/null || true
|
||||
run: tail -100 "$HEXCLAVE_EMULATOR_HOME/run/vm/serial.log" 2>/dev/null || true
|
||||
|
||||
publish:
|
||||
name: Publish to GitHub Releases
|
||||
|
||||
@ -20,8 +20,8 @@ jobs:
|
||||
runs-on: ubicloud-standard-16
|
||||
env:
|
||||
NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX: "69"
|
||||
STACK_EXTERNAL_DB_SYNC_MAX_DURATION_MS: "20000"
|
||||
STACK_EXTERNAL_DB_SYNC_DIRECT: "false"
|
||||
HEXCLAVE_EXTERNAL_DB_SYNC_MAX_DURATION_MS: "20000"
|
||||
HEXCLAVE_EXTERNAL_DB_SYNC_DIRECT: "false"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
4
.github/workflows/setup-tests.yaml
vendored
4
.github/workflows/setup-tests.yaml
vendored
@ -19,8 +19,8 @@ jobs:
|
||||
if: ${{ (github.head_ref || github.ref_name) == 'dev' }}
|
||||
runs-on: ubicloud-standard-16
|
||||
env:
|
||||
STACK_EXTERNAL_DB_SYNC_MAX_DURATION_MS: "20000"
|
||||
STACK_EXTERNAL_DB_SYNC_DIRECT: "false"
|
||||
HEXCLAVE_EXTERNAL_DB_SYNC_MAX_DURATION_MS: "20000"
|
||||
HEXCLAVE_EXTERNAL_DB_SYNC_DIRECT: "false"
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
|
||||
@ -1,127 +1,127 @@
|
||||
# Basic
|
||||
NEXT_PUBLIC_STACK_API_URL=# the base URL of Stack's backend/API. For local development, this is `http://localhost:8102`; for the managed service, this is `https://api.hexclave.com`.
|
||||
NEXT_PUBLIC_STACK_DASHBOARD_URL=# the URL of Stack's dashboard. For local development, this is `http://localhost:8101`; for the managed service, this is `https://app.hexclave.com`.
|
||||
NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=# set to true to enable local emulator-only behaviors (internal local emulator endpoints, read-only environment config overrides, and local emulator auth UX)
|
||||
STACK_SECRET_SERVER_KEY=# a random, unguessable secret key generated by `pnpm generate-keys`
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=# the base URL of Stack's backend/API. For local development, this is `http://localhost:8102`; for the managed service, this is `https://api.hexclave.com`.
|
||||
NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL=# the URL of Stack's dashboard. For local development, this is `http://localhost:8101`; for the managed service, this is `https://app.hexclave.com`.
|
||||
NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR=# set to true to enable local emulator-only behaviors (internal local emulator endpoints, read-only environment config overrides, and local emulator auth UX)
|
||||
HEXCLAVE_SECRET_SERVER_KEY=# a random, unguessable secret key generated by `pnpm generate-keys`
|
||||
|
||||
|
||||
# seed script settings
|
||||
STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED=# true to enable user sign up to the dashboard when seeding
|
||||
STACK_SEED_INTERNAL_PROJECT_OTP_ENABLED=# true to add OTP auth to the dashboard when seeding
|
||||
STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST=# true to allow running dashboard on the localhost, set this to true only in development
|
||||
STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS=# list of oauth providers to add to the dashboard when seeding, separated by comma, for example "github,google,facebook"
|
||||
STACK_SEED_INTERNAL_PROJECT_USER_EMAIL=# default user added to the dashboard
|
||||
STACK_SEED_INTERNAL_PROJECT_USER_PASSWORD=# default user's password, paired with STACK_SEED_INTERNAL_PROJECT_USER_EMAIL
|
||||
STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=# if the default user has access to the internal dashboard project
|
||||
STACK_SEED_INTERNAL_PROJECT_USER_GITHUB_ID=# add github oauth id to the default user
|
||||
STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=# default publishable client key for the internal project
|
||||
STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY=# default secret server key for the internal project
|
||||
STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=# default super secret admin key for the internal project
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED=# true to enable user sign up to the dashboard when seeding
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_OTP_ENABLED=# true to add OTP auth to the dashboard when seeding
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST=# true to allow running dashboard on the localhost, set this to true only in development
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS=# list of oauth providers to add to the dashboard when seeding, separated by comma, for example "github,google,facebook"
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_USER_EMAIL=# default user added to the dashboard
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_USER_PASSWORD=# default user's password, paired with HEXCLAVE_SEED_INTERNAL_PROJECT_USER_EMAIL
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=# if the default user has access to the internal dashboard project
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_USER_GITHUB_ID=# add github oauth id to the default user
|
||||
HEXCLAVE_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=# default publishable client key for the internal project
|
||||
HEXCLAVE_INTERNAL_PROJECT_SECRET_SERVER_KEY=# default secret server key for the internal project
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=# default super secret admin key for the internal project
|
||||
|
||||
# OAuth mock provider settings
|
||||
STACK_OAUTH_MOCK_URL=# enter the URL of the mock OAuth provider here. For local development, use `http://localhost:8114`.
|
||||
HEXCLAVE_OAUTH_MOCK_URL=# enter the URL of the mock OAuth provider here. For local development, use `http://localhost:8114`.
|
||||
|
||||
# OAuth shared keys
|
||||
# Can be set to MOCK to use mock OAuth providers
|
||||
STACK_GITHUB_CLIENT_ID=# client
|
||||
STACK_GITHUB_CLIENT_SECRET=# client secret
|
||||
STACK_GOOGLE_CLIENT_ID=# client id
|
||||
STACK_GOOGLE_CLIENT_SECRET=# client secret
|
||||
STACK_MICROSOFT_CLIENT_ID=# client id
|
||||
STACK_MICROSOFT_CLIENT_SECRET=# client secret
|
||||
STACK_SPOTIFY_CLIENT_ID=# client id
|
||||
STACK_SPOTIFY_CLIENT_SECRET=# client secret
|
||||
HEXCLAVE_GITHUB_CLIENT_ID=# client
|
||||
HEXCLAVE_GITHUB_CLIENT_SECRET=# client secret
|
||||
HEXCLAVE_GOOGLE_CLIENT_ID=# client id
|
||||
HEXCLAVE_GOOGLE_CLIENT_SECRET=# client secret
|
||||
HEXCLAVE_MICROSOFT_CLIENT_ID=# client id
|
||||
HEXCLAVE_MICROSOFT_CLIENT_SECRET=# client secret
|
||||
HEXCLAVE_SPOTIFY_CLIENT_ID=# client id
|
||||
HEXCLAVE_SPOTIFY_CLIENT_SECRET=# client secret
|
||||
|
||||
STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS=# allow shared oauth provider to also use connected account access token, this should only be used for development and testing
|
||||
HEXCLAVE_ALLOW_SHARED_OAUTH_ACCESS_TOKENS=# allow shared oauth provider to also use connected account access token, this should only be used for development and testing
|
||||
|
||||
STACK_DISABLE_PLAN_LIMITS=# set to "true" to bypass enforcement of Hexclave's own internal-tenancy plan limits (analytics_events, session_replays, emails_per_month, dashboard_admins seat cap, auth_users soft cap, analytics_timeout_seconds). Default unset/false preserves enforcement. Intended as a temporary cutover safety net while the plan-limits infrastructure rolls out — customer projects' own item APIs are unaffected by this flag.
|
||||
HEXCLAVE_DISABLE_PLAN_LIMITS=# set to "true" to bypass enforcement of Hexclave's own internal-tenancy plan limits (analytics_events, session_replays, emails_per_month, dashboard_admins seat cap, auth_users soft cap, analytics_timeout_seconds). Default unset/false preserves enforcement. Intended as a temporary cutover safety net while the plan-limits infrastructure rolls out — customer projects' own item APIs are unaffected by this flag.
|
||||
|
||||
# Email
|
||||
# For local development, you can spin up a local SMTP server like inbucket
|
||||
STACK_EMAIL_HOST=# for local inbucket: 127.0.0.1
|
||||
STACK_EMAIL_PORT=# for local inbucket: 8129
|
||||
STACK_EMAIL_USERNAME=# for local inbucket: test
|
||||
STACK_EMAIL_PASSWORD=# for local inbucket: none
|
||||
STACK_EMAIL_SENDER=# for local inbucket: noreply@test.com
|
||||
STACK_EMAILABLE_API_KEY=# Emailable API key for email validation, see https://emailable.com. Use a test key (starting with "test_") for local dev — it does not consume credits. Set to "disable_email_validation" to disable.
|
||||
HEXCLAVE_EMAIL_HOST=# for local inbucket: 127.0.0.1
|
||||
HEXCLAVE_EMAIL_PORT=# for local inbucket: 8129
|
||||
HEXCLAVE_EMAIL_USERNAME=# for local inbucket: test
|
||||
HEXCLAVE_EMAIL_PASSWORD=# for local inbucket: none
|
||||
HEXCLAVE_EMAIL_SENDER=# for local inbucket: noreply@test.com
|
||||
HEXCLAVE_EMAILABLE_API_KEY=# Emailable API key for email validation, see https://emailable.com. Use a test key (starting with "test_") for local dev — it does not consume credits. Set to "disable_email_validation" to disable.
|
||||
|
||||
STACK_DEFAULT_EMAIL_CAPACITY_PER_HOUR=# the number of emails a new project can send. Defaults to 200
|
||||
HEXCLAVE_DEFAULT_EMAIL_CAPACITY_PER_HOUR=# the number of emails a new project can send. Defaults to 200
|
||||
|
||||
# Email branching configuration
|
||||
# If you have multiple deployments of compute accessing the same DB or multiple copies of a DBs connected to compute (as
|
||||
# you would in preview/branching environments), you may want to either disable the auto-triggered email queue steps
|
||||
# (those that trigger whenever an email is sent, besides the cron job), or disable email sending as a whole.
|
||||
STACK_EMAIL_BRANCHING_DISABLE_QUEUE_AUTO_TRIGGER=# set to 'true' to disable the automatic triggering of the email queue step. the cron job must call /email-queue-step to run the queue step. Most useful on production domains where you know the cron job will run on the correct deployment and you don't need the auto-trigger (which may be on the wrong deployment)
|
||||
STACK_EMAIL_BRANCHING_DISABLE_QUEUE_SENDING=# set to 'true' to throw an error instead of sending emails in the email queue step. Most useful on development branches that have a copy of the production DB, but should not send any emails (as otherwise some emails could be sent twice)
|
||||
HEXCLAVE_EMAIL_BRANCHING_DISABLE_QUEUE_AUTO_TRIGGER=# set to 'true' to disable the automatic triggering of the email queue step. the cron job must call /email-queue-step to run the queue step. Most useful on production domains where you know the cron job will run on the correct deployment and you don't need the auto-trigger (which may be on the wrong deployment)
|
||||
HEXCLAVE_EMAIL_BRANCHING_DISABLE_QUEUE_SENDING=# set to 'true' to throw an error instead of sending emails in the email queue step. Most useful on development branches that have a copy of the production DB, but should not send any emails (as otherwise some emails could be sent twice)
|
||||
|
||||
|
||||
# Database
|
||||
# For local development: `docker run -it --rm -e POSTGRES_PASSWORD=password -p "8128:5432" postgres`
|
||||
STACK_DATABASE_CONNECTION_STRING=# enter your connection string here. For local development: `postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:8128/stackframe`
|
||||
HEXCLAVE_DATABASE_CONNECTION_STRING=# enter your connection string here. For local development: `postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:8128/stackframe`
|
||||
|
||||
# Webhooks
|
||||
STACK_SVIX_SERVER_URL=# For prod, leave it empty. For local development, use `http://localhost:8113`
|
||||
STACK_SVIX_API_KEY=# enter the API key for the Svix webhook service here. Use `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NTUxNDA2MzksImV4cCI6MTk3MDUwMDYzOSwibmJmIjoxNjU1MTQwNjM5LCJpc3MiOiJzdml4LXNlcnZlciIsInN1YiI6Im9yZ18yM3JiOFlkR3FNVDBxSXpwZ0d3ZFhmSGlyTXUifQ.En8w77ZJWbd0qrMlHHupHUB-4cx17RfzFykseg95SUk` for local development
|
||||
HEXCLAVE_SVIX_SERVER_URL=# For prod, leave it empty. For local development, use `http://localhost:8113`
|
||||
HEXCLAVE_SVIX_API_KEY=# enter the API key for the Svix webhook service here. Use `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NTUxNDA2MzksImV4cCI6MTk3MDUwMDYzOSwibmJmIjoxNjU1MTQwNjM5LCJpc3MiOiJzdml4LXNlcnZlciIsInN1YiI6Im9yZ18yM3JiOFlkR3FNVDBxSXpwZ0d3ZFhmSGlyTXUifQ.En8w77ZJWbd0qrMlHHupHUB-4cx17RfzFykseg95SUk` for local development
|
||||
|
||||
# S3
|
||||
STACK_S3_PUBLIC_ENDPOINT=# publicly accessible endpoint
|
||||
STACK_S3_ENDPOINT=# S3 API endpoint URL (e.g., 'https://s3.amazonaws.com' for AWS or custom endpoint for S3-compatible services)
|
||||
STACK_S3_REGION=
|
||||
STACK_S3_ACCESS_KEY_ID=
|
||||
STACK_S3_SECRET_ACCESS_KEY=
|
||||
STACK_S3_BUCKET=
|
||||
STACK_S3_PRIVATE_BUCKET=
|
||||
HEXCLAVE_S3_PUBLIC_ENDPOINT=# publicly accessible endpoint
|
||||
HEXCLAVE_S3_ENDPOINT=# S3 API endpoint URL (e.g., 'https://s3.amazonaws.com' for AWS or custom endpoint for S3-compatible services)
|
||||
HEXCLAVE_S3_REGION=
|
||||
HEXCLAVE_S3_ACCESS_KEY_ID=
|
||||
HEXCLAVE_S3_SECRET_ACCESS_KEY=
|
||||
HEXCLAVE_S3_BUCKET=
|
||||
HEXCLAVE_S3_PRIVATE_BUCKET=
|
||||
|
||||
# AWS configuration
|
||||
STACK_AWS_REGION=
|
||||
STACK_AWS_KMS_ENDPOINT=
|
||||
STACK_AWS_ACCESS_KEY_ID=
|
||||
STACK_AWS_SECRET_ACCESS_KEY=
|
||||
STACK_AWS_VERCEL_OIDC_ROLE_ARN=
|
||||
HEXCLAVE_AWS_REGION=
|
||||
HEXCLAVE_AWS_KMS_ENDPOINT=
|
||||
HEXCLAVE_AWS_ACCESS_KEY_ID=
|
||||
HEXCLAVE_AWS_SECRET_ACCESS_KEY=
|
||||
HEXCLAVE_AWS_VERCEL_OIDC_ROLE_ARN=
|
||||
|
||||
# Upstash configuration
|
||||
STACK_QSTASH_URL=
|
||||
STACK_QSTASH_TOKEN=
|
||||
STACK_QSTASH_CURRENT_SIGNING_KEY=
|
||||
STACK_QSTASH_NEXT_SIGNING_KEY=
|
||||
HEXCLAVE_QSTASH_URL=
|
||||
HEXCLAVE_QSTASH_TOKEN=
|
||||
HEXCLAVE_QSTASH_CURRENT_SIGNING_KEY=
|
||||
HEXCLAVE_QSTASH_NEXT_SIGNING_KEY=
|
||||
|
||||
# Email monitor
|
||||
STACK_EMAIL_MONITOR_RESEND_EMAIL_API_KEY=# enter the resend poller api key here
|
||||
STACK_EMAIL_MONITOR_RESEND_EMAIL_DOMAIN=# enter the resend domain that should receive the emails
|
||||
STACK_EMAIL_MONITOR_PROJECT_ID=# enter the project id for the project that the email monitor will attempt to sign up for
|
||||
STACK_EMAIL_MONITOR_PUBLISHABLE_CLIENT_KEY=# enter the publishable client key for email monitor to use when attempting a sign up
|
||||
STACK_EMAIL_MONITOR_VERIFICATION_CALLBACK_URL=# enter a valid verification callback url for the project that the email monitor will attempt to sign up for
|
||||
STACK_EMAIL_MONITOR_INBUCKET_API_URL=# enter a valid inbucket api url for the email monitor to check emails from in test mode
|
||||
STACK_EMAIL_MONITOR_USE_INBUCKET=# enter true/false based on whether the email monitor should use inbucket or resend. Note that if this is set to true in prod, the email monitor will throw an error.
|
||||
STACK_EMAIL_MONITOR_SECRET_TOKEN=# enter the secret token value needed for the request to the email monitor to be accepted
|
||||
HEXCLAVE_EMAIL_MONITOR_RESEND_EMAIL_API_KEY=# enter the resend poller api key here
|
||||
HEXCLAVE_EMAIL_MONITOR_RESEND_EMAIL_DOMAIN=# enter the resend domain that should receive the emails
|
||||
HEXCLAVE_EMAIL_MONITOR_PROJECT_ID=# enter the project id for the project that the email monitor will attempt to sign up for
|
||||
HEXCLAVE_EMAIL_MONITOR_PUBLISHABLE_CLIENT_KEY=# enter the publishable client key for email monitor to use when attempting a sign up
|
||||
HEXCLAVE_EMAIL_MONITOR_VERIFICATION_CALLBACK_URL=# enter a valid verification callback url for the project that the email monitor will attempt to sign up for
|
||||
HEXCLAVE_EMAIL_MONITOR_INBUCKET_API_URL=# enter a valid inbucket api url for the email monitor to check emails from in test mode
|
||||
HEXCLAVE_EMAIL_MONITOR_USE_INBUCKET=# enter true/false based on whether the email monitor should use inbucket or resend. Note that if this is set to true in prod, the email monitor will throw an error.
|
||||
HEXCLAVE_EMAIL_MONITOR_SECRET_TOKEN=# enter the secret token value needed for the request to the email monitor to be accepted
|
||||
|
||||
# Clickhouse
|
||||
STACK_CLICKHOUSE_URL=# URL of the Clickhouse instance
|
||||
STACK_CLICKHOUSE_ADMIN_USER=# username of the admin account
|
||||
STACK_CLICKHOUSE_ADMIN_PASSWORD=# password of the admin account
|
||||
STACK_CLICKHOUSE_EXTERNAL_PASSWORD=# a randomly generated secure string. The user account will be created automatically
|
||||
HEXCLAVE_CLICKHOUSE_URL=# URL of the Clickhouse instance
|
||||
HEXCLAVE_CLICKHOUSE_ADMIN_USER=# username of the admin account
|
||||
HEXCLAVE_CLICKHOUSE_ADMIN_PASSWORD=# password of the admin account
|
||||
HEXCLAVE_CLICKHOUSE_EXTERNAL_PASSWORD=# a randomly generated secure string. The user account will be created automatically
|
||||
|
||||
|
||||
# Misc
|
||||
STACK_ACCESS_TOKEN_EXPIRATION_TIME=# enter the expiration time for the access token here. Optional, don't specify it for default value
|
||||
STACK_SETUP_ADMIN_GITHUB_ID=# enter the account ID of the admin user here, and after running the seed script they will be able to access the internal project in the Stack dashboard. Optional, don't specify it for default value
|
||||
HEXCLAVE_ACCESS_TOKEN_EXPIRATION_TIME=# enter the expiration time for the access token here. Optional, don't specify it for default value
|
||||
HEXCLAVE_SETUP_ADMIN_GITHUB_ID=# enter the account ID of the admin user here, and after running the seed script they will be able to access the internal project in the Stack dashboard. Optional, don't specify it for default value
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=# enter the OpenTelemetry endpoint here. Optional, default is `http://localhost:8131`
|
||||
STACK_INTEGRATION_CLIENTS_CONFIG=# a list of oidc-provider clients for integrations. If not provided, disables integrations
|
||||
STACK_FREESTYLE_API_KEY=# enter your freestyle.sh api key
|
||||
STACK_VERCEL_SANDBOX_PROJECT_ID=# enter the project id for the vercel project that the vercel engine will use
|
||||
STACK_VERCEL_SANDBOX_TEAM_ID=# enter the team id for the vercel project that the vercel engine will use
|
||||
STACK_VERCEL_SANDBOX_TOKEN=# enter the token for the vercel project that the vercel engine will use
|
||||
STACK_OPENAI_API_KEY=# enter your openai api key
|
||||
STACK_FEATUREBASE_API_KEY=# enter your featurebase api key
|
||||
STACK_STRIPE_SECRET_KEY=# enter your stripe api key
|
||||
STACK_STRIPE_WEBHOOK_SECRET=# enter your stripe webhook secret
|
||||
STACK_TELEGRAM_BOT_TOKEN= # enter you telegram bot token
|
||||
STACK_TELEGRAM_CHAT_ID=# enter your telegram chat id
|
||||
HEXCLAVE_INTEGRATION_CLIENTS_CONFIG=# a list of oidc-provider clients for integrations. If not provided, disables integrations
|
||||
HEXCLAVE_FREESTYLE_API_KEY=# enter your freestyle.sh api key
|
||||
HEXCLAVE_VERCEL_SANDBOX_PROJECT_ID=# enter the project id for the vercel project that the vercel engine will use
|
||||
HEXCLAVE_VERCEL_SANDBOX_TEAM_ID=# enter the team id for the vercel project that the vercel engine will use
|
||||
HEXCLAVE_VERCEL_SANDBOX_TOKEN=# enter the token for the vercel project that the vercel engine will use
|
||||
HEXCLAVE_OPENAI_API_KEY=# enter your openai api key
|
||||
HEXCLAVE_FEATUREBASE_API_KEY=# enter your featurebase api key
|
||||
HEXCLAVE_STRIPE_SECRET_KEY=# enter your stripe api key
|
||||
HEXCLAVE_STRIPE_WEBHOOK_SECRET=# enter your stripe webhook secret
|
||||
HEXCLAVE_TELEGRAM_BOT_TOKEN= # enter you telegram bot token
|
||||
HEXCLAVE_TELEGRAM_CHAT_ID=# enter your telegram chat id
|
||||
|
||||
# Docs AI tool bundle
|
||||
STACK_MINTLIFY_MCP_URL=# override the Mintlify MCP server used by the backend's AI docs tool bundle. Defaults to https://stackauth-e0affa27.mintlify.app/mcp
|
||||
HEXCLAVE_MINTLIFY_MCP_URL=# override the Mintlify MCP server used by the backend's AI docs tool bundle. Defaults to https://stackauth-e0affa27.mintlify.app/mcp
|
||||
|
||||
# MCP review tool (SpacetimeDB)
|
||||
STACK_SPACETIMEDB_URI=# SpacetimeDB host URI; default empty (logging disabled)
|
||||
STACK_SPACETIMEDB_DB_NAME=# SpacetimeDB database name
|
||||
STACK_MCP_LOG_TOKEN=# shared secret gating the log_mcp_call reducer; must match EXPECTED_LOG_TOKEN in apps/internal-tool/spacetimedb/src/index.ts
|
||||
HEXCLAVE_SPACETIMEDB_URI=# SpacetimeDB host URI; default empty (logging disabled)
|
||||
HEXCLAVE_SPACETIMEDB_DB_NAME=# SpacetimeDB database name
|
||||
HEXCLAVE_MCP_LOG_TOKEN=# shared secret gating the log_mcp_call reducer; must match EXPECTED_LOG_TOKEN in apps/internal-tool/spacetimedb/src/index.ts
|
||||
|
||||
@ -1,138 +1,138 @@
|
||||
NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_STACK_DASHBOARD_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}01
|
||||
NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX=.localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09
|
||||
NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=false
|
||||
STACK_SERVER_SECRET=23-wuNpik0gIW4mruTz25rbIvhuuvZFrLOLtL7J4tyo
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}01
|
||||
NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_DOMAIN_SUFFIX=.localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09
|
||||
NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR=false
|
||||
HEXCLAVE_SERVER_SECRET=23-wuNpik0gIW4mruTz25rbIvhuuvZFrLOLtL7J4tyo
|
||||
|
||||
STACK_CHANGELOG_URL=https://raw.githubusercontent.com/hexclave/hexclave/refs/heads/dev/CHANGELOG.md
|
||||
HEXCLAVE_CHANGELOG_URL=https://raw.githubusercontent.com/hexclave/hexclave/refs/heads/dev/CHANGELOG.md
|
||||
|
||||
STACK_SEED_ENABLE_DUMMY_PROJECT=true
|
||||
STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED=true
|
||||
STACK_SEED_INTERNAL_PROJECT_OTP_ENABLED=true
|
||||
STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST=true
|
||||
STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS=github,spotify,google,microsoft
|
||||
STACK_SEED_INTERNAL_PROJECT_USER_GITHUB_ID=admin@example.com
|
||||
STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=true
|
||||
STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=this-super-secret-admin-key-is-for-local-development-only
|
||||
HEXCLAVE_SEED_ENABLE_DUMMY_PROJECT=true
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED=true
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_OTP_ENABLED=true
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST=true
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS=github,spotify,google,microsoft
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_USER_GITHUB_ID=admin@example.com
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=true
|
||||
HEXCLAVE_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
HEXCLAVE_INTERNAL_PROJECT_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=this-super-secret-admin-key-is-for-local-development-only
|
||||
|
||||
STACK_OAUTH_MOCK_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}14
|
||||
STACK_TURNSTILE_SITEVERIFY_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}14/turnstile/siteverify
|
||||
HEXCLAVE_OAUTH_MOCK_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}14
|
||||
HEXCLAVE_TURNSTILE_SITEVERIFY_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}14/turnstile/siteverify
|
||||
|
||||
# Cloudflare Turnstile test keys — always-pass widgets, no real challenges
|
||||
# See https://developers.cloudflare.com/turnstile/troubleshooting/testing/
|
||||
NEXT_PUBLIC_STACK_BOT_CHALLENGE_SITE_KEY=1x00000000000000000000AA
|
||||
NEXT_PUBLIC_STACK_BOT_CHALLENGE_INVISIBLE_SITE_KEY=1x00000000000000000000BB
|
||||
STACK_TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
|
||||
NEXT_PUBLIC_HEXCLAVE_BOT_CHALLENGE_SITE_KEY=1x00000000000000000000AA
|
||||
NEXT_PUBLIC_HEXCLAVE_BOT_CHALLENGE_INVISIBLE_SITE_KEY=1x00000000000000000000BB
|
||||
HEXCLAVE_TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
|
||||
# Set to true to disable Turnstile entirely in local development.
|
||||
# This skips invisible/visible bot challenge flow and removes the Turnstile risk penalty.
|
||||
STACK_DISABLE_BOT_CHALLENGE=false
|
||||
HEXCLAVE_DISABLE_BOT_CHALLENGE=false
|
||||
# Default behavior is to block sign-up if the visible challenge cannot be completed.
|
||||
# Flip this only when you intentionally want local sign-up to continue during Turnstile outages.
|
||||
STACK_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE=false
|
||||
HEXCLAVE_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE=false
|
||||
|
||||
STACK_GITHUB_CLIENT_ID=MOCK
|
||||
STACK_GITHUB_CLIENT_SECRET=MOCK
|
||||
STACK_GOOGLE_CLIENT_ID=MOCK
|
||||
STACK_GOOGLE_CLIENT_SECRET=MOCK
|
||||
STACK_MICROSOFT_CLIENT_ID=MOCK
|
||||
STACK_MICROSOFT_CLIENT_SECRET=MOCK
|
||||
STACK_SPOTIFY_CLIENT_ID=MOCK
|
||||
STACK_SPOTIFY_CLIENT_SECRET=MOCK
|
||||
HEXCLAVE_GITHUB_CLIENT_ID=MOCK
|
||||
HEXCLAVE_GITHUB_CLIENT_SECRET=MOCK
|
||||
HEXCLAVE_GOOGLE_CLIENT_ID=MOCK
|
||||
HEXCLAVE_GOOGLE_CLIENT_SECRET=MOCK
|
||||
HEXCLAVE_MICROSOFT_CLIENT_ID=MOCK
|
||||
HEXCLAVE_MICROSOFT_CLIENT_SECRET=MOCK
|
||||
HEXCLAVE_SPOTIFY_CLIENT_ID=MOCK
|
||||
HEXCLAVE_SPOTIFY_CLIENT_SECRET=MOCK
|
||||
|
||||
STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS=true
|
||||
HEXCLAVE_ALLOW_SHARED_OAUTH_ACCESS_TOKENS=true
|
||||
|
||||
# Default to enforcing plan limits in local dev so behavior matches prod.
|
||||
# Flip to "true" to bypass every Stack-Auth-internal plan-limit enforcement
|
||||
# site (e.g. session_replays, analytics_events, emails_per_month). See
|
||||
# apps/backend/src/lib/plan-entitlements.ts:arePlanLimitsEnforced.
|
||||
STACK_DISABLE_PLAN_LIMITS=false
|
||||
HEXCLAVE_DISABLE_PLAN_LIMITS=false
|
||||
|
||||
STACK_DATABASE_CONNECTION_STRING=postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}28/stackframe
|
||||
STACK_DATABASE_REPLICA_CONNECTION_STRING=postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}34/stackframe
|
||||
STACK_DATABASE_REPLICATION_WAIT_STRATEGY=pg-stat-replication
|
||||
HEXCLAVE_DATABASE_CONNECTION_STRING=postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}28/stackframe
|
||||
HEXCLAVE_DATABASE_REPLICA_CONNECTION_STRING=postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}34/stackframe
|
||||
HEXCLAVE_DATABASE_REPLICATION_WAIT_STRATEGY=pg-stat-replication
|
||||
|
||||
STACK_EMAIL_HOST=127.0.0.1
|
||||
STACK_EMAIL_PORT=${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}29
|
||||
STACK_EMAIL_SECURE=false
|
||||
STACK_EMAIL_USERNAME="does not matter, ignored by Inbucket"
|
||||
STACK_EMAIL_PASSWORD="does not matter, ignored by Inbucket"
|
||||
STACK_EMAIL_SENDER=noreply@example.com
|
||||
HEXCLAVE_EMAIL_HOST=127.0.0.1
|
||||
HEXCLAVE_EMAIL_PORT=${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}29
|
||||
HEXCLAVE_EMAIL_SECURE=false
|
||||
HEXCLAVE_EMAIL_USERNAME="does not matter, ignored by Inbucket"
|
||||
HEXCLAVE_EMAIL_PASSWORD="does not matter, ignored by Inbucket"
|
||||
HEXCLAVE_EMAIL_SENDER=noreply@example.com
|
||||
|
||||
STACK_ACCESS_TOKEN_EXPIRATION_TIME=60s
|
||||
HEXCLAVE_ACCESS_TOKEN_EXPIRATION_TIME=60s
|
||||
|
||||
STACK_DEFAULT_EMAIL_CAPACITY_PER_HOUR=100000
|
||||
HEXCLAVE_DEFAULT_EMAIL_CAPACITY_PER_HOUR=100000
|
||||
|
||||
STACK_SVIX_SERVER_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}13
|
||||
STACK_SVIX_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NTUxNDA2MzksImV4cCI6MTk3MDUwMDYzOSwibmJmIjoxNjU1MTQwNjM5LCJpc3MiOiJzdml4LXNlcnZlciIsInN1YiI6Im9yZ18yM3JiOFlkR3FNVDBxSXpwZ0d3ZFhmSGlyTXUifQ.En8w77ZJWbd0qrMlHHupHUB-4cx17RfzFykseg95SUk
|
||||
HEXCLAVE_SVIX_SERVER_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}13
|
||||
HEXCLAVE_SVIX_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NTUxNDA2MzksImV4cCI6MTk3MDUwMDYzOSwibmJmIjoxNjU1MTQwNjM5LCJpc3MiOiJzdml4LXNlcnZlciIsInN1YiI6Im9yZ18yM3JiOFlkR3FNVDBxSXpwZ0d3ZFhmSGlyTXUifQ.En8w77ZJWbd0qrMlHHupHUB-4cx17RfzFykseg95SUk
|
||||
|
||||
# Trusted reverse proxy for reading real client IP addresses.
|
||||
# Set to "vercel", "cloudflare", or leave empty/unset for no proxy trust.
|
||||
STACK_TRUSTED_PROXY=
|
||||
HEXCLAVE_TRUSTED_PROXY=
|
||||
|
||||
STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS=500
|
||||
HEXCLAVE_ARTIFICIAL_DEVELOPMENT_DELAY_MS=500
|
||||
|
||||
STACK_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING=yes
|
||||
HEXCLAVE_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING=yes
|
||||
|
||||
STACK_INTEGRATION_CLIENTS_CONFIG='[{"client_id": "neon-local", "client_secret": "neon-local-secret", "id_token_signed_response_alg": "ES256", "redirect_uris": ["http://localhost:30000/api/v2/identity/authorize", "http://localhost:30000/api/v2/auth/authorize"]}, {"client_id": "custom-local", "client_secret": "custom-local-secret", "id_token_signed_response_alg": "ES256", "redirect_uris": ["http://localhost:30000/api/v2/identity/authorize", "http://localhost:30000/api/v2/auth/authorize"]}]'
|
||||
HEXCLAVE_INTEGRATION_CLIENTS_CONFIG='[{"client_id": "neon-local", "client_secret": "neon-local-secret", "id_token_signed_response_alg": "ES256", "redirect_uris": ["http://localhost:30000/api/v2/identity/authorize", "http://localhost:30000/api/v2/auth/authorize"]}, {"client_id": "custom-local", "client_secret": "custom-local-secret", "id_token_signed_response_alg": "ES256", "redirect_uris": ["http://localhost:30000/api/v2/identity/authorize", "http://localhost:30000/api/v2/auth/authorize"]}]'
|
||||
CRON_SECRET=mock_cron_secret
|
||||
STACK_FREESTYLE_API_KEY=mock_stack_freestyle_key
|
||||
STACK_VERCEL_SANDBOX_TOKEN=vercel_sandbox_disabled_for_local_development
|
||||
STACK_OPENAI_API_KEY=mock_openai_api_key
|
||||
STACK_STRIPE_SECRET_KEY=sk_test_mockstripekey
|
||||
STACK_STRIPE_WEBHOOK_SECRET=mock_stripe_webhook_secret
|
||||
STACK_OPENROUTER_API_KEY=FORWARD_TO_PRODUCTION
|
||||
STACK_FEEDBACK_MODE=FORWARD_TO_PRODUCTION
|
||||
STACK_MINTLIFY_MCP_URL=https://stackauth-e0affa27.mintlify.app/mcp
|
||||
HEXCLAVE_FREESTYLE_API_KEY=mock_stack_freestyle_key
|
||||
HEXCLAVE_VERCEL_SANDBOX_TOKEN=vercel_sandbox_disabled_for_local_development
|
||||
HEXCLAVE_OPENAI_API_KEY=mock_openai_api_key
|
||||
HEXCLAVE_STRIPE_SECRET_KEY=sk_test_mockstripekey
|
||||
HEXCLAVE_STRIPE_WEBHOOK_SECRET=mock_stripe_webhook_secret
|
||||
HEXCLAVE_OPENROUTER_API_KEY=FORWARD_TO_PRODUCTION
|
||||
HEXCLAVE_FEEDBACK_MODE=FORWARD_TO_PRODUCTION
|
||||
HEXCLAVE_MINTLIFY_MCP_URL=https://stackauth-e0affa27.mintlify.app/mcp
|
||||
# Email monitor configuration for tests
|
||||
STACK_EMAIL_MONITOR_VERIFICATION_CALLBACK_URL=http://localhost:8101/handler/email-verification
|
||||
STACK_EMAIL_MONITOR_PROJECT_ID=internal
|
||||
STACK_EMAIL_MONITOR_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
STACK_EMAIL_MONITOR_RESEND_EMAIL_DOMAIN=stack-generated.example.com
|
||||
STACK_EMAIL_MONITOR_RESEND_EMAIL_API_KEY=this-is-a-fake-key
|
||||
STACK_EMAIL_MONITOR_INBUCKET_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}05
|
||||
STACK_EMAIL_MONITOR_USE_INBUCKET=true
|
||||
STACK_EMAIL_MONITOR_SECRET_TOKEN=this-secret-token-is-for-local-development-only
|
||||
HEXCLAVE_EMAIL_MONITOR_VERIFICATION_CALLBACK_URL=http://localhost:8101/handler/email-verification
|
||||
HEXCLAVE_EMAIL_MONITOR_PROJECT_ID=internal
|
||||
HEXCLAVE_EMAIL_MONITOR_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
HEXCLAVE_EMAIL_MONITOR_RESEND_EMAIL_DOMAIN=stack-generated.example.com
|
||||
HEXCLAVE_EMAIL_MONITOR_RESEND_EMAIL_API_KEY=this-is-a-fake-key
|
||||
HEXCLAVE_EMAIL_MONITOR_INBUCKET_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}05
|
||||
HEXCLAVE_EMAIL_MONITOR_USE_INBUCKET=true
|
||||
HEXCLAVE_EMAIL_MONITOR_SECRET_TOKEN=this-secret-token-is-for-local-development-only
|
||||
|
||||
STACK_EMAILABLE_API_KEY=
|
||||
HEXCLAVE_EMAILABLE_API_KEY=
|
||||
|
||||
STACK_INTERNAL_FEEDBACK_RECIPIENTS=team@hexclave.com
|
||||
HEXCLAVE_INTERNAL_FEEDBACK_RECIPIENTS=team@hexclave.com
|
||||
|
||||
# S3 Configuration for local development using s3mock
|
||||
STACK_S3_ENDPOINT=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}21
|
||||
STACK_S3_REGION=us-east-1
|
||||
STACK_S3_ACCESS_KEY_ID=s3mockroot
|
||||
STACK_S3_SECRET_ACCESS_KEY=s3mockroot
|
||||
STACK_S3_BUCKET=stack-storage
|
||||
STACK_S3_PRIVATE_BUCKET=stack-storage-private
|
||||
HEXCLAVE_S3_ENDPOINT=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}21
|
||||
HEXCLAVE_S3_REGION=us-east-1
|
||||
HEXCLAVE_S3_ACCESS_KEY_ID=s3mockroot
|
||||
HEXCLAVE_S3_SECRET_ACCESS_KEY=s3mockroot
|
||||
HEXCLAVE_S3_BUCKET=stack-storage
|
||||
HEXCLAVE_S3_PRIVATE_BUCKET=stack-storage-private
|
||||
|
||||
# AWS region defaults to LocalStack
|
||||
STACK_AWS_REGION=us-east-1
|
||||
STACK_AWS_KMS_ENDPOINT=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}24
|
||||
STACK_AWS_ACCESS_KEY_ID=test
|
||||
STACK_AWS_SECRET_ACCESS_KEY=test
|
||||
HEXCLAVE_AWS_REGION=us-east-1
|
||||
HEXCLAVE_AWS_KMS_ENDPOINT=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}24
|
||||
HEXCLAVE_AWS_ACCESS_KEY_ID=test
|
||||
HEXCLAVE_AWS_SECRET_ACCESS_KEY=test
|
||||
|
||||
# Upstash defaults to one of the pre-build test users of the local emulator
|
||||
STACK_QSTASH_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}25
|
||||
STACK_QSTASH_TOKEN=eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0=
|
||||
STACK_QSTASH_CURRENT_SIGNING_KEY=sig_7kYjw48mhY7kAjqNGcy6cr29RJ6r
|
||||
STACK_QSTASH_NEXT_SIGNING_KEY=sig_5ZB6DVzB1wjE8S6rZ7eenA8Pdnhs
|
||||
HEXCLAVE_QSTASH_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}25
|
||||
HEXCLAVE_QSTASH_TOKEN=eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0=
|
||||
HEXCLAVE_QSTASH_CURRENT_SIGNING_KEY=sig_7kYjw48mhY7kAjqNGcy6cr29RJ6r
|
||||
HEXCLAVE_QSTASH_NEXT_SIGNING_KEY=sig_5ZB6DVzB1wjE8S6rZ7eenA8Pdnhs
|
||||
|
||||
# MCP review tool (SpacetimeDB)
|
||||
STACK_SPACETIMEDB_URI=ws://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}39
|
||||
STACK_SPACETIMEDB_DB_NAME=stack-auth-llm
|
||||
STACK_MCP_LOG_TOKEN=change-me
|
||||
HEXCLAVE_SPACETIMEDB_URI=ws://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}39
|
||||
HEXCLAVE_SPACETIMEDB_DB_NAME=stack-auth-llm
|
||||
HEXCLAVE_MCP_LOG_TOKEN=change-me
|
||||
|
||||
# Clickhouse
|
||||
STACK_CLICKHOUSE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}36
|
||||
STACK_CLICKHOUSE_ADMIN_USER=stackframe
|
||||
STACK_CLICKHOUSE_ADMIN_PASSWORD=PASSWORD-PLACEHOLDER--9gKyMxJeMx
|
||||
STACK_CLICKHOUSE_EXTERNAL_PASSWORD=PASSWORD-PLACEHOLDER--EZeHscBMzE
|
||||
HEXCLAVE_CLICKHOUSE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}36
|
||||
HEXCLAVE_CLICKHOUSE_ADMIN_USER=stackframe
|
||||
HEXCLAVE_CLICKHOUSE_ADMIN_PASSWORD=PASSWORD-PLACEHOLDER--9gKyMxJeMx
|
||||
HEXCLAVE_CLICKHOUSE_EXTERNAL_PASSWORD=PASSWORD-PLACEHOLDER--EZeHscBMzE
|
||||
|
||||
# Managed emails
|
||||
STACK_RESEND_API_KEY=mock_resend_api_key
|
||||
STACK_RESEND_WEBHOOK_SECRET=mock_resend_webhook_secret
|
||||
STACK_DNSIMPLE_API_TOKEN=mock_dnsimple_api_token
|
||||
STACK_DNSIMPLE_ACCOUNT_ID=mock_dnsimple_account_id
|
||||
STACK_DNSIMPLE_API_BASE_URL=https://api.dnsimple.com/v2
|
||||
HEXCLAVE_RESEND_API_KEY=mock_resend_api_key
|
||||
HEXCLAVE_RESEND_WEBHOOK_SECRET=mock_resend_webhook_secret
|
||||
HEXCLAVE_DNSIMPLE_API_TOKEN=mock_dnsimple_api_token
|
||||
HEXCLAVE_DNSIMPLE_ACCOUNT_ID=mock_dnsimple_account_id
|
||||
HEXCLAVE_DNSIMPLE_API_BASE_URL=https://api.dnsimple.com/v2
|
||||
|
||||
@ -19,15 +19,15 @@
|
||||
"build-self-host-migration-script": "tsdown --config scripts/db-migrations.tsdown.config.ts",
|
||||
"analyze-bundle": "next experimental-analyze",
|
||||
"start": "next start --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02",
|
||||
"codegen-prisma": "STACK_DATABASE_CONNECTION_STRING=\"${STACK_DATABASE_CONNECTION_STRING:-placeholder-database-connection-string}\" pnpm run prisma generate",
|
||||
"codegen-prisma:watch": "STACK_DATABASE_CONNECTION_STRING=\"${STACK_DATABASE_CONNECTION_STRING:-placeholder-database-connection-string}\" pnpm run prisma generate --watch",
|
||||
"codegen-prisma": "HEXCLAVE_DATABASE_CONNECTION_STRING=\"${HEXCLAVE_DATABASE_CONNECTION_STRING:-${STACK_DATABASE_CONNECTION_STRING:-placeholder-database-connection-string}}\" pnpm run prisma generate",
|
||||
"codegen-prisma:watch": "HEXCLAVE_DATABASE_CONNECTION_STRING=\"${HEXCLAVE_DATABASE_CONNECTION_STRING:-${STACK_DATABASE_CONNECTION_STRING:-placeholder-database-connection-string}}\" pnpm run prisma generate --watch",
|
||||
"generate-private-sign-up-risk-engine": "pnpm run with-env tsx scripts/generate-private-sign-up-risk-engine.ts",
|
||||
"generate-private-sign-up-risk-engine:watch": "chokidar 'src/private/src/sign-up-risk-engine.ts' -c 'pnpm run generate-private-sign-up-risk-engine'",
|
||||
"codegen-route-info": "pnpm run with-env tsx scripts/generate-route-info.ts",
|
||||
"codegen-route-info:watch": "pnpm run with-env tsx watch --clear-screen=false scripts/generate-route-info.ts",
|
||||
"codegen": "pnpm run with-env pnpm run generate-migration-imports && pnpm run with-env bash -c 'if [ \"$STACK_ACCELERATE_ENABLED\" = \"true\" ]; then pnpm run prisma generate --no-engine; else pnpm run codegen-prisma; fi' && pnpm run generate-private-sign-up-risk-engine && pnpm run codegen-docs && pnpm run codegen-route-info",
|
||||
"codegen:watch": "pnpm run generate-private-sign-up-risk-engine && concurrently -n \"prisma,private-risk-engine,docs,route-info,migration-imports\" -k \"pnpm run codegen-prisma:watch\" \"pnpm run generate-private-sign-up-risk-engine:watch\" \"pnpm run codegen-docs:watch\" \"pnpm run codegen-route-info:watch\" \"pnpm run generate-migration-imports:watch\"",
|
||||
"psql-inner": "psql $(echo $STACK_DATABASE_CONNECTION_STRING | sed 's/\\?.*$//')",
|
||||
"psql-inner": "psql $(echo ${HEXCLAVE_DATABASE_CONNECTION_STRING:-$STACK_DATABASE_CONNECTION_STRING} | sed 's/\\?.*$//')",
|
||||
"clickhouse": "pnpm run with-env clickhouse-client --host localhost --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}37 --user stackframe --password PASSWORD-PLACEHOLDER--9gKyMxJeMx",
|
||||
"psql": "pnpm run with-env:dev pnpm run psql-inner",
|
||||
"prisma-studio": "pnpm run with-env:dev prisma studio --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}06 --browser none",
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
import 'dotenv/config'
|
||||
import { defineConfig, env } from 'prisma/config'
|
||||
|
||||
function getDatabaseConnectionStringEnvVarName() {
|
||||
const hexclaveValue = process.env.HEXCLAVE_DATABASE_CONNECTION_STRING;
|
||||
const stackValue = process.env.STACK_DATABASE_CONNECTION_STRING;
|
||||
if (hexclaveValue && stackValue && hexclaveValue !== stackValue) {
|
||||
throw new Error("Environment variables HEXCLAVE_DATABASE_CONNECTION_STRING and STACK_DATABASE_CONNECTION_STRING are both set to different values. Remove one of them or set them to the same value.");
|
||||
}
|
||||
return hexclaveValue ? 'HEXCLAVE_DATABASE_CONNECTION_STRING' : 'STACK_DATABASE_CONNECTION_STRING';
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
schema: 'prisma/schema.prisma',
|
||||
migrations: {
|
||||
@ -8,7 +17,10 @@ export default defineConfig({
|
||||
seed: 'pnpm run db-seed-script',
|
||||
},
|
||||
datasource: {
|
||||
url: env('STACK_DATABASE_CONNECTION_STRING'),
|
||||
// Hexclave rebrand: prefer the canonical name, fall back to the legacy one
|
||||
// (empty counts as unset — the checked-in .env templates define empty placeholders).
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
url: env(getDatabaseConnectionStringEnvVarName()),
|
||||
},
|
||||
experimental: {
|
||||
externalTables: true,
|
||||
@ -24,4 +36,3 @@ export default defineConfig({
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ import { DEFAULT_EMAIL_THEME_ID } from '@hexclave/shared/dist/helpers/emails';
|
||||
import { AdminUserProjectsCrud } from '@hexclave/shared/dist/interface/crud/projects';
|
||||
import { ITEM_IDS, PLAN_LIMITS } from '@hexclave/shared/dist/plans';
|
||||
import { DayInterval } from '@hexclave/shared/dist/utils/dates';
|
||||
import { getEnvVariable } from '@hexclave/shared/dist/utils/env';
|
||||
import { throwErr } from '@hexclave/shared/dist/utils/errors';
|
||||
import { typedEntries, typedFromEntries } from '@hexclave/shared/dist/utils/objects';
|
||||
|
||||
@ -56,21 +57,22 @@ function enableSeedLogTimestamps() {
|
||||
|
||||
export async function seed() {
|
||||
enableSeedLogTimestamps();
|
||||
process.env.STACK_SEED_MODE = 'true';
|
||||
process.env.HEXCLAVE_SEED_MODE = 'true';
|
||||
console.log('Seeding database...');
|
||||
|
||||
// Optional default admin user
|
||||
const adminEmail = process.env.STACK_SEED_INTERNAL_PROJECT_USER_EMAIL;
|
||||
const adminPassword = process.env.STACK_SEED_INTERNAL_PROJECT_USER_PASSWORD;
|
||||
const adminInternalAccess = process.env.STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS === 'true';
|
||||
const adminGithubId = process.env.STACK_SEED_INTERNAL_PROJECT_USER_GITHUB_ID;
|
||||
const adminEmail = getEnvVariable("STACK_SEED_INTERNAL_PROJECT_USER_EMAIL", "");
|
||||
const adminPassword = getEnvVariable("STACK_SEED_INTERNAL_PROJECT_USER_PASSWORD", "");
|
||||
const adminInternalAccess = getEnvVariable("STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS", "") === 'true';
|
||||
const adminGithubId = getEnvVariable("STACK_SEED_INTERNAL_PROJECT_USER_GITHUB_ID", "");
|
||||
|
||||
// dashboard settings
|
||||
const dashboardDomain = process.env.NEXT_PUBLIC_STACK_DASHBOARD_URL;
|
||||
const oauthProviderIds = process.env.STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS?.split(',') ?? [];
|
||||
const otpEnabled = process.env.STACK_SEED_INTERNAL_PROJECT_OTP_ENABLED === 'true';
|
||||
const signUpEnabled = process.env.STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED === 'true';
|
||||
const allowLocalhost = process.env.STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST === 'true';
|
||||
const dashboardDomain = getEnvVariable("NEXT_PUBLIC_STACK_DASHBOARD_URL", "");
|
||||
const rawOauthProviderIds = getEnvVariable("STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS", "");
|
||||
const oauthProviderIds = rawOauthProviderIds ? rawOauthProviderIds.split(',') : [];
|
||||
const otpEnabled = getEnvVariable("STACK_SEED_INTERNAL_PROJECT_OTP_ENABLED", "") === 'true';
|
||||
const signUpEnabled = getEnvVariable("STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED", "") === 'true';
|
||||
const allowLocalhost = getEnvVariable("STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST", "") === 'true';
|
||||
|
||||
const localEmulatorEnabled = isLocalEmulatorEnabled();
|
||||
|
||||
@ -366,22 +368,22 @@ export async function seed() {
|
||||
// seed, email/svix, clickhouse). The emulator CLI authenticates against the
|
||||
// internal project using the pck stored here, so it must land before the rest
|
||||
// of the seed even if something later fails.
|
||||
const isLocalEmulator = process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR === 'true';
|
||||
const rawPck = process.env.STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY;
|
||||
const isLocalEmulator = getEnvVariable("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR", "") === 'true';
|
||||
const rawPck = getEnvVariable("STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY", "");
|
||||
if (isLocalEmulator && !rawPck) {
|
||||
// Emulator images build before a per-VM pck is available. Runtime boots set
|
||||
// STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY from the VM-generated
|
||||
// HEXCLAVE_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY from the VM-generated
|
||||
// random value and re-run the seed, which upserts the internal key set then.
|
||||
console.log('Skipping internal API key set (no pck provided; emulator mode).');
|
||||
} else {
|
||||
const keySet = {
|
||||
publishableClientKey: rawPck || throwErr('STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY is not set'),
|
||||
publishableClientKey: rawPck || throwErr('HEXCLAVE_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY is not set'),
|
||||
secretServerKey: isLocalEmulator
|
||||
? (process.env.STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY ?? null)
|
||||
: (process.env.STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY || throwErr('STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY is not set')),
|
||||
? (getEnvVariable("STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY", "") || null)
|
||||
: (getEnvVariable("STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY", "") || throwErr('HEXCLAVE_INTERNAL_PROJECT_SECRET_SERVER_KEY is not set')),
|
||||
superSecretAdminKey: isLocalEmulator
|
||||
? (process.env.STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY ?? null)
|
||||
: (process.env.STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY is not set')),
|
||||
? (getEnvVariable("STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY", "") || null)
|
||||
: (getEnvVariable("STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY", "") || throwErr('HEXCLAVE_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY is not set')),
|
||||
};
|
||||
|
||||
await globalPrismaClient.apiKeySet.upsert({
|
||||
@ -401,7 +403,7 @@ export async function seed() {
|
||||
console.log('Updated internal API key set');
|
||||
}
|
||||
|
||||
const shouldSeedDummyProject = process.env.STACK_SEED_ENABLE_DUMMY_PROJECT === 'true';
|
||||
const shouldSeedDummyProject = getEnvVariable("STACK_SEED_ENABLE_DUMMY_PROJECT", "") === 'true';
|
||||
if (shouldSeedDummyProject) {
|
||||
await seedDummyProject({
|
||||
projectId: DUMMY_PROJECT_ID,
|
||||
|
||||
@ -45,7 +45,7 @@ export function isSharedAccessTokenBlocked(providerIsShared: boolean): boolean {
|
||||
import.meta.vitest?.describe("isSharedAccessTokenBlocked", () => {
|
||||
const { test, expect, beforeEach, afterEach, vi } = import.meta.vitest!;
|
||||
beforeEach(() => {
|
||||
vi.stubEnv("STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS", "");
|
||||
vi.stubEnv("HEXCLAVE_ALLOW_SHARED_OAUTH_ACCESS_TOKENS", "");
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
@ -53,7 +53,7 @@ import.meta.vitest?.describe("isSharedAccessTokenBlocked", () => {
|
||||
|
||||
test("non-shared provider is never blocked, regardless of env var", () => {
|
||||
expect(isSharedAccessTokenBlocked(false)).toBe(false);
|
||||
vi.stubEnv("STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS", "true");
|
||||
vi.stubEnv("HEXCLAVE_ALLOW_SHARED_OAUTH_ACCESS_TOKENS", "true");
|
||||
expect(isSharedAccessTokenBlocked(false)).toBe(false);
|
||||
});
|
||||
|
||||
@ -63,22 +63,22 @@ import.meta.vitest?.describe("isSharedAccessTokenBlocked", () => {
|
||||
|
||||
test("shared provider is blocked for any value other than the literal 'true'", () => {
|
||||
for (const v of ["false", "1", "TRUE", "yes", " true "]) {
|
||||
vi.stubEnv("STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS", v);
|
||||
vi.stubEnv("HEXCLAVE_ALLOW_SHARED_OAUTH_ACCESS_TOKENS", v);
|
||||
expect(isSharedAccessTokenBlocked(true)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("shared provider is allowed only when env var === 'true'", () => {
|
||||
vi.stubEnv("STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS", "true");
|
||||
vi.stubEnv("HEXCLAVE_ALLOW_SHARED_OAUTH_ACCESS_TOKENS", "true");
|
||||
expect(isSharedAccessTokenBlocked(true)).toBe(false);
|
||||
});
|
||||
|
||||
test("result does not depend on NODE_ENV", () => {
|
||||
for (const nodeEnv of ["production", "development", "test", "preview", ""]) {
|
||||
vi.stubEnv("NODE_ENV", nodeEnv);
|
||||
vi.stubEnv("STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS", "");
|
||||
vi.stubEnv("HEXCLAVE_ALLOW_SHARED_OAUTH_ACCESS_TOKENS", "");
|
||||
expect(isSharedAccessTokenBlocked(true)).toBe(true);
|
||||
vi.stubEnv("STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS", "true");
|
||||
vi.stubEnv("HEXCLAVE_ALLOW_SHARED_OAUTH_ACCESS_TOKENS", "true");
|
||||
expect(isSharedAccessTokenBlocked(true)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
@ -6,11 +6,25 @@ import { applyMigrations, runMigrationNeeded } from "./index";
|
||||
|
||||
const TEST_DB_PREFIX = 'stack_auth_test_db';
|
||||
|
||||
const getDatabaseConnectionString = (): string => {
|
||||
// @ts-ignore - ImportMeta.env is provided by Vite
|
||||
const hexclaveValue: string | undefined = import.meta.env.HEXCLAVE_DATABASE_CONNECTION_STRING;
|
||||
// @ts-ignore - ImportMeta.env is provided by Vite
|
||||
const stackValue: string | undefined = import.meta.env.STACK_DATABASE_CONNECTION_STRING;
|
||||
if (hexclaveValue && stackValue && hexclaveValue !== stackValue) {
|
||||
throw new Error("Environment variables HEXCLAVE_DATABASE_CONNECTION_STRING and STACK_DATABASE_CONNECTION_STRING are both set to different values. Remove one of them or set them to the same value.");
|
||||
}
|
||||
const value = hexclaveValue || stackValue;
|
||||
if (!value) {
|
||||
throw new Error("Missing environment variable HEXCLAVE_DATABASE_CONNECTION_STRING or STACK_DATABASE_CONNECTION_STRING.");
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const getTestDbURL = (testDbName: string) => {
|
||||
// @ts-ignore - ImportMeta.env is provided by Vite
|
||||
const base = import.meta.env.STACK_DATABASE_CONNECTION_STRING.replace(/\/[^/]*$/, '');
|
||||
// @ts-ignore - ImportMeta.env is provided by Vite
|
||||
const query = import.meta.env.STACK_DATABASE_CONNECTION_STRING.split('?')[1] ?? '';
|
||||
const connString = getDatabaseConnectionString();
|
||||
const base = connString.replace(/\/[^/]*$/, '');
|
||||
const query = connString.split('?')[1] ?? '';
|
||||
return {
|
||||
full: `${base}/${testDbName}`,
|
||||
base,
|
||||
|
||||
@ -12,9 +12,23 @@ const MIGRATIONS_DIR = path.resolve(__dirname, '../../prisma/migrations');
|
||||
|
||||
const TEST_DB_PREFIX = 'stack_migration_test';
|
||||
|
||||
const getTestDbURL = (testDbName: string) => {
|
||||
const getDatabaseConnectionString = (): string => {
|
||||
// @ts-ignore - ImportMeta.env is provided by Vite
|
||||
const connString: string = import.meta.env.STACK_DATABASE_CONNECTION_STRING;
|
||||
const hexclaveValue: string | undefined = import.meta.env.HEXCLAVE_DATABASE_CONNECTION_STRING;
|
||||
// @ts-ignore - ImportMeta.env is provided by Vite
|
||||
const stackValue: string | undefined = import.meta.env.STACK_DATABASE_CONNECTION_STRING;
|
||||
if (hexclaveValue && stackValue && hexclaveValue !== stackValue) {
|
||||
throw new Error("Environment variables HEXCLAVE_DATABASE_CONNECTION_STRING and STACK_DATABASE_CONNECTION_STRING are both set to different values. Remove one of them or set them to the same value.");
|
||||
}
|
||||
const value = hexclaveValue || stackValue;
|
||||
if (!value) {
|
||||
throw new Error("Missing environment variable HEXCLAVE_DATABASE_CONNECTION_STRING or STACK_DATABASE_CONNECTION_STRING.");
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const getTestDbURL = (testDbName: string) => {
|
||||
const connString = getDatabaseConnectionString();
|
||||
const base = connString.replace(/\/[^/]*(\?.*)?$/, '');
|
||||
const query = connString.split('?')[1] ?? '';
|
||||
return { full: `${base}/${testDbName}`, base, query };
|
||||
|
||||
@ -2,6 +2,7 @@ import { stringCompare } from "@hexclave/shared/dist/utils/strings";
|
||||
import postgres from "postgres";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
||||
import type { Table } from "./index";
|
||||
import { resolveTestDatabaseConnectionString } from "./test-db-env";
|
||||
import type { RowData } from "./utilities";
|
||||
import {
|
||||
createBulldozerExecutionContext,
|
||||
@ -57,11 +58,7 @@ type TestDb = { full: string, base: string };
|
||||
const TEST_DB_PREFIX = "stack_bulldozer_db_fuzz_test";
|
||||
|
||||
function getTestDbUrls(): TestDb {
|
||||
const env = Reflect.get(import.meta, "env");
|
||||
const connectionString = Reflect.get(env, "STACK_DATABASE_CONNECTION_STRING");
|
||||
if (typeof connectionString !== "string" || connectionString.length === 0) {
|
||||
throw new Error("Missing STACK_DATABASE_CONNECTION_STRING");
|
||||
}
|
||||
const connectionString = resolveTestDatabaseConnectionString();
|
||||
const base = connectionString.replace(/\/[^/]*(\?.*)?$/, "");
|
||||
const query = connectionString.split("?")[1] ?? "";
|
||||
const dbName = `${TEST_DB_PREFIX}_${Math.random().toString(16).slice(2, 12)}`;
|
||||
|
||||
@ -2,6 +2,7 @@ import { stringCompare } from "@hexclave/shared/dist/utils/strings";
|
||||
import postgres from "postgres";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { Table } from "./index";
|
||||
import { resolveTestDatabaseConnectionString } from "./test-db-env";
|
||||
import type { RowData } from "./utilities";
|
||||
import {
|
||||
createBulldozerExecutionContext,
|
||||
@ -112,11 +113,7 @@ const LOAD_REDUCE_TABLE_INIT_MAX_MS = withCiPerfHeadroom(90_000);
|
||||
const LOAD_REDUCE_TABLE_COUNT_QUERY_MAX_MS = withCiPerfHeadroom(8_000);
|
||||
|
||||
function getTestDbUrls(): TestDb {
|
||||
const env = Reflect.get(import.meta, "env");
|
||||
const connectionString = Reflect.get(env, "STACK_DATABASE_CONNECTION_STRING");
|
||||
if (typeof connectionString !== "string" || connectionString.length === 0) {
|
||||
throw new Error("Missing STACK_DATABASE_CONNECTION_STRING");
|
||||
}
|
||||
const connectionString = resolveTestDatabaseConnectionString();
|
||||
const base = connectionString.replace(/\/[^/]*(\?.*)?$/, "");
|
||||
const query = connectionString.split("?")[1] ?? "";
|
||||
const dbName = `${TEST_DB_PREFIX}_${Math.random().toString(16).slice(2, 12)}`;
|
||||
|
||||
@ -2,6 +2,7 @@ import { stringCompare, templateIdentity } from "@hexclave/shared/dist/utils/str
|
||||
import postgres from "postgres";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, test } from "vitest";
|
||||
import type { Table } from "./index";
|
||||
import { resolveTestDatabaseConnectionString } from "./test-db-env";
|
||||
import {
|
||||
createBulldozerExecutionContext,
|
||||
declareCompactTable,
|
||||
@ -26,11 +27,7 @@ type TestDb = { full: string, base: string };
|
||||
const TEST_DB_PREFIX = "stack_bulldozer_db_test";
|
||||
|
||||
function getTestDbUrls(): TestDb {
|
||||
const env = Reflect.get(import.meta, "env");
|
||||
const connectionString = Reflect.get(env, "STACK_DATABASE_CONNECTION_STRING");
|
||||
if (typeof connectionString !== "string" || connectionString.length === 0) {
|
||||
throw new Error("Missing STACK_DATABASE_CONNECTION_STRING");
|
||||
}
|
||||
const connectionString = resolveTestDatabaseConnectionString();
|
||||
const base = connectionString.replace(/\/[^/]*(\?.*)?$/, "");
|
||||
const query = connectionString.split("?")[1] ?? "";
|
||||
const dbName = `${TEST_DB_PREFIX}_${Math.random().toString(16).slice(2, 12)}`;
|
||||
|
||||
24
apps/backend/src/lib/bulldozer/db/test-db-env.ts
Normal file
24
apps/backend/src/lib/bulldozer/db/test-db-env.ts
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Resolve the test database connection string from the environment, preferring
|
||||
* the canonical `HEXCLAVE_DATABASE_CONNECTION_STRING` and falling back to the
|
||||
* legacy `STACK_DATABASE_CONNECTION_STRING`. Empty counts as unset. Throws when
|
||||
* both names are set to different non-empty values, or when neither is set.
|
||||
*
|
||||
* Shared by the bulldozer/payments DB-backed vitest suites so the dual-read
|
||||
* stays consistent with the rest of the Hexclave rebrand.
|
||||
*/
|
||||
export function resolveTestDatabaseConnectionString(): string {
|
||||
const env = Reflect.get(import.meta, "env");
|
||||
const hexclaveRaw = Reflect.get(env, "HEXCLAVE_DATABASE_CONNECTION_STRING");
|
||||
const stackRaw = Reflect.get(env, "STACK_DATABASE_CONNECTION_STRING");
|
||||
const hexclaveValue = typeof hexclaveRaw === "string" && hexclaveRaw.length > 0 ? hexclaveRaw : undefined;
|
||||
const stackValue = typeof stackRaw === "string" && stackRaw.length > 0 ? stackRaw : undefined;
|
||||
if (hexclaveValue && stackValue && hexclaveValue !== stackValue) {
|
||||
throw new Error("Environment variables HEXCLAVE_DATABASE_CONNECTION_STRING and STACK_DATABASE_CONNECTION_STRING are both set to different values. Remove one of them or set them to the same value.");
|
||||
}
|
||||
const value = hexclaveValue || stackValue;
|
||||
if (!value) {
|
||||
throw new Error("Missing environment variable HEXCLAVE_DATABASE_CONNECTION_STRING or STACK_DATABASE_CONNECTION_STRING.");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@ -22,6 +22,7 @@ import {
|
||||
toQueryableSqlQuery,
|
||||
} from "./index";
|
||||
import { loadProcessQueueFunctionSql } from "./test-sql-loaders";
|
||||
import { resolveTestDatabaseConnectionString } from "./test-db-env";
|
||||
|
||||
type SqlExpression<T> = { type: "expression", sql: string };
|
||||
type SqlStatement = { type: "statement", sql: string, outputName?: string };
|
||||
@ -42,11 +43,7 @@ function predicate(sql: string): SqlPredicate {
|
||||
const TEST_DB_PREFIX = "stack_bulldozer_queue_downstream_test";
|
||||
|
||||
function getTestDbUrls() {
|
||||
const env = Reflect.get(import.meta, "env");
|
||||
const connectionString = Reflect.get(env, "STACK_DATABASE_CONNECTION_STRING");
|
||||
if (typeof connectionString !== "string" || connectionString.length === 0) {
|
||||
throw new Error("Missing STACK_DATABASE_CONNECTION_STRING");
|
||||
}
|
||||
const connectionString = resolveTestDatabaseConnectionString();
|
||||
const base = connectionString.replace(/\/[^/]*(\?.*)?$/, "");
|
||||
const query = connectionString.split("?")[1] ?? "";
|
||||
const dbName = `${TEST_DB_PREFIX}_${Math.random().toString(16).slice(2, 12)}`;
|
||||
|
||||
@ -125,35 +125,59 @@ export async function sendSupportFeedbackEmail(options: {
|
||||
}
|
||||
|
||||
import.meta.vitest?.test("getInternalFeedbackRecipients()", ({ expect }) => {
|
||||
// getEnvVariable resolves HEXCLAVE_INTERNAL_FEEDBACK_RECIPIENTS first and
|
||||
// falls back to the legacy STACK_ name, so clear and restore both.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const previousValue = process.env.STACK_INTERNAL_FEEDBACK_RECIPIENTS;
|
||||
const previousValue = process.env.HEXCLAVE_INTERNAL_FEEDBACK_RECIPIENTS;
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const previousLegacyValue = process.env.STACK_INTERNAL_FEEDBACK_RECIPIENTS;
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
delete process.env.HEXCLAVE_INTERNAL_FEEDBACK_RECIPIENTS;
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
delete process.env.STACK_INTERNAL_FEEDBACK_RECIPIENTS;
|
||||
expect(() => getInternalFeedbackRecipients()).toThrow("Missing environment variable");
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
process.env.STACK_INTERNAL_FEEDBACK_RECIPIENTS = "TEAM@hexclave.com, team@hexclave.com , another@example.com";
|
||||
process.env.HEXCLAVE_INTERNAL_FEEDBACK_RECIPIENTS = "TEAM@hexclave.com, team@hexclave.com , another@example.com";
|
||||
expect(getInternalFeedbackRecipients()).toEqual([
|
||||
"team@hexclave.com",
|
||||
"another@example.com",
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
process.env.STACK_INTERNAL_FEEDBACK_RECIPIENTS = "valid@example.com, ";
|
||||
process.env.HEXCLAVE_INTERNAL_FEEDBACK_RECIPIENTS = "valid@example.com, ";
|
||||
expect(() => getInternalFeedbackRecipients()).toThrow("empty recipient");
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
process.env.STACK_INTERNAL_FEEDBACK_RECIPIENTS = ", ";
|
||||
process.env.HEXCLAVE_INTERNAL_FEEDBACK_RECIPIENTS = ", ";
|
||||
expect(() => getInternalFeedbackRecipients()).toThrow("empty recipient");
|
||||
|
||||
// legacy STACK_ name still resolves when the canonical name is unset or empty
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
delete process.env.HEXCLAVE_INTERNAL_FEEDBACK_RECIPIENTS;
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
process.env.STACK_INTERNAL_FEEDBACK_RECIPIENTS = "legacy@example.com";
|
||||
expect(getInternalFeedbackRecipients()).toEqual(["legacy@example.com"]);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
process.env.HEXCLAVE_INTERNAL_FEEDBACK_RECIPIENTS = "";
|
||||
expect(getInternalFeedbackRecipients()).toEqual(["legacy@example.com"]);
|
||||
} finally {
|
||||
if (previousValue === undefined) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
delete process.env.HEXCLAVE_INTERNAL_FEEDBACK_RECIPIENTS;
|
||||
} else {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
process.env.HEXCLAVE_INTERNAL_FEEDBACK_RECIPIENTS = previousValue;
|
||||
}
|
||||
if (previousLegacyValue === undefined) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
delete process.env.STACK_INTERNAL_FEEDBACK_RECIPIENTS;
|
||||
} else {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
process.env.STACK_INTERNAL_FEEDBACK_RECIPIENTS = previousValue;
|
||||
process.env.STACK_INTERNAL_FEEDBACK_RECIPIENTS = previousLegacyValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -13,17 +13,13 @@ import {
|
||||
toQueryableSqlQuery,
|
||||
} from "@/lib/bulldozer/db/index";
|
||||
import { loadProcessQueueFunctionSql } from "@/lib/bulldozer/db/test-sql-loaders";
|
||||
import { resolveTestDatabaseConnectionString } from "@/lib/bulldozer/db/test-db-env";
|
||||
|
||||
type SqlStatement = { type: "statement", sql: string, outputName?: string };
|
||||
type SqlQuery = { type: "query", sql: string, toStatement(outputName?: string): SqlStatement };
|
||||
|
||||
function getConnectionString(): string {
|
||||
const env = Reflect.get(import.meta, "env");
|
||||
const connectionString = Reflect.get(env, "STACK_DATABASE_CONNECTION_STRING");
|
||||
if (typeof connectionString !== "string" || connectionString.length === 0) {
|
||||
throw new Error("Missing STACK_DATABASE_CONNECTION_STRING");
|
||||
}
|
||||
return connectionString;
|
||||
return resolveTestDatabaseConnectionString();
|
||||
}
|
||||
|
||||
export type CreateTestDbOptions = {
|
||||
|
||||
@ -194,25 +194,25 @@ describe("arePlanLimitsEnforced", () => {
|
||||
});
|
||||
|
||||
it("returns true when env var is unset (default-on enforcement)", () => {
|
||||
vi.stubEnv("STACK_DISABLE_PLAN_LIMITS", "");
|
||||
vi.stubEnv("HEXCLAVE_DISABLE_PLAN_LIMITS", "");
|
||||
expect(arePlanLimitsEnforced()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when env var is exactly 'true'", () => {
|
||||
vi.stubEnv("STACK_DISABLE_PLAN_LIMITS", "true");
|
||||
vi.stubEnv("HEXCLAVE_DISABLE_PLAN_LIMITS", "true");
|
||||
expect(arePlanLimitsEnforced()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when env var is 'false'", () => {
|
||||
vi.stubEnv("STACK_DISABLE_PLAN_LIMITS", "false");
|
||||
vi.stubEnv("HEXCLAVE_DISABLE_PLAN_LIMITS", "false");
|
||||
expect(arePlanLimitsEnforced()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for any non-'true' value (e.g. '1', 'yes', 'TRUE')", () => {
|
||||
// Explicit string match is intentional — we don't want to risk a typo
|
||||
// like STACK_DISABLE_PLAN_LIMITS=trueee silently disabling enforcement.
|
||||
// like HEXCLAVE_DISABLE_PLAN_LIMITS=trueee silently disabling enforcement.
|
||||
for (const value of ["1", "yes", "TRUE", "True", " true", "true ", "trueee"]) {
|
||||
vi.stubEnv("STACK_DISABLE_PLAN_LIMITS", value);
|
||||
vi.stubEnv("HEXCLAVE_DISABLE_PLAN_LIMITS", value);
|
||||
expect(arePlanLimitsEnforced()).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
@ -360,8 +360,12 @@ import.meta.vitest?.describe("verifyTurnstileToken(...)", () => {
|
||||
test("uses development secret key when none is configured", async ({ expect }) => {
|
||||
const processEnv = Reflect.get(process, "env");
|
||||
const originalNodeEnv = Reflect.get(processEnv, "NODE_ENV");
|
||||
const originalKey = Reflect.get(processEnv, "STACK_TURNSTILE_SECRET_KEY");
|
||||
const originalHexclaveKey = Reflect.get(processEnv, "HEXCLAVE_TURNSTILE_SECRET_KEY");
|
||||
const originalStackKey = Reflect.get(processEnv, "STACK_TURNSTILE_SECRET_KEY");
|
||||
Reflect.set(processEnv, "NODE_ENV", "development");
|
||||
// Clear both spellings so the value resolves to the dev-key default; the
|
||||
// canonical HEXCLAVE_ name is set in .env.development and otherwise wins.
|
||||
Reflect.set(processEnv, "HEXCLAVE_TURNSTILE_SECRET_KEY", "");
|
||||
Reflect.set(processEnv, "STACK_TURNSTILE_SECRET_KEY", "");
|
||||
|
||||
let postedSecret = "";
|
||||
@ -380,7 +384,16 @@ import.meta.vitest?.describe("verifyTurnstileToken(...)", () => {
|
||||
.resolves.toEqual({ status: "ok" });
|
||||
} finally {
|
||||
Reflect.set(processEnv, "NODE_ENV", originalNodeEnv);
|
||||
Reflect.set(processEnv, "STACK_TURNSTILE_SECRET_KEY", originalKey);
|
||||
for (const [key, original] of [
|
||||
["HEXCLAVE_TURNSTILE_SECRET_KEY", originalHexclaveKey],
|
||||
["STACK_TURNSTILE_SECRET_KEY", originalStackKey],
|
||||
] as const) {
|
||||
if (original === undefined) {
|
||||
Reflect.deleteProperty(processEnv, key);
|
||||
} else {
|
||||
Reflect.set(processEnv, key, original);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(postedSecret).toBe(turnstileDevelopmentKeys.secretKey);
|
||||
@ -390,25 +403,25 @@ import.meta.vitest?.describe("verifyTurnstileToken(...)", () => {
|
||||
import.meta.vitest?.describe("verifyTurnstileTokenWithOptionalVisibleChallenge(...)", () => {
|
||||
const { vi, test, afterEach, beforeEach } = import.meta.vitest!;
|
||||
const processEnv = Reflect.get(process, "env");
|
||||
const originalFlag = Reflect.get(processEnv, "STACK_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE");
|
||||
const originalDisableFlag = Reflect.get(processEnv, "STACK_DISABLE_BOT_CHALLENGE");
|
||||
const originalFlag = Reflect.get(processEnv, "HEXCLAVE_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE");
|
||||
const originalDisableFlag = Reflect.get(processEnv, "HEXCLAVE_DISABLE_BOT_CHALLENGE");
|
||||
|
||||
beforeEach(() => {
|
||||
Reflect.deleteProperty(processEnv, "STACK_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE");
|
||||
Reflect.deleteProperty(processEnv, "STACK_DISABLE_BOT_CHALLENGE");
|
||||
Reflect.deleteProperty(processEnv, "HEXCLAVE_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE");
|
||||
Reflect.deleteProperty(processEnv, "HEXCLAVE_DISABLE_BOT_CHALLENGE");
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
if (originalFlag === undefined) {
|
||||
Reflect.deleteProperty(processEnv, "STACK_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE");
|
||||
Reflect.deleteProperty(processEnv, "HEXCLAVE_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE");
|
||||
} else {
|
||||
Reflect.set(processEnv, "STACK_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE", originalFlag);
|
||||
Reflect.set(processEnv, "HEXCLAVE_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE", originalFlag);
|
||||
}
|
||||
if (originalDisableFlag === undefined) {
|
||||
Reflect.deleteProperty(processEnv, "STACK_DISABLE_BOT_CHALLENGE");
|
||||
Reflect.deleteProperty(processEnv, "HEXCLAVE_DISABLE_BOT_CHALLENGE");
|
||||
} else {
|
||||
Reflect.set(processEnv, "STACK_DISABLE_BOT_CHALLENGE", originalDisableFlag);
|
||||
Reflect.set(processEnv, "HEXCLAVE_DISABLE_BOT_CHALLENGE", originalDisableFlag);
|
||||
}
|
||||
});
|
||||
|
||||
@ -448,7 +461,7 @@ import.meta.vitest?.describe("verifyTurnstileTokenWithOptionalVisibleChallenge(.
|
||||
});
|
||||
|
||||
test("skips all bot challenge verification when disabled", async ({ expect }) => {
|
||||
Reflect.set(processEnv, "STACK_DISABLE_BOT_CHALLENGE", "true");
|
||||
Reflect.set(processEnv, "HEXCLAVE_DISABLE_BOT_CHALLENGE", "true");
|
||||
const fetchSpy = vi.fn();
|
||||
vi.stubGlobal("fetch", fetchSpy);
|
||||
|
||||
@ -463,7 +476,7 @@ import.meta.vitest?.describe("verifyTurnstileTokenWithOptionalVisibleChallenge(.
|
||||
});
|
||||
|
||||
test("can downgrade visible invalid responses into a scored assessment when bypass is enabled", async ({ expect }) => {
|
||||
Reflect.set(processEnv, "STACK_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE", "true");
|
||||
Reflect.set(processEnv, "HEXCLAVE_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE", "true");
|
||||
vi.stubGlobal("fetch", async () => new Response(JSON.stringify({ success: false }), {
|
||||
status: 200, headers: { "Content-Type": "application/json" },
|
||||
}));
|
||||
|
||||
@ -115,17 +115,17 @@ import.meta.vitest?.test("getDerivedSignUpCountryCode", ({ expect }) => {
|
||||
import.meta.vitest?.describe("visible bot challenge sign-up policy", () => {
|
||||
const { expect, test, beforeEach, afterEach } = import.meta.vitest!;
|
||||
const processEnv = Reflect.get(process, "env");
|
||||
const originalFlag = Reflect.get(processEnv, "STACK_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE");
|
||||
const originalFlag = Reflect.get(processEnv, "HEXCLAVE_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE");
|
||||
|
||||
beforeEach(() => {
|
||||
Reflect.deleteProperty(processEnv, "STACK_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE");
|
||||
Reflect.deleteProperty(processEnv, "HEXCLAVE_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalFlag === undefined) {
|
||||
Reflect.deleteProperty(processEnv, "STACK_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE");
|
||||
Reflect.deleteProperty(processEnv, "HEXCLAVE_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE");
|
||||
} else {
|
||||
Reflect.set(processEnv, "STACK_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE", originalFlag);
|
||||
Reflect.set(processEnv, "HEXCLAVE_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE", originalFlag);
|
||||
}
|
||||
});
|
||||
|
||||
@ -137,7 +137,7 @@ import.meta.vitest?.describe("visible bot challenge sign-up policy", () => {
|
||||
});
|
||||
|
||||
test("allows sign-up when visible challenge failure override is enabled", () => {
|
||||
Reflect.set(processEnv, "STACK_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE", "true");
|
||||
Reflect.set(processEnv, "HEXCLAVE_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE", "true");
|
||||
|
||||
expect(() => assertVisibleBotChallengePassedForSignUp({
|
||||
status: "error",
|
||||
@ -146,7 +146,7 @@ import.meta.vitest?.describe("visible bot challenge sign-up policy", () => {
|
||||
});
|
||||
|
||||
test("treats invalid visible challenges as bypassable failures when the override is enabled", () => {
|
||||
Reflect.set(processEnv, "STACK_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE", "true");
|
||||
Reflect.set(processEnv, "HEXCLAVE_ALLOW_SIGN_UP_ON_VISIBLE_BOT_CHALLENGE_FAILURE", "true");
|
||||
|
||||
expect(() => assertVisibleBotChallengePassedForSignUp({
|
||||
status: "invalid",
|
||||
|
||||
@ -22,7 +22,7 @@ const sentryErrorSink = (location: string, error: unknown, level: "error" | "war
|
||||
|
||||
export function ensurePolyfilled() {
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (key.startsWith("STACK_") || key.startsWith("NEXT_PUBLIC_STACK_")) {
|
||||
if (key.startsWith("STACK_") || key.startsWith("NEXT_PUBLIC_STACK_") || key.startsWith("HEXCLAVE_") || key.startsWith("NEXT_PUBLIC_HEXCLAVE_")) {
|
||||
const replaced = expandHexclavePortPrefix(value ?? undefined);
|
||||
if (replaced !== undefined) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
|
||||
@ -21,6 +21,6 @@ export default mergeConfig(
|
||||
}
|
||||
},
|
||||
envDir: __dirname,
|
||||
envPrefix: 'STACK_',
|
||||
envPrefix: ['HEXCLAVE_', 'STACK_'],
|
||||
})
|
||||
)
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
# Basic
|
||||
NEXT_PUBLIC_STACK_API_URL=# enter your stack endpoint here, For local development: http://localhost:8102 (no trailing slash)
|
||||
NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=# set to true to enable local emulator UI behavior (auto-login + read-only environment config updates)
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=# enter your Stack publishable client key here. For local development, just enter a random string, then run `pnpm db:reset`
|
||||
STACK_SECRET_SERVER_KEY=# enter your Stack secret client key here. For local development, do the same as above
|
||||
NEXT_PUBLIC_STACK_DOCS_BASE_URL=# enter the base URL of the docs here
|
||||
NEXT_PUBLIC_STACK_EXTRA_REQUEST_HEADERS=# a list of extra request headers to add to all Hexclave API requests, as a JSON record
|
||||
NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY=# enter your Stripe publishable key here
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=# enter your stack endpoint here, For local development: http://localhost:8102 (no trailing slash)
|
||||
NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR=# set to true to enable local emulator UI behavior (auto-login + read-only environment config updates)
|
||||
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=# enter your Stack publishable client key here. For local development, just enter a random string, then run `pnpm db:reset`
|
||||
HEXCLAVE_SECRET_SERVER_KEY=# enter your Stack secret client key here. For local development, do the same as above
|
||||
NEXT_PUBLIC_HEXCLAVE_DOCS_BASE_URL=# enter the base URL of the docs here
|
||||
NEXT_PUBLIC_HEXCLAVE_EXTRA_REQUEST_HEADERS=# a list of extra request headers to add to all Hexclave API requests, as a JSON record
|
||||
NEXT_PUBLIC_HEXCLAVE_STRIPE_PUBLISHABLE_KEY=# enter your Stripe publishable key here
|
||||
|
||||
# Webhooks
|
||||
NEXT_PUBLIC_STACK_SVIX_SERVER_URL=# For prod, leave it empty. For local development, use `http://localhost:8113`
|
||||
NEXT_PUBLIC_HEXCLAVE_SVIX_SERVER_URL=# For prod, leave it empty. For local development, use `http://localhost:8113`
|
||||
|
||||
# Misc, optional
|
||||
NEXT_PUBLIC_STACK_HEAD_TAGS='[{ "tagName": "script", "attributes": {}, "innerHTML": "// insert head tags here" }]'
|
||||
STACK_DEVELOPMENT_TRANSLATION_LOCALE=# enter the locale to use for the translation provider here, for example: de-DE. Only works during development, not in production. Optional, by default don't translate
|
||||
NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS='["internal"]'
|
||||
NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR=# set to true to open the debugger on assertion errors (set to true in .env.development)
|
||||
STACK_FEATUREBASE_JWT_SECRET=# used for Featurebase SSO, you probably won't have to set this
|
||||
STACK_CHANGELOG_URL=# Used for raw github link to root changelog.md file.
|
||||
NEXT_PUBLIC_HEXCLAVE_HEAD_TAGS=# a JSON array of head tags to inject, e.g. '[{ "tagName": "script", "attributes": {}, "innerHTML": "..." }]'. Leave empty here so a platform-set legacy NEXT_PUBLIC_STACK_HEAD_TAGS value isn't treated as a conflict in prod builds.
|
||||
HEXCLAVE_DEVELOPMENT_TRANSLATION_LOCALE=# enter the locale to use for the translation provider here, for example: de-DE. Only works during development, not in production. Optional, by default don't translate
|
||||
NEXT_PUBLIC_HEXCLAVE_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS=# JSON array of project IDs that get development features (set to '["internal"]' in .env.development). Leave empty here so a platform-set legacy NEXT_PUBLIC_STACK_* value isn't treated as a conflict in prod builds.
|
||||
NEXT_PUBLIC_HEXCLAVE_DEBUGGER_ON_ASSERTION_ERROR=# set to true to open the debugger on assertion errors (set to true in .env.development)
|
||||
HEXCLAVE_FEATUREBASE_JWT_SECRET=# used for Featurebase SSO, you probably won't have to set this
|
||||
HEXCLAVE_CHANGELOG_URL=# Used for raw github link to root changelog.md file.
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_STACK_DOCS_BASE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}26
|
||||
NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX=.localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09
|
||||
NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=false
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_HEXCLAVE_DOCS_BASE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}26
|
||||
NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_DOMAIN_SUFFIX=.localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09
|
||||
NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR=false
|
||||
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
STACK_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
HEXCLAVE_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
|
||||
NEXT_PUBLIC_STACK_SVIX_SERVER_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}13
|
||||
STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS=50
|
||||
NEXT_PUBLIC_HEXCLAVE_SVIX_SERVER_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}13
|
||||
HEXCLAVE_ARTIFICIAL_DEVELOPMENT_DELAY_MS=50
|
||||
|
||||
NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR=false
|
||||
NEXT_PUBLIC_HEXCLAVE_DEBUGGER_ON_ASSERTION_ERROR=false
|
||||
NEXT_PUBLIC_HEXCLAVE_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS='["internal"]'
|
||||
|
||||
STACK_FEATUREBASE_JWT_SECRET=secret-value
|
||||
HEXCLAVE_FEATUREBASE_JWT_SECRET=secret-value
|
||||
|
||||
@ -51,6 +51,15 @@ const withConfiguredSentryConfig = (nextConfig) =>
|
||||
}
|
||||
);
|
||||
|
||||
function resolveHexclaveStackEnvVar(hexclaveName, stackName) {
|
||||
const hexclaveValue = process.env[hexclaveName];
|
||||
const stackValue = process.env[stackName];
|
||||
if (hexclaveValue && stackValue && hexclaveValue !== stackValue) {
|
||||
throw new Error(`Environment variables ${hexclaveName} and ${stackName} are both set to different values. Remove one of them or set them to the same value.`);
|
||||
}
|
||||
return hexclaveValue || stackValue || undefined;
|
||||
}
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// optionally set output to "standalone" for Docker builds
|
||||
@ -119,7 +128,7 @@ const nextConfig = {
|
||||
},
|
||||
|
||||
async headers() {
|
||||
const isLocalEmulator = process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR === "true";
|
||||
const isLocalEmulator = resolveHexclaveStackEnvVar("NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR", "NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR") === "true";
|
||||
return [
|
||||
{
|
||||
source: "/(.*)",
|
||||
@ -141,7 +150,7 @@ const nextConfig = {
|
||||
key: "X-Content-Type-Options",
|
||||
value: "nosniff",
|
||||
},
|
||||
...process.env.NEXT_PUBLIC_STACK_IS_PREVIEW === "true" ? [] : [{
|
||||
...resolveHexclaveStackEnvVar("NEXT_PUBLIC_HEXCLAVE_IS_PREVIEW", "NEXT_PUBLIC_STACK_IS_PREVIEW") === "true" ? [] : [{
|
||||
key: "X-Frame-Options",
|
||||
value: "SAMEORIGIN",
|
||||
}],
|
||||
|
||||
54
apps/dashboard/src/lib/env.test.tsx
Normal file
54
apps/dashboard/src/lib/env.test.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
async function loadEnvModule() {
|
||||
vi.resetModules();
|
||||
return await import("./env");
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
describe("dashboard public env var dual-read", () => {
|
||||
it("falls back to the legacy Stack name when the Hexclave value is empty", async () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_API_URL", "");
|
||||
vi.stubEnv("NEXT_PUBLIC_STACK_API_URL", "https://stack.example.test");
|
||||
|
||||
const { getPublicEnvVar } = await loadEnvModule();
|
||||
|
||||
expect(getPublicEnvVar("NEXT_PUBLIC_STACK_API_URL")).toBe("https://stack.example.test");
|
||||
});
|
||||
|
||||
it("allows both names when they have the same non-empty value", async () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_API_URL", "https://api.example.test");
|
||||
vi.stubEnv("NEXT_PUBLIC_STACK_API_URL", "https://api.example.test");
|
||||
|
||||
const { getPublicEnvVar } = await loadEnvModule();
|
||||
|
||||
expect(getPublicEnvVar("NEXT_PUBLIC_STACK_API_URL")).toBe("https://api.example.test");
|
||||
});
|
||||
|
||||
it("throws when both names are non-empty and different", async () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_API_URL", "https://hexclave.example.test");
|
||||
vi.stubEnv("NEXT_PUBLIC_STACK_API_URL", "https://stack.example.test");
|
||||
|
||||
await expect(loadEnvModule()).rejects.toThrow(/NEXT_PUBLIC_HEXCLAVE_API_URL.*NEXT_PUBLIC_STACK_API_URL.*different values/);
|
||||
});
|
||||
|
||||
it("does not treat unreplaced post-build sentinels as a conflict", async () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_API_URL", "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_HEXCLAVE_API_URL");
|
||||
vi.stubEnv("NEXT_PUBLIC_STACK_API_URL", "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_STACK_API_URL");
|
||||
|
||||
await expect(loadEnvModule()).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("prefers a real value over a sentinel value", async () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_API_URL", "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_HEXCLAVE_API_URL");
|
||||
vi.stubEnv("NEXT_PUBLIC_STACK_API_URL", "https://stack.example.test");
|
||||
|
||||
const { getPublicEnvVar } = await loadEnvModule();
|
||||
|
||||
expect(getPublicEnvVar("NEXT_PUBLIC_STACK_API_URL")).toBe("https://stack.example.test");
|
||||
});
|
||||
});
|
||||
@ -8,40 +8,63 @@ export function expandHexclavePortPrefix(value?: string | null) {
|
||||
return prefix ? value.replace(/\$\{NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81\}/g, prefix as string) : value;
|
||||
}
|
||||
|
||||
function isPostBuildEnvVarSentinel(value: string): boolean {
|
||||
return value.startsWith("STACK_ENV_VAR_SENTINEL");
|
||||
}
|
||||
|
||||
function resolveInlineRenamedEnvVar(hexclaveName: string, stackName: string, hexclaveValue: string | undefined, stackValue: string | undefined): string | undefined {
|
||||
const usableHexclaveValue = hexclaveValue && !isPostBuildEnvVarSentinel(hexclaveValue) ? hexclaveValue : undefined;
|
||||
const usableStackValue = stackValue && !isPostBuildEnvVarSentinel(stackValue) ? stackValue : undefined;
|
||||
if (
|
||||
usableHexclaveValue
|
||||
&& usableStackValue
|
||||
&& usableHexclaveValue !== usableStackValue
|
||||
) {
|
||||
throw new Error(`Environment variables ${hexclaveName} and ${stackName} are both set to different values. Remove one of them or set them to the same value.`);
|
||||
}
|
||||
// Prefer a real (non-sentinel) Hexclave value, then a real Stack value. When
|
||||
// neither is "real", fall back to the raw value with `??` so an empty-string
|
||||
// placeholder or an unreplaced sentinel is passed through unchanged (matching
|
||||
// the pre-rename `process.env.X ?? process.env.Y` inline behavior) instead of
|
||||
// collapsing to `undefined` — downstream readers use `?? throwErr(...)`, which
|
||||
// treats `undefined` (but not `""`) as "missing" and would crash the build.
|
||||
return usableHexclaveValue || usableStackValue || (hexclaveValue ?? stackValue);
|
||||
}
|
||||
|
||||
// Hexclave rebrand: each entry prefers the NEXT_PUBLIC_HEXCLAVE_* literal, falling back
|
||||
// to the legacy NEXT_PUBLIC_*STACK_* literal. Both operands must stay literal
|
||||
// `process.env.NEXT_PUBLIC_*` references so Next.js can inline them at build time.
|
||||
const _inlineEnvVars = {
|
||||
NEXT_PUBLIC_STACK_API_URL: process.env.NEXT_PUBLIC_HEXCLAVE_API_URL ?? process.env.NEXT_PUBLIC_STACK_API_URL,
|
||||
NEXT_PUBLIC_STACK_DASHBOARD_URL: process.env.NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL ?? process.env.NEXT_PUBLIC_STACK_DASHBOARD_URL,
|
||||
NEXT_PUBLIC_STACK_SVIX_SERVER_URL: process.env.NEXT_PUBLIC_HEXCLAVE_SVIX_SERVER_URL ?? process.env.NEXT_PUBLIC_STACK_SVIX_SERVER_URL,
|
||||
NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR: process.env.NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR ?? process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR,
|
||||
NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT: process.env.NEXT_PUBLIC_HEXCLAVE_IS_REMOTE_DEVELOPMENT_ENVIRONMENT ?? process.env.NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT,
|
||||
NEXT_PUBLIC_STACK_IS_PREVIEW: process.env.NEXT_PUBLIC_HEXCLAVE_IS_PREVIEW ?? process.env.NEXT_PUBLIC_STACK_IS_PREVIEW,
|
||||
NEXT_PUBLIC_STACK_API_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_API_URL", "NEXT_PUBLIC_STACK_API_URL", process.env.NEXT_PUBLIC_HEXCLAVE_API_URL, process.env.NEXT_PUBLIC_STACK_API_URL),
|
||||
NEXT_PUBLIC_STACK_DASHBOARD_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL", "NEXT_PUBLIC_STACK_DASHBOARD_URL", process.env.NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL, process.env.NEXT_PUBLIC_STACK_DASHBOARD_URL),
|
||||
NEXT_PUBLIC_STACK_SVIX_SERVER_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_SVIX_SERVER_URL", "NEXT_PUBLIC_STACK_SVIX_SERVER_URL", process.env.NEXT_PUBLIC_HEXCLAVE_SVIX_SERVER_URL, process.env.NEXT_PUBLIC_STACK_SVIX_SERVER_URL),
|
||||
NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR", "NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR", process.env.NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR, process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR),
|
||||
NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_IS_REMOTE_DEVELOPMENT_ENVIRONMENT", "NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT", process.env.NEXT_PUBLIC_HEXCLAVE_IS_REMOTE_DEVELOPMENT_ENVIRONMENT, process.env.NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT),
|
||||
NEXT_PUBLIC_STACK_IS_PREVIEW: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_IS_PREVIEW", "NEXT_PUBLIC_STACK_IS_PREVIEW", process.env.NEXT_PUBLIC_HEXCLAVE_IS_PREVIEW, process.env.NEXT_PUBLIC_STACK_IS_PREVIEW),
|
||||
NEXT_PUBLIC_HEXCLAVE_LOCAL_DASHBOARD_PORT: process.env.NEXT_PUBLIC_HEXCLAVE_LOCAL_DASHBOARD_PORT,
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID: process.env.NEXT_PUBLIC_HEXCLAVE_PROJECT_ID ?? process.env.NEXT_PUBLIC_STACK_PROJECT_ID,
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY: process.env.NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY ?? process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
NEXT_PUBLIC_STACK_URL: process.env.NEXT_PUBLIC_HEXCLAVE_URL ?? process.env.NEXT_PUBLIC_STACK_URL,
|
||||
NEXT_PUBLIC_STACK_INBUCKET_WEB_URL: process.env.NEXT_PUBLIC_HEXCLAVE_INBUCKET_WEB_URL ?? process.env.NEXT_PUBLIC_STACK_INBUCKET_WEB_URL,
|
||||
NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS: process.env.NEXT_PUBLIC_HEXCLAVE_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS ?? process.env.NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS,
|
||||
NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_HEXCLAVE_STRIPE_PUBLISHABLE_KEY ?? process.env.NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY,
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_PROJECT_ID", "NEXT_PUBLIC_STACK_PROJECT_ID", process.env.NEXT_PUBLIC_HEXCLAVE_PROJECT_ID, process.env.NEXT_PUBLIC_STACK_PROJECT_ID),
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY", "NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY", process.env.NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY, process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY),
|
||||
NEXT_PUBLIC_STACK_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_URL", "NEXT_PUBLIC_STACK_URL", process.env.NEXT_PUBLIC_HEXCLAVE_URL, process.env.NEXT_PUBLIC_STACK_URL),
|
||||
NEXT_PUBLIC_STACK_INBUCKET_WEB_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_INBUCKET_WEB_URL", "NEXT_PUBLIC_STACK_INBUCKET_WEB_URL", process.env.NEXT_PUBLIC_HEXCLAVE_INBUCKET_WEB_URL, process.env.NEXT_PUBLIC_STACK_INBUCKET_WEB_URL),
|
||||
NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS", "NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS", process.env.NEXT_PUBLIC_HEXCLAVE_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS, process.env.NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS),
|
||||
NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_STRIPE_PUBLISHABLE_KEY", "NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY", process.env.NEXT_PUBLIC_HEXCLAVE_STRIPE_PUBLISHABLE_KEY, process.env.NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY),
|
||||
// Hexclave rebrand: port-prefix var renamed outright (no dual-read).
|
||||
NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX: process.env.NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX,
|
||||
NEXT_PUBLIC_STACK_DOCS_BASE_URL: process.env.NEXT_PUBLIC_HEXCLAVE_DOCS_BASE_URL ?? process.env.NEXT_PUBLIC_STACK_DOCS_BASE_URL,
|
||||
NEXT_PUBLIC_STACK_ENABLE_REACT_SCAN_IN_DEVELOPMENT: process.env.NEXT_PUBLIC_HEXCLAVE_ENABLE_REACT_SCAN_IN_DEVELOPMENT ?? process.env.NEXT_PUBLIC_STACK_ENABLE_REACT_SCAN_IN_DEVELOPMENT,
|
||||
NEXT_PUBLIC_STACK_DOCS_BASE_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_DOCS_BASE_URL", "NEXT_PUBLIC_STACK_DOCS_BASE_URL", process.env.NEXT_PUBLIC_HEXCLAVE_DOCS_BASE_URL, process.env.NEXT_PUBLIC_STACK_DOCS_BASE_URL),
|
||||
NEXT_PUBLIC_STACK_ENABLE_REACT_SCAN_IN_DEVELOPMENT: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_ENABLE_REACT_SCAN_IN_DEVELOPMENT", "NEXT_PUBLIC_STACK_ENABLE_REACT_SCAN_IN_DEVELOPMENT", process.env.NEXT_PUBLIC_HEXCLAVE_ENABLE_REACT_SCAN_IN_DEVELOPMENT, process.env.NEXT_PUBLIC_STACK_ENABLE_REACT_SCAN_IN_DEVELOPMENT),
|
||||
|
||||
// TODO: NEXT_PUBLIC_BROWSER_STACK_API_URL should be renamed to NEXT_PUBLIC_STACK_BROWSER_API_URL
|
||||
NEXT_PUBLIC_BROWSER_STACK_API_URL: process.env.NEXT_PUBLIC_BROWSER_HEXCLAVE_API_URL ?? process.env.NEXT_PUBLIC_BROWSER_STACK_API_URL,
|
||||
NEXT_PUBLIC_BROWSER_STACK_API_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_BROWSER_HEXCLAVE_API_URL", "NEXT_PUBLIC_BROWSER_STACK_API_URL", process.env.NEXT_PUBLIC_BROWSER_HEXCLAVE_API_URL, process.env.NEXT_PUBLIC_BROWSER_STACK_API_URL),
|
||||
// TODO: NEXT_PUBLIC_SERVER_STACK_API_URL should be renamed to NEXT_PUBLIC_STACK_SERVER_API_URL
|
||||
NEXT_PUBLIC_SERVER_STACK_API_URL: process.env.NEXT_PUBLIC_SERVER_HEXCLAVE_API_URL ?? process.env.NEXT_PUBLIC_SERVER_STACK_API_URL,
|
||||
NEXT_PUBLIC_SERVER_STACK_API_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_SERVER_HEXCLAVE_API_URL", "NEXT_PUBLIC_SERVER_STACK_API_URL", process.env.NEXT_PUBLIC_SERVER_HEXCLAVE_API_URL, process.env.NEXT_PUBLIC_SERVER_STACK_API_URL),
|
||||
// TODO: NEXT_PUBLIC_SENTRY_DSN should be renamed to NEXT_PUBLIC_STACK_SENTRY_DSN
|
||||
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
// TODO: NEXT_PUBLIC_VERSION_ALERTER_SEVERE_ONLY should be renamed to NEXT_PUBLIC_STACK_VERSION_ALERTER_SEVERE_ONLY
|
||||
NEXT_PUBLIC_VERSION_ALERTER_SEVERE_ONLY: process.env.NEXT_PUBLIC_VERSION_ALERTER_SEVERE_ONLY,
|
||||
// TODO: NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL should be renamed to NEXT_PUBLIC_STACK_BROWSER_DASHBOARD_URL
|
||||
NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL: process.env.NEXT_PUBLIC_BROWSER_HEXCLAVE_DASHBOARD_URL ?? process.env.NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL,
|
||||
NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_BROWSER_HEXCLAVE_DASHBOARD_URL", "NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL", process.env.NEXT_PUBLIC_BROWSER_HEXCLAVE_DASHBOARD_URL, process.env.NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL),
|
||||
// TODO: NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL should be renamed to NEXT_PUBLIC_STACK_SERVER_DASHBOARD_URL
|
||||
NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL: process.env.NEXT_PUBLIC_SERVER_HEXCLAVE_DASHBOARD_URL ?? process.env.NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL,
|
||||
NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_SERVER_HEXCLAVE_DASHBOARD_URL", "NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL", process.env.NEXT_PUBLIC_SERVER_HEXCLAVE_DASHBOARD_URL, process.env.NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL),
|
||||
// TODO: NEXT_PUBLIC_POSTHOG_KEY should be renamed to NEXT_PUBLIC_STACK_POSTHOG_KEY
|
||||
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
|
||||
} as const;
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
STACK_DASHBOARD_BASE_URL=
|
||||
STACK_BACKEND_BASE_URL=
|
||||
STACK_MCP_BASE_URL=
|
||||
STACK_INTERNAL_PROJECT_ID=
|
||||
STACK_INTERNAL_PROJECT_CLIENT_KEY=
|
||||
STACK_INTERNAL_PROJECT_SERVER_KEY=
|
||||
STACK_INTERNAL_PROJECT_ADMIN_KEY=
|
||||
STACK_TEST_SOURCE_OF_TRUTH=
|
||||
STACK_DATABASE_CONNECTION_STRING=
|
||||
HEXCLAVE_DASHBOARD_BASE_URL=
|
||||
HEXCLAVE_BACKEND_BASE_URL=
|
||||
HEXCLAVE_MCP_BASE_URL=
|
||||
HEXCLAVE_INTERNAL_PROJECT_ID=
|
||||
HEXCLAVE_INTERNAL_PROJECT_CLIENT_KEY=
|
||||
HEXCLAVE_INTERNAL_PROJECT_SERVER_KEY=
|
||||
HEXCLAVE_INTERNAL_PROJECT_ADMIN_KEY=
|
||||
HEXCLAVE_TEST_SOURCE_OF_TRUTH=
|
||||
HEXCLAVE_DATABASE_CONNECTION_STRING=
|
||||
|
||||
STACK_INBUCKET_API_URL=
|
||||
HEXCLAVE_INBUCKET_API_URL=
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
STACK_DASHBOARD_BASE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}01
|
||||
STACK_BACKEND_BASE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
STACK_MCP_BASE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}44
|
||||
STACK_INTERNAL_PROJECT_ID=internal
|
||||
STACK_INTERNAL_PROJECT_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
STACK_INTERNAL_PROJECT_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
STACK_INTERNAL_PROJECT_ADMIN_KEY=this-super-secret-admin-key-is-for-local-development-only
|
||||
NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX=.localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09
|
||||
STACK_DATABASE_CONNECTION_STRING=postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}28/stackframe
|
||||
HEXCLAVE_DASHBOARD_BASE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}01
|
||||
HEXCLAVE_BACKEND_BASE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
HEXCLAVE_MCP_BASE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}44
|
||||
HEXCLAVE_INTERNAL_PROJECT_ID=internal
|
||||
HEXCLAVE_INTERNAL_PROJECT_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
HEXCLAVE_INTERNAL_PROJECT_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
HEXCLAVE_INTERNAL_PROJECT_ADMIN_KEY=this-super-secret-admin-key-is-for-local-development-only
|
||||
NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_DOMAIN_SUFFIX=.localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09
|
||||
HEXCLAVE_DATABASE_CONNECTION_STRING=postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}28/stackframe
|
||||
|
||||
STACK_INBUCKET_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}05
|
||||
STACK_SVIX_SERVER_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}13
|
||||
HEXCLAVE_INBUCKET_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}05
|
||||
HEXCLAVE_SVIX_SERVER_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}13
|
||||
|
||||
STACK_EMAIL_MONITOR_SECRET_TOKEN=this-secret-token-is-for-local-development-only
|
||||
HEXCLAVE_EMAIL_MONITOR_SECRET_TOKEN=this-secret-token-is-for-local-development-only
|
||||
|
||||
CRON_SECRET=mock_cron_secret
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import { generateSecureRandomString } from "@hexclave/shared/dist/utils/crypto";
|
||||
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
|
||||
import type { MailboxMessage } from "../../../../../../helpers";
|
||||
import { it } from "../../../../../../helpers";
|
||||
import { Auth, InternalApiKey, Project, ProjectApiKey, Team, backendContext, bumpEmailAddress, niceBackendFetch } from "../../../../../backend-helpers";
|
||||
|
||||
// TODO re-enable these tests when we re-enable credential scanning email notifications
|
||||
|
||||
const isSourceOfTruthTest = () => getEnvVariable("STACK_TEST_SOURCE_OF_TRUTH", "") === "true";
|
||||
|
||||
it("should send email notification to user when revoking an API key through credential scanning", async ({ expect }: { expect: any }) => {
|
||||
await Project.createAndSwitch({ config: { magic_link_enabled: true, allow_team_api_keys: true, allow_user_api_keys: true } });
|
||||
|
||||
@ -44,7 +47,7 @@ it("should send email notification to user when revoking an API key through cred
|
||||
},
|
||||
});
|
||||
|
||||
if (process.env.STACK_TEST_SOURCE_OF_TRUTH === "true") {
|
||||
if (isSourceOfTruthTest()) {
|
||||
expect(revokeResponse).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
"status": 404,
|
||||
@ -180,7 +183,7 @@ it("should send email notification to team members when revoking a team API key
|
||||
},
|
||||
});
|
||||
|
||||
if (process.env.STACK_TEST_SOURCE_OF_TRUTH === "true") {
|
||||
if (isSourceOfTruthTest()) {
|
||||
expect(revokeResponse).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
"status": 404,
|
||||
@ -353,7 +356,7 @@ it("should handle already revoked API keys gracefully", async ({ expect }: { exp
|
||||
},
|
||||
});
|
||||
|
||||
if (process.env.STACK_TEST_SOURCE_OF_TRUTH === "true") {
|
||||
if (isSourceOfTruthTest()) {
|
||||
expect(revokeResponse).toMatchInlineSnapshot(`
|
||||
NiceResponse {
|
||||
"status": 404,
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { describe } from "vitest";
|
||||
import { it } from "../../../../../helpers";
|
||||
import { backendContext, niceBackendFetch } from "../../../../backend-helpers";
|
||||
|
||||
const isLocalEmulator = process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR === "true";
|
||||
const isLocalEmulator = getEnvVariable("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR", "") === "true";
|
||||
const blockedMessage = "cannot be changed in a development environment";
|
||||
const localEmulatorProjectEndpoint = "/api/v1/internal/local-emulator/project";
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
|
||||
import { wait } from "@hexclave/shared/dist/utils/promises";
|
||||
import { stringCompare } from "@hexclave/shared/dist/utils/strings";
|
||||
import { describe, expect } from "vitest";
|
||||
@ -13,6 +14,8 @@ type FailedEmailsBatch = {
|
||||
|
||||
type DigestResponse = Awaited<ReturnType<typeof niceBackendFetch>>;
|
||||
|
||||
const isSourceOfTruthTest = () => getEnvVariable("STACK_TEST_SOURCE_OF_TRUTH", "") === "true";
|
||||
|
||||
// Always uses dry_run=true: the only callers are the polling helper below
|
||||
// (which must be side-effect-free since it fires repeatedly) and the snapshot
|
||||
// assertion (which uses the same SELECT result regardless of dry_run).
|
||||
@ -168,7 +171,7 @@ describe("with valid credentials", () => {
|
||||
const { response, batches: mockProjectFailedEmails } = await waitForFailedEmailsDigest(2);
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
if (process.env.STACK_TEST_SOURCE_OF_TRUTH === "true") {
|
||||
if (isSourceOfTruthTest()) {
|
||||
expect(mockProjectFailedEmails).toMatchInlineSnapshot(`[]`);
|
||||
} else {
|
||||
expect(mockProjectFailedEmails).toMatchInlineSnapshot(`
|
||||
@ -460,7 +463,7 @@ describe("with valid credentials", () => {
|
||||
(batch: any) => batch.project_id === projectId
|
||||
);
|
||||
|
||||
if (process.env.STACK_TEST_SOURCE_OF_TRUTH === "true") {
|
||||
if (isSourceOfTruthTest()) {
|
||||
expect(currentResponses).toMatchInlineSnapshot(`[]`);
|
||||
} else {
|
||||
expect(currentResponses.length).toBe(1);
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
|
||||
import { describe } from "vitest";
|
||||
import { it } from "../../../../../helpers";
|
||||
import { Auth, backendContext, createMailbox, niceBackendFetch, waitForOutboxEmailWithStatus } from "../../../../backend-helpers";
|
||||
|
||||
const isLocalEmulator = process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR === "true";
|
||||
const isLocalEmulator = getEnvVariable("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR", "") === "true";
|
||||
const supportConversationsPath = "/api/v1/internal/dogfood/support/conversations";
|
||||
|
||||
describe("POST /api/v1/internal/feedback", () => {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { describe } from "vitest";
|
||||
@ -7,7 +8,7 @@ import { backendContext, niceBackendFetch } from "../../../../backend-helpers";
|
||||
|
||||
const LOCAL_EMULATOR_PROJECT_ENDPOINT = "/api/v1/internal/local-emulator/project";
|
||||
const LOCAL_EMULATOR_OWNER_TEAM_ID = "5a0c858b-d9e9-49d4-9943-8ce385d86428";
|
||||
const isLocalEmulator = process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR === "true";
|
||||
const isLocalEmulator = getEnvVariable("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR", "") === "true";
|
||||
|
||||
async function createTempConfigFile(): Promise<string> {
|
||||
const filePath = `/tmp/${randomUUID()}/stack.config.ts`;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { createHmac } from "node:crypto";
|
||||
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
|
||||
import { expect } from "vitest";
|
||||
import { it } from "../../../../../helpers";
|
||||
import { Auth, Payments as PaymentsHelper, Project, Team, User, niceBackendFetch } from "../../../../backend-helpers";
|
||||
@ -72,7 +73,7 @@ async function createPurchaseCodeForCustomer(options: { customerType: "user" | "
|
||||
return code as string;
|
||||
}
|
||||
|
||||
const stripeWebhookSecret = process.env.STACK_STRIPE_WEBHOOK_SECRET ?? "mock_stripe_webhook_secret";
|
||||
const stripeWebhookSecret = getEnvVariable("STACK_STRIPE_WEBHOOK_SECRET", "mock_stripe_webhook_secret");
|
||||
|
||||
async function sendStripeWebhook(payload: unknown) {
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
@ -3,11 +3,12 @@ import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import { StackAdminApp } from "@hexclave/js";
|
||||
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
|
||||
import { Result } from "@hexclave/shared/dist/utils/results";
|
||||
import { describe, beforeAll, afterAll } from "vitest";
|
||||
import { it, niceFetch, STACK_BACKEND_BASE_URL, STACK_INTERNAL_PROJECT_CLIENT_KEY, STACK_INTERNAL_PROJECT_SERVER_KEY, STACK_INTERNAL_PROJECT_ADMIN_KEY } from "../helpers";
|
||||
|
||||
const isLocalEmulator = process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR === "true";
|
||||
const isLocalEmulator = getEnvVariable("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR", "") === "true";
|
||||
|
||||
const CLI_BIN = path.resolve("packages/cli/dist/index.js");
|
||||
|
||||
|
||||
@ -241,7 +241,7 @@ export class Mailbox {
|
||||
};
|
||||
|
||||
this.waitForMessagesWithSubjectCount = async (subject: string, minCount: number, options?: { noBody?: boolean }) => {
|
||||
const timeoutMs = Number(process.env.STACK_MAILBOX_WAIT_TIMEOUT_MS ?? 60000);
|
||||
const timeoutMs = Number(getEnvVariable("STACK_MAILBOX_WAIT_TIMEOUT_MS", "60000"));
|
||||
const intervalMs = 500;
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let messages: MailboxMessage[] = [];
|
||||
@ -299,7 +299,7 @@ function expandHexclavePortPrefix(value?: string | null) {
|
||||
return prefix ? value.replace(/\$\{NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81\}/g, prefix) : value;
|
||||
}
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (key.startsWith("STACK_") || key.startsWith("NEXT_PUBLIC_STACK_")) {
|
||||
if (key.startsWith("STACK_") || key.startsWith("NEXT_PUBLIC_STACK_") || key.startsWith("HEXCLAVE_") || key.startsWith("NEXT_PUBLIC_HEXCLAVE_")) {
|
||||
const replaced = expandHexclavePortPrefix(value ?? undefined);
|
||||
if (replaced !== undefined) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
@ -324,7 +324,7 @@ export const STACK_MCP_BASE_URL = getEnvVariable("STACK_MCP_BASE_URL");
|
||||
* fallback port. Always thread this through to SDK constructors instead of
|
||||
* hardcoding `STACK_BACKEND_BASE_URL`.
|
||||
*/
|
||||
export const SDK_BASE_URL: string | undefined = process.env.STACK_TEST_SDK_FALLBACK
|
||||
export const SDK_BASE_URL: string | undefined = getEnvVariable("STACK_TEST_SDK_FALLBACK", "")
|
||||
? undefined
|
||||
: STACK_BACKEND_BASE_URL;
|
||||
export const STACK_INTERNAL_PROJECT_ID = getEnvVariable("STACK_INTERNAL_PROJECT_ID");
|
||||
|
||||
@ -31,24 +31,31 @@ function createMockDocument(): Document {
|
||||
}
|
||||
|
||||
const withHostedDomainSuffix = async (callback: () => Promise<void>) => {
|
||||
const oldHostedHandlerDomainSuffix = process.env.NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX;
|
||||
const oldHostedHandlerUrlTemplate = process.env.NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE;
|
||||
process.env.NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX = ".example-stack-hosted.test";
|
||||
// The SDK resolves NEXT_PUBLIC_HEXCLAVE_* before the legacy NEXT_PUBLIC_STACK_*
|
||||
// names, so override the canonical name and clear both spellings.
|
||||
const oldHostedHandlerDomainSuffix = process.env.NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_DOMAIN_SUFFIX;
|
||||
const oldLegacyHostedHandlerDomainSuffix = process.env.NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX;
|
||||
const oldHostedHandlerUrlTemplate = process.env.NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_URL_TEMPLATE;
|
||||
const oldLegacyHostedHandlerUrlTemplate = process.env.NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE;
|
||||
process.env.NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_DOMAIN_SUFFIX = ".example-stack-hosted.test";
|
||||
delete process.env.NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX;
|
||||
delete process.env.NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_URL_TEMPLATE;
|
||||
delete process.env.NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE;
|
||||
|
||||
try {
|
||||
await callback();
|
||||
} finally {
|
||||
if (oldHostedHandlerDomainSuffix == null) {
|
||||
delete process.env.NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX;
|
||||
} else {
|
||||
process.env.NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX = oldHostedHandlerDomainSuffix;
|
||||
}
|
||||
if (oldHostedHandlerUrlTemplate == null) {
|
||||
delete process.env.NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE;
|
||||
} else {
|
||||
process.env.NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE = oldHostedHandlerUrlTemplate;
|
||||
}
|
||||
const restore = (name: string, value: string | undefined) => {
|
||||
if (value == null) {
|
||||
delete process.env[name];
|
||||
} else {
|
||||
process.env[name] = value;
|
||||
}
|
||||
};
|
||||
restore("NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_DOMAIN_SUFFIX", oldHostedHandlerDomainSuffix);
|
||||
restore("NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX", oldLegacyHostedHandlerDomainSuffix);
|
||||
restore("NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_URL_TEMPLATE", oldHostedHandlerUrlTemplate);
|
||||
restore("NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE", oldLegacyHostedHandlerUrlTemplate);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -1 +1 @@
|
||||
VITE_STACK_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
VITE_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
|
||||
@ -51,7 +51,12 @@ function useProjectIdFromHostname(): string | null | undefined {
|
||||
}
|
||||
|
||||
function getApiBaseUrlFromEnv(): string | undefined {
|
||||
return import.meta.env.VITE_HEXCLAVE_API_URL ?? import.meta.env.VITE_STACK_API_URL ?? undefined;
|
||||
const hexclaveValue = import.meta.env.VITE_HEXCLAVE_API_URL;
|
||||
const stackValue = import.meta.env.VITE_STACK_API_URL;
|
||||
if (hexclaveValue && stackValue && hexclaveValue !== stackValue) {
|
||||
throw new Error("Environment variables VITE_HEXCLAVE_API_URL and VITE_STACK_API_URL are both set to different values. Remove one of them or set them to the same value.");
|
||||
}
|
||||
return hexclaveValue || stackValue || undefined;
|
||||
}
|
||||
|
||||
function isTrustedNavigationTarget(to: string): boolean {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
# Hexclave
|
||||
NEXT_PUBLIC_STACK_API_URL=REPLACE_ME
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=REPLACE_ME
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=REPLACE_ME
|
||||
STACK_SECRET_SERVER_KEY=REPLACE_ME
|
||||
NEXT_PUBLIC_STACK_DASHBOARD_URL=REPLACE_ME
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=REPLACE_ME
|
||||
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=REPLACE_ME
|
||||
NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=REPLACE_ME
|
||||
HEXCLAVE_SECRET_SERVER_KEY=REPLACE_ME
|
||||
NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL=REPLACE_ME
|
||||
# SpacetimeDB
|
||||
NEXT_PUBLIC_SPACETIMEDB_HOST=REPLACE_ME
|
||||
NEXT_PUBLIC_SPACETIMEDB_DB_NAME=REPLACE_ME
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
NEXT_PUBLIC_STACK_DASHBOARD_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}01
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}01
|
||||
NEXT_PUBLIC_SPACETIMEDB_HOST=ws://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}39
|
||||
NEXT_PUBLIC_SPACETIMEDB_DB_NAME=stack-auth-llm
|
||||
|
||||
@ -6,10 +6,20 @@ import { spawnSync } from "node:child_process";
|
||||
|
||||
const target = process.argv[2]; // "local" or "prod"
|
||||
|
||||
function resolveHexclaveStackEnvVar(hexclaveName, stackName) {
|
||||
const hexclaveValue = process.env[hexclaveName];
|
||||
const stackValue = process.env[stackName];
|
||||
if (hexclaveValue && stackValue && hexclaveValue !== stackValue) {
|
||||
throw new Error(`Environment variables ${hexclaveName} and ${stackName} are both set to different values. Remove one of them or set them to the same value.`);
|
||||
}
|
||||
return hexclaveValue || stackValue || undefined;
|
||||
}
|
||||
|
||||
/** HTTP API for 'spacetime publish' (matches docker/dependencies/docker.compose.yaml host port ...39). */
|
||||
function localPublishServerUrl() {
|
||||
if (process.env.STACK_SPACETIME_PUBLISH_URL) {
|
||||
return process.env.STACK_SPACETIME_PUBLISH_URL;
|
||||
const publishUrl = resolveHexclaveStackEnvVar("HEXCLAVE_SPACETIME_PUBLISH_URL", "STACK_SPACETIME_PUBLISH_URL");
|
||||
if (publishUrl) {
|
||||
return publishUrl;
|
||||
}
|
||||
const prefix = process.env.NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX ?? "81";
|
||||
return `http://127.0.0.1:${prefix}39`;
|
||||
@ -36,8 +46,8 @@ if (!args) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (target === "prod" && !process.env.STACK_MCP_LOG_TOKEN) {
|
||||
console.error("Error: STACK_MCP_LOG_TOKEN must be set for prod publish");
|
||||
if (target === "prod" && !resolveHexclaveStackEnvVar("HEXCLAVE_MCP_LOG_TOKEN", "STACK_MCP_LOG_TOKEN")) {
|
||||
console.error("Error: HEXCLAVE_MCP_LOG_TOKEN (or legacy STACK_MCP_LOG_TOKEN) must be set for prod publish");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
@ -11,8 +11,17 @@ const PLACEHOLDER = "__SPACETIMEDB_LOG_TOKEN__";
|
||||
|
||||
const action = process.argv[2];
|
||||
|
||||
function resolveHexclaveStackEnvVar(hexclaveName, stackName) {
|
||||
const hexclaveValue = process.env[hexclaveName];
|
||||
const stackValue = process.env[stackName];
|
||||
if (hexclaveValue && stackValue && hexclaveValue !== stackValue) {
|
||||
throw new Error(`Environment variables ${hexclaveName} and ${stackName} are both set to different values. Remove one of them or set them to the same value.`);
|
||||
}
|
||||
return hexclaveValue || stackValue || undefined;
|
||||
}
|
||||
|
||||
if (action === "inject") {
|
||||
const token = process.env.STACK_MCP_LOG_TOKEN || "change-me";
|
||||
const token = resolveHexclaveStackEnvVar("HEXCLAVE_MCP_LOG_TOKEN", "STACK_MCP_LOG_TOKEN") || "change-me";
|
||||
if (existsSync(BACKUP)) {
|
||||
console.error("Refusing to inject: backup already exists. Run restore first.");
|
||||
process.exit(1);
|
||||
|
||||
@ -12,6 +12,13 @@ import type { McpCallLogRow } from "../types";
|
||||
|
||||
type Tab = "calls" | "knowledge" | "analytics";
|
||||
|
||||
function resolveInlineRenamedEnvVar(hexclaveName: string, stackName: string, hexclaveValue: string | undefined, stackValue: string | undefined): string | undefined {
|
||||
if (hexclaveValue && stackValue && hexclaveValue !== stackValue) {
|
||||
throw new Error(`Environment variables ${hexclaveName} and ${stackName} are both set to different values. Remove one of them or set them to the same value.`);
|
||||
}
|
||||
return hexclaveValue || stackValue || undefined;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const user = useUser({ or: process.env.NODE_ENV === "development" ? "redirect" : "return-null" });
|
||||
const [selectedRow, setSelectedRow] = useState<McpCallLogRow | null>(null);
|
||||
@ -26,7 +33,12 @@ export default function App() {
|
||||
<h1 className="text-lg font-semibold text-gray-900 mb-2">MCP Review Tool</h1>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Sign in to the{" "}
|
||||
<a href={process.env.NEXT_PUBLIC_STACK_DASHBOARD_URL} className="text-blue-600 underline" target="_blank" rel="noreferrer">
|
||||
<a
|
||||
href={resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL", "NEXT_PUBLIC_STACK_DASHBOARD_URL", process.env.NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL, process.env.NEXT_PUBLIC_STACK_DASHBOARD_URL)}
|
||||
className="text-blue-600 underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Hexclave Dashboard
|
||||
</a>
|
||||
{" "}first, then reload this page.
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
NEXT_PUBLIC_STACK_API_URL=
|
||||
NEXT_PUBLIC_SERVER_STACK_API_URL=
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=
|
||||
NEXT_PUBLIC_SERVER_HEXCLAVE_API_URL=
|
||||
NEXT_PUBLIC_POSTHOG_KEY=
|
||||
|
||||
@ -1 +1 @@
|
||||
NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
NEXT_PUBLIC_STACK_API_URL=# the base URL of Stack's backend/API. For local development, this is `http://localhost:8102`; for the managed service, this is `https://api.hexclave.com`.
|
||||
NEXT_PUBLIC_SERVER_STACK_API_URL=# optional server-side override for the backend/API URL. If unset, falls back to NEXT_PUBLIC_STACK_API_URL.
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=# the base URL of Stack's backend/API. For local development, this is `http://localhost:8102`; for the managed service, this is `https://api.hexclave.com`.
|
||||
NEXT_PUBLIC_SERVER_HEXCLAVE_API_URL=# optional server-side override for the backend/API URL. If unset, falls back to NEXT_PUBLIC_HEXCLAVE_API_URL.
|
||||
NEXT_PUBLIC_POSTHOG_KEY=# optional PostHog project key for analytics. Leave blank to use the default public key.
|
||||
|
||||
@ -41,23 +41,35 @@ const parseEnvFile = (filePath) => {
|
||||
const backendEnv = parseEnvFile(backendEnvPath);
|
||||
const dashboardEnv = parseEnvFile(dashboardEnvPath);
|
||||
|
||||
// Hexclave rebrand: the source env files use the canonical HEXCLAVE_* names,
|
||||
// but accept the legacy STACK_* spelling as a fallback. Emitted keys are
|
||||
// always canonical.
|
||||
const toCanonicalKey = (key) => key.includes("STACK_") ? key.replace("STACK_", "HEXCLAVE_") : key;
|
||||
|
||||
const getRequiredEnvValue = (sourceName, envMap, key) => {
|
||||
const value = envMap.get(key);
|
||||
const canonicalKey = toCanonicalKey(key);
|
||||
const canonicalValue = envMap.get(canonicalKey);
|
||||
const legacyValue = canonicalKey === key ? undefined : envMap.get(key);
|
||||
if (canonicalValue && legacyValue && canonicalValue !== legacyValue) {
|
||||
throw new Error(`${sourceName} defines both ${canonicalKey} and ${key} with different non-empty values. Remove one of them or set them to the same value.`);
|
||||
}
|
||||
const nonEmptyValue = canonicalValue || legacyValue;
|
||||
const value = nonEmptyValue !== undefined ? nonEmptyValue : canonicalValue ?? legacyValue;
|
||||
if (value == null) {
|
||||
throw new Error(`Missing ${key} in ${sourceName}; update the generator or source env file.`);
|
||||
throw new Error(`Missing ${canonicalKey} in ${sourceName}; update the generator or source env file.`);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const fromSource = (sourceName, envMap, key) => ({
|
||||
type: "entry",
|
||||
key,
|
||||
key: toCanonicalKey(key),
|
||||
value: getRequiredEnvValue(sourceName, envMap, key),
|
||||
});
|
||||
|
||||
const literal = (key, value) => ({
|
||||
type: "entry",
|
||||
key,
|
||||
key: toCanonicalKey(key),
|
||||
value,
|
||||
});
|
||||
|
||||
|
||||
@ -22,6 +22,14 @@ set -euo pipefail
|
||||
OUTPUT=/run/stack-auth/rotated-secrets.env
|
||||
WORK_DIR="${STACK_RUNTIME_WORK_DIR:-/app}"
|
||||
|
||||
# Hexclave rebrand: the container env may carry the canonical HEXCLAVE_ name
|
||||
# instead of the legacy STACK_ one this script reads.
|
||||
if [ -n "${STACK_DATABASE_CONNECTION_STRING:-}" ] && [ -n "${HEXCLAVE_DATABASE_CONNECTION_STRING:-}" ] && [ "$STACK_DATABASE_CONNECTION_STRING" != "$HEXCLAVE_DATABASE_CONNECTION_STRING" ]; then
|
||||
echo "ERROR: STACK_DATABASE_CONNECTION_STRING and HEXCLAVE_DATABASE_CONNECTION_STRING are both set to different non-empty values. Remove one of them or set them to the same value." >&2
|
||||
exit 1
|
||||
fi
|
||||
STACK_DATABASE_CONNECTION_STRING="${STACK_DATABASE_CONNECTION_STRING:-${HEXCLAVE_DATABASE_CONNECTION_STRING:-}}"
|
||||
|
||||
PLACEHOLDER_PCK="00000000000000000000000000000000ffffffffffffffffffffffffffffffff"
|
||||
|
||||
log() { printf '[rotate-secrets] %s\n' "$*"; }
|
||||
|
||||
@ -1,42 +1,42 @@
|
||||
NEXT_PUBLIC_STACK_API_URL=# https://your-backend-domain.com
|
||||
NEXT_PUBLIC_STACK_DASHBOARD_URL=# https://your-dashboard-domain.com, this will be added as a trusted domain by the seed script
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=# https://your-backend-domain.com
|
||||
NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL=# https://your-dashboard-domain.com, this will be added as a trusted domain by the seed script
|
||||
|
||||
STACK_DATABASE_CONNECTION_STRING=# postgres connection string
|
||||
HEXCLAVE_DATABASE_CONNECTION_STRING=# postgres connection string
|
||||
|
||||
STACK_SERVER_SECRET=# a 32 bytes base64url encoded random string, used for JWT encryption. can be generated with `pnpm generate-keys`
|
||||
HEXCLAVE_SERVER_SECRET=# a 32 bytes base64url encoded random string, used for JWT encryption. can be generated with `pnpm generate-keys`
|
||||
|
||||
# seed script settings
|
||||
STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED=# true to enable user sign up to the dashboard when seeding
|
||||
STACK_SEED_INTERNAL_PROJECT_OTP_ENABLED=# true to add OTP auth to the dashboard when seeding
|
||||
STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST=# true to allow running dashboard on the localhost, set this to true only in development
|
||||
STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS=# list of oauth providers to add to the dashboard when seeding, separated by comma, for example "github,google,facebook"
|
||||
STACK_SEED_INTERNAL_PROJECT_USER_EMAIL=# default user added to the dashboard
|
||||
STACK_SEED_INTERNAL_PROJECT_USER_PASSWORD=# default user's password, paired with STACK_SEED_INTERNAL_PROJECT_USER_EMAIL
|
||||
STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=# if the default user has access to the internal dashboard project
|
||||
STACK_SEED_INTERNAL_PROJECT_USER_GITHUB_ID=# add github oauth id to the default user
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED=# true to enable user sign up to the dashboard when seeding
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_OTP_ENABLED=# true to add OTP auth to the dashboard when seeding
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST=# true to allow running dashboard on the localhost, set this to true only in development
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS=# list of oauth providers to add to the dashboard when seeding, separated by comma, for example "github,google,facebook"
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_USER_EMAIL=# default user added to the dashboard
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_USER_PASSWORD=# default user's password, paired with HEXCLAVE_SEED_INTERNAL_PROJECT_USER_EMAIL
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=# if the default user has access to the internal dashboard project
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_USER_GITHUB_ID=# add github oauth id to the default user
|
||||
|
||||
# Set these if you want to use any email functionality
|
||||
STACK_EMAILABLE_API_KEY=disable_email_validation
|
||||
STACK_EMAIL_HOST=
|
||||
STACK_EMAIL_PORT=
|
||||
STACK_EMAIL_USERNAME=
|
||||
STACK_EMAIL_PASSWORD=
|
||||
STACK_EMAIL_SENDER=
|
||||
HEXCLAVE_EMAILABLE_API_KEY=disable_email_validation
|
||||
HEXCLAVE_EMAIL_HOST=
|
||||
HEXCLAVE_EMAIL_PORT=
|
||||
HEXCLAVE_EMAIL_USERNAME=
|
||||
HEXCLAVE_EMAIL_PASSWORD=
|
||||
HEXCLAVE_EMAIL_SENDER=
|
||||
|
||||
# Set these if you want to use webhooks
|
||||
STACK_SVIX_SERVER_URL=# this is only needed if you self-host the Svix service
|
||||
NEXT_PUBLIC_STACK_SVIX_SERVER_URL=# this is only needed if you are using docker compose and the external and internal urls are different. This is the external url for the Svix service.
|
||||
STACK_SVIX_API_KEY=
|
||||
HEXCLAVE_SVIX_SERVER_URL=# this is only needed if you self-host the Svix service
|
||||
NEXT_PUBLIC_HEXCLAVE_SVIX_SERVER_URL=# this is only needed if you are using docker compose and the external and internal urls are different. This is the external url for the Svix service.
|
||||
HEXCLAVE_SVIX_API_KEY=
|
||||
|
||||
STACK_OPENROUTER_API_KEY=# enter your OpenRouter API key for AI features
|
||||
HEXCLAVE_OPENROUTER_API_KEY=# enter your OpenRouter API key for AI features
|
||||
|
||||
STACK_SKIP_SEED_SCRIPT=# true to skip the seed script
|
||||
HEXCLAVE_SKIP_SEED_SCRIPT=# true to skip the seed script
|
||||
|
||||
STACK_S3_ENDPOINT=
|
||||
STACK_S3_REGION=
|
||||
STACK_S3_ACCESS_KEY_ID=
|
||||
STACK_S3_SECRET_ACCESS_KEY=
|
||||
STACK_S3_BUCKET=
|
||||
STACK_S3_PRIVATE_BUCKET=
|
||||
HEXCLAVE_S3_ENDPOINT=
|
||||
HEXCLAVE_S3_REGION=
|
||||
HEXCLAVE_S3_ACCESS_KEY_ID=
|
||||
HEXCLAVE_S3_SECRET_ACCESS_KEY=
|
||||
HEXCLAVE_S3_BUCKET=
|
||||
HEXCLAVE_S3_PRIVATE_BUCKET=
|
||||
|
||||
STACK_FREESTYLE_API_KEY=# enter your freestyle.sh api key
|
||||
HEXCLAVE_FREESTYLE_API_KEY=# enter your freestyle.sh api key
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
# IMPORTANT: YOU MUST REGENERATE THE STACK_SERVER_SECRET VALUE BELOW
|
||||
# IMPORTANT: YOU MUST REGENERATE THE HEXCLAVE_SERVER_SECRET VALUE BELOW
|
||||
|
||||
NEXT_PUBLIC_STACK_API_URL=http://localhost:8102
|
||||
NEXT_PUBLIC_STACK_DASHBOARD_URL=http://localhost:8101
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=http://localhost:8102
|
||||
NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL=http://localhost:8101
|
||||
|
||||
STACK_DATABASE_CONNECTION_STRING=postgres://postgres:password@host.docker.internal:8128/stackframe
|
||||
HEXCLAVE_DATABASE_CONNECTION_STRING=postgres://postgres:password@host.docker.internal:8128/stackframe
|
||||
|
||||
STACK_SERVER_SECRET=23-wuNpik0gIW4mruTz25rbIvhuuvZFrLOLtL7J4tyo
|
||||
HEXCLAVE_SERVER_SECRET=23-wuNpik0gIW4mruTz25rbIvhuuvZFrLOLtL7J4tyo
|
||||
|
||||
STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST=true
|
||||
STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED=true
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST=true
|
||||
HEXCLAVE_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED=true
|
||||
|
||||
STACK_RUN_MIGRATIONS=true
|
||||
STACK_RUN_SEED_SCRIPT=true
|
||||
HEXCLAVE_RUN_MIGRATIONS=true
|
||||
HEXCLAVE_RUN_SEED_SCRIPT=true
|
||||
|
||||
STACK_FREESTYLE_API_KEY=
|
||||
HEXCLAVE_FREESTYLE_API_KEY=
|
||||
|
||||
STACK_CLICKHOUSE_URL=http://host.docker.internal:8133
|
||||
STACK_CLICKHOUSE_ADMIN_PASSWORD=password
|
||||
STACK_CLICKHOUSE_EXTERNAL_PASSWORD=password
|
||||
HEXCLAVE_CLICKHOUSE_URL=http://host.docker.internal:8133
|
||||
HEXCLAVE_CLICKHOUSE_ADMIN_PASSWORD=password
|
||||
HEXCLAVE_CLICKHOUSE_EXTERNAL_PASSWORD=password
|
||||
|
||||
@ -13,6 +13,32 @@ if [ -f /run/stack-auth/rotated-secrets.env ]; then
|
||||
set +a
|
||||
fi
|
||||
|
||||
# ============= HEXCLAVE_ ↔ STACK_ ENV ALIASING =============
|
||||
# Hexclave rebrand: HEXCLAVE_*-prefixed env vars are canonical, but self-host
|
||||
# operators (and older compose files) may still set the legacy STACK_* names.
|
||||
# Node-side code dual-reads both, but this shell script and the sentinel
|
||||
# substitution below look up vars by one exact name — so mirror every
|
||||
# HEXCLAVE_*/STACK_* var to its unset twin. Called again right before the
|
||||
# sentinel scan to pick up vars exported by the sections in between.
|
||||
mirror_hexclave_stack_env() {
|
||||
local _name _twin
|
||||
for _name in $(compgen -e); do
|
||||
case "$_name" in
|
||||
*HEXCLAVE_*) _twin=${_name/HEXCLAVE_/STACK_} ;;
|
||||
*STACK_*) _twin=${_name/STACK_/HEXCLAVE_} ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
if [ -n "${!_name:-}" ] && [ -n "${!_twin:-}" ] && [ "${!_name}" != "${!_twin}" ]; then
|
||||
echo "ERROR: $_name and $_twin are both set to different non-empty values. Remove one of them or set them to the same value." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${!_twin:-}" ] && [ -n "${!_name:-}" ]; then
|
||||
export "$_twin=${!_name}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
mirror_hexclave_stack_env
|
||||
|
||||
# ============= FORWARD MOCK OAUTH SERVER =============
|
||||
|
||||
# Start socat to forward port 32202 for mock-oauth-server if enabled
|
||||
@ -45,39 +71,15 @@ if [ -n "${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY:-}" ]; then
|
||||
export STACK_SUPER_SECRET_ADMIN_KEY=${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY}
|
||||
fi
|
||||
|
||||
# ============= HEXCLAVE ↔ STACK URL ENV MIRROR =============
|
||||
# The dashboard bundle inlines BOTH process.env.NEXT_PUBLIC_HEXCLAVE_* and
|
||||
# process.env.NEXT_PUBLIC_STACK_* references as sentinels (dual-read). At
|
||||
# runtime the sentinel-replace loop only substitutes a sentinel when the
|
||||
# corresponding env var is set — but the dashboard's fallback chain
|
||||
# (`HEXCLAVE_X ?? STACK_X`) treats an unreplaced sentinel as truthy, so it
|
||||
# would pick the literal sentinel string instead of the real URL whenever
|
||||
# only one of the two env names is set by the self-host operator.
|
||||
# Mirror the URL trio HEXCLAVE → STACK and STACK → HEXCLAVE before the
|
||||
# sentinel-replace runs, so both sentinels resolve to the same real value
|
||||
# regardless of which name the operator chose.
|
||||
for _legacy in STACK_API_URL STACK_DASHBOARD_URL STACK_SVIX_SERVER_URL; do
|
||||
_new=HEXCLAVE_${_legacy#STACK_}
|
||||
_legacy_full=NEXT_PUBLIC_${_legacy}
|
||||
_new_full=NEXT_PUBLIC_${_new}
|
||||
_legacy_val=${!_legacy_full:-}
|
||||
_new_val=${!_new_full:-}
|
||||
if [ -n "$_new_val" ] && [ -z "$_legacy_val" ]; then
|
||||
export "$_legacy_full=$_new_val"
|
||||
elif [ -n "$_legacy_val" ] && [ -z "$_new_val" ]; then
|
||||
export "$_new_full=$_legacy_val"
|
||||
fi
|
||||
done
|
||||
|
||||
export NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL=${NEXT_PUBLIC_STACK_DASHBOARD_URL}
|
||||
# Hexclave rebrand: the port-prefix var was renamed outright to
|
||||
# NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX. The dashboard bundle's post-build sentinel
|
||||
# is STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX, and the sentinel
|
||||
# substitution loop below derives the env var name from the sentinel — so this
|
||||
# MUST export NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX or the sentinel never resolves.
|
||||
# Accept the legacy NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX as input for back-compat with
|
||||
# Accept the legacy NEXT_PUBLIC_STACK_PORT_PREFIX as input for back-compat with
|
||||
# existing self-host configs.
|
||||
export NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX=${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}}
|
||||
export NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX=${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}}
|
||||
PORT_PREFIX=${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX}
|
||||
export NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL="http://localhost:${PORT_PREFIX}01"
|
||||
export NEXT_PUBLIC_BROWSER_STACK_API_URL=${NEXT_PUBLIC_STACK_API_URL}
|
||||
@ -161,6 +163,14 @@ fi
|
||||
|
||||
# ============= ENV VARS =============
|
||||
|
||||
# Mirror again: the sections above exported more STACK_/HEXCLAVE_ vars (internal
|
||||
# project keys, NEXT_PUBLIC_STACK_PROJECT_ID, svix fallback). The dashboard
|
||||
# bundle inlines BOTH process.env.NEXT_PUBLIC_HEXCLAVE_* and
|
||||
# process.env.NEXT_PUBLIC_STACK_* references as sentinels (dual-read), and the
|
||||
# fallback chain treats an unreplaced sentinel as truthy — so both spellings
|
||||
# must resolve to the same real value before the sentinel replacement below.
|
||||
mirror_hexclave_stack_env
|
||||
|
||||
# Create a working directory for our processed files.
|
||||
# Keep this off /tmp so local-emulator config sharing can bind-mount /tmp
|
||||
# without pushing the whole runtime copy step onto the host filesystem.
|
||||
|
||||
10
docs/.env
10
docs/.env
@ -1,6 +1,6 @@
|
||||
# Basic
|
||||
NEXT_PUBLIC_STACK_API_URL=# the base URL of Stack's backend/API
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=# the project ID to use
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=# publishable client key for the project
|
||||
STACK_SECRET_SERVER_KEY=# secret server key for the project
|
||||
STACK_OPENROUTER_API_KEY=# OpenRouter API key for AI-enabled chat
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=# the base URL of Stack's backend/API
|
||||
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=# the project ID to use
|
||||
NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=# publishable client key for the project
|
||||
HEXCLAVE_SECRET_SERVER_KEY=# secret server key for the project
|
||||
HEXCLAVE_OPENROUTER_API_KEY=# OpenRouter API key for AI-enabled chat
|
||||
@ -1,7 +1,7 @@
|
||||
# Contains the credentials for the internal project of Stack's default development environment setup.
|
||||
# Do not use in a production environment, instead replace it with actual values gathered from https://app.stack-auth.com.
|
||||
NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
STACK_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
STACK_OPENROUTER_API_KEY=your-open-router-api-key-for-ai-enabled-chat
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
HEXCLAVE_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
HEXCLAVE_OPENROUTER_API_KEY=your-open-router-api-key-for-ai-enabled-chat
|
||||
|
||||
@ -735,9 +735,9 @@ export async function GET(request: Request) {
|
||||
code: `import { StackServerApp } from "@stackframe/js";
|
||||
|
||||
const stackServerApp = new StackServerApp({
|
||||
projectId: process.env.STACK_PROJECT_ID,
|
||||
publishableClientKey: process.env.STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
secretServerKey: process.env.STACK_SECRET_SERVER_KEY,
|
||||
projectId: process.env.HEXCLAVE_PROJECT_ID,
|
||||
publishableClientKey: process.env.HEXCLAVE_PUBLISHABLE_CLIENT_KEY,
|
||||
secretServerKey: process.env.HEXCLAVE_SECRET_SERVER_KEY,
|
||||
tokenStore: "memory",
|
||||
});
|
||||
|
||||
@ -768,9 +768,9 @@ app.get('/api/protected', async (req, res) => {
|
||||
code: `import { StackServerApp } from "@stackframe/js";
|
||||
|
||||
const stackServerApp = new StackServerApp({
|
||||
projectId: process.env.STACK_PROJECT_ID,
|
||||
publishableClientKey: process.env.STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
secretServerKey: process.env.STACK_SECRET_SERVER_KEY,
|
||||
projectId: process.env.HEXCLAVE_PROJECT_ID,
|
||||
publishableClientKey: process.env.HEXCLAVE_PUBLISHABLE_CLIENT_KEY,
|
||||
secretServerKey: process.env.HEXCLAVE_SECRET_SERVER_KEY,
|
||||
tokenStore: "memory",
|
||||
});
|
||||
|
||||
@ -929,9 +929,9 @@ export async function POST(request: Request) {
|
||||
code: `import { StackServerApp } from "@stackframe/js";
|
||||
|
||||
const stackServerApp = new StackServerApp({
|
||||
projectId: process.env.STACK_PROJECT_ID,
|
||||
publishableClientKey: process.env.STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
secretServerKey: process.env.STACK_SECRET_SERVER_KEY,
|
||||
projectId: process.env.HEXCLAVE_PROJECT_ID,
|
||||
publishableClientKey: process.env.HEXCLAVE_PUBLISHABLE_CLIENT_KEY,
|
||||
secretServerKey: process.env.HEXCLAVE_SECRET_SERVER_KEY,
|
||||
tokenStore: "memory",
|
||||
});
|
||||
|
||||
@ -962,9 +962,9 @@ app.post('/api/team-protected', async (req, res) => {
|
||||
code: `import { StackServerApp } from "@stackframe/js";
|
||||
|
||||
const stackServerApp = new StackServerApp({
|
||||
projectId: process.env.STACK_PROJECT_ID,
|
||||
publishableClientKey: process.env.STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
secretServerKey: process.env.STACK_SECRET_SERVER_KEY,
|
||||
projectId: process.env.HEXCLAVE_PROJECT_ID,
|
||||
publishableClientKey: process.env.HEXCLAVE_PUBLISHABLE_CLIENT_KEY,
|
||||
secretServerKey: process.env.HEXCLAVE_SECRET_SERVER_KEY,
|
||||
tokenStore: "memory",
|
||||
});
|
||||
|
||||
|
||||
@ -80,8 +80,8 @@ STACK_SECRET_SERVER_KEY=<your-secret-server-key>`,
|
||||
language: 'JavaScript',
|
||||
framework: 'React',
|
||||
code: `# Store these in environment variables or directly in the client file during development
|
||||
VITE_STACK_PROJECT_ID=<your-project-id>
|
||||
VITE_STACK_PUBLISHABLE_CLIENT_KEY=<your-publishable-client-key>`,
|
||||
VITE_HEXCLAVE_PROJECT_ID=<your-project-id>
|
||||
VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=<your-publishable-client-key>`,
|
||||
highlightLanguage: 'bash',
|
||||
filename: '.env'
|
||||
},
|
||||
@ -172,8 +172,8 @@ export const stackClientApp = new StackClientApp({
|
||||
// import { useNavigate } from "@tanstack/react-router"; // TanStack Router
|
||||
|
||||
export const stackClientApp = new StackClientApp({
|
||||
projectId: process.env.VITE_STACK_PROJECT_ID || "your-project-id",
|
||||
publishableClientKey: process.env.VITE_STACK_PUBLISHABLE_CLIENT_KEY || "your-publishable-client-key",
|
||||
projectId: process.env.VITE_HEXCLAVE_PROJECT_ID || "your-project-id",
|
||||
publishableClientKey: process.env.VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY || "your-publishable-client-key",
|
||||
tokenStore: "cookie",
|
||||
// redirectMethod: { useNavigate }, // Set this for non-Next.js frameworks
|
||||
});`,
|
||||
@ -187,9 +187,9 @@ export const stackClientApp = new StackClientApp({
|
||||
code: `import { StackServerApp } from "@stackframe/js";
|
||||
|
||||
export const stackServerApp = new StackServerApp({
|
||||
projectId: process.env.STACK_PROJECT_ID,
|
||||
publishableClientKey: process.env.STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
secretServerKey: process.env.STACK_SECRET_SERVER_KEY,
|
||||
projectId: process.env.HEXCLAVE_PROJECT_ID,
|
||||
publishableClientKey: process.env.HEXCLAVE_PUBLISHABLE_CLIENT_KEY,
|
||||
secretServerKey: process.env.HEXCLAVE_SECRET_SERVER_KEY,
|
||||
tokenStore: "memory",
|
||||
});`,
|
||||
highlightLanguage: 'typescript',
|
||||
@ -202,8 +202,8 @@ export const stackServerApp = new StackServerApp({
|
||||
code: `import { StackClientApp } from "@stackframe/js";
|
||||
|
||||
export const stackClientApp = new StackClientApp({
|
||||
projectId: process.env.STACK_PROJECT_ID,
|
||||
publishableClientKey: process.env.STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
projectId: process.env.HEXCLAVE_PROJECT_ID,
|
||||
publishableClientKey: process.env.HEXCLAVE_PUBLISHABLE_CLIENT_KEY,
|
||||
tokenStore: "cookie",
|
||||
});`,
|
||||
highlightLanguage: 'typescript',
|
||||
@ -216,9 +216,9 @@ export const stackClientApp = new StackClientApp({
|
||||
code: `import { StackServerApp } from "@stackframe/js";
|
||||
|
||||
export const stackServerApp = new StackServerApp({
|
||||
projectId: process.env.STACK_PROJECT_ID,
|
||||
publishableClientKey: process.env.STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
secretServerKey: process.env.STACK_SECRET_SERVER_KEY,
|
||||
projectId: process.env.HEXCLAVE_PROJECT_ID,
|
||||
publishableClientKey: process.env.HEXCLAVE_PUBLISHABLE_CLIENT_KEY,
|
||||
secretServerKey: process.env.HEXCLAVE_SECRET_SERVER_KEY,
|
||||
tokenStore: "memory",
|
||||
});`,
|
||||
highlightLanguage: 'javascript',
|
||||
@ -231,8 +231,8 @@ export const stackServerApp = new StackServerApp({
|
||||
code: `import { StackClientApp } from "@stackframe/js";
|
||||
|
||||
export const stackClientApp = new StackClientApp({
|
||||
projectId: process.env.STACK_PROJECT_ID,
|
||||
publishableClientKey: process.env.STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
projectId: process.env.HEXCLAVE_PROJECT_ID,
|
||||
publishableClientKey: process.env.HEXCLAVE_PUBLISHABLE_CLIENT_KEY,
|
||||
tokenStore: "cookie",
|
||||
});`,
|
||||
highlightLanguage: 'javascript',
|
||||
|
||||
@ -12,17 +12,17 @@ export const viteExamples = {
|
||||
declare global {
|
||||
interface ImportMeta {
|
||||
env: {
|
||||
VITE_STACK_API_URL: string;
|
||||
VITE_STACK_PROJECT_ID: string;
|
||||
VITE_STACK_PUBLISHABLE_CLIENT_KEY: string;
|
||||
VITE_HEXCLAVE_API_URL: string;
|
||||
VITE_HEXCLAVE_PROJECT_ID: string;
|
||||
VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const stackClientApp = new StackClientApp({
|
||||
baseUrl: import.meta.env.VITE_STACK_API_URL,
|
||||
projectId: import.meta.env.VITE_STACK_PROJECT_ID,
|
||||
publishableClientKey: import.meta.env.VITE_STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
baseUrl: import.meta.env.VITE_HEXCLAVE_API_URL,
|
||||
projectId: import.meta.env.VITE_HEXCLAVE_PROJECT_ID,
|
||||
publishableClientKey: import.meta.env.VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY,
|
||||
tokenStore: "cookie",
|
||||
urls: {
|
||||
oauthCallback: window.location.origin + "/oauth",
|
||||
|
||||
@ -5,6 +5,7 @@ import { ArrowRight, Check, Code, Copy, Play, Send, Settings, Zap } from 'lucide
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { OpenAPIOperation, OpenAPIParameter, OpenAPISchema, OpenAPISpec } from '../../lib/openapi-types';
|
||||
import { resolveSchema } from '../../lib/openapi-utils';
|
||||
import { resolveInlineRenamedEnvVar } from '../../lib/env';
|
||||
import { useAPIPageContext } from './api-page-wrapper';
|
||||
import { Button } from '../mdx/button';
|
||||
|
||||
@ -45,6 +46,9 @@ const HTTP_METHOD_COLORS = {
|
||||
DELETE: 'from-red-500 to-red-600 text-white shadow-red-500/25',
|
||||
} as const;
|
||||
|
||||
function getLocalApiUrlFromEnv(): string | undefined {
|
||||
return resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_API_URL", "NEXT_PUBLIC_STACK_API_URL", process.env.NEXT_PUBLIC_HEXCLAVE_API_URL, process.env.NEXT_PUBLIC_STACK_API_URL);
|
||||
}
|
||||
|
||||
export function EnhancedAPIPage({ document, operations, description }: EnhancedAPIPageProps) {
|
||||
const apiContext = useAPIPageContext();
|
||||
@ -230,7 +234,7 @@ export function EnhancedAPIPage({ document, operations, description }: EnhancedA
|
||||
}
|
||||
// Use local API URL in development, production URL from OpenAPI spec otherwise
|
||||
const defaultBaseUrl = spec?.servers?.[0]?.url || '';
|
||||
const localApiUrl = process.env.NEXT_PUBLIC_HEXCLAVE_API_URL ?? process.env.NEXT_PUBLIC_STACK_API_URL;
|
||||
const localApiUrl = getLocalApiUrlFromEnv();
|
||||
const baseUrl = localApiUrl ? localApiUrl + '/api/v1' : defaultBaseUrl;
|
||||
|
||||
let url = baseUrl + path;
|
||||
@ -439,7 +443,7 @@ function ModernAPIPlayground({
|
||||
const generateCurlCommand = useCallback(() => {
|
||||
// Use local API URL in development, production URL otherwise
|
||||
const defaultBaseUrl = spec.servers?.[0]?.url || '';
|
||||
const localApiUrl = process.env.NEXT_PUBLIC_HEXCLAVE_API_URL ?? process.env.NEXT_PUBLIC_STACK_API_URL;
|
||||
const localApiUrl = getLocalApiUrlFromEnv();
|
||||
const baseUrl = localApiUrl
|
||||
? localApiUrl + '/api/v1'
|
||||
: defaultBaseUrl;
|
||||
@ -492,7 +496,7 @@ function ModernAPIPlayground({
|
||||
const generateJavaScriptCode = useCallback(() => {
|
||||
// Use local API URL in development, production URL otherwise
|
||||
const defaultBaseUrl = spec.servers?.[0]?.url || '';
|
||||
const localApiUrl = process.env.NEXT_PUBLIC_HEXCLAVE_API_URL ?? process.env.NEXT_PUBLIC_STACK_API_URL;
|
||||
const localApiUrl = getLocalApiUrlFromEnv();
|
||||
const baseUrl = localApiUrl
|
||||
? localApiUrl + '/api/v1'
|
||||
: defaultBaseUrl;
|
||||
@ -554,7 +558,7 @@ function ModernAPIPlayground({
|
||||
const generatePythonCode = useCallback(() => {
|
||||
// Use local API URL in development, production URL otherwise
|
||||
const defaultBaseUrl = spec.servers?.[0]?.url || '';
|
||||
const localApiUrl = process.env.NEXT_PUBLIC_HEXCLAVE_API_URL ?? process.env.NEXT_PUBLIC_STACK_API_URL;
|
||||
const localApiUrl = getLocalApiUrlFromEnv();
|
||||
const baseUrl = localApiUrl
|
||||
? localApiUrl + '/api/v1'
|
||||
: defaultBaseUrl;
|
||||
|
||||
@ -6,6 +6,7 @@ import { runAsynchronously } from '@hexclave/shared/dist/utils/promises';
|
||||
import { convertToModelMessages, DefaultChatTransport, type DynamicToolUIPart } from 'ai';
|
||||
import { ChevronDown, ChevronUp, ExternalLink, FileText, Maximize2, Minimize2, Send, X } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { resolveInlineRenamedEnvVar } from '../../lib/env';
|
||||
import { useSidebar } from '../layouts/sidebar-context';
|
||||
import { MessageFormatter } from './message-formatter';
|
||||
|
||||
@ -351,7 +352,7 @@ export function AIChatDrawer() {
|
||||
const height = isHomePage && isScrolled ? 'h-[calc(100vh-1.5rem)]' : 'h-[calc(100vh-1.5rem)]';
|
||||
|
||||
const [input, setInput] = useState('');
|
||||
const apiBaseUrl = process.env.NEXT_PUBLIC_HEXCLAVE_API_URL ?? process.env.NEXT_PUBLIC_STACK_API_URL ?? throwErr("NEXT_PUBLIC_HEXCLAVE_API_URL or NEXT_PUBLIC_STACK_API_URL is not set");
|
||||
const apiBaseUrl = resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_API_URL", "NEXT_PUBLIC_STACK_API_URL", process.env.NEXT_PUBLIC_HEXCLAVE_API_URL, process.env.NEXT_PUBLIC_STACK_API_URL) || throwErr("NEXT_PUBLIC_HEXCLAVE_API_URL or NEXT_PUBLIC_STACK_API_URL is not set");
|
||||
|
||||
const {
|
||||
messages,
|
||||
|
||||
6
docs/src/lib/env.ts
Normal file
6
docs/src/lib/env.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export function resolveInlineRenamedEnvVar(hexclaveName: string, stackName: string, hexclaveValue: string | undefined, stackValue: string | undefined): string | undefined {
|
||||
if (hexclaveValue && stackValue && hexclaveValue !== stackValue) {
|
||||
throw new Error(`Environment variables ${hexclaveName} and ${stackName} are both set to different values. Remove one of them or set them to the same value.`);
|
||||
}
|
||||
return hexclaveValue || stackValue || undefined;
|
||||
}
|
||||
@ -1,13 +1,14 @@
|
||||
import { StackServerApp } from '@hexclave/next';
|
||||
import "server-only";
|
||||
import { resolveInlineRenamedEnvVar } from './lib/env';
|
||||
|
||||
// Explicitly configure Stack Auth for docs app
|
||||
export const stackServerApp = new StackServerApp({
|
||||
tokenStore: "nextjs-cookie",
|
||||
projectId: process.env.NEXT_PUBLIC_HEXCLAVE_PROJECT_ID ?? process.env.NEXT_PUBLIC_STACK_PROJECT_ID,
|
||||
publishableClientKey: process.env.NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY ?? process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
secretServerKey: process.env.HEXCLAVE_SECRET_SERVER_KEY ?? process.env.STACK_SECRET_SERVER_KEY,
|
||||
baseUrl: process.env.NEXT_PUBLIC_HEXCLAVE_API_URL ?? process.env.NEXT_PUBLIC_STACK_API_URL,
|
||||
projectId: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_PROJECT_ID", "NEXT_PUBLIC_STACK_PROJECT_ID", process.env.NEXT_PUBLIC_HEXCLAVE_PROJECT_ID, process.env.NEXT_PUBLIC_STACK_PROJECT_ID),
|
||||
publishableClientKey: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY", "NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY", process.env.NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY, process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY),
|
||||
secretServerKey: resolveInlineRenamedEnvVar("HEXCLAVE_SECRET_SERVER_KEY", "STACK_SECRET_SERVER_KEY", process.env.HEXCLAVE_SECRET_SERVER_KEY, process.env.STACK_SECRET_SERVER_KEY),
|
||||
baseUrl: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_API_URL", "NEXT_PUBLIC_STACK_API_URL", process.env.NEXT_PUBLIC_HEXCLAVE_API_URL, process.env.NEXT_PUBLIC_STACK_API_URL),
|
||||
analytics: {
|
||||
replays: {
|
||||
enabled: true,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Contains the credentials for the internal project of Stack's default development environment setup.
|
||||
# Do not use in a production environment, instead replace it with actual values gathered from https://app.hexclave.com.
|
||||
NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
STACK_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
HEXCLAVE_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
# Contains the credentials for the internal project of Stack's default development environment setup.
|
||||
# Do not use in a production environment, instead replace it with actual values gathered from https://app.hexclave.com.
|
||||
NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
STACK_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
HEXCLAVE_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
|
||||
CONVEX_DEPLOYMENT=anonymous:anonymous-convex-example
|
||||
NEXT_PUBLIC_CONVEX_URL=http://127.0.0.1:3210
|
||||
|
||||
@ -1,8 +1,19 @@
|
||||
import { getConvexProvidersConfig } from "@hexclave/next/convex-auth.config";
|
||||
|
||||
function throwErr(message: string): never {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
function resolveRenamedEnvVar(hexclaveName: string, stackName: string, hexclaveValue: string | undefined, stackValue: string | undefined): string | undefined {
|
||||
if (hexclaveValue && stackValue && hexclaveValue !== stackValue) {
|
||||
throw new Error(`Environment variables ${hexclaveName} and ${stackName} are both set to different values. Remove one of them or set them to the same value.`);
|
||||
}
|
||||
return hexclaveValue || stackValue || undefined;
|
||||
}
|
||||
|
||||
export default {
|
||||
providers: getConvexProvidersConfig({
|
||||
projectId: process.env.NEXT_PUBLIC_STACK_PROJECT_ID!,
|
||||
baseUrl: process.env.NEXT_PUBLIC_STACK_API_URL,
|
||||
projectId: resolveRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_PROJECT_ID", "NEXT_PUBLIC_STACK_PROJECT_ID", process.env.NEXT_PUBLIC_HEXCLAVE_PROJECT_ID, process.env.NEXT_PUBLIC_STACK_PROJECT_ID) ?? throwErr("NEXT_PUBLIC_HEXCLAVE_PROJECT_ID or NEXT_PUBLIC_STACK_PROJECT_ID must be set"),
|
||||
baseUrl: resolveRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_API_URL", "NEXT_PUBLIC_STACK_API_URL", process.env.NEXT_PUBLIC_HEXCLAVE_API_URL, process.env.NEXT_PUBLIC_STACK_API_URL),
|
||||
}),
|
||||
}
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
NEXT_PUBLIC_STACK_API_URL=# enter your stack endpoint here, e.g. http://localhost:8102
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=# enter your stack project id here
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=# enter your stack publishable client key here
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=# enter your stack endpoint here, e.g. http://localhost:8102
|
||||
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=# enter your stack project id here
|
||||
NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=# enter your stack publishable client key here
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Contains the credentials for the internal project of Stack's default development environment setup.
|
||||
# Do not use in a production environment, instead replace it with actual values gathered from https://app.hexclave.com.
|
||||
NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
STACK_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE=http://{projectId}.localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09/{hostedPath}
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
HEXCLAVE_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_URL_TEMPLATE=http://{projectId}.localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09/{hostedPath}
|
||||
|
||||
@ -2,8 +2,17 @@
|
||||
|
||||
/** Minimal `stack login` flow for local demos. Usage: `node cli-sim.mjs` */
|
||||
|
||||
const API_URL = process.env.STACK_API_URL || "http://localhost:8102";
|
||||
const APP_URL = process.env.STACK_APP_URL || "http://localhost:8103";
|
||||
function resolveHexclaveStackEnvVar(hexclaveName, stackName) {
|
||||
const hexclaveValue = process.env[hexclaveName];
|
||||
const stackValue = process.env[stackName];
|
||||
if (hexclaveValue && stackValue && hexclaveValue !== stackValue) {
|
||||
throw new Error(`Environment variables ${hexclaveName} and ${stackName} are both set to different values. Remove one of them or set them to the same value.`);
|
||||
}
|
||||
return hexclaveValue || stackValue || undefined;
|
||||
}
|
||||
|
||||
const API_URL = resolveHexclaveStackEnvVar("HEXCLAVE_API_URL", "STACK_API_URL") || "http://localhost:8102";
|
||||
const APP_URL = resolveHexclaveStackEnvVar("HEXCLAVE_APP_URL", "STACK_APP_URL") || "http://localhost:8103";
|
||||
const PROJECT_ID = "internal";
|
||||
const PUBLISHABLE_KEY = "this-publishable-client-key-is-for-local-development-only";
|
||||
|
||||
|
||||
@ -66,11 +66,13 @@ function getDebugInternals(app: ReturnType<typeof useStackApp>): {
|
||||
}
|
||||
|
||||
function getDemoApiUrl(): string {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_STACK_API_URL
|
||||
?? process.env.NEXT_PUBLIC_STACK_URL;
|
||||
const baseUrl = process.env.NEXT_PUBLIC_HEXCLAVE_API_URL
|
||||
|| process.env.NEXT_PUBLIC_STACK_API_URL
|
||||
|| process.env.NEXT_PUBLIC_HEXCLAVE_URL
|
||||
|| process.env.NEXT_PUBLIC_STACK_URL;
|
||||
|
||||
if (typeof baseUrl !== "string" || baseUrl.length === 0) {
|
||||
throw new Error("Expected NEXT_PUBLIC_STACK_API_URL to be configured for Turnstile OAuth debug flows");
|
||||
throw new Error("Expected NEXT_PUBLIC_HEXCLAVE_API_URL to be configured for Turnstile OAuth debug flows");
|
||||
}
|
||||
|
||||
return `${baseUrl.replace(/\/$/, "")}/api/v1`;
|
||||
@ -301,9 +303,11 @@ export default function TurnstileSignupPageClient() {
|
||||
const signInWithTokens = internals.signInWithTokens;
|
||||
const apiUrl = getDemoApiUrl();
|
||||
const oauthClientSecret = app[hexclaveAppInternalsSymbol].toClientJson().publishableClientKey
|
||||
?? process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY
|
||||
?? process.env.STACK_PUBLISHABLE_CLIENT_KEY
|
||||
?? publishableClientKeyNotNecessarySentinel;
|
||||
|| process.env.NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY
|
||||
|| process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY
|
||||
|| process.env.HEXCLAVE_PUBLISHABLE_CLIENT_KEY
|
||||
|| process.env.STACK_PUBLISHABLE_CLIENT_KEY
|
||||
|| publishableClientKeyNotNecessarySentinel;
|
||||
|
||||
useEffect(() => {
|
||||
const redirectSource = window.sessionStorage.getItem(authReturnStorageKey);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
NEXT_PUBLIC_STACK_API_URL=# enter your stack endpoint here, e.g. http://localhost:8102
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=# enter your stack project id here
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=# enter your stack publishable client key here
|
||||
STACK_SECRET_SERVER_KEY=# enter your stack secret server key here
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=# enter your stack endpoint here, e.g. http://localhost:8102
|
||||
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=# enter your stack project id here
|
||||
NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=# enter your stack publishable client key here
|
||||
HEXCLAVE_SECRET_SERVER_KEY=# enter your stack secret server key here
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Contains the credentials for the internal project of Stack's default development environment setup.
|
||||
# Do not use in a production environment, instead replace it with actual values gathered from https://app.hexclave.com.
|
||||
NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
STACK_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
HEXCLAVE_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Contains the credentials for the internal project of Stack's default development environment setup.
|
||||
# Do not use in a production environment, instead replace it with actual values gathered from https://app.hexclave.com.
|
||||
NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
STACK_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
HEXCLAVE_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
# Contains the credentials for the internal project of Stack's default development environment setup.
|
||||
# Do not use in a production environment, instead replace it with actual values gathered from https://app.hexclave.com.
|
||||
NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
STACK_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
HEXCLAVE_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
|
||||
VITE_STACK_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
VITE_STACK_PROJECT_ID=internal
|
||||
VITE_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
VITE_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
VITE_HEXCLAVE_PROJECT_ID=internal
|
||||
VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
|
||||
@ -3,9 +3,9 @@
|
||||
import { StackClientApp } from "@hexclave/js";
|
||||
|
||||
export const hexclaveClientApp = new StackClientApp({
|
||||
baseUrl: import.meta.env.VITE_STACK_API_URL,
|
||||
projectId: import.meta.env.VITE_STACK_PROJECT_ID,
|
||||
publishableClientKey: import.meta.env.VITE_STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
baseUrl: import.meta.env.VITE_HEXCLAVE_API_URL || import.meta.env.VITE_STACK_API_URL,
|
||||
projectId: import.meta.env.VITE_HEXCLAVE_PROJECT_ID || import.meta.env.VITE_STACK_PROJECT_ID,
|
||||
publishableClientKey: import.meta.env.VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY || import.meta.env.VITE_STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
tokenStore: "cookie",
|
||||
urls: {
|
||||
oauthCallback: window.location.origin + "/oauth",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Contains the credentials for the internal project of Stack's default development environment setup.
|
||||
# Do not use in a production environment, instead replace it with actual values gathered from https://app.hexclave.com.
|
||||
|
||||
VITE_STACK_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
VITE_STACK_PROJECT_ID=internal
|
||||
VITE_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
VITE_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
VITE_HEXCLAVE_PROJECT_ID=internal
|
||||
VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
|
||||
@ -3,9 +3,9 @@ import { useNavigate } from "react-router-dom";
|
||||
|
||||
export const hexclaveClientApp = new StackClientApp({
|
||||
tokenStore: "cookie",
|
||||
baseUrl: import.meta.env.VITE_STACK_API_URL,
|
||||
projectId: import.meta.env.VITE_STACK_PROJECT_ID,
|
||||
publishableClientKey: import.meta.env.VITE_STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
baseUrl: import.meta.env.VITE_HEXCLAVE_API_URL || import.meta.env.VITE_STACK_API_URL,
|
||||
projectId: import.meta.env.VITE_HEXCLAVE_PROJECT_ID || import.meta.env.VITE_STACK_PROJECT_ID,
|
||||
publishableClientKey: import.meta.env.VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY || import.meta.env.VITE_STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
redirectMethod: {
|
||||
useNavigate,
|
||||
},
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
NEXT_PUBLIC_STACK_API_URL=# enter your stack endpoint here, e.g. http://localhost:8102
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=# enter your stack project id here
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=# enter your stack publishable client key here
|
||||
STACK_SECRET_SERVER_KEY=# enter your stack secret server key here
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=# enter your stack endpoint here, e.g. http://localhost:8102
|
||||
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=# enter your stack project id here
|
||||
NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=# enter your stack publishable client key here
|
||||
HEXCLAVE_SECRET_SERVER_KEY=# enter your stack secret server key here
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Contains the credentials for the internal project of Stack's default development environment setup.
|
||||
# Do not use in a production environment, instead replace it with actual values gathered from https://app.hexclave.com.
|
||||
NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
STACK_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
HEXCLAVE_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Contains the credentials for the internal project of Stack's default development environment setup.
|
||||
# Do not use in a production environment, instead replace it with actual values gathered from https://app.hexclave.com.
|
||||
|
||||
VITE_STACK_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
VITE_STACK_PROJECT_ID=internal
|
||||
VITE_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
VITE_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
VITE_HEXCLAVE_PROJECT_ID=internal
|
||||
VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
|
||||
@ -4,9 +4,9 @@ import { StackClientApp } from "@hexclave/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export const hexclaveClientApp = new StackClientApp({
|
||||
projectId: import.meta.env.VITE_STACK_PROJECT_ID,
|
||||
publishableClientKey: import.meta.env.VITE_STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
baseUrl: import.meta.env.VITE_STACK_API_URL,
|
||||
projectId: import.meta.env.VITE_HEXCLAVE_PROJECT_ID || import.meta.env.VITE_STACK_PROJECT_ID,
|
||||
publishableClientKey: import.meta.env.VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY || import.meta.env.VITE_STACK_PUBLISHABLE_CLIENT_KEY,
|
||||
baseUrl: import.meta.env.VITE_HEXCLAVE_API_URL || import.meta.env.VITE_STACK_API_URL,
|
||||
tokenStore: "cookie",
|
||||
redirectMethod: {
|
||||
useNavigate,
|
||||
|
||||
@ -4,7 +4,7 @@ SUPABASE_JWT_SECRET=supabase-jwt-secret
|
||||
|
||||
# Contains the credentials for the internal project of Stack's default development environment setup.
|
||||
# Do not use in a production environment, instead replace it with actual values gathered from https://app.hexclave.com.
|
||||
NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
STACK_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=internal
|
||||
NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
HEXCLAVE_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
VITE_STACK_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
VITE_STACK_PROJECT_ID=internal
|
||||
VITE_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
VITE_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
VITE_HEXCLAVE_PROJECT_ID=internal
|
||||
VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
|
||||
@ -9,7 +9,7 @@ function replaceHexclavePortPrefix(value: string): string {
|
||||
}
|
||||
|
||||
function getStackApiUrl(): string {
|
||||
const configured = import.meta.env.VITE_STACK_API_URL as string | undefined;
|
||||
const configured = (import.meta.env.VITE_HEXCLAVE_API_URL || import.meta.env.VITE_STACK_API_URL) as string | undefined;
|
||||
return configured ? replaceHexclavePortPrefix(configured) : `http://localhost:${getPortPrefix()}02`;
|
||||
}
|
||||
|
||||
|
||||
@ -93,11 +93,28 @@ describe("resolveProjectId", () => {
|
||||
expect(resolveProjectId(undefined)).toBe("proj_from_env");
|
||||
});
|
||||
|
||||
it("uses the HEXCLAVE_PROJECT_ID env var", () => {
|
||||
process.env.HEXCLAVE_PROJECT_ID = "proj_from_hexclave_env";
|
||||
expect(resolveProjectId(undefined)).toBe("proj_from_hexclave_env");
|
||||
});
|
||||
|
||||
it("throws when the Hexclave and Stack env vars disagree", () => {
|
||||
process.env.HEXCLAVE_PROJECT_ID = "proj_from_hexclave_env";
|
||||
process.env.STACK_PROJECT_ID = "proj_from_stack_env";
|
||||
expect(() => resolveProjectId(undefined)).toThrow(/HEXCLAVE_PROJECT_ID.*STACK_PROJECT_ID.*different values/);
|
||||
});
|
||||
|
||||
it("prefers the option over the env var", () => {
|
||||
process.env.STACK_PROJECT_ID = "proj_from_env";
|
||||
expect(resolveProjectId("proj_from_flag")).toBe("proj_from_flag");
|
||||
});
|
||||
|
||||
it("does not inspect conflicting env vars when the option is present", () => {
|
||||
process.env.HEXCLAVE_PROJECT_ID = "proj_from_hexclave_env";
|
||||
process.env.STACK_PROJECT_ID = "proj_from_stack_env";
|
||||
expect(resolveProjectId("proj_from_flag")).toBe("proj_from_flag");
|
||||
});
|
||||
|
||||
it("treats an empty option string as absent and falls back to the env var", () => {
|
||||
process.env.STACK_PROJECT_ID = "proj_from_env";
|
||||
expect(resolveProjectId("")).toBe("proj_from_env");
|
||||
|
||||
@ -31,13 +31,13 @@ export type ProjectAuth = (ProjectAuthWithRefreshToken | ProjectAuthWithSecretSe
|
||||
};
|
||||
|
||||
function resolveApiUrl(): string {
|
||||
return process.env.STACK_API_URL
|
||||
return resolveHexclaveStackEnvVar("HEXCLAVE_API_URL", "STACK_API_URL")
|
||||
?? readConfigValue("STACK_API_URL")
|
||||
?? DEFAULT_API_URL;
|
||||
}
|
||||
|
||||
function resolveDashboardUrl(): string {
|
||||
return process.env.STACK_DASHBOARD_URL
|
||||
return resolveHexclaveStackEnvVar("HEXCLAVE_DASHBOARD_URL", "STACK_DASHBOARD_URL")
|
||||
?? readConfigValue("STACK_DASHBOARD_URL")
|
||||
?? DEFAULT_DASHBOARD_URL;
|
||||
}
|
||||
@ -51,8 +51,17 @@ function resolveRefreshToken(): string {
|
||||
return token;
|
||||
}
|
||||
|
||||
function resolveHexclaveStackEnvVar(hexclaveName: string, stackName: string): string | undefined {
|
||||
const hexclaveValue = process.env[hexclaveName];
|
||||
const stackValue = process.env[stackName];
|
||||
if (hexclaveValue && stackValue && hexclaveValue !== stackValue) {
|
||||
throw new CliError(`Environment variables ${hexclaveName} and ${stackName} are both set to different values. Remove one of them or set them to the same value.`);
|
||||
}
|
||||
return hexclaveValue || stackValue || undefined;
|
||||
}
|
||||
|
||||
function resolveSecretServerKey(): string | null {
|
||||
return process.env.HEXCLAVE_SECRET_SERVER_KEY ?? process.env.STACK_SECRET_SERVER_KEY ?? null;
|
||||
return resolveHexclaveStackEnvVar("HEXCLAVE_SECRET_SERVER_KEY", "STACK_SECRET_SERVER_KEY") || null;
|
||||
}
|
||||
|
||||
export function resolveLoginConfig(): LoginConfig {
|
||||
@ -91,10 +100,12 @@ export function resolveAuth(projectId: string): ProjectAuth {
|
||||
// STACK_PROJECT_ID name). Empty strings are treated as absent so callers can
|
||||
// pass through optional option values directly.
|
||||
export function resolveProjectId(projectIdOption?: string): string {
|
||||
for (const candidate of [projectIdOption, process.env.HEXCLAVE_PROJECT_ID, process.env.STACK_PROJECT_ID]) {
|
||||
if (candidate != null && candidate !== "") {
|
||||
return candidate;
|
||||
}
|
||||
if (projectIdOption != null && projectIdOption !== "") {
|
||||
return projectIdOption;
|
||||
}
|
||||
const projectIdFromEnv = resolveHexclaveStackEnvVar("HEXCLAVE_PROJECT_ID", "STACK_PROJECT_ID");
|
||||
if (projectIdFromEnv != null && projectIdFromEnv !== "") {
|
||||
return projectIdFromEnv;
|
||||
}
|
||||
throw new CliError("No project ID provided. Pass --cloud-project-id <id> or set the HEXCLAVE_PROJECT_ID environment variable.");
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ export function envPortFirstSet(names: [string, ...string[]], fallback: number):
|
||||
}
|
||||
|
||||
export function emulatorHome(): string {
|
||||
return process.env.STACK_EMULATOR_HOME ?? join(homedir(), ".hexclave", "emulator");
|
||||
return process.env.HEXCLAVE_EMULATOR_HOME ?? process.env.STACK_EMULATOR_HOME ?? join(homedir(), ".hexclave", "emulator");
|
||||
}
|
||||
|
||||
export function emulatorRunDir(): string {
|
||||
|
||||
58
packages/shared/src/utils/env.test.tsx
Normal file
58
packages/shared/src/utils/env.test.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { getEnvVariable, getProcessEnv, resolveHexclaveStackEnvVarValue } from "./env";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
describe("Hexclave/Stack env var dual-read", () => {
|
||||
it("falls back to the legacy Stack name when the Hexclave value is empty", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_API_URL", "");
|
||||
vi.stubEnv("NEXT_PUBLIC_STACK_API_URL", "https://stack.example.test");
|
||||
|
||||
expect(getEnvVariable("NEXT_PUBLIC_STACK_API_URL")).toBe("https://stack.example.test");
|
||||
expect(getProcessEnv("NEXT_PUBLIC_STACK_API_URL")).toBe("https://stack.example.test");
|
||||
});
|
||||
|
||||
it("allows both names when they have the same non-empty value", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_API_URL", "https://api.example.test");
|
||||
vi.stubEnv("NEXT_PUBLIC_STACK_API_URL", "https://api.example.test");
|
||||
|
||||
expect(getEnvVariable("NEXT_PUBLIC_STACK_API_URL")).toBe("https://api.example.test");
|
||||
expect(getProcessEnv("NEXT_PUBLIC_STACK_API_URL")).toBe("https://api.example.test");
|
||||
});
|
||||
|
||||
it("throws when both names are non-empty and different", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_API_URL", "https://hexclave.example.test");
|
||||
vi.stubEnv("NEXT_PUBLIC_STACK_API_URL", "https://stack.example.test");
|
||||
|
||||
expect(() => getEnvVariable("NEXT_PUBLIC_STACK_API_URL")).toThrow(/NEXT_PUBLIC_HEXCLAVE_API_URL.*NEXT_PUBLIC_STACK_API_URL.*different values/);
|
||||
expect(() => getProcessEnv("NEXT_PUBLIC_STACK_API_URL")).toThrow(/NEXT_PUBLIC_HEXCLAVE_API_URL.*NEXT_PUBLIC_STACK_API_URL.*different values/);
|
||||
});
|
||||
|
||||
it("checks renamed legacy aliases when falling back", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_URL", "https://hexclave-url.example.test");
|
||||
vi.stubEnv("NEXT_PUBLIC_STACK_URL", "https://stack-url.example.test");
|
||||
|
||||
expect(() => getEnvVariable("NEXT_PUBLIC_STACK_API_URL")).toThrow(/NEXT_PUBLIC_HEXCLAVE_URL.*NEXT_PUBLIC_STACK_URL.*different values/);
|
||||
});
|
||||
|
||||
it("returns undefined when both names are empty", () => {
|
||||
expect(resolveHexclaveStackEnvVarValue("HEXCLAVE_FOO", "STACK_FOO", "", "")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("falls back to the legacy Stack name when a canonical Hexclave name is looked up", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_STACK_API_URL", "https://stack.example.test");
|
||||
|
||||
// Caller passes the canonical HEXCLAVE_ name but only the legacy value is set.
|
||||
expect(getEnvVariable("NEXT_PUBLIC_HEXCLAVE_API_URL")).toBe("https://stack.example.test");
|
||||
expect(getProcessEnv("NEXT_PUBLIC_HEXCLAVE_API_URL")).toBe("https://stack.example.test");
|
||||
});
|
||||
|
||||
it("throws on a conflict when a canonical Hexclave name is looked up", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_API_URL", "https://hexclave.example.test");
|
||||
vi.stubEnv("NEXT_PUBLIC_STACK_API_URL", "https://stack.example.test");
|
||||
|
||||
expect(() => getEnvVariable("NEXT_PUBLIC_HEXCLAVE_API_URL")).toThrow(/NEXT_PUBLIC_HEXCLAVE_API_URL.*NEXT_PUBLIC_STACK_API_URL.*different values/);
|
||||
});
|
||||
});
|
||||
@ -6,22 +6,37 @@ export function isBrowserLike() {
|
||||
}
|
||||
|
||||
// newName: oldName
|
||||
const ENV_VAR_RENAME: Record<string, string[]> = {
|
||||
const ENV_VAR_RENAME: Record<string, string[] | undefined> = {
|
||||
NEXT_PUBLIC_STACK_API_URL: ['STACK_BASE_URL', 'NEXT_PUBLIC_STACK_URL'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Hexclave rebrand: compute the `HEXCLAVE_*`-prefixed equivalent of a `STACK_*`
|
||||
* env var name by replacing the first `STACK_` occurrence with `HEXCLAVE_`.
|
||||
* Covers `STACK_FOO`, `NEXT_PUBLIC_STACK_FOO`, `NEXT_PUBLIC_BROWSER_STACK_FOO`,
|
||||
* `NEXT_PUBLIC_SERVER_STACK_FOO`, `VITE_STACK_FOO`. Returns `undefined` when the
|
||||
* name has no `STACK_` segment (caller should behave exactly as before).
|
||||
*/
|
||||
function getHexclaveEnvVarName(name: string): string | undefined {
|
||||
if (!name.includes("STACK_")) {
|
||||
return undefined;
|
||||
export function resolveHexclaveStackEnvVarValue(hexclaveName: string, stackName: string, hexclaveValue: string | undefined, stackValue: string | undefined): string | undefined {
|
||||
if (hexclaveValue && stackValue && hexclaveValue !== stackValue) {
|
||||
throw new Error(`Environment variables ${hexclaveName} and ${stackName} are both set to different values. Remove one of them or set them to the same value.`);
|
||||
}
|
||||
return name.replace("STACK_", "HEXCLAVE_");
|
||||
return hexclaveValue || stackValue || undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hexclave rebrand: resolve an env var by reading both the `HEXCLAVE_*` and
|
||||
* `STACK_*` spellings, preferring the canonical Hexclave value and falling back
|
||||
* to the legacy Stack value (empty counts as unset). Works in BOTH directions —
|
||||
* whether the caller passes the legacy `STACK_FOO` name or the canonical
|
||||
* `HEXCLAVE_FOO` name, the other spelling is still honored. Covers `STACK_FOO`,
|
||||
* `NEXT_PUBLIC_STACK_FOO`, `NEXT_PUBLIC_BROWSER_STACK_FOO`,
|
||||
* `NEXT_PUBLIC_SERVER_STACK_FOO`, `VITE_STACK_FOO` and their HEXCLAVE_ twins.
|
||||
* Names with neither segment behave exactly as before.
|
||||
*/
|
||||
function getEnvVarWithHexclaveFallback(name: string): string | undefined {
|
||||
if (name.includes("STACK_")) {
|
||||
const hexclaveName = name.replace("STACK_", "HEXCLAVE_");
|
||||
return resolveHexclaveStackEnvVarValue(hexclaveName, name, process.env[hexclaveName], process.env[name]);
|
||||
}
|
||||
if (name.includes("HEXCLAVE_")) {
|
||||
const stackName = name.replace("HEXCLAVE_", "STACK_");
|
||||
return resolveHexclaveStackEnvVarValue(name, stackName, process.env[name], process.env[stackName]);
|
||||
}
|
||||
return process.env[name];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -45,21 +60,21 @@ export function getEnvVariable(name: string, defaultValue?: string | undefined):
|
||||
|
||||
// throw error if the old name is used as the retrieve key
|
||||
for (const [newName, oldNames] of Object.entries(ENV_VAR_RENAME)) {
|
||||
if (oldNames.includes(name)) {
|
||||
if (oldNames?.includes(name)) {
|
||||
throwErr(`Environment variable ${name} has been renamed to ${newName}. Please update your configuration to use the new name.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Hexclave rebrand: prefer the HEXCLAVE_*-prefixed equivalent, fall back to the STACK_* name.
|
||||
const hexclaveName = getHexclaveEnvVarName(name);
|
||||
let value = (hexclaveName ? process.env[hexclaveName] : undefined) ?? process.env[name];
|
||||
// Treat the empty string as unset — the checked-in .env templates define empty
|
||||
// HEXCLAVE_* placeholders, which must not shadow a real value under the legacy name.
|
||||
let value = getEnvVarWithHexclaveFallback(name);
|
||||
|
||||
// check the key under the old name if the new name is not found
|
||||
if (!value && ENV_VAR_RENAME[name] as any) {
|
||||
for (const oldName of ENV_VAR_RENAME[name]) {
|
||||
// Hexclave rebrand: also accept the HEXCLAVE_*-prefixed equivalent of each old alias.
|
||||
const hexclaveOldName = getHexclaveEnvVarName(oldName);
|
||||
value = (hexclaveOldName ? process.env[hexclaveOldName] : undefined) ?? process.env[oldName];
|
||||
const renamedNames = ENV_VAR_RENAME[name];
|
||||
if (!value && renamedNames != null) {
|
||||
for (const oldName of renamedNames) {
|
||||
value = getEnvVarWithHexclaveFallback(oldName);
|
||||
if (value) break;
|
||||
}
|
||||
}
|
||||
@ -112,6 +127,7 @@ export function getProcessEnv(name: string): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
// Hexclave rebrand: prefer the HEXCLAVE_*-prefixed equivalent, fall back to the STACK_* name.
|
||||
const hexclaveName = getHexclaveEnvVarName(name);
|
||||
return (hexclaveName ? process.env[hexclaveName] : undefined) ?? process.env[name];
|
||||
// Empty counts as unset — the checked-in .env templates define empty HEXCLAVE_* placeholders,
|
||||
// which must not shadow a real value under the legacy name.
|
||||
return getEnvVarWithHexclaveFallback(name);
|
||||
}
|
||||
|
||||
@ -80,7 +80,12 @@ export class HexclaveAssertionError extends Error {
|
||||
|
||||
// Use literal dot-form (guarded with `typeof process`) so Next.js / webpack
|
||||
// DefinePlugin can inline the value at build time. See getProcessEnv in ./env.
|
||||
if ((typeof process !== "undefined" ? process.env.NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR : undefined) === "true") {
|
||||
const hexclaveDebuggerValue = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_HEXCLAVE_DEBUGGER_ON_ASSERTION_ERROR : undefined;
|
||||
const stackDebuggerValue = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR : undefined;
|
||||
if (hexclaveDebuggerValue && stackDebuggerValue && hexclaveDebuggerValue !== stackDebuggerValue) {
|
||||
throw new Error("Environment variables NEXT_PUBLIC_HEXCLAVE_DEBUGGER_ON_ASSERTION_ERROR and NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR are both set to different values. Remove one of them or set them to the same value.");
|
||||
}
|
||||
if ((hexclaveDebuggerValue || stackDebuggerValue) === "true") {
|
||||
debugger;
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,17 +63,31 @@ const envVarsConfig: Record<string, { allowPublic?: boolean, deprecatedLegacyNam
|
||||
},
|
||||
};
|
||||
|
||||
function getEnvVarSnippet(variableName: string) {
|
||||
return deindent`
|
||||
((typeof process !== "undefined" ? process.env.${variableName} : undefined) || import.meta.env?.${variableName})
|
||||
`;
|
||||
}
|
||||
|
||||
function generateEnvVarsConstSnippet() {
|
||||
const getters: string[] = [];
|
||||
for (const [key, config] of Object.entries(envVarsConfig)) {
|
||||
const allVariables = [key, ...(config.deprecatedLegacyNames ?? [])]
|
||||
.flatMap(k => k.startsWith("HEXCLAVE_") ? [k, k.replace("HEXCLAVE_", "STACK_")] : [k])
|
||||
.flatMap(k => config.allowPublic ? [k, `NEXT_PUBLIC_${k}`, `VITE_${k}`] : [k]);
|
||||
const allVariables = [...new Set(
|
||||
[key, ...(config.deprecatedLegacyNames ?? [])]
|
||||
.flatMap(k => k.startsWith("HEXCLAVE_") ? [k, k.replace("HEXCLAVE_", "STACK_")] : [k])
|
||||
.flatMap(k => config.allowPublic ? [k, `NEXT_PUBLIC_${k}`, `VITE_${k}`] : [k])
|
||||
)];
|
||||
// Prefer the canonical HEXCLAVE_* spelling, falling back to the legacy STACK_*
|
||||
// name. Use || (not ??) between candidates so an empty-string env var (e.g. an
|
||||
// empty HEXCLAVE_* placeholder) can't shadow a real value further down the chain.
|
||||
//
|
||||
// IMPORTANT: this getter ships in the customer SDK, so it must NOT throw when
|
||||
// both spellings are set — reading an env var is a hot, side-effect-free path
|
||||
// for SDK consumers. Conflict detection lives only in our own (non-shipped)
|
||||
// env helpers (packages/shared, dashboard inline env, CLI), never here.
|
||||
getters.push(deindent`
|
||||
get ${key}() {
|
||||
return ${allVariables.map(variableName => deindent`
|
||||
((typeof process !== "undefined" ? process.env.${variableName} : undefined) ?? import.meta.env?.${variableName})
|
||||
`).join("\n ?? ")} ?? undefined;
|
||||
return ${allVariables.map(getEnvVarSnippet).join("\n || ")} || undefined;
|
||||
},
|
||||
`);
|
||||
}
|
||||
|
||||
@ -134,13 +134,13 @@ function consumeOAuthCallbackQueryParams(options?: {
|
||||
// If the state can't be found in the cookies, then the callback wasn't meant for us.
|
||||
// Maybe the website uses another OAuth library?
|
||||
console.warn(deindent`
|
||||
Stack found an outer OAuth callback state in the query parameters, but not in cookies.
|
||||
Hexclave found an outer OAuth callback state in the query parameters, but not in cookies.
|
||||
|
||||
This could have multiple reasons:
|
||||
- The cookie expired, because the OAuth flow took too long.
|
||||
- The user's browser deleted the cookie, either manually or because of a very strict cookie policy.
|
||||
- The cookie was already consumed by this page, and the user already logged in.
|
||||
- You are using another OAuth client library with the same callback URL as Stack.
|
||||
- You are using another OAuth client library with the same callback URL as Hexclave.
|
||||
- The user opened the OAuth callback page from their history.
|
||||
|
||||
Either way, it is probably safe to ignore this warning unless you are debugging an OAuth issue.
|
||||
|
||||
@ -81,7 +81,7 @@ describe("handler URL targets", () => {
|
||||
});
|
||||
|
||||
it("uses hosted defaults for unspecified URLs", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX", ".example-stack-hosted.test");
|
||||
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_DOMAIN_SUFFIX", ".example-stack-hosted.test");
|
||||
|
||||
const urls = resolveHandlerUrls({
|
||||
projectId: "project-id",
|
||||
@ -97,7 +97,7 @@ describe("handler URL targets", () => {
|
||||
});
|
||||
|
||||
it("keeps redirect-only post-auth targets local even when the default target is hosted", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX", ".example-stack-hosted.test");
|
||||
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_DOMAIN_SUFFIX", ".example-stack-hosted.test");
|
||||
|
||||
const urls = resolveHandlerUrls({
|
||||
projectId: "project-id",
|
||||
@ -141,7 +141,7 @@ describe("handler URL targets", () => {
|
||||
});
|
||||
|
||||
it("inherits a hosted default target for the OAuth callback", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX", ".example-stack-hosted.test");
|
||||
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_DOMAIN_SUFFIX", ".example-stack-hosted.test");
|
||||
|
||||
const urls = resolveHandlerUrls({
|
||||
projectId: "project-id",
|
||||
@ -179,7 +179,7 @@ describe("handler URL targets", () => {
|
||||
});
|
||||
|
||||
it("uses default target for unknown /handler/* pages", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX", ".example-stack-hosted.test");
|
||||
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_DOMAIN_SUFFIX", ".example-stack-hosted.test");
|
||||
|
||||
const url = resolveUnknownHandlerPathFallbackUrl({
|
||||
defaultTarget: { type: "hosted" },
|
||||
@ -191,7 +191,7 @@ describe("handler URL targets", () => {
|
||||
});
|
||||
|
||||
it("uses the full hosted handler URL template when configured", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE", "http://{projectId}.localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09/{hostedPath}");
|
||||
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_URL_TEMPLATE", "http://{projectId}.localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09/{hostedPath}");
|
||||
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX", "93");
|
||||
|
||||
const urls = resolveHandlerUrls({
|
||||
@ -206,7 +206,7 @@ describe("handler URL targets", () => {
|
||||
});
|
||||
|
||||
it("validates the hosted handler URL template placeholders", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE", "http://localhost:9309/{projectId}/handler");
|
||||
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_URL_TEMPLATE", "http://localhost:9309/{projectId}/handler");
|
||||
|
||||
expect(() => resolveHandlerUrls({
|
||||
projectId: "project-id",
|
||||
@ -217,7 +217,7 @@ describe("handler URL targets", () => {
|
||||
});
|
||||
|
||||
it("rejects hosted handler URL templates that put the project ID in the path", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE", "http://localhost:9309/{projectId}/{hostedPath}");
|
||||
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_URL_TEMPLATE", "http://localhost:9309/{projectId}/{hostedPath}");
|
||||
|
||||
expect(() => resolveHandlerUrls({
|
||||
projectId: "project-id",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user