1import * as Alchemy from "alchemy";
2import * as Axiom from "@alchemy.run/axiom";
3import * as Cloudflare from "@alchemy.run/cloudflare";
4import * as Drizzle from "@alchemy.run/drizzle/Postgres";
5import * as Config from "effect/Config";
6import * as Cause from "effect/Cause";
7import * as DateTime from "effect/DateTime";
8import * as Digest from "effect/crypto/Digest";
9import * as Mac from "effect/crypto/Mac";
10import * as SecureRandom from "effect/crypto/SecureRandom";
11import * as Signature from "effect/crypto/Signature";
12import * as Effect from "effect/Effect";
13import * as Layer from "effect/Layer";
14import * as Option from "effect/Option";
15import * as Redacted from "effect/Redacted";
16import * as Stream from "effect/Stream";
17import * as Etag from "effect/http/Etag";
18import * as HttpMiddleware from "effect/http/HttpMiddleware";
19import * as HttpPlatform from "effect/http/HttpPlatform";
20import * as HttpRouter from "effect/http/HttpRouter";
21import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";
22import * as HttpApiScalar from "effect/http-api/HttpApiScalar";
23
24import { RelayApi } from "@t3tools/contracts/relay";
25
26import {
27 RELAY_HTTP_ROUTER_CONFIG,
28 traceRelayHttpRequestWith,
29 withoutCapturedParentSpan,
30} from "./http/Api.ts";
31import * as CloudMintSigner from "./auth/CloudMintSigner.ts";
32import * as RelayHttpApi from "./http/Api.ts";
33import { ManagedEndpointZone, RelayApiZone, RelayDeploymentConfig } from "./zone.ts";
34import { RelayObservability } from "./observability.ts";
35import * as Observability from "./observability.ts";
36import * as DeliveryAttempts from "./agentActivity/DeliveryAttempts.ts";
37import * as AgentActivityRows from "./agentActivity/AgentActivityRows.ts";
38import * as Devices from "./agentActivity/Devices.ts";
39import * as DpopProofs from "./auth/DpopProofs.ts";
40import * as RelayTokens from "./auth/RelayTokens.ts";
41import * as EnvironmentCredentials from "./environments/EnvironmentCredentials.ts";
42import * as EnvironmentLinks from "./environments/EnvironmentLinks.ts";
43import * as ManagedEndpointAllocations from "./environments/ManagedEndpointAllocations.ts";
44import * as LiveActivities from "./agentActivity/LiveActivities.ts";
45import * as RelayDb from "./db.ts";
46import {
47 RelayApnsDeliveryDeadLetterQueue,
48 RelayApnsDeliveryQueue,
49 RelayFcmDeliveryQueue,
50 RelayFcmDeliveryDeadLetterQueue,
51} from "./queues.ts";
52import * as WebCrypto from "./WebCrypto.ts";
53import * as FcmAssertionSigner from "./agentActivity/FcmAssertionSigner.ts";
54import * as FcmClient from "./agentActivity/FcmClient.ts";
55import * as FcmDeliveryQueueSender from "./agentActivity/FcmDeliveryQueueSender.ts";
56import * as FcmDeliveries from "./agentActivity/FcmDeliveries.ts";
57import * as FcmDeliveryQueueConsumer from "./agentActivity/FcmDeliveryQueueConsumer.ts";
58import * as RelayConfiguration from "./Config.ts";
59import * as AgentActivityPublisher from "./agentActivity/AgentActivityPublisher.ts";
60import * as ApnsClient from "./agentActivity/ApnsClient.ts";
61import * as ApnsProviderTokens from "./agentActivity/ApnsProviderTokens.ts";
62import * as ApnsDeliveryQueue from "./agentActivity/ApnsDeliveryQueue.ts";
63import * as ApnsDeliveries from "./agentActivity/ApnsDeliveries.ts";
64import * as CloudflareApiPause from "./environments/CloudflareApiPause.ts";
65import * as EnvironmentConnector from "./environments/EnvironmentConnector.ts";
66import * as EnvironmentLinker from "./environments/EnvironmentLinker.ts";
67import * as EnvironmentPublishSignatures from "./environments/EnvironmentPublishSignatures.ts";
68import * as ManagedEndpointProvider from "./environments/ManagedEndpointProvider.ts";
69import * as ManagedEndpointReaper from "./environments/ManagedEndpointReaper.ts";
70import * as ManagedTunnelLimits from "./environments/ManagedTunnelLimits.ts";
71import * as T3ChatAccounts from "./t3chat/T3ChatAccounts.ts";
72import * as MobileRegistrations from "./agentActivity/MobileRegistrations.ts";
73import * as HookForwarder from "./hooks/HookForwarder.ts";
74import * as HeldHooks from "./hooks/HeldHooks.ts";
75import * as HookInbox from "./hooks/HookInbox.ts";
76import * as HookInboxObject from "./hooks/HookInboxObject.ts";
77import * as MobileTelemetry from "./mobileTelemetry/MobileTelemetry.ts";
78
79const layerWebcrypto = Layer.mergeAll(
80 Digest.layerWebCrypto,
81 Mac.layerWebCrypto,
82 Signature.layerWebCrypto,
83).pipe(Layer.provideMerge(SecureRandom.layerWebCrypto));
84
85const layerHttpPlatformNotSupported = Layer.succeed(HttpPlatform.HttpPlatform, {
86 platform: "web",
87 compression: {
88 algorithms: new Set<HttpPlatform.CompressionAlgorithm>(),
89 compressResponse: (response) => Effect.succeed(response),
90 },
91 fileResponse: Effect.fn("HttpPlatform.fileResponse")(function () {
92 return Effect.die("Relay API does not serve filesystem responses");
93 }),
94 fileWebResponse: Effect.fn("HttpPlatform.fileWebResponse")(function () {
95 return Effect.die("Relay API does not serve file responses");
96 }),
97});
98
99const layerRelayApi = Layer.mergeAll(
100 RelayHttpApi.layerHealthApi,
101 RelayHttpApi.layerMetadataApi,
102 RelayHttpApi.layerMobileApi,
103 RelayHttpApi.layerClientApi,
104 RelayHttpApi.layerTokenApi,
105 RelayHttpApi.layerDpopClientApi,
106 RelayHttpApi.layerServerApi,
107);
108
109const CloudMintKeyPair = Alchemy.KeyPair("CloudMintKeyPair");
110const ApnsDeliveryJobSigningSecret = Alchemy.makeRandom("ApnsDeliveryJobSigningSecret", {
111 bytes: 32,
112});
113
114export class Api extends Cloudflare.Worker<Api, {}>()("Api") {}
115
116export const layer = Api.make(
117 RelayDeploymentConfig.pipe(
118 Effect.map(({ relayPublicDomain }) => ({
119 main: import.meta.filename,
120 compatibility: {
121 date: "2026-05-22",
122 flags: ["nodejs_compat"],
123 },
124 // Requests run several Postgres queries in sequence. At the user's edge
125 // each one paid the round trip to the database (~300 ms from JNB); next
126 // to PlanetScale `us-west` the request pays that distance once.
127 placement: { region: "aws:us-west-2" },
128 domain: relayPublicDomain,
129 })),
130 Effect.orDie,
131 ),
132 Effect.gen(function* () {
133 const { relayPublicOrigin, stage } = yield* RelayDeploymentConfig;
134 const apnsDeliveryQueue = yield* RelayApnsDeliveryQueue;
135 const apnsDeliveryDeadLetterQueue = yield* RelayApnsDeliveryDeadLetterQueue;
136 const fcmDeliveryQueue = yield* RelayFcmDeliveryQueue;
137 const fcmDeliveryDeadLetterQueue = yield* RelayFcmDeliveryDeadLetterQueue;
138 const cloudMintKeyPair = yield* CloudMintKeyPair;
139 const relayApiZone = yield* RelayApiZone;
140 const managedEndpointZone = yield* ManagedEndpointZone;
141 const randomApnsDeliveryJobSigningSecret = yield* ApnsDeliveryJobSigningSecret;
142 const observability = yield* RelayObservability;
143
144 const apnsEnabled = yield* Config.Boolean("APNS_ENABLED").pipe(Config.withDefault(true));
145 const apnsCredentials = apnsEnabled
146 ? {
147 environment: yield* Config.schema(RelayConfiguration.ApnsEnvironment, "APNS_ENVIRONMENT"),
148 teamId: yield* Config.String("APNS_TEAM_ID"),
149 keyId: yield* Config.String("APNS_KEY_ID"),
150 bundleId: yield* Config.String("APNS_BUNDLE_ID"),
151 privateKey: yield* Config.Redacted("APNS_PRIVATE_KEY"),
152 }
153 : null;
154 const fcmServiceAccount = Option.getOrUndefined(
155 Option.filter(
156 yield* Config.option(Config.Redacted("FCM_SERVICE_ACCOUNT")),
157 (value) => Redacted.value(value).trim().length > 0,
158 ),
159 );
160 const apnsDeliveryJobSigningSecret = yield* randomApnsDeliveryJobSigningSecret;
161 const apnsDeliveryQueueSender = yield* Cloudflare.Queues.WriteQueue(apnsDeliveryQueue);
162 const fcmDeliveryQueueSender = yield* Cloudflare.Queues.WriteQueue(fcmDeliveryQueue);
163
164 const clerkSecretKey = yield* Config.Redacted("CLERK_SECRET_KEY");
165 const clerkPublishableKey = yield* Config.String("CLERK_PUBLISHABLE_KEY");
166 const clerkJwtAudience = yield* Config.String("CLERK_JWT_AUDIENCE");
167 const t3ChatLinkSecret = Option.getOrUndefined(
168 Option.filter(
169 yield* Config.option(Config.Redacted("T3CHAT_LINK_SECRET")),
170 (value) => Redacted.value(value).trim().length > 0,
171 ),
172 );
173 const t3ChatBaseUrl = yield* Config.String("T3CHAT_URL").pipe(
174 Config.withDefault("https://t3.chat"),
175 );
176
177 const cloudMintPrivateKey = yield* cloudMintKeyPair.privateKey;
178 const cloudMintPublicKey = yield* cloudMintKeyPair.publicKey;
179 const hyperdrive = yield* Cloudflare.Hyperdrive.Connect(yield* RelayDb.RelayHyperdrive);
180 // Named statements collide behind Hyperdrive transaction pooling (Effect-TS/effect#8320); use unnamed statements.
181 const db = yield* Drizzle.Postgres(hyperdrive.connectionString, {
182 client: { prepare: false },
183 });
184
185 const managedEndpointTunnelBinding = yield* Cloudflare.Tunnel.ReadWriteTunnel();
186 // Keep Worker custom-domain reconciliation ordered after API zone provisioning.
187 yield* yield* relayApiZone.zoneId;
188 const managedEndpointDnsBinding = yield* Cloudflare.DNS.ReadWriteDns(managedEndpointZone);
189 const managedEndpointZoneName = yield* managedEndpointZone.name;
190 const managedEndpointCleanupMode = yield* RelayConfiguration.managedEndpointCleanupModeConfig;
191 const legacyManagedEndpointCleanupMode =
192 yield* RelayConfiguration.legacyManagedEndpointCleanupModeConfig;
193 const legacyTunnelGraceMinutes = Option.getOrUndefined(
194 yield* RelayConfiguration.legacyTunnelGraceMinutesConfig,
195 );
196 // Endpoint keys and their hashes differ by stage, including in shared accounts.
197 const hookRateLimit = yield* Cloudflare.RateLimit("HOOK_RATE_LIMIT", {
198 namespaceId: 1001,
199 simple: {
200 limit: HookForwarder.RELAY_HOOK_RATE_LIMIT.limit,
201 period: HookForwarder.RELAY_HOOK_RATE_LIMIT.periodSeconds,
202 },
203 });
204 const hookEndpointRateLimit = yield* Cloudflare.RateLimit("HOOK_ENDPOINT_RATE_LIMIT", {
205 namespaceId: 1002,
206 simple: {
207 limit: HookForwarder.RELAY_HOOK_ENDPOINT_RATE_LIMIT.limit,
208 period: HookForwarder.RELAY_HOOK_ENDPOINT_RATE_LIMIT.periodSeconds,
209 },
210 });
211 const hookInboxes = yield* HookInboxObject.HookInboxObject;
212 const mobileTelemetryRateLimit = yield* Cloudflare.RateLimit("MOBILE_TELEMETRY_RATE_LIMIT", {
213 namespaceId: 1003,
214 simple: {
215 limit: MobileTelemetry.MOBILE_TELEMETRY_RATE_LIMIT.limit,
216 period: MobileTelemetry.MOBILE_TELEMETRY_RATE_LIMIT.periodSeconds,
217 },
218 });
219 const mobileSourceMaps = yield* Cloudflare.R2.ReadBucket(observability.mobileSourceMaps);
220 const mobileTelemetryToken = yield* observability.mobileTelemetryIngestToken.token;
221 const mobileTracesUrl = yield* observability.traces.otelTracesEndpoint;
222 const mobileTracesDataset = yield* observability.traces.name;
223 const mobileLogsUrl = yield* observability.mobileLogs.otelLogsEndpoint;
224 const mobileLogsDataset = yield* observability.mobileLogs.name;
225 const mobileMetricsUrl = yield* observability.mobileMetrics.otelMetricsEndpoint;
226 const mobileMetricsDataset = yield* observability.mobileMetrics.name;
227
228 const alchemyRuntimeContext: Alchemy.BaseRuntimeContext = yield* Cloudflare.Worker;
229
230 const loadSettings = Effect.gen(function* () {
231 return RelayConfiguration.RelayConfiguration.of({
232 relayIssuer: relayPublicOrigin,
233 ...(fcmServiceAccount ? { fcmServiceAccount } : {}),
234 apns: apnsCredentials,
235 apnsDeliveryJobSigningSecret: yield* apnsDeliveryJobSigningSecret,
236 clerkSecretKey,
237 clerkPublishableKey,
238 clerkJwtAudience,
239 cloudMintPrivateKey: yield* cloudMintPrivateKey,
240 cloudMintPublicKey: yield* cloudMintPublicKey,
241 managedEndpointBaseDomain: yield* managedEndpointZoneName,
242 managedEndpointNamespace: stage,
243 managedEndpointCleanupMode,
244 legacyManagedEndpointCleanupMode,
245 ...(legacyTunnelGraceMinutes === undefined ? {} : { legacyTunnelGraceMinutes }),
246 ...(t3ChatLinkSecret
247 ? { t3Chat: { baseUrl: t3ChatBaseUrl, linkSecret: t3ChatLinkSecret } }
248 : {}),
249 });
250 });
251
252 const inboxCall =
253 <A>(operation: HookInbox.HookInboxError["operation"], endpointKey: string) =>
254 (effect: Effect.Effect<A, never, Alchemy.RuntimeContext>) =>
255 effect.pipe(
256 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
257 Effect.catchCause((cause) =>
258 Effect.fail(
259 new HookInbox.HookInboxError({
260 operation,
261 endpointKey,
262 cause: Cause.squash(cause),
263 }),
264 ),
265 ),
266 );
267 const layerHookInbox = Layer.succeed(HookInbox.HookInbox, {
268 hold: Effect.fn("HookInbox.hold")(function ({ endpointKey, baseUrl, hook }) {
269 return hookInboxes
270 .getByName(endpointKey)
271 .hold(hook, baseUrl)
272 .pipe(inboxCall("hold", endpointKey));
273 }),
274 wake: Effect.fn("HookInbox.wake")(function ({ endpointKey, baseUrl }) {
275 return hookInboxes
276 .getByName(endpointKey)
277 .wake(baseUrl)
278 .pipe(inboxCall("wake", endpointKey));
279 }),
280 clear: Effect.fn("HookInbox.clear")(function ({ endpointKey }) {
281 return hookInboxes.getByName(endpointKey).clear().pipe(inboxCall("clear", endpointKey));
282 }),
283 });
284
285 const layerRuntime = Layer.empty.pipe(
286 Layer.provideMerge(MobileRegistrations.layer),
287 Layer.provideMerge(AgentActivityPublisher.layer),
288 Layer.provideMerge(EnvironmentConnector.layer),
289 Layer.provideMerge(EnvironmentLinker.layer),
290 Layer.provideMerge(
291 Layer.mergeAll(
292 EnvironmentPublishSignatures.layer,
293 ManagedEndpointReaper.layer,
294 HeldHooks.layer,
295 ),
296 ),
297 Layer.provideMerge(
298 ManagedEndpointProvider.layerCloudflareBindings(
299 managedEndpointTunnelBinding,
300 managedEndpointDnsBinding,
301 alchemyRuntimeContext,
302 ),
303 ),
304 Layer.provideMerge(DpopProofs.layer),
305 Layer.provideMerge(ApnsDeliveries.layer),
306 Layer.provideMerge(
307 FcmDeliveries.layer.pipe(
308 Layer.provide(
309 Layer.succeed(FcmDeliveryQueueSender.FcmDeliveryQueueSender, {
310 send: Effect.fn("FcmDeliveryQueueSender.send")(
311 function* (body) {
312 return yield* fcmDeliveryQueueSender.send(body);
313 },
314 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
315 ),
316 }),
317 ),
318 Layer.provideMerge(
319 FcmClient.layer.pipe(
320 Layer.provide(FcmAssertionSigner.layer),
321 Layer.provide(
322 Layer.succeed(WebCrypto.WebCrypto, { subtle: globalThis.crypto.subtle }),
323 ),
324 ),
325 ),
326 ),
327 ),
328 Layer.provideMerge(ApnsClient.layer.pipe(Layer.provideMerge(ApnsProviderTokens.layer))),
329 Layer.provideMerge(
330 ApnsDeliveryQueue.layerCloudflareQueues(apnsDeliveryQueueSender, alchemyRuntimeContext),
331 ),
332 Layer.provideMerge(Layer.mergeAll(AgentActivityRows.layer, Devices.layer, layerHookInbox)),
333 Layer.provideMerge(EnvironmentCredentials.layer),
334 Layer.provideMerge(
335 Layer.mergeAll(
336 CloudflareApiPause.layer,
337 EnvironmentLinks.layer,
338 ManagedEndpointAllocations.layer,
339 ManagedTunnelLimits.layer,
340 ),
341 ),
342 Layer.provideMerge(Layer.mergeAll(LiveActivities.layer, T3ChatAccounts.layer)),
343 Layer.provideMerge(DeliveryAttempts.layer),
344 Layer.provideMerge(RelayTokens.layer.pipe(Layer.provideMerge(CloudMintSigner.layer))),
345 Layer.provideMerge(
346 RelayDb.RelayTransactions.layer.pipe(
347 Layer.provideMerge(Layer.succeed(RelayDb.RelayDb, db)),
348 ),
349 ),
350 Layer.provideMerge(Layer.effect(RelayConfiguration.RelayConfiguration, loadSettings)),
351 Layer.provideMerge(layerWebcrypto),
352 );
353
354 // Fails open: a limiter outage must not drop webhooks or app telemetry.
355 const allowWith =
356 (
357 limiter: typeof hookRateLimit,
358 failedOpenAttribute = "relay.hook.rate_limiter_failed_open",
359 ) =>
360 (key: string): Effect.Effect<boolean> =>
361 limiter.limit({ key }).pipe(
362 Effect.map((result) => result.success),
363 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
364 Effect.catch((error) =>
365 Effect.logWarning("Rate limiter unavailable", { error: error.message }).pipe(
366 Effect.andThen(Effect.annotateCurrentSpan({ [failedOpenAttribute]: true })),
367 Effect.as(true),
368 ),
369 ),
370 );
371 const layerHookRateLimiter = Layer.succeed(HookForwarder.HookRateLimiter, {
372 allowHook: allowWith(hookRateLimit),
373 allowEndpoint: allowWith(hookEndpointRateLimit),
374 });
375
376 // `fetch` rebuilds its layers per request; the source map cache must outlive one.
377 const mobileSourceMapLookups = yield* MobileTelemetry.SourceMaps.pipe(
378 Effect.provide(MobileTelemetry.layerSourceMaps),
379 Effect.provideService(MobileTelemetry.SourceMapObjects, {
380 get: (key) =>
381 mobileSourceMaps.get(key).pipe(
382 Effect.map((object) =>
383 object === null
384 ? undefined
385 : {
386 size: object.size,
387 text: object.text().pipe(
388 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
389 Effect.mapError(
390 (cause) => new MobileTelemetry.SourceMapObjectError({ key, cause }),
391 ),
392 ),
393 },
394 ),
395 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
396 Effect.mapError((cause) => new MobileTelemetry.SourceMapObjectError({ key, cause })),
397 ),
398 }),
399 );
400 const layerMobileTelemetry = MobileTelemetry.layer.pipe(
401 Layer.provide(
402 Layer.mergeAll(
403 Layer.effect(
404 MobileTelemetry.MobileTelemetrySettings,
405 Effect.gen(function* () {
406 return MobileTelemetry.MobileTelemetrySettings.of({
407 axiomToken: yield* mobileTelemetryToken,
408 destinations: {
409 traces: { url: yield* mobileTracesUrl, dataset: yield* mobileTracesDataset },
410 logs: { url: yield* mobileLogsUrl, dataset: yield* mobileLogsDataset },
411 metrics: { url: yield* mobileMetricsUrl, dataset: yield* mobileMetricsDataset },
412 },
413 });
414 }),
415 ),
416 Layer.succeed(MobileTelemetry.MobileTelemetryRateLimiter, {
417 allow: allowWith(
418 mobileTelemetryRateLimit,
419 "relay.mobile_telemetry.rate_limiter_failed_open",
420 ),
421 }),
422 Layer.succeed(MobileTelemetry.SourceMaps, mobileSourceMapLookups),
423 ),
424 ),
425 );
426
427 const layerApp = Layer.mergeAll(
428 layerRelayApi,
429 HookForwarder.layerApi.pipe(
430 Layer.provide(HookForwarder.layer),
431 Layer.provide(layerHookRateLimiter),
432 ),
433 MobileTelemetry.layerApi.pipe(Layer.provide(layerMobileTelemetry)),
434 ).pipe(
435 Layer.provideMerge(RelayHttpApi.layerClientAuth),
436 Layer.provideMerge(RelayHttpApi.layerDpopClientAuth),
437 Layer.provideMerge(RelayHttpApi.layerEnvironmentAuth),
438 Layer.provide(layerRuntime),
439 );
440
441 yield* Cloudflare.Queues.consumeQueueMessages<unknown>(
442 apnsDeliveryQueue,
443 {
444 batchSize: 10,
445 maxRetries: 5,
446 maxWaitTime: "5 seconds",
447 retryDelay: "30 seconds",
448 deadLetterQueue: apnsDeliveryDeadLetterQueue.queueName as unknown as string,
449 },
450 (stream) =>
451 stream.pipe(
452 Stream.withSpan("relay.apn_delivery_queue.process_batch"),
453 Stream.runForEach((message) =>
454 ApnsDeliveries.ApnsDeliveries.pipe(
455 Effect.flatMap((deliveries) => deliveries.processSignedJob(message.body)),
456 Effect.withSpan("relay.apn_delivery_queue.process_message"),
457 ),
458 ),
459 Effect.provide(layerRuntime),
460 ),
461 );
462
463 yield* Cloudflare.Queues.consumeQueueMessages<unknown>(
464 fcmDeliveryQueue,
465 {
466 batchSize: 10,
467 maxRetries: 5,
468 maxWaitTime: "1 second",
469 retryDelay: "30 seconds",
470 deadLetterQueue: fcmDeliveryDeadLetterQueue.queueName as unknown as string,
471 },
472 (stream) =>
473 stream.pipe(
474 Stream.withSpan("relay.fcm_delivery_queue.process_batch"),
475 Stream.runForEach(FcmDeliveryQueueConsumer.processMessage),
476 Effect.provide(layerRuntime),
477 ),
478 );
479
480 yield* Cloudflare.Workers.cron("*/5 * * * *", () =>
481 Effect.all(
482 [
483 DpopProofs.DpopProofReplay.pipe(
484 Effect.flatMap((dpopProofs) => dpopProofs.pruneExpired),
485 Effect.andThen(
486 Effect.all([AgentActivityRows.AgentActivityRows, DateTime.now]).pipe(
487 Effect.flatMap(([activityRows, now]) =>
488 activityRows.pruneTerminal({
489 updatedBefore: DateTime.formatIso(DateTime.subtract(now, { minutes: 30 })),
490 }),
491 ),
492 ),
493 ),
494 Effect.andThen(
495 DeliveryAttempts.DeliveryAttempts.pipe(
496 Effect.flatMap((attempts) => attempts.pruneExpired),
497 ),
498 ),
499 Effect.catchCause((cause) =>
500 Cause.hasInterrupts(cause)
501 ? Effect.interrupt
502 : Effect.logWarning("Failed to prune expired relay state", { cause }),
503 ),
504 ),
505 ManagedEndpointReaper.ManagedEndpointReaper.pipe(
506 Effect.flatMap((reaper) => reaper.sweep.pipe(Effect.timeout("2 minutes"))),
507 Effect.tap((result) =>
508 result.scanned > 0
509 ? Effect.logInfo("Finished managed tunnel cleanup", result)
510 : Effect.void,
511 ),
512 Effect.catchCause((cause) =>
513 Cause.hasInterrupts(cause)
514 ? Effect.interrupt
515 : Effect.logWarning("Failed to clean up inactive managed tunnels", { cause }),
516 ),
517 ),
518 AgentActivityPublisher.AgentActivityPublisher.pipe(
519 Effect.flatMap((publisher) => publisher.endIdleLiveActivities),
520 Effect.catchCause((cause) =>
521 Cause.hasInterrupts(cause)
522 ? Effect.interrupt
523 : Effect.logWarning("Failed to end idle Live Activities", { cause }),
524 ),
525 ),
526 ],
527 { concurrency: 3, discard: true },
528 ).pipe(
529 Effect.withSpan("relay.cron.prune_expired_state"),
530 Observability.withSchemaErrorSpanAttributes,
531 Effect.provide(layerRuntime),
532 ),
533 );
534
535 const fetch = Layer.merge(
536 Layer.mergeAll(
537 HttpApiBuilder.layer(RelayApi, { openapiPath: "/openapi.json" }).pipe(
538 Layer.provide(layerApp),
539 ),
540 HttpApiScalar.layer(RelayApi, { path: "/docs" }),
541 RelayHttpApi.layerDocsRedirectRoute,
542 ).pipe(
543 Layer.provide([Etag.layerWeak, layerHttpPlatformNotSupported, RelayHttpApi.layerCors]),
544 ),
545 RelayHttpApi.layerNotFoundRoute,
546 ).pipe(
547 HttpRouter.toHttpEffect,
548 Effect.provideService(HttpRouter.RouterConfig, RELAY_HTTP_ROUTER_CONFIG),
549 withoutCapturedParentSpan,
550 Effect.map((httpEffect) =>
551 traceRelayHttpRequestWith(httpEffect, Layer.empty).pipe(
552 Observability.withSchemaErrorSpanAttributes,
553 ),
554 ),
555 );
556
557 return { fetch };
558 }).pipe(
559 Effect.provide(
560 Layer.empty.pipe(
561 Layer.provideMerge(Cloudflare.Hyperdrive.ConnectBinding),
562 Layer.provideMerge(Cloudflare.Workers.CronEventSourceLive),
563 Layer.provideMerge(Cloudflare.Queues.WriteQueueBinding),
564 Layer.provideMerge(Cloudflare.Queues.EventSourceLive),
565 Layer.provideMerge(Cloudflare.Tunnel.ReadWriteTunnelBinding),
566 Layer.provideMerge(Cloudflare.DNS.ReadWriteDnsHttp),
567 Layer.provideMerge(Cloudflare.Workers.RateLimitBinding),
568 Layer.provideMerge(Cloudflare.R2.ReadBucketBinding),
569 Layer.provideMerge(HookInboxObject.layer),
570 // Disable the duplicate runtime span; ours applies route/header/token redaction and rejects sender traceparent.
571 Layer.provideMerge(
572 Alchemy.Telemetry.layer(Layer.succeed(HttpMiddleware.TracerDisabledWhen)(() => true)),
573 ),
574 Layer.provideMerge(
575 Layer.unwrap(
576 Effect.map(RelayObservability, (observability) =>
577 Axiom.Telemetry({
578 serviceName: "t3code-relay",
579 token: observability.workerIngestToken,
580 traces: observability.traces,
581 }),
582 ),
583 ),
584 ),
585 ),
586 ),
587 ),
588);
589
590export default layer;