mirror of
https://github.com/Xevion/rdap.git
synced 2025-12-06 01:16:00 -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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- It's hard to tell at this moment if Typescript can handle this well, but I'll try.
|
||||||
|
|
||||||
[rdap]: https://rdap.xevion.dev
|
[rdap]: https://rdap.xevion.dev
|
||||||
[nextjs]: https://nextjs.org
|
[nextjs]: https://nextjs.org
|
||||||
|
|||||||
@@ -8,69 +8,69 @@ import { FlatCompat } from "@eslint/eslintrc";
|
|||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
const compat = new FlatCompat({
|
const compat = new FlatCompat({
|
||||||
baseDirectory: __dirname,
|
baseDirectory: __dirname,
|
||||||
recommendedConfig: js.configs.recommended,
|
recommendedConfig: js.configs.recommended,
|
||||||
allConfig: js.configs.all
|
allConfig: js.configs.all,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
// Base configuration with ignores
|
// Base configuration with ignores
|
||||||
{
|
{
|
||||||
ignores: [
|
ignores: [
|
||||||
".next/**",
|
".next/**",
|
||||||
"node_modules/**",
|
"node_modules/**",
|
||||||
"out/**",
|
"out/**",
|
||||||
"*.config.mjs",
|
"*.config.mjs",
|
||||||
"*.config.js",
|
"*.config.js",
|
||||||
"next-env.d.ts" // Next.js generated file
|
"next-env.d.ts", // Next.js generated file
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Next.js core web vitals using FlatCompat
|
// Next.js core web vitals using FlatCompat
|
||||||
...compat.extends("next/core-web-vitals"),
|
...compat.extends("next/core-web-vitals"),
|
||||||
|
|
||||||
// TypeScript recommended rules
|
// TypeScript recommended rules
|
||||||
...compat.extends("plugin:@typescript-eslint/recommended"),
|
...compat.extends("plugin:@typescript-eslint/recommended"),
|
||||||
|
|
||||||
// Base TypeScript configuration
|
// Base TypeScript configuration
|
||||||
{
|
{
|
||||||
plugins: {
|
plugins: {
|
||||||
"@typescript-eslint": typescriptEslint,
|
"@typescript-eslint": typescriptEslint,
|
||||||
},
|
},
|
||||||
|
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parser: tsParser,
|
parser: tsParser,
|
||||||
ecmaVersion: "latest",
|
ecmaVersion: "latest",
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: "./tsconfig.json",
|
project: "./tsconfig.json",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/consistent-type-imports": "warn",
|
"@typescript-eslint/consistent-type-imports": "warn",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Additional strict TypeScript rules for .ts and .tsx files
|
// Additional strict TypeScript rules for .ts and .tsx files
|
||||||
{
|
{
|
||||||
files: ["**/*.ts", "**/*.tsx"],
|
files: ["**/*.ts", "**/*.tsx"],
|
||||||
...compat.extends("plugin:@typescript-eslint/recommended-requiring-type-checking")[0],
|
...compat.extends("plugin:@typescript-eslint/recommended-requiring-type-checking")[0],
|
||||||
|
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: "latest",
|
ecmaVersion: "latest",
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: "./tsconfig.json",
|
project: "./tsconfig.json",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Allow CommonJS require in .cjs files
|
// Allow CommonJS require in .cjs files
|
||||||
{
|
{
|
||||||
files: ["**/*.cjs"],
|
files: ["**/*.cjs"],
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/no-require-imports": "off",
|
"@typescript-eslint/no-require-imports": "off",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
i18n: {
|
i18n: {
|
||||||
locales: ["en"],
|
locales: ["en"],
|
||||||
defaultLocale: "en",
|
defaultLocale: "en",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
133
package.json
133
package.json
@@ -1,62 +1,75 @@
|
|||||||
{
|
{
|
||||||
"name": "rdap",
|
"name": "rdap",
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"start": "next start",
|
"lint:fix": "eslint . --fix",
|
||||||
"test": "vitest",
|
"format": "prettier --write .",
|
||||||
"test:ui": "vitest --ui",
|
"format:check": "prettier --check .",
|
||||||
"test:run": "vitest run --exclude '**/*.integration.test.ts'",
|
"start": "next start",
|
||||||
"test:integration": "vitest run --include '**/*.integration.test.ts'",
|
"test": "vitest",
|
||||||
"test:all": "vitest run",
|
"test:ui": "vitest --ui",
|
||||||
"type-check": "tsc --noEmit"
|
"test:run": "vitest run --exclude '**/*.integration.test.ts'",
|
||||||
},
|
"test:integration": "vitest run --include '**/*.integration.test.ts'",
|
||||||
"dependencies": {
|
"test:all": "vitest run",
|
||||||
"@fontsource-variable/inter": "^5.2.8",
|
"type-check": "tsc --noEmit",
|
||||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
"prepare": "husky install",
|
||||||
"@headlessui/react": "^2.2.9",
|
"semantic-release": "semantic-release"
|
||||||
"@heroicons/react": "^2.0.16",
|
},
|
||||||
"@swc/helpers": "^0.5.11",
|
"dependencies": {
|
||||||
"clsx": "^2.1.1",
|
"@fontsource-variable/inter": "^5.2.8",
|
||||||
"date-fns": "^4.1.0",
|
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||||
"immutability-helper": "^3.1.1",
|
"@headlessui/react": "^2.2.9",
|
||||||
"next": "^15.5.6",
|
"@heroicons/react": "^2.0.16",
|
||||||
"react": "19.2.0",
|
"@swc/helpers": "^0.5.11",
|
||||||
"react-dom": "19.2.0",
|
"clsx": "^2.1.1",
|
||||||
"react-hook-form": "^7.42.1",
|
"date-fns": "^4.1.0",
|
||||||
"react-timeago": "^8.3.0",
|
"immutability-helper": "^3.1.1",
|
||||||
"sass": "^1.57.1",
|
"next": "^15.5.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"react": "19.2.0",
|
||||||
"true-myth": "^9.2.0",
|
"react-dom": "19.2.0",
|
||||||
"usehooks-ts": "^3.1.1",
|
"react-hook-form": "^7.42.1",
|
||||||
"zod": "^4.1.12"
|
"react-timeago": "^8.3.0",
|
||||||
},
|
"sass": "^1.57.1",
|
||||||
"devDependencies": {
|
"tailwind-merge": "^3.3.1",
|
||||||
"@tailwindcss/postcss": "^4.1.15",
|
"true-myth": "^9.2.0",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
"@testing-library/react": "^16.3.0",
|
"zod": "^4.1.12"
|
||||||
"@types/node": "^24.9.1",
|
},
|
||||||
"@types/react": "^19.2.2",
|
"devDependencies": {
|
||||||
"@types/react-dom": "^19.2.2",
|
"@commitlint/cli": "^19.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
"@commitlint/config-conventional": "^19.0.0",
|
||||||
"@typescript-eslint/parser": "^8.46.2",
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@semantic-release/git": "^10.0.1",
|
||||||
"eslint": "^9.38.0",
|
"@tailwindcss/postcss": "^4.1.15",
|
||||||
"eslint-config-next": "15.5.6",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"happy-dom": "^20.0.8",
|
"@testing-library/react": "^16.3.0",
|
||||||
"postcss": "^8.4.14",
|
"@types/node": "^24.9.1",
|
||||||
"prettier": "^3.6.2",
|
"@types/react": "^19.2.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.1",
|
"@types/react-dom": "^19.2.2",
|
||||||
"tailwindcss": "^4.1.15",
|
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
||||||
"type-fest": "^5.1.0",
|
"@typescript-eslint/parser": "^8.46.2",
|
||||||
"typescript": "^5.9.3",
|
"@vitest/ui": "^3.2.4",
|
||||||
"vitest": "^3.2.4"
|
"conventional-changelog-conventionalcommits": "^8.0.0",
|
||||||
},
|
"eslint": "^9.38.0",
|
||||||
"ct3aMetadata": {
|
"eslint-config-next": "15.5.6",
|
||||||
"initVersion": "7.2.0"
|
"happy-dom": "^20.0.8",
|
||||||
},
|
"husky": "^9.0.0",
|
||||||
"packageManager": "pnpm@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 = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
'@tailwindcss/postcss': {},
|
"@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.
|
* @returns {number} The relative position of the value to the range as -1, 0 or 1.
|
||||||
*/
|
*/
|
||||||
function compareASN(value: number, range: string): number {
|
function compareASN(value: number, range: string): number {
|
||||||
const [start, end] = range.split("-", 2) as [string, string];
|
const [start, end] = range.split("-", 2) as [string, string];
|
||||||
if (value < parseInt(start)) return -1;
|
if (value < parseInt(start)) return -1;
|
||||||
if (value > parseInt(end)) return 1;
|
if (value > parseInt(end)) return 1;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the range in which a given ASN exists via binary search. If not found, -1 is used.
|
* 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[]) {
|
export function findASN(asn: number, ranges: string[]) {
|
||||||
let start = 0;
|
let start = 0;
|
||||||
let end = ranges.length - 1;
|
let end = ranges.length - 1;
|
||||||
|
|
||||||
while (start <= end) {
|
while (start <= end) {
|
||||||
const mid = Math.floor((start + end) / 2);
|
const mid = Math.floor((start + end) / 2);
|
||||||
const comparison = compareASN(asn, ranges[mid] as string);
|
const comparison = compareASN(asn, ranges[mid] as string);
|
||||||
if (comparison == 0) return mid; // Success case
|
if (comparison == 0) return mid; // Success case
|
||||||
if (comparison == -1) end = mid - 1;
|
if (comparison == -1) end = mid - 1;
|
||||||
else start = mid + 1;
|
else start = mid + 1;
|
||||||
}
|
}
|
||||||
return -1; // Failure case
|
return -1; // Failure case
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,104 +2,107 @@ import type { FunctionComponent, ReactNode } from "react";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useBoolean } from "usehooks-ts";
|
import { useBoolean } from "usehooks-ts";
|
||||||
import {
|
import {
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
CodeBracketIcon,
|
CodeBracketIcon,
|
||||||
DocumentArrowDownIcon,
|
DocumentArrowDownIcon,
|
||||||
ClipboardDocumentIcon,
|
ClipboardDocumentIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
type AbstractCardProps = {
|
type AbstractCardProps = {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
header?: ReactNode;
|
header?: ReactNode;
|
||||||
footer?: ReactNode;
|
footer?: ReactNode;
|
||||||
data?: object;
|
data?: object;
|
||||||
url?: string;
|
url?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AbstractCard: FunctionComponent<AbstractCardProps> = ({
|
const AbstractCard: FunctionComponent<AbstractCardProps> = ({
|
||||||
url,
|
url,
|
||||||
children,
|
children,
|
||||||
header,
|
header,
|
||||||
footer,
|
footer,
|
||||||
data,
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
const { value: showRaw, toggle: toggleRaw } = useBoolean(false);
|
const { value: showRaw, toggle: toggleRaw } = useBoolean(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 overflow-clip rounded bg-zinc-800 shadow">
|
<div className="mb-4 overflow-clip rounded bg-zinc-800 shadow">
|
||||||
{header != undefined || data != undefined ? (
|
{header != undefined || data != undefined ? (
|
||||||
<div className="flex bg-zinc-700 p-2 pl-3 md:pl-5">
|
<div className="flex bg-zinc-700 p-2 pl-3 md:pl-5">
|
||||||
<div className="flex grow gap-2">{header}</div>
|
<div className="flex grow gap-2">{header}</div>
|
||||||
{url != undefined ? (
|
{url != undefined ? (
|
||||||
<div className="pr-2">
|
<div className="pr-2">
|
||||||
<a href={url} target="_blank" rel="noreferrer">
|
<a href={url} target="_blank" rel="noreferrer">
|
||||||
<LinkIcon className="mt-1 h-5 w-5 cursor-pointer" />
|
<LinkIcon className="mt-1 h-5 w-5 cursor-pointer" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{data != undefined ? (
|
{data != undefined ? (
|
||||||
<>
|
<>
|
||||||
<div className="pr-2">
|
<div className="pr-2">
|
||||||
<ClipboardDocumentIcon
|
<ClipboardDocumentIcon
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// stringify the JSON object, then begin the async clipboard write
|
// stringify the JSON object, then begin the async clipboard write
|
||||||
navigator.clipboard
|
navigator.clipboard
|
||||||
.writeText(JSON.stringify(data, null, 4))
|
.writeText(JSON.stringify(data, null, 4))
|
||||||
.then(
|
.then(
|
||||||
() => {
|
() => {
|
||||||
console.log("Copied to clipboard.");
|
console.log("Copied to clipboard.");
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err instanceof Error)
|
if (err instanceof Error)
|
||||||
console.error(
|
console.error(
|
||||||
`Failed to copy to clipboard (${err.toString()}).`
|
`Failed to copy to clipboard (${err.toString()}).`
|
||||||
);
|
);
|
||||||
else console.error("Failed to copy to clipboard.");
|
else
|
||||||
}
|
console.error(
|
||||||
);
|
"Failed to copy to clipboard."
|
||||||
}}
|
);
|
||||||
className="h-6 w-6 cursor-pointer"
|
}
|
||||||
/>
|
);
|
||||||
</div>
|
}}
|
||||||
<div className="pr-2">
|
className="h-6 w-6 cursor-pointer"
|
||||||
<DocumentArrowDownIcon
|
/>
|
||||||
onClick={() => {
|
</div>
|
||||||
const file = new Blob([JSON.stringify(data, null, 4)], {
|
<div className="pr-2">
|
||||||
type: "application/json",
|
<DocumentArrowDownIcon
|
||||||
});
|
onClick={() => {
|
||||||
|
const file = new Blob([JSON.stringify(data, null, 4)], {
|
||||||
|
type: "application/json",
|
||||||
|
});
|
||||||
|
|
||||||
const anchor = document.createElement("a");
|
const anchor = document.createElement("a");
|
||||||
anchor.href = URL.createObjectURL(file);
|
anchor.href = URL.createObjectURL(file);
|
||||||
anchor.download = "response.json";
|
anchor.download = "response.json";
|
||||||
anchor.click();
|
anchor.click();
|
||||||
}}
|
}}
|
||||||
className="h-6 w-6 cursor-pointer"
|
className="h-6 w-6 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="pr-1">
|
<div className="pr-1">
|
||||||
<CodeBracketIcon
|
<CodeBracketIcon
|
||||||
onClick={toggleRaw}
|
onClick={toggleRaw}
|
||||||
className="h-6 w-6 cursor-pointer"
|
className="h-6 w-6 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="max-w-full overflow-x-auto p-2 px-4">
|
<div className="max-w-full overflow-x-auto p-2 px-4">
|
||||||
{showRaw ? (
|
{showRaw ? (
|
||||||
<pre className="scrollbar-thin m-2 max-h-[40rem] max-w-full overflow-y-auto whitespace-pre-wrap rounded">
|
<pre className="scrollbar-thin m-2 max-h-[40rem] max-w-full overflow-y-auto rounded whitespace-pre-wrap">
|
||||||
{JSON.stringify(data, null, 4)}
|
{JSON.stringify(data, null, 4)}
|
||||||
</pre>
|
</pre>
|
||||||
) : (
|
) : (
|
||||||
children
|
children
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{footer != null ? (
|
{footer != null ? (
|
||||||
<div className="flex gap-2 bg-zinc-700 p-2 pl-5">{footer}</div>
|
<div className="flex gap-2 bg-zinc-700 p-2 pl-5">{footer}</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AbstractCard;
|
export default AbstractCard;
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { format } from "date-fns";
|
|||||||
import TimeAgo from "react-timeago";
|
import TimeAgo from "react-timeago";
|
||||||
|
|
||||||
type DynamicDateProps = {
|
type DynamicDateProps = {
|
||||||
value: Date | number;
|
value: Date | number;
|
||||||
absoluteFormat?: string;
|
absoluteFormat?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,24 +13,21 @@ type DynamicDateProps = {
|
|||||||
* @param value The date to be displayed, the Date value, or
|
* @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.
|
* @param absoluteFormat Optional - the date-fns format string to use for the absolute date rendering.
|
||||||
*/
|
*/
|
||||||
const DynamicDate: FunctionComponent<DynamicDateProps> = ({
|
const DynamicDate: FunctionComponent<DynamicDateProps> = ({ value, absoluteFormat }) => {
|
||||||
value,
|
const { value: showAbsolute, toggle: toggleFormat } = useBoolean(true);
|
||||||
absoluteFormat,
|
|
||||||
}) => {
|
|
||||||
const { value: showAbsolute, toggle: toggleFormat } = useBoolean(true);
|
|
||||||
|
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
return (
|
return (
|
||||||
<button onClick={toggleFormat}>
|
<button onClick={toggleFormat}>
|
||||||
<span className="dashed" title={date.toISOString()}>
|
<span className="dashed" title={date.toISOString()}>
|
||||||
{showAbsolute ? (
|
{showAbsolute ? (
|
||||||
format(date, absoluteFormat ?? "LLL do, y HH:mm:ss xxx")
|
format(date, absoluteFormat ?? "LLL do, y HH:mm:ss xxx")
|
||||||
) : (
|
) : (
|
||||||
<TimeAgo date={date} />
|
<TimeAgo date={date} />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DynamicDate;
|
export default DynamicDate;
|
||||||
|
|||||||
@@ -3,49 +3,49 @@ import { XCircleIcon } from "@heroicons/react/20/solid";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export type ErrorCardProps = {
|
export type ErrorCardProps = {
|
||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
description?: ReactNode;
|
description?: ReactNode;
|
||||||
issues?: ReactNode[];
|
issues?: ReactNode[];
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ErrorCard: FunctionComponent<ErrorCardProps> = ({
|
const ErrorCard: FunctionComponent<ErrorCardProps> = ({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
issues,
|
issues,
|
||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
className,
|
className,
|
||||||
"rounded-md border border-red-700/30 bg-zinc-800 px-3 pb-1 pt-3"
|
"rounded-md border border-red-700/30 bg-zinc-800 px-3 pt-3 pb-1"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<XCircleIcon className="h-5 w-5 text-red-300" aria-hidden="true" />
|
<XCircleIcon className="h-5 w-5 text-red-300" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3 w-full text-sm text-red-300">
|
<div className="ml-3 w-full text-sm text-red-300">
|
||||||
<h3 className="font-medium text-red-200">{title}</h3>
|
<h3 className="font-medium text-red-200">{title}</h3>
|
||||||
{description != undefined ? (
|
{description != undefined ? (
|
||||||
<div className="mt-2 max-h-24 w-full overflow-y-auto whitespace-pre-wrap">
|
<div className="mt-2 max-h-24 w-full overflow-y-auto whitespace-pre-wrap">
|
||||||
{description}
|
{description}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
{issues != undefined ? (
|
{issues != undefined ? (
|
||||||
<ul role="list" className="flex list-disc flex-col gap-1 pl-5">
|
<ul role="list" className="flex list-disc flex-col gap-1 pl-5">
|
||||||
{issues.map((issueText, index) => (
|
{issues.map((issueText, index) => (
|
||||||
<li key={index}>{issueText}</li>
|
<li key={index}>{issueText}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ErrorCard;
|
export default ErrorCard;
|
||||||
|
|||||||
@@ -3,24 +3,24 @@ import React from "react";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type PropertyProps = {
|
type PropertyProps = {
|
||||||
title: string | ReactNode;
|
title: string | ReactNode;
|
||||||
children: string | ReactNode;
|
children: string | ReactNode;
|
||||||
titleClass?: string;
|
titleClass?: string;
|
||||||
valueClass?: string;
|
valueClass?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Property: FunctionComponent<PropertyProps> = ({
|
const Property: FunctionComponent<PropertyProps> = ({
|
||||||
title,
|
title,
|
||||||
children,
|
children,
|
||||||
titleClass,
|
titleClass,
|
||||||
valueClass,
|
valueClass,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<dt className={cn("font-medium", titleClass)}>{title}:</dt>
|
<dt className={cn("font-medium", titleClass)}>{title}:</dt>
|
||||||
<dd className={cn("mb-2 ml-6 mt-2", valueClass)}>{children}</dd>
|
<dd className={cn("mt-2 mb-2 ml-6", valueClass)}>{children}</dd>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Property;
|
export default Property;
|
||||||
|
|||||||
@@ -3,33 +3,33 @@ import React from "react";
|
|||||||
import Property from "@/components/common/Property";
|
import Property from "@/components/common/Property";
|
||||||
|
|
||||||
const PropertyListItem: FunctionComponent<{
|
const PropertyListItem: FunctionComponent<{
|
||||||
title: string;
|
title: string;
|
||||||
children: string;
|
children: string;
|
||||||
}> = ({ title, children }) => {
|
}> = ({ title, children }) => {
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<span className="dashed" title={title}>
|
<span className="dashed" title={title}>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type PropertyListProps = {
|
type PropertyListProps = {
|
||||||
title: string;
|
title: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PropertyList: FunctionComponent<PropertyListProps> & {
|
const PropertyList: FunctionComponent<PropertyListProps> & {
|
||||||
Item: typeof PropertyListItem;
|
Item: typeof PropertyListItem;
|
||||||
} = ({ title, children }) => {
|
} = ({ title, children }) => {
|
||||||
return (
|
return (
|
||||||
<Property title={title}>
|
<Property title={title}>
|
||||||
<ul key={2} className="list-disc">
|
<ul key={2} className="list-disc">
|
||||||
{children}
|
{children}
|
||||||
</ul>
|
</ul>
|
||||||
</Property>
|
</Property>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
PropertyList.Item = PropertyListItem;
|
PropertyList.Item = PropertyListItem;
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ import { Fragment, useState } from "react";
|
|||||||
import { onPromise, preventDefault } from "@/helpers";
|
import { onPromise, preventDefault } from "@/helpers";
|
||||||
import type { SimplifiedTargetType, SubmitProps, TargetType } from "@/types";
|
import type { SimplifiedTargetType, SubmitProps, TargetType } from "@/types";
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronUpDownIcon,
|
ChevronUpDownIcon,
|
||||||
LockClosedIcon,
|
LockClosedIcon,
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
ArrowPathIcon,
|
ArrowPathIcon,
|
||||||
} from "@heroicons/react/20/solid";
|
} from "@heroicons/react/20/solid";
|
||||||
import {
|
import {
|
||||||
Listbox,
|
Listbox,
|
||||||
ListboxButton,
|
ListboxButton,
|
||||||
ListboxOptions,
|
ListboxOptions,
|
||||||
ListboxOption,
|
ListboxOption,
|
||||||
Transition,
|
Transition,
|
||||||
} from "@headlessui/react";
|
} from "@headlessui/react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { Maybe } from "true-myth";
|
import type { Maybe } from "true-myth";
|
||||||
@@ -25,304 +25,289 @@ import { placeholders } from "@/constants";
|
|||||||
* Props for the LookupInput component.
|
* Props for the LookupInput component.
|
||||||
*/
|
*/
|
||||||
type LookupInputProps = {
|
type LookupInputProps = {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
/**
|
/**
|
||||||
* Callback function called when a type of registry is detected when a user changes their input.
|
* Callback function called when a type of registry is detected when a user changes their input.
|
||||||
* @param type - The detected type of registry.
|
* @param type - The detected type of registry.
|
||||||
* @returns A promise.
|
* @returns A promise.
|
||||||
*/
|
*/
|
||||||
onRegistry?: (type: TargetType) => Promise<void>;
|
onRegistry?: (type: TargetType) => Promise<void>;
|
||||||
/**
|
/**
|
||||||
* Callback function called when a user hits submit.
|
* Callback function called when a user hits submit.
|
||||||
* @param props - The submit props.
|
* @param props - The submit props.
|
||||||
* @returns A promise.
|
* @returns A promise.
|
||||||
*/
|
*/
|
||||||
onSubmit?: (props: SubmitProps) => Promise<void>;
|
onSubmit?: (props: SubmitProps) => Promise<void>;
|
||||||
/**
|
/**
|
||||||
* Callback function called when a user changes their input (text search) or explicitly changes the type of search.
|
* 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.
|
* @param target - The target object containing the search target and target type.
|
||||||
* @returns Nothing.
|
* @returns Nothing.
|
||||||
*/
|
*/
|
||||||
onChange?: (target: {
|
onChange?: (target: { target: string; targetType: TargetType | null }) => Promise<void>;
|
||||||
target: string;
|
detectedType: Maybe<TargetType>;
|
||||||
targetType: TargetType | null;
|
|
||||||
}) => Promise<void>;
|
|
||||||
detectedType: Maybe<TargetType>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const LookupInput: FunctionComponent<LookupInputProps> = ({
|
const LookupInput: FunctionComponent<LookupInputProps> = ({
|
||||||
isLoading,
|
isLoading,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onChange,
|
onChange,
|
||||||
detectedType,
|
detectedType,
|
||||||
}: LookupInputProps) => {
|
}: LookupInputProps) => {
|
||||||
const { register, handleSubmit, getValues } = useForm<SubmitProps>({
|
const { register, handleSubmit, getValues } = useForm<SubmitProps>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
target: "",
|
target: "",
|
||||||
// Not used at this time.
|
// Not used at this time.
|
||||||
followReferral: false,
|
followReferral: false,
|
||||||
requestJSContact: false,
|
requestJSContact: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A mapping of available (simple) target types to their long-form human-readable names.
|
* A mapping of available (simple) target types to their long-form human-readable names.
|
||||||
*/
|
*/
|
||||||
const objectNames: Record<SimplifiedTargetType | "auto", string> = {
|
const objectNames: Record<SimplifiedTargetType | "auto", string> = {
|
||||||
auto: "Autodetect",
|
auto: "Autodetect",
|
||||||
domain: "Domain",
|
domain: "Domain",
|
||||||
ip: "IP/CIDR", // IPv4/IPv6 are combined into this option
|
ip: "IP/CIDR", // IPv4/IPv6 are combined into this option
|
||||||
tld: "TLD",
|
tld: "TLD",
|
||||||
autnum: "AS Number",
|
autnum: "AS Number",
|
||||||
entity: "Entity Handle",
|
entity: "Entity Handle",
|
||||||
registrar: "Registrar",
|
registrar: "Registrar",
|
||||||
url: "URL",
|
url: "URL",
|
||||||
json: "JSON",
|
json: "JSON",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapping of precise target types to their simplified short-form names.
|
* Mapping of precise target types to their simplified short-form names.
|
||||||
*/
|
*/
|
||||||
const targetShortNames: Record<TargetType, string> = {
|
const targetShortNames: Record<TargetType, string> = {
|
||||||
domain: "Domain",
|
domain: "Domain",
|
||||||
tld: "TLD",
|
tld: "TLD",
|
||||||
ip4: "IPv4",
|
ip4: "IPv4",
|
||||||
ip6: "IPv6",
|
ip6: "IPv6",
|
||||||
autnum: "ASN",
|
autnum: "ASN",
|
||||||
entity: "Entity",
|
entity: "Entity",
|
||||||
registrar: "Registrar",
|
registrar: "Registrar",
|
||||||
url: "URL",
|
url: "URL",
|
||||||
json: "JSON",
|
json: "JSON",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the selected value in the LookupInput component.
|
* Represents the selected value in the LookupInput component.
|
||||||
*/
|
*/
|
||||||
const [selected, setSelected] = useState<SimplifiedTargetType | "auto">(
|
const [selected, setSelected] = useState<SimplifiedTargetType | "auto">("auto");
|
||||||
"auto"
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the target type based on the provided value.
|
* Retrieves the target type based on the provided value.
|
||||||
* @param value - The value to retrieve the target type for.
|
* @param value - The value to retrieve the target type for.
|
||||||
* @returns The target type as ObjectType or null.
|
* @returns The target type as ObjectType or null.
|
||||||
*/
|
*/
|
||||||
function retrieveTargetType(value?: string | null): TargetType | null {
|
function retrieveTargetType(value?: string | null): TargetType | null {
|
||||||
// If the value is null and the selected value is null, return null.
|
// If the value is null and the selected value is null, return null.
|
||||||
if (value == null) value = selected;
|
if (value == null) value = selected;
|
||||||
|
|
||||||
// 'auto' means 'do whatever' so we return null.
|
// 'auto' means 'do whatever' so we return null.
|
||||||
if (value == "auto") return null;
|
if (value == "auto") return null;
|
||||||
|
|
||||||
return value as TargetType;
|
return value as TargetType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchIcon = (
|
const searchIcon = (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={cn({
|
className={cn({
|
||||||
"absolute inset-y-0 left-0 flex items-center pl-3": true,
|
"absolute inset-y-0 left-0 flex items-center pl-3": true,
|
||||||
"pointer-events-none": isLoading,
|
"pointer-events-none": isLoading,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<ArrowPathIcon
|
<ArrowPathIcon
|
||||||
className="h-5 w-5 animate-spin text-zinc-400"
|
className="h-5 w-5 animate-spin text-zinc-400"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<MagnifyingGlassIcon
|
<MagnifyingGlassIcon className="h-5 w-5 text-zinc-400" aria-hidden="true" />
|
||||||
className="h-5 w-5 text-zinc-400"
|
)}
|
||||||
aria-hidden="true"
|
</button>
|
||||||
/>
|
</>
|
||||||
)}
|
);
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const searchInput = (
|
const searchInput = (
|
||||||
<input
|
<input
|
||||||
className={cn(
|
className={cn(
|
||||||
"lg:py-4.5 block w-full rounded-l-md border border-transparent",
|
"block w-full rounded-l-md border border-transparent lg:py-4.5",
|
||||||
"bg-zinc-700 py-2 pl-10 pr-1.5 text-sm placeholder-zinc-400 placeholder:translate-y-2 focus:text-zinc-200",
|
"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"
|
"focus:outline-hidden sm:text-sm md:py-3 md:text-base lg:text-lg"
|
||||||
)}
|
)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
placeholder={placeholders[selected]}
|
placeholder={placeholders[selected]}
|
||||||
type="search"
|
type="search"
|
||||||
{...register("target", {
|
{...register("target", {
|
||||||
required: true,
|
required: true,
|
||||||
onChange: () => {
|
onChange: () => {
|
||||||
if (onChange != undefined)
|
if (onChange != undefined)
|
||||||
void onChange({
|
void onChange({
|
||||||
target: getValues("target"),
|
target: getValues("target"),
|
||||||
// dropdown target will be pulled from state anyways, so no need to provide it here
|
// dropdown target will be pulled from state anyways, so no need to provide it here
|
||||||
targetType: retrieveTargetType(null),
|
targetType: retrieveTargetType(null),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const dropdown = (
|
const dropdown = (
|
||||||
<Listbox
|
<Listbox
|
||||||
value={selected}
|
value={selected}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setSelected(value);
|
setSelected(value);
|
||||||
|
|
||||||
if (onChange != undefined)
|
if (onChange != undefined)
|
||||||
void onChange({
|
void onChange({
|
||||||
target: getValues("target"),
|
target: getValues("target"),
|
||||||
// we provide the value as the state will not have updated yet for this context
|
// we provide the value as the state will not have updated yet for this context
|
||||||
targetType: retrieveTargetType(value),
|
targetType: retrieveTargetType(value),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
className={cn(
|
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",
|
"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",
|
"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 "
|
"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. */}
|
{/* Fetch special text for 'auto' mode, otherwise just use the options. */}
|
||||||
<span className="block">
|
<span className="block">
|
||||||
{selected == "auto" ? (
|
{selected == "auto" ? (
|
||||||
// If the detected type was provided, then notate which in parentheses. Compact object naming might be better in the future.
|
// If the detected type was provided, then notate which in parentheses. Compact object naming might be better in the future.
|
||||||
detectedType.isJust ? (
|
detectedType.isJust ? (
|
||||||
<>
|
<>
|
||||||
Auto (
|
Auto (
|
||||||
<span className="animate-pulse">
|
<span className="animate-pulse">
|
||||||
{targetShortNames[detectedType.value]}
|
{targetShortNames[detectedType.value]}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
objectNames["auto"]
|
objectNames["auto"]
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<LockClosedIcon
|
<LockClosedIcon
|
||||||
className="mb-1 mr-2.5 inline h-4 w-4 animate-pulse text-zinc-500"
|
className="mr-2.5 mb-1 inline h-4 w-4 animate-pulse text-zinc-500"
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
{objectNames[selected]}
|
{objectNames[selected]}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
<ChevronUpDownIcon
|
<ChevronUpDownIcon className="h-5 w-5 text-zinc-200" aria-hidden="true" />
|
||||||
className="h-5 w-5 text-zinc-200"
|
</span>
|
||||||
aria-hidden="true"
|
</ListboxButton>
|
||||||
/>
|
<Transition
|
||||||
</span>
|
as={Fragment}
|
||||||
</ListboxButton>
|
leave="transition ease-in duration-100"
|
||||||
<Transition
|
leaveFrom="opacity-100"
|
||||||
as={Fragment}
|
leaveTo="opacity-0"
|
||||||
leave="transition ease-in duration-100"
|
>
|
||||||
leaveFrom="opacity-100"
|
<ListboxOptions
|
||||||
leaveTo="opacity-0"
|
className={cn(
|
||||||
>
|
"scrollbar-thin absolute right-0 mt-1 max-h-60 min-w-full overflow-auto rounded-md bg-zinc-700 py-1",
|
||||||
<ListboxOptions
|
"text-zinc-200 shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm"
|
||||||
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}
|
||||||
{Object.entries(objectNames).map(([key, value]) => (
|
className={({ focus }) =>
|
||||||
<ListboxOption
|
cn(
|
||||||
key={key}
|
"relative cursor-default py-2 pr-4 pl-10 select-none",
|
||||||
className={({ focus }) =>
|
focus ? "bg-zinc-800 text-zinc-300" : null
|
||||||
cn(
|
)
|
||||||
"relative cursor-default select-none py-2 pl-10 pr-4",
|
}
|
||||||
focus ? "bg-zinc-800 text-zinc-300" : null
|
value={key}
|
||||||
)
|
>
|
||||||
}
|
{({ selected }) => (
|
||||||
value={key}
|
<>
|
||||||
>
|
<span
|
||||||
{({ selected }) => (
|
className={cn(
|
||||||
<>
|
"block text-right text-xs whitespace-nowrap md:text-sm lg:text-base",
|
||||||
<span
|
selected ? "font-medium" : null
|
||||||
className={cn(
|
)}
|
||||||
"block whitespace-nowrap text-right text-xs md:text-sm lg:text-base",
|
>
|
||||||
selected ? "font-medium" : null
|
{value}
|
||||||
)}
|
</span>
|
||||||
>
|
{selected ? (
|
||||||
{value}
|
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-blue-500">
|
||||||
</span>
|
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
{selected ? (
|
</span>
|
||||||
<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" />
|
<button
|
||||||
</span>
|
onClick={(e) => {
|
||||||
) : (
|
e.preventDefault();
|
||||||
<button
|
console.log("TODO: Show Help Explanation");
|
||||||
onClick={(e) => {
|
}}
|
||||||
e.preventDefault();
|
className="absolute inset-y-0 left-0 flex items-center pl-4 text-lg font-bold opacity-20 hover:animate-pulse"
|
||||||
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>
|
||||||
>
|
)}
|
||||||
?
|
</>
|
||||||
</button>
|
)}
|
||||||
)}
|
</ListboxOption>
|
||||||
</>
|
))}
|
||||||
)}
|
</ListboxOptions>
|
||||||
</ListboxOption>
|
</Transition>
|
||||||
))}
|
</div>
|
||||||
</ListboxOptions>
|
</Listbox>
|
||||||
</Transition>
|
);
|
||||||
</div>
|
|
||||||
</Listbox>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="pb-3"
|
className="pb-3"
|
||||||
onSubmit={
|
onSubmit={onSubmit != undefined ? onPromise(handleSubmit(onSubmit)) : preventDefault}
|
||||||
onSubmit != undefined
|
>
|
||||||
? onPromise(handleSubmit(onSubmit))
|
<div className="col">
|
||||||
: preventDefault
|
<label htmlFor="search" className="sr-only">
|
||||||
}
|
Search
|
||||||
>
|
</label>
|
||||||
<div className="col">
|
<div className="relative flex">
|
||||||
<label htmlFor="search" className="sr-only">
|
{searchIcon}
|
||||||
Search
|
{searchInput}
|
||||||
</label>
|
{dropdown}
|
||||||
<div className="relative flex">
|
</div>
|
||||||
{searchIcon}
|
</div>
|
||||||
{searchInput}
|
<div className="col">
|
||||||
{dropdown}
|
<div className="flex flex-wrap pt-3 pb-1 text-sm">
|
||||||
</div>
|
<div className="whitespace-nowrap">
|
||||||
</div>
|
<input
|
||||||
<div className="col">
|
className="mr-1 ml-2 whitespace-nowrap text-zinc-800 accent-blue-700"
|
||||||
<div className="flex flex-wrap pb-1 pt-3 text-sm">
|
type="checkbox"
|
||||||
<div className="whitespace-nowrap">
|
{...register("requestJSContact")}
|
||||||
<input
|
/>
|
||||||
className="ml-2 mr-1 whitespace-nowrap text-zinc-800 accent-blue-700"
|
<label className="text-zinc-300" htmlFor="requestJSContact">
|
||||||
type="checkbox"
|
Request JSContact
|
||||||
{...register("requestJSContact")}
|
</label>
|
||||||
/>
|
</div>
|
||||||
<label className="text-zinc-300" htmlFor="requestJSContact">
|
<div className="whitespace-nowrap">
|
||||||
Request JSContact
|
<input
|
||||||
</label>
|
className="mr-1 ml-2 bg-zinc-500 text-inherit accent-blue-700"
|
||||||
</div>
|
type="checkbox"
|
||||||
<div className="whitespace-nowrap">
|
{...register("followReferral")}
|
||||||
<input
|
/>
|
||||||
className="ml-2 mr-1 bg-zinc-500 text-inherit accent-blue-700"
|
<label className="text-zinc-300" htmlFor="followReferral">
|
||||||
type="checkbox"
|
Follow referral to registrar's RDAP record
|
||||||
{...register("followReferral")}
|
</label>
|
||||||
/>
|
</div>
|
||||||
<label className="text-zinc-300" htmlFor="followReferral">
|
</div>
|
||||||
Follow referral to registrar's RDAP record
|
</div>
|
||||||
</label>
|
</form>
|
||||||
</div>
|
);
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LookupInput;
|
export default LookupInput;
|
||||||
|
|||||||
@@ -7,54 +7,51 @@ import PropertyList from "@/components/common/PropertyList";
|
|||||||
import AbstractCard from "@/components/common/AbstractCard";
|
import AbstractCard from "@/components/common/AbstractCard";
|
||||||
|
|
||||||
export type AutnumCardProps = {
|
export type AutnumCardProps = {
|
||||||
data: AutonomousNumber;
|
data: AutonomousNumber;
|
||||||
url?: string;
|
url?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AutnumCard: FunctionComponent<AutnumCardProps> = ({
|
const AutnumCard: FunctionComponent<AutnumCardProps> = ({ data, url }: AutnumCardProps) => {
|
||||||
data,
|
const asnRange =
|
||||||
url,
|
data.startAutnum === data.endAutnum
|
||||||
}: AutnumCardProps) => {
|
? `AS${data.startAutnum}`
|
||||||
const asnRange =
|
: `AS${data.startAutnum}-AS${data.endAutnum}`;
|
||||||
data.startAutnum === data.endAutnum
|
|
||||||
? `AS${data.startAutnum}`
|
|
||||||
: `AS${data.startAutnum}-AS${data.endAutnum}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AbstractCard
|
<AbstractCard
|
||||||
data={data}
|
data={data}
|
||||||
url={url}
|
url={url}
|
||||||
header={
|
header={
|
||||||
<>
|
<>
|
||||||
<span className="font-mono tracking-tighter">AUTONOMOUS SYSTEM</span>
|
<span className="font-mono tracking-tighter">AUTONOMOUS SYSTEM</span>
|
||||||
<span className="font-mono tracking-wide">{asnRange}</span>
|
<span className="font-mono tracking-wide">{asnRange}</span>
|
||||||
<span className="whitespace-nowrap">({data.handle})</span>
|
<span className="whitespace-nowrap">({data.handle})</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<dl>
|
<dl>
|
||||||
<Property title="Name">{data.name}</Property>
|
<Property title="Name">{data.name}</Property>
|
||||||
<Property title="Handle">{data.handle}</Property>
|
<Property title="Handle">{data.handle}</Property>
|
||||||
<Property title="ASN Range">
|
<Property title="ASN Range">
|
||||||
{data.startAutnum === data.endAutnum
|
{data.startAutnum === data.endAutnum
|
||||||
? `AS${data.startAutnum}`
|
? `AS${data.startAutnum}`
|
||||||
: `AS${data.startAutnum} - AS${data.endAutnum}`}
|
: `AS${data.startAutnum} - AS${data.endAutnum}`}
|
||||||
</Property>
|
</Property>
|
||||||
<Property title="Type">{data.type}</Property>
|
<Property title="Type">{data.type}</Property>
|
||||||
<Property title="Country">{data.country.toUpperCase()}</Property>
|
<Property title="Country">{data.country.toUpperCase()}</Property>
|
||||||
<Property title="Events">
|
<Property title="Events">
|
||||||
<Events key={0} data={data.events} />
|
<Events key={0} data={data.events} />
|
||||||
</Property>
|
</Property>
|
||||||
<PropertyList title="Status">
|
<PropertyList title="Status">
|
||||||
{data.status.map((status, index) => (
|
{data.status.map((status, index) => (
|
||||||
<PropertyList.Item key={index} title={status}>
|
<PropertyList.Item key={index} title={status}>
|
||||||
{status}
|
{status}
|
||||||
</PropertyList.Item>
|
</PropertyList.Item>
|
||||||
))}
|
))}
|
||||||
</PropertyList>
|
</PropertyList>
|
||||||
</dl>
|
</dl>
|
||||||
</AbstractCard>
|
</AbstractCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AutnumCard;
|
export default AutnumCard;
|
||||||
|
|||||||
@@ -8,49 +8,46 @@ import PropertyList from "@/components/common/PropertyList";
|
|||||||
import AbstractCard from "@/components/common/AbstractCard";
|
import AbstractCard from "@/components/common/AbstractCard";
|
||||||
|
|
||||||
export type DomainProps = {
|
export type DomainProps = {
|
||||||
data: Domain;
|
data: Domain;
|
||||||
url?: string;
|
url?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DomainCard: FunctionComponent<DomainProps> = ({
|
const DomainCard: FunctionComponent<DomainProps> = ({ data, url }: DomainProps) => {
|
||||||
data,
|
return (
|
||||||
url,
|
<AbstractCard
|
||||||
}: DomainProps) => {
|
data={data}
|
||||||
return (
|
url={url}
|
||||||
<AbstractCard
|
header={
|
||||||
data={data}
|
<>
|
||||||
url={url}
|
<span className="font-mono tracking-tighter">DOMAIN</span>
|
||||||
header={
|
<span className="font-mono tracking-wide">
|
||||||
<>
|
{data.ldhName ?? data.unicodeName}
|
||||||
<span className="font-mono tracking-tighter">DOMAIN</span>
|
</span>
|
||||||
<span className="font-mono tracking-wide">
|
<span className="whitespace-nowrap">({data.handle})</span>
|
||||||
{data.ldhName ?? data.unicodeName}
|
</>
|
||||||
</span>
|
}
|
||||||
<span className="whitespace-nowrap">({data.handle})</span>
|
>
|
||||||
</>
|
<dl>
|
||||||
}
|
{data.unicodeName != undefined ? (
|
||||||
>
|
<Property title="Unicode Name">{data.unicodeName}</Property>
|
||||||
<dl>
|
) : null}
|
||||||
{data.unicodeName != undefined ? (
|
<Property title={data.unicodeName != undefined ? "LHD Name" : "Name"}>
|
||||||
<Property title="Unicode Name">{data.unicodeName}</Property>
|
{data.ldhName}
|
||||||
) : null}
|
</Property>
|
||||||
<Property title={data.unicodeName != undefined ? "LHD Name" : "Name"}>
|
<Property title="Handle">{data.handle}</Property>
|
||||||
{data.ldhName}
|
<Property title="Events">
|
||||||
</Property>
|
<Events key={0} data={data.events} />
|
||||||
<Property title="Handle">{data.handle}</Property>
|
</Property>
|
||||||
<Property title="Events">
|
<PropertyList title="Status">
|
||||||
<Events key={0} data={data.events} />
|
{data.status.map((statusKey, index) => (
|
||||||
</Property>
|
<PropertyList.Item key={index} title={rdapStatusInfo[statusKey]}>
|
||||||
<PropertyList title="Status">
|
{statusKey}
|
||||||
{data.status.map((statusKey, index) => (
|
</PropertyList.Item>
|
||||||
<PropertyList.Item key={index} title={rdapStatusInfo[statusKey]}>
|
))}
|
||||||
{statusKey}
|
</PropertyList>
|
||||||
</PropertyList.Item>
|
</dl>
|
||||||
))}
|
</AbstractCard>
|
||||||
</PropertyList>
|
);
|
||||||
</dl>
|
|
||||||
</AbstractCard>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DomainCard;
|
export default DomainCard;
|
||||||
|
|||||||
@@ -4,24 +4,24 @@ import { Fragment } from "react";
|
|||||||
import DynamicDate from "@/components/common/DynamicDate";
|
import DynamicDate from "@/components/common/DynamicDate";
|
||||||
|
|
||||||
export type EventsProps = {
|
export type EventsProps = {
|
||||||
data: Event[];
|
data: Event[];
|
||||||
};
|
};
|
||||||
const Events: FunctionComponent<EventsProps> = ({ data }) => {
|
const Events: FunctionComponent<EventsProps> = ({ data }) => {
|
||||||
return (
|
return (
|
||||||
<dl>
|
<dl>
|
||||||
{data.map(({ eventAction, eventDate, eventActor }, index) => {
|
{data.map(({ eventAction, eventDate, eventActor }, index) => {
|
||||||
return (
|
return (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
<dt className="font-weight-bolder">{eventAction}:</dt>
|
<dt className="font-weight-bolder">{eventAction}:</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<DynamicDate value={new Date(eventDate)} />
|
<DynamicDate value={new Date(eventDate)} />
|
||||||
{eventActor != null ? ` (by ${eventActor})` : null}
|
{eventActor != null ? ` (by ${eventActor})` : null}
|
||||||
</dd>
|
</dd>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</dl>
|
</dl>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Events;
|
export default Events;
|
||||||
|
|||||||
@@ -2,53 +2,39 @@ import type { FunctionComponent } from "react";
|
|||||||
import DomainCard from "@/components/lookup/DomainCard";
|
import DomainCard from "@/components/lookup/DomainCard";
|
||||||
import IPCard from "@/components/lookup/IPCard";
|
import IPCard from "@/components/lookup/IPCard";
|
||||||
import AutnumCard from "@/components/lookup/AutnumCard";
|
import AutnumCard from "@/components/lookup/AutnumCard";
|
||||||
import type {
|
import type { Domain, AutonomousNumber, Entity, Nameserver, IpNetwork } from "@/types";
|
||||||
Domain,
|
|
||||||
AutonomousNumber,
|
|
||||||
Entity,
|
|
||||||
Nameserver,
|
|
||||||
IpNetwork,
|
|
||||||
} from "@/types";
|
|
||||||
import AbstractCard from "@/components/common/AbstractCard";
|
import AbstractCard from "@/components/common/AbstractCard";
|
||||||
|
|
||||||
export type ParsedGeneric =
|
export type ParsedGeneric = Domain | Nameserver | Entity | AutonomousNumber | IpNetwork;
|
||||||
| Domain
|
|
||||||
| Nameserver
|
|
||||||
| Entity
|
|
||||||
| AutonomousNumber
|
|
||||||
| IpNetwork;
|
|
||||||
|
|
||||||
export type ObjectProps = {
|
export type ObjectProps = {
|
||||||
data: ParsedGeneric;
|
data: ParsedGeneric;
|
||||||
url?: string;
|
url?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Generic: FunctionComponent<ObjectProps> = ({
|
const Generic: FunctionComponent<ObjectProps> = ({ data, url }: ObjectProps) => {
|
||||||
data,
|
switch (data.objectClassName) {
|
||||||
url,
|
case "domain":
|
||||||
}: ObjectProps) => {
|
return <DomainCard url={url} data={data} />;
|
||||||
switch (data.objectClassName) {
|
case "ip network":
|
||||||
case "domain":
|
return <IPCard url={url} data={data} />;
|
||||||
return <DomainCard url={url} data={data} />;
|
case "autnum":
|
||||||
case "ip network":
|
return <AutnumCard url={url} data={data} />;
|
||||||
return <IPCard url={url} data={data} />;
|
case "entity":
|
||||||
case "autnum":
|
case "nameserver":
|
||||||
return <AutnumCard url={url} data={data} />;
|
default:
|
||||||
case "entity":
|
return (
|
||||||
case "nameserver":
|
<AbstractCard url={url}>
|
||||||
default:
|
Not implemented. (<pre>{data.objectClassName ?? "null"}</pre>)
|
||||||
return (
|
</AbstractCard>
|
||||||
<AbstractCard url={url}>
|
);
|
||||||
Not implemented. (<pre>{data.objectClassName ?? "null"}</pre>)
|
}
|
||||||
</AbstractCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// const title: string = (data.unicodeName ?? data.ldhName ?? data.handle)?.toUpperCase() ?? "Response";
|
// const title: string = (data.unicodeName ?? data.ldhName ?? data.handle)?.toUpperCase() ?? "Response";
|
||||||
// return <div className="card">
|
// return <div className="card">
|
||||||
// <div className="card-header">{title}</div>
|
// <div className="card-header">{title}</div>
|
||||||
// {objectFragment}
|
// {objectFragment}
|
||||||
// </div>
|
// </div>
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Generic;
|
export default Generic;
|
||||||
|
|||||||
@@ -7,50 +7,50 @@ import PropertyList from "@/components/common/PropertyList";
|
|||||||
import AbstractCard from "@/components/common/AbstractCard";
|
import AbstractCard from "@/components/common/AbstractCard";
|
||||||
|
|
||||||
export type IPCardProps = {
|
export type IPCardProps = {
|
||||||
data: IpNetwork;
|
data: IpNetwork;
|
||||||
url?: string;
|
url?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IPCard: FunctionComponent<IPCardProps> = ({ data, url }: IPCardProps) => {
|
const IPCard: FunctionComponent<IPCardProps> = ({ data, url }: IPCardProps) => {
|
||||||
return (
|
return (
|
||||||
<AbstractCard
|
<AbstractCard
|
||||||
data={data}
|
data={data}
|
||||||
url={url}
|
url={url}
|
||||||
header={
|
header={
|
||||||
<>
|
<>
|
||||||
<span className="font-mono tracking-tighter">IP NETWORK</span>
|
<span className="font-mono tracking-tighter">IP NETWORK</span>
|
||||||
<span className="font-mono tracking-wide">
|
<span className="font-mono tracking-wide">
|
||||||
{data.startAddress}
|
{data.startAddress}
|
||||||
{data.startAddress !== data.endAddress && ` - ${data.endAddress}`}
|
{data.startAddress !== data.endAddress && ` - ${data.endAddress}`}
|
||||||
</span>
|
</span>
|
||||||
<span className="whitespace-nowrap">({data.handle})</span>
|
<span className="whitespace-nowrap">({data.handle})</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<dl>
|
<dl>
|
||||||
<Property title="Name">{data.name}</Property>
|
<Property title="Name">{data.name}</Property>
|
||||||
<Property title="Handle">{data.handle}</Property>
|
<Property title="Handle">{data.handle}</Property>
|
||||||
<Property title="IP Version">{data.ipVersion.toUpperCase()}</Property>
|
<Property title="IP Version">{data.ipVersion.toUpperCase()}</Property>
|
||||||
<Property title="Start Address">{data.startAddress}</Property>
|
<Property title="Start Address">{data.startAddress}</Property>
|
||||||
<Property title="End Address">{data.endAddress}</Property>
|
<Property title="End Address">{data.endAddress}</Property>
|
||||||
<Property title="Type">{data.type}</Property>
|
<Property title="Type">{data.type}</Property>
|
||||||
{data.country && <Property title="Country">{data.country}</Property>}
|
{data.country && <Property title="Country">{data.country}</Property>}
|
||||||
{data.parentHandle && (
|
{data.parentHandle && (
|
||||||
<Property title="Parent Handle">{data.parentHandle}</Property>
|
<Property title="Parent Handle">{data.parentHandle}</Property>
|
||||||
)}
|
)}
|
||||||
<Property title="Events">
|
<Property title="Events">
|
||||||
<Events key={0} data={data.events} />
|
<Events key={0} data={data.events} />
|
||||||
</Property>
|
</Property>
|
||||||
<PropertyList title="Status">
|
<PropertyList title="Status">
|
||||||
{data.status.map((status, index) => (
|
{data.status.map((status, index) => (
|
||||||
<PropertyList.Item key={index} title={status}>
|
<PropertyList.Item key={index} title={status}>
|
||||||
{status}
|
{status}
|
||||||
</PropertyList.Item>
|
</PropertyList.Item>
|
||||||
))}
|
))}
|
||||||
</PropertyList>
|
</PropertyList>
|
||||||
</dl>
|
</dl>
|
||||||
</AbstractCard>
|
</AbstractCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default IPCard;
|
export default IPCard;
|
||||||
|
|||||||
162
src/constants.ts
162
src/constants.ts
@@ -1,96 +1,88 @@
|
|||||||
// see https://www.iana.org/assignments/rdap-json-values
|
// see https://www.iana.org/assignments/rdap-json-values
|
||||||
import type {
|
import type { RdapStatusType, RootRegistryType, SimplifiedTargetType } from "@/types";
|
||||||
RdapStatusType,
|
|
||||||
RootRegistryType,
|
|
||||||
SimplifiedTargetType,
|
|
||||||
} from "@/types";
|
|
||||||
|
|
||||||
export const rdapStatusInfo: Record<RdapStatusType, string> = {
|
export const rdapStatusInfo: Record<RdapStatusType, string> = {
|
||||||
validated:
|
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.",
|
"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":
|
"renew prohibited": "Renewal or reregistration of the object instance is forbidden.",
|
||||||
"Renewal or reregistration of the object instance is forbidden.",
|
"update prohibited": "Updates to the object instance are forbidden.",
|
||||||
"update prohibited": "Updates to the object instance are forbidden.",
|
"transfer prohibited":
|
||||||
"transfer prohibited":
|
"Transfers of the registration from one registrar to another are forbidden. This type of status normally applies to DNR domain names.",
|
||||||
"Transfers of the registration from one registrar to another are forbidden. This type of status normally applies to DNR domain names.",
|
"delete prohibited":
|
||||||
"delete prohibited":
|
"Deletion of the registration of the object instance is forbidden. This type of status normally applies to DNR domain names.",
|
||||||
"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.",
|
||||||
proxy:
|
private:
|
||||||
"The registration of the object instance has been performed by a third party. This is most commonly applied to entities.",
|
"The information of the object instance is not designated for public consumption. This is most commonly applied to entities.",
|
||||||
private:
|
removed:
|
||||||
"The information of the object instance is not designated for public consumption. This is most commonly applied to entities.",
|
"Some of the information of the object instance has not been made available and has been removed. This is most commonly applied to entities.",
|
||||||
removed:
|
obscured:
|
||||||
"Some of the information of the object instance has not been made available and has been removed. This is most commonly applied to entities.",
|
"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.",
|
||||||
obscured:
|
associated:
|
||||||
"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.",
|
"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.",
|
||||||
associated:
|
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.",
|
||||||
"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.",
|
inactive: "The object instance is not in use. See 'active'.",
|
||||||
active:
|
locked: "Changes to the object instance cannot be made, including the association of other object instances.",
|
||||||
"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.",
|
"pending create":
|
||||||
inactive: "The object instance is not in use. See 'active'.",
|
"A request has been received for the creation of the object instance but this action is not yet complete.",
|
||||||
locked:
|
"pending renew":
|
||||||
"Changes to the object instance cannot be made, including the association of other object instances.",
|
"A request has been received for the renewal of the object instance but this action is not yet complete.",
|
||||||
"pending create":
|
"pending transfer":
|
||||||
"A request has been received for the creation of the object instance but this action is not yet complete.",
|
"A request has been received for the transfer of the object instance but this action is not yet complete.",
|
||||||
"pending renew":
|
"pending update":
|
||||||
"A request has been received for the renewal of the object instance but this action is not yet complete.",
|
"A request has been received for the update or modification of the object instance but this action is not yet complete.",
|
||||||
"pending transfer":
|
"pending delete":
|
||||||
"A request has been received for the transfer of the object instance but this action is not yet complete.",
|
"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.",
|
||||||
"pending update":
|
"add period":
|
||||||
"A request has been received for the update or modification of the object instance but this action is not yet complete.",
|
"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.",
|
||||||
"pending delete":
|
"auto renew period":
|
||||||
"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.",
|
"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.",
|
||||||
"add period":
|
"client delete prohibited":
|
||||||
"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.",
|
"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.",
|
||||||
"auto renew period":
|
"client hold":
|
||||||
"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.",
|
"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 delete prohibited":
|
"client renew 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.",
|
"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 hold":
|
"client transfer prohibited":
|
||||||
"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.",
|
"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 renew prohibited":
|
"client update 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.",
|
"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.",
|
||||||
"client transfer prohibited":
|
"pending restore":
|
||||||
"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.",
|
"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.",
|
||||||
"client update prohibited":
|
"redemption period":
|
||||||
"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.",
|
"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.",
|
||||||
"pending restore":
|
"renew period":
|
||||||
"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.",
|
"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.",
|
||||||
"redemption period":
|
"server delete prohibited":
|
||||||
"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.",
|
"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.",
|
||||||
"renew period":
|
"server renew prohibited":
|
||||||
"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.",
|
"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 delete prohibited":
|
"server transfer 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.",
|
"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 renew prohibited":
|
"server update 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.",
|
"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 transfer prohibited":
|
"server hold":
|
||||||
"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.",
|
"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.",
|
||||||
"server update prohibited":
|
"transfer period":
|
||||||
"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.",
|
"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.",
|
||||||
"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
|
// list of RDAP bootstrap registry URLs
|
||||||
export const registryURLs: Record<RootRegistryType, string> = {
|
export const registryURLs: Record<RootRegistryType, string> = {
|
||||||
autnum: "https://data.iana.org/rdap/asn.json",
|
autnum: "https://data.iana.org/rdap/asn.json",
|
||||||
domain: "https://data.iana.org/rdap/dns.json",
|
domain: "https://data.iana.org/rdap/dns.json",
|
||||||
ip4: "https://data.iana.org/rdap/ipv4.json",
|
ip4: "https://data.iana.org/rdap/ipv4.json",
|
||||||
ip6: "https://data.iana.org/rdap/ipv6.json",
|
ip6: "https://data.iana.org/rdap/ipv6.json",
|
||||||
entity: "https://data.iana.org/rdap/object-tags.json",
|
entity: "https://data.iana.org/rdap/object-tags.json",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const placeholders: Record<SimplifiedTargetType | "auto", string> = {
|
export const placeholders: Record<SimplifiedTargetType | "auto", string> = {
|
||||||
auto: "A domain, an IP address, a TLD, an RDAP URL...",
|
auto: "A domain, an IP address, a TLD, an RDAP URL...",
|
||||||
ip: "192.168.0.1/16 or 2001:db8::/32",
|
ip: "192.168.0.1/16 or 2001:db8::/32",
|
||||||
autnum: "AS27594",
|
autnum: "AS27594",
|
||||||
entity: "OPS4-RIPE",
|
entity: "OPS4-RIPE",
|
||||||
url: "https://rdap.org/domain/example.com",
|
url: "https://rdap.org/domain/example.com",
|
||||||
tld: ".dev",
|
tld: ".dev",
|
||||||
registrar: "9999",
|
registrar: "9999",
|
||||||
json: `{"objectClassName":"domain", ... }`,
|
json: `{"objectClassName":"domain", ... }`,
|
||||||
domain: "example.com",
|
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);
|
const _clientEnv = clientSchema.safeParse(clientEnv);
|
||||||
|
|
||||||
export const formatErrors = (
|
export const formatErrors = (
|
||||||
/** @type {import('zod').ZodFormattedError<Map<string,string>,string>} */
|
/** @type {import('zod').ZodFormattedError<Map<string,string>,string>} */
|
||||||
errors
|
errors
|
||||||
) =>
|
) =>
|
||||||
Object.entries(errors)
|
Object.entries(errors)
|
||||||
.map(([name, value]) => {
|
.map(([name, value]) => {
|
||||||
if (value && "_errors" in value)
|
if (value && "_errors" in value)
|
||||||
return `${String(name)}: ${value._errors.join(", ")}\n`;
|
return `${String(name)}: ${value._errors.join(", ")}\n`;
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
if (!_clientEnv.success) {
|
if (!_clientEnv.success) {
|
||||||
console.error(
|
console.error(
|
||||||
"❌ Invalid environment variables:\n",
|
"❌ Invalid environment variables:\n",
|
||||||
...formatErrors(_clientEnv.error.format())
|
...formatErrors(_clientEnv.error.format())
|
||||||
);
|
);
|
||||||
throw new Error("Invalid environment variables");
|
throw new Error("Invalid environment variables");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let key of Object.keys(_clientEnv.data)) {
|
for (let key of Object.keys(_clientEnv.data)) {
|
||||||
if (!key.startsWith("NEXT_PUBLIC_")) {
|
if (!key.startsWith("NEXT_PUBLIC_")) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`❌ Invalid public environment variable name: ${key}. It must begin with 'NEXT_PUBLIC_'`
|
`❌ 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;
|
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.
|
* This way you can ensure the app isn't built with invalid env vars.
|
||||||
*/
|
*/
|
||||||
export const serverSchema = z.object({
|
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 }}
|
* @type {{ [k in keyof z.infer<typeof serverSchema>]: z.infer<typeof serverSchema>[k] | undefined }}
|
||||||
*/
|
*/
|
||||||
export const serverEnv = {
|
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_`.
|
* To expose them to the client, prefix them with `NEXT_PUBLIC_`.
|
||||||
*/
|
*/
|
||||||
export const clientSchema = z.object({
|
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 }}
|
* @type {{ [k in keyof z.infer<typeof clientSchema>]: z.infer<typeof clientSchema>[k] | undefined }}
|
||||||
*/
|
*/
|
||||||
export const clientEnv = {
|
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);
|
const _serverEnv = serverSchema.safeParse(serverEnv);
|
||||||
|
|
||||||
if (!_serverEnv.success) {
|
if (!_serverEnv.success) {
|
||||||
console.error(
|
console.error(
|
||||||
"❌ Invalid environment variables:\n",
|
"❌ Invalid environment variables:\n",
|
||||||
...formatErrors(_serverEnv.error.format())
|
...formatErrors(_serverEnv.error.format())
|
||||||
);
|
);
|
||||||
throw new Error("Invalid environment variables");
|
throw new Error("Invalid environment variables");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let key of Object.keys(_serverEnv.data)) {
|
for (let key of Object.keys(_serverEnv.data)) {
|
||||||
if (key.startsWith("NEXT_PUBLIC_")) {
|
if (key.startsWith("NEXT_PUBLIC_")) {
|
||||||
console.warn("❌ You are exposing a server-side env-variable:", key);
|
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 };
|
export const env = { ..._serverEnv.data, ...clientEnv };
|
||||||
|
|||||||
@@ -2,143 +2,143 @@ import { describe, it, expect } from "vitest";
|
|||||||
import { asnInRange } from "./helpers";
|
import { asnInRange } from "./helpers";
|
||||||
|
|
||||||
describe("asnInRange", () => {
|
describe("asnInRange", () => {
|
||||||
describe("basic matching", () => {
|
describe("basic matching", () => {
|
||||||
it("should match ASN in single number range", () => {
|
it("should match ASN in single number range", () => {
|
||||||
expect(asnInRange(100, "100-200")).toBe(true);
|
expect(asnInRange(100, "100-200")).toBe(true);
|
||||||
expect(asnInRange(150, "100-200")).toBe(true);
|
expect(asnInRange(150, "100-200")).toBe(true);
|
||||||
expect(asnInRange(200, "100-200")).toBe(true);
|
expect(asnInRange(200, "100-200")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not match ASN outside single number range", () => {
|
it("should not match ASN outside single number range", () => {
|
||||||
expect(asnInRange(99, "100-200")).toBe(false);
|
expect(asnInRange(99, "100-200")).toBe(false);
|
||||||
expect(asnInRange(201, "100-200")).toBe(false);
|
expect(asnInRange(201, "100-200")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should match ASN at boundaries", () => {
|
it("should match ASN at boundaries", () => {
|
||||||
expect(asnInRange(1, "1-10")).toBe(true);
|
expect(asnInRange(1, "1-10")).toBe(true);
|
||||||
expect(asnInRange(10, "1-10")).toBe(true);
|
expect(asnInRange(10, "1-10")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should match single ASN (same start and end)", () => {
|
it("should match single ASN (same start and end)", () => {
|
||||||
expect(asnInRange(12345, "12345-12345")).toBe(true);
|
expect(asnInRange(12345, "12345-12345")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not match single ASN outside", () => {
|
it("should not match single ASN outside", () => {
|
||||||
expect(asnInRange(12346, "12345-12345")).toBe(false);
|
expect(asnInRange(12346, "12345-12345")).toBe(false);
|
||||||
expect(asnInRange(12344, "12345-12345")).toBe(false);
|
expect(asnInRange(12344, "12345-12345")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("real-world ASN ranges from IANA", () => {
|
describe("real-world ASN ranges from IANA", () => {
|
||||||
// ARIN ranges
|
// ARIN ranges
|
||||||
it("should match ARIN ASN ranges", () => {
|
it("should match ARIN ASN ranges", () => {
|
||||||
// ARIN typically has ranges like 1-1876, 1902-2042, etc.
|
// ARIN typically has ranges like 1-1876, 1902-2042, etc.
|
||||||
expect(asnInRange(100, "1-1876")).toBe(true);
|
expect(asnInRange(100, "1-1876")).toBe(true);
|
||||||
expect(asnInRange(1876, "1-1876")).toBe(true);
|
expect(asnInRange(1876, "1-1876")).toBe(true);
|
||||||
expect(asnInRange(2000, "1902-2042")).toBe(true);
|
expect(asnInRange(2000, "1902-2042")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// RIPE ranges
|
// RIPE ranges
|
||||||
it("should match RIPE ASN ranges", () => {
|
it("should match RIPE ASN ranges", () => {
|
||||||
// RIPE has ranges like 1877-1901, 2043-2109, etc.
|
// RIPE has ranges like 1877-1901, 2043-2109, etc.
|
||||||
expect(asnInRange(1900, "1877-1901")).toBe(true);
|
expect(asnInRange(1900, "1877-1901")).toBe(true);
|
||||||
expect(asnInRange(2100, "2043-2109")).toBe(true);
|
expect(asnInRange(2100, "2043-2109")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// APNIC ranges
|
// APNIC ranges
|
||||||
it("should match APNIC ASN ranges", () => {
|
it("should match APNIC ASN ranges", () => {
|
||||||
// APNIC has ranges like 2110-2136, 4608-4864, etc.
|
// APNIC has ranges like 2110-2136, 4608-4864, etc.
|
||||||
expect(asnInRange(2120, "2110-2136")).toBe(true);
|
expect(asnInRange(2120, "2110-2136")).toBe(true);
|
||||||
expect(asnInRange(4700, "4608-4864")).toBe(true);
|
expect(asnInRange(4700, "4608-4864")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Well-known ASNs
|
// Well-known ASNs
|
||||||
it("should match Google ASN (AS15169)", () => {
|
it("should match Google ASN (AS15169)", () => {
|
||||||
// Google's ASN 15169 falls in range that includes it
|
// Google's ASN 15169 falls in range that includes it
|
||||||
expect(asnInRange(15169, "15000-16000")).toBe(true);
|
expect(asnInRange(15169, "15000-16000")).toBe(true);
|
||||||
expect(asnInRange(15169, "15169-15169")).toBe(true);
|
expect(asnInRange(15169, "15169-15169")).toBe(true);
|
||||||
expect(asnInRange(15169, "15360-16383")).toBe(false); // Not in this range
|
expect(asnInRange(15169, "15360-16383")).toBe(false); // Not in this range
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should match Cloudflare ASN (AS13335)", () => {
|
it("should match Cloudflare ASN (AS13335)", () => {
|
||||||
// Cloudflare's ASN 13335 should be in ARIN range 13312-18431
|
// Cloudflare's ASN 13335 should be in ARIN range 13312-18431
|
||||||
expect(asnInRange(13335, "13312-18431")).toBe(true);
|
expect(asnInRange(13335, "13312-18431")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should match Amazon ASN (AS16509)", () => {
|
it("should match Amazon ASN (AS16509)", () => {
|
||||||
// Amazon's ASN 16509
|
// Amazon's ASN 16509
|
||||||
expect(asnInRange(16509, "15360-16383")).toBe(false);
|
expect(asnInRange(16509, "15360-16383")).toBe(false);
|
||||||
expect(asnInRange(16509, "16384-18431")).toBe(true);
|
expect(asnInRange(16509, "16384-18431")).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("private ASN ranges", () => {
|
describe("private ASN ranges", () => {
|
||||||
it("should match 16-bit private ASN range", () => {
|
it("should match 16-bit private ASN range", () => {
|
||||||
// Private range: 64512-65534
|
// Private range: 64512-65534
|
||||||
expect(asnInRange(64512, "64512-65534")).toBe(true);
|
expect(asnInRange(64512, "64512-65534")).toBe(true);
|
||||||
expect(asnInRange(65000, "64512-65534")).toBe(true);
|
expect(asnInRange(65000, "64512-65534")).toBe(true);
|
||||||
expect(asnInRange(65534, "64512-65534")).toBe(true);
|
expect(asnInRange(65534, "64512-65534")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not match outside private range", () => {
|
it("should not match outside private range", () => {
|
||||||
expect(asnInRange(64511, "64512-65534")).toBe(false);
|
expect(asnInRange(64511, "64512-65534")).toBe(false);
|
||||||
expect(asnInRange(65535, "64512-65534")).toBe(false);
|
expect(asnInRange(65535, "64512-65534")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should match 32-bit private ASN range", () => {
|
it("should match 32-bit private ASN range", () => {
|
||||||
// Private range: 4200000000-4294967294
|
// Private range: 4200000000-4294967294
|
||||||
expect(asnInRange(4200000000, "4200000000-4294967294")).toBe(true);
|
expect(asnInRange(4200000000, "4200000000-4294967294")).toBe(true);
|
||||||
expect(asnInRange(4250000000, "4200000000-4294967294")).toBe(true);
|
expect(asnInRange(4250000000, "4200000000-4294967294")).toBe(true);
|
||||||
expect(asnInRange(4294967294, "4200000000-4294967294")).toBe(true);
|
expect(asnInRange(4294967294, "4200000000-4294967294")).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("large ASN numbers (32-bit)", () => {
|
describe("large ASN numbers (32-bit)", () => {
|
||||||
it("should handle large ASN numbers", () => {
|
it("should handle large ASN numbers", () => {
|
||||||
expect(asnInRange(4200000000, "4200000000-4294967294")).toBe(true);
|
expect(asnInRange(4200000000, "4200000000-4294967294")).toBe(true);
|
||||||
expect(asnInRange(4294967295, "4200000000-4294967294")).toBe(false);
|
expect(asnInRange(4294967295, "4200000000-4294967294")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle ASNs near 32-bit limit", () => {
|
it("should handle ASNs near 32-bit limit", () => {
|
||||||
const maxAsn = 4294967295;
|
const maxAsn = 4294967295;
|
||||||
expect(asnInRange(maxAsn, `${maxAsn}-${maxAsn}`)).toBe(true);
|
expect(asnInRange(maxAsn, `${maxAsn}-${maxAsn}`)).toBe(true);
|
||||||
expect(asnInRange(maxAsn - 1, `${maxAsn}-${maxAsn}`)).toBe(false);
|
expect(asnInRange(maxAsn - 1, `${maxAsn}-${maxAsn}`)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("edge cases", () => {
|
describe("edge cases", () => {
|
||||||
it("should handle invalid range format", () => {
|
it("should handle invalid range format", () => {
|
||||||
expect(asnInRange(100, "invalid")).toBe(false);
|
expect(asnInRange(100, "invalid")).toBe(false);
|
||||||
expect(asnInRange(100, "100")).toBe(false);
|
expect(asnInRange(100, "100")).toBe(false);
|
||||||
expect(asnInRange(100, "100-")).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", () => {
|
it("should handle negative numbers gracefully", () => {
|
||||||
expect(asnInRange(-1, "1-100")).toBe(false);
|
expect(asnInRange(-1, "1-100")).toBe(false);
|
||||||
expect(asnInRange(50, "-100-100")).toBe(false);
|
expect(asnInRange(50, "-100-100")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle reversed ranges (end < start)", () => {
|
it("should handle reversed ranges (end < start)", () => {
|
||||||
// Invalid range where end is less than start
|
// Invalid range where end is less than start
|
||||||
expect(asnInRange(150, "200-100")).toBe(false);
|
expect(asnInRange(150, "200-100")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle zero", () => {
|
it("should handle zero", () => {
|
||||||
expect(asnInRange(0, "0-100")).toBe(true);
|
expect(asnInRange(0, "0-100")).toBe(true);
|
||||||
expect(asnInRange(0, "1-100")).toBe(false);
|
expect(asnInRange(0, "1-100")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ASN number parsing", () => {
|
describe("ASN number parsing", () => {
|
||||||
it("should handle number inputs", () => {
|
it("should handle number inputs", () => {
|
||||||
expect(asnInRange(12345, "10000-20000")).toBe(true);
|
expect(asnInRange(12345, "10000-20000")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle very large numbers", () => {
|
it("should handle very large numbers", () => {
|
||||||
const largeAsn = 4000000000;
|
const largeAsn = 4000000000;
|
||||||
expect(asnInRange(largeAsn, "3000000000-4294967295")).toBe(true);
|
expect(asnInRange(largeAsn, "3000000000-4294967295")).toBe(true);
|
||||||
expect(asnInRange(largeAsn, "1-1000000000")).toBe(false);
|
expect(asnInRange(largeAsn, "1-1000000000")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,178 +2,169 @@ import { describe, it, expect } from "vitest";
|
|||||||
import { ipv4InCIDR, ipv6InCIDR } from "./helpers";
|
import { ipv4InCIDR, ipv6InCIDR } from "./helpers";
|
||||||
|
|
||||||
describe("ipv4InCIDR", () => {
|
describe("ipv4InCIDR", () => {
|
||||||
describe("basic matching", () => {
|
describe("basic matching", () => {
|
||||||
it("should match IP in /8 network", () => {
|
it("should match IP in /8 network", () => {
|
||||||
expect(ipv4InCIDR("8.8.8.8", "8.0.0.0/8")).toBe(true);
|
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.255.255.255", "8.0.0.0/8")).toBe(true);
|
||||||
expect(ipv4InCIDR("8.0.0.0", "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", () => {
|
it("should not match IP outside /8 network", () => {
|
||||||
expect(ipv4InCIDR("9.0.0.0", "8.0.0.0/8")).toBe(false);
|
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);
|
expect(ipv4InCIDR("7.255.255.255", "8.0.0.0/8")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should match IP in /16 network", () => {
|
it("should match IP in /16 network", () => {
|
||||||
expect(ipv4InCIDR("192.168.1.1", "192.168.0.0/16")).toBe(true);
|
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.255.255", "192.168.0.0/16")).toBe(true);
|
||||||
expect(ipv4InCIDR("192.168.0.0", "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", () => {
|
it("should not match IP outside /16 network", () => {
|
||||||
expect(ipv4InCIDR("192.169.1.1", "192.168.0.0/16")).toBe(false);
|
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);
|
expect(ipv4InCIDR("192.167.1.1", "192.168.0.0/16")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should match IP in /24 network", () => {
|
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.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.255", "192.168.1.0/24")).toBe(true);
|
||||||
expect(ipv4InCIDR("192.168.1.0", "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", () => {
|
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.2.1", "192.168.1.0/24")).toBe(false);
|
||||||
expect(ipv4InCIDR("192.168.0.255", "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)", () => {
|
it("should match IP in /32 network (single host)", () => {
|
||||||
expect(ipv4InCIDR("192.168.1.1", "192.168.1.1/32")).toBe(true);
|
expect(ipv4InCIDR("192.168.1.1", "192.168.1.1/32")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not match different IP in /32 network", () => {
|
it("should not match different IP in /32 network", () => {
|
||||||
expect(ipv4InCIDR("192.168.1.2", "192.168.1.1/32")).toBe(false);
|
expect(ipv4InCIDR("192.168.1.2", "192.168.1.1/32")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("real-world RDAP bootstrap ranges", () => {
|
describe("real-world RDAP bootstrap ranges", () => {
|
||||||
// ARIN ranges (from IANA bootstrap data)
|
// ARIN ranges (from IANA bootstrap data)
|
||||||
it("should match Google DNS (8.8.8.8) in ARIN range", () => {
|
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);
|
expect(ipv4InCIDR("8.8.8.8", "8.0.0.0/8")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// APNIC ranges
|
// APNIC ranges
|
||||||
it("should match Cloudflare DNS (1.1.1.1) in APNIC range", () => {
|
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);
|
expect(ipv4InCIDR("1.1.1.1", "1.0.0.0/8")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Private ranges
|
// Private ranges
|
||||||
it("should match private IPs in their ranges", () => {
|
it("should match private IPs in their ranges", () => {
|
||||||
expect(ipv4InCIDR("10.0.0.1", "10.0.0.0/8")).toBe(true);
|
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("172.16.0.1", "172.16.0.0/12")).toBe(true);
|
||||||
expect(ipv4InCIDR("192.168.0.1", "192.168.0.0/16")).toBe(true);
|
expect(ipv4InCIDR("192.168.0.1", "192.168.0.0/16")).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("edge cases", () => {
|
describe("edge cases", () => {
|
||||||
it("should handle /0 (all IPs)", () => {
|
it("should handle /0 (all IPs)", () => {
|
||||||
expect(ipv4InCIDR("0.0.0.0", "0.0.0.0/0")).toBe(true);
|
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("255.255.255.255", "0.0.0.0/0")).toBe(true);
|
||||||
expect(ipv4InCIDR("192.168.1.1", "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", () => {
|
it("should handle invalid CIDR notation", () => {
|
||||||
expect(ipv4InCIDR("192.168.1.1", "invalid")).toBe(false);
|
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/-1")).toBe(false);
|
||||||
expect(ipv4InCIDR("192.168.1.1", "192.168.1.0/33")).toBe(false);
|
expect(ipv4InCIDR("192.168.1.1", "192.168.1.0/33")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle malformed IPs", () => {
|
it("should handle malformed IPs", () => {
|
||||||
expect(ipv4InCIDR("invalid", "192.168.1.0/24")).toBe(false);
|
expect(ipv4InCIDR("invalid", "192.168.1.0/24")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle partial IPs (wrong number of octets)", () => {
|
it("should handle partial IPs (wrong number of octets)", () => {
|
||||||
expect(ipv4InCIDR("8.8", "8.0.0.0/8")).toBe(false);
|
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", "192.168.1.0/24")).toBe(false);
|
||||||
expect(ipv4InCIDR("192.168.1.1.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("ipv6InCIDR", () => {
|
||||||
describe("basic matching", () => {
|
describe("basic matching", () => {
|
||||||
it("should match IPv6 in /32 network", () => {
|
it("should match IPv6 in /32 network", () => {
|
||||||
expect(ipv6InCIDR("2001:db8::", "2001:db8::/32")).toBe(true);
|
expect(ipv6InCIDR("2001:db8::", "2001:db8::/32")).toBe(true);
|
||||||
expect(ipv6InCIDR("2001:db8:1234::", "2001:db8::/32")).toBe(true);
|
expect(ipv6InCIDR("2001:db8:1234::", "2001:db8::/32")).toBe(true);
|
||||||
expect(
|
expect(ipv6InCIDR("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff", "2001:db8::/32")).toBe(
|
||||||
ipv6InCIDR("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff", "2001:db8::/32")
|
true
|
||||||
).toBe(true);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not match IPv6 outside /32 network", () => {
|
it("should not match IPv6 outside /32 network", () => {
|
||||||
expect(ipv6InCIDR("2001:db9::", "2001:db8::/32")).toBe(false);
|
expect(ipv6InCIDR("2001:db9::", "2001:db8::/32")).toBe(false);
|
||||||
expect(ipv6InCIDR("2001:db7::", "2001:db8::/32")).toBe(false);
|
expect(ipv6InCIDR("2001:db7::", "2001:db8::/32")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should match IPv6 in /64 network", () => {
|
it("should match IPv6 in /64 network", () => {
|
||||||
expect(
|
expect(ipv6InCIDR("2001:db8:1234:5678::", "2001:db8:1234:5678::/64")).toBe(true);
|
||||||
ipv6InCIDR("2001:db8:1234:5678::", "2001:db8:1234:5678::/64")
|
expect(ipv6InCIDR("2001:db8:1234:5678:abcd::", "2001:db8:1234:5678::/64")).toBe(true);
|
||||||
).toBe(true);
|
expect(
|
||||||
expect(
|
ipv6InCIDR("2001:db8:1234:5678:ffff:ffff:ffff:ffff", "2001:db8:1234:5678::/64")
|
||||||
ipv6InCIDR("2001:db8:1234:5678:abcd::", "2001:db8:1234:5678::/64")
|
).toBe(true);
|
||||||
).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", () => {
|
it("should not match IPv6 outside /64 network", () => {
|
||||||
expect(
|
expect(ipv6InCIDR("2001:db8:1234:5679::", "2001:db8:1234:5678::/64")).toBe(false);
|
||||||
ipv6InCIDR("2001:db8:1234:5679::", "2001:db8:1234:5678::/64")
|
});
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should match IPv6 in /128 network (single host)", () => {
|
it("should match IPv6 in /128 network (single host)", () => {
|
||||||
expect(ipv6InCIDR("2001:db8::1", "2001:db8::1/128")).toBe(true);
|
expect(ipv6InCIDR("2001:db8::1", "2001:db8::1/128")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not match different IPv6 in /128 network", () => {
|
it("should not match different IPv6 in /128 network", () => {
|
||||||
expect(ipv6InCIDR("2001:db8::2", "2001:db8::1/128")).toBe(false);
|
expect(ipv6InCIDR("2001:db8::2", "2001:db8::1/128")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("real-world RDAP bootstrap ranges", () => {
|
describe("real-world RDAP bootstrap ranges", () => {
|
||||||
it("should match Google IPv6 DNS in ARIN range", () => {
|
it("should match Google IPv6 DNS in ARIN range", () => {
|
||||||
// Google DNS: 2001:4860:4860::8888
|
// Google DNS: 2001:4860:4860::8888
|
||||||
expect(ipv6InCIDR("2001:4860:4860::8888", "2001:4860::/32")).toBe(true);
|
expect(ipv6InCIDR("2001:4860:4860::8888", "2001:4860::/32")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should match Cloudflare IPv6 DNS in APNIC range", () => {
|
it("should match Cloudflare IPv6 DNS in APNIC range", () => {
|
||||||
// Cloudflare DNS: 2606:4700:4700::1111
|
// Cloudflare DNS: 2606:4700:4700::1111
|
||||||
expect(ipv6InCIDR("2606:4700:4700::1111", "2606:4700::/32")).toBe(true);
|
expect(ipv6InCIDR("2606:4700:4700::1111", "2606:4700::/32")).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("IPv6 shorthand notation", () => {
|
describe("IPv6 shorthand notation", () => {
|
||||||
it("should handle :: notation correctly", () => {
|
it("should handle :: notation correctly", () => {
|
||||||
expect(ipv6InCIDR("2001:db8::1", "2001:db8::/32")).toBe(true);
|
expect(ipv6InCIDR("2001:db8::1", "2001:db8::/32")).toBe(true);
|
||||||
expect(ipv6InCIDR("::1", "::1/128")).toBe(true);
|
expect(ipv6InCIDR("::1", "::1/128")).toBe(true);
|
||||||
expect(ipv6InCIDR("::", "::/128")).toBe(true);
|
expect(ipv6InCIDR("::", "::/128")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle expanded vs compressed notation", () => {
|
it("should handle expanded vs compressed notation", () => {
|
||||||
expect(
|
expect(ipv6InCIDR("2001:0db8:0000:0000:0000:0000:0000:0001", "2001:db8::/32")).toBe(
|
||||||
ipv6InCIDR("2001:0db8:0000:0000:0000:0000:0000:0001", "2001:db8::/32")
|
true
|
||||||
).toBe(true);
|
);
|
||||||
expect(
|
expect(ipv6InCIDR("2001:db8::1", "2001:0db8:0000:0000:0000:0000:0000:0000/32")).toBe(
|
||||||
ipv6InCIDR("2001:db8::1", "2001:0db8:0000:0000:0000:0000:0000:0000/32")
|
true
|
||||||
).toBe(true);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("edge cases", () => {
|
describe("edge cases", () => {
|
||||||
it("should handle invalid CIDR notation", () => {
|
it("should handle invalid CIDR notation", () => {
|
||||||
expect(ipv6InCIDR("2001:db8::1", "invalid")).toBe(false);
|
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::/-1")).toBe(false);
|
||||||
expect(ipv6InCIDR("2001:db8::1", "2001:db8::/129")).toBe(false);
|
expect(ipv6InCIDR("2001:db8::1", "2001:db8::/129")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle malformed IPv6", () => {
|
it("should handle malformed IPv6", () => {
|
||||||
expect(ipv6InCIDR("invalid", "2001:db8::/32")).toBe(false);
|
expect(ipv6InCIDR("invalid", "2001:db8::/32")).toBe(false);
|
||||||
expect(ipv6InCIDR("zzzz::1", "2001:db8::/32")).toBe(false);
|
expect(ipv6InCIDR("zzzz::1", "2001:db8::/32")).toBe(false);
|
||||||
expect(ipv6InCIDR("2001:xyz::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";
|
import type { Entries } from "type-fest";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface ObjectConstructor {
|
interface ObjectConstructor {
|
||||||
entries<T extends object>(o: T): Entries<T>;
|
entries<T extends object>(o: T): Entries<T>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function truthy(value: string | null | undefined) {
|
export function truthy(value: string | null | undefined) {
|
||||||
if (value == undefined) return false;
|
if (value == undefined) return false;
|
||||||
return value.toLowerCase() == "true" || value == "1";
|
return value.toLowerCase() == "true" || value == "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onPromise<T>(promise: (event: SyntheticEvent) => Promise<T>) {
|
export function onPromise<T>(promise: (event: SyntheticEvent) => Promise<T>) {
|
||||||
return (event: SyntheticEvent) => {
|
return (event: SyntheticEvent) => {
|
||||||
if (promise) {
|
if (promise) {
|
||||||
promise(event).catch((error) => {
|
promise(event).catch((error) => {
|
||||||
console.log("Unexpected error", 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.
|
* @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 = "...") {
|
export function truncated(input: string, maxLength: number, ellipsis = "...") {
|
||||||
if (maxLength <= 0) return "";
|
if (maxLength <= 0) return "";
|
||||||
if (input.length <= maxLength) return input;
|
if (input.length <= maxLength) return input;
|
||||||
return (
|
return input.substring(0, Math.max(0, maxLength - ellipsis.length)) + ellipsis;
|
||||||
input.substring(0, Math.max(0, maxLength - ellipsis.length)) + ellipsis
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function preventDefault(event: SyntheticEvent | Event) {
|
export function preventDefault(event: SyntheticEvent | Event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert an IPv4 address string to a 32-bit integer
|
* Convert an IPv4 address string to a 32-bit integer
|
||||||
*/
|
*/
|
||||||
function ipv4ToInt(ip: string): number {
|
function ipv4ToInt(ip: string): number {
|
||||||
const parts = ip.split(".").map(Number);
|
const parts = ip.split(".").map(Number);
|
||||||
if (parts.length !== 4) return 0;
|
if (parts.length !== 4) return 0;
|
||||||
const [a, b, c, d] = parts;
|
const [a, b, c, d] = parts;
|
||||||
if (a === undefined || b === undefined || c === undefined || d === undefined)
|
if (a === undefined || b === undefined || c === undefined || d === undefined) return 0;
|
||||||
return 0;
|
return ((a << 24) | (b << 16) | (c << 8) | d) >>> 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
|
* @returns true if the IP is within the CIDR range
|
||||||
*/
|
*/
|
||||||
export function ipv4InCIDR(ip: string, cidr: string): boolean {
|
export function ipv4InCIDR(ip: string, cidr: string): boolean {
|
||||||
const [rangeIp, prefixLenStr] = cidr.split("/");
|
const [rangeIp, prefixLenStr] = cidr.split("/");
|
||||||
const prefixLen = parseInt(prefixLenStr ?? "", 10);
|
const prefixLen = parseInt(prefixLenStr ?? "", 10);
|
||||||
|
|
||||||
if (!rangeIp || isNaN(prefixLen) || prefixLen < 0 || prefixLen > 32) {
|
if (!rangeIp || isNaN(prefixLen) || prefixLen < 0 || prefixLen > 32) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special case: /0 matches all IPs
|
// Special case: /0 matches all IPs
|
||||||
if (prefixLen === 0) {
|
if (prefixLen === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ipInt = ipv4ToInt(ip);
|
const ipInt = ipv4ToInt(ip);
|
||||||
const rangeInt = ipv4ToInt(rangeIp);
|
const rangeInt = ipv4ToInt(rangeIp);
|
||||||
const mask = (0xffffffff << (32 - prefixLen)) >>> 0;
|
const mask = (0xffffffff << (32 - prefixLen)) >>> 0;
|
||||||
|
|
||||||
return (ipInt & mask) === (rangeInt & mask);
|
return (ipInt & mask) === (rangeInt & mask);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert an IPv6 address to a BigInt representation
|
* Convert an IPv6 address to a BigInt representation
|
||||||
*/
|
*/
|
||||||
function ipv6ToBigInt(ip: string): bigint {
|
function ipv6ToBigInt(ip: string): bigint {
|
||||||
// Expand :: notation
|
// Expand :: notation
|
||||||
const expandedIp = expandIPv6(ip);
|
const expandedIp = expandIPv6(ip);
|
||||||
const parts = expandedIp.split(":");
|
const parts = expandedIp.split(":");
|
||||||
|
|
||||||
let result = BigInt(0);
|
let result = BigInt(0);
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
result = (result << BigInt(16)) | BigInt(parseInt(part, 16));
|
result = (result << BigInt(16)) | BigInt(parseInt(part, 16));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expand IPv6 address shorthand notation
|
* Expand IPv6 address shorthand notation
|
||||||
*/
|
*/
|
||||||
function expandIPv6(ip: string): string {
|
function expandIPv6(ip: string): string {
|
||||||
if (ip.includes("::")) {
|
if (ip.includes("::")) {
|
||||||
const [left, right] = ip.split("::");
|
const [left, right] = ip.split("::");
|
||||||
const leftParts = left ? left.split(":") : [];
|
const leftParts = left ? left.split(":") : [];
|
||||||
const rightParts = right ? right.split(":") : [];
|
const rightParts = right ? right.split(":") : [];
|
||||||
const missingParts = 8 - leftParts.length - rightParts.length;
|
const missingParts = 8 - leftParts.length - rightParts.length;
|
||||||
const middleParts: string[] = Array(missingParts).fill("0") as string[];
|
const middleParts: string[] = Array(missingParts).fill("0") as string[];
|
||||||
const allParts = [...leftParts, ...middleParts, ...rightParts];
|
const allParts = [...leftParts, ...middleParts, ...rightParts];
|
||||||
return allParts.map((p: string) => p.padStart(4, "0")).join(":");
|
return allParts.map((p: string) => p.padStart(4, "0")).join(":");
|
||||||
}
|
}
|
||||||
return ip
|
return ip
|
||||||
.split(":")
|
.split(":")
|
||||||
.map((p: string) => p.padStart(4, "0"))
|
.map((p: string) => p.padStart(4, "0"))
|
||||||
.join(":");
|
.join(":");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -120,23 +117,23 @@ function expandIPv6(ip: string): string {
|
|||||||
* @returns true if the IP is within the CIDR range
|
* @returns true if the IP is within the CIDR range
|
||||||
*/
|
*/
|
||||||
export function ipv6InCIDR(ip: string, cidr: string): boolean {
|
export function ipv6InCIDR(ip: string, cidr: string): boolean {
|
||||||
const [rangeIp, prefixLenStr] = cidr.split("/");
|
const [rangeIp, prefixLenStr] = cidr.split("/");
|
||||||
const prefixLen = parseInt(prefixLenStr ?? "", 10);
|
const prefixLen = parseInt(prefixLenStr ?? "", 10);
|
||||||
|
|
||||||
if (!rangeIp || isNaN(prefixLen) || prefixLen < 0 || prefixLen > 128) {
|
if (!rangeIp || isNaN(prefixLen) || prefixLen < 0 || prefixLen > 128) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ipInt = ipv6ToBigInt(ip);
|
const ipInt = ipv6ToBigInt(ip);
|
||||||
const rangeInt = ipv6ToBigInt(rangeIp);
|
const rangeInt = ipv6ToBigInt(rangeIp);
|
||||||
const maxMask = BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
|
const maxMask = BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
|
||||||
const mask = (maxMask << BigInt(128 - prefixLen)) & maxMask;
|
const mask = (maxMask << BigInt(128 - prefixLen)) & maxMask;
|
||||||
|
|
||||||
return (ipInt & mask) === (rangeInt & mask);
|
return (ipInt & mask) === (rangeInt & mask);
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -146,22 +143,22 @@ export function ipv6InCIDR(ip: string, cidr: string): boolean {
|
|||||||
* @returns true if the ASN is within the range
|
* @returns true if the ASN is within the range
|
||||||
*/
|
*/
|
||||||
export function asnInRange(asn: number, range: string): boolean {
|
export function asnInRange(asn: number, range: string): boolean {
|
||||||
const parts = range.split("-");
|
const parts = range.split("-");
|
||||||
|
|
||||||
if (parts.length !== 2) {
|
if (parts.length !== 2) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = parseInt(parts[0] ?? "", 10);
|
const start = parseInt(parts[0] ?? "", 10);
|
||||||
const end = parseInt(parts[1] ?? "", 10);
|
const end = parseInt(parts[1] ?? "", 10);
|
||||||
|
|
||||||
if (isNaN(start) || isNaN(end) || start < 0 || end < 0 || start > end) {
|
if (isNaN(start) || isNaN(end) || start < 0 || end < 0 || start > end) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asn < 0) {
|
if (asn < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return asn >= start && asn <= end;
|
return asn >= start && asn <= end;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { domainMatchPredicate, getBestURL, getType } from "@/rdap";
|
import { domainMatchPredicate, getBestURL, getType } from "@/rdap";
|
||||||
import type {
|
import type {
|
||||||
AutonomousNumber,
|
AutonomousNumber,
|
||||||
Domain,
|
Domain,
|
||||||
IpNetwork,
|
IpNetwork,
|
||||||
Register,
|
Register,
|
||||||
RootRegistryType,
|
RootRegistryType,
|
||||||
SubmitProps,
|
SubmitProps,
|
||||||
TargetType,
|
TargetType,
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
import { registryURLs } from "@/constants";
|
import { registryURLs } from "@/constants";
|
||||||
import {
|
import {
|
||||||
AutonomousNumberSchema,
|
AutonomousNumberSchema,
|
||||||
DomainSchema,
|
DomainSchema,
|
||||||
IpNetworkSchema,
|
IpNetworkSchema,
|
||||||
RegisterSchema,
|
RegisterSchema,
|
||||||
RootRegistryEnum,
|
RootRegistryEnum,
|
||||||
} from "@/schema";
|
} from "@/schema";
|
||||||
import { truncated, ipv4InCIDR, ipv6InCIDR, asnInRange } from "@/helpers";
|
import { truncated, ipv4InCIDR, ipv6InCIDR, asnInRange } from "@/helpers";
|
||||||
import type { ZodSchema } from "zod";
|
import type { ZodSchema } from "zod";
|
||||||
@@ -24,413 +24,383 @@ import { Maybe, Result } from "true-myth";
|
|||||||
|
|
||||||
export type WarningHandler = (warning: { message: string }) => void;
|
export type WarningHandler = (warning: { message: string }) => void;
|
||||||
export type MetaParsedGeneric = {
|
export type MetaParsedGeneric = {
|
||||||
data: ParsedGeneric;
|
data: ParsedGeneric;
|
||||||
url: string;
|
url: string;
|
||||||
completeTime: Date;
|
completeTime: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
// An array of schemas to try and parse unknown JSON data with.
|
// An array of schemas to try and parse unknown JSON data with.
|
||||||
const schemas = [DomainSchema, AutonomousNumberSchema, IpNetworkSchema];
|
const schemas = [DomainSchema, AutonomousNumberSchema, IpNetworkSchema];
|
||||||
|
|
||||||
const useLookup = (warningHandler?: WarningHandler) => {
|
const useLookup = (warningHandler?: WarningHandler) => {
|
||||||
/**
|
/**
|
||||||
* A reference to the registry data, which is used to cache the registry data in memory.
|
* 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.
|
* This uses TargetType as the key, meaning v4/v6 IP/CIDR lookups are differentiated.
|
||||||
*/
|
*/
|
||||||
const registryDataRef = useRef<Record<RootRegistryType, Register | null>>(
|
const registryDataRef = useRef<Record<RootRegistryType, Register | null>>(
|
||||||
{} as Record<RootRegistryType, Register>
|
{} as Record<RootRegistryType, Register>
|
||||||
);
|
);
|
||||||
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [target, setTarget] = useState<string>("");
|
const [target, setTarget] = useState<string>("");
|
||||||
const [uriType, setUriType] = useState<Maybe<TargetType>>(Maybe.nothing());
|
const [uriType, setUriType] = useState<Maybe<TargetType>>(Maybe.nothing());
|
||||||
|
|
||||||
// Used by a callback on LookupInput to forcibly set the type of the lookup.
|
// Used by a callback on LookupInput to forcibly set the type of the lookup.
|
||||||
const [currentType, setTargetType] = useState<TargetType | null>(null);
|
const [currentType, setTargetType] = useState<TargetType | null>(null);
|
||||||
|
|
||||||
// Used to allow repeatable lookups when weird errors happen.
|
// Used to allow repeatable lookups when weird errors happen.
|
||||||
const repeatableRef = useRef<string>("");
|
const repeatableRef = useRef<string>("");
|
||||||
|
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
if (currentType != null) return Maybe.just(currentType);
|
if (currentType != null) return Maybe.just(currentType);
|
||||||
const uri: Maybe<TargetType> = (await getTypeEasy(target)).mapOr(
|
const uri: Maybe<TargetType> = (await getTypeEasy(target)).mapOr(Maybe.nothing(), (type) =>
|
||||||
Maybe.nothing(),
|
Maybe.just(type)
|
||||||
(type) => Maybe.just(type)
|
);
|
||||||
);
|
setUriType(uri);
|
||||||
setUriType(uri);
|
}, [target, currentType, getTypeEasy]);
|
||||||
}, [target, currentType, getTypeEasy]);
|
|
||||||
|
|
||||||
// Fetch & load a specific registry's data into memory.
|
// Fetch & load a specific registry's data into memory.
|
||||||
async function loadBootstrap(type: RootRegistryType, force = false) {
|
async function loadBootstrap(type: RootRegistryType, force = false) {
|
||||||
// Early preload exit condition
|
// Early preload exit condition
|
||||||
if (registryDataRef.current[type] != null && !force) return;
|
if (registryDataRef.current[type] != null && !force) return;
|
||||||
|
|
||||||
// Fetch the bootstrapping file from the registry
|
// Fetch the bootstrapping file from the registry
|
||||||
const response = await fetch(registryURLs[type]);
|
const response = await fetch(registryURLs[type]);
|
||||||
if (response.status != 200)
|
if (response.status != 200) throw new Error(`Error: ${response.statusText}`);
|
||||||
throw new Error(`Error: ${response.statusText}`);
|
|
||||||
|
|
||||||
// Parse it, so we don't make any false assumptions during development & while maintaining the tool.
|
// Parse it, so we don't make any false assumptions during development & while maintaining the tool.
|
||||||
const parsedRegister = RegisterSchema.safeParse(await response.json());
|
const parsedRegister = RegisterSchema.safeParse(await response.json());
|
||||||
if (!parsedRegister.success)
|
if (!parsedRegister.success)
|
||||||
throw new Error(
|
throw new Error(`Could not parse IANA bootstrap response (type: ${type}).`);
|
||||||
`Could not parse IANA bootstrap response (type: ${type}).`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set it in state so we can use it.
|
// Set it in state so we can use it.
|
||||||
registryDataRef.current = {
|
registryDataRef.current = {
|
||||||
...registryDataRef.current,
|
...registryDataRef.current,
|
||||||
[type]: parsedRegister.data,
|
[type]: parsedRegister.data,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRegistry(type: RootRegistryType): Promise<Register> {
|
async function getRegistry(type: RootRegistryType): Promise<Register> {
|
||||||
if (registryDataRef.current[type] == null) await loadBootstrap(type);
|
if (registryDataRef.current[type] == null) await loadBootstrap(type);
|
||||||
const registry = registryDataRef.current[type];
|
const registry = registryDataRef.current[type];
|
||||||
if (registry == null)
|
if (registry == null)
|
||||||
throw new Error(`Could not load bootstrap data for ${type} registry.`);
|
throw new Error(`Could not load bootstrap data for ${type} registry.`);
|
||||||
return registry;
|
return registry;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getTypeEasy(
|
async function getTypeEasy(target: string): Promise<Result<TargetType, Error>> {
|
||||||
target: string
|
return getType(target, getRegistry);
|
||||||
): Promise<Result<TargetType, Error>> {
|
}
|
||||||
return getType(target, getRegistry);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRegistryURL(
|
function getRegistryURL(type: RootRegistryType, lookupTarget: string): string {
|
||||||
type: RootRegistryType,
|
const bootstrap = registryDataRef.current[type];
|
||||||
lookupTarget: string
|
if (bootstrap == null)
|
||||||
): string {
|
throw new Error(`Cannot acquire RDAP URL without bootstrap data for ${type} lookup.`);
|
||||||
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) {
|
typeSwitch: switch (type) {
|
||||||
case "domain":
|
case "domain":
|
||||||
for (const bootstrapItem of bootstrap.services) {
|
for (const bootstrapItem of bootstrap.services) {
|
||||||
if (bootstrapItem[0].some(domainMatchPredicate(lookupTarget))) {
|
if (bootstrapItem[0].some(domainMatchPredicate(lookupTarget))) {
|
||||||
// min length of 1 is validated in zod schema
|
// min length of 1 is validated in zod schema
|
||||||
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
|
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
|
||||||
break typeSwitch;
|
break typeSwitch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(`No matching domain found.`);
|
throw new Error(`No matching domain found.`);
|
||||||
case "ip4": {
|
case "ip4": {
|
||||||
// Extract the IP address without CIDR suffix for matching
|
// Extract the IP address without CIDR suffix for matching
|
||||||
const ipAddress = lookupTarget.split("/")[0] ?? lookupTarget;
|
const ipAddress = lookupTarget.split("/")[0] ?? lookupTarget;
|
||||||
for (const bootstrapItem of bootstrap.services) {
|
for (const bootstrapItem of bootstrap.services) {
|
||||||
// bootstrapItem[0] contains CIDR ranges like ["1.0.0.0/8", "2.0.0.0/8"]
|
// bootstrapItem[0] contains CIDR ranges like ["1.0.0.0/8", "2.0.0.0/8"]
|
||||||
if (bootstrapItem[0].some((cidr) => ipv4InCIDR(ipAddress, cidr))) {
|
if (bootstrapItem[0].some((cidr) => ipv4InCIDR(ipAddress, cidr))) {
|
||||||
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
|
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
|
||||||
break typeSwitch;
|
break typeSwitch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(`No matching IPv4 registry found for ${lookupTarget}.`);
|
throw new Error(`No matching IPv4 registry found for ${lookupTarget}.`);
|
||||||
}
|
}
|
||||||
case "ip6": {
|
case "ip6": {
|
||||||
// Extract the IP address without CIDR suffix for matching
|
// Extract the IP address without CIDR suffix for matching
|
||||||
const ipAddress = lookupTarget.split("/")[0] ?? lookupTarget;
|
const ipAddress = lookupTarget.split("/")[0] ?? lookupTarget;
|
||||||
for (const bootstrapItem of bootstrap.services) {
|
for (const bootstrapItem of bootstrap.services) {
|
||||||
// bootstrapItem[0] contains CIDR ranges like ["2001:0200::/23", "2001:0400::/23"]
|
// bootstrapItem[0] contains CIDR ranges like ["2001:0200::/23", "2001:0400::/23"]
|
||||||
if (bootstrapItem[0].some((cidr) => ipv6InCIDR(ipAddress, cidr))) {
|
if (bootstrapItem[0].some((cidr) => ipv6InCIDR(ipAddress, cidr))) {
|
||||||
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
|
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
|
||||||
break typeSwitch;
|
break typeSwitch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(`No matching IPv6 registry found for ${lookupTarget}.`);
|
throw new Error(`No matching IPv6 registry found for ${lookupTarget}.`);
|
||||||
}
|
}
|
||||||
case "autnum": {
|
case "autnum": {
|
||||||
// Extract ASN number from "AS12345" format
|
// Extract ASN number from "AS12345" format
|
||||||
const asnMatch = lookupTarget.match(/^AS(\d+)$/i);
|
const asnMatch = lookupTarget.match(/^AS(\d+)$/i);
|
||||||
if (!asnMatch || !asnMatch[1]) {
|
if (!asnMatch || !asnMatch[1]) {
|
||||||
throw new Error(`Invalid ASN format: ${lookupTarget}`);
|
throw new Error(`Invalid ASN format: ${lookupTarget}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const asnNumber = parseInt(asnMatch[1], 10);
|
const asnNumber = parseInt(asnMatch[1], 10);
|
||||||
if (isNaN(asnNumber)) {
|
if (isNaN(asnNumber)) {
|
||||||
throw new Error(`Invalid ASN number: ${lookupTarget}`);
|
throw new Error(`Invalid ASN number: ${lookupTarget}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const bootstrapItem of bootstrap.services) {
|
for (const bootstrapItem of bootstrap.services) {
|
||||||
// bootstrapItem[0] contains ASN ranges like ["64512-65534", "13312-18431"]
|
// bootstrapItem[0] contains ASN ranges like ["64512-65534", "13312-18431"]
|
||||||
if (bootstrapItem[0].some((range) => asnInRange(asnNumber, range))) {
|
if (bootstrapItem[0].some((range) => asnInRange(asnNumber, range))) {
|
||||||
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
|
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
|
||||||
break typeSwitch;
|
break typeSwitch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(`No matching registry found for ${lookupTarget}.`);
|
throw new Error(`No matching registry found for ${lookupTarget}.`);
|
||||||
}
|
}
|
||||||
case "entity":
|
case "entity":
|
||||||
throw new Error(`No matching entity found.`);
|
throw new Error(`No matching entity found.`);
|
||||||
default:
|
default:
|
||||||
throw new Error("Invalid lookup target provided.");
|
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
|
// Map internal types to RDAP endpoint paths
|
||||||
// ip4 and ip6 both use the 'ip' endpoint in RDAP
|
// ip4 and ip6 both use the 'ip' endpoint in RDAP
|
||||||
const rdapPath = type === "ip4" || type === "ip6" ? "ip" : type;
|
const rdapPath = type === "ip4" || type === "ip6" ? "ip" : type;
|
||||||
|
|
||||||
return `${url}${rdapPath}/${lookupTarget}`;
|
return `${url}${rdapPath}/${lookupTarget}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const preload = async () => {
|
const preload = async () => {
|
||||||
if (uriType.isNothing) return;
|
if (uriType.isNothing) return;
|
||||||
|
|
||||||
const registryUri = RootRegistryEnum.safeParse(uriType.value);
|
const registryUri = RootRegistryEnum.safeParse(uriType.value);
|
||||||
if (!registryUri.success) return;
|
if (!registryUri.success) return;
|
||||||
|
|
||||||
console.log({
|
console.log({
|
||||||
uriType: uriType.value,
|
uriType: uriType.value,
|
||||||
registryData: registryDataRef.current,
|
registryData: registryDataRef.current,
|
||||||
registryUri: registryUri.data,
|
registryUri: registryUri.data,
|
||||||
});
|
});
|
||||||
if (registryDataRef.current[registryUri.data] != null) return;
|
if (registryDataRef.current[registryUri.data] != null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await loadBootstrap(registryUri.data);
|
await loadBootstrap(registryUri.data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (warningHandler != undefined) {
|
if (warningHandler != undefined) {
|
||||||
const message =
|
const message = e instanceof Error ? `(${truncated(e.message, 15)})` : ".";
|
||||||
e instanceof Error ? `(${truncated(e.message, 15)})` : ".";
|
warningHandler({
|
||||||
warningHandler({
|
message: `Failed to preload registry${message}`,
|
||||||
message: `Failed to preload registry${message}`,
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
preload().catch(console.error);
|
preload().catch(console.error);
|
||||||
}, [target, uriType, warningHandler]);
|
}, [target, uriType, warningHandler]);
|
||||||
|
|
||||||
async function getAndParse<T>(
|
async function getAndParse<T>(url: string, schema: ZodSchema<T>): Promise<Result<T, Error>> {
|
||||||
url: string,
|
const response = await fetch(url);
|
||||||
schema: ZodSchema<T>
|
|
||||||
): Promise<Result<T, Error>> {
|
|
||||||
const response = await fetch(url);
|
|
||||||
|
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
const result = schema.safeParse(await response.json());
|
const result = schema.safeParse(await response.json());
|
||||||
|
|
||||||
if (result.success === false) {
|
if (result.success === false) {
|
||||||
// flatten the errors to make them more readable and simple
|
// flatten the errors to make them more readable and simple
|
||||||
const flatErrors = result.error.flatten(function (issue) {
|
const flatErrors = result.error.flatten(function (issue) {
|
||||||
const path = issue.path.map((value) => value.toString()).join(".");
|
const path = issue.path.map((value) => value.toString()).join(".");
|
||||||
return `${path}: ${issue.message}`;
|
return `${path}: ${issue.message}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(flatErrors);
|
console.log(flatErrors);
|
||||||
|
|
||||||
// combine them all, wrap them in a new error, and return it
|
// combine them all, wrap them in a new error, and return it
|
||||||
return Result.err(
|
return Result.err(
|
||||||
new Error(
|
new Error(
|
||||||
[
|
[
|
||||||
"Could not parse the response from the registry.",
|
"Could not parse the response from the registry.",
|
||||||
...flatErrors.formErrors,
|
...flatErrors.formErrors,
|
||||||
...Object.values(flatErrors.fieldErrors).flat(),
|
...Object.values(flatErrors.fieldErrors).flat(),
|
||||||
].join("\n\t")
|
].join("\n\t")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.ok(result.data);
|
return Result.ok(result.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (response.status) {
|
switch (response.status) {
|
||||||
case 302:
|
case 302:
|
||||||
return Result.err(
|
return Result.err(
|
||||||
new Error(
|
new Error(
|
||||||
"The registry indicated that the resource requested is available at a different location."
|
"The registry indicated that the resource requested is available at a different location."
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
case 400:
|
case 400:
|
||||||
return Result.err(
|
return Result.err(
|
||||||
new Error(
|
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."
|
"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:
|
case 403:
|
||||||
return Result.err(
|
return Result.err(
|
||||||
new Error(
|
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."
|
"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:
|
case 404:
|
||||||
return Result.err(
|
return Result.err(
|
||||||
new Error(
|
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)."
|
"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:
|
case 500:
|
||||||
return Result.err(
|
return Result.err(
|
||||||
new Error(
|
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."
|
"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:
|
default:
|
||||||
return Result.err(
|
return Result.err(
|
||||||
new Error(
|
new Error(`The registry did not return an OK status code: ${response.status}.`)
|
||||||
`The registry did not return an OK status code: ${response.status}.`
|
);
|
||||||
)
|
}
|
||||||
);
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitInternal(
|
async function submitInternal(
|
||||||
target: string
|
target: string
|
||||||
): Promise<Result<{ data: ParsedGeneric; url: string }, Error>> {
|
): Promise<Result<{ data: ParsedGeneric; url: string }, Error>> {
|
||||||
if (target == null || target.length == 0)
|
if (target == null || target.length == 0)
|
||||||
return Result.err(
|
return Result.err(new Error("A target must be given in order to execute a lookup."));
|
||||||
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) {
|
if (targetType.isErr) {
|
||||||
return Result.err(
|
return Result.err(
|
||||||
new Error("Unable to determine type, unable to send query", {
|
new Error("Unable to determine type, unable to send query", {
|
||||||
cause: targetType.error,
|
cause: targetType.error,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (targetType.value) {
|
switch (targetType.value) {
|
||||||
// Block scoped case to allow url const reuse
|
// Block scoped case to allow url const reuse
|
||||||
case "ip4": {
|
case "ip4": {
|
||||||
await loadBootstrap("ip4");
|
await loadBootstrap("ip4");
|
||||||
const url = getRegistryURL(targetType.value, target);
|
const url = getRegistryURL(targetType.value, target);
|
||||||
const result = await getAndParse<IpNetwork>(url, IpNetworkSchema);
|
const result = await getAndParse<IpNetwork>(url, IpNetworkSchema);
|
||||||
if (result.isErr) return Result.err(result.error);
|
if (result.isErr) return Result.err(result.error);
|
||||||
return Result.ok({ data: result.value, url });
|
return Result.ok({ data: result.value, url });
|
||||||
}
|
}
|
||||||
case "ip6": {
|
case "ip6": {
|
||||||
await loadBootstrap("ip6");
|
await loadBootstrap("ip6");
|
||||||
const url = getRegistryURL(targetType.value, target);
|
const url = getRegistryURL(targetType.value, target);
|
||||||
const result = await getAndParse<IpNetwork>(url, IpNetworkSchema);
|
const result = await getAndParse<IpNetwork>(url, IpNetworkSchema);
|
||||||
if (result.isErr) return Result.err(result.error);
|
if (result.isErr) return Result.err(result.error);
|
||||||
return Result.ok({ data: result.value, url });
|
return Result.ok({ data: result.value, url });
|
||||||
}
|
}
|
||||||
case "domain": {
|
case "domain": {
|
||||||
await loadBootstrap("domain");
|
await loadBootstrap("domain");
|
||||||
const url = getRegistryURL(targetType.value, target);
|
const url = getRegistryURL(targetType.value, target);
|
||||||
|
|
||||||
// HTTP
|
// HTTP
|
||||||
if (url.startsWith("http://") && url != repeatableRef.current) {
|
if (url.startsWith("http://") && url != repeatableRef.current) {
|
||||||
repeatableRef.current = url;
|
repeatableRef.current = url;
|
||||||
return Result.err(
|
return Result.err(
|
||||||
new Error(
|
new Error(
|
||||||
"The registry this domain belongs to uses HTTP, which is not secure. " +
|
"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, " +
|
"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."
|
"or worse, a CORS error, this lookup has been blocked. Try again to force the lookup."
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const result = await getAndParse<Domain>(url, DomainSchema);
|
const result = await getAndParse<Domain>(url, DomainSchema);
|
||||||
if (result.isErr) return Result.err(result.error);
|
if (result.isErr) return Result.err(result.error);
|
||||||
|
|
||||||
return Result.ok({ data: result.value, url });
|
return Result.ok({ data: result.value, url });
|
||||||
}
|
}
|
||||||
case "autnum": {
|
case "autnum": {
|
||||||
await loadBootstrap("autnum");
|
await loadBootstrap("autnum");
|
||||||
const url = getRegistryURL(targetType.value, target);
|
const url = getRegistryURL(targetType.value, target);
|
||||||
const result = await getAndParse<AutonomousNumber>(
|
const result = await getAndParse<AutonomousNumber>(url, AutonomousNumberSchema);
|
||||||
url,
|
if (result.isErr) return Result.err(result.error);
|
||||||
AutonomousNumberSchema
|
return Result.ok({ data: result.value, url });
|
||||||
);
|
}
|
||||||
if (result.isErr) return Result.err(result.error);
|
case "tld": {
|
||||||
return Result.ok({ data: result.value, url });
|
// remove the leading dot
|
||||||
}
|
const value = target.startsWith(".") ? target.slice(1) : target;
|
||||||
case "tld": {
|
const url = `https://root.rdap.org/domain/${value}`;
|
||||||
// remove the leading dot
|
const result = await getAndParse<Domain>(url, DomainSchema);
|
||||||
const value = target.startsWith(".") ? target.slice(1) : target;
|
if (result.isErr) return Result.err(result.error);
|
||||||
const url = `https://root.rdap.org/domain/${value}`;
|
return Result.ok({ data: result.value, url });
|
||||||
const result = await getAndParse<Domain>(url, DomainSchema);
|
}
|
||||||
if (result.isErr) return Result.err(result.error);
|
case "url": {
|
||||||
return Result.ok({ data: result.value, url });
|
const response = await fetch(target);
|
||||||
}
|
|
||||||
case "url": {
|
|
||||||
const response = await fetch(target);
|
|
||||||
|
|
||||||
if (response.status != 200)
|
if (response.status != 200)
|
||||||
return Result.err(
|
return Result.err(
|
||||||
new Error(
|
new Error(
|
||||||
`The URL provided returned a non-200 status code: ${response.status}.`
|
`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
|
// Try each schema until one works
|
||||||
for (const schema of schemas) {
|
for (const schema of schemas) {
|
||||||
const result = schema.safeParse(data);
|
const result = schema.safeParse(data);
|
||||||
if (result.success)
|
if (result.success) return Result.ok({ data: result.data, url: target });
|
||||||
return Result.ok({ data: result.data, url: target });
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return Result.err(
|
return Result.err(new Error("No schema was able to parse the response."));
|
||||||
new Error("No schema was able to parse the response.")
|
}
|
||||||
);
|
case "json": {
|
||||||
}
|
const data = JSON.parse(target);
|
||||||
case "json": {
|
for (const schema of schemas) {
|
||||||
const data = JSON.parse(target);
|
const result = schema.safeParse(data);
|
||||||
for (const schema of schemas) {
|
if (result.success) return Result.ok({ data: result.data, url: "" });
|
||||||
const result = schema.safeParse(data);
|
}
|
||||||
if (result.success) return Result.ok({ data: result.data, url: "" });
|
}
|
||||||
}
|
case "registrar": {
|
||||||
}
|
}
|
||||||
case "registrar": {
|
default:
|
||||||
}
|
return Result.err(new Error("The type detected has not been implemented."));
|
||||||
default:
|
}
|
||||||
return Result.err(
|
}
|
||||||
new Error("The type detected has not been implemented.")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submit({
|
async function submit({ target }: SubmitProps): Promise<Maybe<MetaParsedGeneric>> {
|
||||||
target,
|
try {
|
||||||
}: SubmitProps): Promise<Maybe<MetaParsedGeneric>> {
|
// target is already set in state, but it's also provided by the form callback, so we'll use it.
|
||||||
try {
|
const response = await submitInternal(target);
|
||||||
// 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) {
|
if (response.isErr) {
|
||||||
setError(response.error.message);
|
setError(response.error.message);
|
||||||
console.error(response.error);
|
console.error(response.error);
|
||||||
} else setError(null);
|
} else setError(null);
|
||||||
|
|
||||||
return response.isOk
|
return response.isOk
|
||||||
? Maybe.just({
|
? Maybe.just({
|
||||||
data: response.value.data,
|
data: response.value.data,
|
||||||
url: response.value.url,
|
url: response.value.url,
|
||||||
completeTime: new Date(),
|
completeTime: new Date(),
|
||||||
})
|
})
|
||||||
: Maybe.nothing();
|
: Maybe.nothing();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!(e instanceof Error))
|
if (!(e instanceof Error)) setError("An unknown, unprocessable error has occurred.");
|
||||||
setError("An unknown, unprocessable error has occurred.");
|
else setError(e.message);
|
||||||
else setError(e.message);
|
console.error(e);
|
||||||
console.error(e);
|
return Maybe.nothing();
|
||||||
return Maybe.nothing();
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
error,
|
error,
|
||||||
setTarget,
|
setTarget,
|
||||||
setTargetType,
|
setTargetType,
|
||||||
submit,
|
submit,
|
||||||
currentType: uriType,
|
currentType: uriType,
|
||||||
getType: getTypeEasy,
|
getType: getTypeEasy,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useLookup;
|
export default useLookup;
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ import { type ClassValue, clsx } from "clsx";
|
|||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
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";
|
import "../styles/globals.css";
|
||||||
|
|
||||||
const MyApp: AppType = ({ Component, pageProps }) => {
|
const MyApp: AppType = ({ Component, pageProps }) => {
|
||||||
return <Component {...pageProps} />;
|
return <Component {...pageProps} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MyApp;
|
export default MyApp;
|
||||||
|
|||||||
@@ -10,93 +10,83 @@ import { Maybe } from "true-myth";
|
|||||||
import type { TargetType } from "@/types";
|
import type { TargetType } from "@/types";
|
||||||
|
|
||||||
const Index: NextPage = () => {
|
const Index: NextPage = () => {
|
||||||
const { error, setTarget, setTargetType, submit, getType } = useLookup();
|
const { error, setTarget, setTargetType, submit, getType } = useLookup();
|
||||||
const [detectedType, setDetectedType] = useState<Maybe<TargetType>>(
|
const [detectedType, setDetectedType] = useState<Maybe<TargetType>>(Maybe.nothing());
|
||||||
Maybe.nothing()
|
const [response, setResponse] = useState<Maybe<MetaParsedGeneric>>(Maybe.nothing());
|
||||||
);
|
const [isLoading, setLoading] = useState<boolean>(false);
|
||||||
const [response, setResponse] = useState<Maybe<MetaParsedGeneric>>(
|
|
||||||
Maybe.nothing()
|
|
||||||
);
|
|
||||||
const [isLoading, setLoading] = useState<boolean>(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>rdap.xevion.dev</title>
|
<title>rdap.xevion.dev</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A custom, private RDAP lookup client built by Xevion."
|
content="A custom, private RDAP lookup client built by Xevion."
|
||||||
/>
|
/>
|
||||||
<meta property="og:url" content="https://rdap.xevion.dev" />
|
<meta property="og:url" content="https://rdap.xevion.dev" />
|
||||||
<meta property="og:title" content="RDAP | by Xevion.dev" />
|
<meta property="og:title" content="RDAP | by Xevion.dev" />
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
property="og:description"
|
||||||
content="A custom, private RDAP lookup client built by Xevion."
|
content="A custom, private RDAP lookup client built by Xevion."
|
||||||
/>
|
/>
|
||||||
<meta property="og:site_name" content="rdap.xevion.dev" />
|
<meta property="og:site_name" content="rdap.xevion.dev" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta
|
<meta
|
||||||
name="keywords"
|
name="keywords"
|
||||||
content="xevion, rdap, whois, rdap, domain name, dns, ip address"
|
content="xevion, rdap, whois, rdap, domain name, dns, ip address"
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<nav className="bg-zinc-850 px-5 py-4 shadow-xs">
|
<nav className="bg-zinc-850 px-5 py-4 shadow-xs">
|
||||||
<span
|
<span className="text-xl font-medium text-white" style={{ fontSize: "larger" }}>
|
||||||
className="text-xl font-medium text-white"
|
<a href="https://github.com/Xevion/rdap">rdap</a>
|
||||||
style={{ fontSize: "larger" }}
|
<a href={"https://xevion.dev"} className="text-zinc-400 hover:animate-pulse">
|
||||||
>
|
.xevion.dev
|
||||||
<a href="https://github.com/Xevion/rdap">rdap</a>
|
</a>
|
||||||
<a
|
</span>
|
||||||
href={"https://xevion.dev"}
|
</nav>
|
||||||
className="text-zinc-400 hover:animate-pulse"
|
<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">
|
||||||
.xevion.dev
|
<LookupInput
|
||||||
</a>
|
isLoading={isLoading}
|
||||||
</span>
|
detectedType={detectedType}
|
||||||
</nav>
|
onChange={async ({ target, targetType }) => {
|
||||||
<div className="mx-auto max-w-screen-sm px-5 lg:max-w-screen-md xl:max-w-screen-lg">
|
setTarget(target);
|
||||||
<div className="dark container mx-auto w-full py-6 md:py-12 ">
|
setTargetType(targetType);
|
||||||
<LookupInput
|
|
||||||
isLoading={isLoading}
|
|
||||||
detectedType={detectedType}
|
|
||||||
onChange={async ({ target, targetType }) => {
|
|
||||||
setTarget(target);
|
|
||||||
setTargetType(targetType);
|
|
||||||
|
|
||||||
const detectResult = await getType(target);
|
const detectResult = await getType(target);
|
||||||
if (detectResult.isOk) {
|
if (detectResult.isOk) {
|
||||||
setDetectedType(Maybe.just(detectResult.value));
|
setDetectedType(Maybe.just(detectResult.value));
|
||||||
} else {
|
} else {
|
||||||
setDetectedType(Maybe.nothing());
|
setDetectedType(Maybe.nothing());
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onSubmit={async function (props) {
|
onSubmit={async function (props) {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setResponse(await submit(props));
|
setResponse(await submit(props));
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setResponse(Maybe.nothing());
|
setResponse(Maybe.nothing());
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{error != null ? (
|
{error != null ? (
|
||||||
<ErrorCard
|
<ErrorCard
|
||||||
title="An error occurred while performing a lookup."
|
title="An error occurred while performing a lookup."
|
||||||
description={error}
|
description={error}
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{response.isJust ? (
|
{response.isJust ? (
|
||||||
<Generic url={response.value.url} data={response.value.data} />
|
<Generic url={response.value.url} data={response.value.data} />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Index;
|
export default Index;
|
||||||
|
|||||||
@@ -11,43 +11,43 @@ import { registryURLs } from "./constants";
|
|||||||
const registryCache = new Map<RootRegistryType, Register>();
|
const registryCache = new Map<RootRegistryType, Register>();
|
||||||
|
|
||||||
async function getRealRegistry(type: RootRegistryType): Promise<Register> {
|
async function getRealRegistry(type: RootRegistryType): Promise<Register> {
|
||||||
if (registryCache.has(type)) {
|
if (registryCache.has(type)) {
|
||||||
return registryCache.get(type)!;
|
return registryCache.get(type)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(registryURLs[type]);
|
const response = await fetch(registryURLs[type]);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch ${type} registry: ${response.statusText}`);
|
throw new Error(`Failed to fetch ${type} registry: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = (await response.json()) as Register;
|
const data = (await response.json()) as Register;
|
||||||
registryCache.set(type, data);
|
registryCache.set(type, data);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("getType - Integration tests with real registries", () => {
|
describe("getType - Integration tests with real registries", () => {
|
||||||
it("should detect entity with real entity registry", async () => {
|
it("should detect entity with real entity registry", async () => {
|
||||||
// Test with a known entity tag (RIPE)
|
// Test with a known entity tag (RIPE)
|
||||||
const result = await getType("TEST-RIPE", getRealRegistry);
|
const result = await getType("TEST-RIPE", getRealRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("entity");
|
expect(result.value).toBe("entity");
|
||||||
}
|
}
|
||||||
}, 10000); // Longer timeout for network call
|
}, 10000); // Longer timeout for network call
|
||||||
|
|
||||||
it("should detect entity with ARIN tag", async () => {
|
it("should detect entity with ARIN tag", async () => {
|
||||||
const result = await getType("NET-ARIN", getRealRegistry);
|
const result = await getType("NET-ARIN", getRealRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("entity");
|
expect(result.value).toBe("entity");
|
||||||
}
|
}
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
it("should not detect invalid entity tag", async () => {
|
it("should not detect invalid entity tag", async () => {
|
||||||
const result = await getType("INVALID-NOTREAL", getRealRegistry);
|
const result = await getType("INVALID-NOTREAL", getRealRegistry);
|
||||||
// Should either error or detect as something else, but not entity
|
// Should either error or detect as something else, but not entity
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).not.toBe("entity");
|
expect(result.value).not.toBe("entity");
|
||||||
}
|
}
|
||||||
}, 10000);
|
}, 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])
|
// Mock registry getter (matches real IANA structure: [email, tags, urls])
|
||||||
const mockRegistry: Register = {
|
const mockRegistry: Register = {
|
||||||
description: "Test registry",
|
description: "Test registry",
|
||||||
publication: "2024-01-01",
|
publication: "2024-01-01",
|
||||||
version: "1.0",
|
version: "1.0",
|
||||||
services: [
|
services: [
|
||||||
[
|
[
|
||||||
["test@example.com"], // email
|
["test@example.com"], // email
|
||||||
["RIPE", "APNIC"], // tags
|
["RIPE", "APNIC"], // tags
|
||||||
["https://rdap.example.com/"] // urls
|
["https://rdap.example.com/"], // urls
|
||||||
]
|
],
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockGetRegistry = vi.fn(() => Promise.resolve(mockRegistry));
|
const mockGetRegistry = vi.fn(() => Promise.resolve(mockRegistry));
|
||||||
|
|
||||||
describe("getType - IP address detection", () => {
|
describe("getType - IP address detection", () => {
|
||||||
describe("IPv4 detection", () => {
|
describe("IPv4 detection", () => {
|
||||||
it("should detect standard IPv4 addresses", async () => {
|
it("should detect standard IPv4 addresses", async () => {
|
||||||
const result = await getType("192.168.1.1", mockGetRegistry);
|
const result = await getType("192.168.1.1", mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("ip4");
|
expect(result.value).toBe("ip4");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect IPv4 with CIDR notation", async () => {
|
it("should detect IPv4 with CIDR notation", async () => {
|
||||||
const result = await getType("192.168.1.0/24", mockGetRegistry);
|
const result = await getType("192.168.1.0/24", mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("ip4");
|
expect(result.value).toBe("ip4");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect various IPv4 addresses", async () => {
|
it("should detect various IPv4 addresses", async () => {
|
||||||
const ips = [
|
const ips = [
|
||||||
"8.8.8.8",
|
"8.8.8.8",
|
||||||
"1.1.1.1",
|
"1.1.1.1",
|
||||||
"10.0.0.1",
|
"10.0.0.1",
|
||||||
"172.16.0.1",
|
"172.16.0.1",
|
||||||
"255.255.255.255",
|
"255.255.255.255",
|
||||||
"0.0.0.0",
|
"0.0.0.0",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const ip of ips) {
|
for (const ip of ips) {
|
||||||
const result = await getType(ip, mockGetRegistry);
|
const result = await getType(ip, mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("ip4");
|
expect(result.value).toBe("ip4");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect IPv4 with various CIDR prefixes", async () => {
|
it("should detect IPv4 with various CIDR prefixes", async () => {
|
||||||
const cidrs = [
|
const cidrs = [
|
||||||
"192.168.1.0/8",
|
"192.168.1.0/8",
|
||||||
"10.0.0.0/16",
|
"10.0.0.0/16",
|
||||||
"172.16.0.0/12",
|
"172.16.0.0/12",
|
||||||
"8.8.8.0/24",
|
"8.8.8.0/24",
|
||||||
"1.1.1.1/32",
|
"1.1.1.1/32",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const cidr of cidrs) {
|
for (const cidr of cidrs) {
|
||||||
const result = await getType(cidr, mockGetRegistry);
|
const result = await getType(cidr, mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("ip4");
|
expect(result.value).toBe("ip4");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("IPv6 detection", () => {
|
describe("IPv6 detection", () => {
|
||||||
it("should detect standard IPv6 addresses", async () => {
|
it("should detect standard IPv6 addresses", async () => {
|
||||||
const result = await getType("2001:db8::1", mockGetRegistry);
|
const result = await getType("2001:db8::1", mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("ip6");
|
expect(result.value).toBe("ip6");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect IPv6 with CIDR notation", async () => {
|
it("should detect IPv6 with CIDR notation", async () => {
|
||||||
const result = await getType("2001:db8::/32", mockGetRegistry);
|
const result = await getType("2001:db8::/32", mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("ip6");
|
expect(result.value).toBe("ip6");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect various IPv6 addresses", async () => {
|
it("should detect various IPv6 addresses", async () => {
|
||||||
const ips = [
|
const ips = [
|
||||||
"2001:4860:4860::8888", // Google DNS
|
"2001:4860:4860::8888", // Google DNS
|
||||||
"2606:4700:4700::1111", // Cloudflare DNS
|
"2606:4700:4700::1111", // Cloudflare DNS
|
||||||
"::1", // Localhost
|
"::1", // Localhost
|
||||||
"::", // All zeros
|
"::", // All zeros
|
||||||
"fe80::1", // Link-local
|
"fe80::1", // Link-local
|
||||||
"2001:db8:85a3::8a2e:370:7334", // Full notation
|
"2001:db8:85a3::8a2e:370:7334", // Full notation
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const ip of ips) {
|
for (const ip of ips) {
|
||||||
const result = await getType(ip, mockGetRegistry);
|
const result = await getType(ip, mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("ip6");
|
expect(result.value).toBe("ip6");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect IPv6 with various CIDR prefixes", async () => {
|
it("should detect IPv6 with various CIDR prefixes", async () => {
|
||||||
const cidrs = ["2001:db8::/32", "2001:4860::/32", "fe80::/10", "::1/128"];
|
const cidrs = ["2001:db8::/32", "2001:4860::/32", "fe80::/10", "::1/128"];
|
||||||
|
|
||||||
for (const cidr of cidrs) {
|
for (const cidr of cidrs) {
|
||||||
const result = await getType(cidr, mockGetRegistry);
|
const result = await getType(cidr, mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("ip6");
|
expect(result.value).toBe("ip6");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getType - Domain detection", () => {
|
describe("getType - Domain detection", () => {
|
||||||
it("should detect standard domains", async () => {
|
it("should detect standard domains", async () => {
|
||||||
const result = await getType("example.com", mockGetRegistry);
|
const result = await getType("example.com", mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("domain");
|
expect(result.value).toBe("domain");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect various domain formats", async () => {
|
it("should detect various domain formats", async () => {
|
||||||
const domains = [
|
const domains = [
|
||||||
"google.com",
|
"google.com",
|
||||||
"www.example.com",
|
"www.example.com",
|
||||||
"sub.domain.example.com",
|
"sub.domain.example.com",
|
||||||
"test-domain.com",
|
"test-domain.com",
|
||||||
"example123.org",
|
"example123.org",
|
||||||
"a.b.c.d.example.net",
|
"a.b.c.d.example.net",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const domain of domains) {
|
for (const domain of domains) {
|
||||||
const result = await getType(domain, mockGetRegistry);
|
const result = await getType(domain, mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("domain");
|
expect(result.value).toBe("domain");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getType - ASN detection", () => {
|
describe("getType - ASN detection", () => {
|
||||||
it("should detect standard ASN format", async () => {
|
it("should detect standard ASN format", async () => {
|
||||||
const result = await getType("AS12345", mockGetRegistry);
|
const result = await getType("AS12345", mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("autnum");
|
expect(result.value).toBe("autnum");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect various ASN formats", async () => {
|
it("should detect various ASN formats", async () => {
|
||||||
const asns = [
|
const asns = [
|
||||||
"AS1",
|
"AS1",
|
||||||
"AS13335", // Cloudflare
|
"AS13335", // Cloudflare
|
||||||
"AS15169", // Google
|
"AS15169", // Google
|
||||||
"AS8075", // Microsoft
|
"AS8075", // Microsoft
|
||||||
"AS16509", // Amazon
|
"AS16509", // Amazon
|
||||||
"AS999999",
|
"AS999999",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const asn of asns) {
|
for (const asn of asns) {
|
||||||
const result = await getType(asn, mockGetRegistry);
|
const result = await getType(asn, mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("autnum");
|
expect(result.value).toBe("autnum");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getType - TLD detection", () => {
|
describe("getType - TLD detection", () => {
|
||||||
it("should detect TLD format", async () => {
|
it("should detect TLD format", async () => {
|
||||||
const result = await getType(".com", mockGetRegistry);
|
const result = await getType(".com", mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("tld");
|
expect(result.value).toBe("tld");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect various TLDs", async () => {
|
it("should detect various TLDs", async () => {
|
||||||
const tlds = [".com", ".org", ".net", ".dev", ".io", ".ai", ".co"];
|
const tlds = [".com", ".org", ".net", ".dev", ".io", ".ai", ".co"];
|
||||||
|
|
||||||
for (const tld of tlds) {
|
for (const tld of tlds) {
|
||||||
const result = await getType(tld, mockGetRegistry);
|
const result = await getType(tld, mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("tld");
|
expect(result.value).toBe("tld");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getType - URL detection", () => {
|
describe("getType - URL detection", () => {
|
||||||
it("should detect HTTP URLs", async () => {
|
it("should detect HTTP URLs", async () => {
|
||||||
const result = await getType("http://example.com", mockGetRegistry);
|
const result = await getType("http://example.com", mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("url");
|
expect(result.value).toBe("url");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect HTTPS URLs", async () => {
|
it("should detect HTTPS URLs", async () => {
|
||||||
const result = await getType("https://example.com", mockGetRegistry);
|
const result = await getType("https://example.com", mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("url");
|
expect(result.value).toBe("url");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect RDAP URLs", async () => {
|
it("should detect RDAP URLs", async () => {
|
||||||
const urls = [
|
const urls = [
|
||||||
"https://rdap.arin.net/registry/ip/8.8.8.8",
|
"https://rdap.arin.net/registry/ip/8.8.8.8",
|
||||||
"http://rdap.apnic.net/ip/1.1.1.1",
|
"http://rdap.apnic.net/ip/1.1.1.1",
|
||||||
"https://rdap.org/domain/example.com",
|
"https://rdap.org/domain/example.com",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const url of urls) {
|
for (const url of urls) {
|
||||||
const result = await getType(url, mockGetRegistry);
|
const result = await getType(url, mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("url");
|
expect(result.value).toBe("url");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getType - JSON detection", () => {
|
describe("getType - JSON detection", () => {
|
||||||
it("should detect JSON objects", async () => {
|
it("should detect JSON objects", async () => {
|
||||||
const result = await getType(
|
const result = await getType('{"objectClassName":"domain"}', mockGetRegistry);
|
||||||
'{"objectClassName":"domain"}',
|
expect(result.isOk).toBe(true);
|
||||||
mockGetRegistry
|
if (result.isOk) {
|
||||||
);
|
expect(result.value).toBe("json");
|
||||||
expect(result.isOk).toBe(true);
|
}
|
||||||
if (result.isOk) {
|
});
|
||||||
expect(result.value).toBe("json");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should detect various JSON formats", async () => {
|
it("should detect various JSON formats", async () => {
|
||||||
const jsons = [
|
const jsons = [
|
||||||
"{}",
|
"{}",
|
||||||
'{"key": "value"}',
|
'{"key": "value"}',
|
||||||
'{"objectClassName":"ip network"}',
|
'{"objectClassName":"ip network"}',
|
||||||
'{"handle":"TEST"}',
|
'{"handle":"TEST"}',
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const json of jsons) {
|
for (const json of jsons) {
|
||||||
const result = await getType(json, mockGetRegistry);
|
const result = await getType(json, mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("json");
|
expect(result.value).toBe("json");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getType - Invalid inputs", () => {
|
describe("getType - Invalid inputs", () => {
|
||||||
it("should return error for empty string", async () => {
|
it("should return error for empty string", async () => {
|
||||||
const result = await getType("", mockGetRegistry);
|
const result = await getType("", mockGetRegistry);
|
||||||
expect(result.isErr).toBe(true);
|
expect(result.isErr).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return error for unrecognized format", async () => {
|
it("should return error for unrecognized format", async () => {
|
||||||
const result = await getType("not-a-valid-input!!@@##", mockGetRegistry);
|
const result = await getType("not-a-valid-input!!@@##", mockGetRegistry);
|
||||||
expect(result.isErr).toBe(true);
|
expect(result.isErr).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Invalid IPv4 addresses", () => {
|
describe("Invalid IPv4 addresses", () => {
|
||||||
it("should return error for IPv4 with octet > 255", async () => {
|
it("should return error for IPv4 with octet > 255", async () => {
|
||||||
const result = await getType("256.1.1.1", mockGetRegistry);
|
const result = await getType("256.1.1.1", mockGetRegistry);
|
||||||
expect(result.isErr).toBe(true);
|
expect(result.isErr).toBe(true);
|
||||||
if (result.isErr) {
|
if (result.isErr) {
|
||||||
expect(result.error.message).toContain("Invalid IPv4 address");
|
expect(result.error.message).toContain("Invalid IPv4 address");
|
||||||
expect(result.error.message).toContain("octet");
|
expect(result.error.message).toContain("octet");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return error for IPv4 with octet 999", async () => {
|
it("should return error for IPv4 with octet 999", async () => {
|
||||||
const result = await getType("192.999.1.1", mockGetRegistry);
|
const result = await getType("192.999.1.1", mockGetRegistry);
|
||||||
expect(result.isErr).toBe(true);
|
expect(result.isErr).toBe(true);
|
||||||
if (result.isErr) {
|
if (result.isErr) {
|
||||||
expect(result.error.message).toContain("Invalid IPv4 address");
|
expect(result.error.message).toContain("Invalid IPv4 address");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return error for IPv4 with invalid CIDR prefix", async () => {
|
it("should return error for IPv4 with invalid CIDR prefix", async () => {
|
||||||
const result = await getType("192.168.1.1/33", mockGetRegistry);
|
const result = await getType("192.168.1.1/33", mockGetRegistry);
|
||||||
expect(result.isErr).toBe(true);
|
expect(result.isErr).toBe(true);
|
||||||
if (result.isErr) {
|
if (result.isErr) {
|
||||||
expect(result.error.message).toContain("CIDR prefix");
|
expect(result.error.message).toContain("CIDR prefix");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return error for IPv4 with negative CIDR", async () => {
|
it("should return error for IPv4 with negative CIDR", async () => {
|
||||||
const result = await getType("192.168.1.1/-1", mockGetRegistry);
|
const result = await getType("192.168.1.1/-1", mockGetRegistry);
|
||||||
expect(result.isErr).toBe(true);
|
expect(result.isErr).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Invalid IPv6 addresses", () => {
|
describe("Invalid IPv6 addresses", () => {
|
||||||
it("should return error for IPv6 with multiple ::", async () => {
|
it("should return error for IPv6 with multiple ::", async () => {
|
||||||
const result = await getType("2001::db8::1", mockGetRegistry);
|
const result = await getType("2001::db8::1", mockGetRegistry);
|
||||||
expect(result.isErr).toBe(true);
|
expect(result.isErr).toBe(true);
|
||||||
if (result.isErr) {
|
if (result.isErr) {
|
||||||
expect(result.error.message).toContain("::");
|
expect(result.error.message).toContain("::");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return error for IPv6 with invalid CIDR prefix", async () => {
|
it("should return error for IPv6 with invalid CIDR prefix", async () => {
|
||||||
const result = await getType("2001:db8::1/129", mockGetRegistry);
|
const result = await getType("2001:db8::1/129", mockGetRegistry);
|
||||||
expect(result.isErr).toBe(true);
|
expect(result.isErr).toBe(true);
|
||||||
if (result.isErr) {
|
if (result.isErr) {
|
||||||
expect(result.error.message).toContain("CIDR prefix");
|
expect(result.error.message).toContain("CIDR prefix");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not match completely invalid hex strings as IPv6", async () => {
|
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
|
// "gggg" doesn't match the basic IPv6 pattern, so it won't be detected as IPv6
|
||||||
const result = await getType("gggg::1", mockGetRegistry);
|
const result = await getType("gggg::1", mockGetRegistry);
|
||||||
expect(result.isErr).toBe(true);
|
expect(result.isErr).toBe(true);
|
||||||
// Won't have IPv6-specific error since it didn't match the pattern
|
// Won't have IPv6-specific error since it didn't match the pattern
|
||||||
if (result.isErr) {
|
if (result.isErr) {
|
||||||
expect(result.error.message).toContain("No patterns matched");
|
expect(result.error.message).toContain("No patterns matched");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getType - Type detection priority", () => {
|
describe("getType - Type detection priority", () => {
|
||||||
it("should detect URL before domain", async () => {
|
it("should detect URL before domain", async () => {
|
||||||
const result = await getType("https://example.com", mockGetRegistry);
|
const result = await getType("https://example.com", mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("url");
|
expect(result.value).toBe("url");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect JSON before domain", async () => {
|
it("should detect JSON before domain", async () => {
|
||||||
const result = await getType('{"key":"value"}', mockGetRegistry);
|
const result = await getType('{"key":"value"}', mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("json");
|
expect(result.value).toBe("json");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect TLD before domain", async () => {
|
it("should detect TLD before domain", async () => {
|
||||||
const result = await getType(".com", mockGetRegistry);
|
const result = await getType(".com", mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("tld");
|
expect(result.value).toBe("tld");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect IP before domain", async () => {
|
it("should detect IP before domain", async () => {
|
||||||
const result = await getType("8.8.8.8", mockGetRegistry);
|
const result = await getType("8.8.8.8", mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("ip4");
|
expect(result.value).toBe("ip4");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getType - Case sensitivity", () => {
|
describe("getType - Case sensitivity", () => {
|
||||||
it("should detect uppercase domains", async () => {
|
it("should detect uppercase domains", async () => {
|
||||||
const result = await getType("GOOGLE.COM", mockGetRegistry);
|
const result = await getType("GOOGLE.COM", mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("domain");
|
expect(result.value).toBe("domain");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect mixed case domains", async () => {
|
it("should detect mixed case domains", async () => {
|
||||||
const result = await getType("GoOgLe.CoM", mockGetRegistry);
|
const result = await getType("GoOgLe.CoM", mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("domain");
|
expect(result.value).toBe("domain");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect lowercase ASN", async () => {
|
it("should detect lowercase ASN", async () => {
|
||||||
const result = await getType("as12345", mockGetRegistry);
|
const result = await getType("as12345", mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("autnum");
|
expect(result.value).toBe("autnum");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect uppercase ASN", async () => {
|
it("should detect uppercase ASN", async () => {
|
||||||
const result = await getType("AS12345", mockGetRegistry);
|
const result = await getType("AS12345", mockGetRegistry);
|
||||||
expect(result.isOk).toBe(true);
|
expect(result.isOk).toBe(true);
|
||||||
if (result.isOk) {
|
if (result.isOk) {
|
||||||
expect(result.value).toBe("autnum");
|
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 {
|
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 {
|
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
|
// return the first HTTPS url, or the first URL
|
||||||
export function getBestURL(urls: [string, ...string[]]): string {
|
export function getBestURL(urls: [string, ...string[]]): string {
|
||||||
urls.forEach((url) => {
|
urls.forEach((url) => {
|
||||||
if (url.startsWith("https://")) return url;
|
if (url.startsWith("https://")) return url;
|
||||||
});
|
});
|
||||||
return urls[0];
|
return urls[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// given a URL, injects that URL into the query input,
|
// given a URL, injects that URL into the query input,
|
||||||
@@ -758,8 +758,8 @@ export function createRDAPLink(url, title) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
type ValidatorArgs = {
|
type ValidatorArgs = {
|
||||||
value: string;
|
value: string;
|
||||||
getRegistry: (type: RootRegistryType) => Promise<Register>;
|
getRegistry: (type: RootRegistryType) => Promise<Register>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -774,128 +774,114 @@ type ValidatorResult = boolean | string;
|
|||||||
* Type validators in priority order (most specific to most generic).
|
* Type validators in priority order (most specific to most generic).
|
||||||
* Order matters: url/json/tld are checked before domain to avoid false matches.
|
* Order matters: url/json/tld are checked before domain to avoid false matches.
|
||||||
*/
|
*/
|
||||||
const TypeValidators = new Map<
|
const TypeValidators = new Map<TargetType, (args: ValidatorArgs) => Promise<ValidatorResult>>([
|
||||||
TargetType,
|
["url", ({ value }) => Promise.resolve(/^https?:/.test(value))],
|
||||||
(args: ValidatorArgs) => Promise<ValidatorResult>
|
["json", ({ value }) => Promise.resolve(/^{/.test(value))],
|
||||||
>([
|
["tld", ({ value }) => Promise.resolve(/^\.\w+$/.test(value))],
|
||||||
["url", ({ value }) => Promise.resolve(/^https?:/.test(value))],
|
[
|
||||||
["json", ({ value }) => Promise.resolve(/^{/.test(value))],
|
"ip4",
|
||||||
["tld", ({ value }) => Promise.resolve(/^\.\w+$/.test(value))],
|
({ value }) => {
|
||||||
[
|
// Basic format check
|
||||||
"ip4",
|
const match = value.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/\d{1,2})?$/);
|
||||||
({ value }) => {
|
if (!match) return Promise.resolve(false);
|
||||||
// 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
|
// Validate each octet is 0-255
|
||||||
const octets = [match[1], match[2], match[3], match[4]];
|
const octets = [match[1], match[2], match[3], match[4]];
|
||||||
for (let i = 0; i < octets.length; i++) {
|
for (let i = 0; i < octets.length; i++) {
|
||||||
const octet = parseInt(octets[i] ?? "", 10);
|
const octet = parseInt(octets[i] ?? "", 10);
|
||||||
if (isNaN(octet) || octet < 0 || octet > 255) {
|
if (isNaN(octet) || octet < 0 || octet > 255) {
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
`Invalid IPv4 address: octet ${i + 1} (${octets[i] ?? 'undefined'}) must be 0-255`
|
`Invalid IPv4 address: octet ${i + 1} (${octets[i] ?? "undefined"}) must be 0-255`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate CIDR prefix if present
|
// Validate CIDR prefix if present
|
||||||
if (match[5]) {
|
if (match[5]) {
|
||||||
const prefix = parseInt(match[5].substring(1), 10);
|
const prefix = parseInt(match[5].substring(1), 10);
|
||||||
if (isNaN(prefix) || prefix < 0 || prefix > 32) {
|
if (isNaN(prefix) || prefix < 0 || prefix > 32) {
|
||||||
return Promise.resolve(
|
return Promise.resolve("Invalid IPv4 address: CIDR prefix must be 0-32");
|
||||||
"Invalid IPv4 address: CIDR prefix must be 0-32"
|
}
|
||||||
);
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"ip6",
|
"ip6",
|
||||||
({ value }) => {
|
({ value }) => {
|
||||||
// Basic format check (hex characters, colons, optional CIDR)
|
// Basic format check (hex characters, colons, optional CIDR)
|
||||||
const match = value.match(/^([0-9a-fA-F:]+)(\/\d{1,3})?$/);
|
const match = value.match(/^([0-9a-fA-F:]+)(\/\d{1,3})?$/);
|
||||||
if (!match) return Promise.resolve(false);
|
if (!match) return Promise.resolve(false);
|
||||||
|
|
||||||
const ipPart = match[1] ?? "";
|
const ipPart = match[1] ?? "";
|
||||||
|
|
||||||
// Check for invalid characters
|
// Check for invalid characters
|
||||||
if (!/^[0-9a-fA-F:]+$/.test(ipPart)) {
|
if (!/^[0-9a-fA-F:]+$/.test(ipPart)) {
|
||||||
return Promise.resolve(
|
return Promise.resolve("Invalid IPv6 address: contains invalid characters");
|
||||||
"Invalid IPv6 address: contains invalid characters"
|
}
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate double :: only appears once
|
// Validate double :: only appears once
|
||||||
const doubleColonCount = (ipPart.match(/::/g) || []).length;
|
const doubleColonCount = (ipPart.match(/::/g) || []).length;
|
||||||
if (doubleColonCount > 1) {
|
if (doubleColonCount > 1) {
|
||||||
return Promise.resolve("Invalid IPv6 address: :: can only appear once");
|
return Promise.resolve("Invalid IPv6 address: :: can only appear once");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate CIDR prefix if present
|
// Validate CIDR prefix if present
|
||||||
if (match[2]) {
|
if (match[2]) {
|
||||||
const prefix = parseInt(match[2].substring(1), 10);
|
const prefix = parseInt(match[2].substring(1), 10);
|
||||||
if (isNaN(prefix) || prefix < 0 || prefix > 128) {
|
if (isNaN(prefix) || prefix < 0 || prefix > 128) {
|
||||||
return Promise.resolve(
|
return Promise.resolve("Invalid IPv6 address: CIDR prefix must be 0-128");
|
||||||
"Invalid IPv6 address: CIDR prefix must be 0-128"
|
}
|
||||||
);
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
["autnum", ({ value }) => Promise.resolve(/^AS\d+$/i.test(value))],
|
["autnum", ({ value }) => Promise.resolve(/^AS\d+$/i.test(value))],
|
||||||
[
|
[
|
||||||
"entity",
|
"entity",
|
||||||
async ({ value, getRegistry }) => {
|
async ({ value, getRegistry }) => {
|
||||||
// Ensure the entity handle is in the correct format
|
// Ensure the entity handle is in the correct format
|
||||||
const result = value.match(/^\w+-(\w+)$/);
|
const result = value.match(/^\w+-(\w+)$/);
|
||||||
if (result === null || result.length <= 1 || result[1] == undefined)
|
if (result === null || result.length <= 1 || result[1] == undefined) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
// Check if the entity object tag is real
|
// Check if the entity object tag is real
|
||||||
try {
|
try {
|
||||||
const registry = await getRegistry("entity");
|
const registry = await getRegistry("entity");
|
||||||
|
|
||||||
// Check each service to see if tag matches
|
// Check each service to see if tag matches
|
||||||
// Entity registry structure: [email, tags, urls]
|
// Entity registry structure: [email, tags, urls]
|
||||||
for (const service of registry.services) {
|
for (const service of registry.services) {
|
||||||
const tags = service[1]; // Tags are at index 1 (0=email, 1=tags, 2=urls)
|
const tags = service[1]; // Tags are at index 1 (0=email, 1=tags, 2=urls)
|
||||||
if (
|
if (
|
||||||
tags.some(
|
tags.some(
|
||||||
(tag) => tag.toUpperCase() === (result[1] as string).toUpperCase()
|
(tag) => tag.toUpperCase() === (result[1] as string).toUpperCase()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
console.error(new Error("Failed to fetch entity registry", { cause: e }));
|
||||||
new Error("Failed to fetch entity registry", { cause: e })
|
return false;
|
||||||
);
|
}
|
||||||
return false;
|
},
|
||||||
}
|
],
|
||||||
},
|
[
|
||||||
],
|
"domain",
|
||||||
[
|
({ value }) => {
|
||||||
"domain",
|
// Case-insensitive domain matching with support for multiple labels
|
||||||
({ value }) => {
|
// Matches: example.com, www.example.com, a.b.c.d.example.net, etc.
|
||||||
// Case-insensitive domain matching with support for multiple labels
|
return Promise.resolve(
|
||||||
// Matches: example.com, www.example.com, a.b.c.d.example.net, etc.
|
/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/i.test(
|
||||||
return Promise.resolve(
|
value
|
||||||
/^[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)],
|
||||||
],
|
|
||||||
["registrar", () => Promise.resolve(false)],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -910,23 +896,23 @@ const TypeValidators = new Map<
|
|||||||
* or an `Error` if the value is invalid or no patterns match.
|
* or an `Error` if the value is invalid or no patterns match.
|
||||||
*/
|
*/
|
||||||
export async function getType(
|
export async function getType(
|
||||||
value: string,
|
value: string,
|
||||||
getRegistry: (type: RootRegistryType) => Promise<Register>
|
getRegistry: (type: RootRegistryType) => Promise<Register>
|
||||||
): Promise<Result<TargetType, Error>> {
|
): Promise<Result<TargetType, Error>> {
|
||||||
for (const [type, validator] of TypeValidators.entries()) {
|
for (const [type, validator] of TypeValidators.entries()) {
|
||||||
const result = await validator({ value, getRegistry });
|
const result = await validator({ value, getRegistry });
|
||||||
|
|
||||||
if (result === false) {
|
if (result === false) {
|
||||||
// Didn't match this type, try next validator
|
// Didn't match this type, try next validator
|
||||||
continue;
|
continue;
|
||||||
} else if (result === true) {
|
} else if (result === true) {
|
||||||
// Matched and valid
|
// Matched and valid
|
||||||
return Result.ok(type);
|
return Result.ok(type);
|
||||||
} else {
|
} else {
|
||||||
// Matched but invalid (result is error message)
|
// Matched but invalid (result is error message)
|
||||||
return Result.err(new Error(result));
|
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";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const TargetTypeEnum = z.enum([
|
export const TargetTypeEnum = z.enum([
|
||||||
"autnum",
|
"autnum",
|
||||||
"domain",
|
"domain",
|
||||||
"ip4",
|
"ip4",
|
||||||
"ip6",
|
"ip6",
|
||||||
"entity",
|
"entity",
|
||||||
"url",
|
"url",
|
||||||
"tld",
|
"tld",
|
||||||
"registrar",
|
"registrar",
|
||||||
"json",
|
"json",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const RootRegistryEnum = z.enum([
|
export const RootRegistryEnum = z.enum(["autnum", "domain", "ip4", "ip6", "entity"]);
|
||||||
"autnum",
|
|
||||||
"domain",
|
|
||||||
"ip4",
|
|
||||||
"ip6",
|
|
||||||
"entity",
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const StatusEnum = z.enum([
|
export const StatusEnum = z.enum([
|
||||||
"validated",
|
"validated",
|
||||||
"renew prohibited",
|
"renew prohibited",
|
||||||
"update prohibited",
|
"update prohibited",
|
||||||
"transfer prohibited",
|
"transfer prohibited",
|
||||||
"delete prohibited",
|
"delete prohibited",
|
||||||
"proxy",
|
"proxy",
|
||||||
"private",
|
"private",
|
||||||
"removed",
|
"removed",
|
||||||
"obscured",
|
"obscured",
|
||||||
"associated",
|
"associated",
|
||||||
"active",
|
"active",
|
||||||
"inactive",
|
"inactive",
|
||||||
"locked",
|
"locked",
|
||||||
"pending create",
|
"pending create",
|
||||||
"pending renew",
|
"pending renew",
|
||||||
"pending transfer",
|
"pending transfer",
|
||||||
"pending update",
|
"pending update",
|
||||||
"pending delete",
|
"pending delete",
|
||||||
"add period",
|
"add period",
|
||||||
"auto renew period",
|
"auto renew period",
|
||||||
"client delete prohibited",
|
"client delete prohibited",
|
||||||
"client hold",
|
"client hold",
|
||||||
"client renew prohibited",
|
"client renew prohibited",
|
||||||
"client transfer prohibited",
|
"client transfer prohibited",
|
||||||
"client update prohibited",
|
"client update prohibited",
|
||||||
"pending restore",
|
"pending restore",
|
||||||
"redemption period",
|
"redemption period",
|
||||||
"renew period",
|
"renew period",
|
||||||
"server delete prohibited",
|
"server delete prohibited",
|
||||||
"server renew prohibited",
|
"server renew prohibited",
|
||||||
"server transfer prohibited",
|
"server transfer prohibited",
|
||||||
"server update prohibited",
|
"server update prohibited",
|
||||||
"server hold",
|
"server hold",
|
||||||
"transfer period",
|
"transfer period",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const LinkSchema = z.object({
|
export const LinkSchema = z.object({
|
||||||
value: z.string().optional(), // de-facto optional
|
value: z.string().optional(), // de-facto optional
|
||||||
rel: z.string().optional(), // de-facto optional
|
rel: z.string().optional(), // de-facto optional
|
||||||
href: z.string(),
|
href: z.string(),
|
||||||
hrefLang: z.array(z.string()).optional(),
|
hrefLang: z.array(z.string()).optional(),
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
media: z.string().optional(),
|
media: z.string().optional(),
|
||||||
type: z.string().optional(),
|
type: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const EntitySchema = z.object({
|
export const EntitySchema = z.object({
|
||||||
objectClassName: z.literal("entity"),
|
objectClassName: z.literal("entity"),
|
||||||
handle: z.string().optional(),
|
handle: z.string().optional(),
|
||||||
roles: z.array(z.string()),
|
roles: z.array(z.string()),
|
||||||
publicIds: z
|
publicIds: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
identifier: z.string(),
|
identifier: z.string(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NameserverSchema = z.object({
|
export const NameserverSchema = z.object({
|
||||||
objectClassName: z.literal("nameserver"),
|
objectClassName: z.literal("nameserver"),
|
||||||
ldhName: z.string(),
|
ldhName: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const EventSchema = z.object({
|
export const EventSchema = z.object({
|
||||||
eventAction: z.string(),
|
eventAction: z.string(),
|
||||||
eventActor: z.string().optional(),
|
eventActor: z.string().optional(),
|
||||||
eventDate: z.string(),
|
eventDate: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NoticeSchema = z.object({
|
export const NoticeSchema = z.object({
|
||||||
description: z.string().array(), // de jure required
|
description: z.string().array(), // de jure required
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
links: z.array(LinkSchema).optional(),
|
links: z.array(LinkSchema).optional(),
|
||||||
});
|
});
|
||||||
export type Notice = z.infer<typeof NoticeSchema>;
|
export type Notice = z.infer<typeof NoticeSchema>;
|
||||||
|
|
||||||
export const IpNetworkSchema = z.object({
|
export const IpNetworkSchema = z.object({
|
||||||
objectClassName: z.literal("ip network"),
|
objectClassName: z.literal("ip network"),
|
||||||
handle: z.string(),
|
handle: z.string(),
|
||||||
startAddress: z.string(),
|
startAddress: z.string(),
|
||||||
endAddress: z.string(),
|
endAddress: z.string(),
|
||||||
ipVersion: z.enum(["v4", "v6"]),
|
ipVersion: z.enum(["v4", "v6"]),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
country: z.string().optional(),
|
country: z.string().optional(),
|
||||||
parentHandle: z.string().optional(),
|
parentHandle: z.string().optional(),
|
||||||
status: z.string().array(),
|
status: z.string().array(),
|
||||||
entities: z.array(EntitySchema).optional(),
|
entities: z.array(EntitySchema).optional(),
|
||||||
remarks: z.any().optional(),
|
remarks: z.any().optional(),
|
||||||
links: z.any().optional(),
|
links: z.any().optional(),
|
||||||
port43: z.any().optional(),
|
port43: z.any().optional(),
|
||||||
events: z.array(EventSchema),
|
events: z.array(EventSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AutonomousNumberSchema = z.object({
|
export const AutonomousNumberSchema = z.object({
|
||||||
objectClassName: z.literal("autnum"),
|
objectClassName: z.literal("autnum"),
|
||||||
handle: z.string(),
|
handle: z.string(),
|
||||||
startAutnum: z.number().positive(), // TODO: 32bit
|
startAutnum: z.number().positive(), // TODO: 32bit
|
||||||
endAutnum: z.number().positive(), // TODO: 32bit
|
endAutnum: z.number().positive(), // TODO: 32bit
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
status: z.array(z.string()),
|
status: z.array(z.string()),
|
||||||
country: z.string().length(2),
|
country: z.string().length(2),
|
||||||
events: z.array(EventSchema),
|
events: z.array(EventSchema),
|
||||||
entities: z.array(EntitySchema),
|
entities: z.array(EntitySchema),
|
||||||
roles: z.array(z.string()),
|
roles: z.array(z.string()),
|
||||||
links: z.array(LinkSchema),
|
links: z.array(LinkSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const DomainSchema = z.object({
|
export const DomainSchema = z.object({
|
||||||
objectClassName: z.literal("domain"),
|
objectClassName: z.literal("domain"),
|
||||||
handle: z.string(),
|
handle: z.string(),
|
||||||
ldhName: z.string(),
|
ldhName: z.string(),
|
||||||
unicodeName: z.string().optional(),
|
unicodeName: z.string().optional(),
|
||||||
links: z.array(LinkSchema).optional(),
|
links: z.array(LinkSchema).optional(),
|
||||||
status: z.array(StatusEnum),
|
status: z.array(StatusEnum),
|
||||||
entities: z.array(EntitySchema),
|
entities: z.array(EntitySchema),
|
||||||
events: z.array(EventSchema),
|
events: z.array(EventSchema),
|
||||||
secureDNS: z.any(), // TODO: Complete schema
|
secureDNS: z.any(), // TODO: Complete schema
|
||||||
nameservers: z.array(NameserverSchema),
|
nameservers: z.array(NameserverSchema),
|
||||||
rdapConformance: z.string().array(), // TODO: Complete
|
rdapConformance: z.string().array(), // TODO: Complete
|
||||||
notices: z.array(NoticeSchema),
|
notices: z.array(NoticeSchema),
|
||||||
network: IpNetworkSchema.optional(),
|
network: IpNetworkSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const RegistrarSchema = z
|
const RegistrarSchema = z
|
||||||
.tuple([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(
|
.or(
|
||||||
z.tuple([
|
z.tuple([
|
||||||
z.array(z.string()).min(1),
|
z.array(z.string()).min(1),
|
||||||
z.array(z.string()).min(1),
|
z.array(z.string()).min(1),
|
||||||
z.array(z.string()).min(1),
|
z.array(z.string()).min(1),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
export const RegisterSchema = z.object({
|
export const RegisterSchema = z.object({
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
publication: z.string(),
|
publication: z.string(),
|
||||||
services: z.array(RegistrarSchema),
|
services: z.array(RegistrarSchema),
|
||||||
version: z.string(),
|
version: z.string(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,33 +1,37 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@theme {
|
@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-sans:
|
||||||
--font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
"Inter var", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||||
--color-zinc-850: #1D1D20;
|
"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 {
|
dd {
|
||||||
margin: 0.5em 0 1em 2em;
|
margin: 0.5em 0 1em 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashed {
|
.dashed {
|
||||||
border-bottom: 1px dashed silver;
|
border-bottom: 1px dashed silver;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
@apply bg-zinc-900 font-sans text-white;
|
@apply bg-zinc-900 font-sans text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
dd,
|
dd,
|
||||||
dl {
|
dl {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
dl {
|
dl {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-thin {
|
.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 { z } from "zod";
|
||||||
import type {
|
import type {
|
||||||
AutonomousNumberSchema,
|
AutonomousNumberSchema,
|
||||||
DomainSchema,
|
DomainSchema,
|
||||||
EntitySchema,
|
EntitySchema,
|
||||||
EventSchema,
|
EventSchema,
|
||||||
IpNetworkSchema,
|
IpNetworkSchema,
|
||||||
LinkSchema,
|
LinkSchema,
|
||||||
NameserverSchema,
|
NameserverSchema,
|
||||||
TargetTypeEnum,
|
TargetTypeEnum,
|
||||||
RegisterSchema,
|
RegisterSchema,
|
||||||
StatusEnum,
|
StatusEnum,
|
||||||
RootRegistryEnum,
|
RootRegistryEnum,
|
||||||
} from "@/schema";
|
} from "@/schema";
|
||||||
|
|
||||||
// All precise target types that can be placed in the search bar.
|
// 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 Domain = z.infer<typeof DomainSchema>;
|
||||||
|
|
||||||
export type SubmitProps = {
|
export type SubmitProps = {
|
||||||
target: string;
|
target: string;
|
||||||
requestJSContact: boolean;
|
requestJSContact: boolean;
|
||||||
followReferral: boolean;
|
followReferral: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2017",
|
"target": "es2017",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
"baseUrl": "./src/",
|
"baseUrl": "./src/",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/config/*": ["../config/*"],
|
"@/config/*": ["../config/*"],
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { defineConfig } from "vitest/config";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: "happy-dom",
|
environment: "happy-dom",
|
||||||
setupFiles: ["./src/test/setup.ts"],
|
setupFiles: ["./src/test/setup.ts"],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user