ci: implement comprehensive CI/CD and workflow automation

- Add GitHub Actions workflows for CI, release, and deployment
- Configure Renovate for automated dependency updates
- Set up Husky pre-commit hooks with lint-staged
- Add commitlint for enforcing Conventional Commits
- Configure semantic-release for automated versioning
- Add Prettier configuration for consistent formatting
- Reformat codebase with new formatting rules
This commit is contained in:
2025-10-22 02:45:58 -05:00
parent 2c1f882cd9
commit c17f733da1
48 changed files with 5453 additions and 2422 deletions

26
.commitlintrc.json Normal file
View File

@@ -0,0 +1,26 @@
{
"extends": ["@commitlint/config-conventional"],
"rules": {
"type-enum": [
2,
"always",
[
"feat",
"fix",
"docs",
"style",
"refactor",
"perf",
"test",
"build",
"ci",
"chore",
"revert"
]
],
"subject-case": [2, "never", ["upper-case"]],
"subject-empty": [2, "never"],
"subject-full-stop": [2, "never", "."],
"header-max-length": [2, "always", 100]
}
}

85
.github/renovate.json vendored Normal file
View File

@@ -0,0 +1,85 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":dependencyDashboard",
":semanticCommits",
":automergeDigest",
":automergeMinor"
],
"schedule": ["after 10pm every weekday", "before 5am every weekday", "every weekend"],
"timezone": "America/Chicago",
"prConcurrentLimit": 3,
"prCreation": "not-pending",
"rebaseWhen": "behind-base-branch",
"semanticCommitScope": "deps",
"vulnerabilityAlerts": {
"labels": ["security"],
"automerge": true,
"schedule": ["at any time"]
},
"packageRules": [
{
"description": "Automerge dev dependencies",
"matchDepTypes": ["devDependencies"],
"automerge": true,
"automergeType": "pr",
"minimumReleaseAge": "3 days"
},
{
"description": "Automerge TypeScript type packages",
"matchPackagePatterns": ["^@types/"],
"automerge": true,
"automergeType": "pr"
},
{
"description": "Group ESLint packages together",
"matchPackagePatterns": ["^eslint", "^@typescript-eslint/"],
"groupName": "eslint packages",
"automerge": true
},
{
"description": "Group testing packages together",
"matchPackagePatterns": ["^vitest", "^@vitest/", "^@testing-library/"],
"groupName": "testing packages",
"automerge": true
},
{
"description": "Group Next.js related packages",
"matchPackageNames": ["next", "eslint-config-next"],
"groupName": "Next.js packages",
"minimumReleaseAge": "7 days"
},
{
"description": "Group React packages",
"matchPackageNames": ["react", "react-dom", "@types/react", "@types/react-dom"],
"groupName": "React packages",
"minimumReleaseAge": "7 days"
},
{
"description": "Pin Node.js major versions",
"matchPackageNames": ["node"],
"enabled": false
},
{
"description": "Group Tailwind CSS packages",
"matchPackagePatterns": [
"^tailwindcss",
"^@tailwindcss/",
"prettier-plugin-tailwindcss"
],
"groupName": "Tailwind CSS packages"
},
{
"description": "Group font packages",
"matchPackagePatterns": ["^@fontsource"],
"groupName": "font packages",
"automerge": true
}
],
"postUpdateOptions": ["pnpmDedupe"],
"lockFileMaintenance": {
"enabled": true,
"schedule": ["before 5am on monday"]
}
}

165
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,165 @@
name: CI
on:
push:
branches: [master]
pull_request:
branches: [master]
env:
NODE_VERSION: "20"
PNPM_VERSION: "9.0.0"
jobs:
# Code quality checks
quality:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run linting
run: pnpm lint
- name: Type checking
run: pnpm type-check
- name: Check formatting
run: pnpm prettier --check .
# Testing
test:
name: Test Suite
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run unit tests
run: pnpm test:run
- name: Run integration tests
run: pnpm test:integration
- name: Upload coverage
uses: codecov/codecov-action@v4
if: always()
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/coverage-final.json
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
# Build verification
build:
name: Build Application
needs: [quality, test]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build application
run: pnpm build
env:
NODE_ENV: production
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: next-build
path: |
.next/
out/
retention-days: 7
if-no-files-found: warn
# Security scanning
security:
name: Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run security audit
run: pnpm audit --audit-level=moderate
continue-on-error: true
- name: Check for known vulnerabilities
uses: aquasecurity/trivy-action@master
with:
scan-type: "fs"
scan-ref: "."
format: "sarif"
output: "trivy-results.sarif"
severity: "CRITICAL,HIGH"
exit-code: 0
- name: Upload Trivy results
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: "trivy-results.sarif"

71
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
name: Release
on:
push:
branches:
- master
permissions:
contents: write
issues: write
pull-requests: write
jobs:
release:
name: Create Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Check for skip ci
id: check_skip
run: |
if git log -1 --pretty=%B | grep -q '\[skip ci\]'; then
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Setup pnpm
if: steps.check_skip.outputs.skip == 'false'
uses: pnpm/action-setup@v4
with:
version: 9.0.0
- name: Setup Node.js
if: steps.check_skip.outputs.skip == 'false'
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
if: steps.check_skip.outputs.skip == 'false'
run: pnpm install --frozen-lockfile
- name: Build application
if: steps.check_skip.outputs.skip == 'false'
run: pnpm build
env:
NODE_ENV: production
- name: Run semantic release
if: steps.check_skip.outputs.skip == 'false'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm semantic-release
- name: Upload release artifacts
if: steps.check_skip.outputs.skip == 'false'
uses: actions/upload-artifact@v4
with:
name: release-build
path: |
.next/
out/
retention-days: 30

5
.husky/commit-msg Normal file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Validate commit message format
pnpm commitlint --edit "$1"

5
.husky/pre-commit Normal file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Run linting on staged files
pnpm lint-staged

6
.lintstagedrc.json Normal file
View File

@@ -0,0 +1,6 @@
{
"*.{js,jsx,ts,tsx,mjs}": ["eslint --fix", "prettier --write"],
"*.{json,md,yml,yaml}": ["prettier --write"],
"*.{css,scss}": ["prettier --write"],
"package.json": ["prettier --write"]
}

25
.prettierignore Normal file
View File

@@ -0,0 +1,25 @@
# Dependencies
node_modules
pnpm-lock.yaml
# Build outputs
.next
out
dist
build
# Cache
.cache
.turbo
# Coverage
coverage
# Misc
.DS_Store
*.log
*.tsbuildinfo
# Environment
.env
.env*.local

9
.prettierrc.json Normal file
View File

@@ -0,0 +1,9 @@
{
"semi": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 4,
"useTabs": true,
"endOfLine": "lf",
"plugins": ["prettier-plugin-tailwindcss"]
}

109
.releaserc.json Normal file
View File

@@ -0,0 +1,109 @@
{
"branches": [
"master",
{
"name": "beta",
"prerelease": true
},
{
"name": "alpha",
"prerelease": true
}
],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits",
"releaseRules": [
{
"type": "docs",
"scope": "README",
"release": "patch"
},
{
"type": "refactor",
"release": "patch"
},
{
"type": "style",
"release": "patch"
},
{
"type": "perf",
"release": "patch"
}
],
"parserOpts": {
"noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES"]
}
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits",
"presetConfig": {
"types": [
{
"type": "feat",
"section": "Features"
},
{
"type": "fix",
"section": "Bug Fixes"
},
{
"type": "perf",
"section": "Performance Improvements"
},
{
"type": "refactor",
"section": "Code Refactoring"
},
{
"type": "docs",
"section": "Documentation"
},
{
"type": "style",
"section": "Styles"
},
{
"type": "test",
"section": "Tests"
},
{
"type": "build",
"section": "Build System"
},
{
"type": "ci",
"section": "Continuous Integration"
}
]
}
}
],
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md"
}
],
[
"@semantic-release/npm",
{
"npmPublish": false
}
],
[
"@semantic-release/git",
{
"assets": ["CHANGELOG.md", "package.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
],
"@semantic-release/github"
]
}

View File

@@ -35,11 +35,11 @@ But in all seriousness, my project does intend to have more features:
- Whois: RDAP is a newer protocol that is meant to replace Whois, but some servers still use Whois. I might add support for that.
- Punycode: Some domain names use punycode, which is a way to represent Unicode characters in ASCII. I might add support for working with these domains.
- Better Error Handling: The original RDAP client doesn't handle errors very well, or tell you what went wrong. I want to fix that.
- For example, when querying a TLD that does not have a RDAP server, or one that does not exist, the original client doesn't explain this very well.
- For example, when querying a TLD that does not have a RDAP server, or one that does not exist, the original client doesn't explain this very well.
- RDAP Schema Adherence: RDAP servers are supposed to follow a schema, but it appears a large number simply don't. I intend to provide handling for this.
- Essentially, two separate schemas will be available: one for basic type checking, and one for the RFC-compliant schema.
- If the server doesn't follow the RFC-compliant schema, the basic schema will be used instead.
- It's hard to tell at this moment if Typescript can handle this well, but I'll try.
- Essentially, two separate schemas will be available: one for basic type checking, and one for the RFC-compliant schema.
- If the server doesn't follow the RFC-compliant schema, the basic schema will be used instead.
- It's hard to tell at this moment if Typescript can handle this well, but I'll try.
[rdap]: https://rdap.xevion.dev
[nextjs]: https://nextjs.org

View File

@@ -8,69 +8,69 @@ import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [
// Base configuration with ignores
{
ignores: [
".next/**",
"node_modules/**",
"out/**",
"*.config.mjs",
"*.config.js",
"next-env.d.ts" // Next.js generated file
],
},
// Base configuration with ignores
{
ignores: [
".next/**",
"node_modules/**",
"out/**",
"*.config.mjs",
"*.config.js",
"next-env.d.ts", // Next.js generated file
],
},
// Next.js core web vitals using FlatCompat
...compat.extends("next/core-web-vitals"),
// Next.js core web vitals using FlatCompat
...compat.extends("next/core-web-vitals"),
// TypeScript recommended rules
...compat.extends("plugin:@typescript-eslint/recommended"),
// TypeScript recommended rules
...compat.extends("plugin:@typescript-eslint/recommended"),
// Base TypeScript configuration
{
plugins: {
"@typescript-eslint": typescriptEslint,
},
// Base TypeScript configuration
{
plugins: {
"@typescript-eslint": typescriptEslint,
},
languageOptions: {
parser: tsParser,
ecmaVersion: "latest",
sourceType: "module",
parserOptions: {
project: "./tsconfig.json",
},
},
languageOptions: {
parser: tsParser,
ecmaVersion: "latest",
sourceType: "module",
parserOptions: {
project: "./tsconfig.json",
},
},
rules: {
"@typescript-eslint/consistent-type-imports": "warn",
},
},
rules: {
"@typescript-eslint/consistent-type-imports": "warn",
},
},
// Additional strict TypeScript rules for .ts and .tsx files
{
files: ["**/*.ts", "**/*.tsx"],
...compat.extends("plugin:@typescript-eslint/recommended-requiring-type-checking")[0],
// Additional strict TypeScript rules for .ts and .tsx files
{
files: ["**/*.ts", "**/*.tsx"],
...compat.extends("plugin:@typescript-eslint/recommended-requiring-type-checking")[0],
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
parserOptions: {
project: "./tsconfig.json",
},
},
},
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
parserOptions: {
project: "./tsconfig.json",
},
},
},
// Allow CommonJS require in .cjs files
{
files: ["**/*.cjs"],
rules: {
"@typescript-eslint/no-require-imports": "off",
},
},
// Allow CommonJS require in .cjs files
{
files: ["**/*.cjs"],
rules: {
"@typescript-eslint/no-require-imports": "off",
},
},
];

View File

@@ -7,10 +7,10 @@
/** @type {import("next").NextConfig} */
const config = {
reactStrictMode: true,
i18n: {
locales: ["en"],
defaultLocale: "en",
},
reactStrictMode: true,
i18n: {
locales: ["en"],
defaultLocale: "en",
},
};
export default config;

View File

@@ -1,62 +1,75 @@
{
"name": "rdap",
"version": "0.2.0",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev",
"lint": "eslint .",
"start": "next start",
"test": "vitest",
"test:ui": "vitest --ui",
"test:run": "vitest run --exclude '**/*.integration.test.ts'",
"test:integration": "vitest run --include '**/*.integration.test.ts'",
"test:all": "vitest run",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@fontsource-variable/inter": "^5.2.8",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@headlessui/react": "^2.2.9",
"@heroicons/react": "^2.0.16",
"@swc/helpers": "^0.5.11",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"immutability-helper": "^3.1.1",
"next": "^15.5.6",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-hook-form": "^7.42.1",
"react-timeago": "^8.3.0",
"sass": "^1.57.1",
"tailwind-merge": "^3.3.1",
"true-myth": "^9.2.0",
"usehooks-ts": "^3.1.1",
"zod": "^4.1.12"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.15",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@types/node": "^24.9.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.2",
"@vitest/ui": "^3.2.4",
"eslint": "^9.38.0",
"eslint-config-next": "15.5.6",
"happy-dom": "^20.0.8",
"postcss": "^8.4.14",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.7.1",
"tailwindcss": "^4.1.15",
"type-fest": "^5.1.0",
"typescript": "^5.9.3",
"vitest": "^3.2.4"
},
"ct3aMetadata": {
"initVersion": "7.2.0"
},
"packageManager": "pnpm@9.0.0"
"name": "rdap",
"version": "0.2.0",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"start": "next start",
"test": "vitest",
"test:ui": "vitest --ui",
"test:run": "vitest run --exclude '**/*.integration.test.ts'",
"test:integration": "vitest run --include '**/*.integration.test.ts'",
"test:all": "vitest run",
"type-check": "tsc --noEmit",
"prepare": "husky install",
"semantic-release": "semantic-release"
},
"dependencies": {
"@fontsource-variable/inter": "^5.2.8",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@headlessui/react": "^2.2.9",
"@heroicons/react": "^2.0.16",
"@swc/helpers": "^0.5.11",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"immutability-helper": "^3.1.1",
"next": "^15.5.6",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-hook-form": "^7.42.1",
"react-timeago": "^8.3.0",
"sass": "^1.57.1",
"tailwind-merge": "^3.3.1",
"true-myth": "^9.2.0",
"usehooks-ts": "^3.1.1",
"zod": "^4.1.12"
},
"devDependencies": {
"@commitlint/cli": "^19.0.0",
"@commitlint/config-conventional": "^19.0.0",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@tailwindcss/postcss": "^4.1.15",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@types/node": "^24.9.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.2",
"@vitest/ui": "^3.2.4",
"conventional-changelog-conventionalcommits": "^8.0.0",
"eslint": "^9.38.0",
"eslint-config-next": "15.5.6",
"happy-dom": "^20.0.8",
"husky": "^9.0.0",
"lint-staged": "^15.0.0",
"postcss": "^8.4.14",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.7.1",
"semantic-release": "^24.0.0",
"tailwindcss": "^4.1.15",
"type-fest": "^5.1.0",
"typescript": "^5.9.3",
"vitest": "^3.2.4"
},
"ct3aMetadata": {
"initVersion": "7.2.0"
},
"packageManager": "pnpm@9.0.0"
}

2652
pnpm-lock.yaml generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
plugins: {
"@tailwindcss/postcss": {},
},
};

View File

@@ -1,4 +0,0 @@
/** @type {import("prettier").Config} */
module.exports = {
plugins: [require.resolve("prettier-plugin-tailwindcss")],
};

View File

@@ -11,25 +11,25 @@
* @returns {number} The relative position of the value to the range as -1, 0 or 1.
*/
function compareASN(value: number, range: string): number {
const [start, end] = range.split("-", 2) as [string, string];
if (value < parseInt(start)) return -1;
if (value > parseInt(end)) return 1;
return 0;
const [start, end] = range.split("-", 2) as [string, string];
if (value < parseInt(start)) return -1;
if (value > parseInt(end)) return 1;
return 0;
}
/**
* Find the range in which a given ASN exists via binary search. If not found, -1 is used.
*/
export function findASN(asn: number, ranges: string[]) {
let start = 0;
let end = ranges.length - 1;
let start = 0;
let end = ranges.length - 1;
while (start <= end) {
const mid = Math.floor((start + end) / 2);
const comparison = compareASN(asn, ranges[mid] as string);
if (comparison == 0) return mid; // Success case
if (comparison == -1) end = mid - 1;
else start = mid + 1;
}
return -1; // Failure case
while (start <= end) {
const mid = Math.floor((start + end) / 2);
const comparison = compareASN(asn, ranges[mid] as string);
if (comparison == 0) return mid; // Success case
if (comparison == -1) end = mid - 1;
else start = mid + 1;
}
return -1; // Failure case
}

View File

@@ -2,104 +2,107 @@ import type { FunctionComponent, ReactNode } from "react";
import React from "react";
import { useBoolean } from "usehooks-ts";
import {
LinkIcon,
CodeBracketIcon,
DocumentArrowDownIcon,
ClipboardDocumentIcon,
LinkIcon,
CodeBracketIcon,
DocumentArrowDownIcon,
ClipboardDocumentIcon,
} from "@heroicons/react/24/outline";
type AbstractCardProps = {
children?: ReactNode;
header?: ReactNode;
footer?: ReactNode;
data?: object;
url?: string;
children?: ReactNode;
header?: ReactNode;
footer?: ReactNode;
data?: object;
url?: string;
};
const AbstractCard: FunctionComponent<AbstractCardProps> = ({
url,
children,
header,
footer,
data,
url,
children,
header,
footer,
data,
}) => {
const { value: showRaw, toggle: toggleRaw } = useBoolean(false);
const { value: showRaw, toggle: toggleRaw } = useBoolean(false);
return (
<div className="mb-4 overflow-clip rounded bg-zinc-800 shadow">
{header != undefined || data != undefined ? (
<div className="flex bg-zinc-700 p-2 pl-3 md:pl-5">
<div className="flex grow gap-2">{header}</div>
{url != undefined ? (
<div className="pr-2">
<a href={url} target="_blank" rel="noreferrer">
<LinkIcon className="mt-1 h-5 w-5 cursor-pointer" />
</a>
</div>
) : null}
{data != undefined ? (
<>
<div className="pr-2">
<ClipboardDocumentIcon
onClick={() => {
// stringify the JSON object, then begin the async clipboard write
navigator.clipboard
.writeText(JSON.stringify(data, null, 4))
.then(
() => {
console.log("Copied to clipboard.");
},
(err) => {
if (err instanceof Error)
console.error(
`Failed to copy to clipboard (${err.toString()}).`
);
else console.error("Failed to copy to clipboard.");
}
);
}}
className="h-6 w-6 cursor-pointer"
/>
</div>
<div className="pr-2">
<DocumentArrowDownIcon
onClick={() => {
const file = new Blob([JSON.stringify(data, null, 4)], {
type: "application/json",
});
return (
<div className="mb-4 overflow-clip rounded bg-zinc-800 shadow">
{header != undefined || data != undefined ? (
<div className="flex bg-zinc-700 p-2 pl-3 md:pl-5">
<div className="flex grow gap-2">{header}</div>
{url != undefined ? (
<div className="pr-2">
<a href={url} target="_blank" rel="noreferrer">
<LinkIcon className="mt-1 h-5 w-5 cursor-pointer" />
</a>
</div>
) : null}
{data != undefined ? (
<>
<div className="pr-2">
<ClipboardDocumentIcon
onClick={() => {
// stringify the JSON object, then begin the async clipboard write
navigator.clipboard
.writeText(JSON.stringify(data, null, 4))
.then(
() => {
console.log("Copied to clipboard.");
},
(err) => {
if (err instanceof Error)
console.error(
`Failed to copy to clipboard (${err.toString()}).`
);
else
console.error(
"Failed to copy to clipboard."
);
}
);
}}
className="h-6 w-6 cursor-pointer"
/>
</div>
<div className="pr-2">
<DocumentArrowDownIcon
onClick={() => {
const file = new Blob([JSON.stringify(data, null, 4)], {
type: "application/json",
});
const anchor = document.createElement("a");
anchor.href = URL.createObjectURL(file);
anchor.download = "response.json";
anchor.click();
}}
className="h-6 w-6 cursor-pointer"
/>
</div>
<div className="pr-1">
<CodeBracketIcon
onClick={toggleRaw}
className="h-6 w-6 cursor-pointer"
/>
</div>
</>
) : null}
</div>
) : null}
<div className="max-w-full overflow-x-auto p-2 px-4">
{showRaw ? (
<pre className="scrollbar-thin m-2 max-h-[40rem] max-w-full overflow-y-auto whitespace-pre-wrap rounded">
{JSON.stringify(data, null, 4)}
</pre>
) : (
children
)}
</div>
{footer != null ? (
<div className="flex gap-2 bg-zinc-700 p-2 pl-5">{footer}</div>
) : null}
</div>
);
const anchor = document.createElement("a");
anchor.href = URL.createObjectURL(file);
anchor.download = "response.json";
anchor.click();
}}
className="h-6 w-6 cursor-pointer"
/>
</div>
<div className="pr-1">
<CodeBracketIcon
onClick={toggleRaw}
className="h-6 w-6 cursor-pointer"
/>
</div>
</>
) : null}
</div>
) : null}
<div className="max-w-full overflow-x-auto p-2 px-4">
{showRaw ? (
<pre className="scrollbar-thin m-2 max-h-[40rem] max-w-full overflow-y-auto rounded whitespace-pre-wrap">
{JSON.stringify(data, null, 4)}
</pre>
) : (
children
)}
</div>
{footer != null ? (
<div className="flex gap-2 bg-zinc-700 p-2 pl-5">{footer}</div>
) : null}
</div>
);
};
export default AbstractCard;

