Compare commits

...

12 Commits

Author SHA1 Message Date
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
6cd5aa2913 6.3.1 2025-04-26 12:37:19 +00:00
4b82cfbaae fix(core): Improve nested DNS record management and worker script multipart handling 2025-04-26 12:37:19 +00:00
21 changed files with 11460 additions and 4581 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
coverage/
public/
pages/
# installs
node_modules/
@@ -17,4 +16,4 @@ node_modules/
dist/
dist_*/
# custom
#------# custom

View File

@@ -1,5 +1,51 @@
# Changelog
## 2025-11-17 - 6.4.3 - fix(cloudflare.plugins)
Switch to smartrequest namespace export and improve request typing and JSON parsing
- Export smartrequest as a namespace from cloudflare.plugins (replaced named SmartRequest/CoreResponse exports)
- Use plugins.smartrequest.SmartRequest.create() when building HTTP requests
- Type response as InstanceType<typeof plugins.smartrequest.CoreResponse> to match the new smartrequest export shape
- Safer JSON parsing: cast result of response.json() to the expected generic instead of relying on a generic json<T>() call and provide a text fallback when parsing fails
- Adjust imports/usages to align with @push.rocks/smartrequest namespace usage
## 2025-11-17 - 6.4.2 - fix(core)
Switch to SmartRequest fluent API and improve Cloudflare API request handling
- Upgrade runtime dependencies: @push.rocks/smartlog -> ^3.1.10, @push.rocks/smartrequest -> ^5.0.1, @push.rocks/smartstring -> ^4.1.0, @tsclass/tsclass -> ^9.3.0, cloudflare -> ^5.2.0
- Upgrade devDependencies: @git.zone/tsbuild -> ^3.1.0, @git.zone/tsrun -> ^2.0.0, @git.zone/tstest -> ^2.8.2, @push.rocks/qenv -> ^6.1.3, openapi-typescript -> ^7.10.1
- Export SmartRequest and CoreResponse from cloudflare.plugins to align with smartrequest v5 API
- Refactor CloudflareAccount.request to use SmartRequest fluent builder, add detailed logging, default JSON Content-Type, support multipart/form-data via formData(), and use appropriate HTTP method helpers
- Improve response parsing: return a safe fallback when JSON parsing fails by reading response.text() and include a concise message; better HTTP error logging including response body text
- Update usages to rely on the new request behavior (zones/workers managers use account.request for endpoints not covered by the official client)
## 2025-04-30 - 6.4.1 - fix(ci)
Update CI workflows, repository URL, and apply minor code formatting fixes
- Add new Gitea workflows for both tagged and non-tagged push events with security, test, release, and metadata jobs
- Update repository URL in package.json from pushrocks/cflare to mojoio/cloudflare
- Refine .gitignore custom comments
- Apply minor formatting improvements in source code and documentation
## 2025-04-30 - 6.4.0 - feat(CloudflareAccount)
Bump dependency versions and add domain support check in CloudflareAccount
- Upgrade dependencies: @push.rocks/smartrequest, @tsclass/tsclass, @git.zone/tsbuild, @push.rocks/tapbundle, and @types/node
- Implement the isDomainSupported convenience method in CloudflareAccount for validating domain management
## 2025-04-26 - 6.3.2 - fix(worker)
Refactor worker script update and creation to use intermediate parameter objects
- Build updateParams in CloudflareWorker for proper multipart form handling when updating scripts
- Use contentParams in WorkerManager to improve clarity and consistency in worker creation
## 2025-04-26 - 6.3.1 - fix(core)
Improve nested DNS record management and worker script multipart handling
- Add tests for creating, updating, and removing nested subdomain A records
- Refine TXT record cleaning by filtering records with matching name and type
- Clarify multipart form data handling for worker script updates and creation
## 2025-04-26 - 6.3.0 - feat(core)
Release 6.2.0: Improved async iterator support, enhanced error handling and refined API interfaces for better type safety and consistent behavior.

