@stacksjs/bun-router 0.0.14 → 0.0.15
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/README.md +261 -584
- package/bin/cli.ts +9 -0
- package/dist/auth.d.ts +134 -0
- package/dist/cache/lru-cache.d.ts +139 -0
- package/dist/cache/middleware-memoization.d.ts +151 -0
- package/dist/cache/route-cache-warmer.d.ts +161 -0
- package/dist/cache/sqlite-cache.d.ts +209 -0
- package/dist/cache/streaming-cache.d.ts +132 -0
- package/dist/chunk-j0e7z7hd.js +18053 -0
- package/dist/cli/colors.d.ts +15 -0
- package/dist/cli/index.d.ts +10 -0
- package/dist/cli/middleware.d.ts +31 -0
- package/dist/cli/openapi.d.ts +17 -0
- package/dist/cli/router.d.ts +15 -0
- package/dist/cli/routes.d.ts +29 -0
- package/dist/cli/utils.d.ts +74 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3360 -0
- package/dist/config.d.ts +7 -0
- package/dist/container/container.d.ts +273 -0
- package/dist/container/contextual-binding.d.ts +240 -0
- package/dist/container/decorators.d.ts +141 -0
- package/dist/container/service-provider.d.ts +285 -0
- package/dist/development/hot-reload.d.ts +187 -0
- package/dist/development/index.d.ts +167 -0
- package/dist/development/performance-profiler.d.ts +217 -0
- package/dist/development/route-debugger.d.ts +154 -0
- package/dist/development/route-inspector.d.ts +211 -0
- package/dist/development/typescript-utilities.d.ts +209 -0
- package/dist/docs.d.ts +11 -0
- package/dist/errors/circuit-breaker.d.ts +195 -0
- package/dist/errors/error-handler.d.ts +88 -0
- package/dist/errors/error-reporting.d.ts +144 -0
- package/dist/errors/exceptions.d.ts +184 -0
- package/dist/errors/graceful-degradation.d.ts +154 -0
- package/dist/errors/index.d.ts +9 -0
- package/dist/errors/router-errors.d.ts +110 -0
- package/dist/file-serving/static-files.d.ts +143 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13968 -0
- package/dist/middleware/auth.d.ts +63 -0
- package/dist/middleware/content_security_policy.d.ts +53 -0
- package/dist/middleware/cors.d.ts +5 -0
- package/dist/middleware/csrf.d.ts +7 -0
- package/dist/middleware/ddos_protection.d.ts +39 -0
- package/dist/middleware/file_security.d.ts +26 -0
- package/dist/middleware/file_upload.d.ts +38 -0
- package/dist/middleware/helmet.d.ts +54 -0
- package/dist/middleware/index.d.ts +44 -0
- package/dist/middleware/input_validation.d.ts +45 -0
- package/dist/middleware/json_body.d.ts +4 -0
- package/dist/middleware/performance_alerting.d.ts +87 -0
- package/dist/middleware/performance_dashboard.d.ts +87 -0
- package/dist/middleware/performance_monitor.d.ts +209 -0
- package/dist/middleware/pipeline.d.ts +131 -0
- package/dist/middleware/rate_limit.d.ts +36 -0
- package/dist/middleware/request_id.d.ts +4 -0
- package/dist/middleware/request_signing.d.ts +153 -0
- package/dist/middleware/request_tracer.d.ts +72 -0
- package/dist/middleware/response_cache.d.ts +97 -0
- package/dist/middleware/security.d.ts +69 -0
- package/dist/middleware/security_suite.d.ts +46 -0
- package/dist/middleware/session.d.ts +23 -0
- package/dist/model-binding/index.d.ts +2 -0
- package/dist/model-binding/model-middleware.d.ts +118 -0
- package/dist/model-binding/model-registry.d.ts +164 -0
- package/dist/model-binding.d.ts +185 -0
- package/dist/model-resolver-factory.d.ts +31 -0
- package/dist/observability/correlation.d.ts +187 -0
- package/dist/observability/health-checks.d.ts +186 -0
- package/dist/observability/index.d.ts +60 -0
- package/dist/observability/integration.d.ts +147 -0
- package/dist/observability/metrics.d.ts +184 -0
- package/dist/observability/tracing.d.ts +187 -0
- package/dist/optimization/bun-utilities.d.ts +224 -0
- package/dist/query-builder-integration.d.ts +36 -0
- package/dist/request/context.d.ts +34 -0
- package/dist/request/enhanced-request.d.ts +213 -0
- package/dist/request/macros.d.ts +300 -0
- package/dist/response/macros.d.ts +256 -0
- package/dist/response/response-factory.d.ts +133 -0
- package/dist/router/api-routes.d.ts +5 -0
- package/dist/router/file-based-routing.d.ts +118 -0
- package/dist/router/file-streaming.d.ts +5 -0
- package/dist/router/fluent-router.d.ts +315 -0
- package/dist/router/fluent-routing.d.ts +271 -0
- package/dist/router/group-organization.d.ts +5 -0
- package/dist/router/handler-resolver.d.ts +24 -0
- package/dist/router/http-methods.d.ts +10 -0
- package/dist/router/index.d.ts +114 -0
- package/dist/router/middleware-groups.d.ts +94 -0
- package/dist/router/middleware-integration.d.ts +112 -0
- package/dist/router/middleware.d.ts +5 -0
- package/dist/router/model-binding.d.ts +5 -0
- package/dist/router/optimized-route-matching.d.ts +6 -0
- package/dist/router/route-building.d.ts +5 -0
- package/dist/router/route-compiler.d.ts +153 -0
- package/dist/router/route-matching.d.ts +5 -0
- package/dist/router/route-trie.d.ts +114 -0
- package/dist/router/router.d.ts +317 -0
- package/dist/router/server.d.ts +5 -0
- package/dist/router/validation-integration.d.ts +170 -0
- package/dist/router/view-rendering.d.ts +5 -0
- package/dist/router/websocket.d.ts +5 -0
- package/dist/routing/route-caching.d.ts +129 -0
- package/dist/routing/route-throttling.d.ts +150 -0
- package/dist/routing/subdomain-routing.d.ts +206 -0
- package/dist/session/database-store.d.ts +51 -0
- package/dist/session/file-store.d.ts +22 -0
- package/dist/session/index.d.ts +106 -0
- package/dist/session/memory-store.d.ts +21 -0
- package/dist/session/redis-store.d.ts +31 -0
- package/dist/streaming/index.d.ts +3 -0
- package/dist/streaming/sse-handler.d.ts +138 -0
- package/dist/streaming/stream-handler.d.ts +114 -0
- package/dist/testing/auth-testing.d.ts +156 -0
- package/dist/testing/file-upload-testing.d.ts +190 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/middleware-testing.d.ts +138 -0
- package/dist/testing/model-binding-testing.d.ts +186 -0
- package/dist/testing/performance-testing.d.ts +235 -0
- package/dist/testing/test-client.d.ts +117 -0
- package/dist/testing/test-request.d.ts +85 -0
- package/dist/testing/test-response.d.ts +90 -0
- package/dist/testing/types.d.ts +207 -0
- package/dist/testing/websocket-testing.d.ts +227 -0
- package/dist/types/controller-types.d.ts +208 -0
- package/dist/types/core.d.ts +543 -0
- package/dist/types/middleware-types.d.ts +227 -0
- package/dist/types/request-response-augmentation.d.ts +261 -0
- package/dist/types/route-inference.d.ts +168 -0
- package/dist/types.d.ts +1794 -0
- package/dist/url.d.ts +56 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/query-preservation.d.ts +48 -0
- package/dist/utils.d.ts +69 -0
- package/dist/validation/validator.d.ts +140 -0
- package/dist/websocket/clustering.d.ts +185 -0
- package/package.json +45 -44
- package/src/auth.ts +469 -0
- package/src/cache/lru-cache.ts +457 -0
- package/src/cache/middleware-memoization.ts +531 -0
- package/src/cache/route-cache-warmer.ts +486 -0
- package/src/cache/sqlite-cache.ts +783 -0
- package/src/cache/streaming-cache.ts +572 -0
- package/src/cli/colors.ts +29 -0
- package/src/cli/index.ts +287 -0
- package/src/cli/middleware.ts +291 -0
- package/src/cli/openapi.ts +407 -0
- package/src/cli/router.ts +188 -0
- package/src/cli/routes.ts +265 -0
- package/src/cli/utils.ts +531 -0
- package/src/cli.ts +5 -0
- package/src/config.ts +322 -0
- package/src/container/container.ts +740 -0
- package/src/container/contextual-binding.ts +603 -0
- package/src/container/decorators.ts +359 -0
- package/src/container/service-provider.ts +596 -0
- package/src/development/hot-reload.ts +673 -0
- package/src/development/index.ts +499 -0
- package/src/development/performance-profiler.ts +717 -0
- package/src/development/route-debugger.ts +527 -0
- package/src/development/route-inspector.ts +749 -0
- package/src/development/typescript-utilities.ts +682 -0
- package/src/docs.ts +397 -0
- package/src/errors/circuit-breaker.ts +732 -0
- package/src/errors/error-handler.ts +569 -0
- package/src/errors/error-reporting.ts +672 -0
- package/src/errors/exceptions.ts +536 -0
- package/src/errors/graceful-degradation.ts +621 -0
- package/src/errors/index.ts +21 -0
- package/src/errors/router-errors.ts +632 -0
- package/src/file-serving/static-files.ts +581 -0
- package/src/index.ts +14 -0
- package/src/middleware/auth.ts +220 -0
- package/src/middleware/content_security_policy.ts +215 -0
- package/src/middleware/cors.ts +74 -0
- package/src/middleware/csrf.ts +108 -0
- package/src/middleware/ddos_protection.ts +255 -0
- package/src/middleware/file_security.ts +191 -0
- package/src/middleware/file_upload.ts +275 -0
- package/src/middleware/helmet.ts +268 -0
- package/src/middleware/index.ts +117 -0
- package/src/middleware/input_validation.ts +449 -0
- package/src/middleware/json_body.ts +37 -0
- package/src/middleware/performance_alerting.ts +538 -0
- package/src/middleware/performance_dashboard.ts +661 -0
- package/src/middleware/performance_monitor.ts +943 -0
- package/src/middleware/pipeline.ts +489 -0
- package/src/middleware/rate_limit.ts +245 -0
- package/src/middleware/request_id.ts +36 -0
- package/src/middleware/request_signing.ts +636 -0
- package/src/middleware/request_tracer.ts +636 -0
- package/src/middleware/response_cache.ts +743 -0
- package/src/middleware/security.ts +482 -0
- package/src/middleware/security_suite.ts +257 -0
- package/src/middleware/session.ts +91 -0
- package/src/model-binding/index.ts +18 -0
- package/src/model-binding/model-middleware.ts +425 -0
- package/src/model-binding/model-registry.ts +550 -0
- package/src/model-binding.ts +370 -0
- package/src/model-resolver-factory.ts +106 -0
- package/src/observability/correlation.ts +691 -0
- package/src/observability/health-checks.ts +729 -0
- package/src/observability/index.ts +184 -0
- package/src/observability/integration.ts +548 -0
- package/src/observability/metrics.ts +753 -0
- package/src/observability/tracing.ts +638 -0
- package/src/optimization/bun-utilities.ts +778 -0
- package/src/query-builder-integration.ts +137 -0
- package/src/request/context.ts +62 -0
- package/src/request/enhanced-request.ts +857 -0
- package/src/request/macros.ts +688 -0
- package/src/response/macros.ts +665 -0
- package/src/response/response-factory.ts +596 -0
- package/src/router/api-routes.ts +243 -0
- package/src/router/file-based-routing.ts +690 -0
- package/src/router/file-streaming.ts +383 -0
- package/src/router/fluent-router.ts +927 -0
- package/src/router/fluent-routing.ts +797 -0
- package/src/router/group-organization.ts +213 -0
- package/src/router/handler-resolver.ts +291 -0
- package/src/router/http-methods.ts +377 -0
- package/src/router/index.ts +187 -0
- package/src/router/middleware-groups.ts +222 -0
- package/src/router/middleware-integration.ts +399 -0
- package/src/router/middleware.ts +231 -0
- package/src/router/model-binding.ts +215 -0
- package/src/router/optimized-route-matching.ts +253 -0
- package/src/router/route-building.ts +221 -0
- package/src/router/route-compiler.ts +702 -0
- package/src/router/route-matching.ts +349 -0
- package/src/router/route-trie.ts +464 -0
- package/src/router/router.ts +1604 -0
- package/src/router/server.ts +462 -0
- package/src/router/validation-integration.ts +443 -0
- package/src/router/view-rendering.ts +233 -0
- package/src/router/websocket.ts +100 -0
- package/src/routing/route-caching.ts +402 -0
- package/src/routing/route-throttling.ts +469 -0
- package/src/routing/subdomain-routing.ts +492 -0
- package/src/session/database-store.ts +109 -0
- package/src/session/file-store.ts +148 -0
- package/src/session/index.ts +244 -0
- package/src/session/memory-store.ts +88 -0
- package/src/session/redis-store.ts +93 -0
- package/src/streaming/index.ts +17 -0
- package/src/streaming/sse-handler.ts +482 -0
- package/src/streaming/stream-handler.ts +552 -0
- package/src/testing/auth-testing.ts +446 -0
- package/src/testing/file-upload-testing.ts +543 -0
- package/src/testing/index.ts +10 -0
- package/src/testing/middleware-testing.ts +322 -0
- package/src/testing/model-binding-testing.ts +645 -0
- package/src/testing/performance-testing.ts +738 -0
- package/src/testing/test-client.ts +310 -0
- package/src/testing/test-request.ts +315 -0
- package/src/testing/test-response.ts +332 -0
- package/src/testing/types.ts +224 -0
- package/src/testing/websocket-testing.ts +590 -0
- package/src/types/controller-types.ts +385 -0
- package/src/types/core.ts +683 -0
- package/src/types/middleware-types.ts +419 -0
- package/src/types/request-response-augmentation.ts +489 -0
- package/src/types/route-inference.ts +357 -0
- package/src/types.ts +2067 -0
- package/src/url.ts +126 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/query-preservation.ts +201 -0
- package/src/utils.ts +327 -0
- package/src/validation/validator.ts +685 -0
- package/src/websocket/clustering.ts +762 -0
- package/CHANGELOG.md +0 -770
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Signing Middleware
|
|
3
|
+
*
|
|
4
|
+
* Provides HMAC-based request signature verification for secure API authentication.
|
|
5
|
+
* Supports multiple algorithms, timestamp validation, and replay attack prevention.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { EnhancedRequest, Middleware, NextFunction } from '../types'
|
|
9
|
+
|
|
10
|
+
export type SignatureAlgorithm = 'sha256' | 'sha384' | 'sha512'
|
|
11
|
+
|
|
12
|
+
export interface RequestSigningOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Secret key(s) for HMAC verification.
|
|
15
|
+
* Can be a single key or a function that returns the key based on key ID.
|
|
16
|
+
*/
|
|
17
|
+
secret: string | ((_keyId: string) => string | Promise<string | null> | null)
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* HMAC algorithm to use
|
|
21
|
+
* @default 'sha256'
|
|
22
|
+
*/
|
|
23
|
+
algorithm?: SignatureAlgorithm
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Header name containing the signature
|
|
27
|
+
* @default 'X-Signature'
|
|
28
|
+
*/
|
|
29
|
+
signatureHeader?: string
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Header name containing the timestamp
|
|
33
|
+
* @default 'X-Timestamp'
|
|
34
|
+
*/
|
|
35
|
+
timestampHeader?: string
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Header name containing the key ID (for multi-key setups)
|
|
39
|
+
* @default 'X-Key-Id'
|
|
40
|
+
*/
|
|
41
|
+
keyIdHeader?: string
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Header name containing the nonce (for replay protection)
|
|
45
|
+
* @default 'X-Nonce'
|
|
46
|
+
*/
|
|
47
|
+
nonceHeader?: string
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Maximum age of request in seconds (timestamp validation)
|
|
51
|
+
* @default 300 (5 minutes)
|
|
52
|
+
*/
|
|
53
|
+
maxAge?: number
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whether to require timestamp validation
|
|
57
|
+
* @default true
|
|
58
|
+
*/
|
|
59
|
+
requireTimestamp?: boolean
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether to require nonce for replay protection
|
|
63
|
+
* @default false
|
|
64
|
+
*/
|
|
65
|
+
requireNonce?: boolean
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Custom function to check if nonce has been used (for replay protection)
|
|
69
|
+
*/
|
|
70
|
+
nonceStore?: NonceStore
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parts of the request to include in signature
|
|
74
|
+
* @default ['method', 'path', 'timestamp', 'body']
|
|
75
|
+
*/
|
|
76
|
+
signedParts?: SignedPart[]
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Custom function to build the string to sign
|
|
80
|
+
*/
|
|
81
|
+
stringToSign?: (req: EnhancedRequest, timestamp: string, nonce?: string) => Promise<string> | string
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Custom error handler
|
|
85
|
+
*/
|
|
86
|
+
onError?: (error: SignatureError, req: EnhancedRequest) => Response | Promise<Response>
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Skip signature verification for certain paths
|
|
90
|
+
*/
|
|
91
|
+
exclude?: string[] | ((_req: EnhancedRequest) => boolean)
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Additional headers to include in signature
|
|
95
|
+
*/
|
|
96
|
+
signedHeaders?: string[]
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Signature encoding format
|
|
100
|
+
* @default 'hex'
|
|
101
|
+
*/
|
|
102
|
+
encoding?: 'hex' | 'base64'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type SignedPart = 'method' | 'path' | 'query' | 'timestamp' | 'body' | 'nonce' | 'headers'
|
|
106
|
+
|
|
107
|
+
export interface NonceStore {
|
|
108
|
+
/**
|
|
109
|
+
* Check if nonce exists (has been used)
|
|
110
|
+
*/
|
|
111
|
+
has: (nonce: string) => Promise<boolean> | boolean
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Store a nonce
|
|
115
|
+
*/
|
|
116
|
+
set: (nonce: string, expiry?: number) => Promise<void> | void
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type SignatureErrorCode =
|
|
120
|
+
| 'MISSING_SIGNATURE'
|
|
121
|
+
| 'MISSING_TIMESTAMP'
|
|
122
|
+
| 'MISSING_NONCE'
|
|
123
|
+
| 'MISSING_KEY_ID'
|
|
124
|
+
| 'INVALID_KEY'
|
|
125
|
+
| 'INVALID_SIGNATURE'
|
|
126
|
+
| 'EXPIRED_TIMESTAMP'
|
|
127
|
+
| 'FUTURE_TIMESTAMP'
|
|
128
|
+
| 'NONCE_REUSED'
|
|
129
|
+
| 'SIGNATURE_MISMATCH'
|
|
130
|
+
|
|
131
|
+
export class SignatureError extends Error {
|
|
132
|
+
public readonly code: SignatureErrorCode
|
|
133
|
+
public readonly statusCode: number
|
|
134
|
+
|
|
135
|
+
constructor(code: SignatureErrorCode, message: string) {
|
|
136
|
+
super(message)
|
|
137
|
+
this.name = 'SignatureError'
|
|
138
|
+
this.code = code
|
|
139
|
+
this.statusCode = 401
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* In-memory nonce store with automatic expiry
|
|
145
|
+
*/
|
|
146
|
+
export class InMemoryNonceStore implements NonceStore {
|
|
147
|
+
private nonces = new Map<string, number>()
|
|
148
|
+
private cleanupInterval: ReturnType<typeof setInterval> | null = null
|
|
149
|
+
|
|
150
|
+
constructor(cleanupIntervalMs: number = 60000) {
|
|
151
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), cleanupIntervalMs)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
has(nonce: string): boolean {
|
|
155
|
+
const expiry = this.nonces.get(nonce)
|
|
156
|
+
if (expiry === undefined)
|
|
157
|
+
return false
|
|
158
|
+
if (Date.now() > expiry) {
|
|
159
|
+
this.nonces.delete(nonce)
|
|
160
|
+
return false
|
|
161
|
+
}
|
|
162
|
+
return true
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
set(nonce: string, expirySeconds: number = 300): void {
|
|
166
|
+
this.nonces.set(nonce, Date.now() + expirySeconds * 1000)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private cleanup(): void {
|
|
170
|
+
const now = Date.now()
|
|
171
|
+
for (const [nonce, expiry] of this.nonces.entries()) {
|
|
172
|
+
if (now > expiry) {
|
|
173
|
+
this.nonces.delete(nonce)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
destroy(): void {
|
|
179
|
+
if (this.cleanupInterval) {
|
|
180
|
+
clearInterval(this.cleanupInterval)
|
|
181
|
+
this.cleanupInterval = null
|
|
182
|
+
}
|
|
183
|
+
this.nonces.clear()
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Request Signing Middleware
|
|
189
|
+
*/
|
|
190
|
+
export default class RequestSigning implements Middleware {
|
|
191
|
+
private options: Required<Omit<RequestSigningOptions, 'onError' | 'exclude' | 'nonceStore' | 'stringToSign'>> & {
|
|
192
|
+
onError?: RequestSigningOptions['onError']
|
|
193
|
+
exclude?: RequestSigningOptions['exclude']
|
|
194
|
+
nonceStore?: NonceStore
|
|
195
|
+
stringToSign?: RequestSigningOptions['stringToSign']
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
constructor(options: RequestSigningOptions) {
|
|
199
|
+
this.options = {
|
|
200
|
+
secret: options.secret,
|
|
201
|
+
algorithm: options.algorithm ?? 'sha256',
|
|
202
|
+
signatureHeader: options.signatureHeader ?? 'X-Signature',
|
|
203
|
+
timestampHeader: options.timestampHeader ?? 'X-Timestamp',
|
|
204
|
+
keyIdHeader: options.keyIdHeader ?? 'X-Key-Id',
|
|
205
|
+
nonceHeader: options.nonceHeader ?? 'X-Nonce',
|
|
206
|
+
maxAge: options.maxAge ?? 300,
|
|
207
|
+
requireTimestamp: options.requireTimestamp ?? true,
|
|
208
|
+
requireNonce: options.requireNonce ?? false,
|
|
209
|
+
signedParts: options.signedParts ?? ['method', 'path', 'timestamp', 'body'],
|
|
210
|
+
signedHeaders: options.signedHeaders ?? [],
|
|
211
|
+
encoding: options.encoding ?? 'hex',
|
|
212
|
+
onError: options.onError,
|
|
213
|
+
exclude: options.exclude,
|
|
214
|
+
nonceStore: options.nonceStore,
|
|
215
|
+
stringToSign: options.stringToSign,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async handle(req: EnhancedRequest, next: NextFunction): Promise<Response | null> {
|
|
220
|
+
// Check exclusions
|
|
221
|
+
if (this.shouldSkip(req)) {
|
|
222
|
+
return next()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
await this.verifySignature(req)
|
|
227
|
+
return next()
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
if (error instanceof SignatureError) {
|
|
231
|
+
if (this.options.onError) {
|
|
232
|
+
return this.options.onError(error, req)
|
|
233
|
+
}
|
|
234
|
+
return this.defaultErrorResponse(error)
|
|
235
|
+
}
|
|
236
|
+
throw error
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private shouldSkip(req: EnhancedRequest): boolean {
|
|
241
|
+
if (!this.options.exclude)
|
|
242
|
+
return false
|
|
243
|
+
|
|
244
|
+
if (typeof this.options.exclude === 'function') {
|
|
245
|
+
return this.options.exclude(req)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const url = new URL(req.url)
|
|
249
|
+
return this.options.exclude.some(path =>
|
|
250
|
+
url.pathname === path || url.pathname.startsWith(`${path}/`),
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private async verifySignature(req: EnhancedRequest): Promise<void> {
|
|
255
|
+
// Get signature from header
|
|
256
|
+
const signature = req.headers.get(this.options.signatureHeader)
|
|
257
|
+
if (!signature) {
|
|
258
|
+
throw new SignatureError('MISSING_SIGNATURE', `Missing ${this.options.signatureHeader} header`)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Get and validate timestamp
|
|
262
|
+
const timestamp = req.headers.get(this.options.timestampHeader)
|
|
263
|
+
if (this.options.requireTimestamp) {
|
|
264
|
+
if (!timestamp) {
|
|
265
|
+
throw new SignatureError('MISSING_TIMESTAMP', `Missing ${this.options.timestampHeader} header`)
|
|
266
|
+
}
|
|
267
|
+
this.validateTimestamp(timestamp)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Get and validate nonce
|
|
271
|
+
const nonce = req.headers.get(this.options.nonceHeader)
|
|
272
|
+
if (this.options.requireNonce) {
|
|
273
|
+
if (!nonce) {
|
|
274
|
+
throw new SignatureError('MISSING_NONCE', `Missing ${this.options.nonceHeader} header`)
|
|
275
|
+
}
|
|
276
|
+
await this.validateNonce(nonce)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Get secret key
|
|
280
|
+
const secret = await this.getSecret(req)
|
|
281
|
+
if (!secret) {
|
|
282
|
+
throw new SignatureError('INVALID_KEY', 'Invalid or missing API key')
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Build string to sign
|
|
286
|
+
const stringToSign = await this.buildStringToSign(req, timestamp || '', nonce || undefined)
|
|
287
|
+
|
|
288
|
+
// Calculate expected signature
|
|
289
|
+
const expectedSignature = await this.calculateSignature(stringToSign, secret)
|
|
290
|
+
|
|
291
|
+
// Compare signatures (timing-safe)
|
|
292
|
+
if (!this.timingSafeEqual(signature, expectedSignature)) {
|
|
293
|
+
throw new SignatureError('SIGNATURE_MISMATCH', 'Invalid signature')
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Store nonce if replay protection is enabled
|
|
297
|
+
if (this.options.requireNonce && nonce && this.options.nonceStore) {
|
|
298
|
+
await this.options.nonceStore.set(nonce, this.options.maxAge)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private async getSecret(req: EnhancedRequest): Promise<string | null> {
|
|
303
|
+
if (typeof this.options.secret === 'string') {
|
|
304
|
+
return this.options.secret
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const keyId = req.headers.get(this.options.keyIdHeader)
|
|
308
|
+
if (!keyId) {
|
|
309
|
+
throw new SignatureError('MISSING_KEY_ID', `Missing ${this.options.keyIdHeader} header`)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return this.options.secret(keyId)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private validateTimestamp(timestamp: string): void {
|
|
316
|
+
const requestTime = Number.parseInt(timestamp, 10)
|
|
317
|
+
|
|
318
|
+
if (Number.isNaN(requestTime)) {
|
|
319
|
+
throw new SignatureError('EXPIRED_TIMESTAMP', 'Invalid timestamp format')
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const now = Math.floor(Date.now() / 1000)
|
|
323
|
+
const age = now - requestTime
|
|
324
|
+
|
|
325
|
+
if (age > this.options.maxAge) {
|
|
326
|
+
throw new SignatureError('EXPIRED_TIMESTAMP', `Request timestamp expired (${age}s old, max ${this.options.maxAge}s)`)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Check for future timestamps (with small tolerance)
|
|
330
|
+
if (age < -30) {
|
|
331
|
+
throw new SignatureError('FUTURE_TIMESTAMP', 'Request timestamp is in the future')
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private async validateNonce(nonce: string): Promise<void> {
|
|
336
|
+
if (!this.options.nonceStore) {
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const exists = await this.options.nonceStore.has(nonce)
|
|
341
|
+
if (exists) {
|
|
342
|
+
throw new SignatureError('NONCE_REUSED', 'Nonce has already been used (possible replay attack)')
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private async buildStringToSign(req: EnhancedRequest, timestamp: string, nonce?: string): Promise<string> {
|
|
347
|
+
// Use custom string builder if provided
|
|
348
|
+
if (this.options.stringToSign) {
|
|
349
|
+
return this.options.stringToSign(req, timestamp, nonce)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const url = new URL(req.url)
|
|
353
|
+
const parts: string[] = []
|
|
354
|
+
|
|
355
|
+
for (const part of this.options.signedParts) {
|
|
356
|
+
switch (part) {
|
|
357
|
+
case 'method':
|
|
358
|
+
parts.push(req.method.toUpperCase())
|
|
359
|
+
break
|
|
360
|
+
case 'path':
|
|
361
|
+
parts.push(url.pathname)
|
|
362
|
+
break
|
|
363
|
+
case 'query':
|
|
364
|
+
parts.push(url.search)
|
|
365
|
+
break
|
|
366
|
+
case 'timestamp':
|
|
367
|
+
parts.push(timestamp)
|
|
368
|
+
break
|
|
369
|
+
case 'nonce':
|
|
370
|
+
if (nonce)
|
|
371
|
+
parts.push(nonce)
|
|
372
|
+
break
|
|
373
|
+
case 'body':
|
|
374
|
+
if (req.body) {
|
|
375
|
+
const body = await req.clone().text()
|
|
376
|
+
parts.push(body)
|
|
377
|
+
}
|
|
378
|
+
break
|
|
379
|
+
case 'headers':
|
|
380
|
+
for (const header of this.options.signedHeaders) {
|
|
381
|
+
const value = req.headers.get(header)
|
|
382
|
+
if (value) {
|
|
383
|
+
parts.push(`${header.toLowerCase()}:${value}`)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
break
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return parts.join('\n')
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private async calculateSignature(data: string, secret: string): Promise<string> {
|
|
394
|
+
const encoder = new TextEncoder()
|
|
395
|
+
const keyData = encoder.encode(secret)
|
|
396
|
+
const messageData = encoder.encode(data)
|
|
397
|
+
|
|
398
|
+
const algorithmName = {
|
|
399
|
+
sha256: 'SHA-256',
|
|
400
|
+
sha384: 'SHA-384',
|
|
401
|
+
sha512: 'SHA-512',
|
|
402
|
+
}[this.options.algorithm]
|
|
403
|
+
|
|
404
|
+
const key = await crypto.subtle.importKey(
|
|
405
|
+
'raw',
|
|
406
|
+
keyData,
|
|
407
|
+
{ name: 'HMAC', hash: algorithmName },
|
|
408
|
+
false,
|
|
409
|
+
['sign'],
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
const signature = await crypto.subtle.sign('HMAC', key, messageData)
|
|
413
|
+
|
|
414
|
+
if (this.options.encoding === 'base64') {
|
|
415
|
+
return btoa(String.fromCharCode(...new Uint8Array(signature)))
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return Array.from(new Uint8Array(signature))
|
|
419
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
420
|
+
.join('')
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private timingSafeEqual(a: string, b: string): boolean {
|
|
424
|
+
if (a.length !== b.length) {
|
|
425
|
+
return false
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
let result = 0
|
|
429
|
+
for (let i = 0; i < a.length; i++) {
|
|
430
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return result === 0
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private defaultErrorResponse(error: SignatureError): Response {
|
|
437
|
+
return new Response(
|
|
438
|
+
JSON.stringify({
|
|
439
|
+
error: {
|
|
440
|
+
code: error.code,
|
|
441
|
+
message: error.message,
|
|
442
|
+
},
|
|
443
|
+
}),
|
|
444
|
+
{
|
|
445
|
+
status: error.statusCode,
|
|
446
|
+
headers: {
|
|
447
|
+
'Content-Type': 'application/json',
|
|
448
|
+
'WWW-Authenticate': 'HMAC-SHA256',
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Factory function to create request signing middleware
|
|
457
|
+
*/
|
|
458
|
+
export function requestSigning(options: RequestSigningOptions): RequestSigning {
|
|
459
|
+
return new RequestSigning(options)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Helper to sign a request (for clients)
|
|
464
|
+
*/
|
|
465
|
+
export async function signRequest(
|
|
466
|
+
request: Request,
|
|
467
|
+
secret: string,
|
|
468
|
+
options: {
|
|
469
|
+
algorithm?: SignatureAlgorithm
|
|
470
|
+
signedParts?: SignedPart[]
|
|
471
|
+
signedHeaders?: string[]
|
|
472
|
+
signatureHeader?: string
|
|
473
|
+
timestampHeader?: string
|
|
474
|
+
nonceHeader?: string
|
|
475
|
+
keyId?: string
|
|
476
|
+
keyIdHeader?: string
|
|
477
|
+
encoding?: 'hex' | 'base64'
|
|
478
|
+
} = {},
|
|
479
|
+
): Promise<Request> {
|
|
480
|
+
const {
|
|
481
|
+
algorithm = 'sha256',
|
|
482
|
+
signedParts = ['method', 'path', 'timestamp', 'body'],
|
|
483
|
+
signedHeaders = [],
|
|
484
|
+
signatureHeader = 'X-Signature',
|
|
485
|
+
timestampHeader = 'X-Timestamp',
|
|
486
|
+
nonceHeader = 'X-Nonce',
|
|
487
|
+
keyId,
|
|
488
|
+
keyIdHeader = 'X-Key-Id',
|
|
489
|
+
encoding = 'hex',
|
|
490
|
+
} = options
|
|
491
|
+
|
|
492
|
+
const timestamp = Math.floor(Date.now() / 1000).toString()
|
|
493
|
+
const nonce = crypto.randomUUID()
|
|
494
|
+
|
|
495
|
+
const url = new URL(request.url)
|
|
496
|
+
const parts: string[] = []
|
|
497
|
+
|
|
498
|
+
for (const part of signedParts) {
|
|
499
|
+
switch (part) {
|
|
500
|
+
case 'method':
|
|
501
|
+
parts.push(request.method.toUpperCase())
|
|
502
|
+
break
|
|
503
|
+
case 'path':
|
|
504
|
+
parts.push(url.pathname)
|
|
505
|
+
break
|
|
506
|
+
case 'query':
|
|
507
|
+
parts.push(url.search)
|
|
508
|
+
break
|
|
509
|
+
case 'timestamp':
|
|
510
|
+
parts.push(timestamp)
|
|
511
|
+
break
|
|
512
|
+
case 'nonce':
|
|
513
|
+
parts.push(nonce)
|
|
514
|
+
break
|
|
515
|
+
case 'body':
|
|
516
|
+
if (request.body) {
|
|
517
|
+
const body = await request.clone().text()
|
|
518
|
+
parts.push(body)
|
|
519
|
+
}
|
|
520
|
+
break
|
|
521
|
+
case 'headers':
|
|
522
|
+
for (const header of signedHeaders) {
|
|
523
|
+
const value = request.headers.get(header)
|
|
524
|
+
if (value) {
|
|
525
|
+
parts.push(`${header.toLowerCase()}:${value}`)
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
break
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const stringToSign = parts.join('\n')
|
|
533
|
+
|
|
534
|
+
// Calculate signature
|
|
535
|
+
const encoder = new TextEncoder()
|
|
536
|
+
const keyData = encoder.encode(secret)
|
|
537
|
+
const messageData = encoder.encode(stringToSign)
|
|
538
|
+
|
|
539
|
+
const algorithmName = {
|
|
540
|
+
sha256: 'SHA-256',
|
|
541
|
+
sha384: 'SHA-384',
|
|
542
|
+
sha512: 'SHA-512',
|
|
543
|
+
}[algorithm]
|
|
544
|
+
|
|
545
|
+
const key = await crypto.subtle.importKey(
|
|
546
|
+
'raw',
|
|
547
|
+
keyData,
|
|
548
|
+
{ name: 'HMAC', hash: algorithmName },
|
|
549
|
+
false,
|
|
550
|
+
['sign'],
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
const signatureBuffer = await crypto.subtle.sign('HMAC', key, messageData)
|
|
554
|
+
|
|
555
|
+
let signature: string
|
|
556
|
+
if (encoding === 'base64') {
|
|
557
|
+
signature = btoa(String.fromCharCode(...new Uint8Array(signatureBuffer)))
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
signature = Array.from(new Uint8Array(signatureBuffer))
|
|
561
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
562
|
+
.join('')
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Create new request with signature headers
|
|
566
|
+
const headers = new Headers(request.headers)
|
|
567
|
+
headers.set(signatureHeader, signature)
|
|
568
|
+
headers.set(timestampHeader, timestamp)
|
|
569
|
+
headers.set(nonceHeader, nonce)
|
|
570
|
+
|
|
571
|
+
if (keyId) {
|
|
572
|
+
headers.set(keyIdHeader, keyId)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return new Request(request.url, {
|
|
576
|
+
method: request.method,
|
|
577
|
+
headers,
|
|
578
|
+
body: request.body,
|
|
579
|
+
redirect: request.redirect,
|
|
580
|
+
integrity: request.integrity,
|
|
581
|
+
signal: request.signal,
|
|
582
|
+
})
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Verify a signature (utility function)
|
|
587
|
+
*/
|
|
588
|
+
export async function verifySignature(
|
|
589
|
+
stringToSign: string,
|
|
590
|
+
signature: string,
|
|
591
|
+
secret: string,
|
|
592
|
+
algorithm: SignatureAlgorithm = 'sha256',
|
|
593
|
+
encoding: 'hex' | 'base64' = 'hex',
|
|
594
|
+
): Promise<boolean> {
|
|
595
|
+
const encoder = new TextEncoder()
|
|
596
|
+
const keyData = encoder.encode(secret)
|
|
597
|
+
const messageData = encoder.encode(stringToSign)
|
|
598
|
+
|
|
599
|
+
const algorithmName = {
|
|
600
|
+
sha256: 'SHA-256',
|
|
601
|
+
sha384: 'SHA-384',
|
|
602
|
+
sha512: 'SHA-512',
|
|
603
|
+
}[algorithm]
|
|
604
|
+
|
|
605
|
+
const key = await crypto.subtle.importKey(
|
|
606
|
+
'raw',
|
|
607
|
+
keyData,
|
|
608
|
+
{ name: 'HMAC', hash: algorithmName },
|
|
609
|
+
false,
|
|
610
|
+
['sign'],
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
const signatureBuffer = await crypto.subtle.sign('HMAC', key, messageData)
|
|
614
|
+
|
|
615
|
+
let expectedSignature: string
|
|
616
|
+
if (encoding === 'base64') {
|
|
617
|
+
expectedSignature = btoa(String.fromCharCode(...new Uint8Array(signatureBuffer)))
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
expectedSignature = Array.from(new Uint8Array(signatureBuffer))
|
|
621
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
622
|
+
.join('')
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Timing-safe comparison
|
|
626
|
+
if (signature.length !== expectedSignature.length) {
|
|
627
|
+
return false
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
let result = 0
|
|
631
|
+
for (let i = 0; i < signature.length; i++) {
|
|
632
|
+
result |= signature.charCodeAt(i) ^ expectedSignature.charCodeAt(i)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return result === 0
|
|
636
|
+
}
|