stratal 0.0.19 → 0.0.21

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 (163) hide show
  1. package/dist/{base-email.provider-mjynzewK.mjs → base-email.provider-CfQCA08m.mjs} +1 -1
  2. package/dist/{base-email.provider-mjynzewK.mjs.map → base-email.provider-CfQCA08m.mjs.map} +1 -1
  3. package/dist/bin/cloudflare-workers-loader.mjs.map +1 -1
  4. package/dist/bin/quarry.mjs +6 -0
  5. package/dist/bin/quarry.mjs.map +1 -1
  6. package/dist/cache/index.d.mts +2 -154
  7. package/dist/cache/index.d.mts.map +1 -1
  8. package/dist/cache/index.mjs +6 -6
  9. package/dist/cache/index.mjs.map +1 -1
  10. package/dist/cache.service-DsnKuNyO.d.mts +156 -0
  11. package/dist/cache.service-DsnKuNyO.d.mts.map +1 -0
  12. package/dist/cache.tokens-B7Rw1C9Q.mjs +6 -0
  13. package/dist/cache.tokens-B7Rw1C9Q.mjs.map +1 -0
  14. package/dist/command-BgSlsS4M.mjs.map +1 -1
  15. package/dist/{command-DsQq56Lp.d.mts → command-Cmmf0oHX.d.mts} +2 -2
  16. package/dist/{command-DsQq56Lp.d.mts.map → command-Cmmf0oHX.d.mts.map} +1 -1
  17. package/dist/config/index.d.mts +3 -3
  18. package/dist/config/index.mjs +5 -3
  19. package/dist/config/index.mjs.map +1 -1
  20. package/dist/{consumer-registry-Doom7BEh.d.mts → consumer-registry-B7yUNh0q.d.mts} +1 -1
  21. package/dist/{consumer-registry-Doom7BEh.d.mts.map → consumer-registry-B7yUNh0q.d.mts.map} +1 -1
  22. package/dist/{controller.decorator-LZY9aHYG.mjs → controller.decorator-B9vwn0zK.mjs} +3 -3
  23. package/dist/{controller.decorator-LZY9aHYG.mjs.map → controller.decorator-B9vwn0zK.mjs.map} +1 -1
  24. package/dist/cron/index.d.mts +2 -2
  25. package/dist/cron/index.mjs +1 -1
  26. package/dist/{cron-manager-RuPtFVLy.d.mts → cron-manager-CmTimEjf.d.mts} +3 -3
  27. package/dist/cron-manager-CmTimEjf.d.mts.map +1 -0
  28. package/dist/{cron-manager-C30t9UZM.mjs → cron-manager-DQSK8uoV.mjs} +10 -4
  29. package/dist/cron-manager-DQSK8uoV.mjs.map +1 -0
  30. package/dist/di/index.d.mts +1 -1
  31. package/dist/di/index.mjs +2 -2
  32. package/dist/email/index.d.mts +3 -3
  33. package/dist/email/index.mjs +13 -8
  34. package/dist/email/index.mjs.map +1 -1
  35. package/dist/{en-rHmW6vD9.mjs → en-DSH_bhh6.mjs} +7 -1
  36. package/dist/en-DSH_bhh6.mjs.map +1 -0
  37. package/dist/{env-CamWD-U1.d.mts → env-D1rcZ8_r.d.mts} +1 -1
  38. package/dist/env-D1rcZ8_r.d.mts.map +1 -0
  39. package/dist/errors/index.d.mts +1 -1
  40. package/dist/errors/index.mjs +1 -1
  41. package/dist/{errors-B4pYgYON.mjs → errors-COW9-Mar.mjs} +35 -10
  42. package/dist/errors-COW9-Mar.mjs.map +1 -0
  43. package/dist/{errors-BUyUfr2Z.mjs → errors-ORxu1-Bb.mjs} +2 -2
  44. package/dist/{errors-BUyUfr2Z.mjs.map → errors-ORxu1-Bb.mjs.map} +1 -1
  45. package/dist/events/index.d.mts +2 -2
  46. package/dist/events/index.mjs +1 -1
  47. package/dist/{events-COKixqnG.mjs → events-CzCV8jI8.mjs} +4 -2
  48. package/dist/{events-COKixqnG.mjs.map → events-CzCV8jI8.mjs.map} +1 -1
  49. package/dist/{gateway-context-cqZ8wMoi.mjs → gateway-context-CXmXtaUP.mjs} +5 -8
  50. package/dist/{gateway-context-cqZ8wMoi.mjs.map → gateway-context-CXmXtaUP.mjs.map} +1 -1
  51. package/dist/guards/index.d.mts +3 -3
  52. package/dist/guards/index.mjs +1 -1
  53. package/dist/{guards-DMbsAxSX.mjs → guards-DU1_J9YA.mjs} +2 -1
  54. package/dist/{guards-DMbsAxSX.mjs.map → guards-DU1_J9YA.mjs.map} +1 -1
  55. package/dist/{http-method.decorator-BT3ufnz8.mjs → http-method.decorator-BrgHMdLQ.mjs} +3 -3
  56. package/dist/{http-method.decorator-BT3ufnz8.mjs.map → http-method.decorator-BrgHMdLQ.mjs.map} +1 -1
  57. package/dist/i18n/index.d.mts +2 -2
  58. package/dist/i18n/index.mjs +2 -2
  59. package/dist/i18n/messages/en/index.d.mts +1 -1
  60. package/dist/i18n/messages/en/index.mjs +1 -1
  61. package/dist/i18n/utils/index.mjs +1 -1
  62. package/dist/i18n/validation/index.d.mts +2 -2
  63. package/dist/i18n/validation/index.mjs +2 -2
  64. package/dist/{i18n.module-CI_prYFD.mjs → i18n.module-CzXLW9Hy.mjs} +242 -50
  65. package/dist/i18n.module-CzXLW9Hy.mjs.map +1 -0
  66. package/dist/{index-SHx31sBJ.d.mts → index-7-hU3GTV.d.mts} +1 -1
  67. package/dist/{index-SHx31sBJ.d.mts.map → index-7-hU3GTV.d.mts.map} +1 -1
  68. package/dist/{index-B437eK7p.d.mts → index-Bnpfq6uk.d.mts} +58 -10
  69. package/dist/index-Bnpfq6uk.d.mts.map +1 -0
  70. package/dist/{index-DPFqRs8L.d.mts → index-ByOyTmqf.d.mts} +317 -205
  71. package/dist/index-ByOyTmqf.d.mts.map +1 -0
  72. package/dist/{index-DFhEeFfC.d.mts → index-C1KvMncZ.d.mts} +7 -1
  73. package/dist/{index-DFhEeFfC.d.mts.map → index-C1KvMncZ.d.mts.map} +1 -1
  74. package/dist/{index-CWRS7Ri3.d.mts → index-DBd_2wv8.d.mts} +1 -1
  75. package/dist/{index-CWRS7Ri3.d.mts.map → index-DBd_2wv8.d.mts.map} +1 -1
  76. package/dist/{index-Dnqm9ZB6.d.mts → index-DUzWs0z7.d.mts} +5 -5
  77. package/dist/{index-Dnqm9ZB6.d.mts.map → index-DUzWs0z7.d.mts.map} +1 -1
  78. package/dist/index.d.mts +3 -3
  79. package/dist/index.mjs +1 -1
  80. package/dist/is-command-C6a7WTPw.mjs.map +1 -1
  81. package/dist/is-seeder-CebjZCDn.mjs.map +1 -1
  82. package/dist/logger/index.d.mts +1 -1
  83. package/dist/logger/index.mjs +1 -1
  84. package/dist/{logger-V6Ms3QnQ.mjs → logger-DlV7NtvD.mjs} +8 -4
  85. package/dist/{logger-V6Ms3QnQ.mjs.map → logger-DlV7NtvD.mjs.map} +1 -1
  86. package/dist/macroable/index.d.mts +1 -1
  87. package/dist/macroable-BmufBshB.mjs.map +1 -1
  88. package/dist/module/index.d.mts +2 -2
  89. package/dist/module/index.mjs +1 -1
  90. package/dist/{module-qGE_1duv.mjs → module-BzLg57FK.mjs} +139 -5
  91. package/dist/module-BzLg57FK.mjs.map +1 -0
  92. package/dist/openapi/index.d.mts +3 -3
  93. package/dist/openapi/index.mjs +2 -2
  94. package/dist/{openapi-tools.service-CYWGuhue.mjs → openapi-tools.service-Zs-Ewv7F.mjs} +1 -1
  95. package/dist/{openapi-tools.service-CYWGuhue.mjs.map → openapi-tools.service-Zs-Ewv7F.mjs.map} +1 -1
  96. package/dist/{openapi.service-Bv_NioM9.d.mts → openapi.service-Bt9bCIrd.d.mts} +3 -3
  97. package/dist/{openapi.service-Bv_NioM9.d.mts.map → openapi.service-Bt9bCIrd.d.mts.map} +1 -1
  98. package/dist/quarry/index.d.mts +6 -6
  99. package/dist/quarry/index.mjs +2 -2
  100. package/dist/{quarry-registry-DFfRRkA7.mjs → quarry-registry-BwY2hOxm.mjs} +18 -7
  101. package/dist/{quarry-registry-DFfRRkA7.mjs.map → quarry-registry-BwY2hOxm.mjs.map} +1 -1
  102. package/dist/queue/index.d.mts +2 -2
  103. package/dist/queue/index.mjs +3 -2
  104. package/dist/queue/index.mjs.map +1 -1
  105. package/dist/{queue.module-P-G-nCYz.mjs → queue.module-BhCjZp6H.mjs} +13 -4
  106. package/dist/{queue.module-P-G-nCYz.mjs.map → queue.module-BhCjZp6H.mjs.map} +1 -1
  107. package/dist/{r2-storage.provider-LdzK9tfG.mjs → r2-storage.provider-DuonKeYm.mjs} +6 -3
  108. package/dist/{r2-storage.provider-LdzK9tfG.mjs.map → r2-storage.provider-DuonKeYm.mjs.map} +1 -1
  109. package/dist/rate-limit.decorator-6qzNcSOt.mjs +55 -0
  110. package/dist/rate-limit.decorator-6qzNcSOt.mjs.map +1 -0
  111. package/dist/rate-limiter/index.d.mts +420 -0
  112. package/dist/rate-limiter/index.d.mts.map +1 -0
  113. package/dist/rate-limiter/index.mjs +374 -0
  114. package/dist/rate-limiter/index.mjs.map +1 -0
  115. package/dist/{resend.provider-bwILp0WI.mjs → resend.provider-DB4IlFjG.mjs} +3 -2
  116. package/dist/{resend.provider-bwILp0WI.mjs.map → resend.provider-DB4IlFjG.mjs.map} +1 -1
  117. package/dist/router/index.d.mts +2 -2
  118. package/dist/router/index.mjs +6 -6
  119. package/dist/seeder/index.d.mts +3 -3
  120. package/dist/seeder/index.mjs +1 -1
  121. package/dist/{seeder-BcqIFa2X.mjs → seeder-zoEfEw9i.mjs} +6 -3
  122. package/dist/{seeder-BcqIFa2X.mjs.map → seeder-zoEfEw9i.mjs.map} +1 -1
  123. package/dist/{setup-CtekcwuO.mjs → setup-CefZKV_e.mjs} +1 -1
  124. package/dist/{setup-CtekcwuO.mjs.map → setup-CefZKV_e.mjs.map} +1 -1
  125. package/dist/{signed-url-COX7cCWR.mjs → signed-url-BQPbv2In.mjs} +1 -1
  126. package/dist/{signed-url-COX7cCWR.mjs.map → signed-url-BQPbv2In.mjs.map} +1 -1
  127. package/dist/{smtp.provider-B07yuARi.mjs → smtp.provider-B6D7zuWX.mjs} +3 -2
  128. package/dist/{smtp.provider-B07yuARi.mjs.map → smtp.provider-B6D7zuWX.mjs.map} +1 -1
  129. package/dist/storage/index.d.mts +3 -3
  130. package/dist/storage/index.mjs +2 -2
  131. package/dist/storage/providers/index.d.mts +2 -2
  132. package/dist/storage/providers/index.mjs +1 -1
  133. package/dist/{storage-P6X4h9So.mjs → storage-D8CBP72Z.mjs} +14 -9
  134. package/dist/{storage-P6X4h9So.mjs.map → storage-D8CBP72Z.mjs.map} +1 -1
  135. package/dist/{storage-provider.interface-CC1nniHk.d.mts → storage-provider.interface-Bd6vA4ak.d.mts} +2 -2
  136. package/dist/{storage-provider.interface-CC1nniHk.d.mts.map → storage-provider.interface-Bd6vA4ak.d.mts.map} +1 -1
  137. package/dist/{stratal-BCiwCFN9.mjs → stratal-CNwpbSZl.mjs} +12 -10
  138. package/dist/stratal-CNwpbSZl.mjs.map +1 -0
  139. package/dist/{types-DIWemRad.d.mts → types-cySNS_lp.d.mts} +1 -1
  140. package/dist/types-cySNS_lp.d.mts.map +1 -0
  141. package/dist/{usage-generator-MBcRo0Q2.mjs → usage-generator-BUdlhnCK.mjs} +1 -1
  142. package/dist/{usage-generator-MBcRo0Q2.mjs.map → usage-generator-BUdlhnCK.mjs.map} +1 -1
  143. package/dist/{validation-Dbg3ehdP.mjs → validation-DtJwAv7O.mjs} +62 -8
  144. package/dist/validation-DtJwAv7O.mjs.map +1 -0
  145. package/dist/websocket/index.d.mts +8 -3
  146. package/dist/websocket/index.d.mts.map +1 -1
  147. package/dist/websocket/index.mjs +1 -1
  148. package/dist/workers/index.d.mts +2 -2
  149. package/dist/workers/index.mjs +2 -2
  150. package/dist/workers/index.mjs.map +1 -1
  151. package/package.json +18 -14
  152. package/dist/cron-manager-C30t9UZM.mjs.map +0 -1
  153. package/dist/cron-manager-RuPtFVLy.d.mts.map +0 -1
  154. package/dist/en-rHmW6vD9.mjs.map +0 -1
  155. package/dist/env-CamWD-U1.d.mts.map +0 -1
  156. package/dist/errors-B4pYgYON.mjs.map +0 -1
  157. package/dist/i18n.module-CI_prYFD.mjs.map +0 -1
  158. package/dist/index-B437eK7p.d.mts.map +0 -1
  159. package/dist/index-DPFqRs8L.d.mts.map +0 -1
  160. package/dist/module-qGE_1duv.mjs.map +0 -1
  161. package/dist/stratal-BCiwCFN9.mjs.map +0 -1
  162. package/dist/types-DIWemRad.d.mts.map +0 -1
  163. package/dist/validation-Dbg3ehdP.mjs.map +0 -1
