1import * as Data from "effect/Data";
2
3/** A problem the CA attached to an order or one of its identifiers. */
4export interface IdentifierProblem {
5 /** The identifier the problem concerns, when the CA said. */
6 readonly identifier?: string | undefined;
7 /** The `urn:ietf:params:acme:error:*` type. */
8 readonly type?: string | undefined;
9 /** Human-readable detail from the CA. */
10 readonly detail?: string | undefined;
11}
12
13/**
14 * The CA moved the order to `invalid`. Carries the problem documents the
15 * order and its authorizations reported, one per failed identifier where
16 * the CA said which.
17 */
18export class OrderInvalid extends Data.TaggedError("ACME.OrderInvalid")<{
19 readonly orderUrl: string;
20 readonly problems: ReadonlyArray<IdentifierProblem>;
21}> {
22 override get message() {
23 const details = this.problems
24 .map((p) => [p.identifier, p.type, p.detail].filter(Boolean).join(": "))
25 .join("; ");
26 return `ACME order ${this.orderUrl} is invalid${details ? `: ${details}` : ""}`;
27 }
28}
29
30/** The CA rejected the DNS-01 challenge for one identifier. */
31export class ChallengeFailed extends Data.TaggedError("ACME.ChallengeFailed")<{
32 readonly identifier: string;
33 readonly type?: string | undefined;
34 readonly detail?: string | undefined;
35}> {
36 override get message() {
37 return `DNS-01 challenge for ${this.identifier} failed${this.detail ? `: ${this.detail}` : ""}`;
38 }
39}
40
41/** The CA offered no `dns-01` challenge for an identifier. */
42export class ChallengeUnsupported extends Data.TaggedError(
43 "ACME.ChallengeUnsupported",
44)<{
45 readonly identifier: string;
46 readonly offered: ReadonlyArray<string>;
47}> {
48 override get message() {
49 return `The CA offered no dns-01 challenge for ${this.identifier} (offered: ${this.offered.join(", ") || "none"})`;
50 }
51}
52
53/** The order never reached a terminal state within the polling budget. */
54export class OrderTimeout extends Data.TaggedError("ACME.OrderTimeout")<{
55 readonly orderUrl: string;
56 readonly status: string;
57}> {
58 override get message() {
59 return `ACME order ${this.orderUrl} is still "${this.status}" after the polling budget`;
60 }
61}
62
63/** The `_acme-challenge` TXT record never became visible to public resolvers. */
64export class DnsPropagationTimeout extends Data.TaggedError(
65 "ACME.DnsPropagationTimeout",
66)<{
67 readonly fqdn: string;
68 readonly value: string;
69}> {
70 override get message() {
71 return `TXT ${this.fqdn} did not propagate within the wait budget`;
72 }
73}
74
75/** A DNS solver could not publish or remove a challenge record. */
76export class DnsSolverError extends Data.TaggedError("ACME.DnsSolverError")<{
77 readonly message: string;
78 readonly cause?: unknown;
79}> {}
80
81/** A solver descriptor names a `type` no registered DNS provider handles. */
82export class DnsSolverNotRegistered extends Data.TaggedError(
83 "ACME.DnsSolverNotRegistered",
84)<{
85 readonly type: string;
86}> {
87 override get message() {
88 return `No DNS-01 solver is registered for "${this.type}". Include the DNS provider's providers() layer (e.g. Cloudflare.providers()) in the stack.`;
89 }
90}
91
92/** Key generation, CSR encoding or certificate parsing failed. */
93export class PkiError extends Data.TaggedError("ACME.PkiError")<{
94 readonly message: string;
95 readonly cause?: unknown;
96}> {}