@distilled.cloud/acme 0.0.0-placeholder → 1.0.0-rc.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +73 -0
  3. package/lib/credentials.d.ts +50 -0
  4. package/lib/credentials.d.ts.map +1 -0
  5. package/lib/credentials.js +57 -0
  6. package/lib/credentials.js.map +1 -0
  7. package/lib/errors.d.ts +73 -0
  8. package/lib/errors.d.ts.map +1 -0
  9. package/lib/errors.js +50 -0
  10. package/lib/errors.js.map +1 -0
  11. package/lib/index.d.ts +30 -0
  12. package/lib/index.d.ts.map +1 -0
  13. package/lib/index.js +30 -0
  14. package/lib/index.js.map +1 -0
  15. package/lib/jose.d.ts +66 -0
  16. package/lib/jose.d.ts.map +1 -0
  17. package/lib/jose.js +128 -0
  18. package/lib/jose.js.map +1 -0
  19. package/lib/protocol.d.ts +27 -0
  20. package/lib/protocol.d.ts.map +1 -0
  21. package/lib/protocol.js +410 -0
  22. package/lib/protocol.js.map +1 -0
  23. package/lib/response-validation.test.d.ts +4 -0
  24. package/lib/response-validation.test.d.ts.map +1 -0
  25. package/lib/response-validation.test.js +51 -0
  26. package/lib/response-validation.test.js.map +1 -0
  27. package/lib/retry.d.ts +33 -0
  28. package/lib/retry.d.ts.map +1 -0
  29. package/lib/retry.js +33 -0
  30. package/lib/retry.js.map +1 -0
  31. package/lib/services/acme.d.ts +446 -0
  32. package/lib/services/acme.d.ts.map +1 -0
  33. package/lib/services/acme.js +597 -0
  34. package/lib/services/acme.js.map +1 -0
  35. package/lib/services/index.d.ts +2 -0
  36. package/lib/services/index.d.ts.map +1 -0
  37. package/lib/services/index.js +3 -0
  38. package/lib/services/index.js.map +1 -0
  39. package/lib/traits.d.ts +13 -0
  40. package/lib/traits.d.ts.map +1 -0
  41. package/lib/traits.js +13 -0
  42. package/lib/traits.js.map +1 -0
  43. package/package.json +75 -7
  44. package/src/credentials.ts +106 -0
  45. package/src/errors.ts +101 -0
  46. package/src/index.ts +33 -0
  47. package/src/jose.ts +234 -0
  48. package/src/protocol.ts +582 -0
  49. package/src/response-validation.test.ts +65 -0
  50. package/src/retry.ts +55 -0
  51. package/src/services/acme.ts +1140 -0
  52. package/src/services/index.ts +2 -0
  53. package/src/traits.ts +46 -0
