1import type * as Effect from "effect/Effect";
2import * as Binding from "../Binding.ts";
3import type { RuntimeContext } from "../RuntimeContext.ts";
4import type { Account } from "./Account.ts";
5import type {
6 IssueError,
7 IssueRequest,
8 IssuedCertificate,
9 RevokeError,
10 RevokeRequest,
11} from "./Client.ts";
12
13/**
14 * Issue (and revoke) certificates at runtime as an {@link Account} — for
15 * services that terminate TLS themselves and need certificates on demand,
16 * like a relay minting `*.<tenant>.example.com` when a tenant first
17 * connects.
18 *
19 * Init transports the account's directory URL, account URL and redacted
20 * private key through resource Outputs. Protect the runtime and state store
21 * as secret material. The caller chooses where to store the issued PEMs.
22 *
23 * ### Issue on demand
24 * Publish the DNS-01 record through any runtime DNS write client wrapped
25 * as a solver (`Cloudflare.DNS.acmeDnsSolver(dns)`).
26 *
27 * **Example:** Mint a wildcard from a Fly Service and upload it to its App
28 * ```typescript
29 * export default class Relay extends Fly.Service<Relay>()(
30 * "Relay",
31 * { app: RelayApp, main: import.meta.url },
32 * Effect.gen(function* () {
33 * const acme = yield* ACME.IssueCertificate(ZeroSsl);
34 * const dns = yield* Cloudflare.DNS.WriteDns(Zone);
35 * const certs = yield* Fly.WriteCertificates(RelayApp);
36 * return {
37 * fetch: Effect.gen(function* () {
38 * const issued = yield* acme.issue({
39 * identifiers: ["*.tenant.example.com"],
40 * solver: Cloudflare.DNS.acmeDnsSolver(dns),
41 * });
42 * yield* certs.upload({
43 * hostname: "*.tenant.example.com",
44 * fullchain: issued.chain,
45 * privateKey: issued.privateKey,
46 * });
47 * return HttpServerResponse.text("ok");
48 * }),
49 * };
50 * }).pipe(
51 * Effect.provide(ACME.IssueCertificateHttp),
52 * Effect.provide(Cloudflare.DNS.WriteDnsHttp),
53 * Effect.provide(Fly.WriteCertificatesHttp),
54 * ),
55 * ) {}
56 * ```
57 *
58 * ### Rate limits
59 * The SDK retry policy applies to individual requests. Apply
60 * `Acme.Retry.none` from `@distilled.cloud/acme` to surface failures
61 * immediately; `AcmeRateLimited.retryAfter` carries the CA's retry hint.
62 *
63 * ### Which CA from a Cloudflare Worker
64 * CA reachability depends on the host's egress. Let's Encrypt has returned
65 * TLS failures from deployed Cloudflare Workers in live testing; the
66 * deployed-Worker example uses ZeroSSL instead. Verify the selected CA
67 * from the runtime where issuance will run.
68 *
69 * @binding
70 * @product Certificate
71 * @category Certificates
72 */
73export interface IssueCertificate extends Binding.Service<
74 IssueCertificate,
75 "ACME.IssueCertificate",
76 (account: Account) => Effect.Effect<IssueCertificateClient>
77> {}
78
79export const IssueCertificate = Binding.Service<IssueCertificate>(
80 "ACME.IssueCertificate",
81);
82
83/** Runtime issuance client bound to one account. */
84export interface IssueCertificateClient {
85 /** Order, solve DNS-01, finalize and download a certificate. */
86 issue<R = never>(
87 request: IssueRequest<R>,
88 ): Effect.Effect<IssuedCertificate, IssueError, RuntimeContext | R>;
89 /** Revoke a certificate (`AcmeAlreadyRevoked` counts as success). */
90 revoke(
91 request: RevokeRequest,
92 ): Effect.Effect<void, RevokeError, RuntimeContext>;
93}