Skip to content

Commit 908f137

Browse files
author
Artem Shteltser
committed
Fixed eslint & cloned auth strategies from octokit
1 parent de7e02e commit 908f137

32 files changed

+37279
-7557
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ lib
44

55
# Dependency directories
66
node_modules
7+
src/authentication/**/*

package-lock.json

Lines changed: 28593 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"validate:types:index": "tsc --noEmit lib/index.d.ts",
5050
"validate:types:minimal": "tsc --noEmit lib/minimal.d.ts",
5151
"validate:types:plugin:authenticate": "tsc --noEmit lib/plugins/authenticate.d.ts",
52+
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json}\"",
5253
"test": "jest"
5354
},
5455
"dependencies": {
@@ -78,7 +79,7 @@
7879
"eslint-plugin-import": "^2.20.1",
7980
"eslint-plugin-jest": "^23.8.2",
8081
"eslint-plugin-node": "^11.0.0",
81-
"eslint-plugin-prettier": "^3.1.2",
82+
"eslint-plugin-prettier": "^3.3.1",
8283
"eslint-plugin-promise": "^4.2.1",
8384
"eslint-plugin-standard": "^4.0.1",
8485
"husky": "^4.2.5",
@@ -89,7 +90,7 @@
8990
"mkdirp": "^1.0.4",
9091
"mustache": "^4.0.1",
9192
"npm-run-all": "^4.1.5",
92-
"prettier": "^2.0.5",
93+
"prettier": "^2.2.1",
9394
"pretty-quick": "^2.0.1",
9495
"rimraf": "^3.0.2",
9596
"rollup-plugin-typescript2": "^0.27.1",
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { AuthOptions, Authentication, State } from './types'
2+
import { getAppAuthentication } from './get-app-authentication'
3+
import { getInstallationAuthentication } from './get-installation-authentication'
4+
import { getOAuthAuthentication } from './get-oauth-authentication'
5+
6+
export async function auth(
7+
state: State,
8+
options: AuthOptions
9+
): Promise<Authentication> {
10+
const { type } = options
11+
12+
switch (type) {
13+
case 'app':
14+
return getAppAuthentication(state)
15+
case 'installation':
16+
return getInstallationAuthentication(state, options)
17+
case 'oauth':
18+
return getOAuthAuthentication(state, options)
19+
default:
20+
throw new Error(`Invalid auth type: ${type}`)
21+
}
22+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// https://github.com/isaacs/node-lru-cache#readme
2+
import LRU from 'lru-cache'
3+
4+
/* istanbul ignore next */
5+
import {
6+
InstallationAuthOptions,
7+
Cache,
8+
CacheData,
9+
Permissions,
10+
InstallationAccessTokenData,
11+
REPOSITORY_SELECTION,
12+
} from './types'
13+
14+
export function getCache() {
15+
return new LRU<number, string>({
16+
// cache max. 15000 tokens, that will use less than 10mb memory
17+
max: 15000,
18+
// Cache for 1 minute less than GitHub expiry
19+
maxAge: 1000 * 60 * 59,
20+
})
21+
}
22+
23+
export async function get(
24+
cache: Cache,
25+
options: InstallationAuthOptions
26+
): Promise<InstallationAccessTokenData | void> {
27+
const cacheKey = optionsToCacheKey(options)
28+
const result = await cache.get(cacheKey)
29+
30+
if (!result) {
31+
return
32+
}
33+
34+
const [
35+
token,
36+
createdAt,
37+
expiresAt,
38+
repositorySelection,
39+
permissionsString,
40+
singleFileName,
41+
] = result.split('|')
42+
43+
const permissions =
44+
options.permissions ||
45+
permissionsString.split(/,/).reduce((permissions: Permissions, string) => {
46+
if (/!$/.test(string)) {
47+
permissions[string.slice(0, -1)] = 'write'
48+
} else {
49+
permissions[string] = 'read'
50+
}
51+
52+
return permissions
53+
}, {} as Permissions)
54+
55+
return {
56+
token,
57+
createdAt,
58+
expiresAt,
59+
permissions,
60+
repositoryIds: options.repositoryIds,
61+
singleFileName,
62+
repositorySelection: repositorySelection as REPOSITORY_SELECTION,
63+
}
64+
}
65+
export async function set(
66+
cache: Cache,
67+
options: InstallationAuthOptions,
68+
data: CacheData
69+
): Promise<void> {
70+
const key = optionsToCacheKey(options)
71+
72+
const permissionsString = options.permissions
73+
? ''
74+
: Object.keys(data.permissions)
75+
.map(
76+
(name) => `${name}${data.permissions[name] === 'write' ? '!' : ''}`
77+
)
78+
.join(',')
79+
80+
const value = [
81+
data.token,
82+
data.createdAt,
83+
data.expiresAt,
84+
data.repositorySelection,
85+
permissionsString,
86+
data.singleFileName,
87+
].join('|')
88+
89+
await cache.set(key, value)
90+
}
91+
92+
function optionsToCacheKey({
93+
installationId,
94+
permissions = {},
95+
repositoryIds = [],
96+
}: InstallationAuthOptions) {
97+
const permissionsString = Object.keys(permissions)
98+
.sort()
99+
.map((name) => (permissions[name] === 'read' ? name : `${name}!`))
100+
.join(',')
101+
102+
const repositoryIdsString = repositoryIds.sort().join(',')
103+
104+
return [installationId, repositoryIdsString, permissionsString]
105+
.filter(Boolean)
106+
.join('|')
107+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { githubAppJwt } from 'universal-github-app-jwt'
2+
3+
import { AppAuthentication, State } from './types'
4+
5+
export async function getAppAuthentication({
6+
appId,
7+
privateKey,
8+
timeDifference,
9+
}: State): Promise<AppAuthentication> {
10+
const appAuthentication = await githubAppJwt({
11+
id: +appId,
12+
privateKey,
13+
now: timeDifference && Math.floor(Date.now() / 1000) + timeDifference,
14+
})
15+
16+
return {
17+
type: 'app',
18+
token: appAuthentication.token,
19+
appId: appAuthentication.appId,
20+
expiresAt: new Date(appAuthentication.expiration * 1000).toISOString(),
21+
}
22+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { get, set } from './cache'
2+
import { getAppAuthentication } from './get-app-authentication'
3+
import { toTokenAuthentication } from './to-token-authentication'
4+
import {
5+
InstallationAuthOptions,
6+
InstallationAccessTokenAuthentication,
7+
RequestInterface,
8+
State,
9+
} from './types'
10+
11+
export async function getInstallationAuthentication(
12+
state: State,
13+
options: InstallationAuthOptions,
14+
customRequest?: RequestInterface
15+
): Promise<InstallationAccessTokenAuthentication> {
16+
const installationId = Number(options.installationId || state.installationId)
17+
18+
if (!installationId) {
19+
throw new Error(
20+
'[@octokit/auth-app] installationId option is required for installation authentication.'
21+
)
22+
}
23+
24+
if (options.factory) {
25+
const { type, factory, ...factoryAuthOptions } = options
26+
// @ts-ignore if `options.factory` is set, the return type for `auth()` should be `Promise<ReturnType<options.factory>>`
27+
return factory(Object.assign({}, state, factoryAuthOptions))
28+
}
29+
30+
const optionsWithInstallationTokenFromState = Object.assign(
31+
{ installationId },
32+
options
33+
)
34+
35+
if (!options.refresh) {
36+
const result = await get(state.cache, optionsWithInstallationTokenFromState)
37+
if (result) {
38+
const {
39+
token,
40+
createdAt,
41+
expiresAt,
42+
permissions,
43+
repositoryIds,
44+
singleFileName,
45+
repositorySelection,
46+
} = result
47+
48+
return toTokenAuthentication({
49+
installationId,
50+
token,
51+
createdAt,
52+
expiresAt,
53+
permissions,
54+
repositorySelection,
55+
repositoryIds,
56+
singleFileName,
57+
})
58+
}
59+
}
60+
61+
const appAuthentication = await getAppAuthentication(state)
62+
const request = customRequest || state.request
63+
64+
const {
65+
data: {
66+
token,
67+
expires_at: expiresAt,
68+
repositories,
69+
permissions,
70+
// @ts-ignore
71+
repository_selection: repositorySelection,
72+
// @ts-ignore
73+
single_file: singleFileName,
74+
},
75+
} = await request('POST /app/installations/{installation_id}/access_tokens', {
76+
installation_id: installationId,
77+
repository_ids: options.repositoryIds,
78+
permissions: options.permissions,
79+
mediaType: {
80+
previews: ['machine-man'],
81+
},
82+
headers: {
83+
authorization: `bearer ${appAuthentication.token}`,
84+
},
85+
})
86+
87+
const repositoryIds = repositories
88+
? repositories.map((r: { id: number }) => r.id)
89+
: void 0
90+
91+
const createdAt = new Date().toISOString()
92+
await set(state.cache, optionsWithInstallationTokenFromState, {
93+
token,
94+
createdAt,
95+
expiresAt,
96+
repositorySelection,
97+
permissions,
98+
repositoryIds,
99+
singleFileName,
100+
})
101+
102+
return toTokenAuthentication({
103+
installationId,
104+
token,
105+
createdAt,
106+
expiresAt,
107+
repositorySelection,
108+
permissions,
109+
repositoryIds,
110+
singleFileName,
111+
})
112+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {
2+
RequestInterface,
3+
OAuthOptions,
4+
StrategyOptionsWithDefaults,
5+
OAuthAccesTokenAuthentication,
6+
} from './types'
7+
import { RequestError } from '@octokit/request-error'
8+
9+
export async function getOAuthAuthentication(
10+
state: StrategyOptionsWithDefaults,
11+
options: OAuthOptions,
12+
customRequest?: RequestInterface
13+
): Promise<OAuthAccesTokenAuthentication> {
14+
const request = customRequest || state.request
15+
16+
// The "/login/oauth/access_token" is not part of the REST API hosted on api.github.com,
17+
// instead it’s using the github.com domain.
18+
const route = /^https:\/\/(api\.)?github\.com$/.test(
19+
state.request.endpoint.DEFAULTS.baseUrl
20+
)
21+
? 'POST https://github.com/login/oauth/access_token'
22+
: `POST ${state.request.endpoint.DEFAULTS.baseUrl.replace(
23+
'/api/v3',
24+
'/login/oauth/access_token'
25+
)}`
26+
27+
const parameters = {
28+
headers: {
29+
accept: `application/json`,
30+
},
31+
client_id: state.clientId,
32+
client_secret: state.clientSecret,
33+
code: options.code,
34+
state: options.state,
35+
redirect_uri: options.redirectUrl,
36+
}
37+
38+
const response = await request(route, parameters)
39+
40+
if (response.data.error !== undefined) {
41+
throw new RequestError(
42+
`${response.data.error_description} (${response.data.error})`,
43+
response.status,
44+
{
45+
headers: response.headers,
46+
request: request.endpoint(route, parameters),
47+
}
48+
)
49+
}
50+
51+
const {
52+
data: { access_token: token, scope },
53+
} = response
54+
55+
return {
56+
type: 'token',
57+
tokenType: 'oauth',
58+
token,
59+
scopes: scope.split(/,\s*/).filter(Boolean),
60+
}
61+
}

0 commit comments

Comments
 (0)