1import { decodeJwt, importPKCS8, importSPKI, jwtVerify, SignJWT, type JWTPayload } from "jose";
2import * as DateTime from "effect/DateTime";
3import * as Effect from "effect/Effect";
4import * as Predicate from "effect/Predicate";
5import * as Schema from "effect/Schema";
6
7export const RELAY_LINK_PROOF_TYP = "t3-env-link+jwt";
8export const RELAY_MINT_REQUEST_TYP = "t3-cloud-mint+jwt";
9export const RELAY_HEALTH_REQUEST_TYP = "t3-cloud-health+jwt";
10export const RELAY_MINT_RESPONSE_TYP = "t3-env-mint+jwt";
11export const RELAY_HEALTH_RESPONSE_TYP = "t3-env-health+jwt";
12export const RELAY_ACTIVITY_PUBLISH_TYP = "t3-env-activity+jwt";
13export const RELAY_MANAGED_TUNNEL_RECOVERY_TYP = "t3-env-managed-tunnel-recovery+jwt";
14export const RELAY_HOOK_DELIVERY_TYP = "t3-relay-hook-delivery+jwt";
15/** Header carrying the signed proof that a webhook request came from the relay. */
16export const RELAY_HOOK_DELIVERY_HEADER = "x-t3-relay-delivery";
17
18export class RelayJwtError extends Schema.TaggedError<RelayJwtError>()("RelayJwtError", {
19 operation: Schema.Literals(["sign", "verify"]),
20 typ: Schema.String,
21 issuer: Schema.optional(Schema.String),
22 audience: Schema.optional(Schema.String),
23 cause: Schema.Defect(),
24}) {
25 override get message(): string {
26 return `Failed to ${this.operation} relay JWT of type "${this.typ}".`;
27 }
28
29 static diagnosticCode(error: RelayJwtError): string {
30 if (
31 Predicate.isObject(error.cause) &&
32 Predicate.hasProperty(error.cause, "code") &&
33 Predicate.isString(error.cause.code) &&
34 error.cause.code.length > 0
35 ) {
36 return error.cause.code;
37 }
38
39 return error.cause instanceof Error && error.cause.name ? error.cause.name : "unknown";
40 }
41}
42
43export function normalizeRelayIssuer(value: string): string {
44 return value.trim().replace(/\/+$/gu, "");
45}
46
47export function decodeRelayJwt(token: string): JWTPayload {
48 return decodeJwt(token);
49}
50
51function normalizePem(value: string): string {
52 return value.replace(/\\n/gu, "\n").trim();
53}
54
55export function signRelayJwt(input: {
56 readonly privateKey: string;
57 readonly typ: string;
58 readonly payload: JWTPayload;
59}): Effect.Effect<string, RelayJwtError> {
60 return Effect.tryPromise({
61 try: async () => {
62 const key = await importPKCS8(normalizePem(input.privateKey), "EdDSA");
63 return new SignJWT(input.payload)
64 .setProtectedHeader({ alg: "EdDSA", typ: input.typ })
65 .sign(key);
66 },
67 catch: (cause) => new RelayJwtError({ operation: "sign", typ: input.typ, cause }),
68 });
69}
70
71export function verifyRelayJwt(input: {
72 readonly publicKey: string;
73 readonly token: string;
74 readonly typ: string;
75 readonly issuer: string;
76 readonly audience: string;
77 readonly nowEpochSeconds: number;
78 readonly maxTokenAge?: string | number;
79}): Effect.Effect<JWTPayload, RelayJwtError> {
80 return Effect.tryPromise({
81 try: async () => {
82 const key = await importSPKI(normalizePem(input.publicKey), "EdDSA");
83 const verified = await jwtVerify(input.token, key, {
84 algorithms: ["EdDSA"],
85 typ: input.typ,
86 issuer: input.issuer,
87 audience: input.audience,
88 maxTokenAge: input.maxTokenAge ?? "5 minutes",
89 clockTolerance: 60,
90 currentDate: DateTime.toDate(DateTime.makeUnsafe(input.nowEpochSeconds * 1_000)),
91 });
92 return verified.payload;
93 },
94 catch: (cause) =>
95 new RelayJwtError({
96 operation: "verify",
97 typ: input.typ,
98 issuer: input.issuer,
99 audience: input.audience,
100 cause,
101 }),
102 });
103}