Full Code of badgateway/oauth2-client for AI

main 8151413ae17a cached
29 files
118.2 KB
29.5k tokens
69 symbols
1 requests
Download .txt
Repository: badgateway/oauth2-client
Branch: main
Commit: 8151413ae17a
Files: 29
Total size: 118.2 KB

Directory structure:
gitextract_y_zc9pja/

├── .github/
│   ├── CODE_OF_CONDUCT.md
│   ├── CONTRIBUTING.md
│   └── workflows/
│       ├── ci.yml
│       └── npm-publish.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── changelog.md
├── eslint.config.mjs
├── package.json
├── src/
│   ├── client/
│   │   └── authorization-code.ts
│   ├── client.ts
│   ├── error.ts
│   ├── fetch-wrapper.ts
│   ├── index.ts
│   ├── messages.ts
│   └── token.ts
├── test/
│   ├── authorization-code.ts
│   ├── client-credentials.ts
│   ├── client.ts
│   ├── fetch-wrapper.ts
│   ├── password.ts
│   ├── pkce.ts
│   ├── refresh.ts
│   ├── revoke.ts
│   └── test-server.ts
├── tsconfig.json
└── vite.config.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at me@evertpot.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]

[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/


================================================
FILE: .github/CONTRIBUTING.md
================================================
Contributing to this project
============================

Thank you for considering to add to this project! Before you start writing code, here's a few tips:

Small changes / Bugfixes
------------------------

Make sure you target the `version-2.x` branch when you're working on your change and submitting your PR.
The `main` branch will become Version 3, which is currently unreleased and will have a few breaking changes,
such as switching to ESM and dropping support for older Node versions.

There's no release date for 3.x, so if you want your compact change in the next release: base it on `version-2.x`.

Large changes / Major features / new OAuth2 flow/features/grant types
---------------------------------------------------------------------

Drop me a line first with your plan before you start! I'm very open to adding things from the OAuth2 ecosystem,
but getting alignment on the approach can potentially save time. Since I'm likely the person to maintain your
feature after it was merged, I want to have high confidence I understand it really well, and I'm still learning
this massive ecosystem.

OpenID Connect
--------------

Currently this library will not expand it's scope to support OpenID Connect. The main reason is that it requires
bringing in a JWT library, which is in conflict with the design goal of making a 0-dependency, lean OAuth2 library.

This may change in the future, but right now OpenID is not a goal.

However, I am open to small additions / new parameters to this library from the OpenID suite of standards.
For example, this library supports the `response_mode` parameter and will return the `id_token` if it's in the
response.



================================================
FILE: .github/workflows/ci.yml
================================================
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions

name: Node.js CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main, 'version-2.x' ]

jobs:
  test:
    name: Run tests

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [18.x, 20.x, 22.x]
        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/

    steps:
    - uses: actions/checkout@v4
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }}
    - run: npm ci
    - run: npm run build --if-present
    - run: npx tsx --test test/*.ts

  lint:
    name: Lint

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4
    - name: Use Node.js
      uses: actions/setup-node@v4
      with:
        node-version: 20.x
    - run: npm ci
    - run: npm run lint


================================================
FILE: .github/workflows/npm-publish.yml
================================================
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages

name: Publish NPM package

on:
  release:
    types: [created]
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 22
      - run: npm ci
      - run: npm test

  publish-npm:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 22
          registry-url: https://registry.npmjs.org/
      - run: npm ci
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}

  publish-gpr:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 22
      - run: npm ci
      - uses: actions/setup-node@v3
        with:
          node-version: 20
          registry-url: 'https://npm.pkg.github.com'
          scope: '@badgateway'
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}


================================================
FILE: .gitignore
================================================
# npm
/node_modules

# typescript output
/dist

# webpack output
/browser

# zip
/*.zip

# vim
.*.swp

# nyc
/.nyc_output


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2019-2025 Evert Pot

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Makefile
================================================
SOURCE_FILES:=$(shell find src/ -type f -name '*.ts')

.PHONY:build
build: dist/build browser/oauth2-client.min.js browser/oauth2-client.min.js.gz

.PHONY:test
test:
	#npx tsx --test test/*.ts
	node --experimental-strip-types --test test/*.ts

.PHONY:lint
lint:
	./node_modules/.bin/eslint --quiet 'src/**/*.ts' 'test/**/*.ts'

.PHONY:fix
fix:
	./node_modules/.bin/eslint --quiet 'src/**/*.ts' 'test/**/*.ts' --fix

.PHONY:watch
watch:
	./node_modules/.bin/tsc --watch


.PHONY:clean
clean:
	rm -r browser/
	rm -r dist/

dist/build: ${SOURCE_FILES}
	./node_modules/.bin/tsc
	touch dist/build

browser/oauth2-client.min.js: ${SOURCE_FILES} vite.config.js
	mkdir -p browser
	npx vite build

browser/oauth2-client.min.js.gz: browser/oauth2-client.min.js
	gzip --keep -f browser/oauth2-client.min.js
	@# For some reason gzip makes the output file older than the input, so
	@# just making sure it gets a good mtime.
	touch browser/oauth2-client.min.js.gz


================================================
FILE: README.md
================================================
# OAuth2 client for Node and browsers

This package contains an OAuth2 client. It aims to be a fully-featured OAuth2
utility library, for Node.js, Browsers and written in Typescript.

This OAuth2 client is only **4KB** gzipped, it has **0** dependencies and
relies on modern APIs like `fetch()` and [Web Crypto][4] which are built-in
since Node 18.


## Highlights

* 16KB minified (5KB gzipped).
* No dependencies.
* `authorization_code` grant with optional [PKCE][1] support.
* `password` and `client_credentials` grant.
* a `fetch()` wrapper that automatically adds Bearer tokens and refreshes them.
* OAuth2 endpoint discovery via the Server metadata document ([RFC8414][2]).
* OAuth2 Token Introspection ([RFC7662][3]).
* Resource Indicators for OAuth 2.0 ([RFC8707][5]).
* OAuth2 Token Revocation ([RFC7009][6]).
* [OAuth 2.0 Multiple Response Type Encoding Practices](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html)

## Installation

```sh
npm i @badgateway/oauth2-client
```


## Usage

To get started, set up the Client class.


```typescript
import { OAuth2Client } from '@badgateway/oauth2-client';

const client = new OAuth2Client({

  // The base URI of your OAuth2 server
  server: 'https://my-auth-server/',

  // OAuth2 client id
  clientId: '...',

  // OAuth2 client secret. Only required for 'client_credentials', 'password'
  // flows. Don't specify this in insecure contexts, such as a browser using
  // the authorization_code flow.
  clientSecret: '...',


  // The following URIs are all optional. If they are not specified, we will
  // attempt to discover them using the oauth2 discovery document.
  // If your server doesn't have support this, you may need to specify these.
  // you may use relative URIs for any of these.


  // Token endpoint. Most flows need this.
  // If not specified we'll use the information for the discovery document
  // first, and otherwise default to /token
  tokenEndpoint: '/token',

  // Authorization endpoint.
  //
  // You only need this to generate URLs for authorization_code flows.
  // If not specified we'll use the information for the discovery document
  // first, and otherwise default to /authorize
  authorizationEndpoint: '/authorize',

  // OAuth2 Metadata discovery endpoint.
  //
  // This document is used to determine various server features.
  // If not specified, we assume it's on /.well-known/oauth2-authorization-server
  discoveryEndpoint: '/.well-known/oauth2-authorization-server',

});
```

### Tokens

Many functions use or return a 'OAuth2Token' type. This type has the following
shape:

```typescript
export type OAuth2Token = {
  accessToken: string;
  refreshToken: string | null;

  /**
   * When the Access Token expires.
   *
   * This is expressed as a unix timestamp in milliseconds.
   */
  expiresAt: number | null;


  /**
   * If the server returned an OpenID Connect ID token, it will be stored here.
   */
  idToken?: string;

};
```


### client_credentials grant.

```typescript
const token = await client.clientCredentials();
```

### Refreshing tokens

```typescript
const newToken = await client.refreshToken(oldToken);
```


### password grant:

```typescript
const token = await client.password({
  username: '..',
  password: '..',
});
```

### authorization_code

The `authorization_code` flow is the flow for browser-based applications,
and roughly consists of 3 major steps:

1. Redirect the user to an authorization endpoint, where they log in.
2. Authorization endpoint redirects back to app with a 'code' query
   parameter.
3. The `code` is exchanged for a access and refresh token.

This library provides support for these steps, but there's no requirement
to use its functionality as the system is mostly stateless.

```typescript
import { OAuth2Client, generateCodeVerifier } from '@badgateway/oauth2-client';

const client = new OAuth2Client({
  server: 'https://authserver.example/',
  clientId: '...',

  // Note, if urls cannot be auto-detected, also specify these:
  tokenEndpoint: '/token',
  authorizationEndpoint: '/authorize',
});
```

**Redirecting the user to the authorization server**

```typescript
/**
 * This generates a security code that must be passed to the various steps.
 * This is used for 'PKCE' which is an advanced security feature.
 *
 * It doesn't break servers that don't support it, but it makes servers that
 * so support it more secure.
 *
 * It's optional to pass this, but recommended.
 */
const codeVerifier = await generateCodeVerifier();

// In a browser this might work as follows:
document.location = await client.authorizationCode.getAuthorizeUri({

  // URL in the app that the user should get redirected to after authenticating
  redirectUri: 'https://my-app.example/',

  // Optional string that can be sent along to the auth server. This value will
  // be sent along with the redirect back to the app verbatim.
  state: 'some-string',

  codeVerifier,

  scope: ['scope1', 'scope2'],

});
```

**Handling the redirect back to the app and obtain token**

```typescript
const oauth2Token = await client.authorizationCode.getTokenFromCodeRedirect(
  document.location,
  {
    /**
     * The redirect URI is not actually used for any redirects, but MUST be the
     * same as what you passed earlier to "authorizationCode"
     */
    redirectUri: 'https://my-app.example/',

    /**
     * This is optional, but if it's passed then it also MUST be the same as
     * what you passed in the first step.
     *
     * If set, it will verify that the server sent the exact same state back.
     */
    state: 'some-string',

    codeVerifier,

  }
);
```


### Fetch Wrapper

When using an OAuth2-protected API, typically you will need to obtain an Access
token, and then add this token to each request using an `Authorization: Bearer`
header.

Because access tokens have a limited lifetime, and occasionally needs to be
refreshed this is a bunch of potential plumbing.

To make this easier, this library has a 'fetch wrapper'. This is effectively
just like a regular fetch function, except it automatically adds the header
and will automatically refresh tokens when needed.

Usage:

```typescript
import { OAuth2Client, OAuth2Fetch } from '@badgateway/oauth2-client';

const client = new OAuth2Client({
  server: 'https://my-auth-server',
  clientId: 'my-client-id'
});


const fetchWrapper = new OAuth2Fetch({
  client: client,

  /**
   * You are responsible for implementing this function.
   * it's purpose is to supply the 'initial' oauth2 token.
   */
  getNewToken: async () => {

    // Example
    return client.clientCredentials();

    // Another example
    return client.authorizationCode.getToken({
      code: '..',
      redirectUri: '..',
    });

    // You can return null to fail the process. You may want to do this
    // when a user needs to be redirected back to the authorization_code
    // endpoints.
    return null;

  },

  /**
   * Optional. This will be called for any fatal authentication errors.
   */
  onError: (err) => {
    // err is of type Error
  }

});
```

After set up, you can just call `fetch` on the new object to call your API, and
the library will ensure there's always a `Bearer` header.

```typescript
const response = fetchWrapper.fetch('https://my-api', {
  method: 'POST',
  body: 'Hello world'
});
```

### Storing tokens for later use with FetchWrapper

To keep a user logged in between sessions, you may want to avoid full
reauthentication. To do this, you'll need to store authentication token
somewhere.

The fetch wrapper has 2 functions to help with this:

```typescript

const fetchWrapper = new OAuth2Fetch({
  client: client,

  getNewToken: async () => {

    // See above!

  },

  /**
   * This function is called whenever the active token changes. Using this is
   * optional, but it may be used to (for example) put the token in off-line
   * storage for later usage.
   */
  storeToken: (token) => {
    document.localStorage.setItem('token-store', JSON.stringify(token));
  },

  /**
   * Also an optional feature. Implement this if you want the wrapper to try a
   * stored token before attempting a full re-authentication.
   *
   * This function may be async. Return null if there was no token.
   */
  getStoredToken: () => {
    const token = document.localStorage.getItem('token-store');
    if (token) return JSON.parse(token);
    return null;
  }

});
```


### Fetch Middleware function

It might be preferable to use this library as a more traditional 'middleware'.

The OAuth2Fetch object also exposes a `mw` function that returns a middleware
for fetch.

```typescript
const mw = oauth2.mw();
const response = mw(
  myRequest,
  req => fetch(req)
);
```

This syntax looks a bit wild if you're not used to building middlewares, but
this effectively allows you to 'decorate' existing request libraries with
functionality from this oauth2 library.

