Skip to content
Source
Sign inSign up
Open History to see the last commit

publicConfig.ts

225 lines7.9 KB

1import {
2 connectLoopbackRedirectUri,
3 CONNECT_OAUTH_SCOPES,
4 DEFAULT_HOSTED_APP_URL,
5} from "@t3tools/shared/connectAuth";
6import { clerkFrontendApiUrlFromPublishableKey } from "@t3tools/shared/relayAuth";
7import { isRelayTracingEnabled } from "@t3tools/shared/otelEnvironment";
8import { normalizeSecureRelayUrl } from "@t3tools/shared/relayUrl";
9import * as Config from "effect/Config";
10import * as ConfigProvider from "effect/ConfigProvider";
11import * as Effect from "effect/Effect";
12import * as Schema from "effect/Schema";
13import * as SchemaIssue from "effect/SchemaIssue";
14​
15declare const __T3CODE_BUILD_RELAY_URL__: string | undefined;
16declare const __T3CODE_BUILD_CHANNEL__: "nightly" | "latest" | undefined;
17declare const __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: string | undefined;
18declare const __T3CODE_BUILD_CLERK_CLI_OAUTH_CLIENT_ID__: string | undefined;
19declare const __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_URL__: string | undefined;
20declare const __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_DATASET__: string | undefined;
21declare const __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_TOKEN__: string | undefined;
22​
23const CLOUD_CLI_OAUTH_LOOPBACK_PORT = 34338;
24const CLOUD_CLI_OAUTH_SCOPES = CONNECT_OAUTH_SCOPES;
25​
26function validateRelayUrl(value: string) {
27 const relayUrl = normalizeSecureRelayUrl(value);
28 return relayUrl === null
29 ? Effect.fail(
30 new Config.ConfigError(
31 new Schema.SchemaError(
32 new SchemaIssue.InvalidValue({
33 message: "Relay URL must be a secure absolute HTTPS origin.",
34 }),
35 ),
36 ),
37 )
38 : Effect.succeed(relayUrl);
40​
41function readBuildTimeValue(value: string | undefined): string {
42 return typeof value === "undefined" ? "" : value.trim();
44​
45function normalizeSecureUrl(value: string): string | null {
46 try {
47 const url = new URL(value);
48 return url.protocol === "https:" ? url.toString() : null;
49 } catch {
50 return null;
51 }
53​
54const buildTimeRelayUrl =
55 typeof __T3CODE_BUILD_RELAY_URL__ === "undefined"
56 ? ""
57 : (normalizeSecureRelayUrl(__T3CODE_BUILD_RELAY_URL__) ?? "");
58const buildTimeClerkPublishableKey = readBuildTimeValue(
59 typeof __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__ === "undefined"
60 ? undefined
61 : __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__,
62);
63const buildTimeClerkCliOAuthClientId = readBuildTimeValue(
64 typeof __T3CODE_BUILD_CLERK_CLI_OAUTH_CLIENT_ID__ === "undefined"
65 ? undefined
66 : __T3CODE_BUILD_CLERK_CLI_OAUTH_CLIENT_ID__,
67);
68const buildTimeRelayClientTracing = {
69 tracesUrl: readBuildTimeValue(
70 typeof __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_URL__ === "undefined"
71 ? undefined
72 : __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_URL__,
73 ),
74 tracesDataset: readBuildTimeValue(
75 typeof __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_DATASET__ === "undefined"
76 ? undefined
77 : __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_DATASET__,
78 ),
79 tracesToken: readBuildTimeValue(
80 typeof __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_TOKEN__ === "undefined"
81 ? undefined
82 : __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_TOKEN__,
83 ),
84} as const;
85​
86export function resolveRelayClientTracingConfig(
87 env: Readonly<Record<string, string | undefined>> = process.env,
88 fallback = buildTimeRelayClientTracing,
89) {
90 if (!isRelayTracingEnabled(env)) {
91 return null;
92 }
93​
94 const tracesUrl = env.T3CODE_RELAY_CLIENT_OTLP_TRACES_URL?.trim() || fallback.tracesUrl;
95 const tracesDataset =
96 env.T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET?.trim() || fallback.tracesDataset;
97 const tracesToken = env.T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN?.trim() || fallback.tracesToken;
98 const normalizedTracesUrl = normalizeSecureUrl(tracesUrl);
99 return normalizedTracesUrl && tracesDataset && tracesToken
100 ? { tracesUrl: normalizedTracesUrl, tracesDataset, tracesToken }
101 : null;
103​
104export function makeRelayUrlConfig(fallback = buildTimeRelayUrl) {
105 const runtimeConfig = Config.NonEmptyString("T3CODE_RELAY_URL");
106 return (fallback ? runtimeConfig.pipe(Config.withDefault(fallback)) : runtimeConfig).pipe(
107 Config.mapEffect(validateRelayUrl),
108 );
110​
111export const relayUrlConfig = makeRelayUrlConfig();
112​
113export const hostedAppUrlConfig = makePublicValueConfig(
114 "T3CODE_HOSTED_APP_URL",
115 DEFAULT_HOSTED_APP_URL,
116).pipe(Config.mapEffect(validateHostedAppUrl));
117​
118const NIGHTLY_HOSTED_APP_URL = "https://nightly.app.t3.codes";
119​
120/**
121 * Where agent sign-ins on the T3 Connect address are approved. A nightly build defaults to the
122 * nightly hosted app, which ships the same approval page; app.t3.codes serves stable.
123 */
124export const agentApprovalAppUrlConfig = makePublicValueConfig(
125 "T3CODE_HOSTED_APP_URL",
126 typeof __T3CODE_BUILD_CHANNEL__ !== "undefined" && __T3CODE_BUILD_CHANNEL__ === "nightly"
127 ? NIGHTLY_HOSTED_APP_URL
128 : DEFAULT_HOSTED_APP_URL,
129).pipe(Config.mapEffect(validateHostedAppUrl));
130​
131function validateHostedAppUrl(value: string) {
132 try {
133 const url = new URL(value);
134 const isLoopbackHttp =
135 url.protocol === "http:" &&
136 (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]");
137 if (
138 (url.protocol !== "https:" && !isLoopbackHttp) ||
139 url.pathname !== "/" ||
140 url.search !== "" ||
141 url.hash !== ""
142 ) {
143 throw new Error("invalid hosted app origin");
144 }
145 return Effect.succeed(url.origin);
146 } catch {
147 return Effect.fail(
148 new Config.ConfigError(
149 new Schema.SchemaError(
150 new SchemaIssue.InvalidValue({
151 message: "Hosted app URL must be an absolute HTTPS origin (or HTTP loopback origin).",
152 }),
153 ),
154 ),
155 );
156 }
158​
159function makePublicValueConfig(name: string, fallback: string) {
160 const runtimeConfig = Config.NonEmptyString(name);
161 return (fallback ? runtimeConfig.pipe(Config.withDefault(fallback)) : runtimeConfig).pipe(
162 Config.map((value) => value.trim()),
163 );
165​
166export interface CloudCliOAuthConfig {
167 readonly tokenEndpoint: string;
168 readonly deviceAuthorizationEndpoint: string;
169 readonly clientId: string;
170 readonly loopbackPort: number;
171 readonly redirectUri: string;
172 readonly scopes: typeof CLOUD_CLI_OAUTH_SCOPES;
174​
175export function makeCloudCliOAuthConfig({
176 clerkPublishableKeyFallback = buildTimeClerkPublishableKey,
177 clerkCliOAuthClientIdFallback = buildTimeClerkCliOAuthClientId,
178}: {
179 readonly clerkPublishableKeyFallback?: string;
180 readonly clerkCliOAuthClientIdFallback?: string;
181} = {}) {
182 return Config.all({
183 clerkPublishableKey: makePublicValueConfig(
184 "T3CODE_CLERK_PUBLISHABLE_KEY",
185 clerkPublishableKeyFallback,
186 ),
187 clientId: makePublicValueConfig(
188 "T3CODE_CLERK_CLI_OAUTH_CLIENT_ID",
189 clerkCliOAuthClientIdFallback,
190 ),
191 }).pipe(
192 Config.mapEffect(({ clerkPublishableKey, clientId }) =>
193 Effect.try({
194 try: () => clerkFrontendApiUrlFromPublishableKey(clerkPublishableKey),
195 catch: (cause) =>
196 new Config.ConfigError(
197 new ConfigProvider.SourceError({
198 message: "Failed to derive Clerk Frontend API URL from the publishable key.",
199 cause,
200 }),
201 ),
202 }).pipe(
203 Effect.map(
204 (clerkFrontendApiUrl) =>
205 ({
206 tokenEndpoint: `${clerkFrontendApiUrl}/oauth/token`,
207 deviceAuthorizationEndpoint: `${clerkFrontendApiUrl}/oauth/device_authorization`,
208 clientId,
209 loopbackPort: CLOUD_CLI_OAUTH_LOOPBACK_PORT,
210 redirectUri: connectLoopbackRedirectUri(CLOUD_CLI_OAUTH_LOOPBACK_PORT),
211 scopes: CLOUD_CLI_OAUTH_SCOPES,
212 }) satisfies CloudCliOAuthConfig,
213 ),
214 ),
215 ),
216 );
218​
219export const cloudCliOAuthConfig = makeCloudCliOAuthConfig();
220​
221export const hasCloudPublicConfig = Boolean(
222 (normalizeSecureRelayUrl(process.env.T3CODE_RELAY_URL ?? "") ?? buildTimeRelayUrl) &&
223 (process.env.T3CODE_CLERK_PUBLISHABLE_KEY?.trim() || buildTimeClerkPublishableKey) &&
224 (process.env.T3CODE_CLERK_CLI_OAUTH_CLIENT_ID?.trim() || buildTimeClerkCliOAuthClientId),
225);