1import Constants from "expo-constants";
2import { relayClerkTokenOptions } from "@t3tools/shared/relayAuth";
3import { normalizeSecureRelayUrl } from "@t3tools/shared/relayUrl";
4import * as Schema from "effect/Schema";
5
6export class CloudPublicConfigMissingError extends Schema.TaggedError<CloudPublicConfigMissingError>()(
7 "CloudPublicConfigMissingError",
8 {
9 key: Schema.Literal("T3CODE_CLERK_JWT_TEMPLATE"),
10 },
11) {
12 override get message(): string {
13 return `${this.key} is not configured.`;
14 }
15}
16
17export interface CloudPublicConfig {
18 readonly clerk: {
19 readonly publishableKey: string | null;
20 readonly jwtTemplate: string | null;
21 };
22 readonly relay: {
23 readonly url: string | null;
24 };
25 readonly observability: {
26 readonly tracesUrl: string | null;
27 readonly tracesDataset: string | null;
28 readonly tracesToken: string | null;
29 };
30}
31
32type UntrustedSection<T> = {
33 readonly [Key in keyof T]?: unknown;
34};
35
36type ExpoExtra =
37 | {
38 readonly [Section in keyof CloudPublicConfig]?: UntrustedSection<CloudPublicConfig[Section]>;
39 }
40 | undefined;
41
42function trimNonEmpty(value: unknown): string | null {
43 return typeof value === "string" && value.trim() ? value.trim() : null;
44}
45
46function normalizeSecureUrl(value: unknown): string | null {
47 const raw = trimNonEmpty(value);
48 if (raw === null) {
49 return null;
50 }
51 try {
52 const url = new URL(raw);
53 return url.protocol === "https:" ? url.toString() : null;
54 } catch {
55 return null;
56 }
57}
58
59export function resolveCloudPublicConfig(extra: ExpoExtra = Constants.expoConfig?.extra) {
60 return {
61 clerk: {
62 publishableKey: trimNonEmpty(extra?.clerk?.publishableKey),
63 jwtTemplate: trimNonEmpty(extra?.clerk?.jwtTemplate),
64 },
65 relay: {
66 url: normalizeSecureRelayUrl(trimNonEmpty(extra?.relay?.url) ?? ""),
67 },
68 observability: {
69 tracesUrl: normalizeSecureUrl(extra?.observability?.tracesUrl),
70 tracesDataset: trimNonEmpty(extra?.observability?.tracesDataset),
71 tracesToken: trimNonEmpty(extra?.observability?.tracesToken),
72 },
73 } satisfies CloudPublicConfig;
74}
75
76export function hasCloudPublicConfig(): boolean {
77 const config = resolveCloudPublicConfig();
78 return Boolean(config.clerk.publishableKey && config.clerk.jwtTemplate && config.relay.url);
79}
80
81type Configured<T> = {
82 readonly [Key in keyof T]: NonNullable<T[Key]>;
83};
84
85type TracingPublicConfig = Omit<CloudPublicConfig, "observability"> & {
86 readonly observability: Configured<CloudPublicConfig["observability"]>;
87};
88
89export function hasTracingPublicConfig(
90 config: CloudPublicConfig = resolveCloudPublicConfig(),
91): config is TracingPublicConfig {
92 return Boolean(
93 config.observability.tracesUrl &&
94 config.observability.tracesDataset &&
95 config.observability.tracesToken,
96 );
97}
98
99export function resolveRelayClerkTokenOptions() {
100 const { jwtTemplate } = resolveCloudPublicConfig().clerk;
101 if (!jwtTemplate) {
102 throw new CloudPublicConfigMissingError({ key: "T3CODE_CLERK_JWT_TEMPLATE" });
103 }
104 return relayClerkTokenOptions(jwtTemplate);
105}