apertodns 2.0.0 → 2.0.1

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 +8 -63
  2. package/index.js +123 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -11,19 +11,16 @@ 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 | 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 |
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 |
19
18
  | CLI tool | Yes | No | No | No |
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 |
19
+ | IPv6 support | Yes | Paid | Yes | Yes |
20
+ | No forced renewal | Yes | 30 days | Yes | Yes |
21
+ | DynDNS2 compatible | Yes | Yes | No | Yes |
24
22
  | Webhooks | Yes | No | No | No |
25
23
  | Team sharing | Yes | No | No | No |
26
- | Open Source | Yes | No | No | No |
27
24
 
28
25
  ## Features
29
26
 
@@ -36,7 +33,6 @@ ApertoDNS is a free Dynamic DNS service that lets you point a subdomain to your
36
33
  - **IPv4 & IPv6** - Full dual-stack support
37
34
  - **Real-time Stats** - View usage statistics and logs
38
35
  - **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
40
36
 
41
37
  ## Requirements
42
38
 
@@ -127,15 +123,7 @@ apertodns --force
127
123
  | `--config` | Edit configuration |
128
124
  | `--logout` | Remove local configuration |
129
125
  | `--force` | Force DNS update now |
130
-
131
- ### Standalone Update (DynDNS2)
132
-
133
- | Command | Description |
134
- |---------|-------------|
135
- | `--update` | Standalone DynDNS2 update (no config required) |
136
- | `--domain <fqdn>` | Domain to update (with --update) |
137
- | `--token <token>` | Token for authentication (with --update) |
138
- | `--ip <address>` | Custom IP address (optional, auto-detected if omitted) |
126
+ | `--update` | Standalone DynDNS2 update (with `--domain` and `--token`) |
139
127
 
140
128
  ### Daemon Mode
141
129
 
@@ -197,49 +185,6 @@ apertodns --daemon
197
185
  apertodns --daemon --interval 60
