1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Result from "effect/Result";
5
6import * as RelayConfiguration from "../Config.ts";
7import {
8 MANAGED_ENDPOINT_KEY_PATTERN,
9 managedEndpointTunnelNameForKey,
10} from "../deploymentConfig.ts";
11import { validateManagedEndpoint } from "../environments/EnvironmentConnector.ts";
12import * as EnvironmentLinks from "../environments/EnvironmentLinks.ts";
13import * as ManagedEndpointAllocations from "../environments/ManagedEndpointAllocations.ts";
14import * as HookInbox from "./HookInbox.ts";
15
16/** The endpoint key of an environment's own managed endpoint, for authenticated callers. */
17export const endpointKeyForTunnelName = (namespace: string, tunnelName: string): string | null => {
18 const prefix = managedEndpointTunnelNameForKey(namespace, "");
19 const key = tunnelName.startsWith(prefix) ? tunnelName.slice(prefix.length) : "";
20 return MANAGED_ENDPOINT_KEY_PATTERN.test(key) ? key : null;
21};
22
23type PersistenceError =
24 | EnvironmentLinks.EnvironmentLinkEnvironmentLookupPersistenceError
25 | ManagedEndpointAllocations.ManagedEndpointAllocationPersistenceError;
26
27interface HookEndpoint {
28 readonly httpBaseUrl: string;
29 readonly environmentId: string;
30 readonly holdWhileOffline: boolean;
31}
32
33export class HeldHooks extends Context.Service<
34 HeldHooks,
35 {
36 /**
37 * The ready managed endpoint a webhook URL's endpoint key names, with whether
38 * its link opted in to holding webhooks while offline. The key is the tunnel
39 * name's hash of user and environment, so it names exactly one allocation and
40 * at most one active link; nobody else can link their way onto it.
41 */
42 readonly resolveEndpoint: (
43 endpointKey: string,
44 ) => Effect.Effect<HookEndpoint | null, PersistenceError>;
45 /**
46 * Saves an environment's opt-in. Opting out also drops what is already held,
47 * rather than delivering it later to an environment that said it does not
48 * want it. Only the links the caller's environment key proved are touched.
49 */
50 readonly setHoldWhileOffline: (input: {
51 readonly environmentId: string;
52 readonly environmentPublicKey: string;
53 readonly holdWebhooksWhileOffline: boolean;
54 }) => Effect.Effect<void, PersistenceError | HookInbox.HookInboxError>;
55 /** Delivers what the environment's endpoints hold now; true when anything was waiting. */
56 readonly wake: (input: {
57 readonly environmentId: string;
58 readonly environmentPublicKey: string;
59 }) => Effect.Effect<boolean, PersistenceError | HookInbox.HookInboxError>;
60 }
61>()("t3code-relay/hooks/HeldHooks") {}
62
63const make = Effect.gen(function* () {
64 const links = yield* EnvironmentLinks.EnvironmentLinks;
65 const allocations = yield* ManagedEndpointAllocations.ManagedEndpointAllocations;
66 const settings = yield* RelayConfiguration.RelayConfiguration;
67 const inbox = yield* HookInbox.HookInbox;
68
69 const resolveEndpoint: HeldHooks["Service"]["resolveEndpoint"] = Effect.fn(
70 "relay.hooks.resolve_endpoint",
71 )(function* (endpointKey) {
72 if (!settings.managedEndpointNamespace) return null;
73 const allocation = yield* allocations.getByTunnelName(
74 managedEndpointTunnelNameForKey(settings.managedEndpointNamespace, endpointKey),
75 );
76 if (allocation === null) return null;
77 const [link] = yield* links.findActiveManagedForEnvironment({
78 environmentId: allocation.environmentId,
79 userId: allocation.userId,
80 });
81 if (!link) return null;
82 const result = validateManagedEndpoint({
83 link,
84 allocation,
85 baseDomain: settings.managedEndpointBaseDomain,
86 });
87 if (Result.isFailure(result)) return null;
88 return {
89 ...result.success,
90 environmentId: allocation.environmentId,
91 holdWhileOffline: link.holdWebhooksWhileOffline,
92 };
93 });
94
95 /**
96 * Endpoint keys of the managed links the calling environment key proved.
97 * The environment id alone would also match other accounts' links of it.
98 */
99 const ownEndpointKeys = Effect.fn("relay.hooks.own_endpoint_keys")(function* (input: {
100 readonly environmentId: string;
101 readonly environmentPublicKey: string;
102 }) {
103 const namespace = settings.managedEndpointNamespace;
104 if (!namespace) return [];
105 const ownLinks = yield* links.findActiveManagedForEnvironment(input);
106 const keys: Array<string> = [];
107 for (const link of ownLinks) {
108 const allocation = yield* allocations.get({
109 userId: link.userId,
110 environmentId: input.environmentId,
111 });
112 const key = allocation ? endpointKeyForTunnelName(namespace, allocation.tunnelName) : null;
113 if (key !== null) keys.push(key);
114 }
115 return keys;
116 });
117
118 const setHoldWhileOffline: HeldHooks["Service"]["setHoldWhileOffline"] = Effect.fn(
119 "relay.hooks.set_hold_while_offline",
120 )(function* (input) {
121 yield* links.setHoldWebhooksWhileOffline(input);
122 if (input.holdWebhooksWhileOffline) return;
123 for (const endpointKey of yield* ownEndpointKeys(input)) {
124 yield* inbox.clear({ endpointKey });
125 }
126 });
127
128 const wake: HeldHooks["Service"]["wake"] = Effect.fn("relay.hooks.wake")(function* (input) {
129 let pending = false;
130 for (const endpointKey of yield* ownEndpointKeys(input)) {
131 const endpoint = yield* resolveEndpoint(endpointKey);
132 if (endpoint === null) continue;
133 if (yield* inbox.wake({ endpointKey, baseUrl: endpoint.httpBaseUrl })) {
134 pending = true;
135 }
136 }
137 return pending;
138 });
139
140 return HeldHooks.of({ resolveEndpoint, setHoldWhileOffline, wake });
141});
142
143export const layer = Layer.effect(HeldHooks, make);