1import type {
2 CreateTunnelCloudflaredError,
3 CreateTunnelCloudflaredRequest,
4 CreateTunnelCloudflaredResponse,
5 DeleteTunnelCloudflaredError,
6 DeleteTunnelCloudflaredResponse,
7 PatchTunnelCloudflaredError,
8 PatchTunnelCloudflaredRequest,
9 PatchTunnelCloudflaredResponse,
10 PutTunnelCloudflaredConfigurationError,
11 PutTunnelCloudflaredConfigurationRequest,
12 PutTunnelCloudflaredConfigurationResponse,
13} from "@distilled.cloud/cloudflare/zero-trust";
14import * as zeroTrust from "@distilled.cloud/cloudflare/zero-trust";
15import * as Effect from "effect/Effect";
16import * as Binding from "../../Binding.ts";
17import type { Worker } from "../Workers/Worker.ts";
18import type { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
19import type { RuntimeContext } from "../../RuntimeContext.ts";
20import { type TunnelAuth } from "./TunnelBinding.ts";
21
22/**
23 * Binding that lets a Worker create, update, and delete Cloudflare Tunnels at
24 * runtime.
25 *
26 * Creates a scoped {@link AccountApiToken} with only the `Cloudflare Tunnel
27 * Write` permission and binds its outputs into the Worker (the token value as a
28 * `secret_text` binding) so runtime code can authenticate.
29 *
30 * @remarks
31 *
32 * `WriteTunnel` is a single identifier that is simultaneously the binding's
33 * Context tag, its type, and the callable — `yield* Cloudflare.Tunnel.WriteTunnel()`.
34 *
35 * ### Mutating tunnels at runtime
36 * **Example:** Bind the write client
37 * Bind once in the Init phase; every method is available on the returned client.
38 * ```typescript
39 * const tunnels = yield* Cloudflare.Tunnel.WriteTunnel();
40 * ```
41 *
42 * **Example:** Create a tunnel
43 * ```typescript
44 * const tunnel = yield* tunnels.create({ name: "on-demand-tunnel" });
45 * ```
46 *
47 * **Example:** Push ingress configuration
48 * ```typescript
49 * yield* tunnels.putConfiguration(tunnel.id!, {
50 * ingress: [
51 * { hostname: "app.example.com", service: "http://localhost:3000" },
52 * { service: "http_status:404" },
53 * ],
54 * });
55 * ```
56 *
57 * **Example:** Rename and delete a tunnel
58 * ```typescript
59 * yield* tunnels.update(tunnel.id!, { name: "renamed-tunnel" });
60 * yield* tunnels.delete(tunnel.id!);
61 * ```
62 *
63 * ### Runtime Layer
64 * Provide {@link WriteTunnelBinding} in the Worker's runtime layer.
65 * ```typescript
66 * Effect.provide(Cloudflare.Tunnel.WriteTunnelBinding)
67 * ```
68 *
69 * @binding
70 * @product Tunnels
71 * @category Cloudflare One (Zero Trust)
72 */
73export interface WriteTunnel extends Binding.Service<
74 WriteTunnel,
75 "Cloudflare.Tunnel.WriteTunnel",
76 () => Effect.Effect<WriteTunnelClient, never, Worker | CloudflareEnvironment>
77> {}
78
79export const WriteTunnel = Binding.Service<WriteTunnel>(
80 "Cloudflare.Tunnel.WriteTunnel",
81);
82
83/** Create-tunnel request, minus the account id (supplied by the binding). */
84export type CreateTunnelRequest = Omit<
85 CreateTunnelCloudflaredRequest,
86 "accountId"
87>;
88
89/** Update-tunnel request, minus the account id and tunnel id (positional). */
90export type UpdateTunnelRequest = Omit<
91 PatchTunnelCloudflaredRequest,
92 "accountId" | "tunnelId"
93>;
94
95/** Tunnel configuration body, minus the account id and tunnel id (positional). */
96export type ConfigurationBody = NonNullable<
97 PutTunnelCloudflaredConfigurationRequest["config"]
98>;
99
100/**
101 * Mutating tunnel operations. Backed by the `Cloudflare Tunnel Write`
102 * permission group.
103 */
104export interface WriteTunnelClient {
105 /** Create a new tunnel. */
106 create(
107 request: CreateTunnelRequest,
108 ): Effect.Effect<
109 CreateTunnelCloudflaredResponse,
110 CreateTunnelCloudflaredError,
111 RuntimeContext
112 >;
113 /** Update a tunnel's mutable fields (name, secret). */
114 update(
115 tunnelId: string,
116 request: UpdateTunnelRequest,
117 ): Effect.Effect<
118 PatchTunnelCloudflaredResponse,
119 PatchTunnelCloudflaredError,
120 RuntimeContext
121 >;
122 /** Delete a tunnel by id. */
123 delete(
124 tunnelId: string,
125 ): Effect.Effect<
126 DeleteTunnelCloudflaredResponse,
127 DeleteTunnelCloudflaredError,
128 RuntimeContext
129 >;
130 /** Replace the remotely-managed configuration (ingress rules) for a tunnel. */
131 putConfiguration(
132 tunnelId: string,
133 config: ConfigurationBody,
134 ): Effect.Effect<
135 PutTunnelCloudflaredConfigurationResponse,
136 PutTunnelCloudflaredConfigurationError,
137 RuntimeContext
138 >;
139}
140
141/** Build the write client over an injectable {@link TunnelAuth}. */
142export const writeClient = (auth: TunnelAuth): WriteTunnelClient => {
143 const { authorize } = auth;
144 return {
145 create: Effect.fn("Cloudflare.Tunnel.create")(function* (request) {
146 const accountId = yield* auth.accountId;
147 return yield* authorize(
148 zeroTrust.createTunnelCloudflared({ accountId, ...request }),
149 );
150 }),
151 update: Effect.fn("Cloudflare.Tunnel.update")(
152 function* (tunnelId, request) {
153 const accountId = yield* auth.accountId;
154 return yield* authorize(
155 zeroTrust.patchTunnelCloudflared({ accountId, tunnelId, ...request }),
156 );
157 },
158 ),
159 delete: Effect.fn("Cloudflare.Tunnel.delete")(function* (tunnelId) {
160 const accountId = yield* auth.accountId;
161 return yield* authorize(
162 zeroTrust.deleteTunnelCloudflared({ accountId, tunnelId }),
163 );
164 }),
165 putConfiguration: Effect.fn("Cloudflare.Tunnel.putConfiguration")(
166 function* (tunnelId, config) {
167 const accountId = yield* auth.accountId;
168 return yield* authorize(
169 zeroTrust.putTunnelCloudflaredConfiguration({
170 accountId,
171 tunnelId,
172 config,
173 }),
174 );
175 },
176 ),
177 };
178};