mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-30 21:01:54 +08:00
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 -->
336 lines
13 KiB
TypeScript
336 lines
13 KiB
TypeScript
import { generateSecureRandomString } from "@hexclave/shared/dist/utils/crypto";
|
|
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
|
|
import { HexclaveAssertionError } from "@hexclave/shared/dist/utils/errors";
|
|
import { filterUndefined, omit } from "@hexclave/shared/dist/utils/objects";
|
|
import { ignoreUnhandledRejection, wait } from "@hexclave/shared/dist/utils/promises";
|
|
import { Nicifiable } from "@hexclave/shared/dist/utils/strings";
|
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
// eslint-disable-next-line no-restricted-imports
|
|
import { afterEach, beforeEach, onTestFailed, test as vitestTest } from "vitest";
|
|
|
|
export const test: typeof vitestTest = vitestTest.extend({});
|
|
export const it: typeof vitestTest = test;
|
|
|
|
export const afterTestFinishesCallbacks: (() => Promise<void>)[] = [];
|
|
|
|
export function logIfTestFails(...args: any[]) {
|
|
onTestFailed(() => {
|
|
console.error(...args);
|
|
});
|
|
}
|
|
|
|
export function runAsynchronouslyBeforeTestFinishes(callback: () => Promise<void>) {
|
|
const promise = callback();
|
|
ignoreUnhandledRejection(promise);
|
|
afterTestFinishesCallbacks.push(async () => await promise);
|
|
}
|
|
|
|
export function beforeTestFinishes(callback: () => Promise<void>) {
|
|
afterTestFinishesCallbacks.push(callback);
|
|
}
|
|
|
|
export class Context<R, T> {
|
|
// we want to retain order in which the values were set instead of the order in which the beforeEach callback was called, so we keep a Map and a Set together here
|
|
private _values = new Map<string, T>();
|
|
private _yetToReduce = new Set<string>();
|
|
private _deleteOnFinish = new Set<string>();
|
|
|
|
private _reduced: R | undefined;
|
|
private _withStorage: AsyncLocalStorage<T[]> = new AsyncLocalStorage();
|
|
private _isInTest = false;
|
|
|
|
constructor(private readonly _getInitialValue: () => R, private readonly _reducer: (acc: R, value: T) => R) {
|
|
beforeEach(async () => {
|
|
if (this._isInTest) {
|
|
throw new HexclaveAssertionError("beforeEach was called twice without a single afterEach! Are you running tests concurrently? This is not supported by withContext.");
|
|
}
|
|
if (this._withStorage.getStore()) {
|
|
throw new HexclaveAssertionError("Did you wrap an entire test into Context.with(...)?");
|
|
}
|
|
this._reduced = this._getInitialValue();
|
|
this._isInTest = true;
|
|
if (this._yetToReduce.size > 0) {
|
|
throw new HexclaveAssertionError("Something went wrong; _yetToReduce should be empty here.");
|
|
}
|
|
});
|
|
afterEach(async () => {
|
|
if (this._withStorage.getStore()) {
|
|
throw new HexclaveAssertionError("Test finished before _withStorage was cleaned up! This should not happen.");
|
|
}
|
|
this._isInTest = false;
|
|
this._reduced = undefined;
|
|
for (const key of this._deleteOnFinish) {
|
|
this._yetToReduce.delete(key);
|
|
}
|
|
if (this._yetToReduce.size > 0) {
|
|
throw new HexclaveAssertionError("Something went wrong; _yetToReduce should be empty here.");
|
|
}
|
|
});
|
|
}
|
|
|
|
async with<X>(value: T, callback: () => Promise<X>) {
|
|
const oldWithStorage = this._withStorage.getStore() ?? [];
|
|
return await this._withStorage.run([...oldWithStorage, value], async () => {
|
|
return await callback();
|
|
});
|
|
}
|
|
|
|
set(value: T) {
|
|
const randomId = generateSecureRandomString();
|
|
this._values.set(randomId, value);
|
|
const before = () => {
|
|
if (this._yetToReduce.has(randomId)) {
|
|
throw new HexclaveAssertionError("Value setter was called twice without a single afterEach! Are you running tests concurrently? This is not supported by withContext.");
|
|
}
|
|
this._yetToReduce.add(randomId);
|
|
};
|
|
if (this._isInTest) {
|
|
before();
|
|
this._deleteOnFinish.add(randomId);
|
|
} else {
|
|
beforeEach(async () => {
|
|
before();
|
|
});
|
|
afterEach(() => {
|
|
this._yetToReduce.delete(randomId);
|
|
});
|
|
}
|
|
}
|
|
|
|
get value(): R {
|
|
this._reduce();
|
|
const _withStore = this._withStorage.getStore() ?? [];
|
|
return _withStore.reduce((acc, val) => this._reducer(acc, val), this._reduced as R);
|
|
}
|
|
|
|
private _reduce() {
|
|
if (!this._isInTest) {
|
|
throw new HexclaveAssertionError("You can only call this function on Context inside a test.");
|
|
}
|
|
const yetToReduceOrdered = [...this._values.entries()].filter(([key]) => this._yetToReduce.has(key)).map(([, value]) => value);
|
|
for (const value of yetToReduceOrdered) {
|
|
this._reduced = this._reducer(this._reduced as R, value);
|
|
}
|
|
this._yetToReduce = new Set();
|
|
}
|
|
}
|
|
|
|
export function updateCookie(cookieString: string, cookieName: string, cookieValue: string) {
|
|
const cookies = cookieString.split(";").map((cookie) => cookie.trim()).filter((cookie) => cookie.length > 0);
|
|
const newCookie = `${cookieName}=${cookieValue}`;
|
|
const cookieIndex = cookies.findIndex((cookie) => cookie.startsWith(`${cookieName}=`));
|
|
if (cookieIndex === -1) {
|
|
return `${cookieString}; ${newCookie}`;
|
|
}
|
|
cookies[cookieIndex] = newCookie;
|
|
return cookies.join("; ");
|
|
}
|
|
|
|
export function updateCookiesFromResponse(cookieString: string, update: NiceResponse) {
|
|
const setCookies = update.headers.getSetCookie();
|
|
for (const setCookie of setCookies) {
|
|
const [cookieName, cookieValue] = setCookie.split(";")[0].split("=");
|
|
cookieString = updateCookie(cookieString, cookieName, cookieValue);
|
|
}
|
|
return cookieString;
|
|
}
|
|
|
|
export class NiceResponse implements Nicifiable {
|
|
constructor(
|
|
public readonly status: number,
|
|
public readonly headers: Headers,
|
|
public readonly body: any,
|
|
public readonly fromRequestInit?: NiceRequestInit,
|
|
) { }
|
|
|
|
getNicifiableKeys(): string[] {
|
|
// reorder the keys for nicer printing
|
|
return [
|
|
"status",
|
|
...this.body instanceof ArrayBuffer && this.body.byteLength === 0 ? [] : ["body"],
|
|
"headers",
|
|
];
|
|
}
|
|
|
|
async follow(options?: NiceRequestInit) {
|
|
if (![301, 302, 303, 307, 308].includes(this.status)) {
|
|
throw new HexclaveAssertionError(`Cannot follow non-redirect response: ${this.status}`);
|
|
}
|
|
const location = this.headers.get("Location");
|
|
if (!location) {
|
|
throw new HexclaveAssertionError(`Redirect response has no Location header: ${this.status}`);
|
|
}
|
|
const followRes = await niceFetch(location, {
|
|
...[301, 302, 303].includes(this.status) ? { method: "GET" } : {
|
|
body: this.fromRequestInit?.body,
|
|
method: this.fromRequestInit?.method,
|
|
headers: this.fromRequestInit?.headers,
|
|
},
|
|
...options,
|
|
});
|
|
return followRes;
|
|
}
|
|
};
|
|
|
|
export type NiceRequestInit = RequestInit & {
|
|
query?: Record<string, string>,
|
|
};
|
|
|
|
export async function niceFetch(url: string | URL, options?: NiceRequestInit): Promise<NiceResponse> {
|
|
if (options?.query) {
|
|
url = new URL(url);
|
|
for (const [key, value] of Object.entries(options.query)) {
|
|
url.searchParams.append(key, value);
|
|
}
|
|
}
|
|
const fetchRes = await fetch(url, {
|
|
...options,
|
|
headers: {
|
|
"x-stack-disable-artificial-development-delay": "yes",
|
|
"x-stack-development-disable-extended-logging": "yes",
|
|
...filterUndefined(options?.headers ?? {}),
|
|
},
|
|
});
|
|
let body;
|
|
if (fetchRes.headers.get("content-type")?.includes("application/json")) {
|
|
body = await fetchRes.json();
|
|
} else if (fetchRes.headers.get("content-type")?.startsWith("text/")) {
|
|
body = await fetchRes.text();
|
|
} else {
|
|
body = await fetchRes.arrayBuffer();
|
|
}
|
|
return new NiceResponse(fetchRes.status, fetchRes.headers, body, options);
|
|
}
|
|
|
|
export const localRedirectUrl = "http://stack-test.localhost/some-callback-url";
|
|
export const localRedirectUrlRegex = /http:\/\/stack-test\.localhost\/some-callback-url([?#][A-Za-z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]*)?/g;
|
|
|
|
export const generatedEmailSuffix = "@stack-generated.example.com";
|
|
export const generatedEmailRegex = /[a-zA-Z0-9_.+\-]+@stack-generated\.example\.com/;
|
|
|
|
export class Mailbox {
|
|
public readonly fetchMessages: (options?: { noBody?: boolean }) => Promise<MailboxMessage[]>;
|
|
public readonly waitForMessagesWithSubject: (subject: string, options?: { noBody?: boolean }) => Promise<MailboxMessage[]>;
|
|
public readonly waitForMessagesWithSubjectCount: (subject: string, minCount: number, options?: { noBody?: boolean }) => Promise<MailboxMessage[]>;
|
|
|
|
constructor(
|
|
disclaimer: "USE_CREATE_MAILBOX_FUNCTION_INSTEAD",
|
|
public readonly emailAddress: string,
|
|
) {
|
|
const mailboxName = emailAddress.split("@")[0];
|
|
const fullMessageCache = new Map<string, any>();
|
|
|
|
this.fetchMessages = async ({ noBody } = {}) => {
|
|
const res = await niceFetch(new URL(`/api/v1/mailbox/${encodeURIComponent(mailboxName)}`, STACK_INBUCKET_API_URL));
|
|
return await Promise.all((res.body as any[]).map(async (message) => {
|
|
let fullMessage: any;
|
|
if (fullMessageCache.has(message.id)) {
|
|
fullMessage = fullMessageCache.get(message.id);
|
|
} else {
|
|
const fullMessageRes = await niceFetch(new URL(`/api/v1/mailbox/${encodeURIComponent(mailboxName)}/${message.id}`, STACK_INBUCKET_API_URL));
|
|
fullMessage = fullMessageRes.body;
|
|
fullMessageCache.set(message.id, fullMessage);
|
|
}
|
|
const messagePart = noBody ? omit(fullMessage, ["body", "attachments"]) : fullMessage;
|
|
return new MailboxMessage(messagePart);
|
|
}));
|
|
};
|
|
|
|
this.waitForMessagesWithSubject = async (subject: string, options?: { noBody?: boolean }) => {
|
|
return await this.waitForMessagesWithSubjectCount(subject, 1, options);
|
|
};
|
|
|
|
this.waitForMessagesWithSubjectCount = async (subject: string, minCount: number, options?: { noBody?: boolean }) => {
|
|
const timeoutMs = Number(getEnvVariable("STACK_MAILBOX_WAIT_TIMEOUT_MS", "60000"));
|
|
const intervalMs = 500;
|
|
const deadline = Date.now() + timeoutMs;
|
|
let messages: MailboxMessage[] = [];
|
|
while (true) {
|
|
messages = await this.fetchMessages(options);
|
|
const withSubject = messages.filter(m => m.subject.includes(subject));
|
|
if (withSubject.length >= minCount) {
|
|
return withSubject;
|
|
}
|
|
if (Date.now() >= deadline) break;
|
|
await wait(intervalMs);
|
|
}
|
|
throw new HexclaveAssertionError(`Expected at least ${minCount} messages with subject containing "${subject}", but found ${messages.filter(m => m.subject.includes(subject)).length}`, { messages });
|
|
};
|
|
}
|
|
}
|
|
|
|
export class MailboxMessage {
|
|
declare public readonly subject: string;
|
|
declare public readonly from: string;
|
|
declare public readonly to: string;
|
|
declare public readonly date: string;
|
|
declare public readonly id: string;
|
|
declare public readonly size: number;
|
|
declare public readonly seen: boolean;
|
|
declare public readonly "posix-millis": number;
|
|
declare public readonly header?: any;
|
|
declare public readonly body?: { text: string, html: string };
|
|
declare public readonly attachments?: any[];
|
|
|
|
constructor(json: any) {
|
|
Object.assign(this, json);
|
|
}
|
|
|
|
getSnapshotSerializerOptions() {
|
|
return ({
|
|
stripFields: [],
|
|
hideFields: [
|
|
"posix-millis",
|
|
"header",
|
|
"date",
|
|
"mailbox",
|
|
"id",
|
|
"size",
|
|
"seen",
|
|
],
|
|
});
|
|
};
|
|
}
|
|
|
|
|
|
function expandHexclavePortPrefix(value?: string | null) {
|
|
if (!value) return value ?? undefined;
|
|
const prefix = getEnvVariable("NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX", "81");
|
|
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_") || key.startsWith("HEXCLAVE_") || key.startsWith("NEXT_PUBLIC_HEXCLAVE_")) {
|
|
const replaced = expandHexclavePortPrefix(value ?? undefined);
|
|
if (replaced !== undefined) {
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
process.env[key] = replaced;
|
|
}
|
|
}
|
|
}
|
|
export const STACK_DASHBOARD_BASE_URL = getEnvVariable("STACK_DASHBOARD_BASE_URL");
|
|
export const STACK_BACKEND_BASE_URL = getEnvVariable("STACK_BACKEND_BASE_URL");
|
|
export const STACK_MCP_BASE_URL = getEnvVariable("STACK_MCP_BASE_URL");
|
|
|
|
/**
|
|
* The `baseUrl` to pass to SDK constructors (`StackClientApp`, `StackServerApp`,
|
|
* `StackAdminApp`) in JS-SDK e2e tests.
|
|
*
|
|
* Normally this is `STACK_BACKEND_BASE_URL` (single, explicit URL).
|
|
*
|
|
* In the e2e-fallback-tests workflow (`STACK_TEST_SDK_FALLBACK=true`) we leave
|
|
* this `undefined` so the SDK resolves the base URL from
|
|
* `NEXT_PUBLIC_STACK_API_URL` *and* appends its hardcoded fallback URL list,
|
|
* which is what the workflow exercises by running the backend only on the
|
|
* fallback port. Always thread this through to SDK constructors instead of
|
|
* hardcoding `STACK_BACKEND_BASE_URL`.
|
|
*/
|
|
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");
|
|
export const STACK_INTERNAL_PROJECT_CLIENT_KEY = getEnvVariable("STACK_INTERNAL_PROJECT_CLIENT_KEY");
|
|
export const STACK_INTERNAL_PROJECT_SERVER_KEY = getEnvVariable("STACK_INTERNAL_PROJECT_SERVER_KEY");
|
|
export const STACK_INTERNAL_PROJECT_ADMIN_KEY = getEnvVariable("STACK_INTERNAL_PROJECT_ADMIN_KEY");
|
|
export const STACK_INBUCKET_API_URL = getEnvVariable("STACK_INBUCKET_API_URL");
|
|
export const STACK_SVIX_SERVER_URL = getEnvVariable("STACK_SVIX_SERVER_URL");
|