uplink-cli 0.2.8 → 0.2.9
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,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.9 — 2026-08-31
|
|
4
|
+
|
|
5
|
+
The menu's **My domains** now shows verified registration status (the same as `domains list --verify`) instead of "no expiry data" for zone/hosted entries. RDAP results are cached in `~/.uplink/rdap-cache.json` for 24 hours, so only the first open pays for the lookups.
|
|
6
|
+
|
|
3
7
|
## 0.2.8 — 2026-08-31
|
|
4
8
|
|
|
5
9
|
`domains list` is now grouped by provider and honest about what it knows: cPanel entries show as **hosted** (a panel serving a site says nothing about ownership), registrar expiry dates in the past show as **EXPIRED**, and new **`--verify`** RDAP-checks every entry without registration data (DNS zones, hosted sites) to reveal lapsed domains. RDAP goes straight to each TLD registry via the IANA bootstrap, with a DNS-delegation fallback for registries that 404.
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
rdapRegistration,
|
|
23
23
|
type RdapRegistration,
|
|
24
24
|
} from "../utils/domain-availability";
|
|
25
|
+
import { cachedRegistrations, storeRegistrations } from "../utils/rdap-cache";
|
|
25
26
|
import { searchDomains } from "../utils/domain-search";
|
|
26
27
|
import {
|
|
27
28
|
createNamecheapAddFundsRequest,
|
|
@@ -140,9 +141,14 @@ function formatDay(date: Date): string {
|
|
|
140
141
|
|
|
141
142
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
142
143
|
|
|
143
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* RDAP-check registration for domains whose source can't attest ownership.
|
|
146
|
+
* Fresh results (24h) come from the local cache, so repeat runs are instant.
|
|
147
|
+
*/
|
|
144
148
|
async function verifyRegistrations(domains: string[]): Promise<Map<string, RdapRegistration>> {
|
|
145
|
-
const
|
|
149
|
+
const unique = [...new Set(domains)];
|
|
150
|
+
const out = cachedRegistrations(unique);
|
|
151
|
+
const missing = unique.filter((domain) => !out.has(domain));
|
|
146
152
|
const run = async (targets: string[], batchSize: number, pauseMs: number) => {
|
|
147
153
|
for (let i = 0; i < targets.length; i += batchSize) {
|
|
148
154
|
if (i > 0) await sleep(pauseMs);
|
|
@@ -150,13 +156,14 @@ async function verifyRegistrations(domains: string[]): Promise<Map<string, RdapR
|
|
|
150
156
|
for (const result of results) out.set(result.domain, result);
|
|
151
157
|
}
|
|
152
158
|
};
|
|
153
|
-
await run(
|
|
154
|
-
//
|
|
159
|
+
await run(missing, 4, 400);
|
|
160
|
+
// Registries rate-limit bursts; give inconclusive lookups one slower retry.
|
|
155
161
|
const inconclusive = [...out.values()].filter((r) => r.registered === null).map((r) => r.domain);
|
|
156
162
|
if (inconclusive.length > 0) {
|
|
157
163
|
await sleep(2000);
|
|
158
164
|
await run(inconclusive, 2, 1000);
|
|
159
165
|
}
|
|
166
|
+
if (missing.length > 0) storeRegistrations(out.values());
|
|
160
167
|
return out;
|
|
161
168
|
}
|
|
162
169
|
|
|
@@ -302,8 +309,8 @@ domainsCommand
|
|
|
302
309
|
console.log("");
|
|
303
310
|
}
|
|
304
311
|
if (!opts.verify && domains.some((item) => !parseExpiry(item.expiresAt))) {
|
|
305
|
-
console.log("Entries without expiry come from DNS zones / hosted sites
|
|
306
|
-
console.log("
|
|
312
|
+
console.log("Entries without expiry come from DNS zones / hosted sites, which don't include");
|
|
313
|
+
console.log("registration data. Confirm each one: uplink domains list --verify");
|
|
307
314
|
}
|
|
308
315
|
} catch (error) {
|
|
309
316
|
handleError(error, { json: opts.json });
|
|
@@ -58,7 +58,9 @@ export function buildDomainsMenu(deps: Deps): MenuChoice {
|
|
|
58
58
|
label: "My domains",
|
|
59
59
|
action: async () => {
|
|
60
60
|
try {
|
|
61
|
-
|
|
61
|
+
// --verify resolves registration for zone/hosted entries; results
|
|
62
|
+
// are cached for a day, so only the first open is slow.
|
|
63
|
+
const output = runCliCapture(["domains", "list", "--verify"]);
|
|
62
64
|
restoreRawMode();
|
|
63
65
|
return output || "No domains. Connect a registrar first.";
|
|
64
66
|
} catch (error) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import type { RdapRegistration } from "./domain-availability";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Registration status changes rarely, and registry RDAP lookups are slow in
|
|
8
|
+
* bulk. Cache results for a day so `domains list --verify` (and the menu's
|
|
9
|
+
* My domains view) is instant after the first check.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const TTL_MS = 24 * 60 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
type CacheEntry = {
|
|
15
|
+
registered: boolean | null;
|
|
16
|
+
expiresAt?: string;
|
|
17
|
+
detail?: string;
|
|
18
|
+
checkedAt: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type CacheFile = Record<string, CacheEntry>;
|
|
22
|
+
|
|
23
|
+
function cachePath(): string {
|
|
24
|
+
return join(homedir(), ".uplink", "rdap-cache.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function loadCache(): CacheFile {
|
|
28
|
+
const path = cachePath();
|
|
29
|
+
if (!existsSync(path)) return {};
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as CacheFile;
|
|
32
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
33
|
+
} catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function cachedRegistrations(domains: string[]): Map<string, RdapRegistration> {
|
|
39
|
+
const cache = loadCache();
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const out = new Map<string, RdapRegistration>();
|
|
42
|
+
for (const domain of domains) {
|
|
43
|
+
const entry = cache[domain];
|
|
44
|
+
// Inconclusive results (registered === null) are never served from cache.
|
|
45
|
+
if (!entry || entry.registered === null || now - entry.checkedAt > TTL_MS) continue;
|
|
46
|
+
out.set(domain, {
|
|
47
|
+
domain,
|
|
48
|
+
registered: entry.registered,
|
|
49
|
+
...(entry.expiresAt ? { expiresAt: entry.expiresAt } : {}),
|
|
50
|
+
...(entry.detail ? { detail: entry.detail } : {}),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function storeRegistrations(results: Iterable<RdapRegistration>): void {
|
|
57
|
+
const cache = loadCache();
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
for (const result of results) {
|
|
60
|
+
if (result.registered === null) continue;
|
|
61
|
+
cache[result.domain] = {
|
|
62
|
+
registered: result.registered,
|
|
63
|
+
...(result.expiresAt ? { expiresAt: result.expiresAt } : {}),
|
|
64
|
+
...(result.detail ? { detail: result.detail } : {}),
|
|
65
|
+
checkedAt: now,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
mkdirSync(join(homedir(), ".uplink"), { recursive: true });
|
|
69
|
+
writeFileSync(cachePath(), JSON.stringify(cache, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
70
|
+
try {
|
|
71
|
+
chmodSync(cachePath(), 0o600);
|
|
72
|
+
} catch {
|
|
73
|
+
/* ignore */
|
|
74
|
+
}
|
|
75
|
+
}
|
package/package.json
CHANGED