A real example using the [Ketting](https://github.com/badgateway/ketting)
library:

```typescript
import { Client } from 'ketting';
import { OAuth2Client, OAuth2Fetch } from '@badgateway/oauth2-client';

/**
 * Create the oauth2 client
 */
const oauth2Client = new OAuth2Client({
  server: 'https://my-auth.example',
  clientId: 'foo',
});

/**
 * Create the 'fetch helper'
 */
const oauth2Fetch = new OAuth2Fetch({
  client: oauth2Client,
});

/**
 * Add the middleware to Ketting
 */
const ketting = new Client('http://api-root');
ketting.use(oauth2Fetch.mw());
```

### Introspection
Introspection ([RFC7662][3]) lets you find more information about a token,
such as whether it's valid, which user it belongs to, which oauth2 client
was used to generate it, etc.

To be able to use it, your authorization server must have support for the
introspection endpoint. It's location will be automatically detected using
the Metadata discovery document.

```typescript
import { OAuth2Client } from '@badgateway/oauth2-client';

const client = new Client({
  server: 'https://auth-server.example/',

  clientId: '...',

  /**
   * Some servers require OAuth2 clientId/clientSecret to be passed.
   * If they require it, specify it. If not it's fine to omit.
   */
  clientSecret: '...',

});

// Get a token
const token = client.clientCredentials();

// Introspect!
console.log(client.introspect(token));
```

## OAuth2 `client_id` and `client_secret` encoding

OAuth2 allows users to encode the `client_id` and `client_secret` either in a
`Authorization: Basic` header or in the `POST` request body.

Real-world OAuth2 servers may support one or the other, or both. The OAuth2
spec _requires_ that servers support the Authorization header, and don't
recommend using the body.

By default, this library will use the `Authorization` header. OAuth2 also
requires that clients percent-encode the `client_id` and `client_secret`, but
in practice many popular servers break if you do this. By default this library
will *not* percent encode any characters except the `:` character.

You can change this behavior using the `authenticatioMethod` flag:

```typescript
const client = new OAuth2Client({
  server: 'https://auth-server.example/',
  clientId: '...',
  clientSecret: '...',
  authenticationMethod: 'client_secret_post', // encode in POST body 
});
```

The following 3 values are currently supported:

* `client_secret_post` - Encode in POST body
* `client_secret_basic` - Encode in Authorization header using the strict
  standard rules.
* `client_secret_basic_interop` - Encode in Authorization header using less
  strict rules. This is the default and more likely to work with popular
  servers (at least some Google and Ebay APIs want this).

The current OAuth 2.1 draft switches the recommendation to use
`client_secret_post` by default instead. When that document stabilizes and gets
released, this library will also switch to use `client_secret_post` by default
in a major release.

If your OAuth2 server supports POST, we recommend you use `client_secret_post`
as this is more likely to work without a hitch.

If you configured the client using the OAuth2 discovery document, and the
server indicates it prefers `client_secret_basic` we will also default to the
strict form.


## Support for older Node versions

This package works out of the box with modern browsers and Node 18.

For Node 16 and below, use a 2.x version of this package and add
polyfills. The README.md for the 2.x branch of this package contains more information
on the exact steps for older Node versions.

[1]: https://datatracker.ietf.org/doc/html/rfc7636 "Proof Key for Code Exchange by OAuth Public Clients"
[2]: https://datatracker.ietf.org/doc/html/rfc8414 "OAuth 2.0 Authorization Server Metadata"
[3]: https://datatracker.ietf.org/doc/html/rfc7662 "OAuth 2.0 Token Introspection"
[4]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API "Web Crypto API"
[5]: https://datatracker.ietf.org/doc/html/rfc8707 "https://datatracker.ietf.org/doc/html/rfc8707"
[6]: https://datatracker.ietf.org/doc/html/rfc7009 "OAuth 2.0 Token Revocation"


================================================
FILE: changelog.md
================================================
Changelog
=========

3.3.1 (2025-09-09)
------------------

* #193: Fix race condition when multiple function calls are trying to do
  endpoint discovery. (@lukybrody)


3.3.0 (2025-07-30)
------------------

* OAuth2 token operations may now return an `extraParams` property, which is an
  object with all unrecognized properties. Some servers return custom
  properties from OAuth2 responses, and this lets users get access to them.
  (@ericleib, @blarralde)
* The OAuth2Token object now has a `scope` property, containing a list of
  scopes the server returned *if* the server returned this. (@ericleib)


3.2.0 (2025-04-23)
------------------

* #180: The browser build was failing to work for vite and next.js users since
  v3. This was probably due to the switch to ESM. Instead of trying to fix this
  problem in Webpack, this library has switched to vite for the minified
  browser build instead.


3.1.1 (2025-04-17)
------------------

* Ignore auhtentication methods from Discovery document we don't support.


3.1.0 (2025-04-14)
------------------

* #181: Revert back to percent-encoding of the `Authorization: Basic header`.
  Even though this was more correct from a standards perspective, this is
  causing interopability problems with popular real-world OAuth2 servers. If
  you need strict encoding, you can opt-in using the `authorizationMethod`
  option. We recommend using `client_secret_post` if it's possible with your
  server. See README.md for more information on this behaviour and what the
  possible options are.


3.0.0 (2025-03-06)
------------------

* Dropped support for Node 14 and 16.
* Full conversion to ESM.
* Support for the OpenID Connect id_token. If a server returns it, we expose it
  as `idToken`. This is a JWT and would require parsing by a JWT library to get
  access to its information. (@drev74, @redguardtoo).
* #171: `client_id` and `client_secret` are now percent-encoded with the most
  strict rules as specified by RFC 6749. We weren't doing any
  percent/urlencoding before. This is a a BC break if your secret used special
  characters, and the server you're talking is not compliant with the OAuth2
  spec itself (@p2004a, @panva).
* Migrated the test suite from Mocha and Chai to node:test and node:assert
  (@Zen-cronic).
* Package now uses 'erasableSyntaxOnly' flag with Typescript, so it can be used
  with node --experimental-strip-types.


2.4.2 (2024-09-14)
------------------

* #161: Re-use old refresh_token if no new one was issued after a refresh.


2.4.1 (2024-08-22)
------------------

* #151: Add 'Accept' header on token requests to fix a Github compatibility
  issue.
* #151: Throw error when we get an invalid reply from a token endpoint.


2.4.0 (2024-07-27)
------------------

* More robust error handling. When an error is emitted, you now give you access
  to the emitted HTTP Response and response body.
* Support for `response_mode=fragment` in the `authorization_code` flow.


2.3.0 (2024-02-03)
------------------

* Fix for #128: If there's no secret, we should never use Basic auth to encode
  the `client_id`.
* Support for the `resource` parameter from RFC 8707.
* Add support for `scope` parameter to `refresh()`.
* Support for RFC 7009, Token Revocation (@adambom).


2.2.4 (2023-09-05)
------------------

* Added `extraParams` option to `getAuthorizeUri`, allowing users to add
  non-standard arguments to the authorization URI for servers that require
  this. (@pks1989)


2.2.3 (2023-08-03)
------------------

* Moved the `tokenResponseToOAuth2Token` function inside the OAuth2Client
  class, allowing users to override the parsing logic more easily.


2.2.2 (2023-07-28)
------------------

* #111 Some documentation fixes.
* #110: Fix race condition with `getStoredToken` and calling `fetch()`
  immediately after constructing `FetchWrapper`.


2.2.1 (2023-07-07)
------------------

* #15: Fix for 'TypeError: Failed to execute 'fetch' on 'Window': Illegal
  invocation at t.OAuth2Client.request'.


2.2.0 (2023-04-26)
------------------

* Add an option to override which "fetch" implementation is used. (@bradjones1)


2.1.1 (2023-04-23)
------------------

* Re-release. Previous build had an error.


2.1.0 (2023-04-20)
------------------

* Allow users to provide non-standard properties to `client_credentials` token
  requests via an `extraParams` property. This is necessary to support vendors
  like Auth0 and Kinde which both require an `audience` parameter. (@South-Paw)
* Sending `client_id` and `client_secret` in POST request body is now
  optionally supported. By default the credentials will still be sent in the
  `Authorization` header, but users can opt-in to using the body. The
  authentication method will also be discovered if an OAuth2 or OpenID
  discovery document is used. (@parkerduckworth)
* The fetchWrapper now has an option to disable auto-refreshing tokens.
  (@bradjones1)
* Bug fix: If a 'state' parameter was not used in `authorization_code`, it
  should not be required in the redirect.
* Tested with Node 20.


2.0.18 (2023-04-13)
-------------------

* Didn't run `make build` before the last release, which causes some files in
  the `dist/` directory to be out of date.


2.0.17 (2022-10-02)
-------------------

* Correctly pass 'scope' to `authorization_code` redirects.


2.0.16 (2022-07-22)
-------------------

* It was not possible to generate the URL to the authorization endpoint with
  PKCE using Node, due to depending on a global `crypto` object. This is fixed
  with fallbacks all the way back to Node 14.


2.0.15 (2022-07-07)
-------------------

* #70: Sending the client secret is now supported with the `authorization_code`
  flow.


2.0.14 (2022-06-23)
-------------------

* Re-release, to publish on Github packages.


2.0.13 (2022-06-19)
-------------------

* Fixed some docs.


2.0.12 (2022-06-19)
-------------------

* First stable v2 release!
* Renamed this package from `fetch-mw-oauth2` to `@badgateway/oauth2-client`.
* #59: Scope support for `authorization_code` flow.


2.0.11 (2022-06-17)
-------------------

* Released with alpha tag.
* Re-published


2.0.10 (2022-05-10)
-------------------

* Released with alpha tag.
* Tested on Node 14, 16.
* Added polyfills for these node versions (see README).
* `generateCodeVerifier` is now async to support Node 14.


2.0.9 (2022-04-26)
------------------

* Released with alpha tag.
* Set `Content-Type` to `application/x-www-form-urlencoded`.


2.0.8 (2022-04-26)
------------------

* Released with alpha tag.
* Changing the `authorization_code` signature again. It's a bit hard to come up
  with a create signature for this, especially because there's multiple steps
  in the process, and some information has to survive these steps.


2.0.7 (2022-04-26)
------------------

* Released with alpha tag.
* Re-release (broken build).


2.0.6 (2022-04-26)
------------------

* Released with alpha tag.
* Removed redundant parameters.
* `authorization_code` should now also work correctly without PKCE.
* Removed some redundant arguments.


2.0.5 (2022-04-25)
------------------

* Released with alpha tag.
* PKCE support.


2.0.4 (2022-04-20)
------------------

* Released with alpha tag.
* remove `fetchMw` and add `mw()`. `mw()` now _returns_ a middleware function.


2.0.3 (2022-04-19)
------------------

* Released with alpha tag.
* Export `OAuth2AuthorizationCodeClient`
* Client.authorizationCode() should not have been `async`.


2.0.2 (2022-04-19)
------------------

* Released with alpha tag.
* Fix format for `introspect()` function.


2.0.1 (2022-04-19)
------------------

* Released with alpha tag.
* Fix introspection HTTP method name.


2.0.0 (2022-04-19)
------------------

The 2.0 version of this library is a complete rewrite. The original scope of
this library was to provide a wrapper around `fetch()` to add a `Bearer` token
and refresh this token under the hood, but it has now evolved into a
full-featured modern OAuth2 library. The existing 'fetch wrapper' still exists,
but it's not merely one of the features this package offers. The API has
changes, and while I think it shouldn't be difficult to migrate, v2 offers no
backwards compatibility so some rewrites will be required. New features
include:

* Complete support for the `authorization_code` flow, including generating
  redirect urls and parsing query parameters after redirect.
* Support for OAuth2 endpoint discovery, using the OAuth2 Authorization Server
  Metadata document. If your server supports it, just give the library a URL
  and it will figure out the rest. [RFC8414][2].
* Support for OAuth2 token introspection ([RFC7662][3]).
* Generally a better abstraction of the OAuth2 framework.
* Released with alpha tag.


1.0.0 (2021-10-28)
------------------

* Dropped support for Node 10.
* Fixed #45: Call `onAuthError` when a refresh fails.
* Replaced `awesome-typescript-loader` with `ts-loader` for Webpack builds. the
  former appears unmaintained.
* Switched from Travis CI to Github Actions.


0.7.7 (2021-02-22)
------------------

* Last version did not correctly build it's files.


0.7.6 (2021-02-22)
------------------

* Better error handling when the response we got was not a standard OAuth2
  error response body + adding information for when the Basic credentials were
  wrong.
* This fixes the bug when fetch-mw-oauth2 says there's an 'undefined' error.


0.7.5 (2020-12-03)
------------------

* Fixing a few broken links in package.json. Does not alter any behavior.


0.7.3 (2020-12-01)
------------------

* Re-publishing package. Previous version had an old build artifact.


0.7.2 (2020-12-01)
------------------

* Fixed bug that completely broke the token flow.


0.7.1 (2020-11-30)
------------------

* Fix bug in auto-refresh


0.7.0 (2020-11-30)
------------------

* Ensure that only 1 refresh operation will happen in parallel. If there are
  multiple things triggering the refresh, all will wait for the first one to
  finish.
* Automatically schedule a refresh operation 1 minute before the access token
  expires, if the expiry time is known.
* BC Break: If a token is known when setting up OAuth2, this now needs to be
  passed as the second argument. The old behavior still works but will emit a
  warning, and will be removed in a future release.
* 'OAuth2Token' type is now exported.


0.6.1 (2020-11-19)
------------------

* #34: Refresh operation failed for the `authorization_code` flow.


0.6.0 (2020-11-09)
------------------

* Added a onAuthError event, allowing users to intercept this event and
  re-authenticate.
* Simplify types a bit. More duplication in the library, but this should result
  in easier to read errors.
* Typescript 4
* Switch from tslint to eslint.
* Webpack 5


0.5.0 (2020-04-19)
------------------

* Added a `fetchMw()` function that takes a `next` argument so this package can
  behave as a more regular middleware.


0.4.2 (2019-12-09)
------------------

* Files were not correctly built in the last release.


0.4.1 (2019-12-09)
------------------

* Error code 401 will be submitted when authentication fails. Before, we would
  just forward the error code from the OAuth2 server, but this doesn't make a
  lot of sense for a `fetch()` user, as the error might be misinterpreted as an
  error unrelated to auth.


0.4.0 (2019-11-06)
------------------

* Added a `getOptions()` method, which allows a user to get all current tokens
  and store them in LocalStorage. These options can be used as-is in the
  constructor.


0.3.5 (2019-09-05)
------------------

* Include typescript sourcefiles in NPM package, for IDE's.


0.3.4 (2019-03-19)
------------------

* This package now throws OAuth2Error classes for server-side errors.


0.3.3 (2019-03-18)
------------------

* When refreshing a token, browsers don't allow re-use of the same `Request`
  object. Now we're cloning it before use.


0.3.2 (2019-03-13)
------------------

* When refreshing a token, and there's no `client_secret`, the `client_id` must
  be sent in the body.


0.3.1 (2019-03-13)
------------------

* Now correctly exporting all the right symbols.


0.3.0 (2019-03-13)
------------------

* Library is refactored and now uses a class.
* Support for `authorization_code` grant type.
* Exposing some more information to uses.
* Add a new `onTokenUpdate` hook for custom storage.
* It's now possible to construct a client with an existing (old) Access and/or
  refresh token.


0.2.1 (2019-03-13)
------------------

* Shipping `dist/` instead of `src/`.
* Making a browser build lean by not relying on `querystring` or `Buffer`.


0.2.0 (2019-03-12)
------------------

* First public version
* Support for `client_credentials`, `password` and `refresh_token`.
* Will automatically attempt to refresh tokens if it knows an access token is
  expired.

[1]: https://datatracker.ietf.org/doc/html/rfc7636 "Proof Key for Code Exchange
     by OAuth Public Clients"
[2]: https://datatracker.ietf.org/doc/html/rfc8414 "OAuth 2.0 Authorization
     Server Metadata"
[3]: https://datatracker.ietf.org/doc/html/rfc7662 "OAuth 2.0 Token
     Introspection"


================================================
FILE: eslint.config.mjs
================================================
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import globals from "globals";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
    baseDirectory: __dirname,
    recommendedConfig: js.configs.recommended,
    allConfig: js.configs.all
});

export default [...compat.extends(
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
), {
    plugins: {
        "@typescript-eslint": typescriptEslint,
    },

    languageOptions: {
        globals: {
            ...globals.browser,
            ...globals.node,
            Atomics: "readonly",
            SharedArrayBuffer: "readonly",
        },

        parser: tsParser,
        ecmaVersion: 2018,
        sourceType: "module",
    },

    rules: {
        indent: ["error", 2, {
            SwitchCase: 1,
        }],

        "linebreak-style": ["error", "unix"],

        "no-constant-condition": ["error", {
            checkLoops: false,
        }],

        quotes: ["error", "single", {
            allowTemplateLiterals: false,
            avoidEscape: true,
        }],

        semi: ["error", "always"],

        "no-console": ["error", {
            allow: ["warn", "error", "info", "debug"],
        }],

        "no-trailing-spaces": "error",
        "eol-last": "error",

        "@typescript-eslint/ban-ts-comment": ["error", {
            "ts-expect-error": "allow-with-description",
        }],

        "@typescript-eslint/ban-tslint-comment": "error",

        "@typescript-eslint/consistent-type-assertions": ["error", {
            assertionStyle: "as",
            objectLiteralTypeAssertions: "never",
        }],

        "@typescript-eslint/no-inferrable-types": "off",
        "@typescript-eslint/no-explicit-any": 0,
        "@typescript-eslint/no-for-in-array": "error",
        "@typescript-eslint/no-invalid-void-type": "error",
        "@typescript-eslint/no-namespace": "error",
        "@typescript-eslint/no-non-null-asserted-optional-chain": "error",

        "@typescript-eslint/no-unused-vars": ["error", {
            ignoreRestSiblings: true,
            args: "none",
            varsIgnorePattern: "^_",
            caughtErrorsIgnorePattern: "^_"
        }],

        "@typescript-eslint/prefer-for-of": ["error"],
        "@typescript-eslint/prefer-ts-expect-error": ["error"],
    },
}];


