From f5b7ae3e2917aaf9a34d9ace9c6ecc56dd1be5d0 Mon Sep 17 00:00:00 2001
From: Gregor Martynus <39992+gr2m@users.noreply.github.com>
Date: Mon, 26 Oct 2020 15:11:39 -0700
Subject: [PATCH] feat: `factory` auth option (#198)
---
README.md | 29 +++++++++++++++++++
src/get-app-authentication.ts | 2 +-
src/get-installation-authentication.ts | 15 ++++++++++
src/types.ts | 35 +++++++++++------------
test/index.test.ts | 39 ++++++++++++++++++++++++++
5 files changed, 101 insertions(+), 19 deletions(-)
diff --git a/README.md b/README.md
index a13008312..cbf65f7e7 100644
--- a/README.md
+++ b/README.md
@@ -317,6 +317,35 @@ createAppAuth({
The permissions granted to the access token. The permissions object includes the permission names and their access type. For a complete list of permissions and allowable values, see GitHub App permissions.
+
+
+ factory
+ |
+
+ function
+ |
+
+Only relevant if `type` is set to `"installation"`.
+
+When the `factory` option is, the `auth({type: "installation", installationId, factory })` call with resolve with whatever the factory function returns. The `factory` function will be called with all the strategy option that `auth` was created with, plus the additional options passed to `auth`, besides `type` and `factory`.
+
+For example, you can create a new `auth` instance for an installation which shares the internal state (especially the access token cache) with the calling `auth` instance:
+
+```js
+const appAuth = createAppAuth({
+ id: 1,
+ privateKey: "-----BEGIN PRIVATE KEY-----\n...",
+});
+
+const installationAuth123 = await appAuth({
+ type: "installation",
+ installationId: 123,
+ factory: createAppAuth,
+});
+```
+
+ |
+
refresh
diff --git a/src/get-app-authentication.ts b/src/get-app-authentication.ts
index bd45769d6..657a11134 100644
--- a/src/get-app-authentication.ts
+++ b/src/get-app-authentication.ts
@@ -8,7 +8,7 @@ export async function getAppAuthentication({
timeDifference,
}: State): Promise {
const appAuthentication = await githubAppJwt({
- id,
+ id: +id,
privateKey,
now: timeDifference && Math.floor(Date.now() / 1000) + timeDifference,
});
diff --git a/src/get-installation-authentication.ts b/src/get-installation-authentication.ts
index f56d6b51b..b91d69107 100644
--- a/src/get-installation-authentication.ts
+++ b/src/get-installation-authentication.ts
@@ -21,6 +21,21 @@ export async function getInstallationAuthentication(
);
}
+ if (options.factory) {
+ // @ts-ignore if `options.factory` is set, the return type for `auth()` should be `Promise>`
+ return options.factory({
+ cache: state.cache,
+ id: state.id,
+ privateKey: state.privateKey,
+ log: state.log,
+ request: state.request,
+ clientId: state.clientId,
+ clientSecret: state.clientSecret,
+ timeDifference: state.timeDifference,
+ installationId,
+ });
+ }
+
const optionsWithInstallationTokenFromState = Object.assign(
{ installationId },
options
diff --git a/src/types.ts b/src/types.ts
index 99ca9a305..4fd96698c 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -73,12 +73,14 @@ export type Authentication =
| InstallationAccessTokenAuthentication
| OAuthAccesTokenAuthentication;
-export type StrategyOptions = {
+type OAuthStrategyOptions = {
+ clientId?: string;
+ clientSecret?: string;
+};
+export type StrategyOptions = OAuthStrategyOptions & {
id: number | string;
privateKey: string;
installationId?: number | string;
- clientId?: string;
- clientSecret?: string;
request?: OctokitTypes.RequestInterface;
cache?: Cache;
log?: {
@@ -87,11 +89,13 @@ export type StrategyOptions = {
};
};
-export type StrategyOptionsWithDefaults = StrategyOptions & {
- id: number;
- request: OctokitTypes.RequestInterface;
- cache: Cache;
-};
+export type FactoryOptions = Required> &
+ State;
+
+export type StrategyOptionsWithDefaults = StrategyOptions &
+ Required<
+ Omit
+ >;
export type Permissions = {
[name: string]: string;
@@ -102,6 +106,9 @@ export type InstallationAuthOptions = {
repositoryIds?: number[];
permissions?: Permissions;
refresh?: boolean;
+ // TODO: return type of `auth({ type: "installation", installationId, factory })`
+ // should be Promise>
+ factory?: (options: FactoryOptions) => unknown;
};
export type OAuthOptions = {
@@ -119,14 +126,6 @@ export type WithInstallationId = {
installationId: number;
};
-export type State = StrategyOptions & {
- id: number;
- installationId?: number;
- request: OctokitTypes.RequestInterface;
- cache: Cache;
- timeDifference?: number;
- log: {
- warn: (message: string, additionalInfo?: object) => any;
- [key: string]: any;
- };
+export type State = StrategyOptionsWithDefaults & {
+ timeDifference: number;
};
diff --git a/test/index.test.ts b/test/index.test.ts
index 083fd06d6..e3843748c 100644
--- a/test/index.test.ts
+++ b/test/index.test.ts
@@ -1,4 +1,5 @@
import fetchMock, { MockMatcherFunction } from "fetch-mock";
+
import { request } from "@octokit/request";
import { install, Clock } from "@sinonjs/fake-timers";
@@ -1896,3 +1897,41 @@ test("createAppAuth passed with log option", async () => {
expect(calls).toStrictEqual(["warn", "warn"]);
});
+
+test("factory auth option", async () => {
+ const appAuth = createAppAuth({
+ id: APP_ID,
+ privateKey: PRIVATE_KEY,
+ });
+
+ const factory = jest.fn().mockReturnValue({ ok: true });
+
+ const customAuth = await appAuth({
+ type: "installation",
+ installationId: 123,
+ factory,
+ });
+
+ expect(customAuth).toStrictEqual({ ok: true });
+
+ const factoryOptions = factory.mock.calls[0][0];
+ expect(Object.keys(factoryOptions).sort()).toStrictEqual([
+ "cache",
+ "clientId",
+ "clientSecret",
+ "id",
+ "installationId",
+ "log",
+ "privateKey",
+ "request",
+ "timeDifference",
+ ]);
+
+ expect(factoryOptions).toEqual(
+ expect.objectContaining({
+ id: APP_ID,
+ privateKey: PRIVATE_KEY,
+ installationId: 123,
+ })
+ );
+});
|