Compare commits

...

14 Commits

Author SHA1 Message Date
609bcbc6db v7.1.0
Some checks failed
Default (tags) / security (push) Failing after 1m42s
Default (tags) / test (push) Failing after 1m43s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-18 20:47:28 +00:00
2da9370742 feat(cloudflare): Release 7.0.0 — manager-based API, ConvenientDnsProvider, improved utils and worker/zone/record management 2025-11-18 20:47:28 +00:00
18afafd3b3 v7.0.0
Some checks failed
Default (tags) / security (push) Failing after 1m35s
Default (tags) / test (push) Failing after 41s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-18 20:39:08 +00:00
5ce1520e2b BREAKING CHANGE(core): Introduce RecordManager and ConvenientDnsProvider; rename list/get methods for consistent API and deprecate convenience namespace 2025-11-18 20:39:08 +00:00
39d53da4e6 v6.4.3
Some checks failed
Default (tags) / security (push) Successful in 34s
Default (tags) / test (push) Failing after 44s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-17 23:22:00 +00:00
002ac3ae01 fix(cloudflare.plugins): Switch to smartrequest namespace export and improve request typing and JSON parsing 2025-11-17 23:22:00 +00:00
0184371635 v6.4.2
Some checks failed
Default (tags) / security (push) Successful in 42s
Default (tags) / test (push) Failing after 37s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-17 23:10:24 +00:00
038f56b0ce fix(core): Switch to SmartRequest fluent API and improve Cloudflare API request handling 2025-11-17 23:10:24 +00:00
1c0a20ac99 6.4.1
Some checks failed
Default (tags) / security (push) Successful in 41s
Default (tags) / test (push) Failing after 37s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-30 12:09:13 +00:00
36d9db4332 fix(ci): Update CI workflows, repository URL, and apply minor code formatting fixes 2025-04-30 12:09:13 +00:00
5d77214222 6.4.0 2025-04-30 12:05:23 +00:00
f27eaa0e82 feat(CloudflareAccount): Bump dependency versions and add domain support check in CloudflareAccount 2025-04-30 12:05:23 +00:00
4c16e0263a 6.3.2 2025-04-26 12:42:24 +00:00
d8ca3dc115 fix(worker): Refactor worker script update and creation to use intermediate parameter objects 2025-04-26 12:42:24 +00:00
23 changed files with 12212 additions and 4846 deletions

View 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

View 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
View File

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

View File

@@ -1,5 +1,67 @@
# Changelog # 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) ## 2025-04-26 - 6.3.1 - fix(core)
Improve nested DNS record management and worker script multipart handling Improve nested DNS record management and worker script multipart handling

