Unified · Split
| @@ -3,8 +3,9 @@ · 2 unmodified lines | ||
| 3 | 3 | # Release builds inject their public values at build time. Do not add server-side |
| 4 | 4 | # secrets to this file. |
| 5 | 5 | |
| 6 | −# Get this from the Clerk Dashboard under API keys. | |
| 6 | +# Get these from the Clerk Dashboard under API keys and JWT templates. | |
| 7 | 7 | # T3CODE_CLERK_PUBLISHABLE_KEY=pk_test_... |
| 8 | +# T3CODE_CLERK_JWT_TEMPLATE=t3-relay | |
| 8 | 9 | |
| 9 | 10 | # Get this from your relay deployment. `infra/relay` deploys update it automatically. |
| 10 | 11 | # T3CODE_RELAY_URL=https://relay.example.com |
| @@ -32,6 +32,7 @@ jobs: · 31 unmodified lines | ||
| 32 | 32 | RELAY_DOMAIN: ${{ vars.RELAY_DOMAIN }} |
| 33 | 33 | RELAY_ZONE_NAME: ${{ vars.RELAY_ZONE_NAME }} |
| 34 | 34 | CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }} |
| 35 | + CLERK_JWT_AUDIENCE: ${{ vars.CLERK_JWT_AUDIENCE }} | |
| 35 | 36 | CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} |
| 36 | 37 | APNS_ENVIRONMENT: ${{ vars.APNS_ENVIRONMENT }} |
| 37 | 38 | APNS_TEAM_ID: ${{ vars.APNS_TEAM_ID }} |
| @@ -176,10 +176,12 @@ jobs: · 175 unmodified lines | ||
| 176 | 176 | name: production |
| 177 | 177 | outputs: |
| 178 | 178 | clerk_publishable_key: ${{ steps.public_config.outputs.clerk_publishable_key }} |
| 179 | + clerk_jwt_template: ${{ steps.public_config.outputs.clerk_jwt_template }} | |
| 179 | 180 | relay_url: ${{ steps.public_config.outputs.relay_url }} |
| 180 | 181 | env: |
| 181 | 182 | RELAY_DOMAIN: ${{ vars.RELAY_DOMAIN }} |
| 182 | 183 | CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }} |
| 184 | + CLERK_JWT_TEMPLATE: ${{ vars.CLERK_JWT_TEMPLATE }} | |
| 183 | 185 | steps: |
| 184 | 186 | - id: public_config |
| 185 | 187 | name: Resolve production relay public config |
| @@ -190,6 +192,7 @@ jobs: · 4 unmodified lines | ||
| 190 | 192 | required=( |
| 191 | 193 | RELAY_DOMAIN |
| 192 | 194 | CLERK_PUBLISHABLE_KEY |
| 195 | + CLERK_JWT_TEMPLATE | |
| 193 | 196 | ) |
| 194 | 197 | missing=() |
| 195 | 198 | for name in "${required[@]}"; do |
| @@ -203,6 +206,7 @@ jobs: · 7 unmodified lines | ||
| 203 | 206 | fi |
| 204 | 207 | |
| 205 | 208 | echo "clerk_publishable_key=$CLERK_PUBLISHABLE_KEY" >> "$GITHUB_OUTPUT" |
| 209 | + echo "clerk_jwt_template=$CLERK_JWT_TEMPLATE" >> "$GITHUB_OUTPUT" | |
| 206 | 210 | echo "relay_url=https://$RELAY_DOMAIN" >> "$GITHUB_OUTPUT" |
| 207 | 211 | |
| 208 | 212 | build: |
| @@ -213,6 +217,7 @@ jobs: · 4 unmodified lines | ||
| 213 | 217 | timeout-minutes: 30 |
| 214 | 218 | env: |
| 215 | 219 | 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 }} | |
| 216 | 221 | T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }} |
| 217 | 222 | strategy: |
| 218 | 223 | fail-fast: false |
| @@ -473,6 +478,7 @@ jobs: · 254 unmodified lines | ||
| 473 | 478 | id-token: write |
| 474 | 479 | env: |
| 475 | 480 | 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 }} | |
| 476 | 482 | T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }} |
| 477 | 483 | steps: |
| 478 | 484 | - name: Checkout |
| @@ -628,6 +634,7 @@ jobs: · 149 unmodified lines | ||
| 628 | 634 | timeout-minutes: 10 |
| 629 | 635 | env: |
| 630 | 636 | 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 }} | |
| 631 | 638 | T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }} |
| 632 | 639 | VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} |
| 633 | 640 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} |
| @@ -696,6 +703,7 @@ jobs: · 62 unmodified lines | ||
| 696 | 703 | "${vercel_scope_args[@]}" \ |
| 697 | 704 | --build-env "APP_VERSION=${{ needs.preflight.outputs.version }}" \ |
| 698 | 705 | --build-env "T3CODE_CLERK_PUBLISHABLE_KEY=${T3CODE_CLERK_PUBLISHABLE_KEY:-}" \ |
| 706 | + --build-env "T3CODE_CLERK_JWT_TEMPLATE=${T3CODE_CLERK_JWT_TEMPLATE:-}" \ | |
| 699 | 707 | --build-env "T3CODE_RELAY_URL=${T3CODE_RELAY_URL:-}" \ |
| 700 | 708 | --build-env "VITE_HOSTED_APP_URL=$router_url" \ |
| 701 | 709 | --build-env "VITE_HOSTED_APP_CHANNEL=$channel_name" |
| @@ -67,7 +67,8 @@ The native lint task runs SwiftLint for Swift plus ktlint and detekt for Kotlin. · 66 unmodified lines | ||
| 67 | 67 | |
| 68 | 68 | 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. |
| 69 | 69 | |
| 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` | |
| 71 | 72 | as EAS environment variables. Expo config maps the canonical values into the mobile build. |
| 72 | 73 | |
| 73 | 74 | Create a PR preview dev-client build manually: |
| @@ -159,6 +159,7 @@ const config: ExpoConfig = { · 158 unmodified lines | ||
| 159 | 159 | }, |
| 160 | 160 | clerk: { |
| 161 | 161 | publishableKey: repoEnv.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY ?? null, |
| 162 | + jwtTemplate: repoEnv.EXPO_PUBLIC_CLERK_JWT_TEMPLATE ?? null, | |
| 162 | 163 | }, |
| 163 | 164 | eas: { |
| 164 | 165 | projectId: "d763fcb8-d37c-41ea-a773-b54a0ab4a454", |
| @@ -3,7 +3,6 @@ import { Link, Stack } from "expo-router"; · 2 unmodified lines | ||
| 3 | 3 | import { SymbolView } from "expo-symbols"; |
| 4 | 4 | import type { EnvironmentId } from "@t3tools/contracts"; |
| 5 | 5 | import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay"; |
| 6 | −import { RELAY_CLERK_TOKEN_OPTIONS } from "@t3tools/shared/relayAuth"; | |
| 7 | 6 | import * as Effect from "effect/Effect"; |
| 8 | 7 | import { useCallback, useMemo, useState } from "react"; |
| 9 | 8 | import { ActivityIndicator, Alert, Pressable, ScrollView, View } from "react-native"; |
| @@ -11,7 +10,10 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; · 1 unmodified line | ||
| 11 | 10 | |
| 12 | 11 | import { AppText as Text } from "../../components/AppText"; |
| 13 | 12 | import { connectCloudEnvironment } from "../../features/cloud/linkEnvironment"; |
| 14 | −import { hasCloudPublicConfig } from "../../features/cloud/publicConfig"; | |
| 13 | +import { | |
| 14 | + hasCloudPublicConfig, | |
| 15 | + resolveRelayClerkTokenOptions, | |
| 16 | +} from "../../features/cloud/publicConfig"; | |
| 15 | 17 | import { |
| 16 | 18 | useManagedRelayEnvironments, |
| 17 | 19 | useManagedRelayEnvironmentStatus, |
| @@ -142,7 +144,7 @@ function ConfiguredCloudEnvironmentRows() { · 124 unmodified lines | ||
| 142 | 144 | async (environment: RelayClientEnvironmentRecord) => { |
| 143 | 145 | setConnectingCloudEnvironmentId(environment.environmentId); |
| 144 | 146 | try { |
| 145 | − const token = await getToken(RELAY_CLERK_TOKEN_OPTIONS); | |
| 147 | + const token = await getToken(resolveRelayClerkTokenOptions()); | |
| 146 | 148 | if (!token) { |
| 147 | 149 | throw new Error("Sign in to T3 Cloud before connecting."); |
| 148 | 150 | } |
| @@ -1,6 +1,5 @@ | ||
| 1 | 1 | import { useAuth, useUser, useUserProfileModal } from "@clerk/expo"; |
| 2 | 2 | import * as Notifications from "expo-notifications"; |
| 3 | −import { RELAY_CLERK_TOKEN_OPTIONS } from "@t3tools/shared/relayAuth"; | |
| 4 | 3 | import { Link, Stack, useRouter } from "expo-router"; |
| 5 | 4 | import { SymbolView } from "expo-symbols"; |
| 6 | 5 | import * as Effect from "effect/Effect"; |
| @@ -14,7 +13,10 @@ import { setLiveActivityUpdatesEnabled } from "../../features/agent-awareness/li · 7 unmodified lines | ||
| 14 | 13 | import { requestAgentNotificationPermission } from "../../features/agent-awareness/notificationPermissions"; |
| 15 | 14 | import { refreshAgentAwarenessRegistration } from "../../features/agent-awareness/remoteRegistration"; |
| 16 | 15 | import { refreshManagedRelayEnvironments } from "../../features/cloud/managedRelayState"; |
| 17 | −import { hasCloudPublicConfig } from "../../features/cloud/publicConfig"; | |
| 16 | +import { | |
| 17 | + hasCloudPublicConfig, | |
| 18 | + resolveRelayClerkTokenOptions, | |
| 19 | +} from "../../features/cloud/publicConfig"; | |
| 18 | 20 | import { mobileRuntime } from "../../lib/runtime"; |
| 19 | 21 | import { loadPreferences } from "../../lib/storage"; |
| 20 | 22 | import { useThemeColor } from "../../lib/useThemeColor"; |
| @@ -176,7 +178,7 @@ function ConfiguredSettingsRouteScreen() { · 155 unmodified lines | ||
| 176 | 178 | |
| 177 | 179 | setLiveActivityStatus("linking"); |
| 178 | 180 | try { |
| 179 | − const token = await getToken(RELAY_CLERK_TOKEN_OPTIONS); | |
| 181 | + const token = await getToken(resolveRelayClerkTokenOptions()); | |
| 180 | 182 | if (!token) { |
| 181 | 183 | promptSignIn(); |
| 182 | 184 | setLiveActivityStatus("signed-out"); |
| @@ -232,7 +234,7 @@ function ConfiguredSettingsRouteScreen() { · 49 unmodified lines | ||
| 232 | 234 | setLiveActivityStatus("disabled"); |
| 233 | 235 | void (async () => { |
| 234 | 236 | try { |
| 235 | − const token = isSignedIn ? await getToken(RELAY_CLERK_TOKEN_OPTIONS) : null; | |
| 237 | + const token = isSignedIn ? await getToken(resolveRelayClerkTokenOptions()) : null; | |
| 236 | 238 | await mobileRuntime.runPromise( |
| 237 | 239 | setLiveActivityUpdatesEnabled({ |
| 238 | 240 | enabled: false, |
| @@ -2,7 +2,6 @@ import { ClerkProvider, useAuth } from "@clerk/expo"; · 1 unmodified line | ||
| 2 | 2 | import { tokenCache } from "@clerk/expo/token-cache"; |
| 3 | 3 | import { createManagedRelaySession, setManagedRelaySession } from "@t3tools/client-runtime"; |
| 4 | 4 | import { type ReactNode, useEffect, useRef } from "react"; |
| 5 | −import { RELAY_CLERK_TOKEN_OPTIONS } from "@t3tools/shared/relayAuth"; | |
| 6 | 5 | |
| 7 | 6 | import { mobileRuntime } from "../../lib/runtime"; |
| 8 | 7 | import { appAtomRegistry } from "../../state/atom-registry"; |
| @@ -11,7 +10,7 @@ import { · 2 unmodified lines | ||
| 11 | 10 | unregisterAgentAwarenessDeviceForCurrentUser, |
| 12 | 11 | } from "../agent-awareness/remoteRegistration"; |
| 13 | 12 | import { refreshActiveLiveActivityRemoteRegistration } from "../agent-awareness/liveActivityController"; |
| 14 | −import { resolveCloudPublicConfig } from "./publicConfig"; | |
| 13 | +import { resolveCloudPublicConfig, resolveRelayClerkTokenOptions } from "./publicConfig"; | |
| 15 | 14 | |
| 16 | 15 | function CloudAuthBridge(props: { readonly children: ReactNode }) { |
| 17 | 16 | const { getToken, isLoaded, isSignedIn, userId } = useAuth({ treatPendingAsSignedOut: false }); |
| @@ -43,7 +42,7 @@ function CloudAuthBridge(props: { readonly children: ReactNode }) { · 25 unmodified lines | ||
| 43 | 42 | .runPromise(unregisterAgentAwarenessDeviceForCurrentUser(previous.provider)) |
| 44 | 43 | .catch(() => undefined); |
| 45 | 44 | } |
| 46 | − const tokenProvider = () => getToken(RELAY_CLERK_TOKEN_OPTIONS); | |
| 45 | + const tokenProvider = () => getToken(resolveRelayClerkTokenOptions()); | |
| 47 | 46 | previousTokenProviderRef.current = { userId, provider: tokenProvider }; |
| 48 | 47 | setAgentAwarenessRelayTokenProvider(tokenProvider, userId); |
| 49 | 48 | setManagedRelaySession( |
| @@ -14,6 +14,7 @@ describe("resolveCloudPublicConfig", () => { · 13 unmodified lines | ||
| 14 | 14 | it("returns no cloud configuration for an unconfigured build", () => { |
| 15 | 15 | expect(resolveCloudPublicConfig({})).toEqual({ |
| 16 | 16 | clerkPublishableKey: null, |
| 17 | + clerkJwtTemplate: null, | |
| 17 | 18 | relayUrl: null, |
| 18 | 19 | }); |
| 19 | 20 | }); |
| @@ -21,11 +22,12 @@ describe("resolveCloudPublicConfig", () => { · 1 unmodified line | ||
| 21 | 22 | it("normalizes statically injected cloud configuration", () => { |
| 22 | 23 | expect( |
| 23 | 24 | resolveCloudPublicConfig({ |
| 24 | − clerk: { publishableKey: " pk_test_example " }, | |
| 25 | + clerk: { publishableKey: " pk_test_example ", jwtTemplate: " t3-relay " }, | |
| 25 | 26 | relay: { url: " https://relay.example.test/// " }, |
| 26 | 27 | }), |
| 27 | 28 | ).toEqual({ |
| 28 | 29 | clerkPublishableKey: "pk_test_example", |
| 30 | + clerkJwtTemplate: "t3-relay", | |
| 29 | 31 | relayUrl: "https://relay.example.test", |
| 30 | 32 | }); |
| 31 | 33 | }); |
| @@ -1,9 +1,11 @@ | ||
| 1 | 1 | import Constants from "expo-constants"; |
| 2 | +import { relayClerkTokenOptions } from "@t3tools/shared/relayAuth"; | |
| 2 | 3 | |
| 3 | 4 | type ExpoExtra = Readonly<Record<string, unknown>> | undefined; |
| 4 | 5 | |
| 5 | 6 | export interface CloudPublicConfig { |
| 6 | 7 | readonly clerkPublishableKey: string | null; |
| 8 | + readonly clerkJwtTemplate: string | null; | |
| 7 | 9 | readonly relayUrl: string | null; |
| 8 | 10 | } |
| 9 | 11 | |
| @@ -12,16 +14,27 @@ function trimNonEmpty(value: unknown): string | null { · 2 unmodified lines | ||
| 12 | 14 | } |
| 13 | 15 | |
| 14 | 16 | 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; | |
| 16 | 20 | const relay = extra?.relay as { readonly url?: unknown } | undefined; |
| 17 | 21 | |
| 18 | 22 | return { |
| 19 | 23 | clerkPublishableKey: trimNonEmpty(clerk?.publishableKey), |
| 24 | + clerkJwtTemplate: trimNonEmpty(clerk?.jwtTemplate), | |
| 20 | 25 | relayUrl: trimNonEmpty(relay?.url)?.replace(/\/+$/u, "") ?? null, |
| 21 | 26 | } satisfies CloudPublicConfig; |
| 22 | 27 | } |
| 23 | 28 | |
| 24 | 29 | export function hasCloudPublicConfig(): boolean { |
| 25 | 30 | 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); | |
| 27 | 40 | } |
| @@ -1,9 +1,9 @@ | ||
| 1 | 1 | import { useAuth } from "@clerk/react"; |
| 2 | 2 | import { createManagedRelaySession, setManagedRelaySession } from "@t3tools/client-runtime"; |
| 3 | −import { RELAY_CLERK_TOKEN_OPTIONS } from "@t3tools/shared/relayAuth"; | |
| 4 | 3 | import { useEffect, type ReactNode } from "react"; |
| 5 | 4 | |
| 6 | 5 | import { appAtomRegistry } from "../rpc/atomRegistry"; |
| 6 | +import { resolveRelayClerkTokenOptions } from "./publicConfig"; | |
| 7 | 7 | |
| 8 | 8 | let relayTokenProvider: (() => Promise<string | null>) | null = null; |
| 9 | 9 | |
| @@ -15,13 +15,13 @@ export function ManagedRelayAuthProvider({ children }: { readonly children: Reac · 5 unmodified lines | ||
| 15 | 15 | const { getToken, isSignedIn, userId } = useAuth(); |
| 16 | 16 | |
| 17 | 17 | useEffect(() => { |
| 18 | − relayTokenProvider = isSignedIn ? () => getToken(RELAY_CLERK_TOKEN_OPTIONS) : null; | |
| 18 | + relayTokenProvider = isSignedIn ? () => getToken(resolveRelayClerkTokenOptions()) : null; | |
| 19 | 19 | setManagedRelaySession( |
| 20 | 20 | appAtomRegistry, |
| 21 | 21 | isSignedIn && userId |
| 22 | 22 | ? createManagedRelaySession({ |
| 23 | 23 | accountId: userId, |
| 24 | − readClerkToken: () => getToken(RELAY_CLERK_TOKEN_OPTIONS), | |
| 24 | + readClerkToken: () => getToken(resolveRelayClerkTokenOptions()), | |
| 25 | 25 | }) |
| 26 | 26 | : null, |
| 27 | 27 | ); |
| @@ -9,12 +9,16 @@ afterEach(() => { · 8 unmodified lines | ||
| 9 | 9 | describe("hasCloudPublicConfig", () => { |
| 10 | 10 | it("requires both public cloud values", () => { |
| 11 | 11 | vi.stubEnv("VITE_CLERK_PUBLISHABLE_KEY", ""); |
| 12 | + vi.stubEnv("VITE_CLERK_JWT_TEMPLATE", ""); | |
| 12 | 13 | vi.stubEnv("VITE_T3CODE_RELAY_URL", ""); |
| 13 | 14 | expect(hasCloudPublicConfig()).toBe(false); |
| 14 | 15 | |
| 15 | 16 | vi.stubEnv("VITE_CLERK_PUBLISHABLE_KEY", "pk_test_example"); |
| 16 | 17 | expect(hasCloudPublicConfig()).toBe(false); |
| 17 | 18 | |
| 19 | + vi.stubEnv("VITE_CLERK_JWT_TEMPLATE", "t3-relay"); | |
| 20 | + expect(hasCloudPublicConfig()).toBe(false); | |
| 21 | + | |
| 18 | 22 | vi.stubEnv("VITE_T3CODE_RELAY_URL", "https://relay.example.test"); |
| 19 | 23 | expect(hasCloudPublicConfig()).toBe(true); |
| 20 | 24 | }); |
| @@ -1,5 +1,8 @@ | ||
| 1 | +import { relayClerkTokenOptions } from "@t3tools/shared/relayAuth"; | |
| 2 | + | |
| 1 | 3 | export interface CloudPublicConfig { |
| 2 | 4 | readonly clerkPublishableKey: string | null; |
| 5 | + readonly clerkJwtTemplate: string | null; | |
| 3 | 6 | readonly relayUrl: string | null; |
| 4 | 7 | } |
| 5 | 8 | |
| @@ -12,6 +15,7 @@ export function resolveCloudPublicConfig(): CloudPublicConfig { · 6 unmodified lines | ||
| 12 | 15 | clerkPublishableKey: trimNonEmpty( |
| 13 | 16 | import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string | undefined, |
| 14 | 17 | ), |
| 18 | + clerkJwtTemplate: trimNonEmpty(import.meta.env.VITE_CLERK_JWT_TEMPLATE as string | undefined), | |
| 15 | 19 | relayUrl: |
| 16 | 20 | trimNonEmpty(import.meta.env.VITE_T3CODE_RELAY_URL as string | undefined)?.replace( |
| 17 | 21 | /\/+$/u, |
| @@ -22,5 +26,13 @@ export function resolveCloudPublicConfig(): CloudPublicConfig { · 4 unmodified lines | ||
| 22 | 26 | |
| 23 | 27 | export function hasCloudPublicConfig(): boolean { |
| 24 | 28 | 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); | |
| 26 | 38 | } |
| @@ -30,7 +30,6 @@ import { · 29 unmodified lines | ||
| 30 | 30 | type EnvironmentId, |
| 31 | 31 | } from "@t3tools/contracts"; |
| 32 | 32 | import { WsRpcClient } from "@t3tools/client-runtime"; |
| 33 | −import { RELAY_CLERK_TOKEN_OPTIONS } from "@t3tools/shared/relayAuth"; | |
| 34 | 33 | import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay"; |
| 35 | 34 | import * as DateTime from "effect/DateTime"; |
| 36 | 35 | |
| @@ -38,6 +37,7 @@ import { useCopyToClipboard } from "../../hooks/useCopyToClipboard"; · 1 unmodified line | ||
| 38 | 37 | import { cn } from "../../lib/utils"; |
| 39 | 38 | import { formatElapsedDurationLabel, formatExpiresInLabel } from "../../timestampFormat"; |
| 40 | 39 | import { resolveDesktopPairingUrl, resolveHostedPairingUrl } from "./pairingUrls"; |
| 40 | +import { resolveRelayClerkTokenOptions } from "../../cloud/publicConfig"; | |
| 41 | 41 | import { |
| 42 | 42 | SettingsPageContainer, |
| 43 | 43 | SettingsRow, |
| @@ -1640,7 +1640,7 @@ function ConfiguredCloudLinkRow({ canManageRelay }: { readonly canManageRelay: b · 1596 unmodified lines | ||
| 1640 | 1640 | setIsUpdating(true); |
| 1641 | 1641 | setOperationError(null); |
| 1642 | 1642 | try { |
| 1643 | − const clerkToken = await getToken(RELAY_CLERK_TOKEN_OPTIONS); | |
| 1643 | + const clerkToken = await getToken(resolveRelayClerkTokenOptions()); | |
| 1644 | 1644 | if (enabled) { |
| 1645 | 1645 | if (!clerkToken) { |
| 1646 | 1646 | throw new Error("Sign in from T3 Cloud settings before linking this environment."); |
| @@ -1753,7 +1753,7 @@ function ConfiguredCloudRemoteEnvironmentRows({ · 106 unmodified lines | ||
| 1753 | 1753 | const connectEnvironment = async (environment: RelayClientEnvironmentRecord) => { |
| 1754 | 1754 | setConnectingEnvironmentId(environment.environmentId); |
| 1755 | 1755 | try { |
| 1756 | − const clerkToken = await getToken(RELAY_CLERK_TOKEN_OPTIONS); | |
| 1756 | + const clerkToken = await getToken(resolveRelayClerkTokenOptions()); | |
| 1757 | 1757 | if (!clerkToken) { |
| 1758 | 1758 | throw new Error("Sign in from T3 Cloud settings before connecting this environment."); |
| 1759 | 1759 | } |
| @@ -8,6 +8,7 @@ interface ImportMetaEnv { · 7 unmodified lines | ||
| 8 | 8 | readonly VITE_HOSTED_APP_URL: string; |
| 9 | 9 | readonly VITE_HOSTED_APP_CHANNEL: string; |
| 10 | 10 | readonly VITE_CLERK_PUBLISHABLE_KEY: string; |
| 11 | + readonly VITE_CLERK_JWT_TEMPLATE: string; | |
| 11 | 12 | readonly APP_VERSION: string; |
| 12 | 13 | } |
| 13 | 14 | |
| @@ -18,6 +18,7 @@ const host = process.env.HOST?.trim() || "localhost"; · 17 unmodified lines | ||
| 18 | 18 | const configuredWsUrl = process.env.VITE_WS_URL?.trim(); |
| 19 | 19 | const configuredRelayUrl = repoEnv.VITE_T3CODE_RELAY_URL?.trim() || ""; |
| 20 | 20 | const configuredClerkPublishableKey = repoEnv.VITE_CLERK_PUBLISHABLE_KEY?.trim() || ""; |
| 21 | +const configuredClerkJwtTemplate = repoEnv.VITE_CLERK_JWT_TEMPLATE?.trim() || ""; | |
| 21 | 22 | const configuredHostedAppChannel = process.env.VITE_HOSTED_APP_CHANNEL?.trim() || ""; |
| 22 | 23 | const configuredAppVersion = process.env.APP_VERSION?.trim() || pkg.version; |
| 23 | 24 | const configuredHostedAppUrl = (() => { |
| @@ -132,6 +133,7 @@ export default defineConfig(() => { · 108 unmodified lines | ||
| 132 | 133 | "import.meta.env.VITE_WS_URL": JSON.stringify(configuredWsUrl ?? ""), |
| 133 | 134 | "import.meta.env.VITE_T3CODE_RELAY_URL": JSON.stringify(configuredRelayUrl), |
| 134 | 135 | "import.meta.env.VITE_CLERK_PUBLISHABLE_KEY": JSON.stringify(configuredClerkPublishableKey), |
| 136 | + "import.meta.env.VITE_CLERK_JWT_TEMPLATE": JSON.stringify(configuredClerkJwtTemplate), | |
| 135 | 137 | "import.meta.env.VITE_HOSTED_APP_URL": JSON.stringify(configuredHostedAppUrl ?? ""), |
| 136 | 138 | "import.meta.env.VITE_HOSTED_APP_CHANNEL": JSON.stringify(configuredHostedAppChannel), |
| 137 | 139 | "import.meta.env.APP_VERSION": JSON.stringify(configuredAppVersion), |
| @@ -10,7 +10,7 @@ This document covers the unified release workflow for stable and nightly desktop · 9 unmodified lines | ||
| 10 | 10 | - scheduled nightly check every three hours |
| 11 | 11 | - manual `workflow_dispatch` for either channel |
| 12 | 12 | - 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. | |
| 14 | 14 | - Builds four artifacts in parallel for both channels: |
| 15 | 15 | - macOS `arm64` DMG |
| 16 | 16 | - macOS `x64` DMG |
| @@ -38,7 +38,7 @@ release channels. · 21 unmodified lines | ||
| 38 | 38 | |
| 39 | 39 | `.github/workflows/deploy-relay.yml` deploys Alchemy stage `prod` on every push to `main`. It also |
| 40 | 40 | 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 | |
| 42 | 42 | desktop, CLI, or hosted web artifacts. |
| 43 | 43 | |
| 44 | 44 | Required repository variables shared by relay deployments: |
| @@ -59,6 +59,8 @@ Required `production` environment variables: · 14 unmodified lines | ||
| 59 | 59 | - `RELAY_DOMAIN` |
| 60 | 60 | - `RELAY_ZONE_NAME` |
| 61 | 61 | - `CLERK_PUBLISHABLE_KEY` |
| 62 | +- `CLERK_JWT_AUDIENCE` | |
| 63 | +- `CLERK_JWT_TEMPLATE` | |
| 62 | 64 | - `CLERK_CLI_OAUTH_CLIENT_ID` |
| 63 | 65 | - `APNS_ENVIRONMENT` |
| 64 | 66 | - `APNS_TEAM_ID` |
| @@ -1,8 +1,8 @@ | ||
| 1 | 1 | # T3 Cloud Clerk Setup |
| 2 | 2 | |
| 3 | 3 | 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. | |
| 6 | 6 | |
| 7 | 7 | ## Application Keys |
| 8 | 8 | |
| @@ -11,13 +11,13 @@ or `.env.local` file: · 2 unmodified lines | ||
| 11 | 11 | |
| 12 | 12 | ```dotenv |
| 13 | 13 | T3CODE_CLERK_PUBLISHABLE_KEY=<publishable key> |
| 14 | +T3CODE_CLERK_JWT_TEMPLATE=<JWT template name> | |
| 14 | 15 | T3CODE_RELAY_URL=https://relay.example.com |
| 15 | 16 | ``` |
| 16 | 17 | |
| 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. | |
| 21 | 21 | |
| 22 | 22 | Configuration precedence is: |
| 23 | 23 | |
| @@ -25,17 +25,18 @@ Configuration precedence is: · 1 unmodified line | ||
| 25 | 25 | 2. Repository-root `.env.local`. |
| 26 | 26 | 3. Repository-root `.env`. |
| 27 | 27 | |
| 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. | |
| 33 | 34 | |
| 34 | −When either public value is absent, cloud UI is omitted. | |
| 35 | +When any client-facing public value is absent, cloud UI is omitted. | |
| 35 | 36 | |
| 36 | 37 | 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. | |
| 39 | 40 | `bun --cwd infra/relay run deploy` invokes Alchemy from the relay directory, so Alchemy loads |
| 40 | 41 | `infra/relay/.env`. After a successful deployment, the wrapper updates the repository-root `.env` |
| 41 | 42 | with the HTTPS relay URL derived from `RELAY_DOMAIN`. The relay still requires |
| @@ -50,14 +51,16 @@ preview or developer stage. · 8 unmodified lines | ||
| 50 | 51 | |
| 51 | 52 | In **Clerk Dashboard > JWT templates**, create a template with: |
| 52 | 53 | |
| 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" }` | | |
| 57 | 58 | |
| 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. | |
| 61 | 64 | |
| 62 | 65 | ## Desktop OAuth Redirect Allowlist |
| 63 | 66 | |
| @@ -8,9 +8,11 @@ RELAY_ZONE_NAME=example.com · 7 unmodified lines | ||
| 8 | 8 | # RELAY_DOMAIN=relay.example.com |
| 9 | 9 | |
| 10 | 10 | # 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. | |
| 12 | 13 | CLERK_PUBLISHABLE_KEY=pk_test_... |
| 13 | 14 | CLERK_SECRET_KEY=sk_test_... |
| 15 | +CLERK_JWT_AUDIENCE=t3-code-relay | |
| 14 | 16 | |
| 15 | 17 | # Required: Apple Push Notification service |
| 16 | 18 | # Get these values from your Apple Developer account. Use `sandbox` for |
| @@ -133,6 +133,8 @@ The `production` GitHub environment must define these Actions variables: · 132 unmodified lines | ||
| 133 | 133 | - `RELAY_ZONE_NAME` |
| 134 | 134 | - `RELAY_DOMAIN` if overriding the derived production relay domain |
| 135 | 135 | - `CLERK_PUBLISHABLE_KEY` |
| 136 | +- `CLERK_JWT_AUDIENCE` | |
| 137 | +- `CLERK_JWT_TEMPLATE` | |
| 136 | 138 | - `APNS_ENVIRONMENT` |
| 137 | 139 | - `APNS_TEAM_ID` |
| 138 | 140 | - `APNS_KEY_ID` |
| @@ -18,6 +18,7 @@ export interface RelayConfigurationShape { · 17 unmodified lines | ||
| 18 | 18 | readonly apns: ApnsCredentials; |
| 19 | 19 | readonly clerkSecretKey: Redacted.Redacted<string>; |
| 20 | 20 | readonly clerkPublishableKey: string; |
| 21 | + readonly clerkJwtAudience: string; | |
| 21 | 22 | readonly apnsDeliveryJobSigningSecret: Redacted.Redacted<string>; |
| 22 | 23 | readonly cloudMintPrivateKey: Redacted.Redacted<string>; |
| 23 | 24 | readonly cloudMintPublicKey: string; |
| @@ -39,6 +39,7 @@ const config = RelayConfiguration.RelayConfiguration.of({ · 38 unmodified lines | ||
| 39 | 39 | apnsDeliveryJobSigningSecret: Redacted.make("job-signing-secret"), |
| 40 | 40 | clerkSecretKey: Redacted.make("clerk-secret"), |
| 41 | 41 | clerkPublishableKey: "pk_test_test", |
| 42 | + clerkJwtAudience: "t3-code-relay", | |
| 42 | 43 | cloudMintPrivateKey: Redacted.make("cloud-private-key"), |
| 43 | 44 | cloudMintPublicKey: "cloud-public-key", |
| 44 | 45 | managedEndpointBaseDomain: undefined, |
| @@ -129,6 +129,7 @@ const config = RelayConfiguration.RelayConfiguration.of({ · 128 unmodified lines | ||
| 129 | 129 | }, |
| 130 | 130 | clerkSecretKey: Redacted.make("clerk-secret"), |
| 131 | 131 | clerkPublishableKey: "pk_test_test", |
| 132 | + clerkJwtAudience: "t3-code-relay", | |
| 132 | 133 | apnsDeliveryJobSigningSecret: Redacted.make("apns-job-secret"), |
| 133 | 134 | cloudMintPrivateKey: Redacted.make("cloud-private-key"), |
| 134 | 135 | cloudMintPublicKey: "cloud-public-key", |
| @@ -26,6 +26,7 @@ const config = RelayConfiguration.RelayConfiguration.of({ · 25 unmodified lines | ||
| 26 | 26 | apnsDeliveryJobSigningSecret: Redacted.make("job-secret"), |
| 27 | 27 | clerkSecretKey: Redacted.make("clerk-secret"), |
| 28 | 28 | clerkPublishableKey: "pk_test_test", |
| 29 | + clerkJwtAudience: "t3-code-relay", | |
| 29 | 30 | cloudMintPrivateKey: Redacted.make(keyPair.privateKey), |
| 30 | 31 | cloudMintPublicKey: keyPair.publicKey, |
| 31 | 32 | managedEndpointBaseDomain: undefined, |
| @@ -66,6 +66,7 @@ const settings = RelayConfiguration.RelayConfiguration.of({ · 65 unmodified lines | ||
| 66 | 66 | apnsDeliveryJobSigningSecret: Redacted.make("job-secret"), |
| 67 | 67 | clerkSecretKey: Redacted.make("clerk-secret"), |
| 68 | 68 | clerkPublishableKey: "pk_test_test", |
| 69 | + clerkJwtAudience: "t3-code-relay", | |
| 69 | 70 | cloudMintPrivateKey: Redacted.make(cloudKeyPair.privateKey), |
| 70 | 71 | cloudMintPublicKey: cloudKeyPair.publicKey, |
| 71 | 72 | managedEndpointBaseDomain: undefined, |
| @@ -39,6 +39,7 @@ const config = RelayConfiguration.RelayConfiguration.of({ · 38 unmodified lines | ||
| 39 | 39 | apnsDeliveryJobSigningSecret: Redacted.make("job-secret"), |
| 40 | 40 | clerkSecretKey: Redacted.make("clerk-secret"), |
| 41 | 41 | clerkPublishableKey: "pk_test_test", |
| 42 | + clerkJwtAudience: "t3-code-relay", | |
| 42 | 43 | cloudMintPrivateKey: Redacted.make(relayKeyPair.privateKey), |
| 43 | 44 | cloudMintPublicKey: relayKeyPair.publicKey, |
| 44 | 45 | managedEndpointBaseDomain: undefined, |
| @@ -34,6 +34,7 @@ const config = RelayConfiguration.RelayConfiguration.of({ · 33 unmodified lines | ||
| 34 | 34 | apnsDeliveryJobSigningSecret: Redacted.make("job-secret"), |
| 35 | 35 | clerkSecretKey: Redacted.make("clerk-secret"), |
| 36 | 36 | clerkPublishableKey: "pk_test_test", |
| 37 | + clerkJwtAudience: "t3-code-relay", | |
| 37 | 38 | cloudMintPrivateKey: Redacted.make(keyPair.privateKey), |
| 38 | 39 | cloudMintPublicKey: keyPair.publicKey, |
| 39 | 40 | managedEndpointBaseDomain: undefined, |
| @@ -21,6 +21,7 @@ const config = RelayConfiguration.RelayConfiguration.of({ · 20 unmodified lines | ||
| 21 | 21 | apnsDeliveryJobSigningSecret: Redacted.make("job-secret"), |
| 22 | 22 | clerkSecretKey: Redacted.make("clerk-secret"), |
| 23 | 23 | clerkPublishableKey: "pk_test_test", |
| 24 | + clerkJwtAudience: "t3-code-relay", | |
| 24 | 25 | cloudMintPrivateKey: Redacted.make("cloud-private-key"), |
| 25 | 26 | cloudMintPublicKey: "cloud-public-key", |
| 26 | 27 | managedEndpointBaseDomain: "t3code.test", |
| @@ -40,6 +40,7 @@ const relaySettings: RelayConfiguration.RelayConfigurationShape = { · 39 unmodified lines | ||
| 40 | 40 | }, |
| 41 | 41 | clerkSecretKey: Redacted.make("clerk-secret-key"), |
| 42 | 42 | clerkPublishableKey: "pk_test_test", |
| 43 | + clerkJwtAudience: "t3-code-relay", | |
| 43 | 44 | apnsDeliveryJobSigningSecret: Redacted.make("apns-delivery-secret"), |
| 44 | 45 | cloudMintPrivateKey: Redacted.make("cloud-mint-private-key"), |
| 45 | 46 | cloudMintPublicKey: "cloud-mint-public-key", |
| @@ -52,13 +53,17 @@ describe("relay client authentication", () => { · 6 unmodified lines | ||
| 52 | 53 | Effect.gen(function* () { |
| 53 | 54 | vi.mocked(verifyToken).mockResolvedValue({ |
| 54 | 55 | sub: "user_session", |
| 55 | − aud: relaySettings.relayIssuer, | |
| 56 | + aud: relaySettings.clerkJwtAudience, | |
| 56 | 57 | } as never); |
| 57 | 58 | |
| 58 | 59 | expect(yield* verifyRelayClientBearerToken(relaySettings, "session-token")).toEqual({ |
| 59 | 60 | sub: "user_session", |
| 60 | 61 | mode: "clerk_session_bearer", |
| 61 | 62 | }); |
| 63 | + expect(verifyToken).toHaveBeenCalledWith("session-token", { | |
| 64 | + secretKey: "clerk-secret-key", | |
| 65 | + audience: relaySettings.clerkJwtAudience, | |
| 66 | + }); | |
| 62 | 67 | expect(createClerkClient).not.toHaveBeenCalled(); |
| 63 | 68 | }).pipe( |
| 64 | 69 | Effect.ensuring( |
| @@ -578,7 +578,7 @@ export const tokenApi = HttpApiBuilder.group( · 577 unmodified lines | ||
| 578 | 578 | const verified = yield* verifyClerkBearerToken(config, args.payload.subject_token).pipe( |
| 579 | 579 | Effect.catch(() => relayAuthInvalidError("invalid_bearer")), |
| 580 | 580 | ); |
| 581 | − if (!verified.sub || !hasExpectedClerkAudience(verified.aud, config.relayIssuer)) { | |
| 581 | + if (!verified.sub || !hasExpectedClerkAudience(verified.aud, config.clerkJwtAudience)) { | |
| 582 | 582 | return yield* relayAuthInvalidError("invalid_bearer"); |
| 583 | 583 | } |
| 584 | 584 | const proofKeyThumbprint = yield* requireDpopProof().pipe( |
| @@ -960,8 +960,7 @@ function clerkVerificationFailureReason(cause: unknown): string { · 375 unmodified lines | ||
| 960 | 960 | return "unknown"; |
| 961 | 961 | } |
| 962 | 962 | |
| 963 | −function hasExpectedClerkAudience(audience: unknown, relayIssuer: string): boolean { | |
| 964 | − const expectedAudience = normalizeRelayIssuer(relayIssuer); | |
| 963 | +function hasExpectedClerkAudience(audience: unknown, expectedAudience: string): boolean { | |
| 965 | 964 | return typeof audience === "string" |
| 966 | 965 | ? audience === expectedAudience |
| 967 | 966 | : Array.isArray(audience) && |
| @@ -973,7 +972,7 @@ function verifyClerkBearerToken(config: RelayConfiguration.RelayConfigurationSha · 5 unmodified lines | ||
| 973 | 972 | try: () => |
| 974 | 973 | verifyToken(token, { |
| 975 | 974 | secretKey: Redacted.value(config.clerkSecretKey), |
| 976 | − audience: normalizeRelayIssuer(config.relayIssuer), | |
| 975 | + audience: config.clerkJwtAudience, | |
| 977 | 976 | }), |
| 978 | 977 | catch: (cause) => new ClerkTokenVerificationFailed({ cause }), |
| 979 | 978 | }).pipe( |
| @@ -1015,7 +1014,7 @@ export function verifyRelayClientBearerToken( · 35 unmodified lines | ||
| 1015 | 1014 | ) { |
| 1016 | 1015 | return verifyClerkBearerToken(config, token).pipe( |
| 1017 | 1016 | Effect.flatMap((verified) => |
| 1018 | − verified.sub && hasExpectedClerkAudience(verified.aud, config.relayIssuer) | |
| 1017 | + verified.sub && hasExpectedClerkAudience(verified.aud, config.clerkJwtAudience) | |
| 1019 | 1018 | ? Effect.succeed({ sub: verified.sub, mode: "clerk_session_bearer" as const }) |
| 1020 | 1019 | : Effect.fail(new ClerkTokenVerificationFailed({ cause: "missing_relay_audience" })), |
| 1021 | 1020 | ), |
| @@ -130,6 +130,7 @@ export default class Api extends Cloudflare.Worker<Api>()( · 129 unmodified lines | ||
| 130 | 130 | |
| 131 | 131 | const clerkSecretKey = yield* Config.redacted("CLERK_SECRET_KEY"); |
| 132 | 132 | const clerkPublishableKey = yield* Config.string("CLERK_PUBLISHABLE_KEY"); |
| 133 | + const clerkJwtAudience = yield* Config.string("CLERK_JWT_AUDIENCE"); | |
| 133 | 134 | |
| 134 | 135 | const cloudMintPrivateKey = yield* cloudMintKeyPair.privateKey; |
| 135 | 136 | const cloudMintPublicKey = yield* cloudMintKeyPair.publicKey; |
| @@ -158,6 +159,7 @@ export default class Api extends Cloudflare.Worker<Api>()( · 22 unmodified lines | ||
| 158 | 159 | apnsDeliveryJobSigningSecret: yield* apnsDeliveryJobSigningSecret, |
| 159 | 160 | clerkSecretKey, |
| 160 | 161 | clerkPublishableKey, |
| 162 | + clerkJwtAudience, | |
| 161 | 163 | cloudMintPrivateKey: yield* cloudMintPrivateKey, |
| 162 | 164 | cloudMintPublicKey: yield* cloudMintPublicKey, |
| 163 | 165 | managedEndpointBaseDomain: yield* managedEndpointZoneName, |
| @@ -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 | +} | |
| @@ -21,6 +21,9 @@ describe("loadRepoEnv", () => { · 20 unmodified lines | ||
| 21 | 21 | expect(env.T3CODE_CLERK_PUBLISHABLE_KEY).toBeUndefined(); |
| 22 | 22 | expect(env.VITE_CLERK_PUBLISHABLE_KEY).toBeUndefined(); |
| 23 | 23 | 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(); | |
| 24 | 27 | expect(env.T3CODE_RELAY_URL).toBeUndefined(); |
| 25 | 28 | expect(env.VITE_T3CODE_RELAY_URL).toBeUndefined(); |
| 26 | 29 | }); |
| @@ -29,11 +32,11 @@ describe("loadRepoEnv", () => { · 2 unmodified lines | ||
| 29 | 32 | const repoRoot = makeTemporaryDirectory(); |
| 30 | 33 | writeFileSync( |
| 31 | 34 | 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", | |
| 33 | 36 | ); |
| 34 | 37 | writeFileSync( |
| 35 | 38 | 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", | |
| 37 | 40 | ); |
| 38 | 41 | |
| 39 | 42 | expect(loadRepoEnv({ baseEnv: {}, repoRoot }).T3CODE_RELAY_URL).toBe( |
| @@ -43,6 +46,7 @@ describe("loadRepoEnv", () => { · 3 unmodified lines | ||
| 43 | 46 | loadRepoEnv({ |
| 44 | 47 | baseEnv: { |
| 45 | 48 | T3CODE_CLERK_PUBLISHABLE_KEY: "pk_ci", |
| 49 | + T3CODE_CLERK_JWT_TEMPLATE: "template_ci", | |
| 46 | 50 | T3CODE_RELAY_URL: "https://ci.example.test", |
| 47 | 51 | }, |
| 48 | 52 | repoRoot, |
| @@ -51,6 +55,9 @@ describe("loadRepoEnv", () => { · 2 unmodified lines | ||
| 51 | 55 | T3CODE_CLERK_PUBLISHABLE_KEY: "pk_ci", |
| 52 | 56 | VITE_CLERK_PUBLISHABLE_KEY: "pk_ci", |
| 53 | 57 | 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", | |
| 54 | 61 | T3CODE_RELAY_URL: "https://ci.example.test", |
| 55 | 62 | VITE_T3CODE_RELAY_URL: "https://ci.example.test", |
| 56 | 63 | }); |
| @@ -60,10 +67,12 @@ describe("loadRepoEnv", () => { · 3 unmodified lines | ||
| 60 | 67 | expect( |
| 61 | 68 | resolvePublicConfig({ |
| 62 | 69 | VITE_CLERK_PUBLISHABLE_KEY: "pk_legacy", |
| 70 | + VITE_CLERK_JWT_TEMPLATE: "template_legacy", | |
| 63 | 71 | VITE_T3CODE_RELAY_URL: "https://legacy.example.test", |
| 64 | 72 | }), |
| 65 | 73 | ).toEqual({ |
| 66 | 74 | clerkPublishableKey: "pk_legacy", |
| 75 | + clerkJwtTemplate: "template_legacy", | |
| 67 | 76 | relayUrl: "https://legacy.example.test", |
| 68 | 77 | }); |
| 69 | 78 | }); |
| @@ -6,6 +6,7 @@ import * as NodeUtil from "node:util"; · 5 unmodified lines | ||
| 6 | 6 | |
| 7 | 7 | export interface T3CodePublicConfig { |
| 8 | 8 | readonly clerkPublishableKey: string | undefined; |
| 9 | + readonly clerkJwtTemplate: string | undefined; | |
| 9 | 10 | readonly relayUrl: string | undefined; |
| 10 | 11 | } |
| 11 | 12 | |
| @@ -37,6 +38,13 @@ export function loadRepoEnv({ · 25 unmodified lines | ||
| 37 | 38 | EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY: config.clerkPublishableKey, |
| 38 | 39 | } |
| 39 | 40 | : {}), |
| 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 | + : {}), | |
| 40 | 48 | ...(config.relayUrl |
| 41 | 49 | ? { |
| 42 | 50 | T3CODE_RELAY_URL: config.relayUrl, |
| @@ -54,6 +62,12 @@ export function resolvePublicConfig(...sources: readonly Environment[]): T3CodeP · 11 unmodified lines | ||
| 54 | 62 | "VITE_CLERK_PUBLISHABLE_KEY", |
| 55 | 63 | "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY", |
| 56 | 64 | ), |
| 65 | + clerkJwtTemplate: firstNonEmpty( | |
| 66 | + sources, | |
| 67 | + "T3CODE_CLERK_JWT_TEMPLATE", | |
| 68 | + "VITE_CLERK_JWT_TEMPLATE", | |
| 69 | + "EXPO_PUBLIC_CLERK_JWT_TEMPLATE", | |
| 70 | + ), | |
| 57 | 71 | relayUrl: firstNonEmpty(sources, "T3CODE_RELAY_URL", "VITE_T3CODE_RELAY_URL"), |
| 58 | 72 | }; |
| 59 | 73 | } |