1import * as Schema from "effect/Schema";
2
3const ClerkPublishableKeyPrefix = Schema.Literals(["pk_test", "pk_live", "unknown"]);
4
5export class ClerkPublishableKeyDecodeError extends Schema.TaggedError<ClerkPublishableKeyDecodeError>()(
6 "ClerkPublishableKeyDecodeError",
7 {
8 keyPrefix: ClerkPublishableKeyPrefix,
9 cause: Schema.Defect(),
10 },
11) {
12 override get message(): string {
13 return `Failed to decode Clerk publishable key (${this.keyPrefix}).`;
14 }
15}
16
17export class ClerkPublishableKeyFrontendApiError extends Schema.TaggedError<ClerkPublishableKeyFrontendApiError>()(
18 "ClerkPublishableKeyFrontendApiError",
19 {
20 keyPrefix: ClerkPublishableKeyPrefix,
21 frontendApi: Schema.String,
22 reason: Schema.Literals(["empty", "contains-path", "invalid-url"]),
23 cause: Schema.optional(Schema.Defect()),
24 },
25) {
26 override get message(): string {
27 return `Invalid Clerk frontend API decoded from publishable key (${this.keyPrefix}; ${this.reason}).`;
28 }
29}
30
31function parseClerkFrontendApi(publishableKey: string): {
32 readonly hostname: string;
33 readonly url: string;
34} {
35 const keyPrefix = publishableKey.startsWith("pk_test_")
36 ? "pk_test"
37 : publishableKey.startsWith("pk_live_")
38 ? "pk_live"
39 : "unknown";
40 const encodedFrontendApi = publishableKey.split("_").slice(2).join("_");
41 let frontendApi: string;
42 try {
43 frontendApi = globalThis.atob(encodedFrontendApi).replace(/\$$/u, "");
44 } catch (cause) {
45 throw new ClerkPublishableKeyDecodeError({ keyPrefix, cause });
46 }
47
48 if (frontendApi.length === 0) {
49 throw new ClerkPublishableKeyFrontendApiError({
50 keyPrefix,
51 frontendApi,
52 reason: "empty",
53 });
54 }
55 if (frontendApi.includes("/")) {
56 throw new ClerkPublishableKeyFrontendApiError({
57 keyPrefix,
58 frontendApi,
59 reason: "contains-path",
60 });
61 }
62
63 const url = `https://${frontendApi}`;
64 try {
65 return { hostname: new URL(url).hostname, url };
66 } catch (cause) {
67 throw new ClerkPublishableKeyFrontendApiError({
68 keyPrefix,
69 frontendApi,
70 reason: "invalid-url",
71 cause,
72 });
73 }
74}
75
76export function clerkFrontendApiUrlFromPublishableKey(publishableKey: string): string {
77 return parseClerkFrontendApi(publishableKey).url;
78}
79
80export function clerkFrontendApiHostnameFromPublishableKey(publishableKey: string): string {
81 return parseClerkFrontendApi(publishableKey).hostname;
82}
83
84export function relayClerkTokenOptions(template: string) {
85 return {
86 template,
87 skipCache: true,
88 } as const;
89}