two-stroke 6.5.2 → 7.0.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.
package/CLAUDE.md ADDED
@@ -0,0 +1,50 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What is this?
6
+
7
+ Two-stroke is a lightweight TypeScript framework for building type-safe APIs on Cloudflare Workers. It provides structured routing, authentication (PBKDF, JWT/JWK), Zod-based request/response validation, auto-generated OpenAPI docs, queue/cron/email handlers, and Sentry error tracking.
8
+
9
+ ## Commands
10
+
11
+ All commands are exposed as bin scripts (no `npm run` prefix needed when installed):
12
+
13
+ - **`pnpm lint`** — ESLint + Prettier check (fails on violations)
14
+ - **`pnpm format`** — ESLint fix + Prettier write
15
+ - **`pnpm test`** — Builds with `wrangler deploy --dry-run`, then runs Vitest with Cloudflare Workers pool (Miniflare)
16
+ - **`pnpm type-check`** — `wrangler deploy --dry-run` + `tsc --noEmit`
17
+ - **`pnpm dev`** — Local dev server via `wrangler dev`
18
+ - **`pnpm deploy <env> <version>`** — Deploy with Sentry release tracking
19
+ - **`pnpm api-types <url>`** — Generate TypeScript types from an OpenAPI endpoint
20
+
21
+ There is no way to run a single test file directly — use Vitest's built-in filtering (e.g., `vitest run src/foo.test.ts`).
22
+
23
+ ## Architecture
24
+
25
+ ### Core source files (`src/`)
26
+
27
+ - **`index.ts`** — Main `twoStroke<T>()` factory. Returns `fetch`, `queue`, `scheduled`, `email` handlers plus route registration methods (`get`, `post`, `put`, `delete`) and auth builders (`noAuth`, `pbkdf`, `jwt`). Routes are regex-matched with path parameters like `{userId}`.
28
+ - **`types.ts`** — Core type definitions: `Env` (CF bindings union), `Route<T, A>`, `Handler<T, I, O, A, P>`, `ExtractParameterNames<S>` (extracts `{param}` from path strings).
29
+ - **`open-api.ts`** — Generates OpenAPI 3.1.0 spec from registered routes; served at `/doc`.
30
+ - **`test.ts`** — Test utilities: `setupTests()` (fetch mocking, OpenAPI client), `fakeJWK()` (RS256 test tokens), request recording helpers, `waitForQueue()`.
31
+ - **`cmd.mjs`** — Shared utility for spawning subprocesses in bin scripts.
32
+
33
+ ### Request flow
34
+
35
+ 1. Route matched by regex against URL pathname
36
+ 2. Auth handler runs (returns claims or throws)
37
+ 3. Request body parsed and validated against Zod input schema (POST/PUT)
38
+ 4. Handler executes with typed context: `{ req, env, body, params, searchParams, claims, sentry, waitUntil }`
39
+ 5. Response validated against Zod output schema
40
+ 6. CORS + security headers applied automatically
41
+
42
+ ### Key conventions
43
+
44
+ - **Zod v4** — imported as `zod/v4` (not `zod`)
45
+ - **ESM only** — `"type": "module"` with `verbatimModuleSyntax` in tsconfig
46
+ - **Strict TypeScript** — `strict: true`, `noUncheckedIndexedAccess: true`, `isolatedModules: true`
47
+ - **Node 24.9+** required, pnpm 10.30+ via Corepack
48
+ - **Prettier** — 100 char print width
49
+ - **ESLint config** — extends `eslint-config-two-stroke`
50
+ - **Testing** — Vitest with `@cloudflare/vitest-pool-workers` pool; globals enabled (no imports needed for `describe`, `it`, `expect`)
package/README.md CHANGED
@@ -2,22 +2,6 @@
2
2
 
3
3
  A minimalist framework for Cloudflare Workers with built-in routing, authentication, and validation.
4
4
 
5
- ## Overview
6
-
7
- Two-Stroke is a lightweight framework for building APIs with Cloudflare Workers. It provides a structured approach to defining routes, handling authentication, validating requests and responses with Zod, and managing errors with Sentry.
8
-
9
- ## Features
10
-
11
- - **Type-safe routing** with path parameter extraction
12
- - **Schema validation** for request and response bodies using Zod
13
- - **Built-in authentication** methods (JWT, PBKDF)
14
- - **Error handling** with Sentry integration
15
- - **CORS support** out of the box
16
- - **Queue handling** for background processing
17
- - **Cron job support** for scheduled tasks
18
- - **Email handling** capabilities
19
- - **OpenAPI documentation** generation
20
-
21
5
  ## Installation
