@peac/net-node 0.12.6
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 +190 -0
- package/README.md +736 -0
- package/dist/codes.d.ts +47 -0
- package/dist/codes.d.ts.map +1 -0
- package/dist/codes.js +53 -0
- package/dist/codes.js.map +1 -0
- package/dist/constants.d.ts +39 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +39 -0
- package/dist/constants.js.map +1 -0
- package/dist/evidence-utils.d.ts +49 -0
- package/dist/evidence-utils.d.ts.map +1 -0
- package/dist/evidence-utils.js +109 -0
- package/dist/evidence-utils.js.map +1 -0
- package/dist/impl.d.ts +331 -0
- package/dist/impl.d.ts.map +1 -0
- package/dist/impl.js +1188 -0
- package/dist/impl.js.map +1 -0
- package/dist/index.d.ts +772 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1121 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.d.ts +28 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +59 -0
- package/dist/internal.js.map +1 -0
- package/dist/receipt-resolver.d.ts +65 -0
- package/dist/receipt-resolver.d.ts.map +1 -0
- package/dist/receipt-resolver.js +140 -0
- package/dist/receipt-resolver.js.map +1 -0
- package/dist/receipt-url-middleware.d.ts +87 -0
- package/dist/receipt-url-middleware.d.ts.map +1 -0
- package/dist/receipt-url-middleware.js +141 -0
- package/dist/receipt-url-middleware.js.map +1 -0
- package/dist/ssrf.d.ts +114 -0
- package/dist/ssrf.d.ts.map +1 -0
- package/dist/ssrf.js +214 -0
- package/dist/ssrf.js.map +1 -0
- package/dist/testing.d.ts +65 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +103 -0
- package/dist/testing.js.map +1 -0
- package/package.json +79 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PEAC Net (Node.js) - SSRF-Safe Network Utilities with DNS Pinning
|
|
3
|
+
*
|
|
4
|
+
* This module provides a reference implementation of network-layer SSRF protection
|
|
5
|
+
* with proper DNS resolution pinning using undici's custom dispatcher.
|
|
6
|
+
*
|
|
7
|
+
* SECURITY NOTES:
|
|
8
|
+
* - String-level URL checks alone are NOT sufficient for SSRF protection
|
|
9
|
+
* - DNS rebinding attacks require connection-time IP pinning
|
|
10
|
+
* - This implementation resolves DNS, validates ALL candidate IPs, and PINS
|
|
11
|
+
* the chosen IP at the connection layer (not just pre-check)
|
|
12
|
+
*
|
|
13
|
+
* RUNTIME: Node.js only (requires dns module and undici)
|
|
14
|
+
*
|
|
15
|
+
* @since v0.10.x
|
|
16
|
+
* @module @peac/net-node
|
|
17
|
+
*/
|
|
18
|
+
import { type SSRFPolicy } from './ssrf.js';
|
|
19
|
+
import { getAuditQueueStats, type RequestAuditContext } from './impl.js';
|
|
20
|
+
import { SAFE_FETCH_ERROR_CODES } from './codes.js';
|
|
21
|
+
import { SAFE_FETCH_EVENT_SCHEMA_VERSION, SAFE_FETCH_EVIDENCE_SCHEMA_VERSION, MAX_PENDING_AUDIT_EVENTS } from './constants.js';
|
|
22
|
+
export { getAuditQueueStats };
|
|
23
|
+
export type { RequestAuditContext };
|
|
24
|
+
export { SAFE_FETCH_ERROR_CODES };
|
|
25
|
+
export { SAFE_FETCH_EVENT_SCHEMA_VERSION, SAFE_FETCH_EVIDENCE_SCHEMA_VERSION, MAX_PENDING_AUDIT_EVENTS, };
|
|
26
|
+
/**
|
|
27
|
+
* Structured error with code, cause, and warnings
|
|
28
|
+
*
|
|
29
|
+
* NOTE: Error messages are intentionally generic to avoid leaking sensitive
|
|
30
|
+
* information (internal IPs, hostnames). Use `evidence` for audit details.
|
|
31
|
+
*/
|
|
32
|
+
export interface SafeFetchError {
|
|
33
|
+
error: string;
|
|
34
|
+
code: string;
|
|
35
|
+
cause_code?: string;
|
|
36
|
+
warnings?: string[];
|
|
37
|
+
/** Evidence object for audit trail (always present on error) */
|
|
38
|
+
evidence?: SafeFetchEvidence;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Result type for safeFetch operations
|
|
42
|
+
*/
|
|
43
|
+
export type SafeFetchResult<T> = {
|
|
44
|
+
ok: true;
|
|
45
|
+
response: Response;
|
|
46
|
+
data: T;
|
|
47
|
+
warnings?: string[];
|
|
48
|
+
evidence: SafeFetchEvidence;
|
|
49
|
+
} | ({
|
|
50
|
+
ok: false;
|
|
51
|
+
} & SafeFetchError);
|
|
52
|
+
/**
|
|
53
|
+
* Result type for raw fetch (returns stream with cleanup)
|
|
54
|
+
*/
|
|
55
|
+
export type SafeFetchRawResult = {
|
|
56
|
+
ok: true;
|
|
57
|
+
response: Response;
|
|
58
|
+
close: () => Promise<void>;
|
|
59
|
+
warnings?: string[];
|
|
60
|
+
evidence: SafeFetchEvidence;
|
|
61
|
+
} | ({
|
|
62
|
+
ok: false;
|
|
63
|
+
} & SafeFetchError);
|
|
64
|
+
/**
|
|
65
|
+
* DNS resolution result
|
|
66
|
+
*/
|
|
67
|
+
export interface DnsResolutionResult {
|
|
68
|
+
ok: true;
|
|
69
|
+
addresses: string[];
|
|
70
|
+
selectedAddress: string;
|
|
71
|
+
ipv4Addresses: string[];
|
|
72
|
+
ipv6Addresses: string[];
|
|
73
|
+
}
|
|
74
|
+
export interface DnsResolutionError {
|
|
75
|
+
ok: false;
|
|
76
|
+
error: string;
|
|
77
|
+
code: string;
|
|
78
|
+
/** Structured DNS answers for audit trail */
|
|
79
|
+
dns_answers?: DnsAnswer[];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* DNS resolver interface for dependency injection
|
|
83
|
+
* Allows mocking in tests without network calls
|
|
84
|
+
*/
|
|
85
|
+
export interface DnsResolver {
|
|
86
|
+
/**
|
|
87
|
+
* Resolve a hostname to all available IP addresses
|
|
88
|
+
* @param hostname - The hostname to resolve
|
|
89
|
+
* @returns Array of IP addresses (both IPv4 and IPv6)
|
|
90
|
+
*/
|
|
91
|
+
resolveAll(hostname: string): Promise<{
|
|
92
|
+
ipv4: string[];
|
|
93
|
+
ipv6: string[];
|
|
94
|
+
}>;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Phase-specific timeout options for HTTP client (P1.3)
|
|
98
|
+
*
|
|
99
|
+
* These map directly to undici's timeout options for precise control.
|
|
100
|
+
*/
|
|
101
|
+
export interface HttpClientTimeouts {
|
|
102
|
+
/** Total operation timeout (AbortController) */
|
|
103
|
+
totalMs?: number;
|
|
104
|
+
/** TCP/TLS connect timeout (undici Agent connect.timeout) */
|
|
105
|
+
connectMs?: number;
|
|
106
|
+
/** Time to receive headers after connection (undici headersTimeout) */
|
|
107
|
+
headersMs?: number;
|
|
108
|
+
/** Time to receive body after headers (undici bodyTimeout) */
|
|
109
|
+
bodyMs?: number;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* HTTP client interface for dependency injection
|
|
113
|
+
* Pattern B: returns response + close function for proper lifecycle management
|
|
114
|
+
*/
|
|
115
|
+
export interface HttpClient {
|
|
116
|
+
/**
|
|
117
|
+
* Fetch a URL with optional pinned IP
|
|
118
|
+
* Returns response and close function to ensure proper agent cleanup
|
|
119
|
+
* @param url - The URL to fetch
|
|
120
|
+
* @param pinnedIp - The IP address to connect to (DNS pinning)
|
|
121
|
+
* @param options - Fetch options with phase-specific timeouts
|
|
122
|
+
*/
|
|
123
|
+
fetch(url: string, pinnedIp: string | null, options?: RequestInit & {
|
|
124
|
+
timeouts?: HttpClientTimeouts;
|
|
125
|
+
timeoutMs?: number;
|
|
126
|
+
}): Promise<{
|
|
127
|
+
response: Response;
|
|
128
|
+
close: () => Promise<void>;
|
|
129
|
+
}>;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Redirect policy for cross-host redirects
|
|
133
|
+
*/
|
|
134
|
+
export type RedirectPolicy = 'none' | 'same-origin' | 'same-registrable-domain' | 'allowlist';
|
|
135
|
+
/**
|
|
136
|
+
* Evidence redaction level for portable receipts
|
|
137
|
+
*
|
|
138
|
+
* - 'public': Safe for external sharing. Hashes IPs, removes raw addresses.
|
|
139
|
+
* Use this for evidence that may be included in receipts, logs, or telemetry.
|
|
140
|
+
*
|
|
141
|
+
* - 'tenant': Internal correlation with HMAC-SHA256 keyed hashing.
|
|
142
|
+
* Prevents cross-org linkage while allowing intra-org correlation.
|
|
143
|
+
* Requires `redactionKey` option to be provided.
|
|
144
|
+
*
|
|
145
|
+
* - 'private': Full details for internal audit. Includes raw IPs and hostnames.
|
|
146
|
+
* Only use for internal debugging or trusted audit systems.
|
|
147
|
+
*
|
|
148
|
+
* Default is 'public' for defense-in-depth (evidence may leak to external systems).
|
|
149
|
+
*
|
|
150
|
+
* @since v0.10.x
|
|
151
|
+
*/
|
|
152
|
+
export type EvidenceLevel = 'public' | 'tenant' | 'private';
|
|
153
|
+
/**
|
|
154
|
+
* Redacted IP information for public evidence
|
|
155
|
+
*
|
|
156
|
+
* Raw IP addresses are replaced with family + SHA-256 hash.
|
|
157
|
+
* This allows correlation without exposing internal topology.
|
|
158
|
+
*
|
|
159
|
+
* For 'tenant' level, uses HMAC-SHA256 with tenant key to prevent
|
|
160
|
+
* cross-org correlation via rainbow tables.
|
|
161
|
+
*/
|
|
162
|
+
export interface RedactedIpInfo {
|
|
163
|
+
/** Address family (4 or 6) */
|
|
164
|
+
family: 4 | 6;
|
|
165
|
+
/** SHA-256 or HMAC-SHA256 hash of the IP address (lowercase hex, 0x prefixed) */
|
|
166
|
+
hash: string;
|
|
167
|
+
/** Key ID used for HMAC (only present in 'tenant' mode) */
|
|
168
|
+
key_id?: string;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Acknowledgment string for allowing mixed public/private DNS resolution
|
|
172
|
+
*/
|
|
173
|
+
export declare const ALLOW_MIXED_DNS_ACK: "I_UNDERSTAND_MIXED_DNS_SECURITY_RISKS";
|
|
174
|
+
/**
|
|
175
|
+
* Evidence without digest fields (for computing the digest)
|
|
176
|
+
*
|
|
177
|
+
* The digest is computed over evidence with digest fields omitted
|
|
178
|
+
* to avoid self-referential circularity.
|
|
179
|
+
*/
|
|
180
|
+
export type SafeFetchEvidenceCore = Omit<SafeFetchEvidence, 'evidence_digest' | 'evidence_alg' | 'canonicalization'>;
|
|
181
|
+
/**
|
|
182
|
+
* Canonicalize evidence per RFC 8785 (JCS)
|
|
183
|
+
*
|
|
184
|
+
* Returns the canonical JSON string representation of the evidence object.
|
|
185
|
+
* This is deterministic across implementations and can be used for:
|
|
186
|
+
* - Computing digests for signing
|
|
187
|
+
* - Cross-implementation verification
|
|
188
|
+
* - Deduplication
|
|
189
|
+
*
|
|
190
|
+
* IMPORTANT: For digest computation, pass evidence WITHOUT digest fields
|
|
191
|
+
* to avoid self-referential circularity.
|
|
192
|
+
*
|
|
193
|
+
* @param evidence - SafeFetchEvidence or SafeFetchEvidenceCore object
|
|
194
|
+
* @returns Canonical JSON string
|
|
195
|
+
*/
|
|
196
|
+
export declare function canonicalizeEvidence(evidence: SafeFetchEvidence | SafeFetchEvidenceCore): string;
|
|
197
|
+
/**
|
|
198
|
+
* Compute SHA-256 digest of evidence using JCS canonicalization
|
|
199
|
+
*
|
|
200
|
+
* Returns a 0x-prefixed lowercase hex string, consistent with other
|
|
201
|
+
* PEAC cryptographic identifiers (EAS anchors, IP hashes).
|
|
202
|
+
*
|
|
203
|
+
* Self-omission rule: If the evidence includes digest fields, they are
|
|
204
|
+
* stripped before computing the digest to avoid circularity.
|
|
205
|
+
*
|
|
206
|
+
* @param evidence - SafeFetchEvidence or SafeFetchEvidenceCore object
|
|
207
|
+
* @returns 0x-prefixed SHA-256 hex digest
|
|
208
|
+
*/
|
|
209
|
+
export declare function computeEvidenceDigest(evidence: SafeFetchEvidence | SafeFetchEvidenceCore): string;
|
|
210
|
+
/**
|
|
211
|
+
* Event types emitted by safeFetch for audit logging
|
|
212
|
+
*/
|
|
213
|
+
export type SafeFetchEventType = 'dns_start' | 'dns_success' | 'dns_blocked' | 'dns_error' | 'connect_start' | 'connect_success' | 'connect_error' | 'request_start' | 'response_headers' | 'response_body_start' | 'response_body_complete' | 'redirect' | 'error' | 'audit_overflow' | 'audit_hook_error';
|
|
214
|
+
/**
|
|
215
|
+
* Structured DNS answer for audit-clean logging
|
|
216
|
+
*
|
|
217
|
+
* Each answer includes the IP, address family, and blocked reason (if any).
|
|
218
|
+
* This allows downstream verifiers to replay "why blocked/allowed" decisions.
|
|
219
|
+
*/
|
|
220
|
+
export interface DnsAnswer {
|
|
221
|
+
/** Resolved IP address */
|
|
222
|
+
ip: string;
|
|
223
|
+
/** Address family (4 or 6) */
|
|
224
|
+
family: 4 | 6;
|
|
225
|
+
/** If blocked, the reason code; undefined if allowed */
|
|
226
|
+
blocked_reason?: string;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Policy decision for audit trail
|
|
230
|
+
*/
|
|
231
|
+
export type PolicyDecision = 'allow' | 'block';
|
|
232
|
+
/**
|
|
233
|
+
* Audit event with timing and metadata
|
|
234
|
+
*
|
|
235
|
+
* This is a vendor-neutral structure that can be adapted to any logging system.
|
|
236
|
+
* Events are emitted at key points in the fetch lifecycle.
|
|
237
|
+
*
|
|
238
|
+
* IMPORTANT: This schema is versioned via `schema_version` for forward compatibility.
|
|
239
|
+
* @see SAFE_FETCH_EVENT_SCHEMA_VERSION
|
|
240
|
+
*/
|
|
241
|
+
export interface SafeFetchEvent {
|
|
242
|
+
/** Schema version for forward compatibility */
|
|
243
|
+
schema_version: typeof SAFE_FETCH_EVENT_SCHEMA_VERSION;
|
|
244
|
+
/** Event type */
|
|
245
|
+
type: SafeFetchEventType;
|
|
246
|
+
/** Timestamp (Date.now()) */
|
|
247
|
+
timestamp: number;
|
|
248
|
+
/** Original request URL (sanitized - no credentials) */
|
|
249
|
+
url: string;
|
|
250
|
+
/** Resolved hostname (after canonicalization) */
|
|
251
|
+
hostname?: string;
|
|
252
|
+
/** Selected IP address (only on success events) */
|
|
253
|
+
selected_ip?: string;
|
|
254
|
+
/** Error code if applicable */
|
|
255
|
+
code?: string;
|
|
256
|
+
/** Human-readable message (generic, non-sensitive) */
|
|
257
|
+
message?: string;
|
|
258
|
+
/** Policy decision (allow/block) for DNS events */
|
|
259
|
+
policy_decision?: PolicyDecision;
|
|
260
|
+
/** Additional metadata (vendor-neutral, may contain sensitive debug info) */
|
|
261
|
+
meta?: Record<string, unknown>;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Audit hook callback type
|
|
265
|
+
*
|
|
266
|
+
* HOOK CONTRACT (for reliable integration):
|
|
267
|
+
*
|
|
268
|
+
* - Delivery: Asynchronous via queueMicrotask (non-blocking to fetch path)
|
|
269
|
+
* - Ordering: FIFO within a single request (events arrive in fetch order)
|
|
270
|
+
* - Guarantees: At-most-once delivery (events may be dropped under load)
|
|
271
|
+
* - Exceptions: MUST NOT throw (exceptions are swallowed silently)
|
|
272
|
+
* - Side effects: MUST NOT modify request/response or call safeFetch
|
|
273
|
+
*
|
|
274
|
+
* Special events:
|
|
275
|
+
* - `audit_overflow`: Emitted SYNCHRONOUSLY when queue is full (rate-limited 1/sec)
|
|
276
|
+
* This event bypasses the queue to ensure observability of drops.
|
|
277
|
+
*
|
|
278
|
+
* For enterprise reliability, consume events idempotently and monitor
|
|
279
|
+
* `audit_overflow` events as a signal of capacity issues.
|
|
280
|
+
*/
|
|
281
|
+
export type SafeFetchAuditHook = (event: SafeFetchEvent) => void;
|
|
282
|
+
/**
|
|
283
|
+
* Evidence object returned by safeFetch for PEAC-ready receipts
|
|
284
|
+
*
|
|
285
|
+
* This is a portable, signed-ready artifact that captures:
|
|
286
|
+
* - Request URL (normalized, no credentials)
|
|
287
|
+
* - Canonical host processing
|
|
288
|
+
* - DNS resolution decisions
|
|
289
|
+
* - Redirect chain (if any)
|
|
290
|
+
* - Timing summary
|
|
291
|
+
* - Budget enforcement
|
|
292
|
+
* - Final decision code
|
|
293
|
+
*
|
|
294
|
+
* Evidence is redacted based on `evidence_level`:
|
|
295
|
+
* - 'public': Raw IPs replaced with family + SHA-256 hash
|
|
296
|
+
* - 'private': Full details including raw IPs
|
|
297
|
+
*
|
|
298
|
+
* Designed for multi-agent workflow verification and dispute resolution.
|
|
299
|
+
*/
|
|
300
|
+
export interface SafeFetchEvidence {
|
|
301
|
+
/** Schema version for forward compatibility (evidence-specific) */
|
|
302
|
+
schema_version: typeof SAFE_FETCH_EVIDENCE_SCHEMA_VERSION;
|
|
303
|
+
/**
|
|
304
|
+
* Evidence redaction level
|
|
305
|
+
* - 'public': Safe for external sharing (default)
|
|
306
|
+
* - 'tenant': Internal correlation with HMAC (prevents cross-org linkage)
|
|
307
|
+
* - 'private': Full details for internal audit
|
|
308
|
+
*/
|
|
309
|
+
evidence_level: EvidenceLevel;
|
|
310
|
+
/**
|
|
311
|
+
* SHA-256 digest of canonical evidence (0x-prefixed hex)
|
|
312
|
+
* Computed over evidence with this field omitted (self-referential exclusion)
|
|
313
|
+
*/
|
|
314
|
+
evidence_digest: string;
|
|
315
|
+
/**
|
|
316
|
+
* Hash algorithm used for digest
|
|
317
|
+
* @default 'sha-256'
|
|
318
|
+
*/
|
|
319
|
+
evidence_alg: 'sha-256';
|
|
320
|
+
/**
|
|
321
|
+
* Canonicalization scheme used before hashing
|
|
322
|
+
* @default 'RFC8785-JCS'
|
|
323
|
+
*/
|
|
324
|
+
canonicalization: 'RFC8785-JCS';
|
|
325
|
+
/** Request timestamp (start of fetch) */
|
|
326
|
+
request_timestamp: number;
|
|
327
|
+
/** Response timestamp (headers received) */
|
|
328
|
+
response_timestamp?: number;
|
|
329
|
+
/** Normalized request URL (no credentials) */
|
|
330
|
+
request_url: string;
|
|
331
|
+
/** Canonical hostname after processing */
|
|
332
|
+
canonical_host: string;
|
|
333
|
+
/** Whether host was an IP literal */
|
|
334
|
+
is_ip_literal: boolean;
|
|
335
|
+
/**
|
|
336
|
+
* Selected IP info (public evidence - always present on success)
|
|
337
|
+
* Contains family and SHA-256 hash of the selected IP
|
|
338
|
+
*/
|
|
339
|
+
selected_ip_info?: RedactedIpInfo;
|
|
340
|
+
/**
|
|
341
|
+
* DNS answer summary (public evidence)
|
|
342
|
+
* Count of answers by family, without raw IPs
|
|
343
|
+
*/
|
|
344
|
+
dns_answer_count?: {
|
|
345
|
+
ipv4: number;
|
|
346
|
+
ipv6: number;
|
|
347
|
+
};
|
|
348
|
+
/**
|
|
349
|
+
* DNS answers with per-IP decisions (private evidence only)
|
|
350
|
+
* Only populated when evidence_level is 'private'
|
|
351
|
+
*/
|
|
352
|
+
dns_answers?: DnsAnswer[];
|
|
353
|
+
/**
|
|
354
|
+
* Raw selected IP for connection (private evidence only)
|
|
355
|
+
* Only populated when evidence_level is 'private'
|
|
356
|
+
*/
|
|
357
|
+
selected_ip?: string;
|
|
358
|
+
/** Policy decision */
|
|
359
|
+
policy_decision: PolicyDecision;
|
|
360
|
+
/** Final decision code */
|
|
361
|
+
decision_code: string;
|
|
362
|
+
/** Redirect chain (URLs) */
|
|
363
|
+
redirect_chain?: string[];
|
|
364
|
+
/** Response status code */
|
|
365
|
+
response_status?: number;
|
|
366
|
+
/** Actual response bytes received */
|
|
367
|
+
response_bytes?: number;
|
|
368
|
+
/** Max response bytes budget */
|
|
369
|
+
max_response_bytes: number;
|
|
370
|
+
/** Total elapsed time in milliseconds */
|
|
371
|
+
elapsed_ms?: number;
|
|
372
|
+
/** Dropped headers (security enforcement) */
|
|
373
|
+
dropped_headers?: string[];
|
|
374
|
+
/**
|
|
375
|
+
* Audit queue statistics at time of evidence creation (per-request)
|
|
376
|
+
*
|
|
377
|
+
* Provides visibility into audit event processing health for THIS request.
|
|
378
|
+
* All counters are request-scoped - they reset for each safeFetch call.
|
|
379
|
+
* If `dropped > 0`, some events were lost due to queue overflow.
|
|
380
|
+
* This is a signal that evidence quality may be degraded.
|
|
381
|
+
*/
|
|
382
|
+
audit_stats?: {
|
|
383
|
+
/** Number of audit events pending at evidence creation */
|
|
384
|
+
pending: number;
|
|
385
|
+
/** Total number of dropped audit events for this request */
|
|
386
|
+
dropped: number;
|
|
387
|
+
/** Maximum allowed pending events */
|
|
388
|
+
max_pending: number;
|
|
389
|
+
/** Number of hook errors that occurred during this request */
|
|
390
|
+
hook_errors: number;
|
|
391
|
+
/** Number of hook errors suppressed by rate limiting for this request */
|
|
392
|
+
hook_suppressed: number;
|
|
393
|
+
};
|
|
394
|
+
/**
|
|
395
|
+
* Indicates audit data was truncated due to overflow
|
|
396
|
+
*
|
|
397
|
+
* NORMATIVE PRESENCE SEMANTICS:
|
|
398
|
+
* - Present and `true`: Evidence is incomplete, audit events were dropped
|
|
399
|
+
* - Absent (undefined): Evidence is complete, no truncation occurred
|
|
400
|
+
*
|
|
401
|
+
* Producers MUST omit this field when no truncation occurred.
|
|
402
|
+
* Producers MUST NOT emit `audit_truncated: false`.
|
|
403
|
+
*
|
|
404
|
+
* This field uses presence semantics to ensure digest compatibility:
|
|
405
|
+
* `{ ...core }` and `{ ...core, audit_truncated: false }` would produce
|
|
406
|
+
* different digests for logically identical evidence. By requiring omission
|
|
407
|
+
* when false, we guarantee cross-implementation digest parity.
|
|
408
|
+
*
|
|
409
|
+
* Set to `true` when `audit_stats.dropped > 0`.
|
|
410
|
+
*/
|
|
411
|
+
audit_truncated?: true;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Granular timeout configuration for different phases
|
|
415
|
+
*
|
|
416
|
+
* All values are in milliseconds. If not specified, defaults apply.
|
|
417
|
+
*/
|
|
418
|
+
export interface TimeoutConfig {
|
|
419
|
+
/**
|
|
420
|
+
* Total request timeout (overall limit)
|
|
421
|
+
* @default 30000 (30 seconds)
|
|
422
|
+
*/
|
|
423
|
+
totalMs?: number;
|
|
424
|
+
/**
|
|
425
|
+
* DNS resolution timeout
|
|
426
|
+
* @default 5000 (5 seconds)
|
|
427
|
+
*/
|
|
428
|
+
dnsMs?: number;
|
|
429
|
+
/**
|
|
430
|
+
* TCP/TLS connection timeout
|
|
431
|
+
* @default 10000 (10 seconds)
|
|
432
|
+
*/
|
|
433
|
+
connectMs?: number;
|
|
434
|
+
/**
|
|
435
|
+
* Time to receive response headers
|
|
436
|
+
* @default 15000 (15 seconds)
|
|
437
|
+
*/
|
|
438
|
+
headersMs?: number;
|
|
439
|
+
/**
|
|
440
|
+
* Time to receive response body (after headers)
|
|
441
|
+
* @default 30000 (30 seconds)
|
|
442
|
+
*/
|
|
443
|
+
bodyMs?: number;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Default timeout values
|
|
447
|
+
*/
|
|
448
|
+
export declare const DEFAULT_TIMEOUTS: Required<TimeoutConfig>;
|
|
449
|
+
/**
|
|
450
|
+
* Options for safeFetch
|
|
451
|
+
*/
|
|
452
|
+
export interface SafeFetchOptions extends Omit<RequestInit, 'signal'> {
|
|
453
|
+
/**
|
|
454
|
+
* SSRF policy configuration
|
|
455
|
+
* @default DEFAULT_SSRF_POLICY
|
|
456
|
+
*/
|
|
457
|
+
ssrfPolicy?: SSRFPolicy;
|
|
458
|
+
/**
|
|
459
|
+
* Request timeout in milliseconds
|
|
460
|
+
* @default 30000 (30 seconds)
|
|
461
|
+
*/
|
|
462
|
+
timeoutMs?: number;
|
|
463
|
+
/**
|
|
464
|
+
* Maximum number of redirects to follow
|
|
465
|
+
* Set to 0 to disable redirects
|
|
466
|
+
* @default 5
|
|
467
|
+
*/
|
|
468
|
+
maxRedirects?: number;
|
|
469
|
+
/**
|
|
470
|
+
* Redirect policy for handling cross-host redirects
|
|
471
|
+
* @default 'same-origin'
|
|
472
|
+
*/
|
|
473
|
+
redirectPolicy?: RedirectPolicy;
|
|
474
|
+
/**
|
|
475
|
+
* Allowed hosts for redirects when redirectPolicy is 'allowlist'
|
|
476
|
+
* Also used for 'same-registrable-domain' as additional allowed hosts
|
|
477
|
+
*/
|
|
478
|
+
redirectAllowHosts?: string[];
|
|
479
|
+
/**
|
|
480
|
+
* @deprecated Use redirectPolicy instead
|
|
481
|
+
* Allow redirects to different hosts
|
|
482
|
+
*/
|
|
483
|
+
allowCrossHostRedirects?: boolean;
|
|
484
|
+
/**
|
|
485
|
+
* Allowed HTTP methods (SSRF side-effect prevention)
|
|
486
|
+
* @default ['GET', 'HEAD']
|
|
487
|
+
*/
|
|
488
|
+
allowedMethods?: string[];
|
|
489
|
+
/**
|
|
490
|
+
* Maximum response body size in bytes (DoS protection)
|
|
491
|
+
* @default 2097152 (2 MB)
|
|
492
|
+
*/
|
|
493
|
+
maxResponseBytes?: number;
|
|
494
|
+
/**
|
|
495
|
+
* Disable Accept-Encoding: identity default.
|
|
496
|
+
*
|
|
497
|
+
* By default, we send Accept-Encoding: identity to prevent decompression bombs.
|
|
498
|
+
* Setting this to true allows compressed responses (gzip, deflate, br).
|
|
499
|
+
*
|
|
500
|
+
* WARNING: Only set this if you trust the server AND have streaming
|
|
501
|
+
* decompression with size limits in place.
|
|
502
|
+
*
|
|
503
|
+
* @default false
|
|
504
|
+
*/
|
|
505
|
+
allowCompression?: boolean;
|
|
506
|
+
/**
|
|
507
|
+
* Allow DNS resolution that returns BOTH public and private IPs.
|
|
508
|
+
*
|
|
509
|
+
* By default, we REJECT requests where ANY resolved IP is private,
|
|
510
|
+
* even if there are also public IPs. This prevents attackers from
|
|
511
|
+
* hiding a private IP alongside public ones.
|
|
512
|
+
*
|
|
513
|
+
* In some corporate environments, split DNS legitimately returns
|
|
514
|
+
* both internal and external IPs. This escape hatch allows such
|
|
515
|
+
* configurations, but requires explicit acknowledgment.
|
|
516
|
+
*
|
|
517
|
+
* SECURITY WARNING: This weakens SSRF protection. Only use if:
|
|
518
|
+
* 1. You control the DNS infrastructure
|
|
519
|
+
* 2. You understand the risks of mixed DNS responses
|
|
520
|
+
* 3. You have other mitigations in place (network segmentation, etc.)
|
|
521
|
+
*
|
|
522
|
+
* @default false
|
|
523
|
+
*/
|
|
524
|
+
allowMixedPublicAndPrivateDns?: boolean;
|
|
525
|
+
/**
|
|
526
|
+
* Required acknowledgment for allowMixedPublicAndPrivateDns.
|
|
527
|
+
* Set to ALLOW_MIXED_DNS_ACK ('I_UNDERSTAND_MIXED_DNS_SECURITY_RISKS').
|
|
528
|
+
*/
|
|
529
|
+
ack_allow_mixed_dns?: typeof ALLOW_MIXED_DNS_ACK;
|
|
530
|
+
/**
|
|
531
|
+
* Allow connections to dangerous ports (admin, databases, service mesh).
|
|
532
|
+
*
|
|
533
|
+
* By default, we BLOCK connections to ports in DANGEROUS_PORTS set
|
|
534
|
+
* (SSH, databases, container orchestration, etc.) even on public IPs.
|
|
535
|
+
* This prevents SSRF attacks targeting internal services on non-standard ports.
|
|
536
|
+
*
|
|
537
|
+
* SECURITY WARNING: This weakens SSRF protection. Only use if:
|
|
538
|
+
* 1. You specifically need to connect to services on these ports
|
|
539
|
+
* 2. You understand the risks of allowing these connections
|
|
540
|
+
* 3. You have network-level controls in place
|
|
541
|
+
*
|
|
542
|
+
* @default false
|
|
543
|
+
*/
|
|
544
|
+
allowDangerousPorts?: boolean;
|
|
545
|
+
/**
|
|
546
|
+
* Required acknowledgment for allowDangerousPorts.
|
|
547
|
+
* Set to ALLOW_DANGEROUS_PORTS_ACK ('I_UNDERSTAND_DANGEROUS_PORTS_RISK').
|
|
548
|
+
*/
|
|
549
|
+
ack_allow_dangerous_ports?: typeof ALLOW_DANGEROUS_PORTS_ACK;
|
|
550
|
+
/**
|
|
551
|
+
* Granular timeout configuration for different phases.
|
|
552
|
+
* If provided, overrides timeoutMs for specific phases.
|
|
553
|
+
* @see TimeoutConfig
|
|
554
|
+
*/
|
|
555
|
+
timeouts?: TimeoutConfig;
|
|
556
|
+
/**
|
|
557
|
+
* Audit hook for observing fetch lifecycle events.
|
|
558
|
+
*
|
|
559
|
+
* Called at key points (DNS, connect, request, response, error).
|
|
560
|
+
* Useful for logging, metrics, and debugging.
|
|
561
|
+
*
|
|
562
|
+
* MUST NOT throw or modify request/response.
|
|
563
|
+
* Hooks are invoked via queueMicrotask for performance isolation.
|
|
564
|
+
* @see SafeFetchAuditHook
|
|
565
|
+
*/
|
|
566
|
+
onEvent?: SafeFetchAuditHook;
|
|
567
|
+
/**
|
|
568
|
+
* Custom sanitizer for hook error messages.
|
|
569
|
+
*
|
|
570
|
+
* By default, error messages are sanitized to:
|
|
571
|
+
* - Truncate to 200 characters
|
|
572
|
+
* - Redact Bearer tokens, passwords, keys, secrets
|
|
573
|
+
*
|
|
574
|
+
* Use this option to provide a custom sanitizer or disable sanitization:
|
|
575
|
+
* - Pass a function to customize: `(msg: string) => sanitizedMsg`
|
|
576
|
+
* - Pass `'off'` to disable sanitization entirely (NOT RECOMMENDED in production)
|
|
577
|
+
*
|
|
578
|
+
* @default Built-in sanitizer (truncate + redact secrets)
|
|
579
|
+
*/
|
|
580
|
+
sanitizeHookError?: ((message: string) => string) | 'off';
|
|
581
|
+
/**
|
|
582
|
+
* Enable debug mode to include sensitive details in evidence and events.
|
|
583
|
+
*
|
|
584
|
+
* When false (default), error messages and evidence are generic to avoid
|
|
585
|
+
* leaking internal IPs or hostnames through logs/telemetry.
|
|
586
|
+
*
|
|
587
|
+
* When true:
|
|
588
|
+
* - Evidence includes detailed `dns_answers` with per-IP decisions
|
|
589
|
+
* - Event metadata includes blocked IPs and addresses
|
|
590
|
+
*
|
|
591
|
+
* WARNING: Only enable in trusted environments (local dev, internal logging).
|
|
592
|
+
* @default false
|
|
593
|
+
* @deprecated Use evidenceLevel: 'private' instead for evidence, keep debug for events
|
|
594
|
+
*/
|
|
595
|
+
debug?: boolean;
|
|
596
|
+
/**
|
|
597
|
+
* Evidence redaction level for portable receipts.
|
|
598
|
+
*
|
|
599
|
+
* - 'public' (default): Raw IPs replaced with family + SHA-256 hash.
|
|
600
|
+
* Safe for including in receipts, logs, or external telemetry.
|
|
601
|
+
* Note: SHA-256 of IPv4 is rainbow-table vulnerable (only 2^32 inputs).
|
|
602
|
+
*
|
|
603
|
+
* - 'tenant': HMAC-SHA256 with tenant-specific key. Prevents cross-org
|
|
604
|
+
* correlation via rainbow tables while allowing intra-org correlation.
|
|
605
|
+
* Requires `redactionKey` and `redactionKeyId` options.
|
|
606
|
+
*
|
|
607
|
+
* - 'private': Full details including raw IPs and DNS answers.
|
|
608
|
+
* Only use for internal audit systems you control.
|
|
609
|
+
*
|
|
610
|
+
* Defense-in-depth: default 'public' ensures evidence is safe even
|
|
611
|
+
* if accidentally exposed through logs or telemetry pipelines.
|
|
612
|
+
*
|
|
613
|
+
* @default 'public'
|
|
614
|
+
* @since v0.10.x
|
|
615
|
+
*/
|
|
616
|
+
evidenceLevel?: EvidenceLevel;
|
|
617
|
+
/**
|
|
618
|
+
* Tenant-specific key for HMAC-based IP hashing (tenant mode only).
|
|
619
|
+
*
|
|
620
|
+
* Must be at least 32 bytes for security. Used with HMAC-SHA256 to
|
|
621
|
+
* prevent cross-tenant correlation via rainbow tables.
|
|
622
|
+
*
|
|
623
|
+
* Required when evidenceLevel is 'tenant'.
|
|
624
|
+
*
|
|
625
|
+
* @since v0.10.x
|
|
626
|
+
*/
|
|
627
|
+
redactionKey?: Uint8Array;
|
|
628
|
+
/**
|
|
629
|
+
* Key identifier for tenant mode redaction.
|
|
630
|
+
*
|
|
631
|
+
* Included in evidence to identify which key was used for hashing.
|
|
632
|
+
* Allows key rotation while maintaining audit trails.
|
|
633
|
+
*
|
|
634
|
+
* Required when evidenceLevel is 'tenant'.
|
|
635
|
+
*
|
|
636
|
+
* @since v0.10.x
|
|
637
|
+
*/
|
|
638
|
+
redactionKeyId?: string;
|
|
639
|
+
/**
|
|
640
|
+
* Custom DNS resolver for dependency injection (testing)
|
|
641
|
+
* @internal
|
|
642
|
+
*/
|
|
643
|
+
_dnsResolver?: DnsResolver;
|
|
644
|
+
/**
|
|
645
|
+
* Custom HTTP client for dependency injection (testing)
|
|
646
|
+
* @internal
|
|
647
|
+
*/
|
|
648
|
+
_httpClient?: HttpClient;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Default timeout for fetch requests (30 seconds)
|
|
652
|
+
*/
|
|
653
|
+
export declare const DEFAULT_TIMEOUT_MS = 30000;
|
|
654
|
+
/**
|
|
655
|
+
* Default maximum redirects
|
|
656
|
+
*/
|
|
657
|
+
export declare const DEFAULT_MAX_REDIRECTS = 5;
|
|
658
|
+
/**
|
|
659
|
+
* Default allowed HTTP methods (GET/HEAD only for SSRF safety)
|
|
660
|
+
*/
|
|
661
|
+
export declare const DEFAULT_ALLOWED_METHODS: string[];
|
|
662
|
+
/**
|
|
663
|
+
* Default maximum response size (2 MB)
|
|
664
|
+
*
|
|
665
|
+
* COMPRESSION SEMANTICS (P0.4 - Protocol-Grade Evidence):
|
|
666
|
+
*
|
|
667
|
+
* This limit measures DECODED bytes (after decompression), NOT wire bytes.
|
|
668
|
+
* The limit applies to what `response.body.getReader().read()` returns.
|
|
669
|
+
*
|
|
670
|
+
* By default, we request `Accept-Encoding: identity` to prevent compression.
|
|
671
|
+
* This means:
|
|
672
|
+
* - Wire bytes === Decoded bytes (no compression)
|
|
673
|
+
* - Content-Length matches actual body size
|
|
674
|
+
* - No decompression bomb risk
|
|
675
|
+
*
|
|
676
|
+
* If `allowCompression: true`, compression is allowed, and:
|
|
677
|
+
* - Wire bytes may be smaller than decoded bytes
|
|
678
|
+
* - Content-Length reflects compressed size (if set)
|
|
679
|
+
* - We measure DECODED bytes (after browser/runtime decompresses)
|
|
680
|
+
* - Decompression bombs are possible (1KB compressed -> 1GB decoded)
|
|
681
|
+
*
|
|
682
|
+
* WARNING: Only use `allowCompression: true` if you trust the server AND have
|
|
683
|
+
* additional decoded size limits in place.
|
|
684
|
+
*
|
|
685
|
+
* For evidence purposes, `response_bytes` in SafeFetchEvidence reflects
|
|
686
|
+
* decoded bytes (what we actually read), not wire bytes.
|
|
687
|
+
*/
|
|
688
|
+
export declare const DEFAULT_MAX_RESPONSE_BYTES: number;
|
|
689
|
+
/**
|
|
690
|
+
* Whether maxResponseBytes measures wire or decoded bytes.
|
|
691
|
+
* Always 'decoded' - we measure after potential decompression.
|
|
692
|
+
*/
|
|
693
|
+
export declare const RESPONSE_BYTES_MEASUREMENT: "decoded";
|
|
694
|
+
/**
|
|
695
|
+
* Maximum response size for JWKS/OIDC endpoints (512 KB)
|
|
696
|
+
*/
|
|
697
|
+
export declare const MAX_JWKS_RESPONSE_BYTES: number;
|
|
698
|
+
/**
|
|
699
|
+
* Dangerous ports that are ALWAYS blocked regardless of configuration.
|
|
700
|
+
*
|
|
701
|
+
* These ports commonly host:
|
|
702
|
+
* - SSH (22), SMTP (25), DNS (53), FTP (21)
|
|
703
|
+
* - Database services (3306 MySQL, 5432 PostgreSQL, 6379 Redis, 27017 MongoDB)
|
|
704
|
+
* - Kubernetes (6443, 10250), Envoy (15000), Prometheus (9090)
|
|
705
|
+
* - Internal admin panels, debug ports
|
|
706
|
+
*
|
|
707
|
+
* Blocking these prevents SSRF to internal services even on attacker-controlled hosts.
|
|
708
|
+
* To allow these ports, you MUST provide explicit acknowledgment.
|
|
709
|
+
*/
|
|
710
|
+
export declare const DANGEROUS_PORTS: ReadonlySet<number>;
|
|
711
|
+
/**
|
|
712
|
+
* Acknowledgment string for allowing dangerous ports
|
|
713
|
+
*/
|
|
714
|
+
export declare const ALLOW_DANGEROUS_PORTS_ACK: "I_UNDERSTAND_DANGEROUS_PORTS_RISK";
|
|
715
|
+
/**
|
|
716
|
+
* Default DNS resolver using Node.js dns.promises module
|
|
717
|
+
* Resolves both A (IPv4) and AAAA (IPv6) records separately for Happy Eyeballs
|
|
718
|
+
*/
|
|
719
|
+
export declare const defaultDnsResolver: DnsResolver;
|
|
720
|
+
/**
|
|
721
|
+
* Default HTTP client using undici with DNS pinning
|
|
722
|
+
* Returns close function for proper agent lifecycle management
|
|
723
|
+
* P1.3: Supports phase-specific timeouts via undici options
|
|
724
|
+
*/
|
|
725
|
+
export declare const defaultHttpClient: HttpClient;
|
|
726
|
+
/**
|
|
727
|
+
* SSRF-safe raw fetch with proper DNS resolution pinning
|
|
728
|
+
* Returns response stream with close function for proper lifecycle management
|
|
729
|
+
*
|
|
730
|
+
* Use this when you need to process the response stream directly.
|
|
731
|
+
* IMPORTANT: You MUST call the close() function after consuming the response.
|
|
732
|
+
*
|
|
733
|
+
* Security features:
|
|
734
|
+
* - DNS resolution pinning (prevents rebinding attacks)
|
|
735
|
+
* - Private IP blocking (RFC 6890 compliant)
|
|
736
|
+
* - Host header enforcement (prevents SSRF via Host manipulation)
|
|
737
|
+
* - Hop-by-hop header stripping (RFC 7230)
|
|
738
|
+
* - Response size enforcement
|
|
739
|
+
* - Redirect chain validation
|
|
740
|
+
*
|
|
741
|
+
* Returns an evidence object for PEAC-ready receipts and audit trails.
|
|
742
|
+
*/
|
|
743
|
+
export declare function safeFetchRaw(url: string, options?: SafeFetchOptions): Promise<SafeFetchRawResult>;
|
|
744
|
+
/**
|
|
745
|
+
* SSRF-safe JSON fetch with proper DNS resolution pinning and size limits
|
|
746
|
+
*
|
|
747
|
+
* This is the recommended function for fetching JSON APIs (JWKS, OIDC, etc.)
|
|
748
|
+
*/
|
|
749
|
+
export declare function safeFetchJson<T = unknown>(url: string, options?: SafeFetchOptions): Promise<SafeFetchResult<T>>;
|
|
750
|
+
/**
|
|
751
|
+
* SSRF-safe fetch with JSON parsing (legacy API, use safeFetchJson for new code)
|
|
752
|
+
*
|
|
753
|
+
* @deprecated Use safeFetchJson for better type safety and structured errors
|
|
754
|
+
*/
|
|
755
|
+
export declare function safeFetch<T = unknown>(url: string, options?: SafeFetchOptions): Promise<SafeFetchResult<T>>;
|
|
756
|
+
/**
|
|
757
|
+
* SSRF-safe fetch for JWKS endpoints
|
|
758
|
+
*
|
|
759
|
+
* Convenience wrapper around safeFetchJson with JWKS-specific defaults:
|
|
760
|
+
* - Shorter timeout (10 seconds)
|
|
761
|
+
* - No redirects by default
|
|
762
|
+
* - Smaller response size limit (512 KB)
|
|
763
|
+
* - Type-safe JWKS response
|
|
764
|
+
*/
|
|
765
|
+
export declare function safeFetchJWKS(url: string, options?: Omit<SafeFetchOptions, 'maxRedirects'>): Promise<SafeFetchResult<{
|
|
766
|
+
keys: unknown[];
|
|
767
|
+
}>>;
|
|
768
|
+
export { resolveReceiptUrl, verifyReceiptRef } from './receipt-resolver.js';
|
|
769
|
+
export type { ResolveReceiptUrlOptions, ResolveReceiptUrlResult, ResolveReceiptUrlSuccess, ResolveReceiptUrlFailure, } from './receipt-resolver.js';
|
|
770
|
+
export { createReceiptUrlResolver } from './receipt-url-middleware.js';
|
|
771
|
+
export type { ReceiptUrlResolverOptions, RetrievalMetadata, ReceiptUrlMiddleware, } from './receipt-url-middleware.js';
|
|
772
|
+
//# sourceMappingURL=index.d.ts.map
|