@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.
Files changed (48) hide show
  1. package/LICENSE +201 -0
  2. package/dist/adapters/elysia.cjs +307 -0
  3. package/dist/adapters/elysia.cjs.map +1 -0
  4. package/dist/adapters/elysia.d.cts +41 -0
  5. package/dist/adapters/elysia.d.ts +41 -0
  6. package/dist/adapters/elysia.js +98 -0
  7. package/dist/adapters/elysia.js.map +1 -0
  8. package/dist/adapters/express.cjs +286 -0
  9. package/dist/adapters/express.cjs.map +1 -0
  10. package/dist/adapters/express.d.cts +59 -0
  11. package/dist/adapters/express.d.ts +59 -0
  12. package/dist/adapters/express.js +77 -0
  13. package/dist/adapters/express.js.map +1 -0
  14. package/dist/adapters/fastify.cjs +308 -0
  15. package/dist/adapters/fastify.cjs.map +1 -0
  16. package/dist/adapters/fastify.d.cts +54 -0
  17. package/dist/adapters/fastify.d.ts +54 -0
  18. package/dist/adapters/fastify.js +99 -0
  19. package/dist/adapters/fastify.js.map +1 -0
  20. package/dist/adapters/fetch.cjs +359 -0
  21. package/dist/adapters/fetch.cjs.map +1 -0
  22. package/dist/adapters/fetch.d.cts +46 -0
  23. package/dist/adapters/fetch.d.ts +46 -0
  24. package/dist/adapters/fetch.js +149 -0
  25. package/dist/adapters/fetch.js.map +1 -0
  26. package/dist/adapters/hono.cjs +300 -0
  27. package/dist/adapters/hono.cjs.map +1 -0
  28. package/dist/adapters/hono.d.cts +41 -0
  29. package/dist/adapters/hono.d.ts +41 -0
  30. package/dist/adapters/hono.js +91 -0
  31. package/dist/adapters/hono.js.map +1 -0
  32. package/dist/adapters/oak.cjs +318 -0
  33. package/dist/adapters/oak.cjs.map +1 -0
  34. package/dist/adapters/oak.d.cts +48 -0
  35. package/dist/adapters/oak.d.ts +48 -0
  36. package/dist/adapters/oak.js +109 -0
  37. package/dist/adapters/oak.js.map +1 -0
  38. package/dist/chunk-JPT5I5W5.js +225 -0
  39. package/dist/chunk-JPT5I5W5.js.map +1 -0
  40. package/dist/index.cjs +486 -0
  41. package/dist/index.cjs.map +1 -0
  42. package/dist/index.d.cts +201 -0
  43. package/dist/index.d.ts +201 -0
  44. package/dist/index.js +284 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/types-DySgT8rA.d.cts +184 -0
  47. package/dist/types-DySgT8rA.d.ts +184 -0
  48. package/package.json +141 -0
@@ -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 };
@@ -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