mirror of
https://github.com/jakejarvis/rdapper.git
synced 2026-09-23 00:15:31 -04:00
Refactor lookup and normalization functions to improve type safety and error handling. Updated domain lookup to provide a stable ISO string fallback and enhanced error messaging. Modified RDAP normalization to use stricter types and added utility functions for type assertions.
This commit is contained in:
+6
-3
@@ -22,7 +22,9 @@ export async function lookupDomain(
|
|||||||
return { ok: false, error: "Input does not look like a domain" };
|
return { ok: false, error: "Input does not look like a domain" };
|
||||||
}
|
}
|
||||||
const tld = extractTld(domain);
|
const tld = extractTld(domain);
|
||||||
const now = toISO(new Date())!;
|
// Avoid non-null assertion: fallback to a stable ISO string if parsing ever fails
|
||||||
|
const now =
|
||||||
|
toISO(new Date()) ?? new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
||||||
|
|
||||||
// If WHOIS-only, skip RDAP path
|
// If WHOIS-only, skip RDAP path
|
||||||
if (!opts?.whoisOnly) {
|
if (!opts?.whoisOnly) {
|
||||||
@@ -77,7 +79,8 @@ export async function lookupDomain(
|
|||||||
now,
|
now,
|
||||||
);
|
);
|
||||||
return { ok: true, record };
|
return { ok: true, record };
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
return { ok: false, error: String(err?.message || err) };
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
return { ok: false, error: message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+115
-65
@@ -4,7 +4,9 @@ import type {
|
|||||||
Nameserver,
|
Nameserver,
|
||||||
RegistrarInfo,
|
RegistrarInfo,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
import { toISO, uniq } from "./utils.js";
|
import { asDateLike, asString, asStringArray, toISO, uniq } from "./utils.js";
|
||||||
|
|
||||||
|
type RdapDoc = Record<string, unknown>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert RDAP JSON into our normalized DomainRecord.
|
* Convert RDAP JSON into our normalized DomainRecord.
|
||||||
@@ -13,59 +15,83 @@ import { toISO, uniq } from "./utils.js";
|
|||||||
export function normalizeRdap(
|
export function normalizeRdap(
|
||||||
inputDomain: string,
|
inputDomain: string,
|
||||||
tld: string,
|
tld: string,
|
||||||
rdap: any,
|
rdap: unknown,
|
||||||
rdapServersTried: string[],
|
rdapServersTried: string[],
|
||||||
fetchedAtISO: string,
|
fetchedAtISO: string,
|
||||||
): DomainRecord {
|
): DomainRecord {
|
||||||
|
const doc = (rdap ?? {}) as RdapDoc;
|
||||||
// Safe helpers for optional fields
|
// Safe helpers for optional fields
|
||||||
const get = (obj: any, path: string[]): any =>
|
const _get = (obj: unknown, path: string[]): unknown =>
|
||||||
path.reduce((o, k) => (o && k in o ? o[k] : undefined), obj);
|
path.reduce<unknown>(
|
||||||
|
(o, k) => ((o as RdapDoc)?.[k] as unknown) ?? undefined,
|
||||||
|
obj,
|
||||||
|
);
|
||||||
|
|
||||||
// Prefer ldhName (punycode) and unicodeName if provided
|
// Prefer ldhName (punycode) and unicodeName if provided
|
||||||
const ldhName: string | undefined =
|
const ldhName: string | undefined =
|
||||||
rdap?.ldhName || rdap?.handle || undefined;
|
asString(doc.ldhName) || asString(doc.handle);
|
||||||
const unicodeName: string | undefined = rdap?.unicodeName || undefined;
|
const unicodeName: string | undefined = asString(doc.unicodeName);
|
||||||
|
|
||||||
// Registrar entity can be provided with role "registrar"
|
// Registrar entity can be provided with role "registrar"
|
||||||
const registrar: RegistrarInfo | undefined = extractRegistrar(rdap?.entities);
|
const registrar: RegistrarInfo | undefined = extractRegistrar(
|
||||||
|
doc.entities as unknown,
|
||||||
|
);
|
||||||
|
|
||||||
// Nameservers: normalize host + IPs
|
// Nameservers: normalize host + IPs
|
||||||
const nameservers: Nameserver[] | undefined = Array.isArray(rdap?.nameservers)
|
const nameservers: Nameserver[] | undefined = Array.isArray(doc.nameservers)
|
||||||
? rdap.nameservers
|
? (doc.nameservers as RdapDoc[])
|
||||||
.map((ns: any) => ({
|
.map((ns) => {
|
||||||
host: ns?.ldhName || ns?.unicodeName || "",
|
const host = (
|
||||||
ipv4: get(ns, ["ipAddresses", "v4"]) || undefined,
|
asString(ns.ldhName) ??
|
||||||
ipv6: get(ns, ["ipAddresses", "v6"]) || undefined,
|
asString(ns.unicodeName) ??
|
||||||
}))
|
""
|
||||||
.filter((n: Nameserver) => !!n.host)
|
).toLowerCase();
|
||||||
|
const ip = ns.ipAddresses as RdapDoc | undefined;
|
||||||
|
const ipv4 = asStringArray(ip?.v4);
|
||||||
|
const ipv6 = asStringArray(ip?.v6);
|
||||||
|
const n: Nameserver = { host };
|
||||||
|
if (ipv4?.length) n.ipv4 = ipv4;
|
||||||
|
if (ipv6?.length) n.ipv6 = ipv6;
|
||||||
|
return n;
|
||||||
|
})
|
||||||
|
.filter((n) => !!n.host)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// Contacts: RDAP entities include roles like registrant, administrative, technical, billing, abuse
|
// Contacts: RDAP entities include roles like registrant, administrative, technical, billing, abuse
|
||||||
const contacts: Contact[] | undefined = extractContacts(rdap?.entities);
|
const contacts: Contact[] | undefined = extractContacts(
|
||||||
|
doc.entities as unknown,
|
||||||
|
);
|
||||||
|
|
||||||
// RDAP uses IANA EPP status values. Preserve raw plus a description if any remarks are present.
|
// RDAP uses IANA EPP status values. Preserve raw plus a description if any remarks are present.
|
||||||
const statuses = Array.isArray(rdap?.status)
|
const statuses = Array.isArray(doc.status)
|
||||||
? rdap.status.map((s: string) => ({ status: s, raw: s }))
|
? (doc.status as unknown[])
|
||||||
|
.filter((s): s is string => typeof s === "string")
|
||||||
|
.map((s) => ({ status: s, raw: s }))
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// Secure DNS info
|
// Secure DNS info
|
||||||
const secureDNS = rdap?.secureDNS;
|
const secureDNS = doc.secureDNS as
|
||||||
|
| { delegationSigned?: unknown; dsData?: Array<Record<string, unknown>> }
|
||||||
|
| undefined;
|
||||||
const dnssec = secureDNS
|
const dnssec = secureDNS
|
||||||
? {
|
? {
|
||||||
enabled: !!secureDNS.delegationSigned,
|
enabled: !!secureDNS.delegationSigned,
|
||||||
dsRecords: Array.isArray(secureDNS.dsData)
|
dsRecords: Array.isArray(secureDNS.dsData)
|
||||||
? secureDNS.dsData.map((d: any) => ({
|
? (secureDNS.dsData as Array<Record<string, unknown>>).map((d) => ({
|
||||||
keyTag: d.keyTag,
|
keyTag: d.keyTag as number | undefined,
|
||||||
algorithm: d.algorithm,
|
algorithm: d.algorithm as number | undefined,
|
||||||
digestType: d.digestType,
|
digestType: d.digestType as number | undefined,
|
||||||
digest: d.digest,
|
digest: d.digest as string | undefined,
|
||||||
}))
|
}))
|
||||||
: undefined,
|
: undefined,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// RDAP "events" contain timestamps for registration, last changed, expiration, deletion, etc.
|
// RDAP "events" contain timestamps for registration, last changed, expiration, deletion, etc.
|
||||||
const events: any[] = Array.isArray(rdap?.events) ? rdap.events : [];
|
type RdapEvent = { eventAction?: string; eventDate?: string | number | Date };
|
||||||
|
const events: RdapEvent[] = Array.isArray(doc.events)
|
||||||
|
? (doc.events as unknown[] as RdapEvent[])
|
||||||
|
: [];
|
||||||
const byAction = (action: string) =>
|
const byAction = (action: string) =>
|
||||||
events.find(
|
events.find(
|
||||||
(e) =>
|
(e) =>
|
||||||
@@ -73,16 +99,19 @@ export function normalizeRdap(
|
|||||||
e.eventAction.toLowerCase().includes(action),
|
e.eventAction.toLowerCase().includes(action),
|
||||||
);
|
);
|
||||||
const creationDate = toISO(
|
const creationDate = toISO(
|
||||||
byAction("registration")?.eventDate || rdap?.registrationDate,
|
asDateLike(byAction("registration")?.eventDate) ??
|
||||||
|
asDateLike(doc.registrationDate),
|
||||||
);
|
);
|
||||||
const updatedDate = toISO(
|
const updatedDate = toISO(
|
||||||
byAction("last changed")?.eventDate || rdap?.lastChangedDate,
|
asDateLike(byAction("last changed")?.eventDate) ??
|
||||||
|
asDateLike(doc.lastChangedDate),
|
||||||
);
|
);
|
||||||
const expirationDate = toISO(
|
const expirationDate = toISO(
|
||||||
byAction("expiration")?.eventDate || rdap?.expirationDate,
|
asDateLike(byAction("expiration")?.eventDate) ??
|
||||||
|
asDateLike(doc.expirationDate),
|
||||||
);
|
);
|
||||||
const deletionDate = toISO(
|
const deletionDate = toISO(
|
||||||
byAction("deletion")?.eventDate || rdap?.deletionDate,
|
asDateLike(byAction("deletion")?.eventDate) ?? asDateLike(doc.deletionDate),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Derive a simple transfer lock flag from statuses
|
// Derive a simple transfer lock flag from statuses
|
||||||
@@ -91,8 +120,7 @@ export function normalizeRdap(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// The RDAP document may include "port43" pointer to authoritative WHOIS
|
// The RDAP document may include "port43" pointer to authoritative WHOIS
|
||||||
const whoisServer: string | undefined =
|
const whoisServer: string | undefined = asString(doc.port43);
|
||||||
typeof rdap?.port43 === "string" ? rdap.port43 : undefined;
|
|
||||||
|
|
||||||
const record: DomainRecord = {
|
const record: DomainRecord = {
|
||||||
domain: unicodeName || ldhName || inputDomain,
|
domain: unicodeName || ldhName || inputDomain,
|
||||||
@@ -126,35 +154,42 @@ export function normalizeRdap(
|
|||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractRegistrar(
|
function extractRegistrar(entities: unknown): RegistrarInfo | undefined {
|
||||||
entities: any[] | undefined,
|
|
||||||
): RegistrarInfo | undefined {
|
|
||||||
if (!Array.isArray(entities)) return undefined;
|
if (!Array.isArray(entities)) return undefined;
|
||||||
for (const ent of entities) {
|
for (const ent of entities) {
|
||||||
const roles: string[] = Array.isArray(ent?.roles) ? ent.roles : [];
|
const roles: string[] = Array.isArray((ent as RdapDoc)?.roles)
|
||||||
|
? ((ent as RdapDoc).roles as unknown[]).filter(
|
||||||
|
(r): r is string => typeof r === "string",
|
||||||
|
)
|
||||||
|
: [];
|
||||||
if (!roles.some((r) => /registrar/i.test(r))) continue;
|
if (!roles.some((r) => /registrar/i.test(r))) continue;
|
||||||
const v = parseVcard(ent?.vcardArray);
|
const v = parseVcard((ent as RdapDoc)?.vcardArray);
|
||||||
const ianaId = Array.isArray(ent?.publicIds)
|
const ianaId = Array.isArray((ent as RdapDoc)?.publicIds)
|
||||||
? ent.publicIds.find((id: any) => /iana\s*registrar\s*id/i.test(id?.type))
|
? ((ent as RdapDoc).publicIds as Array<RdapDoc>).find((id) =>
|
||||||
?.identifier
|
/iana\s*registrar\s*id/i.test(String(id?.type)),
|
||||||
|
)?.identifier
|
||||||
: undefined;
|
: undefined;
|
||||||
return {
|
return {
|
||||||
name: v.fn || v.org || ent?.handle || undefined,
|
name: v.fn || v.org || asString((ent as RdapDoc)?.handle) || undefined,
|
||||||
ianaId: ianaId,
|
ianaId: asString(ianaId),
|
||||||
url: v.url,
|
url: v.url ?? undefined,
|
||||||
email: v.email,
|
email: v.email ?? undefined,
|
||||||
phone: v.tel,
|
phone: v.tel ?? undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractContacts(entities: any[] | undefined): Contact[] | undefined {
|
function extractContacts(entities: unknown): Contact[] | undefined {
|
||||||
if (!Array.isArray(entities)) return undefined;
|
if (!Array.isArray(entities)) return undefined;
|
||||||
const out: Contact[] = [];
|
const out: Contact[] = [];
|
||||||
for (const ent of entities) {
|
for (const ent of entities) {
|
||||||
const roles: string[] = Array.isArray(ent?.roles) ? ent.roles : [];
|
const roles: string[] = Array.isArray((ent as RdapDoc)?.roles)
|
||||||
const v = parseVcard(ent?.vcardArray);
|
? ((ent as RdapDoc).roles as unknown[]).filter(
|
||||||
|
(r): r is string => typeof r === "string",
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
const v = parseVcard((ent as RdapDoc)?.vcardArray);
|
||||||
const type = roles.find((r) =>
|
const type = roles.find((r) =>
|
||||||
/registrant|administrative|technical|billing|abuse|reseller/i.test(r),
|
/registrant|administrative|technical|billing|abuse|reseller/i.test(r),
|
||||||
);
|
);
|
||||||
@@ -167,9 +202,7 @@ function extractContacts(entities: any[] | undefined): Contact[] | undefined {
|
|||||||
abuse: "abuse",
|
abuse: "abuse",
|
||||||
reseller: "reseller",
|
reseller: "reseller",
|
||||||
} as const;
|
} as const;
|
||||||
const roleKey = (
|
const roleKey = (map[type.toLowerCase()] ?? "unknown") as Contact["type"];
|
||||||
type.toLowerCase() in map ? (map as any)[type.toLowerCase()] : "unknown"
|
|
||||||
) as Contact["type"];
|
|
||||||
out.push({
|
out.push({
|
||||||
type: roleKey,
|
type: roleKey,
|
||||||
name: v.fn,
|
name: v.fn,
|
||||||
@@ -188,8 +221,23 @@ function extractContacts(entities: any[] | undefined): Contact[] | undefined {
|
|||||||
return out.length ? out : undefined;
|
return out.length ? out : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ParsedVCard {
|
||||||
|
fn?: string;
|
||||||
|
org?: string;
|
||||||
|
email?: string;
|
||||||
|
tel?: string;
|
||||||
|
fax?: string;
|
||||||
|
url?: string;
|
||||||
|
street?: string[];
|
||||||
|
locality?: string;
|
||||||
|
region?: string;
|
||||||
|
postcode?: string;
|
||||||
|
country?: string;
|
||||||
|
countryCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Parse a minimal subset of vCard 4.0 arrays as used in RDAP "vcardArray" fields
|
// Parse a minimal subset of vCard 4.0 arrays as used in RDAP "vcardArray" fields
|
||||||
function parseVcard(vcardArray: any): Record<string, any> {
|
function parseVcard(vcardArray: unknown): ParsedVCard {
|
||||||
// vcardArray is typically ["vcard", [["version",{} ,"text","4.0"], ["fn",{} ,"text","Example"], ...]]
|
// vcardArray is typically ["vcard", [["version",{} ,"text","4.0"], ["fn",{} ,"text","Example"], ...]]
|
||||||
if (
|
if (
|
||||||
!Array.isArray(vcardArray) ||
|
!Array.isArray(vcardArray) ||
|
||||||
@@ -197,8 +245,10 @@ function parseVcard(vcardArray: any): Record<string, any> {
|
|||||||
!Array.isArray(vcardArray[1])
|
!Array.isArray(vcardArray[1])
|
||||||
)
|
)
|
||||||
return {};
|
return {};
|
||||||
const entries: any[] = vcardArray[1];
|
const entries = vcardArray[1] as Array<
|
||||||
const out: Record<string, any> = {};
|
[string, Record<string, unknown>, string, unknown]
|
||||||
|
>;
|
||||||
|
const out: ParsedVCard = {};
|
||||||
for (const e of entries) {
|
for (const e of entries) {
|
||||||
const key = e?.[0];
|
const key = e?.[0];
|
||||||
const _valueType = e?.[2];
|
const _valueType = e?.[2];
|
||||||
@@ -206,30 +256,30 @@ function parseVcard(vcardArray: any): Record<string, any> {
|
|||||||
if (!key) continue;
|
if (!key) continue;
|
||||||
switch (String(key).toLowerCase()) {
|
switch (String(key).toLowerCase()) {
|
||||||
case "fn":
|
case "fn":
|
||||||
out.fn = value;
|
out.fn = asString(value);
|
||||||
break;
|
break;
|
||||||
case "org":
|
case "org":
|
||||||
out.org = Array.isArray(value) ? value.join(" ") : value;
|
out.org = Array.isArray(value)
|
||||||
|
? value.map((x) => String(x)).join(" ")
|
||||||
|
: asString(value);
|
||||||
break;
|
break;
|
||||||
case "email":
|
case "email":
|
||||||
out.email = value;
|
out.email = asString(value);
|
||||||
break;
|
break;
|
||||||
case "tel":
|
case "tel":
|
||||||
out.tel = value;
|
out.tel = asString(value);
|
||||||
break;
|
break;
|
||||||
case "url":
|
case "url":
|
||||||
out.url = value;
|
out.url = asString(value);
|
||||||
break;
|
break;
|
||||||
case "adr": {
|
case "adr": {
|
||||||
// adr value is [postOfficeBox, extendedAddress, street, locality, region, postalCode, country]
|
// adr value is [postOfficeBox, extendedAddress, street, locality, region, postalCode, country]
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
out.street = value[2]
|
out.street = value[2] ? String(value[2]).split(/\n|,\s*/) : undefined;
|
||||||
? String(value[2]).split(/\\n|,\s*/)
|
out.locality = asString(value[3]);
|
||||||
: undefined;
|
out.region = asString(value[4]);
|
||||||
out.locality = value[3];
|
out.postcode = asString(value[5]);
|
||||||
out.region = value[4];
|
out.country = asString(value[6]);
|
||||||
out.postcode = value[5];
|
|
||||||
out.country = value[6];
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -57,7 +57,7 @@ export async function fetchRdapDomain(
|
|||||||
domain: string,
|
domain: string,
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
options?: LookupOptions,
|
options?: LookupOptions,
|
||||||
): Promise<{ url: string; json: any }> {
|
): Promise<{ url: string; json: unknown }> {
|
||||||
const url = new URL(
|
const url = new URL(
|
||||||
`domain/${encodeURIComponent(domain)}`,
|
`domain/${encodeURIComponent(domain)}`,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
|
|||||||
Vendored
+1
-1
@@ -71,7 +71,7 @@ export interface DomainRecord {
|
|||||||
contacts?: Contact[];
|
contacts?: Contact[];
|
||||||
whoisServer?: string; // authoritative WHOIS queried (if any)
|
whoisServer?: string; // authoritative WHOIS queried (if any)
|
||||||
rdapServers?: string[]; // RDAP base URLs tried
|
rdapServers?: string[]; // RDAP base URLs tried
|
||||||
rawRdap?: any; // raw RDAP JSON
|
rawRdap?: unknown; // raw RDAP JSON
|
||||||
rawWhois?: string; // raw WHOIS text (last authoritative)
|
rawWhois?: string; // raw WHOIS text (last authoritative)
|
||||||
source: LookupSource; // which source produced data
|
source: LookupSource; // which source produced data
|
||||||
fetchedAt: string; // ISO 8601
|
fetchedAt: string; // ISO 8601
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ test("toISO parses ISO and common whois formats", () => {
|
|||||||
assert.equal(iso, "2023-01-02T03:04:05Z");
|
assert.equal(iso, "2023-01-02T03:04:05Z");
|
||||||
|
|
||||||
const noZ = toISO("2023-01-02 03:04:05");
|
const noZ = toISO("2023-01-02 03:04:05");
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: this is fine
|
||||||
assert.match(noZ!, /^2023-01-02T03:04:05Z$/);
|
assert.match(noZ!, /^2023-01-02T03:04:05Z$/);
|
||||||
|
|
||||||
const slash = toISO("2023/01/02 03:04:05");
|
const slash = toISO("2023/01/02 03:04:05");
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: this is fine
|
||||||
assert.match(slash!, /^2023-01-02T03:04:05Z$/);
|
assert.match(slash!, /^2023-01-02T03:04:05Z$/);
|
||||||
|
|
||||||
const dmy = toISO("02-Jan-2023");
|
const dmy = toISO("02-Jan-2023");
|
||||||
|
|||||||
+28
-3
@@ -92,7 +92,7 @@ function parseWithRegex(m: RegExpMatchArray, _re: RegExp): Date | undefined {
|
|||||||
const mon = monthMap[monStr.toLowerCase()];
|
const mon = monthMap[monStr.toLowerCase()];
|
||||||
return new Date(Date.UTC(Number(yyyy), mon, Number(dd)));
|
return new Date(Date.UTC(Number(yyyy), mon, Number(dd)));
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
// fall through to undefined
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -171,11 +171,16 @@ export function withTimeout<T>(
|
|||||||
reason = "Timeout",
|
reason = "Timeout",
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return promise;
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return promise;
|
||||||
let timer: any;
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||||
const timeout = new Promise<never>((_, reject) => {
|
const timeout = new Promise<never>((_, reject) => {
|
||||||
timer = setTimeout(() => reject(new Error(reason)), timeoutMs);
|
timer = setTimeout(() => reject(new Error(reason)), timeoutMs);
|
||||||
});
|
});
|
||||||
return Promise.race([promise.finally(() => clearTimeout(timer)), timeout]);
|
return Promise.race([
|
||||||
|
promise.finally(() => {
|
||||||
|
if (timer !== undefined) clearTimeout(timer);
|
||||||
|
}),
|
||||||
|
timeout,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sleep(ms: number): Promise<void> {
|
export function sleep(ms: number): Promise<void> {
|
||||||
@@ -191,3 +196,23 @@ export function extractTld(domain: string): string {
|
|||||||
export function isLikelyDomain(input: string): boolean {
|
export function isLikelyDomain(input: string): boolean {
|
||||||
return /^[a-z0-9.-]+$/i.test(input) && input.includes(".");
|
return /^[a-z0-9.-]+$/i.test(input) && input.includes(".");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function asString(value: unknown): string | undefined {
|
||||||
|
return typeof value === "string" ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function asStringArray(value: unknown): string[] | undefined {
|
||||||
|
return Array.isArray(value)
|
||||||
|
? (value.filter((x) => typeof x === "string") as string[])
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function asDateLike(value: unknown): string | number | Date | undefined {
|
||||||
|
if (
|
||||||
|
typeof value === "string" ||
|
||||||
|
typeof value === "number" ||
|
||||||
|
value instanceof Date
|
||||||
|
)
|
||||||
|
return value;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user