@sigil-security/runtime 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.
- package/LICENSE +201 -0
- package/dist/adapters/elysia.cjs +307 -0
- package/dist/adapters/elysia.cjs.map +1 -0
- package/dist/adapters/elysia.d.cts +41 -0
- package/dist/adapters/elysia.d.ts +41 -0
- package/dist/adapters/elysia.js +98 -0
- package/dist/adapters/elysia.js.map +1 -0
- package/dist/adapters/express.cjs +286 -0
- package/dist/adapters/express.cjs.map +1 -0
- package/dist/adapters/express.d.cts +59 -0
- package/dist/adapters/express.d.ts +59 -0
- package/dist/adapters/express.js +77 -0
- package/dist/adapters/express.js.map +1 -0
- package/dist/adapters/fastify.cjs +308 -0
- package/dist/adapters/fastify.cjs.map +1 -0
- package/dist/adapters/fastify.d.cts +54 -0
- package/dist/adapters/fastify.d.ts +54 -0
- package/dist/adapters/fastify.js +99 -0
- package/dist/adapters/fastify.js.map +1 -0
- package/dist/adapters/fetch.cjs +359 -0
- package/dist/adapters/fetch.cjs.map +1 -0
- package/dist/adapters/fetch.d.cts +46 -0
- package/dist/adapters/fetch.d.ts +46 -0
- package/dist/adapters/fetch.js +149 -0
- package/dist/adapters/fetch.js.map +1 -0
- package/dist/adapters/hono.cjs +300 -0
- package/dist/adapters/hono.cjs.map +1 -0
- package/dist/adapters/hono.d.cts +41 -0
- package/dist/adapters/hono.d.ts +41 -0
- package/dist/adapters/hono.js +91 -0
- package/dist/adapters/hono.js.map +1 -0
- package/dist/adapters/oak.cjs +318 -0
- package/dist/adapters/oak.cjs.map +1 -0
- package/dist/adapters/oak.d.cts +48 -0
- package/dist/adapters/oak.d.ts +48 -0
- package/dist/adapters/oak.js +109 -0
- package/dist/adapters/oak.js.map +1 -0
- package/dist/chunk-JPT5I5W5.js +225 -0
- package/dist/chunk-JPT5I5W5.js.map +1 -0
- package/dist/index.cjs +486 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +201 -0
- package/dist/index.d.ts +201 -0
- package/dist/index.js +284 -0
- package/dist/index.js.map +1 -0
- package/dist/types-DySgT8rA.d.cts +184 -0
- package/dist/types-DySgT8rA.d.ts +184 -0
- package/package.json +141 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { S as SigilConfig, a as SigilInstance, T as TokenEndpointResult } from './types-DySgT8rA.cjs';
|
|
2
|
+
export { D as DEFAULT_ONESHOT_ENDPOINT_PATH, b as DEFAULT_TOKEN_ENDPOINT_PATH, E as ErrorResponseBody, M as MetadataExtractor, c as MiddlewareOptions, O as OneShotTokenRequestBody, P as ProtectResult, R as ResolvedSigilConfig, d as TokenEndpointRequest, e as TokenGenerationResponse, f as TokenValidationResponse } from './types-DySgT8rA.cjs';
|
|
3
|
+
import { TokenSource, RequestMetadata } from '@sigil-security/policy';
|
|
4
|
+
import '@sigil-security/core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a Sigil runtime instance.
|
|
8
|
+
*
|
|
9
|
+
* This is the main entry point for Sigil. It initializes keyrings,
|
|
10
|
+
* sets up policy chains, and returns an orchestration instance
|
|
11
|
+
* that adapters use for token generation, validation, and request protection.
|
|
12
|
+
*
|
|
13
|
+
* @param config - Sigil configuration
|
|
14
|
+
* @returns Initialized SigilInstance
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const sigil = await createSigil({
|
|
19
|
+
* masterSecret: process.env.CSRF_SECRET!,
|
|
20
|
+
* allowedOrigins: ['https://example.com'],
|
|
21
|
+
* })
|
|
22
|
+
*
|
|
23
|
+
* // Generate a token
|
|
24
|
+
* const result = await sigil.generateToken()
|
|
25
|
+
*
|
|
26
|
+
* // Protect a request
|
|
27
|
+
* const protection = await sigil.protect(metadata)
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
declare function createSigil(config: SigilConfig): Promise<SigilInstance>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Framework-agnostic error response structure.
|
|
34
|
+
*
|
|
35
|
+
* Used by all adapters to produce consistent 403 responses.
|
|
36
|
+
*/
|
|
37
|
+
interface ErrorResponse {
|
|
38
|
+
readonly status: number;
|
|
39
|
+
readonly body: {
|
|
40
|
+
readonly error: string;
|
|
41
|
+
};
|
|
42
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Creates a uniform 403 error response.
|
|
46
|
+
*
|
|
47
|
+
* - Always returns `403 { error: "CSRF validation failed" }`
|
|
48
|
+
* - If the token is expired, adds `X-CSRF-Token-Expired: true` header
|
|
49
|
+
* (allows client-side silent refresh without exposing failure reason)
|
|
50
|
+
*
|
|
51
|
+
* @param expired - Whether the failure is due to token expiry
|
|
52
|
+
* @returns Framework-agnostic error response
|
|
53
|
+
*/
|
|
54
|
+
declare function createErrorResponse(expired: boolean): ErrorResponse;
|
|
55
|
+
/**
|
|
56
|
+
* Creates a framework-agnostic success response for token generation.
|
|
57
|
+
*
|
|
58
|
+
* @param token - Generated token string
|
|
59
|
+
* @param expiresAt - Token expiration timestamp (milliseconds)
|
|
60
|
+
*/
|
|
61
|
+
declare function createTokenResponse(token: string, expiresAt: number): {
|
|
62
|
+
readonly status: number;
|
|
63
|
+
readonly body: {
|
|
64
|
+
readonly token: string;
|
|
65
|
+
readonly expiresAt: number;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Creates a framework-agnostic success response for one-shot token generation.
|
|
70
|
+
*
|
|
71
|
+
* @param token - Generated one-shot token string
|
|
72
|
+
* @param expiresAt - Token expiration timestamp (milliseconds)
|
|
73
|
+
* @param action - The action the token is bound to
|
|
74
|
+
*/
|
|
75
|
+
declare function createOneShotTokenResponse(token: string, expiresAt: number, action: string): {
|
|
76
|
+
readonly status: number;
|
|
77
|
+
readonly body: {
|
|
78
|
+
readonly token: string;
|
|
79
|
+
readonly expiresAt: number;
|
|
80
|
+
readonly action: string;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Normalizes a URL path for consistent comparison.
|
|
86
|
+
*
|
|
87
|
+
* **Security (L3 fix):** Strips trailing slashes to prevent
|
|
88
|
+
* protection bypass via `/health/` vs `/health` mismatch.
|
|
89
|
+
* Does NOT lowercase (paths are case-sensitive per RFC 3986).
|
|
90
|
+
*
|
|
91
|
+
* @param path - URL path to normalize
|
|
92
|
+
* @returns Normalized path (no trailing slash, except for root "/")
|
|
93
|
+
*/
|
|
94
|
+
declare function normalizePath(path: string): string;
|
|
95
|
+
/**
|
|
96
|
+
* Creates a normalized Set from an array of paths for consistent matching.
|
|
97
|
+
*
|
|
98
|
+
* @param paths - Array of paths to normalize
|
|
99
|
+
* @returns Set of normalized paths
|
|
100
|
+
*/
|
|
101
|
+
declare function normalizePathSet(paths: readonly string[]): Set<string>;
|
|
102
|
+
/**
|
|
103
|
+
* Generic header getter function.
|
|
104
|
+
* Adapters implement this to bridge framework-specific header access.
|
|
105
|
+
*/
|
|
106
|
+
type HeaderGetter = (name: string) => string | null;
|
|
107
|
+
/**
|
|
108
|
+
* Assembles normalized `RequestMetadata` from generic request components.
|
|
109
|
+
*
|
|
110
|
+
* This is the single point where framework-specific HTTP objects
|
|
111
|
+
* are transformed into the policy layer's input format.
|
|
112
|
+
*
|
|
113
|
+
* @param method - HTTP method (will be uppercased)
|
|
114
|
+
* @param getHeader - Framework-specific header getter
|
|
115
|
+
* @param tokenSource - Pre-resolved token source
|
|
116
|
+
* @returns Normalized RequestMetadata for the policy layer
|
|
117
|
+
*/
|
|
118
|
+
declare function extractRequestMetadata(method: string, getHeader: HeaderGetter, tokenSource: TokenSource): RequestMetadata;
|
|
119
|
+
/**
|
|
120
|
+
* Parses Content-Type header, stripping parameters (charset, boundary, etc.).
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* parseContentType("application/json; charset=utf-8") → "application/json"
|
|
124
|
+
* parseContentType(null) → null
|
|
125
|
+
*/
|
|
126
|
+
declare function parseContentType(contentType: string | null): string | null;
|
|
127
|
+
/**
|
|
128
|
+
* Extracts CSRF token from a custom header.
|
|
129
|
+
*
|
|
130
|
+
* @param getHeader - Header getter function
|
|
131
|
+
* @param headerName - Header name to check (default: 'x-csrf-token')
|
|
132
|
+
* @returns TokenSource from header, or { from: 'none' }
|
|
133
|
+
*/
|
|
134
|
+
declare function extractTokenFromHeader(getHeader: HeaderGetter, headerName?: string): TokenSource;
|
|
135
|
+
/**
|
|
136
|
+
* Extracts CSRF token from a parsed JSON body.
|
|
137
|
+
*
|
|
138
|
+
* @param body - Parsed request body (or null/undefined)
|
|
139
|
+
* @param fieldName - JSON field name (default: 'csrf_token')
|
|
140
|
+
* @returns TokenSource if found, or null
|
|
141
|
+
*/
|
|
142
|
+
declare function extractTokenFromJsonBody(body: Record<string, unknown> | null | undefined, fieldName?: string): TokenSource | null;
|
|
143
|
+
/**
|
|
144
|
+
* Extracts CSRF token from a parsed form body.
|
|
145
|
+
*
|
|
146
|
+
* @param body - Parsed form body (or null/undefined)
|
|
147
|
+
* @param fieldName - Form field name (default: 'csrf_token')
|
|
148
|
+
* @returns TokenSource if found, or null
|
|
149
|
+
*/
|
|
150
|
+
declare function extractTokenFromFormBody(body: Record<string, unknown> | null | undefined, fieldName?: string): TokenSource | null;
|
|
151
|
+
/**
|
|
152
|
+
* Resolves token source following the transport precedence from SPECIFICATION.md §8.3:
|
|
153
|
+
*
|
|
154
|
+
* 1. Custom header (highest priority): `X-CSRF-Token`
|
|
155
|
+
* 2. Request body (JSON): `{ "csrf_token": "..." }`
|
|
156
|
+
* 3. Request body (form): `csrf_token=...`
|
|
157
|
+
* 4. Query parameter: NEVER (not supported)
|
|
158
|
+
*
|
|
159
|
+
* First valid token wins. Multiple tokens → first match wins.
|
|
160
|
+
*
|
|
161
|
+
* @param getHeader - Header getter function
|
|
162
|
+
* @param body - Parsed request body (JSON or form-encoded)
|
|
163
|
+
* @param contentType - Parsed Content-Type MIME (lowercase, no params)
|
|
164
|
+
* @param headerName - Custom header name override
|
|
165
|
+
* @param jsonFieldName - Custom JSON field name override
|
|
166
|
+
* @param formFieldName - Custom form field name override
|
|
167
|
+
* @returns Resolved TokenSource
|
|
168
|
+
*/
|
|
169
|
+
declare function resolveTokenSource(getHeader: HeaderGetter, body: Record<string, unknown> | null | undefined, contentType: string | null, headerName?: string, jsonFieldName?: string, formFieldName?: string): TokenSource;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Handles token generation requests.
|
|
173
|
+
*
|
|
174
|
+
* This is a framework-agnostic handler that processes token endpoint requests.
|
|
175
|
+
* Each adapter calls this and maps the result to framework-specific responses.
|
|
176
|
+
*
|
|
177
|
+
* Supported endpoints:
|
|
178
|
+
* - `GET {tokenEndpointPath}` → Generate a regular CSRF token
|
|
179
|
+
* - `POST {oneShotEndpointPath}` → Generate a one-shot token (requires action binding)
|
|
180
|
+
*
|
|
181
|
+
* **Security (M2 fix):** The one-shot endpoint (POST) requires a valid regular
|
|
182
|
+
* CSRF token in the request header. This prevents cross-origin one-shot token
|
|
183
|
+
* generation and nonce cache exhaustion attacks.
|
|
184
|
+
*
|
|
185
|
+
* @param sigil - The Sigil instance
|
|
186
|
+
* @param method - HTTP method (uppercase)
|
|
187
|
+
* @param path - Request path
|
|
188
|
+
* @param body - Parsed request body (for POST endpoints)
|
|
189
|
+
* @param tokenEndpointPath - Token generation endpoint path
|
|
190
|
+
* @param oneShotEndpointPath - One-shot token endpoint path
|
|
191
|
+
* @param csrfTokenValue - CSRF token from request header (required for POST one-shot endpoint)
|
|
192
|
+
* @returns TokenEndpointResult if the request was handled, or null if not a token endpoint
|
|
193
|
+
*/
|
|
194
|
+
declare function handleTokenEndpoint(sigil: SigilInstance, method: string, path: string, body: Record<string, unknown> | null | undefined, tokenEndpointPath: string, oneShotEndpointPath: string, csrfTokenValue?: string | null): Promise<TokenEndpointResult | null>;
|
|
195
|
+
/**
|
|
196
|
+
* Creates a standardized error result for the token endpoint.
|
|
197
|
+
* Used by adapters when they need to produce error responses.
|
|
198
|
+
*/
|
|
199
|
+
declare function createTokenEndpointError(expired: boolean): TokenEndpointResult;
|
|
200
|
+
|
|
201
|
+
export { type ErrorResponse, type HeaderGetter, SigilConfig, SigilInstance, TokenEndpointResult, createErrorResponse, createOneShotTokenResponse, createSigil, createTokenEndpointError, createTokenResponse, extractRequestMetadata, extractTokenFromFormBody, extractTokenFromHeader, extractTokenFromJsonBody, handleTokenEndpoint, normalizePath, normalizePathSet, parseContentType, resolveTokenSource };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { S as SigilConfig, a as SigilInstance, T as TokenEndpointResult } from './types-DySgT8rA.js';
|
|
2
|
+
export { D as DEFAULT_ONESHOT_ENDPOINT_PATH, b as DEFAULT_TOKEN_ENDPOINT_PATH, E as ErrorResponseBody, M as MetadataExtractor, c as MiddlewareOptions, O as OneShotTokenRequestBody, P as ProtectResult, R as ResolvedSigilConfig, d as TokenEndpointRequest, e as TokenGenerationResponse, f as TokenValidationResponse } from './types-DySgT8rA.js';
|
|
3
|
+
import { TokenSource, RequestMetadata } from '@sigil-security/policy';
|
|
4
|
+
import '@sigil-security/core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a Sigil runtime instance.
|
|
8
|
+
*
|
|
9
|
+
* This is the main entry point for Sigil. It initializes keyrings,
|
|
10
|
+
* sets up policy chains, and returns an orchestration instance
|
|
11
|
+
* that adapters use for token generation, validation, and request protection.
|
|
12
|
+
*
|
|
13
|
+
* @param config - Sigil configuration
|
|
14
|
+
* @returns Initialized SigilInstance
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const sigil = await createSigil({
|
|
19
|
+
* masterSecret: process.env.CSRF_SECRET!,
|
|
20
|
+
* allowedOrigins: ['https://example.com'],
|
|
21
|
+
* })
|
|
22
|
+
*
|
|
23
|
+
* // Generate a token
|
|
24
|
+
* const result = await sigil.generateToken()
|
|
25
|
+
*
|
|
26
|
+
* // Protect a request
|
|
27
|
+
* const protection = await sigil.protect(metadata)
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
declare function createSigil(config: SigilConfig): Promise<SigilInstance>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Framework-agnostic error response structure.
|
|
34
|
+
*
|
|
35
|
+
* Used by all adapters to produce consistent 403 responses.
|
|
36
|
+
*/
|
|
37
|
+
interface ErrorResponse {
|
|
38
|
+
readonly status: number;
|
|
39
|
+
readonly body: {
|
|
40
|
+
readonly error: string;
|
|
41
|
+
};
|
|
42
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Creates a uniform 403 error response.
|
|
46
|
+
*
|
|
47
|
+
* - Always returns `403 { error: "CSRF validation failed" }`
|
|
48
|
+
* - If the token is expired, adds `X-CSRF-Token-Expired: true` header
|
|
49
|
+
* (allows client-side silent refresh without exposing failure reason)
|
|
50
|
+
*
|
|
51
|
+
* @param expired - Whether the failure is due to token expiry
|
|
52
|
+
* @returns Framework-agnostic error response
|
|
53
|
+
*/
|
|
54
|
+
declare function createErrorResponse(expired: boolean): ErrorResponse;
|
|
55
|
+
/**
|
|
56
|
+
* Creates a framework-agnostic success response for token generation.
|
|
57
|
+
*
|
|
58
|
+
* @param token - Generated token string
|
|
59
|
+
* @param expiresAt - Token expiration timestamp (milliseconds)
|
|
60
|
+
*/
|
|
61
|
+
declare function createTokenResponse(token: string, expiresAt: number): {
|
|
62
|
+
readonly status: number;
|
|
63
|
+
readonly body: {
|
|
64
|
+
readonly token: string;
|
|
65
|
+
readonly expiresAt: number;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Creates a framework-agnostic success response for one-shot token generation.
|
|
70
|
+
*
|
|
71
|
+
* @param token - Generated one-shot token string
|
|
72
|
+
* @param expiresAt - Token expiration timestamp (milliseconds)
|
|
73
|
+
* @param action - The action the token is bound to
|
|
74
|
+
*/
|
|
75
|
+
declare function createOneShotTokenResponse(token: string, expiresAt: number, action: string): {
|
|
76
|
+
readonly status: number;
|
|
77
|
+
readonly body: {
|
|
78
|
+
readonly token: string;
|
|
79
|
+
readonly expiresAt: number;
|
|
80
|
+
readonly action: string;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Normalizes a URL path for consistent comparison.
|
|
86
|
+
*
|
|
87
|
+
* **Security (L3 fix):** Strips trailing slashes to prevent
|
|
88
|
+
* protection bypass via `/health/` vs `/health` mismatch.
|
|
89
|
+
* Does NOT lowercase (paths are case-sensitive per RFC 3986).
|
|
90
|
+
*
|
|
91
|
+
* @param path - URL path to normalize
|
|
92
|
+
* @returns Normalized path (no trailing slash, except for root "/")
|
|
93
|
+
*/
|
|
94
|
+
declare function normalizePath(path: string): string;
|
|
95
|
+
/**
|
|
96
|
+
* Creates a normalized Set from an array of paths for consistent matching.
|
|
97
|
+
*
|
|
98
|
+
* @param paths - Array of paths to normalize
|
|
99
|
+
* @returns Set of normalized paths
|
|
100
|
+
*/
|
|
101
|
+
declare function normalizePathSet(paths: readonly string[]): Set<string>;
|
|
102
|
+
/**
|
|
103
|
+
* Generic header getter function.
|
|
104
|
+
* Adapters implement this to bridge framework-specific header access.
|
|
105
|
+
*/
|
|
106
|
+
type HeaderGetter = (name: string) => string | null;
|
|
107
|
+
/**
|
|
108
|
+
* Assembles normalized `RequestMetadata` from generic request components.
|
|
109
|
+
*
|
|
110
|
+
* This is the single point where framework-specific HTTP objects
|
|
111
|
+
* are transformed into the policy layer's input format.
|
|
112
|
+
*
|
|
113
|
+
* @param method - HTTP method (will be uppercased)
|
|
114
|
+
* @param getHeader - Framework-specific header getter
|
|
115
|
+
* @param tokenSource - Pre-resolved token source
|
|
116
|
+
* @returns Normalized RequestMetadata for the policy layer
|
|
117
|
+
*/
|
|
118
|
+
declare function extractRequestMetadata(method: string, getHeader: HeaderGetter, tokenSource: TokenSource): RequestMetadata;
|
|
119
|
+
/**
|
|
120
|
+
* Parses Content-Type header, stripping parameters (charset, boundary, etc.).
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* parseContentType("application/json; charset=utf-8") → "application/json"
|
|
124
|
+
* parseContentType(null) → null
|
|
125
|
+
*/
|
|
126
|
+
declare function parseContentType(contentType: string | null): string | null;
|
|
127
|
+
/**
|
|
128
|
+
* Extracts CSRF token from a custom header.
|
|
129
|
+
*
|
|
130
|
+
* @param getHeader - Header getter function
|
|
131
|
+
* @param headerName - Header name to check (default: 'x-csrf-token')
|
|
132
|
+
* @returns TokenSource from header, or { from: 'none' }
|
|
133
|
+
*/
|
|
134
|
+
declare function extractTokenFromHeader(getHeader: HeaderGetter, headerName?: string): TokenSource;
|
|
135
|
+
/**
|
|
136
|
+
* Extracts CSRF token from a parsed JSON body.
|
|
137
|
+
*
|
|
138
|
+
* @param body - Parsed request body (or null/undefined)
|
|
139
|
+
* @param fieldName - JSON field name (default: 'csrf_token')
|
|
140
|
+
* @returns TokenSource if found, or null
|
|
141
|
+
*/
|
|
142
|
+
declare function extractTokenFromJsonBody(body: Record<string, unknown> | null | undefined, fieldName?: string): TokenSource | null;
|
|
143
|
+
/**
|
|
144
|
+
* Extracts CSRF token from a parsed form body.
|
|
145
|
+
*
|
|
146
|
+
* @param body - Parsed form body (or null/undefined)
|
|
147
|
+
* @param fieldName - Form field name (default: 'csrf_token')
|
|
148
|
+
* @returns TokenSource if found, or null
|
|
149
|
+
*/
|
|
150
|
+
declare function extractTokenFromFormBody(body: Record<string, unknown> | null | undefined, fieldName?: string): TokenSource | null;
|
|
151
|
+
/**
|
|
152
|
+
* Resolves token source following the transport precedence from SPECIFICATION.md §8.3:
|
|
153
|
+
*
|
|
154
|
+
* 1. Custom header (highest priority): `X-CSRF-Token`
|
|
155
|
+
* 2. Request body (JSON): `{ "csrf_token": "..." }`
|
|
156
|
+
* 3. Request body (form): `csrf_token=...`
|
|
157
|
+
* 4. Query parameter: NEVER (not supported)
|
|
158
|
+
*
|
|
159
|
+
* First valid token wins. Multiple tokens → first match wins.
|
|
160
|
+
*
|
|
161
|
+
* @param getHeader - Header getter function
|
|
162
|
+
* @param body - Parsed request body (JSON or form-encoded)
|
|
163
|
+
* @param contentType - Parsed Content-Type MIME (lowercase, no params)
|
|
164
|
+
* @param headerName - Custom header name override
|
|
165
|
+
* @param jsonFieldName - Custom JSON field name override
|
|
166
|
+
* @param formFieldName - Custom form field name override
|
|
167
|
+
* @returns Resolved TokenSource
|
|
168
|
+
*/
|
|
169
|
+
declare function resolveTokenSource(getHeader: HeaderGetter, body: Record<string, unknown> | null | undefined, contentType: string | null, headerName?: string, jsonFieldName?: string, formFieldName?: string): TokenSource;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Handles token generation requests.
|
|
173
|
+
*
|
|
174
|
+
* This is a framework-agnostic handler that processes token endpoint requests.
|
|
175
|
+
* Each adapter calls this and maps the result to framework-specific responses.
|
|
176
|
+
*
|
|
177
|
+
* Supported endpoints:
|
|
178
|
+
* - `GET {tokenEndpointPath}` → Generate a regular CSRF token
|
|
179
|
+
* - `POST {oneShotEndpointPath}` → Generate a one-shot token (requires action binding)
|
|
180
|
+
*
|
|
181
|
+
* **Security (M2 fix):** The one-shot endpoint (POST) requires a valid regular
|
|
182
|
+
* CSRF token in the request header. This prevents cross-origin one-shot token
|
|
183
|
+
* generation and nonce cache exhaustion attacks.
|
|
184
|
+
*
|
|
185
|
+
* @param sigil - The Sigil instance
|
|
186
|
+
* @param method - HTTP method (uppercase)
|
|
187
|
+
* @param path - Request path
|
|
188
|
+
* @param body - Parsed request body (for POST endpoints)
|
|
189
|
+
* @param tokenEndpointPath - Token generation endpoint path
|
|
190
|
+
* @param oneShotEndpointPath - One-shot token endpoint path
|
|
191
|
+
* @param csrfTokenValue - CSRF token from request header (required for POST one-shot endpoint)
|
|
192
|
+
* @returns TokenEndpointResult if the request was handled, or null if not a token endpoint
|
|
193
|
+
*/
|
|
194
|
+
declare function handleTokenEndpoint(sigil: SigilInstance, method: string, path: string, body: Record<string, unknown> | null | undefined, tokenEndpointPath: string, oneShotEndpointPath: string, csrfTokenValue?: string | null): Promise<TokenEndpointResult | null>;
|
|
195
|
+
/**
|
|
196
|
+
* Creates a standardized error result for the token endpoint.
|
|
197
|
+
* Used by adapters when they need to produce error responses.
|
|
198
|
+
*/
|
|
199
|
+
declare function createTokenEndpointError(expired: boolean): TokenEndpointResult;
|
|
200
|
+
|
|
201
|
+
export { type ErrorResponse, type HeaderGetter, SigilConfig, SigilInstance, TokenEndpointResult, createErrorResponse, createOneShotTokenResponse, createSigil, createTokenEndpointError, createTokenResponse, extractRequestMetadata, extractTokenFromFormBody, extractTokenFromHeader, extractTokenFromJsonBody, handleTokenEndpoint, normalizePath, normalizePathSet, parseContentType, resolveTokenSource };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_ONESHOT_ENDPOINT_PATH,
|
|
3
|
+
DEFAULT_TOKEN_ENDPOINT_PATH,
|
|
4
|
+
createErrorResponse,
|
|
5
|
+
createOneShotTokenResponse,
|
|
6
|
+
createTokenEndpointError,
|
|
7
|
+
createTokenResponse,
|
|
8
|
+
extractRequestMetadata,
|
|
9
|
+
extractTokenFromFormBody,
|
|
10
|
+
extractTokenFromHeader,
|
|
11
|
+
extractTokenFromJsonBody,
|
|
12
|
+
handleTokenEndpoint,
|
|
13
|
+
normalizePath,
|
|
14
|
+
normalizePathSet,
|
|
15
|
+
parseContentType,
|
|
16
|
+
resolveTokenSource
|
|
17
|
+
} from "./chunk-JPT5I5W5.js";
|
|
18
|
+
|
|
19
|
+
// src/sigil.ts
|
|
20
|
+
import {
|
|
21
|
+
WebCryptoCryptoProvider,
|
|
22
|
+
createKeyring,
|
|
23
|
+
rotateKey,
|
|
24
|
+
getActiveKey,
|
|
25
|
+
generateToken as coreGenerateToken,
|
|
26
|
+
validateToken as coreValidateToken,
|
|
27
|
+
computeContext,
|
|
28
|
+
generateOneShotToken as coreGenerateOneShotToken,
|
|
29
|
+
validateOneShotToken as coreValidateOneShotToken,
|
|
30
|
+
createNonceCache,
|
|
31
|
+
DEFAULT_TOKEN_TTL_MS,
|
|
32
|
+
DEFAULT_GRACE_WINDOW_MS,
|
|
33
|
+
DEFAULT_ONESHOT_TTL_MS
|
|
34
|
+
} from "@sigil-security/core";
|
|
35
|
+
import {
|
|
36
|
+
createFetchMetadataPolicy,
|
|
37
|
+
createOriginPolicy,
|
|
38
|
+
createMethodPolicy,
|
|
39
|
+
createContentTypePolicy,
|
|
40
|
+
detectClientMode,
|
|
41
|
+
isProtectedMethod,
|
|
42
|
+
evaluatePolicyChain,
|
|
43
|
+
DEFAULT_HEADER_NAME,
|
|
44
|
+
DEFAULT_ONESHOT_HEADER_NAME,
|
|
45
|
+
DEFAULT_PROTECTED_METHODS
|
|
46
|
+
} from "@sigil-security/policy";
|
|
47
|
+
var MIN_MASTER_SECRET_BYTES = 32;
|
|
48
|
+
function normalizeMasterSecret(secret) {
|
|
49
|
+
if (typeof secret !== "string") {
|
|
50
|
+
if (secret.byteLength < MIN_MASTER_SECRET_BYTES) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Master secret must be at least ${String(MIN_MASTER_SECRET_BYTES)} bytes, got ${String(secret.byteLength)} bytes. Use a cryptographically strong secret.`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return secret;
|
|
56
|
+
}
|
|
57
|
+
const encoder = new TextEncoder();
|
|
58
|
+
const bytes = encoder.encode(secret);
|
|
59
|
+
if (bytes.byteLength < MIN_MASTER_SECRET_BYTES) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Master secret must be at least ${String(MIN_MASTER_SECRET_BYTES)} bytes when UTF-8 encoded, got ${String(bytes.byteLength)} bytes. Use a cryptographically strong secret.`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const buffer = new ArrayBuffer(bytes.byteLength);
|
|
65
|
+
new Uint8Array(buffer).set(bytes);
|
|
66
|
+
return buffer;
|
|
67
|
+
}
|
|
68
|
+
function resolveConfig(config) {
|
|
69
|
+
return {
|
|
70
|
+
tokenTTL: config.tokenTTL ?? DEFAULT_TOKEN_TTL_MS,
|
|
71
|
+
graceWindow: config.graceWindow ?? DEFAULT_GRACE_WINDOW_MS,
|
|
72
|
+
allowedOrigins: config.allowedOrigins,
|
|
73
|
+
legacyBrowserMode: config.legacyBrowserMode ?? "degraded",
|
|
74
|
+
allowApiMode: config.allowApiMode ?? true,
|
|
75
|
+
protectedMethods: config.protectedMethods ?? DEFAULT_PROTECTED_METHODS,
|
|
76
|
+
contextBinding: config.contextBinding,
|
|
77
|
+
oneShotEnabled: config.oneShotEnabled ?? false,
|
|
78
|
+
oneShotTTL: config.oneShotTTL ?? DEFAULT_ONESHOT_TTL_MS,
|
|
79
|
+
headerName: config.headerName ?? DEFAULT_HEADER_NAME,
|
|
80
|
+
oneShotHeaderName: config.oneShotHeaderName ?? DEFAULT_ONESHOT_HEADER_NAME,
|
|
81
|
+
disableClientModeOverride: config.disableClientModeOverride ?? false
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function validateOneShotWithKeyring(cryptoProvider, keyring, tokenString, expectedAction, nonceCache, expectedContext, ttlMs) {
|
|
85
|
+
let lastResult = { valid: false, reason: "no_keys" };
|
|
86
|
+
for (const key of keyring.keys) {
|
|
87
|
+
const result = await coreValidateOneShotToken(
|
|
88
|
+
cryptoProvider,
|
|
89
|
+
key,
|
|
90
|
+
tokenString,
|
|
91
|
+
expectedAction,
|
|
92
|
+
nonceCache,
|
|
93
|
+
expectedContext,
|
|
94
|
+
ttlMs
|
|
95
|
+
);
|
|
96
|
+
if (result.valid) return result;
|
|
97
|
+
lastResult = result;
|
|
98
|
+
}
|
|
99
|
+
return lastResult;
|
|
100
|
+
}
|
|
101
|
+
async function createSigil(config) {
|
|
102
|
+
const resolved = resolveConfig(config);
|
|
103
|
+
const cryptoProvider = config.cryptoProvider ?? new WebCryptoCryptoProvider();
|
|
104
|
+
const masterSecret = normalizeMasterSecret(config.masterSecret);
|
|
105
|
+
let kidCounter = 0;
|
|
106
|
+
function nextKid() {
|
|
107
|
+
kidCounter = kidCounter + 1 & 255;
|
|
108
|
+
return kidCounter;
|
|
109
|
+
}
|
|
110
|
+
const initialKid = nextKid();
|
|
111
|
+
let csrfKeyring = await createKeyring(cryptoProvider, masterSecret, initialKid, "csrf");
|
|
112
|
+
let oneShotKeyring = null;
|
|
113
|
+
let nonceCache = null;
|
|
114
|
+
if (resolved.oneShotEnabled) {
|
|
115
|
+
oneShotKeyring = await createKeyring(cryptoProvider, masterSecret, initialKid, "oneshot");
|
|
116
|
+
nonceCache = createNonceCache();
|
|
117
|
+
}
|
|
118
|
+
const browserPolicies = [
|
|
119
|
+
createMethodPolicy({ protectedMethods: [...resolved.protectedMethods] }),
|
|
120
|
+
createFetchMetadataPolicy({ legacyBrowserMode: resolved.legacyBrowserMode }),
|
|
121
|
+
createOriginPolicy({ allowedOrigins: [...resolved.allowedOrigins] }),
|
|
122
|
+
createContentTypePolicy()
|
|
123
|
+
];
|
|
124
|
+
const apiPolicies = [
|
|
125
|
+
createMethodPolicy({ protectedMethods: [...resolved.protectedMethods] }),
|
|
126
|
+
createContentTypePolicy()
|
|
127
|
+
];
|
|
128
|
+
const instance = {
|
|
129
|
+
config: resolved,
|
|
130
|
+
async generateToken(context) {
|
|
131
|
+
const activeKey = getActiveKey(csrfKeyring);
|
|
132
|
+
if (activeKey === void 0) {
|
|
133
|
+
return { success: false, reason: "no_active_key" };
|
|
134
|
+
}
|
|
135
|
+
let contextBytes;
|
|
136
|
+
if (context !== void 0 && context.length > 0) {
|
|
137
|
+
contextBytes = await computeContext(cryptoProvider, ...context);
|
|
138
|
+
}
|
|
139
|
+
return coreGenerateToken(cryptoProvider, activeKey, contextBytes, resolved.tokenTTL);
|
|
140
|
+
},
|
|
141
|
+
async validateToken(tokenString, expectedContext) {
|
|
142
|
+
let contextBytes;
|
|
143
|
+
if (expectedContext !== void 0 && expectedContext.length > 0) {
|
|
144
|
+
contextBytes = await computeContext(cryptoProvider, ...expectedContext);
|
|
145
|
+
}
|
|
146
|
+
return coreValidateToken(
|
|
147
|
+
cryptoProvider,
|
|
148
|
+
csrfKeyring,
|
|
149
|
+
tokenString,
|
|
150
|
+
contextBytes,
|
|
151
|
+
resolved.tokenTTL,
|
|
152
|
+
resolved.graceWindow
|
|
153
|
+
);
|
|
154
|
+
},
|
|
155
|
+
async generateOneShotToken(action, context) {
|
|
156
|
+
if (!resolved.oneShotEnabled || oneShotKeyring === null) {
|
|
157
|
+
return { success: false, reason: "oneshot_not_enabled" };
|
|
158
|
+
}
|
|
159
|
+
const activeKey = getActiveKey(oneShotKeyring);
|
|
160
|
+
if (activeKey === void 0) {
|
|
161
|
+
return { success: false, reason: "no_active_key" };
|
|
162
|
+
}
|
|
163
|
+
let contextBytes;
|
|
164
|
+
if (context !== void 0 && context.length > 0) {
|
|
165
|
+
contextBytes = await computeContext(cryptoProvider, ...context);
|
|
166
|
+
}
|
|
167
|
+
return coreGenerateOneShotToken(
|
|
168
|
+
cryptoProvider,
|
|
169
|
+
activeKey,
|
|
170
|
+
action,
|
|
171
|
+
contextBytes,
|
|
172
|
+
resolved.oneShotTTL
|
|
173
|
+
);
|
|
174
|
+
},
|
|
175
|
+
async validateOneShotToken(tokenString, expectedAction, expectedContext) {
|
|
176
|
+
if (!resolved.oneShotEnabled || oneShotKeyring === null || nonceCache === null) {
|
|
177
|
+
return { valid: false, reason: "oneshot_not_enabled" };
|
|
178
|
+
}
|
|
179
|
+
let contextBytes;
|
|
180
|
+
if (expectedContext !== void 0 && expectedContext.length > 0) {
|
|
181
|
+
contextBytes = await computeContext(cryptoProvider, ...expectedContext);
|
|
182
|
+
}
|
|
183
|
+
return validateOneShotWithKeyring(
|
|
184
|
+
cryptoProvider,
|
|
185
|
+
oneShotKeyring,
|
|
186
|
+
tokenString,
|
|
187
|
+
expectedAction,
|
|
188
|
+
nonceCache,
|
|
189
|
+
contextBytes,
|
|
190
|
+
resolved.oneShotTTL
|
|
191
|
+
);
|
|
192
|
+
},
|
|
193
|
+
async rotateKeys() {
|
|
194
|
+
const newKid = nextKid();
|
|
195
|
+
csrfKeyring = await rotateKey(csrfKeyring, cryptoProvider, masterSecret, newKid);
|
|
196
|
+
if (oneShotKeyring !== null) {
|
|
197
|
+
oneShotKeyring = await rotateKey(oneShotKeyring, cryptoProvider, masterSecret, newKid);
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
async protect(metadata, contextBindings) {
|
|
201
|
+
if (!isProtectedMethod(metadata.method, [...resolved.protectedMethods])) {
|
|
202
|
+
return {
|
|
203
|
+
allowed: true,
|
|
204
|
+
tokenValid: false,
|
|
205
|
+
policyResult: { allowed: true, evaluated: [], failures: [] }
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const mode = detectClientMode(metadata, {
|
|
209
|
+
disableClientModeOverride: resolved.disableClientModeOverride
|
|
210
|
+
});
|
|
211
|
+
if (mode === "api" && !resolved.allowApiMode) {
|
|
212
|
+
return {
|
|
213
|
+
allowed: false,
|
|
214
|
+
reason: "api_mode_not_allowed",
|
|
215
|
+
expired: false,
|
|
216
|
+
policyResult: null
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const policies = mode === "browser" ? browserPolicies : apiPolicies;
|
|
220
|
+
const policyResult = evaluatePolicyChain(policies, metadata);
|
|
221
|
+
if (!policyResult.allowed) {
|
|
222
|
+
return {
|
|
223
|
+
allowed: false,
|
|
224
|
+
reason: policyResult.reason,
|
|
225
|
+
expired: false,
|
|
226
|
+
policyResult
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (metadata.tokenSource.from === "none") {
|
|
230
|
+
return {
|
|
231
|
+
allowed: false,
|
|
232
|
+
reason: "no_token_present",
|
|
233
|
+
expired: false,
|
|
234
|
+
policyResult
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
let contextBytes;
|
|
238
|
+
if (contextBindings !== void 0 && contextBindings.length > 0) {
|
|
239
|
+
contextBytes = await computeContext(cryptoProvider, ...contextBindings);
|
|
240
|
+
}
|
|
241
|
+
const tokenResult = await coreValidateToken(
|
|
242
|
+
cryptoProvider,
|
|
243
|
+
csrfKeyring,
|
|
244
|
+
metadata.tokenSource.value,
|
|
245
|
+
contextBytes,
|
|
246
|
+
resolved.tokenTTL,
|
|
247
|
+
resolved.graceWindow
|
|
248
|
+
);
|
|
249
|
+
if (!tokenResult.valid) {
|
|
250
|
+
return {
|
|
251
|
+
allowed: false,
|
|
252
|
+
reason: tokenResult.reason,
|
|
253
|
+
expired: tokenResult.reason === "expired",
|
|
254
|
+
policyResult
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
allowed: true,
|
|
259
|
+
tokenValid: true,
|
|
260
|
+
policyResult
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
return instance;
|
|
265
|
+
}
|
|
266
|
+
export {
|
|
267
|
+
DEFAULT_ONESHOT_ENDPOINT_PATH,
|
|
268
|
+
DEFAULT_TOKEN_ENDPOINT_PATH,
|
|
269
|
+
createErrorResponse,
|
|
270
|
+
createOneShotTokenResponse,
|
|
271
|
+
createSigil,
|
|
272
|
+
createTokenEndpointError,
|
|
273
|
+
createTokenResponse,
|
|
274
|
+
extractRequestMetadata,
|
|
275
|
+
extractTokenFromFormBody,
|
|
276
|
+
extractTokenFromHeader,
|
|
277
|
+
extractTokenFromJsonBody,
|
|
278
|
+
handleTokenEndpoint,
|
|
279
|
+
normalizePath,
|
|
280
|
+
normalizePathSet,
|
|
281
|
+
parseContentType,
|
|
282
|
+
resolveTokenSource
|
|
283
|
+
};
|
|
284
|
+
//# sourceMappingURL=index.js.map
|