two-stroke 1.0.6 → 1.0.8

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/bin/test.mjs CHANGED
@@ -9,14 +9,14 @@ if (fs.existsSync("wrangler.toml")) {
9
9
  await cmd("wrangler deploy --dry-run --outdir=dist");
10
10
  const app = await import(`${process.cwd()}/dist/index.js`);
11
11
  const request = await app.default.fetch(
12
- { url: "http://example.com/doc/", method: "GET" },
12
+ { url: "http://example.com/doc", method: "GET" },
13
13
  { SENTRY_DSN: null, SENTRY_ENVIRONMENT: null },
14
14
  null,
15
15
  );
16
16
  const types = await openapiTS(await consumers.json(request.body));
17
17
  fs.writeFileSync("test/api.d.ts", types);
18
18
  }
19
- await cmd("vitest --globals --no-file-parallelism --coverage", [
19
+ await cmd("vitest --globals --no-file-parallelism", [
20
20
  ...(!process.argv.slice(2).includes("-w") &&
21
21
  !process.argv.slice(2).includes("--watch")
22
22
  ? ["--run"]
package/package.json CHANGED
@@ -9,20 +9,21 @@
9
9
  },
10
10
  "dependencies": {
11
11
  "@anatine/zod-openapi": "^2.2.3",
12
- "@asteasolutions/zod-to-openapi": "^6.3.1",
13
- "@cloudflare/workers-types": "^4.20240208.0",
14
- "@sentry/cli": "^2.28.6",
15
- "@typescript-eslint/eslint-plugin": "^7.0.1",
16
- "@typescript-eslint/parser": "^7.0.1",
17
- "@vitest/coverage-v8": "^1.2.2",
12
+ "@asteasolutions/zod-to-openapi": "^6.4.0",
13
+ "@cloudflare/workers-types": "^4.20240222.0",
14
+ "@sentry/cli": "^2.30.0",
15
+ "@typescript-eslint/eslint-plugin": "^7.1.1",
16
+ "@typescript-eslint/parser": "^7.1.1",
17
+ "@vitest/coverage-v8": "^1.3.1",
18
18
  "eslint-config-prettier": "^9.1.0",
19
19
  "eslint-config-two-stroke": "^1.0.0",
20
20
  "eslint-config-typescript": "^3.0.0",
21
+ "jose": "^5.2.3",
21
22
  "jwk-subtle": "^1.0.6",
22
- "miniflare": "^3.20240129.1",
23
- "openapi-fetch": "^0.9.0",
23
+ "miniflare": "^3.20240223.1",
24
+ "openapi-fetch": "^0.9.3",
24
25
  "openapi-typescript": "^6.7.4",
25
- "openapi3-ts": "^4.2.1",
26
+ "openapi3-ts": "^4.2.2",
26
27
  "pbkdf-subtle": "^1.0.0",
27
28
  "toml": "^3.0.0",
28
29
  "toucan-js": "^3.3.1",
@@ -50,13 +51,13 @@
50
51
  "name": "two-stroke",
51
52
  "packageManager": "yarn@4.1.0",
52
53
  "type": "module",
53
- "version": "1.0.6",
54
+ "version": "1.0.8",
54
55
  "devDependencies": {
55
- "@types/eslint": "^8.56.2",
56
- "@types/node": "^20.11.17",
57
- "eslint": "^8.56.0",
56
+ "@types/eslint": "^8.56.5",
57
+ "@types/node": "^20.11.25",
58
+ "eslint": "^8.57.0",
58
59
  "prettier": "^3.2.5",
59
- "typescript": "^5.3.3",
60
- "vitest": "^1.2.2"
60
+ "typescript": "^5.4.2",
61
+ "vitest": "^1.3.1"
61
62
  }
62
63
  }
package/src/index.ts CHANGED
@@ -16,12 +16,13 @@ export function twoStroke<T extends Env>(title: string, release: string) {
16
16
  env: T;
17
17
  sentry: Toucan;
18
18
  }) => Promise<void>;
19
- const routes: Route<T>[] = [];
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ const routes: Route<T, any>[] = [];
20
21
  routes.push({
21
22
  auth: noAuth,
22
23
  method: "GET",
23
- path: "/doc/",
24
- matcher: /^\/doc\/$/,
24
+ path: "/doc",
25
+ matcher: /^\/doc$/,
25
26
  output: z.object({}),
26
27
  handler: openAPI(title, release, noAuth, routes),
27
28
  });
@@ -53,6 +54,17 @@ export function twoStroke<T extends Env>(title: string, release: string) {
53
54
  release,
54
55
  });
55
56
  try {
57
+ if (req.method == "OPTIONS") {
58
+ return new Response("", {
59
+ status: 204,
60
+ headers: {
61
+ "Access-Control-Allow-Origin": "*",
62
+ "Access-Control-Max-Age": "86400",
63
+ "Access-Control-Allow-Methods": "GET,PUT,POST,DELETE",
64
+ "Access-Control-Allow-Headers": "Authorization,Content-Type",
65
+ },
66
+ });
67
+ }
56
68
  const { pathname } = new URL(req.url);
57
69
  let response;
58
70
  for (const route of routes) {
@@ -68,6 +80,7 @@ export function twoStroke<T extends Env>(title: string, release: string) {
68
80
  statusText: "Invalid Authorization",
69
81
  headers: {
70
82
  "WWW-Authenticate": "Bearer",
83
+ "Access-Control-Allow-Origin": "*",
71
84
  },
72
85
  });
73
86
  }
@@ -94,9 +107,18 @@ export function twoStroke<T extends Env>(title: string, release: string) {
94
107
  error: body.error,
95
108
  body: rawBody,
96
109
  });
97
- return new Response(JSON.stringify(body.error), {
98
- status: 400,
99
- });
110
+ return new Response(
111
+ JSON.stringify({
112
+ error: "Request body schema invalid",
113
+ ...body.error,
114
+ }),
115
+ {
116
+ status: 400,
117
+ headers: {
118
+ "Access-Control-Allow-Origin": "*",
119
+ },
120
+ },
121
+ );
100
122
  }
