Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 609bcbc6db | |||
| 2da9370742 | |||
| 18afafd3b3 | |||
| 5ce1520e2b | |||
| 39d53da4e6 | |||
| 002ac3ae01 | |||
| 0184371635 | |||
| 038f56b0ce | |||
| 1c0a20ac99 | |||
| 36d9db4332 | |||
| 5d77214222 | |||
| f27eaa0e82 | |||
| 4c16e0263a | |||
| d8ca3dc115 |
66
.gitea/workflows/default_nottags.yaml
Normal file
66
.gitea/workflows/default_nottags.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Default (not tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags-ignore:
|
||||
- '**'
|
||||
|
||||
env:
|
||||
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install pnpm and npmci
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
|
||||
- name: Run npm prepare
|
||||
run: npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
||||
124
.gitea/workflows/default_tags.yaml
Normal file
124
.gitea/workflows/default_tags.yaml
Normal file
@@ -0,0 +1,124 @@
|
||||
name: Default (tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
||||
|
||||
release:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Release
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm publish
|
||||
|
||||
metadata:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Code quality
|
||||
run: |
|
||||
npmci command npm install -g typescript
|
||||
npmci npm install
|
||||
|
||||
- name: Trigger
|
||||
run: npmci trigger
|
||||
|
||||
- name: Build docs and upload artifacts
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
pnpm install -g @git.zone/tsdoc
|
||||
npmci command tsdoc
|
||||
continue-on-error: true
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,7 +3,6 @@
|
||||
# artifacts
|
||||
coverage/
|
||||
public/
|
||||
pages/
|
||||
|
||||
# installs
|
||||
node_modules/
|
||||
@@ -17,4 +16,4 @@ node_modules/
|
||||
dist/
|
||||
dist_*/
|
||||
|
||||
# custom
|
||||
#------# custom
|
||||
62
changelog.md
62
changelog.md
@@ -1,5 +1,67 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-11-18 - 7.1.0 - feat(cloudflare)
|
||||
Release 7.0.0 — manager-based API, ConvenientDnsProvider, improved utils and worker/zone/record management
|
||||
|
||||
- Introduce manager-based architecture: ZoneManager, RecordManager, WorkerManager for clearer, consistent API surface
|
||||
- Add ConvenientDnsProvider adapter implementing IConvenientDnsProvider for third-party integrations
|
||||
- New CloudflareUtils helpers (domain validation, API token check, record type validation, URL/TTL formatting, pagination)
|
||||
- Improved Worker support: create/update/delete scripts, route management, and robust listing with fallbacks
|
||||
- Zone and record operations enhanced (create/update/delete/purge) with type-safe wrappers and CloudflareZone/CloudflareRecord models
|
||||
- Deprecated old convenience namespace in favor of managers (kept for backward compatibility with migration guidance)
|
||||
- Full TypeScript exports and typings, updated package metadata for v7.0.0
|
||||
|
||||
## 2025-11-18 - 7.0.0 - BREAKING CHANGE(core)
|
||||
Introduce RecordManager and ConvenientDnsProvider; rename list/get methods for consistent API and deprecate convenience namespace
|
||||
|
||||
- Add RecordManager with listRecords, getRecord, createRecord, updateRecord, deleteRecord and cleanRecords to centralize DNS record operations
|
||||
- Add ConvenientDnsProvider adapter and CloudflareAccount.getConvenientDnsProvider() to provide IConvenientDnsProvider compatibility for third-party modules
|
||||
- Rename methods to consistent list* naming: worker.getRoutes -> worker.listRoutes, WorkerManager.listWorkerScripts -> WorkerManager.listWorkers, ZoneManager.getZones -> ZoneManager.listZones, convenience.listRecords -> recordManager.listRecords
|
||||
- Add ZoneManager.getZoneId() and ZoneManager.purgeZone() (zone cache purge helper)
|
||||
- Deprecate the legacy convenience.* methods (getZoneId, getRecord, createRecord, removeRecord, cleanRecord, updateRecord, listRecords, listZones, isDomainSupported, purgeZone, acmeSetDnsChallenge, acmeRemoveDnsChallenge) — kept for backward compatibility but marked deprecated
|
||||
- Export RecordManager and ConvenientDnsProvider from ts/index.ts and expose cfAccount.recordManager on CloudflareAccount
|
||||
- Update tests to use new method names (listWorkers) and extend test runner timeout; package.json test script updated
|
||||
- Documentation (readme) updated to describe the new manager-based API and migration guide; prepares project for major version 7.0.0
|
||||
|
||||
## 2025-11-17 - 6.4.3 - fix(cloudflare.plugins)
|
||||
Switch to smartrequest namespace export and improve request typing and JSON parsing
|
||||
|
||||
- Export smartrequest as a namespace from cloudflare.plugins (replaced named SmartRequest/CoreResponse exports)
|
||||
- Use plugins.smartrequest.SmartRequest.create() when building HTTP requests
|
||||
- Type response as InstanceType<typeof plugins.smartrequest.CoreResponse> to match the new smartrequest export shape
|
||||
- Safer JSON parsing: cast result of response.json() to the expected generic instead of relying on a generic json<T>() call and provide a text fallback when parsing fails
|
||||
- Adjust imports/usages to align with @push.rocks/smartrequest namespace usage
|
||||
|
||||
## 2025-11-17 - 6.4.2 - fix(core)
|
||||
Switch to SmartRequest fluent API and improve Cloudflare API request handling
|
||||
|
||||
- Upgrade runtime dependencies: @push.rocks/smartlog -> ^3.1.10, @push.rocks/smartrequest -> ^5.0.1, @push.rocks/smartstring -> ^4.1.0, @tsclass/tsclass -> ^9.3.0, cloudflare -> ^5.2.0
|
||||
- Upgrade devDependencies: @git.zone/tsbuild -> ^3.1.0, @git.zone/tsrun -> ^2.0.0, @git.zone/tstest -> ^2.8.2, @push.rocks/qenv -> ^6.1.3, openapi-typescript -> ^7.10.1
|
||||
- Export SmartRequest and CoreResponse from cloudflare.plugins to align with smartrequest v5 API
|
||||
- Refactor CloudflareAccount.request to use SmartRequest fluent builder, add detailed logging, default JSON Content-Type, support multipart/form-data via formData(), and use appropriate HTTP method helpers
|
||||
- Improve response parsing: return a safe fallback when JSON parsing fails by reading response.text() and include a concise message; better HTTP error logging including response body text
|
||||
- Update usages to rely on the new request behavior (zones/workers managers use account.request for endpoints not covered by the official client)
|
||||
|
||||
## 2025-04-30 - 6.4.1 - fix(ci)
|
||||
Update CI workflows, repository URL, and apply minor code formatting fixes
|
||||
|
||||
- Add new Gitea workflows for both tagged and non-tagged push events with security, test, release, and metadata jobs
|
||||
- Update repository URL in package.json from pushrocks/cflare to mojoio/cloudflare
|
||||
- Refine .gitignore custom comments
|
||||
- Apply minor formatting improvements in source code and documentation
|
||||
|
||||
## 2025-04-30 - 6.4.0 - feat(CloudflareAccount)
|
||||
Bump dependency versions and add domain support check in CloudflareAccount
|
||||
|
||||
- Upgrade dependencies: @push.rocks/smartrequest, @tsclass/tsclass, @git.zone/tsbuild, @push.rocks/tapbundle, and @types/node
|
||||
- Implement the isDomainSupported convenience method in CloudflareAccount for validating domain management
|
||||
|
||||
## 2025-04-26 - 6.3.2 - fix(worker)
|
||||
Refactor worker script update and creation to use intermediate parameter objects
|
||||
|
||||
- Build updateParams in CloudflareWorker for proper multipart form handling when updating scripts
|
||||
- Use contentParams in WorkerManager to improve clarity and consistency in worker creation
|
||||
|
||||
## 2025-04-26 - 6.3.1 - fix(core)
|
||||
Improve nested DNS record management and worker script multipart handling
|
||||
|
||||
|
||||
38
package.json
38
package.json
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"name": "@apiclient.xyz/cloudflare",
|
||||
"version": "6.3.1",
|
||||
"version": "7.1.0",
|
||||
"private": false,
|
||||
"description": "A TypeScript client for managing Cloudflare accounts, zones, DNS records, and workers with ease.",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "(tstest test/)",
|
||||
"test": "(tstest test/ --verbose --timeout 600)",
|
||||
"build": "(tsbuild --web --allowimplicitany)",
|
||||
"buildDocs": "tsdoc",
|
||||
"updateOpenapi": "openapi-typescript https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.yaml --output ts/openapi.spec.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://gitlab.com/pushrocks/cflare.git"
|
||||
"url": "https://gitlab.com/mojoio/cloudflare.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Cloudflare",
|
||||
@@ -31,26 +31,25 @@
|
||||
"author": "Lossless GmbH",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://gitlab.com/pushrocks/cflare/issues"
|
||||
"url": "https://gitlab.com/mojoio/cloudflare/issues"
|
||||
},
|
||||
"homepage": "https://gitlab.com/pushrocks/cflare#readme",
|
||||
"homepage": "https://gitlab.com/mojoio/cloudflare#readme",
|
||||
"dependencies": {
|
||||
"@push.rocks/smartdelay": "^3.0.1",
|
||||
"@push.rocks/smartlog": "^3.0.2",
|
||||
"@push.rocks/smartlog": "^3.1.10",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartrequest": "^2.0.23",
|
||||
"@push.rocks/smartstring": "^4.0.5",
|
||||
"@tsclass/tsclass": "^5.0.0",
|
||||
"cloudflare": "^4.2.0"
|
||||
"@push.rocks/smartrequest": "^5.0.1",
|
||||
"@push.rocks/smartstring": "^4.1.0",
|
||||
"@tsclass/tsclass": "^9.3.0",
|
||||
"cloudflare": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.2.7",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^1.0.96",
|
||||
"@push.rocks/qenv": "^6.1.0",
|
||||
"@push.rocks/tapbundle": "^5.6.0",
|
||||
"@types/node": "^22.13.10",
|
||||
"openapi-typescript": "^7.6.1"
|
||||
"@git.zone/tsbuild": "^3.1.0",
|
||||
"@git.zone/tsrun": "^2.0.0",
|
||||
"@git.zone/tstest": "^2.8.2",
|
||||
"@push.rocks/qenv": "^6.1.3",
|
||||
"@types/node": "^22.15.3",
|
||||
"openapi-typescript": "^7.10.1"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@@ -67,5 +66,8 @@
|
||||
"browserslist": [
|
||||
"last 1 chrome versions"
|
||||
],
|
||||
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6",
|
||||
"pnpm": {
|
||||
"overrides": {}
|
||||
}
|
||||
}
|
||||
|
||||
7608
pnpm-lock.yaml
generated
7608
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
630
readme.md
630
readme.md
@@ -1,399 +1,493 @@
|
||||
# @apiclient.xyz/cloudflare
|
||||
|
||||
An elegant, class-based TypeScript client for the Cloudflare API that makes managing your Cloudflare resources simple and type-safe.
|
||||
> 🚀 A modern, elegant TypeScript client for the Cloudflare API with clean manager-based architecture
|
||||
|
||||
[](https://www.npmjs.com/package/@apiclient.xyz/cloudflare)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
## Features
|
||||
Stop fighting with the Cloudflare API. This library gives you an intuitive, type-safe way to manage zones, DNS records, and Workers—all with clean, predictable method names and excellent IDE support.
|
||||
|
||||
- **Comprehensive coverage** of the Cloudflare API including zones, DNS records, and Workers
|
||||
- **Class-based design** with intuitive methods for all Cloudflare operations
|
||||
- **Strong TypeScript typing** for excellent IDE autocompletion and type safety
|
||||
- **Fully integrated with the official Cloudflare client** using modern async iterators
|
||||
- **Convenience methods** for common operations to reduce boilerplate code
|
||||
- **Promise-based API** for easy async/await usage
|
||||
- **ESM compatible** for modern JavaScript projects
|
||||
- **Comprehensive error handling** for robust applications
|
||||
## ✨ Why This Library?
|
||||
|
||||
## Installation
|
||||
- **🎯 Logical & Consistent**: Manager-based architecture with predictable method naming (`listZones()`, `listRecords()`, `listWorkers()`)
|
||||
- **💪 Strongly Typed**: Full TypeScript support with excellent autocompletion
|
||||
- **🔌 Framework Integration**: Built-in `IConvenientDnsProvider` adapter for third-party modules
|
||||
- **⚡ Modern**: Uses async/await, ES modules, and the official Cloudflare SDK
|
||||
- **🛡️ Reliable**: Comprehensive error handling and detailed logging
|
||||
- **📦 Zero Config**: Works out of the box with just your API token
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
```bash
|
||||
# Using npm
|
||||
npm install @apiclient.xyz/cloudflare
|
||||
|
||||
# Using yarn
|
||||
yarn add @apiclient.xyz/cloudflare
|
||||
|
||||
# Using pnpm
|
||||
pnpm add @apiclient.xyz/cloudflare
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import * as cflare from '@apiclient.xyz/cloudflare';
|
||||
import { CloudflareAccount } from '@apiclient.xyz/cloudflare';
|
||||
|
||||
// Initialize with your API token
|
||||
const cfAccount = new cflare.CloudflareAccount('your-cloudflare-api-token');
|
||||
const cf = new CloudflareAccount('your-api-token');
|
||||
|
||||
// Use convenience methods for quick operations
|
||||
await cfAccount.convenience.createRecord('subdomain.example.com', 'A', '192.0.2.1', 3600);
|
||||
// Manage DNS records with ease
|
||||
await cf.recordManager.createRecord('api.example.com', 'A', '192.0.2.1');
|
||||
await cf.recordManager.updateRecord('api.example.com', 'A', '203.0.113.1');
|
||||
|
||||
// Or work with the powerful class-based API
|
||||
const zone = await cfAccount.zoneManager.getZoneByName('example.com');
|
||||
await zone.purgeCache();
|
||||
// Work with zones
|
||||
const zones = await cf.zoneManager.listZones();
|
||||
await cf.zoneManager.purgeZone('example.com');
|
||||
|
||||
// Deploy workers
|
||||
const worker = await cf.workerManager.createWorker('my-api', workerScript);
|
||||
await worker.setRoutes([{ zoneName: 'example.com', pattern: 'api.example.com/*' }]);
|
||||
```
|
||||
|
||||
## Usage Guide
|
||||
## 📚 Core Concepts
|
||||
|
||||
### Account Management
|
||||
### Managers: Your Gateway to Cloudflare
|
||||
|
||||
Initialize your Cloudflare account with your API token:
|
||||
The library is organized around **managers**—specialized classes that handle specific Cloudflare resources:
|
||||
|
||||
- **`recordManager`** - DNS record operations
|
||||
- **`zoneManager`** - Zone (domain) management
|
||||
- **`workerManager`** - Cloudflare Workers deployment
|
||||
|
||||
Each manager provides consistent, predictable methods that feel natural to use.
|
||||
|
||||
## 🎓 Complete Guide
|
||||
|
||||
### Account Setup
|
||||
|
||||
```typescript
|
||||
import * as cflare from '@apiclient.xyz/cloudflare';
|
||||
import { CloudflareAccount } from '@apiclient.xyz/cloudflare';
|
||||
|
||||
const cfAccount = new cflare.CloudflareAccount('your-cloudflare-api-token');
|
||||
const cf = new CloudflareAccount(process.env.CLOUDFLARE_API_TOKEN);
|
||||
|
||||
// If you have multiple accounts, you can preselect one
|
||||
await cfAccount.preselectAccountByName('My Company Account');
|
||||
|
||||
// List all accounts you have access to
|
||||
const myAccounts = await cfAccount.listAccounts();
|
||||
// If you have multiple accounts, select one
|
||||
await cf.preselectAccountByName('Production Account');
|
||||
```
|
||||
|
||||
### Zone Management
|
||||
### 🌐 Zone Management
|
||||
|
||||
Zones represent your domains in Cloudflare.
|
||||
Zones are your domains in Cloudflare.
|
||||
|
||||
```typescript
|
||||
// Get all zones in your account
|
||||
const allZones = await cfAccount.convenience.listZones();
|
||||
// List all zones
|
||||
const zones = await cf.zoneManager.listZones();
|
||||
|
||||
// Get a specific zone by domain name
|
||||
const myZone = await cfAccount.zoneManager.getZoneByName('example.com');
|
||||
// Find a specific zone
|
||||
const zone = await cf.zoneManager.getZoneByName('example.com');
|
||||
|
||||
// Get zone ID directly
|
||||
const zoneId = await cfAccount.convenience.getZoneId('example.com');
|
||||
// Get zone ID for API calls
|
||||
const zoneId = await cf.zoneManager.getZoneId('example.com');
|
||||
|
||||
// Create a new zone
|
||||
const newZone = await cfAccount.zoneManager.createZone('newdomain.com');
|
||||
await cf.zoneManager.createZone('newdomain.com');
|
||||
|
||||
// Purge cache for an entire zone
|
||||
await cfAccount.convenience.purgeZone('example.com');
|
||||
// Or using the zone object
|
||||
await myZone.purgeCache();
|
||||
// Purge entire zone cache
|
||||
await cf.zoneManager.purgeZone('example.com');
|
||||
|
||||
// Purge specific URLs
|
||||
await myZone.purgeUrls(['https://example.com/css/styles.css', 'https://example.com/js/app.js']);
|
||||
// Work with zone objects
|
||||
if (await zone.isActive()) {
|
||||
await zone.purgeCache();
|
||||
await zone.purgeUrls(['https://example.com/css/app.css']);
|
||||
|
||||
// Enable/disable development mode
|
||||
await myZone.enableDevelopmentMode(); // Enables dev mode for 3 hours
|
||||
await myZone.disableDevelopmentMode();
|
||||
|
||||
// Check zone status
|
||||
const isActive = await myZone.isActive();
|
||||
const usingCfNameservers = await myZone.isUsingCloudflareNameservers();
|
||||
// Development mode (bypasses cache for 3 hours)
|
||||
await zone.enableDevelopmentMode();
|
||||
await zone.disableDevelopmentMode();
|
||||
}
|
||||
```
|
||||
|
||||
### DNS Record Management
|
||||
### 📝 DNS Records
|
||||
|
||||
Manage DNS records for your domains with ease.
|
||||
The `recordManager` gives you complete control over DNS records.
|
||||
|
||||
```typescript
|
||||
// List all DNS records for a domain
|
||||
const allRecords = await cfAccount.convenience.listRecords('example.com');
|
||||
// List all records for a domain
|
||||
const records = await cf.recordManager.listRecords('example.com');
|
||||
|
||||
// Create a new DNS record
|
||||
await cfAccount.convenience.createRecord('api.example.com', 'A', '192.0.2.1', 3600);
|
||||
// Create different record types
|
||||
await cf.recordManager.createRecord('www.example.com', 'CNAME', 'example.com', 3600);
|
||||
await cf.recordManager.createRecord('api.example.com', 'A', '192.0.2.1', 300);
|
||||
await cf.recordManager.createRecord('_dmarc.example.com', 'TXT', 'v=DMARC1; p=reject', 3600);
|
||||
|
||||
// Create a CNAME record
|
||||
await cfAccount.convenience.createRecord('www.example.com', 'CNAME', 'example.com', 3600);
|
||||
// Get a specific record
|
||||
const record = await cf.recordManager.getRecord('api.example.com', 'A');
|
||||
console.log(record.content); // "192.0.2.1"
|
||||
|
||||
// Get a specific DNS record
|
||||
const record = await cfAccount.convenience.getRecord('api.example.com', 'A');
|
||||
// Update or create (upsert behavior)
|
||||
await cf.recordManager.updateRecord('api.example.com', 'A', '203.0.113.1');
|
||||
|
||||
// Update a DNS record (automatically creates it if it doesn't exist)
|
||||
await cfAccount.convenience.updateRecord('api.example.com', 'A', '192.0.2.2', 3600);
|
||||
// Delete a record
|
||||
await cf.recordManager.deleteRecord('old.example.com', 'A');
|
||||
|
||||
// Remove a specific DNS record
|
||||
await cfAccount.convenience.removeRecord('api.example.com', 'A');
|
||||
|
||||
// Clean (remove) all records of a specific type
|
||||
await cfAccount.convenience.cleanRecord('example.com', 'TXT');
|
||||
|
||||
// Support for ACME DNS challenges (for certificate issuance)
|
||||
await cfAccount.convenience.acmeSetDnsChallenge({
|
||||
hostName: '_acme-challenge.example.com',
|
||||
challenge: 'token-validation-string'
|
||||
});
|
||||
await cfAccount.convenience.acmeRemoveDnsChallenge({
|
||||
hostName: '_acme-challenge.example.com',
|
||||
challenge: 'token-validation-string'
|
||||
});
|
||||
// Clean up all records of a type (useful for wildcard cleanup)
|
||||
await cf.recordManager.cleanRecords('example.com', 'TXT');
|
||||
```
|
||||
|
||||
### Workers Management
|
||||
### ⚙️ Cloudflare Workers
|
||||
|
||||
Create and manage Cloudflare Workers with full TypeScript support.
|
||||
Deploy and manage serverless functions at the edge.
|
||||
|
||||
```typescript
|
||||
// Create or update a worker
|
||||
// Worker code
|
||||
const workerScript = `
|
||||
addEventListener('fetch', event => {
|
||||
event.respondWith(new Response('Hello from Cloudflare Workers!'))
|
||||
})`;
|
||||
event.respondWith(handleRequest(event.request));
|
||||
});
|
||||
|
||||
const worker = await cfAccount.workerManager.createWorker('my-worker', workerScript);
|
||||
async function handleRequest(request) {
|
||||
return new Response('Hello from the edge!', {
|
||||
headers: { 'content-type': 'text/plain' }
|
||||
});
|
||||
}
|
||||
`;
|
||||
|
||||
// Create/update a worker
|
||||
const worker = await cf.workerManager.createWorker('my-api-worker', workerScript);
|
||||
|
||||
// List all workers
|
||||
const allWorkers = await cfAccount.workerManager.listWorkerScripts();
|
||||
const workers = await cf.workerManager.listWorkers();
|
||||
|
||||
// Get an existing worker
|
||||
const existingWorker = await cfAccount.workerManager.getWorker('my-worker');
|
||||
// Get existing worker
|
||||
const existingWorker = await cf.workerManager.getWorker('my-api-worker');
|
||||
|
||||
// Set routes for a worker
|
||||
// Set up routes
|
||||
await worker.setRoutes([
|
||||
{
|
||||
zoneName: 'example.com',
|
||||
pattern: 'https://api.example.com/*'
|
||||
},
|
||||
{
|
||||
zoneName: 'example.com',
|
||||
pattern: 'https://app.example.com/api/*'
|
||||
}
|
||||
{ zoneName: 'example.com', pattern: 'api.example.com/*' },
|
||||
{ zoneName: 'example.com', pattern: 'example.com/api/*' }
|
||||
]);
|
||||
|
||||
// Get all routes for a worker
|
||||
const routes = await worker.getRoutes();
|
||||
// Check worker routes
|
||||
await worker.listRoutes();
|
||||
console.log(worker.routes); // Array of routes
|
||||
|
||||
// Update a worker's script
|
||||
await worker.updateScript(`
|
||||
addEventListener('fetch', event => {
|
||||
event.respondWith(new Response('Updated worker content!'))
|
||||
})`);
|
||||
// Update worker script
|
||||
await worker.updateScript(updatedWorkerScript);
|
||||
|
||||
// Delete a worker
|
||||
// Delete worker
|
||||
await worker.delete();
|
||||
// Or using the worker manager
|
||||
await cfAccount.workerManager.deleteWorker('my-worker');
|
||||
// or
|
||||
await cf.workerManager.deleteWorker('my-api-worker');
|
||||
```
|
||||
|
||||
### Complete Example
|
||||
### 🔐 ACME DNS Challenges (Let's Encrypt)
|
||||
|
||||
Here's a complete example showing how to manage multiple aspects of your Cloudflare account:
|
||||
Use the `ConvenientDnsProvider` adapter for certificate automation.
|
||||
|
||||
```typescript
|
||||
import * as cflare from '@apiclient.xyz/cloudflare';
|
||||
const dnsProvider = cf.getConvenientDnsProvider();
|
||||
|
||||
async function manageCloudflare() {
|
||||
try {
|
||||
// Initialize with API token from environment variable
|
||||
const cfAccount = new cflare.CloudflareAccount(process.env.CLOUDFLARE_API_TOKEN);
|
||||
// Set DNS challenge
|
||||
await dnsProvider.acmeSetDnsChallenge({
|
||||
hostName: '_acme-challenge.example.com',
|
||||
challenge: 'your-validation-token'
|
||||
});
|
||||
|
||||
// Preselect account if needed
|
||||
await cfAccount.preselectAccountByName('My Company');
|
||||
|
||||
// Get zone and check status
|
||||
const myZone = await cfAccount.zoneManager.getZoneByName('example.com');
|
||||
console.log(`Zone active: ${await myZone.isActive()}`);
|
||||
console.log(`Using CF nameservers: ${await myZone.isUsingCloudflareNameservers()}`);
|
||||
|
||||
// Configure DNS
|
||||
await cfAccount.convenience.createRecord('api.example.com', 'A', '192.0.2.1');
|
||||
await cfAccount.convenience.createRecord('www.example.com', 'CNAME', 'example.com');
|
||||
|
||||
// Create a worker and set up routes
|
||||
const workerCode = `
|
||||
addEventListener('fetch', event => {
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
event.respondWith(new Response(JSON.stringify({ status: 'ok' }), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}));
|
||||
} else {
|
||||
event.respondWith(fetch(event.request));
|
||||
}
|
||||
})`;
|
||||
|
||||
const worker = await cfAccount.workerManager.createWorker('api-handler', workerCode);
|
||||
await worker.setRoutes([
|
||||
{ zoneName: 'example.com', pattern: 'https://api.example.com/*' }
|
||||
]);
|
||||
|
||||
// Purge cache for specific URLs
|
||||
await myZone.purgeUrls(['https://example.com/css/styles.css']);
|
||||
|
||||
console.log('Configuration completed successfully');
|
||||
} catch (error) {
|
||||
console.error('Error managing Cloudflare:', error);
|
||||
}
|
||||
}
|
||||
|
||||
manageCloudflare();
|
||||
// Remove challenge after validation
|
||||
await dnsProvider.acmeRemoveDnsChallenge({
|
||||
hostName: '_acme-challenge.example.com',
|
||||
challenge: 'your-validation-token'
|
||||
});
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
### 🔌 Third-Party Module Integration
|
||||
|
||||
If you're using a framework or library that expects `IConvenientDnsProvider`, use the adapter:
|
||||
|
||||
```typescript
|
||||
import { CloudflareAccount } from '@apiclient.xyz/cloudflare';
|
||||
|
||||
const cf = new CloudflareAccount(process.env.CLOUDFLARE_API_TOKEN);
|
||||
const dnsProvider = cf.getConvenientDnsProvider();
|
||||
|
||||
// Now pass this to any library expecting IConvenientDnsProvider
|
||||
await someFramework.setDnsProvider(dnsProvider);
|
||||
```
|
||||
|
||||
## 🎯 Real-World Example
|
||||
|
||||
Here's a complete example showing a typical deployment workflow:
|
||||
|
||||
```typescript
|
||||
import { CloudflareAccount } from '@apiclient.xyz/cloudflare';
|
||||
|
||||
async function deployApplication() {
|
||||
const cf = new CloudflareAccount(process.env.CLOUDFLARE_API_TOKEN);
|
||||
await cf.preselectAccountByName('Production');
|
||||
|
||||
// 1. Set up DNS
|
||||
console.log('📝 Configuring DNS...');
|
||||
await cf.recordManager.createRecord('app.example.com', 'A', '192.0.2.10');
|
||||
await cf.recordManager.createRecord('www.example.com', 'CNAME', 'example.com');
|
||||
|
||||
// 2. Deploy API worker
|
||||
console.log('⚙️ Deploying API worker...');
|
||||
const apiWorker = await cf.workerManager.createWorker('api-handler', `
|
||||
addEventListener('fetch', event => {
|
||||
const response = new Response(JSON.stringify({ status: 'ok' }), {
|
||||
headers: { 'content-type': 'application/json' }
|
||||
});
|
||||
event.respondWith(response);
|
||||
});
|
||||
`);
|
||||
|
||||
await apiWorker.setRoutes([
|
||||
{ zoneName: 'example.com', pattern: 'api.example.com/*' }
|
||||
]);
|
||||
|
||||
// 3. Configure security headers worker
|
||||
console.log('🛡️ Setting up security...');
|
||||
const securityWorker = await cf.workerManager.createWorker('security-headers', `
|
||||
addEventListener('fetch', event => {
|
||||
event.respondWith(addSecurityHeaders(event.request));
|
||||
});
|
||||
|
||||
async function addSecurityHeaders(request) {
|
||||
const response = await fetch(request);
|
||||
const newHeaders = new Headers(response.headers);
|
||||
newHeaders.set('X-Frame-Options', 'DENY');
|
||||
newHeaders.set('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
headers: newHeaders
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
await securityWorker.setRoutes([
|
||||
{ zoneName: 'example.com', pattern: 'example.com/*' }
|
||||
]);
|
||||
|
||||
// 4. Verify and purge cache
|
||||
console.log('🔄 Purging cache...');
|
||||
const zone = await cf.zoneManager.getZoneByName('example.com');
|
||||
await zone.purgeCache();
|
||||
|
||||
console.log('✅ Deployment complete!');
|
||||
}
|
||||
|
||||
deployApplication().catch(console.error);
|
||||
```
|
||||
|
||||
## 📖 API Reference
|
||||
|
||||
### CloudflareAccount
|
||||
|
||||
The main entry point for all Cloudflare operations.
|
||||
Main entry point for all operations.
|
||||
|
||||
```typescript
|
||||
class CloudflareAccount {
|
||||
constructor(apiToken: string);
|
||||
|
||||
// Account management
|
||||
async listAccounts(): Promise<Array<ICloudflareTypes['Account']>>;
|
||||
async preselectAccountByName(accountName: string): Promise<void>;
|
||||
constructor(apiToken: string)
|
||||
|
||||
// Managers
|
||||
readonly zoneManager: ZoneManager;
|
||||
readonly workerManager: WorkerManager;
|
||||
readonly recordManager: RecordManager
|
||||
readonly zoneManager: ZoneManager
|
||||
readonly workerManager: WorkerManager
|
||||
|
||||
// Official Cloudflare client
|
||||
readonly apiAccount: cloudflare.Cloudflare;
|
||||
// Methods
|
||||
async preselectAccountByName(name: string): Promise<void>
|
||||
getConvenientDnsProvider(): ConvenientDnsProvider
|
||||
|
||||
// Convenience namespace with helper methods
|
||||
readonly convenience: {
|
||||
// Zone operations
|
||||
listZones(domainName?: string): Promise<CloudflareZone[]>;
|
||||
getZoneId(domainName: string): Promise<string>;
|
||||
purgeZone(domainName: string): Promise<void>;
|
||||
// Official Cloudflare SDK client (for advanced use)
|
||||
readonly apiAccount: Cloudflare
|
||||
}
|
||||
```
|
||||
|
||||
// DNS operations
|
||||
listRecords(domainName: string): Promise<CloudflareRecord[]>;
|
||||
getRecord(domainName: string, recordType: string): Promise<CloudflareRecord | undefined>;
|
||||
createRecord(domainName: string, recordType: string, content: string, ttl?: number): Promise<any>;
|
||||
updateRecord(domainName: string, recordType: string, content: string, ttl?: number): Promise<any>;
|
||||
removeRecord(domainName: string, recordType: string): Promise<any>;
|
||||
cleanRecord(domainName: string, recordType: string): Promise<void>;
|
||||
### RecordManager
|
||||
|
||||
// ACME operations
|
||||
acmeSetDnsChallenge(dnsChallenge: IDnsChallenge): Promise<any>;
|
||||
acmeRemoveDnsChallenge(dnsChallenge: IDnsChallenge): Promise<any>;
|
||||
};
|
||||
DNS record management with clean, predictable methods.
|
||||
|
||||
```typescript
|
||||
class RecordManager {
|
||||
async listRecords(domain: string): Promise<CloudflareRecord[]>
|
||||
async getRecord(domain: string, type: string): Promise<CloudflareRecord | undefined>
|
||||
async createRecord(domain: string, type: string, content: string, ttl?: number): Promise<CloudflareRecord>
|
||||
async updateRecord(domain: string, type: string, content: string, ttl?: number): Promise<CloudflareRecord>
|
||||
async deleteRecord(domain: string, type: string): Promise<void>
|
||||
async cleanRecords(domain: string, type: string): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
### ZoneManager
|
||||
|
||||
Zone (domain) management operations.
|
||||
|
||||
```typescript
|
||||
class ZoneManager {
|
||||
async listZones(name?: string): Promise<CloudflareZone[]>
|
||||
async getZoneById(id: string): Promise<CloudflareZone | undefined>
|
||||
async getZoneByName(name: string): Promise<CloudflareZone | undefined>
|
||||
async getZoneId(domain: string): Promise<string>
|
||||
async createZone(name: string): Promise<CloudflareZone | undefined>
|
||||
async deleteZone(id: string): Promise<boolean>
|
||||
async purgeZone(domain: string): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
### CloudflareZone
|
||||
|
||||
Represents a Cloudflare zone (domain).
|
||||
Zone instance with convenience methods.
|
||||
|
||||
```typescript
|
||||
class CloudflareZone {
|
||||
// Properties
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly status: string;
|
||||
readonly paused: boolean;
|
||||
readonly type: string;
|
||||
readonly nameServers: string[];
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly status: string
|
||||
|
||||
// Methods
|
||||
async purgeCache(): Promise<any>;
|
||||
async purgeUrls(urls: string[]): Promise<any>;
|
||||
async isActive(): Promise<boolean>;
|
||||
async isUsingCloudflareNameservers(): Promise<boolean>;
|
||||
async isDevelopmentModeActive(): Promise<boolean>;
|
||||
async enableDevelopmentMode(): Promise<any>;
|
||||
async disableDevelopmentMode(): Promise<any>;
|
||||
async purgeCache(): Promise<void>
|
||||
async purgeUrls(urls: string[]): Promise<void>
|
||||
async isActive(): Promise<boolean>
|
||||
async isUsingCloudflareNameservers(): Promise<boolean>
|
||||
async enableDevelopmentMode(): Promise<void>
|
||||
async disableDevelopmentMode(): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
### CloudflareRecord
|
||||
### WorkerManager
|
||||
|
||||
Represents a DNS record.
|
||||
Cloudflare Workers deployment and management.
|
||||
|
||||
```typescript
|
||||
class CloudflareRecord {
|
||||
// Properties
|
||||
readonly id: string;
|
||||
readonly type: string;
|
||||
readonly name: string;
|
||||
readonly content: string;
|
||||
readonly ttl: number;
|
||||
readonly proxied: boolean;
|
||||
|
||||
// Methods
|
||||
async update(content: string, ttl?: number): Promise<any>;
|
||||
async delete(): Promise<any>;
|
||||
class WorkerManager {
|
||||
async listWorkers(): Promise<WorkerScript[]>
|
||||
async getWorker(name: string): Promise<CloudflareWorker | undefined>
|
||||
async createWorker(name: string, script: string): Promise<CloudflareWorker>
|
||||
async deleteWorker(name: string): Promise<boolean>
|
||||
}
|
||||
```
|
||||
|
||||
### CloudflareWorker
|
||||
|
||||
Represents a Cloudflare Worker.
|
||||
Worker instance for route management and updates.
|
||||
|
||||
```typescript
|
||||
class CloudflareWorker {
|
||||
// Properties
|
||||
readonly id: string;
|
||||
readonly script: string;
|
||||
readonly routes: IWorkerRoute[];
|
||||
readonly id: string
|
||||
readonly script: string
|
||||
readonly routes: WorkerRoute[]
|
||||
|
||||
// Methods
|
||||
async getRoutes(): Promise<IWorkerRoute[]>;
|
||||
async setRoutes(routes: Array<IWorkerRouteDefinition>): Promise<void>;
|
||||
async updateScript(scriptContent: string): Promise<CloudflareWorker>;
|
||||
async delete(): Promise<boolean>;
|
||||
async listRoutes(): Promise<void>
|
||||
async setRoutes(routes: WorkerRouteDefinition[]): Promise<void>
|
||||
async updateScript(script: string): Promise<CloudflareWorker>
|
||||
async delete(): Promise<boolean>
|
||||
}
|
||||
|
||||
interface IWorkerRouteDefinition {
|
||||
zoneName: string;
|
||||
pattern: string;
|
||||
interface WorkerRouteDefinition {
|
||||
zoneName: string
|
||||
pattern: string
|
||||
}
|
||||
```
|
||||
|
||||
## Utility Functions
|
||||
### ConvenientDnsProvider
|
||||
|
||||
The library includes helpful utility functions:
|
||||
Adapter for third-party modules requiring `IConvenientDnsProvider`.
|
||||
|
||||
```typescript
|
||||
// Validate a domain name
|
||||
CloudflareUtils.isValidDomain('example.com'); // true
|
||||
|
||||
// Extract zone name from a domain
|
||||
CloudflareUtils.getZoneName('subdomain.example.com'); // 'example.com'
|
||||
|
||||
// Validate a record type
|
||||
CloudflareUtils.isValidRecordType('A'); // true
|
||||
|
||||
// Format URL for cache purging
|
||||
CloudflareUtils.formatUrlForPurge('example.com/page'); // 'https://example.com/page'
|
||||
|
||||
// Format TTL value
|
||||
CloudflareUtils.formatTtl(3600); // '1 hour'
|
||||
class ConvenientDnsProvider implements IConvenientDnsProvider {
|
||||
async createRecord(domain: string, type: string, content: string, ttl?: number): Promise<any>
|
||||
async updateRecord(domain: string, type: string, content: string, ttl?: number): Promise<any>
|
||||
async removeRecord(domain: string, type: string): Promise<any>
|
||||
async getRecord(domain: string, type: string): Promise<any | undefined>
|
||||
async listRecords(domain: string): Promise<any[]>
|
||||
async cleanRecord(domain: string, type: string): Promise<void>
|
||||
async isDomainSupported(domain: string): Promise<boolean>
|
||||
async acmeSetDnsChallenge(challenge: DnsChallenge): Promise<void>
|
||||
async acmeRemoveDnsChallenge(challenge: DnsChallenge): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
## What's New in 6.2.0
|
||||
## 🔄 Migration from 6.x
|
||||
|
||||
- **Improved async iterator support**: Fully leverages the official Cloudflare client's async iterator pattern
|
||||
- **Enhanced error handling**: Better error detection and recovery
|
||||
- **Simplified API**: More consistent method signatures and return types
|
||||
- **Better type safety**: Improved TypeScript typing throughout the library
|
||||
- **Detailed logging**: More informative logging for easier debugging
|
||||
Version 7.0 introduces a cleaner architecture while maintaining **full backward compatibility**.
|
||||
|
||||
## Development & Testing
|
||||
### What Changed
|
||||
|
||||
To build the project:
|
||||
✅ **New manager-based API** - Clean, logical organization
|
||||
✅ **Consistent naming** - All list operations use `list*()`
|
||||
✅ **IConvenientDnsProvider support** - Better third-party integration
|
||||
⚠️ **Deprecated `convenience` namespace** - Still works, but use managers instead
|
||||
|
||||
### Migration Examples
|
||||
|
||||
```typescript
|
||||
// ❌ Old (deprecated but still works)
|
||||
await cf.convenience.createRecord('example.com', 'A', '1.2.3.4');
|
||||
await cf.convenience.listZones();
|
||||
await cf.workerManager.listWorkerScripts();
|
||||
await worker.getRoutes();
|
||||
|
||||
// ✅ New (recommended)
|
||||
await cf.recordManager.createRecord('example.com', 'A', '1.2.3.4');
|
||||
await cf.zoneManager.listZones();
|
||||
await cf.workerManager.listWorkers();
|
||||
await worker.listRoutes();
|
||||
```
|
||||
|
||||
**No Breaking Changes**: Your existing code will continue to work. The old `convenience` namespace methods now show deprecation warnings in TypeScript with clear migration paths.
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
# or
|
||||
pnpm run build
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Build
|
||||
pnpm build
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
```
|
||||
|
||||
To run tests:
|
||||
## 📝 TypeScript Configuration
|
||||
|
||||
```bash
|
||||
npm test
|
||||
# or
|
||||
pnpm run test
|
||||
This library is written in TypeScript and provides complete type definitions. No `@types` package needed!
|
||||
|
||||
```typescript
|
||||
import { CloudflareAccount, CloudflareZone, CloudflareRecord } from '@apiclient.xyz/cloudflare';
|
||||
|
||||
// Full type inference and autocompletion
|
||||
const cf = new CloudflareAccount(token);
|
||||
const zones: CloudflareZone[] = await cf.zoneManager.listZones();
|
||||
const record: CloudflareRecord = await cf.recordManager.createRecord(/* ... */);
|
||||
```
|
||||
|
||||
## License
|
||||
## 🤔 FAQ
|
||||
|
||||
MIT © [Lossless GmbH](https://lossless.gmbh)
|
||||
**Q: Can I use this with the official Cloudflare SDK?**
|
||||
A: Yes! The library wraps the official SDK. You can access it directly via `cfAccount.apiAccount` for advanced operations.
|
||||
|
||||
**Q: Does this support Cloudflare Workers KV, R2, D1?**
|
||||
A: Currently focuses on zones, DNS, and Workers. Additional features coming in future releases.
|
||||
|
||||
**Q: How do I handle errors?**
|
||||
A: All methods throw descriptive errors. Wrap calls in try/catch for error handling.
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await cf.recordManager.createRecord('example.com', 'A', 'invalid-ip');
|
||||
} catch (error) {
|
||||
console.error('Failed to create record:', error.message);
|
||||
}
|
||||
```
|
||||
|
||||
**Q: Is this production-ready?**
|
||||
A: Absolutely! Used in production environments. Comprehensive error handling and logging built-in.
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// tslint:disable-next-line: no-implicit-dependencies
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
// tslint:disable-next-line: no-implicit-dependencies
|
||||
import { Qenv } from '@push.rocks/qenv';
|
||||
|
||||
@@ -14,7 +14,9 @@ let testZoneName = `test-zone-${randomPrefix}.com`;
|
||||
|
||||
// Basic initialization tests
|
||||
tap.test('should create a valid instance of CloudflareAccount', async () => {
|
||||
testCloudflareAccount = new cloudflare.CloudflareAccount(await testQenv.getEnvVarOnDemand('CF_KEY'));
|
||||
testCloudflareAccount = new cloudflare.CloudflareAccount(
|
||||
await testQenv.getEnvVarOnDemand('CF_KEY'),
|
||||
);
|
||||
expect(testCloudflareAccount).toBeTypeOf('object');
|
||||
expect(testCloudflareAccount.apiAccount).toBeTypeOf('object');
|
||||
});
|
||||
@@ -22,7 +24,7 @@ tap.test('should create a valid instance of CloudflareAccount', async () => {
|
||||
tap.test('should preselect an account', async () => {
|
||||
await testCloudflareAccount.preselectAccountByName('Sandbox Account');
|
||||
expect(testCloudflareAccount.preselectedAccountId).toBeTypeOf('string');
|
||||
})
|
||||
});
|
||||
|
||||
// Zone management tests
|
||||
tap.test('.listZones() -> should list zones in account', async (tools) => {
|
||||
@@ -94,7 +96,7 @@ tap.test('should create A record for subdomain', async (tools) => {
|
||||
subdomain,
|
||||
'A',
|
||||
'127.0.0.1',
|
||||
120
|
||||
120,
|
||||
);
|
||||
expect(result).toBeTypeOf('object');
|
||||
console.log(`Created A record for ${subdomain}`);
|
||||
@@ -107,7 +109,7 @@ tap.test('should create CNAME record for subdomain', async (tools) => {
|
||||
subdomain,
|
||||
'CNAME',
|
||||
'example.com',
|
||||
120
|
||||
120,
|
||||
);
|
||||
expect(result).toBeTypeOf('object');
|
||||
console.log(`Created CNAME record for ${subdomain}`);
|
||||
@@ -120,7 +122,7 @@ tap.test('should create TXT record for subdomain', async (tools) => {
|
||||
subdomain,
|
||||
'TXT',
|
||||
'v=spf1 include:_spf.example.com ~all',
|
||||
120
|
||||
120,
|
||||
);
|
||||
expect(result).toBeTypeOf('object');
|
||||
console.log(`Created TXT record for ${subdomain}`);
|
||||
@@ -142,7 +144,7 @@ tap.test('should update A record content', async (tools) => {
|
||||
subdomain,
|
||||
'A',
|
||||
'192.168.1.1',
|
||||
120
|
||||
120,
|
||||
);
|
||||
expect(result).toBeTypeOf('object');
|
||||
expect(result.content).toEqual('192.168.1.1');
|
||||
@@ -157,7 +159,7 @@ tap.test('should create A record for nested subdomain', async (tools) => {
|
||||
nestedSubdomain,
|
||||
'A',
|
||||
'127.0.0.5',
|
||||
120
|
||||
120,
|
||||
);
|
||||
expect(result).toBeTypeOf('object');
|
||||
console.log(`Created nested A record for ${nestedSubdomain}`);
|
||||
@@ -179,7 +181,7 @@ tap.test('should update A record for nested subdomain', async (tools) => {
|
||||
nestedSubdomain,
|
||||
'A',
|
||||
'127.0.0.6',
|
||||
120
|
||||
120,
|
||||
);
|
||||
expect(result).toBeTypeOf('object');
|
||||
expect(result.content).toEqual('127.0.0.6');
|
||||
@@ -234,7 +236,7 @@ tap.test('should list workers', async (tools) => {
|
||||
tools.timeout(600000);
|
||||
|
||||
try {
|
||||
const workerArray = await testCloudflareAccount.workerManager.listWorkerScripts();
|
||||
const workerArray = await testCloudflareAccount.workerManager.listWorkers();
|
||||
expect(workerArray).toBeTypeOf('array');
|
||||
console.log(`Found ${workerArray.length} workers in account`);
|
||||
} catch (error) {
|
||||
@@ -254,7 +256,7 @@ tap.test('should create a worker', async (tools) => {
|
||||
event.respondWith(new Response('Hello from Cloudflare Workers!', {
|
||||
headers: { 'content-type': 'text/plain' }
|
||||
}))
|
||||
})`
|
||||
})`,
|
||||
);
|
||||
|
||||
expect(worker).toBeTypeOf('object');
|
||||
@@ -293,7 +295,7 @@ tap.test('should get a specific worker by name', async (tools) => {
|
||||
event.respondWith(new Response('Hello from Cloudflare Workers!', {
|
||||
headers: { 'content-type': 'text/plain' }
|
||||
}))
|
||||
})`
|
||||
})`,
|
||||
);
|
||||
|
||||
// Now get the worker
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@apiclient.xyz/cloudflare',
|
||||
version: '6.3.1',
|
||||
version: '7.1.0',
|
||||
description: 'A TypeScript client for managing Cloudflare accounts, zones, DNS records, and workers with ease.'
|
||||
}
|
||||
|
||||
@@ -5,13 +5,16 @@ import * as interfaces from './interfaces/index.js';
|
||||
// interfaces
|
||||
import { WorkerManager } from './cloudflare.classes.workermanager.js';
|
||||
import { ZoneManager } from './cloudflare.classes.zonemanager.js';
|
||||
import { RecordManager } from './cloudflare.classes.recordmanager.js';
|
||||
import { ConvenientDnsProvider } from './cloudflare.classes.convenientdnsprovider.js';
|
||||
|
||||
export class CloudflareAccount {
|
||||
export class CloudflareAccount implements plugins.tsclass.network.IConvenientDnsProvider {
|
||||
private authToken: string;
|
||||
public preselectedAccountId: string;
|
||||
|
||||
public workerManager = new WorkerManager(this);
|
||||
public zoneManager = new ZoneManager(this);
|
||||
public recordManager = new RecordManager(this);
|
||||
|
||||
public apiAccount: plugins.cloudflare.Cloudflare;
|
||||
|
||||
@@ -39,54 +42,80 @@ export class CloudflareAccount {
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
|
||||
endpoint: string,
|
||||
data?: any,
|
||||
customHeaders?: Record<string, string>
|
||||
customHeaders?: Record<string, string>,
|
||||
): Promise<T> {
|
||||
try {
|
||||
const options: plugins.smartrequest.ISmartRequestOptions = {
|
||||
method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.authToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
...customHeaders,
|
||||
},
|
||||
};
|
||||
logger.log('debug', `Making ${method} request to ${endpoint}`);
|
||||
|
||||
// Build the request using fluent API
|
||||
let requestBuilder = plugins.smartrequest.SmartRequest.create()
|
||||
.url(`https://api.cloudflare.com/client/v4${endpoint}`)
|
||||
.header('Authorization', `Bearer ${this.authToken}`);
|
||||
|
||||
// Add custom headers
|
||||
if (customHeaders) {
|
||||
for (const [key, value] of Object.entries(customHeaders)) {
|
||||
requestBuilder = requestBuilder.header(key, value);
|
||||
}
|
||||
} else {
|
||||
// Default to JSON content type if no custom headers
|
||||
requestBuilder = requestBuilder.header('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
// Add request body if provided
|
||||
if (data) {
|
||||
if (customHeaders && customHeaders['Content-Type']?.includes('multipart/form-data')) {
|
||||
// For multipart form data, use the data directly as the request body
|
||||
options.requestBody = data;
|
||||
// For multipart form data, use formData method
|
||||
requestBuilder = requestBuilder.formData(data);
|
||||
} else {
|
||||
// For JSON requests, stringify the data
|
||||
options.requestBody = JSON.stringify(data);
|
||||
// For JSON requests, use json method
|
||||
requestBuilder = requestBuilder.json(data);
|
||||
}
|
||||
}
|
||||
|
||||
logger.log('debug', `Making ${method} request to ${endpoint}`);
|
||||
const response = await plugins.smartrequest.request(`https://api.cloudflare.com/client/v4${endpoint}`, options);
|
||||
|
||||
// Check if response is already an object (might happen with newer smartrequest versions)
|
||||
if (typeof response.body === 'object' && response.body !== null) {
|
||||
return response.body;
|
||||
// Execute the request with the appropriate method
|
||||
let response: InstanceType<typeof plugins.smartrequest.CoreResponse>;
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
response = await requestBuilder.get();
|
||||
break;
|
||||
case 'POST':
|
||||
response = await requestBuilder.post();
|
||||
break;
|
||||
case 'PUT':
|
||||
response = await requestBuilder.put();
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await requestBuilder.delete();
|
||||
break;
|
||||
case 'PATCH':
|
||||
response = await requestBuilder.patch();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported HTTP method: ${method}`);
|
||||
}
|
||||
|
||||
// Otherwise try to parse as JSON
|
||||
// Check response status
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
logger.log('error', `HTTP ${response.status}: ${errorBody}`);
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Parse the response body
|
||||
try {
|
||||
if (typeof response.body === 'string' && response.body.trim()) {
|
||||
return JSON.parse(response.body);
|
||||
} else {
|
||||
// If body is empty or not a string, return an empty result
|
||||
logger.log('warn', `Empty or invalid response body: ${typeof response.body}`);
|
||||
return { result: [] } as T;
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
} catch (parseError) {
|
||||
logger.log('warn', `Failed to parse response as JSON: ${parseError.message}`);
|
||||
|
||||
// Create a fake response object to maintain expected structure
|
||||
// Try to get as text and create a fallback response
|
||||
const textBody = await response.text().catch(() => '');
|
||||
|
||||
return {
|
||||
result: [],
|
||||
success: true,
|
||||
errors: [],
|
||||
messages: [`Failed to parse: ${typeof response.body === 'string' ? response.body?.substring(0, 50) : typeof response.body}...`]
|
||||
messages: [`Failed to parse: ${textBody.substring(0, 50)}...`],
|
||||
} as T;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -107,6 +136,16 @@ export class CloudflareAccount {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a ConvenientDnsProvider instance that implements IConvenientDnsProvider
|
||||
* This allows third-party modules to use the standard DNS provider interface
|
||||
* while internally delegating to the clean RecordManager and ZoneManager structure
|
||||
* @returns ConvenientDnsProvider instance
|
||||
*/
|
||||
public getConvenientDnsProvider(): ConvenientDnsProvider {
|
||||
return new ConvenientDnsProvider(this);
|
||||
}
|
||||
|
||||
public convenience = {
|
||||
/**
|
||||
* Lists all accounts accessible with the current API token
|
||||
@@ -131,6 +170,7 @@ export class CloudflareAccount {
|
||||
/**
|
||||
* gets a zone id of a domain from cloudflare
|
||||
* @param domainName
|
||||
* @deprecated Use zoneManager.getZoneId() instead
|
||||
*/
|
||||
getZoneId: async (domainName: string) => {
|
||||
const domain = new plugins.smartstring.Domain(domainName);
|
||||
@@ -149,17 +189,21 @@ export class CloudflareAccount {
|
||||
* gets a record
|
||||
* @param domainNameArg
|
||||
* @param typeArg
|
||||
* @deprecated Use recordManager.getRecord() or getConvenientDnsProvider().getRecord() instead
|
||||
*/
|
||||
getRecord: async (
|
||||
domainNameArg: string,
|
||||
typeArg: plugins.tsclass.network.TDnsRecordType
|
||||
typeArg: plugins.tsclass.network.TDnsRecordType,
|
||||
): Promise<plugins.ICloudflareTypes['Record'] | undefined> => {
|
||||
try {
|
||||
const domain = new plugins.smartstring.Domain(domainNameArg);
|
||||
const recordArrayArg = await this.convenience.listRecords(domain.zoneName);
|
||||
|
||||
if (!Array.isArray(recordArrayArg)) {
|
||||
logger.log('warn', `Expected records array for ${domainNameArg} but got ${typeof recordArrayArg}`);
|
||||
logger.log(
|
||||
'warn',
|
||||
`Expected records array for ${domainNameArg} but got ${typeof recordArrayArg}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -175,12 +219,13 @@ export class CloudflareAccount {
|
||||
},
|
||||
/**
|
||||
* creates a record
|
||||
* @deprecated Use recordManager.createRecord() or getConvenientDnsProvider().createRecord() instead
|
||||
*/
|
||||
createRecord: async (
|
||||
domainNameArg: string,
|
||||
typeArg: plugins.tsclass.network.TDnsRecordType,
|
||||
contentArg: string,
|
||||
ttlArg = 1
|
||||
ttlArg = 1,
|
||||
): Promise<any> => {
|
||||
const domain = new plugins.smartstring.Domain(domainNameArg);
|
||||
const zoneId = await this.convenience.getZoneId(domain.zoneName);
|
||||
@@ -190,17 +235,18 @@ export class CloudflareAccount {
|
||||
name: domain.fullName,
|
||||
content: contentArg,
|
||||
ttl: ttlArg,
|
||||
})
|
||||
});
|
||||
return response;
|
||||
},
|
||||
/**
|
||||
* removes a record from Cloudflare
|
||||
* @param domainNameArg
|
||||
* @param typeArg
|
||||
* @deprecated Use recordManager.deleteRecord() or getConvenientDnsProvider().removeRecord() instead
|
||||
*/
|
||||
removeRecord: async (
|
||||
domainNameArg: string,
|
||||
typeArg: plugins.tsclass.network.TDnsRecordType
|
||||
typeArg: plugins.tsclass.network.TDnsRecordType,
|
||||
): Promise<any> => {
|
||||
const domain = new plugins.smartstring.Domain(domainNameArg);
|
||||
const zoneId = await this.convenience.getZoneId(domain.zoneName);
|
||||
@@ -222,6 +268,7 @@ export class CloudflareAccount {
|
||||
|
||||
/**
|
||||
* cleanrecord allows the cleaning of any previous records to avoid unwanted sideeffects
|
||||
* @deprecated Use recordManager.cleanRecords() or getConvenientDnsProvider().cleanRecord() instead
|
||||
*/
|
||||
cleanRecord: async (domainNameArg: string, typeArg: plugins.tsclass.network.TDnsRecordType) => {
|
||||
try {
|
||||
@@ -233,7 +280,10 @@ export class CloudflareAccount {
|
||||
const records = await this.convenience.listRecords(domain.zoneName);
|
||||
|
||||
if (!Array.isArray(records)) {
|
||||
logger.log('warn', `Expected records array for ${domainNameArg} but got ${typeof records}`);
|
||||
logger.log(
|
||||
'warn',
|
||||
`Expected records array for ${domainNameArg} but got ${typeof records}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -242,7 +292,10 @@ export class CloudflareAccount {
|
||||
return recordArg.type === typeArg && recordArg.name === domainNameArg;
|
||||
});
|
||||
|
||||
logger.log('info', `Found ${recordsToDelete.length} ${typeArg} records to delete for ${domainNameArg}`);
|
||||
logger.log(
|
||||
'info',
|
||||
`Found ${recordsToDelete.length} ${typeArg} records to delete for ${domainNameArg}`,
|
||||
);
|
||||
|
||||
for (const recordToDelete of recordsToDelete) {
|
||||
try {
|
||||
@@ -263,7 +316,10 @@ export class CloudflareAccount {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('error', `Error cleaning ${typeArg} records for ${domainNameArg}: ${error.message}`);
|
||||
logger.log(
|
||||
'error',
|
||||
`Error cleaning ${typeArg} records for ${domainNameArg}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -274,12 +330,13 @@ export class CloudflareAccount {
|
||||
* @param contentArg New content for the record
|
||||
* @param ttlArg Time to live in seconds (optional)
|
||||
* @returns Updated record
|
||||
* @deprecated Use recordManager.updateRecord() or getConvenientDnsProvider().updateRecord() instead
|
||||
*/
|
||||
updateRecord: async (
|
||||
domainNameArg: string,
|
||||
typeArg: plugins.tsclass.network.TDnsRecordType,
|
||||
contentArg: string,
|
||||
ttlArg: number = 1
|
||||
ttlArg: number = 1,
|
||||
): Promise<plugins.ICloudflareTypes['Record']> => {
|
||||
const domain = new plugins.smartstring.Domain(domainNameArg);
|
||||
const zoneId = await this.convenience.getZoneId(domain.zoneName);
|
||||
@@ -288,7 +345,10 @@ export class CloudflareAccount {
|
||||
const record = await this.convenience.getRecord(domainNameArg, typeArg);
|
||||
|
||||
if (!record) {
|
||||
logger.log('warn', `Record ${domainNameArg} of type ${typeArg} not found for update, creating instead`);
|
||||
logger.log(
|
||||
'warn',
|
||||
`Record ${domainNameArg} of type ${typeArg} not found for update, creating instead`,
|
||||
);
|
||||
return this.convenience.createRecord(domainNameArg, typeArg, contentArg, ttlArg);
|
||||
}
|
||||
|
||||
@@ -299,7 +359,7 @@ export class CloudflareAccount {
|
||||
type: typeArg as any,
|
||||
name: domain.fullName,
|
||||
content: contentArg,
|
||||
ttl: ttlArg
|
||||
ttl: ttlArg,
|
||||
});
|
||||
|
||||
return updatedRecord;
|
||||
@@ -307,6 +367,7 @@ export class CloudflareAccount {
|
||||
/**
|
||||
* list all records of a specified domain name
|
||||
* @param domainNameArg - the domain name that you want to get the records from
|
||||
* @deprecated Use recordManager.listRecords() or getConvenientDnsProvider().listRecords() instead
|
||||
*/
|
||||
listRecords: async (domainNameArg: string) => {
|
||||
try {
|
||||
@@ -331,6 +392,7 @@ export class CloudflareAccount {
|
||||
/**
|
||||
* list all zones in the associated authenticated account
|
||||
* @param domainName optional filter by domain name
|
||||
* @deprecated Use zoneManager.listZones() instead
|
||||
*/
|
||||
listZones: async (domainName?: string) => {
|
||||
try {
|
||||
@@ -346,15 +408,38 @@ export class CloudflareAccount {
|
||||
zones.push(zone);
|
||||
}
|
||||
|
||||
logger.log('info', `Found ${zones.length} zones${domainName ? ` matching ${domainName}` : ''}`);
|
||||
logger.log(
|
||||
'info',
|
||||
`Found ${zones.length} zones${domainName ? ` matching ${domainName}` : ''}`,
|
||||
);
|
||||
return zones;
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to list zones: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Determines whether the given domain can be managed by this account
|
||||
* @param domainName Full domain name to check (e.g., "sub.example.com")
|
||||
* @returns True if the zone for the domain exists in the account, false otherwise
|
||||
* @deprecated Use getConvenientDnsProvider().isDomainSupported() instead
|
||||
*/
|
||||
isDomainSupported: async (domainName: string): Promise<boolean> => {
|
||||
try {
|
||||
// Parse out the apex/zone name from the full domain
|
||||
const domain = new plugins.smartstring.Domain(domainName);
|
||||
// List zones filtered by the zone name
|
||||
const zones = await this.convenience.listZones(domain.zoneName);
|
||||
// If any zone matches, we can manage this domain
|
||||
return Array.isArray(zones) && zones.length > 0;
|
||||
} catch (error) {
|
||||
logger.log('error', `Error checking domain support for ${domainName}: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* purges a zone
|
||||
* @deprecated Use zoneManager.purgeZone() instead
|
||||
*/
|
||||
purgeZone: async (domainName: string): Promise<void> => {
|
||||
const domain = new plugins.smartstring.Domain(domainName);
|
||||
@@ -366,15 +451,21 @@ export class CloudflareAccount {
|
||||
},
|
||||
|
||||
// acme convenience functions
|
||||
/**
|
||||
* @deprecated Use getConvenientDnsProvider().acmeSetDnsChallenge() instead
|
||||
*/
|
||||
acmeSetDnsChallenge: async (dnsChallenge: plugins.tsclass.network.IDnsChallenge) => {
|
||||
await this.convenience.cleanRecord(dnsChallenge.hostName, 'TXT');
|
||||
await this.convenience.createRecord(
|
||||
dnsChallenge.hostName,
|
||||
'TXT',
|
||||
dnsChallenge.challenge,
|
||||
120
|
||||
120,
|
||||
);
|
||||
},
|
||||
/**
|
||||
* @deprecated Use getConvenientDnsProvider().acmeRemoveDnsChallenge() instead
|
||||
*/
|
||||
acmeRemoveDnsChallenge: async (dnsChallenge: plugins.tsclass.network.IDnsChallenge) => {
|
||||
await this.convenience.removeRecord(dnsChallenge.hostName, 'TXT');
|
||||
},
|
||||
|
||||
178
ts/cloudflare.classes.convenientdnsprovider.ts
Normal file
178
ts/cloudflare.classes.convenientdnsprovider.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import * as plugins from './cloudflare.plugins.js';
|
||||
import { logger } from './cloudflare.logger.js';
|
||||
|
||||
/**
|
||||
* Adapter class that implements IConvenientDnsProvider interface
|
||||
* Delegates to RecordManager and ZoneManager internally for clean architecture
|
||||
* This allows third-party modules to use the standard DNS provider interface
|
||||
*/
|
||||
export class ConvenientDnsProvider implements plugins.tsclass.network.IConvenientDnsProvider {
|
||||
/**
|
||||
* The convenience property is required by IConvenientDnsProvider interface
|
||||
* It returns this instance to maintain interface compatibility
|
||||
*/
|
||||
public convenience = this;
|
||||
|
||||
constructor(private cfAccount: any) {}
|
||||
|
||||
/**
|
||||
* Creates a new DNS record
|
||||
* @param domainNameArg - The domain name for the record
|
||||
* @param typeArg - The DNS record type
|
||||
* @param contentArg - The record content (IP address, CNAME target, etc.)
|
||||
* @param ttlArg - Time to live in seconds (default: 1 = automatic)
|
||||
* @returns Created record as raw API object
|
||||
*/
|
||||
public async createRecord(
|
||||
domainNameArg: string,
|
||||
typeArg: plugins.tsclass.network.TDnsRecordType,
|
||||
contentArg: string,
|
||||
ttlArg: number = 1,
|
||||
): Promise<any> {
|
||||
const record = await this.cfAccount.recordManager.createRecord(
|
||||
domainNameArg,
|
||||
typeArg,
|
||||
contentArg,
|
||||
ttlArg,
|
||||
);
|
||||
// Return raw API object format for interface compatibility
|
||||
return this.recordToApiObject(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing DNS record, or creates it if it doesn't exist
|
||||
* @param domainNameArg - The domain name
|
||||
* @param typeArg - The DNS record type
|
||||
* @param contentArg - The new record content
|
||||
* @param ttlArg - Time to live in seconds (default: 1 = automatic)
|
||||
* @returns Updated record as raw API object
|
||||
*/
|
||||
public async updateRecord(
|
||||
domainNameArg: string,
|
||||
typeArg: plugins.tsclass.network.TDnsRecordType,
|
||||
contentArg: string,
|
||||
ttlArg: number = 1,
|
||||
): Promise<any> {
|
||||
const record = await this.cfAccount.recordManager.updateRecord(
|
||||
domainNameArg,
|
||||
typeArg,
|
||||
contentArg,
|
||||
ttlArg,
|
||||
);
|
||||
return this.recordToApiObject(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a DNS record
|
||||
* @param domainNameArg - The domain name
|
||||
* @param typeArg - The DNS record type
|
||||
*/
|
||||
public async removeRecord(
|
||||
domainNameArg: string,
|
||||
typeArg: plugins.tsclass.network.TDnsRecordType,
|
||||
): Promise<any> {
|
||||
await this.cfAccount.recordManager.deleteRecord(domainNameArg, typeArg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific DNS record by domain and type
|
||||
* @param domainNameArg - The domain name
|
||||
* @param typeArg - The DNS record type
|
||||
* @returns Record as raw API object or undefined if not found
|
||||
*/
|
||||
public async getRecord(
|
||||
domainNameArg: string,
|
||||
typeArg: plugins.tsclass.network.TDnsRecordType,
|
||||
): Promise<any | undefined> {
|
||||
const record = await this.cfAccount.recordManager.getRecord(domainNameArg, typeArg);
|
||||
return record ? this.recordToApiObject(record) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all DNS records for a domain
|
||||
* @param domainNameArg - The domain name to list records for
|
||||
* @returns Array of records as raw API objects
|
||||
*/
|
||||
public async listRecords(domainNameArg: string): Promise<any[]> {
|
||||
const records = await this.cfAccount.recordManager.listRecords(domainNameArg);
|
||||
return records.map((record: any) => this.recordToApiObject(record));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all DNS records of a specific type for a domain
|
||||
* @param domainNameArg - The domain name
|
||||
* @param typeArg - The DNS record type to clean
|
||||
*/
|
||||
public async cleanRecord(
|
||||
domainNameArg: string,
|
||||
typeArg: plugins.tsclass.network.TDnsRecordType,
|
||||
): Promise<void> {
|
||||
await this.cfAccount.recordManager.cleanRecords(domainNameArg, typeArg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given domain can be managed by this account
|
||||
* @param domainName - Full domain name to check (e.g., "sub.example.com")
|
||||
* @returns True if the zone for the domain exists in the account, false otherwise
|
||||
*/
|
||||
public async isDomainSupported(domainName: string): Promise<boolean> {
|
||||
try {
|
||||
// Parse out the apex/zone name from the full domain
|
||||
const domain = new plugins.smartstring.Domain(domainName);
|
||||
// List zones filtered by the zone name
|
||||
const zones = await this.cfAccount.zoneManager.listZones(domain.zoneName);
|
||||
// If any zone matches, we can manage this domain
|
||||
return Array.isArray(zones) && zones.length > 0;
|
||||
} catch (error) {
|
||||
logger.log('error', `Error checking domain support for ${domainName}: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an ACME DNS challenge for domain verification
|
||||
* @param dnsChallenge - The DNS challenge object
|
||||
*/
|
||||
public async acmeSetDnsChallenge(
|
||||
dnsChallenge: plugins.tsclass.network.IDnsChallenge,
|
||||
): Promise<void> {
|
||||
await this.cfAccount.recordManager.cleanRecords(dnsChallenge.hostName, 'TXT');
|
||||
await this.cfAccount.recordManager.createRecord(
|
||||
dnsChallenge.hostName,
|
||||
'TXT',
|
||||
dnsChallenge.challenge,
|
||||
120,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an ACME DNS challenge
|
||||
* @param dnsChallenge - The DNS challenge object
|
||||
*/
|
||||
public async acmeRemoveDnsChallenge(
|
||||
dnsChallenge: plugins.tsclass.network.IDnsChallenge,
|
||||
): Promise<void> {
|
||||
await this.cfAccount.recordManager.deleteRecord(dnsChallenge.hostName, 'TXT');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to convert CloudflareRecord instance to raw API object format
|
||||
* This ensures compatibility with the IConvenientDnsProvider interface
|
||||
*/
|
||||
private recordToApiObject(record: any): any {
|
||||
return {
|
||||
id: record.id,
|
||||
type: record.type,
|
||||
name: record.name,
|
||||
content: record.content,
|
||||
proxiable: record.proxiable,
|
||||
proxied: record.proxied,
|
||||
ttl: record.ttl,
|
||||
locked: record.locked,
|
||||
zone_id: record.zone_id,
|
||||
zone_name: record.zone_name,
|
||||
created_on: record.created_on,
|
||||
modified_on: record.modified_on,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,9 @@ export class CloudflareRecord {
|
||||
* @param apiObject Cloudflare DNS record API object
|
||||
* @returns CloudflareRecord instance
|
||||
*/
|
||||
public static createFromApiObject(apiObject: plugins.ICloudflareTypes['Record']): CloudflareRecord {
|
||||
public static createFromApiObject(
|
||||
apiObject: plugins.ICloudflareTypes['Record'],
|
||||
): CloudflareRecord {
|
||||
const record = new CloudflareRecord();
|
||||
Object.assign(record, apiObject);
|
||||
return record;
|
||||
@@ -52,7 +54,7 @@ export class CloudflareRecord {
|
||||
public async update(
|
||||
cloudflareAccount: any,
|
||||
newContent: string,
|
||||
ttl?: number
|
||||
ttl?: number,
|
||||
): Promise<CloudflareRecord> {
|
||||
logger.log('info', `Updating record ${this.name} (${this.type}) with new content`);
|
||||
|
||||
@@ -62,7 +64,7 @@ export class CloudflareRecord {
|
||||
name: this.name,
|
||||
content: newContent,
|
||||
ttl: ttl || this.ttl,
|
||||
proxied: this.proxied
|
||||
proxied: this.proxied,
|
||||
});
|
||||
|
||||
// Update this instance
|
||||
@@ -84,7 +86,7 @@ export class CloudflareRecord {
|
||||
logger.log('info', `Deleting record ${this.name} (${this.type})`);
|
||||
|
||||
await cloudflareAccount.apiAccount.dns.records.delete(this.id, {
|
||||
zone_id: this.zone_id
|
||||
zone_id: this.zone_id,
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
198
ts/cloudflare.classes.recordmanager.ts
Normal file
198
ts/cloudflare.classes.recordmanager.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import * as plugins from './cloudflare.plugins.js';
|
||||
import { logger } from './cloudflare.logger.js';
|
||||
import { CloudflareRecord } from './cloudflare.classes.record.js';
|
||||
|
||||
export class RecordManager {
|
||||
constructor(private cfAccount: any) {}
|
||||
|
||||
/**
|
||||
* Lists all DNS records for a domain
|
||||
* @param domainNameArg - The domain name to list records for
|
||||
* @returns Array of CloudflareRecord instances
|
||||
*/
|
||||
public async listRecords(domainNameArg: string): Promise<CloudflareRecord[]> {
|
||||
try {
|
||||
const domain = new plugins.smartstring.Domain(domainNameArg);
|
||||
const zoneId = await this.cfAccount.zoneManager.getZoneId(domain.zoneName);
|
||||
const records: plugins.ICloudflareTypes['Record'][] = [];
|
||||
|
||||
// Collect all records using async iterator
|
||||
for await (const record of this.cfAccount.apiAccount.dns.records.list({
|
||||
zone_id: zoneId,
|
||||
})) {
|
||||
records.push(record);
|
||||
}
|
||||
|
||||
logger.log('info', `Found ${records.length} DNS records for ${domainNameArg}`);
|
||||
|
||||
// Convert to CloudflareRecord instances
|
||||
return records.map(record => CloudflareRecord.createFromApiObject(record));
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to list records for ${domainNameArg}: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific DNS record by domain and type
|
||||
* @param domainNameArg - The domain name
|
||||
* @param typeArg - The DNS record type (A, AAAA, CNAME, TXT, etc.)
|
||||
* @returns CloudflareRecord instance or undefined if not found
|
||||
*/
|
||||
public async getRecord(
|
||||
domainNameArg: string,
|
||||
typeArg: plugins.tsclass.network.TDnsRecordType,
|
||||
): Promise<CloudflareRecord | undefined> {
|
||||
try {
|
||||
const domain = new plugins.smartstring.Domain(domainNameArg);
|
||||
const recordArray = await this.listRecords(domain.zoneName);
|
||||
|
||||
const filteredRecords = recordArray.filter((recordArg) => {
|
||||
return recordArg.type === typeArg && recordArg.name === domainNameArg;
|
||||
});
|
||||
|
||||
return filteredRecords.length > 0 ? filteredRecords[0] : undefined;
|
||||
} catch (error) {
|
||||
logger.log('error', `Error getting record for ${domainNameArg}: ${error.message}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new DNS record
|
||||
* @param domainNameArg - The domain name for the record
|
||||
* @param typeArg - The DNS record type
|
||||
* @param contentArg - The record content (IP address, CNAME target, etc.)
|
||||
* @param ttlArg - Time to live in seconds (default: 1 = automatic)
|
||||
* @returns Created CloudflareRecord instance
|
||||
*/
|
||||
public async createRecord(
|
||||
domainNameArg: string,
|
||||
typeArg: plugins.tsclass.network.TDnsRecordType,
|
||||
contentArg: string,
|
||||
ttlArg: number = 1,
|
||||
): Promise<CloudflareRecord> {
|
||||
const domain = new plugins.smartstring.Domain(domainNameArg);
|
||||
const zoneId = await this.cfAccount.zoneManager.getZoneId(domain.zoneName);
|
||||
|
||||
const response = await this.cfAccount.apiAccount.dns.records.create({
|
||||
zone_id: zoneId,
|
||||
type: typeArg as any,
|
||||
name: domain.fullName,
|
||||
content: contentArg,
|
||||
ttl: ttlArg,
|
||||
});
|
||||
|
||||
logger.log('info', `Created ${typeArg} record for ${domainNameArg}`);
|
||||
return CloudflareRecord.createFromApiObject(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing DNS record, or creates it if it doesn't exist
|
||||
* @param domainNameArg - The domain name
|
||||
* @param typeArg - The DNS record type
|
||||
* @param contentArg - The new record content
|
||||
* @param ttlArg - Time to live in seconds (default: 1 = automatic)
|
||||
* @returns Updated CloudflareRecord instance
|
||||
*/
|
||||
public async updateRecord(
|
||||
domainNameArg: string,
|
||||
typeArg: plugins.tsclass.network.TDnsRecordType,
|
||||
contentArg: string,
|
||||
ttlArg: number = 1,
|
||||
): Promise<CloudflareRecord> {
|
||||
const domain = new plugins.smartstring.Domain(domainNameArg);
|
||||
const zoneId = await this.cfAccount.zoneManager.getZoneId(domain.zoneName);
|
||||
|
||||
// Find existing record
|
||||
const existingRecord = await this.getRecord(domainNameArg, typeArg);
|
||||
|
||||
if (!existingRecord) {
|
||||
logger.log(
|
||||
'warn',
|
||||
`Record ${domainNameArg} of type ${typeArg} not found for update, creating instead`,
|
||||
);
|
||||
return this.createRecord(domainNameArg, typeArg, contentArg, ttlArg);
|
||||
}
|
||||
|
||||
// Update the record
|
||||
const updatedRecord = await this.cfAccount.apiAccount.dns.records.edit(existingRecord.id, {
|
||||
zone_id: zoneId,
|
||||
type: typeArg as any,
|
||||
name: domain.fullName,
|
||||
content: contentArg,
|
||||
ttl: ttlArg,
|
||||
});
|
||||
|
||||
logger.log('info', `Updated ${typeArg} record for ${domainNameArg}`);
|
||||
return CloudflareRecord.createFromApiObject(updatedRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a DNS record
|
||||
* @param domainNameArg - The domain name
|
||||
* @param typeArg - The DNS record type
|
||||
*/
|
||||
public async deleteRecord(
|
||||
domainNameArg: string,
|
||||
typeArg: plugins.tsclass.network.TDnsRecordType,
|
||||
): Promise<void> {
|
||||
const domain = new plugins.smartstring.Domain(domainNameArg);
|
||||
const zoneId = await this.cfAccount.zoneManager.getZoneId(domain.zoneName);
|
||||
const record = await this.getRecord(domainNameArg, typeArg);
|
||||
|
||||
if (record) {
|
||||
await this.cfAccount.apiAccount.dns.records.delete(record.id, {
|
||||
zone_id: zoneId,
|
||||
});
|
||||
logger.log('info', `Deleted ${typeArg} record for ${domainNameArg}`);
|
||||
} else {
|
||||
logger.log('warn', `Record ${domainNameArg} of type ${typeArg} not found for deletion`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all DNS records of a specific type for a domain
|
||||
* @param domainNameArg - The domain name
|
||||
* @param typeArg - The DNS record type to clean
|
||||
*/
|
||||
public async cleanRecords(
|
||||
domainNameArg: string,
|
||||
typeArg: plugins.tsclass.network.TDnsRecordType,
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.log('info', `Cleaning ${typeArg} records for ${domainNameArg}`);
|
||||
const domain = new plugins.smartstring.Domain(domainNameArg);
|
||||
const zoneId = await this.cfAccount.zoneManager.getZoneId(domain.zoneName);
|
||||
|
||||
// List all records in the zone for this domain
|
||||
const records = await this.listRecords(domain.zoneName);
|
||||
|
||||
// Only delete records matching the specified name and type
|
||||
const recordsToDelete = records.filter((recordArg) => {
|
||||
return recordArg.type === typeArg && recordArg.name === domainNameArg;
|
||||
});
|
||||
|
||||
logger.log(
|
||||
'info',
|
||||
`Found ${recordsToDelete.length} ${typeArg} records to delete for ${domainNameArg}`,
|
||||
);
|
||||
|
||||
for (const recordToDelete of recordsToDelete) {
|
||||
try {
|
||||
await this.cfAccount.apiAccount.dns.records.delete(recordToDelete.id, {
|
||||
zone_id: zoneId,
|
||||
});
|
||||
logger.log('info', `Deleted ${typeArg} record ${recordToDelete.id} for ${domainNameArg}`);
|
||||
} catch (deleteError) {
|
||||
logger.log('error', `Failed to delete record: ${deleteError.message}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log(
|
||||
'error',
|
||||
`Error cleaning ${typeArg} records for ${domainNameArg}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,11 @@ export class CloudflareWorker {
|
||||
// STATIC
|
||||
public static async fromApiObject(
|
||||
workerManager: WorkerManager,
|
||||
apiObject
|
||||
apiObject,
|
||||
): Promise<CloudflareWorker> {
|
||||
const newWorker = new CloudflareWorker(workerManager);
|
||||
Object.assign(newWorker, apiObject);
|
||||
await newWorker.getRoutes();
|
||||
await newWorker.listRoutes();
|
||||
return newWorker;
|
||||
}
|
||||
|
||||
@@ -41,9 +41,9 @@ export class CloudflareWorker {
|
||||
}
|
||||
|
||||
/**
|
||||
* gets all routes for a worker
|
||||
* Lists all routes for this worker
|
||||
*/
|
||||
public async getRoutes() {
|
||||
public async listRoutes() {
|
||||
try {
|
||||
this.routes = []; // Reset routes before fetching
|
||||
|
||||
@@ -68,7 +68,7 @@ export class CloudflareWorker {
|
||||
// Get worker routes for this zone
|
||||
const apiRoutes = [];
|
||||
for await (const route of this.workerManager.cfAccount.apiAccount.workers.routes.list({
|
||||
zone_id: zone.id
|
||||
zone_id: zone.id,
|
||||
})) {
|
||||
apiRoutes.push(route);
|
||||
}
|
||||
@@ -81,7 +81,10 @@ export class CloudflareWorker {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to get worker routes for zone ${zone.name || zone.id}: ${error.message}`);
|
||||
logger.log(
|
||||
'error',
|
||||
`Failed to get worker routes for zone ${zone.name || zone.id}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +102,7 @@ export class CloudflareWorker {
|
||||
*/
|
||||
public async setRoutes(routeArray: IWorkerRouteDefinition[]) {
|
||||
// First get all existing routes to determine what we need to create/update
|
||||
await this.getRoutes();
|
||||
await this.listRoutes();
|
||||
|
||||
for (const newRoute of routeArray) {
|
||||
// Determine whether a route is new, needs an update, or is already up to date
|
||||
@@ -120,7 +123,9 @@ export class CloudflareWorker {
|
||||
|
||||
try {
|
||||
// Get the zone ID
|
||||
const zone = await this.workerManager.cfAccount.zoneManager.getZoneByName(newRoute.zoneName);
|
||||
const zone = await this.workerManager.cfAccount.zoneManager.getZoneByName(
|
||||
newRoute.zoneName,
|
||||
);
|
||||
|
||||
if (!zone) {
|
||||
logger.log('error', `Zone ${newRoute.zoneName} not found`);
|
||||
@@ -132,7 +137,7 @@ export class CloudflareWorker {
|
||||
await this.workerManager.cfAccount.apiAccount.workers.routes.create({
|
||||
zone_id: zone.id,
|
||||
pattern: newRoute.pattern,
|
||||
script: this.id
|
||||
script: this.id,
|
||||
});
|
||||
|
||||
logger.log('info', `Created new route ${newRoute.pattern} for worker ${this.id}`);
|
||||
@@ -140,7 +145,7 @@ export class CloudflareWorker {
|
||||
await this.workerManager.cfAccount.apiAccount.workers.routes.update(existingRouteId, {
|
||||
zone_id: zone.id,
|
||||
pattern: newRoute.pattern,
|
||||
script: this.id
|
||||
script: this.id,
|
||||
});
|
||||
|
||||
logger.log('info', `Updated route ${newRoute.pattern} for worker ${this.id}`);
|
||||
@@ -151,7 +156,7 @@ export class CloudflareWorker {
|
||||
}
|
||||
|
||||
// Refresh routes after all changes
|
||||
await this.getRoutes();
|
||||
await this.listRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,15 +173,18 @@ export class CloudflareWorker {
|
||||
logger.log('info', `Updating script for worker ${this.id}`);
|
||||
|
||||
// Use the official client to update the script (upload new content)
|
||||
const updatedWorker = await this.workerManager.cfAccount.apiAccount.workers.scripts.content.update(this.id, {
|
||||
// Build params as any to include the script form part without TS errors
|
||||
const updateParams: any = {
|
||||
account_id: this.workerManager.cfAccount.preselectedAccountId,
|
||||
// name the multipart part for the updated script code
|
||||
metadata: { body_part: 'script' },
|
||||
/* header to indicate which part contains the script */
|
||||
'CF-WORKER-BODY-PART': 'script',
|
||||
// include the new script as a form part named 'script'
|
||||
script: scriptContent,
|
||||
});
|
||||
};
|
||||
updateParams['CF-WORKER-BODY-PART'] = 'script';
|
||||
updateParams['script'] = scriptContent;
|
||||
const updatedWorker =
|
||||
await this.workerManager.cfAccount.apiAccount.workers.scripts.content.update(
|
||||
this.id,
|
||||
updateParams,
|
||||
);
|
||||
|
||||
// Update this instance with new data
|
||||
if (updatedWorker && typeof updatedWorker === 'object') {
|
||||
@@ -207,7 +215,7 @@ export class CloudflareWorker {
|
||||
|
||||
// Use the official client to delete the worker
|
||||
await this.workerManager.cfAccount.apiAccount.workers.scripts.delete(this.id, {
|
||||
account_id: this.workerManager.cfAccount.preselectedAccountId
|
||||
account_id: this.workerManager.cfAccount.preselectedAccountId,
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -23,15 +23,14 @@ export class WorkerManager {
|
||||
|
||||
try {
|
||||
// Use the official client to create/update the worker (upload script content)
|
||||
await this.cfAccount.apiAccount.workers.scripts.content.update(workerName, {
|
||||
// Build params as any to include the script form part without TS errors
|
||||
const contentParams: any = {
|
||||
account_id: this.cfAccount.preselectedAccountId,
|
||||
// name the multipart part for the script code
|
||||
metadata: { body_part: 'script' },
|
||||
/* header to indicate which part contains the script */
|
||||
'CF-WORKER-BODY-PART': 'script',
|
||||
// include the actual script as a form part named 'script'
|
||||
script: workerScript,
|
||||
});
|
||||
};
|
||||
contentParams['CF-WORKER-BODY-PART'] = 'script';
|
||||
contentParams['script'] = workerScript;
|
||||
await this.cfAccount.apiAccount.workers.scripts.content.update(workerName, contentParams);
|
||||
|
||||
// Create a new worker instance
|
||||
const worker = new CloudflareWorker(this);
|
||||
@@ -40,7 +39,7 @@ export class WorkerManager {
|
||||
|
||||
// Initialize the worker and get its routes
|
||||
try {
|
||||
await worker.getRoutes();
|
||||
await worker.listRoutes();
|
||||
} catch (routeError) {
|
||||
logger.log('warn', `Failed to get routes for worker ${workerName}: ${routeError.message}`);
|
||||
// Continue anyway since the worker was created
|
||||
@@ -66,7 +65,7 @@ export class WorkerManager {
|
||||
try {
|
||||
// Get the worker script using the official client
|
||||
const workerScript = await this.cfAccount.apiAccount.workers.scripts.get(workerName, {
|
||||
account_id: this.cfAccount.preselectedAccountId
|
||||
account_id: this.cfAccount.preselectedAccountId,
|
||||
});
|
||||
|
||||
// Create a new worker instance
|
||||
@@ -80,7 +79,7 @@ export class WorkerManager {
|
||||
|
||||
// Initialize the worker and get its routes
|
||||
try {
|
||||
await worker.getRoutes();
|
||||
await worker.listRoutes();
|
||||
} catch (routeError) {
|
||||
logger.log('warn', `Failed to get routes for worker ${workerName}: ${routeError.message}`);
|
||||
// Continue anyway since we found the worker
|
||||
@@ -97,7 +96,7 @@ export class WorkerManager {
|
||||
* Lists all worker scripts
|
||||
* @returns Array of worker scripts
|
||||
*/
|
||||
public async listWorkerScripts() {
|
||||
public async listWorkers() {
|
||||
if (!this.cfAccount.preselectedAccountId) {
|
||||
throw new Error('No account selected. Please select it first on the account.');
|
||||
}
|
||||
@@ -119,9 +118,9 @@ export class WorkerManager {
|
||||
logger.log('warn', `Error while listing workers with async iterator: ${error.message}`);
|
||||
|
||||
// Try alternative approach if the async iterator fails
|
||||
const result = await this.cfAccount.apiAccount.workers.scripts.list({
|
||||
const result = (await this.cfAccount.apiAccount.workers.scripts.list({
|
||||
account_id: this.cfAccount.preselectedAccountId,
|
||||
}) as any;
|
||||
})) as any;
|
||||
|
||||
// Check if the result has a 'result' property (older API response format)
|
||||
if (result && result.result && Array.isArray(result.result)) {
|
||||
@@ -150,7 +149,7 @@ export class WorkerManager {
|
||||
|
||||
try {
|
||||
await this.cfAccount.apiAccount.workers.scripts.delete(workerName, {
|
||||
account_id: this.cfAccount.preselectedAccountId
|
||||
account_id: this.cfAccount.preselectedAccountId,
|
||||
});
|
||||
logger.log('info', `Worker '${workerName}' deleted successfully`);
|
||||
return true;
|
||||
|
||||
@@ -34,7 +34,7 @@ export class CloudflareZone {
|
||||
*/
|
||||
public static createFromApiObject(
|
||||
apiObject: plugins.ICloudflareTypes['Zone'],
|
||||
cfAccount?: CloudflareAccount
|
||||
cfAccount?: CloudflareAccount,
|
||||
): CloudflareZone {
|
||||
const cloudflareZone = new CloudflareZone();
|
||||
Object.assign(cloudflareZone, apiObject);
|
||||
@@ -62,7 +62,7 @@ export class CloudflareZone {
|
||||
*/
|
||||
public async enableDevelopmentMode(
|
||||
cfAccount?: CloudflareAccount,
|
||||
duration: number = 10800
|
||||
duration: number = 10800,
|
||||
): Promise<CloudflareZone> {
|
||||
const account = cfAccount || this.cfAccount;
|
||||
if (!account) {
|
||||
@@ -76,7 +76,7 @@ export class CloudflareZone {
|
||||
// We'll use the request method for this specific case
|
||||
await account.request('PATCH', `/zones/${this.id}/settings/development_mode`, {
|
||||
value: 'on',
|
||||
time: duration
|
||||
time: duration,
|
||||
});
|
||||
|
||||
this.development_mode = duration;
|
||||
@@ -104,7 +104,7 @@ export class CloudflareZone {
|
||||
// The official client doesn't have a direct method for development mode
|
||||
// We'll use the request method for this specific case
|
||||
await account.request('PATCH', `/zones/${this.id}/settings/development_mode`, {
|
||||
value: 'off'
|
||||
value: 'off',
|
||||
});
|
||||
|
||||
this.development_mode = 0;
|
||||
@@ -131,7 +131,7 @@ export class CloudflareZone {
|
||||
try {
|
||||
await account.apiAccount.cache.purge({
|
||||
zone_id: this.id,
|
||||
purge_everything: true
|
||||
purge_everything: true,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -161,7 +161,7 @@ export class CloudflareZone {
|
||||
try {
|
||||
await account.apiAccount.cache.purge({
|
||||
zone_id: this.id,
|
||||
files: urls
|
||||
files: urls,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -189,7 +189,7 @@ export class CloudflareZone {
|
||||
}
|
||||
|
||||
// If they're different, and current nameservers are Cloudflare's
|
||||
return this.name_servers.some(ns => ns.includes('cloudflare'));
|
||||
return this.name_servers.some((ns) => ns.includes('cloudflare'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,7 +205,7 @@ export class CloudflareZone {
|
||||
vanity_name_servers: string[];
|
||||
type: 'full' | 'partial' | 'secondary';
|
||||
}>,
|
||||
cfAccount?: CloudflareAccount
|
||||
cfAccount?: CloudflareAccount,
|
||||
): Promise<CloudflareZone> {
|
||||
const account = cfAccount || this.cfAccount;
|
||||
if (!account) {
|
||||
@@ -219,7 +219,7 @@ export class CloudflareZone {
|
||||
const response: { result: interfaces.ICflareZone } = await account.request(
|
||||
'PATCH',
|
||||
`/zones/${this.id}`,
|
||||
settings
|
||||
settings,
|
||||
);
|
||||
|
||||
Object.assign(this, response.result);
|
||||
|
||||
@@ -12,11 +12,11 @@ export class ZoneManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all zones, optionally filtered by name
|
||||
* Lists all zones, optionally filtered by name
|
||||
* @param zoneName Optional zone name to filter by
|
||||
* @returns Array of CloudflareZone instances
|
||||
*/
|
||||
public async getZones(zoneName?: string): Promise<CloudflareZone[]> {
|
||||
public async listZones(zoneName?: string): Promise<CloudflareZone[]> {
|
||||
try {
|
||||
const options: any = { per_page: 50 };
|
||||
|
||||
@@ -30,21 +30,41 @@ export class ZoneManager {
|
||||
zones.push(zone);
|
||||
}
|
||||
|
||||
return zones.map(zone => CloudflareZone.createFromApiObject(zone, this.cfAccount));
|
||||
return zones.map((zone) => CloudflareZone.createFromApiObject(zone, this.cfAccount));
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to fetch zones: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the zone ID for a domain name
|
||||
* @param domainName Domain name to get the zone ID for
|
||||
* @returns Zone ID string
|
||||
* @throws Error if domain is not found in this account
|
||||
*/
|
||||
public async getZoneId(domainName: string): Promise<string> {
|
||||
const domain = new plugins.smartstring.Domain(domainName);
|
||||
const zoneArray = await this.listZones(domain.zoneName);
|
||||
const filteredResponse = zoneArray.filter((zoneArg) => {
|
||||
return zoneArg.name === domainName;
|
||||
});
|
||||
if (filteredResponse.length >= 1) {
|
||||
return filteredResponse[0].id;
|
||||
} else {
|
||||
logger.log('error', `the domain ${domainName} does not appear to be in this account!`);
|
||||
throw new Error(`the domain ${domainName} does not appear to be in this account!`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single zone by name
|
||||
* @param zoneName Zone name to find
|
||||
* @returns CloudflareZone instance or undefined if not found
|
||||
*/
|
||||
public async getZoneByName(zoneName: string): Promise<CloudflareZone | undefined> {
|
||||
const zones = await this.getZones(zoneName);
|
||||
return zones.find(zone => zone.name === zoneName);
|
||||
const zones = await this.listZones(zoneName);
|
||||
return zones.find((zone) => zone.name === zoneName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,7 +77,7 @@ export class ZoneManager {
|
||||
// Use the request method instead of the zones.get method to avoid type issues
|
||||
const response: { result: interfaces.ICflareZone } = await this.cfAccount.request(
|
||||
'GET',
|
||||
`/zones/${zoneId}`
|
||||
`/zones/${zoneId}`,
|
||||
);
|
||||
|
||||
return CloudflareZone.createFromApiObject(response.result as any, this.cfAccount);
|
||||
@@ -77,7 +97,7 @@ export class ZoneManager {
|
||||
public async createZone(
|
||||
zoneName: string,
|
||||
jumpStart: boolean = false,
|
||||
accountId?: string
|
||||
accountId?: string,
|
||||
): Promise<CloudflareZone | undefined> {
|
||||
const useAccountId = accountId || this.cfAccount.preselectedAccountId;
|
||||
|
||||
@@ -95,8 +115,8 @@ export class ZoneManager {
|
||||
{
|
||||
name: zoneName,
|
||||
jump_start: jumpStart,
|
||||
account: { id: useAccountId }
|
||||
}
|
||||
account: { id: useAccountId },
|
||||
},
|
||||
);
|
||||
|
||||
return CloudflareZone.createFromApiObject(response.result as any, this.cfAccount);
|
||||
@@ -130,8 +150,8 @@ export class ZoneManager {
|
||||
* @returns True if the zone exists
|
||||
*/
|
||||
public async zoneExists(zoneName: string): Promise<boolean> {
|
||||
const zones = await this.getZones(zoneName);
|
||||
return zones.some(zone => zone.name === zoneName);
|
||||
const zones = await this.listZones(zoneName);
|
||||
return zones.some((zone) => zone.name === zoneName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,8 +168,8 @@ export class ZoneManager {
|
||||
'PATCH',
|
||||
`/zones/${zoneId}`,
|
||||
{
|
||||
status: 'active'
|
||||
}
|
||||
status: 'active',
|
||||
},
|
||||
);
|
||||
|
||||
return CloudflareZone.createFromApiObject(response.result as any, this.cfAccount);
|
||||
@@ -171,7 +191,7 @@ export class ZoneManager {
|
||||
// For this specific endpoint, we'll use the request method
|
||||
const response: { result: interfaces.ICflareZone } = await this.cfAccount.request(
|
||||
'PUT',
|
||||
`/zones/${zoneId}/activation_check`
|
||||
`/zones/${zoneId}/activation_check`,
|
||||
);
|
||||
|
||||
return CloudflareZone.createFromApiObject(response.result as any, this.cfAccount);
|
||||
@@ -180,4 +200,18 @@ export class ZoneManager {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Purges all cached files for a zone
|
||||
* @param domainName Domain name to purge cache for
|
||||
*/
|
||||
public async purgeZone(domainName: string): Promise<void> {
|
||||
const domain = new plugins.smartstring.Domain(domainName);
|
||||
const zoneId = await this.getZoneId(domain.zoneName);
|
||||
await this.cfAccount.apiAccount.cache.purge({
|
||||
zone_id: zoneId,
|
||||
purge_everything: true,
|
||||
});
|
||||
logger.log('info', `Purged cache for zone ${domainName}`);
|
||||
}
|
||||
}
|
||||
@@ -49,9 +49,23 @@ export class CloudflareUtils {
|
||||
*/
|
||||
public static isValidRecordType(type: string): boolean {
|
||||
const validTypes: plugins.tsclass.network.TDnsRecordType[] = [
|
||||
'A', 'AAAA', 'CNAME', 'TXT', 'SRV', 'LOC', 'MX',
|
||||
'NS', 'CAA', 'CERT', 'DNSKEY', 'DS', 'NAPTR', 'SMIMEA',
|
||||
'SSHFP', 'TLSA', 'URI'
|
||||
'A',
|
||||
'AAAA',
|
||||
'CNAME',
|
||||
'TXT',
|
||||
'SRV',
|
||||
'LOC',
|
||||
'MX',
|
||||
'NS',
|
||||
'CAA',
|
||||
'CERT',
|
||||
'DNSKEY',
|
||||
'DS',
|
||||
'NAPTR',
|
||||
'SMIMEA',
|
||||
'SSHFP',
|
||||
'TLSA',
|
||||
'URI',
|
||||
// Note: SPF has been removed as it's not in TDnsRecordType
|
||||
];
|
||||
return validTypes.includes(type as any);
|
||||
@@ -108,7 +122,10 @@ export class CloudflareUtils {
|
||||
* @returns Combined results from all pages
|
||||
*/
|
||||
public static async paginateResults<T>(
|
||||
makeRequest: (page: number, perPage: number) => Promise<{ result: T[], result_info: { total_pages: number } }>
|
||||
makeRequest: (
|
||||
page: number,
|
||||
perPage: number,
|
||||
) => Promise<{ result: T[]; result_info: { total_pages: number } }>,
|
||||
): Promise<T[]> {
|
||||
const perPage = 50; // Cloudflare's maximum
|
||||
let page = 1;
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
export { CloudflareAccount } from './cloudflare.classes.account.js';
|
||||
export { CloudflareWorker, type IWorkerRoute, type IWorkerRouteDefinition } from './cloudflare.classes.worker.js';
|
||||
export {
|
||||
CloudflareWorker,
|
||||
type IWorkerRoute,
|
||||
type IWorkerRouteDefinition,
|
||||
} from './cloudflare.classes.worker.js';
|
||||
export { WorkerManager } from './cloudflare.classes.workermanager.js';
|
||||
export { CloudflareRecord, type ICloudflareRecordInfo } from './cloudflare.classes.record.js';
|
||||
export { RecordManager } from './cloudflare.classes.recordmanager.js';
|
||||
export { CloudflareZone } from './cloudflare.classes.zone.js';
|
||||
export { ZoneManager } from './cloudflare.classes.zonemanager.js';
|
||||
export { ConvenientDnsProvider } from './cloudflare.classes.convenientdnsprovider.js';
|
||||
export { CloudflareUtils } from './cloudflare.utils.js';
|
||||
export { commitinfo } from './00_commitinfo_data.js';
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true
|
||||
"verbatimModuleSyntax": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {}
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
|
||||
Reference in New Issue
Block a user