package/lib/jose.d.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * JOSE helpers for ACME — hand-written, WebCrypto only (no native code).
3
+ *
4
+ * - account keys (ES256 P-256 by default, RS256 accepted)
5
+ * - JWS in the flattened JSON serialization every ACME POST carries
6
+ * (RFC 8555 §6.2), including the HS256 External Account Binding
7
+ * - RFC 7638 JWK thumbprints, key authorizations and the DNS-01 TXT value
8
+ */
9
+ import * as Effect from "effect/Effect";
10
+ import * as Redacted from "effect/Redacted";
11
+ import { JoseError } from "./errors.ts";
12
+ export type Algorithm = "ES256" | "RS256";
13
+ export interface Jwk {
14
+ readonly kty: string;
15
+ readonly crv?: string;
16
+ readonly x?: string;
17
+ readonly y?: string;
18
+ readonly n?: string;
19
+ readonly e?: string;
20
+ readonly d?: string;
21
+ readonly [key: string]: unknown;
22
+ }
23
+ export declare const base64url: (bytes: Uint8Array | ArrayBuffer) => string;
24
+ export declare const base64urlDecode: (text: string) => Uint8Array;
25
+ /** Detect the JWS algorithm for a JWK. */
26
+ export declare const algorithmOf: (jwk: Jwk) => Algorithm;
27
+ /** Generate a fresh account key as a private JWK (JSON text, Redacted). */
28
+ export declare const generateAccountKey: (alg?: Algorithm) => Effect.Effect<Redacted.Redacted<string>>;
29
+ /** Parse a private JWK from its JSON text. */
30
+ export declare const parseJwk: (key: Redacted.Redacted<string> | string) => Effect.Effect<Jwk, JoseError>;
31
+ /** The public half of a JWK, with members in RFC 7638 canonical order. */
32
+ export declare const publicJwk: (jwk: Jwk) => Jwk;
33
+ /** RFC 7638 JWK thumbprint (base64url SHA-256 of the canonical public JWK). */
34
+ export declare const thumbprint: (jwk: Jwk) => Effect.Effect<string>;
35
+ /** `<token>.<thumbprint>` — what a challenge proves the client holds. */
36
+ export declare const keyAuthorization: (token: string, jwk: Jwk) => Effect.Effect<string>;
37
+ /** The TXT record value for a DNS-01 challenge: base64url(SHA-256(keyAuthorization)). */
38
+ export declare const dnsChallengeValue: (token: string, jwk: Jwk) => Effect.Effect<string>;
39
+ export interface FlattenedJws {
40
+ readonly protected: string;
41
+ readonly payload: string;
42
+ readonly signature: string;
43
+ }
44
+ export interface SignOptions {
45
+ readonly jwk: Jwk;
46
+ readonly url: string;
47
+ readonly nonce: string;
48
+ /** Sent as `kid` when known; otherwise the public JWK is embedded. */
49
+ readonly kid?: string | undefined;
50
+ /** `undefined` produces a POST-as-GET (empty payload). */
51
+ readonly payload?: unknown;
52
+ }
53
+ /** Sign an ACME request body (RFC 8555 §6.2) with the account key. */
54
+ export declare const signRequest: (options: SignOptions) => Effect.Effect<FlattenedJws, JoseError>;
55
+ /**
56
+ * The External Account Binding JWS (RFC 8555 §7.3.4): HS256 over the
57
+ * account's public JWK, keyed by the CA-issued HMAC key, with the CA's key
58
+ * id as `kid`.
59
+ */
60
+ export declare const signExternalAccountBinding: (options: {
61
+ readonly jwk: Jwk;
62
+ readonly url: string;
63
+ readonly keyId: string;
64
+ readonly hmacKey: Redacted.Redacted<string> | string;
65
+ }) => Effect.Effect<FlattenedJws, JoseError>;
66
+ //# sourceMappingURL=jose.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jose.d.ts","sourceRoot":"","sources":["../src/jose.ts"],"names":[],"mappings":"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"}
package/lib/jose.js ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * JOSE helpers for ACME — hand-written, WebCrypto only (no native code).
3
+ *
4
+ * - account keys (ES256 P-256 by default, RS256 accepted)
5
+ * - JWS in the flattened JSON serialization every ACME POST carries
6
+ * (RFC 8555 §6.2), including the HS256 External Account Binding
7
+ * - RFC 7638 JWK thumbprints, key authorizations and the DNS-01 TXT value
8
+ */
9
+ import * as Effect from "effect/Effect";
10
+ import * as Redacted from "effect/Redacted";
11
+ import { JoseError } from "./errors.js";
12
+ const encoder = new TextEncoder();
13
+ export const base64url = (bytes) => {
14
+ const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
15
+ let binary = "";
16
+ for (const byte of view)
17
+ binary += String.fromCharCode(byte);
18
+ return btoa(binary)
19
+ .replaceAll("+", "-")
20
+ .replaceAll("/", "_")
21
+ .replace(/=+$/, "");
22
+ };
23
+ export const base64urlDecode = (text) => {
24
+ const padded = text.replaceAll("-", "+").replaceAll("_", "/");
25
+ const binary = atob(padded + "=".repeat((4 - (padded.length % 4)) % 4));
26
+ const out = new Uint8Array(binary.length);
27
+ for (let i = 0; i < binary.length; i++)
28
+ out[i] = binary.charCodeAt(i);
29
+ return out;
30
+ };
31
+ const base64urlJson = (value) => base64url(encoder.encode(JSON.stringify(value)));
32
+ const sha256 = (data) => Effect.promise(async () => {
33
+ const bytes = typeof data === "string" ? encoder.encode(data) : data;
34
+ return new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
35
+ });
36
+ /** Detect the JWS algorithm for a JWK. */
37
+ export const algorithmOf = (jwk) => jwk.kty === "RSA" ? "RS256" : "ES256";
38
+ const importAlgorithm = (alg) => alg === "RS256"
39
+ ? { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }
40
+ : { name: "ECDSA", namedCurve: "P-256" };
41
+ const signAlgorithm = (alg) => alg === "RS256"
42
+ ? { name: "RSASSA-PKCS1-v1_5" }
43
+ : { name: "ECDSA", hash: "SHA-256" };
44
+ /** Generate a fresh account key as a private JWK (JSON text, Redacted). */
45
+ export const generateAccountKey = (alg = "ES256") => Effect.promise(async () => {
46
+ const pair = await crypto.subtle.generateKey(alg === "RS256"
47
+ ? {
48
+ name: "RSASSA-PKCS1-v1_5",
49
+ modulusLength: 2048,
50
+ publicExponent: new Uint8Array([1, 0, 1]),
51
+ hash: "SHA-256",
52
+ }
53
+ : { name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
54
+ const jwk = await crypto.subtle.exportKey("jwk", pair.privateKey);
55
+ return Redacted.make(JSON.stringify(jwk));
56
+ });
57
+ /** Parse a private JWK from its JSON text. */
58
+ export const parseJwk = (key) => Effect.try({
59
+ try: () => JSON.parse(Redacted.isRedacted(key) ? Redacted.value(key) : key),
60
+ catch: (cause) => new JoseError({
61
+ message: "The account key is not a JSON Web Key.",
62
+ cause,
63
+ }),
64
+ });
65
+ /** The public half of a JWK, with members in RFC 7638 canonical order. */
66
+ export const publicJwk = (jwk) => jwk.kty === "RSA"
67
+ ? { e: jwk.e, kty: "RSA", n: jwk.n }
68
+ : { crv: jwk.crv, kty: "EC", x: jwk.x, y: jwk.y };
69
+ /** RFC 7638 JWK thumbprint (base64url SHA-256 of the canonical public JWK). */
70
+ export const thumbprint = (jwk) => sha256(JSON.stringify(publicJwk(jwk))).pipe(Effect.map(base64url));
71
+ /** `<token>.<thumbprint>` — what a challenge proves the client holds. */
72
+ export const keyAuthorization = (token, jwk) => thumbprint(jwk).pipe(Effect.map((print) => `${token}.${print}`));
73
+ /** The TXT record value for a DNS-01 challenge: base64url(SHA-256(keyAuthorization)). */
74
+ export const dnsChallengeValue = (token, jwk) => keyAuthorization(token, jwk).pipe(Effect.flatMap(sha256), Effect.map(base64url));
75
+ /** Sign an ACME request body (RFC 8555 §6.2) with the account key. */
76
+ export const signRequest = (options) => Effect.tryPromise({
77
+ try: async () => {
78
+ const alg = algorithmOf(options.jwk);
79
+ const header = {
80
+ alg,
81
+ nonce: options.nonce,
82
+ url: options.url,
83
+ ...(options.kid !== undefined
84
+ ? { kid: options.kid }
85
+ : { jwk: publicJwk(options.jwk) }),
86
+ };
87
+ const protectedB64 = base64urlJson(header);
88
+ const payloadB64 = options.payload === undefined ? "" : base64urlJson(options.payload);
89
+ const key = await crypto.subtle.importKey("jwk", options.jwk, importAlgorithm(alg), false, ["sign"]);
90
+ const signature = await crypto.subtle.sign(signAlgorithm(alg), key, encoder.encode(`${protectedB64}.${payloadB64}`));
91
+ return {
92
+ protected: protectedB64,
93
+ payload: payloadB64,
94
+ signature: base64url(signature),
95
+ };
96
+ },
97
+ catch: (cause) => new JoseError({ message: "Signing the ACME request failed.", cause }),
98
+ });
99
+ /**
100
+ * The External Account Binding JWS (RFC 8555 §7.3.4): HS256 over the
101
+ * account's public JWK, keyed by the CA-issued HMAC key, with the CA's key
102
+ * id as `kid`.
103
+ */
104
+ export const signExternalAccountBinding = (options) => Effect.tryPromise({
105
+ try: async () => {
106
+ const raw = Redacted.isRedacted(options.hmacKey)
107
+ ? Redacted.value(options.hmacKey)
108
+ : options.hmacKey;
109
+ const key = await crypto.subtle.importKey("raw", base64urlDecode(raw), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
110
+ const protectedB64 = base64urlJson({
111
+ alg: "HS256",
112
+ kid: options.keyId,
113
+ url: options.url,
114
+ });
115
+ const payloadB64 = base64urlJson(publicJwk(options.jwk));
116
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${protectedB64}.${payloadB64}`));
117
+ return {
118
+ protected: protectedB64,
119
+ payload: payloadB64,
120
+ signature: base64url(signature),
121
+ };
122
+ },
123
+ catch: (cause) => new JoseError({
124
+ message: "Signing the External Account Binding failed.",
125
+ cause,
126
+ }),
127
+ });
128
+ //# sourceMappingURL=jose.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jose.js","sourceRoot":"","sources":["../src/jose.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAexC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;AAElC,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,KAA+B,EAAU,EAAE;IACnE,MAAM,IAAI,GAAG,KAAK,YAAY,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;IACzE,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,IAAI;QAAE,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IAC7D,OAAO,IAAI,CAAC,MAAM,CAAC;SAChB,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC;SACpB,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC;SACpB,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACxB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAY,EAAc,EAAE;IAC1D,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAC9D,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACxE,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC1C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IACtE,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAEF,MAAM,aAAa,GAAG,CAAC,KAAc,EAAU,EAAE,CAC/C,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnD,MAAM,MAAM,GAAG,CAAC,IAAyB,EAA6B,EAAE,CACtE,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE;IACxB,MAAM,KAAK,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACrE,OAAO,IAAI,UAAU,CACnB,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,KAAgC,CAAC,CACxE,CAAC;AACJ,CAAC,CAAC,CAAC;AAEL,0CAA0C;AAC1C,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,GAAQ,EAAa,EAAE,CACjD,GAAG,CAAC,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;AAExC,MAAM,eAAe,GAAG,CAAC,GAAc,EAAE,EAAE,CACzC,GAAG,KAAK,OAAO;IACb,CAAC,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,SAAS,EAAE;IAChD,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC;AAE7C,MAAM,aAAa,GAAG,CAAC,GAAc,EAAE,EAAE,CACvC,GAAG,KAAK,OAAO;IACb,CAAC,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE;IAC/B,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AAEzC,2EAA2E;AAC3E,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,GAAG,GAAc,OAAO,EACkB,EAAE,CAC5C,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE;IACxB,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,WAAW,CAC1C,GAAG,KAAK,OAAO;QACb,CAAC,CAAC;YACE,IAAI,EAAE,mBAAmB;YACzB,aAAa,EAAE,IAAI;YACnB,cAAc,EAAE,IAAI,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YACzC,IAAI,EAAE,SAAS;SAChB;QACH,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,EAC1C,IAAI,EACJ,CAAC,MAAM,EAAE,QAAQ,CAAC,CACnB,CAAC;IACF,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAClE,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;AAC5C,CAAC,CAAC,CAAC;AAEL,8CAA8C;AAC9C,MAAM,CAAC,MAAM,QAAQ,GAAG,CACtB,GAAuC,EACR,EAAE,CACjC,MAAM,CAAC,GAAG,CAAC;IACT,GAAG,EAAE,GAAG,EAAE,CACR,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAQ;IACzE,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CACf,IAAI,SAAS,CAAC;QACZ,OAAO,EAAE,wCAAwC;QACjD,KAAK;KACN,CAAC;CACL,CAAC,CAAC;AAEL,0EAA0E;AAC1E,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,GAAQ,EAAO,EAAE,CACzC,GAAG,CAAC,GAAG,KAAK,KAAK;IACf,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE;IACpC,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC;AAEtD,+EAA+E;AAC/E,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,GAAQ,EAAyB,EAAE,CAC5D,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC;AAErE,yEAAyE;AACzE,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAC9B,KAAa,EACb,GAAQ,EACe,EAAE,CACzB,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC;AAEnE,yFAAyF;AACzF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,KAAa,EACb,GAAQ,EACe,EAAE,CACzB,gBAAgB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,IAAI,CAC/B,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EACtB,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CACtB,CAAC;AAkBJ,sEAAsE;AACtE,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,OAAoB,EACoB,EAAE,CAC1C,MAAM,CAAC,UAAU,CAAC;IAChB,GAAG,EAAE,KAAK,IAAI,EAAE;QACd,MAAM,GAAG,GAAG,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACrC,MAAM,MAAM,GAAG;YACb,GAAG;YACH,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,GAAG,EAAE,OAAO,CAAC,GAAG;YAChB,GAAG,CAAC,OAAO,CAAC,GAAG,KAAK,SAAS;gBAC3B,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;gBACtB,CAAC,CAAC,EAAE,GAAG,EAAE,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;SACrC,CAAC;QACF,MAAM,YAAY,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;QAC3C,MAAM,UAAU,GACd,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACtE,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CACvC,KAAK,EACL,OAAO,CAAC,GAAiB,EACzB,eAAe,CAAC,GAAG,CAAC,EACpB,KAAK,EACL,CAAC,MAAM,CAAC,CACT,CAAC;QACF,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CACxC,aAAa,CAAC,GAAG,CAAC,EAClB,GAAG,EACH,OAAO,CAAC,MAAM,CAAC,GAAG,YAAY,IAAI,UAAU,EAAE,CAAC,CAChD,CAAC;QACF,OAAO;YACL,SAAS,EAAE,YAAY;YACvB,OAAO,EAAE,UAAU;YACnB,SAAS,EAAE,SAAS,CAAC,SAAS,CAAC;SAChC,CAAC;IACJ,CAAC;IACD,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CACf,IAAI,SAAS,CAAC,EAAE,OAAO,EAAE,kCAAkC,EAAE,KAAK,EAAE,CAAC;CACxE,CAAC,CAAC;AAEL;;;;GAIG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,OAK1C,EAA0C,EAAE,CAC3C,MAAM,CAAC,UAAU,CAAC;IAChB,GAAG,EAAE,KAAK,IAAI,EAAE;QACd,MAAM,GAAG,GAAG,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC;YAC9C,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;YACjC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;QACpB,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CACvC,KAAK,EACL,eAAe,CAAC,GAAG,CAA4B,EAC/C,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,EACjC,KAAK,EACL,CAAC,MAAM,CAAC,CACT,CAAC;QACF,MAAM,YAAY,GAAG,aAAa,CAAC;YACjC,GAAG,EAAE,OAAO;YACZ,GAAG,EAAE,OAAO,CAAC,KAAK;YAClB,GAAG,EAAE,OAAO,CAAC,GAAG;SACjB,CAAC,CAAC;QACH,MAAM,UAAU,GAAG,aAAa,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;QACzD,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CACxC,MAAM,EACN,GAAG,EACH,OAAO,CAAC,MAAM,CAAC,GAAG,YAAY,IAAI,UAAU,EAAE,CAAC,CAChD,CAAC;QACF,OAAO;YACL,SAAS,EAAE,YAAY;YACvB,OAAO,EAAE,UAAU;YACnB,SAAS,EAAE,SAAS,CAAC,SAAS,CAAC;SAChC,CAAC;IACJ,CAAC;IACD,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CACf,IAAI,SAAS,CAAC;QACZ,OAAO,EAAE,8CAA8C;QACvD,KAAK;KACN,CAAC;CACL,CAAC,CAAC"}
@@ -0,0 +1,27 @@
1
+ import * as Layer from "effect/Layer";
2
+ import * as Redacted from "effect/Redacted";
3
+ import type * as AST from "effect/SchemaAST";
4
+ import * as HttpClient from "effect/http/HttpClient";
5
+ import type * as HttpClientError from "effect/http/HttpClientError";
6
+ import * as API from "@distilled.cloud/core/api";
7
+ import { type ConfigError } from "@distilled.cloud/core/errors";
8
+ import { Credentials } from "./credentials.ts";
9
+ import { type DefaultErrors } from "./errors.ts";
10
+ /**
11
+ * Error channel shared by every generated ACME operation. Generated service
12
+ * files annotate operations with `API.OperationMethod<I, O, AcmeOpError,
13
+ * AcmeOpContext>` explicitly so the compiler never infers these back out of
14
+ * the schema generics.
15
+ */
16
+ export type AcmeOpError = DefaultErrors | ConfigError | HttpClientError.HttpClientError;
17
+ /** Context (requirements) shared by every generated ACME operation. */
18
+ export type AcmeOpContext = Credentials | HttpClient.HttpClient;
19
+ export declare const AcmeProtocol: Layer.Layer<API.Protocol>;
20
+ /** Forget cached directories and nonces (tests, CA switches). */
21
+ export declare const resetProtocolCaches: () => void;
22
+ /** @internal re-exported for tests */
23
+ export declare const _internal: {
24
+ getAnn: (ast: AST.AST, symbol: symbol) => unknown;
25
+ Redacted: typeof Redacted;
26
+ };
27
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAG5C,OAAO,KAAK,KAAK,GAAG,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,KAAK,eAAe,MAAM,6BAA6B,CAAC;AAGpE,OAAO,KAAK,GAAG,MAAM,2BAA2B,CAAC;AAajD,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,8BAA8B,CAAC;AAKtC,OAAO,EAAE,WAAW,EAAe,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AAQrB;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB,aAAa,GACb,WAAW,GACX,eAAe,CAAC,eAAe,CAAC;AAEpC,uEAAuE;AACvE,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,UAAU,CAAC,UAAU,CAAC;AA4dhE,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAOlD,CAAC;AAEF,iEAAiE;AACjE,eAAO,MAAM,mBAAmB,QAAO,IAGtC,CAAC;AAEF,sCAAsC;AACtC,eAAO,MAAM,SAAS;;;CAAuB,CAAC"}
@@ -0,0 +1,410 @@
1
+ /**
2
+ * AcmeProtocol — hand-written.
3
+ *
4
+ * ACME (RFC 8555) is JSON over HTTPS with two twists the generic REST
5
+ * protocol cannot express:
6
+ *
7
+ * signing: every POST body is a flattened JWS (RFC 8555 §6.2) signed by
8
+ * the account key from `Credentials`, carrying a fresh
9
+ * anti-replay nonce and the request URL in its protected
10
+ * header. `newAccount` embeds the public JWK; every later
11
+ * request references the account URL as `kid`. A CA-issued
12
+ * External Account Binding is attached to `newAccount` when
13
+ * the credentials carry one. Reads are "POST-as-GET" (empty
14
+ * payload).
15
+ *
16
+ * addressing: the provider endpoint is the CA's **directory URL**. The
17
+ * protocol fetches the directory once per CA and resolves
18
+ * `newNonce`/`newAccount`/`newOrder`/`revokeCert` from it;
19
+ * every other operation names its resource by the absolute
20
+ * URL the CA returned (`url` label).
21
+ *
22
+ * Responses are JSON; `Location` (account/order URL), `Replay-Nonce` and
23
+ * `Link` headers are folded into declared output members. Failures are
24
+ * RFC 7807 problem documents matched on their `type` URN against the
25
+ * operation's typed error classes; an unmatched URN is `UnknownAcmeError`.
26
+ */
27
+ import { isStrict, validateResponse, } from "@distilled.cloud/core/response-validation";
28
+ import * as Effect from "effect/Effect";
29
+ import * as Layer from "effect/Layer";
30
+ import * as Redacted from "effect/Redacted";
31
+ import * as Schema from "effect/Schema";
32
+ import * as HttpClient from "effect/http/HttpClient";
33
+ import * as HttpClientRequest from "effect/http/HttpClientRequest";
34
+ import * as API from "@distilled.cloud/core/api";
35
+ import { getAnn, getProps, hasPropAnn, mapKeys, } from "@distilled.cloud/core/protocol-http";
36
+ import { unwrapRedactedDeep } from "@distilled.cloud/core/protocol-rest";
37
+ import { getErrorMatchers, httpSymbol, labelSymbol, } from "@distilled.cloud/core/trait";
38
+ import { HTTP_STATUS_MAP, InternalServerError, } from "@distilled.cloud/core/errors";
39
+ import { parseRetryAfter, parseRetryAfterForStatus, } from "@distilled.cloud/core/retry-after";
40
+ import { Credentials } from "./credentials.js";
41
+ import { AcmeParseError, DirectoryMissingResource, JoseError, UnknownAcmeError, } from "./errors.js";
42
+ import { parseJwk, signExternalAccountBinding, signRequest, } from "./jose.js";
43
+ // =============================================================================
44
+ // Directory + nonce caches (per CA, process-wide)
45
+ // =============================================================================
46
+ const DirectorySchema = Schema.Struct({
47
+ newNonce: Schema.String,
48
+ newAccount: Schema.String,
49
+ newOrder: Schema.String,
50
+ revokeCert: Schema.String,
51
+ newAuthz: Schema.optional(Schema.String),
52
+ keyChange: Schema.optional(Schema.String),
53
+ meta: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
54
+ });
55
+ const directories = new Map();
56
+ /** The last unused `Replay-Nonce` per directory URL. Nonces are single-use. */
57
+ const nonces = new Map();
58
+ // HTTP request copies preserve the body identity; entries live only with a request.
59
+ const signedRequests = new WeakMap();
60
+ const resolveCredentials = Effect.gen(function* () {
61
+ const resolve = yield* Credentials;
62
+ return yield* resolve;
63
+ });
64
+ const fetchDirectory = (directoryUrl) => Effect.gen(function* () {
65
+ const cached = directories.get(directoryUrl);
66
+ if (cached)
67
+ return cached;
68
+ const client = yield* HttpClient.HttpClient;
69
+ const response = yield* client.get(directoryUrl);
70
+ rememberNonce(directoryUrl, response);
71
+ if (response.status >= 400) {
72
+ const text = yield* response.text;
73
+ return yield* fail(new UnknownAcmeError({
74
+ message: `GET ${directoryUrl} answered ${response.status}`,
75
+ status: response.status,
76
+ body: Redacted.make(text),
77
+ }));
78
+ }
79
+ const text = yield* response.text;
80
+ const json = yield* parseJson(text);
81
+ const directory = yield* Schema.decodeUnknownEffect(DirectorySchema)(json).pipe(Effect.mapError(() => parseError("Invalid ACME directory response")));
82
+ directories.set(directoryUrl, directory);
83
+ return directory;
84
+ });
85
+ const rememberNonce = (directoryUrl, response) => {
86
+ const nonce = response.headers["replay-nonce"];
87
+ if (typeof nonce === "string" && nonce.length > 0) {
88
+ nonces.set(directoryUrl, nonce);
89
+ }
90
+ };
91
+ /** Take the cached nonce for this CA, or fetch a fresh one from `newNonce`. */
92
+ const takeNonce = (directoryUrl, directory) => Effect.gen(function* () {
93
+ const cached = nonces.get(directoryUrl);
94
+ if (cached !== undefined) {
95
+ nonces.delete(directoryUrl);
96
+ return cached;
97
+ }
98
+ const client = yield* HttpClient.HttpClient;
99
+ const response = yield* client.execute(HttpClientRequest.head(directory.newNonce));
100
+ const nonce = response.headers["replay-nonce"];
101
+ if (typeof nonce !== "string" || nonce.length === 0) {
102
+ return yield* fail(new UnknownAcmeError({
103
+ message: `HEAD ${directory.newNonce} returned no Replay-Nonce header`,
104
+ status: response.status,
105
+ body: undefined,
106
+ }));
107
+ }
108
+ return nonce;
109
+ });
110
+ // =============================================================================
111
+ // Encode
112
+ // =============================================================================
113
+ /**
114
+ * The generated operations carry no name at runtime; the input's `T.Http`
115
+ * uri (from the Smithy model) identifies which ACME resource an
116
+ * operation addresses.
117
+ */
118
+ const OPERATIONS_BY_URI = {
119
+ "/directory": "GetDirectory",
120
+ "/newNonce": "NewNonce",
121
+ "/newAccount": "NewAccount",
122
+ "/account/{url}": "UpdateAccount",
123
+ "/newOrder": "NewOrder",
124
+ "/order/{url}": "GetOrder",
125
+ "/finalize/{url}": "FinalizeOrder",
126
+ "/authz/{url}": "GetAuthorization",
127
+ "/authz/{url}/deactivate": "DeactivateAuthorization",
128
+ "/challenge/{url}": "RespondChallenge",
129
+ "/cert/{url}": "DownloadCertificate",
130
+ "/revokeCert": "RevokeCertificate",
131
+ };
132
+ const operationOf = (inputAst) => {
133
+ const http = getAnn(inputAst, httpSymbol);
134
+ return OPERATIONS_BY_URI[http?.uri ?? ""] ?? "";
135
+ };
136
+ /** Operations resolved from the directory rather than an input `url`. */
137
+ const DIRECTORY_OPERATIONS = {
138
+ NewNonce: "newNonce",
139
+ NewAccount: "newAccount",
140
+ NewOrder: "newOrder",
141
+ RevokeCertificate: "revokeCert",
142
+ };
143
+ /** Operations whose payload is the empty POST-as-GET. */
144
+ const POST_AS_GET = new Set([
145
+ "GetOrder",
146
+ "GetAuthorization",
147
+ "DownloadCertificate",
148
+ ]);
149
+ const JOSE_ACCEPT = "application/json, application/pem-certificate-chain";
150
+ const encode = ({ input, inputAst, }) => Effect.gen(function* () {
151
+ const creds = yield* resolveCredentials;
152
+ const operation = operationOf(inputAst);
153
+ if (operation === "") {
154
+ return yield* fail(new JoseError({
155
+ message: "The operation's input carries no known ACME `T.Http` uri.",
156
+ }));
157
+ }
158
+ if (operation === "GetDirectory") {
159
+ return HttpClientRequest.get(creds.directoryUrl).pipe(HttpClientRequest.setHeader("Accept", "application/json"));
160
+ }
161
+ const directory = yield* fetchDirectory(creds.directoryUrl);
162
+ if (operation === "NewNonce") {
163
+ return HttpClientRequest.head(directory.newNonce);
164
+ }
165
+ // Resolve the request URL: from the directory for the well-known
166
+ // resources, else the absolute `url` label the CA handed back.
167
+ const inputObj = (unwrapRedactedDeep(input) ?? {});
168
+ const directoryKey = DIRECTORY_OPERATIONS[operation];
169
+ let url;
170
+ if (directoryKey !== undefined) {
171
+ const resolved = directory[directoryKey];
172
+ if (typeof resolved !== "string") {
173
+ return yield* fail(new DirectoryMissingResource({
174
+ resource: directoryKey,
175
+ directoryUrl: creds.directoryUrl,
176
+ }));
177
+ }
178
+ url = resolved;
179
+ }
180
+ else {
181
+ const label = getProps(inputAst).find((p) => hasPropAnn(p, labelSymbol));
182
+ const value = label ? inputObj[String(label.name)] : undefined;
183
+ if (typeof value !== "string") {
184
+ return yield* fail(new JoseError({
185
+ message: `${operation} needs the resource URL the CA returned (\`url\`).`,
186
+ }));
187
+ }
188
+ url = value;
189
+ }
190
+ // The payload: every input member that isn't the URL label, or the
191
+ // empty POST-as-GET for reads.
192
+ let payload;
193
+ if (!POST_AS_GET.has(operation)) {
194
+ const body = {};
195
+ for (const prop of getProps(inputAst)) {
196
+ if (hasPropAnn(prop, labelSymbol))
197
+ continue;
198
+ const key = String(prop.name);
199
+ if (inputObj[key] !== undefined)
200
+ body[key] = inputObj[key];
201
+ }
202
+ payload = body;
203
+ }
204
+ const jwk = yield* parseJwk(creds.accountKey);
205
+ // `newAccount` proves the key by embedding it; a CA that requires an
206
+ // External Account Binding gets it from the credentials unless the
207
+ // caller built one.
208
+ const embedKey = operation === "NewAccount" || creds.accountUrl === undefined;
209
+ if (operation === "NewAccount" &&
210
+ creds.externalAccountBinding !== undefined &&
211
+ payload.externalAccountBinding === undefined) {
212
+ payload.externalAccountBinding =
213
+ yield* signExternalAccountBinding({
214
+ jwk,
215
+ url,
216
+ keyId: creds.externalAccountBinding.keyId,
217
+ hmacKey: creds.externalAccountBinding.hmacKey,
218
+ });
219
+ }
220
+ const nonce = yield* takeNonce(creds.directoryUrl, directory);
221
+ const signing = {
222
+ jwk,
223
+ url,
224
+ kid: embedKey ? undefined : creds.accountUrl,
225
+ payload,
226
+ directoryUrl: creds.directoryUrl,
227
+ };
228
+ const request = yield* signedRequest(signing, nonce);
229
+ signedRequests.set(request.body, signing);
230
+ return request;
231
+ });
232
+ const signedRequest = (signing, nonce) => Effect.gen(function* () {
233
+ const jws = yield* signRequest({ ...signing, nonce });
234
+ return HttpClientRequest.post(signing.url).pipe(HttpClientRequest.setHeader("Accept", JOSE_ACCEPT), HttpClientRequest.bodyText(JSON.stringify(jws), "application/jose+json"));
235
+ });
236
+ const isProblem = (value) => isObject(value) && typeof value.type === "string";
237
+ /**
238
+ * Match a problem document against the operation's typed error classes by
239
+ * its `type` URN (the matcher's `message` rule is applied to the URN, so a
240
+ * patch writes `{ "message": { "matches": "^urn:ietf:params:acme:error:badNonce$" } }`).
241
+ */
242
+ const matchProblem = (errorClasses, status, problem, headers) => {
243
+ const urn = problem.type ?? "";
244
+ let best;
245
+ for (const cls of errorClasses) {
246
+ const matchers = getErrorMatchers(cls);
247
+ if (!matchers)
248
+ continue;
249
+ for (const m of matchers) {
250
+ if (m.status !== undefined && m.status !== status)
251
+ continue;
252
+ if (m.code !== undefined && m.code !== status)
253
+ continue;
254
+ const rule = m.message;
255
+ let hit = false;
256
+ if (rule === undefined)
257
+ hit = true;
258
+ else if (typeof rule === "string")
259
+ hit = rule === urn;
260
+ else if (rule.matches !== undefined)
261
+ hit = new RegExp(rule.matches).test(urn);
262
+ else if (rule.includes !== undefined)
263
+ hit = urn.includes(rule.includes);
264
+ if (!hit)
265
+ continue;
266
+ const specificity = (m.status !== undefined ? 1 : 0) + (rule !== undefined ? 2 : 0);
267
+ if (!best || specificity > best.specificity)
268
+ best = { cls, specificity };
269
+ }
270
+ }
271
+ if (!best)
272
+ return undefined;
273
+ return new best.cls({
274
+ code: status,
275
+ message: problem.detail ?? urn,
276
+ type: urn,
277
+ detail: problem.detail,
278
+ subproblems: problem.subproblems,
279
+ ...(urn === "urn:ietf:params:acme:error:rateLimited"
280
+ ? { retryAfter: parseRetryAfter(headers) }
281
+ : {}),
282
+ });
283
+ };
284
+ const parseLinkAlternates = (link) => {
285
+ if (!link)
286
+ return [];
287
+ const out = [];
288
+ for (const part of link.split(",")) {
289
+ const match = part.match(/<([^>]+)>\s*;\s*rel="?alternate"?/);
290
+ if (match)
291
+ out.push(match[1]);
292
+ }
293
+ return out;
294
+ };
295
+ const decode = ({ response, outputAst, errors: errorClasses, }) => Effect.gen(function* () {
296
+ const signing = signedRequests.get(response.request.body);
297
+ signedRequests.delete(response.request.body);
298
+ const directoryUrl = signing?.directoryUrl ?? (yield* resolveCredentials).directoryUrl;
299
+ for (let retries = 0;; retries++) {
300
+ const headers = response.headers;
301
+ const status = response.status;
302
+ const isNewNonce = response.request.method === "HEAD";
303
+ const isCertificate = (headers["content-type"] ?? "").includes("pem-certificate-chain");
304
+ const text = isNewNonce ? "" : yield* response.text;
305
+ const json = isNewNonce || (isCertificate && status < 400)
306
+ ? undefined
307
+ : yield* parseJson(text).pipe(
308
+ // A non-JSON 2xx fails only in strict mode; lenient returns
309
+ // the text as read.
310
+ Effect.catchTag("AcmeParseError", (error) => status >= 400
311
+ ? Effect.succeed(undefined)
312
+ : Effect.flatMap(isStrict, (strict) => strict
313
+ ? Effect.fail(error)
314
+ : Effect.succeed(text))));
315
+ const problem = status >= 400 && isProblem(json) ? json : undefined;
316
+ const badNonce = problem?.type === "urn:ietf:params:acme:error:badNonce";
317
+ if (badNonce) {
318
+ const nonce = headers["replay-nonce"];
319
+ // Rejection nonces belong only to this request, never to the shared cache.
320
+ if (signing && nonce && retries < 2) {
321
+ const client = yield* HttpClient.HttpClient;
322
+ response = yield* client.execute(yield* signedRequest(signing, nonce));
323
+ continue;
324
+ }
325
+ }
326
+ else {
327
+ rememberNonce(directoryUrl, response);
328
+ }
329
+ if (status >= 400) {
330
+ if (problem) {
331
+ const typed = matchProblem(errorClasses, status, problem, headers);
332
+ if (typed !== undefined)
333
+ return yield* fail(typed);
334
+ return yield* fail(new UnknownAcmeError({
335
+ type: problem.type,
336
+ detail: problem.detail,
337
+ subproblems: problem.subproblems,
338
+ message: problem.detail ?? problem.type,
339
+ status,
340
+ body: Redacted.make(json),
341
+ }));
342
+ }
343
+ const message = `HTTP ${status}`;
344
+ const StatusClass = HTTP_STATUS_MAP[status];
345
+ if (StatusClass) {
346
+ return yield* fail(new StatusClass({
347
+ message,
348
+ retryAfter: parseRetryAfterForStatus(status, headers),
349
+ }));
350
+ }
351
+ if (status >= 500) {
352
+ return yield* fail(new InternalServerError({
353
+ message,
354
+ retryAfter: parseRetryAfterForStatus(status, headers),
355
+ }));
356
+ }
357
+ return yield* fail(new UnknownAcmeError({
358
+ message,
359
+ status,
360
+ body: Redacted.make(json ?? text),
361
+ }));
362
+ }
363
+ let body;
364
+ if (isNewNonce) {
365
+ body = { replayNonce: headers["replay-nonce"] || undefined };
366
+ }
367
+ else if (isCertificate) {
368
+ body = {
369
+ chain: text,
370
+ alternates: parseLinkAlternates(headers["link"]),
371
+ };
372
+ }
373
+ else if (isObject(json)) {
374
+ body = json;
375
+ const location = headers["location"];
376
+ if (location)
377
+ body = { ...body, location };
378
+ }
379
+ else {
380
+ // Lenient mode returns a non-object body as read.
381
+ if (!(yield* isStrict))
382
+ return json;
383
+ return yield* fail(parseError("Expected a JSON object"));
384
+ }
385
+ // Strict mode (core/response-validation) checks the output schema.
386
+ return yield* validateResponse(outputAst, mapKeys(outputAst, body, "decode"), () => parseError("Response does not match the output schema"));
387
+ }
388
+ });
389
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
390
+ const parseError = (cause) => new AcmeParseError({
391
+ body: "[REDACTED]",
392
+ cause,
393
+ });
394
+ const parseJson = (text) => Effect.try({
395
+ try: () => (text.trim().length > 0 ? JSON.parse(text) : {}),
396
+ catch: () => parseError("Invalid JSON response"),
397
+ });
398
+ const fail = (error) => Effect.fail(error);
399
+ export const AcmeProtocol = Layer.succeed(API.Protocol, API.Protocol.of({
400
+ encode: (args) => encode(args),
401
+ decode: (args) => decode(args),
402
+ }));
403
+ /** Forget cached directories and nonces (tests, CA switches). */
404
+ export const resetProtocolCaches = () => {
405
+ directories.clear();
406
+ nonces.clear();
407
+ };
408
+ /** @internal re-exported for tests */
409
+ export const _internal = { getAnn, Redacted };
410
+ //# sourceMappingURL=protocol.js.map