@slicedearth/whoisleuth-cli 1.32.0
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/LICENSE +661 -0
- package/LICENSES/Retire.js-Apache-2.0.txt +199 -0
- package/NOTICE +76 -0
- package/README.md +84 -0
- package/TRADEMARKS.md +41 -0
- package/bin/whoisleuth.mjs +35 -0
- package/cli/archive-inspect.mjs +198 -0
- package/cli/arguments.mjs +1022 -0
- package/cli/artifact-verify.mjs +210 -0
- package/cli/bulk-checkpoint.mjs +218 -0
- package/cli/bulk-command-runner.mjs +109 -0
- package/cli/bulk-output.mjs +112 -0
- package/cli/bulk.mjs +133 -0
- package/cli/compare.mjs +238 -0
- package/cli/completion.mjs +175 -0
- package/cli/csv.mjs +1 -0
- package/cli/discover.mjs +50 -0
- package/cli/discovery-command-runner.mjs +36 -0
- package/cli/discovery-observation-snapshot.mjs +315 -0
- package/cli/discovery-scan-command-runner.mjs +141 -0
- package/cli/discovery-scan.mjs +325 -0
- package/cli/discovery-snapshot.mjs +133 -0
- package/cli/discovery-workflow.mjs +73 -0
- package/cli/doctor.mjs +137 -0
- package/cli/errors.mjs +21 -0
- package/cli/evidence-command-arguments.mjs +145 -0
- package/cli/evidence-command-runner.mjs +106 -0
- package/cli/evidence-signing.mjs +215 -0
- package/cli/exit-codes.mjs +9 -0
- package/cli/export-evidence.mjs +28 -0
- package/cli/formatters/evidence-report.mjs +3 -0
- package/cli/formatters/html.mjs +69 -0
- package/cli/formatters/json.mjs +135 -0
- package/cli/formatters/markdown.mjs +3 -0
- package/cli/formatters/terminal.mjs +658 -0
- package/cli/http.mjs +27 -0
- package/cli/lookup-command-runner.mjs +87 -0
- package/cli/lookup-diff.mjs +171 -0
- package/cli/mail-review.mjs +204 -0
- package/cli/manual.mjs +34 -0
- package/cli/network-command-runner.mjs +88 -0
- package/cli/output-file.mjs +97 -0
- package/cli/page-compare.mjs +123 -0
- package/cli/posture.mjs +20 -0
- package/cli/progress-events.mjs +53 -0
- package/cli/progress.mjs +56 -0
- package/cli/registry-access.mjs +3 -0
- package/cli/registry-support.mjs +120 -0
- package/cli/risk-calibration.mjs +294 -0
- package/cli/runner-types.mjs +1 -0
- package/cli/runner.mjs +726 -0
- package/cli/saved-lookup.mjs +87 -0
- package/cli/source-reliability.mjs +727 -0
- package/cli/strict-exit.mjs +55 -0
- package/cli/terminal-presentation.mjs +147 -0
- package/docs/cli.md +827 -0
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +93 -0
- package/frontend/src/lib/analysis/acquisition-due-diligence.js +391 -0
- package/frontend/src/lib/analysis/artifact-integrity.js +21 -0
- package/frontend/src/lib/analysis/brand-profile-model.js +421 -0
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +246 -0
- package/frontend/src/lib/analysis/bulk-mail-exposure.js +192 -0
- package/frontend/src/lib/analysis/bulk-retry-plan.js +86 -0
- package/frontend/src/lib/analysis/bulk-review-export.js +62 -0
- package/frontend/src/lib/analysis/bulk-review-model.js +266 -0
- package/frontend/src/lib/analysis/bulk-session-model.js +515 -0
- package/frontend/src/lib/analysis/bulk-sort.js +49 -0
- package/frontend/src/lib/analysis/campaign-model.js +265 -0
- package/frontend/src/lib/analysis/case-evidence-model.js +642 -0
- package/frontend/src/lib/analysis/case-migration-model.js +248 -0
- package/frontend/src/lib/analysis/case-model.js +5 -0
- package/frontend/src/lib/analysis/case-record-contracts.js +44 -0
- package/frontend/src/lib/analysis/case-record-core.js +232 -0
- package/frontend/src/lib/analysis/case-record-model.js +7 -0
- package/frontend/src/lib/analysis/case-record-operations.js +224 -0
- package/frontend/src/lib/analysis/case-response-model.js +640 -0
- package/frontend/src/lib/analysis/case-response-packet.js +573 -0
- package/frontend/src/lib/analysis/case-storage-model.js +93 -0
- package/frontend/src/lib/analysis/detection-rule-model.js +306 -0
- package/frontend/src/lib/analysis/dns-record-normalization.js +11 -0
- package/frontend/src/lib/analysis/http-summary.js +165 -0
- package/frontend/src/lib/analysis/investigation-guide.js +500 -0
- package/frontend/src/lib/analysis/investigation-template-model.js +144 -0
- package/frontend/src/lib/analysis/page-baseline.js +272 -0
- package/frontend/src/lib/analysis/page-similarity.js +179 -0
- package/frontend/src/lib/analysis/relationship-evidence.js +265 -0
- package/frontend/src/lib/analysis/relationship-observation-model.js +423 -0
- package/frontend/src/lib/analysis/scoring.js +135 -0
- package/frontend/src/lib/analysis/shortlist-model.js +209 -0
- package/frontend/src/lib/analysis/utils.js +265 -0
- package/frontend/src/lib/analysis/watchlist-history.js +643 -0
- package/frontend/src/lib/analysis/watchlist-store.js +127 -0
- package/frontend/src/lib/analysis/website-snapshot-model.js +302 -0
- package/frontend/src/lib/analysis/workspace-archive-crypto.js +282 -0
- package/frontend/src/lib/analysis/workspace-archive.js +472 -0
- package/lib/abort.mjs +26 -0
- package/lib/abusech-auth.mjs +19 -0
- package/lib/availability.mjs +716 -0
- package/lib/bounded-contract-normalizers.mjs +61 -0
- package/lib/browser-library-profile.mjs +310 -0
- package/lib/bulk-comparison-evidence.mjs +83 -0
- package/lib/classify.mjs +93 -0
- package/lib/client-behavior-profile.mjs +83 -0
- package/lib/credential-surface-profile.mjs +66 -0
- package/lib/csv.mjs +23 -0
- package/lib/ct-query.mjs +27 -0
- package/lib/ct-search.mjs +393 -0
- package/lib/distributed-operation-budget.mjs +461 -0
- package/lib/dns-delegation-health.mjs +344 -0
- package/lib/dns-intelligence.mjs +739 -0
- package/lib/dns-mx.mjs +24 -0
- package/lib/dns-resolver-selection.mjs +33 -0
- package/lib/domain-posture-analysis.mjs +254 -0
- package/lib/domain-posture-parsers.mjs +358 -0
- package/lib/domain-posture.mjs +745 -0
- package/lib/error-detail.mjs +10 -0
- package/lib/evidence-export.mjs +431 -0
- package/lib/evidence-report-markdown.mjs +71 -0
- package/lib/evidence-report.mjs +391 -0
- package/lib/external-intelligence-risk.mjs +108 -0
- package/lib/favicon.mjs +146 -0
- package/lib/feature-policy.mjs +70 -0
- package/lib/generated/retire-browser-catalog.mjs +8724 -0
- package/lib/generated/unicode-confusables-17.mjs +93 -0
- package/lib/hostname.mjs +38 -0
- package/lib/html-signals.mjs +680 -0
- package/lib/http-intelligence.mjs +232 -0
- package/lib/idn-confusable-policy.mjs +86 -0
- package/lib/idn-confusables.mjs +465 -0
- package/lib/lookup-cache.mjs +130 -0
- package/lib/lookup-progress.mjs +291 -0
- package/lib/lookup-source-progress.mjs +109 -0
- package/lib/lookup-threat-provider-inventory.mjs +13 -0
- package/lib/lookup.mjs +592 -0
- package/lib/observation.mjs +114 -0
- package/lib/observed-network-context.mjs +355 -0
- package/lib/operation-budget.mjs +489 -0
- package/lib/page-fingerprints.mjs +481 -0
- package/lib/page-role-profile.mjs +117 -0
- package/lib/perceptual-hash.mjs +451 -0
- package/lib/rdap-attempts.mjs +34 -0
- package/lib/rdap-bootstrap.mjs +173 -0
- package/lib/rdap-capabilities.mjs +292 -0
- package/lib/rdap-client.mjs +125 -0
- package/lib/rdap-normalization.mjs +691 -0
- package/lib/rdap-registrar.mjs +196 -0
- package/lib/rdap-transport.mjs +39 -0
- package/lib/rdap-types.mjs +1 -0
- package/lib/rdap-validation.mjs +94 -0
- package/lib/rdap.mjs +16 -0
- package/lib/registry-access.mjs +32 -0
- package/lib/registry-capabilities.mjs +117 -0
- package/lib/registry-capability-catalogue.mjs +1206 -0
- package/lib/registry-comparison.mjs +318 -0
- package/lib/registry-dates.mjs +190 -0
- package/lib/registry-insights.mjs +325 -0
- package/lib/response-policy.mjs +256 -0
- package/lib/risk-scoring.mjs +164 -0
- package/lib/safe-fetch.mjs +446 -0
- package/lib/security-txt.mjs +359 -0
- package/lib/service-binding-dns.mjs +608 -0
- package/lib/sslbl-certificates.generated.mjs +143 -0
- package/lib/sslbl-intelligence.mjs +241 -0
- package/lib/static-html-analysis.mjs +340 -0
- package/lib/structured-data-identity.mjs +323 -0
- package/lib/threat-intelligence-contract.mjs +5 -0
- package/lib/threat-intelligence-definition-registry.mjs +34 -0
- package/lib/threat-intelligence-fixtures.mjs +82 -0
- package/lib/threat-intelligence-manifests.mjs +512 -0
- package/lib/threat-intelligence-runtime.mjs +662 -0
- package/lib/threat-intelligence-targets.mjs +168 -0
- package/lib/threat-intelligence-types.mjs +3 -0
- package/lib/threatfox-intelligence.mjs +307 -0
- package/lib/tls-intelligence.mjs +814 -0
- package/lib/typosquat-generator.mjs +884 -0
- package/lib/urlhaus-intelligence.mjs +361 -0
- package/lib/urlscan-intelligence.mjs +349 -0
- package/lib/website-security-posture.mjs +325 -0
- package/lib/website-technology.mjs +437 -0
- package/lib/whois-authority.mjs +98 -0
- package/lib/whois-chain.mjs +261 -0
- package/lib/whois-contacts.mjs +163 -0
- package/lib/whois-contracts.mjs +3 -0
- package/lib/whois-normalization.mjs +27 -0
- package/lib/whois-transport.mjs +100 -0
- package/lib/whois-values.mjs +92 -0
- package/lib/whois.mjs +1187 -0
- package/package.json +55 -0
- package/third-party-notices.txt +5571 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
// Immediate concurrency leases for network-heavy API operations. In-memory is
|
|
2
|
+
// the zero-configuration default; an optional shared provider can enforce the
|
|
3
|
+
// same limits across runtime instances.
|
|
4
|
+
//
|
|
5
|
+
// A long-lived Express process shares the zero-configuration instance; a
|
|
6
|
+
// serverless runtime only shares it within one warm function instance and loses
|
|
7
|
+
// the state on a cold start. The optional distributed provider can add durable
|
|
8
|
+
// fixed-window accounting. The provider-neutral runner deliberately treats
|
|
9
|
+
// acquisition as one atomic operation rather than exposing a race-prone
|
|
10
|
+
// check-then-consume sequence. Providers may acquire and release either
|
|
11
|
+
// synchronously or asynchronously without changing endpoint orchestration.
|
|
12
|
+
import { createDistributedOperationBudget as createDistributedOperationBudgetImplementation } from "./distributed-operation-budget.mjs";
|
|
13
|
+
const OPERATION_CLASSES = Object.freeze({
|
|
14
|
+
REGISTRY_LIGHT: 'registry_light',
|
|
15
|
+
REGISTRY_DEEP: 'registry_deep',
|
|
16
|
+
CERTIFICATE_SEARCH: 'certificate_search',
|
|
17
|
+
POSTURE_AUDIT: 'posture_audit',
|
|
18
|
+
});
|
|
19
|
+
// Stable server-derived feature identities for future durable accounting.
|
|
20
|
+
// These describe the public operation being admitted, while OPERATION_CLASSES
|
|
21
|
+
// continue to describe its immediate concurrency cost. Compact Lookup is the
|
|
22
|
+
// established Bulk response contract, so it can be attributed without trusting
|
|
23
|
+
// a caller-supplied UI/workflow label. Global limits must still protect against
|
|
24
|
+
// clients deliberately selecting a different compatible response shape.
|
|
25
|
+
const OPERATION_FEATURE_MODEL_VERSION = 1;
|
|
26
|
+
const OPERATION_FEATURES = Object.freeze({
|
|
27
|
+
LOOKUP_FAST: 'lookup_fast',
|
|
28
|
+
LOOKUP_DEEP: 'lookup_deep',
|
|
29
|
+
BULK_FAST: 'bulk_fast',
|
|
30
|
+
BULK_DEEP: 'bulk_deep',
|
|
31
|
+
RDAP: 'rdap',
|
|
32
|
+
RDAP_NAMESERVER_SEARCH: 'rdap_nameserver_search',
|
|
33
|
+
WHOIS: 'whois',
|
|
34
|
+
AVAILABILITY_FAST: 'availability_fast',
|
|
35
|
+
AVAILABILITY_DEEP: 'availability_deep',
|
|
36
|
+
CERTIFICATE_TRANSPARENCY: 'certificate_transparency',
|
|
37
|
+
DOMAIN_POSTURE: 'domain_posture',
|
|
38
|
+
});
|
|
39
|
+
const OPERATION_FEATURE_CLASSES = Object.freeze({
|
|
40
|
+
[OPERATION_FEATURES.LOOKUP_FAST]: OPERATION_CLASSES.REGISTRY_LIGHT,
|
|
41
|
+
[OPERATION_FEATURES.LOOKUP_DEEP]: OPERATION_CLASSES.REGISTRY_DEEP,
|
|
42
|
+
[OPERATION_FEATURES.BULK_FAST]: OPERATION_CLASSES.REGISTRY_LIGHT,
|
|
43
|
+
[OPERATION_FEATURES.BULK_DEEP]: OPERATION_CLASSES.REGISTRY_DEEP,
|
|
44
|
+
[OPERATION_FEATURES.RDAP]: OPERATION_CLASSES.REGISTRY_LIGHT,
|
|
45
|
+
[OPERATION_FEATURES.RDAP_NAMESERVER_SEARCH]: OPERATION_CLASSES.REGISTRY_LIGHT,
|
|
46
|
+
[OPERATION_FEATURES.WHOIS]: OPERATION_CLASSES.REGISTRY_DEEP,
|
|
47
|
+
[OPERATION_FEATURES.AVAILABILITY_FAST]: OPERATION_CLASSES.REGISTRY_LIGHT,
|
|
48
|
+
[OPERATION_FEATURES.AVAILABILITY_DEEP]: OPERATION_CLASSES.REGISTRY_DEEP,
|
|
49
|
+
[OPERATION_FEATURES.CERTIFICATE_TRANSPARENCY]: OPERATION_CLASSES.CERTIFICATE_SEARCH,
|
|
50
|
+
[OPERATION_FEATURES.DOMAIN_POSTURE]: OPERATION_CLASSES.POSTURE_AUDIT,
|
|
51
|
+
});
|
|
52
|
+
// The browser currently runs at most 12 fast Bulk workers, 4 deep Bulk
|
|
53
|
+
// workers, and 3 Brand Profile posture workers. Session ceilings match those
|
|
54
|
+
// established callers; runtime ceilings leave bounded headroom for a small
|
|
55
|
+
// number of simultaneous authenticated sessions without allowing unbounded
|
|
56
|
+
// multiplication across tabs or scripts.
|
|
57
|
+
const DEFAULT_OPERATION_LIMITS = Object.freeze({
|
|
58
|
+
[OPERATION_CLASSES.REGISTRY_LIGHT]: Object.freeze({ session: 12, runtime: 36 }),
|
|
59
|
+
[OPERATION_CLASSES.REGISTRY_DEEP]: Object.freeze({ session: 4, runtime: 12 }),
|
|
60
|
+
[OPERATION_CLASSES.CERTIFICATE_SEARCH]: Object.freeze({ session: 2, runtime: 4 }),
|
|
61
|
+
[OPERATION_CLASSES.POSTURE_AUDIT]: Object.freeze({ session: 3, runtime: 8 }),
|
|
62
|
+
});
|
|
63
|
+
const OPERATION_BUDGET_ERROR_CODE = 'NETWORK_CONCURRENCY_LIMITED';
|
|
64
|
+
const OPERATION_BUDGET_UNAVAILABLE_ERROR_CODE = 'NETWORK_BUDGET_UNAVAILABLE';
|
|
65
|
+
const OPERATION_USAGE_ERROR_CODE = 'NETWORK_USAGE_LIMITED';
|
|
66
|
+
const OPERATION_USAGE_MODEL_VERSION = 1;
|
|
67
|
+
const MAX_OPERATION_USAGE_POLICY_BYTES = 8 * 1024;
|
|
68
|
+
const MAX_OPERATION_USAGE_LIMIT = 1_000_000_000;
|
|
69
|
+
const OPERATION_RETRY_AFTER_SECONDS = 1;
|
|
70
|
+
const OPERATION_PROVIDER_RETRY_AFTER_SECONDS = 5;
|
|
71
|
+
function operationFeatureFor(feature, { fast = false, compact = false } = {}) {
|
|
72
|
+
const fastMode = fast === true;
|
|
73
|
+
const compactMode = compact === true;
|
|
74
|
+
if (feature === 'lookup') {
|
|
75
|
+
if (compactMode)
|
|
76
|
+
return fastMode ? OPERATION_FEATURES.BULK_FAST : OPERATION_FEATURES.BULK_DEEP;
|
|
77
|
+
return fastMode ? OPERATION_FEATURES.LOOKUP_FAST : OPERATION_FEATURES.LOOKUP_DEEP;
|
|
78
|
+
}
|
|
79
|
+
if (feature === 'availability') {
|
|
80
|
+
return fastMode ? OPERATION_FEATURES.AVAILABILITY_FAST : OPERATION_FEATURES.AVAILABILITY_DEEP;
|
|
81
|
+
}
|
|
82
|
+
if (feature === 'rdap')
|
|
83
|
+
return OPERATION_FEATURES.RDAP;
|
|
84
|
+
if (feature === 'rdap_nameserver_search')
|
|
85
|
+
return OPERATION_FEATURES.RDAP_NAMESERVER_SEARCH;
|
|
86
|
+
if (feature === 'whois')
|
|
87
|
+
return OPERATION_FEATURES.WHOIS;
|
|
88
|
+
if (feature === 'certificate_transparency')
|
|
89
|
+
return OPERATION_FEATURES.CERTIFICATE_TRANSPARENCY;
|
|
90
|
+
if (feature === 'domain_posture')
|
|
91
|
+
return OPERATION_FEATURES.DOMAIN_POSTURE;
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
function operationClassFor(feature, options = {}) {
|
|
95
|
+
const operationFeature = operationFeatureFor(feature, options);
|
|
96
|
+
return operationFeature ? OPERATION_FEATURE_CLASSES[operationFeature] : null;
|
|
97
|
+
}
|
|
98
|
+
function operationBudgetTargetFor(feature, options = {}) {
|
|
99
|
+
const operationFeature = operationFeatureFor(feature, options);
|
|
100
|
+
if (!operationFeature)
|
|
101
|
+
return null;
|
|
102
|
+
return Object.freeze({
|
|
103
|
+
operationFeature,
|
|
104
|
+
operationClass: OPERATION_FEATURE_CLASSES[operationFeature],
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function normalizeOperationBudgetTarget(target) {
|
|
108
|
+
if (typeof target === 'string' && /^[a-z0-9_]{1,50}$/.test(target)) {
|
|
109
|
+
return { operationClass: target, operationFeature: null };
|
|
110
|
+
}
|
|
111
|
+
if (!target || typeof target !== 'object' || Array.isArray(target)) {
|
|
112
|
+
throw new TypeError('A valid operation budget target is required');
|
|
113
|
+
}
|
|
114
|
+
const record = target;
|
|
115
|
+
const operationClass = record.operationClass;
|
|
116
|
+
const operationFeature = record.operationFeature;
|
|
117
|
+
if (typeof operationFeature !== 'string'
|
|
118
|
+
|| !Object.prototype.hasOwnProperty.call(OPERATION_FEATURE_CLASSES, operationFeature)
|
|
119
|
+
|| OPERATION_FEATURE_CLASSES[operationFeature] !== operationClass) {
|
|
120
|
+
throw new TypeError('Operation feature and concurrency class do not match');
|
|
121
|
+
}
|
|
122
|
+
return { operationClass: operationClass, operationFeature: operationFeature };
|
|
123
|
+
}
|
|
124
|
+
function normalizeOperationLimits(limits = DEFAULT_OPERATION_LIMITS) {
|
|
125
|
+
if (!limits || typeof limits !== 'object' || Array.isArray(limits)) {
|
|
126
|
+
throw new TypeError('Operation limits must be an object');
|
|
127
|
+
}
|
|
128
|
+
const entries = Object.entries(limits);
|
|
129
|
+
if (!entries.length)
|
|
130
|
+
throw new TypeError('At least one operation limit is required');
|
|
131
|
+
const normalized = {};
|
|
132
|
+
for (const [id, configured] of entries) {
|
|
133
|
+
const limit = configured;
|
|
134
|
+
if (!/^[a-z0-9_]{1,50}$/.test(id)
|
|
135
|
+
|| !limit
|
|
136
|
+
|| !Number.isSafeInteger(limit.session)
|
|
137
|
+
|| !Number.isSafeInteger(limit.runtime)
|
|
138
|
+
|| limit.session < 1
|
|
139
|
+
|| limit.runtime < limit.session
|
|
140
|
+
|| limit.runtime > 1000) {
|
|
141
|
+
throw new TypeError(`Invalid operation limits for ${id}`);
|
|
142
|
+
}
|
|
143
|
+
normalized[id] = Object.freeze({ session: limit.session, runtime: limit.runtime });
|
|
144
|
+
}
|
|
145
|
+
return Object.freeze(normalized);
|
|
146
|
+
}
|
|
147
|
+
function normalizeUsageCeiling(value, label) {
|
|
148
|
+
if (!Number.isSafeInteger(value)
|
|
149
|
+
|| value < 1
|
|
150
|
+
|| value > MAX_OPERATION_USAGE_LIMIT) {
|
|
151
|
+
throw new TypeError(`${label} must be a positive bounded integer`);
|
|
152
|
+
}
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
function normalizeOperationUsageLimits(policy) {
|
|
156
|
+
let value = policy;
|
|
157
|
+
if (typeof policy === 'string') {
|
|
158
|
+
if (!policy || Buffer.byteLength(policy, 'utf8') > MAX_OPERATION_USAGE_POLICY_BYTES) {
|
|
159
|
+
throw new TypeError('Operation usage policy must be bounded JSON');
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
value = JSON.parse(policy);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
throw new TypeError('Operation usage policy must be valid JSON');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
169
|
+
throw new TypeError('Operation usage policy must be an object');
|
|
170
|
+
}
|
|
171
|
+
const record = value;
|
|
172
|
+
const allowedKeys = new Set(['daily', 'monthly', 'features']);
|
|
173
|
+
if (Object.keys(record).some((key) => !allowedKeys.has(key))) {
|
|
174
|
+
throw new TypeError('Operation usage policy contains unknown fields');
|
|
175
|
+
}
|
|
176
|
+
const daily = normalizeUsageCeiling(record.daily, 'Daily operation limit');
|
|
177
|
+
const monthly = normalizeUsageCeiling(record.monthly, '30-day operation limit');
|
|
178
|
+
if (monthly < daily)
|
|
179
|
+
throw new TypeError('30-day operation limit must not be lower than the daily limit');
|
|
180
|
+
const rawFeatures = record.features === undefined ? {} : record.features;
|
|
181
|
+
if (!rawFeatures || typeof rawFeatures !== 'object' || Array.isArray(rawFeatures)) {
|
|
182
|
+
throw new TypeError('Feature operation limits must be an object');
|
|
183
|
+
}
|
|
184
|
+
const featureEntries = Object.entries(rawFeatures)
|
|
185
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
186
|
+
if (featureEntries.length > Object.keys(OPERATION_FEATURE_CLASSES).length) {
|
|
187
|
+
throw new TypeError('Too many feature operation limits were configured');
|
|
188
|
+
}
|
|
189
|
+
const features = {};
|
|
190
|
+
for (const [id, configured] of featureEntries) {
|
|
191
|
+
if (!Object.prototype.hasOwnProperty.call(OPERATION_FEATURE_CLASSES, id)
|
|
192
|
+
|| !configured
|
|
193
|
+
|| typeof configured !== 'object'
|
|
194
|
+
|| Array.isArray(configured)
|
|
195
|
+
|| Object.keys(configured).some((key) => !['daily', 'monthly'].includes(key))) {
|
|
196
|
+
throw new TypeError(`Invalid feature operation limits for ${id}`);
|
|
197
|
+
}
|
|
198
|
+
const featureLimit = configured;
|
|
199
|
+
const featureDaily = normalizeUsageCeiling(featureLimit.daily, `Daily ${id} limit`);
|
|
200
|
+
const featureMonthly = normalizeUsageCeiling(featureLimit.monthly, `30-day ${id} limit`);
|
|
201
|
+
if (featureMonthly < featureDaily
|
|
202
|
+
|| featureDaily > daily
|
|
203
|
+
|| featureMonthly > monthly) {
|
|
204
|
+
throw new TypeError(`Feature operation limits for ${id} must fit within global limits`);
|
|
205
|
+
}
|
|
206
|
+
features[id] = Object.freeze({ daily: featureDaily, monthly: featureMonthly });
|
|
207
|
+
}
|
|
208
|
+
return Object.freeze({
|
|
209
|
+
modelVersion: OPERATION_USAGE_MODEL_VERSION,
|
|
210
|
+
daily,
|
|
211
|
+
monthly,
|
|
212
|
+
features: Object.freeze(features),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function createOperationBudget(limits = DEFAULT_OPERATION_LIMITS) {
|
|
216
|
+
const configuredLimits = normalizeOperationLimits(limits);
|
|
217
|
+
const runtimeCounts = new Map();
|
|
218
|
+
const sessionCounts = new Map();
|
|
219
|
+
function acquire(operationClass, sessionKey) {
|
|
220
|
+
const configured = configuredLimits[operationClass];
|
|
221
|
+
if (!configured)
|
|
222
|
+
throw new Error(`Unknown operation class: ${operationClass}`);
|
|
223
|
+
if (typeof sessionKey !== 'string' || !sessionKey)
|
|
224
|
+
throw new Error('A bounded session key is required');
|
|
225
|
+
const sessionCountKey = `${operationClass}:${sessionKey}`;
|
|
226
|
+
const activeForSession = sessionCounts.get(sessionCountKey) || 0;
|
|
227
|
+
if (activeForSession >= configured.session) {
|
|
228
|
+
return {
|
|
229
|
+
allowed: false,
|
|
230
|
+
operationClass,
|
|
231
|
+
scope: 'session',
|
|
232
|
+
retryAfterSeconds: OPERATION_RETRY_AFTER_SECONDS,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const activeForRuntime = runtimeCounts.get(operationClass) || 0;
|
|
236
|
+
if (activeForRuntime >= configured.runtime) {
|
|
237
|
+
return {
|
|
238
|
+
allowed: false,
|
|
239
|
+
operationClass,
|
|
240
|
+
scope: 'runtime',
|
|
241
|
+
retryAfterSeconds: OPERATION_RETRY_AFTER_SECONDS,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
sessionCounts.set(sessionCountKey, activeForSession + 1);
|
|
245
|
+
runtimeCounts.set(operationClass, activeForRuntime + 1);
|
|
246
|
+
let released = false;
|
|
247
|
+
return {
|
|
248
|
+
allowed: true,
|
|
249
|
+
operationClass,
|
|
250
|
+
release() {
|
|
251
|
+
if (released)
|
|
252
|
+
return;
|
|
253
|
+
released = true;
|
|
254
|
+
const nextSession = (sessionCounts.get(sessionCountKey) || 1) - 1;
|
|
255
|
+
const nextRuntime = (runtimeCounts.get(operationClass) || 1) - 1;
|
|
256
|
+
if (nextSession > 0)
|
|
257
|
+
sessionCounts.set(sessionCountKey, nextSession);
|
|
258
|
+
else
|
|
259
|
+
sessionCounts.delete(sessionCountKey);
|
|
260
|
+
if (nextRuntime > 0)
|
|
261
|
+
runtimeCounts.set(operationClass, nextRuntime);
|
|
262
|
+
else
|
|
263
|
+
runtimeCounts.delete(operationClass);
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function status() {
|
|
268
|
+
return Object.entries(configuredLimits).map(([id, configured]) => ({
|
|
269
|
+
id,
|
|
270
|
+
sessionLimit: configured.session,
|
|
271
|
+
runtimeLimit: configured.runtime,
|
|
272
|
+
active: runtimeCounts.get(id) || 0,
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
mode: 'in_memory',
|
|
277
|
+
distributed: false,
|
|
278
|
+
limits: configuredLimits,
|
|
279
|
+
usageLimits: null,
|
|
280
|
+
acquire,
|
|
281
|
+
status,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function assertOperationBudgetProvider(provider) {
|
|
285
|
+
if (!provider || typeof provider !== 'object'
|
|
286
|
+
|| typeof provider.acquire !== 'function'
|
|
287
|
+
|| typeof provider.status !== 'function') {
|
|
288
|
+
throw new TypeError('Operation budget providers must implement acquire() and status()');
|
|
289
|
+
}
|
|
290
|
+
return provider;
|
|
291
|
+
}
|
|
292
|
+
// Runs one bounded operation against any provider implementing the deliberately
|
|
293
|
+
// small acquire/status contract. A successful acquire returns a lease whose
|
|
294
|
+
// release method is invoked exactly once after downstream work settles. The
|
|
295
|
+
// tagged result keeps transport-specific 429 shaping in Express and Netlify,
|
|
296
|
+
// rather than coupling a future distributed provider to either framework.
|
|
297
|
+
async function runWithOperationBudget(provider, target, sessionKey, callback) {
|
|
298
|
+
const activeProvider = assertOperationBudgetProvider(provider);
|
|
299
|
+
if (typeof callback !== 'function')
|
|
300
|
+
throw new TypeError('An operation budget callback is required');
|
|
301
|
+
const { operationClass, operationFeature } = normalizeOperationBudgetTarget(target);
|
|
302
|
+
const lease = await activeProvider.acquire(operationClass, sessionKey, { operationFeature });
|
|
303
|
+
if (!lease || typeof lease.allowed !== 'boolean') {
|
|
304
|
+
throw new TypeError('Operation budget acquire() must return an allowed decision');
|
|
305
|
+
}
|
|
306
|
+
if (!lease.allowed) {
|
|
307
|
+
const denial = {
|
|
308
|
+
...lease,
|
|
309
|
+
operationClass,
|
|
310
|
+
...(operationFeature ? { operationFeature } : {}),
|
|
311
|
+
};
|
|
312
|
+
return {
|
|
313
|
+
allowed: false,
|
|
314
|
+
denial,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (typeof lease.release !== 'function') {
|
|
318
|
+
throw new TypeError('An allowed operation budget decision must include release()');
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
return { allowed: true, value: await callback() };
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
await lease.release();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function operationBudgetError(denial) {
|
|
328
|
+
const value = denial && typeof denial === 'object' ? denial : null;
|
|
329
|
+
const scope = value && [
|
|
330
|
+
'session',
|
|
331
|
+
'runtime',
|
|
332
|
+
'provider',
|
|
333
|
+
'global_daily',
|
|
334
|
+
'global_30_day',
|
|
335
|
+
'feature_daily',
|
|
336
|
+
'feature_30_day',
|
|
337
|
+
].includes(value.scope)
|
|
338
|
+
? value.scope
|
|
339
|
+
: 'runtime';
|
|
340
|
+
const operationFeature = value
|
|
341
|
+
&& typeof value.operationFeature === 'string'
|
|
342
|
+
&& Object.prototype.hasOwnProperty.call(OPERATION_FEATURE_CLASSES, value.operationFeature)
|
|
343
|
+
? value.operationFeature
|
|
344
|
+
: null;
|
|
345
|
+
if (scope === 'provider') {
|
|
346
|
+
return {
|
|
347
|
+
error: 'Distributed network-operation limits are temporarily unavailable. Please retry shortly.',
|
|
348
|
+
errorCode: OPERATION_BUDGET_UNAVAILABLE_ERROR_CODE,
|
|
349
|
+
operationClass: value && value.operationClass ? value.operationClass : null,
|
|
350
|
+
limitScope: scope,
|
|
351
|
+
...(operationFeature ? {
|
|
352
|
+
operationFeature,
|
|
353
|
+
operationFeatureModelVersion: OPERATION_FEATURE_MODEL_VERSION,
|
|
354
|
+
} : {}),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
if (scope.startsWith('global_') || scope.startsWith('feature_')) {
|
|
358
|
+
const featureScoped = scope.startsWith('feature_');
|
|
359
|
+
const dailyWindow = scope.endsWith('daily');
|
|
360
|
+
return {
|
|
361
|
+
error: `${featureScoped ? 'This operation feature has' : 'This deployment has'} reached its ${dailyWindow ? '24-hour' : '30-day'} network-operation allowance. Please retry after the current accounting window resets.`,
|
|
362
|
+
errorCode: OPERATION_USAGE_ERROR_CODE,
|
|
363
|
+
operationClass: value && value.operationClass ? value.operationClass : null,
|
|
364
|
+
limitScope: scope,
|
|
365
|
+
usageWindow: dailyWindow ? '24_hour' : '30_day',
|
|
366
|
+
usageModelVersion: OPERATION_USAGE_MODEL_VERSION,
|
|
367
|
+
...(operationFeature ? {
|
|
368
|
+
operationFeature,
|
|
369
|
+
operationFeatureModelVersion: OPERATION_FEATURE_MODEL_VERSION,
|
|
370
|
+
} : {}),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
error: scope === 'session'
|
|
375
|
+
? 'This session already has the maximum number of network operations in progress. Please retry shortly.'
|
|
376
|
+
: 'This deployment is already processing the maximum number of network operations for this class. Please retry shortly.',
|
|
377
|
+
errorCode: OPERATION_BUDGET_ERROR_CODE,
|
|
378
|
+
operationClass: value && value.operationClass ? value.operationClass : null,
|
|
379
|
+
limitScope: scope,
|
|
380
|
+
...(operationFeature ? {
|
|
381
|
+
operationFeature,
|
|
382
|
+
operationFeatureModelVersion: OPERATION_FEATURE_MODEL_VERSION,
|
|
383
|
+
} : {}),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function operationBudgetHttpStatus(denial) {
|
|
387
|
+
return denial && typeof denial === 'object' && denial.scope === 'provider' ? 503 : 429;
|
|
388
|
+
}
|
|
389
|
+
function operationBudgetReport(runtime = 'unknown', provider = defaultOperationBudget) {
|
|
390
|
+
const activeProvider = assertOperationBudgetProvider(provider);
|
|
391
|
+
const distributed = activeProvider.distributed === true;
|
|
392
|
+
const providerMode = activeProvider.mode;
|
|
393
|
+
const mode = typeof providerMode === 'string'
|
|
394
|
+
&& ['in_memory', 'redis_rest', 'unavailable'].includes(providerMode)
|
|
395
|
+
? providerMode
|
|
396
|
+
: 'unavailable';
|
|
397
|
+
const scope = distributed
|
|
398
|
+
? 'deployment'
|
|
399
|
+
: mode === 'unavailable'
|
|
400
|
+
? 'unavailable'
|
|
401
|
+
: runtime === 'netlify'
|
|
402
|
+
? 'serverless_instance'
|
|
403
|
+
: runtime === 'express'
|
|
404
|
+
? 'process'
|
|
405
|
+
: 'runtime_instance';
|
|
406
|
+
const limits = activeProvider.limits || DEFAULT_OPERATION_LIMITS;
|
|
407
|
+
return {
|
|
408
|
+
mode,
|
|
409
|
+
scope,
|
|
410
|
+
distributed,
|
|
411
|
+
classes: Object.entries(limits).map(([id, configured]) => ({
|
|
412
|
+
id,
|
|
413
|
+
sessionLimit: configured.session,
|
|
414
|
+
runtimeLimit: configured.runtime,
|
|
415
|
+
})),
|
|
416
|
+
usage: activeProvider.usageLimits
|
|
417
|
+
? {
|
|
418
|
+
mode: 'distributed_fixed_windows',
|
|
419
|
+
modelVersion: OPERATION_USAGE_MODEL_VERSION,
|
|
420
|
+
windowModel: 'utc_epoch_fixed',
|
|
421
|
+
dailyLimit: activeProvider.usageLimits.daily,
|
|
422
|
+
thirtyDayLimit: activeProvider.usageLimits.monthly,
|
|
423
|
+
features: Object.entries(activeProvider.usageLimits.features).map(([id, configured]) => ({
|
|
424
|
+
id,
|
|
425
|
+
dailyLimit: configured.daily,
|
|
426
|
+
thirtyDayLimit: configured.monthly,
|
|
427
|
+
})),
|
|
428
|
+
}
|
|
429
|
+
: {
|
|
430
|
+
mode: mode === 'unavailable' ? 'unavailable' : 'disabled',
|
|
431
|
+
modelVersion: OPERATION_USAGE_MODEL_VERSION,
|
|
432
|
+
windowModel: 'utc_epoch_fixed',
|
|
433
|
+
dailyLimit: null,
|
|
434
|
+
thirtyDayLimit: null,
|
|
435
|
+
features: [],
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function createUnavailableOperationBudget(limits = DEFAULT_OPERATION_LIMITS) {
|
|
440
|
+
const configuredLimits = normalizeOperationLimits(limits);
|
|
441
|
+
return {
|
|
442
|
+
mode: 'unavailable',
|
|
443
|
+
distributed: false,
|
|
444
|
+
limits: configuredLimits,
|
|
445
|
+
usageLimits: null,
|
|
446
|
+
async acquire(operationClass) {
|
|
447
|
+
if (!configuredLimits[operationClass])
|
|
448
|
+
throw new Error(`Unknown operation class: ${operationClass}`);
|
|
449
|
+
return {
|
|
450
|
+
allowed: false,
|
|
451
|
+
operationClass,
|
|
452
|
+
scope: 'provider',
|
|
453
|
+
retryAfterSeconds: OPERATION_PROVIDER_RETRY_AFTER_SECONDS,
|
|
454
|
+
};
|
|
455
|
+
},
|
|
456
|
+
async status() {
|
|
457
|
+
return Object.entries(configuredLimits).map(([id, configured]) => ({
|
|
458
|
+
id,
|
|
459
|
+
sessionLimit: configured.session,
|
|
460
|
+
runtimeLimit: configured.runtime,
|
|
461
|
+
active: null,
|
|
462
|
+
}));
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function createConfiguredOperationBudget(env = process.env, dependencies = {}) {
|
|
467
|
+
const url = env && env.UPSTASH_REDIS_REST_URL;
|
|
468
|
+
const token = env && env.UPSTASH_REDIS_REST_TOKEN;
|
|
469
|
+
const usagePolicy = env && env.WHOISLEUTH_OPERATION_USAGE_LIMITS;
|
|
470
|
+
if (!url && !token && !usagePolicy)
|
|
471
|
+
return createOperationBudget();
|
|
472
|
+
if (!url || !token)
|
|
473
|
+
return createUnavailableOperationBudget();
|
|
474
|
+
try {
|
|
475
|
+
const usageLimits = usagePolicy ? normalizeOperationUsageLimits(usagePolicy) : null;
|
|
476
|
+
return createDistributedOperationBudgetImplementation({
|
|
477
|
+
url,
|
|
478
|
+
token,
|
|
479
|
+
namespace: env.WHOISLEUTH_BUDGET_NAMESPACE,
|
|
480
|
+
limits: DEFAULT_OPERATION_LIMITS,
|
|
481
|
+
usageLimits,
|
|
482
|
+
}, dependencies);
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
return createUnavailableOperationBudget();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const defaultOperationBudget = createConfiguredOperationBudget();
|
|
489
|
+
export { OPERATION_CLASSES, OPERATION_FEATURE_MODEL_VERSION, OPERATION_FEATURES, OPERATION_FEATURE_CLASSES, DEFAULT_OPERATION_LIMITS, OPERATION_BUDGET_ERROR_CODE, OPERATION_BUDGET_UNAVAILABLE_ERROR_CODE, OPERATION_USAGE_ERROR_CODE, OPERATION_USAGE_MODEL_VERSION, MAX_OPERATION_USAGE_POLICY_BYTES, MAX_OPERATION_USAGE_LIMIT, OPERATION_RETRY_AFTER_SECONDS, OPERATION_PROVIDER_RETRY_AFTER_SECONDS, operationClassFor, operationFeatureFor, operationBudgetTargetFor, normalizeOperationBudgetTarget, normalizeOperationLimits, normalizeOperationUsageLimits, createOperationBudget, createUnavailableOperationBudget, createConfiguredOperationBudget, assertOperationBudgetProvider, runWithOperationBudget, operationBudgetError, operationBudgetHttpStatus, operationBudgetReport, defaultOperationBudget, };
|