22
6
 
23
7
  ```bash
@@ -28,121 +12,420 @@ npm install two-stroke
28
12
 
29
13
  ```typescript
30
14
  import { twoStroke } from "two-stroke";
31
- import { z } from "zod";
15
+ import { z } from "zod/v4";
32
16
 
33
- // Define your environment type
34
17
  type MyEnv = {
35
- MY_SECRET: string;
36
18
  MY_KV: KVNamespace;
37
19
  };
38
20
 
39
- // Create a Two-Stroke app
40
21
  const app = twoStroke<MyEnv>("My API", "1.0.0");
41
22
 
42
- // Define routes
43
- app.get(
44
- app.noAuth,
45
- "/hello",
46
- z.object({ message: z.string() }),
47
- async ({ env }) => {
48
- return {
49
- body: { message: "Hello, World!" },
50
- };
51
- },
52
- );
23
+ app.get(app.noAuth, "/hello", z.object({ message: z.string() }), async () => ({
24
+ body: { message: "Hello, World!" },
25
+ }));
26
+
27
+ export default app;
28
+ ```
29
+
30
+ The `twoStroke` function returns an object that directly satisfies the Cloudflare Workers `ExportedHandler` interface — export it as `default` and it handles `fetch`, `queue`, `scheduled`, and `email` events.
31
+
32
+ ## API Reference
33
+
34
+ ### `twoStroke<T>(title, release, origin?)`
35
+
36
+ Creates a new application instance.
37
+
38
+ | Parameter | Type | Description |
39
+ | --------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
40
+ | `title` | `string` | API title, used in the generated OpenAPI spec. |
41
+ | `release` | `string` | Release version, used for Sentry and OpenAPI. |
42
+ | `origin` | `(origin: string \| null) => string` | Optional. Returns the `Access-Control-Allow-Origin` value for a given request origin. If omitted, defaults to `"*"`. In staging environments, `localhost` origins are always allowed. |
43
+
44
+ The environment type `T` must extend `Env`, which allows values of type `string`, `Queue`, `KVNamespace`, `R2Bucket`, `D1Database`, `Fetcher`, `Hyperdrive`, `DurableObjectNamespace`, `Vectorize`, or `ImagesBinding`.
45
+
46
+ Every environment must also include `SENTRY_DSN` and `SENTRY_ENVIRONMENT` string bindings. These are used automatically to initialize Sentry on every request, queue batch, scheduled event, and email.
47
+
48
+ Returns an object with the following methods:
53
49
 
54
- // Define a route with path parameters
50
+ ---
51
+
52
+ ### Route Registration
53
+
54
+ All route methods register an HTTP endpoint with authentication, validation, and a handler.
55
+
56
+ #### `app.get(auth, path, output, handler, params?)`
57
+
58
+ Registers a `GET` route.
59
+
60
+ | Parameter | Type | Description |
61
+ | --------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------ |
62
+ | `auth` | `(c: { req, env }) => Promise<A>` | Authentication function. |
63
+ | `path` | `string` | URL path pattern (e.g. `"/users/{userId}"`). |
64
+ | `output` | `ZodType` | Zod schema validating the response body. |
65
+ | `handler` | `Handler` | Async function handling the request. |
66
+ | `params` | `ZodObject` | Optional. Zod object schema for query parameters. When provided, these appear in the generated OpenAPI spec. |
67
+
68
+ #### `app.post(auth, path, input, output, handler, params?)`
69
+
70
+ Registers a `POST` route.
71
+
72
+ | Parameter | Type | Description |
73
+ | --------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
74
+ | `auth` | `(c: { req, env }) => Promise<A>` | Authentication function. |
75
+ | `path` | `string` | URL path pattern. |
76
+ | `input` | `ZodType \| undefined` | Zod schema validating the request body. Pass `undefined` for routes that don't require a body, or if you want to handle the body yourself (e.g a stream). Accepts both `application/json` and `application/x-www-form-urlencoded` content types. |
77
+ | `output` | `ZodType` | Zod schema validating the response body. |
78
+ | `handler` | `Handler` | Async function handling the request. |
79
+ | `params` | `ZodObject` | Optional. Zod object schema for query parameters. |
80
+
81
+ #### `app.put(auth, path, input, output, handler)`
82
+
83
+ Registers a `PUT` route. Same signature as `post` (without the optional `params`).
84
+
85
+ #### `app.delete(auth, path, output, handler, params?)`
86
+
87
+ Registers a `DELETE` route. Same signature as `get`.
88
+
89
+ ---
90
+
91
+ ### Path Parameters
92
+
93
+ Path parameters use `{name}` syntax and are automatically extracted via regex:
94
+
95
+ ```typescript
55
96
  app.get(
56
97
  app.noAuth,
57
- "/users/{userId}",
58
- z.object({ user: z.object({ id: z.string(), name: z.string() }) }),
98
+ "/users/{userId}/posts/{postId}",
99
+ z.object({ title: z.string() }),
59
100
  async ({ params }) => {
60
- return {
61
- body: { user: { id: params.userId, name: "John Doe" } },
62
- };
101
+ // params.userId and params.postId are typed as string
102
+ return { body: { title: "Hello" } };
63
103
  },
64
104
  );
