@maestro-js/agent-skills 1.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,206 @@
1
+ ---
2
+ name: rate-limiting
3
+ description: "Per-key rate limiting with configurable decay windows. Use when enforcing attempt limits on login, API endpoints, or any action that needs throttling. Provides increment/check/clear workflow, guarded callback execution via attempt(), and ISO 8601 duration reporting. Requires a cache driver (e.g. from @maestro-js/cache)."
4
+ ---
5
+
6
+ # @maestro-js/rate-limiting
7
+
8
+ Per-key rate limiting with configurable decay windows. Use when enforcing attempt limits on login,
9
+ API endpoints, or any action that needs throttling.
10
+
11
+ ## Quick Setup
12
+
13
+ ```ts
14
+ import { RateLimiting } from '@maestro-js/rate-limiting'
15
+ import { Cache } from '@maestro-js/cache'
16
+
17
+ // Create using a cache service as the driver
18
+ const cacheService = Cache.Provider.create({
19
+ driver: Cache.drivers.redis({ url: 'redis-host', port: 6379 })
20
+ })
21
+
22
+ const rateLimiter = RateLimiting.Provider.create(cacheService)
23
+ RateLimiting.Provider.register('default', rateLimiter)
24
+
25
+ // Check before allowing an action
26
+ await RateLimiting.increment('login:user@example.com', 1, 300)
27
+
28
+ if (await RateLimiting.tooManyAttempts('login:user@example.com', 5)) {
29
+ const retryAfter = await RateLimiting.availableIn('login:user@example.com')
30
+ throw new Error(`Too many attempts. Try again in ${retryAfter}`)
31
+ }
32
+ ```
33
+
34
+ ## API Reference
35
+
36
+ ### increment
37
+
38
+ ```ts
39
+ RateLimiting.increment(key: string, amount?: number, decaySeconds?: number): Promise<number>
40
+ ```
41
+
42
+ Records one or more hits against the bucket. Creates the bucket with the given decay window
43
+ (default 60 seconds) if it doesn't exist. Returns the new hit count.
44
+
45
+ ```ts
46
+ const hits = await RateLimiting.increment('api:client-123') // +1, 60s window
47
+ const hits = await RateLimiting.increment('api:client-123', 5, 120) // +5, 120s window
48
+ ```
49
+
50
+ ### tooManyAttempts
51
+
52
+ ```ts
53
+ RateLimiting.tooManyAttempts(key: string, maxAttempts: number): Promise<boolean>
54
+ ```
55
+
56
+ Returns `true` if the bucket has reached or exceeded `maxAttempts` and the decay window is still
57
+ active. If the timer has expired but the counter remains, the counter is cleaned up and `false`
58
+ is returned.
59
+
60
+ ```ts
61
+ if (await RateLimiting.tooManyAttempts('login:user@example.com', 5)) {
62
+ return res.status(429).json({ error: 'Too many login attempts' })
63
+ }
64
+ ```
65
+
66
+ ### remaining
67
+
68
+ ```ts
69
+ RateLimiting.remaining(key: string, maxAttempts: number): Promise<number>
70
+ ```
71
+
72
+ Returns the number of attempts left before `maxAttempts` is reached. Never returns a negative
73
+ number.
74
+
75
+ ```ts
76
+ const left = await RateLimiting.remaining('api:client-123', 100)
77
+ res.setHeader('X-RateLimit-Remaining', left)
78
+ ```
79
+
80
+ ### clear
81
+
82
+ ```ts
83
+ RateLimiting.clear(key: string): Promise<void>
84
+ ```
85
+
86
+ Removes both the hit counter and timer for the bucket, fully resetting it.
87
+
88
+ ```ts
89
+ // Reset after a successful login
90
+ await RateLimiting.clear('login:user@example.com')
91
+ ```
92
+
93
+ ### availableIn
94
+
95
+ ```ts
96
+ RateLimiting.availableIn(key: string): Promise<Iso.Duration | null>
97
+ ```
98
+
99
+ Returns the time remaining until the decay window expires as an `Iso.Duration`, or `null` if no
100
+ active window exists. The duration is rounded down to the nearest second.
101
+
102
+ ```ts
103
+ const retryAfter = await RateLimiting.availableIn('api:client-123')
104
+ if (retryAfter) {
105
+ res.setHeader('Retry-After', retryAfter) // e.g. "PT45S"
106
+ }
107
+ ```
108
+
109
+ ### attempt
110
+
111
+ ```ts
112
+ RateLimiting.attempt<T>(
113
+ key: string,
114
+ maxAttemptsPerWindow: number,
115
+ callback: () => T,
116
+ decaySeconds?: number
117
+ ): Promise<T | boolean>
118
+ ```
119
+
120
+ Guarded callback execution. If the bucket is under the limit, increments and executes
121
+ `callback`, returning its result. If the limit is exceeded, returns `false` without executing
122
+ the callback.
123
+
124
+ ```ts
125
+ const result = await RateLimiting.attempt('send-email:user-1', 10, async () => {
126
+ await sendEmail(to, subject, body)
127
+ return { sent: true }
128
+ }, 3600)
129
+
130
+ if (result === false) {
131
+ throw new Error('Email rate limit exceeded')
132
+ }
133
+ ```
134
+
135
+ ## Common Patterns
136
+
137
+ ### Login throttling
138
+
139
+ ```ts
140
+ async function handleLogin(email: string, password: string) {
141
+ const key = `login:${email}`
142
+
143
+ if (await RateLimiting.tooManyAttempts(key, 5)) {
144
+ const retryAfter = await RateLimiting.availableIn(key)
145
+ return { error: `Too many attempts. Try again in ${retryAfter}` }
146
+ }
147
+
148
+ await RateLimiting.increment(key, 1, 300) // 5-minute window
149
+
150
+ const user = await authenticate(email, password)
151
+ if (user) {
152
+ await RateLimiting.clear(key)
153
+ return { success: true }
154
+ }
155
+
156
+ return { error: 'Invalid credentials' }
157
+ }
158
+ ```
159
+
160
+ ### API rate limiting with Retry-After header
161
+
162
+ ```ts
163
+ async function rateLimitMiddleware(req: Request, res: Response, next: () => void) {
164
+ const key = `api:${req.ip}`
165
+
166
+ if (await RateLimiting.tooManyAttempts(key, 100)) {
167
+ const retryAfter = await RateLimiting.availableIn(key)
168
+ if (retryAfter) {
169
+ res.setHeader('Retry-After', retryAfter)
170
+ }
171
+ return res.status(429).json({ error: 'Rate limit exceeded' })
172
+ }
173
+
174
+ await RateLimiting.increment(key, 1, 60)
175
+ res.setHeader('X-RateLimit-Remaining', await RateLimiting.remaining(key, 100))
176
+ next()
177
+ }
178
+ ```
179
+
180
+ ### Guarded action with attempt()
181
+
182
+ ```ts
183
+ const result = await RateLimiting.attempt('report:org-5', 3, async () => {
184
+ return await generateExpensiveReport()
185
+ }, 3600) // 3 per hour
186
+
187
+ if (result === false) {
188
+ return { error: 'Report generation rate limit exceeded' }
189
+ }
190
+ ```
191
+
192
+ ## Warnings and Gotchas
193
+
194
+ - **No built-in drivers** — `RateLimiting.Provider.create()` takes a cache driver directly
195
+ (anything satisfying `RateLimiting.Driver`). Pass a `@maestro-js/cache` service instance.
196
+ - **`attempt()` returns `T | boolean`** — check `=== false` to distinguish a rate-limited call
197
+ from a callback that returns a falsy value.
198
+ - **`availableIn()` rounds down** — the returned duration is floored to the nearest second.
199
+ - **Two cache keys per bucket** — each rate-limit key uses `_rate_limit:{key}` (counter) and
200
+ `_rate_limit:{key}:timer` (expiry instant). Keep this in mind when inspecting cache contents.
201
+ - **Default decay is 60 seconds** — if you omit `decaySeconds`, the window is one minute.
202
+
203
+ ## Key Source Files
204
+
205
+ - `packages/rate-limiting/src/index.ts` — single source file with create factory, Provider
206
+ pattern, and namespace types
@@ -0,0 +1,337 @@
1
+ ---
2
+ name: react-router-file-routes
3
+ description: ""
4
+ ---
5
+
6
+ # Route File Naming (Polaris Adventures)
7
+
8
+ Adapted from https://remix.run/docs/en/main/file-conventions/route-files-v2
9
+
10
+ ## Basic Routes
11
+
12
+ Any JavaScript or TypeScript files in the `app/routes/` directory will become routes in your application. The filename maps to the route's URL pathname, except for `index.tsx` which is the [index route](https://remix.run/docs/en/main/guides/routing#index-routes) for the [root route](https://remix.run/docs/en/main/file-conventions/route-files-v2#root-route).
13
+
14
+ <!-- prettier-ignore -->
15
+ ```diff
16
+ app/
17
+ ├── routes/
18
+ + │ ├── index.tsx
19
+ + │ └── about.tsx
20
+ └── root.tsx
21
+ ```
22
+
23
+ | URL | Matched Routes |
24
+ | -------- | -------------- |
25
+ | `/` | `index.tsx` |
26
+ | `/about` | `about.tsx` |
27
+
28
+ Note that these routes will be rendered in the outlet of `app/root.tsx` because of [nested-routing](https://remix.run/docs/en/main/guides/routing#what-is-nested-routing).
29
+
30
+ ### Dot Delimiters
31
+
32
+ Adding a `.` to a route filename will create a `/` in the URL.
33
+
34
+ <!-- prettier-ignore -->
35
+ ```diff
36
+ app/
37
+ ├── routes/
38
+ │ ├── index.tsx
39
+ │ ├── about.tsx
40
+ + │ ├── concerts.trending.tsx
41
+ + │ ├── concerts.salt-lake-city.tsx
42
+ + │ └── concerts.san-diego.tsx
43
+ └── root.tsx
44
+ ```
45
+
46
+ | URL | Matched Route |
47
+ | -------------------------- | ----------------------------- |
48
+ | `/concerts/trending` | `concerts.trending.tsx` |
49
+ | `/concerts/salt-lake-city` | `concerts.salt-lake-city.tsx` |
50
+ | `/concerts/san-diego` | `concerts.san-diego.tsx` |
51
+
52
+ ### Folder Structure
53
+
54
+ Adding a parent folder to a route file will also create a `/` in the URL.
55
+
56
+ <!-- prettier-ignore -->
57
+ ```diff
58
+ app/
59
+ ├── routes/
60
+ │ ├── index.tsx
61
+ │ ├── about.tsx
62
+ + │ └── concerts
63
+ + │ ├── trending.tsx
64
+ + │ ├── salt-lake-city.tsx
65
+ + │ └── san-diego.tsx
66
+ └── root.tsx
67
+ ```
68
+
69
+ | URL | Matched Route |
70
+ | -------------------------- | ----------------------------- |
71
+ | `/concerts/trending` | `concerts/trending.tsx` |
72
+ | `/concerts/salt-lake-city` | `concerts/salt-lake-city.tsx` |
73
+ | `/concerts/san-diego` | `concerts/san-diego.tsx` |
74
+
75
+ ### Dynamic Segments
76
+
77
+ Usually your URLs aren't static but data-driven. Dynamic segments allow you to match segments of the URL and use that value in your code. You create them with the `$` prefix.
78
+
79
+ <!-- prettier-ignore -->
80
+ ```diff
81
+ app/
82
+ ├── routes/
83
+ │ ├── index.tsx
84
+ │ ├── about.tsx
85
+ + │ └── concerts
86
+ + │ ├── trending.tsx
87
+ + │ └── $city.tsx
88
+ └── root.tsx
89
+ ```
90
+
91
+ | URL | Matched Route |
92
+ | -------------------------- | ----------------------- |
93
+ | `/concerts/trending` | `concerts/trending.tsx` |
94
+ | `/concerts/salt-lake-city` | `concerts/$city.tsx` |
95
+ | `/concerts/san-diego` | `concerts/$city.tsx` |
96
+
97
+ ### Nested Routes
98
+
99
+ You can create nested routes with nested folders. By default, a file with the name `_layout` will become the layout for all routes in that folder.
100
+
101
+ ```diff
102
+ app/
103
+ ├── routes/
104
+ │ ├── index.tsx
105
+ │ ├── about.tsx
106
+ + │ └── concerts
107
+ + │ ├── _layout.tsx
108
+ + │ ├── $city.tsx
109
+ + │ ├── index.tsx
110
+ + │ └── trending.tsx
111
+ └── root.tsx
112
+ ```
113
+
114
+ All the routes in the `concerts` folder will be child routes of `concerts/_layout.tsx` and render inside the parent route's outlet.
115
+
116
+ | URL | Matched Route | Layout |
117
+ | ------------------------ | --------------------- | ----------------------------------- |
118
+ | / | index.tsx | `root.tsx` |
119
+ | /about | about.tsx | `root.tsx` |
120
+ | /concerts | concerts/index.tsx | `root.tsx` > `concerts/_layout.tsx` |
121
+ | /concerts/trending | concerts/trending.tsx | `root.tsx` > `concerts/_layout.tsx` |
122
+ | /concerts/salt-lake-city | concerts/$city.tsx | `root.tsx` > `concerts/_layout.tsx` |
123
+
124
+ Note you typically want to add an index route when you add nested routes so that something renders inside the parent's outlet when users visit the parent URL directly.
125
+
126
+ ### Nested Layouts without Nested URLs
127
+
128
+ We call these **Pathless Routes**
129
+
130
+ Sometimes you want to share a layout with a group of routes without adding any path segments to the URL. A common example is a set of authentication routes that have a different header/footer than the public pages or the logged in app experience. You can do this with a `_leading` underscore in the folder name.
131
+
132
+ <!-- prettier-ignore -->
133
+ ```diff
134
+ app/
135
+ ├── routes/
136
+ + │ ├── _auth
137
+ + │ │ ├── login.tsx
138
+ + │ │ ├── _layout.tsx
139
+ + │ │ └── register.tsx
140
+ │ ├── index.tsx
141
+ │ └── concerts
142
+ │ ├── _layout.tsx
143
+ │ └── $city.tsx
144
+ └── root.tsx
145
+ ```
146
+
147
+ | URL | Matched Route | Layout |
148
+ | -------------------------- | -------------------- | ----------------------------------- |
149
+ | `/` | `index.tsx` | `root.tsx` |
150
+ | `/login` | `_auth/login.tsx` | `root.tsx` > `_auth/_layout.tsx` |
151
+ | `/register` | `_auth/register.tsx` | `root.tsx` > `_auth/_layout.tsx` |
152
+ | `/concerts/salt-lake-city` | `concerts/$city.tsx` | `root.tsx` > `concerts/_layout.tsx` |
153
+
154
+ Think of the `_leading` underscore as a blanket you're pulling over the filename, hiding the filename from the URL.
155
+
156
+ Pathless routes can be used at any nesting level to create additional shared layouts.
157
+
158
+ <!-- prettier-ignore -->
159
+ ```diff
160
+ app/
161
+ ├── routes/
162
+ │ ├── _sales/
163
+ │ │ ├── _invoices/
164
+ │ │ │ ├── $invoiceId.tsx
165
+ │ │ │ ├── index.tsx
166
+ │ │ │ └── _layout.tsx
167
+ │ │ ├── overview.tsx
168
+ │ │ └── _layout.tsx
169
+ │ └── about.tsx
170
+ └── root.tsx
171
+ ```
172
+
173
+ | URL | Matched Route | Layout |
174
+ | ----------- | --------------------------------- | ----------------------------------------------------------- |
175
+ | `/` | `_sales/_invoices/index.tsx` | `root.tsx` > `_sales/_layout.tsx` > `_invoices/_layout.tsx` |
176
+ | `/about` | `about.tsx` | `root.tsx` |
177
+ | `/overview` | `_sales/overview.tsx` | `root.tsx` > `_sales/_layout.tsx` |
178
+ | `/102000` | `_sales/_invoices/$invoiceId.tsx` | `root.tsx` > `_sales/_layout.tsx` > `_invoices/_layout.tsx` |
179
+
180
+
181
+ > Note: Additional `_leading` underscores in a folder name have no effect so that `__sales/` is interpreted the same as `_sales/`. Files with a `_leading` underscore in the name are given no special treatment (`_about.tsx` matches the url `/_about`)
182
+
183
+ ### Optional Segments
184
+
185
+ Wrapping a route segment in parentheses will make the segment optional.
186
+
187
+ <!-- prettier-ignore -->
188
+ ```diff
189
+ app/
190
+ ├── routes/
191
+ + │ ├── ($lang).tsx
192
+ + │ ├── ($lang).$productId.tsx
193
+ + │ └── ($lang).categories.tsx
194
+ └── root.tsx
195
+ ```
196
+
197
+ | URL | Matched Route |
198
+ | -------------------------- | ------------------------ |
199
+ | `/` | `($lang).tsx` |
200
+ | `/categories` | `($lang).categories.tsx` |
201
+ | `/en/categories` | `($lang).categories.tsx` |
202
+ | `/fr/categories` | `($lang).categories.tsx` |
203
+ | `/american-flag-speedo` | `($lang).tsx` |
204
+ | `/en/american-flag-speedo` | `($lang).$productId.tsx` |
205
+ | `/fr/american-flag-speedo` | `($lang).$productId.tsx` |
206
+
207
+ You may wonder why `/american-flag-speedo` is matching the `($lang).tsx` route instead of `($lang).$productId.tsx`. This is because when you have an optional dynamic param segment followed by another dynamic param, Remix cannot reliably determine if a single-segment URL such as `/american-flag-speedo` should match `/:lang` `/:productId`. Optional segments match eagerly and thus it will match `/:lang`. If you have this type of setup it's recommended to look at `params.lang` in the `($lang).tsx` loader and redirect to `/:lang/american-flag-speedo` for the current/default language if `params.lang` is not a valid language code.
208
+
209
+ ### Splat Routes
210
+
211
+ While [dynamic segments](#dynamic-segments) match a single path segment (the stuff between two `/` in a URL), a splat route will match the rest of a URL, including the slashes.
212
+
213
+ <!-- prettier-ignore -->
214
+ ```diff
215
+ app/
216
+ ├── routes/
217
+ │ ├── index.tsx
218
+ + │ ├── $.tsx
219
+ │ ├── about.tsx
220
+ + │ └── files.$.tsx
221
+ └── root.tsx
222
+ ```
223
+
224
+ | URL | Matched Route |
225
+ | -------------------------------------------- | ------------- |
226
+ | `/` | `index.tsx` |
227
+ | `/beef/and/cheese` | `$.tsx` |
228
+ | `/files` | `files.$.tsx` |
229
+ | `/files/talks/remix-conf_old.pdf` | `files.$.tsx` |
230
+ | `/files/talks/remix-conf_final.pdf` | `files.$.tsx` |
231
+ | `/files/talks/remix-conf-FINAL-MAY_2022.pdf` | `files.$.tsx` |
232
+
233
+ ### Escaping Special Characters
234
+
235
+ If you want one of the special characters Remix uses for these route conventions to actually be a part of the URL, you can escape the conventions with `[]` characters.
236
+
237
+ | Filename | URL |
238
+ | ------------------------------ | ------------------ |
239
+ | `routes/sitemap[.]xml.tsx` | `/sitemap.xml` |
240
+ | `routes/[sitemap.xml].tsx` | `/sitemap.xml` |
241
+ | `routes/weird-url.[index].tsx` | `/weird-url/index` |
242
+ | `routes/dolla-bills-[$].tsx` | `/dolla-bills-$` |
243
+ | `routes/[[so-weird]].tsx` | `/[so-weird]` |
244
+
245
+ ## Folders for Organization
246
+
247
+ All files or folders with a `trailing_` underscore will be excluded from the file based routing. This allows you to organize your code closer to the routes that use them instead of repeating the feature names across other folders.
248
+
249
+ Consider these routes:
250
+
251
+ ```diff
252
+ app/
253
+ ├── routes/
254
+ │ ├── _landing/
255
+ │ │ ├── _layout.tsx
256
+ │ │ ├── about.tsx
257
+ │ │ └── index.tsx
258
+ │ ├── work/
259
+ │ │ ├── _layout.tsx
260
+ │ │ ├── projects.tsx
261
+ │ │ └── index.tsx
262
+ │ └── work.projects.$id.roadmap.tsx
263
+ └── root.tsx
264
+ ```
265
+
266
+ Some, or all of them can be folders holding additional helper files.
267
+
268
+ ```diff
269
+ app/
270
+ ├── routes/
271
+ │ ├── _landing/
272
+ │ │ ├── _layout.tsx
273
+ │ │ ├── about/
274
+ │ │ │ ├── helpers_
275
+ │ │ │ │ ├── employee-profile-card.tsx
276
+ │ │ │ │ ├── get-employee-data.server.tsx
277
+ │ │ │ │ └── team-photo.jpg
278
+ │ │ │ └── index.tsx
279
+ │ │ ├── footer_.tsx
280
+ │ │ ├── header_.tsx
281
+ │ │ ├── scroll-experience_.tsx
282
+ │ │ └── index.tsx
283
+ │ ├── work/
284
+ │ │ ├── _layout.tsx
285
+ │ │ ├── projects/
286
+ │ │ │ ├── get-projects.server_.tsx
287
+ │ │ │ ├── project-card_.tsx
288
+ │ │ │ ├── project-buttons_.tsx
289
+ │ │ │ └── index.tsx
290
+ │ │ ├── primary-nav_.tsx
291
+ │ │ ├── footer_.tsx
292
+ │ │ ├── stats_.tsx
293
+ │ │ └── index.tsx
294
+ │ └── work.projects.$id.roadmap/
295
+ │ ├── chart_.tsx
296
+ │ ├── update-timeline.server_.tsx
297
+ │ └── index.tsx
298
+ └── root.tsx
299
+ ```
300
+
301
+ ```
302
+ # these are the same route:
303
+ routes/work.tsx
304
+ routes/work/index.tsx
305
+ ```
306
+
307
+ # Differences with Remix Routing v1, Remix Routing v2, and Remix Flat Routes
308
+
309
+ This routing convention aims to be more flexible than Remix Routing v1, more intuitive than Remix Routing v2, and simpler than
310
+ Remix Flat Routes. Ultimately this is a matter of choosing your trade-offs and matching personal preferences, but this is the
311
+ convention which makes most sense for us.
312
+
313
+ ### Which files are treated as route files?
314
+
315
+ **Remix Routing v1:** All files in the `routes` directory becomes routes in the application.
316
+
317
+ **Remix Routing v2:** All *top-level* files in the `routes` directory become routes in the application, but folders with a `route.tsx`
318
+ file inside will also become routes in the application. Any files in a folder not named `route.tsx` will be ignored.
319
+
320
+ **Remix Flat Routes:** By default all files in the `routes` directory becomes routes in the application. If a folder is suffixed with
321
+ `+`, only the `index.tsx` file in that folder is treated as a route file.
322
+
323
+ **Pa Routes**: All files in the `routes` directory becomes routes in the application, unless it (or one or its parent folders) is suffixed with `_`.
324
+
325
+
326
+ ### How do I define the layout for a set of routes?
327
+
328
+ **Remix Routing v1:** A file who's name matches the name of an adjacent folder serves as the layout for all files in that folder.
329
+
330
+ **Remix Routing v2:** All the routes that start with `app/routes/concerts.` will be child routes of `app/routes/concerts.tsx`. Sometimes you want
331
+ the URL to be nested, but you don't want the automatic layout nesting. You can opt out of nesting with a trailing underscore on the parent segment.
332
+
333
+ **Remix Flat Routes:** All the routes that start with `app/routes/concerts.` will be child routes of `app/routes/concerts.tsx`. Sometimes you want
334
+ the URL to be nested, but you don't want the automatic layout nesting. You can opt out of nesting with a trailing underscore on the parent segment.
335
+ `app/routes/concerts/_layout.tsx` is equivalent to `app/routes/concerts.tsx`.
336
+
337
+ **Pa Routes**: Files named `_layout` serve as a layout for all other routes in the same folder.