198
186
  ```
199
187
 
200
- ## Standalone Update
201
-
202
- Update DNS without any saved configuration - perfect for scripts and one-off updates:
203
-
204
- ```bash
205
- # Auto-detect IP and update
206
- apertodns --update --domain myserver.apertodns.com --token YOUR_TOKEN
207
-
208
- # Specify custom IP
209
- apertodns --update --domain myserver.apertodns.com --token YOUR_TOKEN --ip 203.0.113.42
210
- ```
211
-
212
- ## Docker
213
-
214
- Run without installing Node.js:
215
-
216
- ```bash
217
- # Run CLI via Docker
218
- docker run --rm apertodns/cli --help
219
-
220
- # Interactive setup (persisted config)
221
- docker run -it -v apertodns_config:/root/.config/apertodns apertodns/cli --setup
222
-
223
- # List domains
224
- docker run -v apertodns_config:/root/.config/apertodns apertodns/cli --domains
225
-
226
- # Standalone update (no config needed)
227
- docker run --rm apertodns/cli --update --domain myhost.apertodns.com --token YOUR_TOKEN
228
- ```
229
-
230
- For continuous updates, use the dedicated updater image:
231
-
232
- ```bash
233
- docker run -d \
234
- --name apertodns-updater \
235
- --restart unless-stopped \
236
- -e TOKEN=your_token \
237
- -e DOMAINS=myhost.apertodns.com \
238
- apertodns/updater
239
- ```
240
-
241
- See [Docker Hub](https://hub.docker.com/r/apertodns/cli) for more options.
242
-
243
188
  ## Automatic Updates (Cron)
244
189
 
245
190
  Set up automatic IP updates with cron:
package/index.js CHANGED
@@ -141,6 +141,16 @@ if (isCron) {
141
141
  process.argv.push("--json");
142
142
  }
143
143
 
144
+ // Subcommand detection (new style: domains list, update domain.com, etc.)
145
+ const subcommand = args[0] && !args[0].startsWith('-') ? args[0] : null;
146
+ const subcommandArg = args[1] && !args[1].startsWith('-') ? args[1] : null;
147
+
148
+ // Helper to get option value from args (works anywhere in args array)
149
+ const getOption = (name) => {
150
+ const idx = args.indexOf(name);
151
+ return idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith('-') ? args[idx + 1] : null;
152
+ };
153
+
144
154
  const isQuiet = args.includes("--quiet");
145
155
  const showHelp = args.includes("--help") || args.includes("-h");
146
156
  const showVersion = args.includes("--version") || args.includes("-v");
@@ -149,29 +159,30 @@ const runVerify = args.includes("--verify");
149
159
  const runSetup = args.includes("--setup");
150
160
  const showStatus = args.includes("--status") || args.includes("--show");
151
161
  const forceUpdate = args.includes("--force");
152
- const enableTokenId = args.includes("--enable") ? args[args.indexOf("--enable") + 1] : null;
153
- const disableTokenId = args.includes("--disable") ? args[args.indexOf("--disable") + 1] : null;
154
- const toggleTokenId = args.includes("--toggle") ? args[args.indexOf("--toggle") + 1] : null;
162
+ const enableTokenId = getOption("--enable");
163
+ const disableTokenId = getOption("--disable");
164
+ const toggleTokenId = getOption("--toggle");
155
165
  const runConfigEdit = args.includes("--config");
156
- const listDomains = args.includes("--domains");
157
- const listTokens = args.includes("--tokens");
158
- const addDomainArg = args.includes("--add-domain") ? args[args.indexOf("--add-domain") + 1] : null;
159
- const deleteDomainArg = args.includes("--delete-domain") ? args[args.indexOf("--delete-domain") + 1] : null;
160
- const showStats = args.includes("--stats");
161
- const showLogs = args.includes("--logs");
162
- const testDns = args.includes("--test") ? args[args.indexOf("--test") + 1] : null;
163
- const showDashboard = args.includes("--dashboard");
164
- const listWebhooks = args.includes("--webhooks");
165
- const listApiKeys = args.includes("--api-keys");
166
- const createApiKeyArg = args.includes("--create-api-key") ? args[args.indexOf("--create-api-key") + 1] : null;
167
- const deleteApiKeyArg = args.includes("--delete-api-key") ? args[args.indexOf("--delete-api-key") + 1] : null;
168
- const showScopes = args.includes("--scopes");
169
- const useApiKey = args.includes("--api-key") ? args[args.indexOf("--api-key") + 1] : null;
166
+ const listDomains = args.includes("--domains") || (subcommand === "domains" && (!subcommandArg || subcommandArg === "list"));
167
+ const listTokens = args.includes("--tokens") || (subcommand === "tokens" && (!subcommandArg || subcommandArg === "list"));
168
+ const addDomainArg = getOption("--add-domain") || (subcommand === "domains" && subcommandArg === "add" ? args[2] : null);
169
+ const deleteDomainArg = getOption("--delete-domain") || (subcommand === "domains" && subcommandArg === "delete" ? args[2] : null);
170
+ const showStats = args.includes("--stats") || subcommand === "stats";
171
+ const showLogs = args.includes("--logs") || subcommand === "logs";
172
+ const testDns = getOption("--test") || (subcommand === "test" ? subcommandArg : null);
173
+ const showDashboard = args.includes("--dashboard") || subcommand === "dashboard";
174
+ const listWebhooks = args.includes("--webhooks") || subcommand === "webhooks";
175
+ const listApiKeys = args.includes("--api-keys") || (subcommand === "api-keys" && (!subcommandArg || subcommandArg === "list"));
176
+ const createApiKeyArg = getOption("--create-api-key") || (subcommand === "api-keys" && subcommandArg === "create" ? args[2] : null);
177
+ const deleteApiKeyArg = getOption("--delete-api-key") || (subcommand === "api-keys" && subcommandArg === "delete" ? args[2] : null);
178
+ const showScopes = args.includes("--scopes") || subcommand === "scopes";
179
+ const useApiKey = getOption("--api-key");
180
+ const updateDomainArg = subcommand === "update" ? subcommandArg : null;
170
181
  const runInteractive = args.length === 0;
171
- const runDaemon = args.includes("--daemon");
172
- const daemonInterval = args.includes("--interval") ? parseInt(args[args.indexOf("--interval") + 1]) : 300;
173
- const showMyIp = args.includes("--my-ip") || args.includes("--ip");
174
- const logout = args.includes("--logout");
182
+ const runDaemon = args.includes("--daemon") || subcommand === "daemon";
183
+ const daemonInterval = getOption("--interval") ? parseInt(getOption("--interval")) : 300;
184
+ const showMyIp = args.includes("--my-ip") || args.includes("--ip") || subcommand === "ip" || subcommand === "my-ip";
185
+ const logout = args.includes("--logout") || subcommand === "logout";
175
186
 
176
187
  // JSON output helper
177
188
  const jsonOutput = (data) => {
@@ -1534,6 +1545,96 @@ const runUpdate = async () => {
1534
1545
  }
1535
1546
  };
1536
1547
 
1548
+ // ==================== UPDATE SINGLE DOMAIN ====================
1549
+
1550
+ const updateSingleDomain = async (domainName) => {
1551
+ const token = await getAuthToken();
1552
+ const spin = !showJson ? spinner(`Rilevamento IP per ${domainName}...`).start() : null;
1553
+
1554
+ try {
1555
+ // Get current IP
1556
+ const currentIP = await getCurrentIP('https://api.ipify.org').catch(() => null);
1557
+ const currentIPv6 = await getCurrentIPv6('https://api6.ipify.org').catch(() => null);
1558
+
1559
+ if (!currentIP && !currentIPv6) {
1560
+ spin?.fail("Nessun IP rilevato");
1561
+ if (showJson) {
1562
+ console.log(JSON.stringify({ error: "Nessun IP rilevato" }));
1563
+ }
1564
+ return;
1565
+ }
1566
+
1567
+ if (spin) spin.text = `Ricerca dominio ${domainName}...`;
1568
+
1569
+ // First, get domain list to find the domain ID
1570
+ const listRes = await fetch(`${API_BASE}/domains`, {
1571
+ headers: getAuthHeaders(token)
1572
+ });
1573
+
1574
+ if (!listRes.ok) {
1575
+ const errData = await listRes.json().catch(() => ({}));
1576
+ spin?.fail(`Errore autenticazione: ${errData.error || errData.message || 'Token non valido'}`);
1577
+ if (showJson) {
1578
+ console.log(JSON.stringify({ error: errData.error || errData.message || 'Autenticazione fallita' }));
1579
+ }
1580
+ return;
1581
+ }
1582
+
1583
+ const listData = await listRes.json();
1584
+ const domains = listData.domains || listData;
1585
+ const domain = domains.find(d => d.name === domainName || d.name.toLowerCase() === domainName.toLowerCase());
1586
+
1587
+ if (!domain) {
1588
+ spin?.fail(`Dominio "${domainName}" non trovato nel tuo account`);
1589
+ if (showJson) {
1590
+ console.log(JSON.stringify({ error: `Dominio "${domainName}" non trovato` }));
1591
+ }
1592
+ return;
1593
+ }
1594
+
1595
+ if (spin) spin.text = `Aggiornamento DNS per ${domainName}...`;
1596
+
1597
+ // Use PATCH /domains/:id to update (supports API Key)
1598
+ const updateBody = { ip: currentIP };
1599
+ if (currentIPv6) updateBody.ipv6 = currentIPv6;
1600
+
1601
+ const res = await fetch(`${API_BASE}/domains/${domain.id}`, {
1602
+ method: "PATCH",
1603
+ headers: {
1604
+ "Content-Type": "application/json",
1605
+ ...getAuthHeaders(token)
1606
+ },
1607
+ body: JSON.stringify(updateBody)
1608
+ });
1609
+
1610
+ const data = await res.json();
1611
+
1612
+ if (res.ok) {
1613
+ spin?.succeed(`DNS aggiornato! ${domainName} → ${currentIP}`);
1614
+ if (showJson) {
1615
+ console.log(JSON.stringify({
1616
+ success: true,
1617
+ domain: domainName,
1618
+ ip: currentIP,
1619
+ ipv6: currentIPv6 || null,
1620
+ previousIp: domain.ip,
1621
+ result: data
1622
+ }, null, 2));
1623
+ }
1624
+ } else {
1625
+ spin?.fail(`Errore: ${data.error || data.details || data.message}`);
1626
+ if (showJson) {
1627
+ console.log(JSON.stringify({ error: data.error || data.details || data.message }));
1628
+ }
1629
+ }
1630
+ } catch (err) {
1631
+ spin?.fail(err.message);
1632
+ if (showJson) {
1633
+ console.log(JSON.stringify({ error: err.message }));
1634
+ }
1635
+ }
1636
+ };
1637
+
1537
1638
  // ==================== DAEMON MODE ====================
1538
1639
 
1539
1640
  const runDaemonMode = async () => {
@@ -1707,6 +1808,7 @@ const main = async () => {
1707
1808
  else if (listDomains) await showDomainsList();
1708
1809
  else if (addDomainArg) await addDomain(addDomainArg);
1709
1810
  else if (deleteDomainArg) await deleteDomain(deleteDomainArg);
1811
+ else if (updateDomainArg) await updateSingleDomain(updateDomainArg);
1710
1812
  else if (testDns) await testDnsResolution(testDns);
1711
1813
  else if (listTokens) await showTokensList();
1712
1814
  else if (showStats) await showStatsCommand();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apertodns",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
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",