View File

@@ -4,8 +4,8 @@ import { format } from "date-fns";
import TimeAgo from "react-timeago";
type DynamicDateProps = {
value: Date | number;
absoluteFormat?: string;
value: Date | number;
absoluteFormat?: string;
};
/**
@@ -13,24 +13,21 @@ type DynamicDateProps = {
* @param value The date to be displayed, the Date value, or
* @param absoluteFormat Optional - the date-fns format string to use for the absolute date rendering.
*/
const DynamicDate: FunctionComponent<DynamicDateProps> = ({
value,
absoluteFormat,
}) => {
const { value: showAbsolute, toggle: toggleFormat } = useBoolean(true);
const DynamicDate: FunctionComponent<DynamicDateProps> = ({ value, absoluteFormat }) => {
const { value: showAbsolute, toggle: toggleFormat } = useBoolean(true);
const date = new Date(value);
return (
<button onClick={toggleFormat}>
<span className="dashed" title={date.toISOString()}>
{showAbsolute ? (
format(date, absoluteFormat ?? "LLL do, y HH:mm:ss xxx")
) : (
<TimeAgo date={date} />
)}
</span>
</button>
);
const date = new Date(value);
return (
<button onClick={toggleFormat}>
<span className="dashed" title={date.toISOString()}>
{showAbsolute ? (
format(date, absoluteFormat ?? "LLL do, y HH:mm:ss xxx")
) : (
<TimeAgo date={date} />
)}
</span>
</button>
);
};
export default DynamicDate;

View File

@@ -3,49 +3,49 @@ import { XCircleIcon } from "@heroicons/react/20/solid";
import { cn } from "@/lib/utils";
export type ErrorCardProps = {
title: ReactNode;
description?: ReactNode;
issues?: ReactNode[];
className?: string;
title: ReactNode;
description?: ReactNode;
issues?: ReactNode[];
className?: string;
};
const ErrorCard: FunctionComponent<ErrorCardProps> = ({
title,
description,
issues,
className,
title,
description,
issues,
className,
}) => {
return (
<div
className={cn(
className,
"rounded-md border border-red-700/30 bg-zinc-800 px-3 pb-1 pt-3"
)}
>
<div className="flex">
<div className="shrink-0">
<XCircleIcon className="h-5 w-5 text-red-300" aria-hidden="true" />
</div>
<div className="ml-3 w-full text-sm text-red-300">
<h3 className="font-medium text-red-200">{title}</h3>
{description != undefined ? (
<div className="mt-2 max-h-24 w-full overflow-y-auto whitespace-pre-wrap">
{description}
</div>
) : null}
<div className="mt-2">
{issues != undefined ? (
<ul role="list" className="flex list-disc flex-col gap-1 pl-5">
{issues.map((issueText, index) => (
<li key={index}>{issueText}</li>
))}
</ul>
) : null}
</div>
</div>
</div>
</div>
);
return (
<div
className={cn(
className,
"rounded-md border border-red-700/30 bg-zinc-800 px-3 pt-3 pb-1"
)}
>
<div className="flex">
<div className="shrink-0">
<XCircleIcon className="h-5 w-5 text-red-300" aria-hidden="true" />
</div>
<div className="ml-3 w-full text-sm text-red-300">
<h3 className="font-medium text-red-200">{title}</h3>
{description != undefined ? (
<div className="mt-2 max-h-24 w-full overflow-y-auto whitespace-pre-wrap">
{description}
</div>
) : null}
<div className="mt-2">
{issues != undefined ? (
<ul role="list" className="flex list-disc flex-col gap-1 pl-5">
{issues.map((issueText, index) => (
<li key={index}>{issueText}</li>
))}
</ul>
) : null}
</div>
</div>
</div>
</div>
);
};
export default ErrorCard;

View File

@@ -3,24 +3,24 @@ import React from "react";
import { cn } from "@/lib/utils";
type PropertyProps = {
title: string | ReactNode;
children: string | ReactNode;
titleClass?: string;
valueClass?: string;
title: string | ReactNode;
children: string | ReactNode;
titleClass?: string;
valueClass?: string;
};
const Property: FunctionComponent<PropertyProps> = ({
title,
children,
titleClass,
valueClass,
title,
children,
titleClass,
valueClass,
}) => {
return (
<>
<dt className={cn("font-medium", titleClass)}>{title}:</dt>
<dd className={cn("mb-2 ml-6 mt-2", valueClass)}>{children}</dd>
</>
);
return (
<>
<dt className={cn("font-medium", titleClass)}>{title}:</dt>
<dd className={cn("mt-2 mb-2 ml-6", valueClass)}>{children}</dd>
</>
);
};
export default Property;

View File

@@ -3,33 +3,33 @@ import React from "react";
import Property from "@/components/common/Property";
const PropertyListItem: FunctionComponent<{
title: string;
children: string;
title: string;
children: string;
}> = ({ title, children }) => {
return (
<li>
<span className="dashed" title={title}>
{children}
</span>
</li>
);
return (
<li>
<span className="dashed" title={title}>
{children}
</span>
</li>
);
};
type PropertyListProps = {
title: string;
children: ReactNode;
title: string;
children: ReactNode;
};
const PropertyList: FunctionComponent<PropertyListProps> & {
Item: typeof PropertyListItem;
Item: typeof PropertyListItem;
} = ({ title, children }) => {
return (
<Property title={title}>
<ul key={2} className="list-disc">
{children}
</ul>
</Property>
);
return (
<Property title={title}>
<ul key={2} className="list-disc">
{children}
</ul>
</Property>
);
};
PropertyList.Item = PropertyListItem;

View File

@@ -4,18 +4,18 @@ import { Fragment, useState } from "react";
import { onPromise, preventDefault } from "@/helpers";
import type { SimplifiedTargetType, SubmitProps, TargetType } from "@/types";
import {
CheckIcon,
ChevronUpDownIcon,
LockClosedIcon,
MagnifyingGlassIcon,
ArrowPathIcon,
CheckIcon,
ChevronUpDownIcon,
LockClosedIcon,
MagnifyingGlassIcon,
ArrowPathIcon,
} from "@heroicons/react/20/solid";
import {
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption,
Transition,
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption,
Transition,
} from "@headlessui/react";
import { cn } from "@/lib/utils";
import type { Maybe } from "true-myth";
@@ -25,304 +25,289 @@ import { placeholders } from "@/constants";
* Props for the LookupInput component.
*/
type LookupInputProps = {
isLoading?: boolean;
/**
* Callback function called when a type of registry is detected when a user changes their input.
* @param type - The detected type of registry.
* @returns A promise.
*/
onRegistry?: (type: TargetType) => Promise<void>;
/**
* Callback function called when a user hits submit.
* @param props - The submit props.
* @returns A promise.
*/
onSubmit?: (props: SubmitProps) => Promise<void>;
/**
* Callback function called when a user changes their input (text search) or explicitly changes the type of search.
* @param target - The target object containing the search target and target type.
* @returns Nothing.
*/
onChange?: (target: {
target: string;
targetType: TargetType | null;
}) => Promise<void>;
detectedType: Maybe<TargetType>;
isLoading?: boolean;
/**
* Callback function called when a type of registry is detected when a user changes their input.
* @param type - The detected type of registry.
* @returns A promise.
*/
onRegistry?: (type: TargetType) => Promise<void>;
/**
* Callback function called when a user hits submit.
* @param props - The submit props.
* @returns A promise.
*/
onSubmit?: (props: SubmitProps) => Promise<void>;
/**
* Callback function called when a user changes their input (text search) or explicitly changes the type of search.
* @param target - The target object containing the search target and target type.
* @returns Nothing.
*/
onChange?: (target: { target: string; targetType: TargetType | null }) => Promise<void>;
detectedType: Maybe<TargetType>;
};
const LookupInput: FunctionComponent<LookupInputProps> = ({
isLoading,
onSubmit,
onChange,
detectedType,
isLoading,
onSubmit,
onChange,
detectedType,
}: LookupInputProps) => {
const { register, handleSubmit, getValues } = useForm<SubmitProps>({
defaultValues: {
target: "",
// Not used at this time.
followReferral: false,
requestJSContact: false,
},
});
const { register, handleSubmit, getValues } = useForm<SubmitProps>({
defaultValues: {
target: "",
// Not used at this time.
followReferral: false,
requestJSContact: false,
},
});
/**
* A mapping of available (simple) target types to their long-form human-readable names.
*/
const objectNames: Record<SimplifiedTargetType | "auto", string> = {
auto: "Autodetect",
domain: "Domain",
ip: "IP/CIDR", // IPv4/IPv6 are combined into this option
tld: "TLD",
autnum: "AS Number",
entity: "Entity Handle",
registrar: "Registrar",
url: "URL",
json: "JSON",
};
/**
* A mapping of available (simple) target types to their long-form human-readable names.
*/
const objectNames: Record<SimplifiedTargetType | "auto", string> = {
auto: "Autodetect",
domain: "Domain",
ip: "IP/CIDR", // IPv4/IPv6 are combined into this option
tld: "TLD",
autnum: "AS Number",
entity: "Entity Handle",
registrar: "Registrar",
url: "URL",
json: "JSON",
};
/**
* Mapping of precise target types to their simplified short-form names.
*/
const targetShortNames: Record<TargetType, string> = {
domain: "Domain",
tld: "TLD",
ip4: "IPv4",
ip6: "IPv6",
autnum: "ASN",
entity: "Entity",
registrar: "Registrar",
url: "URL",
json: "JSON",
};
/**
* Mapping of precise target types to their simplified short-form names.
*/
const targetShortNames: Record<TargetType, string> = {
domain: "Domain",
tld: "TLD",
ip4: "IPv4",
ip6: "IPv6",
autnum: "ASN",
entity: "Entity",
registrar: "Registrar",
url: "URL",
json: "JSON",
};
/**
* Represents the selected value in the LookupInput component.
*/
const [selected, setSelected] = useState<SimplifiedTargetType | "auto">(
"auto"
);
/**
* Represents the selected value in the LookupInput component.
*/
const [selected, setSelected] = useState<SimplifiedTargetType | "auto">("auto");
/**
* Retrieves the target type based on the provided value.
* @param value - The value to retrieve the target type for.
* @returns The target type as ObjectType or null.
*/
function retrieveTargetType(value?: string | null): TargetType | null {
// If the value is null and the selected value is null, return null.
if (value == null) value = selected;
/**
* Retrieves the target type based on the provided value.
* @param value - The value to retrieve the target type for.
* @returns The target type as ObjectType or null.
*/
function retrieveTargetType(value?: string | null): TargetType | null {
// If the value is null and the selected value is null, return null.
if (value == null) value = selected;
// 'auto' means 'do whatever' so we return null.
if (value == "auto") return null;
// 'auto' means 'do whatever' so we return null.
if (value == "auto") return null;
return value as TargetType;
}
return value as TargetType;
}
const searchIcon = (
<>
<button
type="submit"
className={cn({
"absolute inset-y-0 left-0 flex items-center pl-3": true,
"pointer-events-none": isLoading,
})}
>
{isLoading ? (
<ArrowPathIcon
className="h-5 w-5 animate-spin text-zinc-400"
aria-hidden="true"
/>
) : (
<MagnifyingGlassIcon
className="h-5 w-5 text-zinc-400"
aria-hidden="true"
/>
)}
</button>
</>
);
const searchIcon = (
<>
<button
type="submit"
className={cn({
"absolute inset-y-0 left-0 flex items-center pl-3": true,
"pointer-events-none": isLoading,
})}
>
{isLoading ? (
<ArrowPathIcon
className="h-5 w-5 animate-spin text-zinc-400"
aria-hidden="true"
/>
) : (
<MagnifyingGlassIcon className="h-5 w-5 text-zinc-400" aria-hidden="true" />
)}
</button>
</>
);
const searchInput = (
<input
className={cn(
"lg:py-4.5 block w-full rounded-l-md border border-transparent",
"bg-zinc-700 py-2 pl-10 pr-1.5 text-sm placeholder-zinc-400 placeholder:translate-y-2 focus:text-zinc-200",
" focus:outline-hidden sm:text-sm md:py-3 md:text-base lg:text-lg"
)}
disabled={isLoading}
placeholder={placeholders[selected]}
type="search"
{...register("target", {
required: true,
onChange: () => {
if (onChange != undefined)
void onChange({
target: getValues("target"),
// dropdown target will be pulled from state anyways, so no need to provide it here
targetType: retrieveTargetType(null),
});
},
})}
/>
);
const searchInput = (
<input
className={cn(
"block w-full rounded-l-md border border-transparent lg:py-4.5",
"bg-zinc-700 py-2 pr-1.5 pl-10 text-sm placeholder-zinc-400 placeholder:translate-y-2 focus:text-zinc-200",
"focus:outline-hidden sm:text-sm md:py-3 md:text-base lg:text-lg"
)}
disabled={isLoading}
placeholder={placeholders[selected]}
type="search"
{...register("target", {
required: true,
onChange: () => {
if (onChange != undefined)
void onChange({
target: getValues("target"),
// dropdown target will be pulled from state anyways, so no need to provide it here
targetType: retrieveTargetType(null),
});
},
})}
/>
);
const dropdown = (
<Listbox
value={selected}
onChange={(value) => {
setSelected(value);
const dropdown = (
<Listbox
value={selected}
onChange={(value) => {
setSelected(value);
if (onChange != undefined)
void onChange({
target: getValues("target"),
// we provide the value as the state will not have updated yet for this context
targetType: retrieveTargetType(value),
});
}}
disabled={isLoading}
>
<div className="relative">
<ListboxButton
className={cn(
"relative h-full w-full cursor-default whitespace-nowrap rounded-r-lg bg-zinc-700 py-2 pl-1 pr-10 text-right",
"text-xs focus:outline-hidden focus-visible:border-indigo-500 sm:text-sm md:text-base lg:text-lg",
"focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 "
)}
>
{/* Fetch special text for 'auto' mode, otherwise just use the options. */}
<span className="block">
{selected == "auto" ? (
// If the detected type was provided, then notate which in parentheses. Compact object naming might be better in the future.
detectedType.isJust ? (
<>
Auto (
<span className="animate-pulse">
{targetShortNames[detectedType.value]}
</span>
)
</>
) : (
objectNames["auto"]
)
) : (
<>
<LockClosedIcon
className="mb-1 mr-2.5 inline h-4 w-4 animate-pulse text-zinc-500"
aria-hidden
/>
{objectNames[selected]}
</>
)}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon
className="h-5 w-5 text-zinc-200"
aria-hidden="true"
/>
</span>
</ListboxButton>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<ListboxOptions
className={cn(
"scrollbar-thin absolute right-0 mt-1 max-h-60 min-w-full overflow-auto rounded-md bg-zinc-700 py-1",
"text-zinc-200 shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm"
)}
>
{Object.entries(objectNames).map(([key, value]) => (
<ListboxOption
key={key}
className={({ focus }) =>
cn(
"relative cursor-default select-none py-2 pl-10 pr-4",
focus ? "bg-zinc-800 text-zinc-300" : null
)
}
value={key}
>
{({ selected }) => (
<>
<span
className={cn(
"block whitespace-nowrap text-right text-xs md:text-sm lg:text-base",
selected ? "font-medium" : null
)}
>
{value}
</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-blue-500">
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : (
<button
onClick={(e) => {
e.preventDefault();
console.log("TODO: Show Help Explanation");
}}
className="absolute inset-y-0 left-0 flex items-center pl-4 text-lg font-bold opacity-20 hover:animate-pulse"
>
?
</button>
)}
</>
)}
</ListboxOption>
))}
</ListboxOptions>
</Transition>
</div>
</Listbox>
);
if (onChange != undefined)
void onChange({
target: getValues("target"),
// we provide the value as the state will not have updated yet for this context
targetType: retrieveTargetType(value),
});
}}
disabled={isLoading}
>
<div className="relative">
<ListboxButton
className={cn(
"relative h-full w-full cursor-default rounded-r-lg bg-zinc-700 py-2 pr-10 pl-1 text-right whitespace-nowrap",
"text-xs focus:outline-hidden focus-visible:border-indigo-500 sm:text-sm md:text-base lg:text-lg",
"focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300"
)}
>
{/* Fetch special text for 'auto' mode, otherwise just use the options. */}
<span className="block">
{selected == "auto" ? (
// If the detected type was provided, then notate which in parentheses. Compact object naming might be better in the future.
detectedType.isJust ? (
<>
Auto (
<span className="animate-pulse">
{targetShortNames[detectedType.value]}
</span>
)
</>
) : (
objectNames["auto"]
)
) : (
<>
<LockClosedIcon
className="mr-2.5 mb-1 inline h-4 w-4 animate-pulse text-zinc-500"
aria-hidden
/>
{objectNames[selected]}
</>
)}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-5 w-5 text-zinc-200" aria-hidden="true" />
</span>
</ListboxButton>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<ListboxOptions
className={cn(
"scrollbar-thin absolute right-0 mt-1 max-h-60 min-w-full overflow-auto rounded-md bg-zinc-700 py-1",
"text-zinc-200 shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm"
)}
>
{Object.entries(objectNames).map(([key, value]) => (
<ListboxOption
key={key}
className={({ focus }) =>
cn(
"relative cursor-default py-2 pr-4 pl-10 select-none",
focus ? "bg-zinc-800 text-zinc-300" : null
)
}
value={key}
>
{({ selected }) => (
<>
<span
className={cn(
"block text-right text-xs whitespace-nowrap md:text-sm lg:text-base",
selected ? "font-medium" : null
)}
>
{value}
</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-blue-500">
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : (
<button
onClick={(e) => {
e.preventDefault();
console.log("TODO: Show Help Explanation");
}}
className="absolute inset-y-0 left-0 flex items-center pl-4 text-lg font-bold opacity-20 hover:animate-pulse"
>
?
</button>
)}
</>
)}
</ListboxOption>
))}
</ListboxOptions>
</Transition>
</div>
</Listbox>
);
return (
<form
className="pb-3"
onSubmit={
onSubmit != undefined
? onPromise(handleSubmit(onSubmit))
: preventDefault
}
>
<div className="col">
<label htmlFor="search" className="sr-only">
Search
</label>
<div className="relative flex">
{searchIcon}
{searchInput}
{dropdown}
</div>
</div>
<div className="col">
<div className="flex flex-wrap pb-1 pt-3 text-sm">
<div className="whitespace-nowrap">
<input
className="ml-2 mr-1 whitespace-nowrap text-zinc-800 accent-blue-700"
type="checkbox"
{...register("requestJSContact")}
/>
<label className="text-zinc-300" htmlFor="requestJSContact">
Request JSContact
</label>
</div>
<div className="whitespace-nowrap">
<input
className="ml-2 mr-1 bg-zinc-500 text-inherit accent-blue-700"
type="checkbox"
{...register("followReferral")}
/>
<label className="text-zinc-300" htmlFor="followReferral">
Follow referral to registrar&apos;s RDAP record
</label>
</div>
</div>
</div>
</form>
);
return (
<form
className="pb-3"
onSubmit={onSubmit != undefined ? onPromise(handleSubmit(onSubmit)) : preventDefault}
>
<div className="col">
<label htmlFor="search" className="sr-only">
Search
</label>
<div className="relative flex">
{searchIcon}
{searchInput}
{dropdown}
</div>
</div>
<div className="col">
<div className="flex flex-wrap pt-3 pb-1 text-sm">
<div className="whitespace-nowrap">
<input
className="mr-1 ml-2 whitespace-nowrap text-zinc-800 accent-blue-700"
type="checkbox"
{...register("requestJSContact")}
/>
<label className="text-zinc-300" htmlFor="requestJSContact">
Request JSContact
</label>
</div>
<div className="whitespace-nowrap">
<input
className="mr-1 ml-2 bg-zinc-500 text-inherit accent-blue-700"
type="checkbox"
{...register("followReferral")}
/>
<label className="text-zinc-300" htmlFor="followReferral">
Follow referral to registrar&apos;s RDAP record
</label>
</div>
</div>
</div>
</form>
);
};
export default LookupInput;