105
+ ```
65
106
 
66
- // Define a POST route with request validation
67
- app.post(
68
- app.noAuth,
69
- "/messages",
70
- z.object({ content: z.string().min(1) }),
71
- z.object({ id: z.string() }),
72
- async ({ body }) => {
73
- return {
74
- body: { id: "msg_123" },
75
- };
76
- },
77
- );
107
+ The `ExtractParameterNames<P>` utility type extracts parameter names from the path string at compile time, so `params` is fully typed.
78
108
 
79
- // Export the worker handlers
80
- export default app;
109
+ ---
110
+
111
+ ### Handler Context
112
+
113
+ Every route handler receives a single context object:
114
+
115
+ ```typescript
116
+ async (c: {
117
+ req: Request; // Original Cloudflare Request
118
+ env: T; // Environment bindings
119
+ body: z.infer<I>; // Parsed & validated request body (undefined for GET/DELETE)
120
+ params: { ... }; // Extracted path parameters, typed from the path string
121
+ searchParams: URLSearchParams; // URL query parameters
122
+ claims: A; // Authentication claims (type depends on auth method)
123
+ sentry: Toucan; // Sentry instance for error tracking
124
+ waitUntil: (p: Promise<void>) => void; // Extend request lifetime
125
+ }) => Promise<Response>
126
+ ```
127
+
128
+ Handlers must return an object with a `body` and optional `status` and `headers`:
129
+
130
+ ```typescript
131
+ // Success (200 is the default)
132
+ return { body: { id: "123" } };
133
+
134
+ // Redirect
135
+ return { body: { url: "/new-location" }, status: 302, headers: { Location: "/new-location" } };
136
+
137
+ // Error
138
+ return { body: { error: "Not found" }, status: 404 };
81
139
  ```
82
140
 
83
- ## Authentication
141
+ Valid status codes for typed success responses are `200` (default), `301`, and `302`. Any other numeric status is allowed when the body is `{ error: string }` or omitted.
142
+
143
+ ---
144
+
145
+ ### Authentication
146
+
147
+ #### `app.noAuth`
84
148
 
85
- Two-Stroke provides several authentication methods out of the box:
149
+ No authentication. The `claims` value is `null`.
86
150
 
87
151
  ```typescript
