From 01ca85027c59e5c52028b9f30011da6eeb11e27d Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Thu, 12 Mar 2026 20:05:13 -0700 Subject: [PATCH] client sdk local emulator --- .../internal/local-emulator/project/route.tsx | 5 +- apps/backend/src/lib/local-emulator.ts | 4 ++ docker/server/entrypoint.sh | 12 +++-- .../src/interface/admin-interface.ts | 11 +++- .../src/interface/client-interface.ts | 18 ++++++- .../src/interface/server-interface.ts | 11 +++- .../apps/implementations/admin-app-impl.ts | 28 +++++++--- .../apps/implementations/client-app-impl.ts | 25 +++++++-- .../stack-app/apps/implementations/common.ts | 53 +++++++++++++++++-- .../apps/implementations/server-app-impl.ts | 24 +++++++-- .../stack-app/apps/interfaces/client-app.ts | 7 +++ 11 files changed, 169 insertions(+), 29 deletions(-) diff --git a/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx b/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx index 0ad1ea4f8..116fb0fe8 100644 --- a/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx +++ b/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx @@ -141,7 +141,7 @@ async function getOrCreateCredentials(projectId: string) { }, }); - if (!keySet.secretServerKey || !keySet.superSecretAdminKey) { + if (!keySet.publishableClientKey || !keySet.secretServerKey || !keySet.superSecretAdminKey) { throw new StackAssertionError("Local emulator key set is missing required keys.", { projectId, keySetId: keySet.id, @@ -149,6 +149,7 @@ async function getOrCreateCredentials(projectId: string) { } return { + publishableClientKey: keySet.publishableClientKey, secretServerKey: keySet.secretServerKey, superSecretAdminKey: keySet.superSecretAdminKey, }; @@ -178,6 +179,7 @@ export const POST = createSmartRouteHandler({ bodyType: yupString().oneOf(["json"]).defined(), body: yupObject({ project_id: yupString().defined(), + publishable_client_key: yupString().defined(), secret_server_key: yupString().defined(), super_secret_admin_key: yupString().defined(), branch_config_override_string: yupString().defined(), @@ -222,6 +224,7 @@ export const POST = createSmartRouteHandler({ bodyType: "json" as const, body: { project_id: projectId, + publishable_client_key: credentials.publishableClientKey, secret_server_key: credentials.secretServerKey, super_secret_admin_key: credentials.superSecretAdminKey, branch_config_override_string: JSON.stringify(fileConfig), diff --git a/apps/backend/src/lib/local-emulator.ts b/apps/backend/src/lib/local-emulator.ts index ec9b27f29..63bac7684 100644 --- a/apps/backend/src/lib/local-emulator.ts +++ b/apps/backend/src/lib/local-emulator.ts @@ -6,6 +6,10 @@ import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; import { StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { globalPrismaClient } from "@/prisma-client"; +export const LOCAL_EMULATOR_INTERNAL_PUBLISHABLE_CLIENT_KEY = "local-emulator-publishable-client-key"; +export const LOCAL_EMULATOR_INTERNAL_SECRET_SERVER_KEY = "local-emulator-secret-server-key"; +export const LOCAL_EMULATOR_INTERNAL_SUPER_SECRET_ADMIN_KEY = "local-emulator-super-secret-admin-key"; + export const LOCAL_EMULATOR_ADMIN_USER_ID = "63abbc96-5329-454a-ba56-e0460173c6c1"; export const LOCAL_EMULATOR_OWNER_TEAM_ID = "5a0c858b-d9e9-49d4-9943-8ce385d86428"; export const LOCAL_EMULATOR_ADMIN_EMAIL = "local-emulator@stack-auth.com"; diff --git a/docker/server/entrypoint.sh b/docker/server/entrypoint.sh index 1b598b1c4..7a74bb64f 100644 --- a/docker/server/entrypoint.sh +++ b/docker/server/entrypoint.sh @@ -11,9 +11,15 @@ fi # ============= ENV VARS ============= -export STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=${STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY:-$(openssl rand -base64 32)} -export STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=${STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY:-$(openssl rand -base64 32)} -export STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY:-$(openssl rand -base64 32)} +if [ "$NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR" = "true" ]; then + export STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=${STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY:-local-emulator-publishable-client-key} + export STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=${STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY:-local-emulator-secret-server-key} + export STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY:-local-emulator-super-secret-admin-key} +else + export STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=${STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY:-$(openssl rand -base64 32)} + export STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=${STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY:-$(openssl rand -base64 32)} + export STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY:-$(openssl rand -base64 32)} +fi export NEXT_PUBLIC_STACK_PROJECT_ID=internal export NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=${STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY} diff --git a/packages/stack-shared/src/interface/admin-interface.ts b/packages/stack-shared/src/interface/admin-interface.ts index d73004ff9..e213e40cf 100644 --- a/packages/stack-shared/src/interface/admin-interface.ts +++ b/packages/stack-shared/src/interface/admin-interface.ts @@ -57,17 +57,26 @@ export type InternalApiKeyCreateCrudResponse = InternalApiKeysCrud["Admin"]["Rea export class StackAdminInterface extends StackServerInterface { + protected _superSecretAdminKeyOverride?: string; + constructor(public readonly options: AdminAuthApplicationOptions) { super(options); } + override _updateEmulatorCredentials(opts: { projectId?: string, publishableClientKey?: string, secretServerKey?: string, superSecretAdminKey?: string }) { + super._updateEmulatorCredentials(opts); + if (opts.superSecretAdminKey) { + this._superSecretAdminKeyOverride = opts.superSecretAdminKey; + } + } + public async sendAdminRequest(path: string, options: RequestInit, session: InternalSession | null, requestType: "admin" = "admin") { return await this.sendServerRequest( path, { ...options, headers: { - "x-stack-super-secret-admin-key": "superSecretAdminKey" in this.options ? this.options.superSecretAdminKey : "", + "x-stack-super-secret-admin-key": this._superSecretAdminKeyOverride ?? ("superSecretAdminKey" in this.options ? this.options.superSecretAdminKey : ""), ...options.headers, }, }, diff --git a/packages/stack-shared/src/interface/client-interface.ts b/packages/stack-shared/src/interface/client-interface.ts index eefd2f6f3..6a27dc613 100644 --- a/packages/stack-shared/src/interface/client-interface.ts +++ b/packages/stack-shared/src/interface/client-interface.ts @@ -50,12 +50,24 @@ export type ClientInterfaceOptions = { export class StackClientInterface { private pendingNetworkDiagnostics?: ReturnType; + protected _projectIdOverride?: string; + protected _publishableClientKeyOverride?: string; + constructor(public readonly options: ClientInterfaceOptions) { // nothing here } get projectId() { - return this.options.projectId; + return this._projectIdOverride ?? this.options.projectId; + } + + _updateEmulatorCredentials(opts: { projectId?: string, publishableClientKey?: string }) { + if (opts.projectId) { + this._projectIdOverride = opts.projectId; + } + if (opts.publishableClientKey) { + this._publishableClientKeyOverride = opts.publishableClientKey; + } } getApiUrl() { @@ -397,7 +409,9 @@ export class StackClientInterface { "X-Stack-Refresh-Token": tokenObj.refreshToken.token, } : {}), "X-Stack-Allow-Anonymous-User": "true", - ...("publishableClientKey" in this.options && this.options.publishableClientKey ? { + ...(this._publishableClientKeyOverride ? { + "X-Stack-Publishable-Client-Key": this._publishableClientKeyOverride, + } : "publishableClientKey" in this.options && this.options.publishableClientKey ? { "X-Stack-Publishable-Client-Key": this.options.publishableClientKey, } : {}), ...(adminTokenObj ? { diff --git a/packages/stack-shared/src/interface/server-interface.ts b/packages/stack-shared/src/interface/server-interface.ts index 68c4b9059..7bd2d63f0 100644 --- a/packages/stack-shared/src/interface/server-interface.ts +++ b/packages/stack-shared/src/interface/server-interface.ts @@ -39,17 +39,26 @@ export type ServerAuthApplicationOptions = ( ); export class StackServerInterface extends StackClientInterface { + protected _secretServerKeyOverride?: string; + constructor(public override options: ServerAuthApplicationOptions) { super(options); } + override _updateEmulatorCredentials(opts: { projectId?: string, publishableClientKey?: string, secretServerKey?: string }) { + super._updateEmulatorCredentials(opts); + if (opts.secretServerKey) { + this._secretServerKeyOverride = opts.secretServerKey; + } + } + protected async sendServerRequest(path: string, options: RequestInit, session: InternalSession | null, requestType: "server" | "admin" = "server") { return await this.sendClientRequest( path, { ...options, headers: { - "x-stack-secret-server-key": "secretServerKey" in this.options ? this.options.secretServerKey : "", + "x-stack-secret-server-key": this._secretServerKeyOverride ?? ("secretServerKey" in this.options ? this.options.secretServerKey : ""), ...options.headers, }, }, diff --git a/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts b/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts index e939e0897..e85bad857 100644 --- a/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts +++ b/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts @@ -22,7 +22,7 @@ import { AdminProjectPermission, AdminProjectPermissionDefinition, AdminProjectP import { AdminOwnedProject, AdminProject, AdminProjectUpdateOptions, PushConfigOptions, adminProjectUpdateOptionsToCrud } from "../../projects"; import type { AdminSessionReplay, AdminSessionReplayChunk, ListSessionReplayChunksOptions, ListSessionReplayChunksResult, ListSessionReplaysOptions, ListSessionReplaysResult, SessionReplayAllEventsResult } from "../../session-replays"; import { ManagedEmailProviderListItem, ManagedEmailProviderSetupResult, ManagedEmailProviderStatus, EmailOutboxUpdateOptions, StackAdminApp, StackAdminAppConstructorOptions } from "../interfaces/admin-app"; -import { clientVersion, createCache, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getDefaultSecretServerKey, getDefaultSuperSecretAdminKey, resolveConstructorOptions } from "./common"; +import { LOCAL_EMULATOR_INTERNAL_PUBLISHABLE_CLIENT_KEY, clientVersion, createCache, fetchEmulatorProjectCredentials, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getDefaultSecretServerKey, getDefaultSuperSecretAdminKey, getLocalEmulatorConfigFilePath, resolveConstructorOptions } from "./common"; import { _StackServerAppImplIncomplete } from "./server-app-impl"; import { CompleteConfig, EnvironmentConfigOverrideOverride } from "@stackframe/stack-shared/dist/config/schema"; @@ -128,24 +128,40 @@ export class _StackAdminAppImplIncomplete, extraOptions?: { uniqueIdentifier?: string, checkString?: string, interface?: StackAdminInterface }) { const resolvedOptions = resolveConstructorOptions(options); - const publishableClientKey = resolvedOptions.publishableClientKey ?? getDefaultPublishableClientKey(); + + const emulatorConfigFilePath = getLocalEmulatorConfigFilePath(resolvedOptions.localEmulatorConfigFilePath); + const isEmulator = !!emulatorConfigFilePath; + + const publishableClientKey = resolvedOptions.publishableClientKey ?? getDefaultPublishableClientKey() ?? (isEmulator ? LOCAL_EMULATOR_INTERNAL_PUBLISHABLE_CLIENT_KEY : undefined); super(resolvedOptions, { ...extraOptions, interface: extraOptions?.interface ?? new StackAdminInterface({ - getBaseUrl: () => getBaseUrl(resolvedOptions.baseUrl), - projectId: resolvedOptions.projectId ?? getDefaultProjectId(), + getBaseUrl: () => getBaseUrl(resolvedOptions.baseUrl, { isEmulator }), + projectId: resolvedOptions.projectId ?? getDefaultProjectId({ isEmulator }), extraRequestHeaders: resolvedOptions.extraRequestHeaders ?? getDefaultExtraRequestHeaders(), clientVersion, ...resolvedOptions.projectOwnerSession ? { projectOwnerSession: resolvedOptions.projectOwnerSession, } : { ...(publishableClientKey ? { publishableClientKey } : {}), - secretServerKey: resolvedOptions.secretServerKey ?? getDefaultSecretServerKey(), - superSecretAdminKey: resolvedOptions.superSecretAdminKey ?? getDefaultSuperSecretAdminKey(), + secretServerKey: resolvedOptions.secretServerKey ?? getDefaultSecretServerKey({ isEmulator }), + superSecretAdminKey: resolvedOptions.superSecretAdminKey ?? getDefaultSuperSecretAdminKey({ isEmulator }), }, }), }); + + if (isEmulator && !extraOptions?.interface) { + const iface = this._interface; + this._emulatorInitPromise = fetchEmulatorProjectCredentials(emulatorConfigFilePath!).then((data) => { + iface._updateEmulatorCredentials({ + projectId: data.project_id, + publishableClientKey: data.publishable_client_key, + secretServerKey: data.secret_server_key, + superSecretAdminKey: data.super_secret_admin_key, + }); + }); + } } _adminConfigFromCrud(data: { config_string: string }): CompleteConfig { diff --git a/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts b/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts index cfefb40ad..eae9b15cd 100644 --- a/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts +++ b/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts @@ -52,7 +52,7 @@ import { EditableTeamMemberProfile, ReceivedTeamInvitation, SentTeamInvitation, import { ActiveSession, Auth, BaseUser, CurrentUser, InternalUserExtra, OAuthProvider, ProjectCurrentUser, SyncedPartialUser, TokenPartialUser, UserExtra, UserUpdateOptions, userUpdateOptionsToCrud, withUserDestructureGuard } from "../../users"; import { StackClientApp, StackClientAppConstructorOptions, StackClientAppJson } from "../interfaces/client-app"; import { _StackAdminAppImplIncomplete } from "./admin-app-impl"; -import { TokenObject, clientVersion, createCache, createCacheBySession, createEmptyTokenStore, getAnalyticsBaseUrl, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getUrls, resolveConstructorOptions } from "./common"; +import { LOCAL_EMULATOR_INTERNAL_PUBLISHABLE_CLIENT_KEY, TokenObject, clientVersion, createCache, createCacheBySession, createEmptyTokenStore, fetchEmulatorProjectCredentials, getAnalyticsBaseUrl, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getLocalEmulatorConfigFilePath, getUrls, resolveConstructorOptions } from "./common"; import { EventTracker } from "./event-tracker"; import { AnalyticsOptions, SessionRecorder, analyticsOptionsFromJson, analyticsOptionsToJson } from "./session-replay"; @@ -101,6 +101,7 @@ export class _StackClientAppImplIncomplete | null = null; private __DEMO_ENABLE_SLIGHT_FETCH_DELAY = false; private readonly _ownedAdminApps = new DependenciesMap<[InternalSession, string], _StackAdminAppImplIncomplete>(); @@ -499,29 +500,43 @@ export class _StackClientAppImplIncomplete getBaseUrl(resolvedOptions.baseUrl), - getAnalyticsBaseUrl: () => getAnalyticsBaseUrl(getBaseUrl(resolvedOptions.baseUrl)), + getBaseUrl: () => getBaseUrl(resolvedOptions.baseUrl, { isEmulator }), + getAnalyticsBaseUrl: () => getAnalyticsBaseUrl(getBaseUrl(resolvedOptions.baseUrl, { isEmulator })), extraRequestHeaders: resolvedOptions.extraRequestHeaders ?? getDefaultExtraRequestHeaders(), projectId, clientVersion, ...(publishableClientKey != null ? { publishableClientKey } : {}), prepareRequest: async () => { + if (this._emulatorInitPromise) await this._emulatorInitPromise; await cookies?.(); // THIS_LINE_PLATFORM next } }); } + if (isEmulator && !(extraOptions && extraOptions.interface)) { + const iface = this._interface; + this._emulatorInitPromise = fetchEmulatorProjectCredentials(emulatorConfigFilePath!).then((data) => { + iface._updateEmulatorCredentials({ + projectId: data.project_id, + publishableClientKey: data.publishable_client_key, + }); + }); + } + this._tokenStoreInit = resolvedOptions.tokenStore; this._redirectMethod = resolvedOptions.redirectMethod || "none"; this._redirectMethod = resolvedOptions.redirectMethod || "nextjs"; // THIS_LINE_PLATFORM next diff --git a/packages/template/src/lib/stack-app/apps/implementations/common.ts b/packages/template/src/lib/stack-app/apps/implementations/common.ts index 7faa08f44..213792cf5 100644 --- a/packages/template/src/lib/stack-app/apps/implementations/common.ts +++ b/packages/template/src/lib/stack-app/apps/implementations/common.ts @@ -81,7 +81,44 @@ export function getUrls(partial: Partial): HandlerUrls { }; } -export function getDefaultProjectId() { +export const localEmulatorBaseUrl = "http://localhost:9999"; + +export const LOCAL_EMULATOR_INTERNAL_PUBLISHABLE_CLIENT_KEY = "local-emulator-publishable-client-key"; +export const LOCAL_EMULATOR_INTERNAL_SECRET_SERVER_KEY = "local-emulator-secret-server-key"; +export const LOCAL_EMULATOR_INTERNAL_SUPER_SECRET_ADMIN_KEY = "local-emulator-super-secret-admin-key"; + +export function getLocalEmulatorConfigFilePath(explicitOption?: string): string | undefined { + return explicitOption || process.env.NEXT_PUBLIC_STACK_LOCAL_EMULATOR_CONFIG_FILE_PATH || undefined; +} + +export function fetchEmulatorProjectCredentials(emulatorConfigFilePath: string): Promise<{ + project_id: string, + publishable_client_key: string, + secret_server_key: string, + super_secret_admin_key: string, +}> { + return (async () => { + const res = await fetch(`${localEmulatorBaseUrl}/api/v1/internal/local-emulator/project`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Stack-Project-Id": "internal", + "X-Stack-Access-Type": "client", + "X-Stack-Publishable-Client-Key": LOCAL_EMULATOR_INTERNAL_PUBLISHABLE_CLIENT_KEY, + }, + body: JSON.stringify({ absolute_file_path: emulatorConfigFilePath }), + }); + if (!res.ok) { + throw new Error(`Failed to initialize local emulator: ${res.status} ${await res.text()}`); + } + return await res.json(); + })(); +} + +export function getDefaultProjectId(options?: { isEmulator?: boolean }) { + if (options?.isEmulator) { + return process.env.NEXT_PUBLIC_STACK_PROJECT_ID || process.env.STACK_PROJECT_ID || "internal"; + } return process.env.NEXT_PUBLIC_STACK_PROJECT_ID || process.env.STACK_PROJECT_ID || throwErr(new Error("Welcome to Stack Auth! It seems that you haven't provided a project ID. Please create a project on the Stack dashboard at https://app.stack-auth.com and put it in the NEXT_PUBLIC_STACK_PROJECT_ID environment variable.")); } @@ -89,11 +126,17 @@ export function getDefaultPublishableClientKey() { return process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY || process.env.STACK_PUBLISHABLE_CLIENT_KEY; } -export function getDefaultSecretServerKey() { +export function getDefaultSecretServerKey(options?: { isEmulator?: boolean }) { + if (options?.isEmulator) { + return process.env.STACK_SECRET_SERVER_KEY || LOCAL_EMULATOR_INTERNAL_SECRET_SERVER_KEY; + } return process.env.STACK_SECRET_SERVER_KEY || throwErr(new Error("No secret server key provided. Please copy your key from the Stack dashboard and put it in the STACK_SECRET_SERVER_KEY environment variable.")); } -export function getDefaultSuperSecretAdminKey() { +export function getDefaultSuperSecretAdminKey(options?: { isEmulator?: boolean }) { + if (options?.isEmulator) { + return process.env.STACK_SUPER_SECRET_ADMIN_KEY || LOCAL_EMULATOR_INTERNAL_SUPER_SECRET_ADMIN_KEY; + } return process.env.STACK_SUPER_SECRET_ADMIN_KEY || throwErr(new Error("No super secret admin key provided. Please copy your key from the Stack dashboard and put it in the STACK_SUPER_SECRET_ADMIN_KEY environment variable.")); } @@ -119,7 +162,7 @@ export function getDefaultExtraRequestHeaders() { * @returns The configured base URL without trailing slash */ -export function getBaseUrl(userSpecifiedBaseUrl: string | { browser: string, server: string } | undefined) { +export function getBaseUrl(userSpecifiedBaseUrl: string | { browser: string, server: string } | undefined, options?: { isEmulator?: boolean }) { let url; if (userSpecifiedBaseUrl) { if (typeof userSpecifiedBaseUrl === "string") { @@ -138,7 +181,7 @@ export function getBaseUrl(userSpecifiedBaseUrl: string | { browser: string, ser } else { url = process.env.NEXT_PUBLIC_SERVER_STACK_API_URL || process.env.NEXT_PUBLIC_STACK_API_URL_SERVER || process.env.STACK_API_URL_SERVER; } - url = url || process.env.NEXT_PUBLIC_STACK_API_URL || process.env.STACK_API_URL || process.env.NEXT_PUBLIC_STACK_URL || defaultBaseUrl; + url = url || process.env.NEXT_PUBLIC_STACK_API_URL || process.env.STACK_API_URL || process.env.NEXT_PUBLIC_STACK_URL || (options?.isEmulator ? localEmulatorBaseUrl : defaultBaseUrl); } return replaceStackPortPrefix(url.endsWith('/') ? url.slice(0, -1) : url); diff --git a/packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts b/packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts index e5928d960..1850c563b 100644 --- a/packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts +++ b/packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts @@ -35,7 +35,7 @@ import { EditableTeamMemberProfile, ReceivedTeamInvitation, SentTeamInvitation, import { ProjectCurrentServerUser, ServerOAuthProvider, ServerUser, ServerUserCreateOptions, ServerUserUpdateOptions, serverUserCreateOptionsToCrud, serverUserUpdateOptionsToCrud, withUserDestructureGuard } from "../../users"; import { StackServerAppConstructorOptions } from "../interfaces/server-app"; import { _StackClientAppImplIncomplete } from "./client-app-impl"; -import { clientVersion, createCache, createCacheBySession, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getDefaultSecretServerKey, resolveConstructorOptions } from "./common"; +import { LOCAL_EMULATOR_INTERNAL_PUBLISHABLE_CLIENT_KEY, clientVersion, createCache, createCacheBySession, fetchEmulatorProjectCredentials, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getDefaultSecretServerKey, getLocalEmulatorConfigFilePath, resolveConstructorOptions } from "./common"; import { useAsyncCache } from "./common"; // THIS_LINE_PLATFORM react-like @@ -410,19 +410,33 @@ export class _StackServerAppImplIncomplete, extraOptions?: { uniqueIdentifier?: string, checkString?: string, interface?: StackServerInterface }) { const resolvedOptions = resolveConstructorOptions(options); - const publishableClientKey = resolvedOptions.publishableClientKey ?? getDefaultPublishableClientKey(); + const emulatorConfigFilePath = getLocalEmulatorConfigFilePath(resolvedOptions.localEmulatorConfigFilePath); + const isEmulator = !!emulatorConfigFilePath; + + const publishableClientKey = resolvedOptions.publishableClientKey ?? getDefaultPublishableClientKey() ?? (isEmulator ? LOCAL_EMULATOR_INTERNAL_PUBLISHABLE_CLIENT_KEY : undefined); super(resolvedOptions, { ...extraOptions, interface: extraOptions?.interface ?? new StackServerInterface({ - getBaseUrl: () => getBaseUrl(resolvedOptions.baseUrl), - projectId: resolvedOptions.projectId ?? getDefaultProjectId(), + getBaseUrl: () => getBaseUrl(resolvedOptions.baseUrl, { isEmulator }), + projectId: resolvedOptions.projectId ?? getDefaultProjectId({ isEmulator }), extraRequestHeaders: resolvedOptions.extraRequestHeaders ?? getDefaultExtraRequestHeaders(), clientVersion, ...(publishableClientKey != null ? { publishableClientKey } : {}), - secretServerKey: resolvedOptions.secretServerKey ?? getDefaultSecretServerKey(), + secretServerKey: resolvedOptions.secretServerKey ?? getDefaultSecretServerKey({ isEmulator }), }), }); + + if (isEmulator && !extraOptions?.interface) { + const iface = this._interface; + this._emulatorInitPromise = fetchEmulatorProjectCredentials(emulatorConfigFilePath!).then((data) => { + iface._updateEmulatorCredentials({ + projectId: data.project_id, + publishableClientKey: data.publishable_client_key, + secretServerKey: data.secret_server_key, + }); + }); + } } diff --git a/packages/template/src/lib/stack-app/apps/interfaces/client-app.ts b/packages/template/src/lib/stack-app/apps/interfaces/client-app.ts index 74e7c7bae..31888ba2d 100644 --- a/packages/template/src/lib/stack-app/apps/interfaces/client-app.ts +++ b/packages/template/src/lib/stack-app/apps/interfaces/client-app.ts @@ -19,6 +19,13 @@ export type StackClientAppConstructorOptions, + /** + * Path to the local emulator config file. When set, connects to the local + * emulator and automatically fetches project credentials. + * Defaults to NEXT_PUBLIC_STACK_LOCAL_EMULATOR_CONFIG_FILE_PATH env var. + */ + localEmulatorConfigFilePath?: string, + /** * By default, the Stack app will automatically prefetch some data from Stack's server when this app is first * constructed. This improves the performance of your app, but will create network requests that are unnecessary if