BREAKING CHANGE(smartacme): Make wildcard certificates opt-in to fix HTTP-01 only configurations
This commit is contained in:
		
							
								
								
									
										17
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								changelog.md
									
									
									
									
									
								
							@@ -1,5 +1,22 @@
 | 
				
			|||||||
# Changelog
 | 
					# Changelog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 2025-05-19 - 8.0.0 - BREAKING CHANGE(smartacme)
 | 
				
			||||||
 | 
					Make wildcard certificates opt-in to fix HTTP-01 only configurations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- BREAKING CHANGE: Wildcard certificates are no longer automatically requested for all domains
 | 
				
			||||||
 | 
					- Added 'includeWildcard' option to getCertificateForDomain() to explicitly request wildcard certificates
 | 
				
			||||||
 | 
					- HTTP-01 only configurations now work correctly as they do not try to request wildcard certificates automatically
 | 
				
			||||||
 | 
					- Updated certificate CSR generation to match the requested domain configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 2025-05-19 - 7.4.0 - feat(smartacme)
 | 
				
			||||||
 | 
					Make wildcard certificates opt-in to fix HTTP-01 only configurations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- BREAKING CHANGE: Wildcard certificates are no longer automatically requested for all domains
 | 
				
			||||||
 | 
					- Added `includeWildcard` option to `getCertificateForDomain()` to explicitly request wildcards
 | 
				
			||||||
 | 
					- HTTP-01 only configurations now work correctly as they no longer attempt wildcard certificates
 | 
				
			||||||
 | 
					- Wildcard certificates require DNS-01 handler and must be explicitly requested
 | 
				
			||||||
 | 
					- Updated certificate CSR generation to match the requested domain configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 2025-05-18 - 7.3.4 - fix(smartacme)
 | 
					## 2025-05-18 - 7.3.4 - fix(smartacme)
 | 
				
			||||||
