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({
|
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 [
|
||||||
@@ -22,7 +22,7 @@ export default [
|
|||||||
"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
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
15
package.json
15
package.json
@@ -6,13 +6,18 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:ui": "vitest --ui",
|
"test:ui": "vitest --ui",
|
||||||
"test:run": "vitest run --exclude '**/*.integration.test.ts'",
|
"test:run": "vitest run --exclude '**/*.integration.test.ts'",
|
||||||
"test:integration": "vitest run --include '**/*.integration.test.ts'",
|
"test:integration": "vitest run --include '**/*.integration.test.ts'",
|
||||||
"test:all": "vitest run",
|
"test:all": "vitest run",
|
||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit",
|
||||||
|
"prepare": "husky install",
|
||||||
|
"semantic-release": "semantic-release"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter": "^5.2.8",
|
"@fontsource-variable/inter": "^5.2.8",
|
||||||
@@ -35,6 +40,10 @@
|
|||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
"@tailwindcss/postcss": "^4.1.15",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
@@ -44,12 +53,16 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
||||||
"@typescript-eslint/parser": "^8.46.2",
|
"@typescript-eslint/parser": "^8.46.2",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
|
"conventional-changelog-conventionalcommits": "^8.0.0",
|
||||||
"eslint": "^9.38.0",
|
"eslint": "^9.38.0",
|
||||||
"eslint-config-next": "15.5.6",
|
"eslint-config-next": "15.5.6",
|
||||||
"happy-dom": "^20.0.8",
|
"happy-dom": "^20.0.8",
|
||||||
|
"husky": "^9.0.0",
|
||||||
|
"lint-staged": "^15.0.0",
|
||||||
"postcss": "^8.4.14",
|
"postcss": "^8.4.14",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.1",
|
"prettier-plugin-tailwindcss": "^0.7.1",
|
||||||
|
"semantic-release": "^24.0.0",
|
||||||
"tailwindcss": "^4.1.15",
|
"tailwindcss": "^4.1.15",
|
||||||
"type-fest": "^5.1.0",
|
"type-fest": "^5.1.0",
|
||||||
"typescript": "^5.9.3",
|
"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 = {
|
module.exports = {
|
||||||
plugins: {
|
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(
|
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."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -88,7 +91,7 @@ const AbstractCard: FunctionComponent<AbstractCardProps> = ({
|
|||||||
) : 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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -13,10 +13,7 @@ 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,
|
|
||||||
absoluteFormat,
|
|
||||||
}) => {
|
|
||||||
const { value: showAbsolute, toggle: toggleFormat } = useBoolean(true);
|
const { value: showAbsolute, toggle: toggleFormat } = useBoolean(true);
|
||||||
|
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const ErrorCard: FunctionComponent<ErrorCardProps> = ({
|
|||||||
<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">
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const Property: FunctionComponent<PropertyProps> = ({
|
|||||||
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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -43,10 +43,7 @@ type LookupInputProps = {
|
|||||||
* @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;
|
|
||||||
targetType: TargetType | null;
|
|
||||||
}) => Promise<void>;
|
|
||||||
detectedType: Maybe<TargetType>;
|
detectedType: Maybe<TargetType>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,9 +95,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
|||||||
/**
|
/**
|
||||||
* 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.
|
||||||
@@ -132,10 +127,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
|||||||
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>
|
||||||
</>
|
</>
|
||||||
@@ -144,9 +136,9 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
|||||||
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]}
|
||||||
@@ -183,9 +175,9 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
|||||||
<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. */}
|
||||||
@@ -206,7 +198,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<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]}
|
||||||
@@ -214,10 +206,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
|||||||
)}
|
)}
|
||||||
</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"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
</ListboxButton>
|
</ListboxButton>
|
||||||
<Transition
|
<Transition
|
||||||
@@ -237,7 +226,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
|||||||
key={key}
|
key={key}
|
||||||
className={({ focus }) =>
|
className={({ focus }) =>
|
||||||
cn(
|
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
|
focus ? "bg-zinc-800 text-zinc-300" : null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -247,7 +236,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
|||||||
<>
|
<>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
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
|
selected ? "font-medium" : null
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -281,11 +270,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="pb-3"
|
className="pb-3"
|
||||||
onSubmit={
|
onSubmit={onSubmit != undefined ? onPromise(handleSubmit(onSubmit)) : preventDefault}
|
||||||
onSubmit != undefined
|
|
||||||
? onPromise(handleSubmit(onSubmit))
|
|
||||||
: preventDefault
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<label htmlFor="search" className="sr-only">
|
<label htmlFor="search" className="sr-only">
|
||||||
@@ -298,10 +283,10 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col">
|
<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">
|
<div className="whitespace-nowrap">
|
||||||
<input
|
<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"
|
type="checkbox"
|
||||||
{...register("requestJSContact")}
|
{...register("requestJSContact")}
|
||||||
/>
|
/>
|
||||||
@@ -311,7 +296,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="whitespace-nowrap">
|
<div className="whitespace-nowrap">
|
||||||
<input
|
<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"
|
type="checkbox"
|
||||||
{...register("followReferral")}
|
{...register("followReferral")}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,10 +11,7 @@ export type AutnumCardProps = {
|
|||||||
url?: string;
|
url?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AutnumCard: FunctionComponent<AutnumCardProps> = ({
|
const AutnumCard: FunctionComponent<AutnumCardProps> = ({ data, url }: AutnumCardProps) => {
|
||||||
data,
|
|
||||||
url,
|
|
||||||
}: AutnumCardProps) => {
|
|
||||||
const asnRange =
|
const asnRange =
|
||||||
data.startAutnum === data.endAutnum
|
data.startAutnum === data.endAutnum
|
||||||
? `AS${data.startAutnum}`
|
? `AS${data.startAutnum}`
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ export type DomainProps = {
|
|||||||
url?: string;
|
url?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DomainCard: FunctionComponent<DomainProps> = ({
|
const DomainCard: FunctionComponent<DomainProps> = ({ data, url }: DomainProps) => {
|
||||||
data,
|
|
||||||
url,
|
|
||||||
}: DomainProps) => {
|
|
||||||
return (
|
return (
|
||||||
<AbstractCard
|
<AbstractCard
|
||||||
data={data}
|
data={data}
|
||||||
|
|||||||
@@ -2,31 +2,17 @@ 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,
|
|
||||||
url,
|
|
||||||
}: ObjectProps) => {
|
|
||||||
switch (data.objectClassName) {
|
switch (data.objectClassName) {
|
||||||
case "domain":
|
case "domain":
|
||||||
return <DomainCard url={url} data={data} />;
|
return <DomainCard url={url} data={data} />;
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
// 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:
|
proxy: "The registration of the object instance has been performed by a third party. This is most commonly applied to entities.",
|
||||||
"The registration of the object instance has been performed by a third party. This is most commonly applied to entities.",
|
|
||||||
private:
|
private:
|
||||||
"The information of the object instance is not designated for public consumption. 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.",
|
||||||
removed:
|
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.",
|
"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:
|
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.",
|
"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:
|
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 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'.",
|
inactive: "The object instance is not in use. See 'active'.",
|
||||||
locked:
|
locked: "Changes to the object instance cannot be made, including the association of other object instances.",
|
||||||
"Changes to the object instance cannot be made, including the association of other object instances.",
|
|
||||||
"pending create":
|
"pending create":
|
||||||
"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 creation of the object instance but this action is not yet complete.",
|
||||||
"pending renew":
|
"pending renew":
|
||||||
|
|||||||
@@ -94,9 +94,9 @@ describe("ipv6InCIDR", () => {
|
|||||||
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", () => {
|
||||||
@@ -105,24 +105,15 @@ describe("ipv6InCIDR", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should match IPv6 in /64 network", () => {
|
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(
|
expect(
|
||||||
ipv6InCIDR("2001:db8:1234:5678::", "2001:db8:1234:5678::/64")
|
ipv6InCIDR("2001:db8:1234:5678:ffff:ffff:ffff:ffff", "2001:db8:1234:5678::/64")
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
ipv6InCIDR("2001:db8:1234:5678:abcd::", "2001:db8:1234:5678::/64")
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
ipv6InCIDR(
|
|
||||||
"2001:db8:1234:5678:ffff:ffff:ffff:ffff",
|
|
||||||
"2001:db8:1234:5678::/64"
|
|
||||||
)
|
|
||||||
).toBe(true);
|
).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)", () => {
|
||||||
@@ -154,12 +145,12 @@ describe("ipv6InCIDR", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,7 @@ export function onPromise<T>(promise: (event: SyntheticEvent) => Promise<T>) {
|
|||||||
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) {
|
||||||
@@ -48,8 +46,7 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,9 +53,8 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
|||||||
|
|
||||||
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]);
|
||||||
@@ -67,15 +66,12 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
|||||||
|
|
||||||
// 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 = {
|
||||||
@@ -92,21 +88,14 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
|||||||
return registry;
|
return registry;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getTypeEasy(
|
async function getTypeEasy(target: string): Promise<Result<TargetType, Error>> {
|
||||||
target: string
|
|
||||||
): Promise<Result<TargetType, Error>> {
|
|
||||||
return getType(target, getRegistry);
|
return getType(target, getRegistry);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRegistryURL(
|
function getRegistryURL(type: RootRegistryType, lookupTarget: string): string {
|
||||||
type: RootRegistryType,
|
|
||||||
lookupTarget: string
|
|
||||||
): string {
|
|
||||||
const bootstrap = registryDataRef.current[type];
|
const bootstrap = registryDataRef.current[type];
|
||||||
if (bootstrap == null)
|
if (bootstrap == null)
|
||||||
throw new Error(
|
throw new Error(`Cannot acquire RDAP URL without bootstrap data for ${type} lookup.`);
|
||||||
`Cannot acquire RDAP URL without bootstrap data for ${type} lookup.`
|
|
||||||
);
|
|
||||||
|
|
||||||
let url: string | null = null;
|
let url: string | null = null;
|
||||||
|
|
||||||
@@ -198,8 +187,7 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
|||||||
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}`,
|
||||||
});
|
});
|
||||||
@@ -210,10 +198,7 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
|||||||
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,
|
|
||||||
schema: ZodSchema<T>
|
|
||||||
): Promise<Result<T, Error>> {
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
|
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
@@ -277,9 +262,7 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
|||||||
);
|
);
|
||||||
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}.`
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,9 +271,7 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
|||||||
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);
|
||||||
|
|
||||||
@@ -341,10 +322,7 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
|||||||
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,
|
|
||||||
AutonomousNumberSchema
|
|
||||||
);
|
|
||||||
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 });
|
||||||
}
|
}
|
||||||
@@ -371,13 +349,10 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
|||||||
// 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": {
|
case "json": {
|
||||||
const data = JSON.parse(target);
|
const data = JSON.parse(target);
|
||||||
@@ -389,15 +364,11 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
|||||||
case "registrar": {
|
case "registrar": {
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return Result.err(
|
return Result.err(new Error("The type detected has not been implemented."));
|
||||||
new Error("The type detected has not been implemented.")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submit({
|
async function submit({ target }: SubmitProps): Promise<Maybe<MetaParsedGeneric>> {
|
||||||
target,
|
|
||||||
}: SubmitProps): Promise<Maybe<MetaParsedGeneric>> {
|
|
||||||
try {
|
try {
|
||||||
// target is already set in state, but it's also provided by the form callback, so we'll use it.
|
// 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);
|
const response = await submitInternal(target);
|
||||||
@@ -415,8 +386,7 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
|||||||
})
|
})
|
||||||
: 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();
|
||||||
|
|||||||
@@ -11,12 +11,8 @@ 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 [response, setResponse] = useState<Maybe<MetaParsedGeneric>>(
|
|
||||||
Maybe.nothing()
|
|
||||||
);
|
|
||||||
const [isLoading, setLoading] = useState<boolean>(false);
|
const [isLoading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -42,21 +38,15 @@ const Index: NextPage = () => {
|
|||||||
/>
|
/>
|
||||||
</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"
|
|
||||||
style={{ fontSize: "larger" }}
|
|
||||||
>
|
|
||||||
<a href="https://github.com/Xevion/rdap">rdap</a>
|
<a href="https://github.com/Xevion/rdap">rdap</a>
|
||||||
<a
|
<a href={"https://xevion.dev"} className="text-zinc-400 hover:animate-pulse">
|
||||||
href={"https://xevion.dev"}
|
|
||||||
className="text-zinc-400 hover:animate-pulse"
|
|
||||||
>
|
|
||||||
.xevion.dev
|
.xevion.dev
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</nav>
|
</nav>
|
||||||
<div className="mx-auto max-w-screen-sm px-5 lg:max-w-screen-md xl:max-w-screen-lg">
|
<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
|
<LookupInput
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
detectedType={detectedType}
|
detectedType={detectedType}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ const mockRegistry: Register = {
|
|||||||
[
|
[
|
||||||
["test@example.com"], // email
|
["test@example.com"], // email
|
||||||
["RIPE", "APNIC"], // tags
|
["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", () => {
|
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"}',
|
|
||||||
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");
|
||||||
|
|||||||
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).
|
* 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,
|
|
||||||
(args: ValidatorArgs) => Promise<ValidatorResult>
|
|
||||||
>([
|
|
||||||
["url", ({ value }) => Promise.resolve(/^https?:/.test(value))],
|
["url", ({ value }) => Promise.resolve(/^https?:/.test(value))],
|
||||||
["json", ({ value }) => Promise.resolve(/^{/.test(value))],
|
["json", ({ value }) => Promise.resolve(/^{/.test(value))],
|
||||||
["tld", ({ value }) => Promise.resolve(/^\.\w+$/.test(value))],
|
["tld", ({ value }) => Promise.resolve(/^\.\w+$/.test(value))],
|
||||||
@@ -785,9 +782,7 @@ const TypeValidators = new Map<
|
|||||||
"ip4",
|
"ip4",
|
||||||
({ value }) => {
|
({ value }) => {
|
||||||
// Basic format check
|
// Basic format check
|
||||||
const match = value.match(
|
const match = value.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/\d{1,2})?$/);
|
||||||
/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/\d{1,2})?$/
|
|
||||||
);
|
|
||||||
if (!match) return Promise.resolve(false);
|
if (!match) return Promise.resolve(false);
|
||||||
|
|
||||||
// Validate each octet is 0-255
|
// Validate each octet is 0-255
|
||||||
@@ -796,7 +791,7 @@ const TypeValidators = new Map<
|
|||||||
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`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -805,9 +800,7 @@ const TypeValidators = new Map<
|
|||||||
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"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -825,9 +818,7 @@ const TypeValidators = new Map<
|
|||||||
|
|
||||||
// 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
|
||||||
@@ -840,9 +831,7 @@ const TypeValidators = new Map<
|
|||||||
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"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -855,8 +844,7 @@ const TypeValidators = new Map<
|
|||||||
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 {
|
||||||
@@ -876,9 +864,7 @@ const TypeValidators = new Map<
|
|||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,13 +12,7 @@ export const TargetTypeEnum = z.enum([
|
|||||||
"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",
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
@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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user