7414
deno.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,20 @@
{ {
"name": "@apiclient.xyz/cloudflare", "name": "@apiclient.xyz/cloudflare",
"version": "6.3.1", "version": "7.1.0",
"private": false, "private": false,
"description": "A TypeScript client for managing Cloudflare accounts, zones, DNS records, and workers with ease.", "description": "A TypeScript client for managing Cloudflare accounts, zones, DNS records, and workers with ease.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts", "typings": "dist_ts/index.d.ts",
"type": "module", "type": "module",
"scripts": { "scripts": {
"test": "(tstest test/)", "test": "(tstest test/ --verbose --timeout 600)",
"build": "(tsbuild --web --allowimplicitany)", "build": "(tsbuild --web --allowimplicitany)",
"buildDocs": "tsdoc", "buildDocs": "tsdoc",
"updateOpenapi": "openapi-typescript https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.yaml --output ts/openapi.spec.ts" "updateOpenapi": "openapi-typescript https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.yaml --output ts/openapi.spec.ts"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://gitlab.com/pushrocks/cflare.git" "url": "https://gitlab.com/mojoio/cloudflare.git"
}, },
"keywords": [ "keywords": [
"Cloudflare", "Cloudflare",
@@ -31,26 +31,25 @@
"author": "Lossless GmbH", "author": "Lossless GmbH",
"license": "MIT", "license": "MIT",
"bugs": { "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": { "dependencies": {
"@push.rocks/smartdelay": "^3.0.1", "@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/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^2.0.23", "@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartstring": "^4.0.5", "@push.rocks/smartstring": "^4.1.0",
"@tsclass/tsclass": "^5.0.0", "@tsclass/tsclass": "^9.3.0",
"cloudflare": "^4.2.0" "cloudflare": "^5.2.0"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.2.7", "@git.zone/tsbuild": "^3.1.0",
"@git.zone/tsrun": "^1.3.3", "@git.zone/tsrun": "^2.0.0",
"@git.zone/tstest": "^1.0.96", "@git.zone/tstest": "^2.8.2",
"@push.rocks/qenv": "^6.1.0", "@push.rocks/qenv": "^6.1.3",
"@push.rocks/tapbundle": "^5.6.0", "@types/node": "^22.15.3",
"@types/node": "^22.13.10", "openapi-typescript": "^7.10.1"
"openapi-typescript": "^7.6.1"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",
@@ -67,5 +66,8 @@
"browserslist": [ "browserslist": [
"last 1 chrome versions" "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

File diff suppressed because it is too large Load Diff

644
readme.md
View File

@@ -1,399 +1,493 @@
# @apiclient.xyz/cloudflare # @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
[![npm version](https://badge.fury.io/js/%40apiclient.xyz%2Fcloudflare.svg)](https://www.npmjs.com/package/@apiclient.xyz/cloudflare) [![npm version](https://badge.fury.io/js/%40apiclient.xyz%2Fcloudflare.svg)](https://www.npmjs.com/package/@apiclient.xyz/cloudflare)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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 ## ✨ Why This Library?
- **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
## 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 ```bash
# Using npm
npm install @apiclient.xyz/cloudflare npm install @apiclient.xyz/cloudflare
# Using yarn
yarn add @apiclient.xyz/cloudflare
# Using pnpm
pnpm add @apiclient.xyz/cloudflare
``` ```
## Quick Start
```typescript ```typescript
import * as cflare from '@apiclient.xyz/cloudflare'; import { CloudflareAccount } from '@apiclient.xyz/cloudflare';
// Initialize with your API token const cf = new CloudflareAccount('your-api-token');
const cfAccount = new cflare.CloudflareAccount('your-cloudflare-api-token');
// Use convenience methods for quick operations // Manage DNS records with ease
await cfAccount.convenience.createRecord('subdomain.example.com', 'A', '192.0.2.1', 3600); 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 // Work with zones
const zone = await cfAccount.zoneManager.getZoneByName('example.com'); const zones = await cf.zoneManager.listZones();
await zone.purgeCache(); 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 ```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 // If you have multiple accounts, select one
await cfAccount.preselectAccountByName('My Company Account'); await cf.preselectAccountByName('Production Account');
// List all accounts you have access to
const myAccounts = await cfAccount.listAccounts();
``` ```
### Zone Management ### 🌐 Zone Management
Zones represent your domains in Cloudflare. Zones are your domains in Cloudflare.
```typescript ```typescript
// Get all zones in your account // List all zones
const allZones = await cfAccount.convenience.listZones(); const zones = await cf.zoneManager.listZones();
// Get a specific zone by domain name // Find a specific zone
const myZone = await cfAccount.zoneManager.getZoneByName('example.com'); const zone = await cf.zoneManager.getZoneByName('example.com');
// Get zone ID directly // Get zone ID for API calls
const zoneId = await cfAccount.convenience.getZoneId('example.com'); const zoneId = await cf.zoneManager.getZoneId('example.com');
// Create a new zone // Create a new zone
const newZone = await cfAccount.zoneManager.createZone('newdomain.com'); await cf.zoneManager.createZone('newdomain.com');
// Purge cache for an entire zone // Purge entire zone cache
await cfAccount.convenience.purgeZone('example.com'); await cf.zoneManager.purgeZone('example.com');
// Or using the zone object
await myZone.purgeCache();
// Purge specific URLs // Work with zone objects
await myZone.purgeUrls(['https://example.com/css/styles.css', 'https://example.com/js/app.js']); if (await zone.isActive()) {
await zone.purgeCache();
await zone.purgeUrls(['https://example.com/css/app.css']);
// Enable/disable development mode // Development mode (bypasses cache for 3 hours)
await myZone.enableDevelopmentMode(); // Enables dev mode for 3 hours await zone.enableDevelopmentMode();
await myZone.disableDevelopmentMode(); await zone.disableDevelopmentMode();
}
// Check zone status
const isActive = await myZone.isActive();
const usingCfNameservers = await myZone.isUsingCloudflareNameservers();
``` ```
### DNS Record Management ### 📝 DNS Records
Manage DNS records for your domains with ease. The `recordManager` gives you complete control over DNS records.
```typescript ```typescript
// List all DNS records for a domain // List all records for a domain
const allRecords = await cfAccount.convenience.listRecords('example.com'); const records = await cf.recordManager.listRecords('example.com');
// Create a new DNS record // Create different record types
await cfAccount.convenience.createRecord('api.example.com', 'A', '192.0.2.1', 3600); 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 // Get a specific record
await cfAccount.convenience.createRecord('www.example.com', 'CNAME', 'example.com', 3600); const record = await cf.recordManager.getRecord('api.example.com', 'A');
console.log(record.content); // "192.0.2.1"
// Get a specific DNS record // Update or create (upsert behavior)
const record = await cfAccount.convenience.getRecord('api.example.com', 'A'); await cf.recordManager.updateRecord('api.example.com', 'A', '203.0.113.1');
// Update a DNS record (automatically creates it if it doesn't exist) // Delete a record
await cfAccount.convenience.updateRecord('api.example.com', 'A', '192.0.2.2', 3600); await cf.recordManager.deleteRecord('old.example.com', 'A');
// Remove a specific DNS record // Clean up all records of a type (useful for wildcard cleanup)
await cfAccount.convenience.removeRecord('api.example.com', 'A'); await cf.recordManager.cleanRecords('example.com', 'TXT');
// 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'
});
``` ```
### Workers Management ### ⚙️ Cloudflare Workers
Create and manage Cloudflare Workers with full TypeScript support. Deploy and manage serverless functions at the edge.
```typescript ```typescript
// Create or update a worker // Worker code
const workerScript = ` const workerScript = `
addEventListener('fetch', event => { 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 // List all workers
const allWorkers = await cfAccount.workerManager.listWorkerScripts(); const workers = await cf.workerManager.listWorkers();
// Get an existing worker // Get existing worker
const existingWorker = await cfAccount.workerManager.getWorker('my-worker'); const existingWorker = await cf.workerManager.getWorker('my-api-worker');
// Set routes for a worker // Set up routes
await worker.setRoutes([ await worker.setRoutes([
{ { zoneName: 'example.com', pattern: 'api.example.com/*' },
zoneName: 'example.com', { zoneName: 'example.com', pattern: 'example.com/api/*' }
pattern: 'https://api.example.com/*'
},
{
zoneName: 'example.com',
pattern: 'https://app.example.com/api/*'
}
]); ]);
// Get all routes for a worker // Check worker routes
const routes = await worker.getRoutes(); await worker.listRoutes();
console.log(worker.routes); // Array of routes
// Update a worker's script // Update worker script
await worker.updateScript(` await worker.updateScript(updatedWorkerScript);
addEventListener('fetch', event => {
event.respondWith(new Response('Updated worker content!'))
})`);
// Delete a worker // Delete worker
await worker.delete(); await worker.delete();
// Or using the worker manager // or
await cfAccount.workerManager.deleteWorker('my-worker'); 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 ```typescript
import * as cflare from '@apiclient.xyz/cloudflare'; const dnsProvider = cf.getConvenientDnsProvider();
async function manageCloudflare() { // Set DNS challenge
try { await dnsProvider.acmeSetDnsChallenge({
// Initialize with API token from environment variable hostName: '_acme-challenge.example.com',
const cfAccount = new cflare.CloudflareAccount(process.env.CLOUDFLARE_API_TOKEN); 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 ### CloudflareAccount
The main entry point for all Cloudflare operations. Main entry point for all operations.
```typescript ```typescript
class CloudflareAccount { class CloudflareAccount {
constructor(apiToken: string); constructor(apiToken: string)
// Account management
async listAccounts(): Promise<Array<ICloudflareTypes['Account']>>;
async preselectAccountByName(accountName: string): Promise<void>;
// Managers // Managers
readonly zoneManager: ZoneManager; readonly recordManager: RecordManager
readonly workerManager: WorkerManager; readonly zoneManager: ZoneManager
readonly workerManager: WorkerManager
// Official Cloudflare client
readonly apiAccount: cloudflare.Cloudflare; // Methods
async preselectAccountByName(name: string): Promise<void>
// Convenience namespace with helper methods getConvenientDnsProvider(): ConvenientDnsProvider
readonly convenience: {
// Zone operations // Official Cloudflare SDK client (for advanced use)
listZones(domainName?: string): Promise<CloudflareZone[]>; readonly apiAccount: Cloudflare
getZoneId(domainName: string): Promise<string>; }
purgeZone(domainName: string): Promise<void>; ```
// DNS operations ### RecordManager
listRecords(domainName: string): Promise<CloudflareRecord[]>;
getRecord(domainName: string, recordType: string): Promise<CloudflareRecord | undefined>; DNS record management with clean, predictable methods.
createRecord(domainName: string, recordType: string, content: string, ttl?: number): Promise<any>;
updateRecord(domainName: string, recordType: string, content: string, ttl?: number): Promise<any>; ```typescript
removeRecord(domainName: string, recordType: string): Promise<any>; class RecordManager {
cleanRecord(domainName: string, recordType: string): Promise<void>; async listRecords(domain: string): Promise<CloudflareRecord[]>
async getRecord(domain: string, type: string): Promise<CloudflareRecord | undefined>
// ACME operations async createRecord(domain: string, type: string, content: string, ttl?: number): Promise<CloudflareRecord>
acmeSetDnsChallenge(dnsChallenge: IDnsChallenge): Promise<any>; async updateRecord(domain: string, type: string, content: string, ttl?: number): Promise<CloudflareRecord>
acmeRemoveDnsChallenge(dnsChallenge: IDnsChallenge): Promise<any>; 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 ### CloudflareZone
Represents a Cloudflare zone (domain). Zone instance with convenience methods.
```typescript ```typescript
class CloudflareZone { class CloudflareZone {
// Properties readonly id: string
readonly id: string; readonly name: string
readonly name: string; readonly status: string
readonly status: string;
readonly paused: boolean; async purgeCache(): Promise<void>
readonly type: string; async purgeUrls(urls: string[]): Promise<void>
readonly nameServers: string[]; async isActive(): Promise<boolean>
async isUsingCloudflareNameservers(): Promise<boolean>
// Methods async enableDevelopmentMode(): Promise<void>
async purgeCache(): Promise<any>; async disableDevelopmentMode(): Promise<void>
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>;
} }
``` ```
### CloudflareRecord ### WorkerManager
Represents a DNS record. Cloudflare Workers deployment and management.
```typescript ```typescript
class CloudflareRecord { class WorkerManager {
// Properties async listWorkers(): Promise<WorkerScript[]>
readonly id: string; async getWorker(name: string): Promise<CloudflareWorker | undefined>
readonly type: string; async createWorker(name: string, script: string): Promise<CloudflareWorker>
readonly name: string; async deleteWorker(name: string): Promise<boolean>
readonly content: string;
readonly ttl: number;
readonly proxied: boolean;
// Methods
async update(content: string, ttl?: number): Promise<any>;
async delete(): Promise<any>;
} }
``` ```
### CloudflareWorker ### CloudflareWorker
Represents a Cloudflare Worker. Worker instance for route management and updates.
```typescript ```typescript
class CloudflareWorker { class CloudflareWorker {
// Properties readonly id: string
readonly id: string; readonly script: string
readonly script: string; readonly routes: WorkerRoute[]
readonly routes: IWorkerRoute[];
async listRoutes(): Promise<void>
// Methods async setRoutes(routes: WorkerRouteDefinition[]): Promise<void>
async getRoutes(): Promise<IWorkerRoute[]>; async updateScript(script: string): Promise<CloudflareWorker>
async setRoutes(routes: Array<IWorkerRouteDefinition>): Promise<void>; async delete(): Promise<boolean>
async updateScript(scriptContent: string): Promise<CloudflareWorker>;
async delete(): Promise<boolean>;
} }
interface IWorkerRouteDefinition { interface WorkerRouteDefinition {
zoneName: string; zoneName: string
pattern: string; pattern: string
} }
``` ```
## Utility Functions ### ConvenientDnsProvider
The library includes helpful utility functions: Adapter for third-party modules requiring `IConvenientDnsProvider`.
```typescript ```typescript
// Validate a domain name class ConvenientDnsProvider implements IConvenientDnsProvider {
CloudflareUtils.isValidDomain('example.com'); // true async createRecord(domain: string, type: string, content: string, ttl?: number): Promise<any>
async updateRecord(domain: string, type: string, content: string, ttl?: number): Promise<any>
// Extract zone name from a domain async removeRecord(domain: string, type: string): Promise<any>
CloudflareUtils.getZoneName('subdomain.example.com'); // 'example.com' async getRecord(domain: string, type: string): Promise<any | undefined>
async listRecords(domain: string): Promise<any[]>
// Validate a record type async cleanRecord(domain: string, type: string): Promise<void>
CloudflareUtils.isValidRecordType('A'); // true async isDomainSupported(domain: string): Promise<boolean>
async acmeSetDnsChallenge(challenge: DnsChallenge): Promise<void>
// Format URL for cache purging async acmeRemoveDnsChallenge(challenge: DnsChallenge): Promise<void>
CloudflareUtils.formatUrlForPurge('example.com/page'); // 'https://example.com/page' }
// Format TTL value
CloudflareUtils.formatTtl(3600); // '1 hour'
``` ```
## 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 Version 7.0 introduces a cleaner architecture while maintaining **full backward compatibility**.
- **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
## 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 ```bash
npm run build # Install dependencies
# or pnpm install
pnpm run build
# Build
pnpm build
# Run tests
pnpm test
``` ```
To run tests: ## 📝 TypeScript Configuration
```bash This library is written in TypeScript and provides complete type definitions. No `@types` package needed!
npm test
# or ```typescript
pnpm run test 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.

View File

@@ -1,5 +1,5 @@
// tslint:disable-next-line: no-implicit-dependencies // 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 // tslint:disable-next-line: no-implicit-dependencies
import { Qenv } from '@push.rocks/qenv'; import { Qenv } from '@push.rocks/qenv';
@@ -14,7 +14,9 @@ let testZoneName = `test-zone-${randomPrefix}.com`;
// Basic initialization tests // Basic initialization tests
tap.test('should create a valid instance of CloudflareAccount', async () => { 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).toBeTypeOf('object');
expect(testCloudflareAccount.apiAccount).toBeTypeOf('object'); expect(testCloudflareAccount.apiAccount).toBeTypeOf('object');
}); });
@@ -22,12 +24,12 @@ tap.test('should create a valid instance of CloudflareAccount', async () => {
tap.test('should preselect an account', async () => { tap.test('should preselect an account', async () => {
await testCloudflareAccount.preselectAccountByName('Sandbox Account'); await testCloudflareAccount.preselectAccountByName('Sandbox Account');
expect(testCloudflareAccount.preselectedAccountId).toBeTypeOf('string'); expect(testCloudflareAccount.preselectedAccountId).toBeTypeOf('string');
}) });
// Zone management tests // Zone management tests
tap.test('.listZones() -> should list zones in account', async (tools) => { tap.test('.listZones() -> should list zones in account', async (tools) => {
tools.timeout(600000); tools.timeout(600000);
try { try {
const result = await testCloudflareAccount.convenience.listZones(); const result = await testCloudflareAccount.convenience.listZones();
// The test expects an array, but the current API might return an object with a result property // The test expects an array, but the current API might return an object with a result property
@@ -66,7 +68,7 @@ tap.test('ZoneManager: should get zone by name', async (tools) => {
// DNS record tests // DNS record tests
tap.test('.listRecords(domainName) -> should list records for domain', async (tools) => { tap.test('.listRecords(domainName) -> should list records for domain', async (tools) => {
tools.timeout(600000); tools.timeout(600000);
try { try {
const records = await testCloudflareAccount.convenience.listRecords('bleu.de'); const records = await testCloudflareAccount.convenience.listRecords('bleu.de');
// The test expects an array, but the current API might return an object with a result property // The test expects an array, but the current API might return an object with a result property
@@ -94,7 +96,7 @@ tap.test('should create A record for subdomain', async (tools) => {
subdomain, subdomain,
'A', 'A',
'127.0.0.1', '127.0.0.1',
120 120,
); );
expect(result).toBeTypeOf('object'); expect(result).toBeTypeOf('object');
console.log(`Created A record for ${subdomain}`); console.log(`Created A record for ${subdomain}`);
@@ -107,7 +109,7 @@ tap.test('should create CNAME record for subdomain', async (tools) => {
subdomain, subdomain,
'CNAME', 'CNAME',
'example.com', 'example.com',
120 120,
); );
expect(result).toBeTypeOf('object'); expect(result).toBeTypeOf('object');
console.log(`Created CNAME record for ${subdomain}`); console.log(`Created CNAME record for ${subdomain}`);
@@ -120,7 +122,7 @@ tap.test('should create TXT record for subdomain', async (tools) => {
subdomain, subdomain,
'TXT', 'TXT',
'v=spf1 include:_spf.example.com ~all', 'v=spf1 include:_spf.example.com ~all',
120 120,
); );
expect(result).toBeTypeOf('object'); expect(result).toBeTypeOf('object');
console.log(`Created TXT record for ${subdomain}`); console.log(`Created TXT record for ${subdomain}`);
@@ -142,7 +144,7 @@ tap.test('should update A record content', async (tools) => {
subdomain, subdomain,
'A', 'A',
'192.168.1.1', '192.168.1.1',
120 120,
); );
expect(result).toBeTypeOf('object'); expect(result).toBeTypeOf('object');
expect(result.content).toEqual('192.168.1.1'); expect(result.content).toEqual('192.168.1.1');
@@ -157,7 +159,7 @@ tap.test('should create A record for nested subdomain', async (tools) => {
nestedSubdomain, nestedSubdomain,
'A', 'A',
'127.0.0.5', '127.0.0.5',
120 120,
); );
expect(result).toBeTypeOf('object'); expect(result).toBeTypeOf('object');
console.log(`Created nested A record for ${nestedSubdomain}`); console.log(`Created nested A record for ${nestedSubdomain}`);
@@ -179,7 +181,7 @@ tap.test('should update A record for nested subdomain', async (tools) => {
nestedSubdomain, nestedSubdomain,
'A', 'A',
'127.0.0.6', '127.0.0.6',
120 120,
); );
expect(result).toBeTypeOf('object'); expect(result).toBeTypeOf('object');
expect(result.content).toEqual('127.0.0.6'); expect(result.content).toEqual('127.0.0.6');
@@ -209,14 +211,14 @@ tap.test('should remove A and CNAME records', async (tools) => {
tools.timeout(600000); tools.timeout(600000);
const aSubdomain = `${randomPrefix}-a-test.bleu.de`; const aSubdomain = `${randomPrefix}-a-test.bleu.de`;
const cnameSubdomain = `${randomPrefix}-cname-test.bleu.de`; const cnameSubdomain = `${randomPrefix}-cname-test.bleu.de`;
await testCloudflareAccount.convenience.removeRecord(aSubdomain, 'A'); await testCloudflareAccount.convenience.removeRecord(aSubdomain, 'A');
await testCloudflareAccount.convenience.removeRecord(cnameSubdomain, 'CNAME'); await testCloudflareAccount.convenience.removeRecord(cnameSubdomain, 'CNAME');
// Verify records are removed // Verify records are removed
const aRecord = await testCloudflareAccount.convenience.getRecord(aSubdomain, 'A'); const aRecord = await testCloudflareAccount.convenience.getRecord(aSubdomain, 'A');
const cnameRecord = await testCloudflareAccount.convenience.getRecord(cnameSubdomain, 'CNAME'); const cnameRecord = await testCloudflareAccount.convenience.getRecord(cnameSubdomain, 'CNAME');
expect(aRecord).toBeUndefined(); expect(aRecord).toBeUndefined();
expect(cnameRecord).toBeUndefined(); expect(cnameRecord).toBeUndefined();
console.log(`Successfully removed A and CNAME records`); console.log(`Successfully removed A and CNAME records`);
@@ -232,9 +234,9 @@ tap.test('.purgeZone() -> should purge zone cache', async (tools) => {
// Worker tests // Worker tests
tap.test('should list workers', async (tools) => { tap.test('should list workers', async (tools) => {
tools.timeout(600000); tools.timeout(600000);
try { try {
const workerArray = await testCloudflareAccount.workerManager.listWorkerScripts(); const workerArray = await testCloudflareAccount.workerManager.listWorkers();
expect(workerArray).toBeTypeOf('array'); expect(workerArray).toBeTypeOf('array');
console.log(`Found ${workerArray.length} workers in account`); console.log(`Found ${workerArray.length} workers in account`);
} catch (error) { } catch (error) {
@@ -246,7 +248,7 @@ tap.test('should list workers', async (tools) => {
tap.test('should create a worker', async (tools) => { tap.test('should create a worker', async (tools) => {
tools.timeout(600000); tools.timeout(600000);
try { try {
const worker = await testCloudflareAccount.workerManager.createWorker( const worker = await testCloudflareAccount.workerManager.createWorker(
testWorkerName, testWorkerName,
@@ -254,13 +256,13 @@ tap.test('should create a worker', async (tools) => {
event.respondWith(new Response('Hello from Cloudflare Workers!', { event.respondWith(new Response('Hello from Cloudflare Workers!', {
headers: { 'content-type': 'text/plain' } headers: { 'content-type': 'text/plain' }
})) }))
})` })`,
); );
expect(worker).toBeTypeOf('object'); expect(worker).toBeTypeOf('object');
expect(worker.id).toEqual(testWorkerName); expect(worker.id).toEqual(testWorkerName);
console.log(`Created worker: ${testWorkerName}`); console.log(`Created worker: ${testWorkerName}`);
try { try {
// Set routes for the worker // Set routes for the worker
await worker.setRoutes([ await worker.setRoutes([
@@ -269,7 +271,7 @@ tap.test('should create a worker', async (tools) => {
pattern: `https://${testWorkerName}.bleu.de/*`, pattern: `https://${testWorkerName}.bleu.de/*`,
}, },
]); ]);
console.log(`Set routes for worker ${testWorkerName}`); console.log(`Set routes for worker ${testWorkerName}`);
} catch (routeError) { } catch (routeError) {
console.error(`Error setting routes: ${routeError.message}`); console.error(`Error setting routes: ${routeError.message}`);
@@ -284,7 +286,7 @@ tap.test('should create a worker', async (tools) => {
tap.test('should get a specific worker by name', async (tools) => { tap.test('should get a specific worker by name', async (tools) => {
tools.timeout(600000); tools.timeout(600000);
try { try {
// First create a worker to ensure it exists // First create a worker to ensure it exists
await testCloudflareAccount.workerManager.createWorker( await testCloudflareAccount.workerManager.createWorker(
@@ -293,12 +295,12 @@ tap.test('should get a specific worker by name', async (tools) => {
event.respondWith(new Response('Hello from Cloudflare Workers!', { event.respondWith(new Response('Hello from Cloudflare Workers!', {
headers: { 'content-type': 'text/plain' } headers: { 'content-type': 'text/plain' }
})) }))
})` })`,
); );
// Now get the worker // Now get the worker
const worker = await testCloudflareAccount.workerManager.getWorker(testWorkerName); const worker = await testCloudflareAccount.workerManager.getWorker(testWorkerName);
expect(worker).toBeTypeOf('object'); expect(worker).toBeTypeOf('object');
expect(worker?.id).toEqual(testWorkerName); expect(worker?.id).toEqual(testWorkerName);
console.log(`Successfully retrieved worker: ${testWorkerName}`); console.log(`Successfully retrieved worker: ${testWorkerName}`);
@@ -311,17 +313,17 @@ tap.test('should get a specific worker by name', async (tools) => {
tap.test('should update worker script', async (tools) => { tap.test('should update worker script', async (tools) => {
tools.timeout(600000); tools.timeout(600000);
try { try {
const worker = await testCloudflareAccount.workerManager.getWorker(testWorkerName); const worker = await testCloudflareAccount.workerManager.getWorker(testWorkerName);
if (worker) { if (worker) {
await worker.updateScript(`addEventListener('fetch', event => { await worker.updateScript(`addEventListener('fetch', event => {
event.respondWith(new Response('Updated Worker Script!', { event.respondWith(new Response('Updated Worker Script!', {
headers: { 'content-type': 'text/plain' } headers: { 'content-type': 'text/plain' }
})) }))
})`); })`);
console.log(`Updated script for worker ${testWorkerName}`); console.log(`Updated script for worker ${testWorkerName}`);
expect(true).toBeTrue(); expect(true).toBeTrue();
} else { } else {
@@ -338,10 +340,10 @@ tap.test('should update worker script', async (tools) => {
tap.test('should delete the test worker', async (tools) => { tap.test('should delete the test worker', async (tools) => {
tools.timeout(600000); tools.timeout(600000);
try { try {
const worker = await testCloudflareAccount.workerManager.getWorker(testWorkerName); const worker = await testCloudflareAccount.workerManager.getWorker(testWorkerName);
if (worker) { if (worker) {
const result = await worker.delete(); const result = await worker.delete();
console.log(`Deleted worker: ${testWorkerName}`); console.log(`Deleted worker: ${testWorkerName}`);
@@ -381,4 +383,4 @@ tap.test('should format TTL values', async () => {
expect(cloudflare.CloudflareUtils.formatTtl(999)).toEqual('999 seconds'); expect(cloudflare.CloudflareUtils.formatTtl(999)).toEqual('999 seconds');
}); });
tap.start(); tap.start();

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@apiclient.xyz/cloudflare', 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.' description: 'A TypeScript client for managing Cloudflare accounts, zones, DNS records, and workers with ease.'
} }

View File

@@ -5,13 +5,16 @@ import * as interfaces from './interfaces/index.js';
// interfaces // interfaces
import { WorkerManager } from './cloudflare.classes.workermanager.js'; import { WorkerManager } from './cloudflare.classes.workermanager.js';
import { ZoneManager } from './cloudflare.classes.zonemanager.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; private authToken: string;
public preselectedAccountId: string; public preselectedAccountId: string;
public workerManager = new WorkerManager(this); public workerManager = new WorkerManager(this);
public zoneManager = new ZoneManager(this); public zoneManager = new ZoneManager(this);
public recordManager = new RecordManager(this);
public apiAccount: plugins.cloudflare.Cloudflare; public apiAccount: plugins.cloudflare.Cloudflare;
@@ -39,54 +42,80 @@ export class CloudflareAccount {
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
endpoint: string, endpoint: string,
data?: any, data?: any,
customHeaders?: Record<string, string> customHeaders?: Record<string, string>,
): Promise<T> { ): Promise<T> {
try { try {
const options: plugins.smartrequest.ISmartRequestOptions = { logger.log('debug', `Making ${method} request to ${endpoint}`);
method,
headers: {
'Authorization': `Bearer ${this.authToken}`,
'Content-Type': 'application/json',
...customHeaders,
},
};
// 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 (data) {
if (customHeaders && customHeaders['Content-Type']?.includes('multipart/form-data')) { if (customHeaders && customHeaders['Content-Type']?.includes('multipart/form-data')) {
// For multipart form data, use the data directly as the request body // For multipart form data, use formData method
options.requestBody = data; requestBuilder = requestBuilder.formData(data);
} else { } else {
// For JSON requests, stringify the data // For JSON requests, use json method
options.requestBody = JSON.stringify(data); requestBuilder = requestBuilder.json(data);
} }
} }
logger.log('debug', `Making ${method} request to ${endpoint}`); // Execute the request with the appropriate method
const response = await plugins.smartrequest.request(`https://api.cloudflare.com/client/v4${endpoint}`, options); let response: InstanceType<typeof plugins.smartrequest.CoreResponse>;
switch (method) {
// Check if response is already an object (might happen with newer smartrequest versions) case 'GET':
if (typeof response.body === 'object' && response.body !== null) { response = await requestBuilder.get();
return response.body; 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 { try {
if (typeof response.body === 'string' && response.body.trim()) { return (await response.json()) as T;
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;
}
} catch (parseError) { } catch (parseError) {
logger.log('warn', `Failed to parse response as JSON: ${parseError.message}`); 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 { return {
result: [], result: [],
success: true, success: true,
errors: [], 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; } as T;
} }
} catch (error) { } 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 = { public convenience = {
/** /**
* Lists all accounts accessible with the current API token * Lists all accounts accessible with the current API token
@@ -115,12 +154,12 @@ export class CloudflareAccount {
listAccounts: async () => { listAccounts: async () => {
try { try {
const accounts: plugins.ICloudflareTypes['Account'][] = []; const accounts: plugins.ICloudflareTypes['Account'][] = [];
// Collect all accounts using async iterator // Collect all accounts using async iterator
for await (const account of this.apiAccount.accounts.list()) { for await (const account of this.apiAccount.accounts.list()) {
accounts.push(account as interfaces.ICloudflareApiAccountObject); accounts.push(account as interfaces.ICloudflareApiAccountObject);
} }
logger.log('info', `Found ${accounts.length} accounts`); logger.log('info', `Found ${accounts.length} accounts`);
return accounts; return accounts;
} catch (error) { } catch (error) {
@@ -131,6 +170,7 @@ export class CloudflareAccount {
/** /**
* gets a zone id of a domain from cloudflare * gets a zone id of a domain from cloudflare
* @param domainName * @param domainName
* @deprecated Use zoneManager.getZoneId() instead
*/ */
getZoneId: async (domainName: string) => { getZoneId: async (domainName: string) => {
const domain = new plugins.smartstring.Domain(domainName); const domain = new plugins.smartstring.Domain(domainName);
@@ -149,24 +189,28 @@ export class CloudflareAccount {
* gets a record * gets a record
* @param domainNameArg * @param domainNameArg
* @param typeArg * @param typeArg
* @deprecated Use recordManager.getRecord() or getConvenientDnsProvider().getRecord() instead
*/ */
getRecord: async ( getRecord: async (
domainNameArg: string, domainNameArg: string,
typeArg: plugins.tsclass.network.TDnsRecordType typeArg: plugins.tsclass.network.TDnsRecordType,
): Promise<plugins.ICloudflareTypes['Record'] | undefined> => { ): Promise<plugins.ICloudflareTypes['Record'] | undefined> => {
try { try {
const domain = new plugins.smartstring.Domain(domainNameArg); const domain = new plugins.smartstring.Domain(domainNameArg);
const recordArrayArg = await this.convenience.listRecords(domain.zoneName); const recordArrayArg = await this.convenience.listRecords(domain.zoneName);
if (!Array.isArray(recordArrayArg)) { 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; return undefined;
} }
const filteredResponse = recordArrayArg.filter((recordArg) => { const filteredResponse = recordArrayArg.filter((recordArg) => {
return recordArg.type === typeArg && recordArg.name === domainNameArg; return recordArg.type === typeArg && recordArg.name === domainNameArg;
}); });
return filteredResponse.length > 0 ? filteredResponse[0] : undefined; return filteredResponse.length > 0 ? filteredResponse[0] : undefined;
} catch (error) { } catch (error) {
logger.log('error', `Error getting record for ${domainNameArg}: ${error.message}`); logger.log('error', `Error getting record for ${domainNameArg}: ${error.message}`);
@@ -175,12 +219,13 @@ export class CloudflareAccount {
}, },
/** /**
* creates a record * creates a record
* @deprecated Use recordManager.createRecord() or getConvenientDnsProvider().createRecord() instead
*/ */
createRecord: async ( createRecord: async (
domainNameArg: string, domainNameArg: string,
typeArg: plugins.tsclass.network.TDnsRecordType, typeArg: plugins.tsclass.network.TDnsRecordType,
contentArg: string, contentArg: string,
ttlArg = 1 ttlArg = 1,
): Promise<any> => { ): Promise<any> => {
const domain = new plugins.smartstring.Domain(domainNameArg); const domain = new plugins.smartstring.Domain(domainNameArg);
const zoneId = await this.convenience.getZoneId(domain.zoneName); const zoneId = await this.convenience.getZoneId(domain.zoneName);
@@ -190,17 +235,18 @@ export class CloudflareAccount {
name: domain.fullName, name: domain.fullName,
content: contentArg, content: contentArg,
ttl: ttlArg, ttl: ttlArg,
}) });
return response; return response;
}, },
/** /**
* removes a record from Cloudflare * removes a record from Cloudflare
* @param domainNameArg * @param domainNameArg
* @param typeArg * @param typeArg
* @deprecated Use recordManager.deleteRecord() or getConvenientDnsProvider().removeRecord() instead
*/ */
removeRecord: async ( removeRecord: async (
domainNameArg: string, domainNameArg: string,
typeArg: plugins.tsclass.network.TDnsRecordType typeArg: plugins.tsclass.network.TDnsRecordType,
): Promise<any> => { ): Promise<any> => {
const domain = new plugins.smartstring.Domain(domainNameArg); const domain = new plugins.smartstring.Domain(domainNameArg);
const zoneId = await this.convenience.getZoneId(domain.zoneName); const zoneId = await this.convenience.getZoneId(domain.zoneName);
@@ -222,28 +268,35 @@ export class CloudflareAccount {
/** /**
* cleanrecord allows the cleaning of any previous records to avoid unwanted sideeffects * 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) => { cleanRecord: async (domainNameArg: string, typeArg: plugins.tsclass.network.TDnsRecordType) => {
try { try {
logger.log('info', `Cleaning ${typeArg} records for ${domainNameArg}`); logger.log('info', `Cleaning ${typeArg} records for ${domainNameArg}`);
const domain = new plugins.smartstring.Domain(domainNameArg); const domain = new plugins.smartstring.Domain(domainNameArg);
const zoneId = await this.convenience.getZoneId(domain.zoneName); const zoneId = await this.convenience.getZoneId(domain.zoneName);
// List all records in the zone for this domain // List all records in the zone for this domain
const records = await this.convenience.listRecords(domain.zoneName); const records = await this.convenience.listRecords(domain.zoneName);
if (!Array.isArray(records)) { 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; return;
} }
// Only delete records matching the specified name and type // Only delete records matching the specified name and type
const recordsToDelete = records.filter((recordArg) => { const recordsToDelete = records.filter((recordArg) => {
return recordArg.type === typeArg && recordArg.name === domainNameArg; 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) { for (const recordToDelete of recordsToDelete) {
try { try {
// The official client might have different property locations // The official client might have different property locations
@@ -253,7 +306,7 @@ export class CloudflareAccount {
logger.log('warn', `Record ID not found for ${domainNameArg} record`); logger.log('warn', `Record ID not found for ${domainNameArg} record`);
continue; continue;
} }
await this.apiAccount.dns.records.delete(recordId, { await this.apiAccount.dns.records.delete(recordId, {
zone_id: zoneId, zone_id: zoneId,
}); });
@@ -263,7 +316,10 @@ export class CloudflareAccount {
} }
} }
} catch (error) { } 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,24 +330,28 @@ export class CloudflareAccount {
* @param contentArg New content for the record * @param contentArg New content for the record
* @param ttlArg Time to live in seconds (optional) * @param ttlArg Time to live in seconds (optional)
* @returns Updated record * @returns Updated record
* @deprecated Use recordManager.updateRecord() or getConvenientDnsProvider().updateRecord() instead
*/ */
updateRecord: async ( updateRecord: async (
domainNameArg: string, domainNameArg: string,
typeArg: plugins.tsclass.network.TDnsRecordType, typeArg: plugins.tsclass.network.TDnsRecordType,
contentArg: string, contentArg: string,
ttlArg: number = 1 ttlArg: number = 1,
): Promise<plugins.ICloudflareTypes['Record']> => { ): Promise<plugins.ICloudflareTypes['Record']> => {
const domain = new plugins.smartstring.Domain(domainNameArg); const domain = new plugins.smartstring.Domain(domainNameArg);
const zoneId = await this.convenience.getZoneId(domain.zoneName); const zoneId = await this.convenience.getZoneId(domain.zoneName);
// Find existing record // Find existing record
const record = await this.convenience.getRecord(domainNameArg, typeArg); const record = await this.convenience.getRecord(domainNameArg, typeArg);
if (!record) { 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); return this.convenience.createRecord(domainNameArg, typeArg, contentArg, ttlArg);
} }
// Update the record - cast to any to access the id property // Update the record - cast to any to access the id property
const recordId = (record as any).id; const recordId = (record as any).id;
const updatedRecord = await this.apiAccount.dns.records.edit(recordId, { const updatedRecord = await this.apiAccount.dns.records.edit(recordId, {
@@ -299,28 +359,29 @@ export class CloudflareAccount {
type: typeArg as any, type: typeArg as any,
name: domain.fullName, name: domain.fullName,
content: contentArg, content: contentArg,
ttl: ttlArg ttl: ttlArg,
}); });
return updatedRecord; return updatedRecord;
}, },
/** /**
* list all records of a specified domain name * list all records of a specified domain name
* @param domainNameArg - the domain name that you want to get the records from * @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) => { listRecords: async (domainNameArg: string) => {
try { try {
const domain = new plugins.smartstring.Domain(domainNameArg); const domain = new plugins.smartstring.Domain(domainNameArg);
const zoneId = await this.convenience.getZoneId(domain.zoneName); const zoneId = await this.convenience.getZoneId(domain.zoneName);
const records: plugins.ICloudflareTypes['Record'][] = []; const records: plugins.ICloudflareTypes['Record'][] = [];
// Collect all records using async iterator // Collect all records using async iterator
for await (const record of this.apiAccount.dns.records.list({ for await (const record of this.apiAccount.dns.records.list({
zone_id: zoneId, zone_id: zoneId,
})) { })) {
records.push(record); records.push(record);
} }
logger.log('info', `Found ${records.length} DNS records for ${domainNameArg}`); logger.log('info', `Found ${records.length} DNS records for ${domainNameArg}`);
return records; return records;
} catch (error) { } catch (error) {
@@ -331,6 +392,7 @@ export class CloudflareAccount {
/** /**
* list all zones in the associated authenticated account * list all zones in the associated authenticated account
* @param domainName optional filter by domain name * @param domainName optional filter by domain name
* @deprecated Use zoneManager.listZones() instead
*/ */
listZones: async (domainName?: string) => { listZones: async (domainName?: string) => {
try { try {
@@ -338,23 +400,46 @@ export class CloudflareAccount {
if (domainName) { if (domainName) {
options.name = domainName; options.name = domainName;
} }
const zones: plugins.ICloudflareTypes['Zone'][] = []; const zones: plugins.ICloudflareTypes['Zone'][] = [];
// Collect all zones using async iterator // Collect all zones using async iterator
for await (const zone of this.apiAccount.zones.list(options)) { for await (const zone of this.apiAccount.zones.list(options)) {
zones.push(zone); 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; return zones;
} catch (error) { } catch (error) {
logger.log('error', `Failed to list zones: ${error.message}`); logger.log('error', `Failed to list zones: ${error.message}`);
return []; 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 * purges a zone
* @deprecated Use zoneManager.purgeZone() instead
*/ */
purgeZone: async (domainName: string): Promise<void> => { purgeZone: async (domainName: string): Promise<void> => {
const domain = new plugins.smartstring.Domain(domainName); const domain = new plugins.smartstring.Domain(domainName);
@@ -366,17 +451,23 @@ export class CloudflareAccount {
}, },
// acme convenience functions // acme convenience functions
/**
* @deprecated Use getConvenientDnsProvider().acmeSetDnsChallenge() instead
*/
acmeSetDnsChallenge: async (dnsChallenge: plugins.tsclass.network.IDnsChallenge) => { acmeSetDnsChallenge: async (dnsChallenge: plugins.tsclass.network.IDnsChallenge) => {
await this.convenience.cleanRecord(dnsChallenge.hostName, 'TXT'); await this.convenience.cleanRecord(dnsChallenge.hostName, 'TXT');
await this.convenience.createRecord( await this.convenience.createRecord(
dnsChallenge.hostName, dnsChallenge.hostName,
'TXT', 'TXT',
dnsChallenge.challenge, dnsChallenge.challenge,
120 120,
); );
}, },
/**
* @deprecated Use getConvenientDnsProvider().acmeRemoveDnsChallenge() instead
*/
acmeRemoveDnsChallenge: async (dnsChallenge: plugins.tsclass.network.IDnsChallenge) => { acmeRemoveDnsChallenge: async (dnsChallenge: plugins.tsclass.network.IDnsChallenge) => {
await this.convenience.removeRecord(dnsChallenge.hostName, 'TXT'); await this.convenience.removeRecord(dnsChallenge.hostName, 'TXT');
}, },
}; };
} }

View 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,
};
}
}

View File

@@ -22,7 +22,9 @@ export class CloudflareRecord {
* @param apiObject Cloudflare DNS record API object * @param apiObject Cloudflare DNS record API object
* @returns CloudflareRecord instance * @returns CloudflareRecord instance
*/ */
public static createFromApiObject(apiObject: plugins.ICloudflareTypes['Record']): CloudflareRecord { public static createFromApiObject(
apiObject: plugins.ICloudflareTypes['Record'],
): CloudflareRecord {
const record = new CloudflareRecord(); const record = new CloudflareRecord();
Object.assign(record, apiObject); Object.assign(record, apiObject);
return record; return record;
@@ -52,28 +54,28 @@ export class CloudflareRecord {
public async update( public async update(
cloudflareAccount: any, cloudflareAccount: any,
newContent: string, newContent: string,
ttl?: number ttl?: number,
): Promise<CloudflareRecord> { ): Promise<CloudflareRecord> {
logger.log('info', `Updating record ${this.name} (${this.type}) with new content`); logger.log('info', `Updating record ${this.name} (${this.type}) with new content`);
const updatedRecord = await cloudflareAccount.apiAccount.dns.records.edit(this.id, { const updatedRecord = await cloudflareAccount.apiAccount.dns.records.edit(this.id, {
zone_id: this.zone_id, zone_id: this.zone_id,
type: this.type as any, type: this.type as any,
name: this.name, name: this.name,
content: newContent, content: newContent,
ttl: ttl || this.ttl, ttl: ttl || this.ttl,
proxied: this.proxied proxied: this.proxied,
}); });
// Update this instance // Update this instance
this.content = newContent; this.content = newContent;
if (ttl) { if (ttl) {
this.ttl = ttl; this.ttl = ttl;
} }
return this; return this;
} }
/** /**
* Delete this record * Delete this record
* @param cloudflareAccount The Cloudflare account to use * @param cloudflareAccount The Cloudflare account to use
@@ -82,15 +84,15 @@ export class CloudflareRecord {
public async delete(cloudflareAccount: any): Promise<boolean> { public async delete(cloudflareAccount: any): Promise<boolean> {
try { try {
logger.log('info', `Deleting record ${this.name} (${this.type})`); logger.log('info', `Deleting record ${this.name} (${this.type})`);
await cloudflareAccount.apiAccount.dns.records.delete(this.id, { await cloudflareAccount.apiAccount.dns.records.delete(this.id, {
zone_id: this.zone_id zone_id: this.zone_id,
}); });
return true; return true;
} catch (error) { } catch (error) {
logger.log('error', `Failed to delete record: ${error.message}`); logger.log('error', `Failed to delete record: ${error.message}`);
return false; return false;
} }
} }
} }

View 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}`,
);
}
}
}

View File

@@ -16,11 +16,11 @@ export class CloudflareWorker {
// STATIC // STATIC
public static async fromApiObject( public static async fromApiObject(
workerManager: WorkerManager, workerManager: WorkerManager,
apiObject apiObject,
): Promise<CloudflareWorker> { ): Promise<CloudflareWorker> {
const newWorker = new CloudflareWorker(workerManager); const newWorker = new CloudflareWorker(workerManager);
Object.assign(newWorker, apiObject); Object.assign(newWorker, apiObject);
await newWorker.getRoutes(); await newWorker.listRoutes();
return newWorker; return newWorker;
} }
@@ -41,38 +41,38 @@ export class CloudflareWorker {
} }
/** /**
* gets all routes for a worker * Lists all routes for this worker
*/ */
public async getRoutes() { public async listRoutes() {
try { try {
this.routes = []; // Reset routes before fetching this.routes = []; // Reset routes before fetching
// Get all zones using the async iterator // Get all zones using the async iterator
const zones: plugins.ICloudflareTypes['Zone'][] = []; const zones: plugins.ICloudflareTypes['Zone'][] = [];
for await (const zone of this.workerManager.cfAccount.apiAccount.zones.list()) { for await (const zone of this.workerManager.cfAccount.apiAccount.zones.list()) {
zones.push(zone); zones.push(zone);
} }
if (zones.length === 0) { if (zones.length === 0) {
logger.log('warn', 'No zones found for the account'); logger.log('warn', 'No zones found for the account');
return; return;
} }
for (const zone of zones) { for (const zone of zones) {
try { try {
if (!zone || !zone.id) { if (!zone || !zone.id) {
logger.log('warn', 'Zone is missing ID property'); logger.log('warn', 'Zone is missing ID property');
continue; continue;
} }
// Get worker routes for this zone // Get worker routes for this zone
const apiRoutes = []; const apiRoutes = [];
for await (const route of this.workerManager.cfAccount.apiAccount.workers.routes.list({ for await (const route of this.workerManager.cfAccount.apiAccount.workers.routes.list({
zone_id: zone.id zone_id: zone.id,
})) { })) {
apiRoutes.push(route); apiRoutes.push(route);
} }
// Filter for routes that match this worker's ID // Filter for routes that match this worker's ID
for (const route of apiRoutes) { for (const route of apiRoutes) {
if (route.script === this.id) { if (route.script === this.id) {
@@ -81,10 +81,13 @@ export class CloudflareWorker {
} }
} }
} catch (error) { } 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}`,
);
} }
} }
logger.log('info', `Found ${this.routes.length} routes for worker ${this.id}`); logger.log('info', `Found ${this.routes.length} routes for worker ${this.id}`);
} catch (error) { } catch (error) {
logger.log('error', `Failed to get routes for worker ${this.id}: ${error.message}`); logger.log('error', `Failed to get routes for worker ${this.id}: ${error.message}`);
@@ -99,61 +102,63 @@ export class CloudflareWorker {
*/ */
public async setRoutes(routeArray: IWorkerRouteDefinition[]) { public async setRoutes(routeArray: IWorkerRouteDefinition[]) {
// First get all existing routes to determine what we need to create/update // First get all existing routes to determine what we need to create/update
await this.getRoutes(); await this.listRoutes();
for (const newRoute of routeArray) { for (const newRoute of routeArray) {
// Determine whether a route is new, needs an update, or is already up to date // Determine whether a route is new, needs an update, or is already up to date
let routeStatus: 'new' | 'needsUpdate' | 'alreadyUpToDate' = 'new'; let routeStatus: 'new' | 'needsUpdate' | 'alreadyUpToDate' = 'new';
let existingRouteId: string; let existingRouteId: string;
for (const existingRoute of this.routes) { for (const existingRoute of this.routes) {
if (existingRoute.pattern === newRoute.pattern) { if (existingRoute.pattern === newRoute.pattern) {
routeStatus = 'needsUpdate'; routeStatus = 'needsUpdate';
existingRouteId = existingRoute.id; existingRouteId = existingRoute.id;
if (existingRoute.script === this.id) { if (existingRoute.script === this.id) {
routeStatus = 'alreadyUpToDate'; routeStatus = 'alreadyUpToDate';
logger.log('info', `Route ${newRoute.pattern} already exists, no update needed`); logger.log('info', `Route ${newRoute.pattern} already exists, no update needed`);
} }
} }
} }
try { try {
// Get the zone ID // 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) { if (!zone) {
logger.log('error', `Zone ${newRoute.zoneName} not found`); logger.log('error', `Zone ${newRoute.zoneName} not found`);
continue; continue;
} }
// Handle route creation, update, or skip if already up to date // Handle route creation, update, or skip if already up to date
if (routeStatus === 'new') { if (routeStatus === 'new') {
await this.workerManager.cfAccount.apiAccount.workers.routes.create({ await this.workerManager.cfAccount.apiAccount.workers.routes.create({
zone_id: zone.id, zone_id: zone.id,
pattern: newRoute.pattern, pattern: newRoute.pattern,
script: this.id script: this.id,
}); });
logger.log('info', `Created new route ${newRoute.pattern} for worker ${this.id}`); logger.log('info', `Created new route ${newRoute.pattern} for worker ${this.id}`);
} else if (routeStatus === 'needsUpdate') { } else if (routeStatus === 'needsUpdate') {
await this.workerManager.cfAccount.apiAccount.workers.routes.update(existingRouteId, { await this.workerManager.cfAccount.apiAccount.workers.routes.update(existingRouteId, {
zone_id: zone.id, zone_id: zone.id,
pattern: newRoute.pattern, pattern: newRoute.pattern,
script: this.id script: this.id,
}); });
logger.log('info', `Updated route ${newRoute.pattern} for worker ${this.id}`); logger.log('info', `Updated route ${newRoute.pattern} for worker ${this.id}`);
} }
} catch (error) { } catch (error) {
logger.log('error', `Failed to set route ${newRoute.pattern}: ${error.message}`); logger.log('error', `Failed to set route ${newRoute.pattern}: ${error.message}`);
} }
} }
// Refresh routes after all changes // Refresh routes after all changes
await this.getRoutes(); await this.listRoutes();
} }
/** /**
* Upload or update worker script content * Upload or update worker script content
* @param scriptContent The worker script content * @param scriptContent The worker script content
@@ -163,36 +168,39 @@ export class CloudflareWorker {
if (!this.workerManager.cfAccount.preselectedAccountId) { if (!this.workerManager.cfAccount.preselectedAccountId) {
throw new Error('No account selected. Please select it first on the account.'); throw new Error('No account selected. Please select it first on the account.');
} }
try { try {
logger.log('info', `Updating script for worker ${this.id}`); logger.log('info', `Updating script for worker ${this.id}`);
// Use the official client to update the script (upload new content) // 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, account_id: this.workerManager.cfAccount.preselectedAccountId,
// name the multipart part for the updated script code
metadata: { body_part: 'script' }, metadata: { body_part: 'script' },
/* header to indicate which part contains the script */ };
'CF-WORKER-BODY-PART': 'script', updateParams['CF-WORKER-BODY-PART'] = 'script';
// include the new script as a form part named 'script' updateParams['script'] = scriptContent;
script: scriptContent, const updatedWorker =
}); await this.workerManager.cfAccount.apiAccount.workers.scripts.content.update(
this.id,
updateParams,
);
// Update this instance with new data // Update this instance with new data
if (updatedWorker && typeof updatedWorker === 'object') { if (updatedWorker && typeof updatedWorker === 'object') {
Object.assign(this, updatedWorker); Object.assign(this, updatedWorker);
} }
// Always ensure the script property is updated // Always ensure the script property is updated
this.script = scriptContent; this.script = scriptContent;
return this; return this;
} catch (error) { } catch (error) {
logger.log('error', `Failed to update worker script: ${error.message}`); logger.log('error', `Failed to update worker script: ${error.message}`);
throw error; throw error;
} }
} }
/** /**
* Delete this worker script * Delete this worker script
* @returns True if deletion was successful * @returns True if deletion was successful
@@ -201,19 +209,19 @@ export class CloudflareWorker {
if (!this.workerManager.cfAccount.preselectedAccountId) { if (!this.workerManager.cfAccount.preselectedAccountId) {
throw new Error('No account selected. Please select it first on the account.'); throw new Error('No account selected. Please select it first on the account.');
} }
try { try {
logger.log('info', `Deleting worker ${this.id}`); logger.log('info', `Deleting worker ${this.id}`);
// Use the official client to delete the worker // Use the official client to delete the worker
await this.workerManager.cfAccount.apiAccount.workers.scripts.delete(this.id, { await this.workerManager.cfAccount.apiAccount.workers.scripts.delete(this.id, {
account_id: this.workerManager.cfAccount.preselectedAccountId account_id: this.workerManager.cfAccount.preselectedAccountId,
}); });
return true; return true;
} catch (error) { } catch (error) {
logger.log('error', `Failed to delete worker: ${error.message}`); logger.log('error', `Failed to delete worker: ${error.message}`);
return false; return false;
} }
} }
} }

View File

@@ -20,32 +20,31 @@ export class WorkerManager {
if (!this.cfAccount.preselectedAccountId) { if (!this.cfAccount.preselectedAccountId) {
throw new Error('No account selected. Please select it first on the account.'); throw new Error('No account selected. Please select it first on the account.');
} }
try { try {
// Use the official client to create/update the worker (upload script content) // 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, account_id: this.cfAccount.preselectedAccountId,
// name the multipart part for the script code
metadata: { body_part: 'script' }, metadata: { body_part: 'script' },
/* header to indicate which part contains the script */ };
'CF-WORKER-BODY-PART': 'script', contentParams['CF-WORKER-BODY-PART'] = 'script';
// include the actual script as a form part named 'script' contentParams['script'] = workerScript;
script: workerScript, await this.cfAccount.apiAccount.workers.scripts.content.update(workerName, contentParams);
});
// Create a new worker instance // Create a new worker instance
const worker = new CloudflareWorker(this); const worker = new CloudflareWorker(this);
worker.id = workerName; worker.id = workerName;
worker.script = workerScript; worker.script = workerScript;
// Initialize the worker and get its routes // Initialize the worker and get its routes
try { try {
await worker.getRoutes(); await worker.listRoutes();
} catch (routeError) { } catch (routeError) {
logger.log('warn', `Failed to get routes for worker ${workerName}: ${routeError.message}`); logger.log('warn', `Failed to get routes for worker ${workerName}: ${routeError.message}`);
// Continue anyway since the worker was created // Continue anyway since the worker was created
} }
return worker; return worker;
} catch (error) { } catch (error) {
logger.log('error', `Failed to create worker ${workerName}: ${error.message}`); logger.log('error', `Failed to create worker ${workerName}: ${error.message}`);
@@ -62,30 +61,30 @@ export class WorkerManager {
if (!this.cfAccount.preselectedAccountId) { if (!this.cfAccount.preselectedAccountId) {
throw new Error('No account selected. Please select it first on the account.'); throw new Error('No account selected. Please select it first on the account.');
} }
try { try {
// Get the worker script using the official client // Get the worker script using the official client
const workerScript = await this.cfAccount.apiAccount.workers.scripts.get(workerName, { 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 // Create a new worker instance
const worker = new CloudflareWorker(this); const worker = new CloudflareWorker(this);
worker.id = workerName; worker.id = workerName;
// Save script content if available // Save script content if available
if (workerScript && typeof workerScript === 'object') { if (workerScript && typeof workerScript === 'object') {
Object.assign(worker, workerScript); Object.assign(worker, workerScript);
} }
// Initialize the worker and get its routes // Initialize the worker and get its routes
try { try {
await worker.getRoutes(); await worker.listRoutes();
} catch (routeError) { } catch (routeError) {
logger.log('warn', `Failed to get routes for worker ${workerName}: ${routeError.message}`); logger.log('warn', `Failed to get routes for worker ${workerName}: ${routeError.message}`);
// Continue anyway since we found the worker // Continue anyway since we found the worker
} }
return worker; return worker;
} catch (error) { } catch (error) {
logger.log('warn', `Worker '${workerName}' not found: ${error.message}`); logger.log('warn', `Worker '${workerName}' not found: ${error.message}`);
@@ -97,39 +96,39 @@ export class WorkerManager {
* Lists all worker scripts * Lists all worker scripts
* @returns Array of worker scripts * @returns Array of worker scripts
*/ */
public async listWorkerScripts() { public async listWorkers() {
if (!this.cfAccount.preselectedAccountId) { if (!this.cfAccount.preselectedAccountId) {
throw new Error('No account selected. Please select it first on the account.'); throw new Error('No account selected. Please select it first on the account.');
} }
try { try {
// Collect all scripts using the new client's async iterator // Collect all scripts using the new client's async iterator
const workerScripts: plugins.ICloudflareTypes['Script'][] = []; const workerScripts: plugins.ICloudflareTypes['Script'][] = [];
try { try {
for await (const script of this.cfAccount.apiAccount.workers.scripts.list({ for await (const script of this.cfAccount.apiAccount.workers.scripts.list({
account_id: this.cfAccount.preselectedAccountId, account_id: this.cfAccount.preselectedAccountId,
})) { })) {
workerScripts.push(script); workerScripts.push(script);
} }
logger.log('info', `Found ${workerScripts.length} worker scripts`); logger.log('info', `Found ${workerScripts.length} worker scripts`);
return workerScripts; return workerScripts;
} catch (error) { } catch (error) {
logger.log('warn', `Error while listing workers with async iterator: ${error.message}`); logger.log('warn', `Error while listing workers with async iterator: ${error.message}`);
// Try alternative approach if the async iterator fails // 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, account_id: this.cfAccount.preselectedAccountId,
}) as any; })) as any;
// Check if the result has a 'result' property (older API response format) // Check if the result has a 'result' property (older API response format)
if (result && result.result && Array.isArray(result.result)) { if (result && result.result && Array.isArray(result.result)) {
logger.log('info', `Found ${result.result.length} worker scripts using direct result`); logger.log('info', `Found ${result.result.length} worker scripts using direct result`);
return result.result; return result.result;
} }
} }
logger.log('warn', 'Could not retrieve worker scripts'); logger.log('warn', 'Could not retrieve worker scripts');
return []; return [];
} catch (error) { } catch (error) {
@@ -137,7 +136,7 @@ export class WorkerManager {
return []; return [];
} }
} }
/** /**
* Deletes a worker script * Deletes a worker script
* @param workerName Name of the worker to delete * @param workerName Name of the worker to delete
@@ -147,10 +146,10 @@ export class WorkerManager {
if (!this.cfAccount.preselectedAccountId) { if (!this.cfAccount.preselectedAccountId) {
throw new Error('No account selected. Please select it first on the account.'); throw new Error('No account selected. Please select it first on the account.');
} }
try { try {
await this.cfAccount.apiAccount.workers.scripts.delete(workerName, { 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`); logger.log('info', `Worker '${workerName}' deleted successfully`);
return true; return true;
@@ -159,4 +158,4 @@ export class WorkerManager {
return false; return false;
} }
} }
} }

View File

@@ -23,7 +23,7 @@ export class CloudflareZone {
public account: interfaces.ICflareZone['account']; public account: interfaces.ICflareZone['account'];
public permissions: string[]; public permissions: string[];
public plan: interfaces.ICflareZone['plan']; public plan: interfaces.ICflareZone['plan'];
private cfAccount?: CloudflareAccount; // Will be set when created through a manager private cfAccount?: CloudflareAccount; // Will be set when created through a manager
/** /**
@@ -33,19 +33,19 @@ export class CloudflareZone {
* @returns CloudflareZone instance * @returns CloudflareZone instance
*/ */
public static createFromApiObject( public static createFromApiObject(
apiObject: plugins.ICloudflareTypes['Zone'], apiObject: plugins.ICloudflareTypes['Zone'],
cfAccount?: CloudflareAccount cfAccount?: CloudflareAccount,
): CloudflareZone { ): CloudflareZone {
const cloudflareZone = new CloudflareZone(); const cloudflareZone = new CloudflareZone();
Object.assign(cloudflareZone, apiObject); Object.assign(cloudflareZone, apiObject);
if (cfAccount) { if (cfAccount) {
cloudflareZone.cfAccount = cfAccount; cloudflareZone.cfAccount = cfAccount;
} }
return cloudflareZone; return cloudflareZone;
} }
/** /**
* Check if development mode is currently active * Check if development mode is currently active
* @returns True if development mode is active * @returns True if development mode is active
@@ -53,7 +53,7 @@ export class CloudflareZone {
public isDevelopmentModeActive(): boolean { public isDevelopmentModeActive(): boolean {
return this.development_mode > 0; return this.development_mode > 0;
} }
/** /**
* Enable development mode for the zone * Enable development mode for the zone
* @param cfAccount Cloudflare account to use if not already set * @param cfAccount Cloudflare account to use if not already set
@@ -62,23 +62,23 @@ export class CloudflareZone {
*/ */
public async enableDevelopmentMode( public async enableDevelopmentMode(
cfAccount?: CloudflareAccount, cfAccount?: CloudflareAccount,
duration: number = 10800 duration: number = 10800,
): Promise<CloudflareZone> { ): Promise<CloudflareZone> {
const account = cfAccount || this.cfAccount; const account = cfAccount || this.cfAccount;
if (!account) { if (!account) {
throw new Error('CloudflareAccount is required to enable development mode'); throw new Error('CloudflareAccount is required to enable development mode');
} }
logger.log('info', `Enabling development mode for zone ${this.name}`); logger.log('info', `Enabling development mode for zone ${this.name}`);
try { try {
// The official client doesn't have a direct method for development mode // The official client doesn't have a direct method for development mode
// We'll use the request method for this specific case // We'll use the request method for this specific case
await account.request('PATCH', `/zones/${this.id}/settings/development_mode`, { await account.request('PATCH', `/zones/${this.id}/settings/development_mode`, {
value: 'on', value: 'on',
time: duration time: duration,
}); });
this.development_mode = duration; this.development_mode = duration;
return this; return this;
} catch (error) { } catch (error) {
@@ -86,7 +86,7 @@ export class CloudflareZone {
throw error; throw error;
} }
} }
/** /**
* Disable development mode for the zone * Disable development mode for the zone
* @param cfAccount Cloudflare account to use if not already set * @param cfAccount Cloudflare account to use if not already set
@@ -97,16 +97,16 @@ export class CloudflareZone {
if (!account) { if (!account) {
throw new Error('CloudflareAccount is required to disable development mode'); throw new Error('CloudflareAccount is required to disable development mode');
} }
logger.log('info', `Disabling development mode for zone ${this.name}`); logger.log('info', `Disabling development mode for zone ${this.name}`);
try { try {
// The official client doesn't have a direct method for development mode // The official client doesn't have a direct method for development mode
// We'll use the request method for this specific case // We'll use the request method for this specific case
await account.request('PATCH', `/zones/${this.id}/settings/development_mode`, { await account.request('PATCH', `/zones/${this.id}/settings/development_mode`, {
value: 'off' value: 'off',
}); });
this.development_mode = 0; this.development_mode = 0;
return this; return this;
} catch (error) { } catch (error) {
@@ -114,7 +114,7 @@ export class CloudflareZone {
throw error; throw error;
} }
} }
/** /**
* Purge all cached content for this zone * Purge all cached content for this zone
* @param cfAccount Cloudflare account to use if not already set * @param cfAccount Cloudflare account to use if not already set
@@ -125,13 +125,13 @@ export class CloudflareZone {
if (!account) { if (!account) {
throw new Error('CloudflareAccount is required to purge cache'); throw new Error('CloudflareAccount is required to purge cache');
} }
logger.log('info', `Purging all cache for zone ${this.name}`); logger.log('info', `Purging all cache for zone ${this.name}`);
try { try {
await account.apiAccount.cache.purge({ await account.apiAccount.cache.purge({
zone_id: this.id, zone_id: this.id,
purge_everything: true purge_everything: true,
}); });
return true; return true;
} catch (error) { } catch (error) {
@@ -139,7 +139,7 @@ export class CloudflareZone {
return false; return false;
} }
} }
/** /**
* Purge specific URLs from the cache * Purge specific URLs from the cache
* @param urls Array of URLs to purge * @param urls Array of URLs to purge
@@ -151,17 +151,17 @@ export class CloudflareZone {
if (!account) { if (!account) {
throw new Error('CloudflareAccount is required to purge URLs'); throw new Error('CloudflareAccount is required to purge URLs');
} }
if (!urls.length) { if (!urls.length) {
return true; return true;
} }
logger.log('info', `Purging ${urls.length} URLs from cache for zone ${this.name}`); logger.log('info', `Purging ${urls.length} URLs from cache for zone ${this.name}`);
try { try {
await account.apiAccount.cache.purge({ await account.apiAccount.cache.purge({
zone_id: this.id, zone_id: this.id,
files: urls files: urls,
}); });
return true; return true;
} catch (error) { } catch (error) {
@@ -169,7 +169,7 @@ export class CloudflareZone {
return false; return false;
} }
} }
/** /**
* Check if the zone is active * Check if the zone is active
* @returns True if the zone is active * @returns True if the zone is active
@@ -177,7 +177,7 @@ export class CloudflareZone {
public isActive(): boolean { public isActive(): boolean {
return this.status === 'active' && !this.paused; return this.status === 'active' && !this.paused;
} }
/** /**
* Check if the zone is using Cloudflare nameservers * Check if the zone is using Cloudflare nameservers
* @returns True if using Cloudflare nameservers * @returns True if using Cloudflare nameservers
@@ -187,11 +187,11 @@ export class CloudflareZone {
if (!this.original_name_servers || !this.name_servers) { if (!this.original_name_servers || !this.name_servers) {
return false; return false;
} }
// If they're different, and current nameservers are Cloudflare's // 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'));
} }
/** /**
* Update zone settings * Update zone settings
* @param settings Settings to update * @param settings Settings to update
@@ -205,23 +205,23 @@ export class CloudflareZone {
vanity_name_servers: string[]; vanity_name_servers: string[];
type: 'full' | 'partial' | 'secondary'; type: 'full' | 'partial' | 'secondary';
}>, }>,
cfAccount?: CloudflareAccount cfAccount?: CloudflareAccount,
): Promise<CloudflareZone> { ): Promise<CloudflareZone> {
const account = cfAccount || this.cfAccount; const account = cfAccount || this.cfAccount;
if (!account) { if (!account) {
throw new Error('CloudflareAccount is required to update zone settings'); throw new Error('CloudflareAccount is required to update zone settings');
} }
logger.log('info', `Updating settings for zone ${this.name}`); logger.log('info', `Updating settings for zone ${this.name}`);
try { try {
// Use the request method instead of zones.edit to avoid type issues // Use the request method instead of zones.edit to avoid type issues
const response: { result: interfaces.ICflareZone } = await account.request( const response: { result: interfaces.ICflareZone } = await account.request(
'PATCH', 'PATCH',
`/zones/${this.id}`, `/zones/${this.id}`,
settings settings,
); );
Object.assign(this, response.result); Object.assign(this, response.result);
return this; return this;
} catch (error) { } catch (error) {
@@ -229,4 +229,4 @@ export class CloudflareZone {
throw error; throw error;
} }
} }
} }

View File

@@ -12,14 +12,14 @@ 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 * @param zoneName Optional zone name to filter by
* @returns Array of CloudflareZone instances * @returns Array of CloudflareZone instances
*/ */
public async getZones(zoneName?: string): Promise<CloudflareZone[]> { public async listZones(zoneName?: string): Promise<CloudflareZone[]> {
try { try {
const options: any = { per_page: 50 }; const options: any = { per_page: 50 };
// May be optionally filtered by domain name // May be optionally filtered by domain name
if (zoneName) { if (zoneName) {
options.name = zoneName; options.name = zoneName;
@@ -29,24 +29,44 @@ export class ZoneManager {
for await (const zone of this.cfAccount.apiAccount.zones.list(options)) { for await (const zone of this.cfAccount.apiAccount.zones.list(options)) {
zones.push(zone); zones.push(zone);
} }
return zones.map(zone => CloudflareZone.createFromApiObject(zone, this.cfAccount)); return zones.map((zone) => CloudflareZone.createFromApiObject(zone, this.cfAccount));
} catch (error) { } catch (error) {
logger.log('error', `Failed to fetch zones: ${error.message}`); logger.log('error', `Failed to fetch zones: ${error.message}`);
return []; 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 * Get a single zone by name
* @param zoneName Zone name to find * @param zoneName Zone name to find
* @returns CloudflareZone instance or undefined if not found * @returns CloudflareZone instance or undefined if not found
*/ */
public async getZoneByName(zoneName: string): Promise<CloudflareZone | undefined> { public async getZoneByName(zoneName: string): Promise<CloudflareZone | undefined> {
const zones = await this.getZones(zoneName); const zones = await this.listZones(zoneName);
return zones.find(zone => zone.name === zoneName); return zones.find((zone) => zone.name === zoneName);
} }
/** /**
* Get a zone by its ID * Get a zone by its ID
* @param zoneId Zone ID to find * @param zoneId Zone ID to find
@@ -56,17 +76,17 @@ export class ZoneManager {
try { try {
// Use the request method instead of the zones.get method to avoid type issues // Use the request method instead of the zones.get method to avoid type issues
const response: { result: interfaces.ICflareZone } = await this.cfAccount.request( const response: { result: interfaces.ICflareZone } = await this.cfAccount.request(
'GET', 'GET',
`/zones/${zoneId}` `/zones/${zoneId}`,
); );
return CloudflareZone.createFromApiObject(response.result as any, this.cfAccount); return CloudflareZone.createFromApiObject(response.result as any, this.cfAccount);
} catch (error) { } catch (error) {
logger.log('error', `Failed to fetch zone with ID ${zoneId}: ${error.message}`); logger.log('error', `Failed to fetch zone with ID ${zoneId}: ${error.message}`);
return undefined; return undefined;
} }
} }
/** /**
* Create a new zone * Create a new zone
* @param zoneName Name of the zone to create * @param zoneName Name of the zone to create
@@ -75,37 +95,37 @@ export class ZoneManager {
* @returns The created zone * @returns The created zone
*/ */
public async createZone( public async createZone(
zoneName: string, zoneName: string,
jumpStart: boolean = false, jumpStart: boolean = false,
accountId?: string accountId?: string,
): Promise<CloudflareZone | undefined> { ): Promise<CloudflareZone | undefined> {
const useAccountId = accountId || this.cfAccount.preselectedAccountId; const useAccountId = accountId || this.cfAccount.preselectedAccountId;
if (!useAccountId) { if (!useAccountId) {
throw new Error('No account selected. Please select it first on the account.'); throw new Error('No account selected. Please select it first on the account.');
} }
try { try {
logger.log('info', `Creating zone ${zoneName}`); logger.log('info', `Creating zone ${zoneName}`);
// Use the request method for more direct control over the parameters // Use the request method for more direct control over the parameters
const response: { result: interfaces.ICflareZone } = await this.cfAccount.request( const response: { result: interfaces.ICflareZone } = await this.cfAccount.request(
'POST', 'POST',
'/zones', '/zones',
{ {
name: zoneName, name: zoneName,
jump_start: jumpStart, jump_start: jumpStart,
account: { id: useAccountId } account: { id: useAccountId },
} },
); );
return CloudflareZone.createFromApiObject(response.result as any, this.cfAccount); return CloudflareZone.createFromApiObject(response.result as any, this.cfAccount);
} catch (error) { } catch (error) {
logger.log('error', `Failed to create zone ${zoneName}: ${error.message}`); logger.log('error', `Failed to create zone ${zoneName}: ${error.message}`);
return undefined; return undefined;
} }
} }
/** /**
* Delete a zone * Delete a zone
* @param zoneId ID of the zone to delete * @param zoneId ID of the zone to delete
@@ -114,7 +134,7 @@ export class ZoneManager {
public async deleteZone(zoneId: string): Promise<boolean> { public async deleteZone(zoneId: string): Promise<boolean> {
try { try {
logger.log('info', `Deleting zone with ID ${zoneId}`); logger.log('info', `Deleting zone with ID ${zoneId}`);
// Use the request method to avoid type issues // Use the request method to avoid type issues
await this.cfAccount.request('DELETE', `/zones/${zoneId}`); await this.cfAccount.request('DELETE', `/zones/${zoneId}`);
return true; return true;
@@ -123,17 +143,17 @@ export class ZoneManager {
return false; return false;
} }
} }
/** /**
* Check if a zone exists * Check if a zone exists
* @param zoneName Name of the zone to check * @param zoneName Name of the zone to check
* @returns True if the zone exists * @returns True if the zone exists
*/ */
public async zoneExists(zoneName: string): Promise<boolean> { public async zoneExists(zoneName: string): Promise<boolean> {
const zones = await this.getZones(zoneName); const zones = await this.listZones(zoneName);
return zones.some(zone => zone.name === zoneName); return zones.some((zone) => zone.name === zoneName);
} }
/** /**
* Activate a zone (if it's in pending status) * Activate a zone (if it's in pending status)
* @param zoneId ID of the zone to activate * @param zoneId ID of the zone to activate
@@ -142,23 +162,23 @@ export class ZoneManager {
public async activateZone(zoneId: string): Promise<CloudflareZone | undefined> { public async activateZone(zoneId: string): Promise<CloudflareZone | undefined> {
try { try {
logger.log('info', `Activating zone with ID ${zoneId}`); logger.log('info', `Activating zone with ID ${zoneId}`);
// Use the request method for better control // Use the request method for better control
const response: { result: interfaces.ICflareZone } = await this.cfAccount.request( const response: { result: interfaces.ICflareZone } = await this.cfAccount.request(
'PATCH', 'PATCH',
`/zones/${zoneId}`, `/zones/${zoneId}`,
{ {
status: 'active' status: 'active',
} },
); );
return CloudflareZone.createFromApiObject(response.result as any, this.cfAccount); return CloudflareZone.createFromApiObject(response.result as any, this.cfAccount);
} catch (error) { } catch (error) {
logger.log('error', `Failed to activate zone with ID ${zoneId}: ${error.message}`); logger.log('error', `Failed to activate zone with ID ${zoneId}: ${error.message}`);
return undefined; return undefined;
} }
} }
/** /**
* Check the activation status of a zone * Check the activation status of a zone
* @param zoneId ID of the zone to check * @param zoneId ID of the zone to check
@@ -167,17 +187,31 @@ export class ZoneManager {
public async checkZoneActivation(zoneId: string): Promise<CloudflareZone | undefined> { public async checkZoneActivation(zoneId: string): Promise<CloudflareZone | undefined> {
try { try {
logger.log('info', `Checking activation for zone with ID ${zoneId}`); logger.log('info', `Checking activation for zone with ID ${zoneId}`);
// For this specific endpoint, we'll use the request method // For this specific endpoint, we'll use the request method
const response: { result: interfaces.ICflareZone } = await this.cfAccount.request( const response: { result: interfaces.ICflareZone } = await this.cfAccount.request(
'PUT', 'PUT',
`/zones/${zoneId}/activation_check` `/zones/${zoneId}/activation_check`,
); );
return CloudflareZone.createFromApiObject(response.result as any, this.cfAccount); return CloudflareZone.createFromApiObject(response.result as any, this.cfAccount);
} catch (error) { } catch (error) {
logger.log('error', `Failed to check zone activation with ID ${zoneId}: ${error.message}`); logger.log('error', `Failed to check zone activation with ID ${zoneId}: ${error.message}`);
return undefined; 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}`);
}
}

View File

@@ -16,7 +16,7 @@ export class CloudflareUtils {
return false; return false;
} }
} }
/** /**
* Extracts the zone name (apex domain) from a full domain * Extracts the zone name (apex domain) from a full domain
* @param domainName Domain name to process * @param domainName Domain name to process
@@ -31,7 +31,7 @@ export class CloudflareUtils {
throw new Error(`Invalid domain name: ${domainName}`); throw new Error(`Invalid domain name: ${domainName}`);
} }
} }
/** /**
* Checks if a string is a valid Cloudflare API token * Checks if a string is a valid Cloudflare API token
* @param token API token to validate * @param token API token to validate
@@ -41,7 +41,7 @@ export class CloudflareUtils {
// Cloudflare API tokens are typically 40+ characters long and start with specific patterns // Cloudflare API tokens are typically 40+ characters long and start with specific patterns
return /^[A-Za-z0-9_-]{40,}$/.test(token); return /^[A-Za-z0-9_-]{40,}$/.test(token);
} }
/** /**
* Validates a DNS record type * Validates a DNS record type
* @param type DNS record type to validate * @param type DNS record type to validate
@@ -49,14 +49,28 @@ export class CloudflareUtils {
*/ */
public static isValidRecordType(type: string): boolean { public static isValidRecordType(type: string): boolean {
const validTypes: plugins.tsclass.network.TDnsRecordType[] = [ const validTypes: plugins.tsclass.network.TDnsRecordType[] = [
'A', 'AAAA', 'CNAME', 'TXT', 'SRV', 'LOC', 'MX', 'A',
'NS', 'CAA', 'CERT', 'DNSKEY', 'DS', 'NAPTR', 'SMIMEA', 'AAAA',
'SSHFP', 'TLSA', 'URI' '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 // Note: SPF has been removed as it's not in TDnsRecordType
]; ];
return validTypes.includes(type as any); return validTypes.includes(type as any);
} }
/** /**
* Formats a URL for cache purging (ensures it starts with http/https) * Formats a URL for cache purging (ensures it starts with http/https)
* @param url URL to format * @param url URL to format
@@ -68,7 +82,7 @@ export class CloudflareUtils {
} }
return url; return url;
} }
/** /**
* Converts a TTL value in seconds to a human-readable string * Converts a TTL value in seconds to a human-readable string
* @param ttl TTL in seconds * @param ttl TTL in seconds
@@ -101,20 +115,23 @@ export class CloudflareUtils {
return `${ttl} seconds`; return `${ttl} seconds`;
} }
} }
/** /**
* Safely handles API pagination for Cloudflare requests * Safely handles API pagination for Cloudflare requests
* @param makeRequest Function that makes the API request with page parameters * @param makeRequest Function that makes the API request with page parameters
* @returns Combined results from all pages * @returns Combined results from all pages
*/ */
public static async paginateResults<T>( 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[]> { ): Promise<T[]> {
const perPage = 50; // Cloudflare's maximum const perPage = 50; // Cloudflare's maximum
let page = 1; let page = 1;
let totalPages = 1; let totalPages = 1;
const allResults: T[] = []; const allResults: T[] = [];
do { do {
try { try {
const response = await makeRequest(page, perPage); const response = await makeRequest(page, perPage);
@@ -126,7 +143,7 @@ export class CloudflareUtils {
break; break;
} }
} while (page <= totalPages); } while (page <= totalPages);
return allResults; return allResults;
} }
} }

View File

@@ -1,11 +1,17 @@
export { CloudflareAccount } from './cloudflare.classes.account.js'; 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 { WorkerManager } from './cloudflare.classes.workermanager.js';
export { CloudflareRecord, type ICloudflareRecordInfo } from './cloudflare.classes.record.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 { CloudflareZone } from './cloudflare.classes.zone.js';
export { ZoneManager } from './cloudflare.classes.zonemanager.js'; export { ZoneManager } from './cloudflare.classes.zonemanager.js';
export { ConvenientDnsProvider } from './cloudflare.classes.convenientdnsprovider.js';
export { CloudflareUtils } from './cloudflare.utils.js'; export { CloudflareUtils } from './cloudflare.utils.js';
export { commitinfo } from './00_commitinfo_data.js'; export { commitinfo } from './00_commitinfo_data.js';
// Re-export interfaces // Re-export interfaces
export * from './interfaces/index.js'; export * from './interfaces/index.js';

View File

@@ -17,4 +17,4 @@ export interface ICloudflareApiAccountObject {
}; };
}; };
created_on: string; // Assuming ISO date string created_on: string; // Assuming ISO date string
} }

View File

@@ -42,4 +42,4 @@ export interface ICflareZone {
legacy_discount: boolean; legacy_discount: boolean;
externally_managed: boolean; externally_managed: boolean;
}; };
} }

View File

@@ -6,9 +6,11 @@
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"esModuleInterop": true, "esModuleInterop": true,
"verbatimModuleSyntax": true "verbatimModuleSyntax": true,
"baseUrl": ".",
"paths": {}
}, },
"exclude": [ "exclude": [
"dist_*/**/*.d.ts" "dist_*/**/*.d.ts"
] ]
} }