88
- // No authentication
89
- app.get(app.noAuth, "/public", z.object({ message: z.string() }), async () => ({
90
- body: { message: "Public endpoint" },
91
- }));
152
+ app.get(app.noAuth, "/public", outputSchema, handler);
153
+ ```
92
154
 
93
- // PBKDF authentication
94
- app.get(
95
- app.pbkdf("API_KEY"),
96
- "/protected",
97
- z.object({ message: z.string() }),
98
- async () => ({ body: { message: "Protected endpoint" } }),
99
- );
155
+ #### `app.pbkdf(key, customHeaderName?)`
156
+
157
+ PBKDF2 key verification. Validates a `Bearer` or `token` scheme credential against a hashed secret stored in the environment. The `claims` value is `void`.
158
+
159
+ | Parameter | Type | Description |
160
+ | ------------------ | --------- | --------------------------------------------------------------------------------- |
161
+ | `key` | `keyof T` | Environment binding name containing the PBKDF2 hash. |
162
+ | `customHeaderName` | `string` | Optional. Header name to read the credential from. Defaults to `"Authorization"`. |
163
+
164
+ ```typescript
165
+ app.get(app.pbkdf("API_KEY_HASH"), "/protected", outputSchema, handler);
166
+
167
+ // With custom header
168
+ app.get(app.pbkdf("API_KEY_HASH", "X-Api-Key"), "/protected", outputSchema, handler);
169
+ ```
170
+
171
+ #### `app.jwt<J>(key, audience)`
172
+
173
+ JWT verification using JWK (JSON Web Keys). Fetches the OIDC configuration and JWKS from the issuer URL, then verifies the token. The `claims` value is typed as `J`.
174
+
175
+ | Parameter | Type | Description |
176
+ | ---------- | --------- | ---------------------------------------------------------- |
177
+ | `key` | `keyof T` | Environment binding name containing the issuer URL. |
178
+ | `audience` | `keyof T` | Environment binding name containing the expected audience. |
179
+
180
+ ```typescript
181
+ type Claims = { sub: string; email: string };
100
182
 
101
- // JWT authentication
102
183
  app.get(
103
- app.jwt<{ userId: string }>("JWT_SECRET", "JWT_AUDIENCE"),
104
- "/user-data",
105
- z.object({ userId: z.string() }),
106
- async ({ claims }) => ({ body: { userId: claims.userId } }),
184
+ app.jwt<Claims>("AUTH_ISSUER", "AUTH_AUDIENCE"),
185
+ "/me",
186
+ z.object({ email: z.string() }),
187
+ async ({ claims }) => ({
188
+ // claims is typed as Claims
189
+ body: { email: claims.email },
190
+ }),
107
191
  );
108
192
  ```
109
193
 
110
- ## Queue Handling
194
+ Authentication failures return `401` with a `WWW-Authenticate: Bearer` header.
195
+
196
+ ---
197
+
198
+ ### Queue Handling
199
+
200
+ #### `app.queueHandler(input, handler)`
201
+
202
+ Registers a queue consumer. Only one queue handler can be registered per app.
203
+
204
+ | Parameter | Type | Description |
205
+ | --------- | ----------------------------------------------------------- | ---------------------------------------------------- |
206
+ | `input` | `ZodType` | Zod schema for validating each message in the batch. |
207
+ | `handler` | `(c: { env, batch, sentry, parsedBatch }) => Promise<void>` | Async function processing the batch. |
208
+
209
+ The `parsedBatch` is an array of `ZodSafeParseResult` objects, one per message, allowing you to handle valid and invalid messages individually.
111
210
 
112
211
  ```typescript
