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