================================================
FILE: package.json
================================================
{
  "name": "@badgateway/oauth2-client",
  "version": "3.3.1",
  "description": "OAuth2 client for browsers and Node.js. Tiny footprint, PKCE support",
  "main": "dist/index.js",
  "type": "module",
  "scripts": {
    "test": "make test",
    "prepublishOnly": "make build",
    "lint": "make lint"
  },
  "repository": {
    "type": "git",
    "url": "git+ssh://git@github.com/badgateway/oauth2-client.git"
  },
  "keywords": [
    "fetch",
    "oauth2",
    "pkce",
    "security",
    "bearer",
    "RFC6749",
    "RFC7636",
    "RFC7662",
    "RFC8414",
    "RFC8707"
  ],
  "author": "Evert Pot (https://evertpot.com)",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/badgateway/oauth2-client/issues"
  },
  "homepage": "https://github.com/badgateway/oauth2-client#readme",
  "engines": {
    "node": ">= 18"
  },
  "devDependencies": {
    "@curveball/bodyparser": "^1.0.0",
    "@curveball/core": "^1.0.0",
    "@curveball/http-errors": "^1.0.1",
    "@types/node": "^18.0.0",
    "@typescript-eslint/eslint-plugin": "^8.5.0",
    "@typescript-eslint/parser": "^8.5.0",
    "eslint": "^9.10.0",
    "ts-loader": "^9.2.6",
    "typescript": "^5.0.4",
    "vite": "^6.3.2"
  },
  "browser": "browser/oauth2-client.min.js",
  "files": [
    "dist/",
    "src/",
    "browser/",
    "LICENSE",
    "README.md"
  ]
}


================================================
FILE: src/client/authorization-code.ts
================================================
import { OAuth2Client } from '../client.ts';
import type { OAuth2Token } from '../token.ts';
import type { AuthorizationCodeRequest } from '../messages.ts';
import { OAuth2Error } from '../error.ts';

type GetAuthorizeUrlParams = {
  /**
   * Where to redirect the user back to after authentication.
   */
  redirectUri: string;

  /**
   * The 'state' is a string that can be sent to the authentication server,
   * and back to the redirectUri.
   */
  state?: string;

  /**
   * Code verifier for PKCE support. If you used this in the redirect
   * to the authorization endpoint, you also need to use this again
   * when getting the access_token on the token endpoint.
   */
  codeVerifier?: string;

  /**
   * List of scopes.
   */
  scope?: string[];

  /**
   * The resource the client intends to access.
   *
   * This is defined in RFC 8707.
   */
  resource?: string[] | string;

  /**
   * Any parameters listed here will be added to the query string for the authorization server endpoint.
   */
  extraParams?: Record<string, string>;

  /**
   * By default response parameters for the authorization_flow will be added
   * to the query string.
   *
   * Some servers let you put this in the fragment instead. This may be
   * benefical if your client is a browser, as embedding the authorization
   * code in the fragment part of the URI prevents it from being sent back
   * to the server.
   *
   * See: https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html
   */
  responseMode?: 'query' | 'fragment';
}

type ValidateResponseResult = {

  /**
   * The authorization code. This code should be used to obtain an access token.
   */
  code: string;

  /**
   * List of scopes that the client requested.
   */
  scope?: string[];

}

type GetTokenParams = {

  code: string;

  redirectUri: string;
  state?: string;
  codeVerifier?:string;

  /**
   * The resource the client intends to access.
   *
   * @see https://datatracker.ietf.org/doc/html/rfc8707
   */
  resource?: string[] | string;

}

export class OAuth2AuthorizationCodeClient {

  client: OAuth2Client;

  constructor(client: OAuth2Client) {

    this.client = client;

  }

  /**
   * Returns the URi that the user should open in a browser to initiate the
   * authorization_code flow.
   */
  async getAuthorizeUri(params: GetAuthorizeUrlParams): Promise<string> {

    const [
      codeChallenge,
      authorizationEndpoint
    ] = await Promise.all([
      params.codeVerifier ? getCodeChallenge(params.codeVerifier) : undefined,
      this.client.getEndpoint('authorizationEndpoint')
    ]);

    const query = new URLSearchParams({
      client_id: this.client.settings.clientId,
      response_type: 'code',
      redirect_uri: params.redirectUri,
    });
    if (codeChallenge) {
      query.set('code_challenge_method', codeChallenge[0]);
      query.set('code_challenge', codeChallenge[1]);
    }
    if (params.state) {
      query.set('state', params.state);
    }
    if (params.scope) {
      query.set('scope', params.scope.join(' '));
    }

    if (params.resource) for(const resource of [].concat(params.resource as any)) {
      query.append('resource', resource);
    }

    if (params.responseMode && params.responseMode!=='query') {
      query.append('response_mode', params.responseMode);
    }

    if (params.extraParams) for(const [k,v] of Object.entries(params.extraParams)) {
      if (query.has(k)) throw new Error(`Property in extraParams would overwrite standard property: ${k}`);
      query.set(k, v);
    }

    return authorizationEndpoint + '?' + query.toString();

  }

  async getTokenFromCodeRedirect(url: string|URL, params: Omit<GetTokenParams, 'code'> ): Promise<OAuth2Token> {

    const { code } = this.validateResponse(url, {
      state: params.state
    });

    return this.getToken({
      code,
      redirectUri: params.redirectUri,
      codeVerifier: params.codeVerifier,
    });

  }

  /**
   * After the user redirected back from the authorization endpoint, the
   * url will contain a 'code' and other information.
   *
   * This function takes the url and validate the response. If the user
   * redirected back with an error, an error will be thrown.
   */
  validateResponse(url: string|URL, params: {state?: string}): ValidateResponseResult {

    url = new URL(url);
    let queryParams = url.searchParams;
    if (!queryParams.has('code') && !queryParams.has('error') && url.hash.length>0) {
      // Try the fragment
      queryParams = new URLSearchParams(url.hash.slice(1));
    }

    if (queryParams.has('error')) {
      throw new OAuth2Error(
        queryParams.get('error_description') ?? 'OAuth2 error',
        queryParams.get('error') as any,
      );
    }

    if (!queryParams.has('code')) throw new Error(`The url did not contain a code parameter ${url}`);

    if (params.state && params.state !== queryParams.get('state')) {
      throw new Error(`The "state" parameter in the url did not match the expected value of ${params.state}`);
    }

    return {
      code: queryParams.get('code')!,
      scope: queryParams.has('scope') ? queryParams.get('scope')!.split(' ') : undefined,
    };

  }


  /**
   * Receives an OAuth2 token using 'authorization_code' grant
   */
  async getToken(params: GetTokenParams): Promise<OAuth2Token> {

    const body:AuthorizationCodeRequest = {
      grant_type: 'authorization_code',
      code: params.code,
      redirect_uri: params.redirectUri,
      code_verifier: params.codeVerifier,
      resource: params.resource,
    };
    return this.client.tokenResponseToOAuth2Token(this.client.request('tokenEndpoint', body));

  }


}

export async function generateCodeVerifier(): Promise<string> {

  const webCrypto = await getWebCrypto();
  const arr = new Uint8Array(32);
  webCrypto.getRandomValues(arr);
  return base64Url(arr);

}

export async function getCodeChallenge(codeVerifier: string): Promise<['plain' | 'S256', string]> {

  const webCrypto = await getWebCrypto();
  return ['S256', base64Url(await webCrypto.subtle.digest('SHA-256', stringToBuffer(codeVerifier)))];
}

async function getWebCrypto(): Promise<typeof window.crypto> {

  // Browsers
  if ((typeof window !== 'undefined' && window.crypto)) {
    if (!window.crypto.subtle?.digest) {
      throw new Error(
        "The context/environment is not secure, and does not support the 'crypto.subtle' module. See: https://developer.mozilla.org/en-US/docs/Web/API/Crypto/subtle for details"
      );
    }
    return window.crypto;
  }
  // Web workers possibly
  if ((typeof self !== 'undefined' && self.crypto)) {
    return self.crypto;
  }
  // Node
  // eslint-disable-next-line @typescript-eslint/no-var-requires
  const crypto = await import('crypto');
  return crypto.webcrypto as typeof window.crypto;

}

function stringToBuffer(input: string): ArrayBuffer {

  const buf = new Uint8Array(input.length);
  for(let i=0; i<input.length;i++) {
    buf[i] = input.charCodeAt(i) & 0xFF;
  }
  return buf;

}

function base64Url(buf: ArrayBuffer) {
  return (
    btoa(String.fromCharCode(...new Uint8Array(buf)))
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+$/, '')
  );
}



================================================
FILE: src/client.ts
================================================
import type { OAuth2Token } from './token.ts';
import type {
  AuthorizationCodeRequest,
  ClientCredentialsRequest,
  IntrospectionRequest,
  IntrospectionResponse,
  PasswordRequest,
  OAuth2TokenTypeHint,
  RefreshRequest,
  RevocationRequest,
  ServerMetadataResponse,
  TokenResponse,
} from './messages.ts';
import { OAuth2HttpError } from './error.ts';
import { OAuth2AuthorizationCodeClient } from './client/authorization-code.ts';


type ClientCredentialsParams = {
  scope?: string[];
  extraParams?: Record<string, string>;

  /**
   * The resource  the client intends to access.
   *
   * @see https://datatracker.ietf.org/doc/html/rfc8707
   */
  resource?: string | string[];
}

type PasswordParams = {
  username: string;
  password: string;

  scope?: string[];

  /**
   * The resource  the client intends to access.
   *
   * @see https://datatracker.ietf.org/doc/html/rfc8707
   */
  resource?: string | string[];

}

/**
 * Extra options that may be passed to refresh()
 */
type RefreshParams = {
  scope?: string[];

  /**
   * The resource  the client intends to access.
   *
   * @see https://datatracker.ietf.org/doc/html/rfc8707
   */
  resource?: string | string[];

}

export interface ClientSettings {

  /**
   * The hostname of the OAuth2 server.
   * If provided, we'll attempt to discover all the other related endpoints.
   *
   * If this is not desired, just specify the other endpoints manually.
   *
   * This url will also be used as the base URL for all other urls. This lets
   * you specify all the other urls as relative.
   */
  server?: string;

  /**
   * OAuth2 clientId
   */
  clientId: string;

  /**
   * OAuth2 clientSecret
   *
   * This is required when using the 'client_secret_basic' authenticationMethod
   * for the client_credentials and password flows, but not authorization_code
   * or implicit.
   */
  clientSecret?: string;

  /**
   * The /authorize endpoint.
   *
   * Required only for the browser-portion of the authorization_code flow.
   */
  authorizationEndpoint?: string;

  /**
   * The token endpoint.
   *
   * Required for most grant types and refreshing tokens.
   */
  tokenEndpoint?: string;

  /**
   * Introspection endpoint.
   *
   * Required for, well, introspecting tokens.
   * If not provided we'll try to discover it, or otherwise default to /introspect
   */
  introspectionEndpoint?: string;

  /**
   * Revocation endpoint.
   *
   * Required for revoking tokens. Not supported by all servers.
   * If not provided we'll try to discover it, or otherwise default to /revoke
   */
  revocationEndpoint?: string;

  /**
   * OAuth 2.0 Authorization Server Metadata endpoint or OpenID
   * Connect Discovery 1.0 endpoint.
   *
   * If this endpoint is provided it can be used to automatically figure
   * out all the other endpoints.
   *
   * Usually the URL for this is: https://server/.well-known/oauth-authorization-server
   */
  discoveryEndpoint?: string;

  /**
   * Fetch implementation to use.
   *
   * Set this if you wish to explicitly set the fetch implementation, e.g. to
   * implement middlewares or set custom headers.
   */
  fetch?: typeof fetch;

  /**
   * Client authentication method that is used to authenticate
   * when using the token endpoint.
   *
   * When 'client_secret_basic' is used, the client_id and client_secret are
   * encoded in the Authorization header, as per RFC 6749 section 2.3.1. This
   * uses the official encoding, which also percent-encodes special characters.
   *
   * Many popular servers don't expect this, despite being the standard. So we
   * also support 'client_secret_basic_interop', which does not percent-encode
   * special characters except ":". This is 'interop' encoding is the default
   * for this library to maximize compatibility.
   *
   * In the future, we will switch this to 'client_secret_post', which has fewer
   * interopability issues. This setting causes the client to provide the
   * client_id and secret in the POST body.
   *
   * The default value is 'client_secret_basic' if not provided.
   */
  authenticationMethod?: 'client_secret_basic' | 'client_secret_post' | 'client_secret_basic_interop';

}


type OAuth2Endpoint = 'tokenEndpoint' | 'authorizationEndpoint' | 'discoveryEndpoint' | 'introspectionEndpoint' | 'revocationEndpoint';

export class OAuth2Client {

  settings: ClientSettings;

  constructor(clientSettings: ClientSettings) {

    if (!clientSettings?.fetch) {
      clientSettings.fetch = fetch.bind(globalThis);
    }
    this.settings = clientSettings;

  }

  /**
   * Refreshes an existing token, and returns a new one.
   */
  async refreshToken(token: OAuth2Token, params?: RefreshParams): Promise<OAuth2Token> {

    if (!token.refreshToken) {
      throw new Error('This token didn\'t have a refreshToken. It\'s not possible to refresh this');
    }

    const body: RefreshRequest = {
      grant_type: 'refresh_token',
      refresh_token: token.refreshToken,
    };
    if (!this.settings.clientSecret) {
      // If there's no secret, send the clientId in the body.
      body.client_id = this.settings.clientId;
    }

    if (params?.scope) body.scope = params.scope.join(' ');
    if (params?.resource) body.resource = params.resource;

    const newToken = await this.tokenResponseToOAuth2Token(this.request('tokenEndpoint', body));
    if (!newToken.refreshToken && token.refreshToken) {
      // Reuse old refresh token if we didn't get a new one.
      newToken.refreshToken = token.refreshToken;
    }
    return newToken;

  }

  /**
   * Retrieves an OAuth2 token using the client_credentials grant.
   */
  async clientCredentials(params?: ClientCredentialsParams): Promise<OAuth2Token> {

    const disallowed = ['client_id', 'client_secret', 'grant_type', 'scope'];

    if (params?.extraParams && Object.keys(params.extraParams).filter((key) => disallowed.includes(key)).length > 0) {
      throw new Error(`The following extraParams are disallowed: '${disallowed.join("', '")}'`);
    }

    const body: ClientCredentialsRequest = {
      grant_type: 'client_credentials',
      scope: params?.scope?.join(' '),
      resource: params?.resource,
      ...params?.extraParams
    };

    if (!this.settings.clientSecret) {
      throw new Error('A clientSecret must be provided to use client_credentials');
    }

    return this.tokenResponseToOAuth2Token(this.request('tokenEndpoint', body));

  }

