From 31b3f7055599dd94ca07a14b1c7b93d02eab8ba2 Mon Sep 17 00:00:00 2001 From: Baoshan Sheng Date: Sat, 2 Jul 2022 01:31:33 +0800 Subject: [PATCH] feat: `createWebWorkerHandler` for Cloudflare and Deno. Deprecates `createCloudflareHandler` (#287) --- README.md | 8 +- src/index.ts | 31 ++++---- src/middleware/README.md | 4 +- .../{cloudflare => web-worker}/index.ts | 16 +++- .../parse-request.ts | 0 .../send-response.ts | 0 test/deprecations.test.ts | 54 +++++++++++++- test/node-middleware.test.ts | 2 +- ...ler.test.ts => web-worker-handler.test.ts} | 73 ++++++++++--------- 9 files changed, 129 insertions(+), 59 deletions(-) rename src/middleware/{cloudflare => web-worker}/index.ts (71%) rename src/middleware/{cloudflare => web-worker}/parse-request.ts (100%) rename src/middleware/{cloudflare => web-worker}/send-response.ts (100%) rename test/{cloudflare-handler.test.ts => web-worker-handler.test.ts} (91%) diff --git a/README.md b/README.md index f20ee876e..72a676d72 100644 --- a/README.md +++ b/README.md @@ -963,20 +963,20 @@ function onUnhandledRequest(request, response) { -### `createCloudflareHandler(app, options)` +### `createWebWorkerHandler(app, options)` -Event handler for Cloudflare workers. +Event handler for web worker environments (Cloudflare workers or Deno). ```js // worker.js -import { OAuthApp, createCloudflareHandler } from "@octokit/oauth-app"; +import { OAuthApp, createWebWorkerHandler } from "@octokit/oauth-app"; const app = new OAuthApp({ clientType: "oauth-app", clientId: "1234567890abcdef1234", clientSecret: "1234567890abcdef1234567890abcdef12345678", }); -const handleRequest = createCloudflareHandler(app, { +const handleRequest = createWebWorkerHandler(app, { pathPrefix: "/api/github/oauth", }); diff --git a/src/index.ts b/src/index.ts index 99e89e3d0..a129bb2ba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,49 +9,52 @@ import { GetUserOctokitWithStateInterface, } from "./methods/get-user-octokit"; import { - getWebFlowAuthorizationUrlWithState, GetWebFlowAuthorizationUrlInterface, + getWebFlowAuthorizationUrlWithState, } from "./methods/get-web-flow-authorization-url"; import { - createTokenWithState, CreateTokenInterface, + createTokenWithState, } from "./methods/create-token"; import { - checkTokenWithState, CheckTokenInterface, + checkTokenWithState, } from "./methods/check-token"; import { - resetTokenWithState, ResetTokenInterface, + resetTokenWithState, } from "./methods/reset-token"; import { - refreshTokenWithState, RefreshTokenInterface, + refreshTokenWithState, } from "./methods/refresh-token"; import { - scopeTokenWithState, ScopeTokenInterface, + scopeTokenWithState, } from "./methods/scope-token"; import { - deleteTokenWithState, DeleteTokenInterface, + deleteTokenWithState, } from "./methods/delete-token"; import { - deleteAuthorizationWithState, DeleteAuthorizationInterface, + deleteAuthorizationWithState, } from "./methods/delete-authorization"; -import { - Options, +import type { + AddEventHandler, + ClientType, + ClientTypeFromOptions, ConstructorOptions, OctokitTypeFromOptions, - ClientTypeFromOptions, - ClientType, - AddEventHandler, + Options, State, } from "./types"; export { createNodeMiddleware } from "./middleware/node/index"; -export { createCloudflareHandler } from "./middleware/cloudflare/index"; +export { + createCloudflareHandler, + createWebWorkerHandler, +} from "./middleware/web-worker/index"; export { createAWSLambdaAPIGatewayV2Handler } from "./middleware/aws-lambda/api-gateway-v2"; type Constructor = new (...args: any[]) => T; diff --git a/src/middleware/README.md b/src/middleware/README.md index 2a88ca78a..d08c0ba08 100644 --- a/src/middleware/README.md +++ b/src/middleware/README.md @@ -10,7 +10,7 @@ middleware ├── on-unhandled-request-default.ts ├── types.ts ├── node/ -├── cloudflare/ (to be implemented) +├── web-worker/ (Cloudflare Workers & Deno) └── deno/ (to be implemented) ``` @@ -18,7 +18,7 @@ middleware [`handleRequest`](handle-request.ts) function is an abstract HTTP handler which accepts an `OctokitRequest` and returns an `OctokitResponse` if the request matches any predefined route. -> Different environments (e.g., Node.js, Cloudflare, etc.) exposes different APIs when processing HTTP requests (e.g., [`IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage) for Node.js, [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) for Cloudflare workers, etc.). Two HTTP-related types ([`OctokitRequest` and `OctokitResponse`](./types.ts)) are generalized to make an abstract HTTP handler possible. +> Different environments (e.g., Node.js, Cloudflare Workers, Deno, etc.) exposes different APIs when processing HTTP requests (e.g., [`IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage) for Node.js, [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) for Cloudflare workers, etc.). Two HTTP-related types ([`OctokitRequest` and `OctokitResponse`](./types.ts)) are generalized to make an abstract HTTP handler possible. To share the behavior and capability with the existing Node.js middleware (and be compatible with [OAuth user authentication strategy in the browser](https://github.com/octokit/auth-oauth-user-client.js)), it is better to implement your HTTP handler/middleware based on `handleRequest` function. diff --git a/src/middleware/cloudflare/index.ts b/src/middleware/web-worker/index.ts similarity index 71% rename from src/middleware/cloudflare/index.ts rename to src/middleware/web-worker/index.ts index d79a64b7e..f02823fe2 100644 --- a/src/middleware/cloudflare/index.ts +++ b/src/middleware/web-worker/index.ts @@ -5,7 +5,7 @@ import { onUnhandledRequestDefault } from "../on-unhandled-request-default"; import { OAuthApp } from "../../index"; import { HandlerOptions } from "../types"; -async function onUnhandledRequestDefaultCloudflare( +async function onUnhandledRequestDefaultWebWorker( request: Request ): Promise { const octokitRequest = parseRequest(request); @@ -13,11 +13,11 @@ async function onUnhandledRequestDefaultCloudflare( return sendResponse(octokitResponse); } -export function createCloudflareHandler( +export function createWebWorkerHandler( app: OAuthApp, { pathPrefix, - onUnhandledRequest = onUnhandledRequestDefaultCloudflare, + onUnhandledRequest = onUnhandledRequestDefaultWebWorker, }: HandlerOptions & { onUnhandledRequest?: (request: Request) => Response | Promise; } = {} @@ -34,3 +34,13 @@ export function createCloudflareHandler( : await onUnhandledRequest(request); }; } + +/** @deprecated */ +export function createCloudflareHandler( + ...args: Parameters +) { + args[0].octokit.log.warn( + "[@octokit/oauth-app] `createCloudflareHandler` is deprecated, use `createWebWorkerHandler` instead" + ); + return createWebWorkerHandler(...args); +} diff --git a/src/middleware/cloudflare/parse-request.ts b/src/middleware/web-worker/parse-request.ts similarity index 100% rename from src/middleware/cloudflare/parse-request.ts rename to src/middleware/web-worker/parse-request.ts diff --git a/src/middleware/cloudflare/send-response.ts b/src/middleware/web-worker/send-response.ts similarity index 100% rename from src/middleware/cloudflare/send-response.ts rename to src/middleware/web-worker/send-response.ts diff --git a/test/deprecations.test.ts b/test/deprecations.test.ts index 107291e08..7d9c9e871 100644 --- a/test/deprecations.test.ts +++ b/test/deprecations.test.ts @@ -1,3 +1,55 @@ +import { URL } from "url"; +import * as nodeFetch from "node-fetch"; +import fromEntries from "fromentries"; +import { createCloudflareHandler, OAuthApp } from "../src"; +import { Octokit } from "@octokit/core"; + describe("deprecations", () => { - it.todo("no deprecations at this point"); + beforeAll(() => { + Object.fromEntries ||= fromEntries; + (global as any).Request = nodeFetch.Request; + (global as any).Response = nodeFetch.Response; + }); + + afterAll(() => { + delete (global as any).Request; + delete (global as any).Response; + }); + + it("createCloudflareHandler works but logs out deprecation message", async () => { + const warn = jest.fn().mockResolvedValue(undefined); + const handleRequest = createCloudflareHandler( + new OAuthApp({ + clientType: "github-app", + clientId: "client_id_123", + clientSecret: "client_secret_456", + Octokit: Octokit.defaults({ + log: { + debug: () => undefined, + info: () => undefined, + warn, + error: () => undefined, + }, + }), + }) + ); + + expect(warn.mock.calls.length).toEqual(1); + expect(warn.mock.calls[0][0]).toEqual( + "[@octokit/oauth-app] `createCloudflareHandler` is deprecated, use `createWebWorkerHandler` instead" + ); + + const request = new Request("/api/github/oauth/login"); + const { status, headers } = await handleRequest(request); + + expect(status).toEqual(302); + const url = new URL(headers.get("location") as string); + expect(url).toMatchObject({ + origin: "https://github.com", + pathname: "/login/oauth/authorize", + }); + expect(url.searchParams.get("client_id")).toEqual("client_id_123"); + expect(url.searchParams.get("state")).toMatch(/^\w+$/); + expect(url.searchParams.get("scope")).toEqual(null); + }); }); diff --git a/test/node-middleware.test.ts b/test/node-middleware.test.ts index bb9fbff3a..28b2827ae 100644 --- a/test/node-middleware.test.ts +++ b/test/node-middleware.test.ts @@ -551,7 +551,7 @@ describe("createNodeMiddleware(app)", () => { expect(response.status).toEqual(404); }); - it("GET /api/github/oauth/callback without code or state", async () => { + it("GET /api/github/oauth/callback without code", async () => { const appMock = {}; const server = createServer( diff --git a/test/cloudflare-handler.test.ts b/test/web-worker-handler.test.ts similarity index 91% rename from test/cloudflare-handler.test.ts rename to test/web-worker-handler.test.ts index 461c9b4cf..ba26da54c 100644 --- a/test/cloudflare-handler.test.ts +++ b/test/web-worker-handler.test.ts @@ -1,9 +1,14 @@ import { URL } from "url"; import * as nodeFetch from "node-fetch"; import fromEntries from "fromentries"; -import { createCloudflareHandler, OAuthApp } from "../src/"; - -describe("createCloudflareHandler(app)", () => { +import { + createCloudflareHandler, + createWebWorkerHandler, + OAuthApp, +} from "../src"; +import { Octokit } from "@octokit/core"; + +describe("createWebWorkerHandler(app)", () => { beforeAll(() => { Object.fromEntries ||= fromEntries; (global as any).Request = nodeFetch.Request; @@ -21,14 +26,14 @@ describe("createCloudflareHandler(app)", () => { clientId: "0123", clientSecret: "0123secret", }); - createCloudflareHandler(oauthApp); + createWebWorkerHandler(oauthApp); const githubApp = new OAuthApp({ clientType: "github-app", clientId: "0123", clientSecret: "0123secret", }); - createCloudflareHandler(githubApp); + createWebWorkerHandler(githubApp); }); it("allow pre-flight requests", async () => { @@ -36,7 +41,7 @@ describe("createCloudflareHandler(app)", () => { clientId: "0123", clientSecret: "0123secret", }); - const handleRequest = createCloudflareHandler(app); + const handleRequest = createWebWorkerHandler(app); const request = new Request("/api/github/oauth/token", { method: "OPTIONS", }); @@ -49,7 +54,7 @@ describe("createCloudflareHandler(app)", () => { clientId: "0123", clientSecret: "0123secret", }); - const handleRequest = createCloudflareHandler(app); + const handleRequest = createWebWorkerHandler(app); const request = new Request("/api/github/oauth/login"); const { status, headers } = await handleRequest(request); @@ -71,7 +76,7 @@ describe("createCloudflareHandler(app)", () => { clientSecret: "0123secret", defaultScopes: ["repo"], }); - const handleRequest = createCloudflareHandler(app); + const handleRequest = createWebWorkerHandler(app); const request = new Request("/api/github/oauth/login"); const { status, headers } = await handleRequest(request); @@ -92,7 +97,7 @@ describe("createCloudflareHandler(app)", () => { clientId: "0123", clientSecret: "0123secret", }); - const handleRequest = createCloudflareHandler(app); + const handleRequest = createWebWorkerHandler(app); const request = new Request( "/api/github/oauth/login?state=mystate123&scopes=one,two,three" @@ -120,7 +125,7 @@ describe("createCloudflareHandler(app)", () => { }, }), }; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -148,7 +153,7 @@ describe("createCloudflareHandler(app)", () => { }, }), }; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -184,7 +189,7 @@ describe("createCloudflareHandler(app)", () => { }, }), }; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -218,7 +223,7 @@ describe("createCloudflareHandler(app)", () => { }, }), }; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -251,7 +256,7 @@ describe("createCloudflareHandler(app)", () => { }, }), }; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -306,7 +311,7 @@ describe("createCloudflareHandler(app)", () => { }, }), }; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -340,7 +345,7 @@ describe("createCloudflareHandler(app)", () => { }, }), }; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -366,7 +371,7 @@ describe("createCloudflareHandler(app)", () => { const appMock = { deleteToken: jest.fn().mockResolvedValue(undefined), }; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -388,7 +393,7 @@ describe("createCloudflareHandler(app)", () => { const appMock = { deleteAuthorization: jest.fn().mockResolvedValue(undefined), }; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -413,7 +418,7 @@ describe("createCloudflareHandler(app)", () => { clientId: "0123", clientSecret: "0123secret", }); - const handleRequest = createCloudflareHandler(app, { + const handleRequest = createWebWorkerHandler(app, { onUnhandledRequest: async (request: Request) => { expect(request.method).toEqual("POST"); expect(request.url).toEqual("/unrelated"); @@ -439,7 +444,7 @@ describe("createCloudflareHandler(app)", () => { it("GET /unknown", async () => { const appMock = {}; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -448,9 +453,9 @@ describe("createCloudflareHandler(app)", () => { expect(response.status).toEqual(404); }); - it("GET /api/github/oauth/callback without code or state", async () => { + it("GET /api/github/oauth/callback without code", async () => { const appMock = {}; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -465,7 +470,7 @@ describe("createCloudflareHandler(app)", () => { it("GET /api/github/oauth/callback with error", async () => { const appMock = {}; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -483,7 +488,7 @@ describe("createCloudflareHandler(app)", () => { it("POST /api/github/oauth/token without state or code", async () => { const appMock = {}; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -501,7 +506,7 @@ describe("createCloudflareHandler(app)", () => { it("POST /api/github/oauth/token with non-JSON request body", async () => { const appMock = {}; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -519,7 +524,7 @@ describe("createCloudflareHandler(app)", () => { it("GET /api/github/oauth/token without Authorization header", async () => { const appMock = {}; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -536,7 +541,7 @@ describe("createCloudflareHandler(app)", () => { it("PATCH /api/github/oauth/token without authorization header", async () => { const appMock = {}; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -554,7 +559,7 @@ describe("createCloudflareHandler(app)", () => { it("POST /api/github/oauth/token/scoped without authorization header", async () => { const appMock = {}; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -576,7 +581,7 @@ describe("createCloudflareHandler(app)", () => { ok: true, }), }; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -600,7 +605,7 @@ describe("createCloudflareHandler(app)", () => { ok: true, }), }; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -620,7 +625,7 @@ describe("createCloudflareHandler(app)", () => { it("DELETE /api/github/oauth/token without authorization header", async () => { const appMock = {}; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -638,7 +643,7 @@ describe("createCloudflareHandler(app)", () => { it("DELETE /api/github/oauth/grant without authorization header", async () => { const appMock = {}; - const handleRequest = createCloudflareHandler( + const handleRequest = createWebWorkerHandler( appMock as unknown as OAuthApp ); @@ -654,8 +659,8 @@ describe("createCloudflareHandler(app)", () => { }); }); - it("cloudflare worker with options.pathPrefix", async () => { - const handleRequest = createCloudflareHandler( + it("web worker handler with options.pathPrefix", async () => { + const handleRequest = createWebWorkerHandler( new OAuthApp({ clientId: "0123", clientSecret: "0123secret",