113
- // Define a queue handler
114
212
  app.queueHandler(
115
- z.object({ id: z.string() }),
213
+ z.object({ userId: z.string(), action: z.string() }),
116
214
  async ({ batch, parsedBatch, env }) => {
117
215
  for (let i = 0; i < batch.messages.length; i++) {
118
- if (parsedBatch[i].success) {
119
- const data = parsedBatch[i].data;
120
- // Process queue message
121
- console.log(`Processing message: ${data.id}`);
216
+ const result = parsedBatch[i];
217
+ if (result.success) {
218
+ console.log(result.data.userId, result.data.action);
219
+ batch.messages[i].ack();
220
+ } else {
221
+ batch.messages[i].retry();
122
222
  }
123
223
  }
124
224
  },
125
225
  );
226
+ ```
227
+
228
+ ### `addToQueue(queue, message, config?)`
229
+
230
+ Standalone utility for sending messages to a queue with exponential backoff retry.
126
231
 
127
- // Add to queue with retry logic
232
+ ```typescript
128
233
  import { addToQueue } from "two-stroke";
129
234
 
130
- await addToQueue(
131
- env.MY_QUEUE,
132
- { id: "task_123" },
133
- {
134
- retries: 3,
135
- backoffFactor: 2,
136
- },
137
- );
235
+ await addToQueue(env.MY_QUEUE, { userId: "123", action: "sync" });
236
+ ```
237
+
238
+ | Config Option | Type | Default | Description |
239
+ | --------------- | -------- | ------- | ------------------------------------------------------------ |
240
+ | `retries` | `number` | `5` | Maximum number of send attempts. |
241
+ | `backoffFactor` | `number` | `2` | Base for exponential backoff (in seconds: `factor^attempt`). |
242
+
243
+ All other properties on `config` are forwarded to the Cloudflare `Queue.send()` options (e.g. `contentType`, `delaySeconds`).
244
+
245
+ ---
246
+
247
+ ### Scheduled Tasks
248
+
249
+ #### `app.schedule(cron, handler)`
250
+
251
+ Registers a cron-triggered handler. Multiple schedules can be registered.
252
+
253
+ | Parameter | Type | Description |
254
+ | --------- | --------------------------------------- | ---------------------------------------------------------- |
255
+ | `cron` | `string` | Cron expression (must match a trigger in `wrangler.toml`). |
256
+ | `handler` | `(c: { env, sentry }) => Promise<void>` | Async function to run on schedule. |
257
+
258
+ ```typescript
259
+ app.schedule("0 * * * *", async ({ env, sentry }) => {
260
+ // Runs every hour
261
+ });
262
+
263
+ app.schedule("0 0 * * *", async ({ env }) => {
264
+ // Runs daily at midnight
265
+ });
138
266
  ```
139
267
 
140
- ## Scheduled Tasks
268
+ ---
269
+
270
+ ### Email Handling
271
+
272
+ #### `app.emailHandler(handler)`
273
+
274
+ Registers an email handler for Cloudflare Email Routing. Only one email handler can be registered per app.
275
+
276
+ | Parameter | Type | Description |
277
+ | --------- | ------------------------------------------------ | ------------------------------------ |
278
+ | `handler` | `(c: { env, message, sentry }) => Promise<void>` | Async function processing the email. |
279
+
280
+ The `message` is a Cloudflare `ForwardableEmailMessage`.
141
281
 
142
282
  ```typescript
143
- // Define a scheduled task
144
- app.schedule("*/15 * * * *", async ({ env }) => {
145
- // Run every 15 minutes
146
- console.log("Running scheduled task");
283
+ app.emailHandler(async ({ message, env }) => {
284
+ console.log(`Email from ${message.from} to ${message.to}`);
285
+ await message.forward("archive@example.com");
147
286
  });
148
287
  ```
288
+
289
+ ---
290
+
291
+ ### OpenAPI Documentation
292
+
293
+ A `GET /doc` endpoint is automatically registered and serves an OpenAPI 3.1.0 specification generated from all registered routes. It includes:
294
+
295
+ - Path and query parameters (from path patterns and `params` schemas)
296
+ - Request body schemas (from `input`)
297
+ - Response body schemas (from `output`)
298
+ - Security requirements (routes using auth other than `noAuth` are marked with bearer auth)
299
+ - Standard `400` and `500` error response schemas
300
+
301
+ ---
302
+
303
+ ### Request & Response Behavior
304
+
305
+ **Request validation**: `POST` and `PUT` bodies are validated against the `input` schema. Invalid bodies return `400` with an `error` message and Zod `issues` array.
306
+
307
+ **Response validation**: `200` responses are validated against the `output` schema. Validation failures are logged but the response is still sent (the schema acts as a development-time warning, not a gate).
308
+
309
+ **CORS**: All responses include `Access-Control-Allow-Origin`. `OPTIONS` requests are handled automatically with a `204` and appropriate headers allowing `GET`, `HEAD`, `PUT`, `POST`, `DELETE` methods and `Authorization`, `Content-Type` headers.
310
+
311
+ **Security headers**: All responses include `Strict-Transport-Security`, `X-Content-Type-Options: nosniff`, and `Content-Security-Policy: default-src 'self'`. Custom headers from the handler are preserved and take precedence.
312
+
313
+ **Content types**: Responses default to `application/json`. Handlers can override this via the `headers` return value — when `Content-Type` is not `application/json`, the body is sent as-is without JSON serialization.
314
+
315
+ **Errors**: Unhandled exceptions return `500` and are reported to Sentry. Authentication failures return `401`.
316
+
317
+ **404**: Unmatched routes return an empty `404` response.
318
+
319
+ ---
320
+
321
+ ## Testing Utilities
322
+
323
+ Two-stroke exports testing utilities from `two-stroke/test` designed for use with Vitest and `@cloudflare/vitest-pool-workers`.
324
+
325
+ ### `setupTests<Paths>()`
326
+
327
+ Initializes the test environment. Call once per test file. Returns:
328
+
329
+ | Property | Type | Description |
330
+ | ----------------------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
331
+ | `url` | `URL` | Base URL (`https://example.com/`). |
332
+ | `fetchMock` | `MockAgent` | Cloudflare's fetch mock (from `cloudflare:test`). Activated with `disableNetConnect()` in `beforeAll`. |
333
+ | `client` | `Client<Paths>` | An `openapi-fetch` client pointed at the worker, typed with the `Paths` generic (typically generated from the OpenAPI spec). |
334
+ | `waitForQueue(trigger)` | `(trigger: () => Promise<void>) => Promise<void>` | Triggers an action and waits until the queue batch finishes processing. |
335
+ | `fakeJWK(issuer, audience, claims)` | `(issuer, audience, claims) => Promise<string>` | Generates an RS256-signed JWT for testing. Automatically mocks the OIDC discovery and JWKS endpoints on the issuer URL. Returns the signed token string. |
336
+
337
+ ```typescript
338
+ import { setupTests } from "two-stroke/test";
339
+ import type { paths } from "./api";
340
+
341
+ const { client, fetchMock, fakeJWK, waitForQueue } = await setupTests<paths>();
342
+
343
+ describe("GET /hello", () => {
344
+ it("returns a greeting", async () => {
345
+ const { data, response } = await client.GET("/hello");
346
+ expect(response.status).toBe(200);
347
+ expect(data?.message).toBe("Hello, World!");
348
+ });
349
+ });
350
+ ```
351
+
352
+ ### `recordRequest(cb, statusCode, data, responseOptions?)`
353
+
354
+ Creates a fetch mock reply handler that captures the JSON request body and returns a fixed response. Useful for intercepting outgoing API calls.
355
+
356
+ ```typescript
357
+ let captured: unknown;
358
+ fetchMock
359
+ .get("https://api.example.com")
360
+ .intercept({ method: "POST", path: "/webhook" })
361
+ .reply(
362
+ recordRequest(
363
+ (data) => {
364
+ captured = data;
365
+ },
366
+ 200,
367
+ { ok: true },
368
+ ),
369
+ );
370
+ ```
371
+
372
+ ### `recordFormRequest(cb, statusCode, data, responseOptions?)`
373
+
374
+ Same as `recordRequest` but parses the body as `application/x-www-form-urlencoded`.
375
+
376
+ ### `recordFirehoseRequest(cb, statusCode, data, responseOptions?)`
377
+
378
+ Same as `recordRequest` but also decodes and parses a base64-encoded `Record.Data` field from the JSON body (for AWS Firehose-style payloads). Calls `cb` twice: once with the raw body and once with the decoded inner payload.
379
+
380
+ ---
381
+
382
+ ## CLI Commands
383
+
384
+ Two-stroke provides executable bin scripts. In consuming projects, these are available directly as commands (e.g. `npx dev`, `npx test`). For framework development, run them with `pnpm`:
385
+
386
+ ### `dev`
387
+
388
+ Starts a local development server via `wrangler dev`.
389
+
390
+ ### `test`
391
+
392
+ Builds the worker with `wrangler deploy --dry-run`, generates TypeScript types from the OpenAPI spec into `test/api.d.ts`, then runs `vitest`. Passes all arguments through to vitest (e.g. `test --watch`, `test src/users.test.ts`).
393
+
394
+ ### `lint`
395
+
396
+ Runs `eslint --cache --max-warnings=0` followed by `prettier --cache --check .`. Fails on any violation.
397
+
398
+ ### `format`
399
+
400
+ Runs `eslint --cache --fix` followed by `prettier --cache --write .`.
401
+
402
+ ### `type-check`
403
+
404
+ Runs `wrangler deploy --dry-run --outdir=dist` followed by `tsc --noEmit`.
405
+
406
+ ### `deploy <env> <version>`
407
+
408
+ Deploys the worker to a Cloudflare environment with Sentry release tracking:
409
+
410
+ 1. Writes the version to `src/release.ts`
411
+ 2. Uploads secrets via `wrangler secret bulk`
412
+ 3. Creates and finalizes a Sentry release
413
+ 4. Runs `wrangler deploy`
414
+ 5. Uploads sourcemaps to Sentry
415
+
416
+ ### `api-types <urls>`
417
+
418
+ Generates TypeScript type definitions from live OpenAPI endpoints. Accepts a comma-separated list of service URLs. Fetches `/doc` from each, converts to TypeScript via `openapi-typescript`, and writes definition files to `src/__definitions__/`.
419
+
420
+ ### `bulk <entry|rest> <entryfile>`
421
+
422
+ Uploads files from `dist/` to a Cloudflare KV namespace. Used for serving static assets.
423
+
424
+ - `bulk entry index.html` — uploads only the entry file (with `nocache` cache control)
425
+ - `bulk rest index.html` — uploads everything except the entry file (with immutable cache control)
426
+
427
+ Requires `NAMESPACE` and `DOMAIN` environment variables.
428
+
429
+ ## License
430
+
431
+ MIT
package/package.json CHANGED
@@ -10,19 +10,21 @@
10
10
  "type-check": "./bin/type-check.mjs"
11
11
  },
12
12
  "dependencies": {
13
- "@cloudflare/vitest-pool-workers": "^0.12.19",
14
- "@sentry/cli": "^3.3.0",
15
- "@types/node": "^25.3.3",
16
- "@typescript-eslint/eslint-plugin": "^8.56.1",
17
- "@typescript-eslint/parser": "^8.56.1",
18
- "@vitest/coverage-istanbul": "^4.0.18",
13
+ "@cloudflare/vitest-pool-workers": "^0.13.2",
14
+ "@sentry/cli": "^3.3.3",
15
+ "@types/node": "^25.5.0",
16
+ "@typescript-eslint/eslint-plugin": "^8.57.1",
17
+ "@typescript-eslint/parser": "^8.57.1",
18
+ "@vitest/runner": "^4.1.0",
19
+ "@vitest/snapshot": "^4.1.0",
19
20
  "eslint-config-prettier": "^10.1.8",
20
- "eslint-config-two-stroke": "^1.4.6",
21
+ "eslint-config-two-stroke": "^1.5.1",
21
22
  "eslint-config-typescript": "^3.0.0",
22
- "jose": "^6.2.0",
23
+ "jose": "^6.2.1",
23
24
  "jwk-subtle": "^1.1.5",
24
25
  "mime": "^4.1.0",
25
- "miniflare": "^4.20260312.1",
26
+ "miniflare": "^4.20260317.0",
27
+ "msw": "^2.12.13",
26
28
  "openapi-fetch": "^0.17.0",
27
29
  "openapi-typescript": "^7.13.0",
28
30
  "pbkdf-subtle": "^1.1.5",
@@ -31,10 +33,10 @@
31
33
  },
32
34
  "peerDependencies": {
33
35
  "@sentry/cli": ">=2",
34
- "eslint": ">=9",
36
+ "eslint": ">=10",
35
37
  "prettier": ">=3",
36
38
  "typescript": ">=5",
37
- "vitest": ">=3",
39
+ "vitest": ">=4",
38
40
  "wrangler": ">=3"
39
41
  },
40
42
  "description": "Simple Cloudflare Worker framework.",
@@ -57,12 +59,12 @@
57
59
  "url": "https://github.com/change-engine/two-stroke"
58
60
  },
59
61
  "type": "module",
60
- "version": "6.5.2",
62
+ "version": "7.0.0",
61
63
  "devDependencies": {
62
64
  "@types/eslint": "^9.6.1",
63
65
  "eslint": "^10.0.3",
64
66
  "prettier": "^3.8.1",
65
67
  "typescript": "^5.9.3",
66
- "vitest": "^4.0.18"
68
+ "vitest": "^4.1.0"
67
69
  }
68
70
  }
package/src/fake.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { twoStroke } from ".";
2
+
3
+ export default twoStroke("fake", "0.1")