cloudflare-ddns-sync 4.0.0-beta.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## v4
4
+
5
+ ### v4.0.0
6
+
7
+ - ⬆️ Update dependencies, including Cloudflare 2 → 7, node-cron 3 → 4 and public-ip 6 → 8.
8
+ - ♻️ Preserve public auth fields, key/token precedence, record types, sync methods and DNS defaults while adapting to the new SDK.
9
+ - ♻️ Fetch all zone and DNS record pages; report zone-loading failures through the awaited operation.
10
+ - 🚨 Add a DDNS-oriented API with `createDdns()`, `sync()`, `schedule()`, `watch()`, `list()`, `remove()` and `ip()`
11
+ - 🚨 Allow records to be configured as hostnames; A and AAAA records automatically resolve the matching public IP
12
+ - 🚨 Return a dedicated controllable job from schedules and watchers
13
+ - 🐛 Respect the requested DNS type when listing records with the same hostname
14
+ - ♻️ Validate configuration before network access, make jobs safe to stop and add `sync({ipv4, ipv6})` for configured records
15
+
3
16
  ## v3
4
17
 
5
18
  ### 3.0.2
@@ -0,0 +1,64 @@
1
+ # Migrating from v3 to v4
2
+
3
+ v4 replaces the class-based API with `createDdns()` and consolidates related methods. Node.js 20 remains the minimum requirement.
4
+
5
+ ## Initialization
6
+
7
+ ```ts
8
+ // v3
9
+ const ddns = new CloudflareDDNSSync({token});
10
+
11
+ // v4
12
+ const ddns = createDdns({token, records: ['home.example.com']});
13
+ ```
14
+
15
+ If `token` is omitted, v4 reads `CLOUDFLARE_API_TOKEN`. `email`/`key` and user service keys remain supported.
16
+
17
+ ## Synchronizing
18
+
19
+ ```ts
20
+ // v3
21
+ await ddns.syncRecord({name: 'home.example.com'});
22
+ await ddns.syncRecords(records, '203.0.113.10');
23
+
24
+ // v4
25
+ await ddns.sync('home.example.com');
26
+ await ddns.sync(records, {ipv4: '203.0.113.10'});
27
+ await ddns.sync({ipv4: '203.0.113.10'}); // configured records
28
+ ```
29
+
30
+ `sync()` always returns an array. For a single record, use the first entry: `const [record] = await ddns.sync('home.example.com')`.
31
+
32
+ For configured records, pass fixed addresses directly as the first argument. The former placeholder form `sync(undefined, {ipv4})` remains supported for compatibility.
33
+
34
+ A and AAAA records automatically use the matching IP family. Non-IP records now require explicit `content`.
35
+
36
+ ## Scheduling and watching
37
+
38
+ ```ts
39
+ // v3
40
+ const task = ddns.syncByCronTime('*/5 * * * *', records, callback);
41
+ const id = await ddns.syncOnIpChange(records, callback);
42
+ ddns.stopSyncOnIpChange(id);
43
+
44
+ // v4
45
+ const task = ddns.schedule('*/5 * * * *', {records, onSync: callback});
46
+ const watcher = await ddns.watch({records, onSync: callback});
47
+ await watcher.stop();
48
+ ```
49
+
50
+ `schedule()` and `watch()` return a `SyncJob` with `run()`, `start()` and `stop()`. A v4 job replaces both the listener ID and the direct `node-cron` return value.
51
+
52
+ ## Listing, removing and retrieving IPs
53
+
54
+ | v3 | v4 |
55
+ | --- | --- |
56
+ | `getRecordDataForRecord(record)` | `list({records: record})` |
57
+ | `getRecordDataForRecords(records)` | `list({records})` |
58
+ | `getRecordDataForDomain(domain)` | `list({domains: domain})` |
59
+ | `getRecordDataForDomains(domains)` | `list({domains, groupBy: 'domain'})` |
60
+ | `removeRecord(name, type?)` | `remove(name)` oder `remove({name, type})` |
61
+ | `getIp()` | `ip()` |
62
+ | `getIpv6()` | `ip(6)` |
63
+
64
+ `list()` without filters uses the configured records. Without that configuration, a filter is required.
package/README.md CHANGED
@@ -1,160 +1,134 @@
1
1
  <table>
2
2
  <tr>
3
- <td width="25%"><img src="./logo.png" alt="Logo" width="100%"></td>
3
+ <td width="25%"><img src="./logo.png" alt="Cloudflare DDNS Sync logo" width="100%"></td>
4
4
  <td><h1>Cloudflare DDNS Sync</h1></td>
5
5
  </tr>
6
6
  </table>
7
7
 
