History

fix(relay): use stable Clerk JWT audience

Julius Marminge committed 5 months ago

Commit bb99d261 parent 846b3ed34 files · +155 −60Browse files

Unified · Split

34 files
  1. .env.example +2 −1
  2. .github/workflows/deploy-relay.yml +1 −0
  3. .github/workflows/release.yml +8 −0
  4. apps/mobile/README.md +2 −1
  5. apps/mobile/app.config.ts +1 −0
  6. apps/mobile/src/app/settings/environments.tsx +5 −3
  7. apps/mobile/src/app/settings/index.tsx +6 −4
  8. apps/mobile/src/features/cloud/CloudAuthProvider.tsx +2 −3
  9. apps/mobile/src/features/cloud/publicConfig.test.ts +3 −1
  10. apps/mobile/src/features/cloud/publicConfig.ts +15 −2
  11. apps/web/src/cloud/managedAuth.tsx +3 −3
  12. apps/web/src/cloud/publicConfig.test.ts +4 −0
  13. apps/web/src/cloud/publicConfig.ts +13 −1
  14. apps/web/src/components/settings/ConnectionsSettings.tsx +3 −3
  15. apps/web/src/vite-env.d.ts +1 −0
  16. apps/web/vite.config.ts +2 −0
  17. docs/release.md +4 −2
  18. docs/t3-cloud-clerk.md +24 −21
  19. infra/relay/.env.example +3 −1
  20. infra/relay/README.md +2 −0
  21. infra/relay/src/Config.ts +1 −0
  22. infra/relay/src/agentActivity/ApnsDeliveries.test.ts +1 −0
  23. infra/relay/src/agentActivity/MobileRegistrations.test.ts +1 −0
  24. infra/relay/src/auth/RelayTokens.test.ts +1 −0
  25. infra/relay/src/environments/EnvironmentConnector.test.ts +1 −0
  26. infra/relay/src/environments/EnvironmentLinker.test.ts +1 −0
  27. infra/relay/src/environments/EnvironmentPublishSignatures.test.ts +1 −0
  28. infra/relay/src/environments/ManagedEndpointProvider.test.ts +1 −0
  29. infra/relay/src/http/Api.test.ts +6 −1
  30. infra/relay/src/http/Api.ts +4 −5
  31. infra/relay/src/worker.ts +2 −0
  32. packages/shared/src/relayAuth.ts +6 −6
  33. scripts/lib/public-config.test.ts +11 −2
  34. scripts/lib/public-config.ts +14 −0
