diff --git a/package-lock.json b/package-lock.json
index f66c860dc..5c40794f5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10755,8 +10755,8 @@
"binary-extensions": "^2.2.0",
"diff": "^5.0.0",
"minimatch": "^3.0.4",
- "npm-package-arg": "^8.1.4",
- "pacote": "^11.3.4",
+ "npm-package-arg": "^8.1.1",
+ "pacote": "^11.3.0",
"tar": "^6.1.0"
}
},
diff --git a/src/middleware/README.md b/src/middleware/README.md
new file mode 100644
index 000000000..2a88ca78a
--- /dev/null
+++ b/src/middleware/README.md
@@ -0,0 +1,89 @@
+# Middlewares for Common Environments
+
+If you want to use Node.js middleware, read main [README.md](../../README.md#createnodemiddlewareapp-options) instead. If you need to implement a handler/middleware for another environment, read this document.
+
+The `middleware` directory contains the generic HTTP handler. Each sub-directory (e.g., [`node`](node)) exposes an HTTP handler/middleware for a specific environment.
+
+```
+middleware
+├── handle-request.ts
+├── on-unhandled-request-default.ts
+├── types.ts
+├── node/
+├── cloudflare/ (to be implemented)
+└── deno/ (to be implemented)
+```
+
+## Generic HTTP Handler
+
+[`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.
+
+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.
+
+`handleRequest` function takes three parameters:
+
+
+
+
+ |
+ name
+ |
+
+ type
+ |
+
+ description
+ |
+
+
+
+
+
+ app
+ |
+
+ OAuthApp instance
+ |
+
+ Required.
+ |
+
+
+
+ options.pathPrefix
+ |
+
+ string
+ |
+
+
+All exposed paths will be prefixed with the provided prefix. Defaults to `"/api/github/oauth"`
+
+ |
+
+
+
+ request
+ |
+
+ OctokitRequest
+ |
+
+ Generalized HTTP request in `OctokitRequest` type.
+ |
+
+
+
+
+## Adapt for an Environment
+
+Implementing an HTTP handler/middleware for a certain environment involves three steps:
+
+1. Write a function to parse the HTTP request (e.g., `IncomingMessage` in Node.js) into an `OctokitRequest` object. See [`node/parse-request.ts`](node/parse-request.ts) for reference.
+2. Write a function to render an `OctokitResponse` object (e.g., as `ServerResponse` in Node.js). See [`node/send-response.ts`](node/send-response.ts) for reference.
+3. Expose an HTTP handler/middleware in the dialect of the environment which performs three steps:
+ 1. Parse the HTTP request using (1).
+ 2. Process the `OctokitRequest` object using `handleRequest`. If the request is not handled by `handleRequest` (the request does not match any predefined route), [`onUnhandledRequestDefault`](on-unhandled-request-default.ts) can be used to generate a `404` response consistently.
+ 3. Render the `OctokitResponse` object using (2).
diff --git a/src/middleware/node/middleware.ts b/src/middleware/handle-request.ts
similarity index 53%
rename from src/middleware/node/middleware.ts
rename to src/middleware/handle-request.ts
index 60ea9e234..3b576b41c 100644
--- a/src/middleware/node/middleware.ts
+++ b/src/middleware/handle-request.ts
@@ -1,75 +1,71 @@
-// remove type imports from http for Deno compatibility
-// see https://github.com/octokit/octokit.js/issues/24#issuecomment-817361886
-// import { IncomingMessage, ServerResponse } from "http";
-type IncomingMessage = any;
-type ServerResponse = any;
+import { OAuthApp } from "../index";
+import { OctokitRequest, OctokitResponse, HandlerOptions } from "./types";
+import { ClientType, Options } from "../types";
+// @ts-ignore - requires esModuleInterop flag
+import fromEntries from "fromentries";
-import { parseRequest } from "./parse-request";
-
-import { OAuthApp } from "../../index";
-import { MiddlewareOptions } from "./types";
-import { Options, ClientType } from "../../types";
-
-export async function middleware(
+export async function handleRequest(
app: OAuthApp>,
- options: Required,
- request: IncomingMessage,
- response: ServerResponse,
- next?: Function
-) {
- // request.url mayb include ?query parameters which we don't want for `route`
+ { pathPrefix = "/api/github/oauth" }: HandlerOptions,
+ request: OctokitRequest
+): Promise {
+ // request.url may include ?query parameters which we don't want for `route`
// hence the workaround using new URL()
const { pathname } = new URL(request.url as string, "http://localhost");
const route = [request.method, pathname].join(" ");
const routes = {
- getLogin: `GET ${options.pathPrefix}/login`,
- getCallback: `GET ${options.pathPrefix}/callback`,
- createToken: `POST ${options.pathPrefix}/token`,
- getToken: `GET ${options.pathPrefix}/token`,
- patchToken: `PATCH ${options.pathPrefix}/token`,
- patchRefreshToken: `PATCH ${options.pathPrefix}/refresh-token`,
- scopeToken: `POST ${options.pathPrefix}/token/scoped`,
- deleteToken: `DELETE ${options.pathPrefix}/token`,
- deleteGrant: `DELETE ${options.pathPrefix}/grant`,
+ getLogin: `GET ${pathPrefix}/login`,
+ getCallback: `GET ${pathPrefix}/callback`,
+ createToken: `POST ${pathPrefix}/token`,
+ getToken: `GET ${pathPrefix}/token`,
+ patchToken: `PATCH ${pathPrefix}/token`,
+ patchRefreshToken: `PATCH ${pathPrefix}/refresh-token`,
+ scopeToken: `POST ${pathPrefix}/token/scoped`,
+ deleteToken: `DELETE ${pathPrefix}/token`,
+ deleteGrant: `DELETE ${pathPrefix}/grant`,
};
// handle unknown routes
if (!Object.values(routes).includes(route)) {
- const isExpressMiddleware = typeof next === "function";
- if (isExpressMiddleware) {
- // @ts-ignore `next` must be a function as we check two lines above
- return next();
- } else {
- return options.onUnhandledRequest(request, response);
- }
+ return null;
}
- let parsedRequest;
+ let json: any;
try {
- parsedRequest = await parseRequest(request);
+ const text = await request.text();
+ json = text ? JSON.parse(text) : {};
} catch (error) {
- response.writeHead(400, {
- "content-type": "application/json",
- });
- return response.end(
- JSON.stringify({
+ return {
+ status: 400,
+ headers: { "content-type": "application/json" },
+ text: JSON.stringify({
error: "[@octokit/oauth-app] request error",
- })
- );
+ }),
+ };
}
- const { headers, query, body = {} } = parsedRequest;
+ const { searchParams } = new URL(request.url as string, "http://localhost");
+ const query = fromEntries(searchParams) as {
+ state?: string;
+ scopes?: string;
+ code?: string;
+ redirectUrl?: string;
+ allowSignup?: string;
+ error?: string;
+ error_description?: string;
+ error_url?: string;
+ };
+ const headers = request.headers as { authorization?: string };
try {
if (route === routes.getLogin) {
const { url } = app.getWebFlowAuthorizationUrl({
state: query.state,
- scopes: query.scopes?.split(","),
- allowSignup: query.allowSignup,
+ scopes: query.scopes ? query.scopes.split(",") : undefined,
+ allowSignup: query.allowSignup !== "false",
redirectUrl: query.redirectUrl,
});
- response.writeHead(302, { location: url });
- return response.end();
+ return { status: 302, headers: { location: url } };
}
if (route === routes.getCallback) {
@@ -91,18 +87,19 @@ export async function middleware(
code: query.code,
});
- response.writeHead(200, {
- "content-type": "text/html",
- });
- response.write(`Token created successfull
+ return {
+ status: 200,
+ headers: {
+ "content-type": "text/html",
+ },
+ text: `Token created successfull
-Your token is: ${token}. Copy it now as it cannot be shown again.
`);
-
- return response.end();
+Your token is: ${token}. Copy it now as it cannot be shown again.
`,
+ };
}
if (route === routes.createToken) {
- const { state: oauthState, code, redirectUrl } = body;
+ const { state: oauthState, code, redirectUrl } = json;
if (!oauthState || !code) {
throw new Error(
@@ -118,11 +115,11 @@ export async function middleware(
redirectUrl,
});
- response.writeHead(201, {
- "content-type": "application/json",
- });
-
- return response.end(JSON.stringify({ token, scopes }));
+ return {
+ status: 201,
+ headers: { "content-type": "application/json" },
+ text: JSON.stringify({ token, scopes }),
+ };
}
if (route === routes.getToken) {
@@ -138,10 +135,11 @@ export async function middleware(
token,
});
- response.writeHead(200, {
- "content-type": "application/json",
- });
- return response.end(JSON.stringify(result));
+ return {
+ status: 200,
+ headers: { "content-type": "application/json" },
+ text: JSON.stringify(result),
+ };
}
if (route === routes.patchToken) {
@@ -157,10 +155,11 @@ export async function middleware(
token,
});
- response.writeHead(200, {
- "content-type": "application/json",
- });
- return response.end(JSON.stringify(result));
+ return {
+ status: 200,
+ headers: { "content-type": "application/json" },
+ text: JSON.stringify(result),
+ };
}
if (route === routes.patchRefreshToken) {
@@ -172,7 +171,7 @@ export async function middleware(
);
}
- const { refreshToken } = body;
+ const { refreshToken } = json;
if (!refreshToken) {
throw new Error(
@@ -182,10 +181,11 @@ export async function middleware(
const result = await app.refreshToken({ refreshToken });
- response.writeHead(200, {
- "content-type": "application/json",
- });
- return response.end(JSON.stringify(result));
+ return {
+ status: 200,
+ headers: { "content-type": "application/json" },
+ text: JSON.stringify(result),
+ };
}
if (route === routes.scopeToken) {
@@ -199,13 +199,14 @@ export async function middleware(
const result = await app.scopeToken({
token,
- ...body,
+ ...json,
});
- response.writeHead(200, {
- "content-type": "application/json",
- });
- return response.end(JSON.stringify(result));
+ return {
+ status: 200,
+ headers: { "content-type": "application/json" },
+ text: JSON.stringify(result),
+ };
}
if (route === routes.deleteToken) {
@@ -221,8 +222,7 @@ export async function middleware(
token,
});
- response.writeHead(204);
- return response.end();
+ return { status: 204 };
}
// route === routes.deleteGrant
@@ -238,16 +238,12 @@ export async function middleware(
token,
});
- response.writeHead(204);
- return response.end();
+ return { status: 204 };
} catch (error) {
- response.writeHead(400, {
- "content-type": "application/json",
- });
- response.end(
- JSON.stringify({
- error: error.message,
- })
- );
+ return {
+ status: 400,
+ headers: { "content-type": "application/json" },
+ text: JSON.stringify({ error: error.message }),
+ };
}
}
diff --git a/src/middleware/node/index.ts b/src/middleware/node/index.ts
index 99d577eb8..9bc05f71f 100644
--- a/src/middleware/node/index.ts
+++ b/src/middleware/node/index.ts
@@ -1,19 +1,56 @@
-import { OAuthApp } from "../../index";
-import { middleware } from "./middleware";
+// remove type imports from http for Deno compatibility
+// see https://github.com/octokit/octokit.js/issues/2075#issuecomment-817361886
+// import { IncomingMessage, ServerResponse } from "http";
+type IncomingMessage = any;
+type ServerResponse = any;
-import { MiddlewareOptions } from "./types";
-import { onUnhandledRequestDefault } from "./on-unhandled-request-default";
+import { parseRequest } from "./parse-request";
+import { sendResponse } from "./send-response";
+import { onUnhandledRequestDefault } from "../on-unhandled-request-default";
+import { handleRequest } from "../handle-request";
+import { OAuthApp } from "../../index";
+import { HandlerOptions } from "../types";
import { ClientType, Options } from "../../types";
+function onUnhandledRequestDefaultNode(
+ request: IncomingMessage,
+ response: ServerResponse
+) {
+ const octokitRequest = parseRequest(request);
+ const octokitResponse = onUnhandledRequestDefault(octokitRequest);
+ sendResponse(octokitResponse, response);
+}
+
export function createNodeMiddleware(
app: OAuthApp>,
{
- pathPrefix = "/api/github/oauth",
- onUnhandledRequest = onUnhandledRequestDefault,
- }: MiddlewareOptions = {}
-) {
- return middleware.bind(null, app, {
pathPrefix,
- onUnhandledRequest,
- } as Required);
+ onUnhandledRequest = onUnhandledRequestDefaultNode,
+ }: HandlerOptions & {
+ onUnhandledRequest?: (
+ request: IncomingMessage,
+ response: ServerResponse
+ ) => void;
+ } = {}
+) {
+ return async function (
+ request: IncomingMessage,
+ response: ServerResponse,
+ next?: Function
+ ) {
+ const octokitRequest = parseRequest(request);
+ const octokitResponse = await handleRequest(
+ app,
+ { pathPrefix },
+ octokitRequest
+ );
+
+ if (octokitResponse) {
+ sendResponse(octokitResponse, response);
+ } else if (typeof next === "function") {
+ next();
+ } else {
+ onUnhandledRequest(request, response);
+ }
+ };
}
diff --git a/src/middleware/node/on-unhandled-request-default.ts b/src/middleware/node/on-unhandled-request-default.ts
deleted file mode 100644
index 3664190fc..000000000
--- a/src/middleware/node/on-unhandled-request-default.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-// remove type imports from http for Deno compatibility
-// see https://github.com/octokit/octokit.js/issues/24#issuecomment-817361886
-// import { IncomingMessage, ServerResponse } from "http";
-type IncomingMessage = any;
-type ServerResponse = any;
-
-export function onUnhandledRequestDefault(
- request: IncomingMessage,
- response: ServerResponse
-) {
- response.writeHead(404, {
- "content-type": "application/json",
- });
- response.end(
- JSON.stringify({
- error: `Unknown route: ${request.method} ${request.url}`,
- })
- );
-}
diff --git a/src/middleware/node/parse-request.ts b/src/middleware/node/parse-request.ts
index da08fbeac..5df274a26 100644
--- a/src/middleware/node/parse-request.ts
+++ b/src/middleware/node/parse-request.ts
@@ -1,59 +1,21 @@
// remove type imports from http for Deno compatibility
-// see https://github.com/octokit/octokit.js/issues/24#issuecomment-817361886
+// see https://github.com/octokit/octokit.js/issues/2075#issuecomment-817361886
// import { IncomingMessage } from "http";
type IncomingMessage = any;
-// @ts-ignore remove once Node 10 is out maintenance. Replace with Object.fromEntries
-import fromEntries from "fromentries";
+import { OctokitRequest } from "../types";
-type ParsedRequest = {
- headers: {
- authorization?: string;
- };
- query: {
- state?: string;
- scopes?: string;
- code?: string;
- redirectUrl?: string;
- allowSignup?: boolean;
- error?: string;
- error_description?: string;
- error_url?: string;
- };
- body?: {
- code?: string;
- state?: string;
- redirectUrl?: string;
- refreshToken?: string;
- };
-};
-
-export async function parseRequest(
- request: IncomingMessage
-): Promise {
- const { searchParams } = new URL(request.url as string, "http://localhost");
-
- const query = fromEntries(searchParams);
- const headers = request.headers;
-
- if (!["POST", "PATCH"].includes(request.method as string)) {
- return { headers, query };
+export function parseRequest(request: IncomingMessage): OctokitRequest {
+ const { method, url, headers } = request;
+ async function text() {
+ const text = await new Promise((resolve, reject) => {
+ let bodyChunks: Uint8Array[] = [];
+ request
+ .on("error", reject)
+ .on("data", (chunk: Uint8Array) => bodyChunks.push(chunk))
+ .on("end", () => resolve(Buffer.concat(bodyChunks).toString()));
+ });
+ return text;
}
-
- return new Promise((resolve, reject) => {
- let bodyChunks: Uint8Array[] = [];
- request
- .on("error", reject)
- .on("data", (chunk: Uint8Array) => bodyChunks.push(chunk))
- .on("end", async () => {
- const bodyString = Buffer.concat(bodyChunks).toString();
- if (!bodyString) return resolve({ headers, query });
-
- try {
- resolve({ headers, query, body: JSON.parse(bodyString) });
- } catch (error) {
- reject(error);
- }
- });
- });
+ return { method, url, headers, text };
}
diff --git a/src/middleware/node/send-response.ts b/src/middleware/node/send-response.ts
new file mode 100644
index 000000000..d753044b5
--- /dev/null
+++ b/src/middleware/node/send-response.ts
@@ -0,0 +1,13 @@
+// remove type imports from http for Deno compatibility
+// see https://github.com/octokit/octokit.js/issues/2075#issuecomment-817361886
+// import { IncomingMessage, ServerResponse } from "http";
+type ServerResponse = any;
+import { OctokitResponse } from "../types";
+
+export function sendResponse(
+ octokitResponse: OctokitResponse,
+ response: ServerResponse
+) {
+ response.writeHead(octokitResponse.status, octokitResponse.headers);
+ response.end(octokitResponse.text);
+}
diff --git a/src/middleware/node/types.ts b/src/middleware/node/types.ts
deleted file mode 100644
index 6ff8c8ebd..000000000
--- a/src/middleware/node/types.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-// remove type imports from http for Deno compatibility
-// see https://github.com/octokit/octokit.js/issues/24#issuecomment-817361886
-// import { IncomingMessage, ServerResponse } from "http";
-type IncomingMessage = any;
-type ServerResponse = any;
-
-export type MiddlewareOptions = {
- pathPrefix?: string;
- onUnhandledRequest?: (
- request: IncomingMessage,
- response: ServerResponse
- ) => void;
-};
diff --git a/src/middleware/on-unhandled-request-default.ts b/src/middleware/on-unhandled-request-default.ts
new file mode 100644
index 000000000..14dd11757
--- /dev/null
+++ b/src/middleware/on-unhandled-request-default.ts
@@ -0,0 +1,13 @@
+import { OctokitRequest, OctokitResponse } from "./types";
+
+export function onUnhandledRequestDefault(
+ request: OctokitRequest
+): OctokitResponse {
+ return {
+ status: 404,
+ headers: { "content-type": "application/json" },
+ text: JSON.stringify({
+ error: `Unknown route: ${request.method} ${request.url}`,
+ }),
+ };
+}
diff --git a/src/middleware/types.ts b/src/middleware/types.ts
new file mode 100644
index 000000000..f259376d8
--- /dev/null
+++ b/src/middleware/types.ts
@@ -0,0 +1,16 @@
+export type OctokitRequest = {
+ method: string;
+ url: string;
+ headers: Record;
+ text: () => Promise;
+};
+
+export type OctokitResponse = {
+ status: number;
+ headers?: Record;
+ text?: string;
+};
+
+export type HandlerOptions = {
+ pathPrefix?: string;
+};