Compare commits

...

10 Commits

24 changed files with 5396 additions and 3079 deletions

View File

@ -6,8 +6,8 @@ on:
- '**' - '**'
env: env:
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci IMAGE: code.foss.global/host.today/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}} NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}} NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}} NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
@ -26,7 +26,7 @@ jobs:
- name: Install pnpm and npmci - name: Install pnpm and npmci
run: | run: |
pnpm install -g pnpm pnpm install -g pnpm
pnpm install -g @shipzone/npmci pnpm install -g @ship.zone/npmci
- name: Run npm prepare - name: Run npm prepare
run: npmci npm prepare run: npmci npm prepare

View File

@ -6,8 +6,8 @@ on:
- '*' - '*'
env: env:
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci IMAGE: code.foss.global/host.today/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}} NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}} NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}} NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
@ -26,7 +26,7 @@ jobs:
- name: Prepare - name: Prepare
run: | run: |
pnpm install -g pnpm pnpm install -g pnpm
pnpm install -g @shipzone/npmci pnpm install -g @ship.zone/npmci
npmci npm prepare npmci npm prepare
- name: Audit production dependencies - name: Audit production dependencies
@ -54,7 +54,7 @@ jobs:
- name: Prepare - name: Prepare
run: | run: |
pnpm install -g pnpm pnpm install -g pnpm
pnpm install -g @shipzone/npmci pnpm install -g @ship.zone/npmci
npmci npm prepare npmci npm prepare
- name: Test stable - name: Test stable
@ -82,7 +82,7 @@ jobs:
- name: Prepare - name: Prepare
run: | run: |
pnpm install -g pnpm pnpm install -g pnpm
pnpm install -g @shipzone/npmci pnpm install -g @ship.zone/npmci
npmci npm prepare npmci npm prepare
- name: Release - name: Release
@ -104,7 +104,7 @@ jobs:
- name: Prepare - name: Prepare
run: | run: |
pnpm install -g pnpm pnpm install -g pnpm
pnpm install -g @shipzone/npmci pnpm install -g @ship.zone/npmci
npmci npm prepare npmci npm prepare
- name: Code quality - name: Code quality

3
.gitignore vendored
View File

@ -3,7 +3,6 @@
# artifacts # artifacts
coverage/ coverage/
public/ public/
pages/
# installs # installs
node_modules/ node_modules/
@ -17,4 +16,4 @@ node_modules/
dist/ dist/
dist_*/ dist_*/
# custom #------# custom

View File

@ -1,4 +1,4 @@
Copyright (C) 2016, Lossless GmbH Copyright (C) 2016, Task Venture Capital GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in

145
changelog.md Normal file
View File

@ -0,0 +1,145 @@
# Changelog
## 2025-04-27 - 6.1.0 - feat(readme)
Update documentation with detailed built-in challenge handlers and custom handler examples
- Expanded readme to include sections on Dns01Handler and Http01Handler usage
- Added examples for creating and registering custom ACME challenge handlers
- Improved clarity of ACME certificate management instructions using SmartAcme
## 2025-04-27 - 6.0.1 - fix(readme)
Remove extraneous code fence markers from license section in readme
- Removed unnecessary triple backticks wrapping the license information
- Improved clarity of the license section in the documentation
## 2025-04-27 - 6.0.0 - BREAKING CHANGE(SmartAcme)
Refactor challenge handling by removing legacy setChallenge/removeChallenge in favor of pluggable challengeHandlers and update documentation and tests accordingly
- Removed legacy challenge methods and introduced new 'challengeHandlers' and 'challengePriority' options
- Updated readme examples to demonstrate usage with DNS-01 (and HTTP-01) handlers
- Refactored internal SmartAcme flow to select and process challenges via the new handler interface
- Adjusted tests (including integration tests) to align with the updated challenge handling mechanism
## 2025-04-27 - 5.1.0 - feat(smartacme)
Implement exponential backoff retry logic and graceful shutdown handling in SmartAcme; update acme-client dependency to v5.4.0
- Added retry helper with exponential backoff for ACME client operations
- Introduced retryOptions in ISmartAcmeOptions for configurable retry parameters
- Enhanced graceful shutdown handling by cleaning up pending DNS challenges on signal
- Updated acme-client dependency from v4.2.5 to v5.4.0
## 2025-04-26 - 5.0.1 - fix(build)
Update CI workflows, bump dependency versions, and refine import and TypeScript configuration
- Changed CI workflow image and npmci package from '@shipzone/npmci' to '@ship.zone/npmci', and updated repository URLs
- Bumped several dependency versions in package.json (e.g. @api.global/typedserver, @push.rocks/lik, @push.rocks/smartdata, @push.rocks/smartdns, @tsclass/tsclass) to newer releases
- Adjusted smartdns import to use the smartdnsClient module for proper module resolution
- Updated tsconfig.json to add emitDecoratorMetadata and baseUrl settings
- Minor markdown and formatting tweaks in readme and gitignore files, and slight improvements in test async handling
## 2024-06-16 - 5.0.0 - No significant changes
This release contains no userfacing changes.
## 2024-06-16 - 4.0.8 - Structure and configuration updates
- BREAKING CHANGE(structure): renamed classes to avoid confusion
- update description
- update tsconfig
- update npmextra.json: githost
## 2024-01-28 - 4.0.74.0.6 - Internal fixes and updates
- A series of releases with routine bug fixes and maintenance updates.
## 2023-07-21 - 4.0.54.0.4 - Internal fixes and updates
- Multiple releases addressing internal issues and maintenance improvements.
## 2023-07-10 - 4.0.3 - Organizational changes
- switch to new org scheme
## 2022-09-27 - 4.0.04.0.2 - Internal fixes and updates
- Routine maintenance and internal bug fixes.
## 2022-09-27 - 3.0.15 - Breaking changes
- BREAKING CHANGE(core): update
## 2021-01-22 - 3.0.93.0.14 - Internal fixes and updates
- A range of releases focused on routine internal updates.
## 2020-11-18 - 3.0.03.0.8 - Internal fixes and updates
- Routine maintenance and internal bug fixes.
## 2020-02-10 - 2.1.2 - Breaking changes
- BREAKING CHANGE(core): streamline scope to certificate retrieval using dns challenge
## 2020-02-10 - 2.1.02.1.1 - Internal fixes and updates
- Routine fixes and updates.
## 2019-02-06 - 2.0.36 - New feature
- feat(Cert): now has validity check
## 2019-01-18 - 2.0.22.0.35 - Internal fixes and updates
- Routine internal updates and maintenance.
## 2018-10-07 - 2.0.02.0.1 - Internal fixes and updates
- Routine internal updates and maintenance.
## 2018-10-07 - 1.1.4 - Breaking changes
- BREAKING CHANGE(scope): change to @pushrocks
## 2018-08-12 - 1.1.1 - NPM publishing fix
- fix(npm publishing): update
## 2018-08-11 - 1.1.0 - Certificate issuance update
- fix(core): now creating certs all right
## 2018-08-11 - 1.0.11 - Feature update
- feat(swaitch to acme-v2): switch to letsencrypt v2
## 2017-04-28 - 1.0.10 - CI improvements
- add updated ci config
## 2017-04-28 - 1.0.9 - Standards update
- update to latest standards
## 2017-01-27 - 1.0.8 - Basic functionality
- basic functionality
## 2017-01-25 - 1.0.7 - Response and validation improvements
- now getting a valid response
- update validation
- improve README
## 2017-01-15 - 1.0.6 - Async and documentation improvements
- improve README
- add async checkDNS
## 2017-01-15 - 1.0.5 - Standards and process updates
- update to new standards
- now has working requestValidation method
- fix som things
- start better segregation of concerns
- start with certificate signing process
## 2017-01-01 - 1.0.4 - Certificate acquisition improvements
- now getting certificates
- can now agree to TOS
- remove test keys
## 2017-01-01 - 1.0.3 - NPM extra configuration
- add npmextra.json
## 2017-01-01 - 1.0.2 - README and integration update
- add better readme
- switch to rawacme for more basic letsencrypt access
## 2016-11-17 - 1.0.1 - Promise fix
- fix promise
## 2016-11-17 - 1.0.0 - Major initial release changes
- remove superflouous key creation
- switch to acme core
- prepare switch to leacmecore
- improve upon keyCreation
- update to use more promises
- add README
- first version

