1import * as Config from "effect/Config";
2import * as Context from "effect/Context";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Redacted from "effect/Redacted";
6import * as Schema from "effect/Schema";
7
8export const ApnsEnvironment = Schema.Literals(["sandbox", "production"]);
9export type ApnsEnvironment = typeof ApnsEnvironment.Type;
10
11export const ManagedEndpointCleanupMode = Schema.Literals(["off", "dry-run", "enabled"]);
12export type ManagedEndpointCleanupMode = typeof ManagedEndpointCleanupMode.Type;
13const decodeManagedEndpointCleanupMode = Schema.decodeUnknownEffect(ManagedEndpointCleanupMode);
14
15const RELAY_TUNNEL_CLEANUP_MODE = "RELAY_TUNNEL_CLEANUP_MODE";
16/** Separate switch for tunnels whose host never registered recovery. */
17const RELAY_LEGACY_TUNNEL_CLEANUP_MODE = "RELAY_LEGACY_TUNNEL_CLEANUP_MODE";
18
19const cleanupModeConfig = (name: string) =>
20 Config.String(name).pipe(
21 Config.withDefault("off"),
22 Config.map((value) => value.trim() || "off"),
23 Config.mapEffect((value) =>
24 decodeManagedEndpointCleanupMode(value).pipe(
25 Effect.mapError((error) => new Config.ConfigError(error)),
26 ),
27 ),
28 );
29
30export const managedEndpointCleanupModeConfig = cleanupModeConfig(RELAY_TUNNEL_CLEANUP_MODE);
31export const legacyManagedEndpointCleanupModeConfig = cleanupModeConfig(
32 RELAY_LEGACY_TUNNEL_CLEANUP_MODE,
33);
34
35/**
36 * Overrides the 7-day legacy grace period, in minutes, so the disposable
37 * canary stage can exercise legacy cleanup. Ignored on the prod stage.
38 */
39const RELAY_LEGACY_TUNNEL_GRACE_MINUTES = "RELAY_LEGACY_TUNNEL_GRACE_MINUTES";
40
41// A zero or negative override would be ignored at runtime, silently leaving
42// the canary on the 7-day grace period, so reject it when the deploy reads it.
43export const legacyTunnelGraceMinutesConfig = Config.option(
44 Config.schema(
45 Schema.NumberFromString.pipe(Schema.check(Schema.isInt(), Schema.isGreaterThan(0))),
46 RELAY_LEGACY_TUNNEL_GRACE_MINUTES,
47 ),
48);
49
50export interface ApnsCredentials {
51 readonly teamId: string;
52 readonly keyId: string;
53 readonly privateKey: Redacted.Redacted<string>;
54 readonly bundleId: string;
55 readonly environment: ApnsEnvironment;
56}
57
58export class RelayConfiguration extends Context.Service<
59 RelayConfiguration,
60 {
61 readonly relayIssuer: string;
62 readonly apns: ApnsCredentials | null;
63 readonly fcmServiceAccount?: Redacted.Redacted<string>;
64 readonly clerkSecretKey: Redacted.Redacted<string>;
65 readonly clerkPublishableKey: string;
66 readonly clerkJwtAudience: string;
67 readonly apnsDeliveryJobSigningSecret: Redacted.Redacted<string>;
68 readonly cloudMintPrivateKey: Redacted.Redacted<string>;
69 readonly cloudMintPublicKey: string;
70 readonly managedEndpointBaseDomain: string | undefined;
71 readonly managedEndpointNamespace: string | undefined;
72 readonly managedEndpointCleanupMode?: ManagedEndpointCleanupMode;
73 readonly legacyManagedEndpointCleanupMode?: ManagedEndpointCleanupMode;
74 /** Canary-only override of the legacy grace period; ignored on prod. */
75 readonly legacyTunnelGraceMinutes?: number;
76 }
77>()("t3code-relay/Config/RelayConfiguration") {}
78
79export const make = (configuration: RelayConfiguration["Service"]) =>
80 RelayConfiguration.of(configuration);
81
82export const layer = (configuration: RelayConfiguration["Service"]) =>
83 Layer.succeed(RelayConfiguration, make(configuration));