  /**
   * Retrieves an OAuth2 token using the 'password' grant'.
   */
  async password(params: PasswordParams): Promise<OAuth2Token> {

    const body: PasswordRequest = {
      grant_type: 'password',
      ...params,
      scope: params.scope?.join(' '),
    };
    return this.tokenResponseToOAuth2Token(this.request('tokenEndpoint', body));

  }

  /**
   * Returns the helper object for the `authorization_code` grant.
   */
  get authorizationCode(): OAuth2AuthorizationCodeClient {

    return new OAuth2AuthorizationCodeClient(
      this,
    );

  }

  /**
   * Introspect a token
   *
   * This will give information about the validity, owner, which client
   * created the token and more.
   *
   * @see https://datatracker.ietf.org/doc/html/rfc7662
   */
  async introspect(token: OAuth2Token): Promise<IntrospectionResponse> {

    const body: IntrospectionRequest = {
      token: token.accessToken,
      token_type_hint: 'access_token',
    };
    return this.request('introspectionEndpoint', body);

  }

  /**
   * Revoke a token
   *
   * This will revoke a token, provided that the server supports this feature.
   *
   * @see https://datatracker.ietf.org/doc/html/rfc7009
   */
  async revoke(token: OAuth2Token, tokenTypeHint: OAuth2TokenTypeHint = 'access_token'): Promise<void> {
    let tokenValue = token.accessToken;
    if (tokenTypeHint === 'refresh_token') {
      tokenValue = token.refreshToken!;
    }

    const body: RevocationRequest = {
      token: tokenValue,
      token_type_hint: tokenTypeHint,
    };
    return this.request('revocationEndpoint', body);

  }

  /**
   * Returns a url for an OAuth2 endpoint.
   *
   * Potentially fetches a discovery document to get it.
   */
  async getEndpoint(endpoint: OAuth2Endpoint): Promise<string> {

    if (this.settings[endpoint] !== undefined) {
      return resolve(this.settings[endpoint] as string, this.settings.server);
    }

    if (endpoint !== 'discoveryEndpoint') {
      // This condition prevents infinite loops.
      await this.discover();
      if (this.settings[endpoint] !== undefined) {
        return resolve(this.settings[endpoint] as string, this.settings.server);
      }
    }

    // If we got here it means we need to 'guess' the endpoint.
    if (!this.settings.server) {
      throw new Error(`Could not determine the location of ${endpoint}. Either specify ${endpoint} in the settings, or the "server" endpoint to let the client discover it.`);
    }

    switch (endpoint) {
      case 'authorizationEndpoint':
        return resolve('/authorize', this.settings.server);
      case 'tokenEndpoint':
        return resolve('/token', this.settings.server);
      case 'discoveryEndpoint':
        return resolve('/.well-known/oauth-authorization-server', this.settings.server);
      case 'introspectionEndpoint':
        return resolve('/introspect', this.settings.server);
      case 'revocationEndpoint':
        return resolve('/revoke', this.settings.server);
    }

  }

  private discoveryPromise: Promise<void> | undefined;
  private serverMetadata: ServerMetadataResponse | null = null;

  private discover(): Promise<void> {
    // Never discover twice
    if (this.discoveryPromise === undefined) {
      this.discoveryPromise = this.doDiscover();
    }
    return this.discoveryPromise;
  }

  /**
   * Fetches the OAuth2 discovery document
   *
   * Should not call this directly, call `discover()` instead
   */
  private async doDiscover(): Promise<void> {

    let discoverUrl;
    try {
      discoverUrl = await this.getEndpoint('discoveryEndpoint');
    } catch (_err) {
      console.warn('[oauth2] OAuth2 discovery endpoint could not be determined. Either specify the "server" or "discoveryEndpoint');
      return;
    }
    const resp = await this.settings.fetch!(discoverUrl, { headers: { Accept: 'application/json' }});

    if (!resp.ok) return;
    if (!resp.headers.get('Content-Type')?.startsWith('application/json')) {
      console.warn('[oauth2] OAuth2 discovery endpoint was not a JSON response. Response is ignored');
      return;
    }
    this.serverMetadata = await resp.json();

    const urlMap = [
      ['authorization_endpoint', 'authorizationEndpoint'],
      ['token_endpoint', 'tokenEndpoint'],
      ['introspection_endpoint', 'introspectionEndpoint'],
      ['revocation_endpoint', 'revocationEndpoint'],
    ] as const;

    if (this.serverMetadata === null) return;

    for (const [property, setting] of urlMap) {
      if (!this.serverMetadata[property]) continue;
      this.settings[setting] = resolve(this.serverMetadata[property]!, discoverUrl);
    }

    if (
      this.serverMetadata.token_endpoint_auth_methods_supported
      && !this.settings.authenticationMethod
    ) {
      for(const method of this.serverMetadata.token_endpoint_auth_methods_supported) {
        if (method === 'client_secret_basic' || method === 'client_secret_post') {
          this.settings.authenticationMethod = method;
          break;
        }
      }
    }

  }

  /**
   * Does a HTTP request on the 'token' endpoint.
   */
  async request(endpoint: 'tokenEndpoint', body: RefreshRequest | ClientCredentialsRequest | PasswordRequest | AuthorizationCodeRequest): Promise<TokenResponse>;
  async request(endpoint: 'introspectionEndpoint', body: IntrospectionRequest): Promise<IntrospectionResponse>;
  async request(endpoint: 'revocationEndpoint', body: RevocationRequest): Promise<void>;
  async request(endpoint: OAuth2Endpoint, body: Record<string, any>): Promise<unknown> {

    const uri = await this.getEndpoint(endpoint);

    const headers: Record<string, string> = {
      'Content-Type': 'application/x-www-form-urlencoded',
      // Although it shouldn't be needed, Github OAUth2 will return JSON
      // unless this is set.
      'Accept': 'application/json',
    };

    let authMethod = this.settings.authenticationMethod;

    if (!this.settings.clientSecret) {
      // Basic auth should only be used when there's a client_secret, for
      // non-confidential clients we may only have a client_id, which
      // always gets added to the body.
      authMethod = 'client_secret_post';
    }
    if (!authMethod) {
      // If we got here, it means no preference was provided by anything,
      // and we have a secret. In this case its preferred to embed
      // authentication in the Authorization header.
      authMethod = 'client_secret_basic_interop';
    }

    switch(authMethod) {
      case 'client_secret_basic' :
        // Per RFC 6749 section 2.3.1, the client_id and client_secret need
        // to be encoded using application/x-www-form-urlencoded for the
        // basic auth.
        headers.Authorization = 'Basic ' +
          btoa(legacyFormUrlEncode(this.settings.clientId) + ':' + legacyFormUrlEncode(this.settings.clientSecret!));
        break;
      case 'client_secret_basic_interop' :
        // A more relaxed encoding that's more compatible with popular servers.
        headers.Authorization = 'Basic ' +
          btoa(this.settings.clientId.replace(/:/g, '%3A') + ':' + this.settings.clientSecret!.replace(/:/g, '%3A'));
        break;
      case 'client_secret_post' :
        body.client_id = this.settings.clientId;
        if (this.settings.clientSecret) {
          body.client_secret = this.settings.clientSecret;
        }
        break;
      default:
        throw new Error('Authentication method not yet supported:' + authMethod + '. Open a feature request if you want this!');
    }

    const resp = await this.settings.fetch!(uri, {
      method: 'POST',
      body: generateQueryString(body),
      headers,
    });

    let responseBody;
    if (resp.status !== 204 && resp.headers.has('Content-Type') && resp.headers.get('Content-Type')!.match(/^application\/(.*\+)?json/)) {
      responseBody = await resp.json();
    }
    if (resp.ok) {
      return responseBody;
    }

    let errorMessage;
    let oauth2Code;

    if (responseBody?.error) {
      // This is likely an OAUth2-formatted error
      errorMessage = 'OAuth2 error ' + responseBody.error + '.';
      if (responseBody.error_description) {
        errorMessage += ' ' + responseBody.error_description;
      }
      oauth2Code = responseBody.error;

    } else {
      errorMessage = 'HTTP Error ' + resp.status + ' ' + resp.statusText;
      if (resp.status === 401 && this.settings.clientSecret) {
        errorMessage += '. It\'s likely that the clientId and/or clientSecret was incorrect';
      }
      oauth2Code = null;
    }
    throw new OAuth2HttpError(errorMessage, oauth2Code, resp, responseBody);
  }

  /**
   * Converts the JSON response body from the token endpoint to an OAuth2Token type.
   */
  async tokenResponseToOAuth2Token(resp: Promise<TokenResponse>): Promise<OAuth2Token> {

    const body = await resp;

    if (!body?.access_token) {
      console.warn('Invalid OAuth2 Token Response: ', body);
      throw new TypeError('We received an invalid token response from an OAuth2 server.');
    }

    const {
      access_token,
      refresh_token,
      expires_in,
      id_token,
      scope,
      token_type,
      ...extraParams
    } = body;

    const result: OAuth2Token = {
      accessToken: access_token,
      expiresAt: expires_in ? Date.now() + (expires_in * 1000) : null,
      refreshToken: refresh_token ?? null,
    };
    if (id_token) {
      result.idToken = id_token;
    }
    if (scope) {
      result.scope = scope.split(' ');
    }
    if (Object.keys(extraParams).length > 0) {
      result.extraParams = extraParams;
    }
    return result;

  }

}

function resolve(uri: string, base?: string): string {

  return new URL(uri, base).toString();

}

/**
 * Generates a query string.
 *
 * If a value is undefined, it will be ignored.
 * If a value is an array, it will add the parameter multiple times for each array value.
 */
export function generateQueryString(params: Record<string, undefined | number | string | string[]>): string {

  const query = new URLSearchParams();
  for (const [k, v] of Object.entries(params)) {
    if (Array.isArray(v)) {
      for(const vItem of v) query.append(k, vItem);
    } else if (v !== undefined) query.set(k, v.toString());
  }
  return query.toString();

}

/**
 * Encodes string according to the most strict interpretation of RFC 6749 Appendix B.
 *
 * All non-alphanumeric characters are percent encoded, with exception of space which
 * is replaced with '+'.
 */
