@stacksjs/bun-router 0.0.16 → 0.0.17

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 (83) hide show
  1. package/dist/auth.d.ts +22 -8
  2. package/dist/chunk-1ahs68ys.js +18 -0
  3. package/dist/{chunk-0145g1me.js → chunk-cgptvjdf.js} +147 -75
  4. package/dist/chunk-g3ybefhg.js +1923 -0
  5. package/dist/cli.js +3 -2
  6. package/dist/container/container.d.ts +1 -0
  7. package/dist/container/contextual-binding.d.ts +0 -1
  8. package/dist/container/decorators.d.ts +2 -5
  9. package/dist/container/index.d.ts +16 -0
  10. package/dist/container/index.js +322 -0
  11. package/dist/file-serving/static-files.d.ts +16 -0
  12. package/dist/index.d.ts +17 -1
  13. package/dist/index.js +1159 -1380
  14. package/dist/middleware/cors.d.ts +9 -0
  15. package/dist/middleware/csrf.d.ts +7 -0
  16. package/dist/middleware/ddos_protection.d.ts +16 -1
  17. package/dist/middleware/rate_limit.d.ts +7 -1
  18. package/dist/middleware/request_tracer.d.ts +15 -9
  19. package/dist/middleware/response_cache.d.ts +2 -1
  20. package/dist/request/context.d.ts +12 -1
  21. package/dist/request/macros.d.ts +40 -2
  22. package/dist/response/macros.d.ts +3 -3
  23. package/dist/router/fluent-routing.d.ts +50 -13
  24. package/dist/router/handler-resolver.d.ts +8 -0
  25. package/dist/router/index.d.ts +2 -0
  26. package/dist/router/route-compiler.d.ts +8 -10
  27. package/dist/router/route-trie.d.ts +7 -1
  28. package/dist/router/router.d.ts +13 -2
  29. package/dist/router/validation-integration.d.ts +10 -4
  30. package/dist/types/middleware-types.d.ts +1 -1
  31. package/dist/types/route-inference.d.ts +9 -2
  32. package/dist/types.d.ts +62 -9
  33. package/dist/utils.d.ts +1 -1
  34. package/package.json +11 -1
  35. package/src/auth.ts +32 -12
  36. package/src/container/container.ts +58 -12
  37. package/src/container/contextual-binding.ts +4 -1
  38. package/src/container/decorators.ts +16 -7
  39. package/src/container/index.ts +49 -0
  40. package/src/errors/circuit-breaker.ts +3 -2
  41. package/src/errors/graceful-degradation.ts +2 -0
  42. package/src/file-serving/static-files.ts +144 -7
  43. package/src/index.ts +53 -1
  44. package/src/middleware/cors.ts +76 -8
  45. package/src/middleware/csrf.ts +70 -22
  46. package/src/middleware/ddos_protection.ts +27 -3
  47. package/src/middleware/input_validation.ts +10 -6
  48. package/src/middleware/performance_alerting.ts +2 -0
  49. package/src/middleware/performance_monitor.ts +2 -0
  50. package/src/middleware/rate_limit.ts +7 -1
  51. package/src/middleware/request_signing.ts +2 -0
  52. package/src/middleware/request_tracer.ts +34 -17
  53. package/src/middleware/response_cache.ts +48 -13
  54. package/src/middleware/session.ts +3 -3
  55. package/src/observability/metrics.ts +1 -1
  56. package/src/request/context.ts +50 -3
  57. package/src/request/enhanced-request.ts +20 -1
  58. package/src/request/macros.ts +183 -49
  59. package/src/response/macros.ts +3 -3
  60. package/src/router/file-based-routing.ts +17 -10
  61. package/src/router/fluent-routing.ts +117 -48
  62. package/src/router/group-organization.ts +17 -1
  63. package/src/router/handler-resolver.ts +36 -0
  64. package/src/router/http-methods.ts +33 -3
  65. package/src/router/index.ts +6 -0
  66. package/src/router/middleware.ts +3 -0
  67. package/src/router/optimized-route-matching.ts +58 -9
  68. package/src/router/route-compiler.ts +62 -73
  69. package/src/router/route-matching.ts +19 -0
  70. package/src/router/route-trie.ts +34 -48
  71. package/src/router/router.ts +72 -60
  72. package/src/router/server.ts +361 -139
  73. package/src/router/validation-integration.ts +11 -5
  74. package/src/testing/auth-testing.ts +6 -4
  75. package/src/types/middleware-types.ts +7 -4
  76. package/src/types/route-inference.ts +29 -14
  77. package/src/types.ts +63 -7
  78. package/src/url.ts +7 -2
  79. package/src/utils.ts +9 -6
  80. package/src/validation/validator.ts +7 -4
  81. package/src/websocket/clustering.ts +8 -1
  82. package/dist/router/fluent-router.d.ts +0 -315
  83. package/src/router/fluent-router.ts +0 -927
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Server } from 'bun';
2
2
  import type { Router } from './router/router';