View File

@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartacme", "name": "@push.rocks/smartacme",
"version": "5.0.0", "version": "6.1.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",
@ -32,35 +32,35 @@
"certificate renewal", "certificate renewal",
"wildcard certificates" "wildcard certificates"
], ],
"author": "Lossless GmbH", "author": "Task Venture Capital GmbH",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
"url": "https://gitlab.com/umbrellazone/smartacme/issues" "url": "https://code.foss.global/push.rocks/smartacme/issues"
}, },
"homepage": "https://code.foss.global/push.rocks/smartacme", "homepage": "https://code.foss.global/push.rocks/smartacme#readme",
"dependencies": { "dependencies": {
"@api.global/typedserver": "^3.0.50", "@api.global/typedserver": "^3.0.74",
"@push.rocks/lik": "^6.0.15", "@push.rocks/lik": "^6.2.2",
"@push.rocks/smartdata": "^5.2.4", "@push.rocks/smartdata": "^5.15.1",
"@push.rocks/smartdelay": "^3.0.5", "@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartdns": "^5.0.2", "@push.rocks/smartdns": "^6.2.2",
"@push.rocks/smartlog": "^3.0.7", "@push.rocks/smartlog": "^3.0.7",
"@push.rocks/smartpromise": "^4.0.3", "@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^2.0.22", "@push.rocks/smartrequest": "^2.1.0",
"@push.rocks/smartstring": "^4.0.15", "@push.rocks/smartstring": "^4.0.15",
"@push.rocks/smarttime": "^4.0.6", "@push.rocks/smarttime": "^4.1.1",
"@push.rocks/smartunique": "^3.0.9", "@push.rocks/smartunique": "^3.0.9",
"@tsclass/tsclass": "^4.0.58", "@tsclass/tsclass": "^9.0.0",
"acme-client": "^4.2.5" "acme-client": "^5.4.0"
}, },
"devDependencies": { "devDependencies": {
"@apiclient.xyz/cloudflare": "^6.0.3", "@apiclient.xyz/cloudflare": "^6.3.2",
"@git.zone/tsbuild": "^2.1.80", "@git.zone/tsbuild": "^2.3.2",
"@git.zone/tsrun": "^1.2.44", "@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^1.0.90", "@git.zone/tstest": "^1.0.96",
"@push.rocks/qenv": "^6.0.5", "@push.rocks/qenv": "^6.1.0",
"@push.rocks/tapbundle": "^5.0.23", "@push.rocks/tapbundle": "^5.6.3",
"@types/node": "^20.14.2" "@types/node": "^22.15.2"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",
@ -76,5 +76,9 @@
], ],
"browserslist": [ "browserslist": [
"last 1 chrome versions" "last 1 chrome versions"
] ],
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6",
"pnpm": {
"overrides": {}
}
} }

7536
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

140
readme.md
View File