export function legacyFormUrlEncode(value: string): string {
  return encodeURIComponent(value)
    .replace(/%20/g, '+')
    .replace(/[-_.!~*'()]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
}


================================================
FILE: src/error.ts
================================================
import { type OAuth2ErrorCode } from './messages.ts';

/**
 * An error class for any error the server emits.
 *
 * The 'oauth2Code' property will have the oauth2 error type,
 * such as:
 * - invalid_request
 * - invalid_client
 * - invalid_grant
 * - unauthorized_client
 * - unsupported_grant_type
 * - invalid_scope
 */
export class OAuth2Error extends Error {

  oauth2Code: OAuth2ErrorCode|string;

  constructor(message: OAuth2ErrorCode|string, oauth2Code: OAuth2ErrorCode) {

    super(message);
    this.oauth2Code = oauth2Code;

  }

}

/**
 * A OAuth2 error that was emitted as a HTTP error
 *
 * The 'code' property will have the oauth2 error type,
 * such as:
 * - invalid_request
 * - invalid_client
 * - invalid_grant
 * - unauthorized_client
 * - unsupported_grant_type
 * - invalid_scope
 *
 * This Error also gives you access to the HTTP status code and response body.
 */
export class OAuth2HttpError extends OAuth2Error {

  httpCode: number;

  response: Response;
  parsedBody: Record<string, any>;

  constructor(message: string, oauth2Code: OAuth2ErrorCode, response: Response, parsedBody: Record<string, any>) {

    super(message, oauth2Code);

    this.httpCode = response.status;
    this.response = response;
    this.parsedBody = parsedBody;

  }

}


================================================
FILE: src/fetch-wrapper.ts
================================================
import type { OAuth2Token } from './token.ts';
import { OAuth2Client } from './client.ts';

type FetchMiddleware = (request: Request, next: (request: Request) => Promise<Response>) => Promise<Response>;

type OAuth2FetchOptions = {

  /**
   * Reference to OAuth2 client.
   */
  client: OAuth2Client;

  /**
   * You are responsible for implementing this function.
   * it's purpose is to supply the 'initial' oauth2 token.
   *
   * This function may be async. Return `null` to fail the process.
   */
  getNewToken(): OAuth2Token | null | Promise<OAuth2Token | null>;

  /**
   * If set, will be called if authentication fatally failed.
   */
  onError?: (err: Error) => void;

  /**
   * This function is called whenever the active token changes. Using this is
   * optional, but it may be used to (for example) put the token in off-line
   * storage for later usage.
   */
  storeToken?: (token: OAuth2Token) => void;

  /**
   * Also an optional feature. Implement this if you want the wrapper to try a
   * stored token before attempting a full re-authentication.
   *
   * This function may be async. Return null if there was no token.
   */
  getStoredToken?: () => OAuth2Token | null | Promise<OAuth2Token | null>;

  /**
   * Whether to automatically schedule token refresh.
   *
   * Certain execution environments, e.g. React Native, do not handle scheduled
   * tasks with setTimeout() in a graceful or predictable fashion. The default
   * behavior is to schedule refresh. Set this to false to disable scheduling.
   */
  scheduleRefresh?: boolean;

}

export class OAuth2Fetch {

  private options: OAuth2FetchOptions;

  /**
   * Current active token (if any)
   */
  private token: OAuth2Token | null = null;

  /**
   * If the user had a storedToken, the process to fetch it
   * may be async. We keep track of this process in this
   * promise, so it may be awaited to avoid race conditions.
   *
   * As soon as this promise resolves, this property gets nulled.
   */
  private activeGetStoredToken: null | Promise<void> = null;

  constructor(options: OAuth2FetchOptions) {

    if (options?.scheduleRefresh === undefined) {
      options.scheduleRefresh = true;
    }
    this.options = options;
    if (options.getStoredToken) {
      this.activeGetStoredToken = (async () => {
        this.token = await options.getStoredToken!();
        this.activeGetStoredToken = null;
      })();
    }
    this.scheduleRefresh();

  }

  /**
   * Does a fetch request and adds a Bearer / access token.
   *
   * If the access token is not known, this function attempts to fetch it
   * first. If the access token is almost expiring, this function might attempt
   * to refresh it.
   */
  async fetch(input: RequestInfo, init?: RequestInit): Promise<Response> {

    // input might be a string or a Request object, we want to make sure this
    // is always a fully-formed Request object.
    const request = new Request(input, init);

    return this.mw()(
      request,
      req => fetch(req)
    );

  }

  /**
   * This function allows the fetch-mw to be called as more traditional
   * middleware.
   *
   * This function returns a middleware function with the signature
   *    (request, next): Response
   */
  mw(): FetchMiddleware {

    return async (request, next) => {

      const accessToken = await this.getAccessToken();

      // Make a clone. We need to clone if we need to retry the request later.
      let authenticatedRequest = request.clone();
      authenticatedRequest.headers.set('Authorization', 'Bearer '  + accessToken);
      let response = await next(authenticatedRequest);

      if (!response.ok && response.status === 401) {

        const newToken = await this.refreshToken();

        authenticatedRequest = request.clone();
        authenticatedRequest.headers.set('Authorization', 'Bearer '  + newToken.accessToken);
        response = await next(authenticatedRequest);

      }
      return response;
    };

  }

  /**
   * Returns current token information.
   *
   * There result object will have:
   *   * accessToken
   *   * expiresAt - when the token expires, or null.
   *   * refreshToken - may be null
   *
   * This function will attempt to automatically refresh if stale.
   */
  async getToken(): Promise<OAuth2Token> {

    if (this.token && (this.token.expiresAt === null || this.token.expiresAt > Date.now())) {

      // The current token is still valid
      return this.token;

    }

    return this.refreshToken();

  }

  /**
   * Returns an access token.
   *
   * If the current access token is not known, it will attempt to fetch it.
   * If the access token is expiring, it will attempt to refresh it.
   */
  async getAccessToken(): Promise<string> {

    // Ensure getStoredToken finished.
    await this.activeGetStoredToken;

    const token = await this.getToken();
    return token.accessToken;

  }

  /**
   * Keeping track of an active refreshToken operation.
   *
   * This will allow us to ensure only 1 such operation happens at any
   * given time.
   */
  private activeRefresh: Promise<OAuth2Token> | null = null;

  /**
   * Forces an access token refresh
   */
  async refreshToken(): Promise<OAuth2Token> {

    if (this.activeRefresh) {
      // If we are currently already doing this operation,
      // make sure we don't do it twice in parallel.
      return this.activeRefresh;
    }

    const oldToken = this.token;
    this.activeRefresh = (async() => {

      let newToken: OAuth2Token|null = null;

      try {
        if (oldToken?.refreshToken) {
          // We had a refresh token, lets see if we can use it!
          newToken = await this.options.client.refreshToken(oldToken);
        }
      } catch (_err) {
        console.warn('[oauth2] refresh token not accepted, we\'ll try reauthenticating');
      }

      if (!newToken) {
        newToken = await this.options.getNewToken();
      }

      if (!newToken) {
        const err = new Error('Unable to obtain OAuth2 tokens, a full reauth may be needed');
        this.options.onError?.(err);
        throw err;
      }
      return newToken;

    })();

    try {
      const token = await this.activeRefresh;
      this.token = token;
      this.options.storeToken?.(token);
      this.scheduleRefresh();
      return token;
    } catch (err: any) {
      if (this.options.onError) {
        this.options.onError(err);
      }
      throw err;
    } finally {
      // Make sure we clear the current refresh operation.
      this.activeRefresh = null;
    }

  }

  /**
   * Timer trigger for the next automated refresh
   */
  private refreshTimer: ReturnType<typeof setTimeout> | null = null;

  private scheduleRefresh() {
    if (!this.options.scheduleRefresh) {
      return;
    }
    if (this.refreshTimer) {
      clearTimeout(this.refreshTimer);
      this.refreshTimer = null;
    }

    if (!this.token?.expiresAt || !this.token.refreshToken) {
      // If we don't know when the token expires, or don't have a refresh_token, don't bother.
      return;
    }

    const expiresIn = this.token.expiresAt - Date.now();

    // We only schedule this event if it happens more than 2 minutes in the future.
    if (expiresIn < 120*1000) {
      return;
    }

    // Schedule 1 minute before expiry
    this.refreshTimer = setTimeout(async () => {
      try {
        await this.refreshToken();
      } catch (err) {
        // eslint-disable-next-line no-console
        console.error('[fetch-mw-oauth2] error while doing a background OAuth2 auto-refresh', err);
      }
    }, expiresIn - 60*1000);

  }

}


================================================
FILE: src/index.ts
================================================
export { OAuth2Client } from './client.ts';
export { OAuth2AuthorizationCodeClient, generateCodeVerifier } from './client/authorization-code.ts';
export { OAuth2Fetch } from './fetch-wrapper.ts';
export { OAuth2Error, OAuth2HttpError } from './error.ts';

export type { IntrospectionResponse } from './messages.ts';
export type { OAuth2Token } from './token.ts';


================================================
FILE: src/messages.ts
================================================
/**
 * refresh_token request body
 */
export type RefreshRequest = {
  grant_type: 'refresh_token';
  refresh_token: string;

  client_id?: string;
  scope?: string;

  /**
   * The resource  the client intends to access.
   *
   * @see https://datatracker.ietf.org/doc/html/rfc8707
   */
  resource?: string | string[];
}

/**
 * client_credentials request body
 */
export type ClientCredentialsRequest = {
  grant_type: 'client_credentials';
  scope?: string;

  /**
   * The resource  the client intends to access.
   *
   * @see https://datatracker.ietf.org/doc/html/rfc8707
   */
  resource?: string | string[];

  [key: string]: string | undefined | string[];
}

/**
 * password grant_type request body
 */
export type PasswordRequest = {
  grant_type: 'password';
  username: string;
  password: string;
  scope?: string;

  /**
   * The resource  the client intends to access.
   *
   * @see https://datatracker.ietf.org/doc/html/rfc8707
   */
  resource?: string | string[];
}

export type AuthorizationCodeRequest = {
  grant_type: 'authorization_code';
  code: string;
  redirect_uri: string;
  code_verifier: string|undefined;

  /**
   * The resource  the client intends to access.
   *
   * @see https://datatracker.ietf.org/doc/html/rfc8707
   */
  resource?: string | string[];
}

/**
 * Response from the /token endpoint
 */
export type TokenResponse = {
  /**
   * The OAuth 2 access token.
   */
  access_token: string;

  /**
   * The type of token, which is always "Bearer".
   */
  token_type: string;

  /**
   * The lifetime in seconds of the access token.
   */
  expires_in?: number;

  /**
   * The refresh token, which can be used to get a new access token after the current one expires.
   */
  refresh_token?: string;

  /**
   * List of comma-separated scopes that the access token is valid for.
   */
  scope?: string;

  /**
   * The OpenID Connect id_token, which is a JWT encoded value containing
   * information about the authenticated user.
   */
  id_token?: string;
}

type OAuth2ResponseType = 'code' | 'token';
type OAuth2ResponseMode = 'query' | 'fragment';
type OAuth2GrantType = 'authorization_code' | 'implicit' | 'password' | 'client_credentials' | 'refresh_token' | 'urn:ietf:params:oauth:grant-type:jwt-bearer' | 'urn:ietf:params:oauth:grant-type:saml2-bearer';
type OAuth2AuthMethod = 'none' | 'client_secret_basic' | 'client_secret_post' | 'client_secret_jwt' | 'private_key_jwt' | 'tls_client_auth' | 'self_signed_tls_client_auth';
type OAuth2CodeChallengeMethod = 'S256' | 'plain';

export type OAuth2TokenTypeHint = 'access_token' | 'refresh_token';

/**
 * Response from /.well-known/oauth-authorization-server
 *
 * https://datatracker.ietf.org/doc/html/rfc8414
 */
export type ServerMetadataResponse = {

  /**
   * The authorization server's issuer identifier, which is a URL that uses
   * the "https" scheme and has no query or fragment.
   */
  issuer: string;

  /**
   * URL of the authorization server's authorization endpoint.
   */
  authorization_endpoint:string;

  /**
   * URL of the authorization server's token endpoint.
   */
  token_endpoint: string;

  /**
   * URL of the authorization server's JWK Set document
   */
  jwks_uri?: string;

  /**
   * URL of the authorization server's OAuth 2.0 Dynamic Client Registration
   * endpoint.
   */
  registration_endpoint?: string;

  /**
   * List of supported scopes for this server
   */
  scopes_supported?: string[];

  /**
   * List of supported response types for the authorization endpoint.
   *
   * If 'code' appears here it implies authorization_code support,
   * 'token' implies support for implicit auth.
   */
  response_types_supported: OAuth2ResponseType[];

  /**
   * JSON array containing a list of the OAuth 2.0 "response_mode"
   * values that this authorization server supports
   */
  response_modes_supported?: OAuth2ResponseMode[];

  /**
   * List of supported grant types by the server
   */
  grant_types_supported?: OAuth2GrantType[];

  /**
   * Supported auth methods on the token endpoint.
   */
  token_endpoint_auth_methods_supported?: OAuth2AuthMethod[];

  /**
   * JSON array containing a list of the JWS signing algorithms.
   */
  token_endpoint_auth_signing_alg_values_supported?: string[];

  /**
   * URL of a page containing human-readable information that developers might want or need to know when using the authorization server.
   */
  service_documentation?: string;

  /**
   * List of supported languages for the UI
   */
  ui_locales_supported?: string[];

  /**
   * URL that the authorization server provides to the person registering the
   * client to read about the authorization server's requirements on how the
   * client can use the data provided by the authorization server.
   */
  op_policy_uri?: string;

  /**
   * Link to terms of service
   */
  op_tos_uri?: string;

  /**
   * Url to servers revocation endpoint.
   */
  revocation_endpoint?: string;

  /**
   * Auth method that may be used on the revocation endpoint.
   */
  revocation_endpoint_auth_methods_supported?: OAuth2AuthMethod[];

  /**
   * JSON array containing a list of the JWS signing algorithms ("alg" values)
   * supported by the revocation endpoint.
   */
  revocation_endpoint_auth_signing_alg_values_supported?: string[];

  /**
   * Url to introspection endpoint
   */
  introspection_endpoint?: string;

  /**
   * List of authentication methods supported on the introspection endpoint.
   */
  introspection_endpoint_auth_methods_supported?: OAuth2AuthMethod[];

  /**
   * List of JWS signing algorithms supported on the introspection endpoint.
   */
  introspection_endpoint_auth_signing_alg_values_supported?: string[];

  /**
   * List of support PCKE code challenge methods.
   */
  code_challenge_methods_supported?: OAuth2CodeChallengeMethod[];

}

export type IntrospectionRequest = {
  token: string;
  token_type_hint?: OAuth2TokenTypeHint;
};


export type IntrospectionResponse = {

  /**
   * Whether or not the token is still active.
   */
  active: boolean;

  /**
   * Space-separated list of scopes.
   */
  scope?: string;

  /**
   * client_id that requested the token.
   */
  client_id?: string;

  /**
   * Human-readable string of the resource-owner that requested the token.
   */
  username?: string;

  /**
   * Type of token
   */
  token_type?: string;

  /**
   * Unix timestamp of when this token expires.
   */
  exp?: number;

  /**
   * Unix timestamp of when the token was issued.
   */
  iat?: number;

  /**
   * Unix timestamp indicating when the token should not be used before.
   */
  nbf?: number;

  /**
   * Subject of the token. Usually a machine-readable identifier of the
   * resource owner/user.
   */
  sub?: string;

  /**
   * String representing the audience of the token.
   */
  aud?: string;

  /**
   * Issuer of the token.
   */
  iss?: string;

  /**
   * String identifier of the token.
   */
  jti?: string;

}

/**
 * Revocaton request.
 *
 * https://datatracker.ietf.org/doc/html/rfc7009#section-2.1
 */
export type RevocationRequest = {
  token: string;
  token_type_hint?: OAuth2TokenTypeHint;
}

export type OAuth2ErrorCode =
  | 'invalid_request'
  | 'invalid_client'
  | 'invalid_grant'
  | 'unauthorized_client'
  | 'unsupported_grant_type'
  | 'invalid_scope'

  /**
   * RFC 8707
   */
  | 'invalid_target';


================================================
FILE: src/token.ts
================================================
/**
 * Token information
 */
export type OAuth2Token = {

  /**
   * OAuth2 Access Token
   */
  accessToken: string;

  /**
   * When the Access Token expires.
   *
   * This is expressed as a unix timestamp in milliseconds.
   */
  expiresAt: number | null;

  /**
   * OAuth2 refresh token
   */
  refreshToken: string | null;

  /**
   * OpenID Connect ID Token
   */
  idToken?: string;

  /**
   * List of scopes that the access token is valid for.
   * (May be omitted if identical to the requested scope)
   */
  scope?: string[];

  /**
   * Additional tokens properties returned by the OAuth2 server.
   */
  extraParams?: Record<string, any>;
};


================================================
FILE: test/authorization-code.ts
================================================
import * as assert from 'node:assert';
import { testServer } from './test-server.ts';
import { OAuth2Client } from '../src/index.ts';
import { after, describe, it } from 'node:test';

// Example directly taken from https://datatracker.ietf.org/doc/html/rfc7636
const codeVerifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk';
const codeChallenge = 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM';

describe('authorization-code', () => {
  let server: ReturnType<typeof testServer>;

  after(async () => {
    if (server) {
      await server.close();
    }
  });

  describe('Authorization endpoint redirect', () => {
    it('should generate correct urls for the authorization endpoint', async () => {
      server = testServer();
      const client = new OAuth2Client({
        server: server.url,
        authorizationEndpoint: '/authorize',
        clientId: 'test-client-id',
      });

      const redirectUri = 'http://my-app.example/redirect';

      const params = new URLSearchParams({
        client_id: 'test-client-id',
        response_type: 'code',
        redirect_uri: redirectUri,
        scope: 'a b',
      });

      assert.equal(
        await client.authorizationCode.getAuthorizeUri({
          redirectUri,
          scope: ['a', 'b'],
        }),
        server.url + '/authorize?' + params.toString()
      );
    });
    it('should support extraparams', async () => {
      server = testServer();
      const client = new OAuth2Client({
        server: server.url,
        authorizationEndpoint: '/authorize',
        clientId: 'test-client-id',
      });

      const redirectUri = 'http://my-app.example/redirect';

      const params = new URLSearchParams({
        client_id: 'test-client-id',
        response_type: 'code',
        redirect_uri: redirectUri,
        scope: 'a b',
        foo: 'bar',
      });

      assert.equal(
        await client.authorizationCode.getAuthorizeUri({
          redirectUri,
          scope: ['a', 'b'],
          extraParams: {
            foo: 'bar',
          },
        }),
        server.url + '/authorize?' + params.toString()
      );
    });
    it('should throw error when user rewrote params by extraparams', async () => {
      server = testServer();
      const client = new OAuth2Client({
        server: server.url,
        authorizationEndpoint: '/authorize',
        clientId: 'test-client-id',
      });

      const redirectUri = 'http://my-app.example/redirect';

      const params = {
        redirectUri,
        scope: ['a', 'b'],
        state: 'some-state',
      };

      const extraParams = {
        foo: 'bar',
        scope: 'accidentally rewrote core parameter',
      };

      try {
        await client.authorizationCode.getAuthorizeUri({
          ...params,
          extraParams,
        });
      } catch (error: any) {
        assert.match(error.message, /Property in extraParams/);
        return;
      }

      assert.fail('Should have thrown');
    });
    it('should support PKCE', async () => {
      server = testServer();
      const client = new OAuth2Client({
        server: server.url,
        authorizationEndpoint: '/authorize',
        clientId: 'test-client-id',
      });

      const redirectUri = 'http://my-app.example/redirect';

      const params = new URLSearchParams({
        client_id: 'test-client-id',
        response_type: 'code',
        redirect_uri: redirectUri,
        code_challenge_method: 'S256',
        code_challenge: codeChallenge,
      });

      assert.equal(
        await client.authorizationCode.getAuthorizeUri({
          redirectUri,
          codeVerifier,
        }),
        server.url + '/authorize?' + params.toString()
      );
    });
    it('should support the resource parameter', async () => {
      server = testServer();
      const client = new OAuth2Client({
        server: server.url,
        authorizationEndpoint: '/authorize',
        clientId: 'test-client-id',
      });

      const redirectUri = 'http://my-app.example/redirect';
      const resource = ['https://example/foo1', 'https://example/foo2'];
      const params = new URLSearchParams({
        client_id: 'test-client-id',
        response_type: 'code',
        redirect_uri: redirectUri,
      });
      for (const r of resource) params.append('resource', r);

      assert.equal(
        await client.authorizationCode.getAuthorizeUri({
          redirectUri,
          resource,
        }),
        server.url + '/authorize?' + params.toString()
      );
    });
  });

  describe('Token endpoint calls', () => {
    it('should send requests to the token endpoint', async () => {
      server = testServer();

      const client = new OAuth2Client({
        server: server.url,
        tokenEndpoint: '/token',
        clientId: 'test-client-id',
      });

      const result = await client.authorizationCode.getToken({
        code: 'code_000',
        redirectUri: 'http://example/redirect',
      });

      assert.equal(result.accessToken, 'access_token_000');
      assert.equal(result.refreshToken, 'refresh_token_000');
      assert.deepEqual(result.scope, ['foo', 'bar']);
      assert.deepEqual(result.extraParams, {
        foo: 'bar',
      });
      assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);
      assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);

      const request = server.lastRequest();
      assert.equal(request.headers.get('Authorization'), null);
      assert.equal(request.headers.get('Accept'), 'application/json');

      assert.deepEqual(request.body, {
        client_id: 'test-client-id',
        grant_type: 'authorization_code',
        code: 'code_000',
        redirect_uri: 'http://example/redirect',
      });
    });

    it('should send client_id and client_secret in the Authorization header if secret was specified', async () => {
      server = testServer();

      const client = new OAuth2Client({
        server: server.url,
        tokenEndpoint: '/token',
        clientId: 'testClientId',
        clientSecret: 'testClientSecret',
      });

      const result = await client.authorizationCode.getToken({
        code: 'code_000',
        redirectUri: 'http://example/redirect',
      });

      assert.equal(result.accessToken, 'access_token_000');
      assert.equal(result.refreshToken, 'refresh_token_000');
      assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);
      assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);

      const request = server.lastRequest();
      assert.equal(
        request.headers.get('Authorization'),
        'Basic ' + btoa('testClientId:testClientSecret')
      );
      assert.equal(request.headers.get('Accept'), 'application/json');

      assert.deepEqual(request.body, {
        grant_type: 'authorization_code',
        code: 'code_000',
        redirect_uri: 'http://example/redirect',
      });
    });

    it('should should support PKCE', async () => {
      server = testServer();

      const client = new OAuth2Client({
        server: server.url,
        tokenEndpoint: '/token',
        clientId: 'test-client-id',
      });

      const result = await client.authorizationCode.getToken({
        code: 'code_000',
        redirectUri: 'http://example/redirect',
        codeVerifier,
      });

      assert.equal(result.accessToken, 'access_token_000');
      assert.equal(result.refreshToken, 'refresh_token_000');
      assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);
      assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);

      const request = server.lastRequest();
      assert.equal(request.headers.get('Authorization'), null);
      assert.equal(request.headers.get('Accept'), 'application/json');

      assert.deepEqual(request.body, {
        client_id: 'test-client-id',
        grant_type: 'authorization_code',
        code: 'code_000',
        code_verifier: codeVerifier,
        redirect_uri: 'http://example/redirect',
      });
    });
    it('should not use Basic Auth if no secret is provided, even if client_secret_basic is set.', async () => {
      server = testServer();

      const client = new OAuth2Client({
        server: server.url,
        tokenEndpoint: '/token',
        clientId: 'test-client-id',
        authenticationMethod: 'client_secret_basic',
      });

      const result = await client.authorizationCode.getToken({
        code: 'code_000',
        redirectUri: 'http://example/redirect',
      });

      assert.equal(result.accessToken, 'access_token_000');
      assert.equal(result.refreshToken, 'refresh_token_000');
      assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);
      assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);

      const request = server.lastRequest();
      assert.equal(request.headers.get('Authorization'), null);
      assert.equal(request.headers.get('Accept'), 'application/json');

      assert.deepEqual(request.body, {
        client_id: 'test-client-id',
        grant_type: 'authorization_code',
        code: 'code_000',
        redirect_uri: 'http://example/redirect',
      });
    });

    it('should support the resource parameter', async () => {
      server = testServer();

      const client = new OAuth2Client({
        server: server.url,
        tokenEndpoint: '/token',
        clientId: 'test-client-id',
      });
      const resource = ['https://example/foo1', 'https://example/foo2'];

      const result = await client.authorizationCode.getToken({
        code: 'code_000',
        redirectUri: 'http://example/redirect',
        resource,
      });

      assert.equal(result.accessToken, 'access_token_000');
      assert.equal(result.refreshToken, 'refresh_token_000');
      assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);
      assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);

      const request = server.lastRequest();
      assert.equal(request.headers.get('Authorization'), null);
      assert.equal(request.headers.get('Accept'), 'application/json');

      assert.deepEqual(request.body, {
        client_id: 'test-client-id',
        grant_type: 'authorization_code',
        code: 'code_000',
        redirect_uri: 'http://example/redirect',
        resource,
      });
    });
  });

  describe('validateResponse', () => {
    const client = new OAuth2Client({
      server: 'http://foo/',
      tokenEndpoint: '/token',
      clientId: 'test-client-id',
    });

    it('should correctly parse a valid URI from a OAUth2 server redirect', () => {
      assert.deepEqual(
        client.authorizationCode.validateResponse(
          'https://example/?code=123&scope=scope1%20scope2',
          {}
        ),
        {
          code: '123',
          scope: ['scope1', 'scope2'],
        }
      );
    });
    it('should work when paramaters are set into the fragment', () => {
      assert.deepEqual(
        client.authorizationCode.validateResponse(
          'https://example/#code=123&scope=scope1%20scope2',
          {}
        ),
        {
          code: '123',
          scope: ['scope1', 'scope2'],
        }
      );
    });
    it('should validate the state parameter', () => {
      assert.deepEqual(
        client.authorizationCode.validateResponse(
          'https://example/?code=123&scope=scope1%20scope2&state=my-state',
          { state: 'my-state' }
        ),
        {
          code: '123',
          scope: ['scope1', 'scope2'],
        }
      );
    });
    it('should error if the state did not match', () => {
      let caught = false;
      try {
        client.authorizationCode.validateResponse(
          'https://example/?code=123&scope=scope1%20scope2',
          { state: 'my-state' }
        );
      } catch (_err) {
        caught = true;
      }
      assert.equal(caught, true);
    });
  });
});


