mirror of
https://github.com/Xevion/rdap.git
synced 2025-12-05 21:15:57 -06:00
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:
26
.commitlintrc.json
Normal file
26
.commitlintrc.json
Normal 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
85
.github/renovate.json
vendored
Normal 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
165
.github/workflows/ci.yml
vendored
Normal 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
71
.github/workflows/release.yml
vendored
Normal 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
5
.husky/commit-msg
Normal 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
5
.husky/pre-commit
Normal 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
6
.lintstagedrc.json
Normal 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
25
.prettierignore
Normal 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
9
.prettierrc.json
Normal 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
109
.releaserc.json
Normal 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"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
|
||||
133
package.json
133
package.json
@@ -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
2652
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
/** @type {import("prettier").Config} */
|
||||
module.exports = {
|
||||
plugins: [require.resolve("prettier-plugin-tailwindcss")],
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'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's RDAP record
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default LookupInput;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
162
src/constants.ts
162
src/constants.ts
@@ -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
38
src/env/client.mjs
vendored
@@ -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
8
src/env/schema.mjs
vendored
@@ -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
18
src/env/server.mjs
vendored
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
167
src/helpers.ts
167
src/helpers.ts
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
671
src/rdap.test.ts
671
src/rdap.test.ts
@@ -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");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
256
src/rdap.ts
256
src/rdap.ts
@@ -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"));
|
||||
}
|
||||
|
||||
250
src/schema.ts
250
src/schema.ts
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
28
src/types.ts
28
src/types.ts
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user