7414
deno.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@apiclient.xyz/cloudflare",
"version": "6.3.0",
"version": "6.4.3",
"private": false,
"description": "A TypeScript client for managing Cloudflare accounts, zones, DNS records, and workers with ease.",
"main": "dist_ts/index.js",
@@ -14,7 +14,7 @@
},
"repository": {
"type": "git",
"url": "git+https://gitlab.com/pushrocks/cflare.git"
"url": "https://gitlab.com/mojoio/cloudflare.git"
},
"keywords": [
"Cloudflare",
@@ -31,26 +31,25 @@
"author": "Lossless GmbH",
"license": "MIT",
"bugs": {
"url": "https://gitlab.com/pushrocks/cflare/issues"
"url": "https://gitlab.com/mojoio/cloudflare/issues"
},
"homepage": "https://gitlab.com/pushrocks/cflare#readme",
"homepage": "https://gitlab.com/mojoio/cloudflare#readme",
"dependencies": {
"@push.rocks/smartdelay": "^3.0.1",
"@push.rocks/smartlog": "^3.0.2",
"@push.rocks/smartlog": "^3.1.10",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^2.0.23",
"@push.rocks/smartstring": "^4.0.5",
"@tsclass/tsclass": "^5.0.0",
"cloudflare": "^4.2.0"
"@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartstring": "^4.1.0",
"@tsclass/tsclass": "^9.3.0",
"cloudflare": "^5.2.0"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.2.7",
"@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^1.0.96",
"@push.rocks/qenv": "^6.1.0",
"@push.rocks/tapbundle": "^5.6.0",
"@types/node": "^22.13.10",
"openapi-typescript": "^7.6.1"
"@git.zone/tsbuild": "^3.1.0",
"@git.zone/tsrun": "^2.0.0",
"@git.zone/tstest": "^2.8.2",
"@push.rocks/qenv": "^6.1.3",
"@types/node": "^22.15.3",
"openapi-typescript": "^7.10.1"
},
"files": [
"ts/**/*",
@@ -67,5 +66,8 @@
"browserslist": [
"last 1 chrome versions"
],
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6",
"pnpm": {
"overrides": {}
}
}

7608
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -126,11 +126,11 @@ 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'
challenge: 'token-validation-string',
});
await cfAccount.convenience.acmeRemoveDnsChallenge({
hostName: '_acme-challenge.example.com',
challenge: 'token-validation-string'
challenge: 'token-validation-string',
});
```
@@ -157,12 +157,12 @@ const existingWorker = await cfAccount.workerManager.getWorker('my-worker');
await worker.setRoutes([
{
zoneName: 'example.com',
pattern: 'https://api.example.com/*'
pattern: 'https://api.example.com/*',
},
{
zoneName: 'example.com',
pattern: 'https://app.example.com/api/*'
}
pattern: 'https://app.example.com/api/*',
},
]);
// Get all routes for a worker
@@ -219,9 +219,7 @@ async function manageCloudflare() {
})`;
const worker = await cfAccount.workerManager.createWorker('api-handler', workerCode);
await worker.setRoutes([
{ zoneName: 'example.com', pattern: 'https://api.example.com/*' }
]);
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']);
@@ -266,8 +264,18 @@ class CloudflareAccount {
// DNS operations
listRecords(domainName: string): Promise<CloudflareRecord[]>;
getRecord(domainName: string, recordType: string): Promise<CloudflareRecord | undefined>;
createRecord(domainName: string, recordType: string, content: string, ttl?: number): Promise<any>;
updateRecord(domainName: string, recordType: string, content: string, ttl?: number): Promise<any>;
createRecord(
domainName: string,
recordType: string,
content: string,
ttl?: number,
): Promise<any>;
updateRecord(
domainName: string,
recordType: string,
content: string,
ttl?: number,
): Promise<any>;
removeRecord(domainName: string, recordType: string): Promise<any>;
cleanRecord(domainName: string, recordType: string): Promise<void>;

View File

@@ -1,5 +1,5 @@
// tslint:disable-next-line: no-implicit-dependencies
import { expect, tap } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
// tslint:disable-next-line: no-implicit-dependencies
import { Qenv } from '@push.rocks/qenv';
@@ -14,7 +14,9 @@ let testZoneName = `test-zone-${randomPrefix}.com`;
// Basic initialization tests
tap.test('should create a valid instance of CloudflareAccount', async () => {
testCloudflareAccount = new cloudflare.CloudflareAccount(await testQenv.getEnvVarOnDemand('CF_KEY'));
testCloudflareAccount = new cloudflare.CloudflareAccount(
await testQenv.getEnvVarOnDemand('CF_KEY'),
);
expect(testCloudflareAccount).toBeTypeOf('object');
expect(testCloudflareAccount.apiAccount).toBeTypeOf('object');
});
@@ -22,7 +24,7 @@ tap.test('should create a valid instance of CloudflareAccount', async () => {
tap.test('should preselect an account', async () => {
await testCloudflareAccount.preselectAccountByName('Sandbox Account');
expect(testCloudflareAccount.preselectedAccountId).toBeTypeOf('string');
})
});
// Zone management tests
tap.test('.listZones() -> should list zones in account', async (tools) => {
@@ -94,7 +96,7 @@ tap.test('should create A record for subdomain', async (tools) => {
subdomain,
'A',
'127.0.0.1',
120
120,
);
expect(result).toBeTypeOf('object');
console.log(`Created A record for ${subdomain}`);
@@ -107,7 +109,7 @@ tap.test('should create CNAME record for subdomain', async (tools) => {
subdomain,
'CNAME',
'example.com',
120
120,
);
expect(result).toBeTypeOf('object');
console.log(`Created CNAME record for ${subdomain}`);
@@ -120,7 +122,7 @@ tap.test('should create TXT record for subdomain', async (tools) => {
subdomain,
'TXT',
'v=spf1 include:_spf.example.com ~all',
120
120,
);
expect(result).toBeTypeOf('object');
console.log(`Created TXT record for ${subdomain}`);
@@ -142,13 +144,59 @@ tap.test('should update A record content', async (tools) => {
subdomain,
'A',
'192.168.1.1',
120
120,
);
expect(result).toBeTypeOf('object');
expect(result.content).toEqual('192.168.1.1');
console.log(`Updated A record for ${subdomain} to 192.168.1.1`);
});
// Nested subdomain DNS record tests
tap.test('should create A record for nested subdomain', async (tools) => {
tools.timeout(600000);
const nestedSubdomain = `${randomPrefix}-nested.sub.bleu.de`;
const result = await testCloudflareAccount.convenience.createRecord(
nestedSubdomain,
'A',
'127.0.0.5',
120,
);
expect(result).toBeTypeOf('object');
console.log(`Created nested A record for ${nestedSubdomain}`);
});
tap.test('should get A record for nested subdomain', async (tools) => {
tools.timeout(600000);
const nestedSubdomain = `${randomPrefix}-nested.sub.bleu.de`;
const record = await testCloudflareAccount.convenience.getRecord(nestedSubdomain, 'A');
expect(record).toBeTypeOf('object');
expect(record.content).toEqual('127.0.0.5');
console.log(`Successfully retrieved nested A record for ${nestedSubdomain}`);
});
tap.test('should update A record for nested subdomain', async (tools) => {
tools.timeout(600000);
const nestedSubdomain = `${randomPrefix}-nested.sub.bleu.de`;
const result = await testCloudflareAccount.convenience.updateRecord(
nestedSubdomain,
'A',
'127.0.0.6',
120,
);
expect(result).toBeTypeOf('object');
expect(result.content).toEqual('127.0.0.6');
console.log(`Updated nested A record for ${nestedSubdomain}`);
});
tap.test('should remove nested subdomain A record', async (tools) => {
tools.timeout(600000);
const nestedSubdomain = `${randomPrefix}-nested.sub.bleu.de`;
await testCloudflareAccount.convenience.removeRecord(nestedSubdomain, 'A');
const record = await testCloudflareAccount.convenience.getRecord(nestedSubdomain, 'A');
expect(record).toBeUndefined();
console.log(`Successfully removed nested A record for ${nestedSubdomain}`);
});
tap.test('should clean TXT records', async (tools) => {
tools.timeout(600000);
const subdomain = `${randomPrefix}-txt-test.bleu.de`;
@@ -208,7 +256,7 @@ tap.test('should create a worker', async (tools) => {
event.respondWith(new Response('Hello from Cloudflare Workers!', {
headers: { 'content-type': 'text/plain' }
}))
})`
})`,
);
expect(worker).toBeTypeOf('object');
@@ -247,7 +295,7 @@ tap.test('should get a specific worker by name', async (tools) => {
event.respondWith(new Response('Hello from Cloudflare Workers!', {
headers: { 'content-type': 'text/plain' }
}))
})`
})`,
);
// Now get the worker

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@apiclient.xyz/cloudflare',
version: '6.3.0',
version: '6.4.3',
description: 'A TypeScript client for managing Cloudflare accounts, zones, DNS records, and workers with ease.'
}

View File

@@ -6,7 +6,7 @@ import * as interfaces from './interfaces/index.js';
import { WorkerManager } from './cloudflare.classes.workermanager.js';
import { ZoneManager } from './cloudflare.classes.zonemanager.js';
export class CloudflareAccount {
export class CloudflareAccount implements plugins.tsclass.network.IConvenientDnsProvider {
private authToken: string;
public preselectedAccountId: string;
@@ -39,54 +39,80 @@ export class CloudflareAccount {
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
endpoint: string,
data?: any,
customHeaders?: Record<string, string>
customHeaders?: Record<string, string>,
): Promise<T> {
try {
const options: plugins.smartrequest.ISmartRequestOptions = {
method,
headers: {
'Authorization': `Bearer ${this.authToken}`,
'Content-Type': 'application/json',
...customHeaders,
},
};
logger.log('debug', `Making ${method} request to ${endpoint}`);
// Build the request using fluent API
let requestBuilder = plugins.smartrequest.SmartRequest.create()
.url(`https://api.cloudflare.com/client/v4${endpoint}`)
.header('Authorization', `Bearer ${this.authToken}`);
// Add custom headers
if (customHeaders) {
for (const [key, value] of Object.entries(customHeaders)) {
requestBuilder = requestBuilder.header(key, value);
}
} else {
// Default to JSON content type if no custom headers
requestBuilder = requestBuilder.header('Content-Type', 'application/json');
}
// Add request body if provided
if (data) {
if (customHeaders && customHeaders['Content-Type']?.includes('multipart/form-data')) {
// For multipart form data, use the data directly as the request body
options.requestBody = data;
// For multipart form data, use formData method
requestBuilder = requestBuilder.formData(data);
} else {
// For JSON requests, stringify the data
options.requestBody = JSON.stringify(data);
// For JSON requests, use json method
requestBuilder = requestBuilder.json(data);
}
}
logger.log('debug', `Making ${method} request to ${endpoint}`);
const response = await plugins.smartrequest.request(`https://api.cloudflare.com/client/v4${endpoint}`, options);
// Check if response is already an object (might happen with newer smartrequest versions)
if (typeof response.body === 'object' && response.body !== null) {
return response.body;
// Execute the request with the appropriate method
let response: InstanceType<typeof plugins.smartrequest.CoreResponse>;
switch (method) {
case 'GET':
response = await requestBuilder.get();
break;
case 'POST':
response = await requestBuilder.post();
break;
case 'PUT':
response = await requestBuilder.put();
break;
case 'DELETE':
response = await requestBuilder.delete();
break;
case 'PATCH':
response = await requestBuilder.patch();
break;
default:
throw new Error(`Unsupported HTTP method: ${method}`);
}
// Otherwise try to parse as JSON
// Check response status
if (!response.ok) {
const errorBody = await response.text();
logger.log('error', `HTTP ${response.status}: ${errorBody}`);
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// Parse the response body
try {
if (typeof response.body === 'string' && response.body.trim()) {
return JSON.parse(response.body);
} else {
// If body is empty or not a string, return an empty result
logger.log('warn', `Empty or invalid response body: ${typeof response.body}`);
return { result: [] } as T;
}
return (await response.json()) as T;
} catch (parseError) {
logger.log('warn', `Failed to parse response as JSON: ${parseError.message}`);
// Create a fake response object to maintain expected structure
// Try to get as text and create a fallback response
const textBody = await response.text().catch(() => '');
return {
result: [],
success: true,
errors: [],
messages: [`Failed to parse: ${typeof response.body === 'string' ? response.body?.substring(0, 50) : typeof response.body}...`]
messages: [`Failed to parse: ${textBody.substring(0, 50)}...`],
} as T;
}
} catch (error) {
@@ -152,14 +178,17 @@ export class CloudflareAccount {
*/
getRecord: async (
domainNameArg: string,
typeArg: plugins.tsclass.network.TDnsRecordType
typeArg: plugins.tsclass.network.TDnsRecordType,
): Promise<plugins.ICloudflareTypes['Record'] | undefined> => {
try {
const domain = new plugins.smartstring.Domain(domainNameArg);
const recordArrayArg = await this.convenience.listRecords(domain.zoneName);
if (!Array.isArray(recordArrayArg)) {
logger.log('warn', `Expected records array for ${domainNameArg} but got ${typeof recordArrayArg}`);
logger.log(
'warn',
`Expected records array for ${domainNameArg} but got ${typeof recordArrayArg}`,
);
return undefined;
}
@@ -180,7 +209,7 @@ export class CloudflareAccount {
domainNameArg: string,
typeArg: plugins.tsclass.network.TDnsRecordType,
contentArg: string,
ttlArg = 1
ttlArg = 1,
): Promise<any> => {
const domain = new plugins.smartstring.Domain(domainNameArg);
const zoneId = await this.convenience.getZoneId(domain.zoneName);
@@ -190,7 +219,7 @@ export class CloudflareAccount {
name: domain.fullName,
content: contentArg,
ttl: ttlArg,
})
});
return response;
},
/**
@@ -200,7 +229,7 @@ export class CloudflareAccount {
*/
removeRecord: async (
domainNameArg: string,
typeArg: plugins.tsclass.network.TDnsRecordType
typeArg: plugins.tsclass.network.TDnsRecordType,
): Promise<any> => {
const domain = new plugins.smartstring.Domain(domainNameArg);
const zoneId = await this.convenience.getZoneId(domain.zoneName);
@@ -229,18 +258,26 @@ export class CloudflareAccount {
const domain = new plugins.smartstring.Domain(domainNameArg);
const zoneId = await this.convenience.getZoneId(domain.zoneName);
const records = await this.convenience.listRecords(domainNameArg);
// List all records in the zone for this domain
const records = await this.convenience.listRecords(domain.zoneName);
if (!Array.isArray(records)) {
logger.log('warn', `Expected records array for ${domainNameArg} but got ${typeof records}`);
logger.log(
'warn',
`Expected records array for ${domainNameArg} but got ${typeof records}`,
);
return;
}
// Only delete records matching the specified name and type
const recordsToDelete = records.filter((recordArg) => {
return recordArg.type === typeArg;
return recordArg.type === typeArg && recordArg.name === domainNameArg;
});
logger.log('info', `Found ${recordsToDelete.length} ${typeArg} records to delete for ${domainNameArg}`);
logger.log(
'info',
`Found ${recordsToDelete.length} ${typeArg} records to delete for ${domainNameArg}`,
);
for (const recordToDelete of recordsToDelete) {
try {
@@ -261,7 +298,10 @@ export class CloudflareAccount {
}
}
} catch (error) {
logger.log('error', `Error cleaning ${typeArg} records for ${domainNameArg}: ${error.message}`);
logger.log(
'error',
`Error cleaning ${typeArg} records for ${domainNameArg}: ${error.message}`,
);
}
},
@@ -277,7 +317,7 @@ export class CloudflareAccount {
domainNameArg: string,
typeArg: plugins.tsclass.network.TDnsRecordType,
contentArg: string,
ttlArg: number = 1
ttlArg: number = 1,
): Promise<plugins.ICloudflareTypes['Record']> => {
const domain = new plugins.smartstring.Domain(domainNameArg);
const zoneId = await this.convenience.getZoneId(domain.zoneName);
@@ -286,7 +326,10 @@ export class CloudflareAccount {
const record = await this.convenience.getRecord(domainNameArg, typeArg);
if (!record) {
logger.log('warn', `Record ${domainNameArg} of type ${typeArg} not found for update, creating instead`);
logger.log(
'warn',
`Record ${domainNameArg} of type ${typeArg} not found for update, creating instead`,
);
return this.convenience.createRecord(domainNameArg, typeArg, contentArg, ttlArg);
}
@@ -297,7 +340,7 @@ export class CloudflareAccount {
type: typeArg as any,
name: domain.fullName,
content: contentArg,
ttl: ttlArg
ttl: ttlArg,
});
return updatedRecord;
@@ -344,13 +387,34 @@ export class CloudflareAccount {
zones.push(zone);
}
logger.log('info', `Found ${zones.length} zones${domainName ? ` matching ${domainName}` : ''}`);
logger.log(
'info',
`Found ${zones.length} zones${domainName ? ` matching ${domainName}` : ''}`,
);
return zones;
} catch (error) {
logger.log('error', `Failed to list zones: ${error.message}`);
return [];
}
},
/**
* Determines whether the given domain can be managed by this account
* @param domainName Full domain name to check (e.g., "sub.example.com")
* @returns True if the zone for the domain exists in the account, false otherwise
*/
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
*/
@@ -370,7 +434,7 @@ export class CloudflareAccount {
dnsChallenge.hostName,
'TXT',
dnsChallenge.challenge,
120
120,
);
},
acmeRemoveDnsChallenge: async (dnsChallenge: plugins.tsclass.network.IDnsChallenge) => {

View File

@@ -22,7 +22,9 @@ export class CloudflareRecord {
* @param apiObject Cloudflare DNS record API object
* @returns CloudflareRecord instance
*/
public static createFromApiObject(apiObject: plugins.ICloudflareTypes['Record']): CloudflareRecord {
public static createFromApiObject(
apiObject: plugins.ICloudflareTypes['Record'],
): CloudflareRecord {
const record = new CloudflareRecord();
Object.assign(record, apiObject);
return record;
@@ -52,7 +54,7 @@ export class CloudflareRecord {
public async update(
cloudflareAccount: any,
newContent: string,
ttl?: number
ttl?: number,
): Promise<CloudflareRecord> {
logger.log('info', `Updating record ${this.name} (${this.type}) with new content`);
@@ -62,7 +64,7 @@ export class CloudflareRecord {
name: this.name,
content: newContent,
ttl: ttl || this.ttl,
proxied: this.proxied
proxied: this.proxied,
});
// Update this instance
@@ -84,7 +86,7 @@ export class CloudflareRecord {
logger.log('info', `Deleting record ${this.name} (${this.type})`);
await cloudflareAccount.apiAccount.dns.records.delete(this.id, {
zone_id: this.zone_id
zone_id: this.zone_id,
});
return true;

View File

@@ -16,7 +16,7 @@ export class CloudflareWorker {
// STATIC
public static async fromApiObject(
workerManager: WorkerManager,
apiObject
apiObject,
): Promise<CloudflareWorker> {
const newWorker = new CloudflareWorker(workerManager);
Object.assign(newWorker, apiObject);
@@ -68,7 +68,7 @@ export class CloudflareWorker {
// Get worker routes for this zone
const apiRoutes = [];
for await (const route of this.workerManager.cfAccount.apiAccount.workers.routes.list({
zone_id: zone.id
zone_id: zone.id,
})) {
apiRoutes.push(route);
}
@@ -81,7 +81,10 @@ export class CloudflareWorker {
}
}
} catch (error) {
logger.log('error', `Failed to get worker routes for zone ${zone.name || zone.id}: ${error.message}`);
logger.log(
'error',
`Failed to get worker routes for zone ${zone.name || zone.id}: ${error.message}`,
);
}
}
@@ -120,7 +123,9 @@ export class CloudflareWorker {
try {
// Get the zone ID
const zone = await this.workerManager.cfAccount.zoneManager.getZoneByName(newRoute.zoneName);
const zone = await this.workerManager.cfAccount.zoneManager.getZoneByName(
newRoute.zoneName,
);
if (!zone) {
logger.log('error', `Zone ${newRoute.zoneName} not found`);
@@ -132,7 +137,7 @@ export class CloudflareWorker {
await this.workerManager.cfAccount.apiAccount.workers.routes.create({
zone_id: zone.id,
pattern: newRoute.pattern,
script: this.id
script: this.id,
});
logger.log('info', `Created new route ${newRoute.pattern} for worker ${this.id}`);
@@ -140,7 +145,7 @@ export class CloudflareWorker {
await this.workerManager.cfAccount.apiAccount.workers.routes.update(existingRouteId, {
zone_id: zone.id,
pattern: newRoute.pattern,
script: this.id
script: this.id,
});
logger.log('info', `Updated route ${newRoute.pattern} for worker ${this.id}`);
@@ -167,12 +172,19 @@ export class CloudflareWorker {
try {
logger.log('info', `Updating script for worker ${this.id}`);
// Use the official client to update the script
const updatedWorker = await this.workerManager.cfAccount.apiAccount.workers.scripts.content.update(this.id, {
// Use the official client to update the script (upload new content)
// Build params as any to include the script form part without TS errors
const updateParams: any = {
account_id: this.workerManager.cfAccount.preselectedAccountId,
"CF-WORKER-BODY-PART": scriptContent,
metadata: {}
});
metadata: { body_part: 'script' },
};
updateParams['CF-WORKER-BODY-PART'] = 'script';
updateParams['script'] = scriptContent;
const updatedWorker =
await this.workerManager.cfAccount.apiAccount.workers.scripts.content.update(
this.id,
updateParams,
);
// Update this instance with new data
if (updatedWorker && typeof updatedWorker === 'object') {
@@ -203,7 +215,7 @@ export class CloudflareWorker {
// Use the official client to delete the worker
await this.workerManager.cfAccount.apiAccount.workers.scripts.delete(this.id, {
account_id: this.workerManager.cfAccount.preselectedAccountId
account_id: this.workerManager.cfAccount.preselectedAccountId,
});
return true;

View File

@@ -22,12 +22,15 @@ export class WorkerManager {
}
try {
// Use the official client to create/update the worker
await this.cfAccount.apiAccount.workers.scripts.content.update(workerName, {
// Use the official client to create/update the worker (upload script content)
// Build params as any to include the script form part without TS errors
const contentParams: any = {
account_id: this.cfAccount.preselectedAccountId,
"CF-WORKER-BODY-PART": workerScript,
metadata: {}
});
metadata: { body_part: 'script' },
};
contentParams['CF-WORKER-BODY-PART'] = 'script';
contentParams['script'] = workerScript;
await this.cfAccount.apiAccount.workers.scripts.content.update(workerName, contentParams);
// Create a new worker instance
const worker = new CloudflareWorker(this);
@@ -62,7 +65,7 @@ export class WorkerManager {
try {
// Get the worker script using the official client
const workerScript = await this.cfAccount.apiAccount.workers.scripts.get(workerName, {
account_id: this.cfAccount.preselectedAccountId
account_id: this.cfAccount.preselectedAccountId,
});
// Create a new worker instance
@@ -115,9 +118,9 @@ export class WorkerManager {
logger.log('warn', `Error while listing workers with async iterator: ${error.message}`);
// Try alternative approach if the async iterator fails
const result = await this.cfAccount.apiAccount.workers.scripts.list({
const result = (await this.cfAccount.apiAccount.workers.scripts.list({
account_id: this.cfAccount.preselectedAccountId,
}) as any;
})) as any;
// Check if the result has a 'result' property (older API response format)
if (result && result.result && Array.isArray(result.result)) {
@@ -146,7 +149,7 @@ export class WorkerManager {
try {
await this.cfAccount.apiAccount.workers.scripts.delete(workerName, {
account_id: this.cfAccount.preselectedAccountId
account_id: this.cfAccount.preselectedAccountId,
});
logger.log('info', `Worker '${workerName}' deleted successfully`);
return true;

View File

@@ -34,7 +34,7 @@ export class CloudflareZone {
*/
public static createFromApiObject(
apiObject: plugins.ICloudflareTypes['Zone'],
cfAccount?: CloudflareAccount
cfAccount?: CloudflareAccount,
): CloudflareZone {
const cloudflareZone = new CloudflareZone();
Object.assign(cloudflareZone, apiObject);
@@ -62,7 +62,7 @@ export class CloudflareZone {
*/
public async enableDevelopmentMode(
cfAccount?: CloudflareAccount,
duration: number = 10800
duration: number = 10800,
): Promise<CloudflareZone> {
const account = cfAccount || this.cfAccount;
if (!account) {
@@ -76,7 +76,7 @@ export class CloudflareZone {
// We'll use the request method for this specific case
await account.request('PATCH', `/zones/${this.id}/settings/development_mode`, {
value: 'on',
time: duration
time: duration,
});
this.development_mode = duration;
@@ -104,7 +104,7 @@ export class CloudflareZone {
// The official client doesn't have a direct method for development mode
// We'll use the request method for this specific case
await account.request('PATCH', `/zones/${this.id}/settings/development_mode`, {
value: 'off'
value: 'off',
});
this.development_mode = 0;
@@ -131,7 +131,7 @@ export class CloudflareZone {
try {
await account.apiAccount.cache.purge({
zone_id: this.id,
purge_everything: true
purge_everything: true,
});
return true;
} catch (error) {
@@ -161,7 +161,7 @@ export class CloudflareZone {
try {
await account.apiAccount.cache.purge({
zone_id: this.id,
files: urls
files: urls,
});
return true;
} catch (error) {
@@ -189,7 +189,7 @@ export class CloudflareZone {
}
// If they're different, and current nameservers are Cloudflare's
return this.name_servers.some(ns => ns.includes('cloudflare'));
return this.name_servers.some((ns) => ns.includes('cloudflare'));
}
/**
@@ -205,7 +205,7 @@ export class CloudflareZone {
vanity_name_servers: string[];
type: 'full' | 'partial' | 'secondary';
}>,
cfAccount?: CloudflareAccount
cfAccount?: CloudflareAccount,
): Promise<CloudflareZone> {
const account = cfAccount || this.cfAccount;
if (!account) {
@@ -219,7 +219,7 @@ export class CloudflareZone {
const response: { result: interfaces.ICflareZone } = await account.request(
'PATCH',
`/zones/${this.id}`,
settings
settings,
);
Object.assign(this, response.result);

View File

@@ -30,7 +30,7 @@ export class ZoneManager {
zones.push(zone);
}
return zones.map(zone => CloudflareZone.createFromApiObject(zone, this.cfAccount));
return zones.map((zone) => CloudflareZone.createFromApiObject(zone, this.cfAccount));
} catch (error) {
logger.log('error', `Failed to fetch zones: ${error.message}`);
return [];
@@ -44,7 +44,7 @@ export class ZoneManager {
*/
public async getZoneByName(zoneName: string): Promise<CloudflareZone | undefined> {
const zones = await this.getZones(zoneName);
return zones.find(zone => zone.name === zoneName);
return zones.find((zone) => zone.name === zoneName);
}
/**
@@ -57,7 +57,7 @@ export class ZoneManager {
// Use the request method instead of the zones.get method to avoid type issues
const response: { result: interfaces.ICflareZone } = await this.cfAccount.request(
'GET',
`/zones/${zoneId}`
`/zones/${zoneId}`,
);
return CloudflareZone.createFromApiObject(response.result as any, this.cfAccount);
@@ -77,7 +77,7 @@ export class ZoneManager {
public async createZone(
zoneName: string,
jumpStart: boolean = false,
accountId?: string
accountId?: string,
): Promise<CloudflareZone | undefined> {
const useAccountId = accountId || this.cfAccount.preselectedAccountId;
@@ -95,8 +95,8 @@ export class ZoneManager {
{
name: zoneName,
jump_start: jumpStart,
account: { id: useAccountId }
}
account: { id: useAccountId },
},
);
return CloudflareZone.createFromApiObject(response.result as any, this.cfAccount);
@@ -131,7 +131,7 @@ export class ZoneManager {
*/
public async zoneExists(zoneName: string): Promise<boolean> {
const zones = await this.getZones(zoneName);
return zones.some(zone => zone.name === zoneName);
return zones.some((zone) => zone.name === zoneName);
}
/**
@@ -148,8 +148,8 @@ export class ZoneManager {
'PATCH',
`/zones/${zoneId}`,
{
status: 'active'
}
status: 'active',
},
);
return CloudflareZone.createFromApiObject(response.result as any, this.cfAccount);
@@ -171,7 +171,7 @@ export class ZoneManager {
// For this specific endpoint, we'll use the request method
const response: { result: interfaces.ICflareZone } = await this.cfAccount.request(
'PUT',
`/zones/${zoneId}/activation_check`
`/zones/${zoneId}/activation_check`,
);
return CloudflareZone.createFromApiObject(response.result as any, this.cfAccount);

View File

@@ -49,9 +49,23 @@ export class CloudflareUtils {
*/
public static isValidRecordType(type: string): boolean {
const validTypes: plugins.tsclass.network.TDnsRecordType[] = [
'A', 'AAAA', 'CNAME', 'TXT', 'SRV', 'LOC', 'MX',
'NS', 'CAA', 'CERT', 'DNSKEY', 'DS', 'NAPTR', 'SMIMEA',
'SSHFP', 'TLSA', 'URI'
'A',
'AAAA',
'CNAME',
'TXT',
'SRV',
'LOC',
'MX',
'NS',
'CAA',
'CERT',
'DNSKEY',
'DS',
'NAPTR',
'SMIMEA',
'SSHFP',
'TLSA',
'URI',
// Note: SPF has been removed as it's not in TDnsRecordType
];
return validTypes.includes(type as any);
@@ -108,7 +122,10 @@ export class CloudflareUtils {
* @returns Combined results from all pages
*/
public static async paginateResults<T>(
makeRequest: (page: number, perPage: number) => Promise<{ result: T[], result_info: { total_pages: number } }>
makeRequest: (
page: number,
perPage: number,
) => Promise<{ result: T[]; result_info: { total_pages: number } }>,
): Promise<T[]> {
const perPage = 50; // Cloudflare's maximum
let page = 1;

View File

@@ -1,5 +1,9 @@
export { CloudflareAccount } from './cloudflare.classes.account.js';
export { CloudflareWorker, type IWorkerRoute, type IWorkerRouteDefinition } from './cloudflare.classes.worker.js';
export {
CloudflareWorker,
type IWorkerRoute,
type IWorkerRouteDefinition,
} from './cloudflare.classes.worker.js';
export { WorkerManager } from './cloudflare.classes.workermanager.js';
export { CloudflareRecord, type ICloudflareRecordInfo } from './cloudflare.classes.record.js';
export { CloudflareZone } from './cloudflare.classes.zone.js';

View File

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