3
+ import type { SessionManager } from './session/index';
3
4
  import type { QueryPreservationConfig } from './utils/query-preservation';
4
5
  import type { AuthContext, AuthenticatedUser, BoundModels, CacheAdapter, CacheStats, CookieToDelete, CookieToSet, ErrorMetadata, FallbackError, FileInfo, FlashMessages, FormBody, InputValidationSchemas, InputValue, JsonBodyData, JwtHeader, JwtPayload, MetricEntry, MiddlewareParams, OAuth2Flows, OAuth2Profile, RequestContext, RequestInput, RouteMetadata, SanitizationOptions, SanitizationRule, SessionData, SessionSerializer, SessionStore, SSEConnection, StaticResponseBody, TemplateHelper, TemplateHelpers, TraceAttributes, TraceSpan, TypedServerWebSocket, User, ValidatedBody, ValidatedData, ValidationSchema, WebSocketData } from './types/core';
5
6
  export type { QueryPreservationConfig } from './utils/query-preservation';
@@ -702,6 +703,12 @@ export interface EnhancedRequest extends Request, Omit<RequestMacroMethods, 'ip'
702
703
  * Parsed JSON body (if Content-Type is application/json)
703
704
  */
704
705
  jsonBody?: Record<string, unknown>;
706
+ /**
707
+ * Raw unparsed request body cache, populated on first `rawBody()` read (or up
708
+ * front by a framework body parser). Kept so webhook signature checks get the
709
+ * exact bytes the client sent.
710
+ */
711
+ _rawBody?: string;
705
712
  /**
706
713
  * Form body data (if Content-Type is multipart/form-data or application/x-www-form-urlencoded)
707
714
  */
@@ -711,9 +718,11 @@ export interface EnhancedRequest extends Request, Omit<RequestMacroMethods, 'ip'
711
718
  */
712
719
  files?: UploadedFile[];
713
720
  /**
714
- * Session data (if session middleware is used)
721
+ * Session for the current request. The built-in session middleware
722
+ * attaches a `SessionManager` (with `get`/`set`/`flash`/… methods);
723
+ * testing utilities may attach plain `SessionData`.
715
724
  */
716
- session?: SessionData;
725
+ session?: SessionManager | SessionData;
717
726
  /**
718
727
  * User data (if authentication middleware is used)
719
728
  */
@@ -936,6 +945,13 @@ export interface Route {
936
945
  pattern?: {
937
946
  exec: (url: URL) => PatternMatchResult | null;
938
947
  };
948
+ /**
949
+ * Set to `false` (via `router.withoutNativeDispatch()`) to keep this
950
+ * route on the fetch-handler matcher even when the server runs with
951
+ * `nativeRoutes: true` — e.g. when it must respect registration-order
952
+ * precedence against an overlapping pattern.
953
+ */
954
+ nativeDispatch?: boolean;
939
955
  }
940
956
  /**
941
957
  * WebSocket handler configuration
@@ -967,6 +983,22 @@ export interface ServerOptions<T extends WebSocketData = WebSocketData> extends
967
983
  * These bypass the fetch handler entirely for zero-allocation serving (~15% faster).
968
984
  */
969
985
  static?: Record<string, Response>;