8
- ![](https://github.com/SteffenKn/cloudflare-ddns-sync/actions/workflows/push.yml/badge.svg)
9
- [![npm version](https://badge.fury.io/js/cloudflare-ddns-sync.svg)](https://www.npmjs.com/package/cloudflare-ddns-sync)
10
- [![Downloads](https://img.shields.io/npm/dm/cloudflare-ddns-sync.svg)](https://www.npmjs.com/package/cloudflare-ddns-sync)
11
- [![CLI](https://img.shields.io/badge/CLI-npm-important.svg)](https://www.npmjs.com/package/cloudflare-ddns-sync-cli)
8
+ Cloudflare DDNS Sync synchronizes DNS records with the current public IP address. Version 4 provides a small, DDNS-oriented API.
12
9
 
13
- ## Overview
10
+ ## Voraussetzungen
14
11
 
15
- Cloudflare-DDNS-Sync is a simple module that updates Cloudflare DNS records.
12
+ - Node.js 20 or newer
13
+ - A Cloudflare API token with permission to read zones and edit DNS records
16
14
 
17
- For a more detailed overview, have a look at the [Documentation](https://cddnss.knaup.pw/)
15
+ ```sh
16
+ npm install cloudflare-ddns-sync
17
+ ```
18
18
 
19
- You may also have a look at the **official** [CLI version](https://www.npmjs.com/package/cloudflare-ddns-sync-cli) of Cloudflare-DDNS-Sync.
19
+ ## Einstieg
20
20
 
21
- ## How do I set this project up?
21
+ Set `CLOUDFLARE_API_TOKEN` and configure the hostnames once:
22
22
 
23
- ### Prerequisites
23
+ ```ts
24
+ import {createDdns} from 'cloudflare-ddns-sync';
24
25
 
25
- - Node
26
- - Cloudflare Account
26
+ const ddns = createDdns({
27
+ records: ['home.example.com', {name: 'app.example.com', proxied: true}],
28
+ });
27
29
 
28
- ### Installation
30
+ await ddns.sync();
31
+ ```
29
32
 
30
- To install Cloudflare-DDNS-Sync simply run:
33
+ A string represents an A record. Missing A and AAAA records are created; existing records with the same name and type are updated. A records automatically receive the public IPv4 address, and AAAA records receive the IPv6 address.
31
34
 
32
- ```
33
- npm install cloudflare-ddns-sync
35
+ ```ts
36
+ const ddns = createDdns({
37
+ token: process.env.CLOUDFLARE_API_TOKEN,
38
+ records: [
39
+ 'home.example.com',
40
+ {name: 'home.example.com', type: 'AAAA'},
41
+ {name: 'alias.example.com', type: 'CNAME', content: 'home.example.com'},
42
+ ],
43
+ });
44
+
45
+ await ddns.sync();
34
46
  ```
35
47
 
36
- in your project folder.
48
+ Explicit `token`, `email`/`key` and user service keys remain supported. If no credentials are provided, only `CLOUDFLARE_API_TOKEN` is used.
37
49
 
38
- ## Usage
50
+ ## Synchronizing
39
51
 
40
- > Hint: If a record is not existing, CDS will automatically create it when
41
- > syncing.
52
+ ```ts
53
+ await ddns.sync();
54
+ await ddns.sync('temporary.example.com');
55
+ await ddns.sync(['home.example.com', {name: 'vpn.example.com', proxied: true}]);
42
56
 
43
- ### Javascript Example
57
+ await ddns.sync({
58
+ ipv4: '203.0.113.10',
59
+ ipv6: '2001:db8::1',
60
+ });
61
+ ```
44
62
 
45
- ```javascript
46
- const Cddnss = require('cloudflare-ddns-sync').default;
63
+ `sync()` always returns an array of updated DNS records. Explicit `content` on a record takes precedence. Record types other than A and AAAA require `content`.
47
64
 
48
- // either email and key or token
49
- const cddnss = new Cddnss({
50
- email: 'your@email.com',
51
- key: '<your-cloudflare-api-key>',
52
- token: '<your-cloudflare-api-token>',
53
- });
65
+ Use `plan()` to inspect the same operation without writing DNS records. Each entry has an `action` of `create`, `update` or `unchanged`. `sync()` leaves unchanged records untouched. If one or more writes fail, it throws `SyncError` with `succeeded` and `failed` entries.
54
66
 
55
- const records = [
56
- {
57
- name: 'test-1.domain.com',
58
- type: 'A', // optional
59
- proxied: true, // optional
60
- ttl: 1, // optional
61
- priority: 0, // optional
62
- content: '1.2.3.4', // optional
63
- },
64
- {
65
- name: 'test-2.domain.com',
66
- },
67
- ];
68
-
69
- cddnss.syncRecords(records).then((result) => {
70
- console.log(result);
71
- });
67
+ ```ts
68
+ const changes = await ddns.plan();
72
69
  ```
73
70
 
74
- ### Typescript Example
71
+ ## Scheduling and IP watching
75
72
 
76
- ```typescript
77
- import Cddnss, {Record, RecordData} from 'cloudflare-ddns-sync';
73
+ ```ts
74
+ const scheduled = ddns.schedule('*/5 * * * *');
75
+ await scheduled.stop();
78
76
 
79
- // either email and key or token
80
- const cddnss = new Cddnss({
81
- email: 'your@email.com',
82
- key: '<your-cloudflare-api-key>',
83
- token: '<your-cloudflare-api-token>',
77
+ const watching = await ddns.watch({
78
+ onError: error => console.error(error),
84
79
  });
85
80
 
86
- const records: Array<Record> = [
87
- {
88
- name: 'test-1.yourdomain.com',
89
- type: 'A', // optional
90
- proxied: true, // optional
91
- ttl: 1, // optional
92
- priority: 0, // optional
93
- content: '1.2.3.4', // optional
94
- },
95
- {
96
- name: 'test-2.yourdomain.com',
97
- },
98
- ];
99
-
100
- cddnss.syncRecords(records).then((result: Array<RecordData>) => {
101
- console.log(result);
102
- });
81
+ await watching.stop();
103
82
  ```
104
83
 
105
- ### Cron Expression Syntax
84
+ `schedule()` starts immediately and synchronizes at the next cron interval. `watch()` synchronizes first and then watches only the required IP families; the default interval is ten seconds. Fixed addresses and records with explicit `content` are not polled. Both return a job with `run()`, `start()` and `stop()` methods. Errors from `run()` reject its promise; automatic runs call `onError` once.
106
85
 
107
- Cron expressions have the following syntax:
86
+ Configuration errors are `DdnsError`s with `code: 'INVALID_CONFIG'`; errors while looking up a public address have `code: 'IP_UNAVAILABLE'`.
108
87
 
88
+ ## Listing and removing records
89
+
90
+ ```ts
91
+ const records = await ddns.list({records: 'home.example.com'});
92
+ const ipv6Records = await ddns.list({records: {name: 'home.example.com', type: 'AAAA'}});
93
+ const domainRecords = await ddns.list({domains: 'example.com'});
94
+ const grouped = await ddns.list({domains: ['example.com', 'example.org'], groupBy: 'domain'});
95
+
96
+ await ddns.remove('home.example.com');
97
+ await ddns.remove({name: 'home.example.com', type: 'AAAA'});
109
98
  ```
110
- * * * * * *
111
-
112
- │ │ │ │ │ │
113
- │ │ │ └──── weekday (0-7, sunday is 0 or 7)
114
- │ │ │ │ └────── month (1-12)
115
- │ │ │ └──────── day (1-31)
116
- └────────── hour (0-23)
117
- └──────────── minute (0-59)
118
- └────────────── second (0-59) [optional]
99
+
100
+ `remove('name')` removes an A record. Use a record object for other types. `list()` without filters uses the records configured when creating the DDNS instance. A record filter with `type` returns only that DNS type.
101
+
102
+ ## Public IP
103
+
104
+ ```ts
105
+ const ipv4 = await ddns.ip();
106
+ const ipv6 = await ddns.ip(6);
119
107
  ```
120
108
 
121
- ## Methods
122
-
123
- - getIp(): Promise\<string\>
124
- - getIpv6(): Promise\<string\>
125
- - getRecordDataForDomain(domain: string): Promise\<Array\<[RecordData](https://cddnss.knaup.pw/types/recorddata)\>\>
126
- - getRecordDataForDomains(domains: Array\<string\>): Promise\<[DomainRecordList](https://cddnss.knaup.pw/types/domainrecordlist)\>
127
- - getRecordDataForRecord(record: [Record](https://cddnss.knaup.pw/types/record)): Promise\<[RecordData](https://cddnss.knaup.pw/types/recorddata)\>
128
- - getRecordDataForRecords(records: Array\<[Record](https://cddnss.knaup.pw/types/record)\>): Promise\<Array\<[RecordData](https://cddnss.knaup.pw/types/recorddata)\>\>
129
- - removeRecord(recordName: string, recordType?: string): Promise\<void\>
130
- - stopSyncOnIpChange(changeListenerId: string): void
131
- - syncByCronTime(cronExpression: string, records: Array\<[Record](https://cddnss.knaup.pw/types/recorddata)\>, callback: [MultiSyncCallback](https://cddnss.knaup.pw/types/multisynccallback), ip?: string): [ScheduledTask](https://www.npmjs.com/package/node-cron#scheduledtask-methods)
132
- - syncOnIpChange(records: Array\<[Record](https://cddnss.knaup.pw/types/record)\>, callback: multisynccallback): Promise\<string\>
133
- - syncRecord(record: [Record](https://cddnss.knaup.pw/types/record), ip?: string): Promise\<[RecordData](https://cddnss.knaup.pw/types/recorddata)\>
134
- - syncRecords(records: Array\<[Record](https://cddnss.knaup.pw/types/record)\>, ip?: string): Promise\<Array\<[RecordData](https://cddnss.knaup.pw/types/recorddata)\>\>
135
-
136
- For a more detailed view, have a look at the [Documentation](https://cddnss.knaup.pw/)
137
-
138
- ## Get Your Cloudflare API Key
139
-
140
- - Go to **[Cloudflare](https://www.cloudflare.com)**
141
- - **Log In**
142
- - In the upper right corner: **click on the user icon**
143
- - Go to **"My Profile"**
144
- - In the "API Tokens"-Section: **click on the "View"-Button of the Global Key**
145
- - **Enter your password** and **fill the captcha**
146
- - **Copy the API Key**
109
+ ## Optional: eigene IP-Quelle und Zone
147
110
 
148
- ## Tests
111
+ ```ts
112
+ const ddns = createDdns({
113
+ zone: 'example.com',
114
+ records: ['@', 'home'],
115
+ resolveIp: family => readAddressFromRouter(family),
116
+ });
117
+
118
+ await ddns.close();
119
+ ```
149
120
 
150
- In order to run the tests there are two ways to do so
121
+ With `zone`, `@` means the zone apex and simple labels are expanded. `resolveIp` is used by both `sync()` and `watch()`.
151
122
 
152
- ### Use `test-data.json`
123
+ ## Migrating from v3
153
124
 
154
- - Open the `test-data.json` which can be found under `src/tests/test-service/`
155
- - Configure the email, cloudflare api key and the domain
156
- - Run `npm test`
125
+ v4 is a breaking release. See [MIGRATION-V4.md](./MIGRATION-V4.md) for the complete mapping of the previous API.
157
126
 
158
- ### Use `npm test` Only
127
+ ## Tests
128
+
129
+ The integration tests create and remove records in a Cloudflare test zone:
159
130
 
160
- - Run `npm test -- --email="your@email.com" --key="your_cloudflare_api_key" --domain="yourdomain.com"`
131
+ ```sh
132
+ npm run build
133
+ npm test -- --token="$CLOUDFLARE_API_TOKEN" --domain="example.com"
134
+ ```
package/logo.png CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-ddns-sync",
3
- "version": "4.0.0-beta.1",
3
+ "version": "4.0.0",
4
4
  "description": "A simple module to update DNS records on Cloudflare whenever you want",
5
5
  "main": "dist/index.js",
6
6
  "author": "Steffen Knaup <SteffenKnaup@hotmail.de>",
@@ -20,7 +20,8 @@
20
20
  "url": "git://github.com:Steffen982/cloudflare-ddns-sync.git"
21
21
  },
22
22
  "scripts": {
23
- "build": "tsc",
23
+ "clean": "node --input-type=module -e \"import {rm} from 'node:fs/promises'; await rm('dist', {recursive: true, force: true});\"",
24
+ "build": "npm run clean && tsc",
24
25
  "lint": "prettier -c ./src",
25
26
  "lint:fix": "prettier -w ./src",
26
27
  "test": "c8 mocha dist/tests/*.js --timeout 15000 --exit",
@@ -29,23 +30,24 @@
29
30
  "test:coverage-report": "c8 report"
30
31
  },
31
32
  "dependencies": {
32
- "cloudflare": "3.0.0-beta.12",
33
- "node-cron": "3.0.3",
34
- "parse-domain": "8.0.2",
35
- "public-ip": "6.0.2"
33
+ "cloudflare": "7.1.0",
34
+ "node-cron": "4.6.0",
35
+ "parse-domain": "8.4.0",
36
+ "public-ip": "8.0.0"
36
37
  },
37
38
  "devDependencies": {
38
- "@types/chai": "4.3.14",
39
- "@types/cloudflare": "^2.7.14",
39
+ "@types/chai": "5.2.3",
40
40
  "@types/minimist": "1.2.5",
41
- "@types/mocha": "10.0.6",
42
- "@types/node": "20.11.30",
43
- "@types/node-cron": "3.0.11",
44
- "c8": "9.1.0",
45
- "chai": "5.1.0",
41
+ "@types/mocha": "10.0.10",
42
+ "@types/node": "26.5.0",
43
+ "c8": "12.0.0",
44
+ "chai": "6.2.2",
46
45
  "minimist": "1.2.8",
47
- "mocha": "10.3.0",
48
- "prettier": "^3.2.5",
49
- "typescript": "5.4.3"
46
+ "mocha": "12.0.0",
47
+ "prettier": "3.9.6",
48
+ "typescript": "7.0.2"
49
+ },
50
+ "engines": {
51
+ "node": ">=20"
50
52
  }
51
53
  }
package/dist/index.d.ts DELETED
@@ -1,18 +0,0 @@
1
- import { Auth, MultiSyncCallback, Record } from './types/index.js';
2
- export default class CloudflareDDNSSync {
3
- private cloudflareClient;
4
- constructor(auth: Auth);
5
- getIp(): Promise<string>;
6
- getIpv6(): Promise<string>;
7
- getRecordDataForDomain(domain: string): Promise<import("cloudflare/resources/dns/records.mjs").DNSRecord[]>;
8
- getRecordDataForDomains(domains: Array<string>): Promise<{}>;
9
- getRecordDataForRecord(record: Record): Promise<import("cloudflare/resources/dns/records.mjs").DNSRecord>;
10
- getRecordDataForRecords(records: Array<Record>): Promise<any[]>;
11
- removeRecord(recordName: string, recordType?: string): Promise<void>;
12
- stopSyncOnIpChange(changeListenerId: string): void;
13
- syncByCronTime(cronExpression: string, records: Array<Record>, callback: MultiSyncCallback, ip?: string): import("node-cron").ScheduledTask;
14
- syncOnIpChange(records: Array<Record>, callback: MultiSyncCallback): Promise<string>;
15
- syncRecord(record: Record, ip?: string): any;
16
- syncRecords(records: Array<Record>, ip?: string): Promise<import("cloudflare/resources/dns/records.mjs").DNSRecord[]>;
17
- }
18
- export * from './types/index.js';
package/dist/index.js DELETED
@@ -1,58 +0,0 @@
1
- import CloudflareClient from './lib/cloudflare-client.js';
2
- import Cron from './lib/cron.js';
3
- import ipUtils from './lib/ip-utils.js';
4
- export default class CloudflareDDNSSync {
5
- cloudflareClient;
6
- constructor(auth) {
7
- this.cloudflareClient = new CloudflareClient(auth);
8
- }
9
- getIp() {
10
- return ipUtils.getIpv4();
11
- }
12
- getIpv6() {
13
- return ipUtils.getIpv6();
14
- }
15
- getRecordDataForDomain(domain) {
16
- return this.cloudflareClient.getRecordsByDomain(domain);
17
- }
18
- getRecordDataForDomains(domains) {
19
- return this.cloudflareClient.getRecordsByDomains(domains);
20
- }
21
- getRecordDataForRecord(record) {
22
- return this.cloudflareClient.getRecordDataForRecord(record);
23
- }
24
- getRecordDataForRecords(records) {
25
- return this.cloudflareClient.getRecordDataForRecords(records);
26
- }
27
- removeRecord(recordName, recordType) {
28
- return this.cloudflareClient.removeRecordByNameAndType(recordName, recordType);
29
- }
30
- stopSyncOnIpChange(changeListenerId) {
31
- ipUtils.removeIpChangeListener(changeListenerId);
32
- }
33
- syncByCronTime(cronExpression, records, callback, ip) {
34
- return Cron.createCronJob(cronExpression, async () => {
35
- const result = await this.syncRecords(records, ip);
36
- callback(result);
37
- });
38
- }
39
- async syncOnIpChange(records, callback) {
40
- const changeListenerId = await ipUtils.addIpChangeListener(async (ip) => {
41
- const result = await this.syncRecords(records, ip);
42
- callback(result);
43
- });
44
- // Sync records to make sure the current ip is already set.
45
- const currentIp = await ipUtils.getIpv4();
46
- this.syncRecords(records, currentIp).then((syncedRecords) => {
47
- callback(syncedRecords);
48
- });
49
- return changeListenerId;
50
- }
51
- syncRecord(record, ip) {
52
- return this.cloudflareClient.syncRecord(record, ip);
53
- }
54
- syncRecords(records, ip) {
55
- return this.cloudflareClient.syncRecords(records, ip);
56
- }
57
- }
58
- export * from './types/index.js';
@@ -1,25 +0,0 @@
1
- import Cloudflare, { ClientOptions as CloudflareOptions } from 'cloudflare';
2
- import { Record } from '../types/index.js';
3
- export default class CloudflareClient {
4
- private cloudflare;
5
- private zoneMap;
6
- constructor(cloudflareOptions: CloudflareOptions);
7
- syncRecord(record: Record, ip?: string): Promise<Cloudflare.DNS.Records.DNSRecord>;
8
- syncRecords(records: Array<Record>, ip?: string): Promise<Cloudflare.DNS.Records.DNSRecord[]>;
9
- removeRecordByNameAndType(recordName: string, recordType?: string): Promise<void>;
10
- getRecordDataForRecord(record: Record): Promise<Cloudflare.DNS.Records.DNSRecord>;
11
- getRecordDataForRecords(records: Array<Record>): Promise<any[]>;
12
- getRecordsByDomains(domains: Array<string>): Promise<{}>;
13
- getRecordsByDomain(domain: string): Promise<Cloudflare.DNS.Records.DNSRecord[]>;
14
- private createRecord;
15
- private updateRecord;
16
- private updateZoneMap;
17
- private getRecordIdByNameAndType;
18
- private getZoneIdByRecordName;
19
- private getRecordByNameAndType;
20
- private getRecordIdsForRecords;
21
- private getRecordIdMapKey;
22
- private getZoneIdByDomain;
23
- private getDomainsFromRecords;
24
- private getDomainByRecordName;
25
- }
@@ -1,216 +0,0 @@
1
- import { ParseResultType, fromUrl, parseDomain } from 'parse-domain';
2
- import Cloudflare from 'cloudflare';
3
- import IPUtils from './ip-utils.js';
4
- const ipv4Regex = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/u;
5
- const ipv6Regex = /^(?:(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-fA-F]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})))?::(?:(?:(?:[0-9a-fA-F]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,1}(?:(?:[0-9a-fA-F]{1,4})))?::(?:(?:(?:[0-9a-fA-F]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,2}(?:(?:[0-9a-fA-F]{1,4})))?::(?:(?:(?:[0-9a-fA-F]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,3}(?:(?:[0-9a-fA-F]{1,4})))?::(?:(?:[0-9a-fA-F]{1,4})):)(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,4}(?:(?:[0-9a-fA-F]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,5}(?:(?:[0-9a-fA-F]{1,4})))?::)(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,6}(?:(?:[0-9a-fA-F]{1,4})))?::))))$/u;
6
- export default class CloudflareClient {
7
- cloudflare;
8
- zoneMap = new Map();
9
- constructor(cloudflareOptions) {
10
- this.cloudflare = new Cloudflare(cloudflareOptions);
11
- this.updateZoneMap();
12
- }
13
- async syncRecord(record, ip) {
14
- const recordIds = await this.getRecordIdsForRecords([record]);
15
- const ipToUse = ip ? ip : await IPUtils.getIpv4();
16
- const zoneId = await this.getZoneIdByRecordName(record.name);
17
- const recordId = recordIds.get(this.getRecordIdMapKey(record));
18
- const recordExists = recordId !== undefined;
19
- if (recordExists) {
20
- const result = await this.updateRecord(zoneId, recordId, record, ipToUse);
21
- return result;
22
- }
23
- const result = await this.createRecord(zoneId, record, ipToUse);
24
- return result;
25
- }
26
- async syncRecords(records, ip) {
27
- const recordIds = await this.getRecordIdsForRecords(records);
28
- const ipToUse = ip ? ip : await IPUtils.getIpv4();
29
- const resultPromises = records.map(async (record) => {
30
- const zoneId = await this.getZoneIdByRecordName(record.name);
31
- const recordId = recordIds.get(this.getRecordIdMapKey(record));
32
- const recordExists = recordId !== undefined;
33
- if (recordExists) {
34
- const currentResult = await this.updateRecord(zoneId, recordId, record, ipToUse);
35
- return currentResult;
36
- }
37
- const currentResult = await this.createRecord(zoneId, record, ipToUse);
38
- return currentResult;
39
- });
40
- const results = await Promise.all(resultPromises);
41
- return results;
42
- }
43
- async removeRecordByNameAndType(recordName, recordType) {
44
- const recordTypeToUse = recordType ? recordType : 'A';
45
- const zoneId = await this.getZoneIdByRecordName(recordName);
46
- const recordId = await this.getRecordIdByNameAndType(recordName, recordTypeToUse);
47
- await this.cloudflare.dns.records.delete(recordId, { zone_id: zoneId });
48
- }
49
- async getRecordDataForRecord(record) {
50
- const domain = this.getDomainByRecordName(record.name);
51
- const recordDataForDomain = await this.getRecordsByDomain(domain);
52
- const recordData = recordDataForDomain.find((singleRecordData) => record.name.toLowerCase() === singleRecordData.name.toLowerCase());
53
- return recordData;
54
- }
55
- async getRecordDataForRecords(records) {
56
- const domains = this.getDomainsFromRecords(records);
57
- const recordDataPromises = domains.map(async (domain) => {
58
- const recordDataForDomain = await this.getRecordsByDomain(domain);
59
- const recordDataForDomainFilteredByRecords = recordDataForDomain.filter((singleRecordData) => records.some((record) => record.name.toLowerCase() === singleRecordData.name.toLowerCase()));
60
- return recordDataForDomainFilteredByRecords;
61
- });
62
- const recordDataForDomains = await Promise.all(recordDataPromises);
63
- const recordData = [].concat(...recordDataForDomains);
64
- return recordData;
65
- }
66
- async getRecordsByDomains(domains) {
67
- const recordDataPromises = domains.map((domain) => this.getRecordsByDomain(domain));
68
- const recordDataForDomains = await Promise.all(recordDataPromises);
69
- const recordData = {};
70
- recordDataForDomains.forEach((recordDataForDomain, index) => {
71
- recordData[domains[index]] = recordDataForDomain;
72
- });
73
- return recordData;
74
- }
75
- async getRecordsByDomain(domain) {
76
- const zoneId = await this.getZoneIdByDomain(domain);
77
- const allRecords = [];
78
- for await (const recordListResponse of this.cloudflare.dns.records.list({ zone_id: zoneId })) {
79
- allRecords.push(recordListResponse);
80
- }
81
- return allRecords;
82
- }
83
- async createRecord(zoneId, recordData, ip) {
84
- const dnsRecordToCreate = {
85
- ...recordData,
86
- name: recordData.name.toLowerCase(),
87
- content: (recordData.content ? recordData.content : ip),
88
- type: (recordData.type ? recordData.type : 'A'),
89
- ttl: recordData.ttl ? recordData.ttl : 1,
90
- zone_id: zoneId,
91
- };
92
- if (!dnsRecordToCreate.content) {
93
- throw Error(`Could not create Record "${dnsRecordToCreate.name}": Content is missing!`);
94
- }
95
- if (dnsRecordToCreate.type === 'A') {
96
- if (!dnsRecordToCreate.content.match(ipv4Regex)) {
97
- throw Error(`Could not create Record "${dnsRecordToCreate.name}": '${dnsRecordToCreate.content}' is not a valid ipv4!`);
98
- }
99
- }
100
- else if (dnsRecordToCreate.type === 'AAAA') {
101
- if (!dnsRecordToCreate.content.match(ipv6Regex)) {
102
- throw Error(`Could not create Record "${dnsRecordToCreate.name}": '${dnsRecordToCreate.content}' is not a valid ipv6!`);
103
- }
104
- }
105
- else if (dnsRecordToCreate.type === 'CNAME') {
106
- const parsedDomain = parseDomain(fromUrl(dnsRecordToCreate.content));
107
- if (parsedDomain.type !== ParseResultType.Listed || !parsedDomain.domain) {
108
- throw Error(`Could not create Record "${dnsRecordToCreate.name}": '${dnsRecordToCreate.content}' is not a valid domain name!`);
109
- }
110
- }
111
- return this.cloudflare.dns.records.create(dnsRecordToCreate);
112
- }
113
- async updateRecord(zoneId, recordId, recordData, ip) {
114
- const updatedDnsRecord = {
115
- ...recordData,
116
- name: recordData.name.toLowerCase(),
117
- content: (recordData.content ? recordData.content : ip),
118
- type: (recordData.type ? recordData.type : 'A'),
119
- ttl: recordData.ttl ? recordData.ttl : 1,
120
- zone_id: zoneId,
121
- };
122
- if (!updatedDnsRecord.content) {
123
- throw Error(`Could not update Record "${updatedDnsRecord.name}": Content is missing!`);
124
- }
125
- if (updatedDnsRecord.type === 'A') {
126
- if (!updatedDnsRecord.content.match(ipv4Regex)) {
127
- throw Error(`Could not update Record "${updatedDnsRecord.name}": '${updatedDnsRecord.content}' is not a valid ipv4!`);
128
- }
129
- }
130
- else if (updatedDnsRecord.type === 'AAAA') {
131
- if (!updatedDnsRecord.content.match(ipv6Regex)) {
132
- throw Error(`Could not update Record "${updatedDnsRecord.name}": '${updatedDnsRecord.content}' is not a valid ipv6!`);
133
- }
134
- }
135
- else if (updatedDnsRecord.type === 'CNAME') {
136
- const parsedDomain = parseDomain(fromUrl(updatedDnsRecord.content));
137
- if (parsedDomain.type !== ParseResultType.Listed || !parsedDomain.domain) {
138
- throw Error(`Could not update Record "${updatedDnsRecord.name}": '${updatedDnsRecord.content}' is not a valid domain name!`);
139
- }
140
- }
141
- return await this.cloudflare.dns.records.edit(recordId, updatedDnsRecord);
142
- }
143
- async updateZoneMap() {
144
- const response = await this.cloudflare.zones.list();
145
- let zones = response.result;
146
- let nextPageExists = response.hasNextPage();
147
- while (nextPageExists) {
148
- const nextPageResponse = await response.getNextPage();
149
- zones = zones.concat(nextPageResponse.result);
150
- nextPageExists = nextPageResponse.hasNextPage();
151
- }
152
- this.zoneMap = new Map();
153
- for (const zone of zones) {
154
- this.zoneMap.set(zone.name, zone.id);
155
- }
156
- }
157
- async getRecordIdByNameAndType(recordName, recordType) {
158
- const record = await this.getRecordByNameAndType(recordName, recordType);
159
- return record.id;
160
- }
161
- getZoneIdByRecordName(recordName) {
162
- const domain = this.getDomainByRecordName(recordName);
163
- return this.getZoneIdByDomain(domain);
164
- }
165
- async getRecordByNameAndType(recordName, recordType) {
166
- const domain = this.getDomainByRecordName(recordName);
167
- const records = await this.getRecordsByDomain(domain);
168
- const record = records.find((currentRecord) => currentRecord.name.toLowerCase() === recordName.toLowerCase() && currentRecord.type.toLowerCase() === recordType.toLowerCase());
169
- const recordNotFound = record === undefined;
170
- if (recordNotFound) {
171
- throw new Error(`Record '${recordName}' not found.`);
172
- }
173
- return record;
174
- }
175
- async getRecordIdsForRecords(records) {
176
- const recordIdMap = new Map();
177
- const recordData = await this.getRecordDataForRecords(records);
178
- for (const record of recordData) {
179
- recordIdMap.set(this.getRecordIdMapKey(record), record.id);
180
- }
181
- return recordIdMap;
182
- }
183
- getRecordIdMapKey(record) {
184
- const recordName = record.name.toLowerCase();
185
- const recordType = record.type ? record.type.toLowerCase() : 'a';
186
- return `"${recordName}"_"${recordType}"`;
187
- }
188
- async getZoneIdByDomain(domain) {
189
- if (this.zoneMap.has(domain)) {
190
- const zoneId = this.zoneMap.get(domain.toLowerCase());
191
- return zoneId;
192
- }
193
- await this.updateZoneMap();
194
- if (!this.zoneMap.has(domain)) {
195
- throw new Error(`Could not find domain '${domain}'. Make sure the domain is set up for your cloudflare account.`);
196
- }
197
- const zoneId = this.zoneMap.get(domain.toLowerCase());
198
- return zoneId;
199
- }
200
- getDomainsFromRecords(records) {
201
- const domains = records.map((record) => this.getDomainByRecordName(record.name)).filter((domain, index, domainList) => domainList.indexOf(domain.toLowerCase()) === index);
202
- return domains;
203
- }
204
- getDomainByRecordName(recordName) {
205
- const parsedDomain = parseDomain(fromUrl(recordName));
206
- if (parsedDomain.type !== ParseResultType.Listed || !parsedDomain.domain) {
207
- throw new Error(`Could not parse domain. '${JSON.stringify(recordName)}' is not a valid record name.`);
208
- }
209
- let domain = '';
210
- domain += parsedDomain.domain;
211
- for (const tld of parsedDomain.topLevelDomains) {
212
- domain += `.${tld}`;
213
- }
214
- return domain.toLowerCase();
215
- }
216
- }
@@ -1,5 +0,0 @@
1
- import { ScheduledTask } from 'node-cron';
2
- export default class Cron {
3
- static createCronJob(cronExpression: string, callback: Function): ScheduledTask;
4
- static isValid(cronExpression: string): boolean;
5
- }
package/dist/lib/cron.js DELETED
@@ -1,15 +0,0 @@
1
- import cron from 'node-cron';
2
- export default class Cron {
3
- static createCronJob(cronExpression, callback) {
4
- const cronExpressionIsInvalid = !this.isValid(cronExpression);
5
- if (cronExpressionIsInvalid) {
6
- throw new Error(`'${cronExpression}' is not a valid cron expression.\nHere you can see how cron expressions work: https://cddnss.knaup.pw/cron-expression-syntax`);
7
- }
8
- return cron.schedule(cronExpression, () => {
9
- callback();
10
- }, undefined);
11
- }
12
- static isValid(cronExpression) {
13
- return cron.validate(cronExpression);
14
- }
15
- }
@@ -1,9 +0,0 @@
1
- export default class IPUtils {
2
- private static readonly ipPollingDelay;
3
- private static ipChangeEventListeners;
4
- static getIpv4(): Promise<string>;
5
- static getIpv6(): Promise<string>;
6
- static addIpChangeListener(callback: Function): Promise<string>;
7
- static removeIpChangeListener(eventListenerId: string): void;
8
- private static getId;
9
- }
@@ -1,49 +0,0 @@
1
- import { publicIpv4, publicIpv6 } from 'public-ip';
2
- export default class IPUtils {
3
- static ipPollingDelay = 10 * 1000;
4
- static ipChangeEventListeners = new Map();
5
- static async getIpv4() {
6
- /* c8 ignore start*/
7
- try {
8
- return await publicIpv4();
9
- }
10
- catch (error) {
11
- return publicIpv4();
12
- }
13
- /* c8 ignore stop*/
14
- }
15
- static async getIpv6() {
16
- /* c8 ignore start */
17
- try {
18
- return await publicIpv6();
19
- }
20
- catch (error) {
21
- return publicIpv6();
22
- }
23
- /* c8 ignore stop*/
24
- }
25
- static async addIpChangeListener(callback) {
26
- const eventListenerId = this.getId();
27
- let previousIp = await this.getIpv4();
28
- /* c8 ignore start */
29
- const intervalId = setInterval(async () => {
30
- const currentIp = await this.getIpv4();
31
- const ipMustBeUpdated = currentIp !== previousIp;
32
- if (ipMustBeUpdated) {
33
- previousIp = currentIp;
34
- callback(currentIp);
35
- }
36
- }, this.ipPollingDelay);
37
- /* c8 ignore stop*/
38
- this.ipChangeEventListeners.set(eventListenerId, intervalId);
39
- return eventListenerId;
40
- }
41
- static removeIpChangeListener(eventListenerId) {
42
- const eventListenerIntervalId = this.ipChangeEventListeners.get(eventListenerId);
43
- clearInterval(eventListenerIntervalId);
44
- IPUtils.ipChangeEventListeners.delete(eventListenerId);
45
- }
46
- static getId() {
47
- return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
48
- }
49
- }
@@ -1,2 +0,0 @@
1
- import Cloudflare from 'cloudflare';
2
- export type Auth = Cloudflare.AuthObject;
@@ -1 +0,0 @@
1
- export {};
@@ -1,2 +0,0 @@
1
- import Cloudflare from 'cloudflare';
2
- export type MultiSyncCallback = (syncResult: Cloudflare.DNS.DNSRecord[]) => void;
@@ -1 +0,0 @@
1
- export {};
@@ -1,2 +0,0 @@
1
- import { ClientOptions } from 'cloudflare';
2
- export type Auth = ClientOptions;
@@ -1 +0,0 @@
1
- export {};
@@ -1,4 +0,0 @@
1
- import { RecordData } from './index.js';
2
- export type DomainRecordList = {
3
- [domain: string]: Array<RecordData>;
4
- };
@@ -1 +0,0 @@
1
- export {};
@@ -1,2 +0,0 @@
1
- import { Cloudflare } from 'cloudflare';
2
- export type Record = Cloudflare.DNS.DNSRecord;
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export type ZoneMap = Map<string, string>;
@@ -1 +0,0 @@
1
- export {};
@@ -1,21 +0,0 @@
1
- import { RecordTypes } from '../index.js';
2
- export type RecordData = {
3
- id: string;
4
- type: RecordTypes;
5
- name: string;
6
- content: string;
7
- proxiable: boolean;
8
- proxied: boolean;
9
- ttl: number;
10
- locked: boolean;
11
- zone_id: string;
12
- zone_name: string;
13
- modified_on: string;
14
- created_on: string;
15
- meta: RecordMetaData;
16
- };
17
- export type RecordMetaData = {
18
- auto_added: boolean;
19
- managed_by_apps: boolean;
20
- managed_by_argo_tunnel: boolean;
21
- };
@@ -1 +0,0 @@
1
- export {};
@@ -1,43 +0,0 @@
1
- export type ZoneData = {
2
- id: string;
3
- name: string;
4
- status: string;
5
- paused: boolean;
6
- type: string;
7
- development_mode: number;
8
- name_servers: Array<string>;
9
- original_name_servers: Array<string>;
10
- modified_on: Date;
11
- created_on: Date;
12
- activated_on: Date;
13
- meta: {
14
- step: number;
15
- wildcard_proxiable: boolean;
16
- custom_certificate_quota: number;
17
- page_rule_quota: number;
18
- phishing_detected: boolean;
19
- multiple_railguns_allowed: boolean;
20
- };
21
- owner: {
22
- id: string;
23
- type: string;
24
- email: string;
25
- };
26
- account: {
27
- id: string;
28
- name: string;
29
- };
30
- permissions: Array<string>;
31
- plan: {
32
- id: string;
33
- name: string;
34
- price: number;
35
- currency: string;
36
- frequency: string;
37
- is_subscribed: boolean;
38
- can_subscribe: boolean;
39
- legacy_id: string;
40
- legacy_discount: boolean;
41
- externally_managed: boolean;
42
- };
43
- };
@@ -1 +0,0 @@
1
- export {};
@@ -1,2 +0,0 @@
1
- export * from './RecordData.js';
2
- export * from './ZoneData.js';
@@ -1,2 +0,0 @@
1
- export * from './RecordData.js';
2
- export * from './ZoneData.js';
@@ -1,4 +0,0 @@
1
- export * from './CloudflareOptions.js';
2
- export * from './Callbacks.js';
3
- export * from './Record.js';
4
- export * from './ZoneMap.js';
@@ -1,4 +0,0 @@
1
- export * from './CloudflareOptions.js';
2
- export * from './Callbacks.js';
3
- export * from './Record.js';
4
- export * from './ZoneMap.js';