@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 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/types.ts","../src/fetch-metadata.ts","../src/origin.ts","../src/method.ts","../src/content-type.ts","../src/mode-detection.ts","../src/context-binding.ts","../src/policy-chain.ts","../src/token-transport.ts"],"sourcesContent":["// @sigil-security/policy — Public API surface\n// Validation policies for request metadata — Fetch Metadata, Origin, context binding\n\n// ============================================================\n// Types\n// ============================================================\n\nexport type {\n RequestMetadata,\n TokenSource,\n PolicyResult,\n PolicyValidator,\n ClientMode,\n RiskTier,\n LegacyBrowserMode,\n FetchMetadataConfig,\n OriginConfig,\n MethodConfig,\n ContentTypeConfig,\n ContextBindingConfig,\n TokenTransportConfig,\n TokenTransportResult,\n} from './types.js'\n\n// ============================================================\n// Constants\n// ============================================================\n\nexport {\n DEFAULT_PROTECTED_METHODS,\n DEFAULT_ALLOWED_CONTENT_TYPES,\n DEFAULT_HEADER_NAME,\n DEFAULT_ONESHOT_HEADER_NAME,\n DEFAULT_JSON_FIELD_NAME,\n DEFAULT_FORM_FIELD_NAME,\n DEFAULT_CONTEXT_GRACE_PERIOD_MS,\n} from './types.js'\n\n// ============================================================\n// Fetch Metadata Policy\n// ============================================================\n\nexport { createFetchMetadataPolicy } from './fetch-metadata.js'\n\n// ============================================================\n// Origin / Referer Policy\n// ============================================================\n\nexport { createOriginPolicy } from './origin.js'\n\n// ============================================================\n// HTTP Method Policy\n// ============================================================\n\nexport { createMethodPolicy, isProtectedMethod } from './method.js'\n\n// ============================================================\n// Content-Type Policy\n// ============================================================\n\nexport { createContentTypePolicy } from './content-type.js'\n\n// ============================================================\n// Browser vs API Mode Detection\n// ============================================================\n\nexport { detectClientMode } from './mode-detection.js'\nexport type { ModeDetectionConfig } from './mode-detection.js'\n\n// ============================================================\n// Context Binding (Risk Tier Model)\n// ============================================================\n\nexport { createContextBindingPolicy, evaluateContextBinding } from './context-binding.js'\nexport type { ContextBindingResult } from './context-binding.js'\n\n// ============================================================\n// Policy Composition\n// ============================================================\n\nexport { createPolicyChain, evaluatePolicyChain } from './policy-chain.js'\nexport type { PolicyChainResult } from './policy-chain.js'\n\n// ============================================================\n// Token Transport\n// ============================================================\n\nexport {\n resolveTokenTransport,\n isValidTokenTransport,\n getTokenHeaderName,\n getTokenJsonFieldName,\n getTokenFormFieldName,\n} from './token-transport.js'\n","// @sigil-security/policy — Types and interfaces\n// Reference: SPECIFICATION.md Sections 5, 6, 8\n\n// ============================================================\n// Token Source (how the token was transported)\n// ============================================================\n\n/**\n * Describes where a CSRF token was found in the request.\n *\n * Transport precedence (strict order per SPECIFICATION.md §8.3):\n * 1. Custom header (X-CSRF-Token)\n * 2. Request body (JSON)\n * 3. Request body (form)\n * 4. None (no token found)\n *\n * Query parameter transport is NEVER allowed.\n */\nexport type TokenSource =\n | { readonly from: 'header'; readonly value: string }\n | { readonly from: 'body-json'; readonly value: string }\n | { readonly from: 'body-form'; readonly value: string }\n | { readonly from: 'none' }\n\n// ============================================================\n// Request Metadata (normalized, framework-agnostic)\n// ============================================================\n\n/**\n * Normalized request metadata extracted from HTTP requests.\n *\n * **CRITICAL:** This is a plain object — NOT a framework-specific Request, req, res,\n * or any HTTP object. The runtime layer is responsible for extracting RequestMetadata\n * from framework objects (Express, Fastify, Hono, etc.).\n *\n * The policy layer NEVER touches raw HTTP objects.\n */\nexport interface RequestMetadata {\n /** HTTP method (uppercase: GET, POST, PUT, PATCH, DELETE, etc.) */\n readonly method: string\n\n /** Origin header value, or null if absent */\n readonly origin: string | null\n\n /** Referer header value, or null if absent */\n readonly referer: string | null\n\n /** Sec-Fetch-Site header: same-origin, same-site, cross-site, none, or null */\n readonly secFetchSite: string | null\n\n /** Sec-Fetch-Mode header: cors, navigate, no-cors, same-origin, websocket, or null */\n readonly secFetchMode: string | null\n\n /** Sec-Fetch-Dest header: document, embed, font, image, script, style, etc., or null */\n readonly secFetchDest: string | null\n\n /** Content-Type header value (without parameters), or null if absent */\n readonly contentType: string | null\n\n /** Describes how the CSRF token was transported */\n readonly tokenSource: TokenSource\n\n /** Optional: explicit client type override header (X-Client-Type) */\n readonly clientType?: string | undefined\n}\n\n// ============================================================\n// Policy Result\n// ============================================================\n\n/**\n * Result of a policy validation check.\n *\n * - `allowed: true` — request passes this policy check\n * - `allowed: false` — request fails with an internal reason (NEVER exposed to client)\n */\nexport type PolicyResult =\n | { readonly allowed: true }\n | { readonly allowed: false; readonly reason: string }\n\n// ============================================================\n// Policy Validator Interface\n// ============================================================\n\n/**\n * A single validation policy that examines request metadata.\n *\n * Policies are composable via `createPolicyChain`.\n * Each policy is a pure function of `RequestMetadata` — no side effects, no I/O.\n */\nexport interface PolicyValidator {\n /** Unique identifier for this policy (for logging/metrics) */\n readonly name: string\n\n /** Validate request metadata against this policy */\n validate(metadata: RequestMetadata): PolicyResult\n}\n\n// ============================================================\n// Configuration Types\n// ============================================================\n\n/** Legacy browser handling mode for Fetch Metadata policy */\nexport type LegacyBrowserMode = 'degraded' | 'strict'\n\n/** Configuration for Fetch Metadata policy */\nexport interface FetchMetadataConfig {\n /** How to handle requests without Fetch Metadata headers (default: 'degraded') */\n readonly legacyBrowserMode?: LegacyBrowserMode | undefined\n}\n\n/** Configuration for Origin policy */\nexport interface OriginConfig {\n /** List of allowed origins (e.g., ['https://example.com', 'https://api.example.com']) */\n readonly allowedOrigins: readonly string[]\n}\n\n/** Configuration for Method policy */\nexport interface MethodConfig {\n /** HTTP methods that require CSRF protection (default: POST, PUT, PATCH, DELETE) */\n readonly protectedMethods?: readonly string[] | undefined\n}\n\n/** Configuration for Content-Type policy */\nexport interface ContentTypeConfig {\n /** Allowed Content-Type values (default: application/json, application/x-www-form-urlencoded, multipart/form-data) */\n readonly allowedContentTypes?: readonly string[] | undefined\n}\n\n// ============================================================\n// Context Binding Types (Risk Tier Model)\n// ============================================================\n\n/** Risk tier for context binding per SPECIFICATION.md §6.2 */\nexport type RiskTier = 'low' | 'medium' | 'high'\n\n/** Configuration for context binding policy */\nexport interface ContextBindingConfig {\n /** Risk tier determining binding strictness */\n readonly tier: RiskTier\n\n /**\n * Grace period in milliseconds for session rotation tolerance.\n * Only applies to 'medium' tier (soft-fail with grace period).\n * Default: 5 minutes (300_000 ms)\n */\n readonly gracePeriodMs?: number | undefined\n}\n\n// ============================================================\n// Token Transport Types\n// ============================================================\n\n/** Configuration for token transport extraction */\nexport interface TokenTransportConfig {\n /** Custom header name (default: 'x-csrf-token') */\n readonly headerName?: string | undefined\n\n /** JSON body field name (default: 'csrf_token') */\n readonly jsonFieldName?: string | undefined\n\n /** Form body field name (default: 'csrf_token') */\n readonly formFieldName?: string | undefined\n}\n\n/** Result of token transport extraction */\nexport type TokenTransportResult =\n | { readonly found: true; readonly source: TokenSource; readonly warnings: readonly string[] }\n | { readonly found: false; readonly reason: string }\n\n// ============================================================\n// Client Mode\n// ============================================================\n\n/** Detected client mode per SPECIFICATION.md §8.2 */\nexport type ClientMode = 'browser' | 'api'\n\n// ============================================================\n// Default Constants\n// ============================================================\n\n/** Default HTTP methods requiring CSRF protection */\nexport const DEFAULT_PROTECTED_METHODS: readonly string[] = ['POST', 'PUT', 'PATCH', 'DELETE']\n\n/** Default allowed Content-Type values */\nexport const DEFAULT_ALLOWED_CONTENT_TYPES: readonly string[] = [\n 'application/json',\n 'application/x-www-form-urlencoded',\n 'multipart/form-data',\n]\n\n/** Default token header name */\nexport const DEFAULT_HEADER_NAME = 'x-csrf-token'\n\n/** Default one-shot token header name */\nexport const DEFAULT_ONESHOT_HEADER_NAME = 'x-csrf-one-shot-token'\n\n/** Default JSON body field name for CSRF token */\nexport const DEFAULT_JSON_FIELD_NAME = 'csrf_token'\n\n/** Default form body field name for CSRF token */\nexport const DEFAULT_FORM_FIELD_NAME = 'csrf_token'\n\n/** Default grace period for medium-tier context binding (5 minutes) */\nexport const DEFAULT_CONTEXT_GRACE_PERIOD_MS = 5 * 60 * 1000\n","// @sigil-security/policy — Fetch Metadata validation\n// Reference: SPECIFICATION.md §5.1, §8.4\n\nimport type {\n FetchMetadataConfig,\n LegacyBrowserMode,\n PolicyResult,\n PolicyValidator,\n RequestMetadata,\n} from './types.js'\n\n/** Valid Sec-Fetch-Site header values */\nconst VALID_FETCH_SITE_VALUES = new Set([\n 'same-origin',\n 'same-site',\n 'cross-site',\n 'none',\n])\n\n/**\n * Creates a Fetch Metadata policy validator.\n *\n * Validates requests using the `Sec-Fetch-Site` header (W3C Fetch Metadata):\n * - `same-origin` → allow\n * - `same-site` → allow (log warning for cross-origin subdomain)\n * - `cross-site` → reject (state-changing request from external origin)\n * - `none` → reject (browser extension or untrusted origin)\n * - Header absent → depends on `legacyBrowserMode`:\n * - `'degraded'` (default) → allow (fallback to Origin + Token validation)\n * - `'strict'` → reject (modern browser required)\n *\n * @param config - Optional configuration for legacy browser handling\n * @returns PolicyValidator for Fetch Metadata\n */\nexport function createFetchMetadataPolicy(config?: FetchMetadataConfig): PolicyValidator {\n const legacyMode: LegacyBrowserMode = config?.legacyBrowserMode ?? 'degraded'\n\n return {\n name: 'fetch-metadata',\n\n validate(metadata: RequestMetadata): PolicyResult {\n const secFetchSite = metadata.secFetchSite\n\n // Header absent → legacy browser or non-browser client\n if (secFetchSite === null || secFetchSite === '') {\n if (legacyMode === 'strict') {\n return {\n allowed: false,\n reason: 'fetch_metadata_missing_strict',\n }\n }\n // Degraded mode: allow, rely on other validation layers (Origin + Token)\n return { allowed: true }\n }\n\n // Normalize to lowercase for consistent comparison\n const normalized = secFetchSite.toLowerCase()\n\n // Unrecognized value → reject\n if (!VALID_FETCH_SITE_VALUES.has(normalized)) {\n return {\n allowed: false,\n reason: `fetch_metadata_invalid_value:${normalized}`,\n }\n }\n\n // same-origin → allow (trusted)\n if (normalized === 'same-origin') {\n return { allowed: true }\n }\n\n // same-site → allow (subdomain, cross-origin but same site)\n // Per SPECIFICATION.md §8.4: Allow but log (cross-origin)\n if (normalized === 'same-site') {\n return { allowed: true }\n }\n\n // cross-site → reject (external origin)\n if (normalized === 'cross-site') {\n return {\n allowed: false,\n reason: 'fetch_metadata_cross_site',\n }\n }\n\n // none → reject (browser extension or untrusted origin)\n // Per SPECIFICATION.md §8.4: Browser extension initiated requests are rejected\n // This is the last valid value in VALID_FETCH_SITE_VALUES, so no else needed\n return {\n allowed: false,\n reason: 'fetch_metadata_none',\n }\n },\n }\n}\n","// @sigil-security/policy — Origin / Referer validation\n// Reference: SPECIFICATION.md §5.2, RFC 6454\n\nimport type { OriginConfig, PolicyResult, PolicyValidator, RequestMetadata } from './types.js'\n\n/**\n * Extracts origin from a Referer URL.\n *\n * Example: \"https://example.com/path/page?q=1\" → \"https://example.com\"\n *\n * Returns null if the Referer is not a valid URL.\n */\nfunction extractOriginFromReferer(referer: string): string | null {\n try {\n const url = new URL(referer)\n return url.origin\n } catch {\n return null\n }\n}\n\n/**\n * Normalizes an origin string by removing trailing slashes and lowering the scheme/host.\n *\n * **Security (L5 fix):** Returns `null` on URL parse failure instead of a\n * fallback string comparison. A malformed origin can never match any entry\n * in `allowedOrigins`, eliminating unintentional string-level matches.\n *\n * @param origin - Origin string (e.g., \"https://Example.COM/\")\n * @returns Normalized origin (e.g., \"https://example.com\"), or null if invalid\n */\nfunction normalizeOrigin(origin: string): string | null {\n try {\n const url = new URL(origin)\n return url.origin\n } catch {\n // Invalid origin — return null so it never matches any allowed origin\n return null\n }\n}\n\n/**\n * Creates an Origin/Referer policy validator.\n *\n * Validates request provenance using Origin and Referer headers:\n * - If Origin header present → strict match against allowed origins\n * - If Origin absent → Referer header fallback (extract origin from URL)\n * - Both absent → reject (no provenance signal)\n *\n * @param config - Configuration with list of allowed origins\n * @returns PolicyValidator for Origin/Referer\n */\nexport function createOriginPolicy(config: OriginConfig): PolicyValidator {\n // Pre-normalize allowed origins — filter out invalid entries (null from parse failure)\n const normalizedAllowed = new Set<string>()\n for (const o of config.allowedOrigins) {\n const normalized = normalizeOrigin(o)\n if (normalized !== null) {\n normalizedAllowed.add(normalized)\n }\n }\n\n return {\n name: 'origin',\n\n validate(metadata: RequestMetadata): PolicyResult {\n const { origin, referer } = metadata\n\n // Try Origin header first\n if (origin !== null && origin !== '') {\n const normalizedOrigin = normalizeOrigin(origin)\n\n // null = malformed origin → automatic mismatch (L5 fix)\n if (normalizedOrigin !== null && normalizedAllowed.has(normalizedOrigin)) {\n return { allowed: true }\n }\n\n return {\n allowed: false,\n reason: `origin_mismatch:${normalizedOrigin ?? origin}`,\n }\n }\n\n // Origin absent → fallback to Referer\n if (referer !== null && referer !== '') {\n const refererOrigin = extractOriginFromReferer(referer)\n\n if (refererOrigin === null) {\n return {\n allowed: false,\n reason: 'origin_referer_invalid',\n }\n }\n\n const normalizedRefererOrigin = normalizeOrigin(refererOrigin)\n\n // null = malformed → automatic mismatch\n if (normalizedRefererOrigin !== null && normalizedAllowed.has(normalizedRefererOrigin)) {\n return { allowed: true }\n }\n\n return {\n allowed: false,\n reason: `origin_referer_mismatch:${normalizedRefererOrigin ?? refererOrigin}`,\n }\n }\n\n // Both absent → reject (no provenance signal)\n return {\n allowed: false,\n reason: 'origin_missing',\n }\n },\n }\n}\n","// @sigil-security/policy — HTTP Method filtering\n// Reference: SPECIFICATION.md §5.4\n\nimport type { MethodConfig, PolicyResult, PolicyValidator, RequestMetadata } from './types.js'\nimport { DEFAULT_PROTECTED_METHODS } from './types.js'\n\n/**\n * Pre-built Set of default protected methods for hot-path lookups.\n * Avoids creating a new Set on every `isProtectedMethod` call.\n */\nconst DEFAULT_PROTECTED_SET = new Set(\n DEFAULT_PROTECTED_METHODS.map((m) => m.toUpperCase()),\n)\n\n/**\n * Creates an HTTP Method policy validator.\n *\n * This policy acts as a **gate**: it determines whether the request's HTTP method\n * requires CSRF protection. Safe methods (GET, HEAD, OPTIONS) are allowed through\n * immediately. Protected methods (POST, PUT, PATCH, DELETE) pass the gate too —\n * the actual token validation is done by the runtime layer.\n *\n * **Usage in policy chains:** This policy never rejects. The runtime layer uses\n * `isProtectedMethod()` to decide whether to run the CSRF validation pipeline\n * at all. This policy is included in the chain for audit/metrics purposes\n * (knowing which policies were evaluated).\n *\n * @param config - Optional configuration with custom protected methods\n * @returns PolicyValidator for HTTP method classification\n */\nexport function createMethodPolicy(config?: MethodConfig): PolicyValidator {\n const _protectedMethods = config?.protectedMethods\n ? new Set(config.protectedMethods.map((m) => m.toUpperCase()))\n : DEFAULT_PROTECTED_SET\n\n return {\n name: 'method',\n\n validate(_metadata: RequestMetadata): PolicyResult {\n // This policy is a classifier, not a gatekeeper.\n // The runtime layer uses isProtectedMethod() to decide whether to\n // run the full validation pipeline. This always allows through.\n return { allowed: true }\n },\n }\n}\n\n/**\n * Checks whether an HTTP method requires CSRF protection.\n *\n * This is the primary utility used by the runtime layer to determine\n * whether to run the full policy chain + token validation for a request.\n *\n * Uses a pre-built Set for default methods to avoid per-call allocation.\n *\n * @param method - HTTP method string\n * @param protectedMethods - Custom protected methods list (default: POST, PUT, PATCH, DELETE)\n * @returns true if the method requires CSRF protection\n */\nexport function isProtectedMethod(\n method: string,\n protectedMethods?: readonly string[],\n): boolean {\n const methods = protectedMethods\n ? new Set(protectedMethods.map((m) => m.toUpperCase()))\n : DEFAULT_PROTECTED_SET\n return methods.has(method.toUpperCase())\n}\n","// @sigil-security/policy — Content-Type restriction\n// Reference: SPECIFICATION.md §5.5\n\nimport type {\n ContentTypeConfig,\n PolicyResult,\n PolicyValidator,\n RequestMetadata,\n} from './types.js'\nimport { DEFAULT_ALLOWED_CONTENT_TYPES, DEFAULT_PROTECTED_METHODS } from './types.js'\n\n/**\n * Extracts the MIME type from a Content-Type header value,\n * stripping any parameters (charset, boundary, etc.).\n *\n * Example: \"application/json; charset=utf-8\" → \"application/json\"\n * Example: \"multipart/form-data; boundary=---\" → \"multipart/form-data\"\n *\n * @param contentType - Raw Content-Type header value\n * @returns Normalized MIME type (lowercase, no parameters)\n */\nfunction extractMimeType(contentType: string): string {\n const semicolonIndex = contentType.indexOf(';')\n const mimeType = semicolonIndex >= 0 ? contentType.slice(0, semicolonIndex) : contentType\n return mimeType.trim().toLowerCase()\n}\n\n/**\n * Creates a Content-Type policy validator.\n *\n * Restricts requests to known-safe Content-Type values:\n * - `application/json` (default)\n * - `application/x-www-form-urlencoded` (default)\n * - `multipart/form-data` (default)\n *\n * **Security (L6 fix):** State-changing methods (POST, PUT, PATCH, DELETE)\n * WITHOUT a Content-Type header are now rejected. Safe methods (GET, HEAD,\n * OPTIONS) without Content-Type are still allowed (no body expected).\n *\n * Content-Type parameters (charset, boundary) are stripped before comparison.\n *\n * Per SPECIFICATION.md §8.3: Content-Type mismatch (e.g., claiming JSON but\n * sending form data) is handled by the runtime layer, not the policy layer.\n *\n * @param config - Optional configuration with custom allowed Content-Types\n * @returns PolicyValidator for Content-Type restriction\n */\nexport function createContentTypePolicy(config?: ContentTypeConfig): PolicyValidator {\n const allowedTypes = new Set(\n (config?.allowedContentTypes ?? DEFAULT_ALLOWED_CONTENT_TYPES).map((t) => t.toLowerCase()),\n )\n const stateChangingMethods = new Set(DEFAULT_PROTECTED_METHODS)\n\n return {\n name: 'content-type',\n\n validate(metadata: RequestMetadata): PolicyResult {\n const { contentType, method } = metadata\n\n // No Content-Type header\n if (contentType === null || contentType === '') {\n // State-changing methods MUST have a Content-Type (L6 fix)\n if (stateChangingMethods.has(method.toUpperCase())) {\n return {\n allowed: false,\n reason: 'content_type_missing_on_state_change',\n }\n }\n // Safe methods (GET, HEAD, OPTIONS) — allow without Content-Type\n return { allowed: true }\n }\n\n // Extract MIME type without parameters\n const mimeType = extractMimeType(contentType)\n\n if (allowedTypes.has(mimeType)) {\n return { allowed: true }\n }\n\n return {\n allowed: false,\n reason: `content_type_disallowed:${mimeType}`,\n }\n },\n }\n}\n","// @sigil-security/policy — Browser vs API Mode Detection\n// Reference: SPECIFICATION.md §8.2\n\nimport type { ClientMode, RequestMetadata } from './types.js'\n\n/**\n * Configuration for client mode detection.\n */\nexport interface ModeDetectionConfig {\n /**\n * When true, the `X-Client-Type: api` header override is disabled.\n * Clients cannot self-declare as API mode to bypass Fetch Metadata and\n * Origin validation. Mode is determined solely by `Sec-Fetch-Site` presence.\n *\n * **Security (M3 fix):** A server with permissive CORS configuration\n * (`Access-Control-Allow-Headers: *`) would allow cross-origin attackers\n * to set `X-Client-Type: api` and bypass browser-specific policies.\n * Set this to `true` if CORS cannot be tightly controlled.\n *\n * Default: `false` (override allowed for backward compatibility)\n */\n readonly disableClientModeOverride?: boolean | undefined\n}\n\n/**\n * Detects client mode (browser vs API) from request metadata.\n *\n * Mode detection logic (per SPECIFICATION.md §8.2):\n *\n * 1. Manual override: `X-Client-Type: api` → Force API Mode (unless disabled)\n * 2. `Sec-Fetch-Site` header present → Browser Mode\n * (modern browsers always send Fetch Metadata headers)\n * 3. `Sec-Fetch-Site` header absent → API Mode\n * (non-browser clients: mobile apps, CLI, services)\n *\n * **Browser Mode:**\n * - Full multi-layer validation: Fetch Metadata + Origin + Token\n * - All policies in the chain are enforced\n *\n * **API Mode:**\n * - Token-only validation (no Fetch Metadata enforcement)\n * - Context binding recommended (API key hash)\n * - Fetch Metadata and Origin policies are relaxed\n *\n * @param metadata - Normalized request metadata\n * @param config - Optional mode detection configuration\n * @returns 'browser' or 'api'\n */\nexport function detectClientMode(\n metadata: RequestMetadata,\n config?: ModeDetectionConfig,\n): ClientMode {\n // Manual override via X-Client-Type header (unless disabled)\n if (\n config?.disableClientModeOverride !== true &&\n metadata.clientType !== undefined &&\n metadata.clientType.toLowerCase() === 'api'\n ) {\n return 'api'\n }\n\n // Sec-Fetch-Site present → Browser Mode\n // Modern browsers (Chrome 76+, Firefox 90+, Edge 79+, Safari 16.4+)\n // always send this header on navigation and subresource requests\n if (metadata.secFetchSite !== null && metadata.secFetchSite !== '') {\n return 'browser'\n }\n\n // No Fetch Metadata → API Mode (non-browser client)\n return 'api'\n}\n","// @sigil-security/policy — Context Binding (Risk Tier Model)\n// Reference: SPECIFICATION.md §6.2, §6.3\n\nimport type {\n ContextBindingConfig,\n PolicyResult,\n PolicyValidator,\n RequestMetadata,\n RiskTier,\n} from './types.js'\nimport { DEFAULT_CONTEXT_GRACE_PERIOD_MS } from './types.js'\n\n/**\n * Result of context binding validation with tier-specific behavior.\n */\nexport interface ContextBindingResult {\n /** Whether the context matches */\n readonly matches: boolean\n\n /** Whether the result should be enforced (fail-closed) or logged (soft-fail) */\n readonly enforced: boolean\n\n /** Whether the request is within the grace period (medium tier only) */\n readonly inGracePeriod: boolean\n\n /** Risk tier that was applied */\n readonly tier: RiskTier\n}\n\n/**\n * Evaluates context binding based on risk tier.\n *\n * Risk Tier Model (per SPECIFICATION.md §6.2):\n *\n * | Tier | Binding | Failure Mode | Use Case |\n * |--------|----------------------|------------------------|----------------|\n * | Low | Optional / soft-fail | Log only | Read endpoints |\n * | Medium | Session ID hash | Log + allow (grace) | Settings |\n * | High | Session+User+Origin | Reject + audit | Transfers |\n *\n * @param contextMatches - Whether the context hash matches\n * @param config - Context binding configuration with risk tier\n * @param sessionAge - Age of the current session in milliseconds (for grace period)\n * @returns ContextBindingResult with tier-specific behavior\n */\nexport function evaluateContextBinding(\n contextMatches: boolean,\n config: ContextBindingConfig,\n sessionAge?: number,\n): ContextBindingResult {\n const { tier } = config\n const gracePeriodMs = config.gracePeriodMs ?? DEFAULT_CONTEXT_GRACE_PERIOD_MS\n\n if (contextMatches) {\n return {\n matches: true,\n enforced: false,\n inGracePeriod: false,\n tier,\n }\n }\n\n // Context does NOT match — behavior depends on tier\n switch (tier) {\n case 'low':\n // Low assurance: soft-fail, log only, allow the request\n return {\n matches: false,\n enforced: false,\n inGracePeriod: false,\n tier,\n }\n\n case 'medium': {\n // Medium assurance: soft-fail with grace period\n // If session was recently rotated, allow within grace period\n const inGrace =\n sessionAge !== undefined && sessionAge >= 0 && sessionAge < gracePeriodMs\n return {\n matches: false,\n enforced: !inGrace, // enforce only if NOT in grace period\n inGracePeriod: inGrace,\n tier,\n }\n }\n\n case 'high':\n // High assurance: fail-closed, no grace period\n return {\n matches: false,\n enforced: true,\n inGracePeriod: false,\n tier,\n }\n }\n}\n\n/**\n * Creates a context binding policy validator.\n *\n * This policy checks whether context binding validation should result in\n * a hard rejection. For low-tier endpoints, context mismatch is logged\n * but allowed. For high-tier, it's a hard reject.\n *\n * **Note:** The actual context hash comparison is performed by `@sigil-security/core`.\n * This policy determines the *enforcement behavior* based on the risk tier.\n *\n * Since the policy layer doesn't have access to token internals, this validator\n * works with pre-computed context match results passed via metadata extensions.\n *\n * @param config - Context binding configuration\n * @returns PolicyValidator for context binding enforcement\n */\nexport function createContextBindingPolicy(_config: ContextBindingConfig): PolicyValidator {\n return {\n name: 'context-binding',\n\n validate(_metadata: RequestMetadata): PolicyResult {\n // Context binding validation is tier-dependent and requires\n // context match information that comes from core token validation.\n //\n // The actual enforcement is done by `evaluateContextBinding()` at\n // the runtime layer after core validation provides the match result.\n //\n // This policy always allows — the runtime layer uses\n // `evaluateContextBinding()` for the actual decision.\n //\n // This exists in the policy chain primarily as a marker/placeholder\n // that context binding is configured for this endpoint.\n return { allowed: true }\n },\n }\n}\n","// @sigil-security/policy — Policy Composition (no short-circuit)\n// Reference: SPECIFICATION.md §5.8 (Deterministic Failure Model)\n\nimport type { PolicyResult, PolicyValidator, RequestMetadata } from './types.js'\n\n/**\n * Result of a policy chain evaluation.\n *\n * Includes all PolicyResult fields plus metadata about which policies\n * were evaluated and which ones failed (for internal logging only).\n */\nexport type PolicyChainResult =\n | {\n readonly allowed: true\n readonly evaluated: readonly string[]\n readonly failures: readonly string[]\n }\n | {\n readonly allowed: false\n readonly reason: string\n readonly evaluated: readonly string[]\n readonly failures: readonly string[]\n }\n\n/**\n * Creates a composite policy validator from multiple individual policies.\n *\n * **CRITICAL:** All policies in the chain are executed regardless of individual\n * results. There is NO short-circuit evaluation. This follows the Deterministic\n * Failure Model from SPECIFICATION.md §5.8:\n *\n * - Every policy runs, even if an earlier one fails\n * - First failure reason is captured (for internal logging)\n * - All failure names are collected (for metrics)\n * - Single exit point, deterministic execution path\n *\n * @param policies - Array of PolicyValidator instances to compose\n * @returns A composite PolicyValidator that runs all policies\n */\nexport function createPolicyChain(policies: readonly PolicyValidator[]): PolicyValidator {\n return {\n name: 'policy-chain',\n\n validate(metadata: RequestMetadata): PolicyResult {\n return evaluatePolicyChain(policies, metadata)\n },\n }\n}\n\n/**\n * Evaluates a chain of policies against request metadata.\n *\n * Returns a detailed result including all evaluated and failed policy names.\n * No short-circuit: ALL policies execute regardless of individual results.\n *\n * **Security (M4 fix):** An empty policy chain fails closed. A configuration\n * bug that produces an empty chain MUST NOT silently approve all requests.\n *\n * @param policies - Array of policies to evaluate\n * @param metadata - Normalized request metadata\n * @returns Detailed chain evaluation result\n */\nexport function evaluatePolicyChain(\n policies: readonly PolicyValidator[],\n metadata: RequestMetadata,\n): PolicyChainResult {\n // Fail closed on empty policy chain — prevent accidental misconfiguration\n // from silently approving all requests\n if (policies.length === 0) {\n return {\n allowed: false,\n reason: 'empty_policy_chain',\n evaluated: [],\n failures: [],\n }\n }\n\n let allAllowed = true\n let firstReason = ''\n const evaluated: string[] = []\n const failures: string[] = []\n\n // Execute ALL policies — no short-circuit (deterministic timing)\n for (const policy of policies) {\n evaluated.push(policy.name)\n const result = policy.validate(metadata)\n\n if (!result.allowed) {\n failures.push(policy.name)\n\n if (allAllowed) {\n // Capture first failure reason (for internal logging)\n firstReason = result.reason\n allAllowed = false\n }\n }\n }\n\n if (allAllowed) {\n return {\n allowed: true,\n evaluated,\n failures,\n }\n }\n\n return {\n allowed: false,\n reason: firstReason,\n evaluated,\n failures,\n }\n}\n","// @sigil-security/policy — Token Transport Precedence\n// Reference: SPECIFICATION.md §8.3\n\nimport type {\n RequestMetadata,\n TokenSource,\n TokenTransportConfig,\n TokenTransportResult,\n} from './types.js'\nimport {\n DEFAULT_FORM_FIELD_NAME,\n DEFAULT_HEADER_NAME,\n DEFAULT_JSON_FIELD_NAME,\n} from './types.js'\n\n/**\n * Resolves token transport from request metadata.\n *\n * Transport precedence (strict order per SPECIFICATION.md §8.3):\n *\n * 1. **Custom Header** (recommended): `X-CSRF-Token`\n * 2. **Request Body** (JSON): `{ \"csrf_token\": \"...\" }`\n * 3. **Request Body** (form): `csrf_token=...`\n * 4. **Query Parameter**: NEVER allowed (deprecated, insecure — reject with warning)\n *\n * Rules:\n * - First valid token found is used\n * - Multiple tokens → first match wins, warning logged\n * - Token source is captured for audit logging\n *\n * @param metadata - Normalized request metadata with token source\n * @param _config - Optional transport configuration\n * @returns TokenTransportResult with found token and any warnings\n */\nexport function resolveTokenTransport(\n metadata: RequestMetadata,\n _config?: TokenTransportConfig,\n): TokenTransportResult {\n const { tokenSource } = metadata\n\n // Token already extracted by the runtime layer and normalized into TokenSource\n if (tokenSource.from === 'none') {\n return {\n found: false,\n reason: 'no_token_present',\n }\n }\n\n // Token found from a valid source\n return {\n found: true,\n source: tokenSource,\n warnings: [],\n }\n}\n\n/**\n * Validates that a token source is acceptable.\n *\n * Verifies the token was transported via an approved channel:\n * - Header: always acceptable\n * - Body (JSON or form): acceptable\n * - Query parameter: NEVER acceptable\n *\n * @param source - The token source to validate\n * @returns true if the transport method is acceptable\n */\nexport function isValidTokenTransport(source: TokenSource): boolean {\n return source.from === 'header' || source.from === 'body-json' || source.from === 'body-form'\n}\n\n/**\n * Returns the expected header name for CSRF tokens.\n *\n * @param config - Optional transport configuration\n * @returns Header name (lowercase)\n */\nexport function getTokenHeaderName(config?: TokenTransportConfig): string {\n return config?.headerName ?? DEFAULT_HEADER_NAME\n}\n\n/**\n * Returns the expected JSON field name for CSRF tokens.\n *\n * @param config - Optional transport configuration\n * @returns JSON field name\n */\nexport function getTokenJsonFieldName(config?: TokenTransportConfig): string {\n return config?.jsonFieldName ?? DEFAULT_JSON_FIELD_NAME\n}\n\n/**\n * Returns the expected form field name for CSRF tokens.\n *\n * @param config - Optional transport configuration\n * @returns Form field name\n */\nexport function getTokenFormFieldName(config?: TokenTransportConfig): string {\n return config?.formFieldName ?? DEFAULT_FORM_FIELD_NAME\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACsLO,IAAM,4BAA+C,CAAC,QAAQ,OAAO,SAAS,QAAQ;AAGtF,IAAM,gCAAmD;AAAA,EAC9D;AAAA,EACA;AAAA,EACA;AACF;AAGO,IAAM,sBAAsB;AAG5B,IAAM,8BAA8B;AAGpC,IAAM,0BAA0B;AAGhC,IAAM,0BAA0B;AAGhC,IAAM,kCAAkC,IAAI,KAAK;;;AChMxD,IAAM,0BAA0B,oBAAI,IAAI;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAiBM,SAAS,0BAA0B,QAA+C;AACvF,QAAM,aAAgC,QAAQ,qBAAqB;AAEnE,SAAO;AAAA,IACL,MAAM;AAAA,IAEN,SAAS,UAAyC;AAChD,YAAM,eAAe,SAAS;AAG9B,UAAI,iBAAiB,QAAQ,iBAAiB,IAAI;AAChD,YAAI,eAAe,UAAU;AAC3B,iBAAO;AAAA,YACL,SAAS;AAAA,YACT,QAAQ;AAAA,UACV;AAAA,QACF;AAEA,eAAO,EAAE,SAAS,KAAK;AAAA,MACzB;AAGA,YAAM,aAAa,aAAa,YAAY;AAG5C,UAAI,CAAC,wBAAwB,IAAI,UAAU,GAAG;AAC5C,eAAO;AAAA,UACL,SAAS;AAAA,UACT,QAAQ,gCAAgC,UAAU;AAAA,QACpD;AAAA,MACF;AAGA,UAAI,eAAe,eAAe;AAChC,eAAO,EAAE,SAAS,KAAK;AAAA,MACzB;AAIA,UAAI,eAAe,aAAa;AAC9B,eAAO,EAAE,SAAS,KAAK;AAAA,MACzB;AAGA,UAAI,eAAe,cAAc;AAC/B,eAAO;AAAA,UACL,SAAS;AAAA,UACT,QAAQ;AAAA,QACV;AAAA,MACF;AAKA,aAAO;AAAA,QACL,SAAS;AAAA,QACT,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF;;;AClFA,SAAS,yBAAyB,SAAgC;AAChE,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,OAAO;AAC3B,WAAO,IAAI;AAAA,EACb,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAYA,SAAS,gBAAgB,QAA+B;AACtD,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,MAAM;AAC1B,WAAO,IAAI;AAAA,EACb,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAaO,SAAS,mBAAmB,QAAuC;AAExE,QAAM,oBAAoB,oBAAI,IAAY;AAC1C,aAAW,KAAK,OAAO,gBAAgB;AACrC,UAAM,aAAa,gBAAgB,CAAC;AACpC,QAAI,eAAe,MAAM;AACvB,wBAAkB,IAAI,UAAU;AAAA,IAClC;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IAEN,SAAS,UAAyC;AAChD,YAAM,EAAE,QAAQ,QAAQ,IAAI;AAG5B,UAAI,WAAW,QAAQ,WAAW,IAAI;AACpC,cAAM,mBAAmB,gBAAgB,MAAM;AAG/C,YAAI,qBAAqB,QAAQ,kBAAkB,IAAI,gBAAgB,GAAG;AACxE,iBAAO,EAAE,SAAS,KAAK;AAAA,QACzB;AAEA,eAAO;AAAA,UACL,SAAS;AAAA,UACT,QAAQ,mBAAmB,oBAAoB,MAAM;AAAA,QACvD;AAAA,MACF;AAGA,UAAI,YAAY,QAAQ,YAAY,IAAI;AACtC,cAAM,gBAAgB,yBAAyB,OAAO;AAEtD,YAAI,kBAAkB,MAAM;AAC1B,iBAAO;AAAA,YACL,SAAS;AAAA,YACT,QAAQ;AAAA,UACV;AAAA,QACF;AAEA,cAAM,0BAA0B,gBAAgB,aAAa;AAG7D,YAAI,4BAA4B,QAAQ,kBAAkB,IAAI,uBAAuB,GAAG;AACtF,iBAAO,EAAE,SAAS,KAAK;AAAA,QACzB;AAEA,eAAO;AAAA,UACL,SAAS;AAAA,UACT,QAAQ,2BAA2B,2BAA2B,aAAa;AAAA,QAC7E;AAAA,MACF;AAGA,aAAO;AAAA,QACL,SAAS;AAAA,QACT,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF;;;ACxGA,IAAM,wBAAwB,IAAI;AAAA,EAChC,0BAA0B,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC;AACtD;AAkBO,SAAS,mBAAmB,QAAwC;AACzE,QAAM,oBAAoB,QAAQ,mBAC9B,IAAI,IAAI,OAAO,iBAAiB,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,IAC3D;AAEJ,SAAO;AAAA,IACL,MAAM;AAAA,IAEN,SAAS,WAA0C;AAIjD,aAAO,EAAE,SAAS,KAAK;AAAA,IACzB;AAAA,EACF;AACF;AAcO,SAAS,kBACd,QACA,kBACS;AACT,QAAM,UAAU,mBACZ,IAAI,IAAI,iBAAiB,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,IACpD;AACJ,SAAO,QAAQ,IAAI,OAAO,YAAY,CAAC;AACzC;;;AC9CA,SAAS,gBAAgB,aAA6B;AACpD,QAAM,iBAAiB,YAAY,QAAQ,GAAG;AAC9C,QAAM,WAAW,kBAAkB,IAAI,YAAY,MAAM,GAAG,cAAc,IAAI;AAC9E,SAAO,SAAS,KAAK,EAAE,YAAY;AACrC;AAsBO,SAAS,wBAAwB,QAA6C;AACnF,QAAM,eAAe,IAAI;AAAA,KACtB,QAAQ,uBAAuB,+BAA+B,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC;AAAA,EAC3F;AACA,QAAM,uBAAuB,IAAI,IAAI,yBAAyB;AAE9D,SAAO;AAAA,IACL,MAAM;AAAA,IAEN,SAAS,UAAyC;AAChD,YAAM,EAAE,aAAa,OAAO,IAAI;AAGhC,UAAI,gBAAgB,QAAQ,gBAAgB,IAAI;AAE9C,YAAI,qBAAqB,IAAI,OAAO,YAAY,CAAC,GAAG;AAClD,iBAAO;AAAA,YACL,SAAS;AAAA,YACT,QAAQ;AAAA,UACV;AAAA,QACF;AAEA,eAAO,EAAE,SAAS,KAAK;AAAA,MACzB;AAGA,YAAM,WAAW,gBAAgB,WAAW;AAE5C,UAAI,aAAa,IAAI,QAAQ,GAAG;AAC9B,eAAO,EAAE,SAAS,KAAK;AAAA,MACzB;AAEA,aAAO;AAAA,QACL,SAAS;AAAA,QACT,QAAQ,2BAA2B,QAAQ;AAAA,MAC7C;AAAA,IACF;AAAA,EACF;AACF;;;ACrCO,SAAS,iBACd,UACA,QACY;AAEZ,MACE,QAAQ,8BAA8B,QACtC,SAAS,eAAe,UACxB,SAAS,WAAW,YAAY,MAAM,OACtC;AACA,WAAO;AAAA,EACT;AAKA,MAAI,SAAS,iBAAiB,QAAQ,SAAS,iBAAiB,IAAI;AAClE,WAAO;AAAA,EACT;AAGA,SAAO;AACT;;;ACzBO,SAAS,uBACd,gBACA,QACA,YACsB;AACtB,QAAM,EAAE,KAAK,IAAI;AACjB,QAAM,gBAAgB,OAAO,iBAAiB;AAE9C,MAAI,gBAAgB;AAClB,WAAO;AAAA,MACL,SAAS;AAAA,MACT,UAAU;AAAA,MACV,eAAe;AAAA,MACf;AAAA,IACF;AAAA,EACF;AAGA,UAAQ,MAAM;AAAA,IACZ,KAAK;AAEH,aAAO;AAAA,QACL,SAAS;AAAA,QACT,UAAU;AAAA,QACV,eAAe;AAAA,QACf;AAAA,MACF;AAAA,IAEF,KAAK,UAAU;AAGb,YAAM,UACJ,eAAe,UAAa,cAAc,KAAK,aAAa;AAC9D,aAAO;AAAA,QACL,SAAS;AAAA,QACT,UAAU,CAAC;AAAA;AAAA,QACX,eAAe;AAAA,QACf;AAAA,MACF;AAAA,IACF;AAAA,IAEA,KAAK;AAEH,aAAO;AAAA,QACL,SAAS;AAAA,QACT,UAAU;AAAA,QACV,eAAe;AAAA,QACf;AAAA,MACF;AAAA,EACJ;AACF;AAkBO,SAAS,2BAA2B,SAAgD;AACzF,SAAO;AAAA,IACL,MAAM;AAAA,IAEN,SAAS,WAA0C;AAYjD,aAAO,EAAE,SAAS,KAAK;AAAA,IACzB;AAAA,EACF;AACF;;;AC7FO,SAAS,kBAAkB,UAAuD;AACvF,SAAO;AAAA,IACL,MAAM;AAAA,IAEN,SAAS,UAAyC;AAChD,aAAO,oBAAoB,UAAU,QAAQ;AAAA,IAC/C;AAAA,EACF;AACF;AAeO,SAAS,oBACd,UACA,UACmB;AAGnB,MAAI,SAAS,WAAW,GAAG;AACzB,WAAO;AAAA,MACL,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,WAAW,CAAC;AAAA,MACZ,UAAU,CAAC;AAAA,IACb;AAAA,EACF;AAEA,MAAI,aAAa;AACjB,MAAI,cAAc;AAClB,QAAM,YAAsB,CAAC;AAC7B,QAAM,WAAqB,CAAC;AAG5B,aAAW,UAAU,UAAU;AAC7B,cAAU,KAAK,OAAO,IAAI;AAC1B,UAAM,SAAS,OAAO,SAAS,QAAQ;AAEvC,QAAI,CAAC,OAAO,SAAS;AACnB,eAAS,KAAK,OAAO,IAAI;AAEzB,UAAI,YAAY;AAEd,sBAAc,OAAO;AACrB,qBAAa;AAAA,MACf;AAAA,IACF;AAAA,EACF;AAEA,MAAI,YAAY;AACd,WAAO;AAAA,MACL,SAAS;AAAA,MACT;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,SAAS;AAAA,IACT,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,EACF;AACF;;;AC9EO,SAAS,sBACd,UACA,SACsB;AACtB,QAAM,EAAE,YAAY,IAAI;AAGxB,MAAI,YAAY,SAAS,QAAQ;AAC/B,WAAO;AAAA,MACL,OAAO;AAAA,MACP,QAAQ;AAAA,IACV;AAAA,EACF;AAGA,SAAO;AAAA,IACL,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,UAAU,CAAC;AAAA,EACb;AACF;AAaO,SAAS,sBAAsB,QAA8B;AAClE,SAAO,OAAO,SAAS,YAAY,OAAO,SAAS,eAAe,OAAO,SAAS;AACpF;AAQO,SAAS,mBAAmB,QAAuC;AACxE,SAAO,QAAQ,cAAc;AAC/B;AAQO,SAAS,sBAAsB,QAAuC;AAC3E,SAAO,QAAQ,iBAAiB;AAClC;AAQO,SAAS,sBAAsB,QAAuC;AAC3E,SAAO,QAAQ,iBAAiB;AAClC;","names":[]}
@@ -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 };