@@ -0,0 +1,55 @@
1
+ import { f as ROUTE_METADATA_KEYS } from "./errors-COW9-Mar.mjs";
2
+ //#region src/rate-limiter/decorators/rate-limit.decorator.ts
3
+ const KEY = ROUTE_METADATA_KEYS.RATE_LIMIT;
4
+ /**
5
+ * Apply a named rate limiter to a controller class or a single route method.
6
+ *
7
+ * Stacks: multiple `@RateLimit` decorators on the same target push onto
8
+ * the metadata array — every named limiter is enforced. Class-level limits
9
+ * run before method-level limits in the resulting middleware chain.
10
+ *
11
+ * The named limiter must be registered separately via
12
+ * `RateLimiterRegistry.for('name', resolver)` (typically inside a
13
+ * module's `onInitialize` hook) and the user must import
14
+ * `RateLimiterModule.forRoot({ store: ... })` in their AppModule.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * @Controller('/api/v1/users')
19
+ * @RateLimit('api')
20
+ * export class UsersController {
21
+ * @Get('/')
22
+ * list(ctx: RouterContext) { ... }
23
+ *
24
+ * @Post('/')
25
+ * @RateLimit('writes') // stacks with class-level 'api'
26
+ * create(ctx: RouterContext) { ... }
27
+ * }
28
+ * ```
29
+ */
30
+ function RateLimit(name) {
31
+ return (target, propertyKey) => {
32
+ if (propertyKey === void 0) {
33
+ const existing = Reflect.getOwnMetadata(KEY, target) ?? [];
34
+ Reflect.defineMetadata(KEY, [...existing, name], target);
35
+ } else {
36
+ const existing = Reflect.getOwnMetadata(KEY, target, propertyKey) ?? [];
37
+ Reflect.defineMetadata(KEY, [...existing, name], target, propertyKey);
38
+ }
39
+ };
40
+ }
41
+ /**
42
+ * Read the rate-limit names attached to a class or method via `@RateLimit`.
43
+ * Returns an empty array when no decorator was applied.
44
+ *
45
+ * @param target - For class metadata, pass the controller constructor.
46
+ * For method metadata, pass `Controller.prototype` and the method name.
47
+ */
48
+ function getRateLimits(target, propertyKey) {
49
+ const meta = propertyKey === void 0 ? Reflect.getMetadata(KEY, target) : Reflect.getMetadata(KEY, target, propertyKey);
50
+ return Array.isArray(meta) ? meta : [];
51
+ }
52
+ //#endregion
53
+ export { getRateLimits as n, RateLimit as t };
54
+
55
+ //# sourceMappingURL=rate-limit.decorator-6qzNcSOt.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.decorator-6qzNcSOt.mjs","names":[],"sources":["../src/rate-limiter/decorators/rate-limit.decorator.ts"],"sourcesContent":["import { ROUTE_METADATA_KEYS } from '../../router/constants'\n\nconst KEY = ROUTE_METADATA_KEYS.RATE_LIMIT\n\n/**\n * Apply a named rate limiter to a controller class or a single route method.\n *\n * Stacks: multiple `@RateLimit` decorators on the same target push onto\n * the metadata array — every named limiter is enforced. Class-level limits\n * run before method-level limits in the resulting middleware chain.\n *\n * The named limiter must be registered separately via\n * `RateLimiterRegistry.for('name', resolver)` (typically inside a\n * module's `onInitialize` hook) and the user must import\n * `RateLimiterModule.forRoot({ store: ... })` in their AppModule.\n *\n * @example\n * ```typescript\n * @Controller('/api/v1/users')\n * @RateLimit('api')\n * export class UsersController {\n * @Get('/')\n * list(ctx: RouterContext) { ... }\n *\n * @Post('/')\n * @RateLimit('writes') // stacks with class-level 'api'\n * create(ctx: RouterContext) { ... }\n * }\n * ```\n */\nexport function RateLimit(name: string): ClassDecorator & MethodDecorator {\n return (target: object, propertyKey?: string | symbol) => {\n if (propertyKey === undefined) {\n const existing = (Reflect.getOwnMetadata(KEY, target) as string[] | undefined) ?? []\n Reflect.defineMetadata(KEY, [...existing, name], target)\n } else {\n const existing = (Reflect.getOwnMetadata(KEY, target, propertyKey as string) as string[] | undefined) ?? []\n Reflect.defineMetadata(KEY, [...existing, name], target, propertyKey as string)\n }\n }\n}\n\n/**\n * Read the rate-limit names attached to a class or method via `@RateLimit`.\n * Returns an empty array when no decorator was applied.\n *\n * @param target - For class metadata, pass the controller constructor.\n * For method metadata, pass `Controller.prototype` and the method name.\n */\nexport function getRateLimits(target: object, propertyKey?: string): string[] {\n const meta: unknown = propertyKey === undefined\n ? Reflect.getMetadata(KEY, target)\n : Reflect.getMetadata(KEY, target, propertyKey)\n return Array.isArray(meta) ? (meta as string[]) : []\n}\n"],"mappings":";;AAEA,MAAM,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BhC,SAAgB,UAAU,MAAgD;CACxE,QAAQ,QAAgB,gBAAkC;EACxD,IAAI,gBAAgB,KAAA,GAAW;GAC7B,MAAM,WAAY,QAAQ,eAAe,KAAK,OAAO,IAA6B,EAAE;GACpF,QAAQ,eAAe,KAAK,CAAC,GAAG,UAAU,KAAK,EAAE,OAAO;SACnD;GACL,MAAM,WAAY,QAAQ,eAAe,KAAK,QAAQ,YAAsB,IAA6B,EAAE;GAC3G,QAAQ,eAAe,KAAK,CAAC,GAAG,UAAU,KAAK,EAAE,QAAQ,YAAsB;;;;;;;;;;;AAYrF,SAAgB,cAAc,QAAgB,aAAgC;CAC5E,MAAM,OAAgB,gBAAgB,KAAA,IAClC,QAAQ,YAAY,KAAK,OAAO,GAChC,QAAQ,YAAY,KAAK,QAAQ,YAAY;CACjD,OAAO,MAAM,QAAQ,KAAK,GAAI,OAAoB,EAAE"}
@@ -0,0 +1,420 @@
1
+ import { Cn as ModuleContext, Dr as Container, En as OnInitialize, Tn as OnException, V as RouterContext, d as ApplicationError, gn as AsyncModuleOptions, jn as ExceptionHandler, o as HttpException, on as Middleware, sn as Next, vn as DynamicModule } from "../index-ByOyTmqf.mjs";
2
+ import { t as Constructor } from "../types-cySNS_lp.mjs";
3
+ import { t as Macroable } from "../index-7-hU3GTV.mjs";
4
+ import { t as StratalEnv } from "../env-D1rcZ8_r.mjs";
5
+ import { t as CacheService } from "../cache.service-DsnKuNyO.mjs";
6
+
7
+ //#region src/rate-limiter/errors.d.ts
8
+ /**
9
+ * Thrown when a request exceeds a configured rate limit.
10
+ *
11
+ * HTTP Status: 429 Too Many Requests
12
+ * Error Code: 4290
13
+ *
14
+ * The {@link ExceptionHandler} renders the body via content negotiation
15
+ * (HTML for HTML clients, JSON for everything else). Standard rate-limit
16
+ * headers (`Retry-After`, `X-RateLimit-*`) are injected by the
17
+ * `respond()` callback registered via `RateLimiterModule.onException`.
18
+ */
19
+ declare class TooManyRequestsError extends HttpException {
20
+ readonly info: {
21
+ retryAfter: number;
22
+ limit: number;
23
+ resetAt: number;
24
+ };
25
+ constructor(info: {
26
+ retryAfter: number;
27
+ limit: number;
28
+ resetAt: number;
29
+ });
30
+ }
31
+ /**
32
+ * Thrown when `RateLimiterRegistry.handle(name, ...)` is invoked for a
33
+ * name that was never registered via `.for()`.
34
+ *
35
+ * Most likely cause: a typo in `router.throttle('foo')` or `@RateLimit('foo')`,
36
+ * or the module that registers the limiter is missing from imports.
37
+ */
38
+ declare class RateLimiterNotDefinedError extends ApplicationError {
39
+ readonly limiterName: string;
40
+ constructor(limiterName: string);
41
+ }
42
+ /**
43
+ * Thrown by `RateLimiterStoreFactory.create()` (during the module's eager
44
+ * `onInitialize` validation) when the user imported `RateLimiterModule`
45
+ * without calling `.forRoot({ store: ... })`. There is no implicit default
46
+ * store — the user must pick one.
47
+ */
48
+ declare class RateLimiterNotConfiguredError extends HttpException {
49
+ constructor();
50
+ }
51
+ /**
52
+ * Thrown when a throttled route fires but `RateLimiterModule` was never
53
+ * imported in the user's AppModule (so the registry token is unbound).
54
+ *
55
+ * Distinct from {@link RateLimiterNotConfiguredError}, which fires when
56
+ * the module IS imported but `forRoot` was not called.
57
+ */
58
+ declare class RateLimiterModuleNotImportedError extends ApplicationError {
59
+ readonly limiterName: string;
60
+ constructor(limiterName: string);
61
+ }
62
+ //#endregion
63
+ //#region src/rate-limiter/limit.d.ts
64
+ /**
65
+ * Standard rate-limit response headers passed to custom response handlers.
66
+ * Keys are canonical HTTP header names; values are the stringified counts.
67
+ */
68
+ interface RateLimitHeaders {
69
+ 'Retry-After': string;
70
+ 'X-RateLimit-Limit': string;
71
+ 'X-RateLimit-Remaining': string;
72
+ 'X-RateLimit-Reset': string;
73
+ }
74
+ /**
75
+ * Custom response handler invoked when a limit is exceeded and the user
76
+ * supplied `.response()` on the {@link Limit}. Receives the request context
77
+ * and the precomputed standard headers (which the handler can spread, drop,
78
+ * or override).
79
+ */
80
+ type RateLimitResponseHandler = (ctx: RouterContext, headers: RateLimitHeaders) => Response | Promise<Response>;
81
+ /**
82
+ * A single rate limit window.
83
+ *
84
+ * Build via the static factories (`perSecond`, `perMinute`, `perMinutes`,
85
+ * `perHour`, `perDay`, `none`). Chain `.by(key)` to scope per-actor and
86
+ * `.response(handler)` to override the default 429 response.
87
+ *
88
+ * Returned (singly or as an array) by limiter resolvers registered via
89
+ * `RateLimiterRegistry.for()`.
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * Limit.perMinute(60).by(ctx.header('cf-connecting-ip') ?? 'global')
94
+ * Limit.perHour(100).by(userId)
95
+ * Limit.none() // bypass
96
+ * ```
97
+ */
98
+ declare class Limit {
99
+ readonly windowSeconds: number;
100
+ readonly max: number;
101
+ readonly disabled: boolean;
102
+ private _key?;
103
+ private _customResponse?;
104
+ private constructor();
105
+ static perSecond(max: number): Limit;
106
+ static perSeconds(seconds: number, max: number): Limit;
107
+ static perMinute(max: number): Limit;
108
+ static perMinutes(minutes: number, max: number): Limit;
109
+ static perHour(max: number): Limit;
110
+ static perDay(max: number): Limit;
111
+ /** Bypass the limiter entirely for this request. */
112
+ static none(): Limit;
113
+ /** Scope this limit to a specific actor (user id, IP, tenant, etc.). */
114
+ by(key: string | number): this;
115
+ /**
116
+ * Override the default 429 response. The handler receives the standard
117
+ * `RateLimitHeaders` so it can spread them onto its own Response or omit
118
+ * them as it sees fit.
119
+ */
120
+ response(handler: RateLimitResponseHandler): this;
121
+ get key(): string | undefined;
122
+ get customResponse(): RateLimitResponseHandler | undefined;
123
+ }
124
+ //#endregion
125
+ //#region src/rate-limiter/stores/rate-limiter-store.interface.d.ts
126
+ /**
127
+ * Result of a single hit against the rate limiter (registry-internal).
128
+ *
129
+ * The store no longer participates in the increment — it's a typed KV.
130
+ * `RateLimiterRegistry.handle()` produces this value as it runs the
131
+ * get-modify-set sequence.
132
+ */
133
+ interface RateLimitHit {
134
+ /** Counter value AFTER the increment (1 on first hit). */
135
+ count: number;
136
+ /** Absolute reset timestamp in milliseconds since epoch. */
137
+ resetAt: number;
138
+ }
139
+ /**
140
+ * Pluggable backend for rate-limit storage — a typed key-value store with TTL.
141
+ *
142
+ * The framework ships `KvRateLimiterStore` (Cloudflare KV) and
143
+ * `InMemoryRateLimiterStore` (tests). Users can register a custom
144
+ * implementation through `RateLimiterModule.forRoot({ store: { useClass: ... } })`.
145
+ *
146
+ * The increment logic lives in `RateLimiterRegistry`; the store only persists
147
+ * arbitrary values keyed by string. Same shape consumed by the better-auth
148
+ * bridge in `@stratal/framework/auth`, which stores its own `RateLimit`
149
+ * records here under a separate key namespace.
150
+ */
151
+ interface IRateLimiterStore {
152
+ /**
153
+ * Read the value at `key`. Returns `null` when missing or expired.
154
+ * Implementations are responsible for honouring TTL (lazy or active).
155
+ */
156
+ get<T = unknown>(key: string): Promise<T | null>;
157
+ /**
158
+ * Write `value` at `key` with `ttlSeconds` TTL. Overwrites any existing
159
+ * value. The TTL resets on every write — callers compute the remaining
160
+ * window themselves and pass it explicitly.
161
+ */
162
+ set<T = unknown>(key: string, value: T, ttlSeconds: number): Promise<void>;
163
+ /**
164
+ * Remove `key`. No-op when missing.
165
+ */
166
+ delete(key: string): Promise<void>;
167
+ }
168
+ //#endregion
169
+ //#region src/rate-limiter/rate-limiter-registry.d.ts
170
+ /**
171
+ * Resolver function registered via {@link RateLimiterRegistry.for}. Receives
172
+ * the request context and returns the limit (or limits) that apply to this
173
+ * request. Return `Limit.none()` to bypass for the current request.
174
+ */
175
+ type LimitResolver = (ctx: RouterContext) => Limit | Limit[] | Promise<Limit | Limit[]>;
176
+ /**
177
+ * Central registry of named rate limiters and the request-time enforcement
178
+ * pipeline. Resolved as a singleton; consumed by `ThrottleMiddleware`.
179
+ *
180
+ * Register limiters in a module's `onInitialize` hook:
181
+ * ```typescript
182
+ * @Module({})
183
+ * export class RateLimitsModule implements OnInitialize {
184
+ * onInitialize({ container }: ModuleContext): void {
185
+ * const limiter = container.resolve<RateLimiterRegistry>(RATE_LIMITER_TOKENS.Registry)
186
+ * limiter.for('api', (ctx) => Limit.perMinute(60).by(ctx.header('cf-connecting-ip') ?? '*'))
187
+ * }
188
+ * }
189
+ * ```
190
+ *
191
+ * Extensible via `Macroable`: adapter packages (e.g. `@stratal/framework/auth`)
192
+ * can attach extra registration methods such as `forPath()` for better-auth
193
+ * `customRules` interop.
194
+ */
195
+ declare class RateLimiterRegistry extends Macroable {
196
+ private readonly store;
197
+ private readonly resolvers;
198
+ constructor(store: IRateLimiterStore);
199
+ /**
200
+ * Register a named limiter. Names must be unique; calling `for()` again
201
+ * with the same name overwrites the previous resolver (matches Laravel
202
+ * `RateLimiter::for` semantics — last definition wins).
203
+ */
204
+ for(name: string, resolver: LimitResolver): void;
205
+ has(name: string): boolean;
206
+ /**
207
+ * Enforce the named limiter for the current request. Called by
208
+ * `ThrottleMiddleware` (the per-name class produced by
209
+ * `createThrottleMiddleware`). Resolves the limiter, increments the store
210
+ * for each non-bypassed limit, sets `X-RateLimit-*` headers on success, and
211
+ * either invokes the limit's custom `.response()` or throws
212
+ * {@link TooManyRequestsError} when a limit is exceeded.
213
+ */
214
+ handle(name: string, ctx: RouterContext, next: Next): Promise<Response | void>;
215
+ /**
216
+ * Get-modify-set increment over the typed KV store. Not atomic across
217
+ * concurrent edge requests on KV — see `KvRateLimiterStore`'s caveat.
218
+ */
219
+ private hit;
220
+ private makeKey;
221
+ private makeHeaders;
222
+ }
223
+ //#endregion
224
+ //#region src/rate-limiter/stores/store-factory.d.ts
225
+ /**
226
+ * Configuration for `RateLimiterModule.forRoot()`. Picks the backing store.
227
+ *
228
+ * - `'kv'`: Cloudflare KV. `binding` names the KVNamespace on `StratalEnv`.
229
+ * - `'memory'`: in-process Map. Tests / single-isolate only.
230
+ * - `{ useClass }`: any class implementing `IRateLimiterStore`. Resolved
231
+ * from the DI container (so the class can declare its own `@inject` deps,
232
+ * e.g. a Durable Object namespace from `StratalEnv`).
233
+ */
234
+ type RateLimiterModuleOptions = {
235
+ store: 'kv';
236
+ binding: keyof StratalEnv;
237
+ } | {
238
+ store: 'memory';
239
+ } | {
240
+ store: {
241
+ useClass: Constructor<IRateLimiterStore>;
242
+ };
243
+ };
244
+ declare class RateLimiterStoreFactory {
245
+ private readonly env;
246
+ private readonly cache;
247
+ private readonly container;
248
+ private readonly options?;
249
+ constructor(env: StratalEnv, cache: CacheService, container: Container, options?: RateLimiterModuleOptions | undefined);
250
+ create(): IRateLimiterStore;
251
+ }
252
+ //#endregion
253
+ //#region src/rate-limiter/rate-limiter.module.d.ts
254
+ /**
255
+ * Rate limiter module — opt-in, NOT registered automatically by Application.
256
+ *
257
+ * Usage:
258
+ * ```typescript
259
+ * @Module({
260
+ * imports: [RateLimiterModule.forRoot({ store: 'kv', binding: 'RATE_LIMITS' })],
261
+ * })
262
+ * export class AppModule {}
263
+ * ```
264
+ *
265
+ * Define limiters in any module's `onInitialize` hook by resolving
266
+ * `RATE_LIMITER_TOKENS.Registry` from the container. Apply them with
267
+ * `router.throttle('name')` or `@RateLimit('name')`.
268
+ *
269
+ * The module:
270
+ * - eagerly validates the store at app boot (`onInitialize` resolves the
271
+ * factory and calls `create()`); a missing `forRoot` surfaces
272
+ * `RateLimiterNotConfiguredError` before any request is served.
273
+ * - registers a `respond()` callback on the `ExceptionHandler` (via
274
+ * `onException`) that injects `Retry-After` and `X-RateLimit-*` headers
275
+ * on every {@link TooManyRequestsError} response, regardless of whether
276
+ * the body was rendered as JSON, HTML, or via Inertia.
277
+ */
278
+ declare class RateLimiterModule implements OnInitialize, OnException {
279
+ /**
280
+ * Configure the rate limiter with a store choice.
281
+ *
282
+ * @param options - `{ store: 'kv', binding }` | `{ store: 'memory' }` | `{ store: { useClass } }`
283
+ */
284
+ static forRoot(options: RateLimiterModuleOptions): DynamicModule;
285
+ /**
286
+ * Async configuration. Use when store options depend on other services
287
+ * (e.g. ConfigService).
288
+ */
289
+ static forRootAsync(options: AsyncModuleOptions<RateLimiterModuleOptions>): DynamicModule;
290
+ /**
291
+ * Eagerly resolve the store so a missing `forRoot()` fails at boot,
292
+ * not on the first throttled request. Also registers a per-app marker
293
+ * value so ThrottleMiddleware can distinguish "module imported into this
294
+ * app" from "module class loaded somewhere in the process" (the latter is
295
+ * indistinguishable at the DI layer because @Module's `registry()` call
296
+ * binds providers globally at decoration time).
297
+ */
298
+ onInitialize({
299
+ container
300
+ }: ModuleContext): void;
301
+ /**
302
+ * Inject `Retry-After` + `X-RateLimit-*` headers on every 429 response.
303
+ * The body itself is rendered by `ExceptionHandler.defaultRender` —
304
+ * content-negotiated, so HTML clients (browsers, Inertia) and JSON
305
+ * clients both get the right shape.
306
+ */
307
+ onException(handler: ExceptionHandler): void;
308
+ }
309
+ //#endregion
310
+ //#region src/rate-limiter/rate-limiter.tokens.d.ts
311
+ declare const RATE_LIMITER_TOKENS: {
312
+ readonly Registry: symbol;
313
+ readonly Store: symbol;
314
+ readonly StoreFactory: symbol;
315
+ readonly Options: symbol;
316
+ /**
317
+ * Per-app marker registered by RateLimiterModule.onInitialize. Used by
318
+ * ThrottleMiddleware to detect "module not imported" — the @Module
319
+ * decorator globally registers providers via tsyringe's registry(),
320
+ * so the Registry/Store tokens are globally bound the moment the module
321
+ * file is loaded. The only way to confirm the module was actually wired
322
+ * into the *user's* AppModule is to look for an artifact registered
323
+ * inside the user's app container (not the root container).
324
+ */
325
+ readonly ModuleMarker: symbol;
326
+ };
327
+ type RateLimiterToken = (typeof RATE_LIMITER_TOKENS)[keyof typeof RATE_LIMITER_TOKENS];
328
+ //#endregion
329
+ //#region src/rate-limiter/throttle.middleware.d.ts
330
+ /**
331
+ * Memoized factory that produces a Stratal `Middleware` class bound to a
332
+ * named limiter. Calling twice with the same name returns the *same* class
333
+ * — important for `Router.middleware` deduplication via class identity.
334
+ *
335
+ * Detection of "module not imported" works against a per-app marker
336
+ * registered by `RateLimiterModule.onInitialize` (NOT via inject decorator,
337
+ * because tsyringe would still try to construct Registry — whose Store
338
+ * inject would explode with a less-actionable tsyringe wrapping). We hold
339
+ * the user's container, then check `isRegistered(marker, recursive=true)`
340
+ * at request time before resolving Registry.
341
+ */
342
+ declare function createThrottleMiddleware(name: string): Constructor<Middleware>;
343
+ //#endregion
344
+ //#region src/rate-limiter/decorators/rate-limit.decorator.d.ts
345
+ /**
346
+ * Apply a named rate limiter to a controller class or a single route method.
347
+ *
348
+ * Stacks: multiple `@RateLimit` decorators on the same target push onto
349
+ * the metadata array — every named limiter is enforced. Class-level limits
350
+ * run before method-level limits in the resulting middleware chain.
351
+ *
352
+ * The named limiter must be registered separately via
353
+ * `RateLimiterRegistry.for('name', resolver)` (typically inside a
354
+ * module's `onInitialize` hook) and the user must import
355
+ * `RateLimiterModule.forRoot({ store: ... })` in their AppModule.
356
+ *
357
+ * @example
358
+ * ```typescript
359
+ * @Controller('/api/v1/users')
360
+ * @RateLimit('api')
361
+ * export class UsersController {
362
+ * @Get('/')
363
+ * list(ctx: RouterContext) { ... }
364
+ *
365
+ * @Post('/')
366
+ * @RateLimit('writes') // stacks with class-level 'api'
367
+ * create(ctx: RouterContext) { ... }
368
+ * }
369
+ * ```
370
+ */
371
+ declare function RateLimit(name: string): ClassDecorator & MethodDecorator;
372
+ /**
373
+ * Read the rate-limit names attached to a class or method via `@RateLimit`.
374
+ * Returns an empty array when no decorator was applied.
375
+ *
376
+ * @param target - For class metadata, pass the controller constructor.
377
+ * For method metadata, pass `Controller.prototype` and the method name.
378
+ */
379
+ declare function getRateLimits(target: object, propertyKey?: string): string[];
380
+ //#endregion
381
+ //#region src/rate-limiter/stores/kv-store.d.ts
382
+ /**
383
+ * Cloudflare KV-backed typed KV store.
384
+ *
385
+ * KV's minimum `expirationTtl` is 60 seconds; sub-60s windows are still
386
+ * enforced by the registry's algorithm via the persisted `resetAt`, but
387
+ * the key itself may live in KV longer than the logical window.
388
+ *
389
+ * KV has no native atomic increment, so concurrent writes from different
390
+ * edge locations may undercount under high contention. That's an inherent
391
+ * KV tradeoff — pick `{ useClass: MyDurableObjectStore }` for strict
392
+ * accuracy across edges.
393
+ */
394
+ declare class KvRateLimiterStore implements IRateLimiterStore {
395
+ private readonly cache;
396
+ constructor(cache: CacheService);
397
+ get<T>(key: string): Promise<T | null>;
398
+ set<T>(key: string, value: T, ttlSeconds: number): Promise<void>;
399
+ delete(key: string): Promise<void>;
400
+ }
401
+ //#endregion
402
+ //#region src/rate-limiter/stores/memory-store.d.ts
403
+ /**
404
+ * In-process Map-backed typed KV store.
405
+ *
406
+ * Suitable for tests and single-isolate scenarios. Not safe across
407
+ * Cloudflare Worker isolates — entries reset whenever a fresh isolate
408
+ * spins up. Use `KvRateLimiterStore` (or a custom Durable Object store)
409
+ * for production. Expiry is lazy: stale entries are dropped on the next
410
+ * `get`.
411
+ */
412
+ declare class InMemoryRateLimiterStore implements IRateLimiterStore {
413
+ private readonly entries;
414
+ get<T>(key: string): Promise<T | null>;
415
+ set<T>(key: string, value: T, ttlSeconds: number): Promise<void>;
416
+ delete(key: string): Promise<void>;
417
+ }
418
+ //#endregion
419
+ export { type IRateLimiterStore, InMemoryRateLimiterStore, KvRateLimiterStore, Limit, type LimitResolver, RATE_LIMITER_TOKENS, RateLimit, type RateLimitHeaders, type RateLimitHit, type RateLimitResponseHandler, RateLimiterModule, RateLimiterModuleNotImportedError, type RateLimiterModuleOptions, RateLimiterNotConfiguredError, RateLimiterNotDefinedError, RateLimiterRegistry, RateLimiterStoreFactory, type RateLimiterToken, TooManyRequestsError, createThrottleMiddleware, getRateLimits };
420
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/rate-limiter/errors.ts","../../src/rate-limiter/limit.ts","../../src/rate-limiter/stores/rate-limiter-store.interface.ts","../../src/rate-limiter/rate-limiter-registry.ts","../../src/rate-limiter/stores/store-factory.ts","../../src/rate-limiter/rate-limiter.module.ts","../../src/rate-limiter/rate-limiter.tokens.ts","../../src/rate-limiter/throttle.middleware.ts","../../src/rate-limiter/decorators/rate-limit.decorator.ts","../../src/rate-limiter/stores/kv-store.ts","../../src/rate-limiter/stores/memory-store.ts"],"mappings":";;;;;;;;;;;;;AAcA;;;;;cAAa,oBAAA,SAA6B,aAAA;EAAA,SAEtB,IAAA;IAAQ,UAAA;IAAoB,KAAA;IAAe,OAAA;EAAA;cAA3C,IAAA;IAAQ,UAAA;IAAoB,KAAA;IAAe,OAAA;EAAA;AAAA;;;;;;;;cAclD,0BAAA,SAAmC,gBAAA;EAAA,SAClB,WAAA;cAAA,WAAA;AAAA;;;;AA6B9B;;;cAda,6BAAA,SAAsC,aAAA;EAAA,WAAA,CAAA;AAAA;;;;;;;;cActC,iCAAA,SAA0C,gBAAA;EAAA,SACzB,WAAA;cAAA,WAAA;AAAA;;;;;;;UCvDb,gBAAA;EACf,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,mBAAA;AAAA;;;;;;;KASU,wBAAA,IACV,GAAA,EAAK,aAAA,EACL,OAAA,EAAS,gBAAA,KACN,QAAA,GAAW,OAAA,CAAQ,QAAA;;;;;;ADQxB;;;;;;;;;;AAgBA;;cCLa,KAAA;EAAA,SAKO,aAAA;EAAA,SACA,GAAA;EAAA,SACA,QAAA;EAAA,QANV,IAAA;EAAA,QACA,eAAA;EAAA,QAED,WAAA,CAAA;EAAA,OAMA,SAAA,CAAU,GAAA,WAAc,KAAA;EAAA,OAIxB,UAAA,CAAW,OAAA,UAAiB,GAAA,WAAc,KAAA;EAAA,OAI1C,SAAA,CAAU,GAAA,WAAc,KAAA;EAAA,OAIxB,UAAA,CAAW,OAAA,UAAiB,GAAA,WAAc,KAAA;EAAA,OAI1C,OAAA,CAAQ,GAAA,WAAc,KAAA;EAAA,OAItB,MAAA,CAAO,GAAA,WAAc,KAAA;;SAKrB,IAAA,CAAA,GAAQ,KAAA;;EAKf,EAAA,CAAG,GAAA;EA3E4B;;;;;EAqF/B,QAAA,CAAS,OAAA,EAAS,wBAAA;EAAA,IAKd,GAAA,CAAA;EAAA,IAIA,cAAA,CAAA,GAAkB,wBAAA;AAAA;;;;;;;;;;UC7FP,YAAA;EFOiB;EELhC,KAAA;EFKqD;EEHrD,OAAA;AAAA;;;;;;;;;;;AFmBF;;UEJiB,iBAAA;EFI+C;;;;EEC9D,GAAA,cAAiB,GAAA,WAAc,OAAA,CAAQ,CAAA;EFAQ;;AAejD;;;EERE,GAAA,cAAiB,GAAA,UAAa,KAAA,EAAO,CAAA,EAAG,UAAA,WAAqB,OAAA;;AFsB/D;;EEjBE,MAAA,CAAO,GAAA,WAAc,OAAA;AAAA;;;;;AF7BvB;;;KGCY,aAAA,IACV,GAAA,EAAK,aAAA,KACF,KAAA,GAAQ,KAAA,KAAU,OAAA,CAAQ,KAAA,GAAQ,KAAA;;;;;;;;;;;;;AHavC;;;;;;;cGoBa,mBAAA,SAA4B,SAAA;EAAA,iBAIe,KAAA;EAAA,iBAHrC,SAAA;cAGqC,KAAA,EAAO,iBAAA;EHRpB;;;;AAc3C;EGIE,GAAA,CAAI,IAAA,UAAc,QAAA,EAAU,aAAA;EAI5B,GAAA,CAAI,IAAA;EHRiE;;;;;;;;EGoB/D,MAAA,CAAO,IAAA,UAAc,GAAA,EAAK,aAAA,EAAe,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,QAAA;;AF1EtE;;;UE4IgB,GAAA;EAAA,QAgBN,OAAA;EAAA,QAKA,WAAA;AAAA;;;;;AHzJV;;;;;;;KISY,wBAAA;EACN,KAAA;EAAa,OAAA,QAAe,UAAA;AAAA;EAC5B,KAAA;AAAA;EACA,KAAA;IAAS,QAAA,EAAU,WAAA,CAAY,iBAAA;EAAA;AAAA;AAAA,cAOxB,uBAAA;EAAA,iBAEyC,GAAA;EAAA,iBACE,KAAA;EAAA,iBACV,SAAA;EAAA,iBAEzB,OAAA;cAJiC,GAAA,EAAK,UAAA,EACH,KAAA,EAAO,YAAA,EACjB,SAAA,EAAW,SAAA,EAEpC,OAAA,GAAU,wBAAA;EAG7B,MAAA,CAAA,GAAU,iBAAA;AAAA;;;;;;;AJ5BZ;;;;;;;;;;;;;;;;AAgBA;;;;cKgBa,iBAAA,YAA6B,YAAA,EAAc,WAAA;ELf1B;;;;;EAAA,OKqBrB,OAAA,CAAQ,OAAA,EAAS,wBAAA,GAA2B,aAAA;ELNV;;;;EAAA,OKmBlC,YAAA,CAAa,OAAA,EAAS,kBAAA,CAAmB,wBAAA,IAA4B,aAAA;ELL/B;;;;;;;;EK0B7C,YAAA,CAAA;IAAe;EAAA,GAAa,aAAA;;;;AJhF9B;;;EI2FE,WAAA,CAAY,OAAA,EAAS,gBAAA;AAAA;;;cCjGV,mBAAA;EAAA;;;;;;ANcb;;;;;;;;;KMGY,gBAAA,WAA2B,mBAAA,eAAkC,mBAAA;;;;;;;;ANHzE;;;;;;;iBOUgB,wBAAA,CAAyB,IAAA,WAAe,WAAA,CAAY,UAAA;;;;;;;;;;APVpE;;;;;;;;;;;;;;;;AAgBA;;;iBQAgB,SAAA,CAAU,IAAA,WAAe,cAAA,GAAiB,eAAA;;;;;;;ARgB1D;iBQGgB,aAAA,CAAc,MAAA,UAAgB,WAAA;;;;;;;;ARnC9C;;;;;;;cSCa,kBAAA,YAA8B,iBAAA;EAAA,iBACZ,KAAA;cAAA,KAAA,EAAO,YAAA;EAE9B,GAAA,GAAA,CAAO,GAAA,WAAc,OAAA,CAAQ,CAAA;EAI7B,GAAA,GAAA,CAAO,GAAA,UAAa,KAAA,EAAO,CAAA,EAAG,UAAA,WAAqB,OAAA;EAKnD,MAAA,CAAO,GAAA,WAAc,OAAA;AAAA;;;;;;;;;ATb7B;;;cUEa,wBAAA,YAAoC,iBAAA;EAAA,iBAC9B,OAAA;EAEjB,GAAA,GAAA,CAAO,GAAA,WAAc,OAAA,CAAQ,CAAA;EAU7B,GAAA,GAAA,CAAO,GAAA,UAAa,KAAA,EAAO,CAAA,EAAG,UAAA,WAAqB,OAAA;EAKnD,MAAA,CAAO,GAAA,WAAc,OAAA;AAAA"}