66use Psr \Log \LoggerInterface ;
77use CoyoteCert \Enums \AuthorizationChallengeEnum ;
88use CoyoteCert \Enums \KeyType ;
9+ use CoyoteCert \DTO \RenewalWindow ;
910use CoyoteCert \Exceptions \LetsEncryptClientException ;
1011use CoyoteCert \Interfaces \ChallengeHandlerInterface ;
1112use CoyoteCert \Provider \AcmeProviderInterface ;
3435 */
3536class CoyoteCert
3637{
37- private ?StorageInterface $ storage = null ;
38- private ?LoggerInterface $ logger = null ;
39- private array $ domains = [];
38+ private ?StorageInterface $ storage = null ;
39+ private ?LoggerInterface $ logger = null ;
40+ private string $ email = '' ;
41+ private array $ domains = [];
4042 private ?ChallengeHandlerInterface $ challengeHandler = null ;
41- private KeyType $ certKeyType = KeyType::EC_P256 ;
42- private KeyType $ accountKeyType = KeyType::RSA_2048 ;
43- private bool $ localTest = true ;
43+ private KeyType $ certKeyType = KeyType::EC_P256 ;
44+ private KeyType $ accountKeyType = KeyType::RSA_2048 ;
45+ private bool $ localTest = true ;
4446
4547 private function __construct (private readonly AcmeProviderInterface $ provider )
4648 {
@@ -53,6 +55,13 @@ public static function with(AcmeProviderInterface $provider): self
5355 return new self ($ provider );
5456 }
5557
58+ public function email (string $ email ): self
59+ {
60+ $ this ->email = $ email ;
61+
62+ return $ this ;
63+ }
64+
5665 public function storage (StorageInterface $ storage ): self
5766 {
5867 $ this ->storage = $ storage ;
@@ -126,7 +135,17 @@ public function needsRenewal(int $daysBeforeExpiry = 30): bool
126135
127136 $ cert = $ this ->storage ->getCertificate ($ this ->domains [0 ]);
128137
129- return $ cert === null || $ cert ->remainingDays () <= $ daysBeforeExpiry ;
138+ if ($ cert === null ) {
139+ return true ;
140+ }
141+
142+ $ window = $ this ->ariWindow ($ cert );
143+
144+ if ($ window !== null ) {
145+ return $ window ->isOpen ();
146+ }
147+
148+ return $ cert ->remainingDays () <= $ daysBeforeExpiry ;
130149 }
131150
132151 // ── Terminal actions ──────────────────────────────────────────────────────
@@ -148,7 +167,7 @@ public function issue(): StoredCertificate
148167 // ── 1. Get or create ACME account ──────────────────────────────────
149168 $ account = $ api ->account ()->exists ()
150169 ? $ api ->account ()->get ()
151- : $ api ->account ()->create ();
170+ : $ api ->account ()->create ($ this -> email );
152171
153172 // ── 2. Create order ────────────────────────────────────────────────
154173 $ order = $ api ->order ()->new ($ account , $ this ->domains );
@@ -200,16 +219,19 @@ public function issue(): StoredCertificate
200219 throw new LetsEncryptClientException ('Order finalization failed. ' );
201220 }
202221
203- // ── 13. Download certificate bundle ───────────────────────────────
222+ // ── 13. Poll until order transitions processing → valid ────────────
223+ $ order = $ api ->order ()->waitUntilValid ($ order );
224+
225+ // ── 14. Download certificate bundle ───────────────────────────────
204226 $ bundle = $ api ->certificate ()->getBundle ($ order );
205227
206- // ── 14 . Parse expiry date from the DER-encoded certificate ─────────
228+ // ── 15 . Parse expiry date from the DER-encoded certificate ─────────
207229 $ parsed = openssl_x509_parse ($ bundle ->certificate );
208230 $ expiresAt = isset ($ parsed ['validTo_time_t ' ])
209231 ? (new DateTimeImmutable ())->setTimestamp ((int ) $ parsed ['validTo_time_t ' ])
210232 : new DateTimeImmutable ('+90 days ' );
211233
212- // ── 15 . Build and persist the stored certificate ───────────────────
234+ // ── 16 . Build and persist the stored certificate ───────────────────
213235 $ stored = new StoredCertificate (
214236 certificate: $ bundle ->certificate ,
215237 privateKey: $ certKeyPem ,
@@ -252,6 +274,25 @@ public function issueOrRenew(int $daysBeforeExpiry = 30): StoredCertificate
252274
253275 // ── Private helpers ───────────────────────────────────────────────────────
254276
277+ private function ariWindow (StoredCertificate $ cert ): ?RenewalWindow
278+ {
279+ if (empty ($ cert ->caBundle )) {
280+ return null ;
281+ }
282+
283+ if (!preg_match ('~(-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----)~ ' , $ cert ->caBundle , $ m )) {
284+ return null ;
285+ }
286+
287+ try {
288+ return (new Api (provider: $ this ->provider , logger: $ this ->logger ))
289+ ->renewalInfo ()
290+ ->get ($ cert ->certificate , $ m [1 ]);
291+ } catch (\Throwable ) {
292+ return null ;
293+ }
294+ }
295+
255296 private function validate (): void
256297 {
257298 if (empty ($ this ->domains )) {
0 commit comments