@sigil-security/policy 0.0.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.
@@ -0,0 +1,423 @@
1
+ /**
2
+ * Describes where a CSRF token was found in the request.
3
+ *
4
+ * Transport precedence (strict order per SPECIFICATION.md §8.3):
5
+ * 1. Custom header (X-CSRF-Token)
6
+ * 2. Request body (JSON)
7
+ * 3. Request body (form)
8
+ * 4. None (no token found)
9
+ *
10
+ * Query parameter transport is NEVER allowed.
11
+ */
12
+ type TokenSource = {
13
+ readonly from: 'header';
14
+ readonly value: string;
15
+ } | {
16
+ readonly from: 'body-json';
17
+ readonly value: string;
18
+ } | {
19
+ readonly from: 'body-form';
20
+ readonly value: string;
21
+ } | {
22
+ readonly from: 'none';
23
+ };
24
+ /**
25
+ * Normalized request metadata extracted from HTTP requests.
26
+ *
27
+ * **CRITICAL:** This is a plain object — NOT a framework-specific Request, req, res,
28
+ * or any HTTP object. The runtime layer is responsible for extracting RequestMetadata
29
+ * from framework objects (Express, Fastify, Hono, etc.).
30
+ *
31
+ * The policy layer NEVER touches raw HTTP objects.
32
+ */
33
+ interface RequestMetadata {
34
+ /** HTTP method (uppercase: GET, POST, PUT, PATCH, DELETE, etc.) */
35
+ readonly method: string;
36
+ /** Origin header value, or null if absent */
37
+ readonly origin: string | null;
38
+ /** Referer header value, or null if absent */
39
+ readonly referer: string | null;
40
+ /** Sec-Fetch-Site header: same-origin, same-site, cross-site, none, or null */
41
+ readonly secFetchSite: string | null;
42
+ /** Sec-Fetch-Mode header: cors, navigate, no-cors, same-origin, websocket, or null */
43
+ readonly secFetchMode: string | null;
44
+ /** Sec-Fetch-Dest header: document, embed, font, image, script, style, etc., or null */
45
+ readonly secFetchDest: string | null;
46
+ /** Content-Type header value (without parameters), or null if absent */
47
+ readonly contentType: string | null;
48
+ /** Describes how the CSRF token was transported */
49
+ readonly tokenSource: TokenSource;
50
+ /** Optional: explicit client type override header (X-Client-Type) */
51
+ readonly clientType?: string | undefined;
52
+ }
53
+ /**
54
+ * Result of a policy validation check.
55
+ *
56
+ * - `allowed: true` — request passes this policy check
57
+ * - `allowed: false` — request fails with an internal reason (NEVER exposed to client)
58
+ */
59
+ type PolicyResult = {
60
+ readonly allowed: true;
61
+ } | {
62
+ readonly allowed: false;
63
+ readonly reason: string;
64
+ };
65
+ /**
66
+ * A single validation policy that examines request metadata.
67
+ *
68
+ * Policies are composable via `createPolicyChain`.
69
+ * Each policy is a pure function of `RequestMetadata` — no side effects, no I/O.
70
+ */
71
+ interface PolicyValidator {
72
+ /** Unique identifier for this policy (for logging/metrics) */
73
+ readonly name: string;
74
+ /** Validate request metadata against this policy */
75
+ validate(metadata: RequestMetadata): PolicyResult;
76
+ }
77
+ /** Legacy browser handling mode for Fetch Metadata policy */
78
+ type LegacyBrowserMode = 'degraded' | 'strict';
79
+ /** Configuration for Fetch Metadata policy */
80
+ interface FetchMetadataConfig {
81
+ /** How to handle requests without Fetch Metadata headers (default: 'degraded') */
82
+ readonly legacyBrowserMode?: LegacyBrowserMode | undefined;
83
+ }
84
+ /** Configuration for Origin policy */
85
+ interface OriginConfig {
86
+ /** List of allowed origins (e.g., ['https://example.com', 'https://api.example.com']) */
87
+ readonly allowedOrigins: readonly string[];
88
+ }
89
+ /** Configuration for Method policy */
90
+ interface MethodConfig {
91
+ /** HTTP methods that require CSRF protection (default: POST, PUT, PATCH, DELETE) */
92
+ readonly protectedMethods?: readonly string[] | undefined;
93
+ }
94
+ /** Configuration for Content-Type policy */
95
+ interface ContentTypeConfig {
96
+ /** Allowed Content-Type values (default: application/json, application/x-www-form-urlencoded, multipart/form-data) */
97
+ readonly allowedContentTypes?: readonly string[] | undefined;
98
+ }
99
+ /** Risk tier for context binding per SPECIFICATION.md §6.2 */
100
+ type RiskTier = 'low' | 'medium' | 'high';
101
+ /** Configuration for context binding policy */
102
+ interface ContextBindingConfig {
103
+ /** Risk tier determining binding strictness */
104
+ readonly tier: RiskTier;
105
+ /**
106
+ * Grace period in milliseconds for session rotation tolerance.
107
+ * Only applies to 'medium' tier (soft-fail with grace period).
108
+ * Default: 5 minutes (300_000 ms)
109
+ */
110
+ readonly gracePeriodMs?: number | undefined;
111
+ }
112
+ /** Configuration for token transport extraction */
113
+ interface TokenTransportConfig {
114
+ /** Custom header name (default: 'x-csrf-token') */
115
+ readonly headerName?: string | undefined;
116
+ /** JSON body field name (default: 'csrf_token') */
117
+ readonly jsonFieldName?: string | undefined;
118
+ /** Form body field name (default: 'csrf_token') */
119
+ readonly formFieldName?: string | undefined;
120
+ }
121
+ /** Result of token transport extraction */
122
+ type TokenTransportResult = {
123
+ readonly found: true;
124
+ readonly source: TokenSource;
125
+ readonly warnings: readonly string[];
126
+ } | {
127
+ readonly found: false;
128
+ readonly reason: string;
129
+ };
130
+ /** Detected client mode per SPECIFICATION.md §8.2 */
131
+ type ClientMode = 'browser' | 'api';
132
+ /** Default HTTP methods requiring CSRF protection */
133
+ declare const DEFAULT_PROTECTED_METHODS: readonly string[];
134
+ /** Default allowed Content-Type values */
135
+ declare const DEFAULT_ALLOWED_CONTENT_TYPES: readonly string[];
136
+ /** Default token header name */
137
+ declare const DEFAULT_HEADER_NAME = "x-csrf-token";
138
+ /** Default one-shot token header name */
139
+ declare const DEFAULT_ONESHOT_HEADER_NAME = "x-csrf-one-shot-token";
140
+ /** Default JSON body field name for CSRF token */
141
+ declare const DEFAULT_JSON_FIELD_NAME = "csrf_token";
142
+ /** Default form body field name for CSRF token */
143
+ declare const DEFAULT_FORM_FIELD_NAME = "csrf_token";
144
+ /** Default grace period for medium-tier context binding (5 minutes) */
145
+ declare const DEFAULT_CONTEXT_GRACE_PERIOD_MS: number;
146
+
147
+ /**
148
+ * Creates a Fetch Metadata policy validator.
149
+ *
150
+ * Validates requests using the `Sec-Fetch-Site` header (W3C Fetch Metadata):
151
+ * - `same-origin` → allow
152
+ * - `same-site` → allow (log warning for cross-origin subdomain)
153
+ * - `cross-site` → reject (state-changing request from external origin)
154
+ * - `none` → reject (browser extension or untrusted origin)
155
+ * - Header absent → depends on `legacyBrowserMode`:
156
+ * - `'degraded'` (default) → allow (fallback to Origin + Token validation)
157
+ * - `'strict'` → reject (modern browser required)
158
+ *
159
+ * @param config - Optional configuration for legacy browser handling
160
+ * @returns PolicyValidator for Fetch Metadata
161
+ */
162
+ declare function createFetchMetadataPolicy(config?: FetchMetadataConfig): PolicyValidator;
163
+
164
+ /**
165
+ * Creates an Origin/Referer policy validator.
166
+ *
167
+ * Validates request provenance using Origin and Referer headers:
168
+ * - If Origin header present → strict match against allowed origins
169
+ * - If Origin absent → Referer header fallback (extract origin from URL)
170
+ * - Both absent → reject (no provenance signal)
171
+ *
172
+ * @param config - Configuration with list of allowed origins
173
+ * @returns PolicyValidator for Origin/Referer
174
+ */
175
+ declare function createOriginPolicy(config: OriginConfig): PolicyValidator;
176
+
177
+ /**
178
+ * Creates an HTTP Method policy validator.
179
+ *
180
+ * This policy acts as a **gate**: it determines whether the request's HTTP method
181
+ * requires CSRF protection. Safe methods (GET, HEAD, OPTIONS) are allowed through
182
+ * immediately. Protected methods (POST, PUT, PATCH, DELETE) pass the gate too —
183
+ * the actual token validation is done by the runtime layer.
184
+ *
185
+ * **Usage in policy chains:** This policy never rejects. The runtime layer uses
186
+ * `isProtectedMethod()` to decide whether to run the CSRF validation pipeline
187
+ * at all. This policy is included in the chain for audit/metrics purposes
188
+ * (knowing which policies were evaluated).
189
+ *
190
+ * @param config - Optional configuration with custom protected methods
191
+ * @returns PolicyValidator for HTTP method classification
192
+ */
193
+ declare function createMethodPolicy(config?: MethodConfig): PolicyValidator;
194
+ /**
195
+ * Checks whether an HTTP method requires CSRF protection.
196
+ *
197
+ * This is the primary utility used by the runtime layer to determine
198
+ * whether to run the full policy chain + token validation for a request.
199
+ *
200
+ * Uses a pre-built Set for default methods to avoid per-call allocation.
201
+ *
202
+ * @param method - HTTP method string
203
+ * @param protectedMethods - Custom protected methods list (default: POST, PUT, PATCH, DELETE)
204
+ * @returns true if the method requires CSRF protection
205
+ */
206
+ declare function isProtectedMethod(method: string, protectedMethods?: readonly string[]): boolean;
207
+
208
+ /**
209
+ * Creates a Content-Type policy validator.
210
+ *
211
+ * Restricts requests to known-safe Content-Type values:
212
+ * - `application/json` (default)
213
+ * - `application/x-www-form-urlencoded` (default)
214
+ * - `multipart/form-data` (default)
215
+ *
216
+ * **Security (L6 fix):** State-changing methods (POST, PUT, PATCH, DELETE)
217
+ * WITHOUT a Content-Type header are now rejected. Safe methods (GET, HEAD,
218
+ * OPTIONS) without Content-Type are still allowed (no body expected).
219
+ *
220
+ * Content-Type parameters (charset, boundary) are stripped before comparison.
221
+ *
222
+ * Per SPECIFICATION.md §8.3: Content-Type mismatch (e.g., claiming JSON but
223
+ * sending form data) is handled by the runtime layer, not the policy layer.
224
+ *
225
+ * @param config - Optional configuration with custom allowed Content-Types
226
+ * @returns PolicyValidator for Content-Type restriction
227
+ */
228
+ declare function createContentTypePolicy(config?: ContentTypeConfig): PolicyValidator;
229
+
230
+ /**
231
+ * Configuration for client mode detection.
232
+ */
233
+ interface ModeDetectionConfig {
234
+ /**
235
+ * When true, the `X-Client-Type: api` header override is disabled.
236
+ * Clients cannot self-declare as API mode to bypass Fetch Metadata and
237
+ * Origin validation. Mode is determined solely by `Sec-Fetch-Site` presence.
238
+ *
239
+ * **Security (M3 fix):** A server with permissive CORS configuration
240
+ * (`Access-Control-Allow-Headers: *`) would allow cross-origin attackers
241
+ * to set `X-Client-Type: api` and bypass browser-specific policies.
242
+ * Set this to `true` if CORS cannot be tightly controlled.
243
+ *
244
+ * Default: `false` (override allowed for backward compatibility)
245
+ */
246
+ readonly disableClientModeOverride?: boolean | undefined;
247
+ }
248
+ /**
249
+ * Detects client mode (browser vs API) from request metadata.
250
+ *
251
+ * Mode detection logic (per SPECIFICATION.md §8.2):
252
+ *
253
+ * 1. Manual override: `X-Client-Type: api` → Force API Mode (unless disabled)
254
+ * 2. `Sec-Fetch-Site` header present → Browser Mode
255
+ * (modern browsers always send Fetch Metadata headers)
256
+ * 3. `Sec-Fetch-Site` header absent → API Mode
257
+ * (non-browser clients: mobile apps, CLI, services)
258
+ *
259
+ * **Browser Mode:**
260
+ * - Full multi-layer validation: Fetch Metadata + Origin + Token
261
+ * - All policies in the chain are enforced
262
+ *
263
+ * **API Mode:**
264
+ * - Token-only validation (no Fetch Metadata enforcement)
265
+ * - Context binding recommended (API key hash)
266
+ * - Fetch Metadata and Origin policies are relaxed
267
+ *
268
+ * @param metadata - Normalized request metadata
269
+ * @param config - Optional mode detection configuration
270
+ * @returns 'browser' or 'api'
271
+ */
272
+ declare function detectClientMode(metadata: RequestMetadata, config?: ModeDetectionConfig): ClientMode;
273
+
274
+ /**
275
+ * Result of context binding validation with tier-specific behavior.
276
+ */
277
+ interface ContextBindingResult {
278
+ /** Whether the context matches */
279
+ readonly matches: boolean;
280
+ /** Whether the result should be enforced (fail-closed) or logged (soft-fail) */
281
+ readonly enforced: boolean;
282
+ /** Whether the request is within the grace period (medium tier only) */
283
+ readonly inGracePeriod: boolean;
284
+ /** Risk tier that was applied */
285
+ readonly tier: RiskTier;
286
+ }
287
+ /**
288
+ * Evaluates context binding based on risk tier.
289
+ *
290
+ * Risk Tier Model (per SPECIFICATION.md §6.2):
291
+ *
292
+ * | Tier | Binding | Failure Mode | Use Case |
293
+ * |--------|----------------------|------------------------|----------------|
294
+ * | Low | Optional / soft-fail | Log only | Read endpoints |
295
+ * | Medium | Session ID hash | Log + allow (grace) | Settings |
296
+ * | High | Session+User+Origin | Reject + audit | Transfers |
297
+ *
298
+ * @param contextMatches - Whether the context hash matches
299
+ * @param config - Context binding configuration with risk tier
300
+ * @param sessionAge - Age of the current session in milliseconds (for grace period)
301
+ * @returns ContextBindingResult with tier-specific behavior
302
+ */
303
+ declare function evaluateContextBinding(contextMatches: boolean, config: ContextBindingConfig, sessionAge?: number): ContextBindingResult;
304
+ /**
305
+ * Creates a context binding policy validator.
306
+ *
307
+ * This policy checks whether context binding validation should result in
308
+ * a hard rejection. For low-tier endpoints, context mismatch is logged
309
+ * but allowed. For high-tier, it's a hard reject.
310
+ *
311
+ * **Note:** The actual context hash comparison is performed by `@sigil-security/core`.
312
+ * This policy determines the *enforcement behavior* based on the risk tier.
313
+ *
314
+ * Since the policy layer doesn't have access to token internals, this validator
315
+ * works with pre-computed context match results passed via metadata extensions.
316
+ *
317
+ * @param config - Context binding configuration
318
+ * @returns PolicyValidator for context binding enforcement
319
+ */
320
+ declare function createContextBindingPolicy(_config: ContextBindingConfig): PolicyValidator;
321
+
322
+ /**
323
+ * Result of a policy chain evaluation.
324
+ *
325
+ * Includes all PolicyResult fields plus metadata about which policies
326
+ * were evaluated and which ones failed (for internal logging only).
327
+ */
328
+ type PolicyChainResult = {
329
+ readonly allowed: true;
330
+ readonly evaluated: readonly string[];
331
+ readonly failures: readonly string[];
332
+ } | {
333
+ readonly allowed: false;
334
+ readonly reason: string;
335
+ readonly evaluated: readonly string[];
336
+ readonly failures: readonly string[];
337
+ };
338
+ /**
339
+ * Creates a composite policy validator from multiple individual policies.
340
+ *
341
+ * **CRITICAL:** All policies in the chain are executed regardless of individual
342
+ * results. There is NO short-circuit evaluation. This follows the Deterministic
343
+ * Failure Model from SPECIFICATION.md §5.8:
344
+ *
345
+ * - Every policy runs, even if an earlier one fails
346
+ * - First failure reason is captured (for internal logging)
347
+ * - All failure names are collected (for metrics)
348
+ * - Single exit point, deterministic execution path
349
+ *
350
+ * @param policies - Array of PolicyValidator instances to compose
351
+ * @returns A composite PolicyValidator that runs all policies
352
+ */
353
+ declare function createPolicyChain(policies: readonly PolicyValidator[]): PolicyValidator;
354
+ /**
355
+ * Evaluates a chain of policies against request metadata.
356
+ *
357
+ * Returns a detailed result including all evaluated and failed policy names.
358
+ * No short-circuit: ALL policies execute regardless of individual results.
359
+ *
360
+ * **Security (M4 fix):** An empty policy chain fails closed. A configuration
361
+ * bug that produces an empty chain MUST NOT silently approve all requests.
362
+ *
363
+ * @param policies - Array of policies to evaluate
364
+ * @param metadata - Normalized request metadata
365
+ * @returns Detailed chain evaluation result
366
+ */
367
+ declare function evaluatePolicyChain(policies: readonly PolicyValidator[], metadata: RequestMetadata): PolicyChainResult;
368
+
369
+ /**
370
+ * Resolves token transport from request metadata.
371
+ *
372
+ * Transport precedence (strict order per SPECIFICATION.md §8.3):
373
+ *
374
+ * 1. **Custom Header** (recommended): `X-CSRF-Token`
375
+ * 2. **Request Body** (JSON): `{ "csrf_token": "..." }`
376
+ * 3. **Request Body** (form): `csrf_token=...`
377
+ * 4. **Query Parameter**: NEVER allowed (deprecated, insecure — reject with warning)
378
+ *
379
+ * Rules:
380
+ * - First valid token found is used
381
+ * - Multiple tokens → first match wins, warning logged
382
+ * - Token source is captured for audit logging
383
+ *
384
+ * @param metadata - Normalized request metadata with token source
385
+ * @param _config - Optional transport configuration
386
+ * @returns TokenTransportResult with found token and any warnings
387
+ */
388
+ declare function resolveTokenTransport(metadata: RequestMetadata, _config?: TokenTransportConfig): TokenTransportResult;
389
+ /**
390
+ * Validates that a token source is acceptable.
391
+ *
392
+ * Verifies the token was transported via an approved channel:
393
+ * - Header: always acceptable
394
+ * - Body (JSON or form): acceptable
395
+ * - Query parameter: NEVER acceptable
396
+ *
397
+ * @param source - The token source to validate
398
+ * @returns true if the transport method is acceptable
399
+ */
400
+ declare function isValidTokenTransport(source: TokenSource): boolean;
401
+ /**
402
+ * Returns the expected header name for CSRF tokens.
403
+ *
404
+ * @param config - Optional transport configuration
405
+ * @returns Header name (lowercase)
406
+ */
407
+ declare function getTokenHeaderName(config?: TokenTransportConfig): string;
408
+ /**
409
+ * Returns the expected JSON field name for CSRF tokens.
410
+ *
411
+ * @param config - Optional transport configuration
412
+ * @returns JSON field name
413
+ */
414
+ declare function getTokenJsonFieldName(config?: TokenTransportConfig): string;
415
+ /**
416
+ * Returns the expected form field name for CSRF tokens.
417
+ *
418
+ * @param config - Optional transport configuration
419
+ * @returns Form field name
420
+ */
421
+ declare function getTokenFormFieldName(config?: TokenTransportConfig): string;
422
+
423
+ export { type ClientMode, type ContentTypeConfig, type ContextBindingConfig, type ContextBindingResult, DEFAULT_ALLOWED_CONTENT_TYPES, DEFAULT_CONTEXT_GRACE_PERIOD_MS, DEFAULT_FORM_FIELD_NAME, DEFAULT_HEADER_NAME, DEFAULT_JSON_FIELD_NAME, DEFAULT_ONESHOT_HEADER_NAME, DEFAULT_PROTECTED_METHODS, type FetchMetadataConfig, type LegacyBrowserMode, type MethodConfig, type ModeDetectionConfig, type OriginConfig, type PolicyChainResult, type PolicyResult, type PolicyValidator, type RequestMetadata, type RiskTier, type TokenSource, type TokenTransportConfig, type TokenTransportResult, createContentTypePolicy, createContextBindingPolicy, createFetchMetadataPolicy, createMethodPolicy, createOriginPolicy, createPolicyChain, detectClientMode, evaluateContextBinding, evaluatePolicyChain, getTokenFormFieldName, getTokenHeaderName, getTokenJsonFieldName, isProtectedMethod, isValidTokenTransport, resolveTokenTransport };