================================================
FILE: test/client-credentials.ts
================================================
import * as assert from 'node:assert';
import { testServer } from './test-server.ts';
import { OAuth2Client, OAuth2HttpError } from '../src/index.ts';
import { after, describe, it } from 'node:test';

describe('client-credentials', () => {
  let server: ReturnType<typeof testServer>;

  after(async () => {
    if (server) {
      await server.close();
    }

  });
  it('should work with client_secret_basic', async () => {
    server = testServer();

    const client = new OAuth2Client({
      server: server.url,
      tokenEndpoint: '/token',
      clientId: 'testClientId:10',
      clientSecret: 'test=client=secret',
      authenticationMethod: 'client_secret_basic',
    });

    const result = await client.clientCredentials();

    assert.equal(result.accessToken, 'access_token_000');
    assert.equal(result.refreshToken, 'refresh_token_000');
    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);
    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);

    const request = server.lastRequest();
    assert.equal(
      request.headers.get('Authorization'),
      'Basic ' + btoa('testClientId%3A10:test%3Dclient%3Dsecret')
    );

    assert.deepEqual(request.body, {
      grant_type: 'client_credentials',
    });
  });
  it('should apply "interop" encoding when using client_secret_basic_interop', async () => {
    server = testServer();

    const client = new OAuth2Client({
      server: server.url,
      tokenEndpoint: '/token',
      clientId: 'testClientId:10',
      clientSecret: 'test=client=secret',
      authenticationMethod: 'client_secret_basic_interop',
    });

    const result = await client.clientCredentials();

    assert.equal(result.accessToken, 'access_token_000');
    assert.equal(result.refreshToken, 'refresh_token_000');
    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);
    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);

    const request = server.lastRequest();
    assert.equal(
      request.headers.get('Authorization'),
      'Basic ' + btoa('testClientId%3A10:test=client=secret')
    );

    assert.deepEqual(request.body, {
      grant_type: 'client_credentials',
    });
  });
  it('should apply "interop" encoding by default when no authenticationMethod is provided', async () => {
    server = testServer();

    const client = new OAuth2Client({
      server: server.url,
      tokenEndpoint: '/token',
      clientId: 'testClientId:10',
      clientSecret: 'test=client=secret',
    });

    const result = await client.clientCredentials();

    assert.equal(result.accessToken, 'access_token_000');
    assert.equal(result.refreshToken, 'refresh_token_000');
    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);
    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);

    const request = server.lastRequest();
    assert.equal(
      request.headers.get('Authorization'),
      'Basic ' + btoa('testClientId%3A10:test=client=secret')
    );

    assert.deepEqual(request.body, {
      grant_type: 'client_credentials',
    });
  });
  it('should support extra parameters', async () => {
    server = testServer();

    const client = new OAuth2Client({
      server: server.url,
      tokenEndpoint: '/token',
      clientId: 'testClientId',
      clientSecret: 'testClientSecret',
    });

    const result = await client.clientCredentials({
      extraParams: {
        foo: 'bar',
      },
    });

    assert.equal(result.accessToken, 'access_token_000');
    assert.equal(result.refreshToken, 'refresh_token_000');
    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);
    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);

    const request = server.lastRequest();
    assert.equal(
      request.headers.get('Authorization'),
      'Basic ' + btoa('testClientId:testClientSecret')
    );
    assert.equal(request.headers.get('Accept'), 'application/json');

    assert.deepEqual(request.body, {
      grant_type: 'client_credentials',
      foo: 'bar',
    });
  });

  it('should work with client_secret_post', async () => {
    server = testServer();

    const client = new OAuth2Client({
      server: server.url,
      tokenEndpoint: '/token',
      clientId: 'testClientId',
      clientSecret: 'testClientSecret',
      authenticationMethod: 'client_secret_post',
    });

    const result = await client.clientCredentials();

    assert.equal(result.accessToken, 'access_token_000');
    assert.equal(result.refreshToken, 'refresh_token_000');
    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);
    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);

    const request = server.lastRequest();

    assert.deepEqual(request.body, {
      client_id: 'testClientId',
      client_secret: 'testClientSecret',
      grant_type: 'client_credentials',
    });
  });
  it('should support the resource parameter', async () => {
    server = testServer();

    const client = new OAuth2Client({
      server: server.url,
      tokenEndpoint: '/token',
      clientId: 'testClientId',
      clientSecret: 'testClientSecret',
    });

    const resource = ['https://example/resource1', 'https://example/resource2'];

    const result = await client.clientCredentials({
      resource,
    });

    assert.equal(result.accessToken, 'access_token_000');
    assert.equal(result.refreshToken, 'refresh_token_000');
    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);
    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);

    const request = server.lastRequest();
    assert.equal(
      request.headers.get('Authorization'),
      'Basic ' + btoa('testClientId:testClientSecret')
    );

    assert.deepEqual(request.body, {
      grant_type: 'client_credentials',
      resource,
    });
  });
  it('should return an idToken if it was returned from the server', async () => {
    const client = new OAuth2Client({
      clientId: 'foo',
    });
    const token = await client.tokenResponseToOAuth2Token(
      Promise.resolve({
        token_type: 'bearer',
        access_token: 'foo',
        id_token: 'bar',
        refresh_token: 'baz',
      })
    );

    assert.deepEqual(token, {
      accessToken: 'foo',
      idToken: 'bar',
      expiresAt: null,
      refreshToken: 'baz',
    });
  });

  describe('error handling', async () => {
    it('should create a OAuth2HttpError if an error was thrown', async () => {
      server = testServer();

      const client = new OAuth2Client({
        server: server.url,
        tokenEndpoint: '/token',
        clientId: 'oauth2-error',
        clientSecret: 'testClientSecret',
        authenticationMethod: 'client_secret_post',
      });

      const resource = [
        'https://example/resource1',
        'https://example/resource2',
      ];

      try {
        await client.clientCredentials({
          resource,
        });
        throw new Error('This operation should have failed');
      } catch (err: any) {
        assert.ok(err instanceof OAuth2HttpError);
        assert.ok(err.response instanceof Response);
        assert.equal(err.oauth2Code, 'invalid_client');
        assert.deepEqual(err.parsedBody, {
          error: 'invalid_client',
          error_description: 'OOps!',
        });
      }
    });
    it('should create a OAuth2HttpError also if a non-oauth2 error was thrown with a JSON response', async () => {
      server = testServer();

      const client = new OAuth2Client({
        server: server.url,
        tokenEndpoint: '/token',
        clientId: 'json-error',
        clientSecret: 'testClientSecret',
        authenticationMethod: 'client_secret_post',
      });

      const resource = [
        'https://example/resource1',
        'https://example/resource2',
      ];

      try {
        await client.clientCredentials({
          resource,
        });
        throw new Error('This operation should have failed');
      } catch (err: any) {
        assert.ok(err instanceof OAuth2HttpError);
        assert.ok(err.response instanceof Response);
        assert.equal(err.httpCode, 418);
        assert.equal(err.oauth2Code, null);
        assert.deepEqual(err.parsedBody, {
          status: 418,
          title: 'OOps!',
          type: 'https://example/dummy',
        });
      }
    });
    it('should create a OAuth2HttpError when a generic HTTP error was thrown ', async () => {
      server = testServer();

      const client = new OAuth2Client({
        server: server.url,
        tokenEndpoint: '/token',
        clientId: 'general-http-error',
        clientSecret: 'testClientSecret',
        authenticationMethod: 'client_secret_post',
      });

      const resource = [
        'https://example/resource1',
        'https://example/resource2',
      ];

      try {
        await client.clientCredentials({
          resource,
        });
        throw new Error('This operation should have failed');
      } catch (err: any) {
        assert.ok(err instanceof OAuth2HttpError);
        assert.ok(err.response instanceof Response);
        assert.equal(err.oauth2Code, null);
        assert.equal(err.parsedBody, undefined);
      }
    });

  });
});


================================================
FILE: test/client.ts
================================================
import * as assert from 'node:assert';
import { OAuth2Client } from '../src/index.ts';
import { legacyFormUrlEncode } from '../src/client.ts';
import { describe, it } from 'node:test';

