two-stroke 7.11.0 → 8.0.1

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.
@@ -17,4 +17,10 @@ jobs:
17
17
 
18
18
  - uses: ./.github/actions/install
19
19
 
20
- - run: pnpm eslint
20
+ - run: pnpm oxlint
21
+
22
+ - run: pnpm oxfmt --check
23
+
24
+ - run: pnpm tsc --noEmit
25
+
26
+ - run: pnpm knip
package/.knip.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "entry": ["src/*.ts"],
3
+ "ignoreDependencies": ["cloudflare"]
4
+ }
package/bin/api-types.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import fs from "node:fs";
4
4
  import openapiTS from "openapi-typescript";
5
- import prettier from "prettier";
5
+ import { format } from "oxfmt";
6
6
  import ts from "typescript";
7
7
 
8
8
  const services = process.argv[2].split(",");
@@ -17,6 +17,7 @@ await Promise.all(
17
17
  if ("format" in schemaObject && schemaObject.format === "uuid") {
18
18
  return schemaObject.nullable ? ts.factory.createUnionTypeNode([UUID, NULL]) : UUID;
19
19
  }
20
+ return null;
20
21
  },
21
22
  });
22
23
  const printer = ts.createPrinter({});
@@ -28,14 +29,17 @@ await Promise.all(
28
29
  const result = printer.printNode(ts.EmitHint.Unspecified, output[0], resultFile);
29
30
  fs.writeFileSync(
30
31
  `src/__definitions__/${service.replace("https://", "")}-definitions.ts`,
31
- await prettier.format(
32
- `// Autogenerated by two-stroke
32
+ (
33
+ await format(
34
+ `src/__definitions__/${service.replace("https://", "")}-definitions.ts`,
35
+ `// Autogenerated by two-stroke
33
36
 
34
- ${fs.readFileSync("src/__definitions__/type-definitions.ts", "utf-8")}
37
+ ${fs.readFileSync("src/__definitions__/type-definitions.ts", "utf8")}
35
38
 
36
39
  ${result}`,
37
- { parser: "typescript", printWidth: 100 },
38
- ),
40
+ { parser: "typescript", printWidth: 100 },
41
+ )
42
+ ).code,
39
43
  );
40
44
  }),
41
45
  );
package/bin/bulk.mjs CHANGED
@@ -2,11 +2,11 @@
2
2
  "use strict";
3
3
 
4
4
  import fs from "fs";
5
- import { cmd } from "../src/cmd.mjs";
6
5
  import mime from "mime";
6
+ import { cmd } from "../src/cmd.mjs";
7
7
 
8
8
  const entryOrRest = process.argv[2];
9
- const entry = process.argv.slice(3);
9
+ const entry = new Set(process.argv.slice(3));
10
10
 
11
11
  const ents = await fs.promises.readdir("dist", {
12
12
  withFileTypes: true,
@@ -15,14 +15,12 @@ const ents = await fs.promises.readdir("dist", {
15
15
  const files = await Promise.all(
16
16
  ents
17
17
  .filter((ent) => ent.isFile())
18
- .filter((ent) =>
19
- entryOrRest == "entry" ? entry.includes(ent.name) : !entry.includes(ent.name),
20
- )
18
+ .filter((ent) => (entryOrRest == "entry" ? entry.has(ent.name) : !entry.has(ent.name)))
21
19
  .map((ent) => {
22
20
  const type = mime.getType(`${ent.parentPath}/${ent.name}`);
23
21
  const key = `${ent.parentPath.substring(4)}/${ent.name}`;
24
22
  return (async () => ({
25
- key: entry.includes(ent.name) ? `${process.env.DOMAIN}${key}` : key,
23
+ key: entry.has(ent.name) ? `${process.env.DOMAIN}${key}` : key,
26
24
  value: (await fs.promises.readFile(`${ent.parentPath}/${ent.name}`)).toString("base64"),
27
25
  base64: true,
28
26
  metadata: {
@@ -32,7 +30,7 @@ const files = await Promise.all(
32
30
  : type === "text/javascript"
33
31
  ? "text/javascript;charset=utf-8"
34
32
  : type,
35
- "Cache-Control": entry.includes(ent.name) ? "nocache" : "max-age=31536000, immutable",
33
+ "Cache-Control": entry.has(ent.name) ? "nocache" : "max-age=31536000, immutable",
36
34
  },
37
35
  }))();
38
36
  }),
package/bin/deploy.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  import fs from "fs";
4
- import { cmd } from "../src/cmd.mjs";
5
4
  import { basename } from "path";
5
+ import { cmd } from "../src/cmd.mjs";
6
6
 
7
7
  const env = process.argv[2];
8
8
  const release = process.argv[3];
package/bin/format.mjs CHANGED
@@ -2,5 +2,5 @@
2
2
  "use strict";
3
3
  import { cmd } from "../src/cmd.mjs";
4
4
 
5
- cmd("eslint --cache --fix");
6
- cmd("prettier --cache --write .");
5
+ cmd("oxfmt");
6
+ cmd("oxlint --fix");
package/bin/lint.mjs CHANGED
@@ -6,5 +6,5 @@ import { cmd } from "../src/cmd.mjs";
6
6
  if (fs.existsSync("wrangler.jsonc")) {
7
7
  cmd("wrangler types --env-file /dev/null --strict-vars false --check");
8
8
  }
9
- cmd("eslint --cache --max-warnings=0");
10
- cmd("prettier --cache --check .");
9
+ cmd("oxfmt --check");
10
+ cmd("oxlint");
package/bin/test.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  import fs from "fs";
4
- import { cmd } from "../src/cmd.mjs";
5
- import consumers from "stream/consumers";
4
+ import { Miniflare } from "miniflare";
6
5
  import openapiTS from "openapi-typescript";
7
- import prettier from "prettier";
6
+ import { format } from "oxfmt";
7
+ import consumers from "stream/consumers";
8
8
  import ts from "typescript";
9
- import { Miniflare } from "miniflare";
9
+ import { cmd } from "../src/cmd.mjs";
10
10
 
11
11
  if (fs.existsSync("wrangler.jsonc")) {
12
12
  cmd("wrangler deploy --env= --dry-run --outdir=dist");
@@ -32,7 +32,7 @@ if (fs.existsSync("wrangler.jsonc")) {
32
32
  .join("\n\n");
33
33
  fs.writeFileSync(
34
34
  "test/api.d.ts",
35
- await prettier.format(result, { parser: "typescript", printWidth: 100 }),
35
+ (await format("test/api.d.ts", result, { parser: "typescript", printWidth: 100 })).code,
36
36
  );
37
37
  }
38
38
  }
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from "oxfmt";
2
+
3
+ export default defineConfig({
4
+ sortImports: {
5
+ newlinesBetween: false,
6
+ groups: [
7
+ ["value-builtin", "value-external"],
8
+ ["value-internal", "value-parent", "value-sibling", "value-index"],
9
+ { newlinesBetween: true },
10
+ "type-import",
11
+ "unknown",
12
+ ],
13
+ },
14
+ ignorePatterns: ["worker-configuration.d.ts", "wrangler.jsonc"],
15
+ });
@@ -0,0 +1,90 @@
1
+ import { defineConfig } from "oxlint";
2
+
3
+ export default defineConfig({
4
+ plugins: ["eslint", "typescript", "unicorn", "oxc", "import", "promise", "vitest"],
5
+ options: {
6
+ typeAware: true,
7
+ },
8
+ categories: {
9
+ correctness: "error",
10
+ suspicious: "warn",
11
+ perf: "error",
12
+ style: "warn",
13
+ },
14
+ rules: {
15
+ "no-map-spread": "off",
16
+ "no-named-export": "off",
17
+ "no-magic-numbers": "off",
18
+ "consistent-indexed-object-style": "off",
19
+ "id-length": "off",
20
+ "no-await-expression-member": "off",
21
+ "max-nested-calls": "off",
22
+ "sort-keys": "off",
23
+ curly: "off",
24
+ "switch-case-braces": "off",
25
+ "vitest/require-hook": "off",
26
+ "no-ternary": "off",
27
+ "group-exports": "off",
28
+ "exports-last": "off",
29
+ "prefer-default-export": "off",
30
+ "no-null": "off",
31
+ "consistent-type-specifier-style": "off",
32
+ "max-statements": "off",
33
+ "consistent-type-imports": "error",
34
+ "relative-url-style": ["error", "always"],
35
+ "no-namespace": "off",
36
+ "no-duplicate-imports": ["error", { allowSeparateTypeImports: true }],
37
+ "func-style": "off",
38
+ "sort-imports": [
39
+ "error",
40
+ {
41
+ ignoreDeclarationSort: true,
42
+ ignoreMemberSort: false,
43
+ },
44
+ ],
45
+ "new-cap": "off",
46
+ "promise/prefer-await-to-callbacks": "off",
47
+ "promise/avoid-new": "off",
48
+ "prefer-await-to-then": "off",
49
+ "max-params": "off",
50
+ "no-underscore-dangle": "off",
51
+ "no-nested-ternary": "off",
52
+ "unicorn/no-nested-ternary": "off",
53
+ "no-anonymous-default-export": "off",
54
+ "no-continue": "off",
55
+ "init-declarations": "off",
56
+ "prefer-to-be-truthy": "off",
57
+ "vitest/prefer-expect-assertions": "off",
58
+ "vitest/prefer-importing-vitest-globals": "off",
59
+ "vitest/no-importing-vitest-globals": "error",
60
+ "vitest/require-top-level-describe": "off",
61
+ "vitest/max-expects": "off",
62
+ "vitest/no-hooks": "off",
63
+ },
64
+ overrides: [
65
+ {
66
+ files: ["*.test.ts", "*.spec.ts"],
67
+ rules: {
68
+ "@typescript-eslint/no-explicit-any": "off",
69
+ },
70
+ },
71
+ {
72
+ files: ["*-definitions.ts"],
73
+ rules: {
74
+ "no-unused-vars": "off",
75
+ "@typescript-eslint/no-redundant-type-constituents": "off",
76
+ "@typescript-eslint/no-duplicate-type-constituents": "off",
77
+ "group-exports": "off",
78
+ "exports-last": "off",
79
+ "consistent-indexed-object-style": "off",
80
+ },
81
+ },
82
+ {
83
+ files: ["bin/*.mjs", "src/cmd.mjs"],
84
+ rules: {
85
+ "import/no-nodejs-modules": "off",
86
+ "prefer-destructuring": "off",
87
+ },
88
+ },
89
+ ],
90
+ });
package/package.json CHANGED
@@ -1,4 +1,12 @@
1
1
  {
2
+ "name": "two-stroke",
3
+ "version": "8.0.1",
4
+ "description": "Simple Cloudflare Worker framework.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/change-engine/two-stroke"
9
+ },
2
10
  "bin": {
3
11
  "api-types": "./bin/api-types.mjs",
4
12
  "bulk": "./bin/bulk.mjs",
@@ -9,59 +17,44 @@
9
17
  "test": "./bin/test.mjs",
10
18
  "type-check": "./bin/type-check.mjs"
11
19
  },
20
+ "type": "module",
21
+ "main": "src/index.ts",
12
22
  "dependencies": {
13
- "@cloudflare/vitest-pool-workers": "^0.18.0",
14
- "@sentry/cli": "^3.6.0",
15
- "@typescript-eslint/eslint-plugin": "^8.63.0",
16
- "@typescript-eslint/parser": "^8.62.1",
17
- "@vitest/runner": "^4.1.8",
18
- "@vitest/snapshot": "^4.1.8",
19
- "eslint-config-prettier": "^10.1.8",
20
- "eslint-config-two-stroke": "^1.5.7",
21
- "eslint-config-typescript": "^3.0.0",
22
23
  "jose": "^6.2.3",
23
24
  "mime": "^4.1.0",
24
- "miniflare": "^4.20260701.0",
25
- "msw": "^2.14.6",
25
+ "msw": "^2.15.0",
26
26
  "openapi-fetch": "^0.17.0",
27
27
  "openapi-typescript": "^7.13.0",
28
28
  "toucan-js": "^4.1.1",
29
29
  "zod": "^4.4.3"
30
30
  },
31
+ "devDependencies": {
32
+ "knip": "^6.26.0",
33
+ "oxfmt": "^0.58.0",
34
+ "oxlint": "^1.74.0",
35
+ "oxlint-tsgolint": "^0.24.0",
36
+ "typescript": "^6.0.3",
37
+ "vitest": "^4.1.10"
38
+ },
31
39
  "peerDependencies": {
32
- "@sentry/cli": ">=2",
33
- "eslint": ">=10",
34
- "prettier": ">=3",
40
+ "@sentry/cli": "^3.6.0",
41
+ "miniflare": "^4.20260708.1",
42
+ "oxfmt": "^0.58.0",
43
+ "oxlint": "^1.73.0",
44
+ "oxlint-tsgolint": "^0.24.0",
35
45
  "typescript": ">=5",
36
46
  "vitest": ">=4",
37
47
  "wrangler": ">=3"
38
48
  },
39
- "description": "Simple Cloudflare Worker framework.",
40
- "engines": {
41
- "node": "^24.9.0"
49
+ "prettier": {
50
+ "printWidth": 100
42
51
  },
43
52
  "eslintConfig": {
44
53
  "extends": [
45
54
  "two-stroke"
46
55
  ]
47
56
  },
48
- "license": "MIT",
49
- "main": "src/index.ts",
50
- "name": "two-stroke",
51
- "prettier": {
52
- "printWidth": 100
53
- },
54
- "repository": {
55
- "type": "git",
56
- "url": "https://github.com/change-engine/two-stroke"
57
- },
58
- "type": "module",
59
- "version": "7.11.0",
60
- "devDependencies": {
61
- "@types/eslint": "^9.6.1",
62
- "eslint": "^10.6.0",
63
- "prettier": "^3.9.4",
64
- "typescript": "^6.0.3",
65
- "vitest": "^4.1.10"
57
+ "engines": {
58
+ "node": "^24.9.0"
66
59
  }
67
60
  }
package/src/cmd.mjs CHANGED
@@ -8,7 +8,6 @@ export function cmd(program, args = []) {
8
8
  stdio: "inherit",
9
9
  },
10
10
  );
11
- // eslint-disable-next-line n/no-process-exit
12
11
  if (status) process.exit(status);
13
12
  if (error) throw error;
14
13
  }
package/src/index.ts CHANGED
@@ -1,27 +1,22 @@
1
1
  import { Toucan } from "toucan-js";
2
- import { type ZodSafeParseResult, z, ZodObject, ZodType } from "zod/v4";
2
+ import { z } from "zod/v4";
3
3
  import { openAPI } from "./open-api";
4
4
  import { type Handler, type Route } from "./types";
5
+ import { jwkVerifyMulti, pbkdfVerify } from "./util";
6
+
5
7
  import type { JSONWebKeySet } from "jose";
6
- import { pbkdfVerify, jwkVerifyMulti } from "./util";
8
+ import type { ZodObject, ZodSafeParseResult, ZodType } from "zod/v4";
7
9
 
8
- // eslint-disable-next-line @typescript-eslint/require-await
9
10
  const noAuth = async () => null;
10
11
 
11
- const escapeRegex = (str: string) => str.replace(/([.*+?^=!:$()|[\]\\])/g, "\\$&");
12
+ const escapeRegex = (str: string) => str.replace(/(?<seg>[.*+?^=!:$()|[\]\\])/g, String.raw`\$&`);
12
13
 
13
14
  export function twoStroke<T>(
14
15
  title: string,
15
16
  release: string,
16
17
  origin?: (o: string | null) => string,
17
18
  ) {
18
- let _queue: (c: {
19
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
- batch: MessageBatch<any>;
21
- env: T;
22
- sentry: Toucan;
23
- }) => Promise<void>;
24
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ let _queue: (c: { batch: MessageBatch<any>; env: T; sentry: Toucan }) => Promise<void>;
25
20
  const routes: Route<T, any>[] = [];
26
21
  routes.push({
27
22
  auth: noAuth,
@@ -87,10 +82,10 @@ export function twoStroke<T>(
87
82
  );
88
83
  let claims;
89
84
  try {
90
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
85
+ // oxlint-disable-next-line no-await-in-loop
91
86
  claims = await route.auth({ req, env });
92
- } catch (err) {
93
- console.warn(err);
87
+ } catch (error) {
88
+ console.warn(error);
94
89
  return new Response("", {
95
90
  status: 401,
96
91
  statusText: "Invalid Authorization",
@@ -105,19 +100,21 @@ export function twoStroke<T>(
105
100
  try {
106
101
  rawBody = route.input
107
102
  ? req.headers.get("Content-Type") === "application/x-www-form-urlencoded"
108
- ? Object.fromEntries(new URLSearchParams(await req.text()))
109
- : await req.json()
103
+ ? // oxlint-disable-next-line no-await-in-loop
104
+ Object.fromEntries(new URLSearchParams(await req.text()))
105
+ : // oxlint-disable-next-line no-await-in-loop
106
+ await req.json()
110
107
  : undefined;
111
- } catch (e) {
108
+ } catch (error) {
112
109
  console.error({
113
110
  message: "Request body is required'",
114
- error: e instanceof Error ? e.message : "Error",
111
+ error: error instanceof Error ? error.message : "Error",
115
112
  });
116
- return new Response(
117
- JSON.stringify({
113
+ return Response.json(
114
+ {
118
115
  error: "Request body is required",
119
116
  issues: [],
120
- }),
117
+ },
121
118
  {
122
119
  status: 400,
123
120
  headers: defaultHeaders,
@@ -132,11 +129,11 @@ export function twoStroke<T>(
132
129
  error: undefined,
133
130
  };
134
131
  if (body.success)
132
+ // oxlint-disable-next-line no-await-in-loop
135
133
  response = await route.handler({
136
134
  req,
137
135
  env,
138
136
  body: body.data,
139
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
140
137
  claims,
141
138
  params,
142
139
  searchParams: new URL(req.url).searchParams,
@@ -149,12 +146,12 @@ export function twoStroke<T>(
149
146
  error: body.error,
150
147
  body: rawBody,
151
148
  });
152
- return new Response(
153
- JSON.stringify({
149
+ return Response.json(
150
+ {
154
151
  error: "Request body schema invalid",
155
152
  issues: JSON.parse(body.error?.message ?? "{}") as unknown,
156
153
  name: body.error?.name,
157
- }),
154
+ },
158
155
  {
159
156
  status: 400,
160
157
  headers: defaultHeaders,
@@ -162,10 +159,10 @@ export function twoStroke<T>(
162
159
  );
163
160
  }
164
161
  } else {
162
+ // oxlint-disable-next-line no-await-in-loop
165
163
  response = await route.handler({
166
164
  req,
167
165
  env,
168
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
169
166
  claims,
170
167
  body: undefined,
171
168
  params,
@@ -180,7 +177,6 @@ export function twoStroke<T>(
180
177
  console.error({
181
178
  message: "Response body schema invalid",
182
179
  error: output.error,
183
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
184
180
  body: response.body,
185
181
  });
186
182
  }
@@ -202,7 +198,6 @@ export function twoStroke<T>(
202
198
  });
203
199
 
204
200
  return new Response(
205
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
206
201
  responseWithHeaders.headers.get("Content-Type") === "application/json"
207
202
  ? JSON.stringify(response.body)
208
203
  : response.body,
@@ -214,9 +209,9 @@ export function twoStroke<T>(
214
209
  status: 404,
215
210
  headers: defaultHeaders,
216
211
  });
217
- } catch (err) {
218
- console.warn(err);
219
- sentry.captureException(err);
212
+ } catch (error) {
213
+ console.warn(error);
214
+ sentry.captureException(error);
220
215
  return new Response("", {
221
216
  status: 500,
222
217
  statusText: "Internal Server Error",
@@ -240,9 +235,9 @@ export function twoStroke<T>(
240
235
  });
241
236
  try {
242
237
  return await _queue({ batch, env, sentry });
243
- } catch (err) {
244
- console.warn(err);
245
- sentry.captureException(err);
238
+ } catch (error) {
239
+ console.warn(error);
240
+ sentry.captureException(error);
246
241
  }
247
242
  },
248
243
  async scheduled(
@@ -265,9 +260,9 @@ export function twoStroke<T>(
265
260
  throw new Error("CRON Handler not found");
266
261
  }
267
262
  await handler({ env, sentry });
268
- } catch (err) {
269
- console.warn(err);
270
- sentry.captureException(err);
263
+ } catch (error) {
264
+ console.warn(error);
265
+ sentry.captureException(error);
271
266
  }
272
267
  },
273
268
  async email(
@@ -286,9 +281,9 @@ export function twoStroke<T>(
286
281
  });
287
282
  try {
288
283
  await _email({ message, env, sentry });
289
- } catch (err) {
290
- console.warn(err);
291
- sentry.captureException(err);
284
+ } catch (error) {
285
+ console.warn(error);
286
+ sentry.captureException(error);
292
287
  }
293
288
  },
294
289
  emailHandler(
@@ -306,6 +301,7 @@ export function twoStroke<T>(
306
301
  const [scheme, token] = (req.headers.get(customHeaderName) ?? " ").split(" ");
307
302
  if (
308
303
  (scheme === "token" || scheme === "Bearer") &&
304
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
309
305
  (await pbkdfVerify(env[k] as string, token ?? ""))
310
306
  )
311
307
  return;
@@ -316,13 +312,17 @@ export function twoStroke<T>(
316
312
  async ({ req, env }: { req: Request; env: T }) => {
317
313
  const [scheme, token] = (req.headers.get("Authorization") ?? " ").split(" ");
318
314
  if (scheme === "Bearer") {
315
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
319
316
  const rawIssuer = env[k] as string;
317
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
320
318
  const rawAudience = env[ak] as string;
321
319
  const issuer = rawIssuer.startsWith("{")
322
- ? (JSON.parse(rawIssuer) as Record<string, JSONWebKeySet | true>)
320
+ ? // oxlint-disable-next-line typescript/no-unsafe-type-assertion
321
+ (JSON.parse(rawIssuer) as Record<string, JSONWebKeySet | true>)
323
322
  : { [rawIssuer]: true as const };
324
323
  const audience = rawAudience.startsWith("[")
325
- ? (JSON.parse(rawAudience) as string[])
324
+ ? // oxlint-disable-next-line typescript/no-unsafe-type-assertion
325
+ (JSON.parse(rawAudience) as string[])
326
326
  : [rawAudience];
327
327
  const claims = await jwkVerifyMulti<J>(token ?? "", issuer, audience);
328
328
  if (!claims) {
@@ -361,26 +361,21 @@ export function twoStroke<T>(
361
361
  console.log("Queue batch finished");
362
362
  };
363
363
  },
364
- put<
365
- I extends ZodType | undefined,
366
- O extends ZodType,
367
- A,
368
- P extends string,
369
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
370
- PP extends ZodObject<any> | undefined,
371
- >(
364
+ put<I extends ZodType | undefined, O extends ZodType, A, P extends string>(
372
365
  auth: Route<T, A>["auth"],
373
366
  path: P,
374
367
  input: I,
375
368
  output: O,
376
369
  handler: Handler<T, I, O, A, P>,
377
- params?: PP,
370
+ params?: ZodObject<any>,
378
371
  ) {
379
372
  routes.push({
380
373
  auth,
381
374
  method: "PUT",
382
375
  path,
383
- matcher: new RegExp(`^${escapeRegex(path).replaceAll(/\/{([^}]*)}/g, "/(?<$1>[^\\/]*)")}$`),
376
+ matcher: new RegExp(
377
+ `^${escapeRegex(path).replaceAll(/\/{(?<seg>[^}]*)}/g, String.raw`/(?<$seg>[^\/]*)`)}$`,
378
+ ),
384
379
  input,
385
380
  output,
386
381
  handler,
@@ -388,26 +383,21 @@ export function twoStroke<T>(
388
383
  });
389
384
  },
390
385
 
391
- post<
392
- I extends ZodType | undefined,
393
- O extends ZodType,
394
- A,
395
- P extends string,
396
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
397
- PP extends ZodObject<any> | undefined,
398
- >(
386
+ post<I extends ZodType | undefined, O extends ZodType, A, P extends string>(
399
387
  auth: Route<T, A>["auth"],
400
388
  path: P,
401
389
  input: I,
402
390
  output: O,
403
391
  handler: Handler<T, I, O, A, P>,
404
- params?: PP,
392
+ params?: ZodObject<any>,
405
393
  ) {
406
394
  routes.push({
407
395
  auth,
408
396
  method: "POST",
409
397
  path,
410
- matcher: new RegExp(`^${escapeRegex(path).replaceAll(/\/{([^}]*)}/g, "/(?<$1>[^\\/]*)")}$`),
398
+ matcher: new RegExp(
399
+ `^${escapeRegex(path).replaceAll(/\/{(?<seg>[^}]*)}/g, String.raw`/(?<$seg>[^\/]*)`)}$`,
400
+ ),
411
401
  input,
412
402
  output,
413
403
  handler,
@@ -415,47 +405,39 @@ export function twoStroke<T>(
415
405
  });
416
406
  },
417
407
 
418
- get<
419
- O extends ZodType,
420
- A,
421
- P extends string,
422
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
423
- PP extends ZodObject<any> | undefined,
424
- >(
408
+ get<O extends ZodType, A, P extends string>(
425
409
  auth: Route<T, A>["auth"],
426
410
  path: P,
427
411
  output: O,
428
412
  handler: Handler<T, undefined, O, A, P>,
429
- params?: PP,
413
+ params?: ZodObject<any>,
430
414
  ) {
431
415
  routes.push({
432
416
  auth,
433
417
  method: "GET",
434
418
  path,
435
- matcher: new RegExp(`^${escapeRegex(path).replaceAll(/\/{([^}]*)}/g, "/(?<$1>[^\\/]*)")}$`),
419
+ matcher: new RegExp(
420
+ `^${escapeRegex(path).replaceAll(/\/{(?<seg>[^}]*)}/g, String.raw`/(?<$seg>[^\/]*)`)}$`,
421
+ ),
436
422
  output,
437
423
  handler,
438
424
  params,
439
425
  });
440
426
  },
441
- delete<
442
- O extends ZodType,
443
- A,
444
- P extends string,
445
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
446
- PP extends ZodObject<any> | undefined,
447
- >(
427
+ delete<O extends ZodType, A, P extends string>(
448
428
  auth: Route<T, A>["auth"],
449
429
  path: P,
450
430
  output: O,
451
431
  handler: Handler<T, undefined, O, A, P>,
452
- params?: PP,
432
+ params?: ZodObject<any>,
453
433
  ) {
454
434
  routes.push({
455
435
  auth,
456
436
  method: "DELETE",
457
437
  path,
458
- matcher: new RegExp(`^${escapeRegex(path).replaceAll(/\/{([^}]*)}/g, "/(?<$1>[^\\/]*)")}$`),
438
+ matcher: new RegExp(
439
+ `^${escapeRegex(path).replaceAll(/\/{(?<seg>[^}]*)}/g, String.raw`/(?<$seg>[^\/]*)`)}$`,
440
+ ),
459
441
  output,
460
442
  handler,
461
443
  params,
@@ -474,13 +456,15 @@ export async function addToQueue<T>(queue: Queue<T>, message: T, config: AddToQu
474
456
 
475
457
  for (let i = 0; i < (retries ?? 5); i++) {
476
458
  try {
459
+ // oxlint-disable-next-line no-await-in-loop
477
460
  await queue.send(message, options);
478
461
  return;
479
- } catch (err) {
462
+ } catch (error) {
480
463
  const backoff = (backoffFactor ?? 2) ** i;
481
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
482
- console.log(`Error adding to queue: ${err}`);
464
+ // oxlint-disable-next-line typescript/restrict-template-expressions
465
+ console.log(`Error adding to queue: ${error}`);
483
466
  console.log(`Retrying in ${backoff} seconds`);
467
+ // oxlint-disable-next-line no-await-in-loop
484
468
  await new Promise((resolve) => setTimeout(resolve, backoff * 1000));
485
469
  }
486
470
  }
package/src/once.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { z, ZodSafeParseResult, ZodType } from "zod/v4";
1
+ import type { ZodSafeParseResult, ZodType, z } from "zod/v4";
2
2
 
3
3
  export const once = async <T extends { retry_count: number }>(
4
4
  key: string,
@@ -17,7 +17,7 @@ export const once = async <T extends { retry_count: number }>(
17
17
  {
18
18
  delaySeconds: Math.min(
19
19
  Math.round(
20
- (0.5 + Math.random() / 2) * Math.pow(retry_count, backoffExponent) + Math.random() * 4,
20
+ (0.5 + Math.random() / 2) * retry_count ** backoffExponent + Math.random() * 4,
21
21
  ),
22
22
  900,
23
23
  ),
@@ -50,15 +50,9 @@ export const retryHandler =
50
50
  env: T;
51
51
  }) => {
52
52
  const isFinalAttempt = body.retry >= maxRetries;
53
- await handler(
54
- body,
55
- env,
56
- isFinalAttempt,
57
- // eslint-disable-next-line @typescript-eslint/require-await
58
- async (cb) => {
59
- waitUntil(cb());
60
- },
61
- );
53
+ await handler(body, env, isFinalAttempt, async (cb) => {
54
+ waitUntil(cb());
55
+ });
62
56
 
63
57
  return { body: { ok: true } };
64
58
  };
@@ -82,7 +76,7 @@ export const retryQueue =
82
76
  parsedBatch: ZodSafeParseResult<z.output<M>>[];
83
77
  env: T;
84
78
  }) => {
85
- const message = batch.messages[0];
79
+ const [message] = batch.messages;
86
80
  if (!message || batch.messages.length !== 1) {
87
81
  console.warn({ batch });
88
82
  throw new Error(
@@ -101,31 +95,33 @@ export const retryQueue =
101
95
  message.ack();
102
96
  await cb();
103
97
  });
104
- } catch (err) {
98
+ } catch (error) {
105
99
  if (!isFinalAttempt) message.retry();
106
- throw err;
100
+ throw error;
107
101
  }
108
102
  };
109
103
 
110
104
  const isDuplicateMessage = async (key: string, bucket: R2Bucket) => {
111
105
  // Work around for CloudFlare R2 error:
112
- // put: We encountered an internal error. Please try again. (10001)
106
+ // Put: We encountered an internal error. Please try again. (10001)
113
107
  for (let i = 0; i < 5; i++) {
114
108
  try {
115
109
  // Will throw if that error occurs
110
+ // oxlint-disable-next-line no-await-in-loop
116
111
  const putResult = await bucket.put(`lock-${key}`, "", {
117
112
  onlyIf: new Headers({
118
113
  "If-Unmodified-Since": "Wed, 21 Oct 2015 07:28:00 GMT",
119
114
  }),
120
115
  });
121
- // if the returned result is null then the object already existed
116
+ // If the returned result is null then the object already existed
122
117
  const isDupe = putResult === null;
123
118
  if (isDupe) console.log("Duplicate message", { key });
124
119
  return isDupe;
125
- } catch (err) {
126
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
127
- console.warn(`Error putting ${key} to R2: ${err}`);
120
+ } catch (error) {
121
+ // oxlint-disable-next-line typescript/restrict-template-expressions
122
+ console.warn(`Error putting ${key} to R2: ${error}`);
128
123
  console.warn(`Retrying in ${i} seconds`);
124
+ // oxlint-disable-next-line no-await-in-loop
129
125
  await new Promise((resolve) => setTimeout(resolve, i * 1000));
130
126
  }
131
127
  }
package/src/open-api.ts CHANGED
@@ -1,9 +1,10 @@
1
- import { z, ZodType } from "zod/v4";
1
+ import { z } from "zod/v4";
2
2
  import { type Route } from "./types";
3
3
 
4
+ import type { ZodType } from "zod/v4";
5
+
4
6
  export const openAPI =
5
7
  <T, A>(title: string, release: string, noAuth: () => A, routes: Route<T, A>[]) =>
6
- // eslint-disable-next-line @typescript-eslint/require-await
7
8
  async () => ({
8
9
  body: {
9
10
  openapi: "3.1.0",
@@ -27,6 +28,7 @@ export const openAPI =
27
28
  r.method.toLocaleLowerCase(),
28
29
  {
29
30
  parameters: [
31
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
30
32
  ...Object.entries((r.params?.shape ?? {}) as Record<string, ZodType>).map(
31
33
  ([k, v]) => ({
32
34
  name: k,
package/src/test.ts CHANGED
@@ -1,14 +1,12 @@
1
- /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
2
1
  import { env, exports } from "cloudflare:workers";
3
2
  import { type JWTPayload, SignJWT, exportJWK, generateKeyPair } from "jose";
4
- import createClient from "openapi-fetch";
5
- import { http, HttpResponse } from "msw";
3
+ import { HttpResponse, http } from "msw";
6
4
  import { setupServer } from "msw/node";
5
+ import createClient from "openapi-fetch";
7
6
 
8
7
  const msw = setupServer();
9
8
  msw.listen();
10
9
 
11
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/require-await
12
10
  export const setupTests = async <Paths extends {}>() => {
13
11
  const url = new URL("https://example.com/");
14
12
 
@@ -21,11 +19,10 @@ export const setupTests = async <Paths extends {}>() => {
21
19
  url,
22
20
  client,
23
21
  msw,
24
- async waitForQueue(trigger: () => Promise<void>) {
25
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ waitForQueue: async (trigger: () => Promise<void>) => {
26
23
  const log: any[] = [];
27
24
  const orig = console.log;
28
- console.log = function (message) {
25
+ console.log = (message) => {
29
26
  orig(message);
30
27
  log.push(message);
31
28
  };
@@ -33,13 +30,11 @@ export const setupTests = async <Paths extends {}>() => {
33
30
  await waitUntil(() => expect(log).contains("Queue batch finished"));
34
31
  console.log = orig;
35
32
  },
36
- async fakeJWK(issuer: keyof typeof env, audience: keyof typeof env, claims: JWTPayload) {
33
+ fakeJWK: async (issuer: keyof typeof env, audience: keyof typeof env, claims: JWTPayload) => {
37
34
  const { publicKey, privateKey } = await generateKeyPair("RS256");
38
35
  // Hack around Cloudflare not setting
39
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
40
36
  // @ts-expect-error
41
37
  publicKey[Symbol.toStringTag] = "CryptoKey";
42
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
43
38
  // @ts-expect-error
44
39
  privateKey[Symbol.toStringTag] = "CryptoKey";
45
40
  const jwk = await exportJWK(publicKey);
@@ -47,25 +42,25 @@ export const setupTests = async <Paths extends {}>() => {
47
42
  jwk.alg = "RS256";
48
43
  beforeAll(() => {
49
44
  msw.use(
50
- ...[
51
- http.get(`${env[issuer]}/.well-known/openid-configuration`, () =>
52
- HttpResponse.json({
53
- jwks_uri: `${env[issuer]}/.well-known/jwks`,
54
- }),
55
- ),
56
- http.get(`${env[issuer]}/.well-known/jwks`, () =>
57
- HttpResponse.json({
58
- keys: [jwk],
59
- }),
60
- ),
61
- ],
45
+ http.get(`${env[issuer]}/.well-known/openid-configuration`, () =>
46
+ HttpResponse.json({
47
+ jwks_uri: `${env[issuer]}/.well-known/jwks`,
48
+ }),
49
+ ),
50
+ http.get(`${env[issuer]}/.well-known/jwks`, () =>
51
+ HttpResponse.json({
52
+ keys: [jwk],
53
+ }),
54
+ ),
62
55
  );
63
56
  });
64
57
  return await new SignJWT(claims)
65
58
  .setProtectedHeader({ typ: "JWT", alg: "RS256", kid: jwk.kid })
66
59
  .setIssuedAt()
67
60
  .setNotBefore("5 minutes ago")
61
+ // oxlint-disable-next-line typescript/no-unnecessary-type-assertion
68
62
  .setIssuer(env[issuer] as string)
63
+ // oxlint-disable-next-line typescript/no-unnecessary-type-assertion
69
64
  .setAudience([env[audience] as string])
70
65
  .setExpirationTime("1h")
71
66
  .sign(privateKey);
@@ -82,54 +77,3 @@ async function waitUntil(condition: () => void, time = 100) {
82
77
  await waitUntil(condition, time);
83
78
  }
84
79
  }
85
-
86
- export function recordRequest(
87
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
88
- cb: (data: any) => void,
89
- statusCode: number,
90
- data: string | object | undefined,
91
- responseOptions?: {
92
- headers: Record<string, string | string[] | undefined>;
93
- },
94
- ) {
95
- return ({ body }: { body?: BodyInit }) => {
96
- cb(JSON.parse((body?.valueOf() as string) ?? ""));
97
- return { statusCode, data, responseOptions: responseOptions ?? {} };
98
- };
99
- }
100
-
101
- export function recordFormRequest(
102
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
- cb: (data: any) => void,
104
- statusCode: number,
105
- data: string | object | undefined,
106
- responseOptions?: {
107
- headers: Record<string, string | string[] | undefined>;
108
- },
109
- ) {
110
- return ({ body }: { body?: BodyInit }) => {
111
- cb(Object.fromEntries(new URLSearchParams((body?.valueOf() as string) ?? "").entries()));
112
- return { statusCode, data, responseOptions: responseOptions ?? {} };
113
- };
114
- }
115
-
116
- export function recordFirehoseRequest(
117
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
118
- cb: (data: any) => void,
119
- statusCode: number,
120
- data: string | object | undefined,
121
- responseOptions?: {
122
- headers: Record<string, string | string[] | undefined>;
123
- },
124
- ) {
125
- return ({ body }: { body?: BodyInit }) => {
126
- cb(JSON.parse((body?.valueOf() as string) ?? ""));
127
- cb(
128
- JSON.parse(
129
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
130
- atob(JSON.parse((body?.valueOf() as string) ?? "")["Record"]["Data"]),
131
- ),
132
- );
133
- return { statusCode, data, responseOptions: responseOptions ?? {} };
134
- };
135
- }
package/src/types.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Toucan } from "toucan-js";
2
- import { ZodType, ZodObject, z } from "zod/v4";
1
+ import type { Toucan } from "toucan-js";
2
+ import type { ZodObject, ZodType, z } from "zod/v4";
3
3
 
4
4
  export type Route<T, A> =
5
5
  | {
@@ -8,9 +8,7 @@ export type Route<T, A> =
8
8
  path: string;
9
9
  matcher: RegExp;
10
10
  output: ZodType;
11
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
11
  handler: Handler<T, undefined, any, A, string>;
13
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
12
  params?: ZodObject<any>;
15
13
  }
16
14
  | {
@@ -20,16 +18,13 @@ export type Route<T, A> =
20
18
  matcher: RegExp;
21
19
  input: ZodType | undefined;
22
20
  output: ZodType;
23
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
21
  handler: Handler<T, any, any, A, string>;
25
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
22
  params?: ZodObject<any>;
27
23
  };
28
24
 
29
- export type ExtractParameterNames<S extends string> =
30
- S extends `${string}{${infer Name}}${infer Rest}`
31
- ? Record<Name, string> & ExtractParameterNames<Rest>
32
- : Record<string, string>;
25
+ type ExtractParameterNames<S extends string> = S extends `${string}{${infer Name}}${infer Rest}`
26
+ ? Record<Name, string> & ExtractParameterNames<Rest>
27
+ : Record<string, string>;
33
28
 
34
29
  export type Handler<
35
30
  T,
package/src/util.ts CHANGED
@@ -16,8 +16,10 @@ export async function pbkdfVerify(key: string, password: string) {
16
16
  {
17
17
  name: "PBKDF2",
18
18
  hash: "SHA-256",
19
+ // oxlint-disable-next-line typescript/no-misused-spread unicorn/no-useless-spread
19
20
  salt: new Uint8Array([...atob(key).slice(3, 19)].map((ch) => ch.charCodeAt(0))),
20
21
  iterations: parseInt(
22
+ // oxlint-disable-next-line typescript/no-misused-spread unicorn/no-useless-spread
21
23
  [...atob(key).slice(19, 22)].map((ch) => ch.charCodeAt(0).toString(16)).join(""),
22
24
  16,
23
25
  ),
package/eslint.config.js DELETED
@@ -1,3 +0,0 @@
1
- import config from "eslint-config-two-stroke";
2
-
3
- export default config;