two-stroke 1.0.5 → 1.0.7

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.5",
54
+ "version": "1.0.7",
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
  }
@@ -89,10 +102,23 @@ export function twoStroke<T extends Env>(title: string, release: string) {
89
102
  sentry,
90
103
  });
91
104
  else {
92
- console.error(body.error, rawBody);
93
- return new Response(JSON.stringify(body.error), {
94
- status: 400,
105
+ console.error({
106
+ message: "Request body schema invalid",
107
+ error: body.error,
108
+ body: rawBody,
95
109
  });
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
+ );
96
122
  }
97
123
  } else {
98
124
  response = await route.handler({
@@ -105,13 +131,21 @@ export function twoStroke<T extends Env>(title: string, release: string) {
105
131
  sentry,
106
132
  });
107
133
  }
108
- const output = route.output.safeParse(response.body);
109
- if (!output.success) {
110
- console.error(output.error, response.body);
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
+ }
111
143
  }
112
144
  response.headers = response.headers ?? {};
113
145
  response.headers["Content-Type"] =
114
146
  response.headers["Content-Type"] ?? "application/json";
147
+ response.headers["Access-Control-Allow-Origin"] =
148
+ response.headers["Access-Control-Allow-Origin"] ?? "*";
115
149
  return new Response(
116
150
  response.headers["Content-Type"] == "application/json"
117
151
  ? JSON.stringify(response.body)
@@ -120,13 +154,17 @@ export function twoStroke<T extends Env>(title: string, release: string) {
120
154
  );
121
155
  }
122
156
  }
123
- return new Response("", { status: 404 });
157
+ return new Response("", {
158
+ status: 404,
159
+ headers: { "Access-Control-Allow-Origin": "*" },
160
+ });
124
161
  } catch (err) {
125
162
  console.warn(err);
126
163
  sentry.captureException(err);
127
164
  return new Response("", {
128
165
  status: 500,
129
166
  statusText: "Internal Server Error",
167
+ headers: { "Access-Control-Allow-Origin": "*" },
130
168
  });
131
169
  }
132
170
  },
@@ -250,7 +288,7 @@ export function twoStroke<T extends Env>(title: string, release: string) {
250
288
  };
251
289
  },
252
290
  put<I extends ZodSchema, O extends ZodSchema, A, P extends string>(
253
- auth: Route<T>["auth"],
291
+ auth: Route<T, A>["auth"],
254
292
  path: P,
255
293
  input: I,
256
294
  output: O,
@@ -277,7 +315,7 @@ export function twoStroke<T extends Env>(title: string, release: string) {
277
315
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
278
316
  PP extends ZodObject<any> | undefined,
279
317
  >(
280
- auth: Route<T>["auth"],
318
+ auth: Route<T, A>["auth"],
281
319
  path: P,
282
320
  input: I,
283
321
  output: O,
@@ -305,7 +343,7 @@ export function twoStroke<T extends Env>(title: string, release: string) {
305
343
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
306
344
  PP extends ZodObject<any> | undefined,
307
345
  >(
308
- auth: Route<T>["auth"],
346
+ auth: Route<T, A>["auth"],
309
347
  path: P,
310
348
  output: O,
311
349
  handler: Handler<T, undefined, O, A, P>,
@@ -324,7 +362,7 @@ export function twoStroke<T extends Env>(title: string, release: string) {
324
362
  });
325
363
  },
326
364
  delete<O extends ZodSchema, A, P extends string>(
327
- auth: Route<T>["auth"],
365
+ auth: Route<T, A>["auth"],
328
366
  path: P,
329
367
  output: O,
330
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/release.ts CHANGED
@@ -1 +1 @@
1
- export default "undefined";
1
+ export default "undefined";
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;
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
+ }