feat: add entity lookup support and administrative/reserved status types

Implements RDAP entity lookups with service provider tag resolution
and adds support for two additional IANA-defined status types
(administrative and reserved) to improve RDAP compliance and coverage.
This commit is contained in:
2025-10-23 14:31:26 -05:00
parent 0fa8abf490
commit 5646fd8006
4 changed files with 60 additions and 7 deletions

View File

@@ -39,6 +39,8 @@ export const rdapStatusColors: Record<RdapStatusType, BadgeColor> = {
"redemption period": "orange",
"renew period": "blue",
"transfer period": "blue",
administrative: "purple",
reserved: "purple",
};
export const rdapStatusInfo: Record<RdapStatusType, string> = {
@@ -104,6 +106,9 @@ export const rdapStatusInfo: Record<RdapStatusType, string> = {
"The server set the status so that DNS delegation information MUST NOT be published for the object. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731] 'serverHold' status.",
"transfer period":
"This grace period is provided after the successful transfer of object registration sponsorship from one client to another client. If the object is deleted by the client during this period, the server provides a credit to the client for the cost of the transfer. This maps to the Domain Registry Grace Period Mapping for the Extensible Provisioning Protocol (EPP) [RFC3915] 'transferPeriod' status.",
administrative:
"The object instance has been allocated administratively (i.e., not for use by the recipient in their own right in operational networks).",
reserved: "The object instance has been allocated to an IANA special-purpose address registry.",
};
// list of RDAP bootstrap registry URLs

View File

@@ -49,6 +49,8 @@ export const StatusEnum = z.enum([
"server update prohibited",
"server hold",
"transfer period",
"administrative",
"reserved",
]);
export const LinkSchema = z.object({

View File

@@ -1,5 +1,10 @@
import type { AutonomousNumber, Domain, IpNetwork, TargetType } from "@/rdap/schemas";
import { AutonomousNumberSchema, DomainSchema, IpNetworkSchema } from "@/rdap/schemas";
import type { AutonomousNumber, Domain, Entity, IpNetwork, TargetType } from "@/rdap/schemas";
import {
AutonomousNumberSchema,
DomainSchema,
EntitySchema,
IpNetworkSchema,
} from "@/rdap/schemas";
import { Result } from "true-myth";
import { loadBootstrap } from "@/rdap/services/registry";
import { getRegistryURL } from "@/rdap/services/url-resolver";
@@ -7,7 +12,7 @@ import { getAndParse } from "@/rdap/services/rdap-api";
import type { ParsedGeneric } from "@/rdap/components/Generic";
// An array of schemas to try and parse unknown JSON data with.
const schemas = [DomainSchema, AutonomousNumberSchema, IpNetworkSchema];
const schemas = [DomainSchema, AutonomousNumberSchema, IpNetworkSchema, EntitySchema];
/**
* Custom error for HTTP security warnings that includes the URL for repeatability.
@@ -157,9 +162,20 @@ export async function executeRdapQuery(
}
return Result.err(new Error("No schema was able to parse the JSON."));
}
case "entity":
case "entity": {
await loadBootstrap("entity");
const url = getRegistryURL(targetType, target, queryParams);
const result = await getAndParse<Entity>(url, EntitySchema, followReferral);
if (result.isErr) return Result.err(result.error);
return Result.ok({ data: result.value, url });
}
case "registrar":
return Result.err(new Error("The type detected has not been implemented."));
return Result.err(
new Error(
"Registrar lookups are not supported as a separate type. " +
"In RDAP, registrars are entity objects. Please use the entity type with the registrar's handle (e.g., IANA ID format)."
)
);
default:
return Result.err(new Error("The type detected has not been implemented."));
}

View File

@@ -79,8 +79,38 @@ export function getRegistryURL(
}
throw new Error(`No matching registry found for ${lookupTarget}.`);
}
case "entity":
throw new Error(`No matching entity found.`);
case "entity": {
// Extract service provider tag from entity handle (text after last hyphen)
// Example: "OPS4-RIPE" -> tag is "RIPE"
const lastHyphenIndex = lookupTarget.lastIndexOf("-");
if (lastHyphenIndex === -1 || lastHyphenIndex === lookupTarget.length - 1) {
throw new Error(
`Invalid entity handle format: ${lookupTarget}. Expected format: HANDLE-TAG`
);
}
const serviceProviderTag = lookupTarget.substring(lastHyphenIndex + 1).toUpperCase();
// Search for the service provider tag in the bootstrap registry
// Entity registry structure: [email, tags, urls]
for (const bootstrapItem of bootstrap.services) {
const tags = bootstrapItem[1]; // Tags are at index 1 (0=email, 1=tags, 2=urls)
const urls = bootstrapItem[2]; // URLs are at index 2
if (
tags.some((tag) => tag.toUpperCase() === serviceProviderTag) &&
urls &&
urls.length > 0
) {
url = getBestURL(urls as [string, ...string[]]);
break typeSwitch;
}
}
throw new Error(
`No matching registry found for entity service provider tag: ${serviceProviderTag}`
);
}
default:
throw new Error("Invalid lookup target provided.");
}