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, + }) + ); +});