Refine documentation and tests for improved clarity in ACME certificate management
 | 
					Refine documentation and tests for improved clarity in ACME certificate management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "@push.rocks/smartacme",
 | 
					  "name": "@push.rocks/smartacme",
 | 
				
			||||||
  "version": "7.3.4",
 | 
					  "version": "7.4.0",
 | 
				
			||||||
  "private": false,
 | 
					  "private": false,
 | 
				
			||||||
  "description": "A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.",
 | 
					  "description": "A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.",
 | 
				
			||||||
  "main": "dist_ts/index.js",
 | 
					  "main": "dist_ts/index.js",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,2 +1,13 @@
 | 
				
			|||||||
 - this repo is dependent on letsencrypt and its limits
 | 
					 - this repo is dependent on letsencrypt and its limits
 | 
				
			||||||
 - to simpify the outside API, smartacme is stateful, meaning it works with a mongodb and a collection called 'SmartacmeCert'.
 | 
					 - to simpify the outside API, smartacme is stateful, meaning it works with a mongodb and a collection called 'SmartacmeCert'.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Certificate Request Behavior
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					As of v7.4.0, SmartAcme no longer automatically requests wildcard certificates for all domain requests. This change was made to fix issues with HTTP-01 only configurations which cannot validate wildcard domains.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- By default, `getCertificateForDomain('example.com')` only requests a certificate for `example.com`
 | 
				
			||||||
 | 
					- To request both regular and wildcard certificates, use `getCertificateForDomain('example.com', { includeWildcard: true })`
 | 
				
			||||||
 | 
					- Wildcard certificates require a DNS-01 challenge handler to be configured
 | 
				
			||||||
 | 
					- Direct wildcard requests like `getCertificateForDomain('*.example.com')` only request the wildcard certificate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This change ensures HTTP-01 only configurations work properly while still allowing wildcard certificates when needed and supported.
 | 
				
			||||||
							
								
								
									
										10
									
								
								readme.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								readme.md
									
									
									
									
									
								
							@@ -196,9 +196,14 @@ async function main() {
 | 
				
			|||||||
  await smartAcmeInstance.start();
 | 
					  await smartAcmeInstance.start();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const myDomain = 'example.com';
 | 
					  const myDomain = 'example.com';
 | 
				
			||||||
 | 
					  // Get certificate for domain (no wildcard)
 | 
				
			||||||
  const myCert = await smartAcmeInstance.getCertificateForDomain(myDomain);
 | 
					  const myCert = await smartAcmeInstance.getCertificateForDomain(myDomain);
 | 
				
			||||||
  console.log('Certificate:', myCert);
 | 
					  console.log('Certificate:', myCert);
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
 | 
					  // Get certificate with wildcard (requires DNS-01 handler)
 | 
				
			||||||
 | 
					  const certWithWildcard = await smartAcmeInstance.getCertificateForDomain(myDomain, { includeWildcard: true });
 | 
				
			||||||
 | 
					  console.log('Certificate with wildcard:', certWithWildcard);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await smartAcmeInstance.stop();
 | 
					  await smartAcmeInstance.stop();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -306,7 +311,10 @@ The certificate object obtained from the `getCertificateForDomain` method has th
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
- **start()**: Initializes the SmartAcme instance, sets up the ACME client, and registers the account with Let's Encrypt.
 | 
					- **start()**: Initializes the SmartAcme instance, sets up the ACME client, and registers the account with Let's Encrypt.
 | 
				
			||||||
- **stop()**: Closes the MongoDB connection and performs any necessary cleanup.
 | 
					- **stop()**: Closes the MongoDB connection and performs any necessary cleanup.
 | 
				
			||||||
- **getCertificateForDomain(domainArg: string)**: Retrieves or obtains a certificate for the specified domain name. If a valid certificate exists in the database, it is returned. Otherwise, a new certificate is requested and stored.
 | 
					- **getCertificateForDomain(domainArg: string, options?: { includeWildcard?: boolean })**: Retrieves or obtains a certificate for the specified domain name. If a valid certificate exists in the database, it is returned. Otherwise, a new certificate is requested and stored. 
 | 
				
			||||||
 | 
					  - By default, only a certificate for the exact domain is requested
 | 
				
			||||||
 | 
					  - Set `includeWildcard: true` to also request a wildcard certificate (requires DNS-01 handler)
 | 
				
			||||||
 | 
					  - When requesting a wildcard directly (e.g., `*.example.com`), only the wildcard certificate is requested
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Handling Domain Matching
 | 
					### Handling Domain Matching
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										178
									
								
								test/test.http01-only.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								test/test.http01-only.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,178 @@
 | 
				
			|||||||
 | 
					import { tap, expect } from '@push.rocks/tapbundle';
 | 
				
			||||||
 | 
					import { SmartAcme, certmanagers } from '../ts/index.js';
 | 
				
			||||||
 | 
					import { Http01MemoryHandler } from '../ts/handlers/Http01MemoryHandler.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Test that HTTP-01 only configuration works without wildcard certificates
 | 
				
			||||||
 | 
					tap.test('HTTP-01 only configuration should work for regular domains', async () => {
 | 
				
			||||||
 | 
					  const memHandler = new Http01MemoryHandler();
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Stub the domain support check to always return true for testing
 | 
				
			||||||
 | 
					  memHandler.checkWetherDomainIsSupported = async () => true;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  const smartAcmeInstance = new SmartAcme({
 | 
				
			||||||
 | 
					    accountEmail: 'test@example.com',
 | 
				
			||||||
 | 
					    certManager: new certmanagers.MemoryCertManager(),
 | 
				
			||||||
 | 
					    environment: 'integration',
 | 
				
			||||||
 | 
					    retryOptions: {},
 | 
				
			||||||
 | 
					    challengeHandlers: [memHandler],
 | 
				
			||||||
 | 
					    challengePriority: ['http-01'],
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Stub the start method to avoid actual ACME connections
 | 
				
			||||||
 | 
					  smartAcmeInstance.start = async () => {
 | 
				
			||||||
 | 
					    smartAcmeInstance.certmatcher = {
 | 
				
			||||||
 | 
					      getCertificateDomainNameByDomainName: (domain: string) => domain.replace('*.', '')
 | 
				
			||||||
 | 
					    } as any;
 | 
				
			||||||
 | 
					    smartAcmeInstance.interestMap = {
 | 
				
			||||||
 | 
					      checkInterest: async () => false,
 | 
				
			||||||
 | 
					      addInterest: async () => ({ interestFullfilled: new Promise(() => {}) } as any)
 | 
				
			||||||
 | 
					    } as any;
 | 
				
			||||||
 | 
					    await smartAcmeInstance.certmanager.init();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  await smartAcmeInstance.start();
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Stub the core certificate methods to avoid actual ACME calls
 | 
				
			||||||
 | 
					  smartAcmeInstance.client = {
 | 
				
			||||||
 | 
					    createOrder: async (orderPayload: any) => {
 | 
				
			||||||
 | 
					      // Verify no wildcard is included in default request
 | 
				
			||||||
 | 
					      const identifiers = orderPayload.identifiers;
 | 
				
			||||||
 | 
					      expect(identifiers.length).toEqual(1);
 | 
				
			||||||
 | 
					      expect(identifiers[0].value).toEqual('example.com');
 | 
				
			||||||
 | 
					      expect(identifiers.find((id: any) => id.value.startsWith('*.'))).toBeUndefined();
 | 
				
			||||||
 | 
					      return { status: 'pending', authorizations: [], finalize: '', certificate: '' };
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    getAuthorizations: async () => [],
 | 
				
			||||||
 | 
					    finalizeOrder: async () => {},
 | 
				
			||||||
 | 
					    getCertificate: async () => '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
 | 
				
			||||||
 | 
					  } as any;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  smartAcmeInstance.retry = async (fn: () => Promise<any>) => fn();
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Mock certmanager methods
 | 
				
			||||||
 | 
					  smartAcmeInstance.certmanager.retrieveCertificate = async () => null;
 | 
				
			||||||
 | 
					  smartAcmeInstance.certmanager.storeCertificate = async (cert: any) => cert;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Request certificate without wildcard
 | 
				
			||||||
 | 
					  const cert = await smartAcmeInstance.getCertificateForDomain('example.com');
 | 
				
			||||||
 | 
					  expect(cert).toBeDefined();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tap.test('should only include wildcard when explicitly requested with DNS-01', async () => {
 | 
				
			||||||
 | 
					  const dnsHandler = {
 | 
				
			||||||
 | 
					    getSupportedTypes: () => ['dns-01'],
 | 
				
			||||||
 | 
					    prepare: async () => {},
 | 
				
			||||||
 | 
					    cleanup: async () => {},
 | 
				
			||||||
 | 
					    checkWetherDomainIsSupported: async () => true,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  const smartAcmeInstance = new SmartAcme({
 | 
				
			||||||
 | 
					    accountEmail: 'test@example.com',
 | 
				
			||||||
 | 
					    certManager: new certmanagers.MemoryCertManager(),
 | 
				
			||||||
 | 
					    environment: 'integration',
 | 
				
			||||||
 | 
					    retryOptions: {},
 | 
				
			||||||
 | 
					    challengeHandlers: [dnsHandler],
 | 
				
			||||||
 | 
					    challengePriority: ['dns-01'],
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Stub the start method to avoid actual ACME connections
 | 
				
			||||||
 | 
					  smartAcmeInstance.start = async () => {
 | 
				
			||||||
 | 
					    smartAcmeInstance.certmatcher = {
 | 
				
			||||||
 | 
					      getCertificateDomainNameByDomainName: (domain: string) => domain.replace('*.', '')
 | 
				
			||||||
 | 
					    } as any;
 | 
				
			||||||
 | 
					    smartAcmeInstance.interestMap = {
 | 
				
			||||||
 | 
					      checkInterest: async () => false,
 | 
				
			||||||
 | 
					      addInterest: async () => ({ interestFullfilled: new Promise(() => {}) } as any)
 | 
				
			||||||
 | 
					    } as any;
 | 
				
			||||||
 | 
					    await smartAcmeInstance.certmanager.init();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  await smartAcmeInstance.start();
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Stub the core certificate methods
 | 
				
			||||||
 | 
					  smartAcmeInstance.client = {
 | 
				
			||||||
 | 
					    createOrder: async (orderPayload: any) => {
 | 
				
			||||||
 | 
					      const identifiers = orderPayload.identifiers;
 | 
				
			||||||
 | 
					      expect(identifiers.length).toEqual(2);
 | 
				
			||||||
 | 
					      expect(identifiers[0].value).toEqual('example.com');
 | 
				
			||||||
 | 
					      expect(identifiers[1].value).toEqual('*.example.com');
 | 
				
			||||||
 | 
					      return { status: 'pending', authorizations: [], finalize: '', certificate: '' };
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    getAuthorizations: async () => [],
 | 
				
			||||||
 | 
					    finalizeOrder: async () => {},
 | 
				
			||||||
 | 
					    getCertificate: async () => '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
 | 
				
			||||||
 | 
					  } as any;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  smartAcmeInstance.retry = async (fn: () => Promise<any>) => fn();
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Mock certmanager methods
 | 
				
			||||||
 | 
					  smartAcmeInstance.certmanager.retrieveCertificate = async () => null;
 | 
				
			||||||
 | 
					  smartAcmeInstance.certmanager.storeCertificate = async (cert: any) => cert;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Request certificate with wildcard
 | 
				
			||||||
 | 
					  const cert = await smartAcmeInstance.getCertificateForDomain('example.com', { includeWildcard: true });
 | 
				
			||||||
 | 
					  expect(cert).toBeDefined();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tap.test('should skip wildcard if requested but no DNS-01 handler available', async () => {
 | 
				
			||||||
 | 
					  const httpHandler = new Http01MemoryHandler();
 | 
				
			||||||
 | 
					  httpHandler.checkWetherDomainIsSupported = async () => true;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  const smartAcmeInstance = new SmartAcme({
 | 
				
			||||||
 | 
					    accountEmail: 'test@example.com',
 | 
				
			||||||
 | 
					    certManager: new certmanagers.MemoryCertManager(),
 | 
				
			||||||
 | 
					    environment: 'integration',
 | 
				
			||||||
 | 
					    retryOptions: {},
 | 
				
			||||||
 | 
					    challengeHandlers: [httpHandler],
 | 
				
			||||||
 | 
					    challengePriority: ['http-01'],
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Stub the start method to avoid actual ACME connections
 | 
				
			||||||
 | 
					  smartAcmeInstance.start = async () => {
 | 
				
			||||||
 | 
					    smartAcmeInstance.certmatcher = {
 | 
				
			||||||
 | 
					      getCertificateDomainNameByDomainName: (domain: string) => domain.replace('*.', '')
 | 
				
			||||||
 | 
					    } as any;
 | 
				
			||||||
 | 
					    smartAcmeInstance.interestMap = {
 | 
				
			||||||
 | 
					      checkInterest: async () => false,
 | 
				
			||||||
 | 
					      addInterest: async () => ({ interestFullfilled: new Promise(() => {}) } as any)
 | 
				
			||||||
 | 
					    } as any;
 | 
				
			||||||
 | 
					    await smartAcmeInstance.certmanager.init();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  await smartAcmeInstance.start();
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Mock logger to capture warning
 | 
				
			||||||
 | 
					  const logSpy = { called: false, message: '' };
 | 
				
			||||||
 | 
					  smartAcmeInstance.logger.log = async (level: string, message: string) => {
 | 
				
			||||||
 | 
					    if (level === 'warn') {
 | 
				
			||||||
 | 
					      logSpy.called = true;
 | 
				
			||||||
 | 
					      logSpy.message = message;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Stub the core certificate methods
 | 
				
			||||||
 | 
					  smartAcmeInstance.client = {
 | 
				
			||||||
 | 
					    createOrder: async (orderPayload: any) => {
 | 
				
			||||||
 | 
					      const identifiers = orderPayload.identifiers;
 | 
				
			||||||
 | 
					      // Should only have regular domain, no wildcard
 | 
				
			||||||
 | 
					      expect(identifiers.length).toEqual(1);
 | 
				
			||||||
 | 
					      expect(identifiers[0].value).toEqual('example.com');
 | 
				
			||||||
 | 
					      return { status: 'pending', authorizations: [], finalize: '', certificate: '' };
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    getAuthorizations: async () => [],
 | 
				
			||||||
 | 
					    finalizeOrder: async () => {},
 | 
				
			||||||
 | 
					    getCertificate: async () => '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
 | 
				
			||||||
 | 
					  } as any;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  smartAcmeInstance.retry = async (fn: () => Promise<any>) => fn();
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Mock certmanager methods
 | 
				
			||||||
 | 
					  smartAcmeInstance.certmanager.retrieveCertificate = async () => null;
 | 
				
			||||||
 | 
					  smartAcmeInstance.certmanager.storeCertificate = async (cert: any) => cert;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Request certificate with wildcard (should be skipped)
 | 
				
			||||||
 | 
					  const cert = await smartAcmeInstance.getCertificateForDomain('example.com', { includeWildcard: true });
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  expect(cert).toBeDefined();
 | 
				
			||||||
 | 
					  expect(logSpy.called).toBeTrue();
 | 
				
			||||||
 | 
					  expect(logSpy.message).toContain('Wildcard certificate requested but no DNS-01 handler available');
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default tap.start();
 | 
				
			||||||
							
								
								
									
										94
									
								
								test/test.wildcard-options.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								test/test.wildcard-options.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
				
			|||||||
 | 
					import { tap, expect } from '@push.rocks/tapbundle';
 | 
				
			||||||
 | 
					import { SmartAcme, certmanagers } from '../ts/index.js';
 | 
				
			||||||
 | 
					import { SmartacmeCert as Cert } from '../ts/smartacme.classes.cert.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Simple test to verify wildcard options are correctly processed
 | 
				
			||||||
 | 
					tap.test('should not include wildcard by default for regular domain', async () => {
 | 
				
			||||||
 | 
					  let orderPayload: any = null;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Override the SmartAcme prototype methods for testing
 | 
				
			||||||
 | 
					  const origGetCert = SmartAcme.prototype.getCertificateForDomain;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Create a minimal test version of getCertificateForDomain
 | 
				
			||||||
 | 
					  SmartAcme.prototype.getCertificateForDomain = async function(
 | 
				
			||||||
 | 
					    domainArg: string,
 | 
				
			||||||
 | 
					    options?: { includeWildcard?: boolean }
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    const certDomainName = domainArg.replace('*.', '');
 | 
				
			||||||
 | 
					    const identifiers = [];
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if (domainArg.startsWith('*.')) {
 | 
				
			||||||
 | 
					      identifiers.push({ type: 'dns', value: domainArg });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      identifiers.push({ type: 'dns', value: certDomainName });
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      if (options?.includeWildcard) {
 | 
				
			||||||
 | 
					        const hasDnsHandler = this.challengeHandlers.some((h) =>
 | 
				
			||||||
 | 
					          h.getSupportedTypes().includes('dns-01')
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (hasDnsHandler) {
 | 
				
			||||||
 | 
					          identifiers.push({ type: 'dns', value: `*.${certDomainName}` });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    orderPayload = { identifiers };
 | 
				
			||||||
 | 
					    return new Cert({ domainName: certDomainName });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    // Create instance with HTTP-01 only
 | 
				
			||||||
 | 
					    const smartAcme = new SmartAcme({
 | 
				
			||||||
 | 
					      accountEmail: 'test@example.com',
 | 
				
			||||||
 | 
					      certManager: new certmanagers.MemoryCertManager(),
 | 
				
			||||||
 | 
					      environment: 'integration',
 | 
				
			||||||
 | 
					      challengeHandlers: [{
 | 
				
			||||||
 | 
					        getSupportedTypes: () => ['http-01'],
 | 
				
			||||||
 | 
					        prepare: async () => {},
 | 
				
			||||||
 | 
					        cleanup: async () => {},
 | 
				
			||||||
 | 
					        checkWetherDomainIsSupported: async () => true,
 | 
				
			||||||
 | 
					      }],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Test 1: Regular domain without wildcard option
 | 
				
			||||||
 | 
					    await smartAcme.getCertificateForDomain('example.com');
 | 
				
			||||||
 | 
					    expect(orderPayload.identifiers.length).toEqual(1);
 | 
				
			||||||
 | 
					    expect(orderPayload.identifiers[0].value).toEqual('example.com');
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Test 2: Regular domain with wildcard option (but no DNS-01 handler)
 | 
				
			||||||
 | 
					    await smartAcme.getCertificateForDomain('example.com', { includeWildcard: true });
 | 
				
			||||||
 | 
					    expect(orderPayload.identifiers.length).toEqual(1);
 | 
				
			||||||
 | 
					    expect(orderPayload.identifiers[0].value).toEqual('example.com');
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Create instance with DNS-01
 | 
				
			||||||
 | 
					    const smartAcmeDns = new SmartAcme({
 | 
				
			||||||
 | 
					      accountEmail: 'test@example.com',
 | 
				
			||||||
 | 
					      certManager: new certmanagers.MemoryCertManager(),
 | 
				
			||||||
 | 
					      environment: 'integration',
 | 
				
			||||||
 | 
					      challengeHandlers: [{
 | 
				
			||||||
 | 
					        getSupportedTypes: () => ['dns-01'],
 | 
				
			||||||
 | 
					        prepare: async () => {},
 | 
				
			||||||
 | 
					        cleanup: async () => {},
 | 
				
			||||||
 | 
					        checkWetherDomainIsSupported: async () => true,
 | 
				
			||||||
 | 
					      }],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Test 3: Regular domain with wildcard option and DNS-01 handler
 | 
				
			||||||
 | 
					    await smartAcmeDns.getCertificateForDomain('example.com', { includeWildcard: true });
 | 
				
			||||||
 | 
					    expect(orderPayload.identifiers.length).toEqual(2);
 | 
				
			||||||
 | 
					    expect(orderPayload.identifiers[0].value).toEqual('example.com');
 | 
				
			||||||
 | 
					    expect(orderPayload.identifiers[1].value).toEqual('*.example.com');
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Test 4: Direct wildcard request
 | 
				
			||||||
 | 
					    await smartAcmeDns.getCertificateForDomain('*.example.com');
 | 
				
			||||||
 | 
					    expect(orderPayload.identifiers.length).toEqual(1);
 | 
				
			||||||
 | 
					    expect(orderPayload.identifiers[0].value).toEqual('*.example.com');
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    // Restore original method
 | 
				
			||||||
 | 
					    SmartAcme.prototype.getCertificateForDomain = origGetCert;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default tap.start();
 | 
				
			||||||
@@ -3,6 +3,6 @@
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
export const commitinfo = {
 | 
					export const commitinfo = {
 | 
				
			||||||
  name: '@push.rocks/smartacme',
 | 
					  name: '@push.rocks/smartacme',
 | 
				
			||||||
  version: '7.3.4',
 | 
					  version: '8.0.0',
 | 
				
			||||||
  description: 'A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.'
 | 
					  description: 'A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.'
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -221,8 +221,12 @@ export class SmartAcme {
 | 
				
			|||||||
   * * retrieve it from the databse and return it
 | 
					   * * retrieve it from the databse and return it
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param domainArg
 | 
					   * @param domainArg
 | 
				
			||||||
 | 
					   * @param options Optional configuration for certificate generation
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  public async getCertificateForDomain(domainArg: string): Promise<SmartacmeCert> {
 | 
					  public async getCertificateForDomain(
 | 
				
			||||||
 | 
					    domainArg: string,
 | 
				
			||||||
 | 
					    options?: { includeWildcard?: boolean }
 | 
				
			||||||
 | 
					  ): Promise<SmartacmeCert> {
 | 
				
			||||||
    // Determine if this is a wildcard request (e.g., '*.example.com').
 | 
					    // Determine if this is a wildcard request (e.g., '*.example.com').
 | 
				
			||||||
    const isWildcardRequest = domainArg.startsWith('*.');
 | 
					    const isWildcardRequest = domainArg.startsWith('*.');
 | 
				
			||||||
    // Determine the base domain for certificate retrieval/issuance.
 | 
					    // Determine the base domain for certificate retrieval/issuance.
 | 
				
			||||||
@@ -259,12 +263,32 @@ export class SmartAcme {
 | 
				
			|||||||
    // lets make sure others get the same interest
 | 
					    // lets make sure others get the same interest
 | 
				
			||||||
    const currentDomainInterst = await this.interestMap.addInterest(certDomainName);
 | 
					    const currentDomainInterst = await this.interestMap.addInterest(certDomainName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Build identifiers array based on request
 | 
				
			||||||
 | 
					    const identifiers = [];
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if (isWildcardRequest) {
 | 
				
			||||||
 | 
					      // If requesting a wildcard directly, only add the wildcard
 | 
				
			||||||
 | 
					      identifiers.push({ type: 'dns', value: `*.${certDomainName}` });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // Add the regular domain
 | 
				
			||||||
 | 
					      identifiers.push({ type: 'dns', value: certDomainName });
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // Only add wildcard if explicitly requested
 | 
				
			||||||
 | 
					      if (options?.includeWildcard) {
 | 
				
			||||||
 | 
					        const hasDnsHandler = this.challengeHandlers.some((h) =>
 | 
				
			||||||
 | 
					          h.getSupportedTypes().includes('dns-01'),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        if (!hasDnsHandler) {
 | 
				
			||||||
 | 
					          this.logger.log('warn', 'Wildcard certificate requested but no DNS-01 handler available. Skipping wildcard.');
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          identifiers.push({ type: 'dns', value: `*.${certDomainName}` });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /* Place new order with retry */
 | 
					    /* Place new order with retry */
 | 
				
			||||||
    const order = await this.retry(() => this.client.createOrder({
 | 
					    const order = await this.retry(() => this.client.createOrder({
 | 
				
			||||||
      identifiers: [
 | 
					      identifiers,
 | 
				
			||||||
        { type: 'dns', value: certDomainName },
 | 
					 | 
				
			||||||
        { type: 'dns', value: `*.${certDomainName}` },
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
    }), 'createOrder');
 | 
					    }), 'createOrder');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /* Get authorizations and select challenges */
 | 
					    /* Get authorizations and select challenges */
 | 
				
			||||||
@@ -359,9 +383,25 @@ export class SmartAcme {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /* Finalize order */
 | 
					    /* Finalize order */
 | 
				
			||||||
 | 
					    const csrDomains = [];
 | 
				
			||||||
 | 
					    let commonName: string;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if (isWildcardRequest) {
 | 
				
			||||||
 | 
					      // For wildcard requests, use wildcard as common name
 | 
				
			||||||
 | 
					      commonName = `*.${certDomainName}`;
 | 
				
			||||||
 | 
					      csrDomains.push(certDomainName); // Add base domain as alt name
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // For regular requests, use base domain as common name
 | 
				
			||||||
 | 
					      commonName = certDomainName;
 | 
				
			||||||
 | 
					      if (options?.includeWildcard && identifiers.some(id => id.value === `*.${certDomainName}`)) {
 | 
				
			||||||
 | 
					        // If wildcard was successfully added, include it as alt name
 | 
				
			||||||
 | 
					        csrDomains.push(`*.${certDomainName}`);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    const [key, csr] = await plugins.acme.forge.createCsr({
 | 
					    const [key, csr] = await plugins.acme.forge.createCsr({
 | 
				
			||||||
      commonName: `*.${certDomainName}`,
 | 
					      commonName,
 | 
				
			||||||
      altNames: [certDomainName],
 | 
					      altNames: csrDomains,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await this.retry(() => this.client.finalizeOrder(order, csr), 'finalizeOrder');
 | 
					    await this.retry(() => this.client.finalizeOrder(order, csr), 'finalizeOrder');
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user