101
123
  } else {
102
124
  response = await route.handler({
@@ -109,17 +131,21 @@ export function twoStroke<T extends Env>(title: string, release: string) {
109
131
  sentry,
110
132
  });
111
133
  }
112
- const output = route.output.safeParse(response.body);
113
- if (!output.success) {
114
- console.error({
115
- message: "Response body schema invalid",
116
- error: output.error,
117
- body: response.body,
118
- });
134
+ if (response.status === undefined || response.status == 200) {
135
+ const output = route.output.safeParse(response.body);
136
+ if (!output.success) {
137
+ console.error({
138
+ message: "Response body schema invalid",
139
+ error: output.error,
140
+ body: response.body,
141
+ });
142
+ }
119
143
  }
120
144
  response.headers = response.headers ?? {};
121
145
  response.headers["Content-Type"] =
122
146
  response.headers["Content-Type"] ?? "application/json";
147
+ response.headers["Access-Control-Allow-Origin"] =
148
+ response.headers["Access-Control-Allow-Origin"] ?? "*";
123
149
  return new Response(
124
150
  response.headers["Content-Type"] == "application/json"
125
151
  ? JSON.stringify(response.body)
@@ -128,13 +154,17 @@ export function twoStroke<T extends Env>(title: string, release: string) {
128
154
  );
129
155
  }
130
156
  }
131
- return new Response("", { status: 404 });
157
+ return new Response("", {
158
+ status: 404,
159
+ headers: { "Access-Control-Allow-Origin": "*" },
160
+ });
132
161
  } catch (err) {
133
162
  console.warn(err);
134
163
  sentry.captureException(err);
135
164
  return new Response("", {
136
165
  status: 500,
137
166
  statusText: "Internal Server Error",
167
+ headers: { "Access-Control-Allow-Origin": "*" },
138
168
  });
139
169
  }
140
170
  },
@@ -258,7 +288,7 @@ export function twoStroke<T extends Env>(title: string, release: string) {
258
288
  };
259
289
  },
