Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ docs
lib

# Dependency directories
node_modules
node_modules
70 changes: 69 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,13 @@ const clientOptions = {
const bitbucket = new Bitbucket(clientOptions)
```

#### Authentication
### Authentication

Bitbucket supports different authentication strategies:

- OAuth 2
- App passwords
- Basic auth

**Using `username` and `password`**:

Expand All @@ -89,6 +95,68 @@ const clientOptions = {
const bitbucket = new Bitbucket(clientOptions)
```

**Using `Client Credentials Grant`**:

```js
const clientOptions = {
authStrategy: 'OAuth',
auth: {
grant_type: 'clientCredentialsGrant',
client_id: 'client_id',
client_secret: 'client_secret',
},
}

const bitbucket = new Bitbucket(clientOptions)
```

**Using `Authorization Code Grant`**:

```js
const clientOptions = {
authStrategy: 'OAuth',
auth: {
grant_type: 'authorizationCodeGrant',
client_id: 'client_id',
client_secret: 'client_secret',
code: 'code',
},
}

const bitbucket = new Bitbucket(clientOptions)
```

**Using `Resource Owner Password Credentials Grant`**:

```js
const clientOptions = {
authStrategy: 'OAuth',
auth: {
grant_type: 'resourceOwnerPasswordCredentialsGrant',
client_id: 'client_id',
client_secret: 'client_secret',
username: 'username',
password: 'password',
},
}

const bitbucket = new Bitbucket(clientOptions)
```

**Using `Bitbucket Cloud JWT Grant`**:

```js
const clientOptions = {
authStrategy: 'OAuth',
auth: {
grant_type: 'bitbucketCloudJWTGrant',
jwt_token: 'jwt_token',
},
}

const bitbucket = new Bitbucket(clientOptions)
```

#### API Methods

**async/await**
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"build:core": "bili",
"prebuild:plugins": "rimraf lib/plugins/*.js lib/plugins/*.js.map",
"build:plugins": "npm-run-all -s build:plugin:*",
"build:plugin:authenticate": "PLUGIN=authenticate bili",
"build:plugin:auth": "cross-env PLUGIN=auth bili",
"predocs:build": "mkdirp docs && rimraf docs/*",
"docs:build": "npm run generate:api-docs",
"postdocs:build": "apidoc -i docs -o docs",
Expand All @@ -48,7 +48,7 @@
"validate:types:bitbucket": "tsc --noEmit lib/bitbucket.d.ts",
"validate:types:index": "tsc --noEmit lib/index.d.ts",
"validate:types:minimal": "tsc --noEmit lib/minimal.d.ts",
"validate:types:plugin:authenticate": "tsc --noEmit lib/plugins/authenticate.d.ts",
"validate:types:plugin:auth": "tsc --noEmit lib/plugins/auth.d.ts",
"test": "jest"
},
"dependencies": {
Expand All @@ -71,6 +71,7 @@
"apidoc": "0.17.5",
"bili": "^4.10.0",
"clean-deep": "^3.3.0",
"cross-env": "^7.0.3",
"deep-sort-object": "^1.0.2",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.0",
Expand Down
9 changes: 7 additions & 2 deletions src/client/constructor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ export function constructor(
clientOptions: Options = {}
): APIClient {
const requestHook: RequestHook = new Singular()
const requestDefaults = getEndpointOptions(clientOptions, requestHook)

const client = {
request: request.defaults(getEndpointOptions(clientOptions, requestHook)),
const client: APIClient = {
// eslint-disable-next-line @typescript-eslint/require-await
auth: async () => ({
type: 'unauthenticated',
}),
request: request.defaults(requestDefaults),
requestHook,
}

Expand Down
7 changes: 5 additions & 2 deletions src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ type Request = import('../request/types').Request
type Response<T> = import('../request/types').Response<T>

export interface Options {
[option: string]: any
auth?: any
authStrategy?: string
baseUrl?: string
request?: RequestOptions['request']
[option: string]: any
}

export type RequestHook = HookSingular<RequestOptions, Response<any>, HTTPError>

export interface APIClient {
[key: string]: any
auth?: (...args: unknown[]) => Promise<unknown>
request: Request
requestHook: RequestHook
[key: string]: any
}

export type Plugin = (client: APIClient, options: Options) => void
Expand Down
2 changes: 1 addition & 1 deletion src/endpoint/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export interface Endpoint {
DEFAULTS: EndpointDefaults
defaults(endpointOptions: EndpointParams): Endpoint
merge(
endpointRoute: string,
endpointRoute: string | EndpointOptions,
endpointOptions?: EndpointParams
): EndpointDefaults
merge(endpointOptions: EndpointParams): EndpointDefaults
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import validateRequestPlugin from './plugins/validate-request'

const Plugins = [
noticePlugin,
authPlugin,
paginationPlugin,
registerEndpointsPlugin,
registerApiEndpointsPlugin,
validateRequestPlugin,
authPlugin,
]

export const Bitbucket = Client.plugins(Plugins)
2 changes: 1 addition & 1 deletion src/minimal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import validateRequestPlugin from './plugins/validate-request'

const Plugins = [
noticePlugin,
authPlugin,
paginationPlugin,
registerEndpointsPlugin,
validateRequestPlugin,
authPlugin,
]

export const Bitbucket = Client.plugins(Plugins)
9 changes: 0 additions & 9 deletions src/plugins/_oauth/index.js

This file was deleted.

13 changes: 13 additions & 0 deletions src/plugins/auth/OAuth/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { getOAuthAccessToken } from './get-oauth-access-token'

type Authentication = import('./types').Authentication
type AuthState = import('./types').AuthPluginState

export async function auth(authState: AuthState): Promise<Authentication | {}> {
if (!authState.token) {
const newToken = await getOAuthAccessToken(authState)
authState.token = newToken
}

return authState.token ?? {}
}
48 changes: 48 additions & 0 deletions src/plugins/auth/OAuth/get-oauth-access-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import btoa from 'btoa-lite'
import oAuth2Spec from './spec.json'
import getOAuthRoutes from './routes'
import { request as Request } from '../../../request'

type AuthState = import('./types').AuthPluginState
type EndpointOptions = import('./types').EndpointOptions

const routes = getOAuthRoutes(oAuth2Spec).getToken
export const getOAuthAccessToken = async (state: AuthState): Promise<any> => {
const { auth: authState } = state
const { grant_type: grantType } = authState
const { accepts, url, method, grant_type } = routes[grantType]

const endpointOptions: EndpointOptions = {
accepts,
url,
method,
headers: {},
request: {},
grant_type,
}

endpointOptions.headers!.authorization =
authState.grant_type === 'bitbucketCloudJWTGrant'
? `JWT ${btoa(`${authState.jwt_token}`)}`
: `Basic ${btoa(`${authState.client_id}:${authState.client_secret}`)}`

if (authState.grant_type === 'authorizationCodeGrant') {
endpointOptions.code = authState.code
}

if (authState.grant_type === 'resourceOwnerPasswordCredentialsGrant') {
endpointOptions.username = authState.username
endpointOptions.password = authState.password
}

const response = await Request(endpointOptions)
const { data } = response

const newToken = {
access_token: data.access_token,
refresh_token: data.refresh_token,
scopes: data.scopes.split(/\s/).filter(Boolean),
}

return newToken
}
15 changes: 15 additions & 0 deletions src/plugins/auth/OAuth/hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { auth } from './auth'

type AuthState = import('./types').AuthPluginState
type RequestOptions = import('./types').RequestOptions

export async function beforeHook(
authState: AuthState,
requestOptions: RequestOptions
): Promise<void> {
if (!authState.token) {
await auth(authState)
} else if (authState.token.access_token) {
requestOptions.headers.authorization = `Bearer ${authState.token.access_token}`
}
}
24 changes: 24 additions & 0 deletions src/plugins/auth/OAuth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { auth } from './auth'
import { beforeHook } from './hook'
import { validateOptions } from './validate-options'

type APIClient = import('./types').APIClient
type AuthPluginState = import('./types').AuthPluginState
type Options = import('./types').Options

const OAuth = (client: APIClient, clientOptions: Options): void => {
if (!clientOptions.auth || clientOptions.authStrategy !== 'OAuth') return

validateOptions(clientOptions.auth)

const state: AuthPluginState = {
authStrategy: clientOptions.authStrategy,
auth: clientOptions.auth,
}

client.auth = auth.bind(null, state)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
client.requestHook.before(beforeHook.bind(null, state))
}

export default OAuth
Loading