describe('tokenResponseToOAuth2Token', () => {
  it('should convert a JSON response to a OAuth2Token', async () => {
    const client = new OAuth2Client({
      clientId: 'foo',
    });
    const token = await client.tokenResponseToOAuth2Token(
      Promise.resolve({
        token_type: 'bearer',
        access_token: 'foo-bar',
        scope: 'foo bar',
        foo: 'bar'
      })
    );

    assert.deepEqual(token, {
      accessToken: 'foo-bar',
      expiresAt: null,
      refreshToken: null,
      scope: ['foo', 'bar'],
      extraParams: {
        foo: 'bar'
      }
    });
  });

  it('should error when an invalid JSON object is passed', async () => {
    const client = new OAuth2Client({
      clientId: 'foo',
    });

    let caught = false;
    try {
      await client.tokenResponseToOAuth2Token(
        Promise.resolve({
          funzies: 'foo-bar',
        } as any)
      );
    } catch (err) {
      assert.ok(err instanceof TypeError);
      caught = true;
    }

    assert.equal(caught, true);
  });
  it('should error when an empty body is passed', async () => {
    const client = new OAuth2Client({
      clientId: 'foo',
    });

    let caught = false;
    try {
      await client.tokenResponseToOAuth2Token(
        Promise.resolve(undefined as any)
      );
    } catch (err) {
      assert.ok(err instanceof TypeError);
      caught = true;
    }

    assert.equal(caught, true);
  });
});

describe('legacyFormUrlEncode', () => {
  it('correctly encodes full character set', () => {
    const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ ó';
    assert.equal(
      legacyFormUrlEncode(chars),
      '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E%5F%60%7B%7C%7D%7E+%C3%B3');
  });
});

describe('getEndpoint', () => {
  it('should not have a race condition when getting endpoints multiple times before the discovery request comes back', async () => {
    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
    const response = {
      ok: true,
      headers: new Headers([['Content-Type', 'application/json']]),
      json() {
        return Promise.resolve({
          'authorization_endpoint': 'custom'
        });
      },
    } as Response;

    const client = new OAuth2Client({
      server: 'http://server',
      discoveryEndpoint: '/discovery',
      clientId: 'clientId',
      fetch: () => Promise.resolve(response)
    });

    const endpoint1 = client.getEndpoint('authorizationEndpoint');
    const endpoint2 = client.getEndpoint('authorizationEndpoint');

    assert.equal(await endpoint1, 'http://server/custom');
    assert.equal(await endpoint2, 'http://server/custom');
  });
});


================================================
FILE: test/fetch-wrapper.ts
================================================
import * as assert from 'node:assert';
import { OAuth2Fetch, OAuth2Client } from '../src/index.ts';
import { afterEach, describe, it } from 'node:test';

describe('FetchWrapper', () => {
  let fetchWrapper: any;

  afterEach(() => {
    if (fetchWrapper) {
      clearTimeout(fetchWrapper.refreshTimer);
    }
  });

  it('should use the token from getNewToken', async () => {
    const client = new OAuth2Client({
      clientId: 'foo',
      clientSecret: 'bar',
    });

    fetchWrapper = new OAuth2Fetch({
      client,
      getNewToken: () => {
        return {
          accessToken: 'access',
          refreshToken: 'refresh',
          expiresAt: Date.now() + 1000_0000,
        };
      },
    });

    const mw = fetchWrapper.mw();

    const response = await mw(
      new Request('http://example/'),
      (req): any => req
    );

    assert.equal(response.headers.get('Authorization'), 'Bearer access');
  });

  it("should use the token even if it's delayed", async () => {
    const client = new OAuth2Client({
      clientId: 'foo',
      clientSecret: 'bar',
    });

    fetchWrapper = new OAuth2Fetch({
      client,
      getNewToken: async () => {
        await new Promise((res) => setTimeout(res, 200));
        return {
          accessToken: 'access',
          refreshToken: 'refresh',
          expiresAt: Date.now() + 1000_0000,
        };
      },
    });

    const mw = fetchWrapper.mw();

    const response = await mw(
      new Request('http://example/'),
      (req): any => req
    );

    assert.equal(response.headers.get('Authorization'), 'Bearer access');
  });

  it('should use a token from getStoredToken', async () => {
    const client = new OAuth2Client({
      clientId: 'foo',
      clientSecret: 'bar',
    });

    fetchWrapper = new OAuth2Fetch({
      client,
      getNewToken: () => null,
      getStoredToken: () => {
        return {
          accessToken: 'access',
          refreshToken: 'refresh',
          expiresAt: Date.now() + 1000_0000,
        };
      },
    });

    const mw = fetchWrapper.mw();

    const response = await mw(
      new Request('http://example/'),
      (req): any => req
    );

    assert.equal(response.headers.get('Authorization'), 'Bearer access');
  });

  it("should still work with getStoredToken even if it's delayed", async () => {
    const client = new OAuth2Client({
      clientId: 'foo',
      clientSecret: 'bar',
    });

    fetchWrapper = new OAuth2Fetch({
      client,
      getNewToken: () => null,
      getStoredToken: async () => {
        await new Promise((res) => setTimeout(res, 200));
        return {
          accessToken: 'access',
          refreshToken: 'refresh',
          expiresAt: Date.now() + 1000_0000,
        };
      },
    });

    const mw = fetchWrapper.mw();

    const response = await mw(
      new Request('http://example/'),
      (req): any => req
    );

    assert.equal(response.headers.get('Authorization'), 'Bearer access');
  });
});


================================================
FILE: test/password.ts
================================================
import * as assert from 'node:assert';
import { testServer } from './test-server.ts';
import { OAuth2Client } from '../src/index.ts';
import { after, describe, it } from 'node:test';

describe('password', () => {
  let server: ReturnType<typeof testServer>;

  after(async () => {
    if (server) {
      await server.close();
    }
  });

  it('should work with client_secret_basic', async () => {
    server = testServer();

    const client = new OAuth2Client({
      server: server.url,
      tokenEndpoint: '/token',
      clientId: 'testClientId',
      clientSecret: 'testClientSecret',
    });

    const result = await client.password({
      username: 'user123',
      password: 'password',
    });

    assert.equal(result.accessToken, 'access_token_000');
    assert.equal(result.refreshToken, 'refresh_token_000');
    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);
    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);

    const request = server.lastRequest();
    assert.equal(
      request.headers.get('Authorization'),
      'Basic ' + btoa('testClientId:testClientSecret')
    );
    assert.equal(request.headers.get('Accept'), 'application/json');

    assert.deepEqual(request.body, {
      grant_type: 'password',
      password: 'password',
      username: 'user123',
    });
  });

  it('should work with client_secret_post', async () => {
    server = testServer();

    const client = new OAuth2Client({
      server: server.url,
      tokenEndpoint: '/token',
      clientId: 'testClientId',
      authenticationMethod: 'client_secret_post',
    });

    const result = await client.password({
      username: 'user123',
      password: 'password',
    });

    assert.equal(result.accessToken, 'access_token_000');
    assert.equal(result.refreshToken, 'refresh_token_000');
    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);
    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);

    const request = server.lastRequest();

    assert.deepEqual(request.body, {
      grant_type: 'password',
      password: 'password',
      username: 'user123',
      client_id: 'testClientId',
    });
  });

  it('should support the resource parameter', async () => {
    server = testServer();

    const client = new OAuth2Client({
      server: server.url,
      tokenEndpoint: '/token',
      clientId: 'testClientId',
      authenticationMethod: 'client_secret_post',
    });
    const resource = ['https://example/resource1', 'https://example/resource2'];

    const result = await client.password({
      username: 'user123',
      password: 'password',
      resource,
    });

    assert.equal(result.accessToken, 'access_token_000');
    assert.equal(result.refreshToken, 'refresh_token_000');
    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);
    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);

    const request = server.lastRequest();

    assert.deepEqual(request.body, {
      grant_type: 'password',
      password: 'password',
      username: 'user123',
      client_id: 'testClientId',
      resource,
    });
  });
});


================================================
FILE: test/pkce.ts
================================================
import * as assert from 'node:assert';
import { generateCodeVerifier } from '../src/index.ts';
import { getCodeChallenge } from '../src/client/authorization-code.ts';
import { describe, it } from 'node:test';

describe('generateCodeVerifier', () => {

  it('should generate a 32byte base4url string', async () => {

    const out = await generateCodeVerifier();
    //console.debug(out, out.length);
    assert.match(out,/^[A-Za-z0-9-_]{43}$/);

  });

});

describe('getCodeChallenge', () => {

  it('should generate the matching code challenge for a given code verifier', async() => {

    const codeVerifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk';
    const codeChallenge = 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM';

    assert.deepEqual(await getCodeChallenge(codeVerifier),['S256', codeChallenge]);

  });

});


================================================
FILE: test/refresh.ts
================================================
import { testServer } from './test-server.ts';
import { OAuth2Client } from '../src/index.ts';
import * as assert from 'node:assert';
import { after, describe, it } from 'node:test';

describe('refreshing tokens', () => {

  let server: ReturnType<typeof testServer>;
  after(async () => {
    if (server) {
      await server.close();
    }

  });
  it('should work', async () => {

    server = testServer();

    const client = new OAuth2Client({
      server: server.url,
      tokenEndpoint: '/token',
      clientId: 'testClientId',
      clientSecret: 'testClientSecret',
    });

    const result = await client.refreshToken({
      refreshToken: 'refresh_token_000',
      accessToken: 'access_token_000',
      expiresAt: null,
    });

    assert.equal(result.accessToken, 'access_token_001');
    assert.equal(result.refreshToken, 'refresh_token_001');
    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);
    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);

    const request = server.lastRequest();
    assert.equal(
      request.headers.get('Authorization'),
      'Basic ' + btoa('testClientId:testClientSecret')
    );
    assert.equal(
      request.headers.get('Accept'),
      'application/json'
    );

    assert.deepEqual(request.body,{
      grant_type: 'refresh_token',
      refresh_token: 'refresh_token_000',
    });
  });

  it('should re-use the old refresh token if no new one was issued', async () => {

    server = testServer();

    const client = new OAuth2Client({
      server: server.url,
      tokenEndpoint: '/token',
      clientId: 'testClientId',
      clientSecret: 'testClientSecret',
    });

    const result = await client.refreshToken({
      refreshToken: 'refresh_token_001',
      accessToken: 'access_token_001',
      expiresAt: null,
    });

    assert.equal(result.accessToken, 'access_token_002');
    assert.equal(result.refreshToken, 'refresh_token_001');
    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);
    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);

    const request = server.lastRequest();
    assert.equal(
      request.headers.get('Authorization'),
      'Basic ' + btoa('testClientId:testClientSecret')
    );
    assert.equal(
      request.headers.get('Accept'),
      'application/json'
    );

    assert.deepEqual(request.body,{
      grant_type: 'refresh_token',
      refresh_token: 'refresh_token_001',
    });
  });
});


================================================
FILE: test/revoke.ts
================================================
import * as assert from 'node:assert';
import { testServer } from './test-server.ts';
import { OAuth2Client } from '../src/index.ts';
import { after, describe, it } from 'node:test';

describe('Token revocation', () => {
  const server = testServer();

  after(async () => {
    if (server) {
      await server.close();
    }
  });

  describe('should revoke access token when requested', async () => {
    const client = new OAuth2Client({
      server: server.url,
      tokenEndpoint: '/token',
      revocationEndpoint: '/revoke',
      clientId: 'test-client-id',
      clientSecret: 'test-client-secret',
    });

    const token = await client.clientCredentials();

    describe('When token type hint is not specified', () => {
      it('should assume token type is access token', async () => {
        await client.revoke(token);

        const request = server.lastRequest();
        assert.deepEqual(request.body, {
          token: token.accessToken,
          token_type_hint: 'access_token',
        });
      });
    });

    describe('When token type is specified as access token', () => {
      it('should supply access token', async () => {
        await client.revoke(token, 'access_token');

        const request = server.lastRequest();
        assert.deepEqual(request.body, {
          token: token.accessToken,
          token_type_hint: 'access_token',
        });
      });
    });

    describe('When token type is specified as refresh token', () => {
      it('should supply access token', async () => {
        await client.revoke(token, 'refresh_token');

        const request = server.lastRequest();
        assert.deepEqual(request.body, {
          token: token.refreshToken,
          token_type_hint: 'refresh_token',
        });
      });
    });
  });

  describe('Discovery', () => {
    const client = new OAuth2Client({
      server: server.url,
      discoveryEndpoint: '/discover',
      clientId: 'test-client-id',
    });

    it('Should discover revocation endpoint', async () => {
      const result = await client.getEndpoint('revocationEndpoint');
      assert.deepEqual(result, server.url + '/revoke');
    });
  });
});


================================================
FILE: test/test-server.ts
================================================
import { Application, type Middleware, Request } from '@curveball/core';
import bodyParser from '@curveball/bodyparser';
import * as http from 'http';

type TestServer = {
  server: http.Server;
  app: Application;
  lastRequest: () => Request;
  port: number;
  url: string;
  close: () => Promise<void>;
}

let serverCache: null|TestServer = null;

export function testServer() {

  if (serverCache) return serverCache;

  let lastRequest: any = null;

  const app = new Application();

  app.use(bodyParser());
  app.use((ctx, next) => {
    lastRequest = ctx.request;
    return next();
  });
  app.use(oauth2Error);
  app.use(jsonError);
  app.use(generalHttpError);
  app.use(issueToken);
  app.use(revokeToken);
  app.use(discover);
  const port = 40000 + Math.round(Math.random()*9999);
  const server = app.listen(port);

  serverCache = {
    server,
    app,
    lastRequest: (): Request => lastRequest,
    port,
    url: 'http://localhost:' + port,
    close: async() => {

      return new Promise<void>(res => {
        server.close(() => res());
      });

    }

  };
  return serverCache;

}

const oauth2Error: Middleware = (ctx, next) => {

  if (ctx.request.body?.client_id !== 'oauth2-error') {
    return next();
  }

  ctx.response.body = {
    error: 'invalid_client',
    error_description: 'OOps!',
  };

  ctx.response.status = 400;
  ctx.response.type = 'application/json';


};
const jsonError: Middleware = (ctx, next) => {

  if (ctx.request.body?.client_id !== 'json-error') {
    return next();
  }

  ctx.response.body = {
    type: 'https://example/dummy',
    title: 'OOps!',
    status: 418,
  };

  ctx.response.status = 418;
  ctx.response.type = 'application/problem+json';

};
const generalHttpError: Middleware = (ctx, next) => {

  if (ctx.request.body?.client_id !== 'general-http-error') {
    return next();
  }

  ctx.response.body = 'We\'re super broken RN!';
  ctx.response.status = 500;
  ctx.response.type = 'text/plain';

};

const issueToken: Middleware = (ctx, next) => {

  if (ctx.path !== '/token') {
    return next();
  }

  ctx.response.type = 'application/json';
  if (ctx.request.body.refresh_token === 'refresh_token_000') {

    ctx.response.body = {
      token_type: 'Bearer',
      access_token: 'access_token_001',
      refresh_token: 'refresh_token_001',
      expires_in: 3600,
    };

  } else if (ctx.request.body.refresh_token === 'refresh_token_001') {

    ctx.response.body = {
      token_type: 'Bearer',
      access_token: 'access_token_002',
      expires_in: 3600,
    };

  } else {

    ctx.response.body = {
      token_type: 'Bearer',
      access_token: 'access_token_000',
      refresh_token: 'refresh_token_000',
      expires_in: 3600,
      scope: 'foo bar',
      foo: 'bar'  // Additional property returned by the server
    };
  }

};