.env.example+2−1
@@ -3,8 +3,9 @@ · 2 unmodified lines
33 # Release builds inject their public values at build time. Do not add server-side
44 # secrets to this file.
55
6−# Get this from the Clerk Dashboard under API keys.
6+# Get these from the Clerk Dashboard under API keys and JWT templates.
77 # T3CODE_CLERK_PUBLISHABLE_KEY=pk_test_...
8+# T3CODE_CLERK_JWT_TEMPLATE=t3-relay
89
910 # Get this from your relay deployment. `infra/relay` deploys update it automatically.
1011 # T3CODE_RELAY_URL=https://relay.example.com
.github/workflows/deploy-relay.yml+1−0
@@ -32,6 +32,7 @@ jobs: · 31 unmodified lines
3232 RELAY_DOMAIN: ${{ vars.RELAY_DOMAIN }}
3333 RELAY_ZONE_NAME: ${{ vars.RELAY_ZONE_NAME }}
3434 CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }}
35+ CLERK_JWT_AUDIENCE: ${{ vars.CLERK_JWT_AUDIENCE }}
3536 CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
3637 APNS_ENVIRONMENT: ${{ vars.APNS_ENVIRONMENT }}
3738 APNS_TEAM_ID: ${{ vars.APNS_TEAM_ID }}
.github/workflows/release.yml+8−0
@@ -176,10 +176,12 @@ jobs: · 175 unmodified lines
176176 name: production
177177 outputs:
178178 clerk_publishable_key: ${{ steps.public_config.outputs.clerk_publishable_key }}
179+ clerk_jwt_template: ${{ steps.public_config.outputs.clerk_jwt_template }}
179180 relay_url: ${{ steps.public_config.outputs.relay_url }}
180181 env:
181182 RELAY_DOMAIN: ${{ vars.RELAY_DOMAIN }}
182183 CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }}
184+ CLERK_JWT_TEMPLATE: ${{ vars.CLERK_JWT_TEMPLATE }}
183185 steps:
184186 - id: public_config
185187 name: Resolve production relay public config
@@ -190,6 +192,7 @@ jobs: · 4 unmodified lines
190192 required=(
191193 RELAY_DOMAIN
192194 CLERK_PUBLISHABLE_KEY
195+ CLERK_JWT_TEMPLATE
193196 )
194197 missing=()
195198 for name in "${required[@]}"; do
@@ -203,6 +206,7 @@ jobs: · 7 unmodified lines
203206 fi
204207
205208 echo "clerk_publishable_key=$CLERK_PUBLISHABLE_KEY" >> "$GITHUB_OUTPUT"
209+ echo "clerk_jwt_template=$CLERK_JWT_TEMPLATE" >> "$GITHUB_OUTPUT"
206210 echo "relay_url=https://$RELAY_DOMAIN" >> "$GITHUB_OUTPUT"
207211
208212 build:
@@ -213,6 +217,7 @@ jobs: · 4 unmodified lines
213217 timeout-minutes: 30
214218 env:
215219 T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}
220+ T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}
216221 T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }}
217222 strategy:
218223 fail-fast: false
@@ -473,6 +478,7 @@ jobs: · 254 unmodified lines
473478 id-token: write
474479 env:
475480 T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}
481+ T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}
476482 T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }}
477483 steps:
478484 - name: Checkout
@@ -628,6 +634,7 @@ jobs: · 149 unmodified lines
628634 timeout-minutes: 10
629635 env:
630636 T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}
637+ T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}
631638 T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }}
632639 VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
633640 VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
@@ -696,6 +703,7 @@ jobs: · 62 unmodified lines
696703 "${vercel_scope_args[@]}" \
697704 --build-env "APP_VERSION=${{ needs.preflight.outputs.version }}" \
698705 --build-env "T3CODE_CLERK_PUBLISHABLE_KEY=${T3CODE_CLERK_PUBLISHABLE_KEY:-}" \
706+ --build-env "T3CODE_CLERK_JWT_TEMPLATE=${T3CODE_CLERK_JWT_TEMPLATE:-}" \
699707 --build-env "T3CODE_RELAY_URL=${T3CODE_RELAY_URL:-}" \
700708 --build-env "VITE_HOSTED_APP_URL=$router_url" \
701709 --build-env "VITE_HOSTED_APP_CHANNEL=$channel_name"
apps/mobile/README.md+2−1
@@ -67,7 +67,8 @@ The native lint task runs SwiftLint for Swift plus ktlint and detekt for Kotlin. · 66 unmodified lines
6767
6868 CI uses Expo fingerprinting with the `preview:dev` profile to reuse an existing compatible build when possible, or start a new internal EAS build when native runtime inputs change. Production and default local builds continue to use the `appVersion` runtime policy.
6969
70−For preview or production EAS environments, set `T3CODE_CLERK_PUBLISHABLE_KEY` and `T3CODE_RELAY_URL`
70+For preview or production EAS environments, set `T3CODE_CLERK_PUBLISHABLE_KEY`,
71+`T3CODE_CLERK_JWT_TEMPLATE`, and `T3CODE_RELAY_URL`
7172 as EAS environment variables. Expo config maps the canonical values into the mobile build.
7273
7374 Create a PR preview dev-client build manually:
apps/mobile/app.config.ts+1−0
@@ -159,6 +159,7 @@ const config: ExpoConfig = { · 158 unmodified lines
159159 },
160160 clerk: {
161161 publishableKey: repoEnv.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY ?? null,
162+ jwtTemplate: repoEnv.EXPO_PUBLIC_CLERK_JWT_TEMPLATE ?? null,
162163 },
163164 eas: {
164165 projectId: "d763fcb8-d37c-41ea-a773-b54a0ab4a454",
apps/mobile/src/app/settings/environments.tsx+5−3
@@ -3,7 +3,6 @@ import { Link, Stack } from "expo-router"; · 2 unmodified lines
33 import { SymbolView } from "expo-symbols";
44 import type { EnvironmentId } from "@t3tools/contracts";
55 import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay";
6−import { RELAY_CLERK_TOKEN_OPTIONS } from "@t3tools/shared/relayAuth";
76 import * as Effect from "effect/Effect";
87 import { useCallback, useMemo, useState } from "react";
98 import { ActivityIndicator, Alert, Pressable, ScrollView, View } from "react-native";
@@ -11,7 +10,10 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; · 1 unmodified line
1110
1211 import { AppText as Text } from "../../components/AppText";
1312 import { connectCloudEnvironment } from "../../features/cloud/linkEnvironment";
14−import { hasCloudPublicConfig } from "../../features/cloud/publicConfig";
13+import {
14+ hasCloudPublicConfig,
15+ resolveRelayClerkTokenOptions,
16+} from "../../features/cloud/publicConfig";
1517 import {
1618 useManagedRelayEnvironments,
1719 useManagedRelayEnvironmentStatus,
@@ -142,7 +144,7 @@ function ConfiguredCloudEnvironmentRows() { · 124 unmodified lines
142144 async (environment: RelayClientEnvironmentRecord) => {
143145 setConnectingCloudEnvironmentId(environment.environmentId);
144146 try {
145− const token = await getToken(RELAY_CLERK_TOKEN_OPTIONS);
147+ const token = await getToken(resolveRelayClerkTokenOptions());
146148 if (!token) {
147149 throw new Error("Sign in to T3 Cloud before connecting.");
148150 }
apps/mobile/src/app/settings/index.tsx+6−4
@@ -1,6 +1,5 @@
11 import { useAuth, useUser, useUserProfileModal } from "@clerk/expo";
22 import * as Notifications from "expo-notifications";
3−import { RELAY_CLERK_TOKEN_OPTIONS } from "@t3tools/shared/relayAuth";
43 import { Link, Stack, useRouter } from "expo-router";
54 import { SymbolView } from "expo-symbols";
65 import * as Effect from "effect/Effect";
@@ -14,7 +13,10 @@ import { setLiveActivityUpdatesEnabled } from "../../features/agent-awareness/li · 7 unmodified lines
1413 import { requestAgentNotificationPermission } from "../../features/agent-awareness/notificationPermissions";
1514 import { refreshAgentAwarenessRegistration } from "../../features/agent-awareness/remoteRegistration";
1615 import { refreshManagedRelayEnvironments } from "../../features/cloud/managedRelayState";
17−import { hasCloudPublicConfig } from "../../features/cloud/publicConfig";
16+import {
17+ hasCloudPublicConfig,
18+ resolveRelayClerkTokenOptions,
19+} from "../../features/cloud/publicConfig";
1820 import { mobileRuntime } from "../../lib/runtime";
1921 import { loadPreferences } from "../../lib/storage";
2022 import { useThemeColor } from "../../lib/useThemeColor";
@@ -176,7 +178,7 @@ function ConfiguredSettingsRouteScreen() { · 155 unmodified lines
176178
177179 setLiveActivityStatus("linking");
178180 try {
179− const token = await getToken(RELAY_CLERK_TOKEN_OPTIONS);
181+ const token = await getToken(resolveRelayClerkTokenOptions());
180182 if (!token) {
181183 promptSignIn();
182184 setLiveActivityStatus("signed-out");
@@ -232,7 +234,7 @@ function ConfiguredSettingsRouteScreen() { · 49 unmodified lines
232234 setLiveActivityStatus("disabled");
233235 void (async () => {
234236 try {
235− const token = isSignedIn ? await getToken(RELAY_CLERK_TOKEN_OPTIONS) : null;
237+ const token = isSignedIn ? await getToken(resolveRelayClerkTokenOptions()) : null;
236238 await mobileRuntime.runPromise(
237239 setLiveActivityUpdatesEnabled({
238240 enabled: false,
apps/mobile/src/features/cloud/CloudAuthProvider.tsx+2−3
@@ -2,7 +2,6 @@ import { ClerkProvider, useAuth } from "@clerk/expo"; · 1 unmodified line
22 import { tokenCache } from "@clerk/expo/token-cache";
33 import { createManagedRelaySession, setManagedRelaySession } from "@t3tools/client-runtime";
44 import { type ReactNode, useEffect, useRef } from "react";
5−import { RELAY_CLERK_TOKEN_OPTIONS } from "@t3tools/shared/relayAuth";
65
76 import { mobileRuntime } from "../../lib/runtime";
87 import { appAtomRegistry } from "../../state/atom-registry";
@@ -11,7 +10,7 @@ import { · 2 unmodified lines
1110 unregisterAgentAwarenessDeviceForCurrentUser,
1211 } from "../agent-awareness/remoteRegistration";
1312 import { refreshActiveLiveActivityRemoteRegistration } from "../agent-awareness/liveActivityController";
14−import { resolveCloudPublicConfig } from "./publicConfig";
13+import { resolveCloudPublicConfig, resolveRelayClerkTokenOptions } from "./publicConfig";
1514
1615 function CloudAuthBridge(props: { readonly children: ReactNode }) {
1716 const { getToken, isLoaded, isSignedIn, userId } = useAuth({ treatPendingAsSignedOut: false });
@@ -43,7 +42,7 @@ function CloudAuthBridge(props: { readonly children: ReactNode }) { · 25 unmodified lines
4342 .runPromise(unregisterAgentAwarenessDeviceForCurrentUser(previous.provider))
4443 .catch(() => undefined);
4544 }
46− const tokenProvider = () => getToken(RELAY_CLERK_TOKEN_OPTIONS);
45+ const tokenProvider = () => getToken(resolveRelayClerkTokenOptions());
4746 previousTokenProviderRef.current = { userId, provider: tokenProvider };
4847 setAgentAwarenessRelayTokenProvider(tokenProvider, userId);
4948 setManagedRelaySession(
apps/mobile/src/features/cloud/publicConfig.test.ts+3−1
@@ -14,6 +14,7 @@ describe("resolveCloudPublicConfig", () => { · 13 unmodified lines
1414 it("returns no cloud configuration for an unconfigured build", () => {
1515 expect(resolveCloudPublicConfig({})).toEqual({
1616 clerkPublishableKey: null,
17+ clerkJwtTemplate: null,
1718 relayUrl: null,
1819 });
1920 });
@@ -21,11 +22,12 @@ describe("resolveCloudPublicConfig", () => { · 1 unmodified line
2122 it("normalizes statically injected cloud configuration", () => {
2223 expect(
2324 resolveCloudPublicConfig({
24− clerk: { publishableKey: " pk_test_example " },
25+ clerk: { publishableKey: " pk_test_example ", jwtTemplate: " t3-relay " },
2526 relay: { url: " https://relay.example.test/// " },
2627 }),
2728 ).toEqual({
2829 clerkPublishableKey: "pk_test_example",
30+ clerkJwtTemplate: "t3-relay",
2931 relayUrl: "https://relay.example.test",
3032 });
3133 });
apps/mobile/src/features/cloud/publicConfig.ts+15−2
@@ -1,9 +1,11 @@
11 import Constants from "expo-constants";
2+import { relayClerkTokenOptions } from "@t3tools/shared/relayAuth";
23
34 type ExpoExtra = Readonly<Record<string, unknown>> | undefined;
45
56 export interface CloudPublicConfig {
67 readonly clerkPublishableKey: string | null;
8+ readonly clerkJwtTemplate: string | null;
79 readonly relayUrl: string | null;
810 }
911
@@ -12,16 +14,27 @@ function trimNonEmpty(value: unknown): string | null { · 2 unmodified lines
1214 }
1315
1416 export function resolveCloudPublicConfig(extra: ExpoExtra = Constants.expoConfig?.extra) {
15− const clerk = extra?.clerk as { readonly publishableKey?: unknown } | undefined;
17+ const clerk = extra?.clerk as
18+ | { readonly publishableKey?: unknown; readonly jwtTemplate?: unknown }
19+ | undefined;
1620 const relay = extra?.relay as { readonly url?: unknown } | undefined;
1721
1822 return {
1923 clerkPublishableKey: trimNonEmpty(clerk?.publishableKey),
24+ clerkJwtTemplate: trimNonEmpty(clerk?.jwtTemplate),
2025 relayUrl: trimNonEmpty(relay?.url)?.replace(/\/+$/u, "") ?? null,
2126 } satisfies CloudPublicConfig;
2227 }
2328
2429 export function hasCloudPublicConfig(): boolean {
2530 const config = resolveCloudPublicConfig();
26− return Boolean(config.clerkPublishableKey && config.relayUrl);
31+ return Boolean(config.clerkPublishableKey && config.clerkJwtTemplate && config.relayUrl);
32+}
33+
34+export function resolveRelayClerkTokenOptions() {
35+ const { clerkJwtTemplate } = resolveCloudPublicConfig();
36+ if (!clerkJwtTemplate) {
37+ throw new Error("T3CODE_CLERK_JWT_TEMPLATE is not configured.");
38+ }
39+ return relayClerkTokenOptions(clerkJwtTemplate);
2740 }
apps/web/src/cloud/managedAuth.tsx+3−3
@@ -1,9 +1,9 @@
11 import { useAuth } from "@clerk/react";
22 import { createManagedRelaySession, setManagedRelaySession } from "@t3tools/client-runtime";
3−import { RELAY_CLERK_TOKEN_OPTIONS } from "@t3tools/shared/relayAuth";
43 import { useEffect, type ReactNode } from "react";
54
65 import { appAtomRegistry } from "../rpc/atomRegistry";
6+import { resolveRelayClerkTokenOptions } from "./publicConfig";
77
88 let relayTokenProvider: (() => Promise<string | null>) | null = null;
99
@@ -15,13 +15,13 @@ export function ManagedRelayAuthProvider({ children }: { readonly children: Reac · 5 unmodified lines
1515 const { getToken, isSignedIn, userId } = useAuth();
1616
1717 useEffect(() => {
18− relayTokenProvider = isSignedIn ? () => getToken(RELAY_CLERK_TOKEN_OPTIONS) : null;
18+ relayTokenProvider = isSignedIn ? () => getToken(resolveRelayClerkTokenOptions()) : null;
1919 setManagedRelaySession(
2020 appAtomRegistry,
2121 isSignedIn && userId
2222 ? createManagedRelaySession({
2323 accountId: userId,
24− readClerkToken: () => getToken(RELAY_CLERK_TOKEN_OPTIONS),
24+ readClerkToken: () => getToken(resolveRelayClerkTokenOptions()),
2525 })
2626 : null,
2727 );
apps/web/src/cloud/publicConfig.test.ts+4−0
@@ -9,12 +9,16 @@ afterEach(() => { · 8 unmodified lines
99 describe("hasCloudPublicConfig", () => {
1010 it("requires both public cloud values", () => {
1111 vi.stubEnv("VITE_CLERK_PUBLISHABLE_KEY", "");
12+ vi.stubEnv("VITE_CLERK_JWT_TEMPLATE", "");
1213 vi.stubEnv("VITE_T3CODE_RELAY_URL", "");
1314 expect(hasCloudPublicConfig()).toBe(false);
1415
1516 vi.stubEnv("VITE_CLERK_PUBLISHABLE_KEY", "pk_test_example");
1617 expect(hasCloudPublicConfig()).toBe(false);
1718
19+ vi.stubEnv("VITE_CLERK_JWT_TEMPLATE", "t3-relay");
20+ expect(hasCloudPublicConfig()).toBe(false);
21+
1822 vi.stubEnv("VITE_T3CODE_RELAY_URL", "https://relay.example.test");
1923 expect(hasCloudPublicConfig()).toBe(true);
2024 });
apps/web/src/cloud/publicConfig.ts+13−1
@@ -1,5 +1,8 @@
1+import { relayClerkTokenOptions } from "@t3tools/shared/relayAuth";
2+
13 export interface CloudPublicConfig {
24 readonly clerkPublishableKey: string | null;
5+ readonly clerkJwtTemplate: string | null;
36 readonly relayUrl: string | null;
47 }
58
@@ -12,6 +15,7 @@ export function resolveCloudPublicConfig(): CloudPublicConfig { · 6 unmodified lines
1215 clerkPublishableKey: trimNonEmpty(
1316 import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string | undefined,
1417 ),
18+ clerkJwtTemplate: trimNonEmpty(import.meta.env.VITE_CLERK_JWT_TEMPLATE as string | undefined),
1519 relayUrl:
1620 trimNonEmpty(import.meta.env.VITE_T3CODE_RELAY_URL as string | undefined)?.replace(
1721 /\/+$/u,
@@ -22,5 +26,13 @@ export function resolveCloudPublicConfig(): CloudPublicConfig { · 4 unmodified lines
2226
2327 export function hasCloudPublicConfig(): boolean {
2428 const config = resolveCloudPublicConfig();
25− return Boolean(config.clerkPublishableKey && config.relayUrl);
29+ return Boolean(config.clerkPublishableKey && config.clerkJwtTemplate && config.relayUrl);
30+}
31+
32+export function resolveRelayClerkTokenOptions() {
33+ const { clerkJwtTemplate } = resolveCloudPublicConfig();
34+ if (!clerkJwtTemplate) {
35+ throw new Error("T3CODE_CLERK_JWT_TEMPLATE is not configured.");
36+ }
37+ return relayClerkTokenOptions(clerkJwtTemplate);
2638 }
apps/web/src/components/settings/ConnectionsSettings.tsx+3−3
@@ -30,7 +30,6 @@ import { · 29 unmodified lines
3030 type EnvironmentId,
3131 } from "@t3tools/contracts";
3232 import { WsRpcClient } from "@t3tools/client-runtime";
33−import { RELAY_CLERK_TOKEN_OPTIONS } from "@t3tools/shared/relayAuth";
3433 import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay";
3534 import * as DateTime from "effect/DateTime";
3635
@@ -38,6 +37,7 @@ import { useCopyToClipboard } from "../../hooks/useCopyToClipboard"; · 1 unmodified line
3837 import { cn } from "../../lib/utils";
3938 import { formatElapsedDurationLabel, formatExpiresInLabel } from "../../timestampFormat";
4039 import { resolveDesktopPairingUrl, resolveHostedPairingUrl } from "./pairingUrls";
40+import { resolveRelayClerkTokenOptions } from "../../cloud/publicConfig";
4141 import {
4242 SettingsPageContainer,
4343 SettingsRow,
@@ -1640,7 +1640,7 @@ function ConfiguredCloudLinkRow({ canManageRelay }: { readonly canManageRelay: b · 1596 unmodified lines
16401640 setIsUpdating(true);
16411641 setOperationError(null);
16421642 try {
1643− const clerkToken = await getToken(RELAY_CLERK_TOKEN_OPTIONS);
1643+ const clerkToken = await getToken(resolveRelayClerkTokenOptions());
16441644 if (enabled) {
16451645 if (!clerkToken) {
16461646 throw new Error("Sign in from T3 Cloud settings before linking this environment.");
@@ -1753,7 +1753,7 @@ function ConfiguredCloudRemoteEnvironmentRows({ · 106 unmodified lines
17531753 const connectEnvironment = async (environment: RelayClientEnvironmentRecord) => {
17541754 setConnectingEnvironmentId(environment.environmentId);
17551755 try {
1756− const clerkToken = await getToken(RELAY_CLERK_TOKEN_OPTIONS);
1756+ const clerkToken = await getToken(resolveRelayClerkTokenOptions());
17571757 if (!clerkToken) {
17581758 throw new Error("Sign in from T3 Cloud settings before connecting this environment.");
17591759 }
apps/web/src/vite-env.d.ts+1−0
@@ -8,6 +8,7 @@ interface ImportMetaEnv { · 7 unmodified lines
88 readonly VITE_HOSTED_APP_URL: string;
99 readonly VITE_HOSTED_APP_CHANNEL: string;
1010 readonly VITE_CLERK_PUBLISHABLE_KEY: string;
11+ readonly VITE_CLERK_JWT_TEMPLATE: string;
1112 readonly APP_VERSION: string;
1213 }
1314
apps/web/vite.config.ts+2−0
@@ -18,6 +18,7 @@ const host = process.env.HOST?.trim() || "localhost"; · 17 unmodified lines
1818 const configuredWsUrl = process.env.VITE_WS_URL?.trim();
1919 const configuredRelayUrl = repoEnv.VITE_T3CODE_RELAY_URL?.trim() || "";
2020 const configuredClerkPublishableKey = repoEnv.VITE_CLERK_PUBLISHABLE_KEY?.trim() || "";
21+const configuredClerkJwtTemplate = repoEnv.VITE_CLERK_JWT_TEMPLATE?.trim() || "";
2122 const configuredHostedAppChannel = process.env.VITE_HOSTED_APP_CHANNEL?.trim() || "";
2223 const configuredAppVersion = process.env.APP_VERSION?.trim() || pkg.version;
2324 const configuredHostedAppUrl = (() => {
@@ -132,6 +133,7 @@ export default defineConfig(() => { · 108 unmodified lines
132133 "import.meta.env.VITE_WS_URL": JSON.stringify(configuredWsUrl ?? ""),
133134 "import.meta.env.VITE_T3CODE_RELAY_URL": JSON.stringify(configuredRelayUrl),
134135 "import.meta.env.VITE_CLERK_PUBLISHABLE_KEY": JSON.stringify(configuredClerkPublishableKey),
136+ "import.meta.env.VITE_CLERK_JWT_TEMPLATE": JSON.stringify(configuredClerkJwtTemplate),
135137 "import.meta.env.VITE_HOSTED_APP_URL": JSON.stringify(configuredHostedAppUrl ?? ""),
136138 "import.meta.env.VITE_HOSTED_APP_CHANNEL": JSON.stringify(configuredHostedAppChannel),
137139 "import.meta.env.APP_VERSION": JSON.stringify(configuredAppVersion),
docs/release.md+4−2
@@ -10,7 +10,7 @@ This document covers the unified release workflow for stable and nightly desktop · 9 unmodified lines
1010 - scheduled nightly check every three hours
1111 - manual `workflow_dispatch` for either channel
1212 - Runs quality gates first: lint, typecheck, test.
13−- Reads the shared production T3 Cloud relay URL and Clerk publishable key before packaging clients.
13+- Reads the shared production T3 Cloud relay URL and Clerk client configuration before packaging clients.
1414 - Builds four artifacts in parallel for both channels:
1515 - macOS `arm64` DMG
1616 - macOS `x64` DMG
@@ -38,7 +38,7 @@ release channels. · 21 unmodified lines
3838
3939 `.github/workflows/deploy-relay.yml` deploys Alchemy stage `prod` on every push to `main`. It also
4040 supports manual dispatch for retries. The release workflow reads the relay URL and Clerk
41−publishable key from the existing `production` GitHub Actions environment before building
41+client configuration from the existing `production` GitHub Actions environment before building
4242 desktop, CLI, or hosted web artifacts.
4343
4444 Required repository variables shared by relay deployments:
@@ -59,6 +59,8 @@ Required `production` environment variables: · 14 unmodified lines
5959 - `RELAY_DOMAIN`
6060 - `RELAY_ZONE_NAME`
6161 - `CLERK_PUBLISHABLE_KEY`
62+- `CLERK_JWT_AUDIENCE`
63+- `CLERK_JWT_TEMPLATE`
6264 - `CLERK_CLI_OAUTH_CLIENT_ID`
6365 - `APNS_ENVIRONMENT`
6466 - `APNS_TEAM_ID`
docs/t3-cloud-clerk.md+24−21
@@ -1,8 +1,8 @@
11 # T3 Cloud Clerk Setup
22
33 T3 Cloud uses one Clerk application for web, desktop, and mobile authentication. The relay accepts
4−Clerk JWTs only when they are generated from the `t3-relay` template with the relay URL as the
5−audience.
4+Clerk JWTs only when they are generated from the `t3-relay` template with the shared
5+`t3-code-relay` audience.
66
77 ## Application Keys
88
@@ -11,13 +11,13 @@ or `.env.local` file: · 2 unmodified lines
1111
1212 ```dotenv
1313 T3CODE_CLERK_PUBLISHABLE_KEY=<publishable key>
14+T3CODE_CLERK_JWT_TEMPLATE=<JWT template name>
1415 T3CODE_RELAY_URL=https://relay.example.com
1516 ```
1617
17−The shared client loader projects these canonical values into the framework-specific
18−`VITE_CLERK_PUBLISHABLE_KEY`, `VITE_T3CODE_RELAY_URL`, and
19−`EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY` aliases. Existing aliases remain accepted as overrides for
20−compatibility, but new client configuration should use the canonical names.
18+The shared client loader projects these canonical values into framework-specific `VITE_*` and
19+`EXPO_PUBLIC_*` aliases. Existing aliases remain accepted as overrides for compatibility, but new
20+client configuration should use the canonical names.
2121
2222 Configuration precedence is:
2323
@@ -25,17 +25,18 @@ Configuration precedence is: · 1 unmodified line
2525 2. Repository-root `.env.local`.
2626 3. Repository-root `.env`.
2727
28−The Clerk publishable key and relay URL are public identifiers, not secrets. Web, desktop, and
29−mobile builds statically inject them during their build step. A built artifact does not need an
30−environment file at runtime. CI release builds should set `T3CODE_CLERK_PUBLISHABLE_KEY` and
31−`T3CODE_RELAY_URL` before building. EAS preview and production builds should define the same
32−client-facing values in their EAS environment.
28+The Clerk publishable key, JWT template name, and relay URL are public identifiers, not secrets.
29+Web, desktop, and mobile builds statically inject them during their build step. A built artifact
30+does not need an environment file at runtime. CI release builds should set
31+`T3CODE_CLERK_PUBLISHABLE_KEY`, `T3CODE_CLERK_JWT_TEMPLATE`, and `T3CODE_RELAY_URL` before building.
32+EAS preview and production builds should define the same client-facing values in their EAS
33+environment.
3334
34−When either public value is absent, cloud UI is omitted.
35+When any client-facing public value is absent, cloud UI is omitted.
3536
3637 For a hosted relay deployment, copy `infra/relay/.env.example` to `infra/relay/.env`. The relay
37−deployment reads `RELAY_DOMAIN`, `RELAY_ZONE_NAME`, and `CLERK_PUBLISHABLE_KEY` through Effect
38−`Config`. There are no checked-in deployment defaults.
38+deployment reads `RELAY_DOMAIN`, `RELAY_ZONE_NAME`, `CLERK_PUBLISHABLE_KEY`, and
39+`CLERK_JWT_AUDIENCE` through Effect `Config`. There are no checked-in deployment defaults.
3940 `bun --cwd infra/relay run deploy` invokes Alchemy from the relay directory, so Alchemy loads
4041 `infra/relay/.env`. After a successful deployment, the wrapper updates the repository-root `.env`
4142 with the HTTPS relay URL derived from `RELAY_DOMAIN`. The relay still requires
@@ -50,14 +51,16 @@ preview or developer stage. · 8 unmodified lines
5051
5152 In **Clerk Dashboard > JWT templates**, create a template with:
5253
53−| Setting | Value |
54−| ------- | ---------------------------------------- |
55−| Name | `t3-relay` |
56−| Claims | `{ "aud": "https://relay.example.com" }` |
54+| Setting | Value |
55+| ------- | ---------------------------- |
56+| Name | `t3-relay` |
57+| Claims | `{ "aud": "t3-code-relay" }` |
5758
58−The `aud` value must be the deployed relay public URL, with no trailing slash. It must match the
59−client-facing `T3CODE_RELAY_URL` and the HTTPS URL derived from the deployment's `RELAY_DOMAIN`. If
60−the relay domain changes, update both values and the JWT template.
59+Set `T3CODE_CLERK_JWT_TEMPLATE=t3-relay` in the repository-root `.env`, and set
60+`CLERK_JWT_AUDIENCE=t3-code-relay` in `infra/relay/.env`. Define `CLERK_JWT_TEMPLATE` and
61+`CLERK_JWT_AUDIENCE` in the production relay deployment environment as well. The stable `aud` value
62+is shared by production and non-production relay stages. The client-facing `T3CODE_RELAY_URL` still
63+selects the concrete relay deployment, but changing that URL does not require a JWT template change.
6164
6265 ## Desktop OAuth Redirect Allowlist
6366
infra/relay/.env.example+3−1
@@ -8,9 +8,11 @@ RELAY_ZONE_NAME=example.com · 7 unmodified lines
88 # RELAY_DOMAIN=relay.example.com
99
1010 # Required: Clerk
11−# Get both values from the Clerk Dashboard under API keys.
11+# Get the keys from the Clerk Dashboard under API keys. Set the JWT audience to
12+# the `aud` claim configured on the `t3-relay` JWT template.
1213 CLERK_PUBLISHABLE_KEY=pk_test_...
1314 CLERK_SECRET_KEY=sk_test_...
15+CLERK_JWT_AUDIENCE=t3-code-relay
1416
1517 # Required: Apple Push Notification service
1618 # Get these values from your Apple Developer account. Use `sandbox` for
infra/relay/README.md+2−0
@@ -133,6 +133,8 @@ The `production` GitHub environment must define these Actions variables: · 132 unmodified lines
133133 - `RELAY_ZONE_NAME`
134134 - `RELAY_DOMAIN` if overriding the derived production relay domain
135135 - `CLERK_PUBLISHABLE_KEY`
136+- `CLERK_JWT_AUDIENCE`
137+- `CLERK_JWT_TEMPLATE`
136138 - `APNS_ENVIRONMENT`
137139 - `APNS_TEAM_ID`
138140 - `APNS_KEY_ID`
infra/relay/src/Config.ts+1−0
@@ -18,6 +18,7 @@ export interface RelayConfigurationShape { · 17 unmodified lines
1818 readonly apns: ApnsCredentials;
1919 readonly clerkSecretKey: Redacted.Redacted<string>;
2020 readonly clerkPublishableKey: string;
21+ readonly clerkJwtAudience: string;
2122 readonly apnsDeliveryJobSigningSecret: Redacted.Redacted<string>;
2223 readonly cloudMintPrivateKey: Redacted.Redacted<string>;
2324 readonly cloudMintPublicKey: string;
infra/relay/src/agentActivity/ApnsDeliveries.test.ts+1−0
@@ -39,6 +39,7 @@ const config = RelayConfiguration.RelayConfiguration.of({ · 38 unmodified lines
3939 apnsDeliveryJobSigningSecret: Redacted.make("job-signing-secret"),
4040 clerkSecretKey: Redacted.make("clerk-secret"),
4141 clerkPublishableKey: "pk_test_test",
42+ clerkJwtAudience: "t3-code-relay",
4243 cloudMintPrivateKey: Redacted.make("cloud-private-key"),
4344 cloudMintPublicKey: "cloud-public-key",
4445 managedEndpointBaseDomain: undefined,
infra/relay/src/agentActivity/MobileRegistrations.test.ts+1−0
@@ -129,6 +129,7 @@ const config = RelayConfiguration.RelayConfiguration.of({ · 128 unmodified lines
129129 },
130130 clerkSecretKey: Redacted.make("clerk-secret"),
131131 clerkPublishableKey: "pk_test_test",
132+ clerkJwtAudience: "t3-code-relay",
132133 apnsDeliveryJobSigningSecret: Redacted.make("apns-job-secret"),
133134 cloudMintPrivateKey: Redacted.make("cloud-private-key"),
134135 cloudMintPublicKey: "cloud-public-key",
infra/relay/src/auth/RelayTokens.test.ts+1−0
@@ -26,6 +26,7 @@ const config = RelayConfiguration.RelayConfiguration.of({ · 25 unmodified lines
2626 apnsDeliveryJobSigningSecret: Redacted.make("job-secret"),
2727 clerkSecretKey: Redacted.make("clerk-secret"),
2828 clerkPublishableKey: "pk_test_test",
29+ clerkJwtAudience: "t3-code-relay",
2930 cloudMintPrivateKey: Redacted.make(keyPair.privateKey),
3031 cloudMintPublicKey: keyPair.publicKey,
3132 managedEndpointBaseDomain: undefined,
infra/relay/src/environments/EnvironmentConnector.test.ts+1−0
@@ -66,6 +66,7 @@ const settings = RelayConfiguration.RelayConfiguration.of({ · 65 unmodified lines
6666 apnsDeliveryJobSigningSecret: Redacted.make("job-secret"),
6767 clerkSecretKey: Redacted.make("clerk-secret"),
6868 clerkPublishableKey: "pk_test_test",
69+ clerkJwtAudience: "t3-code-relay",
6970 cloudMintPrivateKey: Redacted.make(cloudKeyPair.privateKey),
7071 cloudMintPublicKey: cloudKeyPair.publicKey,
7172 managedEndpointBaseDomain: undefined,
infra/relay/src/environments/EnvironmentLinker.test.ts+1−0
@@ -39,6 +39,7 @@ const config = RelayConfiguration.RelayConfiguration.of({ · 38 unmodified lines
3939 apnsDeliveryJobSigningSecret: Redacted.make("job-secret"),
4040 clerkSecretKey: Redacted.make("clerk-secret"),
4141 clerkPublishableKey: "pk_test_test",
42+ clerkJwtAudience: "t3-code-relay",
4243 cloudMintPrivateKey: Redacted.make(relayKeyPair.privateKey),
4344 cloudMintPublicKey: relayKeyPair.publicKey,
4445 managedEndpointBaseDomain: undefined,
infra/relay/src/environments/EnvironmentPublishSignatures.test.ts+1−0
@@ -34,6 +34,7 @@ const config = RelayConfiguration.RelayConfiguration.of({ · 33 unmodified lines
3434 apnsDeliveryJobSigningSecret: Redacted.make("job-secret"),
3535 clerkSecretKey: Redacted.make("clerk-secret"),
3636 clerkPublishableKey: "pk_test_test",
37+ clerkJwtAudience: "t3-code-relay",
3738 cloudMintPrivateKey: Redacted.make(keyPair.privateKey),
3839 cloudMintPublicKey: keyPair.publicKey,
3940 managedEndpointBaseDomain: undefined,
infra/relay/src/environments/ManagedEndpointProvider.test.ts+1−0
@@ -21,6 +21,7 @@ const config = RelayConfiguration.RelayConfiguration.of({ · 20 unmodified lines
2121 apnsDeliveryJobSigningSecret: Redacted.make("job-secret"),
2222 clerkSecretKey: Redacted.make("clerk-secret"),
2323 clerkPublishableKey: "pk_test_test",
24+ clerkJwtAudience: "t3-code-relay",
2425 cloudMintPrivateKey: Redacted.make("cloud-private-key"),
2526 cloudMintPublicKey: "cloud-public-key",
2627 managedEndpointBaseDomain: "t3code.test",
infra/relay/src/http/Api.test.ts+6−1
@@ -40,6 +40,7 @@ const relaySettings: RelayConfiguration.RelayConfigurationShape = { · 39 unmodified lines
4040 },
4141 clerkSecretKey: Redacted.make("clerk-secret-key"),
4242 clerkPublishableKey: "pk_test_test",
43+ clerkJwtAudience: "t3-code-relay",
4344 apnsDeliveryJobSigningSecret: Redacted.make("apns-delivery-secret"),
4445 cloudMintPrivateKey: Redacted.make("cloud-mint-private-key"),
4546 cloudMintPublicKey: "cloud-mint-public-key",
@@ -52,13 +53,17 @@ describe("relay client authentication", () => { · 6 unmodified lines
5253 Effect.gen(function* () {
5354 vi.mocked(verifyToken).mockResolvedValue({
5455 sub: "user_session",
55− aud: relaySettings.relayIssuer,
56+ aud: relaySettings.clerkJwtAudience,
5657 } as never);
5758
5859 expect(yield* verifyRelayClientBearerToken(relaySettings, "session-token")).toEqual({
5960 sub: "user_session",
6061 mode: "clerk_session_bearer",
6162 });
63+ expect(verifyToken).toHaveBeenCalledWith("session-token", {
64+ secretKey: "clerk-secret-key",
65+ audience: relaySettings.clerkJwtAudience,
66+ });
6267 expect(createClerkClient).not.toHaveBeenCalled();
6368 }).pipe(
6469 Effect.ensuring(
infra/relay/src/http/Api.ts+4−5
@@ -578,7 +578,7 @@ export const tokenApi = HttpApiBuilder.group( · 577 unmodified lines
578578 const verified = yield* verifyClerkBearerToken(config, args.payload.subject_token).pipe(
579579 Effect.catch(() => relayAuthInvalidError("invalid_bearer")),
580580 );
581− if (!verified.sub || !hasExpectedClerkAudience(verified.aud, config.relayIssuer)) {
581+ if (!verified.sub || !hasExpectedClerkAudience(verified.aud, config.clerkJwtAudience)) {
582582 return yield* relayAuthInvalidError("invalid_bearer");
583583 }
584584 const proofKeyThumbprint = yield* requireDpopProof().pipe(
@@ -960,8 +960,7 @@ function clerkVerificationFailureReason(cause: unknown): string { · 375 unmodified lines
960960 return "unknown";
961961 }
962962
963−function hasExpectedClerkAudience(audience: unknown, relayIssuer: string): boolean {
964− const expectedAudience = normalizeRelayIssuer(relayIssuer);
963+function hasExpectedClerkAudience(audience: unknown, expectedAudience: string): boolean {
965964 return typeof audience === "string"
966965 ? audience === expectedAudience
967966 : Array.isArray(audience) &&
@@ -973,7 +972,7 @@ function verifyClerkBearerToken(config: RelayConfiguration.RelayConfigurationSha · 5 unmodified lines
973972 try: () =>
974973 verifyToken(token, {
975974 secretKey: Redacted.value(config.clerkSecretKey),
976− audience: normalizeRelayIssuer(config.relayIssuer),
975+ audience: config.clerkJwtAudience,
977976 }),
978977 catch: (cause) => new ClerkTokenVerificationFailed({ cause }),
979978 }).pipe(
@@ -1015,7 +1014,7 @@ export function verifyRelayClientBearerToken( · 35 unmodified lines
10151014 ) {
10161015 return verifyClerkBearerToken(config, token).pipe(
10171016 Effect.flatMap((verified) =>
1018− verified.sub && hasExpectedClerkAudience(verified.aud, config.relayIssuer)
1017+ verified.sub && hasExpectedClerkAudience(verified.aud, config.clerkJwtAudience)
10191018 ? Effect.succeed({ sub: verified.sub, mode: "clerk_session_bearer" as const })
10201019 : Effect.fail(new ClerkTokenVerificationFailed({ cause: "missing_relay_audience" })),
10211020 ),
infra/relay/src/worker.ts+2−0
@@ -130,6 +130,7 @@ export default class Api extends Cloudflare.Worker<Api>()( · 129 unmodified lines
130130
131131 const clerkSecretKey = yield* Config.redacted("CLERK_SECRET_KEY");
132132 const clerkPublishableKey = yield* Config.string("CLERK_PUBLISHABLE_KEY");
133+ const clerkJwtAudience = yield* Config.string("CLERK_JWT_AUDIENCE");
133134
134135 const cloudMintPrivateKey = yield* cloudMintKeyPair.privateKey;
135136 const cloudMintPublicKey = yield* cloudMintKeyPair.publicKey;
@@ -158,6 +159,7 @@ export default class Api extends Cloudflare.Worker<Api>()( · 22 unmodified lines
158159 apnsDeliveryJobSigningSecret: yield* apnsDeliveryJobSigningSecret,
159160 clerkSecretKey,
160161 clerkPublishableKey,
162+ clerkJwtAudience,
161163 cloudMintPrivateKey: yield* cloudMintPrivateKey,
162164 cloudMintPublicKey: yield* cloudMintPublicKey,
163165 managedEndpointBaseDomain: yield* managedEndpointZoneName,
packages/shared/src/relayAuth.ts+6−6
@@ -1,6 +1,6 @@
1−export const RELAY_CLERK_JWT_TEMPLATE = "t3-relay";
2−
3−export const RELAY_CLERK_TOKEN_OPTIONS = {
4− template: RELAY_CLERK_JWT_TEMPLATE,
5− skipCache: true,
6−} as const;
1+export function relayClerkTokenOptions(template: string) {
2+ return {
3+ template,
4+ skipCache: true,
5+ } as const;
6+}
scripts/lib/public-config.test.ts+11−2
@@ -21,6 +21,9 @@ describe("loadRepoEnv", () => { · 20 unmodified lines
2121 expect(env.T3CODE_CLERK_PUBLISHABLE_KEY).toBeUndefined();
2222 expect(env.VITE_CLERK_PUBLISHABLE_KEY).toBeUndefined();
2323 expect(env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY).toBeUndefined();
24+ expect(env.T3CODE_CLERK_JWT_TEMPLATE).toBeUndefined();
25+ expect(env.VITE_CLERK_JWT_TEMPLATE).toBeUndefined();
26+ expect(env.EXPO_PUBLIC_CLERK_JWT_TEMPLATE).toBeUndefined();
2427 expect(env.T3CODE_RELAY_URL).toBeUndefined();
2528 expect(env.VITE_T3CODE_RELAY_URL).toBeUndefined();
2629 });
@@ -29,11 +32,11 @@ describe("loadRepoEnv", () => { · 2 unmodified lines
2932 const repoRoot = makeTemporaryDirectory();
3033 writeFileSync(
3134 join(repoRoot, ".env"),
32− "T3CODE_CLERK_PUBLISHABLE_KEY=pk_root\nT3CODE_RELAY_URL=https://root.example.test\n",
35+ "T3CODE_CLERK_PUBLISHABLE_KEY=pk_root\nT3CODE_CLERK_JWT_TEMPLATE=template_root\nT3CODE_RELAY_URL=https://root.example.test\n",
3336 );
3437 writeFileSync(
3538 join(repoRoot, ".env.local"),
36− "T3CODE_CLERK_PUBLISHABLE_KEY=pk_local\nT3CODE_RELAY_URL=https://local.example.test\n",
39+ "T3CODE_CLERK_PUBLISHABLE_KEY=pk_local\nT3CODE_CLERK_JWT_TEMPLATE=template_local\nT3CODE_RELAY_URL=https://local.example.test\n",
3740 );
3841
3942 expect(loadRepoEnv({ baseEnv: {}, repoRoot }).T3CODE_RELAY_URL).toBe(
@@ -43,6 +46,7 @@ describe("loadRepoEnv", () => { · 3 unmodified lines
4346 loadRepoEnv({
4447 baseEnv: {
4548 T3CODE_CLERK_PUBLISHABLE_KEY: "pk_ci",
49+ T3CODE_CLERK_JWT_TEMPLATE: "template_ci",
4650 T3CODE_RELAY_URL: "https://ci.example.test",
4751 },
4852 repoRoot,
@@ -51,6 +55,9 @@ describe("loadRepoEnv", () => { · 2 unmodified lines
5155 T3CODE_CLERK_PUBLISHABLE_KEY: "pk_ci",
5256 VITE_CLERK_PUBLISHABLE_KEY: "pk_ci",
5357 EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY: "pk_ci",
58+ T3CODE_CLERK_JWT_TEMPLATE: "template_ci",
59+ VITE_CLERK_JWT_TEMPLATE: "template_ci",
60+ EXPO_PUBLIC_CLERK_JWT_TEMPLATE: "template_ci",
5461 T3CODE_RELAY_URL: "https://ci.example.test",
5562 VITE_T3CODE_RELAY_URL: "https://ci.example.test",
5663 });
@@ -60,10 +67,12 @@ describe("loadRepoEnv", () => { · 3 unmodified lines
6067 expect(
6168 resolvePublicConfig({
6269 VITE_CLERK_PUBLISHABLE_KEY: "pk_legacy",
70+ VITE_CLERK_JWT_TEMPLATE: "template_legacy",
6371 VITE_T3CODE_RELAY_URL: "https://legacy.example.test",
6472 }),
6573 ).toEqual({
6674 clerkPublishableKey: "pk_legacy",
75+ clerkJwtTemplate: "template_legacy",
6776 relayUrl: "https://legacy.example.test",
6877 });
6978 });
scripts/lib/public-config.ts+14−0
@@ -6,6 +6,7 @@ import * as NodeUtil from "node:util"; · 5 unmodified lines
66
77 export interface T3CodePublicConfig {
88 readonly clerkPublishableKey: string | undefined;
9+ readonly clerkJwtTemplate: string | undefined;
910 readonly relayUrl: string | undefined;
1011 }
1112
@@ -37,6 +38,13 @@ export function loadRepoEnv({ · 25 unmodified lines
3738 EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY: config.clerkPublishableKey,
3839 }
3940 : {}),
41+ ...(config.clerkJwtTemplate
42+ ? {
43+ T3CODE_CLERK_JWT_TEMPLATE: config.clerkJwtTemplate,
44+ VITE_CLERK_JWT_TEMPLATE: config.clerkJwtTemplate,
45+ EXPO_PUBLIC_CLERK_JWT_TEMPLATE: config.clerkJwtTemplate,
46+ }
47+ : {}),
4048 ...(config.relayUrl
4149 ? {
4250 T3CODE_RELAY_URL: config.relayUrl,
@@ -54,6 +62,12 @@ export function resolvePublicConfig(...sources: readonly Environment[]): T3CodeP · 11 unmodified lines
5462 "VITE_CLERK_PUBLISHABLE_KEY",
5563 "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY",
5664 ),
65+ clerkJwtTemplate: firstNonEmpty(
66+ sources,
67+ "T3CODE_CLERK_JWT_TEMPLATE",
68+ "VITE_CLERK_JWT_TEMPLATE",
69+ "EXPO_PUBLIC_CLERK_JWT_TEMPLATE",
70+ ),
5771 relayUrl: firstNonEmpty(sources, "T3CODE_RELAY_URL", "VITE_T3CODE_RELAY_URL"),
5872 };
5973 }