986
+ /**
987
+ * Opt-in: register compatible routes with `Bun.serve`'s native router
988
+ * (`routes`), skipping the fetch handler's URL parsing and route
989
+ * matching for them. Compatible means: standard HTTP method, no
990
+ * domain scoping, no `where()`/inline constraints, no optional
991
+ * params, and `{param}`s spanning whole segments. Everything else —
992
+ * incompatible routes, 404/405 handling, HEAD fallback, the generic
993
+ * OPTIONS preflight — still flows through the fetch handler with
994
+ * identical semantics.
995
+ *
996
+ * Caveat: Bun matches by specificity (exact > param > wildcard).
997
+ * Apps that rely on registration order between same-shape overlapping
998
+ * patterns should leave this off. Routes registered after `serve()`
999
+ * only join the native table after `reload()`.
1000
+ */
1001
+ nativeRoutes?: boolean;
970
1002
  /**
971
1003
  * Development mode options for Bun's dev server.
972
1004
  * When true, enables HMR and development features.
@@ -1085,11 +1117,30 @@ export type MiddlewareWithParams<T extends string = string> = T | `${T}:${string
1085
1117
  */
1086
1118
  export type RouteParam<T extends string> = T extends `{${infer P}}` ? P : never;
1087
1119
  /**
1088
- * Extract parameters from route path
1089
- */
1090
- export type ExtractRouteParams<T extends string> = T extends `${string}{${infer Param}}${infer Rest}` ? {
1091
- [K in Param]: string;
1092
- } & ExtractRouteParams<Rest> : object;
1120
+ * Strip an inline constraint from a parameter token:
1121
+ * `id:[0-9]+` → `id`, `slug` → `slug`
1122
+ */
1123
+ export type ExtractParamName<T extends string> = T extends `${infer Name}:${string}` ? Name : T;
1124
+ /**
1125
+ * Extract parameters from a route path into a narrowly-typed object.
1126
+ *
1127
+ * Handles the full parameter grammar:
1128
+ * - `{id}` → `{ id: string }`
1129
+ * - `{id?}` (optional) → `{ id?: string }`
1130
+ * - `{id:[0-9]+}` (inline constraint) → `{ id: string }`
1131
+ * - trailing `*` wildcard → `{ wildcard: string }`
1132
+ *
1133
+ * @example
1134
+ * type P = ExtractRouteParams<'/users/{userId}/posts/{slug?}'>
1135
+ * // ^? { userId: string } & { slug?: string }
1136
+ */
1137
+ export type ExtractRouteParams<T extends string> = T extends `${string}{${infer Param}}${infer Rest}` ? (Param extends `${infer Inner}?` ? {
1138
+ [K in ExtractParamName<Inner>]?: string;
1139
+ } : {
1140
+ [K in ExtractParamName<Param>]: string;
1141
+ }) & ExtractRouteParams<Rest> : T extends `${string}*${string}` ? {
1142
+ wildcard: string;
1143
+ } : object;
1093
1144
  /**
1094
1145
  * Route method constraints
1095
1146
  */
