Skip to content

Commit 01ca850

Browse files
committed
client sdk local emulator
1 parent 612cb71 commit 01ca850

11 files changed

Lines changed: 169 additions & 29 deletions

File tree

apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,15 @@ async function getOrCreateCredentials(projectId: string) {
141141
},
142142
});
143143

144-
if (!keySet.secretServerKey || !keySet.superSecretAdminKey) {
144+
if (!keySet.publishableClientKey || !keySet.secretServerKey || !keySet.superSecretAdminKey) {
145145
throw new StackAssertionError("Local emulator key set is missing required keys.", {
146146
projectId,
147147
keySetId: keySet.id,
148148
});
149149
}
150150

151151
return {
152+
publishableClientKey: keySet.publishableClientKey,
152153
secretServerKey: keySet.secretServerKey,
153154
superSecretAdminKey: keySet.superSecretAdminKey,
154155
};
@@ -178,6 +179,7 @@ export const POST = createSmartRouteHandler({
178179
bodyType: yupString().oneOf(["json"]).defined(),
179180
body: yupObject({
180181
project_id: yupString().defined(),
182+
publishable_client_key: yupString().defined(),
181183
secret_server_key: yupString().defined(),
182184
super_secret_admin_key: yupString().defined(),
183185
branch_config_override_string: yupString().defined(),
@@ -222,6 +224,7 @@ export const POST = createSmartRouteHandler({
222224
bodyType: "json" as const,
223225
body: {
224226
project_id: projectId,
227+
publishable_client_key: credentials.publishableClientKey,
225228
secret_server_key: credentials.secretServerKey,
226229
super_secret_admin_key: credentials.superSecretAdminKey,
227230
branch_config_override_string: JSON.stringify(fileConfig),

apps/backend/src/lib/local-emulator.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
66
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
77
import { globalPrismaClient } from "@/prisma-client";
88

9+
export const LOCAL_EMULATOR_INTERNAL_PUBLISHABLE_CLIENT_KEY = "local-emulator-publishable-client-key";
10+
export const LOCAL_EMULATOR_INTERNAL_SECRET_SERVER_KEY = "local-emulator-secret-server-key";
11+
export const LOCAL_EMULATOR_INTERNAL_SUPER_SECRET_ADMIN_KEY = "local-emulator-super-secret-admin-key";
12+
913
export const LOCAL_EMULATOR_ADMIN_USER_ID = "63abbc96-5329-454a-ba56-e0460173c6c1";
1014
export const LOCAL_EMULATOR_OWNER_TEAM_ID = "5a0c858b-d9e9-49d4-9943-8ce385d86428";
1115
export const LOCAL_EMULATOR_ADMIN_EMAIL = "local-emulator@stack-auth.com";

docker/server/entrypoint.sh

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,15 @@ fi
1111

1212
# ============= ENV VARS =============
1313

14-
export STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=${STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY:-$(openssl rand -base64 32)}
15-
export STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=${STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY:-$(openssl rand -base64 32)}
16-
export STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY:-$(openssl rand -base64 32)}
14+
if [ "$NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR" = "true" ]; then
15+
export STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=${STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY:-local-emulator-publishable-client-key}
16+
export STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=${STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY:-local-emulator-secret-server-key}
17+
export STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY:-local-emulator-super-secret-admin-key}
18+
else
19+
export STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=${STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY:-$(openssl rand -base64 32)}
20+
export STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=${STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY:-$(openssl rand -base64 32)}
21+
export STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY:-$(openssl rand -base64 32)}
22+
fi
1723

1824
export NEXT_PUBLIC_STACK_PROJECT_ID=internal
1925
export NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=${STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY}

packages/stack-shared/src/interface/admin-interface.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,26 @@ export type InternalApiKeyCreateCrudResponse = InternalApiKeysCrud["Admin"]["Rea
5757

5858

5959
export class StackAdminInterface extends StackServerInterface {
60+
protected _superSecretAdminKeyOverride?: string;
61+
6062
constructor(public readonly options: AdminAuthApplicationOptions) {
6163
super(options);
6264
}
6365

66+
override _updateEmulatorCredentials(opts: { projectId?: string, publishableClientKey?: string, secretServerKey?: string, superSecretAdminKey?: string }) {
67+
super._updateEmulatorCredentials(opts);
68+
if (opts.superSecretAdminKey) {
69+
this._superSecretAdminKeyOverride = opts.superSecretAdminKey;
70+
}
71+
}
72+
6473
public async sendAdminRequest(path: string, options: RequestInit, session: InternalSession | null, requestType: "admin" = "admin") {
6574
return await this.sendServerRequest(
6675
path,
6776
{
6877
...options,
6978
headers: {
70-
"x-stack-super-secret-admin-key": "superSecretAdminKey" in this.options ? this.options.superSecretAdminKey : "",
79+
"x-stack-super-secret-admin-key": this._superSecretAdminKeyOverride ?? ("superSecretAdminKey" in this.options ? this.options.superSecretAdminKey : ""),
7180
...options.headers,
7281
},
7382
},

packages/stack-shared/src/interface/client-interface.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,24 @@ export type ClientInterfaceOptions = {
5050
export class StackClientInterface {
5151
private pendingNetworkDiagnostics?: ReturnType<StackClientInterface["_runNetworkDiagnosticsInner"]>;
5252

53+
protected _projectIdOverride?: string;
54+
protected _publishableClientKeyOverride?: string;
55+
5356
constructor(public readonly options: ClientInterfaceOptions) {
5457
// nothing here
5558
}
5659

5760
get projectId() {
58-
return this.options.projectId;
61+
return this._projectIdOverride ?? this.options.projectId;
62+
}
63+
64+
_updateEmulatorCredentials(opts: { projectId?: string, publishableClientKey?: string }) {
65+
if (opts.projectId) {
66+
this._projectIdOverride = opts.projectId;
67+
}
68+
if (opts.publishableClientKey) {
69+
this._publishableClientKeyOverride = opts.publishableClientKey;
70+
}
5971
}
6072

6173
getApiUrl() {
@@ -397,7 +409,9 @@ export class StackClientInterface {
397409
"X-Stack-Refresh-Token": tokenObj.refreshToken.token,
398410
} : {}),
399411
"X-Stack-Allow-Anonymous-User": "true",
400-
...("publishableClientKey" in this.options && this.options.publishableClientKey ? {
412+
...(this._publishableClientKeyOverride ? {
413+
"X-Stack-Publishable-Client-Key": this._publishableClientKeyOverride,
414+
} : "publishableClientKey" in this.options && this.options.publishableClientKey ? {
401415
"X-Stack-Publishable-Client-Key": this.options.publishableClientKey,
402416
} : {}),
403417
...(adminTokenObj ? {

packages/stack-shared/src/interface/server-interface.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,26 @@ export type ServerAuthApplicationOptions = (
3939
);
4040

4141
export class StackServerInterface extends StackClientInterface {
42+
protected _secretServerKeyOverride?: string;
43+
4244
constructor(public override options: ServerAuthApplicationOptions) {
4345
super(options);
4446
}
4547

48+
override _updateEmulatorCredentials(opts: { projectId?: string, publishableClientKey?: string, secretServerKey?: string }) {
49+
super._updateEmulatorCredentials(opts);
50+
if (opts.secretServerKey) {
51+
this._secretServerKeyOverride = opts.secretServerKey;
52+
}
53+
}
54+
4655
protected async sendServerRequest(path: string, options: RequestInit, session: InternalSession | null, requestType: "server" | "admin" = "server") {
4756
return await this.sendClientRequest(
4857
path,
4958
{
5059
...options,
5160
headers: {
52-
"x-stack-secret-server-key": "secretServerKey" in this.options ? this.options.secretServerKey : "",
61+
"x-stack-secret-server-key": this._secretServerKeyOverride ?? ("secretServerKey" in this.options ? this.options.secretServerKey : ""),
5362
...options.headers,
5463
},
5564
},

packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { AdminProjectPermission, AdminProjectPermissionDefinition, AdminProjectP
2222
import { AdminOwnedProject, AdminProject, AdminProjectUpdateOptions, PushConfigOptions, adminProjectUpdateOptionsToCrud } from "../../projects";
2323
import type { AdminSessionReplay, AdminSessionReplayChunk, ListSessionReplayChunksOptions, ListSessionReplayChunksResult, ListSessionReplaysOptions, ListSessionReplaysResult, SessionReplayAllEventsResult } from "../../session-replays";
2424
import { ManagedEmailProviderListItem, ManagedEmailProviderSetupResult, ManagedEmailProviderStatus, EmailOutboxUpdateOptions, StackAdminApp, StackAdminAppConstructorOptions } from "../interfaces/admin-app";
25-
import { clientVersion, createCache, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getDefaultSecretServerKey, getDefaultSuperSecretAdminKey, resolveConstructorOptions } from "./common";
25+
import { LOCAL_EMULATOR_INTERNAL_PUBLISHABLE_CLIENT_KEY, clientVersion, createCache, fetchEmulatorProjectCredentials, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getDefaultSecretServerKey, getDefaultSuperSecretAdminKey, getLocalEmulatorConfigFilePath, resolveConstructorOptions } from "./common";
2626
import { _StackServerAppImplIncomplete } from "./server-app-impl";
2727

2828
import { CompleteConfig, EnvironmentConfigOverrideOverride } from "@stackframe/stack-shared/dist/config/schema";
@@ -128,24 +128,40 @@ export class _StackAdminAppImplIncomplete<HasTokenStore extends boolean, Project
128128

129129
constructor(options: StackAdminAppConstructorOptions<HasTokenStore, ProjectId>, extraOptions?: { uniqueIdentifier?: string, checkString?: string, interface?: StackAdminInterface }) {
130130
const resolvedOptions = resolveConstructorOptions(options);
131-
const publishableClientKey = resolvedOptions.publishableClientKey ?? getDefaultPublishableClientKey();
131+
132+
const emulatorConfigFilePath = getLocalEmulatorConfigFilePath(resolvedOptions.localEmulatorConfigFilePath);
133+
const isEmulator = !!emulatorConfigFilePath;
134+
135+
const publishableClientKey = resolvedOptions.publishableClientKey ?? getDefaultPublishableClientKey() ?? (isEmulator ? LOCAL_EMULATOR_INTERNAL_PUBLISHABLE_CLIENT_KEY : undefined);
132136

133137
super(resolvedOptions, {
134138
...extraOptions,
135139
interface: extraOptions?.interface ?? new StackAdminInterface({
136-
getBaseUrl: () => getBaseUrl(resolvedOptions.baseUrl),
137-
projectId: resolvedOptions.projectId ?? getDefaultProjectId(),
140+
getBaseUrl: () => getBaseUrl(resolvedOptions.baseUrl, { isEmulator }),
141+
projectId: resolvedOptions.projectId ?? getDefaultProjectId({ isEmulator }),
138142
extraRequestHeaders: resolvedOptions.extraRequestHeaders ?? getDefaultExtraRequestHeaders(),
139143
clientVersion,
140144
...resolvedOptions.projectOwnerSession ? {
141145
projectOwnerSession: resolvedOptions.projectOwnerSession,
142146
} : {
143147
...(publishableClientKey ? { publishableClientKey } : {}),
144-
secretServerKey: resolvedOptions.secretServerKey ?? getDefaultSecretServerKey(),
145-
superSecretAdminKey: resolvedOptions.superSecretAdminKey ?? getDefaultSuperSecretAdminKey(),
148+
secretServerKey: resolvedOptions.secretServerKey ?? getDefaultSecretServerKey({ isEmulator }),
149+
superSecretAdminKey: resolvedOptions.superSecretAdminKey ?? getDefaultSuperSecretAdminKey({ isEmulator }),
146150
},
147151
}),
148152
});
153+
154+
if (isEmulator && !extraOptions?.interface) {
155+
const iface = this._interface;
156+
this._emulatorInitPromise = fetchEmulatorProjectCredentials(emulatorConfigFilePath!).then((data) => {
157+
iface._updateEmulatorCredentials({
158+
projectId: data.project_id,
159+
publishableClientKey: data.publishable_client_key,
160+
secretServerKey: data.secret_server_key,
161+
superSecretAdminKey: data.super_secret_admin_key,
162+
});
163+
});
164+
}
149165
}
150166

151167
_adminConfigFromCrud(data: { config_string: string }): CompleteConfig {

packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import { EditableTeamMemberProfile, ReceivedTeamInvitation, SentTeamInvitation,
5252
import { ActiveSession, Auth, BaseUser, CurrentUser, InternalUserExtra, OAuthProvider, ProjectCurrentUser, SyncedPartialUser, TokenPartialUser, UserExtra, UserUpdateOptions, userUpdateOptionsToCrud, withUserDestructureGuard } from "../../users";
5353
import { StackClientApp, StackClientAppConstructorOptions, StackClientAppJson } from "../interfaces/client-app";
5454
import { _StackAdminAppImplIncomplete } from "./admin-app-impl";
55-
import { TokenObject, clientVersion, createCache, createCacheBySession, createEmptyTokenStore, getAnalyticsBaseUrl, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getUrls, resolveConstructorOptions } from "./common";
55+
import { LOCAL_EMULATOR_INTERNAL_PUBLISHABLE_CLIENT_KEY, TokenObject, clientVersion, createCache, createCacheBySession, createEmptyTokenStore, fetchEmulatorProjectCredentials, getAnalyticsBaseUrl, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getLocalEmulatorConfigFilePath, getUrls, resolveConstructorOptions } from "./common";
5656
import { EventTracker } from "./event-tracker";
5757
import { AnalyticsOptions, SessionRecorder, analyticsOptionsFromJson, analyticsOptionsToJson } from "./session-replay";
5858

@@ -101,6 +101,7 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
101101
private _sessionRecorder: SessionRecorder | null = null;
102102
private _eventTracker: EventTracker | null = null;
103103

104+
protected _emulatorInitPromise: Promise<void> | null = null;
104105
private __DEMO_ENABLE_SLIGHT_FETCH_DELAY = false;
105106
private readonly _ownedAdminApps = new DependenciesMap<[InternalSession, string], _StackAdminAppImplIncomplete<false, string>>();
106107

@@ -499,29 +500,43 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
499500
this._options = resolvedOptions;
500501
this._extraOptions = extraOptions;
501502

502-
const projectId = resolvedOptions.projectId ?? getDefaultProjectId();
503+
const emulatorConfigFilePath = getLocalEmulatorConfigFilePath(resolvedOptions.localEmulatorConfigFilePath);
504+
const isEmulator = !!emulatorConfigFilePath;
505+
506+
const projectId = resolvedOptions.projectId ?? getDefaultProjectId({ isEmulator });
503507
if (projectId !== "internal" && !(projectId.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i))) {
504508
throw new Error(`Invalid project ID: ${projectId}. Project IDs must be UUIDs. Please check your environment variables and/or your StackApp.`);
505509
}
506510

507-
const publishableClientKey = resolvedOptions.publishableClientKey ?? getDefaultPublishableClientKey();
511+
const publishableClientKey = resolvedOptions.publishableClientKey ?? getDefaultPublishableClientKey() ?? (isEmulator ? LOCAL_EMULATOR_INTERNAL_PUBLISHABLE_CLIENT_KEY : undefined);
508512

509513
if (extraOptions && extraOptions.interface) {
510514
this._interface = extraOptions.interface;
511515
} else {
512516
this._interface = new StackClientInterface({
513-
getBaseUrl: () => getBaseUrl(resolvedOptions.baseUrl),
514-
getAnalyticsBaseUrl: () => getAnalyticsBaseUrl(getBaseUrl(resolvedOptions.baseUrl)),
517+
getBaseUrl: () => getBaseUrl(resolvedOptions.baseUrl, { isEmulator }),
518+
getAnalyticsBaseUrl: () => getAnalyticsBaseUrl(getBaseUrl(resolvedOptions.baseUrl, { isEmulator })),
515519
extraRequestHeaders: resolvedOptions.extraRequestHeaders ?? getDefaultExtraRequestHeaders(),
516520
projectId,
517521
clientVersion,
518522
...(publishableClientKey != null ? { publishableClientKey } : {}),
519523
prepareRequest: async () => {
524+
if (this._emulatorInitPromise) await this._emulatorInitPromise;
520525
await cookies?.(); // THIS_LINE_PLATFORM next
521526
}
522527
});
523528
}
524529

530+
if (isEmulator && !(extraOptions && extraOptions.interface)) {
531+
const iface = this._interface;
532+
this._emulatorInitPromise = fetchEmulatorProjectCredentials(emulatorConfigFilePath!).then((data) => {
533+
iface._updateEmulatorCredentials({
534+
projectId: data.project_id,
535+
publishableClientKey: data.publishable_client_key,
536+
});
537+
});
538+
}
539+
525540
this._tokenStoreInit = resolvedOptions.tokenStore;
526541
this._redirectMethod = resolvedOptions.redirectMethod || "none";
527542
this._redirectMethod = resolvedOptions.redirectMethod || "nextjs"; // THIS_LINE_PLATFORM next

0 commit comments

Comments
 (0)