apertodns 2.0.1 → 2.0.3

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.
Files changed (3) hide show
  1. package/README.md +89 -13
  2. package/index.js +148 -2
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/apertodns.svg)](https://www.npmjs.com/package/apertodns)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/apertodns.svg)](https://www.npmjs.com/package/apertodns)
5
- [![license](https://img.shields.io/npm/l/apertodns.svg)](https://github.com/1r0n3d3v3l0per/apertodns/blob/main/LICENSE)
5
+ [![license](https://img.shields.io/npm/l/apertodns.svg)](https://github.com/apertodns/apertodns/blob/main/LICENSE)
6
6
  [![node](https://img.shields.io/node/v/apertodns.svg)](https://nodejs.org)
7
7
 
8
8
  **Dynamic DNS management from your terminal.** Manage domains, tokens, API keys, and DNS updates with style.
@@ -11,16 +11,19 @@ ApertoDNS is a free Dynamic DNS service that lets you point a subdomain to your
11
11
 
12
12
  ## Why ApertoDNS?
13
13
 
14
- | Feature | ApertoDNS | No-IP | DuckDNS | Dynu |
15
- |---------|-----------|-------|---------|------|
16
- | Free subdomains | Unlimited | 1 (free) | 5 | 4 |
17
- | API Keys with scopes | Yes | No | No | Limited |
14
+ | Feature | ApertoDNS | Dyn (Oracle) | No-IP | DuckDNS |
15
+ |---------|-----------|--------------|-------|---------|
16
+ | Free plan | Yes | No ($55/yr) | Yes | Yes |
17
+ | Free subdomains | Unlimited | 0 | 1 | 5 |
18
+ | API Keys with scopes | Yes | No | No | No |
18
19
  | CLI tool | Yes | No | No | No |
19
- | IPv6 support | Yes | Paid | Yes | Yes |
20
- | No forced renewal | Yes | 30 days | Yes | Yes |
21
- | DynDNS2 compatible | Yes | Yes | No | Yes |
20
+ | Docker images | Yes | No | No | No |
21
+ | IPv6 support | Yes | Paid | Paid | Yes |
22
+ | No forced renewal | Yes | N/A | 30 days | Yes |
23
+ | DynDNS2 compatible | Yes | Yes | Yes | No |
22
24
  | Webhooks | Yes | No | No | No |
23
25
  | Team sharing | Yes | No | No | No |
26
+ | Open Source | Yes | No | No | No |
24
27
 
25
28
  ## Features
26
29
 
@@ -33,6 +36,7 @@ ApertoDNS is a free Dynamic DNS service that lets you point a subdomain to your
33
36
  - **IPv4 & IPv6** - Full dual-stack support
34
37
  - **Real-time Stats** - View usage statistics and logs
35
38
  - **Router/NAS Compatible** - Works with Synology, QNAP, and DynDNS2-compatible routers
39
+ - **Docker Images** - Official [apertodns/cli](https://hub.docker.com/r/apertodns/cli) and [apertodns/updater](https://hub.docker.com/r/apertodns/updater) images
36
40
 
37
41
  ## Requirements
38
42
 
@@ -94,6 +98,27 @@ apertodns --force
94
98
  | `--add-domain <name>` | Create a new subdomain |
95
99
  | `--delete-domain` | Delete a domain (interactive) |
96
100
  | `--test <domain>` | Test DNS resolution |
101
+ | `update <domain>` | Update a specific domain's IP (use with `--api-key`) |
102
+
103
+ ### TXT Records (ACME DNS-01)
104
+
105
+ | Command | Description |
106
+ |---------|-------------|
107
+ | `--txt-set <host> <name> <value>` | Set a TXT record |
108
+ | `--txt-delete <host> <name>` | Delete a TXT record |
109
+
110
+ Perfect for Let's Encrypt DNS-01 challenges:
111
+
112
+ ```bash
113
+ # Set TXT record for certificate validation
114
+ apertodns --txt-set example.apertodns.com _acme-challenge "validation-token"
115
+
116
+ # Delete TXT record after certificate issuance
117
+ apertodns --txt-delete example.apertodns.com _acme-challenge
118
+
119
+ # Use with API key for automation
120
+ apertodns --api-key apertodns_live_xxx... --txt-set example.apertodns.com _acme-challenge "token" --json
121
+ ```
97
122
 
98
123
  ### Token Management
99
124
 
@@ -123,7 +148,15 @@ apertodns --force
123
148
  | `--config` | Edit configuration |
124
149
  | `--logout` | Remove local configuration |
125
150
  | `--force` | Force DNS update now |
126
- | `--update` | Standalone DynDNS2 update (with `--domain` and `--token`) |
151
+
152
+ ### Standalone Update (DynDNS2)
153
+
154
+ | Command | Description |
155
+ |---------|-------------|
156
+ | `--update` | Standalone DynDNS2 update (no config required) |
157
+ | `--domain <fqdn>` | Domain to update (with --update) |
158
+ | `--token <token>` | Token for authentication (with --update) |
159
+ | `--ip <address>` | Custom IP address (optional, auto-detected if omitted) |
127
160
 
128
161
  ### Daemon Mode
129
162
 
@@ -170,7 +203,7 @@ apertodns --domains --json
170
203
  apertodns --my-ip --json
171
204
 
172
205
  # Combine with API key for scripting
173
- apertodns --api-key ak_xxx... --domains --json
206
+ apertodns --api-key apertodns_live_xxx... --domains --json
174
207
  ```
175
208
 
176
209
  ## Daemon Mode
@@ -185,6 +218,49 @@ apertodns --daemon
185
218
  apertodns --daemon --interval 60
186
219
  ```
187
220
 
221
+ ## Standalone Update
222
+
223
+ Update DNS without any saved configuration - perfect for scripts and one-off updates:
224
+
225
+ ```bash
226
+ # Auto-detect IP and update
227
+ apertodns --update --domain myserver.apertodns.com --token YOUR_TOKEN
228
+
229
+ # Specify custom IP
230
+ apertodns --update --domain myserver.apertodns.com --token YOUR_TOKEN --ip 203.0.113.42
231
+ ```
232
+
233
+ ## Docker
234
+
235
+ Run without installing Node.js:
236
+
237
+ ```bash
238
+ # Run CLI via Docker
239
+ docker run --rm apertodns/cli --help
240
+
241
+ # Interactive setup (persisted config)
242
+ docker run -it -v apertodns_config:/root/.config/apertodns apertodns/cli --setup
243
+
244
+ # List domains
245
+ docker run -v apertodns_config:/root/.config/apertodns apertodns/cli --domains
246
+
247
+ # Standalone update (no config needed)
248
+ docker run --rm apertodns/cli --update --domain myhost.apertodns.com --token YOUR_TOKEN
249
+ ```
250
+
251
+ For continuous updates, use the dedicated updater image:
252
+
253
+ ```bash
254
+ docker run -d \
255
+ --name apertodns-updater \
256
+ --restart unless-stopped \
257
+ -e TOKEN=your_token \
258
+ -e DOMAINS=myhost.apertodns.com \
259
+ apertodns/updater
260
+ ```
261
+
262
+ See [Docker Hub](https://hub.docker.com/r/apertodns/cli) for more options.
263
+
188
264
  ## Automatic Updates (Cron)
189
265
 
190
266
  Set up automatic IP updates with cron:
@@ -295,7 +371,7 @@ apertodns --add-domain newserver.apertodns.com
295
371
  apertodns --domains --json
296
372
 
297
373
  # Use API key for automation
298
- APERTODNS_API_KEY=ak_xxx... apertodns --domains --json
374
+ APERTODNS_API_KEY=apertodns_live_xxx... apertodns --domains --json
299
375
  ```
300
376
 
301
377
  ## Links
@@ -303,12 +379,12 @@ APERTODNS_API_KEY=ak_xxx... apertodns --domains --json
303
379
  - **Website**: [apertodns.com](https://apertodns.com)
304
380
  - **Dashboard**: [apertodns.com/dashboard](https://apertodns.com/dashboard)
305
381
  - **Documentation**: [apertodns.com/docs](https://apertodns.com/docs)
306
- - **Issues**: [GitHub Issues](https://github.com/1r0n3d3v3l0per/apertodns/issues)
382
+ - **Issues**: [GitHub Issues](https://github.com/apertodns/apertodns/issues)
307
383
 
308
384
  ## Support
309
385
 
310
386
  Need help?
311
- - Open an issue on [GitHub](https://github.com/1r0n3d3v3l0per/apertodns/issues)
387
+ - Open an issue on [GitHub](https://github.com/apertodns/apertodns/issues)
312
388
  - Email: support@apertodns.com
313
389
 
314
390
  ## License
package/index.js CHANGED
@@ -184,6 +184,19 @@ const daemonInterval = getOption("--interval") ? parseInt(getOption("--interval"
184
184
  const showMyIp = args.includes("--my-ip") || args.includes("--ip") || subcommand === "ip" || subcommand === "my-ip";
185
185
  const logout = args.includes("--logout") || subcommand === "logout";
186
186
 
187
+ // TXT record commands (ACME DNS-01 challenges)
188
+ const txtSetIdx = args.indexOf("--txt-set");
189
+ const txtSetArgs = txtSetIdx !== -1 ? {
190
+ hostname: args[txtSetIdx + 1],
191
+ name: args[txtSetIdx + 2],
192
+ value: args[txtSetIdx + 3]
193
+ } : null;
194
+ const txtDeleteIdx = args.indexOf("--txt-delete");
195
+ const txtDeleteArgs = txtDeleteIdx !== -1 ? {
196
+ hostname: args[txtDeleteIdx + 1],
197
+ name: args[txtDeleteIdx + 2]
198
+ } : null;
199
+
187
200
  // JSON output helper
188
201
  const jsonOutput = (data) => {
189
202
  if (showJson) {
@@ -214,6 +227,10 @@ ${chalk.bold("GESTIONE DOMINI:")}
214
227
  ${cyan("--delete-domain")} Elimina un dominio (interattivo)
215
228
  ${cyan("--test")} <domain> Testa risoluzione DNS di un dominio
216
229
 
230
+ ${chalk.bold("TXT RECORDS (ACME DNS-01):")}
231
+ ${cyan("--txt-set")} <host> <name> <val> Imposta record TXT
232
+ ${cyan("--txt-delete")} <host> <name> Elimina record TXT
233
+
217
234
  ${chalk.bold("GESTIONE TOKEN:")}
218
235
  ${cyan("--enable")} <id> Attiva un token
219
236
  ${cyan("--disable")} <id> Disattiva un token
@@ -361,9 +378,15 @@ const fetchDomains = async () => {
361
378
  const token = await getAuthToken();
362
379
  const spin = !showJson ? spinner("Caricamento domini...").start() : null;
363
380
  try {
381
+ const controller = new AbortController();
382
+ const timeout = setTimeout(() => controller.abort(), 15000);
383
+
364
384
  const res = await fetch(`${API_BASE}/domains`, {
365
- headers: getAuthHeaders(token)
385
+ headers: getAuthHeaders(token),
386
+ signal: controller.signal
366
387
  });
388
+ clearTimeout(timeout);
389
+
367
390
  spin?.stop();
368
391
  if (!res.ok) {
369
392
  const err = await res.json().catch(() => ({}));
@@ -371,7 +394,7 @@ const fetchDomains = async () => {
371
394
  }
372
395
  return await res.json();
373
396
  } catch (err) {
374
- spin?.fail("Errore caricamento domini");
397
+ spin?.fail(err.name === 'AbortError' ? "Timeout caricamento domini" : "Errore caricamento domini");
375
398
  throw err;
376
399
  }
377
400
  };
@@ -636,6 +659,127 @@ const testDnsResolution = async (domain) => {
636
659
  }
637
660
  };
638
661
 
662
+ // ==================== TXT RECORDS ====================
663
+
664
+ const IETF_BASE = "https://api.apertodns.com/.well-known/apertodns/v1";
665
+
666
+ const setTxtRecord = async (hostname, name, value) => {
667
+ if (!hostname || !name || !value) {
668
+ if (showJson) {
669
+ console.log(JSON.stringify({ error: "Uso: --txt-set <hostname> <name> <value>" }));
670
+ } else {
671
+ console.log(red("\n❌ Uso: --txt-set <hostname> <name> <value>"));
672
+ console.log(gray(" Esempio: --txt-set mio.apertodns.com _acme-challenge abc123\n"));
673
+ }
674
+ return;
675
+ }
676
+
677
+ const token = await getAuthToken();
678
+ const spin = !showJson ? spinner(`Impostazione TXT ${name}.${hostname}...`).start() : null;
679
+
680
+ try {
681
+ const controller = new AbortController();
682
+ const timeout = setTimeout(() => controller.abort(), 15000);
683
+
684
+ const res = await fetch(`${IETF_BASE}/update`, {
685
+ method: "POST",
686
+ headers: {
687
+ "Content-Type": "application/json",
688
+ ...getAuthHeaders(token)
689
+ },
690
+ body: JSON.stringify({
691
+ hostname,
692
+ txt: {
693
+ name,
694
+ value,
695
+ action: "set"
696
+ }
697
+ }),
698
+ signal: controller.signal
699
+ });
700
+ clearTimeout(timeout);
701
+
702
+ const data = await res.json();
703
+
704
+ if (res.ok && data.success !== false) {
705
+ spin?.succeed(`TXT record impostato: ${name}.${hostname}`);
706
+ if (showJson) {
707
+ console.log(JSON.stringify({ success: true, hostname, txt: { name, value, action: "set" }, response: data }, null, 2));
708
+ } else {
709
+ console.log(gray(` Nome: ${cyan(name)}`));
710
+ console.log(gray(` Valore: ${cyan(value)}`));
711
+ console.log();
712
+ }
713
+ } else {
714
+ spin?.fail(`Errore: ${data.error || data.message || 'TXT non supportato'}`);
715
+ if (showJson) {
716
+ console.log(JSON.stringify({ error: data.error || data.message }));
717
+ }
718
+ }
719
+ } catch (err) {
720
+ spin?.fail(err.name === 'AbortError' ? "Timeout" : err.message);
721
+ if (showJson) {
722
+ console.log(JSON.stringify({ error: err.message }));
723
+ }
724
+ }
725
+ };
726
+
727
+ const deleteTxtRecord = async (hostname, name) => {
728
+ if (!hostname || !name) {
729
+ if (showJson) {
730
+ console.log(JSON.stringify({ error: "Uso: --txt-delete <hostname> <name>" }));
731
+ } else {
732
+ console.log(red("\n❌ Uso: --txt-delete <hostname> <name>"));
733
+ console.log(gray(" Esempio: --txt-delete mio.apertodns.com _acme-challenge\n"));
734
+ }
735
+ return;
736
+ }
737
+
738
+ const token = await getAuthToken();
739
+ const spin = !showJson ? spinner(`Eliminazione TXT ${name}.${hostname}...`).start() : null;
740
+
741
+ try {
742
+ const controller = new AbortController();
743
+ const timeout = setTimeout(() => controller.abort(), 15000);
744
+
745
+ const res = await fetch(`${IETF_BASE}/update`, {
746
+ method: "POST",
747
+ headers: {
748
+ "Content-Type": "application/json",
749
+ ...getAuthHeaders(token)
750
+ },
751
+ body: JSON.stringify({
752
+ hostname,
753
+ txt: {
754
+ name,
755
+ action: "delete"
756
+ }
757
+ }),
758
+ signal: controller.signal
759
+ });
760
+ clearTimeout(timeout);
761
+
762
+ const data = await res.json();
763
+
764
+ if (res.ok && data.success !== false) {
765
+ spin?.succeed(`TXT record eliminato: ${name}.${hostname}`);
766
+ if (showJson) {
767
+ console.log(JSON.stringify({ success: true, hostname, txt: { name, action: "delete" }, response: data }, null, 2));
768
+ }
769
+ } else {
770
+ spin?.fail(`Errore: ${data.error || data.message || 'TXT non supportato'}`);
771
+ if (showJson) {
772
+ console.log(JSON.stringify({ error: data.error || data.message }));
773
+ }
774
+ }
775
+ } catch (err) {
776
+ spin?.fail(err.name === 'AbortError' ? "Timeout" : err.message);
777
+ if (showJson) {
778
+ console.log(JSON.stringify({ error: err.message }));
779
+ }
780
+ }
781
+ };
782
+
639
783
  // ==================== TOKENS ====================
640
784
 
641
785
  const fetchTokens = async () => {
@@ -1799,6 +1943,8 @@ const interactiveMode = async () => {
1799
1943
  const main = async () => {
1800
1944
  try {
1801
1945
  if (logout) await runLogout();
1946
+ else if (txtSetArgs) await setTxtRecord(txtSetArgs.hostname, txtSetArgs.name, txtSetArgs.value);
1947
+ else if (txtDeleteArgs) await deleteTxtRecord(txtDeleteArgs.hostname, txtDeleteArgs.name);
1802
1948
  else if (showMyIp) await showMyIpCommand();
1803
1949
  else if (runDaemon) await runDaemonMode();
1804
1950
  else if (enableTokenId) await updateTokenState(enableTokenId, true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apertodns",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "ApertoDNS CLI - Dynamic DNS management from your terminal. Manage domains, tokens, API keys and DNS updates with style.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -30,15 +30,15 @@
30
30
  "qnap",
31
31
  "router"
32
32
  ],
33
- "author": "Aperto Network <info@apertodns.com>",
33
+ "author": "Andrea Ferro <support@apertodns.com>",
34
34
  "license": "MIT",
35
35
  "repository": {
36
36
  "type": "git",
37
- "url": "https://github.com/1r0n3d3v3l0per/apertodns.git"
37
+ "url": "https://github.com/apertodns/apertodns.git"
38
38
  },
39
39
  "homepage": "https://apertodns.com",
40
40
  "bugs": {
41
- "url": "https://github.com/1r0n3d3v3l0per/apertodns/issues"
41
+ "url": "https://github.com/apertodns/apertodns/issues"
42
42
  },
43
43
  "engines": {
44
44
  "node": ">=18.0.0"