@@ -1520,8 +1571,8 @@ export interface StreamingOptions {
1520
1571
  /**
1521
1572
  * SSE (Server-Sent Events) data structure
1522
1573
  */
1523
- export interface SSEData {
1524
- data: any;
1574
+ export interface SSEData<T = unknown> {
1575
+ data: T;
1525
1576
  event?: string;
1526
1577
  id?: string;
1527
1578
  retry?: number;
@@ -1701,6 +1752,8 @@ export interface RequestMacroMethods {
1701
1752
  username: string;
1702
1753
  password: string;
1703
1754
  } | null;
1755
+ /** Raw unparsed request body as a string (cached). Needed for webhook signature verification (Stripe/GitHub/Slack), where a re-serialized jsonBody won't match the HMAC. */
1756
+ rawBody: () => Promise<string>;
1704
1757
  hasHeader: (name: string) => boolean;
1705
1758
  header: (name: string, defaultValue?: string) => string | null;
1706
1759
  allHeaders: () => Record<string, string>;
package/dist/utils.d.ts CHANGED
@@ -66,4 +66,4 @@ export declare function resolveViewPath(viewName: string, viewsPath: string, ext
66
66
  * @param template The HTML template string
67
67
  * @param data The data to inject into the template
68
68
  */
69
- export declare function processHtmlTemplate(template: string, data: Record<string, any>): string;
69
+ export declare function processHtmlTemplate(template: string, data: Record<string, unknown>): string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/bun-router",
3
3
  "type": "module",
4
- "version": "0.0.16",
4
+ "version": "0.0.17",
5
5
  "description": "A fast, type-safe router for Bun.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",
@@ -28,6 +28,11 @@
28
28
  "types": "./dist/index.d.ts",
29
29
  "import": "./dist/index.js"
30
30
  },
31
+ "./container": {
32
+ "bun": "./dist/container/index.js",
33
+ "types": "./dist/container/index.d.ts",
34
+ "import": "./dist/container/index.js"
35
+ },
31
36
  "./*": {
32
37
  "bun": "./dist/*",
33
38
  "import": "./dist/*"
@@ -40,6 +45,11 @@
40
45
  "types": "./dist/index.d.ts",
41
46
  "import": "./dist/index.js"
42
47
  },
48
+ "./container": {
49
+ "bun": "./dist/container/index.js",
50
+ "types": "./dist/container/index.d.ts",
51
+ "import": "./dist/container/index.js"
52
+ },
43
53
  "./*": {
44
54
  "bun": "./dist/*",
45
55
  "import": "./dist/*"
package/src/auth.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { EnhancedRequest, JwtPayload } from './types'
1
+ import type { EnhancedRequest, JwtHeader, JwtPayload } from './types'
2
+ import { timingSafeEqual } from 'node:crypto'
2
3
 
3
4
  // Types
4
5
  export interface JwtVerifyOptions {
@@ -22,10 +23,25 @@ export interface JwtSignOptions {
22
23
  keyid?: string
23
24
  jwtid?: string
24
25
  noTimestamp?: boolean
25
- header?: Record<string, any>
26
+ header?: Partial<JwtHeader>
26
27
  encoding?: string
27
28
  }
28
29
 
30
+ /**
31
+ * Token endpoint response for the OAuth2 authorization-code flow
32
+ * (RFC 6749 §5.1). Providers may include extra fields — they're preserved
33
+ * under the index signature.
34
+ */
35
+ export interface OAuth2TokenResponse {
36
+ access_token: string
37
+ token_type: string
38
+ expires_in?: number
39
+ refresh_token?: string
40
+ scope?: string
41
+ id_token?: string
42
+ [key: string]: string | number | undefined
43
+ }
44
+
29
45
  export interface ApiKeyOptions {
30
46
  source?: 'header' | 'query' | 'cookie'
31
47
  keyName?: string
@@ -74,7 +90,7 @@ export class JWT {
74
90
  /**
75
91
  * Sign payload into a JWT token
76
92
  */
77
- sign(payload: Record<string, any>, options: JwtSignOptions = {}): string {
93
+ sign(payload: JwtPayload, options: JwtSignOptions = {}): string {
78
94
  // Default header
79
95
  const header = {
80
96
  alg: options.algorithm || 'HS256',
@@ -132,7 +148,7 @@ export class JWT {
132
148
  /**
133
149
  * Verify a JWT token and return the decoded payload
134
150
  */
135
- verify(token: string, options: JwtVerifyOptions = {}): Record<string, any> | null {
151
+ verify(token: string, options: JwtVerifyOptions = {}): JwtPayload | null {
136
152
  try {
137
153
  const parts = token.split('.')
138
154
 
@@ -143,8 +159,8 @@ export class JWT {
143
159
  const [encodedHeader, encodedPayload, signature] = parts
144
160
 
145
161
  // Decode header and payload
146
- const header = JSON.parse(base64UrlDecode(encodedHeader))
147
- const payload = JSON.parse(base64UrlDecode(encodedPayload))
162
+ const header = JSON.parse(base64UrlDecode(encodedHeader)) as JwtHeader
163
+ const payload = JSON.parse(base64UrlDecode(encodedPayload)) as JwtPayload
148
164
 
149
165
  // Verify algorithm if specified
150
166
  if (options.algorithms && !options.algorithms.includes(header.alg)) {
@@ -156,7 +172,11 @@ export class JWT {
156
172
  const data = `${encodedHeader}.${encodedPayload}`
157
173
  const expectedSignature = this.createSignature(data, this.secret, header.alg)
158
174
 
159
- if (signature !== expectedSignature) {
175
+ // Constant-time comparison — `!==` leaks how much of the signature
176
+ // matched through response timing
177
+ const sigBuf = Buffer.from(signature)
178
+ const expectedBuf = Buffer.from(expectedSignature)
179
+ if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {
160
180
  throw new Error('Invalid signature')
161
181
  }
162
182
 
@@ -200,7 +220,7 @@ export class JWT {
200
220
  /**
201
221
  * Decode a JWT token without verifying the signature
202
222
  */
203
- decode(token: string): { header: Record<string, any>, payload: Record<string, any> } | null {
223
+ decode(token: string): { header: JwtHeader, payload: JwtPayload } | null {
204
224
  try {
205
225
  const parts = token.split('.')
206
226
 
@@ -402,7 +422,7 @@ export class OAuth2Helper {
402
422
  /**
403
423
  * Exchange authorization code for access token
404
424
  */
405
- async exchangeCodeForToken(code: string): Promise<Record<string, any>> {
425
+ async exchangeCodeForToken(code: string): Promise<OAuth2TokenResponse> {
406
426
  const params = new URLSearchParams({
407
427
  grant_type: 'authorization_code',
408
428
  code,
@@ -423,13 +443,13 @@ export class OAuth2Helper {
423
443
  throw new Error(`OAuth token exchange failed: ${response.status} ${response.statusText}`)
424
444
  }
425
445
 
426
- return await response.json() as Record<string, any>
446
+ return await response.json() as OAuth2TokenResponse
427
447
  }
428
448
 
429
449
  /**
430
450
  * Refresh an access token using a refresh token
431
451
  */
432
- async refreshToken(refreshToken: string): Promise<Record<string, any>> {
452
+ async refreshToken(refreshToken: string): Promise<OAuth2TokenResponse> {
433
453
  const params = new URLSearchParams({
434
454
  grant_type: 'refresh_token',
435
455
  refresh_token: refreshToken,
@@ -449,7 +469,7 @@ export class OAuth2Helper {
449
469
  throw new Error(`OAuth token refresh failed: ${response.status} ${response.statusText}`)
450
470
  }
451
471
 
452
- return await response.json() as Record<string, any>
472
+ return await response.json() as OAuth2TokenResponse
453
473
  }
454
474
  }
455
475
 
@@ -86,6 +86,9 @@ export interface InjectMetadata {
86
86
  */
87
87
  export class Container {
88
88
  private bindings = new Map<Token, Binding>()
89
+ // Tag → tokens index, maintained on register/unbind so tagged lookups
90
+ // are O(matches) instead of scanning every binding
91
+ private tagIndex = new Map<string, Set<Token>>()
89
92
  private instances = new Map<Token, any>()
90
93
  private scopedInstances = new Map<string, Map<Token, any>>()
91
94
  private interceptors = new Map<Token, ((..._args: any[]) => any)[]>()
@@ -115,7 +118,26 @@ export class Container {
115
118
  * Register a binding
116
119
  */
117
120
  register<T>(binding: Binding<T>): this {
121
+ // Re-registering a token replaces its tag index entries
122
+ const existing = this.bindings.get(binding.token)
123
+ if (existing?.metadata?.tags) {
124
+ for (const tag of existing.metadata.tags) {
125
+ this.tagIndex.get(tag)?.delete(binding.token)
126
+ }
127
+ }
128
+
118
129
  this.bindings.set(binding.token, binding)
130
+
131
+ if (binding.metadata?.tags) {
132
+ for (const tag of binding.metadata.tags) {
133
+ let tokens = this.tagIndex.get(tag)
134
+ if (!tokens) {
135
+ tokens = new Set()
136
+ this.tagIndex.set(tag, tokens)
137
+ }
138
+ tokens.add(binding.token)
139
+ }
140
+ }
119
141
  return this
120
142
  }
121
143
 
@@ -177,31 +199,48 @@ export class Container {
177
199
  * Resolve all instances of a token (useful for arrays of services)
178
200
  */
179
201
  resolveAll<T>(token: Token): T[] {
180
- const bindings = Array.from(this.bindings.values()).filter(
181
- binding => binding.token === token || this.hasTag(binding, token as string),
182
- )
202
+ // Direct binding plus anything tagged with the token (when it's a
203
+ // string), deduplicated by token
204
+ const tokens = new Set<Token>()
205
+ if (this.bindings.has(token)) {
206
+ tokens.add(token)
207
+ }
208
+ if (typeof token === 'string') {
209
+ for (const tagged of this.tagIndex.get(token) ?? []) {
210
+ tokens.add(tagged)
211
+ }
212
+ }
183
213
 
184
- return bindings.map(binding => this.resolve<T>(binding.token))
214
+ return Array.from(tokens, t => this.resolve<T>(t))
185
215
  }
186
216
 
187
217
  /**
188
218
  * Resolve with tags
189
219
  */
190
220
  resolveTagged<T>(tag: string): T[] {
191
- const bindings = Array.from(this.bindings.values()).filter(
192
- binding => this.hasTag(binding, tag),
193
- )
194
-
195
- return bindings.map(binding => this.resolve<T>(binding.token))
221
+ const tokens = this.tagIndex.get(tag)
222
+ if (!tokens || tokens.size === 0) {
223
+ return []
224
+ }
225
+ return Array.from(tokens, token => this.resolve<T>(token))
196
226
  }
197
227
 
198
228
  /**
199
229
  * Get all bindings with a specific tag
200
230
  */
201
231
  getTaggedBindings<T>(tag: string): Binding<T>[] {
202
- return Array.from(this.bindings.values()).filter(
203
- (binding): binding is Binding<T> => binding.metadata?.tags?.includes(tag) ?? false,
204
- )
232
+ const tokens = this.tagIndex.get(tag)
233
+ if (!tokens || tokens.size === 0) {
234
+ return []
235
+ }
236
+ const bindings: Binding<T>[] = []
237
+ for (const token of tokens) {
238
+ const binding = this.bindings.get(token)
239
+ if (binding) {
240
+ bindings.push(binding as Binding<T>)
241
+ }
242
+ }
243
+ return bindings
205
244
  }
206
245
 
207
246
  /**
@@ -241,6 +280,12 @@ export class Container {
241
280
  * Unbind a token
242
281
  */
243
282
  unbind(token: Token): this {
283
+ const binding = this.bindings.get(token)
284
+ if (binding?.metadata?.tags) {
285
+ for (const tag of binding.metadata.tags) {
286
+ this.tagIndex.get(tag)?.delete(token)
287
+ }
288
+ }
244
289
  this.bindings.delete(token)
245
290
  this.instances.delete(token)
246
291
  return this
@@ -277,6 +322,7 @@ export class Container {
277
322
  */
278
323
  clear(): this {
279
324
  this.bindings.clear()
325
+ this.tagIndex.clear()
280
326
  this.instances.clear()
281
327
  this.scopedInstances.clear()
282
328
  return this
@@ -600,4 +600,7 @@ export class BindingConditions {
600
600
  }
601
601
  }
602
602
 
603
- export { ContextualContainer as DIContainer }
603
+ // NOTE: this module used to also alias `ContextualContainer` as
604
+ // `DIContainer`, colliding with container.ts's `Container as DIContainer`
605
+ // alias — two different classes under one public name. The base
606
+ // container keeps the alias; use `ContextualContainer` explicitly here.
@@ -4,13 +4,22 @@
4
4
  */
5
5
 
6
6
  import type { ResolutionContext } from './container'
7
- import { Container } from './container'
8
-
9
- // Metadata keys with explicit types
10
- export const INJECTABLE_METADATA_KEY: unique symbol = Symbol('injectable')
11
- export const INJECT_METADATA_KEY: unique symbol = Symbol('inject')
12
- export const OPTIONAL_METADATA_KEY: unique symbol = Symbol('optional')
13
- export const TAGGED_METADATA_KEY: unique symbol = Symbol('tagged')
7
+ import {
8
+ Container,
9
+ INJECT_METADATA_KEY,
10
+ INJECTABLE_METADATA_KEY,
11
+ OPTIONAL_METADATA_KEY,
12
+ TAGGED_METADATA_KEY,
13
+ } from './container'
14
+
15
+ // The injection metadata keys MUST be the container's own symbols — this
16
+ // module previously declared its own `Symbol('inject')` etc., so metadata
17
+ // written by `@Inject` was invisible to `Container.getConstructorDependencies`
18
+ // (which reads with the container's symbols) and constructor injection
19
+ // silently resolved nothing. Re-exported for backward compatibility.
20
+ export { INJECT_METADATA_KEY, INJECTABLE_METADATA_KEY, OPTIONAL_METADATA_KEY, TAGGED_METADATA_KEY }
21
+
22
+ // Decorator-specific metadata keys
14
23
  export const CONTROLLER_METADATA_KEY: unique symbol = Symbol('controller')
15
24
  export const ROUTE_METADATA_KEY: unique symbol = Symbol('route')
16
25
  export const MIDDLEWARE_METADATA_KEY: unique symbol = Symbol('middleware')
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Dependency-injection container — public entry point.
3
+ *
4
+ * ```ts
5
+ * import { Container, createContainer, Inject, Injectable } from '@stacksjs/bun-router/container'
6
+ * ```
7
+ *
8
+ * The core `Container` (and a few companions) are also re-exported from the
9
+ * package root; the decorators live only here because their names (`Get`,
10
+ * `Post`, `Controller`, …) would collide with the router's exports.
11
+ */
12
+
13
+ export * from './container'
14
+
15
+ // Decorators. The duplicate `InjectableMetadata`/`InjectMetadata` interface
16
+ // declarations stay out of the barrel — the canonical ones come from
17
+ // `./container` above.
18
+ export {
19
+ Body,
20
+ Controller,
21
+ CONTROLLER_METADATA_KEY,
22
+ Cookie,
23
+ decoratorContainer,
24
+ DecoratorContainer,
25
+ Delete,
26
+ Get,
27
+ Head,
28
+ Header,
29
+ Inject,
30
+ Injectable,
31
+ InjectParam,
32
+ MetadataReader,
33
+ MIDDLEWARE_METADATA_KEY,
34
+ Optional,
35
+ Options,
36
+ Param,
37
+ PARAM_METADATA_KEY,
38
+ Patch,
39
+ Post,
40
+ Put,
41
+ Query,
42
+ ROUTE_METADATA_KEY,
43
+ Tagged,
44
+ UseMiddleware,
45
+ } from './decorators'
46
+ export type { ControllerMetadata, ParamMetadata, RouteMetadata } from './decorators'
47
+
48
+ export * from './contextual-binding'
49
+ export * from './service-provider'
@@ -87,8 +87,9 @@ export class CircuitBreaker {
87
87
  consecutiveSuccesses: 0,
88
88
  }
89
89
 
90
- // Clean up old requests periodically
91
- setInterval(() => this.cleanupOldRequests(), this.config.monitoringPeriod / 4)
90
+ // Clean up old requests periodically (unref'd so the timer never
91
+ // keeps the process alive)
92
+ setInterval(() => this.cleanupOldRequests(), this.config.monitoringPeriod / 4).unref?.()
92
93
  }
93
94
 
94
95
  /**
@@ -119,6 +119,8 @@ export class ServiceHealthMonitor {
119
119
  () => this.performHealthCheck(serviceName, healthConfig),
120
120
  healthConfig.interval,
121
121
  )
122
+ // Health checks must not keep the process alive on shutdown
123
+ interval.unref?.()
122
124
  this.healthCheckIntervals.set(serviceName, interval)
123
125
  }
124
126
  }