| 1 | import 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"; |
| 14 | import * as zeroTrust from "@distilled.cloud/cloudflare/zero-trust"; |
| 15 | import * as Effect from "effect/Effect"; |
| 16 | import * as Binding from "../../Binding.ts"; |
| 17 | import type { Worker } from "../Workers/Worker.ts"; |
| 18 | import type { CloudflareEnvironment } from "../CloudflareEnvironment.ts"; |
| 19 | import type { RuntimeContext } from "../../RuntimeContext.ts"; |
| 20 | import { 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 | */ |
| 73 | export interface WriteTunnel extends Binding.Service< |
| 74 | WriteTunnel, |
| 75 | "Cloudflare.Tunnel.WriteTunnel", |
| 76 | () => Effect.Effect<WriteTunnelClient, never, Worker | CloudflareEnvironment> |
| 77 | > {} |
| 78 | |
| 79 | export const WriteTunnel = Binding.Service<WriteTunnel>( |
| 80 | "Cloudflare.Tunnel.WriteTunnel", |
| 81 | ); |
| 82 | |
| 83 | /** Create-tunnel request, minus the account id (supplied by the binding). */ |
| 84 | export type CreateTunnelRequest = Omit< |
| 85 | CreateTunnelCloudflaredRequest, |
| 86 | "accountId" |
| 87 | >; |
| 88 | |
| 89 | /** Update-tunnel request, minus the account id and tunnel id (positional). */ |
| 90 | export type UpdateTunnelRequest = Omit< |
| 91 | PatchTunnelCloudflaredRequest, |
| 92 | "accountId" | "tunnelId" |
| 93 | >; |
| 94 | |
| 95 | /** Tunnel configuration body, minus the account id and tunnel id (positional). */ |
| 96 | export type ConfigurationBody = NonNullable< |
| 97 | PutTunnelCloudflaredConfigurationRequest["config"] |
| 98 | >; |
| 99 | |
| 100 | /** |
| 101 | * Mutating tunnel operations. Backed by the `Cloudflare Tunnel Write` |
| 102 | * permission group. |
| 103 | */ |
| 104 | export 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}. */ |
| 142 | export 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 | }; |