const revokeToken: Middleware = (ctx, next) => {

  if (ctx.path !== '/revoke') {
    return next();
  }

  ctx.response.type = 'application/octet-stream';
  ctx.response.body = 'SUCCESS!';
};


const discover: Middleware = (ctx, next) => {

  if (ctx.path !== '/discover') {
    return next();
  }

  ctx.response.type = 'application/json';
  ctx.response.body = {
    revocation_endpoint: '/revoke',
  };
};


================================================
FILE: tsconfig.json
================================================
{
    "compilerOptions": {
        "module": "esnext",
        "target": "es2019",

        "strict": true,
        "noFallthroughCasesInSwitch": true,
        "experimentalDecorators": true,
        "noUnusedLocals": true,
        "erasableSyntaxOnly": true,

        "rewriteRelativeImportExtensions": true,

        "moduleResolution": "node",
        "sourceMap": true,
        "outDir": "dist",
        "baseUrl": ".",
        "paths": {
            "*": [
                "src/types/*"
            ]
        },
        "lib": [
          "DOM",
          "ES2019"
        ],
        "declaration": true
    },
    "include": [
        "src/**/*"
    ]
}


================================================
FILE: vite.config.js
================================================
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    outDir: 'browser/',
    lib: {
      entry: 'src/index.ts',
      fileName: (format) => `oauth2-client.min.js`,
      formats: ['es'],
    },
    minify: true,
  }
});
Download .txt
gitextract_y_zc9pja/

├── .github/
│   ├── CODE_OF_CONDUCT.md
│   ├── CONTRIBUTING.md
│   └── workflows/
│       ├── ci.yml
│       └── npm-publish.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── changelog.md
├── eslint.config.mjs
├── package.json
├── src/
│   ├── client/
│   │   └── authorization-code.ts
│   ├── client.ts
│   ├── error.ts
│   ├── fetch-wrapper.ts
│   ├── index.ts
│   ├── messages.ts
│   └── token.ts
├── test/
│   ├── authorization-code.ts
│   ├── client-credentials.ts
│   ├── client.ts
│   ├── fetch-wrapper.ts
│   ├── password.ts
│   ├── pkce.ts
│   ├── refresh.ts
│   ├── revoke.ts
│   └── test-server.ts
├── tsconfig.json
└── vite.config.js
Download .txt
SYMBOL INDEX (69 symbols across 8 files)

FILE: src/client.ts
  type ClientCredentialsParams (line 18) | type ClientCredentialsParams = {
  type PasswordParams (line 30) | type PasswordParams = {
  type RefreshParams (line 48) | type RefreshParams = {
  type ClientSettings (line 60) | interface ClientSettings {
  type OAuth2Endpoint (line 160) | type OAuth2Endpoint = 'tokenEndpoint' | 'authorizationEndpoint' | 'disco...
  class OAuth2Client (line 162) | class OAuth2Client {
    method constructor (line 166) | constructor(clientSettings: ClientSettings) {
    method refreshToken (line 178) | async refreshToken(token: OAuth2Token, params?: RefreshParams): Promis...
    method clientCredentials (line 208) | async clientCredentials(params?: ClientCredentialsParams): Promise<OAu...
    method password (line 234) | async password(params: PasswordParams): Promise<OAuth2Token> {
    method authorizationCode (line 248) | get authorizationCode(): OAuth2AuthorizationCodeClient {
    method introspect (line 264) | async introspect(token: OAuth2Token): Promise<IntrospectionResponse> {
    method revoke (line 281) | async revoke(token: OAuth2Token, tokenTypeHint: OAuth2TokenTypeHint = ...
    method getEndpoint (line 300) | async getEndpoint(endpoint: OAuth2Endpoint): Promise<string> {
    method discover (line 337) | private discover(): Promise<void> {
    method doDiscover (line 350) | private async doDiscover(): Promise<void> {
    method request (line 402) | async request(endpoint: OAuth2Endpoint, body: Record<string, any>): Pr...
    method tokenResponseToOAuth2Token (line 489) | async tokenResponseToOAuth2Token(resp: Promise<TokenResponse>): Promis...
  function resolve (line 528) | function resolve(uri: string, base?: string): string {
  function generateQueryString (line 540) | function generateQueryString(params: Record<string, undefined | number |...
  function legacyFormUrlEncode (line 558) | function legacyFormUrlEncode(value: string): string {

FILE: src/client/authorization-code.ts
  type GetAuthorizeUrlParams (line 6) | type GetAuthorizeUrlParams = {
  type ValidateResponseResult (line 56) | type ValidateResponseResult = {
  type GetTokenParams (line 70) | type GetTokenParams = {
  class OAuth2AuthorizationCodeClient (line 87) | class OAuth2AuthorizationCodeClient {
    method constructor (line 91) | constructor(client: OAuth2Client) {
    method getAuthorizeUri (line 101) | async getAuthorizeUri(params: GetAuthorizeUrlParams): Promise<string> {
    method getTokenFromCodeRedirect (line 144) | async getTokenFromCodeRedirect(url: string|URL, params: Omit<GetTokenP...
    method validateResponse (line 165) | validateResponse(url: string|URL, params: {state?: string}): ValidateR...
    method getToken (line 198) | async getToken(params: GetTokenParams): Promise<OAuth2Token> {
  function generateCodeVerifier (line 214) | async function generateCodeVerifier(): Promise<string> {
  function getCodeChallenge (line 223) | async function getCodeChallenge(codeVerifier: string): Promise<['plain' ...
  function getWebCrypto (line 229) | async function getWebCrypto(): Promise<typeof window.crypto> {
  function stringToBuffer (line 251) | function stringToBuffer(input: string): ArrayBuffer {
  function base64Url (line 261) | function base64Url(buf: ArrayBuffer) {

FILE: src/error.ts
  class OAuth2Error (line 15) | class OAuth2Error extends Error {
    method constructor (line 19) | constructor(message: OAuth2ErrorCode|string, oauth2Code: OAuth2ErrorCo...
  class OAuth2HttpError (line 42) | class OAuth2HttpError extends OAuth2Error {
    method constructor (line 49) | constructor(message: string, oauth2Code: OAuth2ErrorCode, response: Re...

FILE: src/fetch-wrapper.ts
  type FetchMiddleware (line 4) | type FetchMiddleware = (request: Request, next: (request: Request) => Pr...
  type OAuth2FetchOptions (line 6) | type OAuth2FetchOptions = {
  class OAuth2Fetch (line 52) | class OAuth2Fetch {
    method constructor (line 70) | constructor(options: OAuth2FetchOptions) {
    method fetch (line 93) | async fetch(input: RequestInfo, init?: RequestInit): Promise<Response> {
    method mw (line 113) | mw(): FetchMiddleware {
    method getToken (line 148) | async getToken(): Promise<OAuth2Token> {
    method getAccessToken (line 167) | async getAccessToken(): Promise<string> {
    method refreshToken (line 188) | async refreshToken(): Promise<OAuth2Token> {
    method scheduleRefresh (line 246) | private scheduleRefresh() {

FILE: src/messages.ts
  type RefreshRequest (line 4) | type RefreshRequest = {
  type ClientCredentialsRequest (line 22) | type ClientCredentialsRequest = {
  type PasswordRequest (line 39) | type PasswordRequest = {
  type AuthorizationCodeRequest (line 53) | type AuthorizationCodeRequest = {
  type TokenResponse (line 70) | type TokenResponse = {
  type OAuth2ResponseType (line 103) | type OAuth2ResponseType = 'code' | 'token';
  type OAuth2ResponseMode (line 104) | type OAuth2ResponseMode = 'query' | 'fragment';
  type OAuth2GrantType (line 105) | type OAuth2GrantType = 'authorization_code' | 'implicit' | 'password' | ...
  type OAuth2AuthMethod (line 106) | type OAuth2AuthMethod = 'none' | 'client_secret_basic' | 'client_secret_...
  type OAuth2CodeChallengeMethod (line 107) | type OAuth2CodeChallengeMethod = 'S256' | 'plain';
  type OAuth2TokenTypeHint (line 109) | type OAuth2TokenTypeHint = 'access_token' | 'refresh_token';
  type ServerMetadataResponse (line 116) | type ServerMetadataResponse = {
  type IntrospectionRequest (line 239) | type IntrospectionRequest = {
  type IntrospectionResponse (line 245) | type IntrospectionResponse = {
  type RevocationRequest (line 315) | type RevocationRequest = {
  type OAuth2ErrorCode (line 320) | type OAuth2ErrorCode =

FILE: src/token.ts
  type OAuth2Token (line 4) | type OAuth2Token = {

FILE: test/client.ts
  method json (line 84) | json() {

FILE: test/test-server.ts
  type TestServer (line 5) | type TestServer = {
  function testServer (line 16) | function testServer() {
Condensed preview — 29 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (127K chars).
[
  {
    "path": ".github/CODE_OF_CONDUCT.md",
    "chars": 3212,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "chars": 1671,
    "preview": "Contributing to this project\n============================\n\nThank you for considering to add to this project! Before you "
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1103,
    "preview": "# This workflow will do a clean install of node dependencies, build the source code and run tests across different versi"
  },
  {
    "path": ".github/workflows/npm-publish.yml",
    "chars": 1319,
    "preview": "# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created\n# For "
  },
  {
    "path": ".gitignore",
    "chars": 122,
    "preview": "# npm\n/node_modules\n\n# typescript output\n/dist\n\n# webpack output\n/browser\n\n# zip\n/*.zip\n\n# vim\n.*.swp\n\n# nyc\n/.nyc_outpu"
  },
  {
    "path": "LICENSE",
    "chars": 1071,
    "preview": "MIT License\n\nCopyright (c) 2019-2025 Evert Pot\n\nPermission is hereby granted, free of charge, to any person obtaining a "
  },
  {
    "path": "Makefile",
    "chars": 950,
    "preview": "SOURCE_FILES:=$(shell find src/ -type f -name '*.ts')\n\n.PHONY:build\nbuild: dist/build browser/oauth2-client.min.js brows"
  },
  {
    "path": "README.md",
    "chars": 13043,
    "preview": "# OAuth2 client for Node and browsers\n\nThis package contains an OAuth2 client. It aims to be a fully-featured OAuth2\nuti"
  },
  {
    "path": "changelog.md",
    "chars": 13119,
    "preview": "Changelog\n=========\n\n3.3.1 (2025-09-09)\n------------------\n\n* #193: Fix race condition when multiple function calls are "
  },
  {
    "path": "eslint.config.mjs",
    "chars": 2647,
    "preview": "import typescriptEslint from \"@typescript-eslint/eslint-plugin\";\nimport globals from \"globals\";\nimport tsParser from \"@t"
  },
  {
    "path": "package.json",
    "chars": 1334,
    "preview": "{\n  \"name\": \"@badgateway/oauth2-client\",\n  \"version\": \"3.3.1\",\n  \"description\": \"OAuth2 client for browsers and Node.js."
  },
  {
    "path": "src/client/authorization-code.ts",
    "chars": 7197,
    "preview": "import { OAuth2Client } from '../client.ts';\nimport type { OAuth2Token } from '../token.ts';\nimport type { Authorization"
  },
  {
    "path": "src/client.ts",
    "chars": 17101,
    "preview": "import type { OAuth2Token } from './token.ts';\nimport type {\n  AuthorizationCodeRequest,\n  ClientCredentialsRequest,\n  I"
  },
  {
    "path": "src/error.ts",
    "chars": 1278,
    "preview": "import { type OAuth2ErrorCode } from './messages.ts';\n\n/**\n * An error class for any error the server emits.\n *\n * The '"
  },
  {
    "path": "src/fetch-wrapper.ts",
    "chars": 7589,
    "preview": "import type { OAuth2Token } from './token.ts';\nimport { OAuth2Client } from './client.ts';\n\ntype FetchMiddleware = (requ"
  },
  {
    "path": "src/index.ts",
    "chars": 363,
    "preview": "export { OAuth2Client } from './client.ts';\nexport { OAuth2AuthorizationCodeClient, generateCodeVerifier } from './clien"
  },
  {
    "path": "src/messages.ts",
    "chars": 7347,
    "preview": "/**\n * refresh_token request body\n */\nexport type RefreshRequest = {\n  grant_type: 'refresh_token';\n  refresh_token: str"
  },
  {
    "path": "src/token.ts",
    "chars": 657,
    "preview": "/**\n * Token information\n */\nexport type OAuth2Token = {\n\n  /**\n   * OAuth2 Access Token\n   */\n  accessToken: string;\n\n "
  },
  {
    "path": "test/authorization-code.ts",
    "chars": 11877,
    "preview": "import * as assert from 'node:assert';\nimport { testServer } from './test-server.ts';\nimport { OAuth2Client } from '../s"
  },
  {
    "path": "test/client-credentials.ts",
    "chars": 9243,
    "preview": "import * as assert from 'node:assert';\nimport { testServer } from './test-server.ts';\nimport { OAuth2Client, OAuth2HttpE"
  },
  {
    "path": "test/client.ts",
    "chars": 3006,
    "preview": "import * as assert from 'node:assert';\nimport { OAuth2Client } from '../src/index.ts';\nimport { legacyFormUrlEncode } fr"
  },
  {
    "path": "test/fetch-wrapper.ts",
    "chars": 2986,
    "preview": "import * as assert from 'node:assert';\nimport { OAuth2Fetch, OAuth2Client } from '../src/index.ts';\nimport { afterEach, "
  },
  {
    "path": "test/password.ts",
    "chars": 3162,
    "preview": "import * as assert from 'node:assert';\nimport { testServer } from './test-server.ts';\nimport { OAuth2Client } from '../s"
  },
  {
    "path": "test/pkce.ts",
    "chars": 830,
    "preview": "import * as assert from 'node:assert';\nimport { generateCodeVerifier } from '../src/index.ts';\nimport { getCodeChallenge"
  },
  {
    "path": "test/refresh.ts",
    "chars": 2479,
    "preview": "import { testServer } from './test-server.ts';\nimport { OAuth2Client } from '../src/index.ts';\nimport * as assert from '"
  },
  {
    "path": "test/revoke.ts",
    "chars": 2171,
    "preview": "import * as assert from 'node:assert';\nimport { testServer } from './test-server.ts';\nimport { OAuth2Client } from '../s"
  },
  {
    "path": "test/test-server.ts",
    "chars": 3243,
    "preview": "import { Application, type Middleware, Request } from '@curveball/core';\nimport bodyParser from '@curveball/bodyparser';"
  },
  {
    "path": "tsconfig.json",
    "chars": 660,
    "preview": "{\n    \"compilerOptions\": {\n        \"module\": \"esnext\",\n        \"target\": \"es2019\",\n\n        \"strict\": true,\n        \"noF"
  },
  {
    "path": "vite.config.js",
    "chars": 251,
    "preview": "import { defineConfig } from 'vite';\n\nexport default defineConfig({\n  build: {\n    outDir: 'browser/',\n    lib: {\n      "
  }
]

About this extraction

This page contains the full source code of the badgateway/oauth2-client GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 29 files (118.2 KB), approximately 29.5k tokens, and a symbol index with 69 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!