@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,248 @@
1
+ ---
2
+ name: hash
3
+ description: "Pluggable hashing and verification with driver-based backends for @maestro-js/hash. Use when working with @maestro-js/hash, hashing values, verifying hashes, detecting when a rehash is needed, or rotating signing keys. Built-in drivers: SHA256 (HMAC-SHA256 with key rotation and timing-safe comparison)."
4
+ ---
5
+
6
+ # @maestro-js/hash
7
+
8
+ HMAC-SHA256 signing and verification with transparent key rotation. Built on the
9
+ Provider pattern from `@maestro-js/service-registry`.
10
+
11
+ ## Quick Setup
12
+
13
+ ```ts
14
+ import { Hash } from '@maestro-js/hash'
15
+
16
+ Hash.Provider.register('default', Hash.Provider.create({
17
+ key: process.env.HMAC_SIGNING_KEY!,
18
+ previousKeys: [process.env.HMAC_SIGNING_KEY_PREV!]
19
+ }))
20
+
21
+ // Use the facade directly
22
+ const signed = Hash.signString('order.completed:ord_8k2Xp')
23
+ Hash.isSignatureValid({ plaintext: 'order.completed:ord_8k2Xp', signedText: signed }) // true
24
+ ```
25
+
26
+ ## API Reference
27
+
28
+ ### `Hash.Provider.create(config)` — Factory
29
+
30
+ Create a hash service instance. Accepts a keyset config and returns a `HashService`.
31
+
32
+ ```ts
33
+ interface Keyset {
34
+ key: string // Current signing key, base64-encoded
35
+ previousKeys: string[] // Rotated-out keys, base64-encoded (verification only)
36
+ }
37
+
38
+ const svc = Hash.Provider.create({
39
+ key: 'base64EncodedCurrentKey',
40
+ previousKeys: ['base64EncodedOldKey1', 'base64EncodedOldKey2']
41
+ })
42
+ ```
43
+
44
+ All keys must be valid base64 strings. The factory asserts that at least the
45
+ current key or the first previous key exists at creation time.
46
+
47
+ ### `Hash.Provider.register(name, service)` — Registration
48
+
49
+ Register a created service under a name for later resolution.
50
+
51
+ ```ts
52
+ Hash.Provider.register('default', svc)
53
+ ```
54
+
55
+ ### `Hash.provider(key)` — Resolution
56
+
57
+ Resolve a named instance. The default facade (`Hash.signString`, etc.) resolves
58
+ `'default'` automatically.
59
+
60
+ ```ts
61
+ const custom = Hash.provider('myService')
62
+ custom.signString('data')
63
+ ```
64
+
65
+ ### `signString(plaintext)` — Sign
66
+
67
+ Sign a plaintext string using the current key. Returns a `keyId.digest` string
68
+ where `keyId` is a 4-character identifier derived from the key.
69
+
70
+ ```ts
71
+ const signed = Hash.signString('Testing')
72
+ // Returns something like 'Ab1c.xYzBase64Digest...'
73
+ ```
74
+
75
+ ### `isSignatureValid({ plaintext, signedText })` — Verify
76
+
77
+ Verify a signed string against the plaintext. Checks all keys in the keyset
78
+ whose ID matches the embedded key ID. Uses `crypto.timingSafeEqual` to prevent
79
+ timing attacks.
80
+
81
+ ```ts
82
+ Hash.isSignatureValid({ plaintext: 'Testing', signedText: signed }) // true
83
+ ```
84
+
85
+ Returns `false` for tampered signatures, unknown keys, or length mismatches.
86
+
87
+ ### `getSigningKeyId(signedText)` — Extract Key ID
88
+
89
+ Extract the key ID prefix from a signed string. Returns `undefined` if the
90
+ string has no `.` separator (malformed).
91
+
92
+ ```ts
93
+ Hash.getSigningKeyId(signed) // 'Ab1c'
94
+ Hash.getSigningKeyId('no-dot') // undefined
95
+ ```
96
+
97
+ ### `getCurrentKeyId()` — Current Key ID
98
+
99
+ Return the 4-character ID of the current active signing key.
100
+
101
+ ```ts
102
+ Hash.getCurrentKeyId() // 'Ab1c'
103
+ ```
104
+
105
+ ## Key Rotation
106
+
107
+ Key rotation is the primary design goal. The keyset has one current key (used
108
+ for signing) and any number of previous keys (used only for verification).
109
+
110
+ 1. Generate a new base64-encoded 256-bit key.
111
+ 2. Move the old `key` into `previousKeys`.
112
+ 3. Set the new key as `key`.
113
+
114
+ ```ts
115
+ // Before rotation
116
+ Hash.Provider.register('default', Hash.Provider.create({
117
+ key: 'currentKeyBase64',
118
+ previousKeys: []
119
+ }))
120
+
121
+ // After rotation — old signatures still verify
122
+ Hash.Provider.register('default', Hash.Provider.create({
123
+ key: 'newKeyBase64',
124
+ previousKeys: ['currentKeyBase64']
125
+ }))
126
+ ```
127
+
128
+ New calls to `signString` use the new key. Existing signed strings created with
129
+ the old key still pass `isSignatureValid` because the old key remains in the
130
+ keyset for verification.
131
+
132
+ ### How Key IDs Work
133
+
134
+ Each key gets a 4-character ID computed as:
135
+
136
+ ```ts
137
+ crypto.createHash('sha256').update(key).digest('base64').substring(0, 4)
138
+ ```
139
+
140
+ Signed strings embed this ID before the dot: `keyId.hmacDigest`. During
141
+ verification, only keys matching the embedded ID are tested.
142
+
143
+ ## Common Patterns
144
+
145
+ ### Sign and Verify a Webhook Payload
146
+
147
+ ```ts
148
+ const payload = JSON.stringify({ event: 'order.completed', orderId: 'ord_8k2Xp' })
149
+ const signature = Hash.signString(payload)
150
+
151
+ // Later, verify the signature
152
+ const valid = Hash.isSignatureValid({ plaintext: payload, signedText: signature })
153
+ ```
154
+
155
+ ### Multiple Named Instances
156
+
157
+ ```ts
158
+ Hash.Provider.register('webhooks', Hash.Provider.create({
159
+ key: process.env.WEBHOOK_SIGNING_KEY!,
160
+ previousKeys: []
161
+ }))
162
+
163
+ Hash.Provider.register('tokens', Hash.Provider.create({
164
+ key: process.env.TOKEN_SIGNING_KEY!,
165
+ previousKeys: []
166
+ }))
167
+
168
+ const webhookSigned = Hash.provider('webhooks').signString(payload)
169
+ const tokenSigned = Hash.provider('tokens').signString(tokenData)
170
+ ```
171
+
172
+ Extend `Hash.Provider.Keys` to get type-safe provider names:
173
+
174
+ ```ts
175
+ declare module '@maestro-js/hash' {
176
+ namespace Hash.Provider {
177
+ interface Keys {
178
+ webhooks: unknown
179
+ tokens: unknown
180
+ }
181
+ }
182
+ }
183
+ ```
184
+
185
+ ### Generate a Key for Development
186
+
187
+ ```ts
188
+ import crypto from 'node:crypto'
189
+ const key = crypto.randomBytes(32).toString('base64')
190
+ ```
191
+
192
+ ## Testing
193
+
194
+ Run tests:
195
+
196
+ ```bash
197
+ pnpm --filter @maestro-js/hash test
198
+ # or
199
+ cd packages/hash && npx beartest ./tests/hash.test.ts
200
+ ```
201
+
202
+ Tests use `beartest-js` with `expect`. Create service instances directly with
203
+ `Hash.Provider.create()` without registering — no global state needed.
204
+
205
+ ```ts
206
+ import crypto from 'node:crypto'
207
+ import { Hash } from '@maestro-js/hash'
208
+ import { test } from 'beartest-js'
209
+ import { expect } from 'expect'
210
+
211
+ test('should sign and verify', () => {
212
+ const key = crypto.randomBytes(32).toString('base64')
213
+ const svc = Hash.Provider.create({ key, previousKeys: [] })
214
+
215
+ const signed = svc.signString('hello')
216
+ expect(svc.isSignatureValid({ plaintext: 'hello', signedText: signed })).toBe(true)
217
+ })
218
+
219
+ test('key rotation preserves old signatures', () => {
220
+ const key1 = crypto.randomBytes(32).toString('base64')
221
+ const key2 = crypto.randomBytes(32).toString('base64')
222
+
223
+ const svc1 = Hash.Provider.create({ key: key1, previousKeys: [] })
224
+ const signed1 = svc1.signString('data')
225
+
226
+ const svc2 = Hash.Provider.create({ key: key2, previousKeys: [key1] })
227
+ expect(svc2.isSignatureValid({ plaintext: 'data', signedText: signed1 })).toBe(true)
228
+ })
229
+ ```
230
+
231
+ ## Cross-Package Integration
232
+
233
+ - **Depends on**: `@maestro-js/service-registry` for the Provider pattern
234
+ (registry, deferred proxy resolution, facade).
235
+ - **No downstream dependents**: standalone utility package.
236
+ - **Shares pattern with**: `@maestro-js/crypt` (same Provider + keyset
237
+ rotation architecture, but for encryption rather than signing).
238
+
239
+ ## Package Structure
240
+
241
+ ```
242
+ packages/hash/
243
+ src/index.ts # Single-file implementation and type declarations
244
+ tests/hash.test.ts # beartest-js tests
245
+ package.json # @maestro-js/hash
246
+ ```
247
+
248
+ Source file: `packages/hash/src/index.ts`
@@ -0,0 +1,249 @@
1
+ ---
2
+ name: helpers
3
+ description: "Standalone utility functions for async control flow and file handling in the @maestro-js/helpers package. Use when working with @maestro-js/helpers. Key capabilities: retry with configurable attempts/delay/backoff, sleep, withMaxConcurrency for limiting parallel executions, withRateLimit with fixed or sliding window, raceWithFallback for promise racing with default values, AsyncIterableCollection for lazy async iteration (map, filter, reduce, forEach, toArray), sanitizeFilename for safe cross-platform filenames, and withRetry for wrapping functions with retry behavior."
4
+ ---
5
+
6
+ # @maestro-js/helpers
7
+
8
+ Standalone utility package providing async control flow primitives (retry, concurrency, rate limiting, promise racing) and helper functions (filename sanitization, async iterable collections). No dependencies on other maestro packages.
9
+
10
+ ## Available Utilities
11
+
12
+ - **retry** - Retry an async operation with configurable attempts, delay, and conditional retry logic
13
+ - **withRetry** - Wrap a function to add retry behavior, returning a new function
14
+ - **sleep** - Pause execution for a given number of milliseconds
15
+ - **withMaxConcurrency** - Wrap a function to limit the number of concurrent executions
16
+ - **withRateLimit** - Wrap a function to enforce rate limits using fixed or sliding time windows
17
+ - **raceWithFallback** - Race a promise against another, returning a fallback value if the racer wins
18
+ - **AsyncIterableCollection** - Wrap an `AsyncIterable` with chainable collection methods (map, filter, reduce, forEach, toArray)
19
+ - **sanitizeFilename** - Strip illegal, control, and reserved characters from a filename string
20
+
21
+ ## API Reference
22
+
23
+ ### retry
24
+
25
+ Retry an async function with configurable options. Defaults: 3 attempts, 1000ms delay.
26
+
27
+ ```ts
28
+ function retry<R = void>(
29
+ fn: () => Promise<R>,
30
+ options?: Partial<{
31
+ maxAttempts: number
32
+ delay: number | ((attempt: number) => number)
33
+ shouldRetry: (error: unknown) => boolean
34
+ }>
35
+ ): Promise<R>
36
+ ```
37
+
38
+ - `maxAttempts` - Maximum number of attempts (default: 3)
39
+ - `delay` - Fixed milliseconds or a function receiving the attempt number (default: 1000)
40
+ - `shouldRetry` - Predicate that receives the error; return `false` to stop retrying immediately
41
+
42
+ ### withRetry
43
+
44
+ Return a new function that wraps the original with retry behavior. Accepts the same options as `retry`.
45
+
46
+ ```ts
47
+ function withRetry<Args extends any[], R>(
48
+ callback: (...args: Args) => Promise<R>,
49
+ options?: Partial<Helpers.RetryOptions>
50
+ ): (...args: Args) => Promise<R>
51
+ ```
52
+
53
+ ### sleep
54
+
55
+ Pause execution for the specified duration.
56
+
57
+ ```ts
58
+ function sleep(ms: number): Promise<void>
59
+ ```
60
+
61
+ ### withMaxConcurrency
62
+
63
+ Return a new function that limits how many invocations run in parallel. Excess calls queue and wait.
64
+
65
+ ```ts
66
+ function withMaxConcurrency<Args extends any[], R>(
67
+ callback: (...args: Args) => Promise<R>,
68
+ options?: { concurrency?: number }
69
+ ): (...args: Args) => Promise<R>
70
+ ```
71
+
72
+ - `concurrency` - Maximum parallel executions (default: 10)
73
+
74
+ ### withRateLimit
75
+
76
+ Return a new function that enforces a rate limit using either fixed or sliding time windows.
77
+
78
+ ```ts
79
+ function withRateLimit<Args extends any[], R>(
80
+ callback: (...args: Args) => Promise<R>,
81
+ options?: {
82
+ windowType?: 'fixed' | 'sliding'
83
+ window: number
84
+ limit?: number
85
+ }
86
+ ): (...args: Args) => Promise<R>
87
+ ```
88
+
89
+ - `windowType` - `'fixed'` (clock-aligned windows) or `'sliding'` (rolling window). Default: `'fixed'`
90
+ - `window` - Window size in milliseconds (default: 1000)
91
+ - `limit` - Maximum executions per window (default: 1)
92
+
93
+ ### raceWithFallback
94
+
95
+ Race a primary promise against a competing promise. If the competing promise resolves first, return the fallback value instead.
96
+
97
+ ```ts
98
+ function raceWithFallback<Result, Fallback = null>(
99
+ promise: Promise<Result>,
100
+ options: {
101
+ fallback?: Fallback
102
+ promiseToRace: Promise<unknown>
103
+ }
104
+ ): Promise<{ value: Awaited<Result> | Fallback }>
105
+ ```
106
+
107
+ Returns `{ value }` where value is the primary result or the fallback.
108
+
109
+ ### AsyncIterableCollection
110
+
111
+ Wrap any `AsyncIterable<T>` with chainable collection methods. Create with `Helpers.AsyncIterableCollection.from(iterable)`.
112
+
113
+ ```ts
114
+ interface AsyncIterableCollection<T> extends AsyncIterable<T> {
115
+ forEach(callbackfn: (item: T, index: number) => unknown): Promise<void>
116
+ map<S>(callbackfn: (item: T, index: number) => S): AsyncIterableCollection<S>
117
+ filter(predicate: (value: T, index: number) => unknown): AsyncIterableCollection<T>
118
+ reduce<U>(callbackfn: (prev: U, curr: T, index: number) => U, initialValue: U): Promise<U>
119
+ toArray(): Promise<T[]>
120
+ }
121
+ ```
122
+
123
+ - `map` and `filter` return a new `AsyncIterableCollection` (lazy, chainable)
124
+ - `forEach`, `reduce`, and `toArray` are terminal operations that consume the iterable
125
+
126
+ ### sanitizeFilename
127
+
128
+ Remove illegal, control, unicode whitespace, reserved, and Windows-reserved characters from a filename. Truncate to 255 bytes.
129
+
130
+ ```ts
131
+ function sanitizeFilename(input: string): string
132
+ ```
133
+
134
+ ## Common Patterns
135
+
136
+ ### Retry with exponential backoff
137
+
138
+ ```ts
139
+ import { Helpers } from '@maestro-js/helpers'
140
+
141
+ const result = await Helpers.retry(() => fetchFromApi(), {
142
+ maxAttempts: 5,
143
+ delay: (attempt) => 1000 * 2 ** (attempt - 1)
144
+ })
145
+ ```
146
+
147
+ ### Retry only on specific errors
148
+
149
+ ```ts
150
+ await Helpers.retry(() => callExternalService(), {
151
+ maxAttempts: 3,
152
+ delay: 500,
153
+ shouldRetry: (error) => (error as any)?.code === 'TIMEOUT'
154
+ })
155
+ ```
156
+
157
+ ### Wrap a function with retry behavior
158
+
159
+ ```ts
160
+ const resilientFetch = Helpers.withRetry(fetchFromApi, {
161
+ maxAttempts: 3,
162
+ delay: 1000
163
+ })
164
+ const result = await resilientFetch('/endpoint')
165
+ ```
166
+
167
+ ### Limit concurrent API calls
168
+
169
+ ```ts
170
+ const limitedFetch = Helpers.withMaxConcurrency(fetch, { concurrency: 5 })
171
+
172
+ const results = await Promise.all(
173
+ urls.map((url) => limitedFetch(url))
174
+ )
175
+ ```
176
+
177
+ ### Rate-limit outbound requests
178
+
179
+ ```ts
180
+ const rateLimited = Helpers.withRateLimit(sendRequest, {
181
+ windowType: 'sliding',
182
+ window: 1000,
183
+ limit: 10
184
+ })
185
+ ```
186
+
187
+ ### Race a slow operation with a timeout
188
+
189
+ ```ts
190
+ const { value } = await Helpers.raceWithFallback(
191
+ slowDatabaseQuery(),
192
+ {
193
+ fallback: null,
194
+ promiseToRace: Helpers.sleep(5000)
195
+ }
196
+ )
197
+ // value is the query result, or null if it took longer than 5 seconds
198
+ ```
199
+
200
+ ### Process async iterables with collection methods
201
+
202
+ ```ts
203
+ const collection = Helpers.AsyncIterableCollection.from(asyncGenerator())
204
+
205
+ const results = await collection
206
+ .filter((item) => item.active)
207
+ .map((item) => item.name)
208
+ .toArray()
209
+ ```
210
+
211
+ ### Reduce an async iterable
212
+
213
+ ```ts
214
+ const total = await Helpers.AsyncIterableCollection.from(rowStream())
215
+ .reduce((sum, row) => sum + row.amount, 0)
216
+ ```
217
+
218
+ ### Sanitize a user-provided filename
219
+
220
+ ```ts
221
+ const safe = Helpers.sanitizeFilename(userInput)
222
+ // Strips illegal chars, control chars, Windows reserved names, trailing dots/spaces
223
+ // Truncates to 255 bytes
224
+ ```
225
+
226
+ ## Testing
227
+
228
+ Run tests with beartest-js:
229
+
230
+ ```bash
231
+ pnpm --filter @maestro-js/helpers test
232
+ ```
233
+
234
+ Or run the test file directly:
235
+
236
+ ```bash
237
+ cd packages/helpers && npx beartest ./tests/helpers.test.ts
238
+ ```
239
+
240
+ Tests use `beartest-js` with `expect` for assertions. Test file location: `packages/helpers/tests/helpers.test.ts`.
241
+
242
+ ## Package Info
243
+
244
+ - **Package name**: `@maestro-js/helpers`
245
+ - **Source**: `packages/helpers/src/index.ts`
246
+ - **No dependencies** on other maestro packages
247
+ - **No downstream dependents**
248
+ - Build: `tsup` (ESM + .d.ts)
249
+ - Code style: Prettier (2-space indent, no semicolons, single quotes, 125 char width)