diff --git a/src/rdap/constants.ts b/src/rdap/constants.ts index 2b0de59..759dfe1 100644 --- a/src/rdap/constants.ts +++ b/src/rdap/constants.ts @@ -39,6 +39,8 @@ export const rdapStatusColors: Record = { "redemption period": "orange", "renew period": "blue", "transfer period": "blue", + administrative: "purple", + reserved: "purple", }; export const rdapStatusInfo: Record = { @@ -104,6 +106,9 @@ export const rdapStatusInfo: Record = { "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 diff --git a/src/rdap/schemas.ts b/src/rdap/schemas.ts index 8b889ee..1b1f2b2 100644 --- a/src/rdap/schemas.ts +++ b/src/rdap/schemas.ts @@ -49,6 +49,8 @@ export const StatusEnum = z.enum([ "server update prohibited", "server hold", "transfer period", + "administrative", + "reserved", ]); export const LinkSchema = z.object({ diff --git a/src/rdap/services/rdap-query.ts b/src/rdap/services/rdap-query.ts index 9461784..1ed1b7e 100644 --- a/src/rdap/services/rdap-query.ts +++ b/src/rdap/services/rdap-query.ts @@ -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(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.")); } diff --git a/src/rdap/services/url-resolver.ts b/src/rdap/services/url-resolver.ts index 6705efa..fe29c76 100644 --- a/src/rdap/services/url-resolver.ts +++ b/src/rdap/services/url-resolver.ts @@ -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."); }