@slicedearth/whoisleuth-cli 2.3.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +8 -2
- package/README.md +19 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -408
- package/cli/artifact-validation/case-response.mjs +65 -44
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +15 -9
- package/cli/artifact-validation/signed-review.mjs +136 -68
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +92 -24
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +40 -5
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +1 -1
- package/cli/bulk-command-runner.mjs +4 -2
- package/cli/bulk-output.mjs +29 -3
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +212 -0
- package/cli/case-pack.mjs +50 -32
- package/cli/ci-report.mjs +6 -3
- package/cli/collection-arguments.mjs +123 -0
- package/cli/collection-command-definitions.mjs +330 -0
- package/cli/collection-preflight.mjs +8 -1
- package/cli/command-argument-grammar.mjs +8 -206
- package/cli/command-definition.mjs +329 -0
- package/cli/command-reference.mjs +94 -1357
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/completion.mjs +298 -79
- package/cli/config-profile.mjs +8 -6
- package/cli/ct-event-intake.mjs +16 -16
- package/cli/discover.mjs +5 -4
- package/cli/discovery-observation-snapshot.mjs +46 -18
- package/cli/discovery-scan-command-runner.mjs +5 -3
- package/cli/discovery-scan.mjs +7 -4
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +5 -5
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +151 -103
- package/cli/errors.mjs +37 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +18 -8
- package/cli/evidence-signing.mjs +5 -4
- package/cli/external-observation-mapping.mjs +9 -10
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +24 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/history-arguments.mjs +39 -0
- package/cli/history-command-definitions.mjs +88 -0
- package/cli/indicator-set-command.mjs +33 -0
- package/cli/inline-command-families.mjs +6 -46
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +107 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +8 -10
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +38 -147
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +256 -35
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/local-mmdb-review.mjs +197 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +33 -39
- package/cli/lookup-diff.mjs +47 -12
- package/cli/lookup-plan.mjs +16 -14
- package/cli/lookup-reconcile.mjs +15 -12
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +75 -21
- package/cli/manual.mjs +1 -0
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +45 -31
- package/cli/offline-evidence-review.mjs +39 -6
- package/cli/output-file.mjs +31 -5
- package/cli/posture.mjs +1 -1
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +10 -9
- package/cli/review-arguments.mjs +131 -0
- package/cli/review-command-definitions.mjs +273 -0
- package/cli/review-command-runner.mjs +104 -149
- package/cli/risk-calibration.mjs +8 -8
- package/cli/runner.mjs +74 -37
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +63 -0
- package/cli/sharing-review.mjs +8 -80
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +75 -84
- package/cli/support-arguments.mjs +49 -0
- package/cli/support-command-definitions.mjs +209 -0
- package/cli/support-command-runner.mjs +2 -2
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-arguments.mjs +86 -0
- package/cli/workflow-command-definitions.mjs +78 -0
- package/cli/workflow-command-runner.mjs +75 -13
- package/docs/cli-reference.md +103 -30
- package/docs/cli.md +510 -25
- package/docs/contextual-reviews.md +155 -0
- package/lib/abort.mjs +4 -1
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +27 -754
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +2 -290
- package/lib/browser-library-profile.mjs +99 -130
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +89 -46
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +36 -22
- package/lib/dns-inheritance-review.mjs +207 -0
- package/lib/dns-intelligence.mjs +44 -29
- package/lib/dns-mx.mjs +1 -1
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +18 -14
- package/lib/domain-change-review.mjs +40 -16
- package/lib/domain-control-flight-recorder.mjs +301 -124
- package/lib/domain-control-manifest.mjs +126 -58
- package/lib/domain-posture-analysis.mjs +41 -11
- package/lib/domain-posture-parsers.mjs +88 -6
- package/lib/domain-posture.mjs +120 -32
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +568 -0
- package/lib/evidence-export.mjs +12 -593
- package/lib/evidence-report.mjs +7 -2
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +50 -4
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +2742 -1034
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +87 -85
- package/lib/http-evidence-bounds.mjs +2 -7
- package/lib/http-intelligence.mjs +52 -27
- package/lib/idn-confusable-policy.mjs +6 -4
- package/lib/idn-confusables.mjs +1 -1
- package/lib/interchange-fidelity-registry.mjs +10 -0
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +2 -1
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +79 -13
- package/lib/lookup-response-contract.mjs +1077 -0
- package/lib/lookup-response.mjs +11 -24
- package/lib/lookup-source-progress.mjs +24 -35
- package/lib/lookup.mjs +49 -28
- package/lib/observed-network-context.mjs +43 -27
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +87 -134
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +79 -24
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-client.mjs +26 -14
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +2 -36
- package/lib/registrar-standing-contract.mjs +2 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +7 -3
- package/lib/registry-capability-catalogue.mjs +29 -20
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +56 -18
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +2 -338
- package/lib/risk-scoring.mjs +2 -345
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/security-txt.mjs +10 -7
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +35 -0
- package/lib/smtp-transport-review.mjs +75 -34
- package/lib/sslbl-certificates.generated.mjs +141 -138
- package/lib/static-html-analysis.mjs +117 -146
- package/lib/structured-data-identity.mjs +2 -1
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +2 -36
- package/lib/typosquat-generator.mjs +29 -16
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-enrichment.mjs +388 -0
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +111 -71
- package/lib/whois-authority.mjs +55 -5
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-parser.mjs +5 -3
- package/lib/whois-transport.mjs +23 -7
- package/package.json +14 -14
- package/packages/analysis/analyst-taxonomy.mjs +40 -0
- package/packages/analysis/bounded-json.mjs +330 -0
- package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
- package/packages/analysis/cli-argument-grammar.mjs +216 -0
- package/packages/analysis/ct-query.mjs +27 -0
- package/packages/analysis/ct-response-bounds.mjs +9 -0
- package/packages/analysis/external-intelligence-risk.mjs +124 -0
- package/packages/analysis/http-evidence-bounds.mjs +8 -0
- package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
- package/packages/analysis/opportunity-scoring.mjs +107 -0
- package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
- package/packages/analysis/portable-generator.mjs +29 -0
- package/packages/analysis/project-metadata.mjs +8 -0
- package/packages/analysis/registrable-domain.mjs +56 -0
- package/packages/analysis/risk-calibration-summary.mjs +352 -0
- package/packages/analysis/risk-scoring.mjs +345 -0
- package/packages/analysis/scoring-evidence-quality.mjs +107 -0
- package/packages/analysis/semantic-version.mjs +54 -0
- package/packages/analysis/threat-intelligence-types.mjs +75 -0
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-links.mjs +135 -0
- package/packages/cases/case-evidence-model.mjs +229 -69
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +55 -0
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +98 -48
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +149 -0
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +4 -3
- package/packages/cases/case-record-operations.mjs +123 -133
- package/packages/cases/case-record-projection.mjs +30 -20
- package/packages/cases/case-record-version-input.mjs +19 -0
- package/packages/cases/case-report.mjs +114 -67
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +756 -0
- package/packages/cases/case-response-model.mjs +49 -1129
- package/packages/cases/case-response-outcomes.mjs +338 -0
- package/packages/cases/case-response-packet-render.mjs +157 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +58 -593
- package/packages/cases/case-response-records.mjs +93 -0
- package/packages/cases/case-response-review-inputs.mjs +382 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +26 -1
- package/packages/cases/case-workflow-metadata.mjs +225 -0
- package/packages/cases/case-workflow-migration.mjs +43 -0
- package/packages/cases/http-summary.mjs +6 -1
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +3 -2
- package/packages/comparison/relationship-evidence.mjs +103 -14
- package/packages/comparison/relationship-provenance.mjs +99 -0
- package/packages/contracts/analyst-interchange.mjs +234 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +370 -114
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +3 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/context-review.mjs +24 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +98 -44
- package/packages/contracts/domain-control-manifest.mjs +128 -74
- package/packages/contracts/domain-control-monitor.mjs +59 -45
- package/packages/contracts/domain-control-review.mjs +200 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/har-review.mjs +2 -0
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +18 -0
- package/packages/contracts/ip-address.mjs +44 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +31 -0
- package/packages/contracts/offline-comparison.mjs +4 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/risk-calibration.mjs +0 -1
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +219 -238
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/domain-control-runtime.mjs +155 -27
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +108 -0
- package/packages/evidence/observation.mjs +18 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +93 -61
- package/packages/interchange/managed-indicator-export.mjs +35 -0
- package/packages/interchange/managed-indicator-set.mjs +210 -0
- package/packages/interchange/misp-indicator-export.mjs +93 -0
- package/packages/interchange/sharing-policy.mjs +56 -0
- package/packages/interchange/stix-indicator-export.mjs +129 -0
- package/packages/interchange/web-capture-import.mjs +389 -0
- package/packages/investigation/acquisition-decision-packet.mjs +1 -1
- package/packages/investigation/bulk-mail-exposure.mjs +9 -14
- package/packages/investigation/capture-coverage.mjs +57 -0
- package/packages/investigation/connector-provenance-review.mjs +100 -0
- package/packages/investigation/context-review.mjs +35 -0
- package/packages/investigation/document-intake.mjs +81 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +100 -0
- package/packages/investigation/domain-transition-review.mjs +47 -0
- package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
- package/packages/investigation/har-intake.mjs +68 -0
- package/packages/investigation/identity-event-intake.mjs +131 -0
- package/packages/investigation/identity-incident-review.mjs +39 -0
- package/packages/investigation/incident-sequence-review.mjs +58 -0
- package/packages/investigation/intake-report.mjs +19 -0
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/link-intake.mjs +179 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +161 -141
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/investigation/mail-authentication-review.mjs +222 -0
- package/packages/investigation/mail-header-identity.mjs +209 -0
- package/packages/investigation/message-intake.mjs +195 -0
- package/packages/investigation/page-behaviour.mjs +113 -0
- package/packages/investigation/pdf-intake.mjs +155 -0
- package/packages/investigation/platform-continuity-review.mjs +60 -0
- package/packages/investigation/qr-intake.mjs +56 -0
- package/packages/investigation/requested-action-hints.mjs +11 -0
- package/packages/investigation/selected-input-review.mjs +17 -0
- package/packages/investigation/storefront-review.mjs +70 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +160 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +39 -31
- package/packages/workspace/bulk-session-model.mjs +112 -27
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +1 -1
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +45 -6
- package/packages/workspace/page-baseline.mjs +20 -17
- package/packages/workspace/relationship-observation-model.mjs +54 -35
- package/packages/workspace/shortlist-model.mjs +14 -5
- package/packages/workspace/watchlist-history.mjs +30 -14
- package/packages/workspace/watchlist-store.mjs +24 -6
- package/packages/workspace/website-snapshot-model.mjs +98 -33
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +90 -25
- package/third-party-notices.txt +577 -17
- package/lib/hostname.mjs +0 -38
- package/packages/investigation/bulk-review-export.mjs +0 -63
package/lib/rdap-transport.mjs
CHANGED
|
@@ -2,38 +2,65 @@
|
|
|
2
2
|
// normalization, and registrar enrichment remain separate concerns in
|
|
3
3
|
// lib/rdap.mts.
|
|
4
4
|
import { readTextCapped, safeFetch, safeFetchDetailed } from "./safe-fetch.mjs";
|
|
5
|
+
import { abortable } from "./abort.mjs";
|
|
5
6
|
const MAX_RDAP_BYTES = 2000000;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
finally {
|
|
17
|
-
clearTimeout(timeout);
|
|
7
|
+
export class RdapBodyAdmissionError extends Error {
|
|
8
|
+
status;
|
|
9
|
+
kind;
|
|
10
|
+
constructor(status, kind) {
|
|
11
|
+
super(kind === 'too_large' ? `RDAP response exceeded ${MAX_RDAP_BYTES} bytes.`
|
|
12
|
+
: kind === 'invalid_encoding' ? 'RDAP response contains invalid UTF-8 encoding.'
|
|
13
|
+
: 'RDAP response body could not be read completely.');
|
|
14
|
+
this.name = 'RdapBodyAdmissionError';
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.kind = kind;
|
|
18
17
|
}
|
|
19
18
|
}
|
|
20
|
-
async function
|
|
19
|
+
async function fetchRdapTransport(url, options, timeoutMs, fetchResponse, readText) {
|
|
20
|
+
options.signal?.throwIfAborted();
|
|
21
21
|
const controller = new AbortController();
|
|
22
|
+
const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
22
23
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
24
|
+
let response;
|
|
25
|
+
const cancelBody = () => { void response?.body?.cancel().catch(() => { }); };
|
|
23
26
|
try {
|
|
24
|
-
const result = await (
|
|
25
|
-
|
|
27
|
+
const result = await abortable(async () => {
|
|
28
|
+
const result = await fetchResponse(url, { ...options, signal });
|
|
29
|
+
response = result.response;
|
|
30
|
+
if (signal.aborted) {
|
|
31
|
+
cancelBody();
|
|
32
|
+
signal.throwIfAborted();
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}, signal);
|
|
36
|
+
let body;
|
|
37
|
+
try {
|
|
38
|
+
body = await abortable(() => readText(result.response, MAX_RDAP_BYTES, { fatalUtf8: true }), signal);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
signal.throwIfAborted();
|
|
42
|
+
const invalidEncoding = error instanceof TypeError && 'code' in error
|
|
43
|
+
&& error.code === 'ERR_ENCODING_INVALID_ENCODED_DATA';
|
|
44
|
+
throw new RdapBodyAdmissionError(result.response.status, invalidEncoding ? 'invalid_encoding' : 'unreadable');
|
|
45
|
+
}
|
|
46
|
+
const { text, truncated } = body;
|
|
26
47
|
if (truncated)
|
|
27
|
-
throw new
|
|
48
|
+
throw new RdapBodyAdmissionError(result.response.status, 'too_large');
|
|
28
49
|
return {
|
|
29
|
-
status: result.response.status,
|
|
30
|
-
|
|
31
|
-
text,
|
|
32
|
-
finalUrl: result.finalUrl,
|
|
50
|
+
status: result.response.status, ok: result.response.ok, text,
|
|
51
|
+
...(result.finalUrl === undefined ? {} : { finalUrl: result.finalUrl }),
|
|
33
52
|
};
|
|
34
53
|
}
|
|
35
54
|
finally {
|
|
36
55
|
clearTimeout(timeout);
|
|
56
|
+
if (signal.aborted)
|
|
57
|
+
cancelBody();
|
|
37
58
|
}
|
|
38
59
|
}
|
|
60
|
+
async function fetchRdapWithTimeout(url, options, timeoutMs, dependencies = {}) {
|
|
61
|
+
return fetchRdapTransport(url, options, timeoutMs, async (target, request) => ({ response: await (dependencies.fetch ?? safeFetch)(target, request) }), dependencies.readText ?? readTextCapped);
|
|
62
|
+
}
|
|
63
|
+
async function fetchRdapDetailedWithTimeout(url, options, timeoutMs, dependencies = {}) {
|
|
64
|
+
return fetchRdapTransport(url, options, timeoutMs, dependencies.fetchDetailed ?? safeFetchDetailed, dependencies.readText ?? readTextCapped);
|
|
65
|
+
}
|
|
39
66
|
export { fetchRdapDetailedWithTimeout, fetchRdapWithTimeout, };
|
package/lib/rdap-types.mjs
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const MAX_RDAP_STATUSES = 100;
|
|
2
|
+
export const MAX_RDAP_REDACTIONS = 100;
|
package/lib/rdap.mjs
CHANGED
|
@@ -4,18 +4,33 @@ import { fetchRdapFromBasesWithParser, fetchRdapRecordWithParser, } from "./rdap
|
|
|
4
4
|
import { normalizeRdapEvents, parseRdap, summarizeLifecycle, } from "./rdap-normalization.mjs";
|
|
5
5
|
import { fetchRegistrarRdapRecordWithParser, selectRegistrarRdapLink, } from "./rdap-registrar.mjs";
|
|
6
6
|
import { fetchRdapDetailedWithTimeout, } from "./rdap-transport.mjs";
|
|
7
|
-
import { registryServiceAdmissionFor } from "./registry-capabilities.mjs";
|
|
7
|
+
import { REGISTRY_CAPABILITIES_VERSION, registryServiceAdmissionFor } from "./registry-capabilities.mjs";
|
|
8
|
+
// A null record does not establish domain absence. Keep the HTTP explanation
|
|
9
|
+
// aligned with the same admission decision used before bootstrap discovery.
|
|
10
|
+
function rdapUnavailableResponse(type, value) {
|
|
11
|
+
const admission = type === 'domain' ? registryServiceAdmissionFor(value, 'rdap') : null;
|
|
12
|
+
if (admission?.allowed === false) {
|
|
13
|
+
return {
|
|
14
|
+
error: 'RDAP collection was not attempted because the retained registry capability policy does not admit this service.',
|
|
15
|
+
source: 'retained_registry_capability_policy',
|
|
16
|
+
capabilityVersion: REGISTRY_CAPABILITIES_VERSION,
|
|
17
|
+
limitation: admission.limitation,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return { error: `No RDAP registry found for "${value}" via IANA bootstrap` };
|
|
21
|
+
}
|
|
8
22
|
async function fetchRdapFromBases(type, value, bases, fetchUpstream = fetchRdapDetailedWithTimeout) {
|
|
9
23
|
return fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream);
|
|
10
24
|
}
|
|
11
25
|
async function fetchRdapRecord(type, value, options = {}) {
|
|
26
|
+
options.signal?.throwIfAborted();
|
|
12
27
|
if (type === 'domain' && registryServiceAdmissionFor(value, 'rdap')?.allowed === false) {
|
|
13
28
|
return null;
|
|
14
29
|
}
|
|
15
30
|
const fetchRecord = options.fetchRecord ?? fetchRdapRecordWithParser;
|
|
16
|
-
return fetchRecord(type, value, parseRdap);
|
|
31
|
+
return fetchRecord(type, value, parseRdap, options.signal ? { signal: options.signal } : {});
|
|
17
32
|
}
|
|
18
33
|
async function fetchRegistrarRdapRecord(domain, registryRecord, options = {}) {
|
|
19
34
|
return fetchRegistrarRdapRecordWithParser(domain, registryRecord, parseRdap, options);
|
|
20
35
|
}
|
|
21
|
-
export { BOOTSTRAP_TTL_MS, BOOTSTRAP_STALE_TTL_MS, fetchBootstrap, clearRdapBootstrapCache, fetchRdapRecord, fetchRdapFromBases, fetchRegistrarRdapRecord, selectRegistrarRdapLink, uniqueBases, parseRdap, normalizeRdapEvents, summarizeLifecycle, };
|
|
36
|
+
export { BOOTSTRAP_TTL_MS, BOOTSTRAP_STALE_TTL_MS, fetchBootstrap, clearRdapBootstrapCache, fetchRdapRecord, rdapUnavailableResponse, fetchRdapFromBases, fetchRegistrarRdapRecord, selectRegistrarRdapLink, uniqueBases, parseRdap, normalizeRdapEvents, summarizeLifecycle, };
|
|
@@ -1,36 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
// Returns one canonical registrable-domain identity for an already public DNS
|
|
4
|
-
// hostname. Callers retain the original hostname separately when subdomain
|
|
5
|
-
// context matters; authority, evidence binding and scoring use this value.
|
|
6
|
-
export function canonicalRegistrableDomain(value) {
|
|
7
|
-
if (typeof value !== 'string')
|
|
8
|
-
return null;
|
|
9
|
-
const trimmed = value.trim();
|
|
10
|
-
if (!trimmed || /[\u0000-\u001f\u007f]/u.test(trimmed))
|
|
11
|
-
return null;
|
|
12
|
-
const withoutScheme = trimmed.replace(/^[a-z]+:\/\//iu, '').split(/[/?#]/u)[0] ?? '';
|
|
13
|
-
if (withoutScheme.endsWith('..'))
|
|
14
|
-
return null;
|
|
15
|
-
const candidate = withoutScheme.replace(/\.$/u, '');
|
|
16
|
-
if (!candidate)
|
|
17
|
-
return null;
|
|
18
|
-
let hostname;
|
|
19
|
-
try {
|
|
20
|
-
hostname = new URL(`https://${candidate}/`).hostname.toLowerCase().replace(/\.$/u, '');
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
if (!isValidAsciiDomainName(hostname, { requireDot: true, requireLowercase: true }))
|
|
26
|
-
return null;
|
|
27
|
-
// Keep this boundary identical to classifyQuery() and the collection
|
|
28
|
-
// runtime: registrable identity follows the ICANN Public Suffix List, while
|
|
29
|
-
// privately operated suffixes remain ordinary host context.
|
|
30
|
-
const domain = getDomain(hostname);
|
|
31
|
-
return domain
|
|
32
|
-
&& isValidAsciiDomainName(domain, { requireDot: true, requireLowercase: true })
|
|
33
|
-
&& (hostname === domain || hostname.endsWith(`.${domain}`))
|
|
34
|
-
? domain
|
|
35
|
-
: null;
|
|
36
|
-
}
|
|
1
|
+
// Retained public import; the runtime-neutral implementation lives in the domain layer.
|
|
2
|
+
export * from "../packages/analysis/registrable-domain.mjs";
|
|
@@ -64,7 +64,7 @@ const SOURCE_HEALTH_STATES = new Set(REGISTRAR_STANDING_SOURCE_HEALTH_STATES);
|
|
|
64
64
|
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/u;
|
|
65
65
|
const NOTICE_ID_RE = /^notice-[1-9]\d{0,7}$/u;
|
|
66
66
|
const IANA_ID_RE = /^[1-9]\d{0,7}$/u;
|
|
67
|
-
function normalizedRegistrarIanaId(value) {
|
|
67
|
+
export function normalizedRegistrarIanaId(value) {
|
|
68
68
|
if (typeof value === 'number' && Number.isSafeInteger(value))
|
|
69
69
|
value = String(value);
|
|
70
70
|
if (typeof value !== 'string')
|
|
@@ -101,6 +101,7 @@ function validNullableTimestamp(value) {
|
|
|
101
101
|
function validNullableDate(value) {
|
|
102
102
|
return value === null || (typeof value === 'string'
|
|
103
103
|
&& ISO_DATE_RE.test(value)
|
|
104
|
+
&& Number.isFinite(Date.parse(`${value}T00:00:00.000Z`))
|
|
104
105
|
&& new Date(`${value}T00:00:00.000Z`).toISOString().startsWith(value));
|
|
105
106
|
}
|
|
106
107
|
function validNullableText(value, maximum = MAX_REGISTRAR_STANDING_TEXT_LENGTH) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
import { REGISTRAR_STANDING_CATALOGUE } from "./generated/registrar-standing-catalogue.mjs";
|
|
7
7
|
import { ICANN_COMPLIANCE_SOURCE_URL, IANA_REGISTRAR_SOURCE_URL, REGISTRAR_STANDING_CATALOGUE_SCHEMA, REGISTRAR_STANDING_CATALOGUE_VERSION, REGISTRAR_STANDING_MAX_AGE_DAYS, REGISTRAR_STANDING_MAX_FUTURE_SKEW_MS, registrarStandingOfficialSourceUrl, } from "./registrar-standing-catalogue-contract.mjs";
|
|
8
|
-
import { deriveRegistrarStandingAssessment, MAX_REGISTRAR_COMPLIANCE_ACTIONS, REGISTRAR_STANDING_SCHEMA, REGISTRAR_STANDING_VERSION, } from "./registrar-standing-contract.mjs";
|
|
8
|
+
import { deriveRegistrarStandingAssessment, normalizedRegistrarIanaId, MAX_REGISTRAR_COMPLIANCE_ACTIONS, REGISTRAR_STANDING_SCHEMA, REGISTRAR_STANDING_VERSION, } from "./registrar-standing-contract.mjs";
|
|
9
9
|
const STATUS_BY_CODE = Object.freeze({
|
|
10
10
|
A: 'accredited',
|
|
11
11
|
R: 'reserved',
|
|
@@ -40,6 +40,10 @@ function ageDays(value, now) {
|
|
|
40
40
|
function sourceHealth(value, now) {
|
|
41
41
|
return ageDays(value, now) <= REGISTRAR_STANDING_MAX_AGE_DAYS ? 'current' : 'stale';
|
|
42
42
|
}
|
|
43
|
+
function complianceHealth(catalogue, now) {
|
|
44
|
+
return catalogue.icann.catalogueYear === now.getUTCFullYear()
|
|
45
|
+
? sourceHealth(catalogue.icann.reviewedAt, now) : 'stale';
|
|
46
|
+
}
|
|
43
47
|
function validatedCatalogue(value, now) {
|
|
44
48
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
45
49
|
return null;
|
|
@@ -157,14 +161,6 @@ function accreditationMap(catalogue) {
|
|
|
157
161
|
}
|
|
158
162
|
return result;
|
|
159
163
|
}
|
|
160
|
-
function normalizedIanaId(value) {
|
|
161
|
-
if (typeof value === 'number' && Number.isSafeInteger(value))
|
|
162
|
-
value = String(value);
|
|
163
|
-
if (typeof value !== 'string')
|
|
164
|
-
return null;
|
|
165
|
-
const trimmed = value.trim();
|
|
166
|
-
return /^\d{1,8}$/u.test(trimmed) && Number(trimmed) > 0 ? String(Number(trimmed)) : null;
|
|
167
|
-
}
|
|
168
164
|
function projectedActions(catalogue, ianaId) {
|
|
169
165
|
const matching = catalogue.icann.notices.filter((notice) => String(notice.ianaId) === ianaId);
|
|
170
166
|
const actions = matching.slice(0, MAX_REGISTRAR_COMPLIANCE_ACTIONS).map((notice) => {
|
|
@@ -183,9 +179,9 @@ export function buildRegistrarStanding(options) {
|
|
|
183
179
|
if (!(now instanceof Date) || !Number.isFinite(now.getTime()))
|
|
184
180
|
throw new TypeError('Registrar standing time must be valid.');
|
|
185
181
|
const catalogue = validatedCatalogue(options.catalogue ?? REGISTRAR_STANDING_CATALOGUE, now);
|
|
186
|
-
const ianaId =
|
|
182
|
+
const ianaId = normalizedRegistrarIanaId(options.registrarIanaId);
|
|
187
183
|
const ianaHealth = catalogue ? sourceHealth(catalogue.iana.observedAt, now) : 'unavailable';
|
|
188
|
-
const icannHealth = catalogue ?
|
|
184
|
+
const icannHealth = catalogue ? complianceHealth(catalogue, now) : 'unavailable';
|
|
189
185
|
const accreditation = catalogue && ianaId
|
|
190
186
|
? accreditationMap(catalogue).get(ianaId) ?? 'unknown'
|
|
191
187
|
: 'unknown';
|
|
@@ -205,7 +201,7 @@ export function buildRegistrarStanding(options) {
|
|
|
205
201
|
? `The reviewed ${catalogue.icann.catalogueYear} ICANN index is not a complete enforcement history and excludes third-party allegations.`
|
|
206
202
|
: 'The checked-in official-source catalogue could not be validated, so registrar standing is unavailable.',
|
|
207
203
|
...(!ianaId ? ['No single numeric registrar IANA ID was available across the registration evidence, so no registrar match was attempted.'] : []),
|
|
208
|
-
...(combinedHealth === 'stale' ? ['The catalogue is
|
|
204
|
+
...(combinedHealth === 'stale' ? ['The catalogue is outside its current-year coverage or review-age threshold; current absence and accreditation conclusions require a refresh.'] : []),
|
|
209
205
|
].slice(0, 4);
|
|
210
206
|
const nextActions = hasAction ? [
|
|
211
207
|
'Open the official notice and verify its dates, scope, and current outcome before acting.',
|
|
@@ -259,7 +255,7 @@ export function registrarStandingCatalogueHealth(now = new Date(), value = REGIS
|
|
|
259
255
|
});
|
|
260
256
|
const age = Math.max(ageDays(catalogue.iana.observedAt, now), ageDays(catalogue.icann.reviewedAt, now));
|
|
261
257
|
return Object.freeze({
|
|
262
|
-
state: age <= REGISTRAR_STANDING_MAX_AGE_DAYS ? 'current' : 'stale',
|
|
258
|
+
state: age <= REGISTRAR_STANDING_MAX_AGE_DAYS && complianceHealth(catalogue, now) === 'current' ? 'current' : 'stale',
|
|
263
259
|
sourceObservedAt: [catalogue.iana.observedAt, catalogue.icann.reviewedAt]
|
|
264
260
|
.sort((left, right) => Date.parse(left) - Date.parse(right))[0] ?? null,
|
|
265
261
|
ageDays: age,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Registry capability evaluation over the immutable reviewed catalogue.
|
|
2
|
-
import { CAPABILITY_BY_SUFFIX, DEFAULT_CAPABILITY, EXPLICIT_CAPABILITIES, MAX_CAPABILITY_INPUT_LENGTH, REGISTRY_CAPABILITIES_VERSION, REGISTRY_STANDARDS_COVERAGE_SNAPSHOT,
|
|
2
|
+
import { CAPABILITY_BY_SUFFIX, DEFAULT_CAPABILITY, EXPLICIT_CAPABILITIES, MAX_CAPABILITY_INPUT_LENGTH, REGISTRY_CAPABILITIES_VERSION, REGISTRY_STANDARDS_COVERAGE_SNAPSHOT, REVIEWED_NO_RDAP_SUFFIXES, VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES, } from "./registry-capability-catalogue.mjs";
|
|
3
3
|
const IANA_TLD_RECORD_PREFIX = 'https://www.iana.org/domains/root/db/';
|
|
4
4
|
function documentationUrlsForSuffix(capability, suffixes) {
|
|
5
5
|
if (suffixes.length !== 1 || capability.suffixes.length <= 1) {
|
|
@@ -98,7 +98,11 @@ function registryServiceAdmissionFor(value, service) {
|
|
|
98
98
|
if (!capability || (service !== 'rdap' && service !== 'whois'))
|
|
99
99
|
return null;
|
|
100
100
|
if (service === 'rdap') {
|
|
101
|
-
|
|
101
|
+
// A retained absence is a discovery hint, not an access restriction.
|
|
102
|
+
// Both current profiles permit bounded bootstrap discovery; unknown or
|
|
103
|
+
// future restriction profiles remain denied until explicitly handled.
|
|
104
|
+
const allowed = capability.rdapAccessProfile === 'iana-bootstrap'
|
|
105
|
+
|| capability.rdapAccessProfile === 'no-iana-service';
|
|
102
106
|
return {
|
|
103
107
|
service,
|
|
104
108
|
state: allowed ? 'allowed' : 'unsupported',
|
|
@@ -147,4 +151,4 @@ function registryCompatibilityMatrix() {
|
|
|
147
151
|
})))
|
|
148
152
|
.sort((left, right) => (left.suffixes[0] ?? '').localeCompare(right.suffixes[0] ?? ''));
|
|
149
153
|
}
|
|
150
|
-
export { REGISTRY_CAPABILITIES_VERSION,
|
|
154
|
+
export { REGISTRY_CAPABILITIES_VERSION, REVIEWED_NO_RDAP_SUFFIXES, VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES, registryCapabilityFor, registryCompatibilityMatrix, listRegistryCapabilities, registryAccessDiagnosticFor, registryServiceAdmissionFor, registryStandardsCoverageSnapshot, };
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
// Registry compatibility metadata. IANA bootstrap and referral discovery
|
|
2
2
|
// remain authoritative. Explicit query profiles may alter only the first
|
|
3
3
|
// referred registry query after a fixture-backed adapter is integrated.
|
|
4
|
-
const
|
|
4
|
+
export const REGISTRY_STANDARDS_COVERAGE_SCHEMA = 'whoisleuth.registry-standards-coverage';
|
|
5
|
+
const REGISTRY_CAPABILITIES_VERSION = 31;
|
|
5
6
|
const MAX_CAPABILITY_INPUT_LENGTH = 253;
|
|
6
7
|
const REGISTRY_STANDARDS_COVERAGE_SNAPSHOT = Object.freeze({
|
|
7
|
-
schema:
|
|
8
|
+
schema: REGISTRY_STANDARDS_COVERAGE_SCHEMA,
|
|
8
9
|
version: 1,
|
|
9
|
-
verifiedAt: '2026-
|
|
10
|
+
verifiedAt: '2026-10-03',
|
|
10
11
|
sources: Object.freeze({
|
|
11
|
-
rootZoneVersion: '
|
|
12
|
-
rootZoneLastUpdatedAt: '2026-
|
|
13
|
-
rootZoneTldSetSha256: '
|
|
14
|
-
rdapBootstrapPublication: '2026-
|
|
12
|
+
rootZoneVersion: '2026100200',
|
|
13
|
+
rootZoneLastUpdatedAt: '2026-10-02T07:07:02.000Z',
|
|
14
|
+
rootZoneTldSetSha256: '71093802743b87ee52fac5dd4f662cafd0133dae5089ac6c2e34ee73cebfc836',
|
|
15
|
+
rdapBootstrapPublication: '2026-09-30T23:00:03.000Z',
|
|
15
16
|
rdapBootstrapVersion: '1.0',
|
|
16
17
|
urls: Object.freeze([
|
|
17
18
|
'https://data.iana.org/TLD/tlds-alpha-by-domain.txt',
|
|
@@ -20,15 +21,15 @@ const REGISTRY_STANDARDS_COVERAGE_SNAPSHOT = Object.freeze({
|
|
|
20
21
|
]),
|
|
21
22
|
}),
|
|
22
23
|
counts: Object.freeze({
|
|
23
|
-
activeTlds:
|
|
24
|
+
activeTlds: 1437,
|
|
24
25
|
countryCode: 309,
|
|
25
|
-
nonCountryCode:
|
|
26
|
-
generic:
|
|
26
|
+
nonCountryCode: 1128,
|
|
27
|
+
generic: 1110,
|
|
27
28
|
genericRestricted: 3,
|
|
28
29
|
sponsored: 14,
|
|
29
30
|
infrastructure: 1,
|
|
30
|
-
rdapBootstrapServiceGroups:
|
|
31
|
-
genericAndRestrictedRdapCovered:
|
|
31
|
+
rdapBootstrapServiceGroups: 592,
|
|
32
|
+
genericAndRestrictedRdapCovered: 1113,
|
|
32
33
|
sponsoredRdapCovered: 12,
|
|
33
34
|
infrastructureRdapCovered: 0,
|
|
34
35
|
}),
|
|
@@ -81,7 +82,7 @@ const VERSION_16_NO_IANA_MACHINE_SERVICE_SUFFIXES = Object.freeze([
|
|
|
81
82
|
]);
|
|
82
83
|
const VERSION_19_NO_IANA_MACHINE_SERVICE_SUFFIXES = Object.freeze([
|
|
83
84
|
'xn--mgbai9azgqp6j', 'xn--mgbayh7gpa', 'xn--mgbc0a9azcg',
|
|
84
|
-
'xn--
|
|
85
|
+
'xn--mgbpl2fh', 'xn--wgbh1c',
|
|
85
86
|
]);
|
|
86
87
|
const VERSION_19_RDAP_ONLY_SUFFIXES = Object.freeze(['na', 'pn']);
|
|
87
88
|
const VERSION_20_FIXTURE_SUFFIXES = Object.freeze([
|
|
@@ -116,13 +117,13 @@ const VERSION_24_PROMOTED_SUFFIXES = Object.freeze([
|
|
|
116
117
|
'xn--90ae', 'xn--l1acc', 'xn--wgbl6a',
|
|
117
118
|
]);
|
|
118
119
|
const CURRENT_UNVERIFIED_REFERRAL_SUFFIXES = Object.freeze(VERSION_23_UNVERIFIED_REFERRAL_SUFFIXES.filter((suffix) => !VERSION_24_PROMOTED_SUFFIXES.includes(suffix)));
|
|
119
|
-
const
|
|
120
|
+
const REVIEWED_NO_RDAP_SUFFIXES = Object.freeze([
|
|
120
121
|
'ac', 'ae', 'af', 'am', 'at', 'be', 'bg', 'by', 'cl', 'cn',
|
|
121
122
|
'co', 'de', 'dk', 'ee', 'eu', 'gf', 'gi', 'gt', 'hk', 'hr',
|
|
122
|
-
'hu', 'ie', 'il', 'io', 'ir', 'it', 'jp', '
|
|
123
|
+
'hu', 'ie', 'il', 'io', 'ir', 'it', 'jp', 'kz', 'la',
|
|
123
124
|
'lt', 'lu', 'lv', 'md', 'me', 'mk', 'mo', 'mq', 'mx', 'my',
|
|
124
125
|
'nz', 'pk', 'pt', 'ro', 'rs', 'ru', 'sa', 'se', 'sk', 'su',
|
|
125
|
-
'tn', 'tr', 'us', 'vc', 'xn--2scrj9c',
|
|
126
|
+
'tn', 'tr', 'us', 'vc', 'xn--2scrj9c',
|
|
126
127
|
'xn--3hcrj9c', 'xn--45br5cyl', 'xn--45brj9c', 'xn--80ao21a',
|
|
127
128
|
'xn--90a3ac', 'xn--90ais', 'xn--d1alf', 'xn--e1a4c',
|
|
128
129
|
'xn--fiqs8s', 'xn--fiqz9s', 'xn--fpcrj9c3d', 'xn--gecrj9c',
|
|
@@ -131,7 +132,7 @@ const VERSION_26_NO_RDAP_SUFFIXES = Object.freeze([
|
|
|
131
132
|
'xn--qxa6a', 'xn--rvc1e0am3e', 'xn--s9brj9c',
|
|
132
133
|
'xn--xkc2dl3a5ee0h', 'xn--y9a3aq',
|
|
133
134
|
]);
|
|
134
|
-
const
|
|
135
|
+
const REVIEWED_NO_RDAP_SUFFIX_SET = new Set(REVIEWED_NO_RDAP_SUFFIXES);
|
|
135
136
|
const VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES = Object.freeze([
|
|
136
137
|
'app', 'boo', 'channel', 'dad', 'day', 'dev', 'esq', 'foo', 'how', 'info',
|
|
137
138
|
'ing', 'meme', 'mov', 'new', 'nexus', 'page', 'phd', 'prof', 'rsvp', 'soy',
|
|
@@ -311,6 +312,14 @@ const EXPLICIT_CAPABILITY_SEEDS = [
|
|
|
311
312
|
documentationUrls: [`https://www.iana.org/domains/root/db/${suffix}.html`],
|
|
312
313
|
limitation: RDAP_ONLY_MACHINE_SERVICE_LIMITATION,
|
|
313
314
|
})),
|
|
315
|
+
{
|
|
316
|
+
id: 'no-iana-machine-service-xn--mgbcpq6gpa1a', suffixes: ['xn--mgbcpq6gpa1a'], registryClass: 'country-code',
|
|
317
|
+
whoisParserProfile: 'generic-colon', fixtureScenarios: [],
|
|
318
|
+
coverageState: 'access_documented', whoisAccessProfile: 'no-iana-service',
|
|
319
|
+
rdapAccessProfile: 'iana-bootstrap', verificationFiles: [],
|
|
320
|
+
documentationUrls: ['https://www.iana.org/domains/root/db/xn--mgbcpq6gpa1a.html'],
|
|
321
|
+
limitation: RDAP_ONLY_MACHINE_SERVICE_LIMITATION,
|
|
322
|
+
},
|
|
314
323
|
...VERSION_20_FIXTURE_SUFFIXES.map((suffix) => ({
|
|
315
324
|
id: `iana-cc-colon-${suffix}`,
|
|
316
325
|
suffixes: [suffix],
|
|
@@ -1043,7 +1052,7 @@ const EXPLICIT_CAPABILITY_SEEDS = [
|
|
|
1043
1052
|
{
|
|
1044
1053
|
id: 'nic-bh-icann-colon', suffixes: ['bh'], registryClass: 'country-code',
|
|
1045
1054
|
whoisParserProfile: 'icann-style-colon', fixtureScenarios: ['registered', 'not_found'],
|
|
1046
|
-
rdapAccessProfile: '
|
|
1055
|
+
rdapAccessProfile: 'iana-bootstrap',
|
|
1047
1056
|
documentationUrls: ['https://www.iana.org/domains/root/db/bh.html'],
|
|
1048
1057
|
},
|
|
1049
1058
|
{
|
|
@@ -1180,7 +1189,7 @@ const EXPLICIT_CAPABILITY_SEEDS = [
|
|
|
1180
1189
|
];
|
|
1181
1190
|
function resolvedSeedRdapAccessProfile(entry) {
|
|
1182
1191
|
const declared = entry.rdapAccessProfile || 'iana-bootstrap';
|
|
1183
|
-
const resolved = new Set(entry.suffixes.map((suffix) => (
|
|
1192
|
+
const resolved = new Set(entry.suffixes.map((suffix) => (REVIEWED_NO_RDAP_SUFFIX_SET.has(suffix) ? 'no-iana-service' : declared)));
|
|
1184
1193
|
if (resolved.size !== 1) {
|
|
1185
1194
|
throw new Error(`Registry capability ${entry.id} mixes RDAP access profiles and must be split by suffix.`);
|
|
1186
1195
|
}
|
|
@@ -1214,4 +1223,4 @@ for (const capability of EXPLICIT_CAPABILITIES) {
|
|
|
1214
1223
|
CAPABILITY_BY_SUFFIX.set(suffix, capability);
|
|
1215
1224
|
}
|
|
1216
1225
|
}
|
|
1217
|
-
export { REGISTRY_CAPABILITIES_VERSION, MAX_CAPABILITY_INPUT_LENGTH,
|
|
1226
|
+
export { REGISTRY_CAPABILITIES_VERSION, MAX_CAPABILITY_INPUT_LENGTH, REVIEWED_NO_RDAP_SUFFIXES, VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES, REGISTRY_STANDARDS_COVERAGE_SNAPSHOT, DEFAULT_CAPABILITY, EXPLICIT_CAPABILITIES, CAPABILITY_BY_SUFFIX, };
|
package/lib/registry-dates.mjs
CHANGED
|
@@ -1,190 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// helpers only produce additive Date/ISO companions for comparison, storage,
|
|
4
|
-
// and presentation.
|
|
5
|
-
const REGISTRY_MONTHS = Object.freeze({
|
|
6
|
-
jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6,
|
|
7
|
-
jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12,
|
|
8
|
-
});
|
|
9
|
-
const REGISTRY_MONTH_NAMES = Object.freeze({
|
|
10
|
-
january: 1, february: 2, march: 3, april: 4, may: 5, june: 6,
|
|
11
|
-
july: 7, august: 8, september: 9, october: 10, november: 11, december: 12,
|
|
12
|
-
});
|
|
13
|
-
function utcDateFromParts(year, month, day, hour = 0, minute = 0, second = 0, millisecond = 0) {
|
|
14
|
-
if (![year, month, day, hour, minute, second, millisecond].every(Number.isInteger))
|
|
15
|
-
return null;
|
|
16
|
-
if (year < 1000 || year > 9999 || month < 1 || month > 12 || day < 1
|
|
17
|
-
|| hour < 0 || hour > 23 || minute < 0 || minute > 59
|
|
18
|
-
|| second < 0 || second > 59 || millisecond < 0 || millisecond > 999)
|
|
19
|
-
return null;
|
|
20
|
-
const date = new Date(Date.UTC(year, month - 1, day, hour, minute, second, millisecond));
|
|
21
|
-
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day
|
|
22
|
-
|| date.getUTCHours() !== hour || date.getUTCMinutes() !== minute
|
|
23
|
-
|| date.getUTCSeconds() !== second || date.getUTCMilliseconds() !== millisecond)
|
|
24
|
-
return null;
|
|
25
|
-
return date;
|
|
26
|
-
}
|
|
27
|
-
// Parse the explicit non-ISO formats observed across supported ccTLD WHOIS
|
|
28
|
-
// services before considering ISO-shaped input. Runtime Date parsing treats
|
|
29
|
-
// dotted dates ambiguously and timezone-less timestamps in the host's local
|
|
30
|
-
// timezone, so every accepted shape is validated and normalized through UTC.
|
|
31
|
-
function parseRegistryDate(input) {
|
|
32
|
-
if (typeof input !== 'string')
|
|
33
|
-
return null;
|
|
34
|
-
// The documented .nz WHOIS examples place one space before the RFC3339
|
|
35
|
-
// `T` separator. Canonicalize that registry-specific presentation quirk
|
|
36
|
-
// before applying the existing strict ISO-shaped parser.
|
|
37
|
-
const value = input.trim()
|
|
38
|
-
.replace(/^(\d{4}-\d{2}-\d{2})[ ]+[Tt](?=\d{2}:)/, '$1T')
|
|
39
|
-
// The .ee WHOIS service separates an otherwise ISO-shaped numeric
|
|
40
|
-
// timezone from its clock with one space.
|
|
41
|
-
.replace(/^(\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?)[ ]+([+-]\d{2}:?\d{2})$/, '$1$2');
|
|
42
|
-
if (!value)
|
|
43
|
-
return null;
|
|
44
|
-
// DD.MM.YYYY[ HH:MM:SS] - e.g. 14.03.2024 10:46:48
|
|
45
|
-
let match = value.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2}):(\d{2}))?$/);
|
|
46
|
-
if (match) {
|
|
47
|
-
const [, day = '', month = '', year = '', hour = '0', minute = '0', second = '0'] = match;
|
|
48
|
-
return utcDateFromParts(+year, +month, +day, +(hour || 0), +(minute || 0), +(second || 0));
|
|
49
|
-
}
|
|
50
|
-
// YYYY. MM. DD. - e.g. 2006. 09. 18.
|
|
51
|
-
match = value.match(/^(\d{4})\.\s*(\d{1,2})\.\s*(\d{1,2})\.?$/);
|
|
52
|
-
if (match) {
|
|
53
|
-
const [, year = '', month = '', day = ''] = match;
|
|
54
|
-
return utcDateFromParts(+year, +month, +day);
|
|
55
|
-
}
|
|
56
|
-
// YYYY.MM.DD HH:MM:SS - used by NASK's .pl WHOIS service.
|
|
57
|
-
match = value.match(/^(\d{4})\.(\d{1,2})\.(\d{1,2})\s+(\d{1,2}):(\d{2}):(\d{2})$/);
|
|
58
|
-
if (match) {
|
|
59
|
-
const [, year = '', month = '', day = '', hour = '', minute = '', second = ''] = match;
|
|
60
|
-
return utcDateFromParts(+year, +month, +day, +hour, +minute, +second);
|
|
61
|
-
}
|
|
62
|
-
// YYYY/MM/DD - an unambiguous year-first form used by CIRA WHOIS.
|
|
63
|
-
match = value.match(/^(\d{4})\/(\d{1,2})\/(\d{1,2})$/);
|
|
64
|
-
if (match) {
|
|
65
|
-
const [, year = '', month = '', day = ''] = match;
|
|
66
|
-
return utcDateFromParts(+year, +month, +day);
|
|
67
|
-
}
|
|
68
|
-
// YYYYMMDD with an optional time or Registro.br contact-reference suffix.
|
|
69
|
-
// The raw value remains available as provenance while the validated date
|
|
70
|
-
// and optional clock time are projected into the additive ISO companion.
|
|
71
|
-
match = value.match(/^(\d{4})(\d{2})(\d{2})(?:\s+(\d{1,2}):(\d{2}):(\d{2})|\s+#[0-9]{1,20})?$/);
|
|
72
|
-
if (match) {
|
|
73
|
-
const [, year = '', month = '', day = '', hour = '0', minute = '0', second = '0'] = match;
|
|
74
|
-
return utcDateFromParts(+year, +month, +day, +(hour || 0), +(minute || 0), +(second || 0));
|
|
75
|
-
}
|
|
76
|
-
// YYYY-Mon-DD. - e.g. 1999-Feb-16.
|
|
77
|
-
match = value.match(/^(\d{4})-([A-Za-z]{3})-(\d{1,2})\.?$/);
|
|
78
|
-
if (match) {
|
|
79
|
-
const month = REGISTRY_MONTHS[(match[2] ?? '').toLowerCase()];
|
|
80
|
-
return month ? utcDateFromParts(+(match[1] ?? ''), month, +(match[3] ?? '')) : null;
|
|
81
|
-
}
|
|
82
|
-
// DD-Mon-YYYY[ HH:MM:SS] - used by several ICANN-style ccTLD services.
|
|
83
|
-
match = value.match(/^(\d{1,2})-([A-Za-z]{3})-(\d{4})(?:\s+(\d{1,2}):(\d{2}):(\d{2}))?$/);
|
|
84
|
-
if (match) {
|
|
85
|
-
const month = REGISTRY_MONTHS[(match[2] ?? '').toLowerCase()];
|
|
86
|
-
return month
|
|
87
|
-
? utcDateFromParts(+(match[3] ?? ''), month, +(match[1] ?? ''), +(match[4] || 0), +(match[5] || 0), +(match[6] || 0))
|
|
88
|
-
: null;
|
|
89
|
-
}
|
|
90
|
-
// DD-MM-YYYY - the day-first form published by the currently profiled
|
|
91
|
-
// registries that use bare numeric hyphen dates. A future month-first
|
|
92
|
-
// source must receive its own marker-gated parser rather than reusing this
|
|
93
|
-
// global shape. The four-digit final year remains distinct from ISO below.
|
|
94
|
-
match = value.match(/^(\d{1,2})-(\d{1,2})-(\d{4})$/);
|
|
95
|
-
if (match) {
|
|
96
|
-
const [, day = '', month = '', year = ''] = match;
|
|
97
|
-
return utcDateFromParts(+year, +month, +day);
|
|
98
|
-
}
|
|
99
|
-
// YYYY-MM-DD HH:MM:SS (GMT+H:MM) - the explicit offset form published by
|
|
100
|
-
// the Kazakhstan registry. Parentheses and the GMT marker are required so
|
|
101
|
-
// unrelated prose is not accepted as a lifecycle timestamp.
|
|
102
|
-
match = value.match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})\s+\(GMT([+-])(\d{1,2}):(\d{2})\)$/i);
|
|
103
|
-
if (match) {
|
|
104
|
-
const [, year = '', month = '', day = '', hour = '', minute = '', second = '', sign = '', offsetHours = '', offsetMinutes = '',] = match;
|
|
105
|
-
const local = utcDateFromParts(+year, +month, +day, +hour, +minute, +second);
|
|
106
|
-
if (!local || +offsetHours > 23 || +offsetMinutes > 59)
|
|
107
|
-
return null;
|
|
108
|
-
const offsetMs = (+offsetHours * 60 + +offsetMinutes) * 60000;
|
|
109
|
-
return new Date(local.getTime() + (sign === '+' ? -offsetMs : offsetMs));
|
|
110
|
-
}
|
|
111
|
-
// DD-MM-YYYY HH:MM:SS GMT+H - the Tunisian registry's bounded local-time
|
|
112
|
-
// presentation. Accept an optional minute component but require the GMT
|
|
113
|
-
// marker and validate the calendar and offset before normalization.
|
|
114
|
-
match = value.match(/^(\d{1,2})-(\d{1,2})-(\d{4})\s+(\d{2}):(\d{2}):(\d{2})\s+GMT([+-])(\d{1,2})(?::(\d{2}))?$/i);
|
|
115
|
-
if (match) {
|
|
116
|
-
const [, day = '', month = '', year = '', hour = '', minute = '', second = '', sign = '', offsetHours = '', offsetMinutes = '0',] = match;
|
|
117
|
-
const local = utcDateFromParts(+year, +month, +day, +hour, +minute, +second);
|
|
118
|
-
if (!local || +offsetHours > 23 || +offsetMinutes > 59)
|
|
119
|
-
return null;
|
|
120
|
-
const offsetMs = (+offsetHours * 60 + +offsetMinutes) * 60000;
|
|
121
|
-
return new Date(local.getTime() + (sign === '+' ? -offsetMs : offsetMs));
|
|
122
|
-
}
|
|
123
|
-
// DD Mon YYYY - the compact English month form published by THNIC.
|
|
124
|
-
match = value.match(/^(\d{1,2})\s+([A-Za-z]{3})\s+(\d{4})$/);
|
|
125
|
-
if (match) {
|
|
126
|
-
const month = REGISTRY_MONTHS[(match[2] ?? '').toLowerCase()];
|
|
127
|
-
return month ? utcDateFromParts(+(match[3] ?? ''), month, +(match[1] ?? '')) : null;
|
|
128
|
-
}
|
|
129
|
-
// Ordinal-day Month YYYY[ at HH:MM:SS.sss] - the English form published
|
|
130
|
-
// by the Channel Islands registry. The suffix and `at` token are required
|
|
131
|
-
// so ordinary prose containing a month name is not accepted as lifecycle
|
|
132
|
-
// evidence.
|
|
133
|
-
match = value.match(/^(\d{1,2})(st|nd|rd|th)\s+([A-Za-z]{3,9})\s+(\d{4})(?:\s+at\s+(\d{1,2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?)?$/i);
|
|
134
|
-
if (match) {
|
|
135
|
-
const day = +(match[1] ?? '');
|
|
136
|
-
const suffix = (match[2] ?? '').toLowerCase();
|
|
137
|
-
const expectedSuffix = day % 100 >= 11 && day % 100 <= 13
|
|
138
|
-
? 'th'
|
|
139
|
-
: ({ 1: 'st', 2: 'nd', 3: 'rd' }[day % 10] || 'th');
|
|
140
|
-
if (suffix !== expectedSuffix)
|
|
141
|
-
return null;
|
|
142
|
-
const monthText = (match[3] ?? '').toLowerCase();
|
|
143
|
-
const month = REGISTRY_MONTH_NAMES[monthText] || REGISTRY_MONTHS[monthText];
|
|
144
|
-
const millisecond = match[8] ? Number(`${match[8]}00`.slice(0, 3)) : 0;
|
|
145
|
-
return month
|
|
146
|
-
? utcDateFromParts(+(match[4] ?? ''), month, day, +(match[5] || 0), +(match[6] || 0), +(match[7] || 0), millisecond)
|
|
147
|
-
: null;
|
|
148
|
-
}
|
|
149
|
-
// Ddd Mon DD YYYY[ HH:MM:SS] - the English textual form used by the
|
|
150
|
-
// Belgian registry. Weekday text is presentation-only; the validated
|
|
151
|
-
// calendar components determine the canonical UTC companion.
|
|
152
|
-
match = value.match(/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+([A-Za-z]{3})\s+(\d{1,2})\s+(\d{4})(?:\s+(\d{1,2}):(\d{2}):(\d{2}))?$/i);
|
|
153
|
-
if (match) {
|
|
154
|
-
const month = REGISTRY_MONTHS[(match[1] ?? '').toLowerCase()];
|
|
155
|
-
return month
|
|
156
|
-
? utcDateFromParts(+(match[3] ?? ''), month, +(match[2] ?? ''), +(match[4] || 0), +(match[5] || 0), +(match[6] || 0))
|
|
157
|
-
: null;
|
|
158
|
-
}
|
|
159
|
-
// Month DD YYYY - ISNIC publishes English month names with variable
|
|
160
|
-
// spacing and no timezone. Keep the accepted shape narrow and accept only
|
|
161
|
-
// the twelve exact English names from a fixed table.
|
|
162
|
-
match = value.match(/^([A-Za-z]{3,9})\s+(\d{1,2})\s+(\d{4})$/);
|
|
163
|
-
if (match) {
|
|
164
|
-
const month = REGISTRY_MONTH_NAMES[(match[1] ?? '').toLowerCase()];
|
|
165
|
-
return month ? utcDateFromParts(+(match[3] ?? ''), month, +(match[2] ?? '')) : null;
|
|
166
|
-
}
|
|
167
|
-
// ISO 8601-shaped dates and timestamps. A missing timezone is deliberately
|
|
168
|
-
// interpreted as UTC so Express and function deployments agree.
|
|
169
|
-
match = value.match(/^(\d{4})-(\d{2})-(\d{2})(?:[Tt ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?(?:([Zz])|([+-])(\d{2}):?(\d{2}))?)?$/);
|
|
170
|
-
if (!match)
|
|
171
|
-
return null;
|
|
172
|
-
const [, year = '', month = '', day = '', hour = '0', minute = '0', second = '0', fraction, zulu, offsetSign, offsetHours = '0', offsetMinutes = '0',] = match;
|
|
173
|
-
const millisecond = fraction ? Number(`${fraction}000`.slice(0, 3)) : 0;
|
|
174
|
-
const local = utcDateFromParts(+year, +month, +day, +(hour || 0), +(minute || 0), +(second || 0), millisecond);
|
|
175
|
-
if (!local)
|
|
176
|
-
return null;
|
|
177
|
-
if (!offsetSign || zulu)
|
|
178
|
-
return local;
|
|
179
|
-
const offsetHour = +offsetHours;
|
|
180
|
-
const offsetMinute = +offsetMinutes;
|
|
181
|
-
if (offsetHour > 23 || offsetMinute > 59)
|
|
182
|
-
return null;
|
|
183
|
-
const offsetMs = (offsetHour * 60 + offsetMinute) * 60000;
|
|
184
|
-
return new Date(local.getTime() + (offsetSign === '+' ? -offsetMs : offsetMs));
|
|
185
|
-
}
|
|
186
|
-
function registryDateIso(value) {
|
|
187
|
-
const date = parseRegistryDate(value);
|
|
188
|
-
return date ? date.toISOString() : null;
|
|
189
|
-
}
|
|
190
|
-
export { parseRegistryDate, registryDateIso };
|
|
1
|
+
// Stable source-facing entry point; portable consumers use the evidence owner.
|
|
2
|
+
export { parseRegistryDate, registryDateIso } from "../packages/evidence/registry-dates.mjs";
|