260
290
  put<I extends ZodSchema, O extends ZodSchema, A, P extends string>(
261
- auth: Route<T>["auth"],
291
+ auth: Route<T, A>["auth"],
262
292
  path: P,
263
293
  input: I,
264
294
  output: O,
@@ -285,7 +315,7 @@ export function twoStroke<T extends Env>(title: string, release: string) {
285
315
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
286
316
  PP extends ZodObject<any> | undefined,
287
317
  >(
288
- auth: Route<T>["auth"],
318
+ auth: Route<T, A>["auth"],
289
319
  path: P,
290
320
  input: I,
291
321
  output: O,
@@ -313,7 +343,7 @@ export function twoStroke<T extends Env>(title: string, release: string) {
313
343
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
314
344
  PP extends ZodObject<any> | undefined,
315
345
  >(
316
- auth: Route<T>["auth"],
346
+ auth: Route<T, A>["auth"],
317
347
  path: P,
318
348
  output: O,
319
349
  handler: Handler<T, undefined, O, A, P>,
@@ -332,7 +362,7 @@ export function twoStroke<T extends Env>(title: string, release: string) {
332
362
  });
333
363
  },
334
364
  delete<O extends ZodSchema, A, P extends string>(
335
- auth: Route<T>["auth"],
365
+ auth: Route<T, A>["auth"],
336
366
  path: P,
337
367
  output: O,
338
368
  handler: Handler<T, undefined, O, A, P>,
package/src/openAPI.ts CHANGED
@@ -7,6 +7,7 @@ import { Env, Route } from "./types";
7
7
  import { ZodIssue, ZodType, z } from "zod";
8
8
 
9
9
  const ZodErrorSchema: ZodType<{ issues: ZodIssue[] }> = z.object({
10
+ error: z.string(),
10
11
  issues: z.array(
11
12
  z.object({
12
13
  code: z.literal("invalid_literal"),
@@ -31,11 +32,11 @@ type Method =
31
32
  extendZodWithOpenApi(z);
32
33
 
33
34
  export const openAPI =
34
- <T extends Env>(
35
+ <T extends Env, A>(
35
36
  title: string,
36
37
  release: string,
37
- noAuth: () => void,
38
- routes: Route<T>[],
38
+ noAuth: () => A,
39
+ routes: Route<T, A>[],
39
40
  ) =>
40
41
  async () => {
41
42
  const openAPIRegistry = new OpenAPIRegistry();
@@ -88,6 +89,16 @@ export const openAPI =
88
89
  },
89
90
  },
90
91
  },
92
+ 500: {
93
+ description: "Invalid Request",
94
+ content: {
95
+ "application/json": {
96
+ schema: z.object({
97
+ error: z.string(),
98
+ }),
99
+ },
100
+ },
101
+ },
91
102
  },
92
103
  });
93
104
  });
package/src/test.ts CHANGED
@@ -5,6 +5,7 @@ import { Env } from "./types";
5
5
  import consumers from "stream/consumers";
6
6
  import { URLSearchParams } from "url";
7
7
  import createClient from "openapi-fetch";
8
+ import { generateKeyPair, SignJWT, JWTPayload, exportJWK } from "jose";
8
9
 
9
10
  // eslint-disable-next-line @typescript-eslint/ban-types
10
11
  export const setupTests = async <Paths extends {}>(bindings: Env) => {
@@ -41,6 +42,14 @@ export const setupTests = async <Paths extends {}>(bindings: Env) => {
41
42
  d1Databases: (config.d1_databases ?? []).map(
42
43
  ({ binding }: { binding: string }) => binding,
43
44
  ),
45
+ serviceBindings: Object.fromEntries(
46
+ (config.services ?? []).map(
47
+ ({ binding, service }: { binding: string; service: string }) => [
48
+ binding,
49
+ service,
50
+ ],
51
+ ),
52
+ ),
44
53
  fetchMock,
45
54
  });
46
55
 
@@ -65,6 +74,50 @@ export const setupTests = async <Paths extends {}>(bindings: Env) => {
65
74
  await waitUntil(() => expect(log).contains("Queue batch finished"));
66
75
  console.log = orig;
67
76
  },
77
+ async fakeJWK(issuer: string, audience: string, claims: JWTPayload) {
78
+ const { publicKey, privateKey } = await generateKeyPair("RS256");
79
+ const jwk = await exportJWK(publicKey);
80
+ jwk.kid = "test";
81
+ jwk.alg = "RS256";
82
+ fetchMock
83
+ .get(bindings[issuer] as string)
84
+ .intercept({ method: "GET", path: "/.well-known/openid-configuration" })
85
+ .reply(
86
+ 200,
87
+ {
88
+ jwks_uri: `${bindings[issuer]}/.well-known/jwks`,
89
+ },
90
+ {
91
+ headers: {
92
+ "Content-Type": "application/json",
93
+ },
94
+ },
95
+ )
96
+ .persist();
97
+ fetchMock
98
+ .get(bindings[issuer] as string)
99
+ .intercept({ method: "GET", path: "/.well-known/jwks" })
100
+ .reply(
101
+ 200,
102
+ {
103
+ keys: [jwk],
104
+ },
105
+ {
106
+ headers: {
107
+ "Content-Type": "application/json",
108
+ },
109
+ },
110
+ )
111
+ .persist();
112
+ return await new SignJWT(claims)
113
+ .setProtectedHeader({ typ: "JWT", alg: "RS256", kid: jwk.kid })
114
+ .setIssuedAt()
115
+ .setNotBefore("5 minutes ago")
116
+ .setIssuer(bindings[issuer] as string)
117
+ .setAudience([bindings[audience] as string])
118
+ .setExpirationTime("1h")
119
+ .sign(privateKey);
120
+ },
68
121
  };
69
122
  };
70
123
 
@@ -83,11 +136,14 @@ export function recordRequest(
83
136
  cb: (data: any) => void,
84
137
  statusCode: number,
85
138
  data: string | object | Buffer | undefined,
139
+ responseOptions?: {
140
+ headers: Record<string, string | string[] | undefined>;
141
+ },
86
142
  ) {
87
143
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
88
144
  return ({ body }: any) => {
89
145
  consumers.json(body).then(cb);
90
- return { statusCode, data };
146
+ return { statusCode, data, responseOptions };
91
147
  };
92
148
  }
93
149
 
@@ -96,6 +152,9 @@ export function recordFormRequest(
96
152
  cb: (data: any) => void,
97
153
  statusCode: number,
98
154
  data: string | object | Buffer | undefined,
155
+ responseOptions?: {
156
+ headers: Record<string, string | string[] | undefined>;
157
+ },
99
158
  ) {
100
159
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
101
160
  return ({ body }: any) => {
@@ -105,7 +164,7 @@ export function recordFormRequest(
105
164
  .then((data: any) =>
106
165
  cb(Object.fromEntries(new URLSearchParams(data).entries())),
107
166
  );
108
- return { statusCode, data };
167
+ return { statusCode, data, responseOptions };
109
168
  };
110
169
  }
111
170
 
@@ -114,6 +173,9 @@ export function recordFirehoseRequest(
114
173
  cb: (data: any) => void,
115
174
  statusCode: number,
116
175
  data: string | object | Buffer | undefined,
176
+ responseOptions?: {
177
+ headers: Record<string, string | string[] | undefined>;
178
+ },
117
179
  ) {
118
180
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
119
181
  return ({ body }: any) => {
@@ -121,6 +183,6 @@ export function recordFirehoseRequest(
121
183
  .json(body)
122
184
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
123
185
  .then((data: any) => cb(JSON.parse(atob(data["Record"]["Data"]))));
124
- return { statusCode, data };
186
+ return { statusCode, data, responseOptions };
125
187
  };
126
188
  }
package/src/types.ts CHANGED
@@ -1,29 +1,29 @@
1
1
  import { Toucan } from "toucan-js";
2
2
  import { ZodObject, ZodSchema, z } from "zod";
3
3
  export type Env = {
4
- [k: string]: string | Queue | KVNamespace | R2Bucket | D1Database;
4
+ [k: string]: string | Queue | KVNamespace | R2Bucket | D1Database | Fetcher | Hyperdrive;
5
5
  };
6
- export type Route<T extends Env> =
6
+ export type Route<T extends Env, A> =
7
7
  | {
8
- auth: (c: { req: Request; env: T }) => unknown;
8
+ auth: (c: { req: Request; env: T }) => Promise<A>;
9
9
  method: "GET" | "DELETE";
10
10
  path: string;
11
11
  matcher: RegExp;
12
12
  output: ZodSchema;
13
13
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
- handler: Handler<T, undefined, any, any, string>;
14
+ handler: Handler<T, undefined, any, A, string>;
15
15
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
16
  params?: ZodObject<any>;
17
17
  }
18
18
  | {
19
- auth: (c: { req: Request; env: T }) => unknown;
19
+ auth: (c: { req: Request; env: T }) => Promise<A>;
20
20
  method: "POST" | "PUT";
21
21
  path: string;
22
22
  matcher: RegExp;
23
23
  input: ZodSchema;
24
24
  output: ZodSchema;
25
25
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
- handler: Handler<T, any, any, any, string>;
26
+ handler: Handler<T, any, any, A, string>;
27
27
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
28
  params?: ZodObject<any>;
29
29
  };
@@ -47,8 +47,15 @@ export type Handler<
47
47
  searchParams: URLSearchParams;
48
48
  claims: A;
49
49
  sentry: Toucan;
50
- }) => Promise<{
51
- body: z.infer<O>;
52
- status?: number;
53
- headers?: Record<string, string>;
54
- }>;
50
+ }) => Promise<
51
+ | {
52
+ body: z.infer<O>;
53
+ status?: 200;
54
+ headers?: Record<string, string>;
55
+ }
56
+ | {
57
+ body?: { error: string };
58
+ status: number;
59
+ headers?: Record<string, string>;
60
+ }
61
+ >;
package/tsconfig.json CHANGED
@@ -2,9 +2,7 @@
2
2
  "compilerOptions": {
3
3
  "esModuleInterop": true,
4
4
  "isolatedModules": true,
5
- "lib": [
6
- "ESNext"
7
- ],
5
+ "lib": ["ESNext"],
8
6
  "module": "ESNext",
9
7
  "moduleResolution": "bundler",
10
8
  "noUncheckedIndexedAccess": true,
@@ -12,9 +10,6 @@
12
10
  "skipLibCheck": true,
13
11
  "strict": true,
14
12
  "target": "ESNext",
15
- "types": [
16
- "@cloudflare/workers-types",
17
- "vitest/globals"
18
- ]
13
+ "types": ["@cloudflare/workers-types", "vitest/globals"]
19
14
  }
20
- }
15
+ }