View File

@@ -7,54 +7,51 @@ import PropertyList from "@/components/common/PropertyList";
import AbstractCard from "@/components/common/AbstractCard";
export type AutnumCardProps = {
data: AutonomousNumber;
url?: string;
data: AutonomousNumber;
url?: string;
};
const AutnumCard: FunctionComponent<AutnumCardProps> = ({
data,
url,
}: AutnumCardProps) => {
const asnRange =
data.startAutnum === data.endAutnum
? `AS${data.startAutnum}`
: `AS${data.startAutnum}-AS${data.endAutnum}`;
const AutnumCard: FunctionComponent<AutnumCardProps> = ({ data, url }: AutnumCardProps) => {
const asnRange =
data.startAutnum === data.endAutnum
? `AS${data.startAutnum}`
: `AS${data.startAutnum}-AS${data.endAutnum}`;
return (
<AbstractCard
data={data}
url={url}
header={
<>
<span className="font-mono tracking-tighter">AUTONOMOUS SYSTEM</span>
<span className="font-mono tracking-wide">{asnRange}</span>
<span className="whitespace-nowrap">({data.handle})</span>
</>
}
>
<dl>
<Property title="Name">{data.name}</Property>
<Property title="Handle">{data.handle}</Property>
<Property title="ASN Range">
{data.startAutnum === data.endAutnum
? `AS${data.startAutnum}`
: `AS${data.startAutnum} - AS${data.endAutnum}`}
</Property>
<Property title="Type">{data.type}</Property>
<Property title="Country">{data.country.toUpperCase()}</Property>
<Property title="Events">
<Events key={0} data={data.events} />
</Property>
<PropertyList title="Status">
{data.status.map((status, index) => (
<PropertyList.Item key={index} title={status}>
{status}
</PropertyList.Item>
))}
</PropertyList>
</dl>
</AbstractCard>
);
return (
<AbstractCard
data={data}
url={url}
header={
<>
<span className="font-mono tracking-tighter">AUTONOMOUS SYSTEM</span>
<span className="font-mono tracking-wide">{asnRange}</span>
<span className="whitespace-nowrap">({data.handle})</span>
</>
}
>
<dl>
<Property title="Name">{data.name}</Property>
<Property title="Handle">{data.handle}</Property>
<Property title="ASN Range">
{data.startAutnum === data.endAutnum
? `AS${data.startAutnum}`
: `AS${data.startAutnum} - AS${data.endAutnum}`}
</Property>
<Property title="Type">{data.type}</Property>
<Property title="Country">{data.country.toUpperCase()}</Property>
<Property title="Events">
<Events key={0} data={data.events} />
</Property>
<PropertyList title="Status">
{data.status.map((status, index) => (
<PropertyList.Item key={index} title={status}>
{status}
</PropertyList.Item>
))}
</PropertyList>
</dl>
</AbstractCard>
);
};
export default AutnumCard;

View File

@@ -8,49 +8,46 @@ import PropertyList from "@/components/common/PropertyList";
import AbstractCard from "@/components/common/AbstractCard";
export type DomainProps = {
data: Domain;
url?: string;
data: Domain;
url?: string;
};
const DomainCard: FunctionComponent<DomainProps> = ({
data,
url,
}: DomainProps) => {
return (
<AbstractCard
data={data}
url={url}
header={
<>
<span className="font-mono tracking-tighter">DOMAIN</span>
<span className="font-mono tracking-wide">
{data.ldhName ?? data.unicodeName}
</span>
<span className="whitespace-nowrap">({data.handle})</span>
</>
}
>
<dl>
{data.unicodeName != undefined ? (
<Property title="Unicode Name">{data.unicodeName}</Property>
) : null}
<Property title={data.unicodeName != undefined ? "LHD Name" : "Name"}>
{data.ldhName}
</Property>
<Property title="Handle">{data.handle}</Property>
<Property title="Events">
<Events key={0} data={data.events} />
</Property>
<PropertyList title="Status">
{data.status.map((statusKey, index) => (
<PropertyList.Item key={index} title={rdapStatusInfo[statusKey]}>
{statusKey}
</PropertyList.Item>
))}
</PropertyList>
</dl>
</AbstractCard>
);
const DomainCard: FunctionComponent<DomainProps> = ({ data, url }: DomainProps) => {
return (
<AbstractCard
data={data}
url={url}
header={
<>
<span className="font-mono tracking-tighter">DOMAIN</span>
<span className="font-mono tracking-wide">
{data.ldhName ?? data.unicodeName}
</span>
<span className="whitespace-nowrap">({data.handle})</span>
</>
}
>
<dl>
{data.unicodeName != undefined ? (
<Property title="Unicode Name">{data.unicodeName}</Property>
) : null}
<Property title={data.unicodeName != undefined ? "LHD Name" : "Name"}>
{data.ldhName}
</Property>
<Property title="Handle">{data.handle}</Property>
<Property title="Events">
<Events key={0} data={data.events} />
</Property>
<PropertyList title="Status">
{data.status.map((statusKey, index) => (
<PropertyList.Item key={index} title={rdapStatusInfo[statusKey]}>
{statusKey}
</PropertyList.Item>
))}
</PropertyList>
</dl>
</AbstractCard>
);
};
export default DomainCard;

View File

@@ -4,24 +4,24 @@ import { Fragment } from "react";
import DynamicDate from "@/components/common/DynamicDate";
export type EventsProps = {
data: Event[];
data: Event[];
};
const Events: FunctionComponent<EventsProps> = ({ data }) => {
return (
<dl>
{data.map(({ eventAction, eventDate, eventActor }, index) => {
return (
<Fragment key={index}>
<dt className="font-weight-bolder">{eventAction}:</dt>
<dd>
<DynamicDate value={new Date(eventDate)} />
{eventActor != null ? ` (by ${eventActor})` : null}
</dd>
</Fragment>
);
})}
</dl>
);
return (
<dl>
{data.map(({ eventAction, eventDate, eventActor }, index) => {
return (
<Fragment key={index}>
<dt className="font-weight-bolder">{eventAction}:</dt>
<dd>
<DynamicDate value={new Date(eventDate)} />
{eventActor != null ? ` (by ${eventActor})` : null}
</dd>
</Fragment>
);
})}
</dl>
);
};
export default Events;

View File

@@ -2,53 +2,39 @@ import type { FunctionComponent } from "react";
import DomainCard from "@/components/lookup/DomainCard";
import IPCard from "@/components/lookup/IPCard";
import AutnumCard from "@/components/lookup/AutnumCard";
import type {
Domain,
AutonomousNumber,
Entity,
Nameserver,
IpNetwork,
} from "@/types";
import type { Domain, AutonomousNumber, Entity, Nameserver, IpNetwork } from "@/types";
import AbstractCard from "@/components/common/AbstractCard";
export type ParsedGeneric =
| Domain
| Nameserver
| Entity
| AutonomousNumber
| IpNetwork;
export type ParsedGeneric = Domain | Nameserver | Entity | AutonomousNumber | IpNetwork;
export type ObjectProps = {
data: ParsedGeneric;
url?: string;
data: ParsedGeneric;
url?: string;
};
const Generic: FunctionComponent<ObjectProps> = ({
data,
url,
}: ObjectProps) => {
switch (data.objectClassName) {
case "domain":
return <DomainCard url={url} data={data} />;
case "ip network":
return <IPCard url={url} data={data} />;
case "autnum":
return <AutnumCard url={url} data={data} />;
case "entity":
case "nameserver":
default:
return (
<AbstractCard url={url}>
Not implemented. (<pre>{data.objectClassName ?? "null"}</pre>)
</AbstractCard>
);
}
const Generic: FunctionComponent<ObjectProps> = ({ data, url }: ObjectProps) => {
switch (data.objectClassName) {
case "domain":
return <DomainCard url={url} data={data} />;
case "ip network":
return <IPCard url={url} data={data} />;
case "autnum":
return <AutnumCard url={url} data={data} />;
case "entity":
case "nameserver":
default:
return (
<AbstractCard url={url}>
Not implemented. (<pre>{data.objectClassName ?? "null"}</pre>)
</AbstractCard>
);
}
// const title: string = (data.unicodeName ?? data.ldhName ?? data.handle)?.toUpperCase() ?? "Response";
// return <div className="card">
// <div className="card-header">{title}</div>
// {objectFragment}
// </div>
// const title: string = (data.unicodeName ?? data.ldhName ?? data.handle)?.toUpperCase() ?? "Response";
// return <div className="card">
// <div className="card-header">{title}</div>
// {objectFragment}
// </div>
};
export default Generic;

View File

@@ -7,50 +7,50 @@ import PropertyList from "@/components/common/PropertyList";
import AbstractCard from "@/components/common/AbstractCard";
export type IPCardProps = {
data: IpNetwork;
url?: string;
data: IpNetwork;
url?: string;
};
const IPCard: FunctionComponent<IPCardProps> = ({ data, url }: IPCardProps) => {
return (
<AbstractCard
data={data}
url={url}
header={
<>
<span className="font-mono tracking-tighter">IP NETWORK</span>
<span className="font-mono tracking-wide">
{data.startAddress}
{data.startAddress !== data.endAddress && ` - ${data.endAddress}`}
</span>
<span className="whitespace-nowrap">({data.handle})</span>
</>
}
>
<dl>
<Property title="Name">{data.name}</Property>
<Property title="Handle">{data.handle}</Property>
<Property title="IP Version">{data.ipVersion.toUpperCase()}</Property>
<Property title="Start Address">{data.startAddress}</Property>
<Property title="End Address">{data.endAddress}</Property>
<Property title="Type">{data.type}</Property>
{data.country && <Property title="Country">{data.country}</Property>}
{data.parentHandle && (
<Property title="Parent Handle">{data.parentHandle}</Property>
)}
<Property title="Events">
<Events key={0} data={data.events} />
</Property>
<PropertyList title="Status">
{data.status.map((status, index) => (
<PropertyList.Item key={index} title={status}>
{status}
</PropertyList.Item>
))}
</PropertyList>
</dl>
</AbstractCard>
);
return (
<AbstractCard
data={data}
url={url}
header={
<>
<span className="font-mono tracking-tighter">IP NETWORK</span>
<span className="font-mono tracking-wide">
{data.startAddress}
{data.startAddress !== data.endAddress && ` - ${data.endAddress}`}
</span>
<span className="whitespace-nowrap">({data.handle})</span>
</>
}
>
<dl>
<Property title="Name">{data.name}</Property>
<Property title="Handle">{data.handle}</Property>
<Property title="IP Version">{data.ipVersion.toUpperCase()}</Property>
<Property title="Start Address">{data.startAddress}</Property>
<Property title="End Address">{data.endAddress}</Property>
<Property title="Type">{data.type}</Property>
{data.country && <Property title="Country">{data.country}</Property>}
{data.parentHandle && (
<Property title="Parent Handle">{data.parentHandle}</Property>
)}
<Property title="Events">
<Events key={0} data={data.events} />
</Property>
<PropertyList title="Status">
{data.status.map((status, index) => (
<PropertyList.Item key={index} title={status}>
{status}
</PropertyList.Item>
))}
</PropertyList>
</dl>
</AbstractCard>
);
};
export default IPCard;

View File

@@ -1,96 +1,88 @@
// see https://www.iana.org/assignments/rdap-json-values
import type {
RdapStatusType,
RootRegistryType,
SimplifiedTargetType,
} from "@/types";
import type { RdapStatusType, RootRegistryType, SimplifiedTargetType } from "@/types";
export const rdapStatusInfo: Record<RdapStatusType, string> = {
validated:
"Signifies that the data of the object instance has been found to be accurate. This type of status is usually found on entity object instances to note the validity of identifying contact information.",
"renew prohibited":
"Renewal or reregistration of the object instance is forbidden.",
"update prohibited": "Updates to the object instance are forbidden.",
"transfer prohibited":
"Transfers of the registration from one registrar to another are forbidden. This type of status normally applies to DNR domain names.",
"delete prohibited":
"Deletion of the registration of the object instance is forbidden. This type of status normally applies to DNR domain names.",
proxy:
"The registration of the object instance has been performed by a third party. This is most commonly applied to entities.",
private:
"The information of the object instance is not designated for public consumption. This is most commonly applied to entities.",
removed:
"Some of the information of the object instance has not been made available and has been removed. This is most commonly applied to entities.",
obscured:
"Some of the information of the object instance has been altered for the purposes of not readily revealing the actual information of the object instance. This is most commonly applied to entities.",
associated:
"The object instance is associated with other object instances in the registry. This is most commonly used to signify that a nameserver is associated with a domain or that an entity is associated with a network resource or domain.",
active:
"The object instance is in use. For domain names, it signifies that the domain name is published in DNS. For network and autnum registrations it signifies that they are allocated or assigned for use in operational networks. This maps to the Extensible Provisioning Protocol (EPP) [RFC5730] 'OK' status.",
inactive: "The object instance is not in use. See 'active'.",
locked:
"Changes to the object instance cannot be made, including the association of other object instances.",
"pending create":
"A request has been received for the creation of the object instance but this action is not yet complete.",
"pending renew":
"A request has been received for the renewal of the object instance but this action is not yet complete.",
"pending transfer":
"A request has been received for the transfer of the object instance but this action is not yet complete.",
"pending update":
"A request has been received for the update or modification of the object instance but this action is not yet complete.",
"pending delete":
"A request has been received for the deletion or removal of the object instance but this action is not yet complete. For domains, this might mean that the name is no longer published in DNS but has not yet been purged from the registry database.",
"add period":
"This grace period is provided after the initial registration of the object. If the object is deleted by the client during this period, the server provides a credit to the client for the cost of the registration. This maps to the Domain Registry Grace Period Mapping for the Extensible Provisioning Protocol (EPP) [RFC3915] 'addPeriod' status.",
"auto renew period":
"This grace period is provided after an object registration period expires and is extended (renewed) automatically by the server. If the object is deleted by the client during this period, the server provides a credit to the client for the cost of the auto renewal. This maps to the Domain Registry Grace Period Mapping for the Extensible Provisioning Protocol (EPP) [RFC3915] 'autoRenewPeriod' status.",
"client delete prohibited":
"The client requested that requests to delete the object MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731], Extensible Provisioning Protocol (EPP) Host Mapping [RFC5732], and Extensible Provisioning Protocol (EPP) Contact Mapping [RFC5733] 'clientDeleteProhibited' status.",
"client hold":
"The client requested that the DNS delegation information MUST NOT be published for the object. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731] 'clientHold' status.",
"client renew prohibited":
"The client requested that requests to renew the object MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731] 'clientRenewProhibited' status.",
"client transfer prohibited":
"The client requested that requests to transfer the object MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731] and Extensible Provisioning Protocol (EPP) Contact Mapping [RFC5733] 'clientTransferProhibited' status.",
"client update prohibited":
"The client requested that requests to update the object (other than to remove this status) MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731], Extensible Provisioning Protocol (EPP) Host Mapping [RFC5732], and Extensible Provisioning Protocol (EPP) Contact Mapping [RFC5733] 'clientUpdateProhibited' status.",
"pending restore":
"An object is in the process of being restored after being in the redemption period state. This maps to the Domain Registry Grace Period Mapping for the Extensible Provisioning Protocol (EPP) [RFC3915] 'pendingRestore' status.",
"redemption period":
"A delete has been received, but the object has not yet been purged because an opportunity exists to restore the object and abort the deletion process. This maps to the Domain Registry Grace Period Mapping for the Extensible Provisioning Protocol (EPP) [RFC3915] 'redemptionPeriod' status.",
"renew period":
"This grace period is provided after an object registration period is explicitly extended (renewed) by the 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 renewal. This maps to the Domain Registry Grace Period Mapping for the Extensible Provisioning Protocol (EPP) [RFC3915] 'renewPeriod' status.",
"server delete prohibited":
"The server set the status so that requests to delete the object MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731], Extensible Provisioning Protocol (EPP) Host Mapping [RFC5732], and Extensible Provisioning Protocol (EPP) Contact Mapping [RFC5733] 'serverDeleteProhibited' status.",
"server renew prohibited":
"The server set the status so that requests to renew the object MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731] 'serverRenewProhibited' status.",
"server transfer prohibited":
"The server set the status so that requests to transfer the object MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731] and Extensible Provisioning Protocol (EPP) Contact Mapping [RFC5733] 'serverTransferProhibited' status.",
"server update prohibited":
"The server set the status so that requests to update the object (other than to remove this status) MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731], Extensible Provisioning Protocol (EPP) Host Mapping [RFC5732], and Extensible Provisioning Protocol (EPP) Contact Mapping [RFC5733] 'serverUpdateProhibited' status.",
"server hold":
"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.",
validated:
"Signifies that the data of the object instance has been found to be accurate. This type of status is usually found on entity object instances to note the validity of identifying contact information.",
"renew prohibited": "Renewal or reregistration of the object instance is forbidden.",
"update prohibited": "Updates to the object instance are forbidden.",
"transfer prohibited":
"Transfers of the registration from one registrar to another are forbidden. This type of status normally applies to DNR domain names.",
"delete prohibited":
"Deletion of the registration of the object instance is forbidden. This type of status normally applies to DNR domain names.",
proxy: "The registration of the object instance has been performed by a third party. This is most commonly applied to entities.",
private:
"The information of the object instance is not designated for public consumption. This is most commonly applied to entities.",
removed:
"Some of the information of the object instance has not been made available and has been removed. This is most commonly applied to entities.",
obscured:
"Some of the information of the object instance has been altered for the purposes of not readily revealing the actual information of the object instance. This is most commonly applied to entities.",
associated:
"The object instance is associated with other object instances in the registry. This is most commonly used to signify that a nameserver is associated with a domain or that an entity is associated with a network resource or domain.",
active: "The object instance is in use. For domain names, it signifies that the domain name is published in DNS. For network and autnum registrations it signifies that they are allocated or assigned for use in operational networks. This maps to the Extensible Provisioning Protocol (EPP) [RFC5730] 'OK' status.",
inactive: "The object instance is not in use. See 'active'.",
locked: "Changes to the object instance cannot be made, including the association of other object instances.",
"pending create":
"A request has been received for the creation of the object instance but this action is not yet complete.",
"pending renew":
"A request has been received for the renewal of the object instance but this action is not yet complete.",
"pending transfer":
"A request has been received for the transfer of the object instance but this action is not yet complete.",
"pending update":
"A request has been received for the update or modification of the object instance but this action is not yet complete.",
"pending delete":
"A request has been received for the deletion or removal of the object instance but this action is not yet complete. For domains, this might mean that the name is no longer published in DNS but has not yet been purged from the registry database.",
"add period":
"This grace period is provided after the initial registration of the object. If the object is deleted by the client during this period, the server provides a credit to the client for the cost of the registration. This maps to the Domain Registry Grace Period Mapping for the Extensible Provisioning Protocol (EPP) [RFC3915] 'addPeriod' status.",
"auto renew period":
"This grace period is provided after an object registration period expires and is extended (renewed) automatically by the server. If the object is deleted by the client during this period, the server provides a credit to the client for the cost of the auto renewal. This maps to the Domain Registry Grace Period Mapping for the Extensible Provisioning Protocol (EPP) [RFC3915] 'autoRenewPeriod' status.",
"client delete prohibited":
"The client requested that requests to delete the object MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731], Extensible Provisioning Protocol (EPP) Host Mapping [RFC5732], and Extensible Provisioning Protocol (EPP) Contact Mapping [RFC5733] 'clientDeleteProhibited' status.",
"client hold":
"The client requested that the DNS delegation information MUST NOT be published for the object. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731] 'clientHold' status.",
"client renew prohibited":
"The client requested that requests to renew the object MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731] 'clientRenewProhibited' status.",
"client transfer prohibited":
"The client requested that requests to transfer the object MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731] and Extensible Provisioning Protocol (EPP) Contact Mapping [RFC5733] 'clientTransferProhibited' status.",
"client update prohibited":
"The client requested that requests to update the object (other than to remove this status) MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731], Extensible Provisioning Protocol (EPP) Host Mapping [RFC5732], and Extensible Provisioning Protocol (EPP) Contact Mapping [RFC5733] 'clientUpdateProhibited' status.",
"pending restore":
"An object is in the process of being restored after being in the redemption period state. This maps to the Domain Registry Grace Period Mapping for the Extensible Provisioning Protocol (EPP) [RFC3915] 'pendingRestore' status.",
"redemption period":
"A delete has been received, but the object has not yet been purged because an opportunity exists to restore the object and abort the deletion process. This maps to the Domain Registry Grace Period Mapping for the Extensible Provisioning Protocol (EPP) [RFC3915] 'redemptionPeriod' status.",
"renew period":
"This grace period is provided after an object registration period is explicitly extended (renewed) by the 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 renewal. This maps to the Domain Registry Grace Period Mapping for the Extensible Provisioning Protocol (EPP) [RFC3915] 'renewPeriod' status.",
"server delete prohibited":
"The server set the status so that requests to delete the object MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731], Extensible Provisioning Protocol (EPP) Host Mapping [RFC5732], and Extensible Provisioning Protocol (EPP) Contact Mapping [RFC5733] 'serverDeleteProhibited' status.",
"server renew prohibited":
"The server set the status so that requests to renew the object MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731] 'serverRenewProhibited' status.",
"server transfer prohibited":
"The server set the status so that requests to transfer the object MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731] and Extensible Provisioning Protocol (EPP) Contact Mapping [RFC5733] 'serverTransferProhibited' status.",
"server update prohibited":
"The server set the status so that requests to update the object (other than to remove this status) MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731], Extensible Provisioning Protocol (EPP) Host Mapping [RFC5732], and Extensible Provisioning Protocol (EPP) Contact Mapping [RFC5733] 'serverUpdateProhibited' status.",
"server hold":
"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.",
};
// list of RDAP bootstrap registry URLs
export const registryURLs: Record<RootRegistryType, string> = {
autnum: "https://data.iana.org/rdap/asn.json",
domain: "https://data.iana.org/rdap/dns.json",
ip4: "https://data.iana.org/rdap/ipv4.json",
ip6: "https://data.iana.org/rdap/ipv6.json",
entity: "https://data.iana.org/rdap/object-tags.json",
autnum: "https://data.iana.org/rdap/asn.json",
domain: "https://data.iana.org/rdap/dns.json",
ip4: "https://data.iana.org/rdap/ipv4.json",
ip6: "https://data.iana.org/rdap/ipv6.json",
entity: "https://data.iana.org/rdap/object-tags.json",
};
export const placeholders: Record<SimplifiedTargetType | "auto", string> = {
auto: "A domain, an IP address, a TLD, an RDAP URL...",
ip: "192.168.0.1/16 or 2001:db8::/32",
autnum: "AS27594",
entity: "OPS4-RIPE",
url: "https://rdap.org/domain/example.com",
tld: ".dev",
registrar: "9999",
json: `{"objectClassName":"domain", ... }`,
domain: "example.com",
auto: "A domain, an IP address, a TLD, an RDAP URL...",
ip: "192.168.0.1/16 or 2001:db8::/32",
autnum: "AS27594",
entity: "OPS4-RIPE",
url: "https://rdap.org/domain/example.com",
tld: ".dev",
registrar: "9999",
json: `{"objectClassName":"domain", ... }`,
domain: "example.com",
};

38
src/env/client.mjs vendored
View File

@@ -4,32 +4,32 @@ import { clientEnv, clientSchema } from "./schema.mjs";
const _clientEnv = clientSchema.safeParse(clientEnv);
export const formatErrors = (
/** @type {import('zod').ZodFormattedError<Map<string,string>,string>} */
errors
/** @type {import('zod').ZodFormattedError<Map<string,string>,string>} */
errors
) =>
Object.entries(errors)
.map(([name, value]) => {
if (value && "_errors" in value)
return `${String(name)}: ${value._errors.join(", ")}\n`;
})
.filter(Boolean);
Object.entries(errors)
.map(([name, value]) => {
if (value && "_errors" in value)
return `${String(name)}: ${value._errors.join(", ")}\n`;
})
.filter(Boolean);
if (!_clientEnv.success) {
console.error(
"❌ Invalid environment variables:\n",
...formatErrors(_clientEnv.error.format())
);
throw new Error("Invalid environment variables");
console.error(
"❌ Invalid environment variables:\n",
...formatErrors(_clientEnv.error.format())
);
throw new Error("Invalid environment variables");
}
for (let key of Object.keys(_clientEnv.data)) {
if (!key.startsWith("NEXT_PUBLIC_")) {
console.warn(
`❌ Invalid public environment variable name: ${key}. It must begin with 'NEXT_PUBLIC_'`
);
if (!key.startsWith("NEXT_PUBLIC_")) {
console.warn(
`❌ Invalid public environment variable name: ${key}. It must begin with 'NEXT_PUBLIC_'`
);
throw new Error("Invalid public environment variable name");
}
throw new Error("Invalid public environment variable name");
}
}
export const env = _clientEnv.data;

8
src/env/schema.mjs vendored
View File

@@ -6,7 +6,7 @@ import { z } from "zod";
* This way you can ensure the app isn't built with invalid env vars.
*/
export const serverSchema = z.object({
NODE_ENV: z.enum(["development", "test", "production"]),
NODE_ENV: z.enum(["development", "test", "production"]),
});
/**
@@ -15,7 +15,7 @@ export const serverSchema = z.object({
* @type {{ [k in keyof z.infer<typeof serverSchema>]: z.infer<typeof serverSchema>[k] | undefined }}
*/
export const serverEnv = {
NODE_ENV: process.env.NODE_ENV,
NODE_ENV: process.env.NODE_ENV,
};
/**
@@ -24,7 +24,7 @@ export const serverEnv = {
* To expose them to the client, prefix them with `NEXT_PUBLIC_`.
*/
export const clientSchema = z.object({
// NEXT_PUBLIC_CLIENTVAR: z.string(),
// NEXT_PUBLIC_CLIENTVAR: z.string(),
});
/**
@@ -34,5 +34,5 @@ export const clientSchema = z.object({
* @type {{ [k in keyof z.infer<typeof clientSchema>]: z.infer<typeof clientSchema>[k] | undefined }}
*/
export const clientEnv = {
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
};

18
src/env/server.mjs vendored
View File

@@ -9,19 +9,19 @@ import { env as clientEnv, formatErrors } from "./client.mjs";
const _serverEnv = serverSchema.safeParse(serverEnv);
if (!_serverEnv.success) {
console.error(
"❌ Invalid environment variables:\n",
...formatErrors(_serverEnv.error.format())
);
throw new Error("Invalid environment variables");
console.error(
"❌ Invalid environment variables:\n",
...formatErrors(_serverEnv.error.format())
);
throw new Error("Invalid environment variables");
}
for (let key of Object.keys(_serverEnv.data)) {
if (key.startsWith("NEXT_PUBLIC_")) {
console.warn("❌ You are exposing a server-side env-variable:", key);
if (key.startsWith("NEXT_PUBLIC_")) {
console.warn("❌ You are exposing a server-side env-variable:", key);
throw new Error("You are exposing a server-side env-variable");
}
throw new Error("You are exposing a server-side env-variable");
}
}
export const env = { ..._serverEnv.data, ...clientEnv };

View File

@@ -2,143 +2,143 @@ import { describe, it, expect } from "vitest";
import { asnInRange } from "./helpers";
describe("asnInRange", () => {
describe("basic matching", () => {
it("should match ASN in single number range", () => {
expect(asnInRange(100, "100-200")).toBe(true);
expect(asnInRange(150, "100-200")).toBe(true);
expect(asnInRange(200, "100-200")).toBe(true);
});
describe("basic matching", () => {
it("should match ASN in single number range", () => {
expect(asnInRange(100, "100-200")).toBe(true);
expect(asnInRange(150, "100-200")).toBe(true);
expect(asnInRange(200, "100-200")).toBe(true);
});
it("should not match ASN outside single number range", () => {
expect(asnInRange(99, "100-200")).toBe(false);
expect(asnInRange(201, "100-200")).toBe(false);
});
it("should not match ASN outside single number range", () => {
expect(asnInRange(99, "100-200")).toBe(false);
expect(asnInRange(201, "100-200")).toBe(false);
});
it("should match ASN at boundaries", () => {
expect(asnInRange(1, "1-10")).toBe(true);
expect(asnInRange(10, "1-10")).toBe(true);
});
it("should match ASN at boundaries", () => {
expect(asnInRange(1, "1-10")).toBe(true);
expect(asnInRange(10, "1-10")).toBe(true);
});
it("should match single ASN (same start and end)", () => {
expect(asnInRange(12345, "12345-12345")).toBe(true);
});
it("should match single ASN (same start and end)", () => {
expect(asnInRange(12345, "12345-12345")).toBe(true);
});
it("should not match single ASN outside", () => {
expect(asnInRange(12346, "12345-12345")).toBe(false);
expect(asnInRange(12344, "12345-12345")).toBe(false);
});
});
it("should not match single ASN outside", () => {
expect(asnInRange(12346, "12345-12345")).toBe(false);
expect(asnInRange(12344, "12345-12345")).toBe(false);
});
});
describe("real-world ASN ranges from IANA", () => {
// ARIN ranges
it("should match ARIN ASN ranges", () => {
// ARIN typically has ranges like 1-1876, 1902-2042, etc.
expect(asnInRange(100, "1-1876")).toBe(true);
expect(asnInRange(1876, "1-1876")).toBe(true);
expect(asnInRange(2000, "1902-2042")).toBe(true);
});
describe("real-world ASN ranges from IANA", () => {
// ARIN ranges
it("should match ARIN ASN ranges", () => {
// ARIN typically has ranges like 1-1876, 1902-2042, etc.
expect(asnInRange(100, "1-1876")).toBe(true);
expect(asnInRange(1876, "1-1876")).toBe(true);
expect(asnInRange(2000, "1902-2042")).toBe(true);
});
// RIPE ranges
it("should match RIPE ASN ranges", () => {
// RIPE has ranges like 1877-1901, 2043-2109, etc.
expect(asnInRange(1900, "1877-1901")).toBe(true);
expect(asnInRange(2100, "2043-2109")).toBe(true);
});
// RIPE ranges
it("should match RIPE ASN ranges", () => {
// RIPE has ranges like 1877-1901, 2043-2109, etc.
expect(asnInRange(1900, "1877-1901")).toBe(true);
expect(asnInRange(2100, "2043-2109")).toBe(true);
});
// APNIC ranges
it("should match APNIC ASN ranges", () => {
// APNIC has ranges like 2110-2136, 4608-4864, etc.
expect(asnInRange(2120, "2110-2136")).toBe(true);
expect(asnInRange(4700, "4608-4864")).toBe(true);
});
// APNIC ranges
it("should match APNIC ASN ranges", () => {
// APNIC has ranges like 2110-2136, 4608-4864, etc.
expect(asnInRange(2120, "2110-2136")).toBe(true);
expect(asnInRange(4700, "4608-4864")).toBe(true);
});
// Well-known ASNs
it("should match Google ASN (AS15169)", () => {
// Google's ASN 15169 falls in range that includes it
expect(asnInRange(15169, "15000-16000")).toBe(true);
expect(asnInRange(15169, "15169-15169")).toBe(true);
expect(asnInRange(15169, "15360-16383")).toBe(false); // Not in this range
});
// Well-known ASNs
it("should match Google ASN (AS15169)", () => {
// Google's ASN 15169 falls in range that includes it
expect(asnInRange(15169, "15000-16000")).toBe(true);
expect(asnInRange(15169, "15169-15169")).toBe(true);
expect(asnInRange(15169, "15360-16383")).toBe(false); // Not in this range
});
it("should match Cloudflare ASN (AS13335)", () => {
// Cloudflare's ASN 13335 should be in ARIN range 13312-18431
expect(asnInRange(13335, "13312-18431")).toBe(true);
});
it("should match Cloudflare ASN (AS13335)", () => {
// Cloudflare's ASN 13335 should be in ARIN range 13312-18431
expect(asnInRange(13335, "13312-18431")).toBe(true);
});
it("should match Amazon ASN (AS16509)", () => {
// Amazon's ASN 16509
expect(asnInRange(16509, "15360-16383")).toBe(false);
expect(asnInRange(16509, "16384-18431")).toBe(true);
});
});
it("should match Amazon ASN (AS16509)", () => {
// Amazon's ASN 16509
expect(asnInRange(16509, "15360-16383")).toBe(false);
expect(asnInRange(16509, "16384-18431")).toBe(true);
});
});
describe("private ASN ranges", () => {
it("should match 16-bit private ASN range", () => {
// Private range: 64512-65534
expect(asnInRange(64512, "64512-65534")).toBe(true);
expect(asnInRange(65000, "64512-65534")).toBe(true);
expect(asnInRange(65534, "64512-65534")).toBe(true);
});
describe("private ASN ranges", () => {
it("should match 16-bit private ASN range", () => {
// Private range: 64512-65534
expect(asnInRange(64512, "64512-65534")).toBe(true);
expect(asnInRange(65000, "64512-65534")).toBe(true);
expect(asnInRange(65534, "64512-65534")).toBe(true);
});
it("should not match outside private range", () => {
expect(asnInRange(64511, "64512-65534")).toBe(false);
expect(asnInRange(65535, "64512-65534")).toBe(false);
});
it("should not match outside private range", () => {
expect(asnInRange(64511, "64512-65534")).toBe(false);
expect(asnInRange(65535, "64512-65534")).toBe(false);
});
it("should match 32-bit private ASN range", () => {
// Private range: 4200000000-4294967294
expect(asnInRange(4200000000, "4200000000-4294967294")).toBe(true);
expect(asnInRange(4250000000, "4200000000-4294967294")).toBe(true);
expect(asnInRange(4294967294, "4200000000-4294967294")).toBe(true);
});
});
it("should match 32-bit private ASN range", () => {
// Private range: 4200000000-4294967294
expect(asnInRange(4200000000, "4200000000-4294967294")).toBe(true);
expect(asnInRange(4250000000, "4200000000-4294967294")).toBe(true);
expect(asnInRange(4294967294, "4200000000-4294967294")).toBe(true);
});
});
describe("large ASN numbers (32-bit)", () => {
it("should handle large ASN numbers", () => {
expect(asnInRange(4200000000, "4200000000-4294967294")).toBe(true);
expect(asnInRange(4294967295, "4200000000-4294967294")).toBe(false);
});
describe("large ASN numbers (32-bit)", () => {
it("should handle large ASN numbers", () => {
expect(asnInRange(4200000000, "4200000000-4294967294")).toBe(true);
expect(asnInRange(4294967295, "4200000000-4294967294")).toBe(false);
});
it("should handle ASNs near 32-bit limit", () => {
const maxAsn = 4294967295;
expect(asnInRange(maxAsn, `${maxAsn}-${maxAsn}`)).toBe(true);
expect(asnInRange(maxAsn - 1, `${maxAsn}-${maxAsn}`)).toBe(false);
});
});
it("should handle ASNs near 32-bit limit", () => {
const maxAsn = 4294967295;
expect(asnInRange(maxAsn, `${maxAsn}-${maxAsn}`)).toBe(true);
expect(asnInRange(maxAsn - 1, `${maxAsn}-${maxAsn}`)).toBe(false);
});
});
describe("edge cases", () => {
it("should handle invalid range format", () => {
expect(asnInRange(100, "invalid")).toBe(false);
expect(asnInRange(100, "100")).toBe(false);
expect(asnInRange(100, "100-")).toBe(false);
expect(asnInRange(100, "-100")).toBe(false);
});
describe("edge cases", () => {
it("should handle invalid range format", () => {
expect(asnInRange(100, "invalid")).toBe(false);
expect(asnInRange(100, "100")).toBe(false);
expect(asnInRange(100, "100-")).toBe(false);
expect(asnInRange(100, "-100")).toBe(false);
});
it("should handle negative numbers gracefully", () => {
expect(asnInRange(-1, "1-100")).toBe(false);
expect(asnInRange(50, "-100-100")).toBe(false);
});
it("should handle negative numbers gracefully", () => {
expect(asnInRange(-1, "1-100")).toBe(false);
expect(asnInRange(50, "-100-100")).toBe(false);
});
it("should handle reversed ranges (end < start)", () => {
// Invalid range where end is less than start
expect(asnInRange(150, "200-100")).toBe(false);
});
it("should handle reversed ranges (end < start)", () => {
// Invalid range where end is less than start
expect(asnInRange(150, "200-100")).toBe(false);
});
it("should handle zero", () => {
expect(asnInRange(0, "0-100")).toBe(true);
expect(asnInRange(0, "1-100")).toBe(false);
});
});
it("should handle zero", () => {
expect(asnInRange(0, "0-100")).toBe(true);
expect(asnInRange(0, "1-100")).toBe(false);
});
});
describe("ASN number parsing", () => {
it("should handle number inputs", () => {
expect(asnInRange(12345, "10000-20000")).toBe(true);
});
describe("ASN number parsing", () => {
it("should handle number inputs", () => {
expect(asnInRange(12345, "10000-20000")).toBe(true);
});
it("should handle very large numbers", () => {
const largeAsn = 4000000000;
expect(asnInRange(largeAsn, "3000000000-4294967295")).toBe(true);
expect(asnInRange(largeAsn, "1-1000000000")).toBe(false);
});
});
it("should handle very large numbers", () => {
const largeAsn = 4000000000;
expect(asnInRange(largeAsn, "3000000000-4294967295")).toBe(true);
expect(asnInRange(largeAsn, "1-1000000000")).toBe(false);
});
});
});

View File

@@ -2,178 +2,169 @@ import { describe, it, expect } from "vitest";
import { ipv4InCIDR, ipv6InCIDR } from "./helpers";
describe("ipv4InCIDR", () => {
describe("basic matching", () => {
it("should match IP in /8 network", () => {
expect(ipv4InCIDR("8.8.8.8", "8.0.0.0/8")).toBe(true);
expect(ipv4InCIDR("8.255.255.255", "8.0.0.0/8")).toBe(true);
expect(ipv4InCIDR("8.0.0.0", "8.0.0.0/8")).toBe(true);
});
describe("basic matching", () => {
it("should match IP in /8 network", () => {
expect(ipv4InCIDR("8.8.8.8", "8.0.0.0/8")).toBe(true);
expect(ipv4InCIDR("8.255.255.255", "8.0.0.0/8")).toBe(true);
expect(ipv4InCIDR("8.0.0.0", "8.0.0.0/8")).toBe(true);
});
it("should not match IP outside /8 network", () => {
expect(ipv4InCIDR("9.0.0.0", "8.0.0.0/8")).toBe(false);
expect(ipv4InCIDR("7.255.255.255", "8.0.0.0/8")).toBe(false);
});
it("should not match IP outside /8 network", () => {
expect(ipv4InCIDR("9.0.0.0", "8.0.0.0/8")).toBe(false);
expect(ipv4InCIDR("7.255.255.255", "8.0.0.0/8")).toBe(false);
});
it("should match IP in /16 network", () => {
expect(ipv4InCIDR("192.168.1.1", "192.168.0.0/16")).toBe(true);
expect(ipv4InCIDR("192.168.255.255", "192.168.0.0/16")).toBe(true);
expect(ipv4InCIDR("192.168.0.0", "192.168.0.0/16")).toBe(true);
});
it("should match IP in /16 network", () => {
expect(ipv4InCIDR("192.168.1.1", "192.168.0.0/16")).toBe(true);
expect(ipv4InCIDR("192.168.255.255", "192.168.0.0/16")).toBe(true);
expect(ipv4InCIDR("192.168.0.0", "192.168.0.0/16")).toBe(true);
});
it("should not match IP outside /16 network", () => {
expect(ipv4InCIDR("192.169.1.1", "192.168.0.0/16")).toBe(false);
expect(ipv4InCIDR("192.167.1.1", "192.168.0.0/16")).toBe(false);
});
it("should not match IP outside /16 network", () => {
expect(ipv4InCIDR("192.169.1.1", "192.168.0.0/16")).toBe(false);
expect(ipv4InCIDR("192.167.1.1", "192.168.0.0/16")).toBe(false);
});
it("should match IP in /24 network", () => {
expect(ipv4InCIDR("192.168.1.1", "192.168.1.0/24")).toBe(true);
expect(ipv4InCIDR("192.168.1.255", "192.168.1.0/24")).toBe(true);
expect(ipv4InCIDR("192.168.1.0", "192.168.1.0/24")).toBe(true);
});
it("should match IP in /24 network", () => {
expect(ipv4InCIDR("192.168.1.1", "192.168.1.0/24")).toBe(true);
expect(ipv4InCIDR("192.168.1.255", "192.168.1.0/24")).toBe(true);
expect(ipv4InCIDR("192.168.1.0", "192.168.1.0/24")).toBe(true);
});
it("should not match IP outside /24 network", () => {
expect(ipv4InCIDR("192.168.2.1", "192.168.1.0/24")).toBe(false);
expect(ipv4InCIDR("192.168.0.255", "192.168.1.0/24")).toBe(false);
});
it("should not match IP outside /24 network", () => {
expect(ipv4InCIDR("192.168.2.1", "192.168.1.0/24")).toBe(false);
expect(ipv4InCIDR("192.168.0.255", "192.168.1.0/24")).toBe(false);
});
it("should match IP in /32 network (single host)", () => {
expect(ipv4InCIDR("192.168.1.1", "192.168.1.1/32")).toBe(true);
});
it("should match IP in /32 network (single host)", () => {
expect(ipv4InCIDR("192.168.1.1", "192.168.1.1/32")).toBe(true);
});
it("should not match different IP in /32 network", () => {
expect(ipv4InCIDR("192.168.1.2", "192.168.1.1/32")).toBe(false);
});
});
it("should not match different IP in /32 network", () => {
expect(ipv4InCIDR("192.168.1.2", "192.168.1.1/32")).toBe(false);
});
});
describe("real-world RDAP bootstrap ranges", () => {
// ARIN ranges (from IANA bootstrap data)
it("should match Google DNS (8.8.8.8) in ARIN range", () => {
expect(ipv4InCIDR("8.8.8.8", "8.0.0.0/8")).toBe(true);
});
describe("real-world RDAP bootstrap ranges", () => {
// ARIN ranges (from IANA bootstrap data)
it("should match Google DNS (8.8.8.8) in ARIN range", () => {
expect(ipv4InCIDR("8.8.8.8", "8.0.0.0/8")).toBe(true);
});
// APNIC ranges
it("should match Cloudflare DNS (1.1.1.1) in APNIC range", () => {
expect(ipv4InCIDR("1.1.1.1", "1.0.0.0/8")).toBe(true);
});
// APNIC ranges
it("should match Cloudflare DNS (1.1.1.1) in APNIC range", () => {
expect(ipv4InCIDR("1.1.1.1", "1.0.0.0/8")).toBe(true);
});
// Private ranges
it("should match private IPs in their ranges", () => {
expect(ipv4InCIDR("10.0.0.1", "10.0.0.0/8")).toBe(true);
expect(ipv4InCIDR("172.16.0.1", "172.16.0.0/12")).toBe(true);
expect(ipv4InCIDR("192.168.0.1", "192.168.0.0/16")).toBe(true);
});
});
// Private ranges
it("should match private IPs in their ranges", () => {
expect(ipv4InCIDR("10.0.0.1", "10.0.0.0/8")).toBe(true);
expect(ipv4InCIDR("172.16.0.1", "172.16.0.0/12")).toBe(true);
expect(ipv4InCIDR("192.168.0.1", "192.168.0.0/16")).toBe(true);
});
});
describe("edge cases", () => {
it("should handle /0 (all IPs)", () => {
expect(ipv4InCIDR("0.0.0.0", "0.0.0.0/0")).toBe(true);
expect(ipv4InCIDR("255.255.255.255", "0.0.0.0/0")).toBe(true);
expect(ipv4InCIDR("192.168.1.1", "0.0.0.0/0")).toBe(true);
});
describe("edge cases", () => {
it("should handle /0 (all IPs)", () => {
expect(ipv4InCIDR("0.0.0.0", "0.0.0.0/0")).toBe(true);
expect(ipv4InCIDR("255.255.255.255", "0.0.0.0/0")).toBe(true);
expect(ipv4InCIDR("192.168.1.1", "0.0.0.0/0")).toBe(true);
});
it("should handle invalid CIDR notation", () => {
expect(ipv4InCIDR("192.168.1.1", "invalid")).toBe(false);
expect(ipv4InCIDR("192.168.1.1", "192.168.1.0/-1")).toBe(false);
expect(ipv4InCIDR("192.168.1.1", "192.168.1.0/33")).toBe(false);
});
it("should handle invalid CIDR notation", () => {
expect(ipv4InCIDR("192.168.1.1", "invalid")).toBe(false);
expect(ipv4InCIDR("192.168.1.1", "192.168.1.0/-1")).toBe(false);
expect(ipv4InCIDR("192.168.1.1", "192.168.1.0/33")).toBe(false);
});
it("should handle malformed IPs", () => {
expect(ipv4InCIDR("invalid", "192.168.1.0/24")).toBe(false);
});
it("should handle malformed IPs", () => {
expect(ipv4InCIDR("invalid", "192.168.1.0/24")).toBe(false);
});
it("should handle partial IPs (wrong number of octets)", () => {
expect(ipv4InCIDR("8.8", "8.0.0.0/8")).toBe(false);
expect(ipv4InCIDR("192.168.1", "192.168.1.0/24")).toBe(false);
expect(ipv4InCIDR("192.168.1.1.1", "192.168.1.0/24")).toBe(false);
});
});
it("should handle partial IPs (wrong number of octets)", () => {
expect(ipv4InCIDR("8.8", "8.0.0.0/8")).toBe(false);
expect(ipv4InCIDR("192.168.1", "192.168.1.0/24")).toBe(false);
expect(ipv4InCIDR("192.168.1.1.1", "192.168.1.0/24")).toBe(false);
});
});
});
describe("ipv6InCIDR", () => {
describe("basic matching", () => {
it("should match IPv6 in /32 network", () => {
expect(ipv6InCIDR("2001:db8::", "2001:db8::/32")).toBe(true);
expect(ipv6InCIDR("2001:db8:1234::", "2001:db8::/32")).toBe(true);
expect(
ipv6InCIDR("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff", "2001:db8::/32")
).toBe(true);
});
describe("basic matching", () => {
it("should match IPv6 in /32 network", () => {
expect(ipv6InCIDR("2001:db8::", "2001:db8::/32")).toBe(true);
expect(ipv6InCIDR("2001:db8:1234::", "2001:db8::/32")).toBe(true);
expect(ipv6InCIDR("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff", "2001:db8::/32")).toBe(
true
);
});
it("should not match IPv6 outside /32 network", () => {
expect(ipv6InCIDR("2001:db9::", "2001:db8::/32")).toBe(false);
expect(ipv6InCIDR("2001:db7::", "2001:db8::/32")).toBe(false);
});
it("should not match IPv6 outside /32 network", () => {
expect(ipv6InCIDR("2001:db9::", "2001:db8::/32")).toBe(false);
expect(ipv6InCIDR("2001:db7::", "2001:db8::/32")).toBe(false);
});
it("should match IPv6 in /64 network", () => {
expect(
ipv6InCIDR("2001:db8:1234:5678::", "2001:db8:1234:5678::/64")
).toBe(true);
expect(
ipv6InCIDR("2001:db8:1234:5678:abcd::", "2001:db8:1234:5678::/64")
).toBe(true);
expect(
ipv6InCIDR(
"2001:db8:1234:5678:ffff:ffff:ffff:ffff",
"2001:db8:1234:5678::/64"
)
).toBe(true);
});
it("should match IPv6 in /64 network", () => {
expect(ipv6InCIDR("2001:db8:1234:5678::", "2001:db8:1234:5678::/64")).toBe(true);
expect(ipv6InCIDR("2001:db8:1234:5678:abcd::", "2001:db8:1234:5678::/64")).toBe(true);
expect(
ipv6InCIDR("2001:db8:1234:5678:ffff:ffff:ffff:ffff", "2001:db8:1234:5678::/64")
).toBe(true);
});
it("should not match IPv6 outside /64 network", () => {
expect(
ipv6InCIDR("2001:db8:1234:5679::", "2001:db8:1234:5678::/64")
).toBe(false);
});
it("should not match IPv6 outside /64 network", () => {
expect(ipv6InCIDR("2001:db8:1234:5679::", "2001:db8:1234:5678::/64")).toBe(false);
});
it("should match IPv6 in /128 network (single host)", () => {
expect(ipv6InCIDR("2001:db8::1", "2001:db8::1/128")).toBe(true);
});
it("should match IPv6 in /128 network (single host)", () => {
expect(ipv6InCIDR("2001:db8::1", "2001:db8::1/128")).toBe(true);
});
it("should not match different IPv6 in /128 network", () => {
expect(ipv6InCIDR("2001:db8::2", "2001:db8::1/128")).toBe(false);
});
});
it("should not match different IPv6 in /128 network", () => {
expect(ipv6InCIDR("2001:db8::2", "2001:db8::1/128")).toBe(false);
});
});
describe("real-world RDAP bootstrap ranges", () => {
it("should match Google IPv6 DNS in ARIN range", () => {
// Google DNS: 2001:4860:4860::8888
expect(ipv6InCIDR("2001:4860:4860::8888", "2001:4860::/32")).toBe(true);
});
describe("real-world RDAP bootstrap ranges", () => {
it("should match Google IPv6 DNS in ARIN range", () => {
// Google DNS: 2001:4860:4860::8888
expect(ipv6InCIDR("2001:4860:4860::8888", "2001:4860::/32")).toBe(true);
});
it("should match Cloudflare IPv6 DNS in APNIC range", () => {
// Cloudflare DNS: 2606:4700:4700::1111
expect(ipv6InCIDR("2606:4700:4700::1111", "2606:4700::/32")).toBe(true);
});
});
it("should match Cloudflare IPv6 DNS in APNIC range", () => {
// Cloudflare DNS: 2606:4700:4700::1111
expect(ipv6InCIDR("2606:4700:4700::1111", "2606:4700::/32")).toBe(true);
});
});
describe("IPv6 shorthand notation", () => {
it("should handle :: notation correctly", () => {
expect(ipv6InCIDR("2001:db8::1", "2001:db8::/32")).toBe(true);
expect(ipv6InCIDR("::1", "::1/128")).toBe(true);
expect(ipv6InCIDR("::", "::/128")).toBe(true);
});
describe("IPv6 shorthand notation", () => {
it("should handle :: notation correctly", () => {
expect(ipv6InCIDR("2001:db8::1", "2001:db8::/32")).toBe(true);
expect(ipv6InCIDR("::1", "::1/128")).toBe(true);
expect(ipv6InCIDR("::", "::/128")).toBe(true);
});
it("should handle expanded vs compressed notation", () => {
expect(
ipv6InCIDR("2001:0db8:0000:0000:0000:0000:0000:0001", "2001:db8::/32")
).toBe(true);
expect(
ipv6InCIDR("2001:db8::1", "2001:0db8:0000:0000:0000:0000:0000:0000/32")
).toBe(true);
});
});
it("should handle expanded vs compressed notation", () => {
expect(ipv6InCIDR("2001:0db8:0000:0000:0000:0000:0000:0001", "2001:db8::/32")).toBe(
true
);
expect(ipv6InCIDR("2001:db8::1", "2001:0db8:0000:0000:0000:0000:0000:0000/32")).toBe(
true
);
});
});
describe("edge cases", () => {
it("should handle invalid CIDR notation", () => {
expect(ipv6InCIDR("2001:db8::1", "invalid")).toBe(false);
expect(ipv6InCIDR("2001:db8::1", "2001:db8::/-1")).toBe(false);
expect(ipv6InCIDR("2001:db8::1", "2001:db8::/129")).toBe(false);
});
describe("edge cases", () => {
it("should handle invalid CIDR notation", () => {
expect(ipv6InCIDR("2001:db8::1", "invalid")).toBe(false);
expect(ipv6InCIDR("2001:db8::1", "2001:db8::/-1")).toBe(false);
expect(ipv6InCIDR("2001:db8::1", "2001:db8::/129")).toBe(false);
});
it("should handle malformed IPv6", () => {
expect(ipv6InCIDR("invalid", "2001:db8::/32")).toBe(false);
expect(ipv6InCIDR("zzzz::1", "2001:db8::/32")).toBe(false);
expect(ipv6InCIDR("2001:xyz::1", "2001:db8::/32")).toBe(false);
});
});
it("should handle malformed IPv6", () => {
expect(ipv6InCIDR("invalid", "2001:db8::/32")).toBe(false);
expect(ipv6InCIDR("zzzz::1", "2001:db8::/32")).toBe(false);
expect(ipv6InCIDR("2001:xyz::1", "2001:db8::/32")).toBe(false);
});
});
});

View File

@@ -2,24 +2,24 @@ import type { SyntheticEvent } from "react";
import type { Entries } from "type-fest";
declare global {
interface ObjectConstructor {
entries<T extends object>(o: T): Entries<T>;
}
interface ObjectConstructor {
entries<T extends object>(o: T): Entries<T>;
}
}
export function truthy(value: string | null | undefined) {
if (value == undefined) return false;
return value.toLowerCase() == "true" || value == "1";
if (value == undefined) return false;
return value.toLowerCase() == "true" || value == "1";
}
export function onPromise<T>(promise: (event: SyntheticEvent) => Promise<T>) {
return (event: SyntheticEvent) => {
if (promise) {
promise(event).catch((error) => {
console.log("Unexpected error", error);
});
}
};
return (event: SyntheticEvent) => {
if (promise) {
promise(event).catch((error) => {
console.log("Unexpected error", error);
});
}
};
}
/**
@@ -30,27 +30,24 @@ export function onPromise<T>(promise: (event: SyntheticEvent) => Promise<T>) {
* @param ellipsis A string representing what should be placed on the end when the max length is hit.
*/
export function truncated(input: string, maxLength: number, ellipsis = "...") {
if (maxLength <= 0) return "";
if (input.length <= maxLength) return input;
return (
input.substring(0, Math.max(0, maxLength - ellipsis.length)) + ellipsis
);
if (maxLength <= 0) return "";
if (input.length <= maxLength) return input;
return input.substring(0, Math.max(0, maxLength - ellipsis.length)) + ellipsis;
}
export function preventDefault(event: SyntheticEvent | Event) {
event.preventDefault();
event.preventDefault();
}
/**
* Convert an IPv4 address string to a 32-bit integer
*/
function ipv4ToInt(ip: string): number {
const parts = ip.split(".").map(Number);
if (parts.length !== 4) return 0;
const [a, b, c, d] = parts;
if (a === undefined || b === undefined || c === undefined || d === undefined)
return 0;
return ((a << 24) | (b << 16) | (c << 8) | d) >>> 0;
const parts = ip.split(".").map(Number);
if (parts.length !== 4) return 0;
const [a, b, c, d] = parts;
if (a === undefined || b === undefined || c === undefined || d === undefined) return 0;
return ((a << 24) | (b << 16) | (c << 8) | d) >>> 0;
}
/**
@@ -60,57 +57,57 @@ function ipv4ToInt(ip: string): number {
* @returns true if the IP is within the CIDR range
*/
export function ipv4InCIDR(ip: string, cidr: string): boolean {
const [rangeIp, prefixLenStr] = cidr.split("/");
const prefixLen = parseInt(prefixLenStr ?? "", 10);
const [rangeIp, prefixLenStr] = cidr.split("/");
const prefixLen = parseInt(prefixLenStr ?? "", 10);
if (!rangeIp || isNaN(prefixLen) || prefixLen < 0 || prefixLen > 32) {
return false;
}
if (!rangeIp || isNaN(prefixLen) || prefixLen < 0 || prefixLen > 32) {
return false;
}
// Special case: /0 matches all IPs
if (prefixLen === 0) {
return true;
}
// Special case: /0 matches all IPs
if (prefixLen === 0) {
return true;
}
const ipInt = ipv4ToInt(ip);
const rangeInt = ipv4ToInt(rangeIp);
const mask = (0xffffffff << (32 - prefixLen)) >>> 0;
const ipInt = ipv4ToInt(ip);
const rangeInt = ipv4ToInt(rangeIp);
const mask = (0xffffffff << (32 - prefixLen)) >>> 0;
return (ipInt & mask) === (rangeInt & mask);
return (ipInt & mask) === (rangeInt & mask);
}
/**
* Convert an IPv6 address to a BigInt representation
*/
function ipv6ToBigInt(ip: string): bigint {
// Expand :: notation
const expandedIp = expandIPv6(ip);
const parts = expandedIp.split(":");
// Expand :: notation
const expandedIp = expandIPv6(ip);
const parts = expandedIp.split(":");
let result = BigInt(0);
for (const part of parts) {
result = (result << BigInt(16)) | BigInt(parseInt(part, 16));
}
return result;
let result = BigInt(0);
for (const part of parts) {
result = (result << BigInt(16)) | BigInt(parseInt(part, 16));
}
return result;
}
/**
* Expand IPv6 address shorthand notation
*/
function expandIPv6(ip: string): string {
if (ip.includes("::")) {
const [left, right] = ip.split("::");
const leftParts = left ? left.split(":") : [];
const rightParts = right ? right.split(":") : [];
const missingParts = 8 - leftParts.length - rightParts.length;
const middleParts: string[] = Array(missingParts).fill("0") as string[];
const allParts = [...leftParts, ...middleParts, ...rightParts];
return allParts.map((p: string) => p.padStart(4, "0")).join(":");
}
return ip
.split(":")
.map((p: string) => p.padStart(4, "0"))
.join(":");
if (ip.includes("::")) {
const [left, right] = ip.split("::");
const leftParts = left ? left.split(":") : [];
const rightParts = right ? right.split(":") : [];
const missingParts = 8 - leftParts.length - rightParts.length;
const middleParts: string[] = Array(missingParts).fill("0") as string[];
const allParts = [...leftParts, ...middleParts, ...rightParts];
return allParts.map((p: string) => p.padStart(4, "0")).join(":");
}
return ip
.split(":")
.map((p: string) => p.padStart(4, "0"))
.join(":");
}
/**
@@ -120,23 +117,23 @@ function expandIPv6(ip: string): string {
* @returns true if the IP is within the CIDR range
*/
export function ipv6InCIDR(ip: string, cidr: string): boolean {
const [rangeIp, prefixLenStr] = cidr.split("/");
const prefixLen = parseInt(prefixLenStr ?? "", 10);
const [rangeIp, prefixLenStr] = cidr.split("/");
const prefixLen = parseInt(prefixLenStr ?? "", 10);
if (!rangeIp || isNaN(prefixLen) || prefixLen < 0 || prefixLen > 128) {
return false;
}
if (!rangeIp || isNaN(prefixLen) || prefixLen < 0 || prefixLen > 128) {
return false;
}
try {
const ipInt = ipv6ToBigInt(ip);
const rangeInt = ipv6ToBigInt(rangeIp);
const maxMask = BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
const mask = (maxMask << BigInt(128 - prefixLen)) & maxMask;
try {
const ipInt = ipv6ToBigInt(ip);
const rangeInt = ipv6ToBigInt(rangeIp);
const maxMask = BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
const mask = (maxMask << BigInt(128 - prefixLen)) & maxMask;
return (ipInt & mask) === (rangeInt & mask);
} catch {
return false;
}
return (ipInt & mask) === (rangeInt & mask);
} catch {
return false;
}
}
/**
@@ -146,22 +143,22 @@ export function ipv6InCIDR(ip: string, cidr: string): boolean {
* @returns true if the ASN is within the range
*/
export function asnInRange(asn: number, range: string): boolean {
const parts = range.split("-");
const parts = range.split("-");
if (parts.length !== 2) {
return false;
}
if (parts.length !== 2) {
return false;
}
const start = parseInt(parts[0] ?? "", 10);
const end = parseInt(parts[1] ?? "", 10);
const start = parseInt(parts[0] ?? "", 10);
const end = parseInt(parts[1] ?? "", 10);
if (isNaN(start) || isNaN(end) || start < 0 || end < 0 || start > end) {
return false;
}
if (isNaN(start) || isNaN(end) || start < 0 || end < 0 || start > end) {
return false;
}
if (asn < 0) {
return false;
}
if (asn < 0) {
return false;
}
return asn >= start && asn <= end;
return asn >= start && asn <= end;
}

View File

@@ -1,21 +1,21 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { domainMatchPredicate, getBestURL, getType } from "@/rdap";
import type {
AutonomousNumber,
Domain,
IpNetwork,
Register,
RootRegistryType,
SubmitProps,
TargetType,
AutonomousNumber,
Domain,
IpNetwork,
Register,
RootRegistryType,
SubmitProps,
TargetType,
} from "@/types";
import { registryURLs } from "@/constants";
import {
AutonomousNumberSchema,
DomainSchema,
IpNetworkSchema,
RegisterSchema,
RootRegistryEnum,
AutonomousNumberSchema,
DomainSchema,
IpNetworkSchema,
RegisterSchema,
RootRegistryEnum,
} from "@/schema";
import { truncated, ipv4InCIDR, ipv6InCIDR, asnInRange } from "@/helpers";
import type { ZodSchema } from "zod";
@@ -24,413 +24,383 @@ import { Maybe, Result } from "true-myth";
export type WarningHandler = (warning: { message: string }) => void;
export type MetaParsedGeneric = {
data: ParsedGeneric;
url: string;
completeTime: Date;
data: ParsedGeneric;
url: string;
completeTime: Date;
};
// An array of schemas to try and parse unknown JSON data with.
const schemas = [DomainSchema, AutonomousNumberSchema, IpNetworkSchema];
const useLookup = (warningHandler?: WarningHandler) => {
/**
* A reference to the registry data, which is used to cache the registry data in memory.
* This uses TargetType as the key, meaning v4/v6 IP/CIDR lookups are differentiated.
*/
const registryDataRef = useRef<Record<RootRegistryType, Register | null>>(
{} as Record<RootRegistryType, Register>
);
/**
* A reference to the registry data, which is used to cache the registry data in memory.
* This uses TargetType as the key, meaning v4/v6 IP/CIDR lookups are differentiated.
*/
const registryDataRef = useRef<Record<RootRegistryType, Register | null>>(
{} as Record<RootRegistryType, Register>
);
const [error, setError] = useState<string | null>(null);
const [target, setTarget] = useState<string>("");
const [uriType, setUriType] = useState<Maybe<TargetType>>(Maybe.nothing());
const [error, setError] = useState<string | null>(null);
const [target, setTarget] = useState<string>("");
const [uriType, setUriType] = useState<Maybe<TargetType>>(Maybe.nothing());
// Used by a callback on LookupInput to forcibly set the type of the lookup.
const [currentType, setTargetType] = useState<TargetType | null>(null);
// Used by a callback on LookupInput to forcibly set the type of the lookup.
const [currentType, setTargetType] = useState<TargetType | null>(null);
// Used to allow repeatable lookups when weird errors happen.
const repeatableRef = useRef<string>("");
// Used to allow repeatable lookups when weird errors happen.
const repeatableRef = useRef<string>("");
useCallback(async () => {
if (currentType != null) return Maybe.just(currentType);
const uri: Maybe<TargetType> = (await getTypeEasy(target)).mapOr(
Maybe.nothing(),
(type) => Maybe.just(type)
);
setUriType(uri);
}, [target, currentType, getTypeEasy]);
useCallback(async () => {
if (currentType != null) return Maybe.just(currentType);
const uri: Maybe<TargetType> = (await getTypeEasy(target)).mapOr(Maybe.nothing(), (type) =>
Maybe.just(type)
);
setUriType(uri);
}, [target, currentType, getTypeEasy]);
// Fetch & load a specific registry's data into memory.
async function loadBootstrap(type: RootRegistryType, force = false) {
// Early preload exit condition
if (registryDataRef.current[type] != null && !force) return;
// Fetch & load a specific registry's data into memory.
async function loadBootstrap(type: RootRegistryType, force = false) {
// Early preload exit condition
if (registryDataRef.current[type] != null && !force) return;
// Fetch the bootstrapping file from the registry
const response = await fetch(registryURLs[type]);
if (response.status != 200)
throw new Error(`Error: ${response.statusText}`);
// Fetch the bootstrapping file from the registry
const response = await fetch(registryURLs[type]);
if (response.status != 200) throw new Error(`Error: ${response.statusText}`);
// Parse it, so we don't make any false assumptions during development & while maintaining the tool.
const parsedRegister = RegisterSchema.safeParse(await response.json());
if (!parsedRegister.success)
throw new Error(
`Could not parse IANA bootstrap response (type: ${type}).`
);
// Parse it, so we don't make any false assumptions during development & while maintaining the tool.
const parsedRegister = RegisterSchema.safeParse(await response.json());
if (!parsedRegister.success)
throw new Error(`Could not parse IANA bootstrap response (type: ${type}).`);
// Set it in state so we can use it.
registryDataRef.current = {
...registryDataRef.current,
[type]: parsedRegister.data,
};
}
// Set it in state so we can use it.
registryDataRef.current = {
...registryDataRef.current,
[type]: parsedRegister.data,
};
}
async function getRegistry(type: RootRegistryType): Promise<Register> {
if (registryDataRef.current[type] == null) await loadBootstrap(type);
const registry = registryDataRef.current[type];
if (registry == null)
throw new Error(`Could not load bootstrap data for ${type} registry.`);
return registry;
}
async function getRegistry(type: RootRegistryType): Promise<Register> {
if (registryDataRef.current[type] == null) await loadBootstrap(type);
const registry = registryDataRef.current[type];
if (registry == null)
throw new Error(`Could not load bootstrap data for ${type} registry.`);
return registry;
}
async function getTypeEasy(
target: string
): Promise<Result<TargetType, Error>> {
return getType(target, getRegistry);
}
async function getTypeEasy(target: string): Promise<Result<TargetType, Error>> {
return getType(target, getRegistry);
}
function getRegistryURL(
type: RootRegistryType,
lookupTarget: string
): string {
const bootstrap = registryDataRef.current[type];
if (bootstrap == null)
throw new Error(
`Cannot acquire RDAP URL without bootstrap data for ${type} lookup.`
);
function getRegistryURL(type: RootRegistryType, lookupTarget: string): string {
const bootstrap = registryDataRef.current[type];
if (bootstrap == null)
throw new Error(`Cannot acquire RDAP URL without bootstrap data for ${type} lookup.`);
let url: string | null = null;
let url: string | null = null;
typeSwitch: switch (type) {
case "domain":
for (const bootstrapItem of bootstrap.services) {
if (bootstrapItem[0].some(domainMatchPredicate(lookupTarget))) {
// min length of 1 is validated in zod schema
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
break typeSwitch;
}
}
throw new Error(`No matching domain found.`);
case "ip4": {
// Extract the IP address without CIDR suffix for matching
const ipAddress = lookupTarget.split("/")[0] ?? lookupTarget;
for (const bootstrapItem of bootstrap.services) {
// bootstrapItem[0] contains CIDR ranges like ["1.0.0.0/8", "2.0.0.0/8"]
if (bootstrapItem[0].some((cidr) => ipv4InCIDR(ipAddress, cidr))) {
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
break typeSwitch;
}
}
throw new Error(`No matching IPv4 registry found for ${lookupTarget}.`);
}
case "ip6": {
// Extract the IP address without CIDR suffix for matching
const ipAddress = lookupTarget.split("/")[0] ?? lookupTarget;
for (const bootstrapItem of bootstrap.services) {
// bootstrapItem[0] contains CIDR ranges like ["2001:0200::/23", "2001:0400::/23"]
if (bootstrapItem[0].some((cidr) => ipv6InCIDR(ipAddress, cidr))) {
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
break typeSwitch;
}
}
throw new Error(`No matching IPv6 registry found for ${lookupTarget}.`);
}
case "autnum": {
// Extract ASN number from "AS12345" format
const asnMatch = lookupTarget.match(/^AS(\d+)$/i);
if (!asnMatch || !asnMatch[1]) {
throw new Error(`Invalid ASN format: ${lookupTarget}`);
}
typeSwitch: switch (type) {
case "domain":
for (const bootstrapItem of bootstrap.services) {
if (bootstrapItem[0].some(domainMatchPredicate(lookupTarget))) {
// min length of 1 is validated in zod schema
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
break typeSwitch;
}
}
throw new Error(`No matching domain found.`);
case "ip4": {
// Extract the IP address without CIDR suffix for matching
const ipAddress = lookupTarget.split("/")[0] ?? lookupTarget;
for (const bootstrapItem of bootstrap.services) {
// bootstrapItem[0] contains CIDR ranges like ["1.0.0.0/8", "2.0.0.0/8"]
if (bootstrapItem[0].some((cidr) => ipv4InCIDR(ipAddress, cidr))) {
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
break typeSwitch;
}
}
throw new Error(`No matching IPv4 registry found for ${lookupTarget}.`);
}
case "ip6": {
// Extract the IP address without CIDR suffix for matching
const ipAddress = lookupTarget.split("/")[0] ?? lookupTarget;
for (const bootstrapItem of bootstrap.services) {
// bootstrapItem[0] contains CIDR ranges like ["2001:0200::/23", "2001:0400::/23"]
if (bootstrapItem[0].some((cidr) => ipv6InCIDR(ipAddress, cidr))) {
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
break typeSwitch;
}
}
throw new Error(`No matching IPv6 registry found for ${lookupTarget}.`);
}
case "autnum": {
// Extract ASN number from "AS12345" format
const asnMatch = lookupTarget.match(/^AS(\d+)$/i);
if (!asnMatch || !asnMatch[1]) {
throw new Error(`Invalid ASN format: ${lookupTarget}`);
}
const asnNumber = parseInt(asnMatch[1], 10);
if (isNaN(asnNumber)) {
throw new Error(`Invalid ASN number: ${lookupTarget}`);
}
const asnNumber = parseInt(asnMatch[1], 10);
if (isNaN(asnNumber)) {
throw new Error(`Invalid ASN number: ${lookupTarget}`);
}
for (const bootstrapItem of bootstrap.services) {
// bootstrapItem[0] contains ASN ranges like ["64512-65534", "13312-18431"]
if (bootstrapItem[0].some((range) => asnInRange(asnNumber, range))) {
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
break typeSwitch;
}
}
throw new Error(`No matching registry found for ${lookupTarget}.`);
}
case "entity":
throw new Error(`No matching entity found.`);
default:
throw new Error("Invalid lookup target provided.");
}
for (const bootstrapItem of bootstrap.services) {
// bootstrapItem[0] contains ASN ranges like ["64512-65534", "13312-18431"]
if (bootstrapItem[0].some((range) => asnInRange(asnNumber, range))) {
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
break typeSwitch;
}
}
throw new Error(`No matching registry found for ${lookupTarget}.`);
}
case "entity":
throw new Error(`No matching entity found.`);
default:
throw new Error("Invalid lookup target provided.");
}
if (url == null) throw new Error("No lookup target was resolved.");
if (url == null) throw new Error("No lookup target was resolved.");
// Map internal types to RDAP endpoint paths
// ip4 and ip6 both use the 'ip' endpoint in RDAP
const rdapPath = type === "ip4" || type === "ip6" ? "ip" : type;
// Map internal types to RDAP endpoint paths
// ip4 and ip6 both use the 'ip' endpoint in RDAP
const rdapPath = type === "ip4" || type === "ip6" ? "ip" : type;
return `${url}${rdapPath}/${lookupTarget}`;
}
return `${url}${rdapPath}/${lookupTarget}`;
}
useEffect(() => {
const preload = async () => {
if (uriType.isNothing) return;
useEffect(() => {
const preload = async () => {
if (uriType.isNothing) return;
const registryUri = RootRegistryEnum.safeParse(uriType.value);
if (!registryUri.success) return;
const registryUri = RootRegistryEnum.safeParse(uriType.value);
if (!registryUri.success) return;
console.log({
uriType: uriType.value,
registryData: registryDataRef.current,
registryUri: registryUri.data,
});
if (registryDataRef.current[registryUri.data] != null) return;
console.log({
uriType: uriType.value,
registryData: registryDataRef.current,
registryUri: registryUri.data,
});
if (registryDataRef.current[registryUri.data] != null) return;
try {
await loadBootstrap(registryUri.data);
} catch (e) {
if (warningHandler != undefined) {
const message =
e instanceof Error ? `(${truncated(e.message, 15)})` : ".";
warningHandler({
message: `Failed to preload registry${message}`,
});
}
}
};
try {
await loadBootstrap(registryUri.data);
} catch (e) {
if (warningHandler != undefined) {
const message = e instanceof Error ? `(${truncated(e.message, 15)})` : ".";
warningHandler({
message: `Failed to preload registry${message}`,
});
}
}
};
preload().catch(console.error);
}, [target, uriType, warningHandler]);
preload().catch(console.error);
}, [target, uriType, warningHandler]);
async function getAndParse<T>(
url: string,
schema: ZodSchema<T>
): Promise<Result<T, Error>> {
const response = await fetch(url);
async function getAndParse<T>(url: string, schema: ZodSchema<T>): Promise<Result<T, Error>> {
const response = await fetch(url);
if (response.status == 200) {
const result = schema.safeParse(await response.json());
if (response.status == 200) {
const result = schema.safeParse(await response.json());
if (result.success === false) {
// flatten the errors to make them more readable and simple
const flatErrors = result.error.flatten(function (issue) {
const path = issue.path.map((value) => value.toString()).join(".");
return `${path}: ${issue.message}`;
});
if (result.success === false) {
// flatten the errors to make them more readable and simple
const flatErrors = result.error.flatten(function (issue) {
const path = issue.path.map((value) => value.toString()).join(".");
return `${path}: ${issue.message}`;
});
console.log(flatErrors);
console.log(flatErrors);
// combine them all, wrap them in a new error, and return it
return Result.err(
new Error(
[
"Could not parse the response from the registry.",
...flatErrors.formErrors,
...Object.values(flatErrors.fieldErrors).flat(),
].join("\n\t")
)
);
}
// combine them all, wrap them in a new error, and return it
return Result.err(
new Error(
[
"Could not parse the response from the registry.",
...flatErrors.formErrors,
...Object.values(flatErrors.fieldErrors).flat(),
].join("\n\t")
)
);
}
return Result.ok(result.data);
}
return Result.ok(result.data);
}
switch (response.status) {
case 302:
return Result.err(
new Error(
"The registry indicated that the resource requested is available at a different location."
)
);
case 400:
return Result.err(
new Error(
"The registry indicated that the request was malformed or could not be processed. Check that you typed in the correct information and try again."
)
);
case 403:
return Result.err(
new Error(
"The registry indicated that the request was forbidden. This could be due to rate limiting, abusive behavior, or other reasons. Try again later or contact the registry for more information."
)
);
switch (response.status) {
case 302:
return Result.err(
new Error(
"The registry indicated that the resource requested is available at a different location."
)
);
case 400:
return Result.err(
new Error(
"The registry indicated that the request was malformed or could not be processed. Check that you typed in the correct information and try again."
)
);
case 403:
return Result.err(
new Error(
"The registry indicated that the request was forbidden. This could be due to rate limiting, abusive behavior, or other reasons. Try again later or contact the registry for more information."
)
);
case 404:
return Result.err(
new Error(
"The registry indicated that the resource requested could not be found; the resource either does not exist, or is something that the registry does not track (i.e. this software queried incorrectly, which is unlikely)."
)
);
case 500:
return Result.err(
new Error(
"The registry indicated that an internal server error occurred. This could be due to a misconfiguration, a bug, or other reasons. Try again later or contact the registry for more information."
)
);
default:
return Result.err(
new Error(
`The registry did not return an OK status code: ${response.status}.`
)
);
}
}
case 404:
return Result.err(
new Error(
"The registry indicated that the resource requested could not be found; the resource either does not exist, or is something that the registry does not track (i.e. this software queried incorrectly, which is unlikely)."
)
);
case 500:
return Result.err(
new Error(
"The registry indicated that an internal server error occurred. This could be due to a misconfiguration, a bug, or other reasons. Try again later or contact the registry for more information."
)
);
default:
return Result.err(
new Error(`The registry did not return an OK status code: ${response.status}.`)
);
}
}
async function submitInternal(
target: string
): Promise<Result<{ data: ParsedGeneric; url: string }, Error>> {
if (target == null || target.length == 0)
return Result.err(
new Error("A target must be given in order to execute a lookup.")
);
async function submitInternal(
target: string
): Promise<Result<{ data: ParsedGeneric; url: string }, Error>> {
if (target == null || target.length == 0)
return Result.err(new Error("A target must be given in order to execute a lookup."));
const targetType = await getTypeEasy(target);
const targetType = await getTypeEasy(target);
if (targetType.isErr) {
return Result.err(
new Error("Unable to determine type, unable to send query", {
cause: targetType.error,
})
);
}
if (targetType.isErr) {
return Result.err(
new Error("Unable to determine type, unable to send query", {
cause: targetType.error,
})
);
}
switch (targetType.value) {
// Block scoped case to allow url const reuse
case "ip4": {
await loadBootstrap("ip4");
const url = getRegistryURL(targetType.value, target);
const result = await getAndParse<IpNetwork>(url, IpNetworkSchema);
if (result.isErr) return Result.err(result.error);
return Result.ok({ data: result.value, url });
}
case "ip6": {
await loadBootstrap("ip6");
const url = getRegistryURL(targetType.value, target);
const result = await getAndParse<IpNetwork>(url, IpNetworkSchema);
if (result.isErr) return Result.err(result.error);
return Result.ok({ data: result.value, url });
}
case "domain": {
await loadBootstrap("domain");
const url = getRegistryURL(targetType.value, target);
switch (targetType.value) {
// Block scoped case to allow url const reuse
case "ip4": {
await loadBootstrap("ip4");
const url = getRegistryURL(targetType.value, target);
const result = await getAndParse<IpNetwork>(url, IpNetworkSchema);
if (result.isErr) return Result.err(result.error);
return Result.ok({ data: result.value, url });
}
case "ip6": {
await loadBootstrap("ip6");
const url = getRegistryURL(targetType.value, target);
const result = await getAndParse<IpNetwork>(url, IpNetworkSchema);
if (result.isErr) return Result.err(result.error);
return Result.ok({ data: result.value, url });
}
case "domain": {
await loadBootstrap("domain");
const url = getRegistryURL(targetType.value, target);
// HTTP
if (url.startsWith("http://") && url != repeatableRef.current) {
repeatableRef.current = url;
return Result.err(
new Error(
"The registry this domain belongs to uses HTTP, which is not secure. " +
"In order to prevent a cryptic error from appearing due to mixed active content, " +
"or worse, a CORS error, this lookup has been blocked. Try again to force the lookup."
)
);
}
const result = await getAndParse<Domain>(url, DomainSchema);
if (result.isErr) return Result.err(result.error);
// HTTP
if (url.startsWith("http://") && url != repeatableRef.current) {
repeatableRef.current = url;
return Result.err(
new Error(
"The registry this domain belongs to uses HTTP, which is not secure. " +
"In order to prevent a cryptic error from appearing due to mixed active content, " +
"or worse, a CORS error, this lookup has been blocked. Try again to force the lookup."
)
);
}
const result = await getAndParse<Domain>(url, DomainSchema);
if (result.isErr) return Result.err(result.error);
return Result.ok({ data: result.value, url });
}
case "autnum": {
await loadBootstrap("autnum");
const url = getRegistryURL(targetType.value, target);
const result = await getAndParse<AutonomousNumber>(
url,
AutonomousNumberSchema
);
if (result.isErr) return Result.err(result.error);
return Result.ok({ data: result.value, url });
}
case "tld": {
// remove the leading dot
const value = target.startsWith(".") ? target.slice(1) : target;
const url = `https://root.rdap.org/domain/${value}`;
const result = await getAndParse<Domain>(url, DomainSchema);
if (result.isErr) return Result.err(result.error);
return Result.ok({ data: result.value, url });
}
case "url": {
const response = await fetch(target);
return Result.ok({ data: result.value, url });
}
case "autnum": {
await loadBootstrap("autnum");
const url = getRegistryURL(targetType.value, target);
const result = await getAndParse<AutonomousNumber>(url, AutonomousNumberSchema);
if (result.isErr) return Result.err(result.error);
return Result.ok({ data: result.value, url });
}
case "tld": {
// remove the leading dot
const value = target.startsWith(".") ? target.slice(1) : target;
const url = `https://root.rdap.org/domain/${value}`;
const result = await getAndParse<Domain>(url, DomainSchema);
if (result.isErr) return Result.err(result.error);
return Result.ok({ data: result.value, url });
}
case "url": {
const response = await fetch(target);
if (response.status != 200)
return Result.err(
new Error(
`The URL provided returned a non-200 status code: ${response.status}.`
)
);
if (response.status != 200)
return Result.err(
new Error(
`The URL provided returned a non-200 status code: ${response.status}.`
)
);
const data = await response.json();
const data = await response.json();
// Try each schema until one works
for (const schema of schemas) {
const result = schema.safeParse(data);
if (result.success)
return Result.ok({ data: result.data, url: target });
}
// Try each schema until one works
for (const schema of schemas) {
const result = schema.safeParse(data);
if (result.success) return Result.ok({ data: result.data, url: target });
}
return Result.err(
new Error("No schema was able to parse the response.")
);
}
case "json": {
const data = JSON.parse(target);
for (const schema of schemas) {
const result = schema.safeParse(data);
if (result.success) return Result.ok({ data: result.data, url: "" });
}
}
case "registrar": {
}
default:
return Result.err(
new Error("The type detected has not been implemented.")
);
}
}
return Result.err(new Error("No schema was able to parse the response."));
}
case "json": {
const data = JSON.parse(target);
for (const schema of schemas) {
const result = schema.safeParse(data);
if (result.success) return Result.ok({ data: result.data, url: "" });
}
}
case "registrar": {
}
default:
return Result.err(new Error("The type detected has not been implemented."));
}
}
async function submit({
target,
}: SubmitProps): Promise<Maybe<MetaParsedGeneric>> {
try {
// target is already set in state, but it's also provided by the form callback, so we'll use it.
const response = await submitInternal(target);
async function submit({ target }: SubmitProps): Promise<Maybe<MetaParsedGeneric>> {
try {
// target is already set in state, but it's also provided by the form callback, so we'll use it.
const response = await submitInternal(target);
if (response.isErr) {
setError(response.error.message);
console.error(response.error);
} else setError(null);
if (response.isErr) {
setError(response.error.message);
console.error(response.error);
} else setError(null);
return response.isOk
? Maybe.just({
data: response.value.data,
url: response.value.url,
completeTime: new Date(),
})
: Maybe.nothing();
} catch (e) {
if (!(e instanceof Error))
setError("An unknown, unprocessable error has occurred.");
else setError(e.message);
console.error(e);
return Maybe.nothing();
}
}
return response.isOk
? Maybe.just({
data: response.value.data,
url: response.value.url,
completeTime: new Date(),
})
: Maybe.nothing();
} catch (e) {
if (!(e instanceof Error)) setError("An unknown, unprocessable error has occurred.");
else setError(e.message);
console.error(e);
return Maybe.nothing();
}
}
return {
error,
setTarget,
setTargetType,
submit,
currentType: uriType,
getType: getTypeEasy,
};
return {
error,
setTarget,
setTargetType,
submit,
currentType: uriType,
getType: getTypeEasy,
};
};
export default useLookup;

View File

@@ -2,5 +2,5 @@ import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
return twMerge(clsx(inputs));
}

View File

@@ -6,7 +6,7 @@ import "@fontsource/ibm-plex-mono/400.css";
import "../styles/globals.css";
const MyApp: AppType = ({ Component, pageProps }) => {
return <Component {...pageProps} />;
return <Component {...pageProps} />;
};
export default MyApp;

View File

@@ -10,93 +10,83 @@ import { Maybe } from "true-myth";
import type { TargetType } from "@/types";
const Index: NextPage = () => {
const { error, setTarget, setTargetType, submit, getType } = useLookup();
const [detectedType, setDetectedType] = useState<Maybe<TargetType>>(
Maybe.nothing()
);
const [response, setResponse] = useState<Maybe<MetaParsedGeneric>>(
Maybe.nothing()
);
const [isLoading, setLoading] = useState<boolean>(false);
const { error, setTarget, setTargetType, submit, getType } = useLookup();
const [detectedType, setDetectedType] = useState<Maybe<TargetType>>(Maybe.nothing());
const [response, setResponse] = useState<Maybe<MetaParsedGeneric>>(Maybe.nothing());
const [isLoading, setLoading] = useState<boolean>(false);
return (
<>
<Head>
<title>rdap.xevion.dev</title>
<meta
name="description"
content="A custom, private RDAP lookup client built by Xevion."
/>
<meta property="og:url" content="https://rdap.xevion.dev" />
<meta property="og:title" content="RDAP | by Xevion.dev" />
<meta
property="og:description"
content="A custom, private RDAP lookup client built by Xevion."
/>
<meta property="og:site_name" content="rdap.xevion.dev" />
<meta property="og:type" content="website" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="keywords"
content="xevion, rdap, whois, rdap, domain name, dns, ip address"
/>
</Head>
<nav className="bg-zinc-850 px-5 py-4 shadow-xs">
<span
className="text-xl font-medium text-white"
style={{ fontSize: "larger" }}
>
<a href="https://github.com/Xevion/rdap">rdap</a>
<a
href={"https://xevion.dev"}
className="text-zinc-400 hover:animate-pulse"
>
.xevion.dev
</a>
</span>
</nav>
<div className="mx-auto max-w-screen-sm px-5 lg:max-w-screen-md xl:max-w-screen-lg">
<div className="dark container mx-auto w-full py-6 md:py-12 ">
<LookupInput
isLoading={isLoading}
detectedType={detectedType}
onChange={async ({ target, targetType }) => {
setTarget(target);
setTargetType(targetType);
return (
<>
<Head>
<title>rdap.xevion.dev</title>
<meta
name="description"
content="A custom, private RDAP lookup client built by Xevion."
/>
<meta property="og:url" content="https://rdap.xevion.dev" />
<meta property="og:title" content="RDAP | by Xevion.dev" />
<meta
property="og:description"
content="A custom, private RDAP lookup client built by Xevion."
/>
<meta property="og:site_name" content="rdap.xevion.dev" />
<meta property="og:type" content="website" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="keywords"
content="xevion, rdap, whois, rdap, domain name, dns, ip address"
/>
</Head>
<nav className="bg-zinc-850 px-5 py-4 shadow-xs">
<span className="text-xl font-medium text-white" style={{ fontSize: "larger" }}>
<a href="https://github.com/Xevion/rdap">rdap</a>
<a href={"https://xevion.dev"} className="text-zinc-400 hover:animate-pulse">
.xevion.dev
</a>
</span>
</nav>
<div className="mx-auto max-w-screen-sm px-5 lg:max-w-screen-md xl:max-w-screen-lg">
<div className="dark container mx-auto w-full py-6 md:py-12">
<LookupInput
isLoading={isLoading}
detectedType={detectedType}
onChange={async ({ target, targetType }) => {
setTarget(target);
setTargetType(targetType);
const detectResult = await getType(target);
if (detectResult.isOk) {
setDetectedType(Maybe.just(detectResult.value));
} else {
setDetectedType(Maybe.nothing());
}
}}
onSubmit={async function (props) {
try {
setLoading(true);
setResponse(await submit(props));
setLoading(false);
} catch (e) {
console.error(e);
setResponse(Maybe.nothing());
setLoading(false);
}
}}
/>
{error != null ? (
<ErrorCard
title="An error occurred while performing a lookup."
description={error}
className="mb-2"
/>
) : null}
{response.isJust ? (
<Generic url={response.value.url} data={response.value.data} />
) : null}
</div>
</div>
</>
);
const detectResult = await getType(target);
if (detectResult.isOk) {
setDetectedType(Maybe.just(detectResult.value));
} else {
setDetectedType(Maybe.nothing());
}
}}
onSubmit={async function (props) {
try {
setLoading(true);
setResponse(await submit(props));
setLoading(false);
} catch (e) {
console.error(e);
setResponse(Maybe.nothing());
setLoading(false);
}
}}
/>
{error != null ? (
<ErrorCard
title="An error occurred while performing a lookup."
description={error}
className="mb-2"
/>
) : null}
{response.isJust ? (
<Generic url={response.value.url} data={response.value.data} />
) : null}
</div>
</div>
</>
);
};
export default Index;

View File

@@ -11,43 +11,43 @@ import { registryURLs } from "./constants";
const registryCache = new Map<RootRegistryType, Register>();
async function getRealRegistry(type: RootRegistryType): Promise<Register> {
if (registryCache.has(type)) {
return registryCache.get(type)!;
}
if (registryCache.has(type)) {
return registryCache.get(type)!;
}
const response = await fetch(registryURLs[type]);
if (!response.ok) {
throw new Error(`Failed to fetch ${type} registry: ${response.statusText}`);
}
const response = await fetch(registryURLs[type]);
if (!response.ok) {
throw new Error(`Failed to fetch ${type} registry: ${response.statusText}`);
}
const data = (await response.json()) as Register;
registryCache.set(type, data);
return data;
const data = (await response.json()) as Register;
registryCache.set(type, data);
return data;
}
describe("getType - Integration tests with real registries", () => {
it("should detect entity with real entity registry", async () => {
// Test with a known entity tag (RIPE)
const result = await getType("TEST-RIPE", getRealRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("entity");
}
}, 10000); // Longer timeout for network call
it("should detect entity with real entity registry", async () => {
// Test with a known entity tag (RIPE)
const result = await getType("TEST-RIPE", getRealRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("entity");
}
}, 10000); // Longer timeout for network call
it("should detect entity with ARIN tag", async () => {
const result = await getType("NET-ARIN", getRealRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("entity");
}
}, 10000);
it("should detect entity with ARIN tag", async () => {
const result = await getType("NET-ARIN", getRealRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("entity");
}
}, 10000);
it("should not detect invalid entity tag", async () => {
const result = await getType("INVALID-NOTREAL", getRealRegistry);
// Should either error or detect as something else, but not entity
if (result.isOk) {
expect(result.value).not.toBe("entity");
}
}, 10000);
it("should not detect invalid entity tag", async () => {
const result = await getType("INVALID-NOTREAL", getRealRegistry);
// Should either error or detect as something else, but not entity
if (result.isOk) {
expect(result.value).not.toBe("entity");
}
}, 10000);
});

View File

@@ -4,406 +4,403 @@ import type { Register } from "./types";
// Mock registry getter (matches real IANA structure: [email, tags, urls])
const mockRegistry: Register = {
description: "Test registry",
publication: "2024-01-01",
version: "1.0",
services: [
[
["test@example.com"], // email
["RIPE", "APNIC"], // tags
["https://rdap.example.com/"] // urls
]
],
description: "Test registry",
publication: "2024-01-01",
version: "1.0",
services: [
[
["test@example.com"], // email
["RIPE", "APNIC"], // tags
["https://rdap.example.com/"], // urls
],
],
};
const mockGetRegistry = vi.fn(() => Promise.resolve(mockRegistry));
describe("getType - IP address detection", () => {
describe("IPv4 detection", () => {
it("should detect standard IPv4 addresses", async () => {
const result = await getType("192.168.1.1", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("ip4");
}
});
describe("IPv4 detection", () => {
it("should detect standard IPv4 addresses", async () => {
const result = await getType("192.168.1.1", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("ip4");
}
});
it("should detect IPv4 with CIDR notation", async () => {
const result = await getType("192.168.1.0/24", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("ip4");
}
});
it("should detect IPv4 with CIDR notation", async () => {
const result = await getType("192.168.1.0/24", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("ip4");
}
});
it("should detect various IPv4 addresses", async () => {
const ips = [
"8.8.8.8",
"1.1.1.1",
"10.0.0.1",
"172.16.0.1",
"255.255.255.255",
"0.0.0.0",
];
it("should detect various IPv4 addresses", async () => {
const ips = [
"8.8.8.8",
"1.1.1.1",
"10.0.0.1",
"172.16.0.1",
"255.255.255.255",
"0.0.0.0",
];
for (const ip of ips) {
const result = await getType(ip, mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("ip4");
}
}
});
for (const ip of ips) {
const result = await getType(ip, mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("ip4");
}
}
});
it("should detect IPv4 with various CIDR prefixes", async () => {
const cidrs = [
"192.168.1.0/8",
"10.0.0.0/16",
"172.16.0.0/12",
"8.8.8.0/24",
"1.1.1.1/32",
];
it("should detect IPv4 with various CIDR prefixes", async () => {
const cidrs = [
"192.168.1.0/8",
"10.0.0.0/16",
"172.16.0.0/12",
"8.8.8.0/24",
"1.1.1.1/32",
];
for (const cidr of cidrs) {
const result = await getType(cidr, mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("ip4");
}
}
});
});
for (const cidr of cidrs) {
const result = await getType(cidr, mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("ip4");
}
}
});
});
describe("IPv6 detection", () => {
it("should detect standard IPv6 addresses", async () => {
const result = await getType("2001:db8::1", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("ip6");
}
});
describe("IPv6 detection", () => {
it("should detect standard IPv6 addresses", async () => {
const result = await getType("2001:db8::1", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("ip6");
}
});
it("should detect IPv6 with CIDR notation", async () => {
const result = await getType("2001:db8::/32", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("ip6");
}
});
it("should detect IPv6 with CIDR notation", async () => {
const result = await getType("2001:db8::/32", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("ip6");
}
});
it("should detect various IPv6 addresses", async () => {
const ips = [
"2001:4860:4860::8888", // Google DNS
"2606:4700:4700::1111", // Cloudflare DNS
"::1", // Localhost
"::", // All zeros
"fe80::1", // Link-local
"2001:db8:85a3::8a2e:370:7334", // Full notation
];
it("should detect various IPv6 addresses", async () => {
const ips = [
"2001:4860:4860::8888", // Google DNS
"2606:4700:4700::1111", // Cloudflare DNS
"::1", // Localhost
"::", // All zeros
"fe80::1", // Link-local
"2001:db8:85a3::8a2e:370:7334", // Full notation
];
for (const ip of ips) {
const result = await getType(ip, mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("ip6");
}
}
});
for (const ip of ips) {
const result = await getType(ip, mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("ip6");
}
}
});
it("should detect IPv6 with various CIDR prefixes", async () => {
const cidrs = ["2001:db8::/32", "2001:4860::/32", "fe80::/10", "::1/128"];
it("should detect IPv6 with various CIDR prefixes", async () => {
const cidrs = ["2001:db8::/32", "2001:4860::/32", "fe80::/10", "::1/128"];
for (const cidr of cidrs) {
const result = await getType(cidr, mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("ip6");
}
}
});
});
for (const cidr of cidrs) {
const result = await getType(cidr, mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("ip6");
}
}
});
});
});
describe("getType - Domain detection", () => {
it("should detect standard domains", async () => {
const result = await getType("example.com", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("domain");
}
});
it("should detect standard domains", async () => {
const result = await getType("example.com", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("domain");
}
});
it("should detect various domain formats", async () => {
const domains = [
"google.com",
"www.example.com",
"sub.domain.example.com",
"test-domain.com",
"example123.org",
"a.b.c.d.example.net",
];
it("should detect various domain formats", async () => {
const domains = [
"google.com",
"www.example.com",
"sub.domain.example.com",
"test-domain.com",
"example123.org",
"a.b.c.d.example.net",
];
for (const domain of domains) {
const result = await getType(domain, mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("domain");
}
}
});
for (const domain of domains) {
const result = await getType(domain, mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("domain");
}
}
});
});
describe("getType - ASN detection", () => {
it("should detect standard ASN format", async () => {
const result = await getType("AS12345", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("autnum");
}
});
it("should detect standard ASN format", async () => {
const result = await getType("AS12345", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("autnum");
}
});
it("should detect various ASN formats", async () => {
const asns = [
"AS1",
"AS13335", // Cloudflare
"AS15169", // Google
"AS8075", // Microsoft
"AS16509", // Amazon
"AS999999",
];
it("should detect various ASN formats", async () => {
const asns = [
"AS1",
"AS13335", // Cloudflare
"AS15169", // Google
"AS8075", // Microsoft
"AS16509", // Amazon
"AS999999",
];
for (const asn of asns) {
const result = await getType(asn, mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("autnum");
}
}
});
for (const asn of asns) {
const result = await getType(asn, mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("autnum");
}
}
});
});
describe("getType - TLD detection", () => {
it("should detect TLD format", async () => {
const result = await getType(".com", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("tld");
}
});
it("should detect TLD format", async () => {
const result = await getType(".com", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("tld");
}
});
it("should detect various TLDs", async () => {
const tlds = [".com", ".org", ".net", ".dev", ".io", ".ai", ".co"];
it("should detect various TLDs", async () => {
const tlds = [".com", ".org", ".net", ".dev", ".io", ".ai", ".co"];
for (const tld of tlds) {
const result = await getType(tld, mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("tld");
}
}
});
for (const tld of tlds) {
const result = await getType(tld, mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("tld");
}
}
});
});
describe("getType - URL detection", () => {
it("should detect HTTP URLs", async () => {
const result = await getType("http://example.com", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("url");
}
});
it("should detect HTTP URLs", async () => {
const result = await getType("http://example.com", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("url");
}
});
it("should detect HTTPS URLs", async () => {
const result = await getType("https://example.com", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("url");
}
});
it("should detect HTTPS URLs", async () => {
const result = await getType("https://example.com", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("url");
}
});
it("should detect RDAP URLs", async () => {
const urls = [
"https://rdap.arin.net/registry/ip/8.8.8.8",
"http://rdap.apnic.net/ip/1.1.1.1",
"https://rdap.org/domain/example.com",
];
it("should detect RDAP URLs", async () => {
const urls = [
"https://rdap.arin.net/registry/ip/8.8.8.8",
"http://rdap.apnic.net/ip/1.1.1.1",
"https://rdap.org/domain/example.com",
];
for (const url of urls) {
const result = await getType(url, mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("url");
}
}
});
for (const url of urls) {
const result = await getType(url, mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("url");
}
}
});
});
describe("getType - JSON detection", () => {
it("should detect JSON objects", async () => {
const result = await getType(
'{"objectClassName":"domain"}',
mockGetRegistry
);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("json");
}
});
it("should detect JSON objects", async () => {
const result = await getType('{"objectClassName":"domain"}', mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("json");
}
});
it("should detect various JSON formats", async () => {
const jsons = [
"{}",
'{"key": "value"}',
'{"objectClassName":"ip network"}',
'{"handle":"TEST"}',
];
it("should detect various JSON formats", async () => {
const jsons = [
"{}",
'{"key": "value"}',
'{"objectClassName":"ip network"}',
'{"handle":"TEST"}',
];
for (const json of jsons) {
const result = await getType(json, mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("json");
}
}
});
for (const json of jsons) {
const result = await getType(json, mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("json");
}
}
});
});
describe("getType - Invalid inputs", () => {
it("should return error for empty string", async () => {
const result = await getType("", mockGetRegistry);
expect(result.isErr).toBe(true);
});
it("should return error for empty string", async () => {
const result = await getType("", mockGetRegistry);
expect(result.isErr).toBe(true);
});
it("should return error for unrecognized format", async () => {
const result = await getType("not-a-valid-input!!@@##", mockGetRegistry);
expect(result.isErr).toBe(true);
});
it("should return error for unrecognized format", async () => {
const result = await getType("not-a-valid-input!!@@##", mockGetRegistry);
expect(result.isErr).toBe(true);
});
describe("Invalid IPv4 addresses", () => {
it("should return error for IPv4 with octet > 255", async () => {
const result = await getType("256.1.1.1", mockGetRegistry);
expect(result.isErr).toBe(true);
if (result.isErr) {
expect(result.error.message).toContain("Invalid IPv4 address");
expect(result.error.message).toContain("octet");
}
});
describe("Invalid IPv4 addresses", () => {
it("should return error for IPv4 with octet > 255", async () => {
const result = await getType("256.1.1.1", mockGetRegistry);
expect(result.isErr).toBe(true);
if (result.isErr) {
expect(result.error.message).toContain("Invalid IPv4 address");
expect(result.error.message).toContain("octet");
}
});
it("should return error for IPv4 with octet 999", async () => {
const result = await getType("192.999.1.1", mockGetRegistry);
expect(result.isErr).toBe(true);
if (result.isErr) {
expect(result.error.message).toContain("Invalid IPv4 address");
}
});
it("should return error for IPv4 with octet 999", async () => {
const result = await getType("192.999.1.1", mockGetRegistry);
expect(result.isErr).toBe(true);
if (result.isErr) {
expect(result.error.message).toContain("Invalid IPv4 address");
}
});
it("should return error for IPv4 with invalid CIDR prefix", async () => {
const result = await getType("192.168.1.1/33", mockGetRegistry);
expect(result.isErr).toBe(true);
if (result.isErr) {
expect(result.error.message).toContain("CIDR prefix");
}
});
it("should return error for IPv4 with invalid CIDR prefix", async () => {
const result = await getType("192.168.1.1/33", mockGetRegistry);
expect(result.isErr).toBe(true);
if (result.isErr) {
expect(result.error.message).toContain("CIDR prefix");
}
});
it("should return error for IPv4 with negative CIDR", async () => {
const result = await getType("192.168.1.1/-1", mockGetRegistry);
expect(result.isErr).toBe(true);
});
});
it("should return error for IPv4 with negative CIDR", async () => {
const result = await getType("192.168.1.1/-1", mockGetRegistry);
expect(result.isErr).toBe(true);
});
});
describe("Invalid IPv6 addresses", () => {
it("should return error for IPv6 with multiple ::", async () => {
const result = await getType("2001::db8::1", mockGetRegistry);
expect(result.isErr).toBe(true);
if (result.isErr) {
expect(result.error.message).toContain("::");
}
});
describe("Invalid IPv6 addresses", () => {
it("should return error for IPv6 with multiple ::", async () => {
const result = await getType("2001::db8::1", mockGetRegistry);
expect(result.isErr).toBe(true);
if (result.isErr) {
expect(result.error.message).toContain("::");
}
});
it("should return error for IPv6 with invalid CIDR prefix", async () => {
const result = await getType("2001:db8::1/129", mockGetRegistry);
expect(result.isErr).toBe(true);
if (result.isErr) {
expect(result.error.message).toContain("CIDR prefix");
}
});
it("should return error for IPv6 with invalid CIDR prefix", async () => {
const result = await getType("2001:db8::1/129", mockGetRegistry);
expect(result.isErr).toBe(true);
if (result.isErr) {
expect(result.error.message).toContain("CIDR prefix");
}
});
it("should not match completely invalid hex strings as IPv6", async () => {
// "gggg" doesn't match the basic IPv6 pattern, so it won't be detected as IPv6
const result = await getType("gggg::1", mockGetRegistry);
expect(result.isErr).toBe(true);
// Won't have IPv6-specific error since it didn't match the pattern
if (result.isErr) {
expect(result.error.message).toContain("No patterns matched");
}
});
});
it("should not match completely invalid hex strings as IPv6", async () => {
// "gggg" doesn't match the basic IPv6 pattern, so it won't be detected as IPv6
const result = await getType("gggg::1", mockGetRegistry);
expect(result.isErr).toBe(true);
// Won't have IPv6-specific error since it didn't match the pattern
if (result.isErr) {
expect(result.error.message).toContain("No patterns matched");
}
});
});
});
describe("getType - Type detection priority", () => {
it("should detect URL before domain", async () => {
const result = await getType("https://example.com", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("url");
}
});
it("should detect URL before domain", async () => {
const result = await getType("https://example.com", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("url");
}
});
it("should detect JSON before domain", async () => {
const result = await getType('{"key":"value"}', mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("json");
}
});
it("should detect JSON before domain", async () => {
const result = await getType('{"key":"value"}', mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("json");
}
});
it("should detect TLD before domain", async () => {
const result = await getType(".com", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("tld");
}
});
it("should detect TLD before domain", async () => {
const result = await getType(".com", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("tld");
}
});
it("should detect IP before domain", async () => {
const result = await getType("8.8.8.8", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("ip4");
}
});
it("should detect IP before domain", async () => {
const result = await getType("8.8.8.8", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("ip4");
}
});
});
describe("getType - Case sensitivity", () => {
it("should detect uppercase domains", async () => {
const result = await getType("GOOGLE.COM", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("domain");
}
});
it("should detect uppercase domains", async () => {
const result = await getType("GOOGLE.COM", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("domain");
}
});
it("should detect mixed case domains", async () => {
const result = await getType("GoOgLe.CoM", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("domain");
}
});
it("should detect mixed case domains", async () => {
const result = await getType("GoOgLe.CoM", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("domain");
}
});
it("should detect lowercase ASN", async () => {
const result = await getType("as12345", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("autnum");
}
});
it("should detect lowercase ASN", async () => {
const result = await getType("as12345", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("autnum");
}
});
it("should detect uppercase ASN", async () => {
const result = await getType("AS12345", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("autnum");
}
});
it("should detect uppercase ASN", async () => {
const result = await getType("AS12345", mockGetRegistry);
expect(result.isOk).toBe(true);
if (result.isOk) {
expect(result.value).toBe("autnum");
}
});
});

View File

@@ -10,11 +10,11 @@ import { Result } from "true-myth";
// };
export function domainMatchPredicate(domain: string): (tld: string) => boolean {
return (tld) => domainMatch(tld, domain);
return (tld) => domainMatch(tld, domain);
}
export function domainMatch(tld: string, domain: string): boolean {
return domain.toUpperCase().endsWith(`.${tld.toUpperCase()}`);
return domain.toUpperCase().endsWith(`.${tld.toUpperCase()}`);
}
/*
@@ -43,10 +43,10 @@ export function ipMatch(prefix: string, ip: string) {
// return the first HTTPS url, or the first URL
export function getBestURL(urls: [string, ...string[]]): string {
urls.forEach((url) => {
if (url.startsWith("https://")) return url;
});
return urls[0];
urls.forEach((url) => {
if (url.startsWith("https://")) return url;
});
return urls[0];
}
// given a URL, injects that URL into the query input,
@@ -758,8 +758,8 @@ export function createRDAPLink(url, title) {
*/
type ValidatorArgs = {
value: string;
getRegistry: (type: RootRegistryType) => Promise<Register>;
value: string;
getRegistry: (type: RootRegistryType) => Promise<Register>;
};
/**
@@ -774,128 +774,114 @@ type ValidatorResult = boolean | string;
* Type validators in priority order (most specific to most generic).
* Order matters: url/json/tld are checked before domain to avoid false matches.
*/
const TypeValidators = new Map<
TargetType,
(args: ValidatorArgs) => Promise<ValidatorResult>
>([
["url", ({ value }) => Promise.resolve(/^https?:/.test(value))],
["json", ({ value }) => Promise.resolve(/^{/.test(value))],
["tld", ({ value }) => Promise.resolve(/^\.\w+$/.test(value))],
[
"ip4",
({ value }) => {
// Basic format check
const match = value.match(
/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/\d{1,2})?$/
);
if (!match) return Promise.resolve(false);
const TypeValidators = new Map<TargetType, (args: ValidatorArgs) => Promise<ValidatorResult>>([
["url", ({ value }) => Promise.resolve(/^https?:/.test(value))],
["json", ({ value }) => Promise.resolve(/^{/.test(value))],
["tld", ({ value }) => Promise.resolve(/^\.\w+$/.test(value))],
[
"ip4",
({ value }) => {
// Basic format check
const match = value.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/\d{1,2})?$/);
if (!match) return Promise.resolve(false);
// Validate each octet is 0-255
const octets = [match[1], match[2], match[3], match[4]];
for (let i = 0; i < octets.length; i++) {
const octet = parseInt(octets[i] ?? "", 10);
if (isNaN(octet) || octet < 0 || octet > 255) {
return Promise.resolve(
`Invalid IPv4 address: octet ${i + 1} (${octets[i] ?? 'undefined'}) must be 0-255`
);
}
}
// Validate each octet is 0-255
const octets = [match[1], match[2], match[3], match[4]];
for (let i = 0; i < octets.length; i++) {
const octet = parseInt(octets[i] ?? "", 10);
if (isNaN(octet) || octet < 0 || octet > 255) {
return Promise.resolve(
`Invalid IPv4 address: octet ${i + 1} (${octets[i] ?? "undefined"}) must be 0-255`
);
}
}
// Validate CIDR prefix if present
if (match[5]) {
const prefix = parseInt(match[5].substring(1), 10);
if (isNaN(prefix) || prefix < 0 || prefix > 32) {
return Promise.resolve(
"Invalid IPv4 address: CIDR prefix must be 0-32"
);
}
}
// Validate CIDR prefix if present
if (match[5]) {
const prefix = parseInt(match[5].substring(1), 10);
if (isNaN(prefix) || prefix < 0 || prefix > 32) {
return Promise.resolve("Invalid IPv4 address: CIDR prefix must be 0-32");
}
}
return Promise.resolve(true);
},
],
[
"ip6",
({ value }) => {
// Basic format check (hex characters, colons, optional CIDR)
const match = value.match(/^([0-9a-fA-F:]+)(\/\d{1,3})?$/);
if (!match) return Promise.resolve(false);
return Promise.resolve(true);
},
],
[
"ip6",
({ value }) => {
// Basic format check (hex characters, colons, optional CIDR)
const match = value.match(/^([0-9a-fA-F:]+)(\/\d{1,3})?$/);
if (!match) return Promise.resolve(false);
const ipPart = match[1] ?? "";
const ipPart = match[1] ?? "";
// Check for invalid characters
if (!/^[0-9a-fA-F:]+$/.test(ipPart)) {
return Promise.resolve(
"Invalid IPv6 address: contains invalid characters"
);
}
// Check for invalid characters
if (!/^[0-9a-fA-F:]+$/.test(ipPart)) {
return Promise.resolve("Invalid IPv6 address: contains invalid characters");
}
// Validate double :: only appears once
const doubleColonCount = (ipPart.match(/::/g) || []).length;
if (doubleColonCount > 1) {
return Promise.resolve("Invalid IPv6 address: :: can only appear once");
}
// Validate double :: only appears once
const doubleColonCount = (ipPart.match(/::/g) || []).length;
if (doubleColonCount > 1) {
return Promise.resolve("Invalid IPv6 address: :: can only appear once");
}
// Validate CIDR prefix if present
if (match[2]) {
const prefix = parseInt(match[2].substring(1), 10);
if (isNaN(prefix) || prefix < 0 || prefix > 128) {
return Promise.resolve(
"Invalid IPv6 address: CIDR prefix must be 0-128"
);
}
}
// Validate CIDR prefix if present
if (match[2]) {
const prefix = parseInt(match[2].substring(1), 10);
if (isNaN(prefix) || prefix < 0 || prefix > 128) {
return Promise.resolve("Invalid IPv6 address: CIDR prefix must be 0-128");
}
}
return Promise.resolve(true);
},
],
["autnum", ({ value }) => Promise.resolve(/^AS\d+$/i.test(value))],
[
"entity",
async ({ value, getRegistry }) => {
// Ensure the entity handle is in the correct format
const result = value.match(/^\w+-(\w+)$/);
if (result === null || result.length <= 1 || result[1] == undefined)
return false;
return Promise.resolve(true);
},
],
["autnum", ({ value }) => Promise.resolve(/^AS\d+$/i.test(value))],
[
"entity",
async ({ value, getRegistry }) => {
// Ensure the entity handle is in the correct format
const result = value.match(/^\w+-(\w+)$/);
if (result === null || result.length <= 1 || result[1] == undefined) return false;
// Check if the entity object tag is real
try {
const registry = await getRegistry("entity");
// Check if the entity object tag is real
try {
const registry = await getRegistry("entity");
// Check each service to see if tag matches
// Entity registry structure: [email, tags, urls]
for (const service of registry.services) {
const tags = service[1]; // Tags are at index 1 (0=email, 1=tags, 2=urls)
if (
tags.some(
(tag) => tag.toUpperCase() === (result[1] as string).toUpperCase()
)
)
return true;
}
// Check each service to see if tag matches
// Entity registry structure: [email, tags, urls]
for (const service of registry.services) {
const tags = service[1]; // Tags are at index 1 (0=email, 1=tags, 2=urls)
if (
tags.some(
(tag) => tag.toUpperCase() === (result[1] as string).toUpperCase()
)
)
return true;
}
return false;
} catch (e) {
console.error(
new Error("Failed to fetch entity registry", { cause: e })
);
return false;
}
},
],
[
"domain",
({ value }) => {
// Case-insensitive domain matching with support for multiple labels
// Matches: example.com, www.example.com, a.b.c.d.example.net, etc.
return Promise.resolve(
/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/i.test(
value
)
);
},
],
["registrar", () => Promise.resolve(false)],
return false;
} catch (e) {
console.error(new Error("Failed to fetch entity registry", { cause: e }));
return false;
}
},
],
[
"domain",
({ value }) => {
// Case-insensitive domain matching with support for multiple labels
// Matches: example.com, www.example.com, a.b.c.d.example.net, etc.
return Promise.resolve(
/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/i.test(
value
)
);
},
],
["registrar", () => Promise.resolve(false)],
]);
/**
@@ -910,23 +896,23 @@ const TypeValidators = new Map<
* or an `Error` if the value is invalid or no patterns match.
*/
export async function getType(
value: string,
getRegistry: (type: RootRegistryType) => Promise<Register>
value: string,
getRegistry: (type: RootRegistryType) => Promise<Register>
): Promise<Result<TargetType, Error>> {
for (const [type, validator] of TypeValidators.entries()) {
const result = await validator({ value, getRegistry });
for (const [type, validator] of TypeValidators.entries()) {
const result = await validator({ value, getRegistry });
if (result === false) {
// Didn't match this type, try next validator
continue;
} else if (result === true) {
// Matched and valid
return Result.ok(type);
} else {
// Matched but invalid (result is error message)
return Result.err(new Error(result));
}
}
if (result === false) {
// Didn't match this type, try next validator
continue;
} else if (result === true) {
// Matched and valid
return Result.ok(type);
} else {
// Matched but invalid (result is error message)
return Result.err(new Error(result));
}
}
return Result.err(new Error("No patterns matched the input"));
return Result.err(new Error("No patterns matched the input"));
}

View File

@@ -1,166 +1,160 @@
import { z } from "zod";
export const TargetTypeEnum = z.enum([
"autnum",
"domain",
"ip4",
"ip6",
"entity",
"url",
"tld",
"registrar",
"json",
"autnum",
"domain",
"ip4",
"ip6",
"entity",
"url",
"tld",
"registrar",
"json",
]);
export const RootRegistryEnum = z.enum([
"autnum",
"domain",
"ip4",
"ip6",
"entity",
]);
export const RootRegistryEnum = z.enum(["autnum", "domain", "ip4", "ip6", "entity"]);
export const StatusEnum = z.enum([
"validated",
"renew prohibited",
"update prohibited",
"transfer prohibited",
"delete prohibited",
"proxy",
"private",
"removed",
"obscured",
"associated",
"active",
"inactive",
"locked",
"pending create",
"pending renew",
"pending transfer",
"pending update",
"pending delete",
"add period",
"auto renew period",
"client delete prohibited",
"client hold",
"client renew prohibited",
"client transfer prohibited",
"client update prohibited",
"pending restore",
"redemption period",
"renew period",
"server delete prohibited",
"server renew prohibited",
"server transfer prohibited",
"server update prohibited",
"server hold",
"transfer period",
"validated",
"renew prohibited",
"update prohibited",
"transfer prohibited",
"delete prohibited",
"proxy",
"private",
"removed",
"obscured",
"associated",
"active",
"inactive",
"locked",
"pending create",
"pending renew",
"pending transfer",
"pending update",
"pending delete",
"add period",
"auto renew period",
"client delete prohibited",
"client hold",
"client renew prohibited",
"client transfer prohibited",
"client update prohibited",
"pending restore",
"redemption period",
"renew period",
"server delete prohibited",
"server renew prohibited",
"server transfer prohibited",
"server update prohibited",
"server hold",
"transfer period",
]);
export const LinkSchema = z.object({
value: z.string().optional(), // de-facto optional
rel: z.string().optional(), // de-facto optional
href: z.string(),
hrefLang: z.array(z.string()).optional(),
title: z.string().optional(),
media: z.string().optional(),
type: z.string().optional(),
value: z.string().optional(), // de-facto optional
rel: z.string().optional(), // de-facto optional
href: z.string(),
hrefLang: z.array(z.string()).optional(),
title: z.string().optional(),
media: z.string().optional(),
type: z.string().optional(),
});
export const EntitySchema = z.object({
objectClassName: z.literal("entity"),
handle: z.string().optional(),
roles: z.array(z.string()),
publicIds: z
.array(
z.object({
type: z.string(),
identifier: z.string(),
})
)
.optional(),
objectClassName: z.literal("entity"),
handle: z.string().optional(),
roles: z.array(z.string()),
publicIds: z
.array(
z.object({
type: z.string(),
identifier: z.string(),
})
)
.optional(),
});
export const NameserverSchema = z.object({
objectClassName: z.literal("nameserver"),
ldhName: z.string(),
objectClassName: z.literal("nameserver"),
ldhName: z.string(),
});
export const EventSchema = z.object({
eventAction: z.string(),
eventActor: z.string().optional(),
eventDate: z.string(),
eventAction: z.string(),
eventActor: z.string().optional(),
eventDate: z.string(),
});
export const NoticeSchema = z.object({
description: z.string().array(), // de jure required
title: z.string().optional(),
links: z.array(LinkSchema).optional(),
description: z.string().array(), // de jure required
title: z.string().optional(),
links: z.array(LinkSchema).optional(),
});
export type Notice = z.infer<typeof NoticeSchema>;
export const IpNetworkSchema = z.object({
objectClassName: z.literal("ip network"),
handle: z.string(),
startAddress: z.string(),
endAddress: z.string(),
ipVersion: z.enum(["v4", "v6"]),
name: z.string(),
type: z.string(),
country: z.string().optional(),
parentHandle: z.string().optional(),
status: z.string().array(),
entities: z.array(EntitySchema).optional(),
remarks: z.any().optional(),
links: z.any().optional(),
port43: z.any().optional(),
events: z.array(EventSchema),
objectClassName: z.literal("ip network"),
handle: z.string(),
startAddress: z.string(),
endAddress: z.string(),
ipVersion: z.enum(["v4", "v6"]),
name: z.string(),
type: z.string(),
country: z.string().optional(),
parentHandle: z.string().optional(),
status: z.string().array(),
entities: z.array(EntitySchema).optional(),
remarks: z.any().optional(),
links: z.any().optional(),
port43: z.any().optional(),
events: z.array(EventSchema),
});
export const AutonomousNumberSchema = z.object({
objectClassName: z.literal("autnum"),
handle: z.string(),
startAutnum: z.number().positive(), // TODO: 32bit
endAutnum: z.number().positive(), // TODO: 32bit
name: z.string(),
type: z.string(),
status: z.array(z.string()),
country: z.string().length(2),
events: z.array(EventSchema),
entities: z.array(EntitySchema),
roles: z.array(z.string()),
links: z.array(LinkSchema),
objectClassName: z.literal("autnum"),
handle: z.string(),
startAutnum: z.number().positive(), // TODO: 32bit
endAutnum: z.number().positive(), // TODO: 32bit
name: z.string(),
type: z.string(),
status: z.array(z.string()),
country: z.string().length(2),
events: z.array(EventSchema),
entities: z.array(EntitySchema),
roles: z.array(z.string()),
links: z.array(LinkSchema),
});
export const DomainSchema = z.object({
objectClassName: z.literal("domain"),
handle: z.string(),
ldhName: z.string(),
unicodeName: z.string().optional(),
links: z.array(LinkSchema).optional(),
status: z.array(StatusEnum),
entities: z.array(EntitySchema),
events: z.array(EventSchema),
secureDNS: z.any(), // TODO: Complete schema
nameservers: z.array(NameserverSchema),
rdapConformance: z.string().array(), // TODO: Complete
notices: z.array(NoticeSchema),
network: IpNetworkSchema.optional(),
objectClassName: z.literal("domain"),
handle: z.string(),
ldhName: z.string(),
unicodeName: z.string().optional(),
links: z.array(LinkSchema).optional(),
status: z.array(StatusEnum),
entities: z.array(EntitySchema),
events: z.array(EventSchema),
secureDNS: z.any(), // TODO: Complete schema
nameservers: z.array(NameserverSchema),
rdapConformance: z.string().array(), // TODO: Complete
notices: z.array(NoticeSchema),
network: IpNetworkSchema.optional(),
});
const RegistrarSchema = z
.tuple([z.array(z.string()).min(1), z.array(z.string()).min(1)])
.or(
z.tuple([
z.array(z.string()).min(1),
z.array(z.string()).min(1),
z.array(z.string()).min(1),
])
);
.tuple([z.array(z.string()).min(1), z.array(z.string()).min(1)])
.or(
z.tuple([
z.array(z.string()).min(1),
z.array(z.string()).min(1),
z.array(z.string()).min(1),
])
);
export const RegisterSchema = z.object({
description: z.string(),
publication: z.string(),
services: z.array(RegistrarSchema),
version: z.string(),
description: z.string(),
publication: z.string(),
services: z.array(RegistrarSchema),
version: z.string(),
});

View File

@@ -1,33 +1,37 @@
@import "tailwindcss";
@theme {
--font-sans: "Inter var", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--color-zinc-850: #1D1D20;
--font-sans:
"Inter var", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol", "Noto Color Emoji";
--font-mono:
"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--color-zinc-850: #1d1d20;
}
dd {
margin: 0.5em 0 1em 2em;
margin: 0.5em 0 1em 2em;
}
.dashed {
border-bottom: 1px dashed silver;
border-bottom: 1px dashed silver;
}
body {
color-scheme: dark;
@apply bg-zinc-900 font-sans text-white;
color-scheme: dark;
@apply bg-zinc-900 font-sans text-white;
}
dd,
dl {
white-space: nowrap;
white-space: nowrap;
}
dl {
margin: 0;
margin: 0;
}
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-width: thin;
}

View File

@@ -1,16 +1,16 @@
import type { z } from "zod";
import type {
AutonomousNumberSchema,
DomainSchema,
EntitySchema,
EventSchema,
IpNetworkSchema,
LinkSchema,
NameserverSchema,
TargetTypeEnum,
RegisterSchema,
StatusEnum,
RootRegistryEnum,
AutonomousNumberSchema,
DomainSchema,
EntitySchema,
EventSchema,
IpNetworkSchema,
LinkSchema,
NameserverSchema,
TargetTypeEnum,
RegisterSchema,
StatusEnum,
RootRegistryEnum,
} from "@/schema";
// All precise target types that can be placed in the search bar.
@@ -33,7 +33,7 @@ export type Register = z.infer<typeof RegisterSchema>;
export type Domain = z.infer<typeof DomainSchema>;
export type SubmitProps = {
target: string;
requestJSContact: boolean;
followReferral: boolean;
target: string;
requestJSContact: boolean;
followReferral: boolean;
};

View File

@@ -1,26 +1,26 @@
{
"compilerOptions": {
"target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"noUncheckedIndexedAccess": true,
"baseUrl": "./src/",
"paths": {
"@/config/*": ["../config/*"],
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs"],
"exclude": ["node_modules"]
"compilerOptions": {
"target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"noUncheckedIndexedAccess": true,
"baseUrl": "./src/",
"paths": {
"@/config/*": ["../config/*"],
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs"],
"exclude": ["node_modules"]
}

View File

@@ -2,14 +2,14 @@ import { defineConfig } from "vitest/config";
import path from "path";
export default defineConfig({
test: {
globals: true,
environment: "happy-dom",
setupFiles: ["./src/test/setup.ts"],
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
test: {
globals: true,
environment: "happy-dom",
setupFiles: ["./src/test/setup.ts"],
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});