mirror of
https://github.com/Xevion/rdap.git
synced 2025-12-05 23:15:58 -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"
|
||||
]
|
||||
}
|
||||
@@ -10,7 +10,7 @@ const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default [
|
||||
@@ -22,7 +22,7 @@ export default [
|
||||
"out/**",
|
||||
"*.config.mjs",
|
||||
"*.config.js",
|
||||
"next-env.d.ts" // Next.js generated file
|
||||
"next-env.d.ts", // Next.js generated file
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
15
package.json
15
package.json
@@ -6,13 +6,18 @@
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"start": "next start",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:run": "vitest run --exclude '**/*.integration.test.ts'",
|
||||
"test:integration": "vitest run --include '**/*.integration.test.ts'",
|
||||
"test:all": "vitest run",
|
||||
"type-check": "tsc --noEmit"
|
||||
"type-check": "tsc --noEmit",
|
||||
"prepare": "husky install",
|
||||
"semantic-release": "semantic-release"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "^5.2.8",
|
||||
@@ -35,6 +40,10 @@
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.0.0",
|
||||
"@commitlint/config-conventional": "^19.0.0",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@tailwindcss/postcss": "^4.1.15",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@@ -44,12 +53,16 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
||||
"@typescript-eslint/parser": "^8.46.2",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"conventional-changelog-conventionalcommits": "^8.0.0",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint-config-next": "15.5.6",
|
||||
"happy-dom": "^20.0.8",
|
||||
"husky": "^9.0.0",
|
||||
"lint-staged": "^15.0.0",
|
||||
"postcss": "^8.4.14",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-tailwindcss": "^0.7.1",
|
||||
"semantic-release": "^24.0.0",
|
||||
"tailwindcss": "^4.1.15",
|
||||
"type-fest": "^5.1.0",
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
2652
pnpm-lock.yaml
generated
2652
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
/** @type {import("prettier").Config} */
|
||||
module.exports = {
|
||||
plugins: [require.resolve("prettier-plugin-tailwindcss")],
|
||||
};
|
||||
@@ -54,7 +54,10 @@ const AbstractCard: FunctionComponent<AbstractCardProps> = ({
|
||||
console.error(
|
||||
`Failed to copy to clipboard (${err.toString()}).`
|
||||
);
|
||||
else console.error("Failed to copy to clipboard.");
|
||||
else
|
||||
console.error(
|
||||
"Failed to copy to clipboard."
|
||||
);
|
||||
}
|
||||
);
|
||||
}}
|
||||
@@ -88,7 +91,7 @@ const AbstractCard: FunctionComponent<AbstractCardProps> = ({
|
||||
) : null}
|
||||
<div className="max-w-full overflow-x-auto p-2 px-4">
|
||||
{showRaw ? (
|
||||
<pre className="scrollbar-thin m-2 max-h-[40rem] max-w-full overflow-y-auto whitespace-pre-wrap rounded">
|
||||
<pre className="scrollbar-thin m-2 max-h-[40rem] max-w-full overflow-y-auto rounded whitespace-pre-wrap">
|
||||
{JSON.stringify(data, null, 4)}
|
||||
</pre>
|
||||
) : (
|
||||
|
||||
@@ -13,10 +13,7 @@ type DynamicDateProps = {
|
||||
* @param value The date to be displayed, the Date value, or
|
||||
* @param absoluteFormat Optional - the date-fns format string to use for the absolute date rendering.
|
||||
*/
|
||||
const DynamicDate: FunctionComponent<DynamicDateProps> = ({
|
||||
value,
|
||||
absoluteFormat,
|
||||
}) => {
|
||||
const DynamicDate: FunctionComponent<DynamicDateProps> = ({ value, absoluteFormat }) => {
|
||||
const { value: showAbsolute, toggle: toggleFormat } = useBoolean(true);
|
||||
|
||||
const date = new Date(value);
|
||||
|
||||
@@ -19,7 +19,7 @@ const ErrorCard: FunctionComponent<ErrorCardProps> = ({
|
||||
<div
|
||||
className={cn(
|
||||
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">
|
||||
|
||||
@@ -18,7 +18,7 @@ const Property: FunctionComponent<PropertyProps> = ({
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -43,10 +43,7 @@ type LookupInputProps = {
|
||||
* @param target - The target object containing the search target and target type.
|
||||
* @returns Nothing.
|
||||
*/
|
||||
onChange?: (target: {
|
||||
target: string;
|
||||
targetType: TargetType | null;
|
||||
}) => Promise<void>;
|
||||
onChange?: (target: { target: string; targetType: TargetType | null }) => Promise<void>;
|
||||
detectedType: Maybe<TargetType>;
|
||||
};
|
||||
|
||||
@@ -98,9 +95,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
||||
/**
|
||||
* Represents the selected value in the LookupInput component.
|
||||
*/
|
||||
const [selected, setSelected] = useState<SimplifiedTargetType | "auto">(
|
||||
"auto"
|
||||
);
|
||||
const [selected, setSelected] = useState<SimplifiedTargetType | "auto">("auto");
|
||||
|
||||
/**
|
||||
* Retrieves the target type based on the provided value.
|
||||
@@ -132,10 +127,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<MagnifyingGlassIcon
|
||||
className="h-5 w-5 text-zinc-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<MagnifyingGlassIcon className="h-5 w-5 text-zinc-400" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
@@ -144,9 +136,9 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
||||
const searchInput = (
|
||||
<input
|
||||
className={cn(
|
||||
"lg:py-4.5 block w-full rounded-l-md border border-transparent",
|
||||
"bg-zinc-700 py-2 pl-10 pr-1.5 text-sm placeholder-zinc-400 placeholder:translate-y-2 focus:text-zinc-200",
|
||||
" focus:outline-hidden sm:text-sm md:py-3 md:text-base lg:text-lg"
|
||||
"block w-full rounded-l-md border border-transparent lg:py-4.5",
|
||||
"bg-zinc-700 py-2 pr-1.5 pl-10 text-sm placeholder-zinc-400 placeholder:translate-y-2 focus:text-zinc-200",
|
||||
"focus:outline-hidden sm:text-sm md:py-3 md:text-base lg:text-lg"
|
||||
)}
|
||||
disabled={isLoading}
|
||||
placeholder={placeholders[selected]}
|
||||
@@ -183,9 +175,9 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
||||
<div className="relative">
|
||||
<ListboxButton
|
||||
className={cn(
|
||||
"relative h-full w-full cursor-default whitespace-nowrap rounded-r-lg bg-zinc-700 py-2 pl-1 pr-10 text-right",
|
||||
"relative h-full w-full cursor-default rounded-r-lg bg-zinc-700 py-2 pr-10 pl-1 text-right whitespace-nowrap",
|
||||
"text-xs focus:outline-hidden focus-visible:border-indigo-500 sm:text-sm md:text-base lg:text-lg",
|
||||
"focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 "
|
||||
"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. */}
|
||||
@@ -206,7 +198,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
||||
) : (
|
||||
<>
|
||||
<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
|
||||
/>
|
||||
{objectNames[selected]}
|
||||
@@ -214,10 +206,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
||||
)}
|
||||
</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronUpDownIcon
|
||||
className="h-5 w-5 text-zinc-200"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<ChevronUpDownIcon className="h-5 w-5 text-zinc-200" aria-hidden="true" />
|
||||
</span>
|
||||
</ListboxButton>
|
||||
<Transition
|
||||
@@ -237,7 +226,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
||||
key={key}
|
||||
className={({ focus }) =>
|
||||
cn(
|
||||
"relative cursor-default select-none py-2 pl-10 pr-4",
|
||||
"relative cursor-default py-2 pr-4 pl-10 select-none",
|
||||
focus ? "bg-zinc-800 text-zinc-300" : null
|
||||
)
|
||||
}
|
||||
@@ -247,7 +236,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
||||
<>
|
||||
<span
|
||||
className={cn(
|
||||
"block whitespace-nowrap text-right text-xs md:text-sm lg:text-base",
|
||||
"block text-right text-xs whitespace-nowrap md:text-sm lg:text-base",
|
||||
selected ? "font-medium" : null
|
||||
)}
|
||||
>
|
||||
@@ -281,11 +270,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
||||
return (
|
||||
<form
|
||||
className="pb-3"
|
||||
onSubmit={
|
||||
onSubmit != undefined
|
||||
? onPromise(handleSubmit(onSubmit))
|
||||
: preventDefault
|
||||
}
|
||||
onSubmit={onSubmit != undefined ? onPromise(handleSubmit(onSubmit)) : preventDefault}
|
||||
>
|
||||
<div className="col">
|
||||
<label htmlFor="search" className="sr-only">
|
||||
@@ -298,10 +283,10 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="flex flex-wrap pb-1 pt-3 text-sm">
|
||||
<div className="flex flex-wrap pt-3 pb-1 text-sm">
|
||||
<div className="whitespace-nowrap">
|
||||
<input
|
||||
className="ml-2 mr-1 whitespace-nowrap text-zinc-800 accent-blue-700"
|
||||
className="mr-1 ml-2 whitespace-nowrap text-zinc-800 accent-blue-700"
|
||||
type="checkbox"
|
||||
{...register("requestJSContact")}
|
||||
/>
|
||||
@@ -311,7 +296,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
||||
</div>
|
||||
<div className="whitespace-nowrap">
|
||||
<input
|
||||
className="ml-2 mr-1 bg-zinc-500 text-inherit accent-blue-700"
|
||||
className="mr-1 ml-2 bg-zinc-500 text-inherit accent-blue-700"
|
||||
type="checkbox"
|
||||
{...register("followReferral")}
|
||||
/>
|
||||
|
||||
@@ -11,10 +11,7 @@ export type AutnumCardProps = {
|
||||
url?: string;
|
||||
};
|
||||
|
||||
const AutnumCard: FunctionComponent<AutnumCardProps> = ({
|
||||
data,
|
||||
url,
|
||||
}: AutnumCardProps) => {
|
||||
const AutnumCard: FunctionComponent<AutnumCardProps> = ({ data, url }: AutnumCardProps) => {
|
||||
const asnRange =
|
||||
data.startAutnum === data.endAutnum
|
||||
? `AS${data.startAutnum}`
|
||||
|
||||
@@ -12,10 +12,7 @@ export type DomainProps = {
|
||||
url?: string;
|
||||
};
|
||||
|
||||
const DomainCard: FunctionComponent<DomainProps> = ({
|
||||
data,
|
||||
url,
|
||||
}: DomainProps) => {
|
||||
const DomainCard: FunctionComponent<DomainProps> = ({ data, url }: DomainProps) => {
|
||||
return (
|
||||
<AbstractCard
|
||||
data={data}
|
||||
|
||||
@@ -2,31 +2,17 @@ import type { FunctionComponent } from "react";
|
||||
import DomainCard from "@/components/lookup/DomainCard";
|
||||
import IPCard from "@/components/lookup/IPCard";
|
||||
import AutnumCard from "@/components/lookup/AutnumCard";
|
||||
import type {
|
||||
Domain,
|
||||
AutonomousNumber,
|
||||
Entity,
|
||||
Nameserver,
|
||||
IpNetwork,
|
||||
} from "@/types";
|
||||
import type { Domain, AutonomousNumber, Entity, Nameserver, IpNetwork } from "@/types";
|
||||
import AbstractCard from "@/components/common/AbstractCard";
|
||||
|
||||
export type ParsedGeneric =
|
||||
| Domain
|
||||
| Nameserver
|
||||
| Entity
|
||||
| AutonomousNumber
|
||||
| IpNetwork;
|
||||
export type ParsedGeneric = Domain | Nameserver | Entity | AutonomousNumber | IpNetwork;
|
||||
|
||||
export type ObjectProps = {
|
||||
data: ParsedGeneric;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
const Generic: FunctionComponent<ObjectProps> = ({
|
||||
data,
|
||||
url,
|
||||
}: ObjectProps) => {
|
||||
const Generic: FunctionComponent<ObjectProps> = ({ data, url }: ObjectProps) => {
|
||||
switch (data.objectClassName) {
|
||||
case "domain":
|
||||
return <DomainCard url={url} data={data} />;
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
// see https://www.iana.org/assignments/rdap-json-values
|
||||
import type {
|
||||
RdapStatusType,
|
||||
RootRegistryType,
|
||||
SimplifiedTargetType,
|
||||
} from "@/types";
|
||||
import type { RdapStatusType, RootRegistryType, SimplifiedTargetType } from "@/types";
|
||||
|
||||
export const rdapStatusInfo: Record<RdapStatusType, string> = {
|
||||
validated:
|
||||
"Signifies that the data of the object instance has been found to be accurate. This type of status is usually found on entity object instances to note the validity of identifying contact information.",
|
||||
"renew prohibited":
|
||||
"Renewal or reregistration of the object instance is forbidden.",
|
||||
"renew prohibited": "Renewal or reregistration of the object instance is forbidden.",
|
||||
"update prohibited": "Updates to the object instance are forbidden.",
|
||||
"transfer prohibited":
|
||||
"Transfers of the registration from one registrar to another are forbidden. This type of status normally applies to DNR domain names.",
|
||||
"delete prohibited":
|
||||
"Deletion of the registration of the object instance is forbidden. This type of status normally applies to DNR domain names.",
|
||||
proxy:
|
||||
"The registration of the object instance has been performed by a third party. This is most commonly applied to entities.",
|
||||
proxy: "The registration of the object instance has been performed by a third party. This is most commonly applied to entities.",
|
||||
private:
|
||||
"The information of the object instance is not designated for public consumption. This is most commonly applied to entities.",
|
||||
removed:
|
||||
@@ -25,11 +19,9 @@ export const rdapStatusInfo: Record<RdapStatusType, string> = {
|
||||
"Some of the information of the object instance has been altered for the purposes of not readily revealing the actual information of the object instance. This is most commonly applied to entities.",
|
||||
associated:
|
||||
"The object instance is associated with other object instances in the registry. This is most commonly used to signify that a nameserver is associated with a domain or that an entity is associated with a network resource or domain.",
|
||||
active:
|
||||
"The object instance is in use. For domain names, it signifies that the domain name is published in DNS. For network and autnum registrations it signifies that they are allocated or assigned for use in operational networks. This maps to the Extensible Provisioning Protocol (EPP) [RFC5730] 'OK' status.",
|
||||
active: "The object instance is in use. For domain names, it signifies that the domain name is published in DNS. For network and autnum registrations it signifies that they are allocated or assigned for use in operational networks. This maps to the Extensible Provisioning Protocol (EPP) [RFC5730] 'OK' status.",
|
||||
inactive: "The object instance is not in use. See 'active'.",
|
||||
locked:
|
||||
"Changes to the object instance cannot be made, including the association of other object instances.",
|
||||
locked: "Changes to the object instance cannot be made, including the association of other object instances.",
|
||||
"pending create":
|
||||
"A request has been received for the creation of the object instance but this action is not yet complete.",
|
||||
"pending renew":
|
||||
|
||||
@@ -94,9 +94,9 @@ describe("ipv6InCIDR", () => {
|
||||
it("should match IPv6 in /32 network", () => {
|
||||
expect(ipv6InCIDR("2001:db8::", "2001:db8::/32")).toBe(true);
|
||||
expect(ipv6InCIDR("2001:db8:1234::", "2001:db8::/32")).toBe(true);
|
||||
expect(
|
||||
ipv6InCIDR("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff", "2001:db8::/32")
|
||||
).toBe(true);
|
||||
expect(ipv6InCIDR("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff", "2001:db8::/32")).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it("should not match IPv6 outside /32 network", () => {
|
||||
@@ -105,24 +105,15 @@ describe("ipv6InCIDR", () => {
|
||||
});
|
||||
|
||||
it("should match IPv6 in /64 network", () => {
|
||||
expect(ipv6InCIDR("2001:db8:1234:5678::", "2001:db8:1234:5678::/64")).toBe(true);
|
||||
expect(ipv6InCIDR("2001:db8:1234:5678:abcd::", "2001:db8:1234:5678::/64")).toBe(true);
|
||||
expect(
|
||||
ipv6InCIDR("2001:db8:1234:5678::", "2001:db8:1234:5678::/64")
|
||||
).toBe(true);
|
||||
expect(
|
||||
ipv6InCIDR("2001:db8:1234:5678:abcd::", "2001:db8:1234:5678::/64")
|
||||
).toBe(true);
|
||||
expect(
|
||||
ipv6InCIDR(
|
||||
"2001:db8:1234:5678:ffff:ffff:ffff:ffff",
|
||||
"2001:db8:1234:5678::/64"
|
||||
)
|
||||
ipv6InCIDR("2001:db8:1234:5678:ffff:ffff:ffff:ffff", "2001:db8:1234:5678::/64")
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("should not match IPv6 outside /64 network", () => {
|
||||
expect(
|
||||
ipv6InCIDR("2001:db8:1234:5679::", "2001:db8:1234:5678::/64")
|
||||
).toBe(false);
|
||||
expect(ipv6InCIDR("2001:db8:1234:5679::", "2001:db8:1234:5678::/64")).toBe(false);
|
||||
});
|
||||
|
||||
it("should match IPv6 in /128 network (single host)", () => {
|
||||
@@ -154,12 +145,12 @@ describe("ipv6InCIDR", () => {
|
||||
});
|
||||
|
||||
it("should handle expanded vs compressed notation", () => {
|
||||
expect(
|
||||
ipv6InCIDR("2001:0db8:0000:0000:0000:0000:0000:0001", "2001:db8::/32")
|
||||
).toBe(true);
|
||||
expect(
|
||||
ipv6InCIDR("2001:db8::1", "2001:0db8:0000:0000:0000:0000:0000:0000/32")
|
||||
).toBe(true);
|
||||
expect(ipv6InCIDR("2001:0db8:0000:0000:0000:0000:0000:0001", "2001:db8::/32")).toBe(
|
||||
true
|
||||
);
|
||||
expect(ipv6InCIDR("2001:db8::1", "2001:0db8:0000:0000:0000:0000:0000:0000/32")).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -32,9 +32,7 @@ export function onPromise<T>(promise: (event: SyntheticEvent) => Promise<T>) {
|
||||
export function truncated(input: string, maxLength: number, ellipsis = "...") {
|
||||
if (maxLength <= 0) return "";
|
||||
if (input.length <= maxLength) return input;
|
||||
return (
|
||||
input.substring(0, Math.max(0, maxLength - ellipsis.length)) + ellipsis
|
||||
);
|
||||
return input.substring(0, Math.max(0, maxLength - ellipsis.length)) + ellipsis;
|
||||
}
|
||||
|
||||
export function preventDefault(event: SyntheticEvent | Event) {
|
||||
@@ -48,8 +46,7 @@ function ipv4ToInt(ip: string): number {
|
||||
const parts = ip.split(".").map(Number);
|
||||
if (parts.length !== 4) return 0;
|
||||
const [a, b, c, d] = parts;
|
||||
if (a === undefined || b === undefined || c === undefined || d === undefined)
|
||||
return 0;
|
||||
if (a === undefined || b === undefined || c === undefined || d === undefined) return 0;
|
||||
return ((a << 24) | (b << 16) | (c << 8) | d) >>> 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,9 +53,8 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
||||
|
||||
useCallback(async () => {
|
||||
if (currentType != null) return Maybe.just(currentType);
|
||||
const uri: Maybe<TargetType> = (await getTypeEasy(target)).mapOr(
|
||||
Maybe.nothing(),
|
||||
(type) => Maybe.just(type)
|
||||
const uri: Maybe<TargetType> = (await getTypeEasy(target)).mapOr(Maybe.nothing(), (type) =>
|
||||
Maybe.just(type)
|
||||
);
|
||||
setUriType(uri);
|
||||
}, [target, currentType, getTypeEasy]);
|
||||
@@ -67,15 +66,12 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
||||
|
||||
// Fetch the bootstrapping file from the registry
|
||||
const response = await fetch(registryURLs[type]);
|
||||
if (response.status != 200)
|
||||
throw new Error(`Error: ${response.statusText}`);
|
||||
if (response.status != 200) throw new Error(`Error: ${response.statusText}`);
|
||||
|
||||
// Parse it, so we don't make any false assumptions during development & while maintaining the tool.
|
||||
const parsedRegister = RegisterSchema.safeParse(await response.json());
|
||||
if (!parsedRegister.success)
|
||||
throw new Error(
|
||||
`Could not parse IANA bootstrap response (type: ${type}).`
|
||||
);
|
||||
throw new Error(`Could not parse IANA bootstrap response (type: ${type}).`);
|
||||
|
||||
// Set it in state so we can use it.
|
||||
registryDataRef.current = {
|
||||
@@ -92,21 +88,14 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
||||
return registry;
|
||||
}
|
||||
|
||||
async function getTypeEasy(
|
||||
target: string
|
||||
): Promise<Result<TargetType, Error>> {
|
||||
async function getTypeEasy(target: string): Promise<Result<TargetType, Error>> {
|
||||
return getType(target, getRegistry);
|
||||
}
|
||||
|
||||
function getRegistryURL(
|
||||
type: RootRegistryType,
|
||||
lookupTarget: string
|
||||
): string {
|
||||
function getRegistryURL(type: RootRegistryType, lookupTarget: string): string {
|
||||
const bootstrap = registryDataRef.current[type];
|
||||
if (bootstrap == null)
|
||||
throw new Error(
|
||||
`Cannot acquire RDAP URL without bootstrap data for ${type} lookup.`
|
||||
);
|
||||
throw new Error(`Cannot acquire RDAP URL without bootstrap data for ${type} lookup.`);
|
||||
|
||||
let url: string | null = null;
|
||||
|
||||
@@ -198,8 +187,7 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
||||
await loadBootstrap(registryUri.data);
|
||||
} catch (e) {
|
||||
if (warningHandler != undefined) {
|
||||
const message =
|
||||
e instanceof Error ? `(${truncated(e.message, 15)})` : ".";
|
||||
const message = e instanceof Error ? `(${truncated(e.message, 15)})` : ".";
|
||||
warningHandler({
|
||||
message: `Failed to preload registry${message}`,
|
||||
});
|
||||
@@ -210,10 +198,7 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
||||
preload().catch(console.error);
|
||||
}, [target, uriType, warningHandler]);
|
||||
|
||||
async function getAndParse<T>(
|
||||
url: string,
|
||||
schema: ZodSchema<T>
|
||||
): Promise<Result<T, Error>> {
|
||||
async function getAndParse<T>(url: string, schema: ZodSchema<T>): Promise<Result<T, Error>> {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (response.status == 200) {
|
||||
@@ -277,9 +262,7 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
||||
);
|
||||
default:
|
||||
return Result.err(
|
||||
new Error(
|
||||
`The registry did not return an OK status code: ${response.status}.`
|
||||
)
|
||||
new Error(`The registry did not return an OK status code: ${response.status}.`)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -288,9 +271,7 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
||||
target: string
|
||||
): Promise<Result<{ data: ParsedGeneric; url: string }, Error>> {
|
||||
if (target == null || target.length == 0)
|
||||
return Result.err(
|
||||
new Error("A target must be given in order to execute a lookup.")
|
||||
);
|
||||
return Result.err(new Error("A target must be given in order to execute a lookup."));
|
||||
|
||||
const targetType = await getTypeEasy(target);
|
||||
|
||||
@@ -341,10 +322,7 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
||||
case "autnum": {
|
||||
await loadBootstrap("autnum");
|
||||
const url = getRegistryURL(targetType.value, target);
|
||||
const result = await getAndParse<AutonomousNumber>(
|
||||
url,
|
||||
AutonomousNumberSchema
|
||||
);
|
||||
const result = await getAndParse<AutonomousNumber>(url, AutonomousNumberSchema);
|
||||
if (result.isErr) return Result.err(result.error);
|
||||
return Result.ok({ data: result.value, url });
|
||||
}
|
||||
@@ -371,13 +349,10 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
||||
// Try each schema until one works
|
||||
for (const schema of schemas) {
|
||||
const result = schema.safeParse(data);
|
||||
if (result.success)
|
||||
return Result.ok({ data: result.data, url: target });
|
||||
if (result.success) return Result.ok({ data: result.data, url: target });
|
||||
}
|
||||
|
||||
return Result.err(
|
||||
new Error("No schema was able to parse the response.")
|
||||
);
|
||||
return Result.err(new Error("No schema was able to parse the response."));
|
||||
}
|
||||
case "json": {
|
||||
const data = JSON.parse(target);
|
||||
@@ -389,15 +364,11 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
||||
case "registrar": {
|
||||
}
|
||||
default:
|
||||
return Result.err(
|
||||
new Error("The type detected has not been implemented.")
|
||||
);
|
||||
return Result.err(new Error("The type detected has not been implemented."));
|
||||
}
|
||||
}
|
||||
|
||||
async function submit({
|
||||
target,
|
||||
}: SubmitProps): Promise<Maybe<MetaParsedGeneric>> {
|
||||
async function submit({ target }: SubmitProps): Promise<Maybe<MetaParsedGeneric>> {
|
||||
try {
|
||||
// target is already set in state, but it's also provided by the form callback, so we'll use it.
|
||||
const response = await submitInternal(target);
|
||||
@@ -415,8 +386,7 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
||||
})
|
||||
: Maybe.nothing();
|
||||
} catch (e) {
|
||||
if (!(e instanceof Error))
|
||||
setError("An unknown, unprocessable error has occurred.");
|
||||
if (!(e instanceof Error)) setError("An unknown, unprocessable error has occurred.");
|
||||
else setError(e.message);
|
||||
console.error(e);
|
||||
return Maybe.nothing();
|
||||
|
||||
@@ -11,12 +11,8 @@ import type { TargetType } from "@/types";
|
||||
|
||||
const Index: NextPage = () => {
|
||||
const { error, setTarget, setTargetType, submit, getType } = useLookup();
|
||||
const [detectedType, setDetectedType] = useState<Maybe<TargetType>>(
|
||||
Maybe.nothing()
|
||||
);
|
||||
const [response, setResponse] = useState<Maybe<MetaParsedGeneric>>(
|
||||
Maybe.nothing()
|
||||
);
|
||||
const [detectedType, setDetectedType] = useState<Maybe<TargetType>>(Maybe.nothing());
|
||||
const [response, setResponse] = useState<Maybe<MetaParsedGeneric>>(Maybe.nothing());
|
||||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
@@ -42,21 +38,15 @@ const Index: NextPage = () => {
|
||||
/>
|
||||
</Head>
|
||||
<nav className="bg-zinc-850 px-5 py-4 shadow-xs">
|
||||
<span
|
||||
className="text-xl font-medium text-white"
|
||||
style={{ fontSize: "larger" }}
|
||||
>
|
||||
<span className="text-xl font-medium text-white" style={{ fontSize: "larger" }}>
|
||||
<a href="https://github.com/Xevion/rdap">rdap</a>
|
||||
<a
|
||||
href={"https://xevion.dev"}
|
||||
className="text-zinc-400 hover:animate-pulse"
|
||||
>
|
||||
<a href={"https://xevion.dev"} className="text-zinc-400 hover:animate-pulse">
|
||||
.xevion.dev
|
||||
</a>
|
||||
</span>
|
||||
</nav>
|
||||
<div className="mx-auto max-w-screen-sm px-5 lg:max-w-screen-md xl:max-w-screen-lg">
|
||||
<div className="dark container mx-auto w-full py-6 md:py-12 ">
|
||||
<div className="dark container mx-auto w-full py-6 md:py-12">
|
||||
<LookupInput
|
||||
isLoading={isLoading}
|
||||
detectedType={detectedType}
|
||||
|
||||
@@ -11,8 +11,8 @@ const mockRegistry: Register = {
|
||||
[
|
||||
["test@example.com"], // email
|
||||
["RIPE", "APNIC"], // tags
|
||||
["https://rdap.example.com/"] // urls
|
||||
]
|
||||
["https://rdap.example.com/"], // urls
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
@@ -240,10 +240,7 @@ describe("getType - URL detection", () => {
|
||||
|
||||
describe("getType - JSON detection", () => {
|
||||
it("should detect JSON objects", async () => {
|
||||
const result = await getType(
|
||||
'{"objectClassName":"domain"}',
|
||||
mockGetRegistry
|
||||
);
|
||||
const result = await getType('{"objectClassName":"domain"}', mockGetRegistry);
|
||||
expect(result.isOk).toBe(true);
|
||||
if (result.isOk) {
|
||||
expect(result.value).toBe("json");
|
||||
|
||||
30
src/rdap.ts
30
src/rdap.ts
@@ -774,10 +774,7 @@ type ValidatorResult = boolean | string;
|
||||
* Type validators in priority order (most specific to most generic).
|
||||
* Order matters: url/json/tld are checked before domain to avoid false matches.
|
||||
*/
|
||||
const TypeValidators = new Map<
|
||||
TargetType,
|
||||
(args: ValidatorArgs) => Promise<ValidatorResult>
|
||||
>([
|
||||
const TypeValidators = new Map<TargetType, (args: ValidatorArgs) => Promise<ValidatorResult>>([
|
||||
["url", ({ value }) => Promise.resolve(/^https?:/.test(value))],
|
||||
["json", ({ value }) => Promise.resolve(/^{/.test(value))],
|
||||
["tld", ({ value }) => Promise.resolve(/^\.\w+$/.test(value))],
|
||||
@@ -785,9 +782,7 @@ const TypeValidators = new Map<
|
||||
"ip4",
|
||||
({ value }) => {
|
||||
// Basic format check
|
||||
const match = value.match(
|
||||
/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/\d{1,2})?$/
|
||||
);
|
||||
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
|
||||
@@ -796,7 +791,7 @@ const TypeValidators = new Map<
|
||||
const octet = parseInt(octets[i] ?? "", 10);
|
||||
if (isNaN(octet) || octet < 0 || octet > 255) {
|
||||
return Promise.resolve(
|
||||
`Invalid IPv4 address: octet ${i + 1} (${octets[i] ?? 'undefined'}) must be 0-255`
|
||||
`Invalid IPv4 address: octet ${i + 1} (${octets[i] ?? "undefined"}) must be 0-255`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -805,9 +800,7 @@ const TypeValidators = new Map<
|
||||
if (match[5]) {
|
||||
const prefix = parseInt(match[5].substring(1), 10);
|
||||
if (isNaN(prefix) || prefix < 0 || prefix > 32) {
|
||||
return Promise.resolve(
|
||||
"Invalid IPv4 address: CIDR prefix must be 0-32"
|
||||
);
|
||||
return Promise.resolve("Invalid IPv4 address: CIDR prefix must be 0-32");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -825,9 +818,7 @@ const TypeValidators = new Map<
|
||||
|
||||
// Check for invalid characters
|
||||
if (!/^[0-9a-fA-F:]+$/.test(ipPart)) {
|
||||
return Promise.resolve(
|
||||
"Invalid IPv6 address: contains invalid characters"
|
||||
);
|
||||
return Promise.resolve("Invalid IPv6 address: contains invalid characters");
|
||||
}
|
||||
|
||||
// Validate double :: only appears once
|
||||
@@ -840,9 +831,7 @@ const TypeValidators = new Map<
|
||||
if (match[2]) {
|
||||
const prefix = parseInt(match[2].substring(1), 10);
|
||||
if (isNaN(prefix) || prefix < 0 || prefix > 128) {
|
||||
return Promise.resolve(
|
||||
"Invalid IPv6 address: CIDR prefix must be 0-128"
|
||||
);
|
||||
return Promise.resolve("Invalid IPv6 address: CIDR prefix must be 0-128");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -855,8 +844,7 @@ const TypeValidators = new Map<
|
||||
async ({ value, getRegistry }) => {
|
||||
// Ensure the entity handle is in the correct format
|
||||
const result = value.match(/^\w+-(\w+)$/);
|
||||
if (result === null || result.length <= 1 || result[1] == undefined)
|
||||
return false;
|
||||
if (result === null || result.length <= 1 || result[1] == undefined) return false;
|
||||
|
||||
// Check if the entity object tag is real
|
||||
try {
|
||||
@@ -876,9 +864,7 @@ const TypeValidators = new Map<
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.error(
|
||||
new Error("Failed to fetch entity registry", { cause: e })
|
||||
);
|
||||
console.error(new Error("Failed to fetch entity registry", { cause: e }));
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -12,13 +12,7 @@ export const TargetTypeEnum = z.enum([
|
||||
"json",
|
||||
]);
|
||||
|
||||
export const RootRegistryEnum = z.enum([
|
||||
"autnum",
|
||||
"domain",
|
||||
"ip4",
|
||||
"ip6",
|
||||
"entity",
|
||||
]);
|
||||
export const RootRegistryEnum = z.enum(["autnum", "domain", "ip4", "ip6", "entity"]);
|
||||
|
||||
export const StatusEnum = z.enum([
|
||||
"validated",
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--font-sans: "Inter var", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--color-zinc-850: #1D1D20;
|
||||
--font-sans:
|
||||
"Inter var", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-mono:
|
||||
"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
"Courier New", monospace;
|
||||
--color-zinc-850: #1d1d20;
|
||||
}
|
||||
|
||||
dd {
|
||||
|
||||
Reference in New Issue
Block a user