@ -1,4 +1,3 @@
```markdown
# @push.rocks/smartacme # @push.rocks/smartacme
A TypeScript-based ACME client with an easy yet powerful interface for LetsEncrypt certificate management. A TypeScript-based ACME client with an easy yet powerful interface for LetsEncrypt certificate management.
@ -40,35 +39,40 @@ Ensure your project includes the necessary TypeScript configuration and dependen
### Creating a SmartAcme Instance ### Creating a SmartAcme Instance
Start by importing the `SmartAcme` class from the `@push.rocks/smartacme` package. You'll also need to import or define interfaces for your setup options: Start by importing the `SmartAcme` class and any built-in handlers you plan to use. For example, to use DNS-01 via Cloudflare:
```typescript ```typescript
import { SmartAcme } from '@push.rocks/smartacme'; import { SmartAcme } from '@push.rocks/smartacme';
import * as cloudflare from '@apiclient.xyz/cloudflare';
import { Dns01Handler } from '@push.rocks/smartacme/ts/handlers/Dns01Handler.js';
// Create a Cloudflare account client with your API token
const cfAccount = new cloudflare.CloudflareAccount('YOUR_CF_TOKEN');
// Instantiate SmartAcme with one or more ACME challenge handlers
const smartAcmeInstance = new SmartAcme({ const smartAcmeInstance = new SmartAcme({
accountEmail: 'youremail@example.com', // Email used for Let's Encrypt registration and recovery accountEmail: 'youremail@example.com',
accountPrivateKey: null, // Private key for the account (optional, if not provided it will be generated)
mongoDescriptor: { mongoDescriptor: {
mongoDbUrl: 'mongodb://yourmongoURL', mongoDbUrl: 'mongodb://yourmongoURL',
mongoDbName: 'yourDbName', mongoDbName: 'yourDbName',
mongoDbPass: 'yourDbPassword', mongoDbPass: 'yourDbPassword',
}, },
removeChallenge: async (dnsChallenge) => { environment: 'integration', // 'production' to request real certificates
// Implement logic here to remove DNS challenge records retryOptions: {}, // optional retry/backoff settings
}, challengeHandlers: [
setChallenge: async (dnsChallenge) => { new Dns01Handler(cfAccount),
// Implement logic here to create DNS challenge records // you can add more handlers, e.g. Http01Handler
}, ],
environment: 'integration', // Use 'production' for actual certificates challengePriority: ['dns-01'], // optional ordering of challenge types
}); });
``` ```
### Initializing SmartAcme ### Initializing SmartAcme
Before proceeding to request certificates, initialize your SmartAcme instance: Before proceeding to request certificates, start your SmartAcme instance:
```typescript ```typescript
await smartAcmeInstance.init(); await smartAcmeInstance.start();
``` ```
### Obtaining a Certificate for a Domain ### Obtaining a Certificate for a Domain
@ -83,34 +87,7 @@ console.log('Certificate:', myCert);
### Automating DNS Challenges ### Automating DNS Challenges
Part of the ACME protocol involves responding to DNS challenges issued by the certificate authority to prove control over a domain. Implement the `setChallenge` and `removeChallenge` functions in your SmartAcme configuration to automate this process. These functions receive a `dnsChallenge` argument containing details needed to create or remove the necessary DNS records. SmartAcme uses pluggable ACME challenge handlers (see built-in handlers below) to automate domain validation. You configure handlers via the `challengeHandlers` array when creating the instance, and SmartAcme will invoke each handlers `prepare`, optional `verify`, and `cleanup` methods during the ACME order flow.
```typescript
import * as cloudflare from '@apiclient.xyz/cloudflare';
import { Qenv } from '@push.rocks/qenv';
const testQenv = new Qenv('./', './.nogit/');
const testCloudflare = new cloudflare.CloudflareAccount(testQenv.getEnvVarOnDemand('CF_TOKEN'));
const smartAcmeInstance = new SmartAcme({
accountEmail: 'domains@example.com',
accountPrivateKey: null,
mongoDescriptor: {
mongoDbName: testQenv.getEnvVarRequired('MONGODB_DATABASE'),
mongoDbPass: testQenv.getEnvVarRequired('MONGODB_PASSWORD'),
mongoDbUrl: testQenv.getEnvVarRequired('MONGODB_URL'),
},
removeChallenge: async (dnsChallenge) => {
testCloudflare.convenience.acmeRemoveDnsChallenge(dnsChallenge);
},
setChallenge: async (dnsChallenge) => {
testCloudflare.convenience.acmeSetDnsChallenge(dnsChallenge);
},
environment: 'integration',
});
await smartAcmeInstance.init();
```
### Managing Certificates ### Managing Certificates
@ -130,7 +107,7 @@ When creating an instance of `SmartAcme`, you can specify an `environment` optio
### Complete Example ### Complete Example
Below is a complete example demonstrating how to use `@push.rocks/smartacme` to obtain and manage an ACME certificate with Let's Encrypt: Below is a complete example demonstrating how to use `@push.rocks/smartacme` to obtain and manage an ACME certificate with Let's Encrypt using a DNS-01 handler:
```typescript ```typescript
import { SmartAcme } from '@push.rocks/smartacme'; import { SmartAcme } from '@push.rocks/smartacme';
@ -143,22 +120,16 @@ const cloudflareAccount = new cloudflare.CloudflareAccount(qenv.getEnvVarOnDeman
async function main() { async function main() {
const smartAcmeInstance = new SmartAcme({ const smartAcmeInstance = new SmartAcme({
accountEmail: 'youremail@example.com', accountEmail: 'youremail@example.com',
accountPrivateKey: null,
mongoDescriptor: { mongoDescriptor: {
mongoDbUrl: qenv.getEnvVarRequired('MONGODB_URL'), mongoDbUrl: qenv.getEnvVarRequired('MONGODB_URL'),
mongoDbName: qenv.getEnvVarRequired('MONGODB_DATABASE'), mongoDbName: qenv.getEnvVarRequired('MONGODB_DATABASE'),
mongoDbPass: qenv.getEnvVarRequired('MONGODB_PASSWORD'), mongoDbPass: qenv.getEnvVarRequired('MONGODB_PASSWORD'),
}, },
setChallenge: async (dnsChallenge) => {
await cloudflareAccount.convenience.acmeSetDnsChallenge(dnsChallenge);
},
removeChallenge: async (dnsChallenge) => {
await cloudflareAccount.convenience.acmeRemoveDnsChallenge(dnsChallenge);
},
environment: 'integration', environment: 'integration',
challengeHandlers: [ new Dns01Handler(cloudflareAccount) ],
}); });
await smartAcmeInstance.init(); await smartAcmeInstance.start();
const myDomain = 'example.com'; const myDomain = 'example.com';
const myCert = await smartAcmeInstance.getCertificateForDomain(myDomain); const myCert = await smartAcmeInstance.getCertificateForDomain(myDomain);
@ -170,6 +141,74 @@ async function main() {
main().catch(console.error); main().catch(console.error);
``` ```
## Built-in Challenge Handlers
This module includes two out-of-the-box ACME challenge handlers:
- **Dns01Handler**
- Uses a Cloudflare account (from `@apiclient.xyz/cloudflare`) and Smartdns client to set and remove DNS TXT records, then wait for propagation.
- Import path:
```typescript
import { Dns01Handler } from '@push.rocks/smartacme/ts/handlers/Dns01Handler.js';
```
- Example:
```typescript
import * as cloudflare from '@apiclient.xyz/cloudflare';
const cfAccount = new cloudflare.CloudflareAccount('CF_TOKEN');
const dnsHandler = new Dns01Handler(cfAccount);
```
- **Http01Handler**
- Writes ACME HTTP-01 challenge files under a file-system webroot (`/.well-known/acme-challenge/`), and removes them on cleanup.
- Import path:
```typescript
import { Http01Handler } from '@push.rocks/smartacme/ts/handlers/Http01Handler.js';
```
- Example:
```typescript
const httpHandler = new Http01Handler({ webroot: '/var/www/html' });
```
Both handlers implement the `IChallengeHandler<T>` interface and can be combined in the `challengeHandlers` array.
## Creating Custom Handlers
To support additional challenge types or custom validation flows, implement the `IChallengeHandler<T>` interface:
```typescript
import type { IChallengeHandler } from '@push.rocks/smartacme/ts/handlers/IChallengeHandler.js';
// Define your custom challenge payload type
interface MyChallenge { type: string; /* ... */ }
class MyCustomHandler implements IChallengeHandler<MyChallenge> {
getSupportedTypes(): string[] {
return ['my-01'];
}
// Prepare the challenge (set DNS records, start servers, etc.)
async prepare(ch: MyChallenge): Promise<void> {
// preparation logic
}
// Optional verify step after prepare
async verify?(ch: MyChallenge): Promise<void> {
// verification logic
}
// Cleanup after challenge (remove records, stop servers)
async cleanup(ch: MyChallenge): Promise<void> {
// cleanup logic
}
}
// Then register your handler:
const customInstance = new SmartAcme({
/* other options */,
challengeHandlers: [ new MyCustomHandler() ],
challengePriority: ['my-01'],
});
In this example, `Qenv` is used to manage environment variables, and `cloudflare` library is used to handle DNS challenges required by Let's Encrypt ACME protocol. The `setChallenge` and `removeChallenge` methods are essential for automating the DNS challenge process, which is a key part of domain validation. In this example, `Qenv` is used to manage environment variables, and `cloudflare` library is used to handle DNS challenges required by Let's Encrypt ACME protocol. The `setChallenge` and `removeChallenge` methods are essential for automating the DNS challenge process, which is a key part of domain validation.
## Additional Details ## Additional Details
@ -265,7 +304,6 @@ tap.start();
This comprehensive guide ensures you can set up, manage, and test ACME certificates efficiently and effectively using `@push.rocks/smartacme`. This comprehensive guide ensures you can set up, manage, and test ACME certificates efficiently and effectively using `@push.rocks/smartacme`.
--- ---
```
## License and Legal Information ## License and Legal Information

21
test/test.certmatcher.ts Normal file
View File

@ -0,0 +1,21 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { SmartacmeCertMatcher } from '../ts/smartacme.classes.certmatcher.js';
tap.test('should match 2-level domain', async () => {
const matcher = new SmartacmeCertMatcher();
expect(matcher.getCertificateDomainNameByDomainName('example.com')).toEqual('example.com');
});
tap.test('should match 3-level domain', async () => {
const matcher = new SmartacmeCertMatcher();
expect(matcher.getCertificateDomainNameByDomainName('subdomain.example.com')).toEqual('example.com');
});
tap.test('should return undefined for deeper domain', async () => {
const matcher = new SmartacmeCertMatcher();
// domain with 4 or more levels
const result = matcher.getCertificateDomainNameByDomainName('a.b.example.com');
expect(result).toEqual(undefined);
});
export default tap.start();

View File

@ -0,0 +1,38 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { Dns01Handler } from '../ts/handlers/Dns01Handler.js';
tap.test('Dns01Handler prepare and cleanup calls Cloudflare and DNS functions', async () => {
let setCalled = false;
let removeCalled = false;
// fake Cloudflare API
const fakeCF: any = {
convenience: {
acmeSetDnsChallenge: async (ch: any) => {
setCalled = true;
expect(ch).toHaveProperty('hostName');
expect(ch).toHaveProperty('challenge');
},
acmeRemoveDnsChallenge: async (ch: any) => {
removeCalled = true;
expect(ch).toHaveProperty('hostName');
expect(ch).toHaveProperty('challenge');
},
},
};
// fake DNS checker
const fakeDNS: any = {
checkUntilAvailable: async (host: string, rr: string, val: string, count: number, interval: number) => {
expect(host).toEqual('test.host');
expect(rr).toEqual('TXT');
expect(val).toEqual('token');
},
};
const handler = new Dns01Handler(fakeCF, fakeDNS);
const input = { hostName: 'test.host', challenge: 'token' };
await handler.prepare(input);
expect(setCalled).toEqual(true);
await handler.cleanup(input);
expect(removeCalled).toEqual(true);
});
export default tap.start();

View File

@ -0,0 +1,26 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { Http01Handler } from '../ts/handlers/Http01Handler.js';
import { promises as fs } from 'fs';
import * as path from 'path';
import os from 'os';
tap.test('Http01Handler writes challenge file and removes it on cleanup', async () => {
// create temporary webroot directory
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'http01-'));
const handler = new Http01Handler({ webroot: tmpDir });
const token = 'testtoken';
const keyAuth = 'keyAuthValue';
const webPath = `/.well-known/acme-challenge/${token}`;
const input = { type: 'http-01', token, keyAuthorization: keyAuth, webPath };
// prepare should write the file
await handler.prepare(input);
const filePath = path.join(tmpDir, webPath);
const content = await fs.readFile(filePath, 'utf8');
expect(content).toEqual(keyAuth);
// cleanup should remove the file
await handler.cleanup(input);
const exists = await fs.stat(filePath).then(() => true).catch(() => false);
expect(exists).toEqual(false);
});
export default tap.start();

View File

@ -0,0 +1,47 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { Qenv } from '@push.rocks/qenv';
import * as cloudflare from '@apiclient.xyz/cloudflare';
import { SmartAcme } from '../ts/index.js';
import { Dns01Handler } from '../ts/handlers/Dns01Handler.js';
// Load environment variables for credentials (stored under .nogit/)
const testQenv = new Qenv('./', './.nogit/');
// Cloudflare API token for DNS-01 challenge (must be set in .nogit/ or env)
const cfToken = (await testQenv.getEnvVarOnDemand('CF_TOKEN'))!;
const cfAccount = new cloudflare.CloudflareAccount(cfToken);
// MongoDB connection settings for certificate storage (must be set in .nogit/ or env)
const mongoDbName = (await testQenv.getEnvVarOnDemand('MONGODB_DATABASE'))!;
const mongoDbPass = (await testQenv.getEnvVarOnDemand('MONGODB_PASSWORD'))!;
const mongoDbUrl = (await testQenv.getEnvVarOnDemand('MONGODB_URL'))!;
let smartAcmeInstance: SmartAcme;
tap.test('create SmartAcme instance with DNS-01 handler and start', async () => {
smartAcmeInstance = new SmartAcme({
accountEmail: 'domains@lossless.org',
mongoDescriptor: { mongoDbName, mongoDbPass, mongoDbUrl },
environment: 'integration',
retryOptions: {},
challengeHandlers: [new Dns01Handler(cfAccount)],
challengePriority: ['dns-01'],
});
await smartAcmeInstance.start();
expect(smartAcmeInstance).toBeInstanceOf(SmartAcme);
});
tap.test('get a domain certificate via DNS-01 challenge', async () => {
// Replace 'bleu.de' with your test domain if different
const domain = 'bleu.de';
const cert = await smartAcmeInstance.getCertificateForDomain(domain);
expect(cert).toHaveProperty('domainName');
expect(cert.domainName).toEqual(domain);
expect(cert).toHaveProperty('publicKey');
expect(typeof cert.publicKey).toEqual('string');
expect(cert.publicKey.length).toBeGreaterThan(0);
});
tap.test('stop SmartAcme instance', async () => {
await smartAcmeInstance.stop();
});
export default tap.start();

32
test/test.smartacme.ts Normal file
View File

@ -0,0 +1,32 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { SmartAcme } from '../ts/index.js';
import type { IChallengeHandler } from '../ts/handlers/IChallengeHandler.js';
// Dummy handler for testing
class DummyHandler implements IChallengeHandler<any> {
getSupportedTypes(): string[] { return ['dns-01']; }
async prepare(_: any): Promise<void> { /* no-op */ }
async cleanup(_: any): Promise<void> { /* no-op */ }
}
tap.test('constructor throws without challengeHandlers', async () => {
expect(() => new SmartAcme({
accountEmail: 'test@example.com',
mongoDescriptor: { mongoDbName: 'db', mongoDbPass: 'pass', mongoDbUrl: 'url' },
environment: 'integration',
retryOptions: {},
} as any)).toThrow();
});
tap.test('constructor accepts valid challengeHandlers', async () => {
const sa = new SmartAcme({
accountEmail: 'test@example.com',
mongoDescriptor: { mongoDbName: 'db', mongoDbPass: 'pass', mongoDbUrl: 'url' },
environment: 'integration',
retryOptions: {},
challengeHandlers: [new DummyHandler()],
});
expect(sa).toBeInstanceOf(SmartAcme);
});
export default tap.start();

View File

@ -1,48 +0,0 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { Qenv } from '@push.rocks/qenv';
import * as cloudflare from '@apiclient.xyz/cloudflare';
const testQenv = new Qenv('./', './.nogit/');
const testCloudflare = new cloudflare.CloudflareAccount(testQenv.getEnvVarOnDemand('CF_TOKEN'));
import * as smartacme from '../ts/index.js';
let smartAcmeInstance: smartacme.SmartAcme;
tap.test('should create a valid instance of SmartAcme', async () => {
smartAcmeInstance = new smartacme.SmartAcme({
accountEmail: 'domains@lossless.org',
accountPrivateKey: null,
mongoDescriptor: {
mongoDbName: testQenv.getEnvVarRequired('MONGODB_DATABASE'),
mongoDbPass: testQenv.getEnvVarRequired('MONGODB_PASSWORD'),
mongoDbUrl: testQenv.getEnvVarRequired('MONGODB_URL'),
},
removeChallenge: async (dnsChallenge) => {
testCloudflare.convenience.acmeRemoveDnsChallenge(dnsChallenge);
},
setChallenge: async (dnsChallenge) => {
testCloudflare.convenience.acmeSetDnsChallenge(dnsChallenge);
},
environment: 'integration',
});
await smartAcmeInstance.start();
});
tap.test('should get a domain certificate', async () => {
const certificate = await smartAcmeInstance.getCertificateForDomain('bleu.de');
console.log(certificate);
});
tap.test('certmatcher should correctly match domains', async () => {
const certMatcherMod = await import('../ts/smartacme.classes.certmatcher.js');
const certMatcher = new certMatcherMod.SmartacmeCertMatcher();
const matchedCert = certMatcher.getCertificateDomainNameByDomainName('level3.level2.level1');
expect(matchedCert).toEqual('level2.level1');
});
tap.test('should stop correctly', async () => {
await smartAcmeInstance.stop();
});
tap.start();

View File

@ -1,8 +1,8 @@
/** /**
* autocreated commitinfo by @pushrocks/commitinfo * autocreated commitinfo by @push.rocks/commitinfo
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartacme', name: '@push.rocks/smartacme',
version: '5.0.0', version: '6.1.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.'
} }

View File

@ -0,0 +1,40 @@
import * as plugins from '../smartacme.plugins.js';
import type { IChallengeHandler } from './IChallengeHandler.js';
/**
* DNS-01 challenge handler using CloudflareAccount and Smartdns.
*/
export class Dns01Handler implements IChallengeHandler<plugins.tsclass.network.IDnsChallenge> {
private cf: any;
private smartdns: plugins.smartdnsClient.Smartdns;
constructor(
cloudflareAccount: any,
smartdnsInstance?: plugins.smartdnsClient.Smartdns,
) {
this.cf = cloudflareAccount;
this.smartdns = smartdnsInstance ?? new plugins.smartdnsClient.Smartdns({});
}
public getSupportedTypes(): string[] {
return ['dns-01'];
}
public async prepare(ch: plugins.tsclass.network.IDnsChallenge): Promise<void> {
// set DNS TXT record
await this.cf.convenience.acmeSetDnsChallenge(ch);
// wait for DNS propagation
await this.smartdns.checkUntilAvailable(
ch.hostName,
'TXT',
ch.challenge,
100,
5000,
);
}
public async cleanup(ch: plugins.tsclass.network.IDnsChallenge): Promise<void> {
// remove DNS TXT record
await this.cf.convenience.acmeRemoveDnsChallenge(ch);
}
}

View File

@ -0,0 +1,54 @@
import { promises as fs } from 'fs';
import * as path from 'path';
import type { IChallengeHandler } from './IChallengeHandler.js';
/**
* HTTP-01 ACME challenge handler using file-system webroot.
* Writes and removes the challenge file under <webroot>/.well-known/acme-challenge/.
*/
export interface Http01HandlerOptions {
/**
* Directory that serves HTTP requests for /.well-known/acme-challenge
*/
webroot: string;
}
export class Http01Handler implements IChallengeHandler<{
type: string;
token: string;
keyAuthorization: string;
webPath: string;
}> {
private webroot: string;
constructor(options: Http01HandlerOptions) {
this.webroot = options.webroot;
}
public getSupportedTypes(): string[] {
return ['http-01'];
}
public async prepare(ch: { token: string; keyAuthorization: string; webPath: string }): Promise<void> {
const relWebPath = ch.webPath.replace(/^\/+/, '');
const filePath = path.join(this.webroot, relWebPath);
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(filePath, ch.keyAuthorization, 'utf8');
}
public async verify(ch: { webPath: string; keyAuthorization: string }): Promise<void> {
// Optional: implement HTTP polling if desired
return;
}
public async cleanup(ch: { token: string; webPath: string }): Promise<void> {
const relWebPath = ch.webPath.replace(/^\/+/, '');
const filePath = path.join(this.webroot, relWebPath);
try {
await fs.unlink(filePath);
} catch {
// ignore missing file
}
}
}

View File

@ -0,0 +1,22 @@
/**
* Pluggable interface for ACME challenge handlers.
* Supports DNS-01, HTTP-01, TLS-ALPN-01, or custom challenge types.
*/
export interface IChallengeHandler<T> {
/**
* ACME challenge types this handler supports (e.g. ['dns-01']).
*/
getSupportedTypes(): string[];
/**
* Prepare the challenge: set DNS record, start HTTP/TLS server, etc.
*/
prepare(ch: T): Promise<void>;
/**
* Optional extra verify step (HTTP GET, ALPN handshake).
*/
verify?(ch: T): Promise<void>;
/**
* Clean up resources: remove DNS record, stop server.
*/
cleanup(ch: T): Promise<void>;
}

4
ts/handlers/index.ts Normal file
View File

@ -0,0 +1,4 @@
export type { IChallengeHandler } from './IChallengeHandler.js';
// Removed legacy handler adapter
export { Dns01Handler } from './Dns01Handler.js';
export { Http01Handler } from './Http01Handler.js';

View File

@ -22,7 +22,7 @@ export class SmartacmeCertManager {
smartAcmeArg: SmartAcme, smartAcmeArg: SmartAcme,
optionsArg: { optionsArg: {
mongoDescriptor: plugins.smartdata.IMongoDescriptor; mongoDescriptor: plugins.smartdata.IMongoDescriptor;
} },
) { ) {
this.mongoDescriptor = optionsArg.mongoDescriptor; this.mongoDescriptor = optionsArg.mongoDescriptor;
} }

View File

@ -11,9 +11,30 @@ export interface ISmartAcmeOptions {
accountPrivateKey?: string; accountPrivateKey?: string;
accountEmail: string; accountEmail: string;
mongoDescriptor: plugins.smartdata.IMongoDescriptor; mongoDescriptor: plugins.smartdata.IMongoDescriptor;
setChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>; // Removed legacy setChallenge/removeChallenge in favor of `challengeHandlers`
removeChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
environment: 'production' | 'integration'; environment: 'production' | 'integration';
/**
* Optional retry/backoff configuration for transient failures
*/
retryOptions?: {
/** number of retry attempts */
retries?: number;
/** backoff multiplier */
factor?: number;
/** initial delay in milliseconds */
minTimeoutMs?: number;
/** maximum delay cap in milliseconds */
maxTimeoutMs?: number;
};
/**
* Pluggable ACME challenge handlers (DNS-01, HTTP-01, TLS-ALPN-01, etc.)
*/
challengeHandlers?: plugins.handlers.IChallengeHandler<any>[];
/**
* Order of challenge types to try (e.g. ['http-01','dns-01']).
* Defaults to ['dns-01'] or first supported type from handlers.
*/
challengePriority?: string[];
} }
/** /**
@ -30,24 +51,50 @@ export class SmartAcme {
private options: ISmartAcmeOptions; private options: ISmartAcmeOptions;
// the acme client // the acme client
private client: any; private client: plugins.acme.Client;
private smartdns = new plugins.smartdns.Smartdns({}); private smartdns = new plugins.smartdnsClient.Smartdns({});
public logger: plugins.smartlog.Smartlog; public logger: plugins.smartlog.Smartlog;
// the account private key // the account private key
private privateKey: string; private privateKey: string;
// challenge fullfillment
private setChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
private removeChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
// certmanager // certmanager
private certmanager: SmartacmeCertManager; private certmanager: SmartacmeCertManager;
private certmatcher: SmartacmeCertMatcher; private certmatcher: SmartacmeCertMatcher;
// retry/backoff configuration (resolved with defaults)
private retryOptions: { retries: number; factor: number; minTimeoutMs: number; maxTimeoutMs: number };
// track pending DNS challenges for graceful shutdown
private pendingChallenges: plugins.tsclass.network.IDnsChallenge[] = [];
// configured pluggable ACME challenge handlers
private challengeHandlers: plugins.handlers.IChallengeHandler<any>[];
// priority order of challenge types
private challengePriority: string[];
constructor(optionsArg: ISmartAcmeOptions) { constructor(optionsArg: ISmartAcmeOptions) {
this.options = optionsArg; this.options = optionsArg;
this.logger = plugins.smartlog.Smartlog.createForCommitinfo(commitinfo); this.logger = plugins.smartlog.Smartlog.createForCommitinfo(commitinfo);
// enable console output for structured logging
this.logger.enableConsole();
// initialize retry/backoff options
this.retryOptions = {
retries: optionsArg.retryOptions?.retries ?? 3,
factor: optionsArg.retryOptions?.factor ?? 2,
minTimeoutMs: optionsArg.retryOptions?.minTimeoutMs ?? 1000,
maxTimeoutMs: optionsArg.retryOptions?.maxTimeoutMs ?? 30000,
};
// initialize challenge handlers (must provide at least one)
if (!optionsArg.challengeHandlers || optionsArg.challengeHandlers.length === 0) {
throw new Error(
'You must provide at least one ACME challenge handler via options.challengeHandlers',
);
}
this.challengeHandlers = optionsArg.challengeHandlers;
// initialize challenge priority
this.challengePriority =
optionsArg.challengePriority && optionsArg.challengePriority.length > 0
? optionsArg.challengePriority
: this.challengeHandlers.map((h) => h.getSupportedTypes()[0]);
} }
/** /**
@ -59,8 +106,6 @@ export class SmartAcme {
public async start() { public async start() {
this.privateKey = this.privateKey =
this.options.accountPrivateKey || (await plugins.acme.forge.createPrivateKey()).toString(); this.options.accountPrivateKey || (await plugins.acme.forge.createPrivateKey()).toString();
this.setChallenge = this.options.setChallenge;
this.removeChallenge = this.options.removeChallenge;
// CertMangaer // CertMangaer
this.certmanager = new SmartacmeCertManager(this, { this.certmanager = new SmartacmeCertManager(this, {
@ -88,11 +133,65 @@ export class SmartAcme {
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
contact: [`mailto:${this.options.accountEmail}`], contact: [`mailto:${this.options.accountEmail}`],
}); });
// Setup graceful shutdown handlers
process.on('SIGINT', () => this.handleSignal('SIGINT'));
process.on('SIGTERM', () => this.handleSignal('SIGTERM'));
} }
public async stop() { public async stop() {
await this.certmanager.smartdataDb.close(); await this.certmanager.smartdataDb.close();
} }
/** Retry helper with exponential backoff */
private async retry<T>(operation: () => Promise<T>, operationName: string = 'operation'): Promise<T> {
let attempt = 0;
let delay = this.retryOptions.minTimeoutMs;
while (true) {
try {
return await operation();
} catch (err) {
attempt++;
if (attempt > this.retryOptions.retries) {
await this.logger.log('error', `Operation ${operationName} failed after ${attempt} attempts`, err);
throw err;
}
await this.logger.log('warn', `Operation ${operationName} failed on attempt ${attempt}, retrying in ${delay}ms`, err);
await plugins.smartdelay.delayFor(delay);
delay = Math.min(delay * this.retryOptions.factor, this.retryOptions.maxTimeoutMs);
}
}
}
/** Clean up pending challenges and shut down */
private async handleShutdown(): Promise<void> {
for (const input of [...this.pendingChallenges]) {
const type: string = (input as any).type;
const handler = this.challengeHandlers.find((h) => h.getSupportedTypes().includes(type));
if (handler) {
try {
await handler.cleanup(input);
await this.logger.log('info', `Removed pending ${type} challenge during shutdown`, input);
} catch (err) {
await this.logger.log('error', `Failed to remove pending ${type} challenge during shutdown`, err);
}
} else {
await this.logger.log(
'warn',
`No handler for pending challenge type '${type}' during shutdown; skipping cleanup`,
input,
);
}
}
this.pendingChallenges = [];
await this.stop();
}
/** Handle process signals for graceful shutdown */
private handleSignal(sig: string): void {
this.logger.log('info', `Received signal ${sig}, shutting down gracefully`);
this.handleShutdown()
.then(() => process.exit(0))
.catch((err) => {
this.logger.log('error', 'Error during shutdown', err).then(() => process.exit(1));
});
}
/** /**
* gets a certificate * gets a certificate
@ -128,54 +227,71 @@ export class SmartAcme {
// lets make sure others get the same interest // lets make sure others get the same interest
const currentDomainInterst = await this.certmanager.interestMap.addInterest(certDomainName); const currentDomainInterst = await this.certmanager.interestMap.addInterest(certDomainName);
/* Place new order */ /* Place new order with retry */
const order = await this.client.createOrder({ const order = await this.retry(() => this.client.createOrder({
identifiers: [ identifiers: [
{ type: 'dns', value: certDomainName }, { type: 'dns', value: certDomainName },
{ type: 'dns', value: `*.${certDomainName}` }, { type: 'dns', value: `*.${certDomainName}` },
], ],
}); }), 'createOrder');
/* Get authorizations and select challenges */ /* Get authorizations and select challenges */
const authorizations = await this.client.getAuthorizations(order); const authorizations = await this.retry(() => this.client.getAuthorizations(order), 'getAuthorizations');
for (const authz of authorizations) { for (const authz of authorizations) {
console.log(authz); await this.logger.log('debug', 'Authorization received', authz);
const fullHostName: string = `_acme-challenge.${authz.identifier.value}`; // select a handler based on configured priority
const dnsChallenge: string = authz.challenges.find((challengeArg) => { let selectedHandler: { type: string; handler: plugins.handlers.IChallengeHandler<any> } | null = null;
return challengeArg.type === 'dns-01'; let selectedChallengeArg: any = null;
}); for (const type of this.challengePriority) {
// process.exit(1); const candidate = authz.challenges.find((c: any) => c.type === type);
const keyAuthorization: string = await this.client.getChallengeKeyAuthorization(dnsChallenge); if (!candidate) continue;
const handler = this.challengeHandlers.find((h) => h.getSupportedTypes().includes(type));
if (handler) {
selectedHandler = { type, handler };
selectedChallengeArg = candidate;
break;
}
}
if (!selectedHandler) {
throw new Error(`No challenge handler for domain ${authz.identifier.value}: supported types [${this.challengePriority.join(',')}]`);
}
const { type, handler } = selectedHandler;
// build handler input with keyAuthorization
let input: any;
// retrieve keyAuthorization for challenge
const keyAuth = await this.client.getChallengeKeyAuthorization(selectedChallengeArg);
if (type === 'dns-01') {
input = { type, hostName: `_acme-challenge.${authz.identifier.value}`, challenge: keyAuth };
} else if (type === 'http-01') {
// HTTP-01 requires serving token at webPath
input = {
type,
token: (selectedChallengeArg as any).token,
keyAuthorization: keyAuth,
webPath: `/.well-known/acme-challenge/${(selectedChallengeArg as any).token}`,
};
} else {
// generic challenge input: include raw challenge properties
input = { type, keyAuthorization: keyAuth, ...selectedChallengeArg };
}
this.pendingChallenges.push(input);
try { try {
/* Satisfy challenge */ await this.retry(() => handler.prepare(input), `${type}.prepare`);
await this.setChallenge({ if (handler.verify) {
hostName: fullHostName, await this.retry(() => handler.verify!(input), `${type}.verify`);
challenge: keyAuthorization, } else {
}); await this.retry(() => this.client.verifyChallenge(authz, selectedChallengeArg), `${type}.verifyChallenge`);
await plugins.smartdelay.delayFor(30000); }
await this.smartdns.checkUntilAvailable(fullHostName, 'TXT', keyAuthorization, 100, 5000); await this.retry(() => this.client.completeChallenge(selectedChallengeArg), `${type}.completeChallenge`);
console.log('Cool down an extra 60 second for region availability'); await this.retry(() => this.client.waitForValidStatus(selectedChallengeArg), `${type}.waitForValidStatus`);
await plugins.smartdelay.delayFor(60000);
/* Verify that challenge is satisfied */
await this.client.verifyChallenge(authz, dnsChallenge);
/* Notify ACME provider that challenge is satisfied */
await this.client.completeChallenge(dnsChallenge);
/* Wait for ACME provider to respond with valid status */
await this.client.waitForValidStatus(dnsChallenge);
} finally { } finally {
/* Clean up challenge response */
try { try {
await this.removeChallenge({ await this.retry(() => handler.cleanup(input), `${type}.cleanup`);
hostName: fullHostName, } catch (err) {
challenge: keyAuthorization, await this.logger.log('error', `Error during ${type}.cleanup`, err);
}); } finally {
} catch (e) { this.pendingChallenges = this.pendingChallenges.filter((c) => c !== input);
console.log(e);
} }
} }
} }
@ -186,8 +302,8 @@ export class SmartAcme {
altNames: [certDomainName], altNames: [certDomainName],
}); });
await this.client.finalizeOrder(order, csr); await this.retry(() => this.client.finalizeOrder(order, csr), 'finalizeOrder');
const cert = await this.client.getCertificate(order); const cert = await this.retry(() => this.client.getCertificate(order), 'getCertificate');
/* Done */ /* Done */

View File

@ -7,7 +7,7 @@ export { typedserver };
import * as lik from '@push.rocks/lik'; import * as lik from '@push.rocks/lik';
import * as smartdata from '@push.rocks/smartdata'; import * as smartdata from '@push.rocks/smartdata';
import * as smartdelay from '@push.rocks/smartdelay'; import * as smartdelay from '@push.rocks/smartdelay';
import * as smartdns from '@push.rocks/smartdns'; import * as smartdnsClient from '@push.rocks/smartdns/client';
import * as smartlog from '@push.rocks/smartlog'; import * as smartlog from '@push.rocks/smartlog';
import * as smartpromise from '@push.rocks/smartpromise'; import * as smartpromise from '@push.rocks/smartpromise';
import * as smartrequest from '@push.rocks/smartrequest'; import * as smartrequest from '@push.rocks/smartrequest';
@ -19,7 +19,7 @@ export {
lik, lik,
smartdata, smartdata,
smartdelay, smartdelay,
smartdns, smartdnsClient,
smartlog, smartlog,
smartpromise, smartpromise,
smartrequest, smartrequest,
@ -37,3 +37,6 @@ export { tsclass };
import * as acme from 'acme-client'; import * as acme from 'acme-client';
export { acme }; export { acme };
// local handlers for challenge types
import * as handlers from './handlers/index.js';
export { handlers };

View File

@ -1,14 +1,22 @@
{ {
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true,
"useDefineForClassFields": false, "useDefineForClassFields": false,
"target": "ES2022", "target": "ES2022",
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"esModuleInterop": true, "esModuleInterop": true,
"verbatimModuleSyntax": true "verbatimModuleSyntax": true,
"baseUrl": ".",
"paths": {}
}, },
"include": [
"ts/**/*.ts"
],
"exclude": [ "exclude": [
"node_modules",
"test",
"dist_*/**/*.d.ts" "dist_*/**/*.d.ts"
] ]
} }