Skip to content

Commit

Permalink
feat: factory auth option (#198)
Browse files Browse the repository at this point in the history
  • Loading branch information
gr2m committed Oct 26, 2020
1 parent 2bdb97d commit f5b7ae3
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 19 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href="https://developer.github.com/apps/building-github-apps/creating-github-apps-using-url-parameters/#github-app-permissions">GitHub App permissions</a>.
</td>
</tr>
<tr>
<th>
<code>factory</code>
</th>
<th>
<code>function</code>
</th>
<td>
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,
});
```

</td>
</tr>
<tr>
<th>
<code>refresh</code>
Expand Down
2 changes: 1 addition & 1 deletion src/get-app-authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export async function getAppAuthentication({
timeDifference,
}: State): Promise<AppAuthentication> {
const appAuthentication = await githubAppJwt({
id,
id: +id,
privateKey,
now: timeDifference && Math.floor(Date.now() / 1000) + timeDifference,
});
Expand Down
15 changes: 15 additions & 0 deletions src/get-installation-authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReturnType<options.factory>>`
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
Expand Down
35 changes: 17 additions & 18 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?: {
Expand All @@ -87,11 +89,13 @@ export type StrategyOptions = {
};
};

export type StrategyOptionsWithDefaults = StrategyOptions & {
id: number;
request: OctokitTypes.RequestInterface;
cache: Cache;
};
export type FactoryOptions = Required<Omit<StrategyOptions, keyof State>> &
State;

export type StrategyOptionsWithDefaults = StrategyOptions &
Required<
Omit<StrategyOptions, keyof OAuthStrategyOptions | "installationId">
>;

export type Permissions = {
[name: string]: string;
Expand All @@ -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<ReturnType<factory>>
factory?: (options: FactoryOptions) => unknown;
};

export type OAuthOptions = {
Expand All @@ -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;
};
39 changes: 39 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import fetchMock, { MockMatcherFunction } from "fetch-mock";

import { request } from "@octokit/request";
import { install, Clock } from "@sinonjs/fake-timers";

Expand Down Expand Up @@ -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,
})
);
});

0 comments on commit f5b7ae3

Please sign in to comment.